• 单调队列优化多重背包


      记得有一个经典的问题:

      有一个容量为$V$的背包,和$n$种物品。第$i$种物品的重量为$w_{i}$,价值为$v_{i}$,数量为$c_{i}$。问背包中装的物品的最大价值和为多少。

    Subtask 1 $1leqslant nleqslant 100, 1leqslant Vleqslant 1000,1leqslant 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 ( nVsum c_{i} ight )$的时间内解决掉了这个子任务。

    Subtask 2 $1leqslant nleqslant 1000, 1leqslant 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 = kcdot w_{i} + r left( kin mathbb{N} , 0leqslant r < w_{i} ight )$

      那么来看看对 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]

      这样,转移的时候就一视同仁了,都是加上$kcdot v_{i}$,所以只用维护剩余部分的最大值。

      于是我们可以手写堆来在$O left ( log n ight )$的时间复杂度内完成插入、删除和查询最大值。

      因此总时间复杂度变为了$O left ( nVlog n ight )$。

    Subtask 3 $1leqslant nleqslant 7000, 1leqslant V,c_{i}leqslant 7000$,答案在32位有符号整数内。(这评测机速度。。佩服佩服。。)

      友善的数据范围。。。刚刚的做法被卡掉了。。肿么办呢?

      虽然用堆来维护有贡献的转移中的最优解,看似很优秀,但是实际上可以发现仍然有很多状态从进堆到出堆根本就没有机会作为最大值。

      现在来考虑哪些状态一定没有用。

      如果存在一个$j'$使得$j < j'$并且$j$和$j'$在模$w_{i}$意义下同余,同时也满足维护的那一坨的值$gleft(j ight) < gleft(j' ight)$,那么显然$j$永远不可能在$j'$之后作为最大值。

      所以每次加入的时候就可以把满足这样性质的前面的状态丢掉。

      这样维护下来的数据结构中的值是单调递减的,所以我们只需要一个队列就行了。

      由于每个状态至多进队1次,出队1次,所以总时间复杂度为$Oleft ( nV ight )$

      又因为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 更新一处笔误
  • 相关阅读:
    2012年春晚剧本
    研究机器人
    85.圆角边框的两大要素 Walker
    81.边框设置 Walker
    76.背景固定 Walker
    84.边框方向 Walker
    86.圆角边框设置 Walker
    82.边框宽度和边框颜色 Walker
    88.轮廓和样式重置 Walker
    77.背景简写 Walker
  • 原文地址:https://www.cnblogs.com/yyf0309/p/Multiple_knapsack.html
Copyright © 2020-2023  润新知