THE WHY BEHIND EVERY DSA PROBLEM
SHEET: STRIVER A2Z · REV 0.1
UNDERSTOOD: 0 / 79 DRAFTED
STEP 10 · SLIDING WINDOW · LAST-SEEN BOUNDARY · MEDIUM

Number of Substrings Containing All Three Characters

WHAT IT SAYS

Given a string of only a, b and c, count the substrings that contain at least one of each.

WHAT IT'S REALLY ASKING

"This is an 'at least' condition, so the usual instinct is upside down: adding characters can never destroy a substring's validity, only create it. Fix a right end — the valid starts can't be scattered, they have to be a solid block reaching back to index 0. So the only question left is where that block stops, and which of the three characters is the one stopping it."

THE INSIGHT LADDER — FROM BRUTE FORCE TO OPTIMAL

Every substring, check for all three

O(n²) time, O(1) space

Fix a start, extend right, keep three counters, and tick the answer whenever all three are non-zero. Restart from the next index. Correct, obviously — every substring gets inspected.

WHERE THE WORK IS WASTED — You keep re-testing a question whose answer can never flip back. Once s[i..r] contains all three, so does s[i..r+1] and every longer end — yet the inner loop re-checks the three counters at every single one of them, when the honest work ended the moment the third character first appeared. And the outer restart is worse: the scan from i + 1 re-tallies the very characters the scan from i just tallied, only to find the same third character at (usually) the same place.

!
KEY OBSERVATION — THE UNLOCKlink

The scarcest character draws the line; behind it is free.

Presence is upward-closed: a substring containing all three keeps containing all three when you glue more characters onto either end. So for a fixed right end r, the valid starts can't be a scattered set — if start i works, every start to the left of i works too. They form a solid block running from 0 up to some cutoff m, and the entire problem for this r collapses to finding m. m is forced, and here's the argument. Let m = min(last[a], last[b], last[c]), the most recent occurrence of whichever character you've seen least recently. If your start is at or before m, the window contains all three of those last occurrences, so it contains all three characters — valid. If your start is even one step past m, then the character that achieved that minimum has no occurrence at all in [start..r], because its most recent one was at m and it hasn't appeared since — invalid. So m is exactly the cutoff, not an estimate of it, and the number of substrings ending at r is precisely m + 1. Notice which character sets the boundary: not the frequent one, but the one you've gone longest without. It's the bottleneck, and it changes hands as you walk.

Three integers, one pass

O(n) time, O(1) space

Keep the last index at which you saw each of a, b and c, all starting at −1. At each right end, update the one you just read, then add min(last[a], last[b], last[c]) + 1 to the answer — which is zero, harmlessly, until you've seen all three at least once. No window, no counters, no shrinking.

The same idea wearing a window

O(n) time, O(1) space

If you'd rather see it as two pointers: for each right end, shrink the left edge inward as long as the window still contains all three, then add the left edge's index to the answer — because every start before it is valid too. That left edge lands on exactly the m above, so it's the identical count with more moving parts; the last-seen version just reads m straight off instead of walking to it.

WHAT YOU TRADED — Nothing is traded away here — you buy the speedup purely by noticing which direction the constraint is closed in, and that costs nothing but attention. The lesson generalises: for an 'at least' condition, valid starts form a prefix and you count them by finding the boundary; for an 'at most' condition, valid starts form a suffix and you count them by shrinking a window. Getting that orientation right is most of the battle, and reaching for a sliding window on an 'at least' problem is how people end up shrinking in the wrong direction.
WATCH THE IDEA RUN
a
0
a
1
b
2
c
3
b
4
a
5
c
6
substrings so far 0
bottleneck '-' -1
free starts here 0
Track only one thing per character: where you last saw it. The boundary for any right end is the oldest of those three positions.
step 1 / 8
THE PATTERN — SO YOU RECOGNIZE IT NEXT TIME

Bottleneck Boundary

YOU'LL SEE IT AGAIN WHEN

  • The condition is 'contains at least one of each' (or 'covers all of X'), so adding characters can only help — validity is never destroyed by growing.
  • For a fixed right end, the valid starts run in an unbroken block back to the start of the array, which means you're counting a boundary index, not enumerating candidates.
  • Whether a window qualifies depends on the element you've gone longest without, not on any total or tally.
  • The alphabet or required set is small and fixed, so 'when did I last see each required thing' is O(1) to maintain.

SAME BLUEPRINT, DIFFERENT PROBLEM

Count Subarrays Where Max Element Appears at Least K TimesSubarrays with K Different IntegersMinimum Window SubstringCount Complete Subarrays in an Array
The bar isn't "solved it once." It's "could rebuild it from the observation."