The monotone deque
A sum can lose an element in O(1); a maximum cannot. A deque that keeps only the elements that could still become the maximum can, and each element costs O(1) over the whole scan.
Why the maximum is the hard case
Sums and counts undo cleanly: subtract what leaves. A maximum does not. When the largest element leaves the window, the new maximum could be anywhere inside, and scanning for it makes the window O(nk). A heap fixes that in O(log n) per step, with lazy deletion of elements that have left. A monotone deque does it in O(1) amortised.
The observation behind the deque: if an element arrives that is at least as large as an earlier one, the earlier one can never be the maximum again. It is no larger and it leaves the window first. So it can be thrown away immediately. What remains, from front to back, is a list of indices with strictly decreasing values, and the front is the maximum of the window.
The maximum of every window of length 3 in [1, 3, −1, −3, 5, 3, 6, 7].
-
Step 1 / 8nums10i31-12-3354356677deque (values)10maximanone yet0k3
Index 0 (value 1). The first window is not complete yet.
-
Step 2 / 8nums1031i-12-3354356677deque (values)30maximanone yet0k3
Index 1 (value 3). It beats 1, which can never be a maximum again, so they leave from the back. The first window is not complete yet.
-
Step 3 / 8nums1031-12i-3354356677deque (values)30-11maxima30k3
Index 2 (value -1). The front, 3, is the maximum of window [0, 2].
-
Step 4 / 8nums1031-12-33i54356677deque (values)30-11-32maxima3031k3
Index 3 (value -3). The front, 3, is the maximum of window [1, 3].
-
Step 5 / 8nums1031-12-3354i356677deque (values)50maxima303152k3
Index 4 (value 5). It beats -3, -1, 3, which can never be a maximum again, so they leave from the back. The front, 5, is the maximum of window [2, 4].
-
Step 6 / 8nums1031-12-335435i6677deque (values)5031maxima30315253k3
Index 5 (value 3). The front, 5, is the maximum of window [3, 5].
-
Step 7 / 8nums1031-12-33543566i77deque (values)60maxima3031525364k3
Index 6 (value 6). It beats 3, 5, which can never be a maximum again, so they leave from the back. The front, 6, is the maximum of window [4, 6].
-
Step 8 / 8nums1031-12-3354356677ideque (values)70maxima303152536475k3
Index 7 (value 7). It beats 6, which can never be a maximum again, so they leave from the back. The front, 7, is the maximum of window [5, 7]. Each index entered and left the deque at most once: O(n) in all.
The code
The deque stores indices, not values. That is how the front can be recognised as having left the window:
its index is less than hi − k + 1.
from collections import deque
def window_maximum(nums, k):
dq = deque() # indices; their values fall from front to back
out = []
for i, x in enumerate(nums):
while dq and nums[dq[-1]] <= x: # dominated: leaves sooner and is no larger
dq.pop()
dq.append(i)
if dq[0] <= i - k: # the front has left the window
dq.popleft()
if i >= k - 1:
out.append(nums[dq[0]])
return out#include <deque>
#include <vector>
std::vector<int> solve(const std::vector<int>& nums, int k) {
std::deque<int> dq; // indices; values fall from front to back
std::vector<int> out;
for (int i = 0; i < static_cast<int>(nums.size()); ++i) {
while (!dq.empty() && nums[dq.back()] <= nums[i]) dq.pop_back();
dq.push_back(i);
if (dq.front() <= i - k) dq.pop_front(); // the front has left the window
if (i >= k - 1) out.push_back(nums[dq.front()]);
}
return out;
}class Solution {
public int[] windowMaximum(int[] nums, int k) {
int n = nums.length;
int[] dq = new int[n]; // indices in dq[head..tail); values fall front to back
int head = 0, tail = 0;
int[] out = new int[n - k + 1];
for (int i = 0; i < n; i++) {
while (tail > head && nums[dq[tail - 1]] <= nums[i]) tail--;
dq[tail++] = i;
if (dq[head] <= i - k) head++; // the front has left the window
if (i >= k - 1) out[i - k + 1] = nums[dq[head]];
}
return out;
}
}function windowMaximum(nums, k) {
const dq = new Array(nums.length); // a deque of indices in a plain array: dq[head..tail)
let head = 0, tail = 0;
const out = [];
for (let i = 0; i < nums.length; i++) {
while (tail > head && nums[dq[tail - 1]] <= nums[i]) tail--; // dominated candidates leave
dq[tail++] = i;
if (dq[head] <= i - k) head++; // the front has left the window
if (i >= k - 1) out.push(nums[dq[head]]);
}
return out;
}use std::collections::VecDeque;
fn solve(nums: &[i64], k: i64) -> Vec<i64> {
let k = k as usize;
let mut dq: VecDeque<usize> = VecDeque::new(); // indices; values fall front to back
let mut out = Vec::new();
for i in 0..nums.len() {
while let Some(&j) = dq.back() {
if nums[j] > nums[i] {
break;
}
dq.pop_back();
}
dq.push_back(i);
if dq[0] + k <= i {
dq.pop_front(); // the front has left the window
}
if i + 1 >= k {
out.push(nums[dq[0]]);
}
}
out
}import Data.Array
import qualified Data.Sequence as Seq
import Data.Sequence (ViewL(..), ViewR(..), viewl, viewr, (|>))
solve :: [Int] -> Int -> [Int]
solve xs k = go 0 Seq.empty
where
n = length xs
arr = listArray (0, n - 1) xs
-- the deque holds indices; their values fall from front to back
go i dq
| i == n = []
| otherwise =
let dropBack d = case viewr d of
rest :> j | arr ! j <= arr ! i -> dropBack rest
_ -> d
pushed = dropBack dq |> i
live = case viewl pushed of
j :< rest | j <= i - k -> rest -- the front has left the window
_ -> pushed
front = case viewl live of
j :< _ -> arr ! j
EmptyL -> 0
in if i >= k - 1 then front : go (i + 1) live else go (i + 1) live
The same code is the reference solution to Maximum of every window, where it is compiled and run against the tests in all six languages.
Each index is pushed once and popped at most once, from the back as dominated or from the front as expired. The inner while loop can run long on one step, but never more than n times in total.
Inside a variable window
The deque is a state like any other, so it plugs into the variable template. "The largest and smallest
elements differ by at most a limit" survives shrinking, so it is a longest-window question; its test needs the
maximum and the minimum, so keep two deques, one decreasing and one increasing. When lo passes a
deque's front index, pop it.
The longest run of [10, 1, 2, 4, 7, 2] whose spread is at most 5.
-
Step 1 / 6nums100lo/hi1122437425max deque[10]min deque[10]best1
10 enters. Max 10, min 10, spread 0. Longest so far: 1.
-
Step 2 / 6nums10011lo/hi22437425max deque[1]min deque[1]best1
1 enters. Max 10, min 1, spread 9. Over 5, so move lo to 1; the spread is now 0.
-
Step 3 / 6nums10011lo22hi437425max deque[2]min deque[1, 2]best2
2 enters. Max 2, min 1, spread 1. Longest so far: 2.
-
Step 4 / 6nums10011lo2243hi7425max deque[4]min deque[1, 2, 4]best3
4 enters. Max 4, min 1, spread 3. Longest so far: 3.
-
Step 5 / 6nums1001122lo4374hi25max deque[7]min deque[2, 4, 7]best3
7 enters. Max 7, min 1, spread 6. Over 5, so move lo to 2; the spread is now 5.
-
Step 6 / 6nums1001122lo437425himax deque[7, 2]min deque[2]best4
2 enters. Max 7, min 2, spread 5. Longest so far: 4.