看了一天的背包问题,也想了很多,下面决定对0-1背包,完全背包,多重背包做个总结。
一.0-1背包
定义:
何谓0-1背包,可以这样想,那里有一堆值钱的东西,每一样东西只有一件,他们的价值和体积都不一样,现在要你从这n件里面挑选一些放到一个容量一定的背包里面,使得你的背包里的东西总价值最大。对于这些东西的每一件,你可以选择放进你的背包或者是不放进去。(这里放与不放就对应着两种状态0,1),所以称之为0-1背包。
解法:
为什么我们要选用动态规划的思想呢?
考虑到运用dp(动态规划)的几个步骤
1. 描述一个最优解的结构;
2. 递归地定义最优解的值;
3. 以“自底向上”的方式计算最优解的值;
4. 从已计算的信息中构建出最优解的路径。
其中第1-3点是动态规划求解问题的基础,若题目只求最优解,则第四点可以省略。(以上几点都是算法导论上面的原话)
为了下面讲解方便,先我们规定f[i][v]来表示前i件物品放入到容量为v的背包里所获得的最大总价值,c[i]为第i件物品的体积,w[i]表示第i件物品的价值
现在对于这个问题,第i件物品不放或是放,则有两种状态,
第一种是不放,则f[i][v] = f[i-1][v] ;
第二种是放,则f[i][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]) ;(dd大牛的背包九讲上说这个方程很重要,几乎所有的背包问题都是从这个方程衍生出来的)
于是从第1件物品开始,一件一件决定放还是不放。
我们可以写下伪代码
for i=1:n
for j=0:v
{
if(j>=c[i])
{
f[i][j] = max{f[i-1][j],f[i-1][j-c[i]]+w[i]};
}
else
{
f[i][j] = f[i-1][j];//第i件物品放不进来
}
}
当然我们得初始化f[0][0]-----f[0][v]都为0(以上伪代码省略)
例子:
为了方便理解我们举个例子:
有三件物品
1. c = 2 w = 1
2. c = 3 w = 2
3 c = 5 w = 4
背包的总容量v为5
于是打出下表
最后f[3][5]就是答案了,
滚动数组:
注意到我们的动态转移方程f[i][j] = max{f[i-1][j] , f[i-1][j-c[i]]+w[i]};
当前状态只与二维数组的前一行有关,那么之前的那些行都没用了啊,何必在那里浪费空间呢?这就引申了滚动数组的一个概念(其实就是一维数组啦)为何叫滚动呢?因为是一行一行,要求的当前行只与上一行有关。到此我们的0-1背包就可以用一维数组来求解了。看完下面,我们就可以彻底理解滚动数组了。
我们定义f[v]为前i件物品放进容量为v的背包里所获得的最大价值
于是写下动态转移方程: f[v]=max{f[v] , f[v-c[i]]+w[i]}
那么这次写程序还是j从0开始循环么?(也就是上表从左到右一行一行遍历)显然不对,因为这样我们就没有利用到前一行所求的结果。只有j从v开始才能保证是从利用到前一行的值(仔细想想这里),因为我们在更新f[v]之前f[v-c[i]]的值还在嘛。再想想滚动数组,是不是有点体会了?滚动滚动,从右到左,既更新了当前的值,也利用到了上一行的值。大大节省了内存空间。
现在我们可以写下程序了
for i=1..n
for j=v..0
f[j]=max{f[j],f[j-c[i]]+w[i]};
到此0-1背包完毕。
二、完全背包
定义:
何谓完全背包呢?其实和0-1背包一样,只是现在每一样物品都有无限件可以选。只是一个量的不同。
与0-1背包的联系:
那么他又和0-1背包有什么联系呢?在此引用一下dd大牛讲的。我们可以这样想:第i件物品有v/c[i]件(因为背包最多放v/c[i]件编号为i的物品嘛)然后把他们都看成是体积相同,价值相同的不同物品,这不就成了0-1背包问题吗?当然这里只是讲一下二者的联系,如果这么去做。显然耗时是很多的。
求解:
稍微想一下我们就可以写下动态转移方程f[i+1][j] = max{f[i][j-k*c[i]]+k*w[i]} 0<=k<=j/c[i]
用白话讲就是第i件物品选k件放入背包所能获得的最大价值
显然我们可以写一个三重循环的程序出来,不过效率很低。稍微分析一下。
在f[i+1][j]的计算中选择k个的情况与在f[i+1][j-c[i]]的计算中选择k-1个的情况是一样的,所以f[i+1][j]的递推中k>=1的部分的计算已经在f[i+1][j-c[i]]的计算中完成了。那么可以根据如下方式进行变换。
f[i+1][j] = max{f[i][j-k*c[i]]+k*v[i] k>=0}
= max{f[i][j] , max{f[i][j-k*c[i]]+k*v[i]} k>=1}
= max{f[i][j] , max{f[i][(j-c[i])-k*c[i]]+v[i]+k*v[i]} k>=0} (注意联系k的取值范围)
= max {f[i][j], f[i+1][j-c[i]]+v[i]}
于是可以写出下面的两重循环的程序
void solve()
{
for(int i=0;i
同样根据0-1背包,我们也可以用一维数组优化空间
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k*c[i]<=v}
先给出答案再解释
for i=1..n
for j=c[i]..v
f[v]=max{f[j],f[j-c[i]]+w[i]}
初学者可能会感到诧异,为什么这里没看到取k个中的最大价值呢?
其实内层循环就隐含了这个意思了啊:先放第一件物品,内层循环就是试探当前放几个第i件物品,容量为j时取最大价值啊。
比如第一件体积是2 价值是3 背包总容量是7 一次循环之后f[1]=0,f[2]=3,f[3]=3,f[4]=6 f[5]=6 f[6]=9 f[7]=9
那么为什么这里j是从0...v呢?
先来对比一下0-1背包和完全背包的动态转移方程
0-1背包: f[j]=max{ f[v] , f[j-c[i]]+w[i]}
完全背包: f[j]=max{ f[j-k*c[i]] + k*w[i] } |0<=k*c[i]<=v
对于0-1背包,前面已经讲了要求f[v] ,则必须f[v-c[i]]的值还没更新,因为他是在两者之中取大值嘛。所以j从v倒退着走。
而对于完全背包,我们注意他的动态转移方程。要更新f[j]则必然先更新了f[j-k*c[i]],换句话说,一旦第i件物品加入之后,则势必影响之后的最优解,还是拿上面那个例子来说
第一件体积是2 价值是3 背包总容量是7 一次循环之后f[1]=0,f[2]=3,f[3]=3,f[4]=6 f[5]=6 f[6]=9 f[7]=9
加入第二件物品(体积是3,价值是5)之后 f[1]=0,f[2]=3,f[3]=5...从此之后f[3]就已经影响后面的解了。可以动手写写。
完全背包到此结束。。
三:多重背包
定义:
其实也是和0-1背包差不多,只不过是对于第i件物品,有有限个个数为m[i],这三种背包问题其实就只有一个区别嘛,那就是某件物品的量的差别。
解法:
有了前面的基础,写下这个动态转移方程还不是轻而易举么?
f[i][v]=max{f[i-1][v-k*c[i]]+k*w[i]|0<=k<=m[i]}
优化的方法:
运用神奇的二进制,进行物品拆分,转化成01背包
物品拆分,把13个相同的物品分成4组(1,2,4,6)
用这4组可以组成任意一个1~13之间的数!
原理:一个数总可以用2^k表示
而且总和等于13,所以不会组成超过13的数
所以可将一种有C个的物品拆分成1,2,4,...,2^(k-1),C-(2^k-1)
然后进行01背包