问题描述:
现有一个容量为 V V V的背包,以及 n n n件物品,其分别占据的容量为 c i c_i ci ,其分别带来的价值为 w i w_i wi,( i = 1 , 2 , . . . , n i={1,2,...,n} i=1,2,...,n)
要求:这 n n n件物品只能放入背包0次或1次(即,不放入或者放入)
问:背包的最大价值为多少?
使用动态规划进行求解:
子问题定义状态:
d p [ i ] [ j ] dp[i][j] dp[i][j]表示 前 i i i件物品,放入容量为 j j j的背包中,可以获得的最大价值。
状态转移方程:
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − c i ] + w i ) dp[i][j] = max(dp[i-1][j],dp[i-1][j-c_i]+w_i) dp[i][j]=max(dp[i−1][j],dp[i−1][j−ci]+wi)
其中 d p [ i − 1 ] [ j ] dp[i-1][j] dp[i−1][j] 表示:第 i i i个物品不放入背包时的最大价值(即,将前 i − 1 i-1 i−1 个物品放入容量为 j j j的背包中的最大价值)。
d p [ i − 1 ] [ j − c i ] + w i dp[i-1][j-c_i]+w_i dp[i−1][j−ci]+wi 表示:第 i i i个物品放入背包时的最大价值(即,将前 i − 1 i-1 i−1 个物品放入容量为 j − c i j-c_i j−ci的背包中的最大价值 + w i w_i wi)。
最终需要的答案为 d p [ n ] [ V ] dp[n][V] dp[n][V]
代码如下:
public static int knapsackProblem(int v, int[] c, int[] w){
int n = c.length;
int[][] dp = new int[n+1][v+1];
// 背包容量为0时,价值为0
for(int i = 0; i <= n; i++)
dp[i][0] = 0;
// 背包里不放任何物品时,其价值也为0
for(int j = 0; j <= v; j++)
dp[0][j] = 0;
// 计算剩余每个dp[i][j]
for(int i = 1; i <= n; i++){
for(int j = c[i-1]; j <= v; j++){ // c[i-1]是因为c是从0开始的,i-1表示的为第i个,w[i-1]同理
dp[i][j] = Math.max(dp[i-1][j], dp[i-1][j-c[i-1]]+w[i-1]);
}
}
return dp[n][v];
}
优化方案:
上述解决方案使用的是一个二维数组 d p [ ] [ ] dp[][] dp[][],每一行的值与其上一行数据相关,(即,前 i i i 个物品产生的最大价值,与前 i − 1 i-1 i−1 个物品产生的最大值相关),我们完全可以只用一行数据,来表示每个不同容量带来的最大价值,(即使用 d p [ j ] dp[j] dp[j]来表示背包容量为 j j j 时带来的最大价值。)而省去表示前 i i i 件物品的空间。通过不断更新dp[j]来得到最终的结果。
代码如下:
public static int knapsackProblemOptimized(int v, int[] c, int[] w){
int n = c.length;
int[] dp = new int[v+1];
// 初始化dp,其表示前0个物品放入不同容量的背包,产生的最大价值均为0
for(int i = 0; i <= v; i++)
dp[i] = 0;
// 更新dp值
for(int i = 1; i <= n; i++)
for(int j = v; j >= c[i-1]; j--) // (1)
dp[j] = Math.max(dp[j], dp[j-c[i-1]]+w[i-1]); // (2)
return dp[v];
}
注意点:
在代码 (1) 处,其是一个从 v v v -> c [ i − 1 ] c[i-1] c[i−1] 的过程,原因在于:更新值时,依赖的总是 i − 1 i-1 i−1 时的数据。例如,如果现在循环中, i = 3 i=3 i=3,此时 第二重循环还未开始,那 d p [ ] dp[] dp[] 数组中的数据保存的是 : i = 2 i=2 i=2 时,不同容量(从 0 0 0 -> v v v)背包的最大价值。而当第二重循环开始后, d p [ ] dp[] dp[] 中的值开始更新,从(2)中的代码可以看出, d p [ j ] dp[j] dp[j] 依赖于 d p [ j ] dp[j] dp[j] 和 d p [ j − c [ i − 1 ] ] dp[j-c[i-1]] dp[j−c[i−1]] 处的值,而这两个值指的均是 i = 2 i = 2 i=2 时的值,若 j j j 值从 c [ i − 1 ] c[i-1] c[i−1] -> v v v 更新, d p [ j − c [ i − 1 ] ] dp[j-c[i-1]] dp[j−c[i−1]] 必然会在 d p [ j ] dp[j] dp[j] 之前被更新,那么当计算 d p [ j ] dp[j] dp[j] 时, d p [ j − c [ i − 1 ] ] dp[j-c[i-1]] dp[j−c[i−1]] 表示的是 i = 3 i=3 i=3 时的值,而非 i = 2 i = 2 i=2 ,这样的话,就会出现错误。因此 j j j 不能从 c [ i − 1 ] c[i-1] c[i−1] -> v v v 更新,而应该从大到小更新。
实质上,使用一维数组来优化的过程,就是将二维数组按行拆分,每次更新保存一行数据,以此来降低存储开销。