这周学习的0/1背包是非常经典的DP问题。
在学习贪心算法的时候也接触过背包问题,查阅资料易知贪心算法用来解决假设每个物体可以切分的一般背包问题——例如吃自助餐,只要考虑从最贵的食物开始吃就可以,吃一半饱了,剩下的一半就不吃了;而0/1背包问题则是物体不可分割的情况——例如小偷偷金子,一次必须偷一整块,不可能把金子切开了。
先解释一下0/1背包的名字,我上高中时第一节数学课讲的是集合,一个元素必满足排中律,要么在集合里,要么不在集合里——0/1也是排中的,要么装进背包里(1),要么不装进背包里(0),不存在装一部分的情况。
设Xi表示重量为Wi的物品i装入容量为C的背包的情况,当Xi=0时不装入背包,当Xi=1时放入背包,则约束条件为:;目标函数为:
假设有4个物品,其重量分别是2、3、6、5,价值分别为6、3、5、4,背包容量为9
DP问题有一个笨比需要考虑清楚的地方——聪明人需要思考,动态转移方程怎么列,而这一说法是十分抽象的,我好久不得真谛。但是现在我想明白了,应该先考虑清楚,dp[i][j]中的i和j各要代表什么含义,这其实就是打出了一个行列表格,存储两个限制条件下的一个值。
在这里,可以把dp[i][j]看成一个背包,dp[i][j]表示把前i个物品装入容量为j的背包里获得的最大价值;那么很容易得到一个表:
背包容量 | → | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
不装 | 0 | ||||||||||
装第1个 | 1 | ||||||||||
装前2个 | 2 | ||||||||||
装前3个 | 3 | ||||||||||
装前4个 | 4 |
首先考虑只装一个物品的情况,由于物品1的重量是2,所以背包容量小于2的都放不进去,那么dp[1][0]=dp[1][1]=0;
物品1的重量等于背包容量,装进去后背包的价值等于物品1的价值,dp[1][2]=6;容量大于2的背包,多余的容量就用不到了,所以价值和容量2的背包一样,得到我们的第一个表:
→ | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | |
W1=2 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
V1=6 | 1 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
然后考虑只装入前两个物品,如果物品2大于背包容积同理放弃,如果物品2的等于背包容积,则有装和不装两种情况(就是我们的0/1):
(1)如果装物品2,那么相当于只把物品1装入容量为(-3)的背包中;
W1=2 | → | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
V1=6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
W2=3 | 1 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
V1=3 | 2 | 0 | 0 | 6 | 3+0 |
(2)如果不装物品2,那么相当于只把物品1装入背包中;
W1=2 | → | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
V1=6 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
W2=3 | 1 | 0 | 0 | 6 | 6 | 6 | 6 | 6 | 6 | 6 | 6 |
V1=3 | 2 | 0 | 0 | 6 | 6 |
取(1)和(2)的max,就有dp[2][3]=max{3,6}=6了。
重复这个过程,就能求出最后答案dp=[4][9]=11,显而易见,我们推导出了我们想要的状态转移方程!!!
HDU2602
The bone collector had a big bag with a volume of V ,and along his trip of collecting there are a lot of bones , obviously , different bone has different value and different volume, now given the each bone’s value along his trip , can you calculate out the maximum of the total value the bone collector can get ?
Input
The first line contain a integer T , the number of cases.
Followed by T cases , each case three lines , the first line contain two integer N , V, (N <= 1000 , V <= 1000 )representing the number of bones and the volume of his bag. And the second line contain N integers representing the value of each bone. The third line contain N integers representing the volume of each bone.Output
One integer per line representing the maximum of the total value (this number will be less than 231).
Sample Input
1
5 10
1 2 3 4 5
5 4 3 2 1
Sample Output
14
结合刚刚的思考过程,我们可以轻松套用进这一道题目,得到的题解代码如下:
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
struct BONE
{
int val;
int vol;
}bone[1111];
int T,N,V;
int dp[1111][1111];
int ans()
{
memset(dp,0,sizeof(dp));
for(int i=1;i<=N;i++)
for(int j=0;j<=V;j++)
{
if(bone[i].vol>j
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j],dp[i-1][j-bone[i].vol]+bone.val];
}
return do[N][V];
}
int main()
{
ios::sync_with_stdio(false);
cin>>T;
while(T--)
{
cin>>N>>V;
for(int i=1;i<=N;i++)
cin>>bone[i].val;
for(int i=1;i<=N;i++)
cin>>bone[i].vol;
cout<<ans()<<endl;
}
return 0;
}
滚动数组的使用:
在处理dp[ ][ ]状态数组的时候可以把它变成一维的dp[ ],以节省空同。观察上面的二维表dp[ ][ ]可以发现,每一行是从上面一行算出来的,只跟上面一行有关系,跟更前面的行没有关系。那么用新的一行覆盖原来的一行就可以了
int dp[1111];
int ans()
{
memset(dp,0,sizeof(dp));
for(int i=1;i<=N;i++)
for(int j=V;j>=bone[i].vol;j--)//反过来循环
dp[j]=max(dp[j],dp[j-bone[i].vol]+bone[i].val);
return dp[V];
}
这里比较需要注意的是j应该反过来循环,即从后面往前面覆盖。
经过滚动数组的优化,空间复杂度从O(NV)减少为O(V),这样可以极大程度的防止N和V过大导致MLE;但是滚动数组也有缺点,它把中间的各个转移状态覆盖了,只留下了最后的状态,所以损失了很多信息,不能输出背包的方案,在一些细节题中就不能用了。
如果我们想输出背包的方案呢?这需要我们倒过来思考一下我们的解题过程,接上面的叙述(而不是例题):
dp[4][9]=max{dp[3][4]+4,dp[3,9]}=dp[3][9],这说明没有装物品4,那么我们可以令X4=0;
dp[3][9]=max{dp[2][3]+5,dp[2][9]}=dp[2][3]+5=11,这说明装下了物品3,令X3=1;
依此类推,我们就用0/1把背包方案表打出来啦!
————————————————————————————————————————————————————————————————————————————
写在后面,我现在终于能理解贪心和DP的区别了。
虽然物品可分不可分的说法很有道理,但是这一段叙述根本不能解答我“什么时候使用贪心算法,什么时候使用0/1背包(或者说是动态规划)”的问题。
举一个我刚学完贪心算法,再学DP时钻了挺久牛角尖的问题:
走格子问题,求从左上角走到右下角时走过格子的和最大
0 22 48 55 10000 12 36 99 22 11 1 224 666 111 48 24 123 233 36 66 6666 34 56 76 120
我一直纠结的是,我每次开始追溯最优子结构中的最优解,明显很难从起点局部最优到10000啊。
为什么我会纠结这个问题?因为那时候我错误的把贪心的思想带进了DP,还不清楚,虽然贪心算法和动态规划都具备最优子结构,但是对最优子结构的追溯方法是不同的!
找了很多资料,都称贪心算法是一种自顶向下的算法,动态规划是一种自底向上的算法,但却没有给出相应的解释。在我反复对比硬币问题、背包问题的代码之后,我终于找出了我想要的“破题”之处,可能这样理解并不正确,却让我能有基本的解题思路了。
贪心算法和动态规划需要具备最优子结构,但是贪心是从起点开始摸索的,它走一步算一步,是“目光短浅的”,把子问题分成两半,一半是局部最优,一半是空,选择能看到的最优解,这就导致局部最优解的积累并不能保证得到全局最优解;而DP的问题则是从终点开始回溯,全局最优解向前退步,前一步必然是再往前一步的局部最优解,从而回到起点。
这么简单的问题,竟然想了这么久才想通了……