跳转至

单调队列(单调栈)优化 DP

引入

单调队列主要用于维护两端指针单调不减的区间最值,而单调栈则主要用于维护前/后第一个大于/小于当前值的数。

注意
  • 求最小值要维护 单调递增/不减 的单调队列/单调栈,反之亦然。
  • 维护单调递增/递减比较时用 小于等于/大于等于,维护单调不减/不增比较时用 小于/大于

单调队列优化具体步骤

  • 加入所需元素:向单调队列重复加入元素直到当前元素达到所求区间的右边界,这样就能保证所需元素都在单调队列中。
  • 弹出越界队首:单调队列本质上是维护的是所有已插入元素的最值,但我们想要的往往是一个区间最值。于是我们弹出在左边界外的元素,以保证单调队列中的元素都在所求区间中。
  • 获取最值:直接取队首作为答案即可。

单调栈优化具体步骤

  • 弹出非法栈顶:通过比较当前元素与栈顶的大小,弹出不满足单调栈性质的栈顶。以单调递增的栈(即栈顶最大,维护最小值)为例,将所有大于等于当前元素的栈内元素全部弹出。
  • 加入当前元素:将当前元素入栈即可。

单调队列优化多重背包

P1776 宝物筛选

你有 \(n\) 个物品,每个物品重量为 \(v_i\),价值为 \(w_i\),数量为 \(c_i\)。你有一个承重上限为 \(m\) 的背包,现在要求你在不超过重量上限的情况下选取价值和尽可能大的物品放入背包。求最大价值。

\(f_{i,j}\) 表示前 \(i\) 个物品装入承重为 \(j\) 的背包的最大价值,朴素的转移方程为

\[ f_{i,j}=\max_{k=0}^{c_i}\{f_{i-1,j-k\times v_i}+k\times w_i\} \]

时间复杂度 \(O(m\sum c_i)\)

这看起来无法优化,但注意到,对于每一个物品,转移一定相隔 \(v_i\) 的倍数,也就是说,转移一定在 \(f_{i, d}, f_{i, d + v_i}, f_{i, d + 2v_i}, ..., f_{i, d + k \times v_i}\) 之间进行。

由此为出发点,我们枚举这个 \(d\)(它实际上是当前容量模 \(v_i\) 的结果),改写转移方程:

\[ \begin{aligned} f_{i, d + j \times v_i} &= \max_{0 \leq j - k \leq c_i}\{f_{i - 1, d + k \times v_i} + (j - k) \times w_i\} \\ &= \max_{j - c_i \leq k \leq j}\{f_{i - 1, d + k \times v_i} - k \times w_i\} + j \times w_i \end{aligned} \]

这样就可以单调队列优化了,每次转移就是 \(O(d) \times O(\lfloor\frac{m}{d}\rfloor) = O(m)\) 的,总复杂度为 \(O(nm)\)

显然可以滚动数组优化。

实现
 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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
#include <bits/stdc++.h>
using namespace std;
#ifdef ONLINE_JUDGE
#define debug(...)
#else
#define debug(fmt, ...) fprintf(stderr, "Debug at function %s, line %d: " fmt, __func__, __LINE__, ##__VA_ARGS__)
#endif
#define endl '\n'
#define eb emplace_back
#define ep emplace
#define fi first
#define se second
#define rep(i, l, r, ...) for (int i = (l), i##e = (r), ##__VA_ARGS__; i <= i##e; ++i)
#define per(i, r, l, ...) for (int i = (r), i##e = (l), ##__VA_ARGS__; i >= i##e; --i)
#define mst(x, val, len) memset(x, val, sizeof((x)[0]) * (int(len) + 1))
#define mcp(from, to, len) memcpy(to, from, sizeof((to)[0]) * (int(len) + 1))
#define mid (((l) + (r)) >> 1)
#define ls ((x) << 1)
#define rs ((x) << 1 | 1)
#define pbds __gnu_pbds
#define int ll
typedef long long ll;
typedef vector<int> vi;
typedef pair<int, int> pii;
typedef vector<pii> vpii;
constexpr int MAXN = 1e5 + 10, inf = 1e9, mod = 998244353, MAXV = MAXN << 2;
template <typename T> inline void chkmx(T &a, const T b) { a = a < b ? b : a; }
template <typename T> inline void chkmn(T &a, const T b) { a = a > b ? b : a; }
template <typename T> inline void Add(T &a, const T b) { a = ((a + b) % mod + mod) % mod; }
template <typename T> inline void Mul(T &a, const T b) { a = ((a * b) % mod + mod) % mod; }
template <typename T> inline void add(T &a, const T b) { a = (a + b >= mod ? a + b - mod : a + b); }
template <typename T> inline void mul(T &a, const T b) { a = a * b % mod; }
bool MST;
// q[i] 为可以转移的 k,g[j] 为 f[i - 1][d + j * v[i]] - j * w[i]
int n, m, ans, hd, tl, q[MAXN], f[MAXN], g[MAXN];
bool MED;
signed main()
{
    ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
    debug("Memory Used: %.2lf Mib.\n", (&MST - &MED) / 1024. / 1024.);
    cin >> n >> m;
    rep(i, 1, n, v, w, c)
    {
        cin >> w >> v >> c;
        if (!v)
        {
            ans += w * c;
            continue;
        }
        chkmn(c, m / v);
        rep(d, 0, v - 1)
        {
            hd = 1, tl = 0;
            rep(j, 0, (m - d) / v)
            {
                while (hd <= tl && f[d + j * v] - j * w >= g[tl]) tl--;
                q[++tl] = j, g[tl] = f[d + j * v] - j * w;
                while (hd <= tl && q[hd] < j - c) hd++;
                chkmx(f[d + j * v], g[hd] + j * w);
            }
        }
    }
    cout << ans + f[m];
    return 0;
}