Monotonic queue techniques
A monotonic deque keeps the minimum or maximum of a sliding window in amortised O(1) per element, and speeds up dynamic programming whose transitions look at a window.
Learning goals
- Maintain the maximum or minimum of a window with a deque of indices
- Explain why every index enters and leaves the deque once
- Reuse the deque as a queue with an O(1) maximum
- Speed up a windowed DP transition from O(n k) to O(n)
Prerequisites
To find the tallest person in every group of k neighbours, you do not need to rescan each group. If someone shorter stands before a taller newcomer, the shorter one can never be the tallest again, so forget them at once.
Sliding window minima
Keep a deque of indices whose values are **increasing** from front to back. For each new index i, pop from the back every index whose value is greater than or equal to a[i] (they are older and no smaller, so they can never be a window minimum again), then push i. Pop the front if it has left the window. The front is then the index of the window minimum.
In the preset "Window minimum" on 1 3 -1 -3 5 3 6 7 with k = 3, the minima are [-1, -3, -3, -3, 3, 3], found with 5 removals in total. The animation shows the deque after every step.
1static int[] slidingMin(int[] a, int k) {2Deque<Integer> dq = new ArrayDeque<>(); // indices; values increase from front to back3int[] out = new int[a.length - k + 1];4for (int i = 0; i < a.length; i++) {5while (!dq.isEmpty() && a[dq.peekLast()] >= a[i]) dq.pollLast(); // dominated: never the minimum again6dq.addLast(i);7if (dq.peekFirst() <= i - k) dq.pollFirst(); // left the window8if (i >= k - 1) out[i - k + 1] = a[dq.peekFirst()]; // the front is the window minimum9}10return out;11}
Monotonic deque: window minima, maxima and DP transitions in O(n)
Interactive · edit the inputa[] (the current window is under the pointer i)
deque of indices (largest value at the front)
window maximum
The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n).
1int[] slidingMax(int[] a, int k) {2Deque<Integer> dq = new ArrayDeque<>(); // indices; values decrease from front to back3int[] out = new int[a.length - k + 1];4for (int i = 0; i < a.length; i++) {5while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast(); // dominated: never the maximum again6dq.addLast(i);7if (dq.peekFirst() <= i - k) dq.pollFirst(); // left the window8if (i >= k - 1) out[i - k + 1] = a[dq.peekFirst()]; // the front is the window maximum9}10return out;11}12int maxScore(int[] a, int k) { // jump at most k steps right; sum of the values visited13int n = a.length; int[] dp = new int[n]; dp[0] = a[0];14Deque<Integer> dq = new ArrayDeque<>(); dq.add(0);15for (int i = 1; i < n; i++) {16while (dq.peekFirst() < i - k) dq.pollFirst();17dp[i] = a[i] + dp[dq.peekFirst()]; // best predecessor in the window18while (!dq.isEmpty() && dp[dq.peekLast()] <= dp[i]) dq.pollLast();19dq.addLast(i);20}21return dp[n - 1];22}
Live state
k
3
Execution trace (dry run table)Show 23 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 | The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n). |
| 2 | L6 | i=0 value=1 size=1 | Add index 0. |
| 3 | L5 | i=1 value=3 removed=0 | 3 ≥ 1: index 0 is older and no better, so it can never be the maximum of a window that contains 1. Remove it from the back. |
| 4 | L6 | i=1 value=3 size=1 | Add index 1. |
| 5 | L6 | i=2 value=-1 size=2 | Add index 2. |
| 6 | L8 | i=2 window=[0, 2] answer=3 | Window [0, 2]: the front of the deque is index 1, so the maximum is 3. |
| 7 | L6 | i=3 value=-3 size=3 | Add index 3. |
| 8 | L8 | i=3 window=[1, 3] answer=3 | Window [1, 3]: the front of the deque is index 1, so the maximum is 3. |
| 9 | L5 | i=4 value=5 removed=3 | 5 ≥ -3: index 3 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 10 | L5 | i=4 value=5 removed=2 | 5 ≥ -1: index 2 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 11 | L5 | i=4 value=5 removed=1 | 5 ≥ 3: index 1 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 12 | L6 | i=4 value=5 size=1 | Add index 4. |
| 13 | L8 | i=4 window=[2, 4] answer=5 | Window [2, 4]: the front of the deque is index 4, so the maximum is 5. |
| 14 | L6 | i=5 value=3 size=2 | Add index 5. |
| 15 | L8 | i=5 window=[3, 5] answer=5 | Window [3, 5]: the front of the deque is index 4, so the maximum is 5. |
| 16 | L5 | i=6 value=6 removed=5 | 6 ≥ 3: index 5 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 17 | L5 | i=6 value=6 removed=4 | 6 ≥ 5: index 4 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 18 | L6 | i=6 value=6 size=1 | Add index 6. |
| 19 | L8 | i=6 window=[4, 6] answer=6 | Window [4, 6]: the front of the deque is index 6, so the maximum is 6. |
| 20 | L5 | i=7 value=7 removed=6 | 7 ≥ 6: index 6 is older and no better, so it can never be the maximum of a window that contains 7. Remove it from the back. |
| 21 | L6 | i=7 value=7 size=1 | Add index 7. |
| 22 | L8 | i=7 window=[5, 7] answer=7 | Window [5, 7]: the front of the deque is index 7, so the maximum is 7. |
| 23 | L10 | return=[3, 3, 5, 5, 6, 7] deque pops=7 | Window maxima: [3, 3, 5, 5, 6, 7], with 7 removals in total (at most n = 8). |
Sliding window maxima
The maximum version is the mirror image: keep values **decreasing** from front to back, and pop from the back while a[back] <= a[i]. The preset "Window maximum" (1 3 -1 -3 5 3 6 7, k = 3) gives [3, 3, 5, 5, 6, 7] with 7 removals; "Decreasing input: the deque fills up" shows the worst case for the deque's size, since nothing is ever popped from the back and only the window edge removes elements.
Store indices, not values: the index tells you when an element leaves the window, and the value is one array read away. Using <= (not <) when popping keeps the deque free of equal values, which keeps it shorter; either choice is correct.
1static int[] slidingMax(int[] a, int k) {2Deque<Integer> dq = new ArrayDeque<>(); // indices; values decrease from front to back3int[] out = new int[a.length - k + 1];4for (int i = 0; i < a.length; i++) {5while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast(); // dominated: never the maximum again6dq.addLast(i);7if (dq.peekFirst() <= i - k) dq.pollFirst(); // left the window8if (i >= k - 1) out[i - k + 1] = a[dq.peekFirst()];9}10return out;11}
Monotonic deque: window minima, maxima and DP transitions in O(n)
Interactive · edit the inputa[] (the current window is under the pointer i)
deque of indices (largest value at the front)
window maximum
The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n).
1int[] slidingMax(int[] a, int k) {2Deque<Integer> dq = new ArrayDeque<>(); // indices; values decrease from front to back3int[] out = new int[a.length - k + 1];4for (int i = 0; i < a.length; i++) {5while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast(); // dominated: never the maximum again6dq.addLast(i);7if (dq.peekFirst() <= i - k) dq.pollFirst(); // left the window8if (i >= k - 1) out[i - k + 1] = a[dq.peekFirst()]; // the front is the window maximum9}10return out;11}12int maxScore(int[] a, int k) { // jump at most k steps right; sum of the values visited13int n = a.length; int[] dp = new int[n]; dp[0] = a[0];14Deque<Integer> dq = new ArrayDeque<>(); dq.add(0);15for (int i = 1; i < n; i++) {16while (dq.peekFirst() < i - k) dq.pollFirst();17dp[i] = a[i] + dp[dq.peekFirst()]; // best predecessor in the window18while (!dq.isEmpty() && dp[dq.peekLast()] <= dp[i]) dq.pollLast();19dq.addLast(i);20}21return dp[n - 1];22}
Live state
k
3
Execution trace (dry run table)Show 23 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 | The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n). |
| 2 | L6 | i=0 value=1 size=1 | Add index 0. |
| 3 | L5 | i=1 value=3 removed=0 | 3 ≥ 1: index 0 is older and no better, so it can never be the maximum of a window that contains 1. Remove it from the back. |
| 4 | L6 | i=1 value=3 size=1 | Add index 1. |
| 5 | L6 | i=2 value=-1 size=2 | Add index 2. |
| 6 | L8 | i=2 window=[0, 2] answer=3 | Window [0, 2]: the front of the deque is index 1, so the maximum is 3. |
| 7 | L6 | i=3 value=-3 size=3 | Add index 3. |
| 8 | L8 | i=3 window=[1, 3] answer=3 | Window [1, 3]: the front of the deque is index 1, so the maximum is 3. |
| 9 | L5 | i=4 value=5 removed=3 | 5 ≥ -3: index 3 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 10 | L5 | i=4 value=5 removed=2 | 5 ≥ -1: index 2 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 11 | L5 | i=4 value=5 removed=1 | 5 ≥ 3: index 1 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 12 | L6 | i=4 value=5 size=1 | Add index 4. |
| 13 | L8 | i=4 window=[2, 4] answer=5 | Window [2, 4]: the front of the deque is index 4, so the maximum is 5. |
| 14 | L6 | i=5 value=3 size=2 | Add index 5. |
| 15 | L8 | i=5 window=[3, 5] answer=5 | Window [3, 5]: the front of the deque is index 4, so the maximum is 5. |
| 16 | L5 | i=6 value=6 removed=5 | 6 ≥ 3: index 5 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 17 | L5 | i=6 value=6 removed=4 | 6 ≥ 5: index 4 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 18 | L6 | i=6 value=6 size=1 | Add index 6. |
| 19 | L8 | i=6 window=[4, 6] answer=6 | Window [4, 6]: the front of the deque is index 6, so the maximum is 6. |
| 20 | L5 | i=7 value=7 removed=6 | 7 ≥ 6: index 6 is older and no better, so it can never be the maximum of a window that contains 7. Remove it from the back. |
| 21 | L6 | i=7 value=7 size=1 | Add index 7. |
| 22 | L8 | i=7 window=[5, 7] answer=7 | Window [5, 7]: the front of the deque is index 7, so the maximum is 7. |
| 23 | L10 | return=[3, 3, 5, 5, 6, 7] deque pops=7 | Window maxima: [3, 3, 5, 5, 6, 7], with 7 removals in total (at most n = 8). |
O(n) deque processing
The whole pass is O(n), although there is a loop inside the loop: every index is pushed once and popped at most once, so the total number of pops over the run is at most n. This is the same amortised argument as for the monotonic stack. The deque never holds more than k indices, so the extra space is O(k).
The idea also yields a **queue with maximum**: push and pop work like a normal queue, and max() reads the front of a second deque of candidates. Each operation is amortised O(1). The animation's counter 'deque pops' totals all removals, which never exceed the number of elements.
1class MaxQueue {2private final Deque<Integer> items = new ArrayDeque<>();3private final Deque<Integer> maxima = new ArrayDeque<>(); // candidates, decreasing from front to back4void push(int x) {5items.addLast(x);6while (!maxima.isEmpty() && maxima.peekLast() < x) maxima.pollLast(); // smaller values are dominated7maxima.addLast(x);8}9int pop() { // removes the oldest item10int x = items.pollFirst();11if (maxima.peekFirst() == x) maxima.pollFirst(); // it was the current maximum12return x;13}14int max() { return maxima.peekFirst(); }15boolean isEmpty() { return items.isEmpty(); }16}
Monotonic deque: window minima, maxima and DP transitions in O(n)
Interactive · edit the inputa[] (the current window is under the pointer i)
deque of indices (largest value at the front)
window maximum
The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n).
1int[] slidingMax(int[] a, int k) {2Deque<Integer> dq = new ArrayDeque<>(); // indices; values decrease from front to back3int[] out = new int[a.length - k + 1];4for (int i = 0; i < a.length; i++) {5while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast(); // dominated: never the maximum again6dq.addLast(i);7if (dq.peekFirst() <= i - k) dq.pollFirst(); // left the window8if (i >= k - 1) out[i - k + 1] = a[dq.peekFirst()]; // the front is the window maximum9}10return out;11}12int maxScore(int[] a, int k) { // jump at most k steps right; sum of the values visited13int n = a.length; int[] dp = new int[n]; dp[0] = a[0];14Deque<Integer> dq = new ArrayDeque<>(); dq.add(0);15for (int i = 1; i < n; i++) {16while (dq.peekFirst() < i - k) dq.pollFirst();17dp[i] = a[i] + dp[dq.peekFirst()]; // best predecessor in the window18while (!dq.isEmpty() && dp[dq.peekLast()] <= dp[i]) dq.pollLast();19dq.addLast(i);20}21return dp[n - 1];22}
Live state
k
3
Execution trace (dry run table)Show 23 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 | The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n). |
| 2 | L6 | i=0 value=1 size=1 | Add index 0. |
| 3 | L5 | i=1 value=3 removed=0 | 3 ≥ 1: index 0 is older and no better, so it can never be the maximum of a window that contains 1. Remove it from the back. |
| 4 | L6 | i=1 value=3 size=1 | Add index 1. |
| 5 | L6 | i=2 value=-1 size=2 | Add index 2. |
| 6 | L8 | i=2 window=[0, 2] answer=3 | Window [0, 2]: the front of the deque is index 1, so the maximum is 3. |
| 7 | L6 | i=3 value=-3 size=3 | Add index 3. |
| 8 | L8 | i=3 window=[1, 3] answer=3 | Window [1, 3]: the front of the deque is index 1, so the maximum is 3. |
| 9 | L5 | i=4 value=5 removed=3 | 5 ≥ -3: index 3 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 10 | L5 | i=4 value=5 removed=2 | 5 ≥ -1: index 2 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 11 | L5 | i=4 value=5 removed=1 | 5 ≥ 3: index 1 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 12 | L6 | i=4 value=5 size=1 | Add index 4. |
| 13 | L8 | i=4 window=[2, 4] answer=5 | Window [2, 4]: the front of the deque is index 4, so the maximum is 5. |
| 14 | L6 | i=5 value=3 size=2 | Add index 5. |
| 15 | L8 | i=5 window=[3, 5] answer=5 | Window [3, 5]: the front of the deque is index 4, so the maximum is 5. |
| 16 | L5 | i=6 value=6 removed=5 | 6 ≥ 3: index 5 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 17 | L5 | i=6 value=6 removed=4 | 6 ≥ 5: index 4 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 18 | L6 | i=6 value=6 size=1 | Add index 6. |
| 19 | L8 | i=6 window=[4, 6] answer=6 | Window [4, 6]: the front of the deque is index 6, so the maximum is 6. |
| 20 | L5 | i=7 value=7 removed=6 | 7 ≥ 6: index 6 is older and no better, so it can never be the maximum of a window that contains 7. Remove it from the back. |
| 21 | L6 | i=7 value=7 size=1 | Add index 7. |
| 22 | L8 | i=7 window=[5, 7] answer=7 | Window [5, 7]: the front of the deque is index 7, so the maximum is 7. |
| 23 | L10 | return=[3, 3, 5, 5, 6, 7] deque pops=7 | Window maxima: [3, 3, 5, 5, 6, 7], with 7 removals in total (at most n = 8). |
DP transitions
Many DPs have the form dp[i] = a[i] + max(dp[j]) over a window j in [i - k, i - 1]. Computed directly this is O(n k). The window slides to the right as i grows, and the quantity needed is exactly a window maximum of the dp values, so a monotonic deque gives it in O(1) amortised and the DP runs in O(n). The only change from the plain sliding maximum is that the values are computed on the fly: add dp[i] to the deque right after computing it.
The preset "DP: jump game with scores" uses 1 -1 -2 4 -7 3 with jumps of at most k = 2: the best total score from index 0 to the last index is 7 (visiting the values 1, -1, 4 and 3). The animation shows dp and the deque side by side.
1static int maxScore(int[] a, int k) { // jump at most k steps right; sum of the values visited, ending at n - 12int n = a.length;3int[] dp = new int[n];4dp[0] = a[0];5Deque<Integer> dq = new ArrayDeque<>();6dq.add(0);7for (int i = 1; i < n; i++) {8while (dq.peekFirst() < i - k) dq.pollFirst(); // too far to jump from9dp[i] = a[i] + dp[dq.peekFirst()]; // the best predecessor in the window10while (!dq.isEmpty() && dp[dq.peekLast()] <= dp[i]) dq.pollLast();11dq.addLast(i);12}13return dp[n - 1];14}
Monotonic deque: window minima, maxima and DP transitions in O(n)
Interactive · edit the inputa[] (the current window is under the pointer i)
deque of indices (largest value at the front)
window maximum
The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n).
1int[] slidingMax(int[] a, int k) {2Deque<Integer> dq = new ArrayDeque<>(); // indices; values decrease from front to back3int[] out = new int[a.length - k + 1];4for (int i = 0; i < a.length; i++) {5while (!dq.isEmpty() && a[dq.peekLast()] <= a[i]) dq.pollLast(); // dominated: never the maximum again6dq.addLast(i);7if (dq.peekFirst() <= i - k) dq.pollFirst(); // left the window8if (i >= k - 1) out[i - k + 1] = a[dq.peekFirst()]; // the front is the window maximum9}10return out;11}12int maxScore(int[] a, int k) { // jump at most k steps right; sum of the values visited13int n = a.length; int[] dp = new int[n]; dp[0] = a[0];14Deque<Integer> dq = new ArrayDeque<>(); dq.add(0);15for (int i = 1; i < n; i++) {16while (dq.peekFirst() < i - k) dq.pollFirst();17dp[i] = a[i] + dp[dq.peekFirst()]; // best predecessor in the window18while (!dq.isEmpty() && dp[dq.peekLast()] <= dp[i]) dq.pollLast();19dq.addLast(i);20}21return dp[n - 1];22}
Live state
k
3
Execution trace (dry run table)Show 23 stepsHide
| Step | Line | State | What happens |
|---|---|---|---|
| 1 | L2 | k=3 | The deque keeps indices of the window whose values are decreasing from front to back, so the front is always the maximum. A new value removes every back element that it makes useless, and the front is removed when it leaves the window. Every index enters and leaves once: O(n). |
| 2 | L6 | i=0 value=1 size=1 | Add index 0. |
| 3 | L5 | i=1 value=3 removed=0 | 3 ≥ 1: index 0 is older and no better, so it can never be the maximum of a window that contains 1. Remove it from the back. |
| 4 | L6 | i=1 value=3 size=1 | Add index 1. |
| 5 | L6 | i=2 value=-1 size=2 | Add index 2. |
| 6 | L8 | i=2 window=[0, 2] answer=3 | Window [0, 2]: the front of the deque is index 1, so the maximum is 3. |
| 7 | L6 | i=3 value=-3 size=3 | Add index 3. |
| 8 | L8 | i=3 window=[1, 3] answer=3 | Window [1, 3]: the front of the deque is index 1, so the maximum is 3. |
| 9 | L5 | i=4 value=5 removed=3 | 5 ≥ -3: index 3 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 10 | L5 | i=4 value=5 removed=2 | 5 ≥ -1: index 2 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 11 | L5 | i=4 value=5 removed=1 | 5 ≥ 3: index 1 is older and no better, so it can never be the maximum of a window that contains 4. Remove it from the back. |
| 12 | L6 | i=4 value=5 size=1 | Add index 4. |
| 13 | L8 | i=4 window=[2, 4] answer=5 | Window [2, 4]: the front of the deque is index 4, so the maximum is 5. |
| 14 | L6 | i=5 value=3 size=2 | Add index 5. |
| 15 | L8 | i=5 window=[3, 5] answer=5 | Window [3, 5]: the front of the deque is index 4, so the maximum is 5. |
| 16 | L5 | i=6 value=6 removed=5 | 6 ≥ 3: index 5 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 17 | L5 | i=6 value=6 removed=4 | 6 ≥ 5: index 4 is older and no better, so it can never be the maximum of a window that contains 6. Remove it from the back. |
| 18 | L6 | i=6 value=6 size=1 | Add index 6. |
| 19 | L8 | i=6 window=[4, 6] answer=6 | Window [4, 6]: the front of the deque is index 6, so the maximum is 6. |
| 20 | L5 | i=7 value=7 removed=6 | 7 ≥ 6: index 6 is older and no better, so it can never be the maximum of a window that contains 7. Remove it from the back. |
| 21 | L6 | i=7 value=7 size=1 | Add index 7. |
| 22 | L8 | i=7 window=[5, 7] answer=7 | Window [5, 7]: the front of the deque is index 7, so the maximum is 7. |
| 23 | L10 | return=[3, 3, 5, 5, 6, 7] deque pops=7 | Window maxima: [3, 3, 5, 5, 6, 7], with 7 removals in total (at most n = 8). |
Practice ladder
Now try it yourself
Easy
- Sliding Window Maximum (brute force first)
Medium
- Sliding Window Maximum
- Jump Game VI
- Shortest Subarray with Sum at Least K
Advanced
- Constrained Subsequence Sum
- Max Value of Equation
- Longest Continuous Subarray With Absolute Diff <= Limit
Key takeaways
- Keep indices in a deque with monotone values: pop dominated indices from the back, expired ones from the front.
- Every index enters and leaves once, so the whole pass is O(n).
- A DP that takes a window maximum of its own earlier values becomes O(n) with the same deque.