【背包问题九讲】第一讲:01背包问题(含Java实现代码)

一、题目

N N N 件物品和一个容量为 V V V 的背包。放入第 i i i 件物品耗费的费用是 C i C_i Ci,得到的 价值是 W i W_i Wi。求解将哪些物品装入背包可使价值总和最大。

二、基本思路

2.1 讲解

这是最基础的背包问题,特点是:每种物品仅有一件,可以选择放或不放。 用子问题定义状态:即 F [ i , v ] F[i, v] F[i,v] 表示前 i i i 件物品放入一个容量为 v v v 的背包可以获得的最大价值。则其状态转移方程便是:
F [ i , v ] = m a x { F [ i − 1 , v ] , F [ i − 1 , v − C i ] + W i } F[i, v] = max\{F[i − 1, v], F[i − 1, v − C_i] + W_i\} F[i,v]=max{F[i1,v],F[i1,vCi]+Wi}这个方程非常重要,基本上所有跟背包相关的问题的方程都是由它衍生出来的。所以有必要将它详细解释一下: i i i 件物品放入一个容量为 v v v 的背包可以获得的最大价值这个子问题:

对于 F [ i , v ] F[i, v] F[i,v],若只考虑第 i i i 件物品放与不放,则有两种情况:

  1. 不放 i i i 件物品:则这种情况可以获得的最大价值等价于 “前 i − 1 i-1 i1 件物品放入一个容量为 v v v 的背包可以获得的最大价值”

    即: F [ i , v ] = F [ i − 1 , v ] F[i, v] = F[i-1, v] F[i,v]=F[i1,v]

  2. i i i 件物品:则这种情况可以获得的最大价值等价于 “前 i − 1 i-1 i1 件物品放入一个容量为 v − C i v-C_i vCi 的背包可以获得的最大价值 与第 i i i 件物品的价值之和”

    即: F [ i , v ] = F [ i − 1 , v − C i ] + W i F[i, v] = F[i-1, v-C_i]+W_i F[i,v]=F[i1,vCi]+Wi

  3. 我们需要的最大价值是上面两种情况的最大值,故: F [ i , v ] = m a x { F [ i − 1 , v ] , F [ i − 1 , v − C i ] + W i } F[i, v] = max\{F[i − 1, v], F[i − 1, v − C_i] + W_i\} F[i,v]=max{F[i1,v],F[i1,vCi]+Wi}

伪代码如下:
F [ 0 , 0 ⋯ V ] ← 0 f o r    i    ←    1    t o    N f o r    v    ←    C i    t o    V F [ i , v ]    ←    m a x { F [ i − 1 , v ] , F [ i − 1 , v − C i ] + W i } \begin{aligned} &F[0, 0\cdots V] \leftarrow 0 \\ &for \; i \; \leftarrow \; 1 \; to \; N \\ &\qquad for \; v \; \leftarrow \; C_i \; to \; V \\ &\qquad\qquad F[i, v] \; \leftarrow \; max\{F[i − 1, v], F[i − 1, v − C_i] + W_i\} \end{aligned} F[0,0V]0fori1toNforvCitoVF[i,v]max{F[i1,v],F[i1,vCi]+Wi}

2.2 代码

Java代码如下,这里我给出了完整的包含输入输出的代码,以方便大家理解。后续代码将只给出 problem() 方法。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        Scanner in = new Scanner(System.in);
        while (in.hasNext()) {
            // 输入
            int N = in.nextInt();   // 物品数量
            int V = in.nextInt();   // 背包容量
            int[] C = new int[N];   // N件物品的费用(体积)
            int[] W = new int[N];   // N件物品的价值
            for (int i = 0; i < C.length; i++) {
                C[i] = in.nextInt();
            }
            for (int i = 0; i < W.length; i++) {
                W[i] = in.nextInt();
            }

            // 调用
            int ans = problem(N, V, C, W);
            System.out.println(ans);
        }
    }

    // 01背包问题
    public static int problem(int N, int V, int[] C, int[] W){
        int[][] dp = new int[N+1][V+1];
        for (int i = 1; i <= N; i++) {
            for (int j = C[i-1]; j <= V; j++) {
                dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-C[i-1]] + W[i-1]);
            }
        }
        return dp[N][V];
    }

}

三、优化空间复杂度

3.1 讲解

以上方法的时间和空间复杂度均为 O ( V N ) O(VN) O(VN),其中时间复杂度应该已经不能再优化 了,但空间复杂度却可以优化到 O ( V ) O(V) O(V)

上一小节基本思路中,主循环 for (int i = 1; i <= N; i++) 依次考察每一件物品放入与不放入的情况,并依次修改 dp[][] 数组每一行的元素。通过观察可以发现,每一行元素的修改,都只与上一行元素的值有关。那我们是否可以只存储一行的元素的值,并在这一行中原地执行算法呢?

当然是可以的!此时 F [ N ] [ V ] F[N][V] F[N][V] 将简化为 F [ V ] F[V] F[V],我们只需要在每一行中以递减顺序计算 F [ v ] F[v] F[v] 的值。在第 i i i 次循环时,当前的 F [ v ] F[v] F[v] 是上一次循环时计算得到的值,相当于上一小节中的 F [ i − 1 ] [ v ] F[i-1][v] F[i1][v],而我们所需要计算的 F [ v ] F[v] F[v] 相当于上一小节中的 F [ i ] [ v ] F[i][v] F[i][v]。以递减顺序计算 F [ v ] F[v] F[v] 的值,就可以得到上一次循环时计算得到的值,即 F [ i − 1 ] [ v ] F[i-1][v] F[i1][v] F [ i − 1 ] [ v − C i ] F[i-1][v-C_i] F[i1][vCi]

伪代码如下:
F [ 0 ⋯ V ]    ←    0 f o r    i    ←    1    t o    N f o r    v    ←    V    t o    C i F [ v ]    ←    m a x { F [ v ] , F [ v − C i ] + W i } \begin{aligned} & F[0\cdots V]\;\leftarrow\;0 \\ & for \; i \;\leftarrow\; 1 \; to \; N \\ & \qquad for \; v \;\leftarrow\; V \; to \; C_i \\ & \qquad\qquad F[v] \;\leftarrow\; max\{F[v],F[v-C_i]+W_i\} \end{aligned} F[0V]0fori1toNforvVtoCiF[v]max{F[v],F[vCi]+Wi}其中的 F [ v ]    ←    m a x { F [ v ] , F [ v − C i ] + W i } F[v] \;\leftarrow\; max\{F[v],F[v-C_i]+W_i\} F[v]max{F[v],F[vCi]+Wi} 就对应于我们原来的转移方程。

事实上,使用一维数组解 01 背包的程序在后面会被多次用到,所以这里抽象出一个处理一件 01 背包中的物品过程,以后的代码中直接调用不加说明。01 背包问题的伪代码就可以这样写:
d e f    Z e r o O n e P a c k ( F , C , W ) f o r    v    ←    V    t o    C F [ v ]    ←    m a x { F [ v ] , F [ v − C ] + W } F [ 0 ⋯ V ]    ←    0 f o r    i    ←    1    t o    N Z e r o O n e P a c k ( F , C i , W i ) \begin{aligned} & def \; ZeroOnePack(F, C, W) \\ & \qquad for \; v \;\leftarrow\; V \; to \; C \\ & \qquad\qquad F[v] \;\leftarrow\; max\{F[v],F[v-C]+W\} \\ & \\ & F[0\cdots V] \;\leftarrow\; 0 \\ & for \; i \;\leftarrow\; 1 \; to \; N \\ & \qquad ZeroOnePack(F, C_i, W_i) \end{aligned} defZeroOnePack(F,C,W)forvVtoCF[v]max{F[v],F[vC]+W}F[0V]0fori1toNZeroOnePack(F,Ci,Wi)

3.2 代码

Java代码如下:

	// 01背包问题
    public static int problem(int N, int V, int[] C, int[] W){
        int[] dp = new int[V+1];
        for (int i = 1; i <= N; i++) {
            ZeroOnePack(dp, C[i-1], W[i-1]);
        }
        return dp[V];
    }

    public static void ZeroOnePack(int[] dp, int C, int W) {
        for (int i = dp.length-1; i >= C; i--) {
            dp[i] = Math.max(dp[i], dp[i-C] + W);
        }
    }

四、初始化的细节问题

我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求**“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同**。

  • 如果是第一种问法,要求恰好装满背包,那么在初始化时除了 F [ 0 ] F[0] F[0] 0 0 0,其它 F [ 1 ⋯ V ] F[1\cdots V ] F[1V] 均设为 − ∞ -\infty ,这样就可以保证最终得到的 F [ V ] F[V] F[V] 是一种恰好装满背包的最优解。

  • 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 F [ 0 ⋯ V ] F[0\cdots V ] F[0V] 全部设为 0 0 0

这是为什么呢?可以这样理解:初始化的 F F F 数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为 0 0 0 的背包可以在什么也不装且价值为 0 0 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 − ∞ -\infty 了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0 0 0,所以初始时状态的值也就全部为 0 0 0 了。

这个小技巧完全可以推广到其它类型的背包问题,后面不再对进行状态转移之前的初始化进行讲解。

五、一个常数优化

上面伪代码中的
f o r    i    ←    1    t o    N f o r    v    ←    V    t o    C i \begin{aligned} & for \; i \;\leftarrow\; 1 \; to \; N \\ & \qquad for \; v \;\leftarrow\; V \; to \; C_i \\ \end{aligned} fori1toNforvVtoCi中第二重循环的下限可以改进。它可以被优化为
f o r    i    ←    1    t o    N f o r    v    ←    V    t o    m a x { V − Σ i N W i , C i } \begin{aligned} & for \; i \;\leftarrow\; 1 \; to \; N \\ & \qquad for \; v \;\leftarrow\; V \; to \; max\{V-\Sigma_i^N W_i, C_i\} \\ \end{aligned} fori1toNforvVtomax{VΣiNWi,Ci}这个优化之所以成立的原因请读者自己思考。(提示:使用二维的转移方程思考较易。)

六、小结

01 背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想。另外,别的类型的背包问题往往也可以转换成 01 背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及空间复杂度怎样被优化。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

鱼儿听雨眠

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值