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 · 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]

Fixed vs variable windows

Fixed-size windowVariable-size window (this page)
sizealways k (the problem gives k)grows and shrinks
how it movesadd the new right item, drop the item that falls off the leftexpand right every step; while the window is invalid, shrink from the left
examplemax of every window of size kwindows with at most K different numbers

Three kinds of questions a variable window answers

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.

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:

patternuse it when…
Two pointersyou only care about the two ends (two items), not what's between them
Sliding windowyou need everything between a start and an end (a whole continuous piece)
Prefix sumyou need sums of many ranges, asked again and again (many queries)
Kadane's algorithmyou 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.

index01234
nums12123k = 2 → answer 7

The teacher reads example 1 by size. Starting at index 0:

Doing this from every start gives all 7 good subarrays:

#subarrayindexesdifferent 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

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:

4Building the logic, the way the teacher does

Doubt 1: why a map and not a set? A set already holds "the different values".
→ 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.
Doubt 2: does 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.
Doubt 3: where do I create the map?
→ 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.
Doubt 4: when the map size goes above k, why 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

  1. count = 0.
  2. For each start i: make an empty map.
  3. For each end j from i: add nums[j] to the map.
  4. If the map has exactly k keys → count += 1. Else if it has more than k → break.
  5. Return count.

6Code (Python)

Brute force, O(n²)
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 count

7Code line by line

linewhat 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) + 1Add the new item. If it's new, get gives 0, so it becomes 1.
if len(freq) == k: count += 1The number of keys = the number of different values. Exactly k → good subarray.
elif len(freq) > k: breakToo many different values. Every longer subarray from this start is bad too.

8Dry run for i = 0 (the teacher's walk-through)

jaddedsubarraymap after addingkeysactioncount
01[1]{1:1}1fewer than 2, keep going0
12[1,2]{1:1, 2:1}2count it1
21[1,2,1]{1:2, 2:1}2count it (1 already a key, only its count grows)2
32[1,2,1,2]{1:2, 2:2}2count it3
43[1,2,1,2,3]{1:2, 2:2, 3:1}33 > 2 → break3

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 ✓.

i=1 again12123re-reads 2, 1, 2, which i = 0 already read
i j

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

Remember the brute forceFresh map per start · add 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:

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:

start 312134[3]
start 212134[1,3]
start 112134[2,1,3]
start 012134[1,2,1,3] → 4 subarrays = length 4

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.

Doubt 1: why 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.
Doubt 2: after 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].
Doubt 3: where does the count line go, inside or after the shrink loop?
→ 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

  1. left = 0, count = 0, empty map.
  2. For right from 0 to n − 1: add nums[right] to the map (expand).
  3. While the map has more than k keys: decrease nums[left]'s count, delete it if it's 0, left += 1 (shrink).
  4. count += right − left + 1.
  5. Return count.

6Code (Python)

Count subarrays with at most k different values, O(n)
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 count

7Code line by line

linewhat 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) + 1Expand: 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]] -= 1The 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 += 1The window now starts one step later.
count += right - left + 1Number of valid subarrays that end at right.

8Dry run: atMost(2) on [1, 2, 1, 2, 3]

stepright (added)window beforemap after addingshrink? (what leaves, why)window aftercount
10 (1)[1]{1:1}no, 1 key ≤ 2[1] (0..0)0 + 1 = 1
21 (2)[1,2]{1:1, 2:1}no, 2 keys[1,2] (0..1)1 + 2 = 3
32 (1)[1,2,1]{1:2, 2:1}no, still 2 keys[1,2,1] (0..2)3 + 3 = 6
43 (2)[1,2,1,2]{1:2, 2:2}no[1,2,1,2] (0..3)6 + 4 = 10
54 (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
step 412123window 0..3, 4 subarrays end here
L R
step 5 in121233 different → must shrink
L R
step 5 out12123grey = left behind for good
LR

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

Remember atMost(k)Add right · while too many keys: remove left (delete at 0) and move left · 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…

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 atwindowsubarrays added (shortest first)added
00..0[1]1
10..1[2], [1,2]2
20..2[1], [2,1], [1,2,1]3
30..3[2], [1,2], [2,1,2], [1,2,1,2]4
43..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.

Doubt 1: in the video she says "let's say k is three" while explaining this. Is the example different?
→ 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.
Doubt 2: why can't the window count "exactly k" by itself, e.g. by counting only when 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 → addedatMost(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] → 43 keys: drop 1, drop 2 (delete) → [1,3] → 2
4 (4)4 keys: drop 1, drop 2 (delete) → [1,3,4] → 33 keys: drop 1 (delete) → [3,4] → 2
total1310

13 − 10 = 3 ✓ ([1,2,1,3], [2,1,3], [1,3,4]).

5Approach steps

  1. Write the helper atMost(nums, k) from Part B. (The teacher makes it a separate private function in Java.)
  2. Call it twice: once with k, once with k − 1.
  3. Return the difference.

6Code (Python)

Exactly k different, O(n): the final answer
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 count
Doubt 3: why the k <= 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

linewhat 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 atMostExactly Part B.

8Dry run summary

  1. subarraysWithKDistinct([1,2,1,2,3], 2) calls atMost(…, 2) → 12 (Part B table).
  2. 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.
  3. 12 − 5 = 7 ✓.
atMost(1), r=312123window is always a single item here
L R

9Complexity & remember

Remember exactly(K)A window can only count "at most". So: exactly(K) = atMost(K) − atMost(K − 1). Two calls of the same helper, subtract.

Part D · Revision page

Brute forceatMost(k) windowexactly(k)
what it countsexactly kk or fewerexactly k
loopsstart i, end jone for right + a while for lefttwo atMost calls
mapfresh for each ione map, kept while slidingone per call
stop / shrinkbreak when > kwhile > k: remove left, delete at 0same
counting+= 1 when == k+= right − left + 1subtract
time / spaceO(n²) / O(n)O(n) / O(n)O(n) (≈4n) / O(n)
[1,2,1,2,3]valuewhich subarrays
atMost(2)12all 15 subarrays except the three that contain 1, 2 and 3 together
atMost(1)5the five single items
exactly(2)712 − 5
If you remember only 5 lines 1. Use a frequency map, not a set: removing one copy must not erase a value that still has copies.
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).
Mistakes to avoid ✗ using a set (removal breaks when a value appears twice)
✗ 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 loop
test it yourself (paste under the final solution)
s = 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