- 01背包:每件物品只能选一个,背包体积为
V,每个物品的体积和价值分别是 v i , W i v_{i},W_{i} vi,Wi - 完全背包:每件物品能取无限个
- 多重背包:每个物品个数不一样,每个物品最多有
xi个 - 分组背包:物品有n组,每组物品有若干个(组里的物品的属性可能不相同),每组最多只能选一个物品
对于闫氏dp法的基本流程:
先以最朴素方式考虑
对于一个dp问题分成两种情况考虑:状态表示和状态计算
对于状态表示,再从两个角度考虑:
- 它所表示的集合是什么(这个集合的定义是什么)
- 这个集合所包含的属性是什么(一般为MAX,MIN,COUNT;即题目要求的)
而这个状态表示的结合,要考虑的就是两个方面:
- 他的定义是什么
- 这个集合所满足的条件是什么
例如01背包的画图表示:

对于状态计算来说,就可以用ven图来进行详细表示;而对于ven图表示的集合,我们对其划分的原则是:不重,不漏
01背包
dp做法两个角度:
-
状态表示:用 f [ i , j ] f[i, j] f[i,j]表示所有只从前i个物品中选,且总体积不超过j的所有方案的集合
属性:求MAX
-
状态计算:(1)集合的划分所有不选择第i个物品的方案 + (2)所有选择第i个物品的方案
常用集合划分依据:找最后一步或者最后一个不同点,找不到就找倒数第二步
所以说,有了上述的状态计算的公式,我们就可以推出来这样一个数学公式:
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
−
1
,
j
−
v
[
i
]
]
+
w
[
i
]
)
f[i,j]=max(f[i-1,j], f[i - 1, j -v[i]] + w[i])
f[i,j]=max(f[i−1,j],f[i−1,j−v[i]]+w[i])
#include <iostream>
using namespace std;
int n, m;
int v[1000 + 10];
int w[1000 + 10];
int f[1000 + 10][1000 + 10];
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for (int i = 1; i <= n; i++){
for (int j = 0; j <= m; j++){
f[i][j] = f[i - 1][j]; // 同步集合(1)的情况,因为集合(2)并不是所有情况都一定存在的,就如下一行判断条件所述
if(j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout << f[n][m]; // 根据集合定义,从前n个物品中选,总体积不超过m的最大价值
return 0;
}
这时候时间复杂度应该是不能优化了,但是我们应该可以优化空间复杂度
所有的 f [ i , j ] f[i,j] f[i,j]的答案更新都是由 f [ i − 1 , j ] f[i - 1, j] f[i−1,j] 得到的,即只通过上一层来更新,那么我们可以把它优化一下,只用一维状态来更新。
用一维数组来保存上一层的状态,那么:
f
[
j
]
=
m
a
x
(
f
[
j
]
,
f
[
j
−
v
[
i
]
]
+
w
[
i
]
)
f[j] = max(f[j], f[j - v[i]] + w[i])
f[j]=max(f[j],f[j−v[i]]+w[i])
那么我们现在来看,(1)和(2)两个式子差在了哪里:
集合(1)可以从 f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i−1][j]变成 f [ j ] = f [ j ] f[j]=f[j] f[j]=f[j];集合(2)变成 f [ j ] = m a x ( f [ j ] , f [ j − v [ i ] ] ) f[j]=max(f[j], f[j - v[i]]) f[j]=max(f[j],f[j−v[i]])
那么我们要注意一下了,我们希望的是通过 f [ i − 1 ] f[i-1] f[i−1]来更新,如果这时候还用从0到m的体积循环的话,此时的 f [ j − v [ i ] ] f[j-v[i]] f[j−v[i]]对应的是 f [ i ] [ j − v [ i ] ] f[i][j-v[i]] f[i][j−v[i]]而不是 f [ i − 1 ] [ j − v [ i ] ] f[i - 1][j - v[i]] f[i−1][j−v[i]]。因为 j − v [ i ] j-v[i] j−v[i]一定比j小,又因为我们是从小到大循环的,所以 j − v [ i ] j-v[i] j−v[i]的更新要比 j j j要早,所以已经是第i个了。而如果我们是从大到小循环,我们用到了 j − v [ i ] j-v[i] j−v[i]此时就是上一层还没更新过的,而得到此时的答案,因为不存在加法操作使得答案的来源是通过 j + v [ i ] j+v[i] j+v[i]来得到的,所以通过从大到小的循环也是不会影响当前层的答案
要解决这种情况我们需要从大到小枚举,这就就能保证了全部的元素都是从上一层来的而非当前的层
#include <iostream>
using namespace std;
int n, m;
int v[1000 + 10];
int w[1000 + 10];
int f[1000 + 10];
int main(){
cin >> n >> m;
for (int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for (int i = 1; i <= n; i++){
for (int j = m; j >= v[i]; j--){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m];
return 0;
}
我们看到的求最优解的背包问题题目中,事实上有两种不太相同的问法。有的题目要求“恰好装满背包”时的最优解,有的题目则并没有要求必须把背包装满。一种区别这两种问法的实现方法是在初始化的时候有所不同。
如果是第一种问法,要求恰好装满背包,那么在初始化时除了f[0]为0其它f[1…V]均设为-∞,这样就可以保证最终得到的*f[N]*是一种恰好装满背包的最优解。
如果并没有要求必须把背包装满,而是只希望价格尽量大,初始化时应该将f[0…V]全部设为0。
为什么呢?可以这样理解:初始化的f数组事实上就是在没有任何物品可以放入背包时的合法状态。如果要求背包恰好装满,那么此时只有容量为0的背包可能被价值为0的nothing“恰好装满”,其它容量的背包均没有合法的解,属于未定义的状态,它们的值就都应该是-∞了。如果背包并非必须被装满,那么任何容量的背包都有一个合法解“什么都不装”,这个解的价值为0,所以初始时状态的值也就全部为0了。
这个小技巧完全可以推广到其它类型的背包问题,后面也就不再对进行状态转移之前的初始化进行讲解。
转载至https://blog.csdn.net/u013445530/article/details/40210587
完全背包
对于每个物品可以取无限次
对于每一种物品,我们同样用01背包的方法考虑,但此时我们要加上对个数的循环考虑
即对于每一种情况 f [ i , j ] f[i,j] f[i,j]为只考虑前i个物品,且总体积不大于j的所有选法
曲线救国的方法就是:
- 去掉k个物品i
- 求MAX,f[i-1][j-k*v[i]]
- 再加回来k个物品i
那么方程就是 f [ i , j ] = f [ i − 1 , j − v [ i ] ∗ k ] + w [ i ] ∗ k f[i,j] = f[i-1, j-v[i]*k] + w[i] * k f[i,j]=f[i−1,j−v[i]∗k]+w[i]∗k
#include <iostream>
using namespace std;
const int N = 1000 + 10;
int f[N][N];
int v[N];
int w[N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
for(int k = 0; k * v[i] <= j; k++){
f[i][j] = max(f[i][j], f[i - 1][j - v[i] * k] + w[i] * k);
}
}
}
cout << f[n][m];
return 0;
}
因为上式的时间复杂度较高,我们可以根据答案,来进行下面的化简,就可以得到时间优化的做法

#include <iostream>
using namespace std;
const int N = 1000 + 10;
int f[N][N];
int v[N];
int w[N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
f[i][j] = f[i - 1][j];
if(j >= v[i]) f[i][j] = max(f[i][j], f[i][j - v[i]] + w[i]);
/*
这里别与01背包的混了
这里max后面的f是i不是i-1
因为我们推出来的式子是根据i一定选择的是根据右边的式子加上一个w得到的
不是根据原来的基础方程得到的
*/
}
}
cout << f[n][m];
return 0;
}
这里可以和01背包的递推式对比一下
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
−
1
,
j
−
v
[
i
]
]
+
w
[
i
]
)
/
/
01
背
包
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
,
j
]
,
f
[
i
,
j
−
v
[
i
]
]
+
w
[
i
]
)
/
/
完
全
背
包
f[i,j]=max(f[i-1,j], f[i - 1, j -v[i]] + w[i]) //01背包 \\\\ f[i,j]=max(f[i-1,j], f[i, j -v[i]] + w[i]) //完全背包
f[i,j]=max(f[i−1,j],f[i−1,j−v[i]]+w[i])//01背包f[i,j]=max(f[i−1,j],f[i,j−v[i]]+w[i])//完全背包
同样的,我们也可以对这个空间进行优化
#include <iostream>
using namespace std;
const int N = 1000 + 10;
int f[N];
int v[N];
int w[N];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i];
}
for(int i = 1; i <= n; i++){
for(int j = v[i]; j <= m; j++){ //这里要从v[i]开始
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m];
return 0;
}
为什么这里第二层循环要从v[i]开始而不是向01背包一样从m倒着来呢?
因为我们这里的递推式是从f[i]来的,而不是像01背包从f[i-1]得到的(记得回看为什么f[i-1]是要倒着来的)
因为j < j - v[i],所以j - v[i]得到的答案是在j之前的,这样我们在遍历同一层的时候,就能够使用这一层的数据对其进行更新
多重背包
每件物品的个数有限
状态表示,状态计算
状态表示(f[i][j]);集合定义,集合属性
集合定义:从前i个物品里选,最大体积不超过j的选法的价值
属性:max
状态转移方程:
f
[
i
]
[
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
k
]
+
w
[
i
]
∗
k
)
f[i][j] = max(f[i - 1][j - v[i] * k] + w[i] * k)
f[i][j]=max(f[i−1][j−v[i]∗k]+w[i]∗k)
暴力写法
#include <iostream>
using namespace std;
int n, m;
int f[110][110];
int v[110];
int w[110];
int s[110];
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> v[i] >> w[i] >> s[i];
}
for(int i = 1; i <= n; i++){
for(int j = 0; j <= m; j++){
for(int k = 0; k <= s[i] && k * v[i] <= j; k++){
f[i][j] = max(f[i][j] , f[i - 1][j - v[i] * k] + w[i] * k);
}
}
}
cout << f[n][m];
return 0;
}
对于这个方程,我们可以优化一下
这里采用二进制的优化方式(不是我想的,别问我为什么用二进制
我们考虑2的幂次
对于2的幂次而言,可以想象成对第 2 i 2^i 2i个物品进行打包,对于打包的每组我们只能选一个,
2的幂次有0,1,2,4,8…很多
那么我们先枚举1次,那么我们就能枚举到了0~1的每个物品,枚举到2次就可以把0~1+2,同理到0~3+4=7个物品都可以通过前面2的幂次的拼凑可以得到;根据这个性质,后面的所有的数都可以通过前面的2的幂次来拼凑出来
如果是一个一般的数
我们可以枚举到最大的小于等于它的幂次,到这时候就需要把差值补上就可以凑出来的
例如我们前面证明了整次幂的情况,我们要对200进行操作,我们可以枚举到0~127的每一种拼法,然后对里面每一个数+73就可以拼凑出来73~200内的每个数,那么连接起来就能拼凑出全部的可能性
这样时间复杂度降到了 N ∗ l o g 2 N ∗ V N*log_2N*V N∗log2N∗V
这就转换成了01背包的问题
#include <iostream>
using namespace std;
int n, m;
int f[2010];
int v[12010];
int w[12010]; // 1000 * log2000 左右
int main(){
cin >> n >> m;
int cnt = 0;
for(int i = 1; i <= n; i++){
int a, b, s; //体积 价值 个数
cin >> a >> b >> s;
int k = 1;
while(k <= s){
cnt++;
v[cnt] = k * a;
w[cnt] = k * b;
s -= k;
k <<= 1;
}
if(s > 0){ // 把剩下的补上
cnt++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt; // 打包之后还剩下这些
for(int i = 1; i <= n; i++){
for(int j = m; j >= v[i]; j--){
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout << f[m];
return 0;
}
分组背包
每组物品有若干个,同一组物品内最多只能选一个
状态表示:从前i组物品中里选,且总体积不超过j的所有选法
属性:最大值(集合当中所有选法的最大值)
状态计算:集合划分
f
[
i
−
1
]
[
j
−
v
[
i
,
k
]
]
+
w
[
i
,
k
]
f[i - 1][j - v[i, k]] +w[i, k]
f[i−1][j−v[i,k]]+w[i,k]
#include <iostream>
using namespace std;
int f[110];
int v[110][110];
int w[110][110];
int s[110];
int n, m;
int main(){
cin >> n >> m;
for(int i = 1; i <= n; i++){
cin >> s[i];
for(int j = 1; j <= s[i]; j++){
cin >> v[i][j] >> w[i][j];
}
}
for(int i = 1; i <= n; i++){ // 第几组
for(int j = m; j >= 0; j--){ // 从大到小枚举体积,压缩原因同上
for(int k = 1; k <= s[i]; k++){ // 看这一组有多少个,注意每次只能选一个
if(j >= v[i][k]) f[j] = max(f[j], f[j - v[i][k]] + w[i][k]);
}
}
}
cout << f[m];
return 0;
}
简单总结方法:
- 答案是从上一层转移过来的可以枚举体积从大到小
- 答案是从这一层来的可以枚举体积从小到大
1万+

被折叠的 条评论
为什么被折叠?



