多重背包-暴力法+ 单调队列优化法
暴力法
问题:一共 n 类物品,背包的容量是 m 每类物品的体积为v, 价值为w,个数为s,求背包内价值的最大值。
数据范围:
0
<
N
≤
1000
0
<
V
≤
20000
0
<
v
i
,
w
i
,
s
i
≤
20000
0 < N \le 1000\\ 0 < V \le 20000\\ 0 < v_i,w_i,s_i \le 20000
0<N≤10000<V≤200000<vi,wi,si≤20000
思路:因为每个物品的选几个受当前背包容量的影响,如果选的话还会改变背包剩下的容量,然后进一步影响后面的物品的选择,所以将背包的体积作为状态,(这是我自己为什么选这个作为状态的原因,,,)
那么定义 d p [ i ] [ j ] dp[i][j] dp[i][j]为从前i个物品中,且体积不超过 j 的选法的集合。
那么先考虑第 i 个物品选多少个,那么就有下面的方程 (v为当前物品的体积,w为当前物品的价值 s i s_i si为最多取几个)
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) dp[i][j] = max(dp[i - 1][j],dp[i-1][j - v] + w,dp[i-1][j -2 * v] + 2*w,...,dp[i-1][j-s_i*v]+s_i*w) dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w,...,dp[i−1][j−si∗v]+si∗w)
然后我们发现求 d p [ i ] [ j ] dp[i][j] dp[i][j]的时候涉及到的都是 i − 1 i-1 i−1中 j j j前面的数据,所以如果我们倒叙遍历 j j j就不需要第一维度了。
通过第一层for遍历物品,第二层for遍历体积,第三层遍历选法就可以得到结果了,但这样做的时间复杂度是时间复杂度是 O ( 物 品 数 ∗ 背 包 的 体 积 ∗ 选 法 ) O(物品数*背包的体积*选法) O(物品数∗背包的体积∗选法) = O ( 1000 ∗ 20000 ∗ 20000 ) O(1000 * 20000 * 20000) O(1000∗20000∗20000)显然会超时,只不过这里先不管。
下面是省掉一维时候的代码:
#include <stdio.h>
#include <algorithm>
using namespace std;
const int N = 6005;
int dp[N], n, m;
int main()
{
scanf("%d%d", &n, &m);
//遍历物品
while (n--)
{
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
//遍历体积
for (int i = m; i >= v; i--)
//遍历选法
for (int j = 0; j <= s && v * j <= i; j++)
dp[i] = max(dp[i], dp[i - j * v] + j * w);
}
printf("%d", dp[m]);
return 0;
}
对应习题:Acwing 1019庆功宴(这道题直接暴力就可以了)
单调队列优化法
因为上面暴力必然超时,所以我们这里要寻找哪里可以优化,先看下最原始的方程
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , d p [ i − 1 ] [ j − 2 ∗ v ] + 2 ∗ w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) dp[i][j] = max(dp[i - 1][j],dp[i-1][j - v] + w,dp[i-1][j -2 * v] + 2*w,...,dp[i-1][j-s_i*v]+s_i*w) dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w,...,dp[i−1][j−si∗v]+si∗w)
我们可以发现该方程的第二维是
j
−
v
,
j
−
2
∗
v
,
j
−
3
∗
v
,
j
−
s
i
∗
v
j-v,j-2*v,j-3*v,j-s_i*v
j−v,j−2∗v,j−3∗v,j−si∗v,因为受到完全背包的感染,所以写以下
d
p
[
i
]
[
j
−
v
]
dp[i][j-v]
dp[i][j−v]的方程来对比看一下。
d
p
[
i
]
[
j
−
v
]
=
m
a
x
(
d
p
[
i
−
1
]
[
j
−
v
]
,
d
p
[
i
−
1
]
[
j
−
2
∗
v
]
+
w
,
.
.
.
,
d
p
[
i
−
1
]
[
j
−
s
i
∗
v
]
+
(
s
i
−
1
)
∗
w
)
+
d
p
[
i
−
1
]
[
j
−
(
s
i
+
1
)
v
+
w
∗
s
i
]
dp[i][j - v] =max(dp[i-1][j - v],dp[i-1][j -2 * v]+w\\ ,...,dp[i-1][j-s_i*v]+(s_i-1)*w) + dp[i-1][j-(s_i+1)v + w*s_i]
dp[i][j−v]=max(dp[i−1][j−v],dp[i−1][j−2∗v]+w,...,dp[i−1][j−si∗v]+(si−1)∗w)+dp[i−1][j−(si+1)v+w∗si]
从图中可以看出来,一开始是匹配的,但是最后一个不匹配,也就是$ j
没
有
没有
没有j-1$的最后一项,所以
m
a
x
(
d
p
[
i
−
1
]
[
j
]
,
d
p
[
i
−
1
]
[
j
−
v
]
+
w
,
d
p
[
i
−
1
]
[
j
−
2
∗
v
]
+
2
∗
w
,
.
.
.
,
d
p
[
i
−
1
]
[
j
−
s
i
∗
v
]
+
s
i
∗
w
)
≠
m
a
x
(
d
p
[
i
−
1
]
[
j
−
v
]
,
d
p
[
i
]
[
j
−
v
]
)
max(dp[i - 1][j],dp[i-1][j - v] + w,dp[i-1][j -2 * v] + 2*w,...,dp[i-1][j-s_i*v]+s_i*w)\\ \not=\\ max(dp[i-1][j - v],dp[i][j - v])
max(dp[i−1][j],dp[i−1][j−v]+w,dp[i−1][j−2∗v]+2∗w,...,dp[i−1][j−si∗v]+si∗w)=max(dp[i−1][j−v],dp[i][j−v])
虽然猜想不对,但是我们发现了
d
p
[
i
]
[
j
]
对
比
d
p
[
i
]
[
j
−
v
]
dp[i][j]对比dp[i][j-v]
dp[i][j]对比dp[i][j−v]的方程中间有一大部分是匹配的,而且是后面少一项和前面多一项的关系,想利用这一部分,就想到了一个非常匹配这个过程的方法—滑动窗格。
为了方便,我们这里使用滚动数组,将 d p [ i − 1 ] [ j ] dp[i -1][j] dp[i−1][j]的值全部存在 p r e pre pre数组里面。 p r e [ j ] = d p [ i − 1 ] [ j ] pre[j] = dp[i-1][j] pre[j]=dp[i−1][j]
p r e [ j ] pre[j] pre[j]的含义是从前 i − 1 i-1 i−1个物品中选,且体积不超过 j j j的最大价值。
那么假设 s i s_i si==3, d p [ i ] [ j ] = m a x ( p r e [ j ] , p r e [ j − v ] + w , p r e [ j − 2 ∗ v ] + 2 ∗ w , p r e [ j − 3 ∗ v ] + 3 ∗ w ) dp[i][j] = max(pre[j],pre[j-v]+w,pre[j-2*v]+2*w,pre[j-3*v]+3*w) dp[i][j]=max(pre[j],pre[j−v]+w,pre[j−2∗v]+2∗w,pre[j−3∗v]+3∗w)
可以看出来,当前的窗格需要从前面往后推,然后边推还可以边把 d p [ i ] [ j − v ] , d p [ i ] [ j − ? ∗ v ] dp[i][j - v],dp[i][j-?*v] dp[i][j−v],dp[i][j−?∗v]全给赋值了,然后可以发现以下两点
-
不能像暴力法那样,直接丢掉一维,不然你求 j − 5 ∗ v j-5*v j−5∗v的时候,要用到 j − 6 ∗ v j-6*v j−6∗v已经被覆盖掉了,所以用一个滚动数组pre来存储
-
因为前往后推,所以我们先要找到起点的体积比如说上图就是 j − 6 ∗ v , 假 设 0 < j − 6 ∗ v < v j-6*v,假设0<j-6*v<v j−6∗v,假设0<j−6∗v<v。
起点有0 到
v
−
1
v-1
v−1种可能,那么我们这里就可以换一种写法了比如说
假
设
j
−
6
∗
v
=
2
,
0
<
2
<
v
那
么
j
−
5
∗
v
=
2
+
v
,
j
−
4
v
=
2
+
2
∗
v
.
.
.
j
=
2
+
6
∗
v
去
掉
等
号
一
边
就
是
2
,
2
+
v
,
2
+
2
∗
v
,
.
.
.
,
2
+
6
∗
v
那
么
m
a
x
(
p
r
e
[
j
]
,
p
r
e
[
j
−
v
]
+
w
,
p
r
e
[
j
−
2
∗
v
]
+
2
∗
w
,
p
r
e
[
j
−
3
∗
v
]
+
3
∗
w
)
变
成
m
a
x
(
p
r
e
[
2
+
6
∗
v
]
,
p
r
e
[
2
+
5
∗
v
]
+
w
,
p
r
e
[
2
+
4
∗
v
]
+
2
∗
w
,
p
r
e
[
2
+
3
∗
v
]
+
3
∗
w
)
假设j-6*v = 2,0<2<v\\ 那么j - 5*v = 2+v,\;j-4v=2+2*v...j = 2 + 6*v\\ 去掉等号一边就是 2,2+v,2+2*v,...,2+6*v\\ 那么max(pre[j],pre[j-v]+w,pre[j-2*v]+2*w,pre[j-3*v]+3*w)\\ 变成max(pre[2 + 6*v],pre[2 + 5*v]+w,pre[2 + 4*v]+2*w,pre[2 + 3*v]+3*w)
假设j−6∗v=2,0<2<v那么j−5∗v=2+v,j−4v=2+2∗v...j=2+6∗v去掉等号一边就是2,2+v,2+2∗v,...,2+6∗v那么max(pre[j],pre[j−v]+w,pre[j−2∗v]+2∗w,pre[j−3∗v]+3∗w)变成max(pre[2+6∗v],pre[2+5∗v]+w,pre[2+4∗v]+2∗w,pre[2+3∗v]+3∗w)
上面是具体的写法,下面将其抽象化:
总
体
积
m
=
k
∗
v
+
j
,
0
<
j
<
v
总体积m = k*v+j,0<j<v
总体积m=k∗v+j,0<j<v,先不管后面加的部分
p
r
e
[
2
]
,
p
r
e
[
2
+
v
]
,
p
r
e
[
2
+
2
∗
v
]
,
p
r
e
[
2
+
3
∗
v
]
,
.
.
.
,
p
r
e
[
k
∗
v
+
2
]
pre[2],pre[2+v],pre[2 + 2 *v],pre[2 + 3*v],...,pre[k*v+2]
pre[2],pre[2+v],pre[2+2∗v],pre[2+3∗v],...,pre[k∗v+2]
写到这里,我们发现,其实我们枚举
0
<
j
<
v
0<j<v
0<j<v然后一直推到这个
j
j
j对应的结尾就可以完成这个物品的更新了。这里补全一下抽象化后的整个表示
p
r
e
[
0
]
,
p
r
e
[
v
]
,
p
r
e
[
2
∗
v
]
,
p
r
e
[
3
∗
v
]
,
.
.
.
,
p
r
e
[
k
∗
v
]
p
r
e
[
1
]
,
p
r
e
[
v
+
1
]
,
p
r
e
[
2
∗
v
+
1
]
,
p
r
e
[
3
∗
v
+
1
]
,
.
.
.
,
p
r
e
[
k
∗
v
+
1
]
p
r
e
[
2
]
,
p
r
e
[
v
+
2
]
,
p
r
e
[
2
∗
v
+
2
]
,
p
r
e
[
3
∗
v
+
2
]
,
.
.
.
,
p
r
e
[
k
∗
v
+
2
]
p
r
e
[
3
]
,
p
r
e
[
v
+
3
]
,
p
r
e
[
2
∗
v
+
3
]
,
p
r
e
[
3
∗
v
+
3
]
,
.
.
.
,
p
r
e
[
k
∗
v
+
3
]
.
.
.
p
r
e
[
j
]
,
p
r
e
[
v
+
j
]
,
p
r
e
[
2
∗
v
+
j
]
,
p
r
e
[
3
∗
v
+
j
]
,
.
.
.
,
p
r
e
[
k
∗
v
+
j
]
每
一
行
滑
动
过
去
,
比
如
s
=
3
的
话
,
一
开
始
是
j
,
v
+
j
,
2
∗
v
+
j
下
一
个
就
是
v
+
j
,
2
∗
v
+
j
,
3
∗
v
+
j
这
样
边
滑
边
更
新
pre[0] , pre[v], pre[2*v], pre[3*v], ... , pre[k*v]\\ pre[1], pre[v+1], pre[2*v+1], pre[3*v+1], ... , pre[k*v+1]\\ pre[2], pre[v+2], pre[2*v+2], pre[3*v+2], ... , pre[k*v+2]\\ pre[3], pre[v+3], pre[2*v+3], pre[3*v+3], ... , pre[k*v+3]\\ ...\\ pre[j], pre[v+j], pre[2*v+j], pre[3*v+j], ... , pre[k*v+j]\\ 每一行滑动过去,比如s = 3的话,一开始是j,v+j,2*v+j\\下一个就是v+j,2*v+j,3*v+j这样边滑边更新
pre[0],pre[v],pre[2∗v],pre[3∗v],...,pre[k∗v]pre[1],pre[v+1],pre[2∗v+1],pre[3∗v+1],...,pre[k∗v+1]pre[2],pre[v+2],pre[2∗v+2],pre[3∗v+2],...,pre[k∗v+2]pre[3],pre[v+3],pre[2∗v+3],pre[3∗v+3],...,pre[k∗v+3]...pre[j],pre[v+j],pre[2∗v+j],pre[3∗v+j],...,pre[k∗v+j]每一行滑动过去,比如s=3的话,一开始是j,v+j,2∗v+j下一个就是v+j,2∗v+j,3∗v+j这样边滑边更新
然后现在知道怎么更新,还知道怎么全部都更新,剩下的就是怎么快速的找到滑动窗格的最大值,那么单调队列登场,因为直接用deque会超时,所以这里用数组模拟整个过程,有点小复杂?只不过我会尽力讲清楚的。
另起一个数组为q[N],里面存的是背包的体积,设置队列头为head,尾巴为tail q[头, , , ,…,尾巴],保证队列里面是单调的,也就是head代表的体积的价值>tail代表的体积的价值。
现在问题来到怎么维护单调队列的单调性?
当前的背包体积k,因为要单调队列的单调性,所以要找到一个合适的位置讲当前背包放入队列中,这个合适的位置的体积的最大价值大于体积k的最大价值才行, p r e [ q [ t a i l ] ] pre[q[tail]] pre[q[tail]]取得的就是当前尾部体积的价值相当于 d p [ i − 1 ] [ q [ t a i l ] ] dp[i - 1][q[tail]] dp[i−1][q[tail]]和当前的价值 d p [ i − 1 ] [ k ] dp[i-1][k] dp[i−1][k]比较,有没有发现什么不对劲?
p r e [ j ] pre[j] pre[j]的含义是从前 i − 1 i-1 i−1个物品中选,且体积不超过 j j j的最大价值。 p r e [ h ] = d p [ i − 1 ] [ h ] pre[h] = dp[i-1][h] pre[h]=dp[i−1][h]
d p [ i ] [ j ] = m a x ( d p [ i − 1 ] [ j ] , d p [ i − 1 ] [ j − v ] + w , . . . , d p [ i − 1 ] [ j − s i ∗ v ] + s i ∗ w ) dp[i][j] = max(dp[i - 1][j],dp[i-1][j - v] + w,...,dp[i-1][j-s_i*v]+s_i*w) dp[i][j]=max(dp[i−1][j],dp[i−1][j−v]+w,...,dp[i−1][j−si∗v]+si∗w)
看眼原始方程,回忆下变量的含义。没错漏掉了 k − q [ t a i l ] k - q[tail] k−q[tail]这部分给 d p [ i − 1 ] [ q [ t a i l ] ] dp[i - 1][q[tail]] dp[i−1][q[tail]]带来的价值,所以比较的时候要给加回去继续比较,那么就可以写出这部分的代码了
可以对比起来看一下
d
p
[
i
−
1
]
[
j
−
?
∗
v
]
+
?
∗
w
p
r
e
[
q
[
t
a
i
l
]
]
+
(
k
−
q
[
t
a
i
l
]
)
/
v
∗
w
dp[i-1][j - ?*v] + ?*w\\ pre[q[tail]] + (k - q[tail]) / v * w
dp[i−1][j−?∗v]+?∗wpre[q[tail]]+(k−q[tail])/v∗w
while (head <= tail && pre[q[tail]] + (k - q[tail]) / v * w <= pre[k])
tail--;
接下来直接看完整代码就可以了:
#include <stdio.h>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 20005;
// pre存储i - 1的时候的值
// q为偏移量为?时候的单调队列
int dp[N], pre[N], q[N], n, m;
int main()
{
//n为物品的总数,背包容量为m
scanf("%d%d", &n, &m);
while (n--)
{
int v, w, s;
scanf("%d%d%d", &v, &w, &s);
//赋值上一层结果
memcpy(pre, dp, sizeof(dp));
//枚举偏移量(起点)
for (int i = 0; i < v; i++)
{
//建新的单调队列 头>>>尾巴,里面存的是体积
int head = 0, tail = -1;
//更新该起点代表的行,k是此时的体积
for (int k = i; k <= m; k += v)
{
//判断窗格是否超过了k,头需要往后滑动
if (head <= tail && k - q[head] > s * v)
head++;
//赋值
//pre[q[head]]+(k - q[head]) / v * w):前i-1个中选,且体积不大于q[head]的最大价值 + 第i个物品选(k - q[head]) / v 个的价值
if (head <= tail)
dp[k] = max(dp[k], pre[q[head]] + (k - q[head]) / v * w);
//队列中加入k,并且维护队列单调性
//pre[k]:前i-1个中选,且体积不大于q[head]的最大价值 + 第i个物品选0个
//pre[q[tail]] + (k - q[tail]) / v * w :前i-1个中选,且体积不大于q[tail]的最大价值 + 第i个物品选(k - q[tail]) / v个
while (head <= tail && pre[q[tail]] + (k - q[tail]) / v * w < pre[k])
tail--;
q[++tail] = k;
}
}
}
printf("%d", dp[m]);
return 0;
}
习题:AcWing 6. 多重背包问题 III