本文将讲解动态规划中背包问题,常见有三类:
- 0-1背包问题
- 多重背包问题
- 完全背包问题
上面三种都是背包问题,那么怎么区分呢?其实三种问题都很相似,解法也大体相同。
别急,先来区分上面三种,我们以问题描述来进行区分。
(1)0-1背包问题的描述
现在有四种物品,每种物品只有1件,它们的重量与价值如下表。
现在有一个背包,总容量为8。问怎么选取物品,可以使得背包装的物品价值最大?
物品编号 | 物品重量 | 物品价值 | 物品数量 |
1 | 2 | 3 | 1 |
2 | 3 | 4 | 1 |
3 | 4 | 5 | 1 |
4 | 5 | 8 | 1 |
(2)多重背包问题的描述
现在有四种物品,每种物品有若干件,它们的重量与价值如下表。
现在有一个背包,总容量为8。问怎么选取物品,可以使得背包装的物品价值最大?
物品编号 | 物品重量 | 物品价值 | 物品数量 |
1 | 2 | 3 | 2 |
2 | 3 | 4 | 2 |
3 | 4 | 5 | 2 |
4 | 5 | 8 | 2 |
(3)完全背包问题的描述
现在有四种物品,每种物品有无数件,它们的重量与价值如下表。
现在有一个背包,总容量为8。问怎么选取物品,可以使得背包装的物品价值最大?
物品编号 | 物品重量 | 物品价值 | 物品数量 |
1 | 2 | 3 | 无数件 |
2 | 3 | 4 | 无数件 |
3 | 4 | 5 | 无数件 |
4 | 5 | 8 | 无数件 |
通过以上例子,相信大家大概搞清楚了它们的区别吧。
0-1背包问题就是每种物品只有1件。
多重背包问题就是每种物品有若干件。
完全背包问题就是每种物品有无数件。
当然,我们讨论的背包问题对于每件物品而言,是不能够将该物品分割的,如果可以分割就要用到贪心算法了。
下面来讲解这三种背包问题的实现思路。
一、0-1背包问题
思路:对于每件物品,由于是不可分割的放入,所以,就有两种情况:该物品放入背包与该物品不放入背包;为了将以上问题求解出来,我们需要设置好状态以及状态转移方程。
- 定义状态
DP[k][w]:表示当背包剩余容量为w,现在有前k件物品可放的情况下,背包所能装物品的最大价值。
那么,状态确定好了,上面所描述的题目中,只要求出DP[4][8]就可以了。
等等,DP[4][8]是什么啊?DP[4][8]表示当背包剩余容量为8,现在有前4件物品可放的情况下,背包所能装物品的最大价值。
请问,这个DP[4][8]搞懂了没,搞懂了的话,我们继续看看DP[4][8]怎么求,那么也就是DP[k][w]怎么求的问题。
DP[k][w]怎么求呢,这就是状态转移方程的问题。
- 状态转移方程
我们先将状态转移方程写出来吧,就是:
DP[k][w] 等于下列两种情况:
①DP[k][w]=DP[k-1][w],当wk>w时
②DP[k][w]=max(DP[k-1][w],DP[k-1][w-wi]),当wk<=w时
解释:
第一种情况,当第k件物品的重量大于背包剩余的容量时,则DP[k][w]=DP[k-1][w];
这个关系式大家好好理解下,DP[k][w]表示的是当背包剩余容量为w,现在有前k件物品可放时,背包所能装物品的最大价值。由于第k件物品太重放不下背包,既然放不下第k件物品,就将其pass(排除)掉呗,相当于考虑放置前k-1件物品的情况,即f[k][w]=f[k-1][w]。
第二种情况,当第k件物品的重量不大于背包剩余的容量时,意味着第k件物品可以放置在背包中,“可以放置”并不意味着一定要放在背包中吧,所以,存在两种情形,第k件物品放在背包中或者第k件物品不放在背包中,这两种情形分别对应两种递推关系式,DP[k][w]=DP[k-1][w-wi]+v[i];(第k件物品放在背包中的情况,后面的v[i]是指第i件物品的价值,因为放进了背包,所以背包的价值多了v[i],同理,背包剩余容量就减少了wi)
DP[k][w]=DP[k-1][w];(第k件物品不放在背包中的情况)
所以,第二种情况的结果就是取两种情形的最大值
DP[k][w]=max(DP[k-1][w-wi]+v[i],DP[k-1][w])
综合以上两种情况,DP[k][w]的值就是:
现在,我们要做的就是如何求出DP[4][8]的值,这个值的求解就是通过上面的状态转移方程进行推导,推导的过程就是填写以下表格。
首先,我们对以上表格进行初始化赋值,第0行的值都为0,DP[0][0]=0,DP[0][1]=0,DP[0][2]=0,DP[0][3]=0,…,DP[0][8]=0,为什么都是0呢,你想呀,因为是考虑前0件物品放进背包时的价值,0件物品的意思是说没有物品可放,既然没有物品可放,当然价值为0;
同理,第0列也都是为0,DP[0][0]=0,DP[1][0]=0,DP[2][0]=0,DP[3][0]=0,DP[4][0]=0,为什么都是0呢,因为背包剩余容量为0,说明背包装不下任何东西了,那么能装的价值当然为0;
以上就是赋初始值,将第0行与第0列都赋值为0,那么其它值该如何推导呢,我们也是根据状态转移方程进行推导,进而可以完成以下表格。
我们举个栗子(哈哈),看看图中红色方格中的数字怎么得到,红色方格代表DP[2][3],表示的是当背包剩余容量为3,现在有前2件物品可放时,背包所能装的最大价值。因为第2件物品的重量w2=3,此时背包剩余容量为3,说明第2件物品可以放置在背包中,所以,对应于状态转移方程中的第二种情况,DP[2][3]=max(DP[1][0]+4,DP[1][3])=4。
DP[2][3]能这样得到,那么其它方格中的数字,相信大家也一定能推导出来吧。不妨试试推导下DP[4][8]的值吧。
手撕代码,相信大家一定能看懂
#include<bits/stdc++.h>
using namespace std;
const int N=10010;
const int V=10010;
int dp[N][V];
int n,v;//n:物品数量,v:背包实际容量
int w[N];//第n件物品的重量
int c[N];//第n件物品的价值
void knap_01()
{
for(int i=1;i<=n;i++)
for(int j=1;j<=v;j++)
{
if(w[i]>j)
dp[i][j]=dp[i-1][j];
else
dp[i][j]=max(dp[i-1][j-w[i]]+c[i],dp[i-1][j]);
}
}
int main()
{
cin>>n>>v;
for(int i=1;i<=n;i++)
cin>>w[i];
for(int i=1;i<=n;i++)
cin>>c[i];
knap_01();
cout<<dp[n][v];
return 0;
}
时间复杂度:O(nv)
以上代码我们可以得出答案:DP[4][8]=12,如果我们要输出具体选择的方案,即具体选择了哪些物品呢?我们可以通过DP[4][8]往前进行回溯得到。
怎么回溯呢?再看如下表
从DP[4][8]进行反向推导,由于DP[3][8]的值等于9,如果背包中没有放入第4件物品,那么DP[4][8]的值一定等于DP[3][8]的值,而两者不相等,说明一定选择了第4件物品。
既然选择了第4件物品,那么DP[4][8]是怎么得到呢,DP[4][8]是通过DP[3][3]+8得到。
DP[3][3]的值与DP[2][3]的值相等,说明没有选择第3件物品。
DP[2][3]的值与DP[1][3]的值不相等,说明选择了第2件物品。
既然选择了第2件物品,那么DP[2][3]是怎么得到呢,DP[2][3]是通过DP[1][0]+4得到,DP[1][0]就是回溯的终点了,无需再进行回溯。
根据以上的过程,本题选择的物品编号分别为2号与4号。
以上过程可以通过递归进行解决,代码如下:
//0-1背包问题
//选择方案的输出
void print(int i,int j)
{
if(i==0||j==0) return;
if(dp[i][j]==dp[i-1][j])
print(i-1,j);
else
{
print(i-1,j-w[i]);
cout<<i<<" ";
}
}
以上就是0-1背包的思想,总结如下:
- 题目特点:每种物品只有1件,每次要么选择,要么不选择,就有原子性
- 定义状态
DP[k][w]:表示当背包剩余容量为w,现在有前k件物品可放的情况下,背包所能装物品的最大价值。
3.状态转移方程
4. 时间复杂度:O(nv)
5. 方案的输出:使用回溯,运用递归,轻松输出
0-1背包问题我已经将视频分享到B站平台,引起了众多朋友的关注,如果有需要,大家可以前往B站平台观看:
https://www.bilibili.com/video/BV1g7411B7SP?share_source=copy_web&vd_source=d1f6364105c853f569fdfac1f2d6cb40
后续将继续推出多重背包,完全背包等经典问题,欢迎大家关注!