背包入门

本文最后更新于:2023年6月28日 晚上

背包笔记

大部分内容参考来自这篇文章[1]和wiki上的内容[2]

01背包问题

题目:有N件物品和一个容量为V的背包。第i件物品的费用是v[i],价值是w[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

这是最基础的背包问题,后面的背包问题都由01背包问题衍生。问题的特点是:每种物品仅有一件,只能选择放或不放。

如果这个问题采用枚举的思想,他的时间复杂度为$O(n^2)$,我们可以利用一张二维表记录数据,运用递推,这样时间复杂度就降低为$O(N*V)$

基本思路:用子问题定义状态:即f[i][v]表示前i件物品恰放入一个容量为v的背包可以获得的最大价值。则其状态转移方程便是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。 具体来说就是我们就利用一张二位表,记录了整个题目中每一个问题的解答。

用一个例子来讲大致是什么

输入

4 5
1 2
2 4
3 4
4 5

输出

8

这个例子我们可以利用一张二维表来直观展示表示

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include<bits/stdc++.h>
using namespace std;
int w[1002],v[1002];
int f[1002][1002];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++){
cin>>v[i]>>w[i];
}

for(int i = 1; i <= N; i++) {
for(int j = 1; j <= V; j++)
{
if(j < v[i])
f[i][j] = f[i - 1][j];
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]);
}
}
cout<<f[N][V];
return 0;
}

对于这个代码的优化,其中时间复杂度基本已经不能再优化了,我们可以将他改为一维数组,这样空间复杂度却可以优化到O(V)。

注意改成一维数组后要从后往前逆序推,如果正推一个物品会取多次,这也是完全背包的解法。

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
using namespace std;
int w[1002],v[1002];
int f[1002];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++){
cin>>v[i]>>w[i];
}

for(int i = 1; i <= N; i++) {
for(int j = V; j >= v[i]; j--)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout<<f[V];
return 0;
}

总结

01背包问题是最基本的背包问题,它包含了背包问题中设计状态、方程的最基本思想,另外,别的类型的背包问题往往也可以转换成01背包问题求解。故一定要仔细体会上面基本思路的得出方法,状态转移方程的意义,以及最后怎样优化的空间复杂度。

完全背包问题

完全背包模型与 0-1 背包类似,与 0-1 背包的区别仅在于一个物品可以选取无限次,而非仅能选取一次。

我们可以借鉴 0-1 背包的思路,进行状态定义。其状态转移方程依然还是:f[i][v]=max{f[i-1][v],f[i-1][v-c[i]]+w[i]}。但其推到方式与0-1背包不同

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include<bits/stdc++.h>
using namespace std;
int w[1002],v[1002];
int f[1002];
int main(){
int N,V;
cin>>N>>V;
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++)
{
f[j] = max(f[j], f[j - v[i]] + w[i]);
}
}
cout<<f[V];
return 0;
}

多重背包问题

【题目描述】
设有n种物品,每种物品有一个重量及一个价值。但每种物品的数量是无限的,同时有一个背包,最大载重量为M,今从n种物品中选取若干件(同一种物品可以多次选取),使其重量的和小于等于M,而价值的和为最大。

【输入】
第一行:两个整数,M(背包容量,M≤200)和N(物品数量,N≤30);
第2..N+1行:每行二个整数Wi,Ci,表示每个物品的重量和价值。

【输出】
仅一行,一个数,表示最大总价值。

【输入样例】
10 4
2 1
3 3
4 5
7 9
【输出样例】
12

可以用和01背包一样的方式,f[i][v]表示前i种物品恰放入一个容量为v的背包的最大价值,可以用k表示当前容量下可以装第i种物品的件数,那么k的范围应该是0≤k≤v/c[i],既然要用当前物品i把当前容量装满那需要0≤k≤v/c[i]件,其中k表示件数。
状态转移方程为:f[i][v] = max{f[i-1][v],f[i-1][j - k * c[i]] + k * w[i]}(0<=k*c[i]<=v)

基于01背包问题的代码进行简单修改,我们可以写出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include<bits/stdc++.h>
using namespace std;
int c[1002],w[1002];
int f[1002][1002];
int main(){
int N,V;
cin>>V>>N;
for(int i=1;i<=N;i++){
cin>>c[i]>>w[i];
}
for (int i = 1; i<=N; i++){
for (int j = 1; j <= V; j++){
for (int k = 0; k*c[i] <= j; k++){
f[i][j] =max(f[i-1][j],f[i-1][j-k*c[i]]+k*w[i]) ;
//要么不取,要么取0件、取1件、取2件……取k件
}
}
}
cout<<f[N][V];
return 0;
}

二进制分组优化

在朴素的做法中,一个一个枚举通常效率低下,因为在逐个枚举的过程中,出现了同时选了等效的问题,我们可以对数量进行二进制分组。

例子
6=1+2+3
8=1+2+3+1
1
2
3
4
5
6
7
8
9
10
11
12
13
index = 0;
for (int i = 1; i <= m; i++) {
int c = 1, p, h, k;
cin >> p >> h >> k;
while (k - c > 0) {
k -= c;
list[++index].w = c * p;
list[index].v = c * h;
c *= 2;
}
list[++index].w = p * k;
list[index].v = h * k;
}

单调队列优化

(不会,暂时空着)

混合背包问题

混合背包就是将前面三种的背包问题混合起来,有的只能取一次,有的能取无限次,有的只能取 次。

这种题目看起来很吓人,可是只要领悟了前面几种背包的中心思想,并将其合并在一起就可以了。

参考文献


背包入门
https://www.liahnu.top/2022/07/16/背包入门/
作者
liahnu
发布于
2022年7月16日
许可协议