背包问题不仅仅只适用于求最优解的情况,它同样适用于计数型dp,求解可行等问题~这些问题没有最优解的帽子,看起来就不那么容易想到背包了
今天也是为了cc,努力奋斗的一天ヾ(≧▽≦*)o
0. 引言
背包问题是一类经典的动态规划问题,它有很多的形式,本节只介绍三种最简单的背包问题:
- 0-1背包问题
- 完全背包问题
- 多重背包问题
而在这三种背包中,又以 0-1背包为重。
之所以说“0-1背包最重要”是因为完全背包问题和多重背包问题都可以转换成0-1背包问题,而且大多数的背包问题是在0-1背包的基础上解决的。
1. 多阶段动态规划问题
- 定义
有一类动态规划可解的问题,它可以描述成若干个有序的阶段,且每个阶段的状态只和上一个阶段的状态有关,一般把这类问题称为 多阶段动态规划问题。 - 举例
如下图所示,问题被分为了5个阶段,其中状态F属于阶段3,它由阶段2的状态C和状态D推得。显然,对这种问题,只需要从第一个问题开始,按照阶段顺序解决每个阶段中的状态的计算,就可以得到最后一个阶段中的状态的解。
这对设计状态的具体含义是很有帮助的,01背包问题就是这样一个例子。01背包的阶段就是物品们的编号1~n,阶段中的状态就是状态数组dp[i][j]。 - ⭐在我看来,多阶段动态规划问题的
dp
数组的维度通常是2维的,一个维度是阶段,一个维度是阶段中的状态。 - 为什么要讲解这样一个多阶段呢?
因为正是因为多阶段,当前阶段的状态只与前一阶段的状态有关,所以我们可以将背包问题中的二维数组转换成一维数组。
2. 0-1背包问题
2.1 问题描述
0-1背包的注意事项:
- 背包可以不被装满(后面会有专门的题目说必须要求装满的);
- 名字的由来:因为在最优解中,每个物品都有两种可能的情况,即在背包中或者不存在(背包中有0个该物品或者1个),所以我们把这个问题称为0-1背包问题。
2.2 暴力解决方案
时间复杂度为:
O
(
2
n
)
O(2^n)
O(2n)
2.3 动态规划解决方案
时间复杂度为: O ( n V ) O(nV) O(nV)
状态定义
令dp[i][v]
表示从前i件物品(1≤i≤n,0≤v≤V)选择物品装入容量为v的背包中所能获得的最大价值。 怎么求解dp[i][v]
呢?
考虑对第i
件物品的选择策略,只有两种策略:
- 不放第
i
件物品,那么问题转换为前i-1
件物品恰好装入容量为v的背包中所能获得的最大价值,也即dp[i-1][v]
。 - 放第
i
件物品,那么问题转化为前i-1件物品恰好装入容量为v-w[i]
的背包中所能获得的最大价值,也即dp[i-1][v-w[i]]+c[i]
。
状态转移方程
由于只有这两种策略,且要求获得最大价值,因此有:
上面这个就是状态转移方程。注意到dp[i][v]
只与之前的状态dp[i-1][]
有关,所以可以枚举i
从1到n,v
从0到V,通过边界dp[0][v] = 0
(0≤v≤V)(即前0件物品放入任何容量v的背包中都只能获得价值0)就可以把整个dp
数组递推出来。
因此可以写出代码:
for(int i=1;i<=n;i++){
for(int v=w[i];v<=V;v++){
dp[i][v] = max(dp[i-1][v],dp[i-1][v-w[i]]+c[i]);
}
}
可以知道,时间复杂度和空间复杂度都是 O ( n V ) O(nV) O(nV),其中时间复杂度已经无法再优化,但是空间复杂度还可以再优化。
代码如下:
for(int i=1;i<=n;i++){
for(v = V; v >= w[i] ;v--){ //逆序枚举v
dp[v] = max(dp[v],dp[v-w[i]]+c[i]);
}
}
这样01背包问题就可以用一维数组表示来解决了,空间复杂度为
O
(
V
)
O(V)
O(V)。
特别说明:如果是用二维数组存放,v的枚举是顺序还是逆序都无所谓;如果使用一维数组存放,则v的枚举必须是逆序!
完整的求解01背包问题的代码如下:
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn = 100; //物品最大件数
const int maxv = 1000; //V的上限
int w[maxn],c[maxn],dp[maxv];
int main(){
int n,V;
scanf("%d%d",&n,&V);
for(int i=1;i<=n;i++){
scanf("%d",&w[i]);
}
for(int i=1;i<=n;i++){
scanf("%d",&c[i]);
}
//边界
for(int v=0;v<=V;v++){
dp[v] = 0;
}
for(int i = 1;i<=n;i++){
for(int v=V;v>=w[i];v--){
//状态转移方程
dp[v] = max(dp[v],dp[v-w[i]]+c[i]);
}
}
printf("%d\n",dp[V]);
return 0;
}
事实上,对多阶段动态规划问题来说,都可以尝试把阶段作为状态的一维。
如果当前设计的状态不满足无后效性,那么不妨把状态进行升维,即增加一维或若干维来表示相应的信息,这样可能就满足无后效性。
2.4 恰好装满背包⭐⭐⭐⭐⭐
总结一下:
- 无论怎样
dp[0][0]
= 0; - 如果是求最大值,那么
dp[0][1~maxc]=-inf
;(若大则负) - 如果是求最小值,那么
dp[[0]1~maxc]=inf
;(若小则正)
3. 完全背包问题
3.1 问题描述
3.2 状态定义
同样,令dp[i][v]
表示前i
件物品放入容量为v
的背包中能获得的最大价值。
和01背包一样,完全背包问题的每种物品都有两种策略,但是也有不同点。对第i
件物品来说:
- 不放第
i
件物品,那么dp[i][v] = dp[i-1][v]
,这步跟01背包是一样的; - 放第
i
件物品。这里的处理和01背包有所不同。
3.3 状态转移方程
看上去和01背包很像是不是?其实唯一的区别就在于max的第二个参数是dp[i]而不是dp[i-1]。而这个状态转移方程同样可以改成一维的形式,即:
写成一维形式之后和01背包完全相同,唯一区别在于这里v的枚举顺序是 正向枚举,而01背包的一维形式中v必须是 逆向枚举。完全背包的一维形式代码如下:
- 怎么理解必须正向枚举呢?
如下图所示,求解dp[i][v]
需要它左边的dp[i][v-w[i]]
和它上方的dp[i-1][v]
,显然如果让v
从小到大枚举,dp[i][v-w[i]]
就总是已经计算出的结果;而计算出dp[i][v]
之后dp[i-1][v]
就再也用不到了,直接可以覆盖。
4. 多重背包问题——二进制表示法⭐⭐⭐⭐⭐
上面的二进制拆解就是用二进制的思想拆解数字k
,从而可以 表示0~k
中的任何数字 ,它是一种优化复杂度的方法。
其实也并不需要怎么特别理解,只需要举个例子就会了:
对于13,有:
13=[1+2+4] +6
对于7,有:
7=[1+2+4]
对于5,有:
5=[1+2] +2
这样理解就好了~~
5. 题型训练
- 【0-1背包基础入门题】采药
- 【0-1背包基础入门题】点菜问题
- 【0-1背包之必须充满背包】最小邮票数
- 【完全背包基础入门,计数型背包】LeetCode 518. Coin Change 2
- 【完全背包之必须充满背包】Piggy-Bank
- 【多重背包】珍惜现在,感恩生活
- ⭐⭐⭐【多重背包之可行解型】POJ 1014 Dividing
6. 参考文档
- 算法笔记
- 王道机试指南