DSA sheet · Arrays · Sliding window pattern (counting)
Subarrays with K Different Integers
This is a hard problem, and the reason it's hard is one small word: exactly. A sliding window is very good at counting windows with at most K different numbers, but it can't directly count windows with exactly K. The teacher first writes the two-loop brute force, shows that it's too slow, then builds the "at most K" window, and finally turns "at most" into "exactly" with one subtraction:
exactly(K) = atMost(K) − atMost(K − 1)
If you understand why this subtraction works on this page, many other "exactly K" counting problems (binary subarrays with sum K, nice subarrays with K odd numbers…) become the same 10 lines.
Every part below follows the same order:
① the question in simple words → ② what the constraints tell us → ③ intuition → ④ building the logic from examples → ⑤ approach steps → ⑥ code → ⑦ code line by line → ⑧ dry run → ⑨ complexity & remember
- Part 0 · Sliding window from scratch (what this page needs)
- Part A · Brute force: two loops + a frequency map
- Part B · Sliding window that counts "at most K different"
- Part C · Exactly K = atMost(K) − atMost(K − 1)
- Part D · Revision page
Part 0 · Before starting
Subarray = a continuous piece
A subarray is a piece of the array taken without gaps: you pick a start index and an end index and take everything in between. For [1, 2, 1] the subarrays are [1], [2], [1], [1,2], [2,1], [1,2,1]. But [1, 1] (skipping the 2) is not a subarray. (For strings the same idea is called a substring.)
An array of size n has n·(n+1)/2 subarrays. For n = 5 that's 15; for n = 2·10⁴ it's about 2·10⁸.
The brute force and why it repeats work
The obvious way to look at every subarray is two loops: i = start, j = end. That's O(n²) windows. The waste: when i moves by one step, the inner loop walks again over almost the same elements it just walked over. A sliding window avoids this by keeping what it already knows about the current piece and only adding one item on the right or removing one item on the left.
The window [left..right]
- Two indexes:
left(start) andright(end). The window is everything fromlefttoright, both included. Its length isright − left + 1. - Expanding: move
rightone step and addnums[right]to whatever we're tracking (a sum, a count, a map). - Shrinking: remove
nums[left]from what we're tracking, then moveleftone step. - Both pointers only ever move forward. That's why the total work is about 2n, not n².
Fixed vs variable windows
| Fixed-size window | Variable-size window (this page) | |
|---|---|---|
| size | always k (the problem gives k) | grows and shrinks |
| how it moves | add the new right item, drop the item that falls off the left | expand right every step; while the window is invalid, shrink from the left |
| example | max of every window of size k | windows with at most K different numbers |
Three kinds of questions a variable window answers
- Longest valid window: after shrinking, the window is valid →
best = max(best, right − left + 1). - Shortest valid window: while the window is valid, record its length, then shrink to try for shorter.
- Count of valid windows (this page): after shrinking,
count += right − left + 1. Part B explains exactly why.
Why a sliding window needs a "monotonic" condition
Shrinking only makes sense if the rule behaves nicely: if a window is valid, every smaller window inside it is valid too, and if it's invalid, every bigger window around it is invalid too. "At most K different numbers" has this property: taking items away can never add a new different number. So once the window has too many, the only fix is to shrink.
"Exactly K different" does not have it. [1,2] has exactly 2, but its piece [2] has only 1. That's why we can't count "exactly" directly and need the subtraction trick of Part C. (The same problem shows up in sum problems with negative numbers: adding an item can make the sum smaller, so "too big → shrink" stops being true.)
Frequency map (dict / Counter)
To know "how many different numbers are in my window", we keep a dictionary freq: key = a number, value = how many times it appears in the window right now.
- Number of different values =
len(freq), the number of keys. - Add x:
freq[x] = freq.get(x, 0) + 1. Theget(x, 0)gives 0 when x isn't a key yet. (Java'sgetOrDefault, which the teacher uses, is the same thing.) - Remove x:
freq[x] -= 1, and if it drops to 0, delete the key (del freq[x]), otherwiselen(freq)would still count it. - Python also has
collections.Counter, a dict that returns 0 for missing keys. A plain dict is enough here.
Which optimisation pattern? (the teacher's 4-pattern check)
Every time a brute force is too slow, the teacher runs through four array patterns she taught at the start of the playlist:
| pattern | use it when… |
|---|---|
| Two pointers | you only care about the two ends (two items), not what's between them |
| Sliding window | you need everything between a start and an end (a whole continuous piece) |
| Prefix sum | you need sums of many ranges, asked again and again (many queries) |
| Kadane's algorithm | you want a best subarray sum when negative numbers are allowed |
Here: we care about whole subarrays (everything in between) → sliding window. Values are ≥ 1 (no negatives, no sums anyway) → Kadane and prefix sums don't fit, and two pointers alone isn't enough. So: sliding window.
Part A · Brute force: two loops + a frequency map
LeetCode 992 · Hard
1The question in simple words
You get an integer array nums and a number k. A subarray is good if the number of different values inside it is exactly k. Return how many good subarrays there are.
The teacher reads example 1 by size. Starting at index 0:
[1,2](size 2): different values {1, 2} → 2 ✓[1,2,1](size 3): still {1, 2} → 2 ✓[1,2,1,2](size 4): still {1, 2} → 2 ✓[1,2,1,2,3](size 5): {1, 2, 3} → 3 ✗, more than k is not allowed.
Doing this from every start gives all 7 good subarrays:
| # | subarray | indexes | different values |
|---|---|---|---|
| 1 | [1,2] | 0..1 | {1,2} |
| 2 | [1,2,1] | 0..2 | {1,2} |
| 3 | [1,2,1,2] | 0..3 | {1,2} |
| 4 | [2,1] | 1..2 | {1,2} |
| 5 | [2,1,2] | 1..3 | {1,2} |
| 6 | [1,2] | 2..3 | {1,2} |
| 7 | [2,3] | 3..4 | {2,3} |
Example 2 on LeetCode: nums = [1,2,1,3,4], k = 3 → 3: [1,2,1,3], [2,1,3], [1,3,4].
2What the constraints tell us
1 ≤ nums.length ≤ 2·10⁴. So n² = 4·10⁸. The teacher's rule: beyond about 10⁸ simple steps you risk TLE (Time Limit Exceeded). 4·10⁸ is past that → the brute force is only a starting point. We need something close to O(n).1 ≤ nums[i] ≤ n: no negative numbers, so Kadane is ruled out (see Part 0).1 ≤ k ≤ n: k is at least 1, so laterk − 1is at least 0. Keep this in mind for Part C.
3Intuition
Put your left finger i on a start index. Move your right finger j to the right, one item at a time, and keep a little notebook (the frequency map) of which values you've seen and how often. After each new item, look at how many different values the notebook has:
- exactly k → this subarray is good, count it;
- more than k → stop moving
j; nothing further right can fix it; - fewer than k → not good yet, keep going.
4Building the logic, the way the teacher does
→ The teacher recalls Fruits Into Baskets. Think of the window
[1,2,1] and drop the first 1 to get [2,1]. With a set you would remove 1 from the set, but there is still a 1 in the window! The set can't tell how many copies there were. A map stores counts: 1 → 2, 2 → 1. Dropping one 1 makes it 1 → 1, and the key stays. Nothing has to be rebuilt. In the brute force we only add, so a set would work there, but the sliding window in Part B removes items, so we use a map from the start.j start at 0 or at i?→ At
i. Subarrays that start before i were already counted when i was smaller. Starting j at i also counts the 1-item subarray [nums[i]], which is good when k = 1.→ Inside the
i loop, so every new start begins with an empty notebook. If it were created once outside, the counts from the previous start would leak into the next one.break instead of just skipping?→ In the dry run, at
j = 4 the 3 enters and the map has 3 keys. Moving j further only adds items. Adding can never reduce the number of different values. So for this i, every longer subarray is also bad. Stop and move to the next i. That's the extra elif len(freq) > k: break.5Approach steps
count = 0.- For each start
i: make an empty map. - For each end
jfromi: addnums[j]to the map. - If the map has exactly k keys →
count += 1. Else if it has more than k → break. - Return
count.
6Code (Python)
class Solution:
def subarraysWithKDistinct(self, nums, k):
n = len(nums)
count = 0
for i in range(n): # every start
freq = {} # fresh map for this start
for j in range(i, n): # every end, from i
freq[nums[j]] = freq.get(nums[j], 0) + 1
if len(freq) == k: # exactly k different
count += 1
elif len(freq) > k: # too many: longer is worse
break
return count7Code line by line
| line | what it means |
|---|---|
| for i in range(n): | Try every start position. |
| freq = {} | New, empty notebook for this start. |
| for j in range(i, n): | Grow the subarray one item at a time: nums[i..j]. |
| freq[nums[j]] = freq.get(nums[j], 0) + 1 | Add the new item. If it's new, get gives 0, so it becomes 1. |
| if len(freq) == k: count += 1 | The number of keys = the number of different values. Exactly k → good subarray. |
| elif len(freq) > k: break | Too many different values. Every longer subarray from this start is bad too. |
8Dry run for i = 0 (the teacher's walk-through)
| j | added | subarray | map after adding | keys | action | count |
|---|---|---|---|---|---|---|
| 0 | 1 | [1] | {1:1} | 1 | fewer than 2, keep going | 0 |
| 1 | 2 | [1,2] | {1:1, 2:1} | 2 | count it | 1 |
| 2 | 1 | [1,2,1] | {1:2, 2:1} | 2 | count it (1 already a key, only its count grows) | 2 |
| 3 | 2 | [1,2,1,2] | {1:2, 2:2} | 2 | count it | 3 |
| 4 | 3 | [1,2,1,2,3] | {1:2, 2:2, 3:1} | 3 | 3 > 2 → break | 3 |
Then i = 1 starts over with an empty map: [2], [2,1] ✓, [2,1,2] ✓, [2,1,2,3] → break (count 5). i = 2: [1], [1,2] ✓, then 3 → break (count 6). i = 3: [2], [2,3] ✓ (count 7). i = 4: [3] only. Answer 7 ✓.
Look at the yellow cells: the inner loop walks over the same items again for every new i. This repeated work is exactly what the sliding window removes.
9Complexity & remember
- Time O(n²): the outer loop runs n times, and for each start the inner loop can run up to n times (worst case: few different values, so it never breaks early). With n = 2·10⁴ that's up to 4·10⁸ steps → TLE risk, and slow even when it passes. An interviewer will ask for better.
- Space O(n): the map can hold up to n keys (actually at most k + 1 keys here, because we break at k + 1).
nums[j] · exactly k → count · more than k → break. Correct, but O(n²).Part B · Sliding window that counts "at most K different"
1The question (a slightly different one)
Before solving "exactly k", we solve an easier question: how many subarrays have at most k different values? "At most k" means k or fewer (≤ k). A sliding window answers this directly, and Part C turns it into "exactly k".
2What the constraints tell us
Same as Part A: n up to 2·10⁴, so we want O(n). Values are positive, and the "at most k different" rule is monotonic (Part 0): removing items never adds a different value. So "too many → shrink from the left" is always the right move.
3Intuition
In the brute force, when i moves, we threw away everything we knew about [2, 1, 2] and counted it again. The sliding window says: don't recount. Add one item on the right; if that breaks the rule, remove items from the left until it's fine again.
The teacher splits it into two phases:
- Expand phase:
rightmoves forward every step and addsnums[right]to the map, as long as the rule holds. - Shrink phase: when the map has more than k keys,
leftremovesnums[left]and moves forward, until the map has ≤ k keys again.
A sliding window is naturally an "at most" tool: after shrinking, the window has ≤ k different values, which includes both "fewer than k" and "exactly k". That's why this part counts too many for our real question, and Part C fixes it.
4Building the logic
Why count += right − left + 1?
After shrinking, [left..right] is the longest valid window that ends at right. Every shorter window that also ends at right sits inside it, so it's valid too (monotonic rule). How many windows end at right and start somewhere in left..right? One per start → right − left + 1, which is just the window's length.
The teacher's picture uses [1, 2, 1, 3, 4]. Say the window is [1, 2, 1, 3] (indexes 0..3). The subarrays that end exactly at index 3 are:
And the subarrays ending at index 0, 1 and 2 were already counted at those earlier steps (1, then 2, then 3 of them). So adding the window length at every step counts every valid subarray exactly once, grouped by where it ends.
while for shrinking, not if?→ One removal may not be enough. In the dry run below, the 3 arrives and we have to remove three items (1, 2, 1) before only 2 different values remain.
freq[x] -= 1, why delete the key when it hits 0?→ The teacher caught this during her own dry run. After removing the last 1, the map was
{1:0, 2:1, 3:1}: the count of 1 was zero, but the key was still there, so the size still said 3 and the loop wouldn't stop. A value with count 0 isn't in the window, so its key must go: if freq[x] == 0: del freq[x].→ After it. We only count once the window is valid again. Every step adds something (the window always has at least the new item… unless k = 0, see Part C).
5Approach steps
left = 0,count = 0, empty map.- For
rightfrom 0 to n − 1: addnums[right]to the map (expand). - While the map has more than k keys: decrease
nums[left]'s count, delete it if it's 0,left += 1(shrink). count += right − left + 1.- Return
count.
6Code (Python)
class Solution:
def atMost(self, nums, k):
left = 0
count = 0
freq = {}
for right in range(len(nums)):
# expand: take nums[right] into the window
freq[nums[right]] = freq.get(nums[right], 0) + 1
# shrink: too many different values -> drop from the left
while len(freq) > k:
freq[nums[left]] -= 1
if freq[nums[left]] == 0:
del freq[nums[left]] # gone from the window
left += 1
# every window ending at right and starting in left..right is valid
count += right - left + 1
return count7Code line by line
| line | what it means |
|---|---|
| left = 0; count = 0; freq = {} | The window starts empty at the front. freq lives outside the loop: it's the memory we keep while sliding. |
| for right in range(len(nums)): | The right edge visits every index once. |
| freq[nums[right]] = freq.get(nums[right], 0) + 1 | Expand: one more copy of this value in the window. |
| while len(freq) > k: | The window breaks the rule (too many different values). |
| freq[nums[left]] -= 1 | The leftmost item leaves, so one copy fewer. (The teacher's first run forgot the nums[left] key inside her Java put and fixed it.) |
| if freq[nums[left]] == 0: del freq[nums[left]] | No copies left → that value is no longer in the window → remove the key, so len(freq) is right. |
| left += 1 | The window now starts one step later. |
| count += right - left + 1 | Number of valid subarrays that end at right. |
8Dry run: atMost(2) on [1, 2, 1, 2, 3]
| step | right (added) | window before | map after adding | shrink? (what leaves, why) | window after | count |
|---|---|---|---|---|---|---|
| 1 | 0 (1) | [1] | {1:1} | no, 1 key ≤ 2 | [1] (0..0) | 0 + 1 = 1 |
| 2 | 1 (2) | [1,2] | {1:1, 2:1} | no, 2 keys | [1,2] (0..1) | 1 + 2 = 3 |
| 3 | 2 (1) | [1,2,1] | {1:2, 2:1} | no, still 2 keys | [1,2,1] (0..2) | 3 + 3 = 6 |
| 4 | 3 (2) | [1,2,1,2] | {1:2, 2:2} | no | [1,2,1,2] (0..3) | 6 + 4 = 10 |
| 5 | 4 (3) | [1,2,1,2,3] | {1:2, 2:2, 3:1} | yes, 3 keys > 2: • 1 leaves → {1:1,…}, left = 1, still 3 keys • 2 leaves → {2:1,…}, left = 2, still 3 keys • 1 leaves → 1:0 → delete key, left = 3 → {2:1, 3:1}, 2 keys ✓ | [2,3] (3..4) | 10 + 2 = 12 |
The function returns 12, but the expected answer is 7. That's not a bug: these 12 subarrays have at most 2 different values, so they include the 5 single-item subarrays that have only 1. Part C removes them.
9Complexity & remember
- Time O(n): there's a
whileinside thefor, butleftonly moves forward and never comes back. Over the whole run,righttouches each index once andlefttouches each index at most once (worst case: all values different). Total about 2n steps → O(2n) = O(n). - Space O(n): the map, in the worst case when all values are different. (At most k + 1 keys at any moment, so O(k).)
count += right − left + 1. It counts windows with k or fewer different values.Part C · Exactly K = atMost(K) − atMost(K − 1)
1The question in simple words
Back to the real question: count the subarrays with exactly k different values. We have a fast tool that counts "k or fewer". We need to throw away the ones with "fewer than k".
2What the constraints tell us
k ≥ 1, so k − 1 ≥ 0. atMost(0) works: every new item makes the map size 1 > 0, the loop removes it straight away, the window becomes empty, and we add right − (right+1) + 1 = 0. So atMost(0) = 0, which is right (no subarray has 0 different values).
3Intuition
Sort every subarray into buckets by how many different values it has: bucket 1, bucket 2, bucket 3…
atMost(k)= bucket 1 + bucket 2 + … + bucket k.atMost(k − 1)= bucket 1 + … + bucket k−1.- Subtract: everything cancels except bucket k, which is exactly what we want.
It's like asking "how many students are aged exactly 15?" = "how many are 15 or younger" − "how many are 14 or younger".
4Building it with the actual subarrays (k = 2)
All 12 subarrays counted by atMost(2) on [1,2,1,2,3], grouped by the index they end at (this is exactly how count += right − left + 1 adds them):
| ends at | window | subarrays added (shortest first) | added |
|---|---|---|---|
| 0 | 0..0 | [1] | 1 |
| 1 | 0..1 | [2], [1,2] | 2 |
| 2 | 0..2 | [1], [2,1], [1,2,1] | 3 |
| 3 | 0..3 | [2], [1,2], [2,1,2], [1,2,1,2] | 4 |
| 4 | 3..4 | [3], [2,3] | 2 |
| atMost(2) | 12 | ||
The red ones have only 1 different value. Those are exactly what atMost(1) counts. Its run on the same array: each new item differs from the one before, so the window always shrinks back to just the new item and adds 1 each time → 1 + 1 + 1 + 1 + 1 = 5: [1], [2], [1], [2], [3].
12 − 5 = 7 ✓, the 7 black ones: [1,2], [2,1], [1,2,1], [1,2], [2,1,2], [1,2,1,2], [2,3]. The same 7 as the table in Part A.
→ No, it's a slip of the tongue. The numbers (12 and 5) are for k = 2 on
[1,2,1,2,3]: "sizes 1 and 2" give 12, "size 1 only" gives 5. If you want a real k = 3 check, use LeetCode example 2 below.len(freq) == k?→ Because the shortcut
right − left + 1 assumes every shorter window ending at right is valid. That's true for "at most" but not for "exactly". At step 4 the window [1,2,1,2] has exactly 2 values, but its piece [2] has only 1. Adding 4 would count [2] wrongly, and adding just 1 would miss [1,2] and [2,1,2]. The subtraction avoids this whole problem.A second check: [1, 2, 1, 3, 4], k = 3 (LeetCode example 2)
| right (added) | atMost(3): window → added | atMost(2): window → added |
|---|---|---|
| 0 (1) | [1] → 1 | [1] → 1 |
| 1 (2) | [1,2] → 2 | [1,2] → 2 |
| 2 (1) | [1,2,1] → 3 | [1,2,1] → 3 |
| 3 (3) | [1,2,1,3] → 4 | 3 keys: drop 1, drop 2 (delete) → [1,3] → 2 |
| 4 (4) | 4 keys: drop 1, drop 2 (delete) → [1,3,4] → 3 | 3 keys: drop 1 (delete) → [3,4] → 2 |
| total | 13 | 10 |
13 − 10 = 3 ✓ ([1,2,1,3], [2,1,3], [1,3,4]).
5Approach steps
- Write the helper
atMost(nums, k)from Part B. (The teacher makes it a separate private function in Java.) - Call it twice: once with
k, once withk − 1. - Return the difference.
6Code (Python)
class Solution:
def subarraysWithKDistinct(self, nums, k):
if k <= 0: # constraints say k >= 1; safety guard
return 0
return self.atMost(nums, k) - self.atMost(nums, k - 1)
def atMost(self, nums, k):
left = 0
count = 0
freq = {}
for right in range(len(nums)):
freq[nums[right]] = freq.get(nums[right], 0) + 1 # expand
while len(freq) > k: # use k, not 2!
freq[nums[left]] -= 1 # shrink
if freq[nums[left]] == 0:
del freq[nums[left]]
left += 1
count += right - left + 1
return countk <= 0 guard? The teacher doesn't have it.→ With k = 0 the second call would be
atMost(-1). Then len(freq) > -1 is true even for an empty map, the loop runs past the window and reads a key that isn't there → crash. LeetCode never sends k = 0 (k ≥ 1), so her code is fine there. The guard just makes the function safe for any input (and "exactly 0 different" has answer 0).7Code line by line
| line | what it means |
|---|---|
| self.atMost(nums, k) | Buckets 1..k: subarrays with k or fewer different values. |
| - self.atMost(nums, k - 1) | Remove buckets 1..k−1. What's left is bucket k. |
| while len(freq) > k: | The teacher first typed > 2 (from her hand example) and fixed it to k, because the helper is called with different k values. |
| rest of atMost | Exactly Part B. |
8Dry run summary
subarraysWithKDistinct([1,2,1,2,3], 2)callsatMost(…, 2)→ 12 (Part B table).- Then
atMost(…, 1): right 0 → [1] +1; right 1 → [1,2] too many → drop 1 → [2] +1; right 2 → drop 2 → [1] +1; right 3 → drop 1 → [2] +1; right 4 → drop 2 → [3] +1 → 5. - 12 − 5 = 7 ✓.
9Complexity & remember
- Time O(n): one atMost call is about 2n steps (right and left each pass once). We call it twice → about 4n. The teacher says O(4n) to be precise; it's still linear. On LeetCode it ran among the fastest solutions.
- Space O(n): the map, worst case when all values are different (more exactly O(k)). The second call reuses nothing from the first, but they don't run at the same time, so the space doesn't add up.
Part D · Revision page
| Brute force | atMost(k) window | exactly(k) | |
|---|---|---|---|
| what it counts | exactly k | k or fewer | exactly k |
| loops | start i, end j | one for right + a while for left | two atMost calls |
| map | fresh for each i | one map, kept while sliding | one per call |
| stop / shrink | break when > k | while > k: remove left, delete at 0 | same |
| counting | += 1 when == k | += right − left + 1 | subtract |
| time / space | O(n²) / O(n) | O(n) / O(n) | O(n) (≈4n) / O(n) |
| [1,2,1,2,3] | value | which subarrays |
|---|---|---|
| atMost(2) | 12 | all 15 subarrays except the three that contain 1, 2 and 3 together |
| atMost(1) | 5 | the five single items |
| exactly(2) | 7 | 12 − 5 |
2.
len(freq) = number of different values. Delete a key when its count hits 0.3. Expand right;
while len(freq) > k shrink left.4. After shrinking,
count += right − left + 1 = valid subarrays ending at right.5. A window counts "at most". exactly(k) = atMost(k) − atMost(k − 1).
✗ forgetting to delete keys with count 0 (map size stays too big, loop never ends correctly)
✗ hard-coding the k of your example (
> 2) inside the helper✗
if instead of while for shrinking✗ trying to count "exactly k" directly with
right − left + 1✗ in the brute force, creating the map once outside the
i loops = Solution() print(s.subarraysWithKDistinct([1, 2, 1, 2, 3], 2)) # 7 print(s.subarraysWithKDistinct([1, 2, 1, 3, 4], 3)) # 3 print(s.atMost([1, 2, 1, 2, 3], 2)) # 12 print(s.atMost([1, 2, 1, 2, 3], 1)) # 5 print(s.subarraysWithKDistinct([5, 5, 5], 1)) # 6 (every subarray) print(s.subarraysWithKDistinct([1, 2, 3], 4)) # 0 (never 4 different)
Based on this video: Subarrays with K Different Integers | Sliding Window