Count substrings holding a, b and c
Given a string s made only of the letters a, b and
c, return the number of substrings that contain at least one of each.
Examples
| Input | Output | Why |
|---|---|---|
s = "abcabc" | 10 | Ending at index 2: 1 substring; at 3: 2; at 4: 3; at 5: 4. |
s = "aaacb" | 3 | "aaacb", "aacb", "acb". |
s = "abab" | 0 | No c anywhere. |
Constraints
- 1 ≤ |s| ≤ 5 × 10⁴
- s consists of a, b and c only
Hints
Hint 1
If a substring contains all three letters, so does every substring that contains it.
Hint 2
For a right end hi, the valid starts are 0, 1, …, up to some last start. Find that last start.
Hint 3
Shrink while the window still holds all three. When it stops, lo is the number of valid starts for this hi.
Where people lose marks
- Adding hi − lo + 1 out of habit: that counts the invalid starts here.
- Adding inside the shrink loop instead of once per right end, which counts the same substrings several times.
Dry run on "abcabc"
Read it one frame at a time, the way you would trace the code on paper in an interview. Each frame shows the state after the step it describes.
-
Step 1 / 6sa0lo/hib1c2a3b4c5a1b0c0count0
'a' enters. No start works yet. Total 0.
-
Step 2 / 6sa0lob1hic2a3b4c5a1b1c0count0
'b' enters. No start works yet. Total 0.
-
Step 3 / 6sa0b1loc2hia3b4c5a0b1c1count1
'c' enters. The window holds all three, so shrink while it still does: lo moves from 0 to 1. Starts 0 to 0 all work for this end: add 1. Total 1.
-
Step 4 / 6sa0b1c2loa3hib4c5a1b0c1count3
'a' enters. The window holds all three, so shrink while it still does: lo moves from 1 to 2. Starts 0 to 1 all work for this end: add 2. Total 3.
-
Step 5 / 6sa0b1c2a3lob4hic5a1b1c0count6
'b' enters. The window holds all three, so shrink while it still does: lo moves from 2 to 3. Starts 0 to 2 all work for this end: add 3. Total 6.
-
Step 6 / 6sa0b1c2a3b4loc5hia0b1c1count10
'c' enters. The window holds all three, so shrink while it still does: lo moves from 3 to 4. Starts 0 to 3 all work for this end: add 4. Total 10.
How to think about it
This property survives growing rather than shrinking, so the counting is reversed. For each right end
hi, the valid starts are a prefix 0, 1, …, p. To find p, shrink the window from the left while it
still holds all three letters. When the loop stops, lo has just moved past the last valid start,
so exactly lo starts work: add lo.
Compare it with counting products below k, where validity survived shrinking and every start from
lo to hi worked. Which way validity goes decides whether you add
hi − lo + 1 or lo, and getting that right is most of the problem.
Complexity. Time O(n) · Space O(1)
The solution, in six languages
Each one is compiled and run against the test table before it is published. Java is reviewed by hand.
def count_all_three(s):
count = {"a": 0, "b": 0, "c": 0}
total = lo = 0
for ch in s:
count[ch] += 1
while count["a"] and count["b"] and count["c"]: # still valid: shrink
count[s[lo]] -= 1
lo += 1
total += lo # starts 0 .. lo - 1 all work for this end
return total#include <string>
long long solve(const std::string& s) {
int count[3] = {0, 0, 0}, lo = 0;
long long total = 0;
for (char ch : s) {
++count[ch - 'a'];
while (count[0] && count[1] && count[2]) --count[s[lo++] - 'a']; // still valid: shrink
total += lo; // starts 0 .. lo - 1 all work for this end
}
return total;
}class Solution {
public long countAllThree(String s) {
int[] count = new int[3];
int lo = 0;
long total = 0;
for (int hi = 0; hi < s.length(); hi++) {
count[s.charAt(hi) - 'a']++;
while (count[0] > 0 && count[1] > 0 && count[2] > 0) count[s.charAt(lo++) - 'a']--;
total += lo; // starts 0 .. lo - 1 all work for this end
}
return total;
}
}function countAllThree(s) {
const count = { a: 0, b: 0, c: 0 };
let total = 0, lo = 0;
for (const ch of s) {
count[ch]++;
while (count.a && count.b && count.c) count[s[lo++]]--; // still valid: shrink
total += lo; // starts 0 .. lo - 1 all work for this end
}
return total;
}fn solve(s: &str) -> i64 {
let b = s.as_bytes();
let mut count = [0usize; 3];
let (mut lo, mut total) = (0usize, 0i64);
for &ch in b {
count[(ch - b'a') as usize] += 1;
while count.iter().all(|&c| c > 0) { // still valid: shrink
count[(b[lo] - b'a') as usize] -= 1;
lo += 1;
}
total += lo as i64; // starts 0 .. lo - 1 all work for this end
}
total
}import qualified Data.Map.Strict as M
-- Fold carrying (counts, letters from the window's left end, lo, total).
solve :: String -> Int
solve s = total
where
(_, _, _, total) = foldl step (M.fromList [(c, 0) | c <- "abc"], s, 0, 0) s
step (m, win, lo, t) ch =
let (m', win', lo') = shrink (M.adjust (+ 1) ch m) win lo
in (m', win', lo', t + lo')
shrink m win lo
| all (> 0) (M.elems m), (x:xs) <- win = shrink (M.adjust (subtract 1) x m) xs (lo + 1)
| otherwise = (m, win, lo)
Variants to try
- At least k distinct letters from a larger alphabet: the same code with a counter of kinds.
- Exactly all three and nothing else: with a three-letter alphabet it is the same question.
Next problems
Count runs whose product is below k
Counting windows rather than measuring them: once the window ending at hi is the longest valid one, all of its suffixes are valid too.
Open the problem → Hard30 minCount runs with exactly k distinct values
"Exactly k" is not monotone, so no single window counts it. Two windows that count "at most" do, and their difference is the answer.
Open the problem → Easy10 minBest total over k consecutive days
A window of fixed width slides one place at a time: add the element that enters, subtract the one that leaves.
Open the problem →Write a solution and run it against the real test table.
Keep going
Count runs whose product is below k
Counting windows rather than measuring them: once the window ending at hi is the longest valid one, all of its suffixes are valid too.
Open the problem → Hard30 minCount runs with exactly k distinct values
"Exactly k" is not monotone, so no single window counts it. Two windows that count "at most" do, and their difference is the answer.
Open the problem → Easy10 minBest total over k consecutive days
A window of fixed width slides one place at a time: add the element that enters, subtract the one that leaves.
Open the problem → Sliding windowThe pattern behind it
A window that grows on the right, shrinks on the left, and holds an invariant at all times.
Read the pattern →Preparing for a real process? Quant interview preparation, or book a free 20-minute call.