A nested-loop scan over every subarray is O(n²). Two pointers make it O(n): a left and a right edge that only ever move forward, enclosing a window that grows and shrinks. This deck teaches the four templates — a constant window, the longest window under a condition, counting subarrays with an exact property, and the shortest covering window — each animated on a live L/R track you can step and predict, and each wrapped around its own lecture: prime it, watch it, drill it, then solve the problem it unlocks.
This is not a list of problems. It is 13 learning units, and every one runs the same four beats. Go in order and the problems stop feeling random.
12 PROBLEMS · EVERY ONE LINKS TO LEETCODE TO SOLVE
Click any problem to jump straight to it. Solved ones turn green. Press I from anywhere to come back here. The number is the sheet's row, not your position — the deck teaches in the order that builds, so you will meet them slightly out of numerical sequence.
Sliding window is four templates wearing different problems. The cards are the phrases in a statement that pick the template — “longest such that”, “at most k”, “count exactly”, “smallest containing” — before you write a line.
a variable window: grow the right edge, shrink the left when the condition breaks
SLIDING WINDOW (LONGEST / SHORTEST)O(n)grow R; while the count exceeds k, shrink L — the window stays valid
LONGEST VARIABLE WINDOWO(n)you can't count exactly directly — subtract two at-most passes
atMost(k) − atMost(k−1)O(n)the width is fixed, so just slide: add the entering cell, drop the leaving one
CONSTANT WINDOWO(n)expand R until valid, then contract L as far as it stays valid
SHORTEST COVERING WINDOWO(n)two pointers walking inward — move the one that can improve the answer
OPPOSITE-ENDS TWO POINTERSO(n)Sliding window lives at n ≤ 10⁵–10⁷: an O(n) scan where each of the two pointers crosses the array once. If the bound already permits O(n²) you can brute-force every subarray — but the window is what makes the big inputs pass.
CONTIGUOUS SUBARRAY + n ≥ 10⁵ ⇒ TWO POINTERS / SLIDING WINDOW, O(n)
Thirteen units. Unit 1 lays out the four templates on the live track; then one lecture per problem, grouped constant → longest → counting → shortest. The last unit (Minimum Window Subsequence) goes a step beyond the sheet with no lecture.
A sliding-window loop looks like a nested loop (an outer R, an inner while that moves L). Why is it O(n), not O(n²)?
Both pointers are monotonic — they never move backward. R advances n times total, and L also only advances, so across the entire run L moves at most n steps in total — not n steps per R. Amortised, each element is added once and removed at most once, giving O(n). This 'the inner loop doesn't reset' argument is the whole reason two pointers beats the quadratic brute force.
Which window template do you reach for: “longest subarray containing at most 2 distinct integers”?
Longest variable window with an 'at most k' shrink condition. The width isn't fixed and you want the longest valid one, so you grow R greedily and only shrink L when the distinct-count exceeds 2. That's exactly Fruit Into Baskets (k=2). If the question had said “count subarrays with exactly 2 distinct”, you'd switch to the atMost subtraction instead — recognising which of the four templates fits is the skill this deck drills.
Almost every sliding-window problem is one of four templates, and the hardest part is recognising which. (1) Constant window — a fixed width that slides, reusing the sum. (2) Longest window — grow the right edge, shrink the left when a condition breaks, keep the best length. (3) Counting — count subarrays with an exact property as atMost(k) − atMost(k−1). (4) Shortest window — expand to valid, then contract to minimal. In all four, both pointers only move forward, which is what turns an O(n²) subarray scan into O(n).
WHICH OF THE FOUR WINDOW TEMPLATES DOES THIS PROBLEM WANT?
What is the single property of L and R that makes the whole family O(n)?
Monotonic pointers — neither ever moves backward. Even though the code has an inner while that moves L, that loop doesn't restart from 0 for each R; L resumes where it left off. So the total number of L-moves over the whole scan is at most n, and the same for R — amortised O(n). This is the one insight that separates two pointers from the quadratic double loop.
“Maximum sum of any fixed window of size k” — which template, and what's the key optimisation?
Constant window with an incremental sum. Consecutive size-k windows overlap in k−1 cells, so recomputing the sum from scratch wastes that overlap. Add the new right element and drop the old left element in O(1) per slide. That's the template behind Maximum Points from Cards (where the 'window' is the un-taken middle) and any max-average-subarray question.
“How many subarrays have exactly 3 distinct integers?” Which template, and why not count it directly?
The at-most subtraction. A single sliding window can count subarrays with at most k distinct (add R−L+1 per step), but 'exactly k' has no clean per-window count. The identity exactly(k) = atMost(k) − atMost(k−1) turns one hard count into two easy ones. This is literally Subarrays with K Different Integers, and the same shape counts Binary Subarrays With Sum and Nice Subarrays.
The simplest window: a fixed width that just slides. The naive scan recomputes each window's sum from scratch — O(n·k). The trick is that consecutive windows overlap in all but two cells, so when the window steps right you add the entering element and subtract the leaving one: sum += a[R] − a[R−k]. One pass, O(n). Watch the gold cell enter on the right and the red cell leave on the left — that single swap is the whole idea, and it's the pattern behind Maximum Points from Cards.
The workhorse template. The right edge grows greedily, pulling in one element per step; the moment the window violates its condition (here, a repeated character), the left edge shrinks until the window is valid again. Both pointers only ever move forward, so the whole scan is O(n) even though it looks like a nested loop. Track the best length as you go. Every “longest substring/subarray such that …” problem — no repeats, at most k zeros, at most k distinct — is this one shape with a different validity test.
Maximum Points from Cards looks like it needs choices from both ends, but flip it: you take exactly k cards total from the two ends, which means the cards you leave behind form one contiguous block of size n − k in the middle. Maximising what you take is the same as minimising that leftover block — a constant-size window of width n − k. So slide a fixed window of size n − k, find its minimum sum, and the answer is total − minWindow.
HOW DO YOU MAXIMISE CARDS FROM BOTH ENDS WITHOUT TRYING EVERY SPLIT?
Why does “take k cards from the two ends” reduce to a single fixed-size window in the middle?
The untaken cards are always a contiguous middle block. If you take i from the front and k−i from the back, the leftovers are exactly the cards from index i to n−(k−i)−1 — a window of width n−k. As i ranges over its choices, that window slides across the array. Maximising the taken sum equals minimising this fixed window's sum, so answer = total − minWindowSum.
Cards [1, 2, 3, 4, 5, 1], k = 3. Using total − (min sum of a size-(n−k) window), what is the max score?
10. Total = 16, n − k = 3. The size-3 windows are [1,2,3]=6, [2,3,4]=9, [3,4,5]=12, [4,5,1]=10; the smallest leftover is 6, so the most you can take is 16 − 6 = 10 (leave [1,2,3], take [4,5,1] from the back). A greedy 'grab the bigger end each time' would instead take 1 then 1… and get it wrong — which is exactly why the leftover-window reframing matters.
Taking k cards from the two ends always leaves a contiguous block of n−k in the middle, so the choice has a second reading: maximising your hand is minimising that block. That turns a problem about two ends into a fixed-size window over one span — unit 01's first template, applied to the complement of the answer rather than the answer.
“Take exactly k cards from either end to maximise the score.” 'From both ends' + 'exactly k taken' is the tell to look at the fixed leftover window in the middle.
Instead of choosing how many to take from each end, note the untaken cards form a contiguous block of size n − k. Minimise that block's sum with a constant-size sliding window, and the answer is total − minWindowSum. If k == n, take everything.
// Max taken from ends = total − min sum of the middle window (size n−k). int maxScore(vector<int>& cards, int k) { int n = cards.size(), total = 0; for (int x : cards) total += x; int windowSize = n - k; if (windowSize == 0) return total; // take all cards int sum = 0; for (int i = 0; i < windowSize; i++) sum += cards[i]; int minWindow = sum; for (int R = windowSize; R < n; R++) { sum += cards[R] - cards[R - windowSize]; // slide the fixed window minWindow = min(minWindow, sum); } return total - minWindow; }
// Max taken from ends = total - min sum of the middle window (size n-k). public int maxScore(int[] cards, int k) { int n = cards.length, total = 0; for (int x : cards) total += x; int windowSize = n - k; if (windowSize == 0) return total; // take all cards int sum = 0; for (int i = 0; i < windowSize; i++) sum += cards[i]; int minWindow = sum; for (int R = windowSize; R < n; R++) { sum += cards[R] - cards[R - windowSize]; // slide, do not recompute minWindow = Math.min(minWindow, sum); } return total - minWindow; }
# Max taken from ends = total − min sum of the middle window (size n−k). def maxScore(cards, k): n, total = len(cards), sum(cards) window = n - k if window == 0: return total # take all cards s = sum(cards[:window]) min_window = s for R in range(window, n): s += cards[R] - cards[R - window] # slide the fixed window min_window = min(min_window, s) return total - min_window
Simulating the greedy 'take the bigger end' choice. Greedily grabbing the larger of the two ends is wrong — a small card now can guard a big one later. The reframing to total − min fixed window sidesteps all of that. Also handle k == n (window size 0 → take everything) or the loop indexes out of range.
The archetype of the longest variable window. Grow the right edge one character at a time; keep a set (or a last-seen map) of what's in the window. When s[R] is already inside, the window has a repeat — shrink L until it's gone. With a last-seen index map you can jump L straight to lastSeen[s[R]] + 1 instead of stepping it. Track the best length throughout.
HOW LONG IS THE LONGEST STRETCH WITH NO REPEATED CHARACTER?
With a last-seen index map, when s[R] was seen before at index j (inside the window), where should L move to?
L = max(L, lastSeen[s[R]] + 1). Moving L to one past the previous occurrence removes exactly the duplicate. The max(L, …) is essential: an old occurrence of s[R] might sit to the left of the current L, and you must never drag L backward (that would re-admit already-dropped characters and break the monotonic-pointer guarantee).
For s = "abcba", what is the longest substring without repeating characters?
3. Growing R: "a","ab","abc" (len 3). At R=3 the second 'b' repeats (last at 1), so L jumps to 2 → window "cb". At R=4 'a' was last at 0, which is outside the window (L=2), so no shrink → "cba" (len 3). Best = 3. The mechanism slide runs exactly this string.
The archetype for the longest template, on data unit 01 does not use. R only ever advances; L advances only while the window is broken and never retreats. That is the whole reason the scan is O(n) and not O(n²) — each index is entered once by R and left once by L, no matter how the two interleave.
“Longest substring with no repeating character.” 'Longest … such that' over a contiguous run is the longest variable window.
Grow the right edge; keep the last-seen index of each character. When s[R] has been seen at index j ≥ L, a duplicate is in the window, so jump L to j + 1. Record the width R − L + 1 at every step.
// Longest window with no repeat; L jumps past the last occurrence. int lengthOfLongestSubstring(string s) { unordered_map<char, int> last; int L = 0, best = 0; for (int R = 0; R < (int)s.size(); R++) { auto it = last.find(s[R]); if (it != last.end() && it->second >= L) L = it->second + 1; // jump past the duplicate last[s[R]] = R; best = max(best, R - L + 1); } return best; }
// Longest window with no repeat; L jumps past the last occurrence. public int lengthOfLongestSubstring(String s) { Map<Character, Integer> last = new HashMap<>(); int L = 0, best = 0; for (int R = 0; R < s.length(); R++) { char c = s.charAt(R); Integer prev = last.get(c); if (prev != null && prev >= L) L = prev + 1; // jump past the duplicate last.put(c, R); best = Math.max(best, R - L + 1); } return best; }
# Longest window with no repeat; L jumps past the last occurrence. def lengthOfLongestSubstring(s): last = {} L = best = 0 for R, ch in enumerate(s): if ch in last and last[ch] >= L: L = last[ch] + 1 # jump past the duplicate last[ch] = R best = max(best, R - L + 1) return best
Dragging L backward. A previous occurrence of s[R] may lie to the left of the current L; only jump when lastSeen[s[R]] ≥ L (or use L = max(L, lastSeen[s[R]]+1)). Moving L back re-admits dropped characters and breaks the O(n) guarantee, silently inflating the answer.
Max Consecutive Ones III is the longest window template with a budget: you may flip up to k zeros to ones, so you want the longest window containing at most k zeros. Grow R, count the zeros inside; the moment the count exceeds k, shrink L until it drops back to k. The answer is the largest window width you ever hold valid.
LONGEST RUN OF 1s IF YOU MAY FLIP AT MOST k ZEROS?
Why reframe “flip at most k zeros to ones” as “longest window with at most k zeros”?
A window with ≤ k zeros is achievable as all ones. You have a budget of k flips, so any contiguous stretch containing at most k zeros can be made entirely ones — its whole length counts. The task becomes: find the longest window whose zero-count never exceeds k. You track just the zero count, not the actual flips, which is what keeps it O(n).
When the zero count exceeds k, why shrink with a while and only decrement the counter when the leaving element is a zero?
Only a leaving zero reduces the count. As L advances you decrement zeros exactly when a[L] == 0; skipping that guard would under-count and shrink too far. Here removing one zero restores validity, so a single shrink suffices, but writing it as a while (zeros > k) is the safe general form that also covers templates where several elements must leave.
Same template, and the only thing that changed is valid(): a window is legal while it holds at most k zeros. Note what the code never does — nothing is actually flipped. The array is untouched and the budget is the entire state, which is what makes this a window problem rather than a search over which zeros to change.
“Flip at most k zeros; longest run of 1s.” A budget of flips over a contiguous run is the at-most-k longest window.
A window is achievable as all-ones iff it holds at most k zeros. Grow R, counting zeros; whenever the count exceeds k, shrink L (decrementing the count when a zero leaves). The largest valid width is the answer.
// Longest window containing at most k zeros. int longestOnes(vector<int>& nums, int k) { int L = 0, zeros = 0, best = 0; for (int R = 0; R < (int)nums.size(); R++) { if (nums[R] == 0) zeros++; while (zeros > k) { // too many zeros -> shrink if (nums[L] == 0) zeros--; L++; } best = max(best, R - L + 1); } return best; }
// Longest window containing at most k zeros. public int longestOnes(int[] nums, int k) { int L = 0, zeros = 0, best = 0; for (int R = 0; R < nums.length; R++) { if (nums[R] == 0) zeros++; while (zeros > k) { // too many zeros -> shrink if (nums[L] == 0) zeros--; L++; } best = Math.max(best, R - L + 1); } return best; }
# Longest window containing at most k zeros. def longestOnes(nums, k): L = zeros = best = 0 for R, v in enumerate(nums): if v == 0: zeros += 1 while zeros > k: # too many zeros -> shrink if nums[L] == 0: zeros -= 1 L += 1 best = max(best, R - L + 1) return best
Decrementing zeros for every left move. Only a leaving zero reduces the count — guard with if (nums[L] == 0). And update best after the shrink loop, not during it, or you count an invalid (too many zeros) window.
Fruit Into Baskets is a story wrapped around longest subarray with at most 2 distinct values. Two baskets each hold one fruit type; walking left to right you collect a contiguous run using at most two types. Grow R, track the count of each type in a frequency map; when a third distinct type appears, shrink L, dropping counts, until only two types remain. The answer is the longest valid window — it's exactly “at most k distinct” with k = 2.
LONGEST CONTIGUOUS RUN USING AT MOST 2 FRUIT TYPES?
Using a frequency map for the window, how do you know the number of distinct fruit types, and when does a type truly leave the window?
Distinct = keys with positive count; a type leaves at count 0. The map's size is the distinct count. When L advances you decrement map[a[L]], and only when it reaches 0 do you erase the key and reduce the distinct count. Forgetting the zero-erase leaves a phantom type in the map, so the window thinks it has too many types and shrinks incorrectly — a classic stale-map bug.
Fruits [1, 2, 1, 2, 3]. Longest run with at most 2 distinct types?
4. [1,2,1,2] uses only types 1 and 2 — width 4. When the 3 at index 4 enters, the window holds 3 distinct types, so L shrinks past the earlier fruits until only two types remain ([2,3]). Best stays 4. This is at-most-2-distinct, the seed of the general k-distinct template.
The story is fruit and baskets; the constraint is at most two distinct values in the window. Watch the shrink carefully: dropping one copy of a fruit does not remove that type — the count has to reach zero before the distinct total falls. That is the step this template is most often got wrong on, and unit 06 is this run with the 2 lifted out.
“At most two basket types; longest collectable run.” 'At most 2 distinct' over a contiguous run is the at-most-k-distinct window with k = 2.
Slide a window keeping a frequency map of fruit types. Grow R; when the map holds more than 2 distinct types, shrink L, decrementing counts and erasing a type when its count hits 0. Track the longest valid width.
// Longest window with at most 2 distinct values (k=2). int totalFruit(vector<int>& fruits) { unordered_map<int, int> count; int L = 0, best = 0; for (int R = 0; R < (int)fruits.size(); R++) { count[fruits[R]]++; while ((int)count.size() > 2) { // a third type -> shrink if (--count[fruits[L]] == 0) count.erase(fruits[L]); L++; } best = max(best, R - L + 1); } return best; }
// Longest window with at most 2 distinct values (k=2). public int totalFruit(int[] fruits) { Map<Integer, Integer> count = new HashMap<>(); int L = 0, best = 0; for (int R = 0; R < fruits.length; R++) { count.merge(fruits[R], 1, Integer::sum); while (count.size() > 2) { // a third type -> shrink if (count.merge(fruits[L], -1, Integer::sum) == 0) count.remove(fruits[L]); L++; } best = Math.max(best, R - L + 1); } return best; }
# Longest window with at most 2 distinct values (k=2). def totalFruit(fruits): count = {} L = best = 0 for R, f in enumerate(fruits): count[f] = count.get(f, 0) + 1 while len(count) > 2: # a third type -> shrink count[fruits[L]] -= 1 if count[fruits[L]] == 0: del count[fruits[L]] L += 1 best = max(best, R - L + 1) return best
Not erasing a key when its count hits 0. The distinct count is the map's size; if you leave zero-count keys behind, size() overstates the distinct types and the window shrinks when it shouldn't. This is the exact at-most-k-distinct template with k = 2 — the next problem just parameterises the 2.
At Most K Distinct is Fruit Into Baskets generalised to k: the longest substring containing at most k distinct characters. Identical machinery — a frequency map, grow R, and while the map holds more than k distinct keys, shrink L. The only change from Fruit is the literal 2 becomes the parameter k. Seeing that one problem is another with a constant swapped is half of pattern recognition.
LONGEST SUBSTRING WITH AT MOST k DISTINCT CHARACTERS?
What is the only substantive difference between the Fruit Into Baskets solution and At Most K Distinct?
Fruit is literally k = 2. The shrink guard becomes while (map.size() > k) and that's the whole change. Recognising that a 'new' problem is an old one with a constant turned into a parameter is a core interview skill — it means you already have the code. (This problem is LeetCode premium, but the pattern is the same free one you drilled on Fruit.)
For counting-style variants (exactly k distinct), how does this at-most-k window get reused?
The same window, switched from 'max length' to 'count subarrays', is atMost. Keep the identical grow/shrink-on-> k structure, but instead of best = max(...) do count += R−L+1. Then exactly(k) = atMost(k) − atMost(k−1) — which is precisely how Subarrays with K Different Integers is solved later in the deck.
Unit 05 with its 2 replaced by k, which is worth seeing as one motion rather than two problems. The template, the shrink condition and the bookkeeping are identical; only the number differs. Recognising that the fruit-basket problem was never about fruit is the transfer this unit tests.
“Longest substring with at most k distinct characters.” The general form of Fruit Into Baskets — at-most-k-distinct window.
Identical to Fruit with the 2 replaced by k: a frequency map, grow R, and while the map holds more than k distinct keys, shrink L (erasing a key at count 0). Track the longest valid width.
// Longest window with at most k distinct characters. int lengthOfLongestSubstringKDistinct(string s, int k) { unordered_map<char, int> count; int L = 0, best = 0; for (int R = 0; R < (int)s.size(); R++) { count[s[R]]++; while ((int)count.size() > k) { // more than k distinct -> shrink if (--count[s[L]] == 0) count.erase(s[L]); L++; } best = max(best, R - L + 1); } return best; }
// Longest window with at most k distinct characters. public int lengthOfLongestSubstringKDistinct(String s, int k) { Map<Character, Integer> count = new HashMap<>(); int L = 0, best = 0; for (int R = 0; R < s.length(); R++) { count.merge(s.charAt(R), 1, Integer::sum); while (count.size() > k) { // more than k distinct -> shrink if (count.merge(s.charAt(L), -1, Integer::sum) == 0) count.remove(s.charAt(L)); L++; } best = Math.max(best, R - L + 1); } return best; }
# Longest window with at most k distinct characters. def lengthOfLongestSubstringKDistinct(s, k): count = {} L = best = 0 for R, ch in enumerate(s): count[ch] = count.get(ch, 0) + 1 while len(count) > k: # more than k distinct -> shrink count[s[L]] -= 1 if count[s[L]] == 0: del count[s[L]] L += 1 best = max(best, R - L + 1) return best
Treating it as a new problem. It is Fruit Into Baskets with the constant 2 turned into the parameter k — same map, same forward pointers, same zero-erase. Edge case: k == 0 should return 0. (LeetCode premium, but the pattern is the free one you already drilled.)
Number of Substrings Containing All Three flips from 'longest' to counting, with a slick trick. For each right index R, track the last seen index of a, b and c. Once all three have appeared, every substring that starts at or before the minimum of those three last-seen indices and ends at R contains all three — that's 1 + min(lastA, lastB, lastC) substrings ending at R. Sum over all R.
HOW MANY SUBSTRINGS CONTAIN AT LEAST ONE a, ONE b AND ONE c?
Why is the number of valid substrings ending at R equal to 1 + min(last[a], last[b], last[c])?
The binding constraint is the rarest-recent character. To contain all three, the substring's start must be ≤ every character's most recent index — so ≤ the minimum of last[a], last[b], last[c]. Start positions 0, 1, …, min all work: that's min + 1 substrings ending at R. Summing across R counts every qualifying substring exactly once, in O(n).
For s = "abcabc" (indices 0–5), at R = 3 (s[3]='a') the last-seen indices are a→3, b→1, c→2. How many valid substrings end at R = 3?
2. min(3,1,2) = 1, so starts 0 and 1 qualify: "abca" and "bca" each contain all three. Start 2 ("ca") is missing a b, which matches the cut-off at min = 1. The count is 1 + min = 2.
This one is filed with the window problems and is not one. For each R, the substrings ending there that contain all three letters are exactly those starting at or before the earliest of the three last-seen positions, so the count is min(last) + 1. Nothing shrinks, nothing is maintained, and the rarest letter is what bounds the answer.
“Count substrings containing at least one a, one b and one c.” 'Count substrings with a coverage property' is a counting window (last-seen flavour).
For each right index, track the last-seen index of a, b and c. Once all three have appeared, every substring ending at R whose start is at or before min(lastA, lastB, lastC) contains all three — that's 1 + min(...) substrings. Sum over all R.
// For each R, valid starts = 1 + min(last a, last b, last c). int numberOfSubstrings(string s) { int last[3] = {-1, -1, -1}; long total = 0; for (int R = 0; R < (int)s.size(); R++) { last[s[R] - 'a'] = R; int m = min({last[0], last[1], last[2]}); if (m >= 0) total += m + 1; // starts 0..m all contain a,b,c } return (int)total; }
// For each R, valid starts = 1 + min(last a, last b, last c). public int numberOfSubstrings(String s) { int[] last = {-1, -1, -1}; long total = 0; for (int R = 0; R < s.length(); R++) { last[s.charAt(R) - 'a'] = R; int m = Math.min(last[0], Math.min(last[1], last[2])); if (m >= 0) total += m + 1; // starts 0..m all contain a,b,c } return (int) total; }
# For each R, valid starts = 1 + min(last a, last b, last c). def numberOfSubstrings(s): last = {'a': -1, 'b': -1, 'c': -1} total = 0 for R, ch in enumerate(s): last[ch] = R m = min(last['a'], last['b'], last['c']) if m >= 0: total += m + 1 # starts 0..m all contain a,b,c return total
Adding R + 1 instead of min + 1. The number of valid start positions is bounded by the rarest-recent character — the minimum of the three last-seen indices — not by R. Also only add once all three have been seen (min ≥ 0); before that, no substring qualifies.
Longest Repeating Character Replacement is a longest window with a subtle validity test. You may replace up to k characters, so a window is valid when (window length − count of its most frequent character) ≤ k — the non-majority characters are the ones you'd replace. Grow R, keep a frequency map and the running maxFreq; when (R − L + 1) − maxFreq > k, shrink L. The best window width is the answer.
LONGEST WINDOW MADE UNIFORM BY REPLACING AT MOST k CHARACTERS?
What makes a window valid, i.e. convertible to all-same-character with at most k replacements?
windowLen − maxFreq ≤ k. Keep the most frequent character as-is; everything else must be replaced, and there are windowLen − maxFreq of those. If that's within your budget k, the whole window can be made uniform. You maximise the window under this constraint.
A well-known optimisation: maxFreq is never decreased even when the window shrinks. Why is the final answer still correct?
A stale (too-high) maxFreq can't manufacture a larger valid window than one already seen. The window width only increases when a genuine maxFreq supported it; keeping an old maxFreq merely means the window doesn't shrink as eagerly, but best is only updated to the current width, which never exceeds a legitimately-achieved one. So skipping the (expensive) maxFreq recompute on shrink is safe — a classic and initially-surprising optimisation.
A window is valid when everything that is not the commonest letter fits the replacement budget: (R−L+1) − maxFreq ≤ k. The part worth pausing on is what the code leaves out — maxFreq is not recalculated when the window shrinks. A stale maximum can only make a window look worse than it is, so it may cost an unnecessary shrink but can never produce a wrong answer.
“Replace at most k characters; longest same-letter run.” 'Longest window fixable within a budget' is the longest window with a maxFreq test.
A window is valid when the characters you'd replace — everything but the most frequent one — number at most k: (R − L + 1) − maxFreq ≤ k. Grow R, update the frequency map and maxFreq; if the window becomes invalid, move L once. Track the best width.
// Valid while windowLen - maxFreq <= k; maxFreq kept (not lowered). int characterReplacement(string s, int k) { int count[26] = {0}, L = 0, best = 0, maxFreq = 0; for (int R = 0; R < (int)s.size(); R++) { maxFreq = max(maxFreq, ++count[s[R] - 'A']); if ((R - L + 1) - maxFreq > k) { // must replace too many -> shrink one count[s[L] - 'A']--; L++; } best = max(best, R - L + 1); } return best; }
// Valid while windowLen - maxFreq <= k; maxFreq kept (not lowered). public int characterReplacement(String s, int k) { int[] count = new int[26]; int L = 0, best = 0, maxFreq = 0; for (int R = 0; R < s.length(); R++) { maxFreq = Math.max(maxFreq, ++count[s.charAt(R) - 'A']); if ((R - L + 1) - maxFreq > k) { // must replace too many -> shrink one count[s.charAt(L) - 'A']--; L++; } best = Math.max(best, R - L + 1); } return best; }
# Valid while windowLen - maxFreq <= k; maxFreq kept (not lowered). def characterReplacement(s, k): count = {} L = best = maxFreq = 0 for R, ch in enumerate(s): count[ch] = count.get(ch, 0) + 1 maxFreq = max(maxFreq, count[ch]) if (R - L + 1) - maxFreq > k: # too many to replace -> shrink one count[s[L]] -= 1 L += 1 best = max(best, R - L + 1) return best
Recomputing maxFreq on every shrink (or shrinking with a while). Because the answer only grows, a stale-high maxFreq can't inflate best beyond a genuinely-achieved width, so a single if-shrink and a never-lowered maxFreq are correct and faster. Recomputing is safe but unnecessary; using if keeps the window from ever shrinking below the best.
Binary Subarrays With Sum is the first pure counting problem, and it introduces the identity the whole group rests on: exactly(goal) = atMost(goal) − atMost(goal−1). atMost(x) is a plain variable window on a 0/1 array — grow R adding to the running sum, shrink L while the sum exceeds x, and add R − L + 1 (every subarray ending at R) to the count. Run it twice and subtract.
HOW MANY SUBARRAYS SUM TO EXACTLY goal — WITHOUT COUNTING EXACTLY?
In atMost(x), once the window [L,R] has sum ≤ x, why do you add exactly R − L + 1 to the count?
They're the subarrays ending at R. If [L,R] is valid, so is every [i,R] for L ≤ i ≤ R (a shorter subarray has a smaller-or-equal sum on a non-negative array). That's R − L + 1 new valid subarrays, all ending at R — counting them per step, without double-counting, gives atMost(x) in one pass.
Why can't you count “exactly goal” with a single sliding window directly?
'Exactly' doesn't compose the way 'at most' does. The R − L + 1 trick works because every shorter suffix of a valid at-most window is also valid. For 'exactly goal' that's false — trim one element and the sum drops below goal. So you express the exact count as the difference of two monotone at-most counts, each of which does compose. (A prefix-sum + hashmap count is the other standard route.)
Counting subarrays with an exact property is deceptively hard directly, so flip it: exactly(k) = atMost(k) − atMost(k−1). And atMost is a plain variable window with one beautiful line: when the window [L,R] is valid, every subarray ending at R and starting anywhere from L to R is valid too — that's R − L + 1 new subarrays, added in one step. Watch the count jump by the window width each time R advances. Binary Subarrays With Sum, Nice Subarrays and K Different Integers are all this subtraction.
“Count subarrays with sum exactly goal” on a 0/1 array. 'Count subarrays with an exact sum' is the atMost subtraction.
You can't cleanly count 'exactly goal' with one window, so use exactly(goal) = atMost(goal) − atMost(goal−1). Each atMost(x) is a variable window: grow R adding to the sum, shrink L while the sum exceeds x, and add R − L + 1 per step.
// exactly(goal) = atMost(goal) - atMost(goal-1) on a 0/1 array. int atMost(vector<int>& a, int x) { if (x < 0) return 0; int L = 0, sum = 0, count = 0; for (int R = 0; R < (int)a.size(); R++) { sum += a[R]; while (sum > x) sum -= a[L++]; // shrink until sum <= x count += R - L + 1; // subarrays ending at R } return count; } int numSubarraysWithSum(vector<int>& a, int goal) { return atMost(a, goal) - atMost(a, goal - 1); }
// exactly(goal) = atMost(goal) - atMost(goal-1) on a 0/1 array. int atMost(int[] a, int x) { if (x < 0) return 0; int L = 0, sum = 0, count = 0; for (int R = 0; R < a.length; R++) { sum += a[R]; while (sum > x) sum -= a[L++]; // shrink until sum <= x count += R - L + 1; // subarrays ending at R } return count; } public int numSubarraysWithSum(int[] a, int goal) { return atMost(a, goal) - atMost(a, goal - 1); }
# exactly(goal) = atMost(goal) - atMost(goal-1) on a 0/1 array. def numSubarraysWithSum(a, goal): def atMost(x): if x < 0: return 0 L = s = count = 0 for R in range(len(a)): s += a[R] while s > x: s -= a[L]; L += 1 # shrink until sum <= x count += R - L + 1 # subarrays ending at R return count return atMost(goal) - atMost(goal - 1)
Forgetting the atMost(goal−1) pass, or the x < 0 guard. atMost(goal) alone counts too many; the subtraction isolates 'exactly'. And atMost(−1) must return 0 (when goal == 0) or you over-subtract. A prefix-sum + hashmap counting method is the equally valid alternative.
Count Nice Subarrays asks for subarrays with exactly k odd numbers — and it is Binary Subarrays With Sum in disguise. Treat each odd number as a 1 and each even as a 0; then 'exactly k odds' becomes 'sum exactly k', solved by the very same atMost(k) − atMost(k−1). The only change from the previous problem is what you add to the running sum.
HOW MANY SUBARRAYS CONTAIN EXACTLY k ODD NUMBERS?
What single change turns the Binary Subarrays With Sum solution into Count Nice Subarrays?
Just relabel odds as 1s. The property 'contains exactly k odd numbers' is 'the parity-array sum equals k'. Feed nums[i] % 2 into the same atMost window and subtract — no new algorithm. Recognising this reuse is the entire point of grouping these problems together.
Both Binary Subarrays and Nice Subarrays are 0/1 counting problems. What's the other classic way to count 'subarrays with sum k' on such arrays?
Prefix sum + hash map. Keep a running prefix sum and a map of how many times each prefix value has occurred; the number of subarrays ending at R with sum k is the count of prefix value prefix − k seen so far. It's a good alternative to atMost − atMost and generalises to arrays with negatives, where the monotone-window trick breaks.
There is no window whose validity is exactly three odds — growing R can jump straight past it. So the problem is rewritten: atMost(k) − atMost(k−1), and each of those is an ordinary shrink-while-invalid run. This slide is the first of the two calls; the answer is the difference between them.
“Count subarrays with exactly k odd numbers.” 'Exactly k of a property' → the atMost subtraction, with odds mapped to 1s.
Treat each odd number as 1 and even as 0; then 'exactly k odds' is 'sum exactly k', identical to Binary Subarrays With Sum. Compute atMost(k) − atMost(k−1) where atMost counts subarrays whose odd-count is ≤ x.
-// exactly(goal) = atMost(goal) - atMost(goal-1) on a 0/1 array.+// Odd -> 1, so exactly k odds = atMost(k) - atMost(k-1). int atMost(vector<int>& a, int x) { if (x < 0) return 0; int L = 0, sum = 0, count = 0; for (int R = 0; R < (int)a.size(); R++) {- sum += a[R];- while (sum > x) sum -= a[L++]; // shrink until sum <= x+ sum += a[R] & 1;+ while (sum > x) sum -= a[L++] & 1; // shrink until #odds <= x count += R - L + 1; // subarrays ending at R } return count; }-int numSubarraysWithSum(vector<int>& a, int goal) {- return atMost(a, goal) - atMost(a, goal - 1);+int numberOfSubarrays(vector<int>& a, int k) {+ return atMost(a, k) - atMost(a, k - 1); }
-// exactly(goal) = atMost(goal) - atMost(goal-1) on a 0/1 array.+// Odd -> 1, so exactly k odds = atMost(k) - atMost(k-1). int atMost(int[] a, int x) { if (x < 0) return 0; int L = 0, sum = 0, count = 0; for (int R = 0; R < a.length; R++) {- sum += a[R];- while (sum > x) sum -= a[L++]; // shrink until sum <= x- count += R - L + 1; // subarrays ending at R+ sum += a[R] & 1;+ while (sum > x) sum -= a[L++] & 1; // shrink until #odds <= x+ count += R - L + 1; // subarrays ending at R } return count; }-public int numSubarraysWithSum(int[] a, int goal) {- return atMost(a, goal) - atMost(a, goal - 1);+public int numberOfSubarrays(int[] a, int k) {+ return atMost(a, k) - atMost(a, k - 1); }
-# exactly(goal) = atMost(goal) - atMost(goal-1) on a 0/1 array.-def numSubarraysWithSum(a, goal):+# Odd -> 1, so exactly k odds = atMost(k) - atMost(k-1).+def numberOfSubarrays(a, k): def atMost(x): if x < 0: return 0 L = s = count = 0 for R in range(len(a)):- s += a[R]+ s += a[R] & 1 while s > x:- s -= a[L]; L += 1 # shrink until sum <= x- count += R - L + 1 # subarrays ending at R+ s -= a[L] & 1; L += 1 # shrink until #odds <= x+ count += R - L + 1 return count- return atMost(goal) - atMost(goal - 1)+ return atMost(k) - atMost(k - 1)
Over-thinking it. This is Binary Subarrays With Sum with a[i] & 1 in place of a[i] — the same two-pass subtraction. The usual slip is a fresh, buggy 'exactly' window instead of reusing the proven atMost − atMost. Prefix-sum-of-parities + hashmap also works.
Subarrays with K Different Integers is the counting group's hardest, and its cleanest demonstration of the identity: exactly k distinct = atMost(k) − atMost(k−1). Here atMost(x) is the at-most-x-distinct counting window from Unit 6 — grow R, shrink L while the distinct count exceeds x, and add R − L + 1. Two such passes, subtracted, give the exact-distinct count in O(n).
HOW MANY SUBARRAYS HAVE EXACTLY k DISTINCT INTEGERS?
This 'Hard' problem reuses which earlier window, and with what modification?
The at-most-k-distinct window, in counting mode, run twice. You already built the map-based distinct window for Fruit / K Distinct. Replace best = max(...) with count += R − L + 1 to get atMost(x), then compute atMost(k) − atMost(k−1). A 'Hard' rating that dissolves into two things you've already drilled — that's the payoff of learning the templates.
Why is atMost(k) − atMost(k−1) exactly the count of subarrays with exactly k distinct?
Set subtraction on nested conditions. The subarrays with at most k distinct split cleanly into those with at most k−1 and those with exactly k. So exactly(k) = atMost(k) − atMost(k−1) — exact, not approximate. The same reasoning underlies every 'exactly k' counting problem in this deck.
Identical to unit 10 with one line changed: the validity test counts distinct values instead of odd ones. Everything else — the shrink, the count += R−L+1, the second call with k−1 — is untouched. Learning this as a template rather than as a problem is what makes the second one free.
“Count subarrays with exactly k distinct integers” (Hard). 'Exactly k distinct' → atMost(k) − atMost(k−1) on the distinct-count window.
Reuse the at-most-k-distinct window from Fruit / K Distinct, but in counting mode: for each R, shrink L while the window holds more than x distinct values, then add R − L + 1. That's atMost(x); the answer is atMost(k) − atMost(k−1).
// exactly k distinct = atMost(k) - atMost(k-1), counting window. int atMost(vector<int>& a, int x) { unordered_map<int, int> cnt; int L = 0, count = 0; for (int R = 0; R < (int)a.size(); R++) { cnt[a[R]]++; while ((int)cnt.size() > x) { // > x distinct -> shrink if (--cnt[a[L]] == 0) cnt.erase(a[L]); L++; } count += R - L + 1; } return count; } int subarraysWithKDistinct(vector<int>& a, int k) { return atMost(a, k) - atMost(a, k - 1); }
// exactly k distinct = atMost(k) - atMost(k-1), counting window. int atMost(int[] a, int x) { Map<Integer, Integer> cnt = new HashMap<>(); int L = 0, count = 0; for (int R = 0; R < a.length; R++) { cnt.merge(a[R], 1, Integer::sum); while (cnt.size() > x) { // > x distinct -> shrink if (cnt.merge(a[L], -1, Integer::sum) == 0) cnt.remove(a[L]); L++; } count += R - L + 1; } return count; } public int subarraysWithKDistinct(int[] a, int k) { return atMost(a, k) - atMost(a, k - 1); }
# exactly k distinct = atMost(k) - atMost(k-1), counting window. def subarraysWithKDistinct(a, k): def atMost(x): cnt = {} L = count = 0 for R in range(len(a)): cnt[a[R]] = cnt.get(a[R], 0) + 1 while len(cnt) > x: # > x distinct -> shrink cnt[a[L]] -= 1 if cnt[a[L]] == 0: del cnt[a[L]] L += 1 count += R - L + 1 return count return atMost(k) - atMost(k - 1)
Trying to count exactly-k distinct in one pass. There's no clean per-R count for 'exactly', so the direct attempt double-counts or misses. The whole trick is the subtraction of two at-most-distinct windows — the same window you built for Fruit, switched from max-length to counting. Remember to erase zero-count keys so size() is the true distinct count.
Minimum Window Substring is the shortest covering window — the hardest problem in the deck, and just the window template run in reverse. You want the smallest window of s that contains all characters of t (with multiplicity). Expand R until the window is valid, then contract L as far as it stays valid, recording the minimum length each time. A need/have counter makes validity an O(1) check.
WHAT IS THE SMALLEST WINDOW OF s CONTAINING ALL OF t?
How does the need/have bookkeeping decide, in O(1), whether the window currently covers t?
Count fully-satisfied requirements, not characters. required is the number of distinct characters in t. Each time a character's window-count reaches its need, increment have; when it drops below, decrement. The window covers t exactly when have == required — an O(1) test you maintain incrementally as R and L move.
During the contract phase, when do you stop moving L?
Stop when the next removal breaks validity. While the window stays valid you keep shrinking and recording the (shrinking) length; as soon as dropping s[L] would take some character below its required count, have falls below required and you stop, going back to growing R. The smallest window for each right endpoint is captured right before that break.
The mirror image of the longest template. Here you want the smallest window that covers a requirement (contains all of t). So grow the right edge until the window becomes valid, then contract the left edge as far as it stays valid, recording the minimum length each time it's valid. When shrinking finally breaks validity, resume growing R. A need/have counter tells you validity in O(1). It's the exact shape of Minimum Window Substring — the hardest problem in the deck, and just this loop with bookkeeping.
“Smallest window of s containing all of t” (Hard). 'Minimum window covering a requirement' is the shortest window template.
Expand R until the window contains every character of t (with multiplicity), tracked by a need/have counter. Once valid, contract L as far as it stays valid, recording the minimum length. Resume expanding when it breaks.
// Expand to valid, contract to minimal; need/have tracks coverage. string minWindow(string s, string t) { if (t.empty() || s.size() < t.size()) return ""; unordered_map<char, int> need; for (char c : t) need[c]++; int required = need.size(), have = 0; int L = 0, bestLen = INT_MAX, bestL = 0; unordered_map<char, int> win; for (int R = 0; R < (int)s.size(); R++) { char c = s[R]; win[c]++; if (need.count(c) && win[c] == need[c]) have++; while (have == required) { // valid -> shrink if (R - L + 1 < bestLen) { bestLen = R - L + 1; bestL = L; } char d = s[L]; win[d]--; if (need.count(d) && win[d] < need[d]) have--; L++; } } return bestLen == INT_MAX ? "" : s.substr(bestL, bestLen); }
// Expand to valid, contract to minimal; need/have tracks coverage. public String minWindow(String s, String t) { if (t.isEmpty() || s.length() < t.length()) return ""; Map<Character, Integer> need = new HashMap<>(); for (char c : t.toCharArray()) need.merge(c, 1, Integer::sum); int required = need.size(), have = 0; int L = 0, bestLen = Integer.MAX_VALUE, bestL = 0; Map<Character, Integer> win = new HashMap<>(); for (int R = 0; R < s.length(); R++) { char c = s.charAt(R); win.merge(c, 1, Integer::sum); if (need.containsKey(c) && win.get(c).intValue() == need.get(c).intValue()) have++; // this char is now fully met while (have == required) { // valid: try to tighten if (R - L + 1 < bestLen) { bestLen = R - L + 1; bestL = L; } char d = s.charAt(L); win.merge(d, -1, Integer::sum); if (need.containsKey(d) && win.get(d) < need.get(d)) have--; L++; } } return bestLen == Integer.MAX_VALUE ? "" : s.substring(bestL, bestL + bestLen); }
# Expand to valid, contract to minimal; need/have tracks coverage. from collections import Counter def minWindow(s, t): if not t or len(s) < len(t): return "" need = Counter(t) required, have = len(need), 0 win = {} L, best_len, best_l = 0, float('inf'), 0 for R, c in enumerate(s): win[c] = win.get(c, 0) + 1 if c in need and win[c] == need[c]: have += 1 while have == required: # valid -> shrink if R - L + 1 < best_len: best_len, best_l = R - L + 1, L d = s[L] win[d] -= 1 if d in need and win[d] < need[d]: have -= 1 L += 1 return "" if best_len == float('inf') else s[best_l:best_l + best_len]
Recording the answer while expanding, or comparing counts wrong. The minimum windows appear only while contracting — update best inside the while have == required loop. Increment have exactly when a count reaches its need (==, not ≥) and decrement when it drops below, or the validity check drifts.
Minimum Window Subsequence is the twist ending, and where two pointers meets a little DP. You want the smallest window of s that contains t as a subsequence (same order, not necessarily contiguous) — so the frequency-count trick of Minimum Window Substring no longer applies; order matters. The classic O(n·m) approach: sweep s, and whenever you finish matching t forward, walk backward to find the tightest start for that end. It has no lecture in this playlist — the drills carry it.
SMALLEST WINDOW OF s THAT CONTAINS t AS A SUBSEQUENCE (ORDER MATTERS)?
Why does the Minimum Window Substring approach (need/have frequency counts) fail for Minimum Window Subsequence?
Counts don't encode order. Minimum Window Substring only needs enough of each character anywhere in the window. A subsequence needs t's letters to appear in order (with gaps allowed), which a multiset can't check. So you match t against s positionally — advance a pointer in t only when the current s character equals it — rather than counting frequencies.
After a forward pass matches all of t ending at some index e in s, what does the backward walk accomplish?
It tightens the start for a fixed end. A forward match tells you an end index where t is complete, but the start it used may be unnecessarily early. Walking backward from e, re-matching t in reverse, snaps the start as late as possible, giving the minimum-length window ending at e. Sweeping all such ends and keeping the shortest is the O(n·m) solution; a DP table over (i in s, j in t) is the other standard route.
Unit 12 kept a window valid incrementally. That is impossible here, because the letters need not touch — so this walks forward to match t in order, and then backward from the landing point, matching t in reverse, to pull the left edge in as far as it will go. The backward pass is the entire difference, and it exists because a subsequence has slack a substring does not.
“Smallest window of s with t as a subsequence” (Hard, premium). 'Subsequence, order matters' rules out frequency counting — it's a positional two-pointer / DP.
Sweep s matching t forward one character at a time; each time you finish t, you have an end index. Walk backward from there, re-matching t in reverse, to snap the start as late as possible — that's the tightest window ending there. Keep the shortest across all completions.
// Forward-match t, then walk back to tighten the start. string minWindow(string s, string t) { int n = s.size(), m = t.size(), bestLen = INT_MAX, bestStart = -1; int i = 0; while (i < n) { int j = 0; while (i < n) { // forward match t if (s[i] == t[j]) j++; if (j == m) break; i++; } if (j < m) break; // t not completed int end = i; j = m - 1; while (j >= 0) { // walk back to latest start if (s[i] == t[j]) j--; i--; } i++; // i is now the tightest start if (end - i + 1 < bestLen) { bestLen = end - i + 1; bestStart = i; } i++; // resume just past this start } return bestStart < 0 ? "" : s.substr(bestStart, bestLen); }
// Forward-match t, then walk back to tighten the start. public String minWindow(String s, String t) { int n = s.length(), m = t.length(); int bestLen = Integer.MAX_VALUE, bestStart = -1; int i = 0; while (i < n) { int j = 0; while (i < n) { // forward match t if (s.charAt(i) == t.charAt(j)) j++; if (j == m) break; i++; } if (i == n) break; // t never completed int end = i, k = m - 1; while (k >= 0) { // walk BACK to the latest start if (s.charAt(i) == t.charAt(k)) k--; i--; } int start = i + 1; if (end - start + 1 < bestLen) { bestLen = end - start + 1; bestStart = start; } i = start + 1; // resume just past this start } return bestStart < 0 ? "" : s.substring(bestStart, bestStart + bestLen); }
# Forward-match t, then walk back to tighten the start. def minWindow(s, t): n, m = len(s), len(t) best_len, best_start = float('inf'), -1 i = 0 while i < n: j = 0 while i < n: # forward match t if s[i] == t[j]: j += 1 if j == m: break i += 1 if j < m: break # t not completed end = i j = m - 1 while j >= 0: # walk back to latest start if s[i] == t[j]: j -= 1 i -= 1 i += 1 # tightest start if end - i + 1 < best_len: best_len, best_start = end - i + 1, i i += 1 # resume past this start return "" if best_start < 0 else s[best_start:best_start + best_len]
Reusing the Minimum Window Substring frequency counts. A subsequence needs t's characters in order, which multiset counts can't verify — you must match positionally. The forward-then-backward sweep is the neat O(n·m) way; the backward walk is what makes each window minimal. A DP over (i in s, j in t) is the alternative. (LeetCode premium.)
Match the phrasing to the template: “count subarrays with exactly K odd numbers”.
Counting via the at-most subtraction. You cannot cleanly count “exactly K” in one window pass, but “at most K” is a standard variable window (shrink while the odd-count exceeds K, add R−L+1 each step). Then exactly(K) = atMost(K) − atMost(K−1). That's Count Nice Subarrays verbatim (odds treated as 1s), and the identical trick solves Binary Subarrays With Sum and Subarrays with K Different Integers.
In the shortest-window template (Minimum Window Substring), when do you record a candidate answer — while expanding R, or while contracting L?
While contracting L. Expanding R only ever makes the window bigger, so the smallest windows appear while you're shrinking. The pattern is: grow R until valid, then contract L and record the length on every step that remains valid; when a contraction finally breaks validity, go back to growing R. Recording during expansion would only ever capture over-long windows.
Window bugs pass the sample and fail the judge: an if that should be a while, a best updated too early, an exactly counted directly, a stale frequency map. Every one returns a believable number.
When the window becomes invalid you often must move L more than once. An if shrinks a single step and can leave the window still invalid; use a while so L advances until validity is restored. (The one exception is the O(n) 'never shrink below best' longest-window trick, which deliberately uses a single if.)
For a longest-window problem, record R−L+1 only after the shrink loop has restored validity. Recording mid-shrink counts an invalid window and overstates the answer — a classic silent off-by-one.
Trying to count subarrays with exactly k in a single window doesn't compose — the same left boundary can serve many rights ambiguously. Always go through atMost(k) − atMost(k−1). Forgetting the k−1 pass is the usual bug, and it returns a plausible-but-wrong count.
In the counting template each new R contributes every subarray ending at R that starts in [L,R] — that's R−L+1, not 1. Adding 1 counts only the single-element or full window and undercounts massively.
On a sorted array with pointers at both ends, if the sum is too small you must move L rightward (to increase it), and if too big move R leftward. Moving the wrong one walks away from the answer and can loop past it.
When L leaves an element you must decrement its frequency and, if it hits zero, decrement the distinct-count. Skipping the zero check leaves a phantom distinct value in the map and the window never shrinks correctly — it usually manifests as an answer that's too long.
Seven windows, one page. The right-hand column is the phrase in the statement that should trigger each — the night-before surface.
Four templates — constant, longest, counting, shortest — and a single idea underneath: two pointers that only move forward, turning an O(n²) subarray scan into O(n). Every one ran live on the L/R track, and every problem was primed, watched, drilled and applied. Next on the sheet: stacks & queues, then heaps.
All 12 playlist lectures are used. Minimum Window Subsequence (premium) has no lecture in this playlist and is a lecture-less unit. Two problems (At Most K Distinct, Min Window Subsequence) are LeetCode premium — the pattern is taught in full regardless.
Not a preference — an arithmetic one. Every slide is a fixed 1280 × 720 stage: a graph animating beside the code that drives it, with the problems laid out two columns wide. It scales as a single piece, so on a screen this size the body text comes out around 4px tall.
Shrinking it further would not help, and rebuilding it to reflow would mean losing the thing that makes it worth reading.