Learn/DSA Patterns
DSA PatternsArrays & Two Pointersmedium16 min read

Sliding Window — Variable Size

How to find the shortest or longest subarray satisfying a condition in O(n) by expanding and shrinking a window on demand. Built from a rubber-band story, proven correct, applied to five classic problems.

#sliding-window#variable-window#arrays#strings#beginner#interview
Table of contents

Before we start

The previous chapter used a window of fixed size K — it always slid one step at a time. This chapter makes the window elastic: it can grow and shrink based on whether a condition is being satisfied. This is the more powerful — and more commonly tested — form of the sliding window. By the end you will be able to:

  • See the variable window as a rubber band that stretches and snaps back.
  • Explain the two-pointer expansion-shrink loop and why it's still O(n).
  • Recognise this pattern in substring and subarray problems that ask for "longest" or "shortest."

Stop at every Pause & Think box before reading on.


Picture this first (no code yet)

A real-life story

You and a friend are playing a game with a long row of numbered tiles on the floor. The rule: you both hold one end of a rubber band stretched across some tiles. You start standing next to each other (band has zero width).

Your friend (the right end) only moves forward — stretching the band to cover more tiles. You (the left end) only move forward — snapping the band back when it covers too many tiles.

The goal: find the longest stretch where the sum of all covered tiles is at most some limit T.

Every time your friend stretches right and the sum exceeds T, you snap left — one step at a time — until the sum drops back to T or below. Then your friend stretches right again.

Neither of you ever moves backward. Together you walk the entire row in one pass.

The rubber band is the window. The right end is the right pointer; you are the left pointer. Together they scan the array exactly once.


The actual problem

Given an array of positive integers and a target sum S, find the length of the smallest contiguous subarray whose sum is greater than or equal to S.

Example:

array  = [2, 1, 5, 2, 3, 2]
S      = 7

subarrays with sum ≥ 7:
  [2,1,5,2] sum=10  length 4
  [5,2]     sum=7   length 2   ← shortest
  [5,2,3]   sum=10  length 3
  ...
answer = 2

First, the slow way (so you feel the pain)

Check every possible subarray:

def min_subarray_slow(arr, s):
    n = len(arr)
    best = float('inf')
    for start in range(n):
        total = 0
        for end in range(start, n):
            total += arr[end]
            if total >= s:
                best = min(best, end - start + 1)
                break       # can't get shorter with same start
    return best if best != float('inf') else 0

Even with the early break, worst case is still O(n²) — for each start, you may scan to the end. With large arrays:

n = 10,000    →  up to 50,000,000 inner iterations
n = 100,000   →  up to 5,000,000,000 inner iterations

The variable sliding window does the same job in O(n).


The turning point

Why does brute force waste work? Because when you fix start = 0 and find the minimum valid end, then move to start = 1, you rebuild the window sum from scratch — even though arr[1] through the old end is still valid.

Pause & think

Suppose your current window [left..right] has sum = 10 and your target is S = 7. The condition is satisfied. What should you do — expand right further, or shrink left? And once you shrink, should you record the window size before or after shrinking?

The answer: record the window size first, then shrink. The current window satisfies the condition — that's a valid answer. Shrinking it might give a shorter valid window (better answer). Expanding it would only give a longer window (worse answer). So: record, shrink, repeat until the condition breaks. Then expand.


The one idea to remember

The entire pattern in one sentence

Expand right until the condition is satisfied, then shrink left as far as the condition holds — recording the window size each time — and repeat until right reaches the end.

The window breathes: expand to find a candidate, shrink to minimise it, expand again to find the next candidate.


Watch it happen, frame by frame

Array: [2, 1, 5, 2, 3, 2], S = 7. We want the smallest window with sum ≥ 7.

left=0, right=0, window_sum=0, best=∞

Expand right=0: add arr[0]=2  →  sum=2  < 7, keep expanding
Expand right=1: add arr[1]=1  →  sum=3  < 7, keep expanding
Expand right=2: add arr[2]=5  →  sum=87  ← condition met!
  Record length = right-left+1 = 3-0+1 = 3.  best=3
  Shrink: remove arr[left=0]=2 → sum=6  < 7, stop shrinking.
  left=1

Expand right=3: add arr[3]=2  →  sum=87  ← condition met!
  Record length = 3-1+1 = 3.  best=3
  Shrink: remove arr[left=1]=1 → sum=77  ← still valid!
    Record length = 3-2+1 = 2.  best=2
    Shrink: remove arr[left=2]=5 → sum=2  < 7, stop shrinking.
    left=3

Expand right=4: add arr[4]=3  →  sum=5  < 7, keep expanding
Expand right=5: add arr[5]=2  →  sum=77  ← condition met!
  Record length = 5-3+1 = 3.  best=2
  Shrink: remove arr[left=3]=2 → sum=5  < 7, stop shrinking.
  left=4

right exhausted.  Answer = best = 2  (the window [5,2])  ✅

Pause & think

Cover the code below. In [3, 4, 1, 1, 6] with S = 8, trace the window step by step. What is the shortest subarray with sum ≥ 8?

Check your trace
left=0, sum=0, best=∞
right=0: +3 → sum=3  < 8
right=1: +4 → sum=7  < 8
right=2: +1 → sum=88  → record len=3, best=3
  shrink: -3 → sum=5  < 8, stop. left=1
right=3: +1 → sum=6  < 8
right=4: +6 → sum=128  → record len=4, best=3
  shrink: -4 → sum=88  → record len=3, best=3
    shrink: -1 → sum=7  < 8, stop. left=3
right exhausted.  Answer = 3 (window [3,4,1] or [1,1,6])

Now, the code — line by line

def min_subarray_sum(arr, s):
    n = len(arr)
    left = 0
    window_sum = 0
    best = float('inf')        # track the smallest valid window seen

    for right in range(n):
        window_sum += arr[right]           # expand: new element enters from the right

        while window_sum >= s:             # condition is satisfied — try to shrink
            best = min(best, right - left + 1)   # record this window's length
            window_sum -= arr[left]        # remove the leftmost element
            left += 1                      # shrink from the left

    return best if best != float('inf') else 0   # 0 means no valid subarray found

Mapping to the rubber-band story:

  • for right in range(n): — your friend (the right end) moves one step forward every iteration, never going back.
  • window_sum += arr[right] — the rubber band now covers one more tile.
  • while window_sum >= s: — the band is stretched across a valid section — check if this is the shortest so far.
  • best = min(best, right - left + 1) — record the window length before you shrink.
  • window_sum -= arr[left]; left += 1 — you (the left end) snap the band back one step.
  • The while (not if) means you keep shrinking as long as the condition holds — every smaller valid window is a candidate for best.

Why is it O(n) even though there's a while loop inside a for loop?

This is the question everyone asks. The answer is in the pointer movement:

  • right only ever moves right: n steps total.
  • left only ever moves right: n steps total.

Together, the two pointers collectively take at most 2n steps. The inner while loop runs the left steps — it looks like it could run n times per outer iteration, but across the entire algorithm it runs at most n times total. Every step of left is paid for exactly once.

right moves:  n total steps
left moves:   at most n total steps
Total:         2n steps    O(n)

This is called amortised analysis: the total cost is O(n), even if individual iterations look expensive.


The fixed-window vs variable-window comparison

Fixed WindowVariable Window
Window sizeAlways exactly KGrows and shrinks based on condition
Slide ruleAlways slide by 1Expand right freely; shrink left when condition holds
Use when"Every window of size K""Longest/shortest window satisfying condition X"
Inner loopNo inner loopwhile loop to shrink
Both are O(n)✓ (amortised)

When should I reach for this? (the trigger list)

Reach for variable sliding window when:

  • The problem asks for the longest or shortest subarray (or substring) satisfying some condition.
  • The condition is monotone: if a window of size W satisfies it, then any larger window (for "at least" conditions) also satisfies it — or vice versa. This monotonicity is what makes expand-shrink work.
  • You see words like "at most K distinct characters," "sum exactly S," "all characters of P," "no repeats."
  • The brute-force is O(n²) nested loops where both bounds slide.
  • Elements are non-negative integers (for sum-based problems) — negative numbers break the monotonicity of the sum condition.

The monotonicity requirement

Variable sliding window only works when the condition has a monotone structure: as the window grows, the condition either keeps being satisfied or keeps being violated — it doesn't flip back and forth randomly. For sum ≥ S with positive numbers: once the sum meets S, adding more elements keeps it ≥ S. This lets us shrink confidently. If numbers can be negative, sums can oscillate — use prefix sums + hash map instead (Chapter 1.6).


The same trick in five disguises

Disguise 1 — Longest Substring Without Repeating Characters (LC #3)

Condition: no character appears twice in the window. Window is "valid" while all characters are unique.

def length_of_longest_substring(s):
    left = 0
    seen = {}           # char → most recent index
    best = 0

    for right in range(len(s)):
        ch = s[right]
        if ch in seen and seen[ch] >= left:
            left = seen[ch] + 1     # jump left past the duplicate
        seen[ch] = right
        best = max(best, right - left + 1)

    return best

Here we expand and record the max (not shrink to find the min). When a duplicate enters, we snap left past it in one jump instead of one-step shrinking. Same two-pointer logic — the window never holds a duplicate.

Disguise 2 — Minimum Window Substring (LC #76)

Find the smallest substring of s that contains all characters of t. This is the boss version of the pattern.

from collections import Counter

def min_window(s, t):
    need = Counter(t)       # characters required and their counts
    have = {}               # characters currently in window
    formed = 0              # how many unique chars in 'need' are satisfied
    required = len(need)

    left = 0
    best = (float('inf'), 0, 0)   # (length, left, right)

    for right in range(len(s)):
        ch = s[right]
        have[ch] = have.get(ch, 0) + 1
        if ch in need and have[ch] == need[ch]:
            formed += 1             # one more character fully satisfied

        while formed == required:   # all characters covered → try shrinking
            if right - left + 1 < best[0]:
                best = (right - left + 1, left, right)
            out = s[left]
            have[out] -= 1
            if out in need and have[out] < need[out]:
                formed -= 1         # losing a required character
            left += 1

    return s[best[1]:best[2]+1] if best[0] != float('inf') else ""

The expand-shrink skeleton is identical. The "condition" is now formed == required instead of sum >= S.

Disguise 3 — Longest Subarray with Sum ≤ K (positive integers)

Flip the problem: instead of finding the minimum window with sum ≥ S, find the maximum window with sum ≤ K:

def longest_subarray_sum_k(arr, k):
    left = 0
    window_sum = 0
    best = 0

    for right in range(len(arr)):
        window_sum += arr[right]
        while window_sum > k:           # condition violated → shrink
            window_sum -= arr[left]
            left += 1
        best = max(best, right - left + 1)   # record AFTER shrinking (window is valid here)

    return best

For "longest" problems: record after shrinking (the window is in a valid state). For "shortest" problems: record before shrinking (you just found a valid window; now try to make it smaller).

Disguise 4 — Longest Substring with At Most K Distinct Characters (LC #340)

def length_of_longest_substring_k_distinct(s, k):
    left = 0
    freq = {}
    best = 0

    for right in range(len(s)):
        ch = s[right]
        freq[ch] = freq.get(ch, 0) + 1

        while len(freq) > k:            # too many distinct chars → shrink
            out = s[left]
            freq[out] -= 1
            if freq[out] == 0:
                del freq[out]
            left += 1

        best = max(best, right - left + 1)

    return best

The pattern remains: expand right, shrink left when condition breaks, record the valid window.

Level up — Longest Repeating Character Replacement (LC #424)

Replace at most K characters in a string to make the longest substring of a single repeated character. The window is valid if window_size - count_of_most_frequent_char ≤ K. Expand right always; shrink left when this condition breaks.

def character_replacement(s, k):
    left = 0
    freq = {}
    max_freq = 0          # max frequency of any single char in current window
    best = 0

    for right in range(len(s)):
        freq[s[right]] = freq.get(s[right], 0) + 1
        max_freq = max(max_freq, freq[s[right]])

        # chars to replace = window_size - max_freq
        if (right - left + 1) - max_freq > k:
            freq[s[left]] -= 1
            left += 1

        best = max(best, right - left + 1)

    return best

Note: here we use if (not while) to shrink — we only ever shrink by one when the window grows by one, so the window size never decreases. The best window found so far is preserved.


Traps that catch beginners

Watch out for these

  • Using if instead of while for shrinking (for minimum-length problems). After removing one element from the left, the condition might still hold — you must keep shrinking. Use while, not if.
  • Recording before shrinking vs after shrinking. For minimum windows: record inside the while (before each shrink step). For maximum windows: record after the while (once the window is valid again).
  • Applying to arrays with negative numbers. The sum can oscillate — adding a negative number might make the window valid again after it was invalid. Expand-shrink breaks. Use prefix sum + hash map instead.
  • Forgetting to handle "no valid window found." If best stays at float('inf') (or 0 for max), return 0 or "" — not infinity.
BugFix
if window_sum >= s:Use while window_sum >= s: — keep shrinking as long as valid
Record after shrink in a minimum problemRecord inside the while, before each left++
Variable window on negative-number sum problemUse prefix sum + hash map (Chapter 1.6)
Return float('inf') when no answer foundReturn 0 or "" — check for unchanged best first

Say it like a pro (interview one-liner)

"I'll use a variable sliding window with two pointers. The right pointer always advances, expanding the window. Whenever the condition is satisfied, I shrink from the left — recording the window size each time — until the condition breaks. Both pointers only move forward, so the total work is O(n) amortised, with O(1) extra space or O(K) for any auxiliary structure like a frequency map."


Remember this forever

Sliding Window — Variable Size

right always moves forward (expand). When the condition is satisfied, shrink left forward and record. When the condition breaks, expand right again.

Both pointers move forward only → total work = O(n).


Cost: O(n) time, O(1) or O(alphabet) space · Trigger: longest/shortest subarray or substring satisfying a monotone condition · Key rule: for minimum → record inside the shrink loop; for maximum → record after the shrink loop

Only works with monotone conditions — positive-integer sums, distinct-char counts. For negative numbers: use prefix sum + hash map.


Check yourself

Why is the variable window O(n) even though there's a while loop inside the for loop?

The right pointer moves forward n times total. The left pointer also only moves forward — it can move at most n times total across the entire algorithm. The inner while loop accounts for left's movement — it looks expensive per iteration but the total movement of left across all iterations is bounded by n. So total operations ≤ 2n → O(n).

For a minimum-length problem, should you record the window size before or after shrinking? Why?

Before each shrink step (inside the while). The moment you enter the while, the current window satisfies the condition — that's a valid candidate. You record it, then try shrinking. If shrinking still satisfies the condition, you record again (a shorter candidate). This way you never miss a valid window.

Why does variable sliding window fail when array elements can be negative?

The expand-shrink logic relies on monotonicity: once the window sum exceeds S, adding more elements keeps it above S (for positive numbers), so it's safe to shrink. With negative numbers, adding a negative element reduces the sum — a window that was "too big" might become valid again after growing. The shrink-when-satisfied assumption breaks. You need prefix sums + hash maps for that case.

What's the difference between `if` and `while` for the shrink step?

if shrinks at most once — after one shrink, you move on even if the window is still valid. This means you might miss shorter valid windows. while keeps shrinking as long as the condition holds, catching every possible shorter valid window. For minimum-length problems, always use while. The if variant appears in special cases (like LC #424) where the window size never decreases — read the problem carefully.


Practice problems

ProblemDifficultyWhat to noticeLink
Minimum Size Subarray SumMediumPure variable-window template; record inside whileLC #209
Longest Substring Without Repeating CharactersMediumMax-length window; no char appears twiceLC #3
Longest Substring with At Most K Distinct CharsMediumMax-length; shrink when distinct count exceeds KLC #340
Fruits Into BasketsMediumSame as "at most 2 distinct values"LC #904
Longest Repeating Character ReplacementMediumWindow valid if replacements ≤ K; use if not whileLC #424
Minimum Window SubstringHardBoss-level: track all required chars with a counterLC #76

When you can solve Minimum Size Subarray Sum without notes, explain the amortised O(n) argument, and name the conditions under which variable window breaks — you have fully learned this pattern.

Next up: Prefix Sum, where instead of a moving window, we precompute a lookup table that answers any range-sum query in O(1).