说明:

本文所讲内容摘录自崔添翼:背包九讲,并对其中的数学内容和一些较为复杂的内容进行了删减,增加了基础的例题,只是面向初学者或者不需要深入理解背包及其衍生问题的读者,如果有能力并且有意愿加深理解,本文可能会对您形成误导,请移步崔添翼:背包九讲.

01背包问题

题目

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

基本思路

最基础的背包问题,特点是每种物品仅有一件,可以选择放或者不放.

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 件物品,那么问题就转化为 “前 i − 1 i − 1 i1 件物品放入容量为 v v v 的背 包中”,价值为 F [ i − 1 , v ] F[i − 1, v] F[i1,v]
  • 如果放第 i i i 件物品,那么问题就转化为 “前 i − 1 i − 1 i1 件物品放 入剩下的容量为 v − C i v −C_i vCi 的背包中”,此时能获得的最大价值就是 F [ i − 1 , v − C i ] F[i − 1, v −C_i] F[i1,vCi] 再加上 通过放入第 i i i 件物品获得的价值 W i W_i Wi

上述问题的伪代码:

int N, V;
// cv[i][0]是放第i件物品消耗的费用,cv[i][1]是放第i件物品的收益价值,其中i从1开始
int[][] cv = new int[N + 1][2];
int dp[][] = new int[N + 1][V + 1];
dp[0][0] = 0;
for (int i = 1; i <= N; i++) {
   
    for (int j = 0; j <= V; j++) {
   
        if (cv[i][0] <= j) {
   
            dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - cv[i][0]] + cv[i][1]);
        } else {
   
            dp[i][j] = dp[i - 1][j];
        }
    }
}
return dp[N][V];

注意❗: 外层枚举物品的数量,内层枚举背包的容量

初始化的细节问题

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

  • 如果要求恰好装满背包,那么在初始化时除了 F [ 0 ] F[0] F[0] 0 0 0,其它 F [ 1.. V ] F[1..V] F[1..V] 均设为 − ∞ −∞ ,这样就可以保证最终得到的 F [ V ] F[V] F[V] 是一种恰好装满背包的最优解。
  • 如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将 F [ 0.. V ] F[0..V] F[0..V]全部设为 0。

这是为什么呢?可以这样理解:初始化的 F F F 数组事实上就是在没有任何物品可以放入背包时的合法状态

  • 如果要求背包恰好装满,那么此时只有容量为 0 0 0 的背包可以在什么也不装且价值为 0 0 0 的情况下被“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,应该被赋值为 − ∞ -∞ 了。

  • 如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为 0 0 0,所以初始时状态的值也就全部为 0 0 0了。

优化空间复杂度

略🚮🤐(太菜了,慢慢来)

相关题目练习

题目URL

N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

i 件物品的体积是 v i v_i vi,价值是 w i w_i wi

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。

输入格式

第一行两个整数,NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式

输出一个整数,表示最大价值。

数据范围

0 < N , V ≤ 10000 < N , V ≤ 1000 0<N,V≤10000<N,V≤1000 0<N,V10000<N,V1000

0 < v i , w i ≤ 10000 < v i , w i ≤ 1000 0<vi,wi≤10000<vi,wi≤1000 0<vi,wi10000<vi,wi1000

输入样例

4 5
1 2
2 4
3 4
4 5

输出样例:

8

题目解法

import java.util.*;

public class Main {
   
    public static void main(String[] args) {
   
        Scanner sc = new Scanner(System.in);
        // 物品的数量
        int N = sc.nextInt();
        // 背包的容量
        int V = sc.nextInt();
        // 每个物品的体积和价值
        int[][] vw = new int[N + 1][2];
        for (int i = 1; i <= N; i++) {
   
            // 体积
            vw[i][0] = sc.nextInt();
            // 价值
            vw[i][1] = sc.nextInt();
        }

        int dp[][] = new int[N + 1][V + 1];
        // 不要求完全装满背包,初始化全设置为0,因为java默认int数组为0所以象征性的初始化第一个
        dp[0][0] = 0;
        // 枚举第i件物品
        for (int i = 1; i <= N; i++) {
   
            // 枚举背包的容量
            for (int j = 0; j <= V; j++) {
   
                // 如果当前物品的体积小于背包的剩余容量
                if (vw[i][0] <= j) {
   
                    // 可以选择当前的物品加入背包或者不加入背包
                    // 不加入背包时:问题转化成i-1件物品装入容量为j的背包的子问题
                    // 加入背包时:问题转化成i-1件物品装入容量为j-vw[i][0]的背包的子问题
                    dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - vw[i][0]] + vw[i][1]);
                } else {
   
                    // 如果当前物品的体积大于背包的容量,那么不能装入,所以问题转化成
                    // i-1件物品装入容量为j的背包的子问题
                    dp[i][j] = dp[i - 1][j];
                }
            }
        }
        // 返回N件物品,背包容量为V的装入的最大价值
        System.out.println(dp[N][V]);
    }
}

参考文献

崔添翼:背包九讲