Pattern family: Array / String Optimization Difficulty range: Easy β Hard Language: Java Problems file: 02_sliding_window_pattern_problems.md
- How to identify this pattern β start here
- What is this pattern?
- Core rules
- 2-Question framework
- Variants table
- Universal Java template
- Quick reference cheatsheet
- Common mistakes
- Complexity summary
| If you see this... | It means |
|---|---|
| "subarray / substring of size k" | fixed window |
| "maximum / minimum sum of subarray of size k" | fixed window |
| "longest substring with at most k distinct" | variable window (maximize) |
| "smallest subarray with sum β₯ target" | variable window (minimize) |
| "no-repeat / without repeating characters" | variable window with HashSet/Map |
| "permutation / anagram of pattern in string" | fixed window (pattern length) |
| "minimum window containing all characters" | variable window (minimize) |
| "replace at most k characters" | variable window with frequency map |
| "contiguous subarray / substring" | sliding window candidate |
| "subarray product less than k" | variable window |
Brute force: check every subarray β O(nΒ²) or O(nΒ³)
β If you need max/min/count of a contiguous subarray/substring property
AND that property is monotone (adding element doesn't decrease window validity)
β Sliding Window = O(n)
Key insight: instead of recomputing from scratch each time,
SLIDE the window β add one element on right, remove one on left.
Signal 1 β "Contiguous subarray or substring"
Non-contiguous = DP or greedy. Contiguous + optimize = Sliding Window.
Signal 2 β Fixed size k given
Window never changes size β Fixed window. Slide one step at a time.
Signal 3 β "Longest / maximum" with a constraint
Expand right always, shrink left when constraint violated β Variable window maximize.
Signal 4 β "Shortest / minimum" with a constraint
Expand right until condition met, then shrink left as much as possible β Variable window minimize.
Signal 5 β "At most k" convertible to exact count
exactly(k) = atMost(k) - atMost(k-1)β use variable window twice.
Problem asks about contiguous subarray/substring?
β
βββ YES β Is window size fixed (given as k)?
β βββ YES β Fixed Window
β βββ NO β Is it maximize or minimize?
β βββ MAXIMIZE β Variable Window (expand right, shrink on violation)
β βββ MINIMIZE β Variable Window (expand until met, shrink as much as possible)
β βββ COUNT EXACT k β At Most trick: atMost(k) - atMost(k-1)
β
βββ NO β Not Sliding Window (try DP / Two Pointers / Binary Search)
A window is a contiguous portion of an array or string defined by two pointers β left and right. Instead of recomputing the entire window from scratch each step, you slide it:
Expand: right++ (add new element to window)
Shrink: left++ (remove oldest element from window)
This converts O(nΒ²) brute force into O(n) by reusing computation.
arr = [2, 1, 5, 2, 3, 2], k = 3
Window [2,1,5] β sum=8
Slide [1,5,2] β sum = 8 - 2 + 2 = 8 (don't recompute from scratch)
Slide [5,2,3] β sum = 8 - 1 + 3 = 10
Slide [2,3,2] β sum = 10 - 5 + 2 = 7
Two fundamentally different window types:
| Type | Window size | Right pointer | Left pointer | Goal |
|---|---|---|---|---|
| Fixed | always = k | moves every step | moves every step (right - k) | aggregate over all windows of size k |
| Variable | changes | always moves right | moves when condition violated (maximize) OR when condition met (minimize) | find optimal window |
Rule 1 β Fixed window: add right, remove left simultaneously
// Window size always = k
for (int right = 0; right < n; right++) {
// add arr[right] to window
windowSum += arr[right];
if (right >= k - 1) {
// window is full β use result
maxSum = Math.max(maxSum, windowSum);
// remove leftmost element
windowSum -= arr[right - k + 1]; // left = right - k + 1
}
}Rule 2 β Variable window maximize: shrink until valid, never discard answer
int left = 0;
for (int right = 0; right < n; right++) {
// expand β add arr[right]
while (/* window invalid */) {
// shrink β remove arr[left]
left++;
}
// window is now valid β update answer
maxLen = Math.max(maxLen, right - left + 1);
}Rule 3 β Variable window minimize: shrink as long as valid, update answer inside shrink
int left = 0;
for (int right = 0; right < n; right++) {
// expand β add arr[right]
while (/* window still valid β can shrink more */) {
// update answer BEFORE shrinking
minLen = Math.min(minLen, right - left + 1);
// shrink β remove arr[left]
left++;
}
}Key difference:
- Maximize β update answer after the while loop (window just became valid)
- Minimize β update answer inside the while loop (window is currently valid)
| Answer | Type | Clue words |
|---|---|---|
| Size k given explicitly | Fixed Window | "subarray of size k", "window of length k" |
| Size changes based on condition | Variable Window | "longest", "shortest", "at most k", "minimum window" |
| Goal | Approach | Update answer |
|---|---|---|
| Maximize window (longest/max) | shrink when invalid | after while loop |
| Minimize window (shortest/min) | shrink when valid | inside while loop |
| Count windows with exact k | atMost(k) - atMost(k-1) |
count = right - left + 1 per step |
Decision shortcut:
- "fixed size k" β fixed window
- "longest / maximum" + constraint β variable maximize
- "shortest / minimum" + condition β variable minimize
- "count subarrays with exactly k" β at-most trick
- "permutation / anagram of pattern" β fixed window = pattern.length()
Common core:
leftandrightpointers, expand right, conditionally shrink left
What differs: when to shrink, what to track, when to update answer
| Variant | Track in window | Shrink condition | Answer update |
|---|---|---|---|
| Fixed window β max/min sum | running sum | always when right >= k-1 |
each full window |
| Fixed window β pattern match | char frequency map | always when right >= k-1 |
when map matches |
| Variable β maximize length | char/int frequency | when constraint violated | after shrink |
| Variable β minimize length | running sum | when condition still holds | inside shrink loop |
| Variable β at most k distinct | distinct count | when distinct > k | right - left + 1 per step |
| Variable β at most k replacements | max freq in window | when (window - maxFreq) > k |
after shrink |
| At-most to exact | β | atMost(k) - atMost(k-1) |
accumulated count |
Window size = k throughout. Slide one step at a time.
Pattern:
for (int right = 0; right < n; right++) {
add(arr[right]);
if (right >= k - 1) {
updateAnswer();
remove(arr[right - k + 1]);
}
}Problems: Max Sum Subarray Size K, Find All Anagrams, Permutation in String, Max Consecutive Ones (fixed)
Expand right always. Shrink left only when window becomes invalid.
Answer = max window size seen while valid.
Pattern:
int left = 0;
for (int right = 0; right < n; right++) {
add(arr[right]);
while (isInvalid()) {
remove(arr[left++]);
}
maxLen = Math.max(maxLen, right - left + 1); // update AFTER shrink
}Sub-types:
- 2a. Maximize with distinct count limit β use
HashMap<char, count>, shrink whenmap.size() > k - 2b. Maximize with replacement limit β track
maxFreq, shrink when(window - maxFreq) > k - 2c. Maximize with no-repeat constraint β use
HashSetorHashMap<char, lastIndex>
Problems: Longest Substring K Distinct, Fruits into Baskets, No-repeat Substring, Longest After Replacement, Max Consecutive Ones III
Expand right until condition met. Then shrink left as much as possible.
Answer = min window size seen while valid.
Pattern:
int left = 0;
for (int right = 0; right < n; right++) {
add(arr[right]);
while (isValid()) {
minLen = Math.min(minLen, right - left + 1); // update INSIDE shrink
remove(arr[left++]);
}
}Problems: Smallest Subarray with Given Sum, Minimum Window Substring, Min Size Subarray Sum
"Exactly k" is hard directly. Convert using:
count(exactly k) = count(at most k) - count(at most k-1)
Pattern:
return atMost(nums, k) - atMost(nums, k - 1);
private int atMost(int[] nums, int k) {
int left = 0, count = 0;
for (int right = 0; right < nums.length; right++) {
// add nums[right]
while (/* invalid: exceeded k */) left++;
count += right - left + 1; // all windows ending at right
}
return count;
}Problems: Subarrays with K Different Integers, Binary Subarrays with Sum, Count Nice Subarrays
| # | Type | Window size | Shrink when | Update answer |
|---|---|---|---|---|
| 1 | Fixed window | always k | every step | each full window |
| 2a | Variable maximize (distinct) | grows/shrinks | distinct > k | after shrink |
| 2b | Variable maximize (replace) | grows/shrinks | replacements > k | after shrink |
| 2c | Variable maximize (no-repeat) | grows/shrinks | duplicate found | after shrink |
| 3 | Variable minimize | grows/shrinks | condition still holds | inside shrink |
| 4 | At-most to exact | variable | β | atMost(k) - atMost(k-1) |
// βββ TEMPLATE A: Fixed Window βββββββββββββββββββββββββββββββββββββββββββββββββ
public int fixedWindow(int[] arr, int k) {
int windowSum = 0, maxSum = 0;
for (int right = 0; right < arr.length; right++) {
windowSum += arr[right]; // expand
if (right >= k - 1) {
maxSum = Math.max(maxSum, windowSum); // update answer
windowSum -= arr[right - k + 1]; // shrink: remove leftmost
}
}
return maxSum;
}
// βββ TEMPLATE B: Variable Window β Maximize βββββββββββββββββββββββββββββββββββ
// "Longest substring / subarray satisfying condition"
public int variableMaximize(String s, int k) {
Map<Character, Integer> freq = new HashMap<>();
int left = 0, maxLen = 0;
for (int right = 0; right < s.length(); right++) {
char c = s.charAt(right);
freq.put(c, freq.getOrDefault(c, 0) + 1); // expand
while (isInvalid(freq, k)) { // shrink until valid
char lc = s.charAt(left);
freq.put(lc, freq.get(lc) - 1);
if (freq.get(lc) == 0) freq.remove(lc);
left++;
}
maxLen = Math.max(maxLen, right - left + 1); // update AFTER shrink
}
return maxLen;
}
// βββ TEMPLATE C: Variable Window β Minimize βββββββββββββββββββββββββββββββββββ
// "Smallest subarray / substring satisfying condition"
public int variableMinimize(int[] arr, int target) {
int windowSum = 0, left = 0;
int minLen = Integer.MAX_VALUE;
for (int right = 0; right < arr.length; right++) {
windowSum += arr[right]; // expand
while (windowSum >= target) { // condition MET β shrink
minLen = Math.min(minLen, right - left + 1); // update INSIDE shrink
windowSum -= arr[left++];
}
}
return minLen == Integer.MAX_VALUE ? 0 : minLen;
}
// βββ TEMPLATE D: At-Most to Exact (Count exact k) ββββββββββββββββββββββββββββ
public int countExact(int[] nums, int k) {
return atMost(nums, k) - atMost(nums, k - 1);
}
private int atMost(int[] nums, int k) {
Map<Integer, Integer> freq = new HashMap<>();
int left = 0, count = 0;
for (int right = 0; right < nums.length; right++) {
freq.put(nums[right], freq.getOrDefault(nums[right], 0) + 1);
while (freq.size() > k) { // shrink until valid
int lv = nums[left++];
freq.put(lv, freq.get(lv) - 1);
if (freq.get(lv) == 0) freq.remove(lv);
}
count += right - left + 1; // all valid windows ending at right
}
return count;
}
// βββ TEMPLATE E: Fixed Window β Pattern Match (Anagram / Permutation) βββββββββ
public boolean patternMatch(String s, String p) {
if (p.length() > s.length()) return false;
int[] pFreq = new int[26], wFreq = new int[26];
for (char c : p.toCharArray()) pFreq[c - 'a']++;
for (int right = 0; right < s.length(); right++) {
wFreq[s.charAt(right) - 'a']++; // expand
if (right >= p.length()) { // shrink: remove leftmost
wFreq[s.charAt(right - p.length()) - 'a']--;
}
if (Arrays.equals(pFreq, wFreq)) return true; // match check
}
return false;
}SIGNAL IN PROBLEM β TYPE β KEY OPERATION
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
"max/min sum of subarray of size k" β Fixed window β add right, remove arr[right-k+1]
"permutation / anagram of pattern in s" β Fixed window β int[26] freq compare
"longest substring with at most k distinct" β Variable maximize β map.size() > k β shrink
"longest with at most k replacements" β Variable maximize β (window-maxFreq) > k β shrink
"longest without repeating characters" β Variable maximize β duplicate in map β shrink
"smallest subarray with sum β₯ target" β Variable minimize β sum >= target β shrink inside
"minimum window containing all chars" β Variable minimize β formed==required β shrink inside
"count subarrays with exactly k distinct" β At-most trick β atMost(k) - atMost(k-1)
"subarray product less than k" β Variable minimize β product >= k β shrink
"max consecutive ones with k flips" β Variable maximize β zeros > k β shrink
Template picker β one question decides:
Fixed size k given? β Template A (Fixed Window)
Maximize + constraint? β Template B (Variable Maximize)
Minimize + condition to meet?β Template C (Variable Minimize)
Count exact k subarrays? β Template D (At-Most Trick)
Anagram / permutation match? β Template E (Pattern Match)
The one insight that unlocks all variable window problems:
Maximize β shrink when INVALID β answer AFTER while
Minimize β shrink when VALID β answer INSIDE while
// BUG (maximize): updating inside shrink loop β counts invalid windows
while (invalid()) { left++; }
maxLen = Math.max(maxLen, right - left + 1); // WRONG β should be after
// FIX (maximize): update AFTER shrink
while (invalid()) { remove(arr[left]); left++; }
maxLen = Math.max(maxLen, right - left + 1); // CORRECT
// BUG (minimize): updating after shrink β misses valid windows
while (valid()) { left++; }
minLen = Math.min(minLen, right - left + 1); // WRONG β already invalid
// FIX (minimize): update INSIDE shrink
while (valid()) {
minLen = Math.min(minLen, right - left + 1); // CORRECT
remove(arr[left]); left++;
}// BUG β returns Integer.MAX_VALUE if no valid window exists
return minLen;
// FIX
return minLen == Integer.MAX_VALUE ? 0 : minLen;// BUG β map.size() counts distinct keys, but you may want total count
while (map.size() > k) { ... } // correct for "at most k distinct"
// But for replacement problems, track maxFreq separately:
maxFreq = Math.max(maxFreq, freq[c - 'a']);
while ((right - left + 1) - maxFreq > k) { ... }
// Note: maxFreq may be stale but safe β it only grows, never causes over-shrinking// BUG β decreasing maxFreq when shrinking (not needed, causes wrong answers)
while ((right - left + 1) - maxFreq > k) {
freq[s.charAt(left) - 'a']--;
maxFreq--; // WRONG β maxFreq is a historical max, don't decrease
left++;
}
// FIX β only increase maxFreq when expanding, never decrease
maxFreq = Math.max(maxFreq, freq[s.charAt(right) - 'a']);
while ((right - left + 1) - maxFreq > k) {
freq[s.charAt(left) - 'a']--;
left++; // maxFreq stays β safe because window can only shrink to valid size
}// BUG β removing wrong element
windowSum -= arr[left]; // left not updated yet β wrong index
// FIX β remove element that just left the window
windowSum -= arr[right - k + 1]; // or: windowSum -= arr[left]; left++;// BUG β removing key without decrementing first
map.remove(arr[left]); // count lost
// FIX β decrement first, then remove if zero
map.put(arr[left], map.get(arr[left]) - 1);
if (map.get(arr[left]) == 0) map.remove(arr[left]);
left++;| Variant | Time | Space | Notes |
|---|---|---|---|
| Fixed window β sum | O(n) | O(1) | running sum only |
| Fixed window β pattern match (int[26]) | O(n) | O(1) | fixed 26-char array |
| Variable maximize β distinct count | O(n) | O(k) | HashMap of at most k distinct |
| Variable maximize β replacement | O(n) | O(1) | int[26] array |
| Variable maximize β no-repeat | O(n) | O(min(n,Ξ±)) | Ξ± = alphabet size |
| Variable minimize β sum | O(n) | O(1) | |
| Variable minimize β min window substring | O(n + m) | O(m) | m = pattern length |
| At-most to exact | O(n) | O(k) | two passes of variable window |
General rules:
- All sliding window β O(n) time β each element enters and leaves the window at most once
- Space is O(1) for integer arrays, O(k) or O(alphabet) for HashMap-based windows
- Never O(nΒ²) β if your solution has nested loops over different ranges, it is not sliding window