76. Minimum Window Substring
Problem Description
You are given two strings s
and t
with lengths m
and n
respectively. Your task is to find the minimum window substring in s
that contains all characters from t
, including duplicates.
A window substring means a contiguous sequence of characters within s
. The window must contain every character that appears in t
, with at least the same frequency. For example, if t = "AAB"
, then the window must contain at least two 'A's and one 'B'.
The goal is to find the shortest such substring. If multiple substrings of the same minimum length exist, you can return any one of them (the problem guarantees the answer is unique). If no valid window exists that contains all characters from t
, return an empty string ""
.
Key requirements:
- The window must be a substring (contiguous characters) of
s
- Every character in
t
must appear in the window with at least the same frequency as int
- You need to find the minimum length window that satisfies these conditions
- If no valid window exists, return an empty string
Example scenarios:
- If
s = "ADOBECODEBANC"
andt = "ABC"
, the minimum window would be"BANC"
which contains all characters A, B, and C - If
s = "a"
andt = "aa"
, no valid window exists sinces
doesn't have enough 'a's, so return""
- If
s = "ab"
andt = "b"
, the minimum window would be"b"
Intuition
The key insight is that we need to find a substring that contains all characters from t
, and we want this substring to be as short as possible. A brute force approach would check every possible substring of s
, but that would be inefficient with time complexity O(n^3)
.
Instead, we can use a sliding window technique. Think of it like a flexible window that can expand and contract as we move through the string s
. The idea is:
- Expand the window by moving the right pointer to include more characters until we have all characters from
t
- Once we have a valid window, contract it by moving the left pointer to make it as small as possible while still containing all required characters
- Record the minimum valid window found so far
To efficiently track whether our current window contains all characters from t
, we use two key components:
- A frequency map
need
that tells us how many of each character we need fromt
- A frequency map
window
that tracks what we currently have in our sliding window - A counter
cnt
that tracks how many characters we've satisfied (matching the required frequency)
The clever part is knowing when we have a valid window. Instead of checking the entire frequency map each time, we maintain a count cnt
of how many characters from t
are satisfied in our current window. When cnt
equals the length of t
, we know we have all required characters.
The algorithm naturally finds the minimum window because:
- We only expand when we don't have enough characters
- We always try to contract when we have a valid window
- We keep track of the smallest valid window seen throughout the process
This approach ensures we examine each character at most twice (once when expanding, once when contracting), giving us an efficient O(m + n)
solution where m
and n
are the lengths of strings s
and t
.
Learn more about Sliding Window patterns.
Solution Approach
We implement the sliding window technique using two pointers and frequency counting:
1. Initialize Data Structures:
need = Counter(t)
: Count the frequency of each character in stringt
. This tells us what we need to find.window = Counter()
: Track the frequency of characters in our current sliding window.cnt = 0
: Count how many characters fromt
are satisfied in the current window.l = 0
: Left pointer of the sliding window.k = -1, mi = inf
: Track the starting position and length of the minimum window found so far.
2. Expand the Window (Right Pointer Movement):
Iterate through string s
using index r
and character c
:
- Add the current character to the window:
window[c] += 1
- Check if this character contributes to our requirement: If
need[c] >= window[c]
, it means this character instance is "necessary" (we still need more of this character or just got enough), so incrementcnt += 1
3. Contract the Window (Left Pointer Movement):
When cnt == len(t)
, we have a valid window containing all characters from t
:
- First, try to update the minimum window: If
r - l + 1 < mi
, we found a shorter window, so updatemi = r - l + 1
andk = l
- Then, try to shrink the window from the left:
- Check if removing
s[l]
would break the validity: Ifneed[s[l]] >= window[s[l]]
, removing this character would make the window invalid, so decrementcnt -= 1
- Remove the leftmost character:
window[s[l]] -= 1
- Move the left pointer:
l += 1
- Check if removing
- Continue shrinking while the window remains valid (
cnt == len(t)
)
4. Return the Result:
- If
k < 0
, no valid window was found, return empty string""
- Otherwise, return the substring
s[k : k + mi]
Key Algorithm Details:
The condition need[c] >= window[c]
is crucial for correctness:
- When expanding: It ensures we only increment
cnt
for characters that are actually needed - When contracting: It tells us when removing a character would make the window invalid
The algorithm maintains the invariant that cnt
represents the total count of characters from t
that are satisfied in the current window. This allows us to check window validity in O(1)
time instead of comparing entire frequency maps.
Time and Space Complexity:
- Time:
O(m + n)
wherem = len(s)
andn = len(t)
. Each character ins
is visited at most twice. - Space:
O(|Σ|)
where|Σ|
is the size of the character set (constant for ASCII/Unicode).
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the algorithm with s = "ADOBECODEBANC"
and t = "ABC"
.
Initial Setup:
need = {'A': 1, 'B': 1, 'C': 1}
(we need 1 of each character)window = {}
(empty window)cnt = 0
(no characters satisfied yet)l = 0
(left pointer at start)k = -1, mi = inf
(no minimum window found yet)
Expanding Phase (r = 0 to 5):
r | char | window | cnt | Action |
---|---|---|---|---|
0 | A | {'A': 1} | 1 | Add A, need[A]=1 >= window[A]=1, cnt++ |
1 | D | {'A': 1, 'D': 1} | 1 | Add D, D not needed |
2 | O | {'A': 1, 'D': 1, 'O': 1} | 1 | Add O, O not needed |
3 | B | {'A': 1, 'D': 1, 'O': 1, 'B': 1} | 2 | Add B, need[B]=1 >= window[B]=1, cnt++ |
4 | E | {'A': 1, 'D': 1, 'O': 1, 'B': 1, 'E': 1} | 2 | Add E, E not needed |
5 | C | {'A': 1, 'D': 1, 'O': 1, 'B': 1, 'E': 1, 'C': 1} | 3 | Add C, need[C]=1 >= window[C]=1, cnt++ |
First Valid Window Found (cnt = 3):
- Window is "ADOBEC" (indices 0-5), length = 6
- Update:
mi = 6, k = 0
Contracting Phase: Now we try to shrink from left while maintaining validity:
l | Remove | window after removal | cnt | Can continue? |
---|---|---|---|---|
0 | A | {'D': 1, 'O': 1, 'B': 1, 'E': 1, 'C': 1} | 2 | No, A was needed |
Since removing 'A' breaks validity (cnt < 3), we stop contracting and continue expanding.
Continue Expanding (r = 6 to 9):
r | char | window | cnt | Action |
---|---|---|---|---|
6 | O | {..., 'O': 2} | 2 | Add O, O not needed |
7 | D | {..., 'D': 2} | 2 | Add D, D not needed |
8 | E | {..., 'E': 2} | 2 | Add E, E not needed |
9 | B | {..., 'B': 2} | 2 | Add B, B already satisfied |
Still not valid (cnt = 2), keep expanding.
r = 10 (char = 'A'):
- Add A:
window['A'] = 1
, nowcnt = 3
again - Current window is "DOBECCODEBA" (indices 1-10), but we need to contract
Second Contraction Phase: Window is valid again. Current substring length = 10, not better than mi = 6.
Try shrinking from left (l = 1):
l | Remove | Effect |
---|---|---|
1 | D | D not needed, can remove |
2 | O | O not needed, can remove |
3 | B | B needed! need[B]=1 >= window[B]=2, but after removal window[B]=1, still valid |
4 | E | E not needed, can remove |
5 | C | C needed! need[C]=1 >= window[C]=1, removing breaks validity |
After removing D, O, B, E, we have window "CODEBA" (indices 5-10), length = 6 (same as current minimum).
Continue to r = 12 (char = 'C'): After more expansion and contraction, we eventually find:
- Window "BANC" (indices 9-12)
- Length = 4
- Update:
mi = 4, k = 9
Final Result:
Return s[9:13] = "BANC"
The algorithm efficiently found the minimum window by expanding only when needed and contracting whenever possible, examining each character at most twice.
Solution Implementation
1from collections import Counter
2from math import inf
3
4class Solution:
5 def minWindow(self, s: str, t: str) -> str:
6 # Count frequency of each character in target string t
7 target_freq = Counter(t)
8
9 # Track frequency of characters in current window
10 window_freq = Counter()
11
12 # Count of characters in window that match required frequency
13 matched_chars = 0
14
15 # Left pointer of sliding window
16 left = 0
17
18 # Track the start index and length of minimum window found
19 min_start = -1
20 min_length = inf
21
22 # Expand window by moving right pointer
23 for right, char in enumerate(s):
24 # Add current character to window
25 window_freq[char] += 1
26
27 # Check if this character contributes to matching t
28 if char in target_freq and window_freq[char] <= target_freq[char]:
29 matched_chars += 1
30
31 # Contract window when all characters from t are matched
32 while matched_chars == len(t):
33 # Update minimum window if current window is smaller
34 current_window_size = right - left + 1
35 if current_window_size < min_length:
36 min_length = current_window_size
37 min_start = left
38
39 # Remove leftmost character from window
40 left_char = s[left]
41 if left_char in target_freq and window_freq[left_char] <= target_freq[left_char]:
42 matched_chars -= 1
43 window_freq[left_char] -= 1
44
45 # Move left pointer to contract window
46 left += 1
47
48 # Return empty string if no valid window found, otherwise return minimum window
49 return "" if min_start == -1 else s[min_start : min_start + min_length]
50
1class Solution {
2 public String minWindow(String s, String t) {
3 // Frequency map for characters needed from string t
4 int[] targetFreq = new int[128];
5 // Frequency map for characters in current window
6 int[] windowFreq = new int[128];
7
8 // Count frequency of each character in t
9 for (char ch : t.toCharArray()) {
10 targetFreq[ch]++;
11 }
12
13 int sourceLen = s.length();
14 int targetLen = t.length();
15
16 // Variables to track the minimum window
17 int minWindowStart = -1; // Starting index of minimum window
18 int minWindowLen = sourceLen + 1; // Length of minimum window
19 int validCharCount = 0; // Count of valid characters matched
20
21 // Sliding window approach with two pointers
22 int left = 0;
23 for (int right = 0; right < sourceLen; right++) {
24 // Expand window by including character at right pointer
25 char rightChar = s.charAt(right);
26 windowFreq[rightChar]++;
27
28 // If this character contributes to a valid match, increment count
29 if (windowFreq[rightChar] <= targetFreq[rightChar]) {
30 validCharCount++;
31 }
32
33 // Contract window from left while it remains valid
34 while (validCharCount == targetLen) {
35 // Update minimum window if current window is smaller
36 if (right - left + 1 < minWindowLen) {
37 minWindowLen = right - left + 1;
38 minWindowStart = left;
39 }
40
41 // Remove leftmost character from window
42 char leftChar = s.charAt(left);
43
44 // If removing this character breaks validity, decrement count
45 if (windowFreq[leftChar] <= targetFreq[leftChar]) {
46 validCharCount--;
47 }
48
49 windowFreq[leftChar]--;
50 left++;
51 }
52 }
53
54 // Return empty string if no valid window found, otherwise return minimum window
55 return minWindowStart < 0 ? "" : s.substring(minWindowStart, minWindowStart + minWindowLen);
56 }
57}
58
1class Solution {
2public:
3 string minWindow(string s, string t) {
4 // Frequency map for characters needed from string t
5 vector<int> targetFreq(128, 0);
6 // Frequency map for characters in current window
7 vector<int> windowFreq(128, 0);
8
9 // Count frequency of each character in t
10 for (char c : t) {
11 targetFreq[c]++;
12 }
13
14 int sLen = s.length();
15 int tLen = t.length();
16
17 // Variables to track the minimum window
18 int minStart = -1; // Starting index of minimum window
19 int minLength = sLen + 1; // Length of minimum window
20 int validCharCount = 0; // Count of characters from t that are satisfied in current window
21
22 // Sliding window with two pointers
23 int left = 0;
24 for (int right = 0; right < sLen; right++) {
25 // Expand window by including character at right pointer
26 char rightChar = s[right];
27 windowFreq[rightChar]++;
28
29 // If this character contributes to satisfying the requirement
30 if (windowFreq[rightChar] <= targetFreq[rightChar]) {
31 validCharCount++;
32 }
33
34 // Contract window from left while it still contains all characters from t
35 while (validCharCount == tLen) {
36 // Update minimum window if current window is smaller
37 if (right - left + 1 < minLength) {
38 minLength = right - left + 1;
39 minStart = left;
40 }
41
42 // Try to shrink window from left
43 char leftChar = s[left];
44
45 // If removing this character breaks the valid window
46 if (windowFreq[leftChar] <= targetFreq[leftChar]) {
47 validCharCount--;
48 }
49
50 windowFreq[leftChar]--;
51 left++;
52 }
53 }
54
55 // Return empty string if no valid window found, otherwise return the minimum window
56 return minStart == -1 ? "" : s.substr(minStart, minLength);
57 }
58};
59
1function minWindow(s: string, t: string): string {
2 // Array to store character frequency requirements from string t
3 // Using ASCII table size (128) for O(1) access
4 const targetCharCount: number[] = Array(128).fill(0);
5
6 // Array to track current window's character frequencies
7 const windowCharCount: number[] = Array(128).fill(0);
8
9 // Count frequency of each character in target string t
10 for (let i = 0; i < t.length; i++) {
11 targetCharCount[t.charCodeAt(i)]++;
12 }
13
14 // Store lengths for cleaner code
15 const sourceLength: number = s.length;
16 const targetLength: number = t.length;
17
18 // Variables to track the minimum window
19 let minWindowStart: number = -1; // Starting index of minimum window
20 let minWindowLength: number = sourceLength + 1; // Length of minimum window
21 let matchedChars: number = 0; // Count of matched characters
22
23 // Sliding window approach with two pointers
24 let left: number = 0;
25 for (let right: number = 0; right < sourceLength; right++) {
26 // Expand window by including character at right pointer
27 const rightChar: number = s.charCodeAt(right);
28
29 // Add current character to window
30 windowCharCount[rightChar]++;
31
32 // If this character is needed and we haven't exceeded the required count
33 if (windowCharCount[rightChar] <= targetCharCount[rightChar]) {
34 matchedChars++;
35 }
36
37 // Contract window from left while all target characters are matched
38 while (matchedChars === targetLength) {
39 // Update minimum window if current window is smaller
40 const currentWindowLength: number = right - left + 1;
41 if (currentWindowLength < minWindowLength) {
42 minWindowLength = currentWindowLength;
43 minWindowStart = left;
44 }
45
46 // Remove leftmost character from window
47 const leftChar: number = s.charCodeAt(left);
48
49 // If removing this character breaks the valid window
50 if (windowCharCount[leftChar] <= targetCharCount[leftChar]) {
51 matchedChars--;
52 }
53
54 // Update window character count and move left pointer
55 windowCharCount[leftChar]--;
56 left++;
57 }
58 }
59
60 // Return the minimum window substring, or empty string if not found
61 return minWindowStart < 0 ? '' : s.substring(minWindowStart, minWindowStart + minWindowLength);
62}
63
Time and Space Complexity
The time complexity is O(m + n)
, where m
is the length of string s
and n
is the length of string t
. The algorithm initializes a Counter
for string t
which takes O(n)
time. Then it uses a sliding window approach with two pointers traversing string s
once, where each character is visited at most twice (once by the right pointer and once by the left pointer), resulting in O(m)
operations. Therefore, the total time complexity is O(m + n)
.
The space complexity is O(|Σ|)
, where |Σ|
represents the size of the character set. The algorithm uses two Counter
objects (need
and window
) to store character frequencies. In the worst case, these counters will store all unique characters that appear in strings s
and t
. Since the problem deals with ASCII characters, the maximum size of the character set is 128
, making the space complexity O(128)
which simplifies to O(1)
constant space in terms of the character set size, or more precisely O(|Σ|)
where |Σ| = 128
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
Pitfall: Incorrect Character Counting Logic
The Problem: The most common pitfall in this solution is the condition used to determine when a character contributes to or detracts from the valid window count. The code uses:
- When adding:
window_freq[char] <= target_freq[char]
- When removing:
window_freq[left_char] <= target_freq[left_char]
This logic is incorrect because it counts characters inconsistently. Consider this example:
s = "ADOBECODEBANC"
,t = "ABC"
- When we encounter the second 'A' in position 10,
window_freq['A']
becomes 2 whiletarget_freq['A']
is 1 - The condition
window_freq['A'] <= target_freq['A']
(2 <= 1) is false, so we don't incrementmatched_chars
- However, this second 'A' doesn't actually contribute to matching
t
, which is correct behavior accidentally
But the real issue appears when removing characters:
- If we have
window_freq['A'] = 2
andtarget_freq['A'] = 1
, and we remove one 'A' - After removal,
window_freq['A']
becomes 1 - The condition checks if 2 <= 1 (before removal), which is false
- So we don't decrement
matched_chars
, even though we might have just removed a necessary 'A'
The Solution: The correct conditions should be:
- When adding a character:
window_freq[char] <= target_freq[char]
(this one is actually correct) - When removing a character:
window_freq[left_char] < target_freq[left_char]
(use strict inequality)
Or alternatively, use the approach from the solution description:
- When adding:
target_freq[char] >= window_freq[char]
(check after adding) - When removing:
target_freq[left_char] >= window_freq[left_char]
(check before removing)
Corrected Code:
from collections import Counter
from math import inf
class Solution:
def minWindow(self, s: str, t: str) -> str:
target_freq = Counter(t)
window_freq = Counter()
matched_chars = 0
left = 0
min_start = -1
min_length = inf
for right, char in enumerate(s):
window_freq[char] += 1
# Check if this character contributes to matching t
if char in target_freq and window_freq[char] <= target_freq[char]:
matched_chars += 1
while matched_chars == len(t):
current_window_size = right - left + 1
if current_window_size < min_length:
min_length = current_window_size
min_start = left
left_char = s[left]
# CORRECTED: Use strict inequality when removing
if left_char in target_freq and window_freq[left_char] < target_freq[left_char]:
matched_chars -= 1
window_freq[left_char] -= 1
left += 1
return "" if min_start == -1 else s[min_start : min_start + min_length]
Additional Pitfall: Using Dictionary Size Instead of Character Count
Another common mistake is using len(need)
(number of unique characters) instead of len(t)
(total character count) for the validity check. This would fail for cases where t
has duplicate characters like "AAB"
because it would consider the window valid as soon as it has at least one 'A' and one 'B', ignoring the frequency requirement.
Which of the following array represent a max heap?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!