01背包问题的资料看下来,我总结了一句话,物品一件一件增加,背包一点一点变大。
01背包问题描述
- 最基本的01背包问题描述是,有一个限重W的背包,有好几件重量为weight,价值为value的物品供你挑选,要在不超过背包限重的前提下,巧妙地选择物品,使得背包里面的物品价值最大。
- 面试笔试遇到的是01背包问题的应用题,重点在于把原始问题转化为背包问题,就跟做数学题套公式有点像,把题目中的已知条件跟背包限重W,以及物品的重量weight和价值value对应起来,问题就会迎刃而解。
动归三部曲
由于01背包问题过于常见,递推公式很容易就能背下来,有人写dp时对初始化也比较佛系,思考深度不够。然而深刻理解01背包问题的推导和初始化,对理解动态规划的本质意义重大。
题目举例:W = 4 weight=[1,3,4] value=[15,20,30]
1.定义dp数组
这时需要把物品一件一件增加,背包一点一点变大这句话放在心里。
- 一共有三件物品可以选,所谓物品一件一件增加就是这样,第一次没物品可以挑,第二次1件物品可以挑,第三次2件物品可以挑... 每步都多出了一件物品供自己挑选,那自己就可以决定,放进去还是不放了。
- 限重为4,所谓背包一点一点变大就是这样,4的限重太大,先从限重为0开始算起,直到达到4为止。
最终dp是一个3*5的数组,dp[i][j]
的含义就是有0-i件物品供你挑选,放入容量为j的背包能获得的最大价值。
(对于动态规划问题的分析,要始终牢记dp的含义)
那最后的结果就是有0-2件物品供你挑选,放入容量为4的背包能获得的最大价值,自然是数组右下角那个格子dp[2][4]
了。
2.递推公式
递推公式怎么去想呢?核心就是每次多了件物品可以挑,要区分这个物品到底要不要放进背包。
- 这个物品要
dp[i][j] = dp[i-1][j]
比如说0-2件物品供你挑,限重j,那你决定不要第2件物品,那好,背包限重没变,这个时候背包的价值就是0-1件物品供你挑,限重j时计算的最大价值dpi-1(也可以理解成,这个物品要是不拿,就跟没他没什么区别,所以还是之前没他时候,即i-1时的最大价值)。
- 这个物品不要
dp[i][j] = dp[i-1][j-weight[i]]+value[i]
比如说0-2件物品供你挑,限重j,那你决定要第2件物品,那好,这个时候背包装的价值肯定先把这件物品的价值给算上,那剩下的价值呢,就给0-1件物品去完成吧,只不过因为我放了第2件物品,背包的限重变少了weight[i]。
- 取最大价值 可挑可不挑,那就看看哪个价值大就选哪个作为最大价值。
递推的顺序就是下图了
3. 初始化
要初始化的地方就是递推不到的位置,然后算别的dp[i][j]
的时候需要用到的值。图中就是第0列和第0行。
- 第0列 限重0,无论多少物品那都没的放,价值肯定为0,所以第0列全0。
- 第0行 有第0件物品可以挑,那如果限重>=这个物品,就代表物品可以放进去,价值就是这个物品的值。
C++代码实现 含详细注释
#include
#include
using namespace std;
int main(){
int W = 4;//限重
int weight[3] = {1,3,4};//物品重量
int value[3] = {15,20,30};//物品价值
vector> dp(3,vector(W+1,0)); //3*5的dp
//初始化
for(int i = 0;i < 3;i++){
dp[i][0] = 0;
}
for(int j = 1;j <= W;j++){
if(j >= weight[0]) dp[0][j] = value[0];
}
//递推
for(int i = 1;i < 3;i++){
for(int j = 1;j <= 4;j++){
dp[i][j] = dp[i-1][j];//不拿这件物品
if(j >= weight[i])//下标不越界 或者说之后限重>=物品才能拿物品
//拿这件物品和不拿这件物品 找最大
dp[i][j] = max(dp[i-1][j],dp[i-1][j-weight[i]]+value[i]);
}
}
//打印下dp
for(int i = 0; i < 3;i++){
for(int j = 0;j <=4;j++){
cout << dp[i][j] << " ";
}
cout << endl;
}
cout << dp[2][4];//最终结果
return 0;
}
手推一次dp数组加深理解 为优化打基础
手推时,发现可以先把上一行的数据整体移动下来,当限重>=该物品重量时再思考如果挑选这个物品价值是多少,选更大价值即可(要是限重小了那这个物品肯定不能放,只能从上一行移下来了)。
手推二维dp数组可以为滚动数组优化做铺垫。
滚动数组优化
滚动数组要着重理解的是初始化和遍历顺序的问题。
动归三部曲
1. 定义dp数组
根据手推dp数组中的经验我们可以知道,算下一行是先把上一行的数据给搬下来了。所以优化时可以只用一行格子,而不用全部的格子。
dp[j] 含义就是限重j时的最大价值。一件物品一件物品去迭代,返回的是最后一个格子的值。
2. 递推
!用一排格子就得思考一个问题,值覆盖了怎么办?
在二维dp中,显然我们用到了上方和左上方的值,那一维dp就会用到原地和左方的值。那要是从左往右算,值变了咋整,不就不是上一轮结束的dp了吗。所以只能==倒着往前遍历==,就不会有这种事了,反正dp[i]算的时候又跟后面的值没关系。
下图所示的是迭代到第1件物品时要用到的其他值。(weight[2] = 3,所以只需要算限重3和4。绿色是橙色格子在递推时需要用到的值)
3. 初始化
当然可以像为二维dp初始化第一行一样初始化一维dp,然后从第1件物品开始迭代。但其实可以初始化为全0,从第0件物品开始迭代,效果一样。
检验全0的初始化对不对,主要看第0件物品的迭代是否正确。此时发现,仍然是当限重>=weight[0]时,dp[i]=max(dp[i],dp[i-weight[i]]+value[i])=max(0,value[i])=value[i]
。
C++代码加注释
#include
#include
using namespace std;
int main(){
int W = 4;//限重
int weight[3] = {1,3,4};//物品重量
int value[3] = {15,20,30};//物品价值
vector dp(W+1,0); //5的dp
//初始化 全0
//递推
for(int i = 0;i < 3;i++){
for(int j = 4;j >= 0;j--){
if(j >= weight[i])
dp[j] = max(dp[j],dp[j-weight[i]]+value[i]);
}
}
cout << dp[4];//最终结果
return 0;
}