文章目录
参考
博客:背包问题九讲
视频:背包九讲专题
练习:AcWing题库
0-1背包问题
O
(
V
×
N
)
O(V\times N)
O(V×N)
有N件物品和一个容量为V的背包。第i件物品的费用是c[i],价值是w[i]。求解将哪些物品装入背包可使价值总和最大。
状态转移方程:
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示前
i
i
i 件物品,容量为
j
j
j 的背包,可获得最大的总价值。
d
p
[
i
]
[
j
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
c
[
i
]
]
+
w
[
i
]
)
,
if
j
>
=
c
[
i
]
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
,
if
j
<
c
[
i
]
dp[i][j] = max(dp[i-1][j], dp[i-1][j-c[i]]+w[i]), \quad \text{if}\quad j >= c[i] \\ dp[i][j] = dp[i-1][j], \quad \text{if}\quad j < c[i]
dp[i][j]=max(dp[i−1][j],dp[i−1][j−c[i]]+w[i]),ifj>=c[i]dp[i][j]=dp[i−1][j],ifj<c[i]
初始化状体:容量为 V 的背包不要求装满,第一行和第一列的状态均为0,即
d
p
[
0
]
[
j
]
=
0
,
d
p
[
i
]
[
0
]
=
0
dp[0][j]=0,dp[i][0]=0
dp[0][j]=0,dp[i][0]=0;容量为V的背包要求装满,
d
p
[
0
]
[
0
]
=
0
dp[0][0] = 0
dp[0][0]=0,其余状态设置为负无穷,这样确保每一个有效的状态值(大于等于0)都是从
d
p
[
0
]
[
0
]
dp[0][0]
dp[0][0] 转移过来的,即装入的物品体积为背包的大小。
模板题:2. 01背包问题
#include<iostream>
#include<vector>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N+1);
vector<int> w(N+1);
for(int i = 1; i <= N; i++){
cin >> v[i] >> w[i];
}
vector<int> dp(V+1,0);
for(int i = 1; i <= N; i++){
for(int j = V; j >= v[i]; j--){
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout << dp[V] << endl;
return 0;
}
0-1背包不超过背包体积下的最大重量
1049. 最后一块石头的重量 II
问题转化:
- 将石头分成两堆,使两堆石头的重量差最小。
- 进一步转化问题为前 i 块石头均可以选择放入或者不放入背包,背包的重量不超过所有石头重量总和的一半,求可以获得的最大重量。
- 0-1背包问题,本题目背包的体积和物品的价值均是值石头的重量。
二维状态: d p [ i ] [ j ] dp[i][j] dp[i][j]表示已经对前 i 块石头做出选择,背包可以容纳的重量为 j 时,背包的最大重量。
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int stone: stones){
sum += stone;
}
int v = sum/2; // 必须向下取整
int n = stones.size();
vector<vector<int>> dp(n+1,vector<int>(v+1,0)); // 初始化全部为0
for(int i = 1; i <= n; i++){
for(int j = 1; j <= v; j++){
// 注意stones的下标是从0开始的,所以使用i-1!
if(j>=stones[i-1]){
dp[i][j] = max(dp[i-1][j],dp[i-1][j-stones[i-1]]+stones[i-1]);
}else{
dp[i][j] = dp[i-1][j];
}
}
}
return sum-2*dp[n][v];
}
思考两个问题:
- i i i 和 j j j 的循环能不能交换? -> 可以
- 为什么 d p [ n ] [ v ] dp[n][v] dp[n][v]就是不超过重量 v v v的最大重量?-> 与状态初始化有关,因为所有状态都被初始化成了0
状态降成一维:
int lastStoneWeightII(vector<int>& stones) {
int sum = 0;
for(int stone: stones){
sum += stone;
}
int v = sum/2; // 必须向下取整
int n = stones.size();
vector<int> dp(v+1,0); // 初始化全部为0
for(int i = 1; i <= n; i++){
for(int j = v; j >= stones[i-1] ; j--){
// 注意stones的下标是从0开始的,所以使用i-1!
dp[j] = max(dp[j],dp[j-stones[i-1]]+stones[i-1]);
}else{
dp[j] = dp[j];
}
}
return sum-2*dp[v];
}
dp[j] = max(dp[j],dp[j-stones[i-1]]+stones[i-1]);
中左边的dp[j]表示对第 i 件物品做出选择后的状态,右边的dp[j]表示对第 i-1 件物品做出选择后的状态。如果 j 是由1开始递增遍历,则右边的dp[j]会被 i 的状态下的dp[j]覆盖;可以让 j 从大到小开始遍历,由于 j - stones[i-1]
必然小于 j ,因此不会出现状态的覆盖(覆盖的dp[j]是不需要被用到的)。
思考:
- 状态降成一维后 i i i 和 j j j 的循环能不能交换?-> 不可以,因为降成一维后,只能记录 i 的 上一个状态,因此 i 必须放在外层循环。
0-1背包恰好装满问题
416. 分割等和子集
选择子集和等于所有元素值和的一半。
d
p
[
i
]
[
j
]
dp[i][j]
dp[i][j]表示已经对前 i 个元素做出选择,选择的元素的和是否可以为 j,true表示可以,false表示不可以。(下标从1开始)
d
p
[
i
]
[
j
]
=
d
p
[
i
−
1
]
[
j
]
∣
d
p
[
i
−
1
]
[
j
−
n
u
m
s
[
i
]
]
dp[i][j] = dp[i-1][j] \quad | \quad dp[i-1][j-nums[i]]
dp[i][j]=dp[i−1][j]∣dp[i−1][j−nums[i]]
二维状态:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum%2==1){
return false;
}
sum/=2;
int n = nums.size();
vector<vector<bool>> dp(n+1,vector<bool>(sum+1,false));
dp[0][0] = true;
// 0-1背包,O(sum*n),sum为数组的元素和的一般,n为数组元素的个数2. 01背包问题2. 01背包问题
for(int j = 1; j <= n; j++){
// nums下标从0开始,所以用j-1
for(int i = 0; i <= sum; i++){
if(i >= nums[j-1]){
dp[j][i] = dp[j-1][i] | dp[j-1][i-nums[j-1]];
}else{
dp[j][i] = dp[j-1][i];
}
}
}
return dp[n][sum];
}
此处要求背包刚好装满,所以初始设置状态的时候只设置
d
p
[
0
]
[
0
]
dp[0][0]
dp[0][0] 为true,其余为false。
状态降成一维:
bool canPartition(vector<int>& nums) {
int sum = 0;
for(int num : nums){
sum += num;
}
if(sum%2==1){
return false;
}
sum/=2;
vector<bool> dp(sum+1,false);
dp[0] = true;
// 0-1背包,O(sum*n),sum为数组的元素和的一般,n为数组元素的个数
for(int j = 0; j < nums.size(); j++){
for(int i = sum; i >= nums[j]; i--){
dp[i] = dp[i] | dp[i-nums[j]];
}
}
return dp[sum];
}
完全背包问题
O
(
V
×
N
)
O(V\times N)
O(V×N)
有 N 种物品和一个容量是 V 的背包,每种物品都有无限件可用。第 i 种物品的体积是 v[i],价值是 w[i]。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。输出最大价值。
模板题:3. 完全背包问题
#include<iostream>
#include<vector>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N+1);
vector<int> w(N+1);
for(int i = 1; i <= N; i++){
cin >> v[i] >> w[i];
}
vector<int> dp(V+1,0);
for(int i = 1; i <= N; i++){ // 前i件物品
for(int j = v[i]; j <= V; j++){ // 背包容量j
dp[j] = max(dp[j],dp[j-v[i]]+w[i]);
}
}
cout << dp[V] << endl;
return 0;
}
与0-1背包问题最大的区别在于内层循环 j 从小到大遍历。j 从小到大遍历,等号右边的dp[j]为已经更新的状态,即表示不断地在放入第i件物品,直到背包放不下。0-1背包中 j 从大到小遍历,等号右边的 dp[j] 为 i-1 时的状态。
多重背包
O
(
V
×
N
×
s
[
i
]
)
O(V\times N \times s[i])
O(V×N×s[i])
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。输出最大价值。
模板题:4. 多重背包问题 I
#include<iostream>
#include<vector>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N+1);
vector<int> w(N+1);
vector<int> s(N+1);
for(int i = 1; i <= N; i++){
cin >> v[i] >> w[i] >> s[i];
}
vector<int> dp(V+1,0);
for(int i = 1; i <= N; i++){
for(int j = V; j >= v[i]; j--){
for(int k = 1; k <= s[i]; k++){
if(j>=k*v[i]){
dp[j] = max(dp[j],dp[j-k*v[i]]+k*w[i]);
}
}
}
}
cout << dp[V] << endl;
return 0;
}
在0-1背包的基础上多了一层循环,用于第 i 件物品选择不同次数计算背包中的价值。
二进制优化方法
多重背包问题可以转成0-1背包问题。
最简单的做法是把第i种物品的s[i]件转化体积均为v[i]的s[i]种物品,保证了可以取到所有的情况。但是这样并没有减少时间复杂度。
可以用二进制来表示,如7件物品,用3位二进制就可以表示取0,1,2,3,4,5,6,7件的所有情况。
模板题:5. 多重背包问题 II
#include<iostream>
#include<vector>
using namespace std;
struct Good{
int v, w;
Good(int _v, int _w): v(_v), w(_w){}
};
int main(){
int N,V;
int v,w,s;
cin >> N >> V;
vector<Good> goods;
for(int i = 0; i < N; i++){
cin >> v >> w >> s;
// i的把s件物品转化成体积为v[i],2*v[i],4*v[i],8*v[i],...的log(s)件物品
for(int k = 1; k <= s; k*=2){
s -= k;
goods.push_back(Good(k*v,k*w));
}
// 把剩余的s件物品合成一种物品
if(s>0){
goods.push_back(Good(s*v,s*w));
}
}
vector<int> dp(V+1,0);
for(Good good: goods){
for(int i = V; i >= good.v; i--){
dp[i] = max(dp[i],dp[i-good.v]+good.w);
}
}
cout << dp[V] << endl;
}
单调队列优化
混合背包
模板题:7. 混合背包问题
思路:将背包问题分成两类,0-1背包和完全背包。把多重背包转化为0-1背包。
#include<bits/stdc++.h>
using namespace std;
struct Good{
int v,w;
bool flag;
Good(int _v, int _w, bool _flag):v(_v),w(_w),flag(_flag){}
};
int main(){
int N,V;
cin >> N >> V;
vector<Good> goods;
for(int i = 0; i < N; i++){
int v,w,s;
cin >> v >> w >> s;
if(s == -1){
goods.push_back(Good(v,w,true));
}else if(s == 0){
// false 表示是完全背包
goods.push_back(Good(v,w,false));
}else{
for(int k = 1; s >= k; k*=2){
s -= k;
goods.push_back(Good(k*v,k*w,true));
}
if(s > 0){
goods.push_back(Good(s*v,s*w,true));
}
}
}
vector<int> dp(V+1,0);
for(int i = 0; i < goods.size(); i++){
if(goods[i].flag){
for(int j = V; j >= goods[i].v; j--){
dp[j] = max(dp[j],dp[j-goods[i].v]+goods[i].w);
}
}else{
for(int j = goods[i].v; j <= V; j++){
dp[j] = max(dp[j],dp[j-goods[i].v]+goods[i].w);
}
}
}
cout << dp[V] << endl;
return 0;
}
二维费用的背包问题
有 N 件物品和一个容量是 V 的背包,背包能承受的最大重量是 M。每件物品只能用一次。体积是 vi,重量是 mi,价值是 wi。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
模板题:8. 二维费用的背包问题
#include <bits/stdc++.h>
using namespace std;
struct Good{
int v,m,w;
Good(int _v, int _m, int _w):v(_v),m(_m),w(_w){}
};
int main(){
int N,V,M;
cin >> N >> V >> M;
vector<Good> goods;
for(int i = 0 ; i < N ; i++){
int v,m,w;
cin >> v >> m >> w;
goods.push_back(Good(v,m,w));
}
vector<vector<int>> dp(V+1,vector<int>(M+1));
for(int i = 0; i < N; i++){
for(int j = V; j >= goods[i].v; j--){
for(int k = M; k >= goods[i].m; k--){
dp[j][k] = max(dp[j][k],dp[j-goods[i].v][k-goods[i].m]+goods[i].w);
}
}
}
cout << dp[V][M] << endl;
}
分组背包问题
有 N 组物品和一个容量是 V 的背包。每组物品有若干个,同一组内的物品最多只能选一个。每件物品的体积是 vij,价值是 wij,其中 i 是组号,j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
模板题:9. 分组背包问题
#include <bits/stdc++.h>
using namespace std;
int main(){
int N,V;
cin >> N >> V;
vector<vector<pair<int,int>>> goods;
for(int i = 0; i < N; i++){
int x;
cin >> x;
vector<pair<int,int>> good;
for(int j = 0; j < x; j++){
int v,w;
cin >> v >> w;
good.push_back(pair<int,int>(v,w));
}
goods.push_back(good);
}
vector<int> dp(V+1,0);
for(int i = 0; i < N; i++){
for(int j = V; j >= 0; j--){
for(auto t : goods[i]){
if(j >= t.first){
dp[j] = max(dp[j],dp[j-t.first]+t.second);
}
}
}
}
cout << dp[V] << endl;
}
思考:多重背包问题是分组背包问题的一种特殊情况。
分组背包问题是物品分成 N 组,每次只能选每组中的 1 种物品,假设某个组内有 s 种物品,每种物品就一件,则一共有 s+1 种情况,即可以都不选,也可以选 i 号物品。
多重背包问题是每组就 1 种物品,但是每种物品有 s 件,则一共也是 s+1 种情况。
因此都是需要再加一层循环,多重背包是选择物品件数,分组背包是选择哪一种物品。(但是多重背包可以进行优化,分组背包更一般,只能用三层循环)
背包问题求方案数
题目:有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最优选法的方案数。注意答案可能很大,请输出答案模 109+7 的结果。
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N);
vector<int> w(N);
for(int i = 0; i < N; i++){
cin >> v[i] >> w[i];
}
// dp[j]为体积为j背包可以获得的最大价值(不一定装满)
vector<int> dp(V+1,0);
// cnt[j]为体积为j的背包恰好装满的方案数
vector<int> cnt(V+1,0);
cnt[0] = 1;
for(int i = 0; i < N; i++){
for(int j = V; j >= v[i]; j--){
if(dp[j] < dp[j-v[i]]+w[i]){
dp[j] = dp[j-v[i]]+w[i];
cnt[j] = cnt[j-v[i]];
}else if(dp[j] == dp[j-v[i]]+w[i]){
cnt[j] = (cnt[j] + cnt[j-v[i]])%mod;
}
}
}
// 体积为V的背包可以获得的最大价值(不一定装满),即最优选法
int maxw = dp[V];
int re = 0;
for(int i = 1; i <= V; i++){
// 装满体积为i的背包可获得的价值为maxw的方案数累加
if(dp[i]==maxw){
re += cnt[i];
}
}
cout << re << endl;
}
改进:将cnt的初始状态全部设置为1,表示状态可以从任意起点开始转移,因此最终cnt[j]求得的是最优选法的方案总数。
#include <bits/stdc++.h>
using namespace std;
const int mod = 1e9+7;
int main(){
int N,V;
cin >> N >> V;
vector<int> v(N);
vector<int> w(N);
for(int i = 0; i < N; i++){
cin >> v[i] >> w[i];
}
vector<int> dp(V+1,0);
// 初始状态全部设为1
vector<int> cnt(V+1,1);
for(int i = 0; i < N; i++){
for(int j = V; j >= v[i]; j--){
if(dp[j] < dp[j-v[i]]+w[i]){
dp[j] = dp[j-v[i]]+w[i];
cnt[j] = cnt[j-v[i]];
}else if(dp[j] == dp[j-v[i]]+w[i]){
cnt[j] = (cnt[j] + cnt[j-v[i]])%mod;
}
}
}
cout << cnt[V] << endl;
}
背包问题求具体方案
有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出字典序物品编号最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
思路:https://www.acwing.com/solution/content/2687/
题目要求输出字典序最小的解,假设存在一个包含第 1 个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就转化成从 2~N 这些物品中找到最优解。之前的
f
(
i
,
j
)
f(i,j)
f(i,j)记录的都是前 i 个物品总容量为 j 的最优解,那么我们现在将
f
(
i
,
j
)
f(i,j)
f(i,j)定义为从第 i 个元素到最后一个元素总容量为 j 的最优解。接下来考虑状态转移:
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])
两种情况,第一种是不选第 i 个物品,那么最优解等同于从第 i+1 个物品到最后一个元素总容量为 j 的最优解;第二种是选了第 i 个物品,那么最优解等于当前物品的价值 w[i] 加上从第 i+1 个物品到最后一个元素总容量为 j−v[i] 的最优解。
计算完状态表示后,考虑如何的到最小字典序的解。首先 f ( 1 , V ) f(1,V) f(1,V)肯定是最大价值,那么我们便开始考虑能否选取第 1 个物品。
如果 f ( 1 , m ) = f ( 2 , m − v [ 1 ] ) + w [ 1 ] f(1,m)=f(2,m−v[1])+w[1] f(1,m)=f(2,m−v[1])+w[1],说明选取了第1个物品可以得到最优解。
如果 f ( 1 , m ) = f ( 2 , m ) f(1,m)=f(2,m) f(1,m)=f(2,m),说明不选取第一个物品才能得到最优解。
如果 f ( 1 , m ) = f ( 2 , m ) = f ( 2 , m − v [ 1 ] ) + w [ 1 ] f(1,m)=f(2,m)=f(2,m−v[1])+w[1] f(1,m)=f(2,m)=f(2,m−v[1])+w[1],说明选不选都可以得到最优解,但是为了考虑字典序最小,我们也需要选取该物品。
#include <bits/stdc++.h>
using namespace std;
struct Good{
int v,w;
Good(int _v, int _w):v(_v),w(_w){}
};
int main(){
int N,V;
cin >> N >> V;
vector<Good> goods;
for(int i = 0; i < N; i++){
int v,w;
cin >> v >> w;
goods.push_back(Good(v,w));
}
vector<vector<int>> dp(N+10,vector<int>(V+10,0));
for(int i = N; i >= 1; i--){
for(int j = 0; j <= V; j++){
if(j >= goods[i-1].v && dp[i+1][j-goods[i-1].v]+goods[i-1].w > dp[i+1][j]){
dp[i][j] = dp[i+1][j-goods[i-1].v]+goods[i-1].w;
}else{
dp[i][j] = dp[i+1][j];
}
}
}
// cout << dp[1][V] << endl;
int vol = V;
for(int i = 1; i <= N; i++){
if(vol >= goods[i-1].v && dp[i+1][vol-goods[i-1].v]+goods[i-1].w == dp[i][vol]){
cout << i << " ";
vol -= goods[i-1].v;
}
}
}
背包问题内外循环和初始状态
0-1背包问题思路就是每件物品选或者不选(多重背包也可以转化为0-1背包),在二维dp降成一维dp后,dp[j] 只记了对前 i 件物品做出选择的最大价值,记为状态 i 。
计算不同背包体积 j 下,对第 i+1 件物品进行选择后可以获得的最大价值。此时需要用到状态 i ,由于一维 dp[j] 只能记录一个状态的 i,因此第一层循环必须是物品种类 i 的循环。
518. 零钱兑换 II
题目:给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
分析:这是一个完全背包恰好装满求方案数问题。
恰好装满 -> 初始状态应该设置成 dp[0]=1 ,其余为0。因此凑到总金额amount是从金额为0开始。
如果dp[1]=1,则dp[amount]可能由dp[1]转移而来,此时只凑了amount-1的金额。
完全背包 -> 第二层循环从1开始。
求方案数 -> 初始状态设置为1和0。(求最大价值初始状态设置为0和-inf)
int change(int amount, vector<int>& coins) {
vector<int> dp(amount+1,0);
dp[0] = 1;
// 把coins放在外层循环,限制了coins使用顺序
for(int k = 0; k < coins.size(); k++){
for(int i = 1; i <= amount; i++){
if(i-coins[k] >= 0){
dp[i] += dp[i-coins[k]];
}
}
}
return dp[amount];
}
// 硬币i的体积为coins[i], 价值为1
// 题目转化为完全背包问题恰好装满体积amount,价值最小
int coinChange(vector<int>& coins, int amount) {
int inf = 0x3f3f3f3f;
vector<int> dp(amount+1,inf);
dp[0] = 0;
for(int k = 0; k < coins.size() ; k++){
for(int i = coins[k]; i <= amount; i++){
dp[i] = min(dp[i-coins[k]]+1,dp[i]);
}
}
return dp[amount] == inf ? -1: dp[amount];
}