多重背包
弱化版退化成01背包做就可以,时间复杂度为 O ( ∑ 1 n c [ i ] × M ) O(\sum_1^n{c[i]}\times M) O(∑1nc[i]×M)
多重背包二进制优化:
时间复杂度为 O ( ∑ 1 n log c [ i ] × M ) O(\sum_{1}^n \log c[i]\times M) O(∑1nlogc[i]×M) ,基本上可以认为就是 N ≤ 1000 , M ≤ 2000 N\le 1000 ,M \le 2000 N≤1000,M≤2000 外加一个 l o g \ log log
原理:通过二进制拆分,拆成 p + 2 p+2 p+2 个物品,使得,可以组成 0 0 0 到 c [ i ] c[i] c[i] 中所有数。
推导: 因为我们知道 2 0 , 2 1 . . . . 2 k 2^0,2^1....2^k 20,21....2k 从中选择数字,可以组合成 0 0 0 到 2 k + 1 − 1 2^{k+1}-1 2k+1−1 所有的数字。
我们找到一个 p p p 满足 2 0 + 2 1 . . . + 2 p ≤ C 2^0+2^1...+2^p\le C 20+21...+2p≤C ,其中 C C C 表示物品个数,那么我们设 R = C − ( 2 0 + 2 1 . . . + 2 p ) R=C-(2^0+2^1...+2^p) R=C−(20+21...+2p) 。
由于 2 p + 1 > C 2^{p+1}>C 2p+1>C 则 2 0 + 2 1 . . . + 2 p + 2 p + 1 > C 2^0+2^1...+2^p+2^{p+1}>C 20+21...+2p+2p+1>C
移项得 2 p + 1 > C − ( 2 0 + 2 1 . . . + 2 p ) 2^{p+1}>C-(2^0+2^1...+2^p) 2p+1>C−(20+21...+2p) 因为 R = C − ( 2 0 + 2 1 . . . + 2 p ) R=C-(2^0+2^1...+2^p) R=C−(20+21...+2p)
所以得 2 p + 1 > R 2^{p+1}>R 2p+1>R
因为有推导给出的结论,所以 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 20,21...2p 可以组成 0 0 0 到 R R R 的任意数。
我们再来证明 R R R 到 C C C 中的数如何全部组合成功。
显然的 R + 2 0 + 2 1 . . . + 2 p = C R+2^0+2^1...+2^p=C R+20+21...+2p=C 则 2 p + 1 > C − R 2^{p+1}>C-R 2p+1>C−R ,即 R R R 到 C C C 部分。
所以可知 2 0 , 2 1 . . . 2 p 2^0,2^1...2^p 20,21...2p 可以组成 C − R C-R C−R 任意部分。
进推得的 R + 2 0 , 2 1 . . . 2 p R+2^0,2^1...2^p R+20,21...2p 可以组成 R R R 到 C C C 部分
所以得出 R , 2 0 , 2 1 . . . 2 p R,2^0,2^1...2^p R,20,21...2p ,一共 p + 2 p+2 p+2 元素可以组成 C C C 的任意部分。
总的来说,其实就是先证明 R R R 部分可以组成,然后在证明 R R R 到 C C C 的部分也可以组成。
模板如下:
注意点
- 更新之后的体积和价值中不在包含以前的物品
#include<bits/stdc++.h>
#define int long long
using namespace std;
int read(){int x;scanf("%lld",&x);return x;}
const int B=1e6+10;
const int inf=0x3f3f3f3f;
int T;
int f[B];
int n,m;
int v[B],w[B],c[B];
int a[B],b[B],cnt;
void work()
{
n=read(),m=read();
for (int i=1;i<=n;i++)
{
w[i]=read();
v[i]=read();
c[i]=read();
}
for (int i=1;i<=n;i++)
{
for (int j=1;j<=c[i];j<<=1)
{
a[++cnt]=w[i]*j;
b[cnt]=v[i]*j;
c[i]-=j;
}
if (c[i]) {a[++cnt]=c[i]*w[i];b[cnt]=c[i]*v[i];}
}
for (int i=1;i<=cnt;i++)
for (int j=m;j>=a[i];j--)
f[j]=max(f[j],f[j-a[i]]+b[i]);
cout<<f[m];
}
signed main()
{
T=1;
while (T--) work();
return 0;
}
多重背包单调队列优化
时间复杂度将至到线型 O ( N M ) O(NM) O(NM) ,做多可以实现 1000 × 10000 1000\times 10000 1000×10000
原理:通过减少DP方程转移方向的数量来简化时间复杂度
实现:通过多重背包一般转移式可以发现:
f
[
i
]
[
j
]
=
max
{
f
[
i
−
1
]
[
j
]
,
f
[
i
−
1
]
[
j
−
k
×
w
[
i
]
]
+
k
×
v
[
i
]
}
f[i][j]=\max\{f[i-1][j],f[i-1][j-k\times w[i]]+k\times v[i]\}
f[i][j]=max{f[i−1][j],f[i−1][j−k×w[i]]+k×v[i]}
每次转移,体积差距都在
w
[
i
]
w[i]
w[i] 的倍数,说明转移和被转移的体积对
w
[
i
]
w[i]
w[i] 取模余数相同的。我们发现,余数不同的之间不会相互影响,因此我们可以考虑分组进行转移。
我再来对单调队列进行一下补充:为什么用单调队列,想必大家都已经明白了,这个算法的难点在于如何维护单调队列。单单这么讲,我们直观的看,就是滑动窗口模板,但是我们发现,从
j
→
j
+
w
j\to j+w
j→j+w 的时候。
f
[
i
]
[
j
+
w
]
=
max
{
f
[
i
−
1
]
[
j
]
+
v
,
f
[
i
−
1
]
[
j
−
w
]
+
2
×
w
.
.
.
.
.
.
}
f[i][j+w]=\max\{f[i-1][j]+v,f[i-1][j-w]+2\times w......\}
f[i][j+w]=max{f[i−1][j]+v,f[i−1][j−w]+2×w......}
而在转移
f
[
i
]
[
j
]
f[i][j]
f[i][j] 的时候是如下:
f
[
i
]
[
j
]
=
max
{
f
[
i
−
1
]
[
j
−
w
]
+
v
,
f
[
i
−
1
]
[
j
−
2
×
w
]
+
2
×
w
.
.
.
.
.
.
}
f[i][j]=\max\{f[i-1][j-w]+v,f[i-1][j-2\times w]+2\times w......\}
f[i][j]=max{f[i−1][j−w]+v,f[i−1][j−2×w]+2×w......}
我们发现在求解
f
[
i
]
[
j
]
f[i][j]
f[i][j] 的时候,我们的需要集合转移和
f
[
i
]
[
j
+
w
]
f[i][j+w]
f[i][j+w] 的集合转移是不一样的。我们所说的连续是指对于按照余数分组之后,体积是连续,但是我们发现,对于相同位置,需要的状态转移表达式是不一样的。
比如
- f [ i ] [ j + w ] f[i][j+w] f[i][j+w] 中有 f [ i − 1 ] [ j − w ] + 2 × v f[i-1][j-w]+2\times v f[i−1][j−w]+2×v
- f [ i ] [ j ] f[i][j] f[i][j] 中有 f [ i − 1 ] [ j − w ] + v f[i-1][j-w]+v f[i−1][j−w]+v
可是我们的单调队列维护的就是这个最大值。
不难发现,当体积发生变换的时候 j → j + w j\to j+w j→j+w,相同位置上的值发生改变了,这该如何办。
所以我们发现,这题目的真正的难点在这是一个数字发生变化的移动窗口
那么我们如何解决这个问题?
我们发现当 j → j + w j\to j+w j→j+w 的时候,队列中的每个状态都会 + v +v +v ,因为我们队列中存放的是每个转移的体积,维护的是 f [ i − 1 ] [ k ] + V f[i-1][k]+V f[i−1][k]+V(随机数) 同时加是不会影响单调性的,相当于没加,我们可以当成不操作,但是,有一个新数会加入到队列 f [ i − 1 ] [ j ] + v f[i-1][j]+v f[i−1][j]+v 此时,我们为保证大小,就必须更改队列在维护时的元素的 f + V f+V f+V 的值,使得他们都加 v v v。
我们可以反过来看,我们需要比较的只是大小关系,我把 f [ i − 1 ] [ j ] f[i-1][j] f[i−1][j] 加入,省去 v v v,这样就不需要队列中的其他元素加了,只需要加上自己原有的 n u m × v num\times v num×v 就可以了。
所以得出了出队代码
int V=(j-q[tail])*v;
while (head<=tail && f[i-1][q[tail]]+V<=f[i-1][j]) tail--;//因为我们一直维护的是上一维度的f值,所以和 f[i-1][j] 比较,而不是 f[i][j]
q[++tail]=j;
模板:
int f[3][B];
int q[B];
int n,m;
void work()
{
cin>>n>>m;
int x=0;
for (int i=1;i<=n;i++)
{
x^=1;
int w=read(),v=read(),c=read();
for (int j=0;j<w;j++)
{
int head=1,tail=0;
for (int k=j;k<=m;k+=w)
{
f[x][k]=f[x^1][k];
while (head<=tail && k-c*w>q[head]) head++;
if (head<=tail) f[x][k]=max(f[x][k],f[x^1][q[head]]+(k-q[head])/w*v);
while (head<=tail && f[x^1][q[tail]]+(k-q[tail])/w*v<=f[x^1][k]) tail--;//维护的是上一维度的f值
q[++tail]=k;
}
}
}
cout<<f[x][m];
}