leetcode / Sliding window / Variable-size sliding window core

Variable-size sliding window

When the question is "the longest (or shortest) stretch that satisfies a condition", the window cannot have a fixed width, so it breathes: the right edge grows it one item at a time and the left edge shrinks it whenever the condition is broken (or, for shortest, whenever it still holds). Each edge only moves forward, so the whole scan is linear. This page covers the no-repeat window, the positive-sum window and the "at most k replacements" window, and why negative numbers break the pattern.

On this page
  1. Problem
  2. Intuition
  3. Mechanics
  4. The invariant
  5. Two templates
  6. A window whose condition is a formula
  7. Worked example
  8. Implementation
  9. Variations and edge cases
  10. Complexity and tradeoffs
  11. Problems
  12. Medium
  13. Hard
  14. Also uses this pattern
  15. Connections
  16. Read first
  17. Read next
  18. Check yourself
  19. References

Problem

"Longest substring with no repeated letter", "shortest run of numbers adding to at least a target", "longest stretch that becomes uniform after k edits": every candidate is a contiguous range, and there are about n²/2 of them. Checking each from scratch is cubic or quadratic. The structure that rescues you is monotonicity: if a range is valid, shrinking it keeps it valid (or if it is invalid, growing it keeps it invalid), so you never need to look at a left edge again once it has been passed.

SignalPatternWhy
"Longest substring or subarray such that ..." with a condition that breaks as the range growsExpand right, shrink left while invalidOnce invalid, a larger range with the same left edge can never become valid, so move the left edge.
"Shortest / minimum-length range that reaches a target"Expand right, shrink left while validValid ranges stay valid when extended, so record each valid window and then try to make it smaller.
"No duplicates in the range"Last-seen map, jump the left edgeThe left edge can leap straight past the earlier copy instead of stepping.
"At most k distinct values" or "at most k changes"Counts plus a cost formulaThe condition can be computed from counts that update in O(1).
Array contains negative numbers and the target is a sumNot a window: use prefix sums with a hash mapGrowing the range no longer moves the sum in one direction, so the shrinking rule is unsound.
"Exactly k" of somethingWindow(at most k) − window(at most k − 1)"Exactly" is not monotonic but "at most" is.

Intuition

Picture an inchworm on a number line. The head (right edge) always steps forward. The tail (left edge) only ever catches up. After every head step you ask whether the worm is still allowed to be this long, and if not, you pull the tail forward until it is. The worm never goes backwards, so head and tail together take at most 2n steps, however the condition behaves in between.

The skill is in two choices: what summary of the window lets you test the condition in O(1) (a set, a count map, a sum, a max count), and which direction of the condition is monotonic, which tells you whether to shrink while invalid (longest) or while valid (shortest).

The no-repeat window growing and jumping over abcbdace One row per step over the letters of abcbdace. The window after each step is a, ab, abc, cb, cbd, cbda, bdac, bdace. The longest window is bdace with 5 letters. a b c b d a c e a b c b d a c e a b c b d a c e a b c b d a c e a b c b d a c e a b c b d a c e a b c b d a c e a b c b d a c e

One row per right-edge step. Amber is the window; green is the longest one found. Watch the left edge jump over the earlier copy of a repeated letter.

Mechanics

The invariant

After each iteration, [left, right] is the longest valid window ending at right (for "longest" problems) or, for "shortest" problems, the longest invalid window ending at right, so that [left - 1, right] is the shortest valid one whenever one exists. The proof has two halves. Safety: the left edge only moves past positions that cannot start a valid window ending at this or any later right edge, because validity is monotonic. Progress: left and right each only increase and are bounded by n, so the loop body runs O(n) times in total even though it contains an inner while. This is amortised analysis: the inner loop's total work is the total distance the left edge travels.

Two templates

Longest: add nums[right]; while the window is invalid, remove nums[left] and advance; then record the length. The record sits after the shrink because only then is the window valid.

Shortest: add nums[right]; while the window is valid, record the length, remove nums[left] and advance. The record sits inside the shrink because each valid state is a candidate and the shrink looks for a smaller one.

A window whose condition is a formula

Replacement problems turn "can I make this window all one letter with at most k edits" into arithmetic. The cheapest plan keeps the most common letter and rewrites the rest, so the cost is length − top count. The window is valid while that cost is at most k. Because the cost can only be computed from counts that update in O(1), no scan of the window is needed.

Edits needed per right-edge step in ABAABCBBA with k = 2 A bar per step showing the edit cost of the window after shrinking: costs 0, 1, 1, 1, 2, 2, 2, 2, 2, never above 2. The best window length is 5. k = 2 (limit) 0 1 A 1 2 B 1 3 A 1 4 A 2 5 B 2 4 C 2 4 B 2 5 B 2 5 A

Bar height is the edit cost after shrinking; the first number under each bar is the window length, the second the letter that just arrived. The cost never crosses the limit.

Worked example

Longest window with no repeated letter in abcbdace. The left edge jumps to one past the previous copy of the arriving letter.

RightLetterLeft afterWindowBest so far
0a0a1
1b0ab2
2c0abc3
3b2 (jumped)cb3
4d2cbd3
5a2cbda4
6c3 (jumped)bdac4
7e3bdace5

The longest window is bdace, 5 letters, from a single pass of 8 steps.

Shortest run with sum at least 11 in [3, 1, 4, 1, 2, 9, 1, 6]. Every row is a valid window that the shrink loop then tries to make smaller.

LeftRightSumLengthBest
041155
151755
251644
351233
451122
571632

The shortest valid run is 2 long, at indices 4 to 5.

Longest stretch fixable with 2 edits in ABAABCBBA:

RightWindow after shrinkTop letter countEdits neededBest
0A101
1AB112
2ABA213
3ABAA314
4ABAAB325
5AABC225
6ABCB225
7ABCBB325
8BCBBA325

The best window is 5 letters long.

Implementation

def longest_unique(s):
    last = {}
    left = best = 0
    for right, ch in enumerate(s):
        if ch in last and last[ch] >= left:
            left = last[ch] + 1                 # jump past the earlier copy
        last[ch] = right
        best = max(best, right - left + 1)
    return best


def shortest_at_least(target, nums):           # nums must be positive
    left = total = 0
    best = float("inf")
    for right, x in enumerate(nums):
        total += x
        while total >= target:
            best = min(best, right - left + 1)
            total -= nums[left]
            left += 1
    return 0 if best == float("inf") else best


def longest_after_replacements(s, k):
    counts = {}
    left = best = 0
    for right, ch in enumerate(s):
        counts[ch] = counts.get(ch, 0) + 1
        while (right - left + 1) - max(counts.values()) > k:
            counts[s[left]] -= 1
            left += 1
        best = max(best, right - left + 1)
    return best

Variations and edge cases

  • Covering a multiset. The "smallest window containing every needed letter" problem keeps a need-count map and a single integer, how many letters still lack their quota, like the mismatch counter of the fixed window. Shrink while that integer is zero, recording each state.
  • At most k distinct. Keep a count map; the window is invalid when len(counts) > k; delete keys that reach zero, or the length of the map lies.
  • Exactly k. Count windows with at most k, subtract those with at most k − 1.
  • Max-count shortcut. In the replacement problem the top count can be kept as a value that never decreases and the answer stays right, because only a larger top count can produce a longer window. Calling max over at most 26 counts is simpler and safe.
  • Jump versus step. The last-seen map lets the left edge leap; stepping one at a time with a set is also O(n) overall. Check last[ch] >= left, or an old copy outside the window will drag the edge backwards.
  • Edge cases: empty input, a target that is never reached (answer 0, not infinity), a single element that already meets the target, all elements equal, and negative numbers, which require prefix sums instead.

Complexity and tradeoffs

  • Time: O(n) edge moves in total, with O(1) work per move for sums, sets and count maps. The replacement version costs O(alphabet) per step if you recompute the max.
  • Space: O(1) for a sum, O(alphabet) or O(distinct values) for sets and maps.
  • Baseline. Trying every pair of edges with an O(n) validity check is O(n³); caching a running summary gets O(n²); the window gets O(n).
  • When it fails. Validity must be monotonic in the window's extent. A sum target with negative numbers is not, which is why those questions use prefix sums and a hash map. A condition that depends on the window's median or an arbitrary property needs a different structure, such as a sorted container or two heaps.

Problems

The home problems are the classics of the pattern: a no-repeat window, a smallest covering window, a positive-sum window, a cost-formula window and a restart-on-break scan. The turbulent subarray is the odd one out: its "window" is the current alternating run, which restarts instead of shrinking.

Medium

Hard

  • 76. Minimum Window Substring Blind 75 Grind 169 NeetCode 150 NeetCode 250
    Grow the right edge until the window covers every needed count, then shrink from the left while it still does, recording the smallest.

Also uses this pattern

These problems have a different home page, but this idea is part of their solution.

Connections

Read first

Read next

No page builds on this one yet.

This is the fixed window with the width released. The need-count map is the counting page's frequency map; the last-seen jump is the hash map's lookup. When the sum has negative numbers, the prefix sums page takes over. The monotonic deque later handles windows whose summary is a maximum.

Check yourself

The shrink loop is nested inside the grow loop. Why is the algorithm still O(n)?

The left edge only moves forward and never passes n, so the inner loop runs at most n times in total across the whole scan, not n times per outer step. In the sum example the shrink fired 6 times for 8 elements.

Why does the "shortest" template record the length inside the shrink loop but the "longest" template records it after?

For shortest, every state where the window is valid is a candidate, and shrinking looks for smaller valid states. For longest, the window is valid only after the shrink loop has restored it, so the length is read there. Reading too early records an invalid window.

Why does "shortest subarray with sum at least target" fail on arrays with negative numbers?

Removing the left element may increase the sum, and adding a right element may decrease it, so a shrunk window can become valid again later and the left edge may not safely be skipped. Prefix sums with a monotonic structure or a hash map are needed.

In the no-repeat window, what goes wrong if you drop the check last[ch] >= left?

A copy of the letter from before the window would pull the left edge backwards and the window would include repeats. In "abcbdace" the letter a at index 5 had an earlier copy at 0, which is already behind the left edge (2).

References

  • Cormen, Leiserson, Rivest and Stein, Introduction to Algorithms: the aggregate method of amortised analysis (chapter 16 in the fourth edition), which is the proof that the two edges together move O(n) steps.
  • Skiena, The Algorithm Design Manual: the discussion of scanning with two moving indices and of loop invariants.
  • Sedgewick and Wayne, Algorithms, section 1.4 (analysis of algorithms), including amortized analysis, the argument behind the window's O(n) total.
  • Bentley, Programming Pearls, column 8 on the maximum-subarray problem, the best-known linear scan that works on arrays with negative numbers, where windows do not.
  • The NeetCode roadmap and the Blind 75, Grind 169, NeetCode 150 and NeetCode 250 lists, used as the index of topics and problems.