Builds on Fixed Window, which you haven’t finished yet.
Variable-Size Sliding Window (Longest Window)
You have a string s. Among all of its contiguous substrings, find the longest one that never repeats a character, and report its length.
Longest Window
left -> right -> (Check Before Growing, Contract To Repair)| 1 | from typing import Callable |
| 2 | Â |
| 3 | Â |
| 4 | def longest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], would_be_valid: Callable[[int], bool]) -> int: |
| 5 | """Longest Window: the length of the longest valid window [left, right] of indices 0..n-1. |
| 6 | Precondition: every shorter window inside a valid window is valid too, and the empty window is valid. |
| 7 | add(i) and remove(i) update the window's state for index i. |
| 8 | would_be_valid(i) reads whether adding index i to the current window would keep it valid, without changing the window's state. |
| 9 | T = the cost of one add, remove or would_be_valid call. |
| 10 | Time: O(N * T) | Space: O(1) besides the window's state |
| 11 | """ |
| 12 | left = 0 |
| 13 | max_len = 0 |
| 14 | Â |
| 15 | for right in range(n): |
| 16 | # 1. Repair By Evicting From left: |
| 17 | # would_be_valid(right) asks whether adding index right would still be a valid window, without changing the window's state. Every window [j, right] with j < left contains everything [left, right] contains, so if adding right to [left, right] would still be invalid, no later left can keep right out of it; evicting left is the only way back to a window that right can be added to. |
| 18 | while not would_be_valid(right): |
| 19 | remove(left) |
| 20 | left += 1 |
| 21 | # 2. Absorb Index right: |
| 22 | # Once would_be_valid(right) confirms the window stays valid, right is actually added. |
| 23 | add(right) |
| 24 | # 3. Record Only After The Add: |
| 25 | # The window [left, right] is now valid and fully updated, so its length can be measured. |
| 26 | max_len = max(max_len, right - left + 1) |
| 27 | Â |
| 28 | return max_len |
max_len is updated, seen_chars is exactly the set of characters in s[left..right], with no repeats.Before you add right to the window, check whether adding it would break the rule. If it would, move left forward until adding right is safe, then add right and record the window's length.
The window is always valid at the moment you add right, because right is only added once the check says adding it keeps the window valid, so the largest measurement is the answer. Moving left never loses a longer answer: any window that starts before left already contains everything [left, right] contains, so adding right to it would break the rule too.
The trap is answering the check by mutating the window's state to test it: the check must read what adding right would do without changing the window, or the add call right after it double-counts right.
See it run
Longest Window, step by step
left 0right -seen_chars {}max_len 0
Each box is one character of s, with its index above it. The window holds no repeated character at the moment max_len is measured.
leftthe first index of the windowrightthe index being addedleft, rightleft and right on the same entrywindowan entry inside the windowevictedthe entry that just left the window
Start: left = 0, seen_chars is empty and max_len = 0. For each right, check first whether adding s[right] makes a repeat.
Gotcha 1 of 4
- The trap
Gotcha 1: Measuring the window before the
whileloop repairs itMistakefor right in range(len(s)): max_len = max(max_len, right - left + 1) while s[right] in seen_chars:Fixfor right in range(len(s)): while s[right] in seen_chars:What happens: Before the
whileloop,s[right]may already be in the window. Then the windows[left..right]holds a letter twice. A length measured there counts an invalid window, somax_lencan become too large. Thewhileloop movesleftuntil the repeat is gone. After that loop,right - left + 1is the length of a valid window.Example: s = "abcabcbb"Mistake: returns 4Fix: returns 3
- The question asks for the shortest window that meets a requirement, as in Minimum Window Substring (LC 76): adding elements helps the window instead of breaking it. See Shortest Window
- The window length is fixed by the input, as in Find All Anagrams in a String (LC 438): every window has the size of
p, so there is no longest one to find. See Fixed Window
Theory
The question we build
What we know
s is a sequence of characters.
A window holds a repeat, or it does not; adding a character can only turn a valid window invalid, never the other way.
What the problem gives us
The input is s.
Return the length of the longest substring with no repeated character.
The question we build
For each right, we ask: "does the window s[left..right] hold a repeat?"
When it does, which starts can still give a valid window ending at right?
A window [j, right] with j < left contains everything [left, right] contains, plus more, so if [left, right] already holds the repeat, [j, right] holds it too.
Narrowing down to the template
Which windows that end at right can still be the answer?
If the window [left, right] holds a repeat, every window [j, right] with j < left holds it too, so left only moves right.
So once left passes a start, no window from that start, ending anywhere at or after right, can ever become valid again: left never needs to move backward.
That is exactly Longest Window: check whether adding right is still safe, repair by moving left forward until it is, then add right and measure.
Step 1: Every window
Check every window (left, right) with a fresh set.
Correct: a repeat is caught by set membership.
States the contract in one listing: the longest window with no repeated character.
| 1 | def length_of_longest_brute(s): |
| 2 | n = len(s) |
| 3 | best = 0 |
| 4 | for left in range(n): |
| 5 | seen = set() |
| 6 | for right in range(left, n): |
| 7 | if s[right] in seen: |
| 8 | break # A fresh set per left: this window holds a repeat |
| 9 | seen.add(s[right]) |
| 10 | best = max(best, right - left + 1) |
| 11 | return best |
Cost: O(N^2) time, O(N) extra space.
Step 2: Check and repair
If adding right to the window [left, right - 1] would hold a repeat, adding right to any window [j, right - 1] with j < left would hold it too.
So left never needs to move backward: before adding right, check whether it would create a repeat, and while it would, evict from left.
Only once the while loop confirms adding right is safe is right actually added, so measuring after the add never counts a window that holds a repeat.
| 1 | def lengthOfLongestSubstring(s: str) -> int: |
| 2 | seen_chars = set() # characters currently inside the window s[left..right - 1] |
| 3 | left = 0 |
| 4 | max_len = 0 |
| 5 | Â |
| 6 | # 1. Repair By Evicting From left: |
| 7 | # s[right] in seen_chars asks whether adding s[right] would create a repeat, without changing seen_chars. Every window [j, right] with j < left still holds the same duplicate, so left is the only pointer that can move. |
| 8 | for right in range(len(s)): |
| 9 | while s[right] in seen_chars: |
| 10 | seen_chars.remove(s[left]) |
| 11 | left += 1 |
| 12 | Â |
| 13 | # 2. Absorb Index right: |
| 14 | # Once the while loop confirms s[right] is not in seen_chars, it is actually added. |
| 15 | seen_chars.add(s[right]) |
| 16 | # 3. Record Only After The Add: |
| 17 | max_len = max(max_len, right - left + 1) |
| 18 | Â |
| 19 | return max_len |
Cost: O(N) time, O(min(N, Sigma)) space, where Sigma is the number of different characters.
Step 3: The named template
The loop does not change between problems.
Only add, remove and would_be_valid change.
| 1 | New line: from typing import Callable |
| 2 | |
| 3 | New line: def longest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], would_be_valid: Callable[[int], bool]) -> int: |
| 4 | New line: """Longest Window: the length of the longest valid window [left, right] of indices 0..n-1. |
| 5 | New line: Precondition: every shorter window inside a valid window is valid too, and the empty window is valid. |
| 6 | New line: add(i) and remove(i) update the window's state for index i. |
| 7 | New line: would_be_valid(i) reads whether adding index i to the current window would keep it valid, without changing the window's state. |
| 8 | New line: T = the cost of one add, remove or would_be_valid call. |
| 9 | New line: Time: O(N * T) | Space: O(1) besides the window's state |
| 10 | New line: """ |
| 11 | left = 0 |
| 12 | max_len = 0 |
| 13 | |
| 14 | New line: for right in range(n): |
| 15 | New line: # 1. Repair By Evicting From left: |
| 16 | New line: # would_be_valid(right) asks whether adding index right would still be a valid window, without changing the window's state. Every window [j, right] with j < left contains everything [left, right] contains, so if adding right to [left, right] would still be invalid, no later left can keep right out of it; evicting left is the only way back to a window that right can be added to. |
| 17 | New line: while not would_be_valid(right): |
| 18 | New line: remove(left) |
| 19 | left += 1 |
| 20 | # 2. Absorb Index right: |
| 21 | New line: # Once would_be_valid(right) confirms the window stays valid, right is actually added. |
| 22 | New line: add(right) |
| 23 | # 3. Record Only After The Add: |
| 24 | New line: # The window [left, right] is now valid and fully updated, so its length can be measured. |
| 25 | max_len = max(max_len, right - left + 1) |
| 26 | |
| 27 | return max_len |
Cost: O(N * T), where T is the cost of one add, remove or would_be_valid call.
| Before | After | What changed |
|---|---|---|
while s[right] in seen_chars: seen_chars.remove(s[left]); left += 1 | while not would_be_valid(right): remove(left); left += 1 | The repeat check becomes would_be_valid(right), which reads whether adding right would still be valid without changing the window, and the eviction becomes remove. |
seen_chars.add(s[right]) | add(right) | Absorbing the incoming character becomes add, still called only after would_be_valid(right) confirms the window stays valid. |
max_len = max(max_len, right - left + 1) | max_len = max(max_len, right - left + 1) | Identical, still after the add. |
Practice
Builds on Fixed Window, which you haven’t finished yet.
Variable-Size Sliding Window (Longest Window)
You have a string s. Among all of its contiguous substrings, find the longest one that never repeats a character, and report its length.
Longest Window
left -> right -> (Check Before Growing, Contract To Repair)| 1 | from typing import Callable |
| 2 | Â |
| 3 | Â |
| 4 | def longest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], would_be_valid: Callable[[int], bool]) -> int: |
| 5 | """Longest Window: the length of the longest valid window [left, right] of indices 0..n-1. |
| 6 | Precondition: every shorter window inside a valid window is valid too, and the empty window is valid. |
| 7 | add(i) and remove(i) update the window's state for index i. |
| 8 | would_be_valid(i) reads whether adding index i to the current window would keep it valid, without changing the window's state. |
| 9 | T = the cost of one add, remove or would_be_valid call. |
| 10 | Time: O(N * T) | Space: O(1) besides the window's state |
| 11 | """ |
| 12 | left = 0 |
| 13 | max_len = 0 |
| 14 | Â |
| 15 | for right in range(n): |
| 16 | # 1. Repair By Evicting From left: |
| 17 | # would_be_valid(right) asks whether adding index right would still be a valid window, without changing the window's state. Every window [j, right] with j < left contains everything [left, right] contains, so if adding right to [left, right] would still be invalid, no later left can keep right out of it; evicting left is the only way back to a window that right can be added to. |
| 18 | while not would_be_valid(right): |
| 19 | remove(left) |
| 20 | left += 1 |
| 21 | # 2. Absorb Index right: |
| 22 | # Once would_be_valid(right) confirms the window stays valid, right is actually added. |
| 23 | add(right) |
| 24 | # 3. Record Only After The Add: |
| 25 | # The window [left, right] is now valid and fully updated, so its length can be measured. |
| 26 | max_len = max(max_len, right - left + 1) |
| 27 | Â |
| 28 | return max_len |
max_len is updated, seen_chars is exactly the set of characters in s[left..right], with no repeats.Before you add right to the window, check whether adding it would break the rule. If it would, move left forward until adding right is safe, then add right and record the window's length.
The window is always valid at the moment you add right, because right is only added once the check says adding it keeps the window valid, so the largest measurement is the answer. Moving left never loses a longer answer: any window that starts before left already contains everything [left, right] contains, so adding right to it would break the rule too.
The trap is answering the check by mutating the window's state to test it: the check must read what adding right would do without changing the window, or the add call right after it double-counts right.
See it run
Longest Window, step by step
left 0right -seen_chars {}max_len 0
Each box is one character of s, with its index above it. The window holds no repeated character at the moment max_len is measured.
leftthe first index of the windowrightthe index being addedleft, rightleft and right on the same entrywindowan entry inside the windowevictedthe entry that just left the window
Start: left = 0, seen_chars is empty and max_len = 0. For each right, check first whether adding s[right] makes a repeat.
Gotcha 1 of 4
- The trap
Gotcha 1: Measuring the window before the
whileloop repairs itMistakefor right in range(len(s)): max_len = max(max_len, right - left + 1) while s[right] in seen_chars:Fixfor right in range(len(s)): while s[right] in seen_chars:What happens: Before the
whileloop,s[right]may already be in the window. Then the windows[left..right]holds a letter twice. A length measured there counts an invalid window, somax_lencan become too large. Thewhileloop movesleftuntil the repeat is gone. After that loop,right - left + 1is the length of a valid window.Example: s = "abcabcbb"Mistake: returns 4Fix: returns 3
- The question asks for the shortest window that meets a requirement, as in Minimum Window Substring (LC 76): adding elements helps the window instead of breaking it. See Shortest Window
- The window length is fixed by the input, as in Find All Anagrams in a String (LC 438): every window has the size of
p, so there is no longest one to find. See Fixed Window
Theory
The question we build
What we know
s is a sequence of characters.
A window holds a repeat, or it does not; adding a character can only turn a valid window invalid, never the other way.
What the problem gives us
The input is s.
Return the length of the longest substring with no repeated character.
The question we build
For each right, we ask: "does the window s[left..right] hold a repeat?"
When it does, which starts can still give a valid window ending at right?
A window [j, right] with j < left contains everything [left, right] contains, plus more, so if [left, right] already holds the repeat, [j, right] holds it too.
Narrowing down to the template
Which windows that end at right can still be the answer?
If the window [left, right] holds a repeat, every window [j, right] with j < left holds it too, so left only moves right.
So once left passes a start, no window from that start, ending anywhere at or after right, can ever become valid again: left never needs to move backward.
That is exactly Longest Window: check whether adding right is still safe, repair by moving left forward until it is, then add right and measure.
Step 1: Every window
Check every window (left, right) with a fresh set.
Correct: a repeat is caught by set membership.
States the contract in one listing: the longest window with no repeated character.
| 1 | def length_of_longest_brute(s): |
| 2 | n = len(s) |
| 3 | best = 0 |
| 4 | for left in range(n): |
| 5 | seen = set() |
| 6 | for right in range(left, n): |
| 7 | if s[right] in seen: |
| 8 | break # A fresh set per left: this window holds a repeat |
| 9 | seen.add(s[right]) |
| 10 | best = max(best, right - left + 1) |
| 11 | return best |
Cost: O(N^2) time, O(N) extra space.
Step 2: Check and repair
If adding right to the window [left, right - 1] would hold a repeat, adding right to any window [j, right - 1] with j < left would hold it too.
So left never needs to move backward: before adding right, check whether it would create a repeat, and while it would, evict from left.
Only once the while loop confirms adding right is safe is right actually added, so measuring after the add never counts a window that holds a repeat.
| 1 | def lengthOfLongestSubstring(s: str) -> int: |
| 2 | seen_chars = set() # characters currently inside the window s[left..right - 1] |
| 3 | left = 0 |
| 4 | max_len = 0 |
| 5 | Â |
| 6 | # 1. Repair By Evicting From left: |
| 7 | # s[right] in seen_chars asks whether adding s[right] would create a repeat, without changing seen_chars. Every window [j, right] with j < left still holds the same duplicate, so left is the only pointer that can move. |
| 8 | for right in range(len(s)): |
| 9 | while s[right] in seen_chars: |
| 10 | seen_chars.remove(s[left]) |
| 11 | left += 1 |
| 12 | Â |
| 13 | # 2. Absorb Index right: |
| 14 | # Once the while loop confirms s[right] is not in seen_chars, it is actually added. |
| 15 | seen_chars.add(s[right]) |
| 16 | # 3. Record Only After The Add: |
| 17 | max_len = max(max_len, right - left + 1) |
| 18 | Â |
| 19 | return max_len |
Cost: O(N) time, O(min(N, Sigma)) space, where Sigma is the number of different characters.
Step 3: The named template
The loop does not change between problems.
Only add, remove and would_be_valid change.
| 1 | New line: from typing import Callable |
| 2 | |
| 3 | New line: def longest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], would_be_valid: Callable[[int], bool]) -> int: |
| 4 | New line: """Longest Window: the length of the longest valid window [left, right] of indices 0..n-1. |
| 5 | New line: Precondition: every shorter window inside a valid window is valid too, and the empty window is valid. |
| 6 | New line: add(i) and remove(i) update the window's state for index i. |
| 7 | New line: would_be_valid(i) reads whether adding index i to the current window would keep it valid, without changing the window's state. |
| 8 | New line: T = the cost of one add, remove or would_be_valid call. |
| 9 | New line: Time: O(N * T) | Space: O(1) besides the window's state |
| 10 | New line: """ |
| 11 | left = 0 |
| 12 | max_len = 0 |
| 13 | |
| 14 | New line: for right in range(n): |
| 15 | New line: # 1. Repair By Evicting From left: |
| 16 | New line: # would_be_valid(right) asks whether adding index right would still be a valid window, without changing the window's state. Every window [j, right] with j < left contains everything [left, right] contains, so if adding right to [left, right] would still be invalid, no later left can keep right out of it; evicting left is the only way back to a window that right can be added to. |
| 17 | New line: while not would_be_valid(right): |
| 18 | New line: remove(left) |
| 19 | left += 1 |
| 20 | # 2. Absorb Index right: |
| 21 | New line: # Once would_be_valid(right) confirms the window stays valid, right is actually added. |
| 22 | New line: add(right) |
| 23 | # 3. Record Only After The Add: |
| 24 | New line: # The window [left, right] is now valid and fully updated, so its length can be measured. |
| 25 | max_len = max(max_len, right - left + 1) |
| 26 | |
| 27 | return max_len |
Cost: O(N * T), where T is the cost of one add, remove or would_be_valid call.
| Before | After | What changed |
|---|---|---|
while s[right] in seen_chars: seen_chars.remove(s[left]); left += 1 | while not would_be_valid(right): remove(left); left += 1 | The repeat check becomes would_be_valid(right), which reads whether adding right would still be valid without changing the window, and the eviction becomes remove. |
seen_chars.add(s[right]) | add(right) | Absorbing the incoming character becomes add, still called only after would_be_valid(right) confirms the window stays valid. |
max_len = max(max_len, right - left + 1) | max_len = max(max_len, right - left + 1) | Identical, still after the add. |