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 items. Each item has weight , value , and quantity . You have a knapsack with weight capacity . 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 denote the maximum value obtained by putting the first items into a knapsack with capacity . The naive transition equation is
The time complexity is .
Consider optimizing the transition of . For convenience, let , where . Then the transition equation can be written as:
Let . Then the equation can be written as:
This is transformed into a classic monotonic queue optimization form. can be calculated in , so for a fixed , we can calculate in time to obtain . Therefore, the complexity of calculating all is . Thus, the total complexity of this transition is reduced to .
In the implementation, we need to enumerate first, so that the monotonic queue can be used to optimize the enumeration of . The monotonic queue stores , not . When using it, obtain the corresponding through f[last][q.front() * w[i] + y] - q.front() * v[i]. It is not difficult to see that , so when enumerating , 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 positions in a town, and fireworks are to be launched. The launch time of the -th firework is denoted by , and its launch position by . If you are at position when the firework is launched, you gain happiness points.
Initially, you may be at any position. In each unit of time, you can move a distance of no more than units. You now need to maximize the happiness you can obtain.
Let denote the maximum happiness obtainable when you are at position when the -th firework is launched.
Write the state transition equation: , where .
Try transforming it:
Since the contains a definite constant , we can take it outside.
If the values of and are fixed, the value of is also fixed, so this part can also be taken outside.
Finally, the expression becomes:
Next, consider monotonic queue optimization. Since the 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 , we only need to construct a monotonic queue from the original and maintain the monotonic queue so that can be calculated in amortized time, after which the value of can be calculated from the formula.
The total time complexity is .
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;
}