Builds on Longest Window, which you haven’t finished yet.
Variable-Size Sliding Window (Minimum Window)
nums is an array of positive integers, and target is a positive integer. Among all contiguous subarrays whose sum reaches at least target, find the shortest one and return its length — or 0 if no subarray sums that high.
Shortest Window
left -> right -> (Expand to Validate, Shrink to Minimize)| 1 | from typing import Callable, List, Tuple |
| 2 | Â |
| 3 | Â |
| 4 | def shortest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], is_valid: Callable[[], bool]) -> Tuple[float, int]: |
| 5 | """Shortest Window: the shortest valid window [left, right] of indices 0..n-1, as (length, left). |
| 6 | Returns (0, 0) when the empty window is already valid, and (float('inf'), -1) when no window is valid. |
| 7 | Precondition: every longer window that holds a valid window is valid too. |
| 8 | add(i) and remove(i) update the window's state for index i. |
| 9 | Smallest Range Covering Elements from K Lists does not fit, because it measures a range of values, not a length. |
| 10 | T = the cost of one add, remove or is_valid call. |
| 11 | Time: O(N * T) | Space: O(1) besides the window's state |
| 12 | """ |
| 13 | if is_valid(): |
| 14 | return (0, 0) # Nothing needs to change: the empty window is already valid |
| 15 | Â |
| 16 | left = 0 |
| 17 | min_len, best_left = float('inf'), -1 # length and start of the shortest valid window found so far |
| 18 | Â |
| 19 | for right in range(n): |
| 20 | # 1. Expand Right Frontier: |
| 21 | # Growing the window with the next index can only help reach a valid window, since add never removes anything the window already needed. |
| 22 | add(right) |
| 23 | Â |
| 24 | # 2. While Valid, Record And Shrink: |
| 25 | # A window starting at left that is already valid stays valid or better as it grows, so any longer window from the same left is worse; record it, then shrink from left since no longer window from this left can beat the one just recorded. |
| 26 | while is_valid(): |
| 27 | if right - left + 1 < min_len: |
| 28 | min_len, best_left = right - left + 1, left |
| 29 | remove(left) |
| 30 | left += 1 |
| 31 | Â |
| 32 | return (min_len, best_left) |
curr_sum is the sum of nums[left..right], and every window that starts before left has already had its shortest valid length recorded.Grow the window by moving right. As soon as it is valid (curr_sum >= target), shrink it from left while it stays valid, recording its length each time.
Once a window starting at left is valid, any longer window from the same start is worse, so left can move on for good: neither pointer ever moves back, and no shorter window is skipped.
The trap is negative numbers: dropping nums[left] can then raise curr_sum, so shrinking can turn an invalid window valid again and the scan misses answers; Shortest Subarray with Sum at Least K (LC 862) allows negatives and needs prefix sums with a monotonic deque instead.
See it run
Shortest Window, step by step
left 0right -curr_sum 0min_len -
Each box is one entry of nums, with its index above it. The window runs from left to right.
leftthe first index of the windowrightthe last index of the windowleft, righta window of one entrywindowan entry between left and rightremovedthe entry that left the window
Start: left = 0 and curr_sum = 0. Move right to grow the window until curr_sum reaches 7.
Gotcha 1 of 4
- The trap
Gotcha 1: Using the shrinking window when
numshas negative numbersMistakeminSubArrayLen(k, nums)Fixshortest_subarray(nums, k)What happens: The loop shrinks by dropping
nums[left], and it expectscurr_sumto go down. A negativenums[left]makescurr_sumgo up instead. So the sum can fall belowtargetand then rise again as more values leave. The loop stops at the first fall and misses the shorter windows. Shortest Subarray with Sum at Least K (LC 862) allows negatives. It needs prefix sums with a monotonic deque, as inshortest_subarray.Example: Shortest Subarray with Sum at Least K (LC 862): nums = [1, -1, 5], k = 5Mistake: returns 3Fix: returns 1
- The question asks for the longest window that stays within a limit, as in Longest Substring Without Repeating Characters (LC 3): adding elements makes the window worse, not better. See Longest Window
- The sum must be exactly
kand values can be negative, as in Subarray Sum Equals K (LC 560): adding an element does not always move the window closer to valid. See Prefix Sum Map
Theory
The question we build
What we know
nums holds non-negative integers.
Adding an element to a window never lowers its sum, so a window that is valid stays valid as it grows.
What the problem gives us
The input is nums and target.
Return the length of the shortest contiguous subarray whose sum is at least target, or 0 if none exists.
The question we build
For each right, we ask: "is the sum of nums[left..right] at least target?"
Once that window is valid, is any longer window starting at the same left worth recording?
A longer window from the same left has a larger sum and a larger length, so it can never beat the one just found.
Narrowing down to the template
Which windows that end at right can still be the answer?
Once a window starting at left is valid, every longer window from the same left is worse (same or larger sum, more length), so left can move on for good.
So shrinking from left while the window stays valid never skips a shorter answer, and left never needs to move backward.
That is exactly Shortest Window: grow with right, and while the window is valid, record its length and shrink from left.
Step 1: Every window
Check every window (left, right), summing it from scratch.
Correct: the sum is recomputed for each window.
States the contract in one listing: the shortest window whose sum is at least target.
| 1 | def min_sub_array_len_brute(target, nums): |
| 2 | n = len(nums) |
| 3 | best = float('inf') |
| 4 | for left in range(n): |
| 5 | total = 0 |
| 6 | for right in range(left, n): |
| 7 | total += nums[right] |
| 8 | if total >= target: |
| 9 | best = min(best, right - left + 1) |
| 10 | break # Longer windows from this left are worse |
| 11 | return 0 if best == float('inf') else best |
Cost: O(N^2) time, O(1) extra space.
Step 2: Grow and shrink
Once a window starting at left is valid, every longer window from the same left is worse.
So grow with right, and while the window stays valid, record its length and shrink from left.
On input with negative numbers, dropping nums[left] can raise curr_sum instead of lowering it, so a shrink can turn an invalid window valid again and the scan misses the true shortest window.
| 1 | def minSubArrayLen(target: int, nums: List[int]) -> int: |
| 2 | left = 0 |
| 3 | curr_sum = 0 # sum of nums[left..right], the current window |
| 4 | min_len = float('inf') |
| 5 | Â |
| 6 | for right in range(len(nums)): |
| 7 | # 1. Expand Right Frontier: |
| 8 | # Growing the window with nums[right] can only help reach curr_sum >= target, since every value added only raises the sum. |
| 9 | curr_sum += nums[right] |
| 10 | Â |
| 11 | # 2. While Valid, Record And Shrink: |
| 12 | # A window starting at left that already reaches target stays at least as large as it grows, so any longer window from the same left is worse; record it, then shrink from left since no longer window from this left can beat the one just recorded. |
| 13 | while curr_sum >= target: |
| 14 | min_len = min(min_len, right - left + 1) |
| 15 | curr_sum -= nums[left] |
| 16 | left += 1 |
| 17 | Â |
| 18 | return 0 if min_len == float('inf') else min_len |
Cost: O(N) time, O(1) extra space.
Step 3: The named template
The loop does not change between problems.
Only add, remove and is_valid change.
| 1 | New line: from typing import Callable, Tuple |
| 2 | |
| 3 | New line: def shortest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], is_valid: Callable[[], bool]) -> Tuple[float, int]: |
| 4 | New line: """Shortest Window: the shortest valid window [left, right] of indices 0..n-1, as (length, left). |
| 5 | New line: Returns (0, 0) when the empty window is already valid, and (float('inf'), -1) when no window is valid. |
| 6 | New line: Precondition: every longer window that holds a valid window is valid too. |
| 7 | New line: add(i) and remove(i) update the window's state for index i. |
| 8 | New line: Smallest Range Covering Elements from K Lists does not fit, because it measures a range of values, not a length. |
| 9 | New line: T = the cost of one add, remove or is_valid call. |
| 10 | New line: Time: O(N * T) | Space: O(1) besides the window's state |
| 11 | New line: """ |
| 12 | New line: if is_valid(): |
| 13 | New line: return (0, 0) # Nothing needs to change: the empty window is already valid |
| 14 | |
| 15 | left = 0 |
| 16 | New line: min_len, best_left = float('inf'), -1 # length and start of the shortest valid window found so far |
| 17 | |
| 18 | New line: for right in range(n): |
| 19 | # 1. Expand Right Frontier: |
| 20 | New line: # Growing the window with the next index can only help reach a valid window, since add never removes anything the window already needed. |
| 21 | New line: add(right) |
| 22 | |
| 23 | # 2. While Valid, Record And Shrink: |
| 24 | New line: # A window starting at left that is already valid stays valid or better as it grows, so any longer window from the same left is worse; record it, then shrink from left since no longer window from this left can beat the one just recorded. |
| 25 | New line: while is_valid(): |
| 26 | New line: if right - left + 1 < min_len: |
| 27 | New line: min_len, best_left = right - left + 1, left |
| 28 | New line: remove(left) |
| 29 | left += 1 |
| 30 | |
| 31 | New line: return (min_len, best_left) |
Cost: O(N * T), where T is the cost of one add, remove or is_valid call.
| Before | After | What changed |
|---|---|---|
curr_sum += nums[right] | add(right) | Expanding the window becomes add. |
while curr_sum >= target: | while is_valid(): | The sum check becomes is_valid. |
curr_sum -= nums[left] | remove(left) | Contracting the window becomes remove. |
min_len = min(min_len, right - left + 1) | min_len, best_left = right - left + 1, left (when smaller) | The template also remembers where the shortest window started. |
Practice
Builds on Longest Window, which you haven’t finished yet.
Variable-Size Sliding Window (Minimum Window)
nums is an array of positive integers, and target is a positive integer. Among all contiguous subarrays whose sum reaches at least target, find the shortest one and return its length — or 0 if no subarray sums that high.
Shortest Window
left -> right -> (Expand to Validate, Shrink to Minimize)| 1 | from typing import Callable, List, Tuple |
| 2 | Â |
| 3 | Â |
| 4 | def shortest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], is_valid: Callable[[], bool]) -> Tuple[float, int]: |
| 5 | """Shortest Window: the shortest valid window [left, right] of indices 0..n-1, as (length, left). |
| 6 | Returns (0, 0) when the empty window is already valid, and (float('inf'), -1) when no window is valid. |
| 7 | Precondition: every longer window that holds a valid window is valid too. |
| 8 | add(i) and remove(i) update the window's state for index i. |
| 9 | Smallest Range Covering Elements from K Lists does not fit, because it measures a range of values, not a length. |
| 10 | T = the cost of one add, remove or is_valid call. |
| 11 | Time: O(N * T) | Space: O(1) besides the window's state |
| 12 | """ |
| 13 | if is_valid(): |
| 14 | return (0, 0) # Nothing needs to change: the empty window is already valid |
| 15 | Â |
| 16 | left = 0 |
| 17 | min_len, best_left = float('inf'), -1 # length and start of the shortest valid window found so far |
| 18 | Â |
| 19 | for right in range(n): |
| 20 | # 1. Expand Right Frontier: |
| 21 | # Growing the window with the next index can only help reach a valid window, since add never removes anything the window already needed. |
| 22 | add(right) |
| 23 | Â |
| 24 | # 2. While Valid, Record And Shrink: |
| 25 | # A window starting at left that is already valid stays valid or better as it grows, so any longer window from the same left is worse; record it, then shrink from left since no longer window from this left can beat the one just recorded. |
| 26 | while is_valid(): |
| 27 | if right - left + 1 < min_len: |
| 28 | min_len, best_left = right - left + 1, left |
| 29 | remove(left) |
| 30 | left += 1 |
| 31 | Â |
| 32 | return (min_len, best_left) |
curr_sum is the sum of nums[left..right], and every window that starts before left has already had its shortest valid length recorded.Grow the window by moving right. As soon as it is valid (curr_sum >= target), shrink it from left while it stays valid, recording its length each time.
Once a window starting at left is valid, any longer window from the same start is worse, so left can move on for good: neither pointer ever moves back, and no shorter window is skipped.
The trap is negative numbers: dropping nums[left] can then raise curr_sum, so shrinking can turn an invalid window valid again and the scan misses answers; Shortest Subarray with Sum at Least K (LC 862) allows negatives and needs prefix sums with a monotonic deque instead.
See it run
Shortest Window, step by step
left 0right -curr_sum 0min_len -
Each box is one entry of nums, with its index above it. The window runs from left to right.
leftthe first index of the windowrightthe last index of the windowleft, righta window of one entrywindowan entry between left and rightremovedthe entry that left the window
Start: left = 0 and curr_sum = 0. Move right to grow the window until curr_sum reaches 7.
Gotcha 1 of 4
- The trap
Gotcha 1: Using the shrinking window when
numshas negative numbersMistakeminSubArrayLen(k, nums)Fixshortest_subarray(nums, k)What happens: The loop shrinks by dropping
nums[left], and it expectscurr_sumto go down. A negativenums[left]makescurr_sumgo up instead. So the sum can fall belowtargetand then rise again as more values leave. The loop stops at the first fall and misses the shorter windows. Shortest Subarray with Sum at Least K (LC 862) allows negatives. It needs prefix sums with a monotonic deque, as inshortest_subarray.Example: Shortest Subarray with Sum at Least K (LC 862): nums = [1, -1, 5], k = 5Mistake: returns 3Fix: returns 1
- The question asks for the longest window that stays within a limit, as in Longest Substring Without Repeating Characters (LC 3): adding elements makes the window worse, not better. See Longest Window
- The sum must be exactly
kand values can be negative, as in Subarray Sum Equals K (LC 560): adding an element does not always move the window closer to valid. See Prefix Sum Map
Theory
The question we build
What we know
nums holds non-negative integers.
Adding an element to a window never lowers its sum, so a window that is valid stays valid as it grows.
What the problem gives us
The input is nums and target.
Return the length of the shortest contiguous subarray whose sum is at least target, or 0 if none exists.
The question we build
For each right, we ask: "is the sum of nums[left..right] at least target?"
Once that window is valid, is any longer window starting at the same left worth recording?
A longer window from the same left has a larger sum and a larger length, so it can never beat the one just found.
Narrowing down to the template
Which windows that end at right can still be the answer?
Once a window starting at left is valid, every longer window from the same left is worse (same or larger sum, more length), so left can move on for good.
So shrinking from left while the window stays valid never skips a shorter answer, and left never needs to move backward.
That is exactly Shortest Window: grow with right, and while the window is valid, record its length and shrink from left.
Step 1: Every window
Check every window (left, right), summing it from scratch.
Correct: the sum is recomputed for each window.
States the contract in one listing: the shortest window whose sum is at least target.
| 1 | def min_sub_array_len_brute(target, nums): |
| 2 | n = len(nums) |
| 3 | best = float('inf') |
| 4 | for left in range(n): |
| 5 | total = 0 |
| 6 | for right in range(left, n): |
| 7 | total += nums[right] |
| 8 | if total >= target: |
| 9 | best = min(best, right - left + 1) |
| 10 | break # Longer windows from this left are worse |
| 11 | return 0 if best == float('inf') else best |
Cost: O(N^2) time, O(1) extra space.
Step 2: Grow and shrink
Once a window starting at left is valid, every longer window from the same left is worse.
So grow with right, and while the window stays valid, record its length and shrink from left.
On input with negative numbers, dropping nums[left] can raise curr_sum instead of lowering it, so a shrink can turn an invalid window valid again and the scan misses the true shortest window.
| 1 | def minSubArrayLen(target: int, nums: List[int]) -> int: |
| 2 | left = 0 |
| 3 | curr_sum = 0 # sum of nums[left..right], the current window |
| 4 | min_len = float('inf') |
| 5 | Â |
| 6 | for right in range(len(nums)): |
| 7 | # 1. Expand Right Frontier: |
| 8 | # Growing the window with nums[right] can only help reach curr_sum >= target, since every value added only raises the sum. |
| 9 | curr_sum += nums[right] |
| 10 | Â |
| 11 | # 2. While Valid, Record And Shrink: |
| 12 | # A window starting at left that already reaches target stays at least as large as it grows, so any longer window from the same left is worse; record it, then shrink from left since no longer window from this left can beat the one just recorded. |
| 13 | while curr_sum >= target: |
| 14 | min_len = min(min_len, right - left + 1) |
| 15 | curr_sum -= nums[left] |
| 16 | left += 1 |
| 17 | Â |
| 18 | return 0 if min_len == float('inf') else min_len |
Cost: O(N) time, O(1) extra space.
Step 3: The named template
The loop does not change between problems.
Only add, remove and is_valid change.
| 1 | New line: from typing import Callable, Tuple |
| 2 | |
| 3 | New line: def shortest_window(n: int, add: Callable[[int], None], remove: Callable[[int], None], is_valid: Callable[[], bool]) -> Tuple[float, int]: |
| 4 | New line: """Shortest Window: the shortest valid window [left, right] of indices 0..n-1, as (length, left). |
| 5 | New line: Returns (0, 0) when the empty window is already valid, and (float('inf'), -1) when no window is valid. |
| 6 | New line: Precondition: every longer window that holds a valid window is valid too. |
| 7 | New line: add(i) and remove(i) update the window's state for index i. |
| 8 | New line: Smallest Range Covering Elements from K Lists does not fit, because it measures a range of values, not a length. |
| 9 | New line: T = the cost of one add, remove or is_valid call. |
| 10 | New line: Time: O(N * T) | Space: O(1) besides the window's state |
| 11 | New line: """ |
| 12 | New line: if is_valid(): |
| 13 | New line: return (0, 0) # Nothing needs to change: the empty window is already valid |
| 14 | |
| 15 | left = 0 |
| 16 | New line: min_len, best_left = float('inf'), -1 # length and start of the shortest valid window found so far |
| 17 | |
| 18 | New line: for right in range(n): |
| 19 | # 1. Expand Right Frontier: |
| 20 | New line: # Growing the window with the next index can only help reach a valid window, since add never removes anything the window already needed. |
| 21 | New line: add(right) |
| 22 | |
| 23 | # 2. While Valid, Record And Shrink: |
| 24 | New line: # A window starting at left that is already valid stays valid or better as it grows, so any longer window from the same left is worse; record it, then shrink from left since no longer window from this left can beat the one just recorded. |
| 25 | New line: while is_valid(): |
| 26 | New line: if right - left + 1 < min_len: |
| 27 | New line: min_len, best_left = right - left + 1, left |
| 28 | New line: remove(left) |
| 29 | left += 1 |
| 30 | |
| 31 | New line: return (min_len, best_left) |
Cost: O(N * T), where T is the cost of one add, remove or is_valid call.
| Before | After | What changed |
|---|---|---|
curr_sum += nums[right] | add(right) | Expanding the window becomes add. |
while curr_sum >= target: | while is_valid(): | The sum check becomes is_valid. |
curr_sum -= nums[left] | remove(left) | Contracting the window becomes remove. |
min_len = min(min_len, right - left + 1) | min_len, best_left = right - left + 1, left (when smaller) | The template also remembers where the shortest window started. |