背包九讲
一、01背包问题
题目概览
有
N
N
N件物品和一个容量是
V
V
V的背包。每件物品只能使用一次。
第
i
i
i件物品的体积是
v
i
v_i
vi,价值是
w
i
w_i
wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数
N
,
V
N,V
N,V用空格隔开,分别表示物品数量和背包容积。
接下来有
N
N
N行,每行两个整数
v
i
,
w
i
v_i,w_i
vi,wi用空格隔开,分别表示第
i
i
i件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<v_i,w_i≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
(一)先使用DP去做
思路:
f
[
i
]
[
j
]
f[i][j]
f[i][j]表示只看前i
个物品,总体积是j
的情况下,总价值最大是多少
r e s u l t = m a x ( f [ n ] [ 0 − v ] ) result = max(f[n][0-v]) result=max(f[n][0−v])
状态转移公式:
- 不选第
i
个物品, f [ i ] [ j ] = f [ i − 1 ] [ j ] f[i][j] = f[i - 1][j] f[i][j]=f[i−1][j]; - 选第
i
个物品, f [ i ] [ j ] = f [ i − 1 ] [ j − v [ i ] ] f[i][j] = f[i - 1][j - v[i]] f[i][j]=f[i−1][j−v[i]]
f [ i ] [ j ] = m a x ( f [ i − 1 ] [ j ] , f [ i − 1 ] [ j − v [ i ] ] ) f[i][j] = max(f[i - 1][j],f[i - 1][j - v[i]]) f[i][j]=max(f[i−1][j],f[i−1][j−v[i]])
初始化工作: f [ 0 ] [ 0 ] = 0 f[0][0] = 0 f[0][0]=0
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int f[N][N];
int v[N],w[N];
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 - 1][j - v[i]] + w[i]);
}
int res = 0;
for(int i = 0;i <= m;i++) res = max(res,f[n][i]);
cout << res << endl;
return 0;
}
(二)进行优化
思路:
目标一:省掉一维空间
一共就二维,所以,我们不妨每一维都试一试。
-
省去j那一维
显而易见,所有的操作都是基于这一维的,如果没有这一维,无法进行转移。
换一种说法,省去j这一维, f [ i ] f[i] f[i]数组的含义变成了前i
个物品,总价值是多少,好像不太行吧。。。 -
省去i那一维
仔细观察,不难发现,所有的状态均是由
i - 1
那一维转移过来的,所以,可以尝试使用滚动数组
即 f [ j ] f[j] f[j]中原先存的是 f [ i − 1 ] [ j ] f[i - 1][j] f[i−1][j]的数据,更新后变为 f [ i ] [ j ] f[i][j] f[i][j]的数据
我们先尝试,强行去掉一维
主体代码:for(int i = 1;i <= n;i++) for(int j = 0;j <= m;j++){ if(j >= v[i]) f[j] = max(f[j],f[j - v[i]] + w[i]); }
那么,就有问题了,由于
j
是从小到大循环, 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 [ j ] f[j] f[j]先更新,再让 f [ j − v [ i ] ] f[j - v[i]] f[j−v[i]]先更新即可。实现也非常简单,只需要把
j
倒着循环即可。目标二:少一个循环
找答案的循环不需要啦!
原因:
初始化的时候,把所有的 f [ i ] f[i] f[i]都初始化成了0f [ m ] f[m] f[m]就是体积为 m m m的方案。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int f[N];
int v[N],w[N];
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 >= 1;j--){
if(j >= v[i])
f[j] = max(f[j],f[j - v[i]] + w[i]);
}
cout << f[m] << endl;
return 0;
}
二、完全背包问题
题目概览:
有
N
N
N种物品和一个容量是
V
V
V的背包,每种物品都有无限件可用。
第
i
i
i种物品的体积是
v
i
v_i
vi,价值是
w
i
w_i
wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数, N N N, V V V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N N N行,每行两个整数 v i v_i vi, w i w_i wi,用空格隔开,分别表示第 i i i种物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<v_i,w_i≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
10
思路与解
对比 01 01 01背包,区别在于,每个物品有无限件可以选。
思路:
f
[
i
]
f[i]
f[i]表示总体积是i的情况下,最大价值是多少
r
e
s
u
l
t
=
m
a
x
(
f
[
0
…
m
]
)
result = max(f[0…m])
result=max(f[0…m])
刚开始的想法肯定很朴实,会这样写:
for(int i = 0;i < n;i++)
for(int j = m;j >= v[i];j--)//到这里都和01背包一样
for(int k = 0;k * v[i] <= j;k++)//依次尝试能放多少
f[j] = max(f[j],f[j - k * [i]] * w[i]);
但是时间复杂度太大了。。
优化:
for(int i = 0;i < n;i++)
for(int j = v[i];j <= m;j++)
f[j] = max(f[j],f[j - v[i]] + w[i]);
因为是从小到大枚举,所以
f
[
j
−
v
[
i
]
]
f[j - v[i]]
f[j−v[i]]已经算过了
考虑前
i
i
i个物品,包括第
i
i
i个物品,可能里面已经有一些第
i
i
i个物品了
证明:
数学归纳法
-
假设考虑前 i − 1 i-1 i−1个物品之后,所有的 f [ j ] f[j] f[j]都是正确的
-
来证明:考虑完第 i i i个物品后,所有的 f [ j ] f[j] f[j]也都是正确的
对于某个 j j j而言,如果最优解中包含 k k k个 v [ i ] v[i] v[i];
从小到大枚举的时候,一定会枚举到 f [ j − f ∗ v [ i ] ] f[j - f * v[i]] f[j−f∗v[i]],那么这个状态就会用 f [ j − k ∗ v [ i ] − v [ i ] ] + w [ i ] f[j - k * v[i] - v[i]] + w[i] f[j−k∗v[i]−v[i]]+w[i]来更新它。
枚举到 f [ j − ( k − 1 ) ∗ v [ i ] − v [ i ] ] + w [ i ] f[j - (k - 1) * v[i] - v[i]] + w[i] f[j−(k−1)∗v[i]−v[i]]+w[i]包含1个 v [ i ] v[i] v[i]
…
所以 f [ j ] f[j] f[j]就一定枚举到有 k k k个 v [ i ] v[i] v[i]的情况
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int f[N];
int main(){
cin >> n >> m;
for(int i = 0;i < n;i++){
int v,w;
cin >> v >> w;
for(int j = v;j <= m;j++)
f[j] = max(f[j],f[j - v] + w);
}
cout << f[m];//与上一题的原因一样,不需要比较
return 0;
}
三、多重背包问题
题目概览:
有
N
N
N种物品和一个容量是
V
V
V的背包。
第
i
i
i种物品最多有
s
i
s_i
si件,每件体积是
v
i
vi
vi,价值是
w
i
wi
wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数,
N
N
N,
V
V
V,用空格隔开,分别表示物品种数和背包容积.
接下来有
N
N
N行,每行三个整数
v
i
v_i
vi,
w
i
w_i
wi,
s
i
s_i
si,分别表示第 i� 种物品的体积、价值和数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0
<
N
,
V
≤
100
0<N,V≤100
0<N,V≤100
0
<
v
i
,
w
i
,
s
i
≤
100
0<v_i,w_i,s_i≤100
0<vi,wi,si≤100(注意,下面会改)
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
(一)最暴力的方法:
思路:
f [ i ] f[i] f[i]总体积是 i i i的情况下,最大价值是多少
for(int i = 0;i < n;i++)
for(int j = m;j >= v[i];j--)
f[j] = max(f[j],f[j - v[i]] + w[i],f[j - 2 * v[i]],...)
将f的所有值初始化为 0 0 0,结果为 f [ m ] f[m] f[m]
代码与解
#include<bits/stdc++.h>
using namespace std;
const int N = 105;
int n,m;
int f[N];
int main(){
cin >> n >> m;
for(int i = 0;i < n;i++){
int v,w,s;
cin >> v >> w >> s;
for(int j = m;j >= 0;j--)
for(int k = 1;k <= s && k * v <= j;k++)
f[j] = max(f[j],f[j - k * v] + k * w);
}
cout << f[m] << endl;
return 0;
}
(二)二进制优化
题目不变
数据范围
0
<
N
≤
1000
0<N≤1000
0<N≤1000
0
<
V
≤
2000
0 < V \le 2000
0<V≤2000
0
<
v
i
,
w
i
,
s
i
≤
2000
0<v_i,w_i,s_i\le2000
0<vi,wi,si≤2000
思路:
- 把一个多重背包问题变成一个01背包问题
我们可以把物品重复 s s s份,放到物品堆里去,每个物品就独立了,成功转化为一个01背包
例如,我们要把7拆成不同的方案
笨方法:1 1 1 1 1 1
每个1都有选和不选聪明一下,我们只需要1,2,4,就可以表示出来1~7的所有数了
-
那么,回归到题目,只需要 l o g ( s ) log(s) log(s)上取整个数即可
时间复杂度来到了可观的 1000 × 11 × 2000 = 2 × 1 0 7 1000\times11\times2000 = 2 \times 10 ^ 7 1000×11×2000=2×107
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N = 2010;
int n,m;
int f[N];
struct Good{
int v,w;
};
int main(){
vector<Good> goods;
cin >> n >> m;
for(int i = 0;i < n;i++){
int v,w,s;
cin >> v >> w >> s;
for(int k = 1;k <= s;k *= 2){
s -= k;
goods.push_back({v * k,w * k});
}
if(s > 0) goods.push_back({v * s,w * s});
}
for(auto good:goods)
for(int j = m;j >= good.v;j--)
f[j] = max(f[j],f[j - good.v] + good.w);
cout << f[m] << endl;
return 0;
}
(三)单调队列优化
题目不变。
数据范围
0
<
N
≤
1000
0<N≤1000
0<N≤1000
0
<
V
≤
2000
0 < V \le 2000
0<V≤2000
0
<
v
i
,
w
i
,
s
i
≤
20000
0<v_i,w_i,s_i\le20000
0<vi,wi,si≤20000
思路:
先回看一下我们最原始的代码:
for(int i = 0;i < n;i++){
int v,w,s;
cin >> v >> w >> s;
for(int j = m;j >= 0;j--)
for(int k = 1;k <= s && k * v <= j;k++)
f[j] = max(f[j],f[j - k * v] + k * w);
}
最里面一重循环是我们的决策,01
背包是只有0
和1
两种决策的,多重背包的话,是从0
到s
,s + 1
种决策。
观察决策有没有什么性质,能不能用一些数据结构把它优化掉。
答案是可行的。
我们在去循环这个体积的时候,我们把所有的体积归一个类,根据我们的体积 j j j,模上v,把 m o d v = 0 \mod v=0 modv=0的归为一类, m o d v = 1 \mod v = 1 modv=1的归为一类,依次类推,分成若干类,每类之间一定是没有交集的,然后他所有类加在一块就是我们的全集。也就是我们把整个从0到 m m m的集合分成了 v v v类,从 m o d v = 0 \mod v = 0 modv=0一直到 m o d v = ( v − 1 ) \mod v = (v - 1) modv=(v−1),类与类之间相互独立,毫不影响。
所以,在转移的时候,只需要到余数相同的集合中进行转移,不需要到其他类。
在实现的时候,我们要分别考虑每一类,枚举每一个余数,毕竟每个余数相互独立。在枚举的时候,还有一点比较绕。假设我正在枚举体积为j
的情况,朴素的状态转移方程即为:
f
[
j
]
=
m
a
x
(
f
[
j
−
v
]
+
w
,
f
[
j
−
2
∗
v
]
+
w
∗
2
,
.
.
.
,
f
[
j
−
k
∗
v
]
+
k
∗
w
)
f[j] = max(f[j - v] + w,f[j - 2 * v] + w * 2,...,f[j - k * v] + k * w)
f[j]=max(f[j−v]+w,f[j−2∗v]+w∗2,...,f[j−k∗v]+k∗w)。
我们每次都要找前以上一串中的最大值,共k个数。进一步,当我们再算 f [ j + v ] f[j + v] f[j+v]时,我们只是把这个框向右移了一位,框里还是有 k k k个数。
这时,我们就可以顺理成章的想到,假设
j
m
o
d
v
=
x
j \mod v = x
jmodv=x,根据数学知识,我们就可以推断出
(
j
−
k
∗
v
)
m
o
d
v
[
k
为常数
]
(j - k * v) \mod v[k为常数]
(j−k∗v)modv[k为常数]的结果也一定是
x
x
x,所以
f
[
j
]
f[j]
f[j]只会从对
v
v
v取余相同的状态转移过来。
现在,这个问题已经被转移成了一个求窗口内最大值的问题。
好啦,可以看下一下几个题:
239. 滑动窗口最大值 - 力扣(LeetCode)
P1886 滑动窗口 - 洛谷
154. 滑动窗口 - AcWing题库
但是,还是有区别的,滑动窗口的原题是不会变的,但这里它是会变的,毕竟这里还有w的问题。
所以这里,给他们减去一个等差数列,每多一个
v
v
v,说明剩余要装入的物品数量就少了一个,总价值也少了一个
w
w
w。
f
[
0
]
不变
f
[
v
]
→
f
[
v
]
−
1
∗
w
f
[
2
∗
v
]
→
f
[
2
∗
v
]
−
2
∗
w
f[0]不变\\ f[v] → f[v] - 1 * w\\ f[2 * v] → f[2 * v] - 2 * w
f[0]不变f[v]→f[v]−1∗wf[2∗v]→f[2∗v]−2∗w
实现过程:第一个循环枚举余数,第二个循环枚举这个余数里的所有数,这个过程可以用单调队列来优化,那么这里就是一个经典的单调队列问题。每次把队首取出来,队列的首部一定是最大的数,然后用最大数去更新一下我当前的数。每次把当前数往队列里插的时候,需要剔除队列中一定不会被用到的元素。然后我们把当前数放到队列离去,最后,
f
[
m
]
f[m]
f[m]就是答案。
代码实现
#include<bits/stdc++.h>
using namespace std;
const int N = 20010;
int n,m;
int f[N],g[N],q[N];
int main(){
cin >> n >> m;
for(int i = 0;i < n;i++){
int v,w,s;
cin >> v >> w >> s;
memcpy(g,f,sizeof f);
for(int j = 0;j < v;j++){
int hh = 0,tt = -1;
for(int k = j;k <= m;k += v){
f[k] = g[k];
if(hh <= tt && k - s * v > q[hh]) hh++;
if(hh <= tt) f[k] = max(f[k],g[q[hh]] + (k - q[hh]) / v * w);
while(hh <= tt && g[q[tt]] - (q[tt] - j) / v * w <= g[k] - (k - j) / v * w) tt--;
q[++tt] = k;
}
}
}
cout << f[m] << endl;
return 0;
}
四、混合背包问题
题目概览
有 N N N种物品和一个容量是 V V V 的背包。
物品一共有三类:
- 第一类物品只能用1次(01背包);
- 第二类物品可以用无限次(完全背包);
- 第三类物品最多只能用 s i s_i si次(多重背包);
每种体积是 v i v_i vi,价值是 w i w_i wi。
求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且价值总和最大。
输出最大价值。
输入格式
第一行两个整数, N N N, V V V,用空格隔开,分别表示物品种数和背包容积。
接下来有 N N N行,每行三个整数 v i v_i vi, w i w_i wi, s i s_i si,用空格隔开,分别表示第 i i i种物品的体积、价值和数量。
- s i = − 1 s_i=−1 si=−1 表示第 i i i种物品只能用1次;
- s i = 0 s_i=0 si=0 表示第 i i i 种物品可以用无限次;
- s i > 0 s_i>0 si>0 表示第 i i i种物品可以使用 s i s_i si 次;
输出格式
输出一个整数,表示最大价值。
数据范围
0
<
N
,
V
≤
1000
0<N,V≤1000
0<N,V≤1000
0
<
v
i
,
w
i
≤
1000
0<v_i,w_i≤1000
0<vi,wi≤1000
−
1
≤
s
i
≤
1000
−1≤s_i≤1000
−1≤si≤1000
输入样例
4 5
1 2 -1
2 4 1
3 4 0
4 5 2
输出样例:
8
思路:
只需要判断一下是什么类的,然后按照每一类的进行转移就可以了。把所有的都拆成01背包的形式。
代码实现:
#include<bits/stdc++.h>
using namespace std;
const int N = 1010;
int n,m;
int f[N];
struct Thing{
int kind;
int v,w;
};
vector<Thing> things;
int main(){
cin >> n >> m;
for(int i = 0;i < n;i++){
int v,w,s;
cin >> v >> w >> s;
if(s < 0) things.push_back({-1,v,w});
else if(s == 0) things.push_back({0,v,w});
else{
for(int k = 1;k <= s;k *= 2){
s -= k;
things.push_back({-1,v * k,w * k});
}
if(s > 0) things.push_back({-1,v * s,w * s});
}
}
for(auto thing:things){
if(thing.kind < 0){
for(int j = m;j >= thing.v;j--) f[j] = max(f[j],f[j - thing.v] + thing.w);
}
else
for(int j = thing.v;j <= m;j++) f[j] = max(f[j],f[j - thing.v] + thing.w);
}
cout << f[m] << endl;
return 0;
}
五、二维费用的背包问题
题目概述
有 N N N件物品和一个容量是 V V V的背包,背包能承受的最大重量是 M M M。
每件物品只能用一次。体积是 v i v_i vi,重量是 m i m_i mi,价值$ w_i$。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,总重量不超过背包可承受的最大重量,且价值总和最大。
输出最大价值。
输入格式
第一行三个整数, N N N, V V V, M M M,用空格隔开,分别表示物品件数、背包容积和背包可承受的最大重量。
接下来有 N N N 行,每行三个整数 v i v_i vi, m i m_i mi, w i w_i wi,用空格隔开,分别表示第 i i i 件物品的体积、重量和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0
<
N
≤
1000
0<N≤1000
0<N≤1000
0
<
V
,
M
≤
100
0<V,M≤100
0<V,M≤100
0
<
v
i
,
m
i
≤
100
0<v_i,mi≤100
0<vi,mi≤100
0
<
w
i
≤
1000
0<wi≤1000
0<wi≤1000
输入样例
4 5 6
1 2 3
2 4 4
3 4 5
4 5 6
输出样例:
8
六、分组背包问题
题目概述
有 N N N组物品和一个容量是 V V V 的背包。
每组物品有若干个,同一组内的物品最多只能选一个。
每件物品的体积是
v
i
,
j
v_{i,j}
vi,j,价值是
w
i
,
j
w_{i,j}
wi,j,其中$ i$ 是组号,
j
j
j 是组内编号。
求解将哪些物品装入背包,可使物品总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行有两个整数 N N N, V V V,用空格隔开,分别表示物品组数和背包容量。
接下来有 N N N 组数据:
- 每组数据第一行有一个整数 S i S_i Si,表示第$ i$ 个物品组的物品数量;
- 每组数据接下来有$ S_i 行,每行有两个整数 行,每行有两个整数 行,每行有两个整数v_{i,j},w_{i,j} ,用空格隔开,分别表示第 ,用空格隔开,分别表示第 ,用空格隔开,分别表示第i$ 个物品组的第 j j j个物品的体积和价值;
输出格式
输出一个整数,表示最大价值。
数据范围
0
<
N
,
V
≤
100
0<N,V≤100
0<N,V≤100
0
<
S
i
≤
100
0<S_i≤100
0<Si≤100
0
<
v
i
j
,
w
i
,
j
≤
100
0<v_{i_j},w_{i,j}≤100
0<vij,wi,j≤100
输入样例
3 5
2
1 2
2 4
1
3 4
1
4 5
输出样例:
8
七、背包问题求方案数
题目概述
八、背包问题求具体方案
九、有依赖的背包问题
持续更新。。。