跳转至

单调队列/单调栈优化

引入

前置知识:单调队列单调栈

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

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

单调队列优化具体步骤

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

单调栈优化具体步骤

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

单调队列优化多重背包

问题描述

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

不了解背包 DP 的请先阅读 背包 DP。设 表示前 个物品装入承重为 的背包的最大价值,朴素的转移方程为

时间复杂度

考虑优化 的转移。为方便表述,设 ,其中 ,则转移方程可以表示为:

。则方程可以表示为:

这样就转化为一个经典的单调队列优化形式了。 可以 计算,因此对于固定的 ,我们可以在 的时间内计算出 。因此求出所有 的复杂度为 。这样转移的总复杂度就降为

在实现的时候,我们需要先枚举 ,这样才能保证枚举 的时候利用单调队列进行优化,而单调队列中存储的是 ,并不存储 ,这样使用的时候需要通过 f[last][q.front() * w[i] + y] - q.front() * v[i] 获取对应的 ,不难发现 ,因此在枚举 的时候,我们需要删除队列中不在这个范围内的元素。

参考代码
 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
#include <array>
#include <deque>
#include <iostream>

constexpr int MAXV = 4e4 + 10;
constexpr int MAXN = 1e2 + 10;

using namespace std;

int n, W, last = 0, now = 1;
array<int, MAXN> v, w, k;
array<array<int, MAXV>, 2> f;
deque<int> q;

int main() {
  ios::sync_with_stdio(false);
  cin >> n >> W;
  for (int i = 1; i <= n; i++) {
    cin >> v[i] >> w[i] >> k[i];
  }
  for (int i = 1; i <= n; i++) {
    for (int y = 0; y < w[i]; y++) {
      // 清空队列
      deque<int>().swap(q);
      for (int x = 0; x * w[i] + y <= W; x++) {
        // 弹出不在范围的元素
        while (!q.empty() && q.front() < x - k[i]) {
          q.pop_front();
        }
        // 保证队列单调
        while (!q.empty() && f[last][q.back() * w[i] + y] - q.back() * v[i] <
                                 f[last][x * w[i] + y] - x * v[i]) {
          q.pop_back();
        }
        q.push_back(x);
        f[now][x * w[i] + y] =
            f[last][q.front() * w[i] + y] - q.front() * v[i] + x * v[i];
      }
    }
    swap(last, now);
  }
  cout << f[last][W] << endl;
  return 0;
}

习题

例题 CF372C Watching Fireworks is Fun

题目大意:城镇中有 个位置,有 个烟花要放。第 个烟花放出的时间记为 ,放出的位置记为 。如果烟花放出的时候,你处在位置 ,那么你将收获 点快乐值。

初始你可在任意位置,你每个单位时间可以移动不大于 个单位距离。现在你需要最大化你能获得的快乐值。

表示在放第 个烟花时,你的位置在 所能获得的最大快乐值。

写出状态转移方程:,其中

尝试变形:

由于 里出现了一个确定的常量 ,我们可以将它提到外面去。

如果确定了 的值,那么 的值也是确定的,也可以将这一部分提到外面去。

最后,式子变为:

接下来考虑单调队列优化。由于最终式子中的 只和上一状态中连续的一段的最大值有关,所以我们在计算一个新的 的状态值时候只需将原来的 构造成一个单调队列,并维护单调队列,使得其能在均摊 的时间复杂度内计算出 的值,从而根据公式计算出 的值。

总的时间复杂度为

参考代码
 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
#include <algorithm>
#include <cstring>
#include <iostream>
using namespace std;
using ll = long long;

constexpr int MAXN = 150000 + 10;
constexpr int MAXM = 300 + 10;

ll f[2][MAXN];
ll a[MAXM], b[MAXM], t[MAXM];
int n, m, d;

int que[MAXN];

int fl = 1;

void init() {
  memset(f, 207, sizeof(f));
  memset(que, 0, sizeof(que));
  for (int i = 1; i <= n; i++) f[0][i] = 0;
  fl = 1;
}

void dp() {
  init();
  for (int i = 1; i <= m; i++) {
    int l = 1, r = 0, k = 1;
    for (int j = 1; j <= n;
         j++) {  // 在这里使用了单调队列的优化,推式子详见上面
      for (; k <= min(1ll * n, j + d * (t[i] - t[i - 1])); k++) {
        while (l <= r && f[fl ^ 1][que[r]] <= f[fl ^ 1][k]) r--;
        que[++r] = k;
      }

      while (l <= r && que[l] < max(1ll, j - d * (t[i] - t[i - 1]))) l++;
      f[fl][j] = f[fl ^ 1][que[l]] - abs(a[i] - j) + b[i];
    }

    fl ^= 1;
  }
}

int main() {
  cin >> n >> m >> d;
  for (int i = 1; i <= m; i++) cin >> a[i] >> b[i] >> t[i];

  // then dp
  dp();
  ll ans = -1e18;
  for (int i = 1; i <= n; i++) ans = max(ans, f[fl ^ 1][i]);
  cout << ans << endl;
  return 0;
}