前言
多重背包问题的时间复杂度为 ,复杂度很高,所以我们需要将其优化。其中一种办法就是使用单调队列优化,可以使复杂度达到 。
单调队列
单调队列的一个元素有两个值:元素的值和位置(下标),单调队列会保证队首元素是原数列中值最小(或最大)的。单调队列的作用可以看下面的模板题:
P1886 滑动窗口 /【模板】单调队列
有一个长为 的序列 ,以及一个大小为 的窗口。现在这个从左边开始向右滑动,每次滑动一个单位,求出每次滑动后窗口中的最大值和最小值。
了解了单调队列的用途,我们以样例为例来看它如何实现。STL中的双端队列可以实现单调队列,但是常数不得不说有一点大,所以一般单调队列都用手写队列来实现。
首先定义两个队列 与 (也可以定义结构体队列), 中是每个元素在原数列的位置, 中是每个元素的值。
(1) 将 入队,,此时队首元素为 ;
(2) 将 入队,由于 ,所以 ,此时队首元素为 ;
(3) 将 入队,由于 ,所以 和 都出队,,此时队首元素为 ;
(4) 将 入队,同理 ,所以将 出队,此时,队首元素为 ;
(5) 将 入队,此时,队首元素为 ;
(6) 将 入队,由于 ,所以,队首元素为 ;
(7) 将 入队,由于 ,也就是 已经不在窗口中了,所以弹出 ,此时,队首元素为 ;
(8) 将 入队,此时,队首元素为 ;
可以观察到每一次操作的队首元素都是当前窗口中的最小值(除了(1)(2),因为此时已经入队的元素个数少于窗口大小)。我们可以简单总结以下这些操作:设元素总数为 ,窗口大小为 ,对于一个即将入队的元素 ,如果队尾元素满足 ,那么弹出队尾元素,如果队首元素对应的在原数列中的位置 ,那么弹出队首元素,然后将 加入到 的队尾, 加入到 的队尾,如果 ,则队首元素就是当前窗口中的最小元素。
同理,我们也可以推出最大值的求法。下面上代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| #include<cstdio> using namespace std; int n,m,a[1000001],q[1000001],p[1000001],T,R; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) { scanf("%d",&a[i]); } T=0,R=-1; for(int i=1;i<=n;++i) { while(T<=R&&p[T]<=i-m) ++T; while(T<=R&&q[R]>a[i]) --R; q[++R]=a[i]; p[R]=i; if(i>=m) printf("%d ",q[T]); } printf("\n"); T=0,R=-1; for(int i=1;i<=n;++i) { while(T<=R&&p[T]<=i-m) ++T; while(T<=R&&q[R]<a[i]) --R; q[++R]=a[i]; p[R]=i; if(i>=m) printf("%d ",q[T]); } return 0; }
|
单调队列优化多重背包
单调队列如何能和背包扯上关系的?设这件物品体积为 ,价值为 ,数量为,我们来看一下多重背包的状态转移方程:
将 换为其他数,我们就可以得到:
稍加转换,可得:
是不是惊人的相似。
这样就可以得到:
我们就可以看成有一个大小为 的窗口在数列上扫过,每一个状态对应一个窗口。这样这个问题就成功地转换成了单调队列的问题了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include<cstdio> using namespace std; int n,m,q[1000001],p[1000001],T=-1,R=0,f[100001]; int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;++i) { int a,b,c; scanf("%d%d%d",&b,&a,&c); for(int j=0;j<a;++j) { T=0; R=-1; for(int k=j;k<=m;k+=a) { while(T<=R&&k-p[T]>a*c) ++T; while(T<=R&&q[R]+(k-p[R])/a*b<=f[k]) --R; p[++R]=k; q[R]=f[k]; f[k]=q[T]+(k-p[T])/a*b; } } } printf("%d",f[m]); return 0; }
|