单调队列优化多重背包
记得有一个经典的问题:
有一个容量为$V$的背包,和$n$种物品。第$i$种物品的重量为$w_{i}$,价值为$v_{i}$,数量为$c_{i}$。问背包中装的物品的最大价值和为多少。
Subtask 1 $1\leqslant n\leqslant 100, 1\leqslant V\leqslant 1000,1\leqslant c_{i}\leqslant 100$,答案在32位有符号整数内。
做法非常简单。so easy~
设 f[i][j] 表示考虑用前$i$中物品,装容量为$j$的背包可以得到的最大物品价值和。
转移方程 f[i][j] = max{f[i - 1][j - k * w[i]] + k * v[i]} 。
于是在$O \left ( nV\sum c_{i} \right )$的时间内解决掉了这个子任务。
Subtask 2 $1\leqslant n\leqslant 1000, 1\leqslant V,c_{i}\leqslant 1000$,答案在32位有符号整数内。
友善的数据范围。。。只能想办法优化。
动态规划的常用优化方向:
- 状态数:状态数已经很优秀了。这样继续优化只能降低空间复杂度但不能降低时间复杂度。
- 转移:似乎转移的过程中重复对很多数取了最大值。
例如 f[i - 1][j] 会对 f[i][j],f[i][j + w[i]],f[i][j + w[i] * 2]...... 做出贡献。
发现要想 f[i - 1][j] 对当前dp值做出贡献,那么一个必要条件是当前枚举的$j'$在模$w_{i}$意义下与$j$同余。
所以,考虑阶段$i$的时候可以将dp数组进行分组,对于模$w_{i}$的值为$r$的部分分别跑一次dp。
然而这有什么卵用呢?
刚刚考虑了一个dp值对别的dp值的贡献,现在考虑别的dp值对当前考虑的 f[i][j] 的贡献。
显然它们是: f[i - 1][j], f[i - 1][j - w[i]], f[i - 1][j - w[i] * 2], ..., f[i - 1][j - w[i] * c[i]] 。
然后对比一下对 f[i][j + w[i]] 的有贡献的状态: f[i - 1][j + w[i]], f[i - 1][j], f[i - 1][j - w[i]], ..., f[i - 1][j - w[i] * (c[i] - 1)] 。
无非就是多了一项和少了一项的差别。
现在如何来维护这些这些转移中的最优决策呢?
设
$j = k\cdot w_{i} + r \ \ \ \ \ \ \ \left( k\in \mathbb{N} , 0\leqslant r < w_{i}\right )$
那么来看看对 f[i][j] 有贡献的状态都是怎么样转移的:
f[i - 1][j - 0 * w[i]] + 0 * v[i] f[i - 1][j - 1 * w[i]] + 1 * v[i] f[i - 1][j - 2 * w[i]] + 2 * v[i] f[i - 1][j - 3 * w[i]] + 3 * v[i]
这些可以表示成:
f[i - 1][j - 0 * w[i]] + (0 - k) * v[i] + k * v[i] f[i - 1][j - 1 * w[i]] + (1 - k) * v[i] + k * v[i] f[i - 1][j - 2 * w[i]] + (2 - k) * v[i] + k * v[i] f[i - 1][j - 3 * w[i]] + (3 - k) * v[i] + k * v[i]
这样,转移的时候就一视同仁了,都是加上$k\cdot v_{i}$,所以只用维护剩余部分的最大值。
于是我们可以手写堆来在$O \left ( \log n\right )$的时间复杂度内完成插入、删除和查询最大值。
因此总时间复杂度变为了$O \left ( nV\log n\right )$。
Subtask 3 $1\leqslant n\leqslant 7000, 1\leqslant V,c_{i}\leqslant 7000$,答案在32位有符号整数内。(这评测机速度。。佩服佩服。。)
友善的数据范围。。。刚刚的做法被卡掉了。。肿么办呢?
虽然用堆来维护有贡献的转移中的最优解,看似很优秀,但是实际上可以发现仍然有很多状态从进堆到出堆根本就没有机会作为最大值。
现在来考虑哪些状态一定没有用。
如果存在一个$j'$使得$j < j'$并且$j$和$j'$在模$w_{i}$意义下同余,同时也满足维护的那一坨的值$g\left(j\right) < g\left(j'\right)$,那么显然$j$永远不可能在$j'$之后作为最大值。
所以每次加入的时候就可以把满足这样性质的前面的状态丢掉。
这样维护下来的数据结构中的值是单调递减的,所以我们只需要一个队列就行了。
由于每个状态至多进队1次,出队1次,所以总时间复杂度为$O\left ( nV \right )$
又因为codevs的评测鸡跑得飞快,然后就过了。
题目传送门 [codevs 5429]
Code
1 /** 2 * codevs 3 * Problem#5429 4 * Accepted 5 * Time: 1151ms 6 * Memory: 492k 7 */ 8 #include <bits/stdc++.h> 9 using namespace std; 10 #define pii pair<int, int> 11 #define fi first 12 #define sc second 13 14 int n, V; 15 int *vs, *wss, *cs; 16 int *f; 17 pii *que; 18 int front, rear; 19 20 inline void init() { 21 scanf("%d%d", &n, &V); 22 vs = new int[(n + 1)]; 23 wss = new int[(n + 1)]; 24 cs = new int[(n + 1)]; 25 for(int i = 1; i <= n; i++) 26 scanf("%d%d%d", wss + i, vs + i, cs + i); 27 } 28 29 inline void solve() { 30 f = new int[(V + 1)]; 31 que = new pii[(V + 1)]; 32 fill(f, f + V + 1, 0); 33 for(int i = 1; i <= n; i++) { 34 for(int r = 0, lim; r < wss[i]; r++) { 35 front = 1, rear = 1, lim = cs[i] * wss[i], que[1] = pii(f[r], r), lim = wss[i] * cs[i]; 36 for(int j = r + wss[i], k = 1, g; j <= V; j += wss[i], k++) { 37 g = f[j] - k * vs[i]; 38 while(front <= rear && que[front].sc + lim < j) front++; 39 while(front <= rear && que[rear].fi < g) rear--; 40 que[++rear] = pii(g, j); 41 f[j] = que[front].fi + k * vs[i]; 42 } 43 } 44 } 45 printf("%d", f[V]); 46 } 47 48 int main() { 49 init(); 50 solve(); 51 return 0; 52 }
更新日志
- 2018-2-10 更新一处笔误