对使用倒序的一维数组解决0/1背包问题的理解

对使用倒序的一维数组解决0/1背包问题的理解

大略的题目:

   N个物品的价值,从这些物品中选出一些(可能全选)并装入容积为V的背包,求背包中的物品的最大价值。

输入: V,N, 接下来第一行是各个物品的体积v,第二行是各个物品的价值w。

输出: 背包能装入的物品的最大价值。

要想理解这个问题,可以使用表格来说明、使用二维数组的解法来做对比。

 

测试数据:

9  3
5  3  4
3  5  4

 

首先看二维数组代码(已经明白了就跳过):

 

View Code
 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表

 

再看一维数组写法:

(为了和二维数组的代码比较,我做了尽量少的改变)

View Code
 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背包问题了。

 

 

本文结束。

posted @ 2012-12-05 08:57  -六月飞雪-  阅读(4457)  评论(1编辑  收藏  举报