3306. Count of Substrings Containing Every Vowel and K Consonants II
Problem Description
You are given a string word
and a non-negative integer k
.
Your task is to count the total number of substrings that satisfy both of the following conditions:
- The substring contains every vowel (
'a'
,'e'
,'i'
,'o'
, and'u'
) at least once - The substring contains exactly
k
consonants
A substring is a contiguous sequence of characters within the string. For this problem, any character that is not a vowel is considered a consonant.
For example, if word = "aeioubcd"
and k = 2
, one valid substring would be "aeioub"
because it contains all five vowels and exactly 2 consonants ('b'
and the implied count from the position).
The function should return the total count of all such valid substrings in the given string.
Intuition
The key insight is recognizing that finding substrings with exactly k
consonants is tricky to do directly. However, finding substrings with at least k
consonants is much easier using a sliding window approach.
Think about it this way: if we can count all substrings that have all vowels and at least k
consonants, and then subtract all substrings that have all vowels and at least k+1
consonants, we're left with exactly those substrings that have all vowels and exactly k
consonants.
This transforms our problem into: f(k) - f(k+1)
, where f(k)
counts substrings with all vowels and at least k
consonants.
Why is f(k)
easier to compute? With a sliding window, we can maintain a valid window (one that has all vowels and at least k
consonants) and for each valid position of the right pointer, count how many valid substrings end at that position.
The trick is: when our window [l, r]
is valid (has all 5 vowels and at least k
consonants), we shrink from the left until it becomes invalid. At this point, any substring ending at r
and starting anywhere from index 0
to l-1
would be valid. That gives us l
valid substrings for this particular right endpoint.
This counting method works because if [l, r]
is the minimal valid window ending at r
, then adding any characters to the left (indices 0
to l-1
) will still maintain validity - we'll still have all vowels and at least k
consonants.
Learn more about Sliding Window patterns.
Solution Approach
The solution implements the transformation f(k) - f(k+1)
using a helper function f(k)
that counts substrings with all vowels and at least k
consonants.
Implementation of f(k)
:
-
Data Structures:
cnt
: A Counter (hash table) to track the frequency of each vowel in the current windowx
: Counter for the number of consonants in the current windowl
: Left pointer of the sliding windowans
: Accumulator for the total count of valid substrings
-
Sliding Window Algorithm:
For each character
c
at positionr
(right pointer):- If
c
is a vowel, increment its count incnt
- If
c
is a consonant, incrementx
- If
-
Shrinking the Window:
When the window becomes valid (
x >= k
andlen(cnt) == 5
):- Shrink from the left while maintaining validity
- For each left character
d = word[l]
:- If
d
is a vowel: decrement its count and remove fromcnt
if count becomes 0 - If
d
is a consonant: decrementx
- Increment
l
- If
- Stop shrinking when the window becomes invalid
-
Counting Valid Substrings:
After shrinking,
l
represents the leftmost position where the window[l, r]
would be invalid. This means:- All substrings ending at
r
and starting at positions[0, 1, ..., l-1]
are valid - There are exactly
l
such substrings - Add
l
toans
- All substrings ending at
-
Final Answer:
Return
f(k) - f(k+1)
to get the count of substrings with exactlyk
consonants.
Why This Works:
The algorithm efficiently counts all substrings with at least k
consonants by recognizing that once we have a minimal valid window ending at position r
, any extension to the left maintains validity. By computing f(k) - f(k+1)
, we isolate substrings with exactly k
consonants, since substrings with k+1
or more consonants are counted in both f(k)
and f(k+1)
, and their difference cancels them out.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with word = "aeioubcd"
and k = 1
.
We need to find substrings with all 5 vowels and exactly 1 consonant. Using our approach, we'll calculate f(1) - f(2)
.
Computing f(1) - substrings with all vowels and at least 1 consonant:
Starting with l = 0
, we'll expand our right pointer r
:
-
r = 0
: 'a' (vowel),cnt = {'a': 1}
,x = 0
consonants- Not valid (need all 5 vowels and at least 1 consonant)
- No substrings counted
-
r = 1
: 'e' (vowel),cnt = {'a': 1, 'e': 1}
,x = 0
- Not valid
- No substrings counted
-
r = 2
: 'i' (vowel),cnt = {'a': 1, 'e': 1, 'i': 1}
,x = 0
- Not valid
- No substrings counted
-
r = 3
: 'o' (vowel),cnt = {'a': 1, 'e': 1, 'i': 1, 'o': 1}
,x = 0
- Not valid
- No substrings counted
-
r = 4
: 'u' (vowel),cnt = {'a': 1, 'e': 1, 'i': 1, 'o': 1, 'u': 1}
,x = 0
- Not valid (have all vowels but no consonants)
- No substrings counted
-
r = 5
: 'b' (consonant),cnt = all 5 vowels,
x = 1`- Valid! (all vowels and 1 consonant)
- Shrink from left while maintaining validity:
- Window [0,5] "aeioub" is valid
- Try removing 'a': would still have 1 consonant but lose a vowel → invalid
- Stop shrinking at
l = 0
- Count:
l = 0
, so 0 valid substrings ending at position 5
-
r = 6
: 'c' (consonant),cnt = all 5 vowels,
x = 2`- Valid! (all vowels and 2 consonants)
- Shrink from left:
- Window [0,6] "aeioubc" has 2 consonants
- Remove 'a': [1,6] "eioubc" still has all vowels and 2 consonants → still valid
- Remove 'e': [2,6] "ioubc" loses 'e' vowel → becomes invalid
- Stop at
l = 2
- Count: 2 valid substrings ending at position 6 ([0,6] and [1,6])
-
r = 7
: 'd' (consonant),cnt = all 5 vowels,
x = 3`- Valid! (all vowels and 3 consonants)
- Shrink from left:
- Keep removing while valid...
- Stop at
l = 3
(removing 'o' would make it invalid)
- Count: 3 valid substrings ending at position 7
Total for f(1) = 0 + 2 + 3 = 5
Computing f(2) - substrings with all vowels and at least 2 consonants:
Following similar logic:
- Positions 0-5: Not valid (only 0-1 consonants)
- Position 6: 'c' gives us 2 consonants, window becomes valid
- After shrinking,
l = 0
- Count: 0
- After shrinking,
- Position 7: 'd' gives us 3 consonants
- After shrinking,
l = 2
- Count: 2
- After shrinking,
Total for f(2) = 0 + 2 = 2
Final Answer:
f(1) - f(2) = 5 - 2 = 3
The 3 substrings with exactly 1 consonant are:
- "aeioub" (all vowels + 'b')
- "eioub" (all vowels + 'b')
- "ioub" (contains 'i','o','u' from original positions and would need checking for 'a','e')
This demonstrates how the sliding window efficiently counts valid substrings by maintaining the minimal valid window and counting all possible left extensions.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def countOfSubstrings(self, word: str, k: int) -> int:
5 """
6 Count substrings that contain all 5 vowels and exactly k consonants.
7 Uses the technique: exactly(k) = at_least(k) - at_least(k+1)
8 """
9
10 def count_substrings_with_at_least_k_consonants(min_consonants: int) -> int:
11 """
12 Count substrings that have all 5 vowels and at least min_consonants consonants.
13 Uses sliding window technique.
14 """
15 vowel_counter = Counter() # Track count of each vowel in current window
16 total_substrings = 0
17 left_pointer = 0
18 consonant_count = 0
19
20 # Expand window by moving right pointer
21 for char in word:
22 # Update window with new character
23 if char in "aeiou":
24 vowel_counter[char] += 1
25 else:
26 consonant_count += 1
27
28 # Shrink window from left while we have valid substring
29 # (all 5 vowels present and at least min_consonants consonants)
30 while consonant_count >= min_consonants and len(vowel_counter) == 5:
31 left_char = word[left_pointer]
32
33 # Remove leftmost character from window
34 if left_char in "aeiou":
35 vowel_counter[left_char] -= 1
36 if vowel_counter[left_char] == 0:
37 vowel_counter.pop(left_char)
38 else:
39 consonant_count -= 1
40
41 left_pointer += 1
42
43 # All substrings ending at current position with starting point
44 # from index 0 to left_pointer-1 are valid
45 total_substrings += left_pointer
46
47 return total_substrings
48
49 # Calculate exactly k consonants using inclusion-exclusion principle
50 return count_substrings_with_at_least_k_consonants(k) - count_substrings_with_at_least_k_consonants(k + 1)
51
1class Solution {
2 /**
3 * Counts the number of substrings that contain all vowels and exactly k consonants.
4 * Uses the sliding window technique with the formula: exactly(k) = atLeast(k) - atLeast(k+1)
5 *
6 * @param word the input string
7 * @param k the exact number of consonants required
8 * @return the count of valid substrings
9 */
10 public long countOfSubstrings(String word, int k) {
11 // Count substrings with at least k consonants minus those with at least k+1 consonants
12 // This gives us exactly k consonants
13 return countWithAtLeastKConsonants(word, k) - countWithAtLeastKConsonants(word, k + 1);
14 }
15
16 /**
17 * Helper method that counts substrings containing all 5 vowels and at least k consonants.
18 * Uses a sliding window approach where the left pointer moves to maintain the constraint.
19 *
20 * @param word the input string
21 * @param k minimum number of consonants required
22 * @return count of valid substrings ending at each position
23 */
24 private long countWithAtLeastKConsonants(String word, int k) {
25 long totalCount = 0;
26 int leftPointer = 0;
27 int consonantCount = 0;
28
29 // Map to track count of each vowel in current window
30 Map<Character, Integer> vowelFrequency = new HashMap<>(5);
31
32 // Iterate through each character as the right endpoint of substrings
33 for (char currentChar : word.toCharArray()) {
34 // Update window state based on current character
35 if (isVowel(currentChar)) {
36 // Add vowel to frequency map
37 vowelFrequency.merge(currentChar, 1, Integer::sum);
38 } else {
39 // Increment consonant count
40 consonantCount++;
41 }
42
43 // Shrink window from left while maintaining valid state
44 // Valid state: has all 5 vowels AND at least k consonants
45 while (consonantCount >= k && vowelFrequency.size() == 5) {
46 char leftChar = word.charAt(leftPointer);
47 leftPointer++;
48
49 // Update window state after removing left character
50 if (isVowel(leftChar)) {
51 // Decrement vowel count and remove if it becomes 0
52 if (vowelFrequency.merge(leftChar, -1, Integer::sum) == 0) {
53 vowelFrequency.remove(leftChar);
54 }
55 } else {
56 // Decrement consonant count
57 consonantCount--;
58 }
59 }
60
61 // All substrings from index 0 to leftPointer-1 ending at current position are valid
62 totalCount += leftPointer;
63 }
64
65 return totalCount;
66 }
67
68 /**
69 * Checks if a character is a vowel (a, e, i, o, u).
70 *
71 * @param c the character to check
72 * @return true if the character is a vowel, false otherwise
73 */
74 private boolean isVowel(char c) {
75 return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
76 }
77}
78
1class Solution {
2public:
3 long long countOfSubstrings(string word, int k) {
4 // Lambda function to count substrings with all 5 vowels and at least minConsonants consonants
5 auto countWithAtLeastKConsonants = [&](int minConsonants) -> long long {
6 long long totalCount = 0;
7 int leftPointer = 0;
8 int consonantCount = 0;
9 unordered_map<char, int> vowelFrequency;
10
11 // Helper lambda to check if a character is a vowel
12 auto isVowel = [&](char character) -> bool {
13 return character == 'a' || character == 'e' || character == 'i' ||
14 character == 'o' || character == 'u';
15 };
16
17 // Iterate through each character using the right pointer of sliding window
18 for (char currentChar : word) {
19 // Update counts based on whether current character is vowel or consonant
20 if (isVowel(currentChar)) {
21 vowelFrequency[currentChar]++;
22 } else {
23 consonantCount++;
24 }
25
26 // Shrink window from left while we have all conditions met:
27 // - At least minConsonants consonants
28 // - All 5 vowels present
29 while (consonantCount >= minConsonants && vowelFrequency.size() == 5) {
30 char leftChar = word[leftPointer];
31 leftPointer++;
32
33 // Update counts for the character being removed from window
34 if (isVowel(leftChar)) {
35 vowelFrequency[leftChar]--;
36 if (vowelFrequency[leftChar] == 0) {
37 vowelFrequency.erase(leftChar);
38 }
39 } else {
40 consonantCount--;
41 }
42 }
43
44 // Add the number of valid starting positions (0 to leftPointer-1)
45 // These form valid substrings ending at current position
46 totalCount += leftPointer;
47 }
48
49 return totalCount;
50 };
51
52 // Calculate substrings with exactly k consonants using inclusion-exclusion principle:
53 // (at least k consonants) - (at least k+1 consonants) = exactly k consonants
54 return countWithAtLeastKConsonants(k) - countWithAtLeastKConsonants(k + 1);
55 }
56};
57
1/**
2 * Counts the number of substrings that contain all vowels and exactly k consonants
3 * @param word - The input string to analyze
4 * @param k - The exact number of consonants required
5 * @returns The count of valid substrings
6 */
7function countOfSubstrings(word: string, k: number): number {
8 /**
9 * Helper function that counts substrings with all vowels and at least k consonants
10 * @param minConsonants - Minimum number of consonants required
11 * @returns Count of valid substrings
12 */
13 const countSubstringsWithAtLeastKConsonants = (minConsonants: number): number => {
14 let validSubstringCount = 0;
15 let leftPointer = 0;
16 let consonantCount = 0;
17 const vowelFrequencyMap = new Map<string, number>();
18
19 /**
20 * Checks if a character is a vowel
21 * @param character - The character to check
22 * @returns True if the character is a vowel, false otherwise
23 */
24 const isVowel = (character: string): boolean => {
25 return character === 'a' || character === 'e' || character === 'i' ||
26 character === 'o' || character === 'u';
27 };
28
29 // Iterate through each character using sliding window technique
30 for (const currentChar of word) {
31 // Update counts based on current character
32 if (isVowel(currentChar)) {
33 vowelFrequencyMap.set(currentChar, (vowelFrequencyMap.get(currentChar) || 0) + 1);
34 } else {
35 consonantCount++;
36 }
37
38 // Shrink window from left while conditions are met
39 // (at least k consonants AND all 5 vowels present)
40 while (consonantCount >= minConsonants && vowelFrequencyMap.size === 5) {
41 const leftChar = word[leftPointer++];
42
43 if (isVowel(leftChar)) {
44 // Decrement vowel count and remove if it reaches zero
45 vowelFrequencyMap.set(leftChar, vowelFrequencyMap.get(leftChar)! - 1);
46 if (vowelFrequencyMap.get(leftChar) === 0) {
47 vowelFrequencyMap.delete(leftChar);
48 }
49 } else {
50 consonantCount--;
51 }
52 }
53
54 // Add the number of valid starting positions for current ending position
55 validSubstringCount += leftPointer;
56 }
57
58 return validSubstringCount;
59 };
60
61 // Calculate substrings with exactly k consonants using inclusion-exclusion principle
62 // (at least k consonants) - (at least k+1 consonants) = exactly k consonants
63 return countSubstringsWithAtLeastKConsonants(k) - countSubstringsWithAtLeastKConsonants(k + 1);
64}
65
Time and Space Complexity
The time complexity is O(n)
, where n
is the length of the string word
. The function f(k)
is called twice - once with parameter k
and once with parameter k+1
. Each call to f(k)
involves a single pass through the string using a sliding window approach. In the sliding window, each character is visited at most twice - once when expanding the window (adding to the right) and once when shrinking the window (removing from the left). Therefore, despite the nested while loop, the amortized time complexity for each call to f(k)
is O(n)
, making the overall time complexity O(n) + O(n) = O(n)
.
The space complexity is O(1)
. The algorithm uses a Counter object cnt
to store vowel frequencies, but since there are only 5 vowels (a
, e
, i
, o
, u
), the Counter will store at most 5 key-value pairs regardless of the input size. The variable x
tracks consonant count, and variables ans
and l
are simple integers. All these auxiliary space requirements are constant and independent of the input size n
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Window Shrinking Logic
The Pitfall: A common mistake is shrinking the window too aggressively or not understanding when to stop. Developers often write:
# WRONG: Continue shrinking while window is valid
while consonant_count >= min_consonants and len(vowel_counter) == 5 and left_pointer < len(word):
# shrink window
left_pointer += 1
This keeps the window valid but doesn't help count all valid substrings correctly.
Why It's Wrong:
The algorithm needs to shrink until the window becomes invalid, not maintain validity. When the window [l, r]
becomes invalid, it means all substrings from [0, r]
to [l-1, r]
are valid.
The Fix: Shrink while the window IS valid, and stop when it becomes invalid:
# CORRECT: Shrink while valid, stop when invalid
while consonant_count >= min_consonants and len(vowel_counter) == 5:
# shrink from left
# This will naturally stop when window becomes invalid
2. Off-by-One Error in Counting
The Pitfall:
After shrinking, developers might incorrectly count valid substrings as left_pointer + 1
or left_pointer - 1
.
Why It's Wrong:
- If
left_pointer = 3
after shrinking, it means the window[3, r]
is invalid - But windows
[0, r]
,[1, r]
,[2, r]
are all valid - That's exactly 3 substrings, not 2 or 4
The Fix:
Always add exactly left_pointer
to the count:
total_substrings += left_pointer # Correct count
3. Forgetting to Handle Empty Counter
The Pitfall: When decrementing vowel counts, forgetting to remove vowels with zero count:
# WRONG: Just decrement without cleanup if left_char in "aeiou": vowel_counter[left_char] -= 1
Why It's Wrong:
This leaves vowels with count 0 in the counter, making len(vowel_counter)
always 5 after seeing all vowels once, breaking the validity check.
The Fix: Remove vowels from counter when their count reaches zero:
# CORRECT: Clean up zero counts if left_char in "aeiou": vowel_counter[left_char] -= 1 if vowel_counter[left_char] == 0: vowel_counter.pop(left_char)
4. Misunderstanding the Mathematical Transformation
The Pitfall:
Implementing f(k+1) - f(k)
instead of f(k) - f(k+1)
.
Why It's Wrong:
f(k)
counts substrings with ≥k consonantsf(k+1)
counts substrings with ≥k+1 consonantsf(k) - f(k+1)
gives exactly k consonantsf(k+1) - f(k)
would give a negative number!
The Fix: Always use the correct order:
return count_at_least(k) - count_at_least(k + 1) # Correct
Which data structure is used to implement recursion?
Recommended Readings
https assets algo monster cover_photos stack svg Sliding Window Maximum Monotonic Stack We have an array and a sliding window defined by a start index and an end index The sliding window moves from left of the array to right There are always k elements in the window The window
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Recursion Recursion is one of the most important concepts in computer science Simply speaking recursion is the process of a function calling itself Using a real life analogy imagine a scenario where you invite your friends to lunch https assets algo monster recursion jpg You first call Ben and ask
Want a Structured Path to Master System Design Too? Don’t Miss This!