- chjshen 的博客
基础算法-01背包
- 2023-5-31 20:35:46 @
1、背包问题
基本模型:一个容量为V的背包,在一定的限制条件下,放进最多(或最少)价值的东西。
根据限制条件的不同,分为三种情况:
- 01背包(ZeroOnePack):每种物品只有一件,即只能选择放或者不放
- 完全背包(CompletePack):每种物品有无限件
- 多重背包(MultiplePack):每种物品最多有n[i]件可用
2、01背包动态规划
基本模型:一个容量为V的背包与N件物品,第i件物品的体积是c[i],价值为w[i],求将哪些物品装入背包可使得价值总和最大。
维度
为:i件物品、j容量的背包
定义状态
:设dp[i][j]表示把i件物品放入容量j的背包可获得的最大价值。
对于第i件物品(体积为c[i],价值为w[i]):
- 不放到背包里,则最大价值为dp[i-1][j]
- 放到背包里,则最大价值为dp[i-1][j-c[i]] + w[i]
状态转移方程
为:
- 初始化:dp[0][j] = 0, dp[i][0] = 0
- j < c[i]:dp[i][j] = dp[i-1][j]
- j>=c[i]:dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]] + w[i])
3、理解过程
假设背包容量V=8,物品数量N=4:
物品编号 | 1 | 2 | 3 | 4 |
---|---|---|---|---|
体积c | 2 | 3 | 4 | 5 |
价值w | 3 | 4 | 5 | 6 |
3.1 第一步:初始化
- dp[i][0]和dp[0][j]都为0
- 前两列体积c和价值w,为方便查询
c | w | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|---|---|
- | 0 | 0 | 0 | ||||||||
2 | 3 | 1 | |||||||||
3 | 4 | 2 | |||||||||
4 | 5 | 3 | |||||||||
5 | 6 | 4 |
3.2 第二步:怎么填?
- 1的列:由于1 < c[1](体积为2)所以dp[1][1] = dp[1-1][1] = 0
- 同样,dp[2][1]、dp[3][1]、dp[4][1]等都是0
- dp[1][2]:由于2==c[1], dp[1][2]=max(dp[1-1][2], dp[1-1][2-2] + 3) = 3
- 理解:把1物品放进去,体积为2,获得价值为3
- dp[2][2]:由于2<c[2], dp[2][2] = dp[2-1][2] = 3
- 理解:2物品放不进去,所以还是只能放一个1物品
- ....依次类推到dp[1][3]、dp[1][4]...、dp[1][8] 都为3
c | w | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|---|---|
- | 0 | 0 | 0 | ||||||||
2 | 3 | 1 | 0 |
3 |
3 | ||||||
3 | 4 | 2 | |||||||||
4 | 5 | 3 | |||||||||
5 | 6 | 4 |
3.3 第三步:dp[2][3]
dp[2][3] = max(dp[2-1][3], dp[2-1][3-c[2]]+w[2])=max(3, 0+4)=4
- 理解:物品1或物品2都可以放进去,选择价值大的:
- 物品2放进去,则剩余容量为0,为dp[1][0] + w[2]=4
- 物品2不放进去,为dp[1][3]=3
c | w | i\j | 0 | 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|---|---|
- | 0 | 0 | 0 | ||||||||
2 | 3 | 1 | 0 |
3 | 3 |
3 | |||||
3 |
4 |
2 | 0 | 4 |
|||||||
4 | 5 | 3 | |||||||||
5 | 6 | 4 |
dp[2][3]更直观的理解:
- 如果放进去
- 本列第一行的3,减去本行的体积列(c[i]),结果为0
- 找到上一行的0列的值,值为0
- 0加上本行的价值列(w[i]),结果为4
- 如果不放进去:取上一行的本列的,值为3
- dp[2][3]填写4和3的最大值4
3.4 最后结果,按以上理解逐步填写
c | w | i\j | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 |
---|---|---|---|---|---|---|---|---|---|---|---|
- | 0 | 0 | 0 | ||||||||
2 | 3 | 1 | 3 | 3 | |||||||
3 | 4 | 2 | 4 | 4 | 7 | 7 | |||||
4 | 5 | 3 | 5 | 8 | 9 | 9 | |||||
5 | 6 | 4 | 10 |
最后,dp[4][8] = 10
4、关键代码
for(int i=1; i<=N; i++)
for(int j=1; j<=V; j++) {
if(j >= c[i])
dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]] + w[i]);
else
dp[i][j] = dp[i-1][j];
}
优化
1、复杂度
以上动态规划的时间复杂度与空间复杂度均为O(N* V)。
但空间复杂度可以优化到O(m),因为每一次状态转移只与上一行的值有关,我们可以:
- 状态只用一维数组
- 同一行里,从后往前填写,这样前面的值还被保留(上一行的值)
定义状态
:设dp[j]表示把i件物品放入容量j的背包可获得的最大价值。
状态转移方程
为:
- 初始化:dp[j] = 0
- j>=c[i]:dp[j] = max(dp[j], dp[j-c[i]] + w[i])
2、记录路径
如果想知道最大价值的方案里,背包里都放了哪些物品,该怎么做?
只要用一个二维数组path来存储每次更新放入的物品,然后从后往前遍历输出。
用path逆推出装入的物品:
- i从N、j从V往下倒序
- 如果Path[i][j]为1,表示被装入:
- 输出物品i和价值,价值用括号
- j = j - c[i]
- i = i - 1
如果使用二维状态,则无需path也可以倒推出装入的物品。
3、满背包
有时候,题目会要求背包刚好装满,这种情况与不装满的情况比,初始化不一样:
- 把dp[0][j](j>0)初始化为负无穷大,表示背包没有装满。
- 把dp[0][i]初始化为0,表示装满了
- 当某个状态为不能装满时,值为负无穷,即使加上w[i]也算负无穷
- 判断某个状态,小于0就表示不能装满
在c++中,一般可用0x3f3f3f3f表示无穷大。
#define inf 0x3f3f3f3f;