对使用倒序的一维数组解决0/1背包问题的理解
对使用倒序的一维数组解决0/1背包问题的理解
大略的题目:
N个物品的价值,从这些物品中选出一些(可能全选)并装入容积为V的背包,求背包中的物品的最大价值。
输入: V,N, 接下来第一行是各个物品的体积v,第二行是各个物品的价值w。
输出: 背包能装入的物品的最大价值。
要想理解这个问题,可以使用表格来说明、使用二维数组的解法来做对比。
测试数据:
9 3
5 3 4
3 5 4
首先看二维数组代码(已经明白了就跳过):
1 # include <stdio.h> 2 # include <string.h> 3 # define max(a, b) a>b ? a : b //宏替换 4 5 int dp[1010][1010], w[1010], v[1010]; //dp是动态规划的简写,dp[i][j]代表前i个元素装进容量为j的背包的最优解 6 7 int main () { 8 int V, N; 9 while (scanf("%d %d", &V, &N) == 2) { 10 int i; 11 for (i = 1; i <= N; ++i) 12 scanf("%d", &v[i]); //输入N个物品的体积 13 for (i = 1; i <= N; ++i) 14 scanf("%d", &w[i]); //输入N个物品的价值 15 memset(dp, 0, sizeof(dp)); //将dp的元素全部初始化为0 16 int j; 17 for (i = 1; i <= N; ++i) { 18 for (j = 0; j <= V; ++j) { 19 if (j >= v[i]) {//循环到第i次时,只有容积大于第i个物品的体积才可能装下第i个物品 20 //即dp[i-1][j-v[i]] + w[i] > dp[i-1][j] 才可能成立,此时比较大小才有意义 21 dp[i][j] = max(dp[i-1][j], dp[i-1][j-v[i]] + w[i]); /****状态转移方程****/ 22 //dp[i-1][j-v[i]] + w[i] 表示前i-1个物品装入容积恰好等于他们的体积之和的背包的最优解再加上第i个物品的价值 23 //其实也就是前i个物品装入容积为j的背包(不一定是恰好) 24 } else { 25 dp[i][j] = dp[i-1][j];//否则装不下第i个物品,所以dp[i][j]等于前i-1个物品装入j的背包的最优解 26 } 27 if (j == 0) continue;//打印dp表格 28 printf("%5d", dp[i][j]);//打印dp表格 29 } 30 putchar(10);//打印dp表格 31 } 32 printf("%d\n", dp[N][V]);//输出前N个物品装入容积为V的背包的最优解 33 } 34 35 return 0; 36 }
接着看这种解法的dp表
再看一维数组写法:
(为了和二维数组的代码比较,我做了尽量少的改变)
1 # include <stdio.h> 2 # include <string.h> 3 # define max(a, b) a>b ? a : b //宏替换 4 5 int dp[1010], w[1010], v[1010]; //dp是动态规划的简写,当循环到第i次时,dp[j]代表前i个元素装进容量为j的背包的最优解 6 7 int main () { 8 int V, N; 9 while (scanf("%d %d", &V, &N) == 2) { 10 int i; 11 for (i = 1; i <= N; ++i) 12 scanf("%d", &v[i]); //输入N个物品的体积 13 for (i = 1; i <= N; ++i) 14 scanf("%d", &w[i]); //输入N个物品的价值 15 memset(dp, 0, sizeof(dp)); //将dp的元素全部初始化为0 16 int j; 17 for (i = 1; i <= N; ++i) { 18 for (j = V; j >= 0; --j) { 19 if (j >= v[i]) {//循环到第i次时,只有容积大于第i个物品的体积才可能装下第i个物品 20 //即dp[j-v[i]] + w[i] > dp[j] 才可能成立,此时比较大小才有意义 21 dp[j] = max(dp[j], dp[j-v[i]] + w[i]); /****状态转移方程****/ 22 //dp[j-v[i]] + w[i] 当循环到第i次时, 23 //表示前i-1个物品装入容积恰好等于他们的体积之和的背包的最优解再加上第i个物品的价值 24 //其实也就是前i个物品装入容积为j的背包(不一定是恰好) 25 } else { 26 dp[j] = dp[j];//否则装不下第i个物品,所以dp[j]等于前i-1个物品装入j的背包的最优解 27 } 28 if (j == 0) continue;//打印dp表格 29 printf("%5d", dp[j]);//打印dp表格 30 } 31 putchar(10);//打印dp表格 32 } 33 printf("%d\n", dp[V]);//输出前N个物品装入容积为V的背包的最优解 34 } 35 36 return 0; 37 }
接着看这种解法的dp表:
可以发现,这两种写法导致了dp表左右相反。
二维dp写法的状态转移公式表明当前的dp值需要用dp表上面一个数值和左上的数值来确定。
那么一维dp写法是怎么实现状态的转换的呢?根据状态转换公式dp[j]可能等于dp[j](循环到i时的dp[j]和循环到i-1时的dp[j]是不同的! )即上面一个数值,
也可能等于dp[j-v[i]],即右上的数值。(这点比较难想到,原先我以为和二维的一样是由左上和上面的值推出来的)
为什么一维数组写法不能用顺序呢?我们看一下顺序dp表:
比较一下二维写法的dp表可以发现第2行、第三行的第6列之后的数值不对了,为什么呢?
以第二行第六列为例,此时i = 2, j = 6。dp[6] = max(dp[6], dp[6-v[2]]+w[2]) --①
也即dp[6] = max(3, dp[6-3]+5) --②
也即dp[6] = max(3, 5+5)
所以dp[6] = 10。
①式中的max中的dp[6]是指i=1时的dp[6],而max外的dp[6]是指i=2时的dp[6]。
②式中的dp[6-3],即dp[3],是指i=2时的dp[3],而不是i=1时的dp[3],原因是遍历到i=2,j=6时dp[3]已经被更新为i=2层的dp[3],
但是状态转移方程要求max中的dp是上一层的,而不是当前层的,所以从此开始dp表的值开始出错。
再返回来看一维dp的倒序写法,由于先更新下标比较大的dp数组元素,此时通过状态转移方程求最大值的时候还未更新下标较小的dp数组元素,
即下标较小的dp数组元素还是上一层的值,因此倒序的方法可以使用!
至此,我们就能够全部理解为什么能用一维数组来解决0/1背包问题了。
本文结束。