背包问题题谱
1. 多重背包问题基础版
1. 问题描述
有 N 种物品和一个容量是 V 的背包。第 i 种物品最多有 si 件,每件体积是
vi,价值是 wi。求解将哪些物品装入背包,可使物品体积总和不超过背包容量,且
价值总和最大。输出最大价值。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品种数和背包容积。接下来有 N
行,每行三个整数 vi,wi,si,用空格隔开,分别表示第 i 种物品的体积、价值和
数量。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤100
0<vi,wi,si≤100
输入样例
4 5
1 2 3
2 4 1
3 4 3
4 5 2
输出样例:
10
要点
- 每种物品只能使用si次
- 总体积不超过V
- 总价值最大
2. 问题分析
按照第i个物品选取的个数进行集合划分。
按照上面的分析可以这样表示出
- 第i种物品1件物品都不选 f[i - 1][j]
- 第i种物品选1件 f[i - 1][ j - v[i]] + w[i]
- 第i种物品选2件 f[i - 1][ j - v[i] * 2] + w[i] * 2
- …
- 第i种物品选si件 f[i - 1][ j - v[i] * si] + w[i] * si
问题的属性是 最大值,则f[i,j]可以表示如下
f
[
i
,
j
]
=
m
a
x
(
f
[
i
−
1
]
[
j
]
,
f
[
i
−
1
]
[
j
−
v
[
i
]
]
+
w
[
i
]
,
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
2
]
+
w
[
i
]
∗
2
,
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
3
]
+
w
[
i
]
∗
3
,
.
.
.
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
s
i
]
+
w
[
i
]
∗
s
i
)
f[i,j] = max(f[i - 1][j],f[i - 1][j - v[i]] + w[i],f[i - 1][j - v[i] * 2] + w[i] * 2, f[i - 1][j - v[i] * 3] + w[i] * 3,...f[i - 1][j - v[i] * si] + w[i] * si)
f[i,j]=max(f[i−1][j],f[i−1][j−v[i]]+w[i],f[i−1][j−v[i]∗2]+w[i]∗2,f[i−1][j−v[i]∗3]+w[i]∗3,...f[i−1][j−v[i]∗si]+w[i]∗si) (1)
按照之前我们在完全背包问题的经验,我们观察(2)式如下
f
[
i
,
j
−
v
[
i
]
]
=
m
a
x
(
f
[
i
−
1
]
[
j
−
v
[
i
]
]
,
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
2
]
+
w
[
i
]
,
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
3
]
+
w
[
i
]
∗
2
,
.
.
.
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
(
s
i
)
]
+
w
[
i
]
∗
(
s
i
−
1
)
,
f
[
i
−
1
]
[
j
−
v
[
i
]
∗
(
s
i
+
1
)
]
+
w
[
i
]
∗
s
i
)
f[i,j - v[i]] = max( f[i - 1][j - v[i]] ,f[i - 1][j - v[i] * 2] + w[i] , f[i - 1][j - v[i] * 3] + w[i] * 2,...f[i - 1][j - v[i] * (si)] + w[i] * (si - 1) ,f[i - 1][j - v[i] * (si + 1)] + w[i] * si )
f[i,j−v[i]]=max(f[i−1][j−v[i]],f[i−1][j−v[i]∗2]+w[i],f[i−1][j−v[i]∗3]+w[i]∗2,...f[i−1][j−v[i]∗(si)]+w[i]∗(si−1),f[i−1][j−v[i]∗(si+1)]+w[i]∗si)(2)
可以看到多了一项,无法进行直接带入。
3.代码
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
//v[i]代表第i种物品的体积
//w[i]代表第i种物品的价值
//s[i]代表第i种物品的个数
int v[N],w[N],s[N],f[N][N];
int main()
{
int n,m;
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 ++)
{
//保证j有足够的空间放下k * v[i]
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];
}
2.多重背包问题二进制优化
1.分析
我们分析一下上面的时间复杂度。有三重循环。二进制优化的思路是这样的。
对于每种物品si件的数量,我们可以把si表示如下
s
i
=
2
0
+
2
1
+
.
.
.
+
2
k
+
C
si = 2^0 + 2^1+ ...+ 2^k+C
si=20+21+...+2k+C,其中
k
<
⌊
l
o
g
2
s
i
⌋
k <\lfloor log_2{si}\rfloor
k<⌊log2si⌋
例如si = 17,则
s
i
=
2
0
+
2
1
+
2
2
+
2
3
+
1
si = 2^0 + 2^1+2^2+ 2^3+1
si=20+21+22+23+1
给
2
k
2^k
2k的各个项及C赋予系数{0,1}可以表示出0 - si的所有情况。
例如对于0 - 17 中的12,可以表示为
14
=
0
∗
2
0
+
1
∗
2
1
+
1
∗
2
2
+
1
∗
2
3
+
0
∗
1
14 = 0* 2^0 + 1 * 2^1+1 * 2^2 + 1 * 2^3 + 0 * 1
14=0∗20+1∗21+1∗22+1∗23+0∗1
如此之下,我们就无需在第三层循环中枚举k,而是可以将si分成
l
o
g
2
s
i
log_2si
log2si堆物品,每堆物品
2
k
2^k
2k件物品,最后一堆为C件物品,将每堆物品视为一件物品,这样每"件”物品就有了新的体积和价值(也就是组成它的所有物品的体积之和和价值之和)。将每种物品都做这样的分解,这样我们就可以得到总共
N
l
o
g
2
s
i
Nlog_2si
Nlog2si“件”新物品,这些每件只能取一个或者0个。这就把原来的多重背包问题转换成了新物品的01背包问题。这样就降低了时间复杂度。
2. 代码
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 10010;
//v存储每件"新"物品的体积
//w存储每件“新”物品的价值
int f[N],v[N],w[N];
int main()
{
int n,m,cnt = 0;
scanf("%d%d",&n,&m);
for(int i = 1; i <= n; i ++)
{
int a,b,s,k = 1;
scanf("%d%d%d",&a,&b,&s);
//将旧物品转换为“新”物品
//cnt 记录新物品件数
while(s >= k)
{
cnt ++;
v[cnt] = a * k;
w[cnt] = b * k;
s -= k;
k += k;
}
//C
if(s){
cnt ++;
v[cnt] = s * a;
w[cnt] = s * b;
}
}
n = cnt;
//01背包问题解法
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]);
}
}
printf("%d",f[m]);
}
3.多重背包问题滑动窗口优化
继续观察我们从完全背包问题获得的经验虽然无法像完全背包问题一样直接带入进行计算,但相邻的两个状态总会有部分项相同。我们把所有的情况全部列出(r 代表j 除以 v的余数)可以看到
图片来源
将f(i - 1,y)记为y,观察可以看到,相同的项均与上一个阶段的自己相差一个w,所以我们可以忽略w观察,计算时加上即可
可以看到有如下的情形:
这里我们会把同一个阶段i的所有状态j根据模 vi 进行分类。对于同余的一类,再用单调队列优化更新。这样就将全部的状态都考虑到了。
和01背包的优化类似,观察到状态转移方程,对于i阶段,只会用到i-1层的状态。因此可以采用拷贝数组或滚动数组的写法。需要熟悉单调队列,可参考https://blog.csdn.net/qq_43583902/article/details/120396322
关于w的添加,在代码中看。
#include<iostream>
#include<cstring>
#include<algorithm>
using namespace std;
const int N = 20010;
int f[N],g[N],q[N];
int main()
{
int n,m;
cin >> n >> m;
for(int i = 1; i <= n; i ++)
{
int v,w,s;
cin >> v >> w >> s;
//将f[i-1]的状态暂存
memcpy(g,f,sizeof f);
for(int r = 0; r < v; r ++)
{
int hh = 0, tt = -1;
//对于每一个余数,都要初始化队列,在队列中存储的是状态j
for(int j = r; j <= m; j += v)
{
//如果队头出队了,也就是队头的状态比当前的状态还要小至少(s+1)v
if(hh <= tt && j - q[hh] > s * v) hh ++;
//更新f,这里要把w加上,具体加多少呢,可以在上面的图中观察到
if(hh <= tt) f[j] = max(f[j],g[q[hh]] + (j - q[hh]) / v * w);
//找到j自己的位置,这个时候要减掉所有的w进行比较。
while(hh <= tt && g[q[tt]] - (q[tt] - r) / v * w <= g[j] - (j - r) / v * w) tt --;
q[++ tt] = j;
}
}
}
cout << f[m];
}