Monotonic Queue/Stack Optimization

Introduction

Prerequisite knowledge: Monotonic Queue, Monotonic Stack.

A monotonic queue is mainly used to maintain the range extrema of a pair of pointers that are monotonic nondecreasing, while a monotonic stack is mainly used to maintain the first number before/after the current value that is greater/less than it.

Note
  • To find a minimum, maintain a monotonically increasing/nondecreasing monotonic queue/stack, and vice versa.
  • When maintaining a monotonically increasing/decreasing comparison, use less than or equal to/greater than or equal to; when maintaining a monotonically nondecreasing/nonincreasing comparison, use less than/greater than.

Specific Steps for Monotonic Queue Optimization

  • Add the required elements: repeatedly add elements to the monotonic queue until the current element reaches the right boundary of the required interval, ensuring that all required elements are in the monotonic queue.
  • Remove the out-of-range front element: a monotonic queue essentially maintains the extrema of all inserted elements, but what we usually want is the extrema of an interval. Therefore, remove elements outside the left boundary to ensure that all elements in the monotonic queue are in the required interval.
  • Obtain the extrema: directly take the front of the queue as the answer.

Specific Steps for Monotonic Stack Optimization

  • Remove an invalid stack top: compare the current element with the stack top and remove stack-top elements that do not satisfy the monotonic stack property. Taking a monotonically increasing stack (that is, the stack top is the largest and the minimum is maintained) as an example, remove all stack elements greater than or equal to the current element.
  • Add the current element: simply push the current element onto the stack.

Monotonic Queue Optimization for Multiple Knapsack

Problem Description

You have nn items. Each item has weight wiw_i, value viv_i, and quantity kik_i. You have a knapsack with weight capacity WW. You are now required to put items with the largest possible total value into the knapsack without exceeding the weight capacity. Find the maximum value.

If you are unfamiliar with knapsack DP, read Knapsack DP first. Let fi,jf_{i,j} denote the maximum value obtained by putting the first ii items into a knapsack with capacity jj. The naive transition equation is

fi,j=max⁡k=0ki(fi−1,j−k×wi+vi×k)f_{i,j}=\max_{k=0}^{k_i}(f_{i-1,j-k\times w_i}+v_i\times k)

The time complexity is O(W∑ki)O(W\sum k_i).

Consider optimizing the transition of fif_i. For convenience, let gx,y=fi,x×wi+y,gx,y′=fi−1,x×wi+yg_{x,y}=f_{i,x\times w_i+y},g'_{x,y}=f_{i-1,x\times w_i+y}, where 0≤y<wi0\le y \lt w_i. Then the transition equation can be written as:

gx,y=max⁡k=0ki(gx−k,y′+vi×k)g_{x,y}=\max_{k=0}^{k_i}(g'_{x-k,y}+v_i\times k)

Let Gx,y=gx,y′−vi×xG_{x,y}=g'_{x,y}-v_i\times x. Then the equation can be written as:

gx,y=max⁡k=0ki(Gx−k,y)+vi×xg_{x,y}=\max_{k=0}^{k_i}(G_{x-k,y})+v_i\times x

This is transformed into a classic monotonic queue optimization form. Gx,yG_{x,y} can be calculated in O(1)O(1), so for a fixed yy, we can calculate in O(⌊Wwi⌋)O\left( \left\lfloor \dfrac{W}{w_i} \right\rfloor \right) time to obtain gx,yg_{x,y}. Therefore, the complexity of calculating all gx,yg_{x,y} is O(⌊Wwi⌋)×O(wi)=O(W)O\left( \left\lfloor \dfrac{W}{w_i} \right\rfloor \right)\times O(w_i)=O(W). Thus, the total complexity of this transition is reduced to O(nW)O(nW).

In the implementation, we need to enumerate yy first, so that the monotonic queue can be used to optimize the enumeration of xx. The monotonic queue stores x−kx-k, not kk. When using it, obtain the corresponding Gx−k,yG_{x-k,y} through f[last][q.front() * w[i] + y] - q.front() * v[i]. It is not difficult to see that x−k∈[x−ki,x]x-k\in [x - k_i,x], so when enumerating xx, we need to delete elements from the queue that are outside this range.

Reference Code
#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++) {
      // Clear the queue
      deque<int>().swap(q);
      for (int x = 0; x * w[i] + y <= W; x++) {
        // Remove elements outside the range
        while (!q.empty() && q.front() < x - k[i]) {
          q.pop_front();
        }
        // Ensure that the queue is monotonic
        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;
}

Exercises

Example Problem CF372C Watching Fireworks is Fun

Problem statement: There are nn positions in a town, and mm fireworks are to be launched. The launch time of the ii-th firework is denoted by tit_i, and its launch position by aia_i. If you are at position xx when the firework is launched, you gain bi−∣ai−x∣b_i-|a_i-x| happiness points.

Initially, you may be at any position. In each unit of time, you can move a distance of no more than dd units. You now need to maximize the happiness you can obtain.

Let fi,jf_{i,j} denote the maximum happiness obtainable when you are at position jj when the ii-th firework is launched.

Write the state transition equation: fi,j=max⁡{fi−1,k+bi−∣ai−j∣}f_{i,j}=\max\{f_{i-1,k}+b_i-|a_i-j|\}, where j−(ti−ti−1)×d≤k≤j+(ti−ti−1)×dj-(t_{i}-t_{i-1})\times d\le k\le j+(t_{i}-t_{i-1})\times d.

Try transforming it:

Since the max⁡\max contains a definite constant bib_i, we can take it outside.

fi,j=max⁡{fi−1,k+bi−∣ai−j∣}=max⁡{fi−1,k−∣ai−j∣}+bif_{i,j}=\max\{f_{i-1,k}+b_i-|a_i-j|\}=\max\{f_{i-1,k}-|a_i-j|\}+b_i

If the values of ii and jj are fixed, the value of ∣ai−j∣|a_i-j| is also fixed, so this part can also be taken outside.

Finally, the expression becomes:

fi,j=max⁡{fi−1,k−∣ai−j∣}+bi=max⁡{fi−1,k}−∣ai−j∣+bif_{i,j}=\max\{f_{i-1,k}-|a_i-j|\}+b_i=\max\{f_{i-1,k}\}-|a_i-j|+b_i

Next, consider monotonic queue optimization. Since the max⁡\max in the final expression depends only on the maximum value of a contiguous segment in the previous state, when calculating the state values for a new ii, we only need to construct a monotonic queue from the original fi−1f_{i-1} and maintain the monotonic queue so that max⁡{fi−1,k}\max\{f_{i-1,k}\} can be calculated in amortized O(1)O(1) time, after which the value of fi,jf_{i,j} can be calculated from the formula.

The total time complexity is O(nm)O(nm).

Reference Code
#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++) {  // Monotonic queue optimization is used here; the derivation is detailed above
      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;
}

This content comes from OI Wiki: Monotonic Queue/Stack Optimization. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.