Sliding Window Maximum
Problem Overview
| Attribute | Value |
|---|---|
| Difficulty | Medium |
| Category | Sliding Window |
| Time Limit | 1 second |
| Key Technique | Monotonic Deque |
| CSES Link | Sliding Window Maximum |
Learning Goals
After solving this problem, you will be able to:
- Understand why a monotonic deque achieves O(n) time complexity
- Implement a decreasing monotonic deque for maximum tracking
- Recognize sliding window problems that benefit from deque optimization
- Apply the same pattern to minimum queries by reversing the comparison
Problem Statement
Problem: Given an array of n integers and a window size k, find the maximum element in each sliding window of size k as it moves from left to right.
Input:
- Line 1: Two integers n (array size) and k (window size)
- Line 2: n space-separated integers
Output:
- n-k+1 integers: the maximum of each window
Constraints:
- 1 <= k <= n <= 2 x 10^5
- 1 <= arr[i] <= 10^9
Example
Input:
8 3
2 1 4 5 3 4 1 2
Output:
4 5 5 5 4 4
Explanation:
- Window [2,1,4] -> max = 4
- Window [1,4,5] -> max = 5
- Window [4,5,3] -> max = 5
- Window [5,3,4] -> max = 5
- Window [3,4,1] -> max = 4
- Window [4,1,2] -> max = 4
Intuition: How to Think About This Problem
Pattern Recognition
Key Question: When the window slides, how can we efficiently update the maximum without rechecking all k elements?
The core insight is that many elements in the window can never become the maximum. If we see a larger element, all smaller elements before it (still in the window) become irrelevant because the larger element will outlast them or we will find an even larger one.
Breaking Down the Problem
- What are we looking for? The maximum value in each window position
- What information do we have? Array values and their indices (positions)
- What is the relationship? We need to track “candidates” that could be maximum as the window slides
The Monotonic Deque Idea
Think of it like a queue of “potential champions”:
- New elements enter from the right
- Before entering, they “knock out” all weaker elements (smaller values)
- The strongest (largest) is always at the front
- Old elements that leave the window are removed from the front
This maintains a decreasing sequence from front to back.
Solution 1: Brute Force
Idea
For each window position, scan all k elements to find the maximum.
Code
def sliding_max_brute(arr, k):
n = len(arr)
result = []
for i in range(n - k + 1):
result.append(max(arr[i:i+k]))
return result
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(n*k) | k comparisons for each of n-k+1 windows |
| Space | O(1) | No extra space beyond output |
Why This Works (But Is Slow)
Correctness is obvious - we check every element. But with n = 200,000 and k = 100,000, this is 10^10 operations, far too slow.
Solution 2: Optimal - Monotonic Deque
Key Insight
The Trick: Maintain a deque of indices where values are in decreasing order. The front always holds the maximum for the current window.
Why Store Indices?
We store indices (not values) because:
- We can look up the value anytime via
arr[index] - We need to know when an element leaves the window (when
index <= i - k)
Algorithm
- Process each element at index i:
- Remove expired: Pop from front if index is outside window
- Remove smaller: Pop from back while back element <= current (they can never be max)
- Add current: Push current index to back
- Record answer: If window is complete (i >= k-1), front is the max
Dry Run Example
Input: arr = [2, 1, 4, 5, 3], k = 3
Deque stores INDICES. Values shown for clarity: deque -> [idx:val, ...]
i=0, arr[0]=2:
Deque empty, add 0
Deque: [0:2]
Window incomplete (need k=3 elements)
i=1, arr[1]=1:
arr[1]=1 < arr[0]=2, just add 1
Deque: [0:2, 1:1]
Window incomplete
i=2, arr[2]=4:
arr[2]=4 > arr[1]=1, pop 1
arr[2]=4 > arr[0]=2, pop 0
Add 2
Deque: [2:4]
Window [0,1,2] complete -> max = arr[2] = 4
i=3, arr[3]=5:
arr[3]=5 > arr[2]=4, pop 2
Add 3
Deque: [3:5]
Front index 3 > 3-3=0, still valid
Window [1,2,3] -> max = arr[3] = 5
i=4, arr[4]=3:
arr[4]=3 < arr[3]=5, just add 4
Deque: [3:5, 4:3]
Front index 3 > 4-3=1, still valid
Window [2,3,4] -> max = arr[3] = 5
Output: [4, 5, 5]
Visual Diagram
Array: [2, 1, 4, 5, 3] k=3
Window 1: [2, 1, 4]
Deque maintains: [4] (index 2)
Max = 4
Window 2: [1, 4, 5]
5 removes 4, Deque: [5] (index 3)
Max = 5
Window 3: [4, 5, 3]
3 < 5, Deque: [5, 3] (indices 3, 4)
Max = 5 (front)
Code
Python:
from collections import deque
def sliding_max(arr, k):
"""
Find maximum in each sliding window of size k.
Time: O(n) - each element pushed/popped at most once
Space: O(k) - deque holds at most k indices
"""
n = len(arr)
dq = deque() # stores indices
result = []
for i in range(n):
# Remove indices outside current window
while dq and dq[0] <= i - k:
dq.popleft()
# Remove indices of elements smaller than current
# They can never be the maximum while current exists
while dq and arr[dq[-1]] <= arr[i]:
dq.pop()
dq.append(i)
# Window is complete when i >= k-1
if i >= k - 1:
result.append(arr[dq[0]])
return result
# CSES I/O wrapper
def main():
import sys
input_data = sys.stdin.read().split()
n, k = int(input_data[0]), int(input_data[1])
arr = list(map(int, input_data[2:2+n]))
result = sliding_max(arr, k)
print(' '.join(map(str, result)))
if __name__ == "__main__":
main()
Complexity
| Metric | Value | Explanation |
|---|---|---|
| Time | O(n) | Each index is pushed and popped at most once |
| Space | O(k) | Deque stores at most k indices |
Common Mistakes
Mistake 1: Using Values Instead of Indices
# WRONG - Cannot track when elements leave the window
dq.append(arr[i]) # Stores value
while dq and dq[0] < ???: # How do we know if it's out of window?
dq.popleft()
Problem: Without indices, you cannot determine when the front element has left the window.
Fix: Store indices and check dq[0] <= i - k.
Mistake 2: Wrong Removal Order
# WRONG - Removing from front before back
for i in range(n):
while dq and dq[0] <= i - k: # Front removal
dq.popleft()
dq.append(i) # Adding without back removal!
Problem: Not removing smaller elements from back breaks the decreasing property. Fix: Remove from back (smaller elements) before adding current element.
Mistake 3: Strict vs Non-Strict Comparison
# WRONG for some cases
while dq and arr[dq[-1]] < arr[i]: # Strict less-than
dq.pop()
Problem: With < instead of <=, duplicates can accumulate unnecessarily.
Fix: Use <= to remove equal elements too (the newer one is equally good and stays longer).
Mistake 4: Off-by-One Window Start
# WRONG
if i >= k: # Should be k-1
result.append(arr[dq[0]])
Problem: Misses the first window or outputs too late.
Fix: First complete window is at index k-1 (0-indexed), so check i >= k - 1.
Edge Cases
| Case | Input | Expected | Why |
|---|---|---|---|
| k equals n | arr=[3,1,4], k=3 |
4 |
Single window, just find max |
| k equals 1 | arr=[3,1,4], k=1 |
3 1 4 |
Output is the array itself |
| All same | arr=[5,5,5], k=2 |
5 5 |
Deque works correctly with duplicates |
| Decreasing | arr=[5,4,3,2], k=2 |
5 4 3 |
Deque keeps all elements |
| Increasing | arr=[1,2,3,4], k=2 |
2 3 4 |
Each new element clears deque |
When to Use This Pattern
Use Monotonic Deque When:
- Finding max/min in fixed-size sliding windows
- You need O(n) time and O(k) space
- Processing elements left-to-right (streaming)
Do Not Use When:
- Window size varies dynamically (consider segment tree)
- You need random access to any window (consider sparse table)
- The “window” is defined by value range, not position (consider different structure)
Pattern Recognition Checklist:
- Fixed window size k? -> Consider monotonic deque
- Need max OR min (not both)? -> Monotonic deque is ideal
- Need both max AND min? -> Use two deques
- Range updates involved? -> Consider segment tree instead
Sliding Minimum Variant
To find the minimum instead of maximum, reverse the comparison:
# For MINIMUM: maintain INCREASING deque
while dq and arr[dq[-1]] >= arr[i]: # Remove larger elements
dq.pop()
The front will hold the minimum instead of the maximum.
Related Problems
CSES Problems
| Problem | Technique |
|---|---|
| Sliding Window Median | Two multisets or segment tree |
| Sliding Window Cost | Extension with cost calculation |
LeetCode Problems
| Problem | Key Difference |
|---|---|
| Sliding Window Maximum | Same problem |
| Shortest Subarray with Sum at Least K | Monotonic deque on prefix sums |
| Constrained Subsequence Sum | DP with monotonic deque optimization |
Key Takeaways
-
Core Idea: A decreasing deque keeps the maximum at the front; smaller elements are discarded because they cannot become maximum while a larger element exists in the window.
-
Time Optimization: From O(n*k) to O(n) because each element enters and leaves the deque exactly once.
-
Space Trade-off: O(k) space for the deque enables O(1) time per query.
-
Pattern: Monotonic data structures are powerful for range min/max queries with sliding windows.
Practice Checklist
Before moving on, make sure you can:
- Implement monotonic deque without looking at code
- Explain why each element is pushed/popped at most once
- Modify for sliding minimum
- Identify this pattern in new problems within 2 minutes