Keep a window whose ends only move forward. Each pointer moves at most times, so the scan is times the cost of updating the window.
The template
for (int l = 0, r = 0; r < n; r++) {
add(a[r]);
while (!valid()) remove(a[l++]);
// [l, r] is the longest valid window ending at r
best = max(best, r - l + 1);
cnt += r - l + 1; // number of valid windows ending at r
}- Shortest valid window:
while (valid()) { best = min(best, r - l + 1); remove(a[l++]); }. - Exactly = at most − at most (distinct values, odd numbers, sum with non-negative values).
- Requirement: validity is monotone. If is valid, every sub-window is valid (or every super-window, for the shortest-window form).
Proof: the two-pointer template is correct and
Correctness. By induction on : suppose before processing , is the smallest index making valid (or if no such window existed). After
add(a[r]), may become invalid; thewhileshrinks one step at a time, and since validity of windows ending at is monotone in (sub-windows of an invalid-because-too-wide window stay invalid only until shrinking enough — formally, the monotonicity hypothesis says “valid at valid at every up to ”), the loop stops at exactly the smallest valid , or at if even is invalid. So the invariant is maintained, andr-l+1at each step is the longest valid window ending at . Complexity. only increases and is bounded by , so the total number ofwhile-loop iterations across all is at most ; eachadd/removecall is charged once per unit of pointer movement, giving total calls.
| Condition | Monotone? |
|---|---|
| sum , values | yes |
| sum with negatives | no: prefix sums + monotonic deque, or BIT |
| at most distinct | yes |
| yes, with two monotonic deques | |
| , , | yes (the aggregate only moves one way as the window grows); use the two-stack queue for gcd |
| contains every required item | yes (shortest-window form) |
| sum exactly | not in general: hash map of prefix sums |
| Window aggregate | Structure |
|---|---|
| sum, count | a running variable |
| distinct count | a frequency map plus a counter |
| maximum / minimum | monotonic deque, amortized |
| both max and min | two deques |
| -th smallest | PBDS or two heaps |
| anything non-invertible (gcd, matrix product, …) | the two-stack queue (SWAG) below |
Other two-pointer shapes
- Opposite ends of a sorted array: two-sum, counting pairs with , container with most water. Three-sum: fix one element, two pointers on the rest, . General -sum: recurse by fixing one element and reducing to -sum, down to two-pointers at — nested fixed loops plus one two-pointer scan gives , and this is believed optimal for (3SUM-hardness, see fine-grained reductions).
- Two sorted sequences: merge, intersect, count pairs with .
- The pointer is a monotone function of : whenever the optimal for is non-decreasing in (it often is for convex costs), a pointer replaces a binary search. This is the same monotonicity that powers DP optimisations.
- Fast and slow pointers: cycle detection in functional graphs (Floyd/Brent), middle of a list.
Monotonic deque: sliding window min/max
deque<int> dq; // indices; values decreasing front to back
for (int i = 0; i < n; i++) {
while (!dq.empty() && a[dq.back()] <= a[i]) dq.pop_back();
dq.push_back(i);
if (dq.front() <= i - k) dq.pop_front();
if (i >= k - 1) out.push_back(a[dq.front()]); // max of a[i-k+1..i]
}An element dominated by a newer, larger one can never be the maximum again, so popping it is safe. Each index is pushed and popped once, total.
Proof: popping is safe and the deque is
Safety. If
a[j] <= a[i]with , then for any future window containing : since and the window’s right edge only grows, once ‘s right edge reaches , if is still in then is too (as right edge and the window is contiguous, provided hasn’t already fallen out the left — but entered after so it falls out no earlier). So every window containing from this point on also contains , and means is at least as good a maximum candidate; can safely be discarded. Monotonicity is preserved: the deque is maintained decreasing front-to-back by construction (thewhilepops everything before pushing ), sodq.front()is always the maximum of the current window. Complexity. Each index is pushed exactly once; it is popped at most once (from the back, when dominated, or from the front, when it falls outside the window), so the total work across all iterations is .
Uses:
- DP over a window: (Monotone Queue Optimization); bounded knapsack in by running a window per residue class mod weight.
- Shortest subarray with sum , negatives allowed: a deque of prefix indices with increasing ; pop the front while , recording the length.
- Max sum with length in : a deque of the minimum over .
- 2D sliding window max ( windows): run the 1D deque along rows, then along columns of the result. .
Two-stack queue (SWAG)
A queue built from two stacks supports any associative operation, even non-invertible ones (gcd, min, matrix product, function composition), in amortized per push, pop and query. Each stack stores the running aggregate. The back stack aggregates bottom to top; the front stack is filled by moving everything over when it’s empty and aggregates in reverse.
template<class T, class Op> struct SWAG { // queue with fold of all elements, op associative
Op op; T id;
vector<T> fv, fa, bv; T ba; // front values/aggregates, back values/aggregate
SWAG(Op o, T e) : op(o), id(e), ba(e) {}
void push(T x) { bv.push_back(x); ba = op(ba, x); }
void pop() {
if (fv.empty()) { // move back stack to front
while (!bv.empty()) { T x = bv.back(); bv.pop_back(); fv.push_back(x); fa.push_back(fa.empty() ? x : op(x, fa.back())); }
ba = id;
}
fv.pop_back(); fa.pop_back();
}
T query() const { return op(fa.empty() ? id : fa.back(), ba); } // fold front-to-back
};This is what you use for “two pointers with gcd / with matrix composition”, and for “Mo’s algorithm needs removals but the aggregate can’t remove” when the queries form a sliding window.
Proof: the two-stack queue is correct and amortized
Correctness. The queue’s elements, in FIFO order, are exactly
fvreversed followed bybv(front-to-back: oldest-pushed-and-not-yet-popped first).fa.back()holds the fold of all offvin queue order (built incrementally asop(x, fa.back())when moving an element frombvtofv, which reversesbv’s stack order back into queue order), andbaholds the fold ofbvin push order, which is already queue order sincebvonly ever receives pushes at the back. Soquery()foldingfa.back()thenbagives the fold of the whole queue in order — correct even for non-commutativeop, since both halves preserve queue order internally and are combined in the right order. Amortized cost. Each element is pushed tobvonce (), later moved frombvtofvat most once (the move happens only whenfvis empty, and moves every element ofbvacross in one pass), and popped fromfvonce. Charging the move’s cost to the elements moved, each element participates in at most one push, one move, and one pop over its lifetime, so total work over operations is , i.e. amortized per operation.
Min stack: store (value, min so far) per element, per op since a stack’s minimum only ever depends on a prefix of it. Min queue: two min stacks (in receives pushes, out serves pops; when out empties, move everything from in across, reversing order; the queue’s minimum is the smaller of the two stacks’ minimums) — each element moves from in to out exactly once, so amortized. For a sliding window specifically the monotonic deque above is simpler and has a better constant, but it needs to know which value is leaving (FIFO order only); the two-stack version works for any push/pop sequence.
| Situation | Structure |
|---|---|
| sliding window over an array | monotonic deque |
| arbitrary push/pop, need the minimum | two-stack minimum queue |
| LIFO only | minimum stack |
| need arbitrary deletion | multiset, |
The two-stack minimum queue is what makes Mo’s algorithm with rollback work for a non-invertible aggregate (min needs both add and remove; when remove is impossible, this stack-based structure with undo substitutes), and it’s the same trick behind simulating a full min-deque with two stacks.
When it fails
If the left end sometimes needs to move back, two pointers don’t apply. Alternatives:
- binary search on the window length, when validity is monotone in length;
- a segment tree or sparse table to query arbitrary windows;
- divide and conquer on the array: count windows that cross the midpoint, and solve each half recursively;
- Mo’s algorithm for offline arbitrary ranges.