🎉 All 30 days are live — the full DSA-30 course, from Big-O to System Design. See the roadmap →
Day 24 - Sliding WindowVariable-Size Window

Variable-Size Window

Fixed windows move in lockstep — one in, one out. Variable windows set the two pointers free: the right edge always advances, but the left edge chases it only when the window goes “invalid,” shrinking until it’s valid again. That asymmetric dance keeps the whole thing O(n) — and the one decision that trips everyone up is where and when you record the answer.

Variable-size sliding window is the more powerful half of the pattern. The window length isn’t fixed — it grows and shrinks based on whether the current contents are valid under some problem-specific predicate.

There are two genuine pointers, l (left edge) and r (right edge), and they move at different speeds:

  • r advances every iteration of the outer loop (always one step forward).
  • l advances only when the window becomes invalid, and keeps advancing until validity is restored.

That asymmetric motion is what makes the technique O(n) overall — each element enters the window at most once (r advances) and leaves at most once (l advances). Total work: O(2n) = O(n).

The expand-contract template

l = 0
state = empty
answer = identity

for r in 0 .. n - 1:
    add(state, nums[r])                  # expand right

    while state is INVALID:
        remove(state, nums[l])           # contract left
        l += 1

    answer = update(answer, l, r, state) # record the current valid window

That’s the whole template. Three things vary problem-to-problem:

  1. What state is (sum, count of distinct, hash map, etc.).
  2. What “invalid” means (sum exceeds target, more than k distinct, duplicate present, etc.).
  3. What you record (r − l + 1 for longest, r − l + 1 for shortest just when valid, a running count, etc.).

The inner while loop is the secret. It looks like it makes the algorithm O(n²), but it doesn’t — l only ever moves forward, never backwards, across the whole input. So the total number of inner-loop iterations across all outer iterations is at most n.

Variant A — “Longest valid window”

The most common shape. “Find the longest contiguous subarray such that the contents satisfy property P.”

The window is always valid by the time you update the answer (because the while loop just finished shrinking it back into validity). So the current window length r − l + 1 is a candidate answer at every iteration.

Worked example: Longest Substring Without Repeating Characters

Given a string s, find the length of the longest substring without repeating characters.

State: a set of characters in the window (or a frequency map). Invalid when: the just-added character s[r] is already in the set. Record: r − l + 1 after the shrink loop.

Time: O(n). Space: O(min(n, charset)).

Recall the contract step — the two things that happen each time the window is invalid:

python · fill in the blanks0/2 hints
def length_of_longest_substring(s):
window = set()
l = best = 0
for r in range(len(s)):
while s[r] in window: # window invalid (duplicate) -> contract
# ??? contract from the left
# ??? contract from the left
window.add(s[r]) # now valid -> add the new char
best = max(best, r - l + 1) # record the longest valid window
return best

Worked example: Longest Substring with At Most K Distinct Characters

Find the longest substring of s containing at most k distinct characters.

State: a hash map char → frequency. Invalid when: len(map) > k. Record: r − l + 1 after the shrink loop.

Time: O(n). Space: O(k).

Variant B — “Shortest valid window”

The window is allowed to be invalid most of the time. You only record the answer when it becomes valid, then immediately try to shrink further to see if a smaller window also works.

Worked example: Minimum Size Subarray Sum

Given a positive-integer array nums and target target, find the minimal length of a contiguous subarray of which the sum is >= target. Return 0 if no such subarray.

State: the running sum. Valid when: sum >= target. Record: r − l + 1 every time we’re valid, then try to shrink.

Time: O(n). Space: O(1).

Where does the answer update go? In “longest valid”, it’s after the shrink (window is just-barely valid, record its length). In “shortest valid”, it’s inside the shrink loop (every smaller window that’s still valid is a candidate). Get this swap wrong and your answers are off.

Worked example: Minimum Window Substring

Given two strings s and t, return the minimum window in s which contains all characters of t (including duplicates). Return "" if no such window.

This is the boss-level variable-window problem. State is a character frequency map for t, plus a “characters fully satisfied” counter.

State: need[c] = how many more of c we need. matches = how many distinct chars currently have count >= need[c]. Valid when: matches == distinct chars in t. Record: the (l, r) pair of the smallest valid window seen.

Time: O(|s| + |t|). Space: O(|s| + |t|).

The “match counter” trick from the fixed-window page returns here: instead of comparing two hash maps every iteration, we track how many distinct characters have hit their target count, and increment / decrement that counter in O(1) whenever a frequency crosses or moves away from the threshold.

Variant C — “Count of valid windows”

The third common shape: “How many contiguous subarrays satisfy property P?”

The standard move: at every iteration, count how many valid windows end at index r. The total is the sum across all r.

Worked example: Subarrays with Sum Less Than K

Given a positive-integer array nums and integer k, count the number of contiguous subarrays whose sum is strictly less than k.

State: running sum. Invalid when: sum >= k. Count contribution: every time the window is valid, every subarray ending at r that starts at >= l is valid. That’s r − l + 1 of them.

def subarrays_less_than_k(nums, k):
    l = total = count = 0
    for r in range(len(nums)):
        total += nums[r]
        while total >= k and l <= r:
            total -= nums[l]
            l += 1
        count += r - l + 1
    return count

Time: O(n). Space: O(1).

The r − l + 1 increment is the count contribution for the right endpoint at index r. Every valid subarray ends at exactly one r, so summing across all r gives the total.

Putting it all together

VariantWhen window invalidAnswer update locationWhat you record
Longestshrink until validAfter the shrink loopr − l + 1
Shortestnot allowed; valid means “found one we want”Inside the shrink loopr − l + 1, but track min
Countshrink until validAfter the shrink loopsum of r − l + 1

Memorize this table. It’s the difference between the right algorithm with the wrong answer and an actually correct submission.

A subtle gotcha: the shrink condition

Predict first
For a 'longest valid window' problem you shrink while the window is INVALID. For a 'shortest valid window' problem — do you shrink while invalid, or while it's still VALID?

For variable-size windows, the shrink loop runs while the window is INVALID (longest/count variants) or while it’s STILL VALID (shortest variant). Mixing those up is the most common subtle bug.

Problem flavorShrink while…
”Longest with property P”not P(window) (shrink until P holds)
“Shortest with property P”P(window) (keep shrinking as long as we’re still valid, recording each time)
“Count of subarrays with property P”not P(window) (count once we’re valid)

If your code returns 0 or the whole-array length for every input, the shrink condition is almost certainly flipped.

Quick check

In a variable-size sliding window, why is the inner `while` loop NOT an O(n²) trap?
In a 'longest valid' problem, where do you put the answer update — inside the shrink loop or after it?
For Minimum Window Substring, the 'match counter' trick lets us check window validity in:

Variable windows handle “longest / shortest / count of valid.” Next: the at-most-K trick — the slickest move in the sliding-window playbook, which turns “exactly K” problems into the difference of two “at most K” windows.

Finished this page?