Part VI · More Patterns Pattern 22 4 problems

Monotonic Deque

A monotonic stack that can also forget. Pop dominated candidates from the back, pop expired ones from the front, and the best candidate in the window is always at the front.

Two earlier patterns meet here. The sliding window moves a range across an array, but it cannot tell you the window’s maximum without rescanning. The monotonic stack throws away values that can never matter again, but it only ever grows from one end. Put the stack in a deque and you can also drop indices that slide out of the window. C# has no deque type, so this page shows what to use instead. Every index enters once and leaves once, so the whole pass is O(n).

Contents

  1. When to use
  2. Core idea
  3. The templates
  4. Choosing a deque in C#
  5. Common mistakes
  6. Sliding Window Maximum
  7. Shortest Subarray with Sum at Least K
  8. Constrained Subsequence Sum
  9. Longest Continuous Subarray With Absolute Diff Limit
  10. Recap

When to use

The trigger. You need the max or min of a moving range, and the range moves forward only. Fixed window, variable window, or “the last k states of a DP” all qualify. A heap also works but costs O(log n) per step and needs stale-entry cleanup. The deque is O(1) amortised.

Core idea

Keep a deque of indices whose values are monotone from front to back. For a window maximum, values decrease. When a new index arrives, pop from the back every index whose value is no bigger: the new one is both larger and newer, so the old one can never be the maximum again. It is dominated. Then pop from the front every index that has slid out of the window. It is stale. What remains at the front is the answer.
nums 8 i0 1 i1 6 i2 4 i3 2 i4 7 i5 window k = 3, ending at i5 deque before front back 6 (i2) 4 (i3) 2 (i4) 7 (i5) incoming step 1 6 (i2) Front i2 ≤ 5 - 3, so it left the window: pop front. step 2 2 (i4) 4 (i3) Back values 2, then 4, are ≤ 7: dominated, pop. step 3 7 (i5) Append i5. The front, 7, is the window max.
Figure 22.1 — One step of a max deque: drop the stale front, drop dominated values at the back, read the front.

Reading the figure. Cell colors match the fate of each index. Violet means stale: the index slid out of the window. Red means dominated: a newer, larger value arrived, so it can never be the max. Amber is the new value and green is the answer. Notice that the dominated values leave from the back, newest first.

Why indices, not values. The front check is “has this left the window?”, and only an index can answer that. Same rule as on the monotonic stack page.

Why O(n). Each index is appended once. It can be popped at most once, from one end or the other. So the total work across all the inner while loops is at most n pops, even though one step may pop many.

The templates

Template A — fixed window minimum
public static class SlidingWindowMin
{
    /// <summary>Minimum of every window of size k, left to right.</summary>
    /// <param name="nums">The values.</param>
    /// <param name="k">Window size, from 1 up to nums.Length.</param>
    /// <returns>One minimum per window, nums.Length - k + 1 values.</returns>
    /// <example>
    /// Mins([4, 2, 12, 3, 8, 1], 3) returns [2, 2, 3, 1].
    /// </example>
    public static List<int> Mins(int[] nums, int k)
    {
        // The deque is an int[] with two pointers. Live slots are head .. tail - 1.
        // Each index is pushed once, so nums.Length slots never run out. No wraparound.
        var window = new int[nums.Length];   // indices. nums[...] increases front to back.
        int head = 0, tail = 0;           // 0, 0: head == tail means empty
        var output = new List<int>();

        // index is the right edge of the window. The window is index - k + 1 .. index.
        // Invariant at the top of each pass: window holds in-range, undominated indices.
        for (int index = 0; index < nums.Length; index++)
        {
            int value = nums[index];
            // Stale: index - k is the first position left of the window. Drop it.
            while (head < tail && window[head] <= index - k)
                head++;                   // pop front: move head right by one
            // Dominated: an older value >= the new one can never be the minimum again.
            // tail - 1: the back slot of the deque.
            while (head < tail && nums[window[tail - 1]] >= value)
                tail--;                   // pop back: move tail left by one
            window[tail++] = index;       // push back, then move tail one past it

            // k - 1: the first index where a full window of k values exists.
            if (index >= k - 1)
                output.Add(nums[window[head]]);   // the front holds the window minimum
        }

        return output;
    }
}

Flip both comparisons for the maximum: pop the back while nums[window[tail - 1]] <= value. Everything else stays the same. Problem 1 is that flip.

i, value nums, window in blue popped deque values out i=0, 4 4 2 12 3 8 1 4 — i=1, 2 4 2 12 3 8 1 pop 4 2 — i=2, 12 4 2 12 3 8 1 2 12 2 i=3, 3 4 2 12 3 8 1 pop 12 2 3 2 i=4, 8 4 2 12 3 8 1 stale 2 3 8 3 i=5, 1 4 2 12 3 8 1 pop 8, 3 1 1
Figure 22.2 — The front of the deque is always the window minimum, and each value is popped at most once.

Reading the figure. Each row is one pass of the loop. In the array, amber is the new value and blue is the rest of the window. Red text names values popped from the back as dominated. Violet text names a value dropped from the front as stale. The green deque cell is the front, which is the minimum written to out.

Choosing a deque in C#

The Python version of this page uses collections.deque. .NET has no deque type. Queue<T> removes only from the front and Stack<T> only from the top. This pattern needs both ends. There are two good stand-ins.

Why the array wins here. A monotonic deque only ever holds indices, and each index enters once. That is exactly the case where a plain array with two pointers is a complete deque. Use a real ring buffer, with % capacity on both pointers, only when items can be pushed again or the stream has no end. In an interview, say which one you picked and why in one sentence.

public static class SlidingWindowMinList
{
    /// <summary>Same as SlidingWindowMin.Mins, with LinkedList as the deque.</summary>
    /// <param name="nums">The values.</param>
    /// <param name="k">Window size, from 1 up to nums.Length.</param>
    /// <returns>One minimum per window, nums.Length - k + 1 values.</returns>
    /// <example>
    /// Mins([4, 2, 12, 3, 8, 1], 3) returns [2, 2, 3, 1].
    /// </example>
    public static List<int> Mins(int[] nums, int k)
    {
        // First is the front, Last is the back. Every end operation is O(1).
        var window = new LinkedList<int>();   // indices. nums[...] increases front to back.
        var output = new List<int>();

        // index is the right edge. Invariant: window holds in-range, undominated indices.
        for (int index = 0; index < nums.Length; index++)
        {
            // Stale: index - k is the first position left of the window.
            while (window.Count > 0 && window.First!.Value <= index - k)
                window.RemoveFirst();
            // Dominated: an older value >= the new one can never be the minimum again.
            while (window.Count > 0 && nums[window.Last!.Value] >= nums[index])
                window.RemoveLast();
            window.AddLast(index);

            // k - 1: the first index where a full window of k values exists.
            if (index >= k - 1)
                output.Add(nums[window.First!.Value]);
        }

        return output;
    }
}

Same logic, same answers. The tests run both versions on the same inputs. Pick the array for speed or the list for clarity. Both are O(n) overall.

Template B — DP that looks back at most k steps
public static class MinCostJumps
{
    /// <summary>Cheapest walk from the first stone to the last, jumping 1 to k stones.
    /// You pay costs[i] for every stone you land on, including the first.</summary>
    /// <param name="costs">Cost of each stone, at least one stone.</param>
    /// <param name="k">Longest jump allowed, at least 1.</param>
    /// <returns>The minimum total cost to stand on the last stone.</returns>
    /// <example>
    /// Cheapest([1, 100, 1, 1, 100, 1], 2) returns 4.
    /// </example>
    public static int Cheapest(int[] costs, int k)
    {
        int n = costs.Length;
        var best = new int[n];            // best[i]: cheapest total to stand on stone i
        var window = new int[n];          // indices. best[...] increases front to back.
        int head = 0, tail = 0;           // live slots head .. tail - 1. Equal means empty.

        // Invariant at the top of pass i: window holds indices i - k .. i - 1 that
        // are not dominated, so best[window[head]] is the cheapest stone that can reach i.
        for (int i = 0; i < n; i++)
        {
            // Stale: a stone before i - k is more than k stones back.
            while (head < tail && window[head] < i - k)
                head++;
            // Recurrence: land on i from the cheapest reachable stone.
            // Stone 0 has nothing before it, so it adds 0.
            best[i] = costs[i] + (head < tail ? best[window[head]] : 0);
            // Dominated: an older stone that costs at least as much is never better.
            while (head < tail && best[window[tail - 1]] >= best[i])   // tail - 1: the back
                tail--;
            window[tail++] = i;
        }

        return best[^1];                  // ^1: the last stone
    }
}

The recurrence best[i] = cost[i] + min(best[i-k..i-1]) is O(n × k) if you scan. The deque turns the scan into reading window[head]. Any DP whose transition is “best of the last k states” gets this speed-up. Jump Game VI is this template with max in place of min.

cost front best deque i0 1 — 1 [0] i1 100 i0 100+1=101 [0, 1] i2 1 i0 1+1=2 [0, 2] i3 1 i2 stale i0 1+2=3 [2, 3] i4 100 i2 100+2=102 [2, 3, 4] i5 1 i3 stale i2 1+3=4 [3, 5] Cheapest walk: stones 0, 2, 3, 5. Total 1 + 1 + 1 + 1 = 4. front = the cheapest stone at most k = 2 back. Its best is added to cost.
Figure 22.3 — Each best value reads one deque front instead of scanning the last k stones.

Reading the figure. Each column is one stone. The front row is window[head], the cheapest stone in reach. Violet notes show a stone dropped from the front for being more than k back. Green stones form the cheapest walk, and the green cell is the answer, best[^1] = 4.

Common mistakes

The problems

1. Sliding Window Maximum Hard

Problem

Given an array nums and a window size k, return the maximum of each window of k consecutive values as the window slides from left to right.

Approach

Solution

public static class SlidingWindowMax
{
    /// <summary>Maximum of every window of size k, left to right.</summary>
    /// <param name="nums">The values.</param>
    /// <param name="k">Window size, from 1 up to nums.Length.</param>
    /// <returns>One maximum per window, nums.Length - k + 1 values.</returns>
    /// <example>
    /// MaxSlidingWindow([1, 3, -1, -3, 5, 3, 6, 7], 3) returns [3, 3, 5, 5, 6, 7].
    /// </example>
    public static int[] MaxSlidingWindow(int[] nums, int k)
    {
        // n - k + 1 windows: right edges k - 1 .. n - 1.
        var result = new int[nums.Length - k + 1];
        var window = new int[nums.Length];   // indices. nums[...] decreases front to back.
        int head = 0, tail = 0;           // live slots head .. tail - 1. Equal means empty.

        // right is the right edge. The window covers right - k + 1 .. right.
        // Invariant at the top of each pass: window holds in-range, undominated indices.
        for (int right = 0; right < nums.Length; right++)
        {
            // Stale: right - k is one left of the window, so drop it and anything older.
            while (head < tail && window[head] <= right - k)
                head++;
            // Dominated: an older value <= the new one can never be the max again.
            while (head < tail && nums[window[tail - 1]] <= nums[right])   // tail - 1: back
                tail--;
            window[tail++] = right;

            // k - 1: the first right edge with a full window of k values.
            // right - k + 1 is the window's left edge, which is also its output slot.
            if (right >= k - 1)
                result[right - k + 1] = nums[window[head]];   // the front is the window max
        }

        return result;
    }
}

Walkthrough

nums = [1, 3, -1, -3, 5, 3, 6, 7], k = 3. The deque is shown as values.

i, value nums, window in blue popped deque values max i=0, 1 1 3 -1 -3 5 3 6 7 1 — i=1, 3 1 3 -1 -3 5 3 6 7 pop 1 3 — i=2, -1 1 3 -1 -3 5 3 6 7 3 -1 3 i=3, -3 1 3 -1 -3 5 3 6 7 3 -1 -3 3 i=4, 5 1 3 -1 -3 5 3 6 7 stale 3 pop -3, -1 5 5 i=5, 3 1 3 -1 -3 5 3 6 7 5 3 5 i=6, 6 1 3 -1 -3 5 3 6 7 pop 3, 5 6 6 i=7, 7 1 3 -1 -3 5 3 6 7 pop 6 7 7
Figure 22.4 — The output is the green front of each row: 3, 3, 5, 5, 6, 7.

Reading the figure. Each row adds one value. Amber marks it in the array, blue marks the rest of its window. Red text lists values popped from the back because the new value is at least as big. At i=4 the 3 is popped from the front in violet, because index 1 left the window. The green cell is the front, the max of that window.

TimeO(n)SpaceO(k)

Edge cases to raise

Say this out loud: “I keep indices with decreasing values. A new value evicts every smaller one from the back, because those are older and smaller and can never win. The front is the max, and I drop it once it leaves the window. Each index enters and leaves once, so it is linear.”

2. Shortest Subarray with Sum at Least K Hard

Problem

Given an integer array nums, which may contain negatives, and an integer k, return the length of the shortest non-empty contiguous subarray whose sum is at least k. Return -1 if none exists.

The idea

Solution

public static class ShortestSubarray
{
    /// <summary>Length of the shortest subarray with sum at least k, or -1.</summary>
    /// <param name="nums">Integers, may be negative.</param>
    /// <param name="k">The target, at least 1.</param>
    /// <returns>The shortest qualifying length, or -1 if none exists.</returns>
    /// <example>
    /// ShortestSubarrayAtLeastK([84, -37, 32, 40, 95], 167) returns 3.
    /// </example>
    public static int ShortestSubarrayAtLeastK(int[] nums, int k)
    {
        int n = nums.Length;
        // prefix[j] is the sum of nums[0..j). n + 1 slots: prefix[0] = 0 is the empty prefix.
        // long: n values near int.MaxValue would overflow an int sum.
        var prefix = new long[n + 1];
        for (int j = 0; j < n; j++)
            prefix[j + 1] = prefix[j] + nums[j];   // + 1: prefix runs one ahead of nums

        // n + 1 is longer than any real subarray, so it marks "none yet".
        int best = n + 1;
        // n + 1 slots: every prefix index is pushed once at most.
        var starts = new int[n + 1];      // indices into prefix, prefix[...] increasing
        int head = 0, tail = 0;           // live slots head .. tail - 1. Equal means empty.

        // end is a prefix index. nums[start..end) sums to prefix[end] - prefix[start].
        // Invariant at the top of each pass: starts holds useful start indices below end.
        for (int end = 0; end <= n; end++)
        {
            long total = prefix[end];
            // Front: the oldest start works. No later end can give it a shorter answer.
            while (head < tail && total - prefix[starts[head]] >= k)
                best = Math.Min(best, end - starts[head++]);   // read the front, then pop it
            // Back: end is later and no larger, so it is a better start from now on.
            while (head < tail && prefix[starts[tail - 1]] >= total)   // tail - 1: the back
                tail--;
            starts[tail++] = end;
        }

        // Still the sentinel n + 1 means nothing qualified, so -1.
        return best <= n ? best : -1;
    }
}

Walkthrough

nums = [2, -1, 2], k = 3, so prefix = [0, 2, 1, 3]:

prefix = [0, 2, 1, 3], k = 3 0 index 0 2 index 1 1 index 2 3 index 3 3 - 0 ≥ 3 end, prefix deque after what happens end 0, P = 0 0 push 0 end 1, P = 2 0 1 2 - 0 < 3: push 1 end 2, P = 1 0 2 back P = 2 ≥ 1: pop 1 push 2 end 3, P = 3 2 3 3 - 0 ≥ 3: length 3, pop front 0, push 3 Return 3, the whole array.
Figure 22.5 — Index 2 is later and lower than index 1, so index 1 goes, and the pair 0 to 3 gives length 3.

Reading the figure. On the left, each circle is prefix[index], plotted by height. The red circle is index 1. Index 2 comes later with a smaller prefix, so index 1 can never be the better start. The green dashed line joins the start and end of the answer. On the right, the deque holds start indices, front on the left.

TimeO(n)SpaceO(n)

Edge cases to raise

Say this out loud: “Negatives break the sliding window, so I use prefix sums. The deque holds start indices with increasing prefix sums. I pop the front while it gives a valid subarray, since later ends would only be longer, and I pop the back while it is no smaller than the new prefix, since the new index is a better start.”

3. Constrained Subsequence Sum Hard

Problem

Given an integer array nums and an integer k, return the maximum sum of a non-empty subsequence such that any two consecutive chosen elements are at most k positions apart.

The idea

Solution

public static class ConstrainedSubsetSum
{
    /// <summary>Max sum of a non-empty subsequence with chosen indices at most k apart.</summary>
    /// <param name="nums">Integers, at least one, may be negative.</param>
    /// <param name="k">Largest allowed gap between consecutive chosen indices, at least 1.</param>
    /// <returns>The maximum achievable sum.</returns>
    /// <example>
    /// MaxSum([10, 2, -10, 5, 20], 2) returns 37.
    /// </example>
    public static int MaxSum(int[] nums, int k)
    {
        // best[i]: largest sum of a valid subsequence ending at i. Clone gives a copy,
        // so each slot starts as "take nums[i] alone".
        var best = (int[])nums.Clone();
        var window = new int[nums.Length];   // indices. best[...] decreases front to back.
        int head = 0, tail = 0;           // live slots head .. tail - 1. Equal means empty.

        // Invariant at the top of pass i: window holds undominated indices in
        // i - k .. i - 1, so best[window[head]] is the best predecessor for i.
        for (int i = 0; i < nums.Length; i++)
        {
            // Stale: an index before i - k is too far back to precede i.
            while (head < tail && window[head] < i - k)
                head++;
            if (head < tail)
            {
                // Math.Max(0, ...): a negative predecessor only hurts, so start fresh.
                best[i] = nums[i] + Math.Max(0, best[window[head]]);   // front is the max
            }
            // Dominated: an older index with a sum no larger is never the better pick.
            while (head < tail && best[window[tail - 1]] <= best[i])   // tail - 1: the back
                tail--;
            window[tail++] = i;
        }

        return best.Max();                // the best subsequence may end anywhere
    }
}

Walkthrough

nums = [10, 2, -10, 5, 20], k = 2:

nums front best deque i0 10 — 10 [0] i1 2 i0: 10 2 + 10 = 12 [1] i2 -10 i1: 12 -10 + 12 = 2 [1, 2] i3 5 i1: 12 5 + 12 = 17 [3] i4 20 i3: 17 20 + 17 = 37 [4] max(best) = 37: pick 10, 2, 5, 20 and skip -10. front = best of the last k = 2 states, used only if it is positive.
Figure 22.6 — At index 3 the front still points at index 1, so the subsequence jumps over -10 and reaches 37.

Reading the figure. Each column is one index. The front row shows which earlier state best[i] builds on. Green cells in the top row are the chosen subsequence. The grey -10 is skipped. Notice that i3 reads i1, two steps back, which is still inside k = 2.

TimeO(n)SpaceO(n)

Edge cases to raise

Say this out loud: “The DP is best sum ending at i, equal to nums of i plus the best of the previous k states, or zero. That max over the last k states is a sliding window maximum, so a monotonic deque makes the whole DP linear.”

4. Longest Continuous Subarray With Absolute Diff Limit Medium

Problem

Given an integer array nums and an integer limit, return the length of the longest contiguous subarray in which the absolute difference between any two elements is at most limit.

The idea

Solution

public static class LongestSubarrayLimit
{
    /// <summary>Longest subarray whose max minus min is at most limit.</summary>
    /// <param name="nums">Integers, at least one.</param>
    /// <param name="limit">Largest allowed difference, 0 or more.</param>
    /// <returns>The length of the longest qualifying subarray.</returns>
    /// <example>
    /// LongestSubarray([10, 1, 2, 4, 7, 2], 5) returns 4.
    /// </example>
    public static int LongestSubarray(int[] nums, int limit)
    {
        int n = nums.Length;
        // Two int[] deques, n slots each because every index is pushed once.
        var highs = new int[n];           // indices. Values decrease, front is the max.
        var lows = new int[n];            // indices. Values increase, front is the min.
        int hHead = 0, hTail = 0;         // highs live in hHead .. hTail - 1
        int lHead = 0, lTail = 0;         // lows live in lHead .. lTail - 1
        int left = 0;                     // 0: the window starts at the first index
        int best = 0;                     // 0: no window seen yet

        // right is the new right edge. Invariant at the end of each pass: the window
        // left .. right is valid, and both deques hold only indices in it.
        for (int right = 0; right < n; right++)
        {
            int value = nums[right];
            // Dominated in highs: older and no bigger, never the max again.
            while (hHead < hTail && nums[highs[hTail - 1]] <= value)   // - 1: the back
                hTail--;
            highs[hTail++] = right;
            // Dominated in lows: older and no smaller, never the min again.
            while (lHead < lTail && nums[lows[lTail - 1]] >= value)    // - 1: the back
                lTail--;
            lows[lTail++] = right;

            // Shrink while invalid. Each head is a front: the window max and min.
            while ((long)nums[highs[hHead]] - nums[lows[lHead]] > limit)   // long: no overflow
            {
                left++;                   // drop one index from the left edge
                if (highs[hHead] < left)  // the max fell out of the window
                    hHead++;
                if (lows[lHead] < left)   // the min fell out of the window
                    lHead++;
            }

            // + 1: right - left counts gaps. Add one to count the elements.
            best = Math.Max(best, right - left + 1);
        }

        return best;
    }
}

Walkthrough

nums = [8, 2, 4, 7], limit = 4. Deques shown as values.

right nums, window highs (max) lows (min) max - min len r=0, 8 8 2 4 7 8 8 8 - 8 = 0 1 r=1, 2 8 2 4 7 8 2 2 8 - 2 > 4: left = 1 1 r=2, 4 8 2 4 7 4 2 4 4 - 2 = 2 ≤ 4 2 r=3, 7 8 2 4 7 7 2 4 7 7 - 2 > 4: left = 2 2 Best length 2, from [2, 4] or [4, 7].
Figure 22.7 — The two deque fronts give the window max and min, and the window shrinks when they differ too much.

Reading the figure. Each row adds nums[right], shown in amber, and the blue cells are the rest of the window after any shrink. Deques read front to back, left to right. A violet cell is a front that went stale when left moved past it. Values popped from the back as dominated are not drawn. The green length is the best seen.

TimeO(n)SpaceO(n)

Edge cases to raise

Say this out loud: “Any two within the limit means max minus min within the limit. I grow a sliding window and keep two monotonic deques for the max and the min, shrinking from the left while the fronts differ by too much.”

Recap

The six things to carry forward

Where this goes next

The deque here is a tool inside an algorithm. Pattern 23, Design: Hash Map plus Linked List, turns the same “cheap at both ends” idea into a data structure you build yourself, the shape behind the LRU cache.


← 21 — Weighted Shortest Paths 23 — Design: Hash Map plus Linked List →