3305. Count of Substrings Containing Every Vowel and K Consonants I
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. Any character that is not a vowel is considered a consonant.
For example, if word = "aeioubcd"
and k = 2
, one valid substring would be "aeioubcd"
because it contains all five vowels at least once and has exactly 2 consonants ('b'
and 'c'
).
The function should return the total count of all such valid substrings in the given word.
Intuition
The key insight is 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 the count of substrings that have all vowels and at least k+1
consonants, we get exactly what we want - substrings with all vowels and exactly k
consonants.
This is because:
- Substrings with at least
k
consonants = Substrings with exactlyk
consonants + Substrings with exactlyk+1
consonants + Substrings with exactlyk+2
consonants + ... - Substrings with at least
k+1
consonants = Substrings with exactlyk+1
consonants + Substrings with exactlyk+2
consonants + ...
Therefore: f(k) - f(k+1)
gives us substrings with exactly k
consonants.
For the sliding window part, we maintain a window that always satisfies our conditions (all vowels present and at least k
consonants). As we expand the window to the right, whenever we have a valid window, we need to count how many valid substrings end at the current position. This count equals the number of possible starting positions, which is the current left boundary position l
. This is because any substring starting from positions 0
to l-1
and ending at the current position will contain our valid window and thus be valid.
Learn more about Sliding Window patterns.
Solution Approach
The solution implements the transformation strategy using a helper function f(k)
that counts substrings with all vowels and at least k
consonants.
Implementation of f(k)
using Sliding Window:
-
Data Structures:
cnt
: A Counter (hash table) to track occurrences 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
-
Expanding the Window (right pointer):
- Iterate through each character
c
inword
- If
c
is a vowel, add it tocnt
- If
c
is a consonant, incrementx
- Iterate through each character
-
Contracting the Window (left pointer):
- When we have a valid window (
x >= k
andlen(cnt) == 5
), we need to find the leftmost position where the window remains valid - Move the left pointer
l
to the right while the window is valid:- If
word[l]
is a vowel, decrement its count incnt
- If count becomes 0, remove it from
cnt
- If count becomes 0, remove it from
- If
word[l]
is a consonant, decrementx
- Increment
l
- If
- The loop stops when either
x < k
or we don't have all 5 vowels
- When we have a valid window (
-
Counting Valid Substrings:
- After adjusting the left boundary, all substrings ending at the current position and starting from any position in
[0, l-1]
are valid - Add
l
toans
(there are exactlyl
such valid substrings)
- After adjusting the left boundary, all substrings ending at the current position and starting from any position in
-
Final Answer:
- Return
f(k) - f(k + 1)
to get substrings with exactlyk
consonants
- Return
Why This Works:
- The sliding window maintains the smallest valid window ending at each position
- By keeping track of when we have all vowels (
len(cnt) == 5
) and enough consonants (x >= k
), we ensure our counting condition - The subtraction
f(k) - f(k + 1)
transforms the "at least" problem into an "exactly" problem
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 = "aeioubc"
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) (at least 1 consonant):
Starting with left pointer l = 0
, we expand our window:
-
i = 0, c = 'a': vowel, add to cnt = {'a': 1}, x = 0 consonants
- Not valid (need x ≥ 1 and all vowels)
-
i = 1, c = 'e': vowel, cnt = {'a': 1, 'e': 1}, x = 0
- Not valid
-
i = 2, c = 'i': vowel, cnt = {'a': 1, 'e': 1, 'i': 1}, x = 0
- Not valid
-
i = 3, c = 'o': vowel, cnt = {'a': 1, 'e': 1, 'i': 1, 'o': 1}, x = 0
- Not valid
-
i = 4, c = 'u': vowel, cnt = {'a': 1, 'e': 1, 'i': 1, 'o': 1, 'u': 1}, x = 0
- Have all vowels but x < 1, not valid
-
i = 5, c = 'b': consonant, x = 1
- Now valid! (all 5 vowels and x ≥ 1)
- Contract window: Currently l = 0, window is "aeiouб"
- Try removing word[0] = 'a': Would lose a vowel, making len(cnt) = 4 < 5
- Stop contracting, l stays at 0
- Add l = 0 to answer (0 valid substrings ending at position 5)
-
i = 6, c = 'c': consonant, x = 2
- Valid! (all 5 vowels and x ≥ 1)
- Contract window: Currently l = 0, window is "aeioubc"
- Try removing word[0] = 'a': Would lose a vowel
- Stop contracting, l stays at 0
- Add l = 0 to answer
Total for f(1) = 0 + 0 = 0
Computing f(2) (at least 2 consonants):
Reset and expand again:
- i = 0-4: Same as before, accumulating vowels, x = 0
- i = 5, c = 'b': x = 1, not valid (need x ≥ 2)
- i = 6, c = 'c': x = 2
- Now valid! (all 5 vowels and x ≥ 2)
- Contract window: Currently l = 0
- Try removing word[0] = 'a': Would lose a vowel
- Stop contracting, l stays at 0
- Add l = 0 to answer
Total for f(2) = 0
Final Answer: f(1) - f(2) = 0 - 0 = 0
This makes sense! The only substring with all vowels and exactly 1 consonant would be something like "aeiouб", but that's not a valid substring since we'd need to skip the 'c'. The full string "aeioubc" has 2 consonants, not 1.
Let's verify with a different example where we expect a non-zero result. Consider word = "aaeioubc"
and k = 2
:
For f(2), when we reach the end (i = 7, after 'c'), we have all vowels and exactly 2 consonants. The window can start from position 0 or 1 (both 'a's can be removed while keeping all vowels), so l would be 2 after contraction, giving us 2 valid substrings: "aaeioubc" and "aeioubc".
For f(3), we never get 3 consonants, so f(3) = 0.
Result: f(2) - f(3) = 2 - 0 = 2, which correctly counts substrings with exactly 2 consonants.
Solution Implementation
1from collections import Counter
2from typing import List
3
4class Solution:
5 def countOfSubstrings(self, word: str, k: int) -> int:
6 """
7 Count the number of substrings that contain all 5 vowels (a, e, i, o, u)
8 and exactly k consonants.
9
10 Uses the technique: exactly(k) = at_least(k) - at_least(k+1)
11 """
12
13 def count_substrings_with_at_least_k_consonants(min_consonants: int) -> int:
14 """
15 Count substrings that have all 5 vowels and at least min_consonants consonants.
16 Uses a sliding window approach.
17 """
18 # Track count of each vowel in current window
19 vowel_counter = Counter()
20
21 # Initialize variables
22 total_substrings = 0
23 left_pointer = 0
24 consonant_count = 0
25
26 # Expand window by moving right pointer
27 for char in word:
28 if char in "aeiou":
29 # Add vowel to counter
30 vowel_counter[char] += 1
31 else:
32 # Increment consonant count
33 consonant_count += 1
34
35 # Shrink window from left while we have valid substring
36 # (at least min_consonants consonants AND all 5 vowels)
37 while consonant_count >= min_consonants and len(vowel_counter) == 5:
38 left_char = word[left_pointer]
39
40 if left_char in "aeiou":
41 # Remove vowel from counter
42 vowel_counter[left_char] -= 1
43 if vowel_counter[left_char] == 0:
44 vowel_counter.pop(left_char)
45 else:
46 # Decrement consonant count
47 consonant_count -= 1
48
49 left_pointer += 1
50
51 # Add count of valid substrings ending at current position
52 # All substrings from index 0 to left_pointer-1 can be starting points
53 total_substrings += left_pointer
54
55 return total_substrings
56
57 # Calculate exactly k consonants using inclusion-exclusion principle
58 return count_substrings_with_at_least_k_consonants(k) - count_substrings_with_at_least_k_consonants(k + 1)
59
1class Solution {
2 /**
3 * Counts substrings containing all 5 vowels and exactly k consonants
4 * Uses the principle: exactly k = (at least k) - (at least k+1)
5 */
6 public int countOfSubstrings(String word, int k) {
7 return countWithAtLeastKConsonants(word, k) - countWithAtLeastKConsonants(word, k + 1);
8 }
9
10 /**
11 * Counts substrings with all 5 vowels and at least k consonants
12 * Uses sliding window technique
13 */
14 private int countWithAtLeastKConsonants(String word, int minConsonants) {
15 int totalCount = 0;
16 int leftPointer = 0;
17 int consonantCount = 0;
18 // Map to track count of each vowel (a, e, i, o, u)
19 Map<Character, Integer> vowelCounts = new HashMap<>(5);
20
21 // Expand window with right pointer
22 for (char currentChar : word.toCharArray()) {
23 // Add current character to window
24 if (isVowel(currentChar)) {
25 vowelCounts.merge(currentChar, 1, Integer::sum);
26 } else {
27 consonantCount++;
28 }
29
30 // Shrink window from left while we have valid substrings
31 // Valid means: at least minConsonants consonants AND all 5 vowels
32 while (consonantCount >= minConsonants && vowelCounts.size() == 5) {
33 char leftChar = word.charAt(leftPointer);
34 leftPointer++;
35
36 // Remove leftmost character from window
37 if (isVowel(leftChar)) {
38 // Decrease vowel count and remove if it becomes 0
39 if (vowelCounts.merge(leftChar, -1, Integer::sum) == 0) {
40 vowelCounts.remove(leftChar);
41 }
42 } else {
43 consonantCount--;
44 }
45 }
46
47 // All substrings ending at current position with start from 0 to leftPointer-1
48 // are valid (contain all vowels and at least minConsonants consonants)
49 totalCount += leftPointer;
50 }
51
52 return totalCount;
53 }
54
55 /**
56 * Checks if a character is a vowel (a, e, i, o, u)
57 */
58 private boolean isVowel(char c) {
59 return c == 'a' || c == 'e' || c == 'i' || c == 'o' || c == 'u';
60 }
61}
62
1class Solution {
2public:
3 int countOfSubstrings(string word, int k) {
4 // Lambda function to count substrings with all 5 vowels and at least minConsonants consonants
5 auto countSubstringsWithAtLeastKConsonants = [&](int minConsonants) -> int {
6 int 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
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 5 vowels and enough consonants
27 while (consonantCount >= minConsonants && vowelFrequency.size() == 5) {
28 char leftChar = word[leftPointer];
29 leftPointer++;
30
31 if (isVowel(leftChar)) {
32 vowelFrequency[leftChar]--;
33 if (vowelFrequency[leftChar] == 0) {
34 vowelFrequency.erase(leftChar);
35 }
36 } else {
37 consonantCount--;
38 }
39 }
40
41 // Add the number of valid substrings ending at current position
42 totalCount += leftPointer;
43 }
44
45 return totalCount;
46 };
47
48 // Calculate substrings with exactly k consonants using the difference:
49 // (substrings with at least k consonants) - (substrings with at least k+1 consonants)
50 return countSubstringsWithAtLeastKConsonants(k) - countSubstringsWithAtLeastKConsonants(k + 1);
51 }
52};
53
1/**
2 * Counts the number of substrings that contain all 5 vowels and exactly k consonants
3 * @param word - The input string to search for substrings
4 * @param k - The exact number of consonants required in each substring
5 * @returns The count of valid substrings
6 */
7function countOfSubstrings(word: string, k: number): number {
8 /**
9 * Helper function to count substrings with all 5 vowels and at least minConsonants consonants
10 * @param minConsonants - Minimum number of consonants required
11 * @returns Count of substrings meeting the criteria
12 */
13 const countSubstringsWithAtLeastKConsonants = (minConsonants: number): number => {
14 let substringCount = 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 char - Character to check
22 * @returns True if the character is a vowel, false otherwise
23 */
24 const isVowel = (char: string): boolean => {
25 return char === 'a' || char === 'e' || char === 'i' || char === 'o' || char === 'u';
26 };
27
28 // Iterate through each character using the right pointer of the sliding window
29 for (const currentChar of word) {
30 // Expand window by adding current character
31 if (isVowel(currentChar)) {
32 vowelFrequencyMap.set(currentChar, (vowelFrequencyMap.get(currentChar) || 0) + 1);
33 } else {
34 consonantCount++;
35 }
36
37 // Shrink window from left while we have all vowels and enough consonants
38 while (consonantCount >= minConsonants && vowelFrequencyMap.size === 5) {
39 const leftChar = word[leftPointer++];
40
41 if (isVowel(leftChar)) {
42 // Decrease vowel count and remove from map if count reaches 0
43 vowelFrequencyMap.set(leftChar, vowelFrequencyMap.get(leftChar)! - 1);
44 if (vowelFrequencyMap.get(leftChar) === 0) {
45 vowelFrequencyMap.delete(leftChar);
46 }
47 } else {
48 consonantCount--;
49 }
50 }
51
52 // All substrings ending at current position with starting positions [0, leftPointer-1]
53 // are valid (they all contain all 5 vowels and at least minConsonants consonants)
54 substringCount += leftPointer;
55 }
56
57 return substringCount;
58 };
59
60 // Calculate substrings with exactly k consonants using the difference:
61 // (at least k consonants) - (at least k+1 consonants)
62 return countSubstringsWithAtLeastKConsonants(k) - countSubstringsWithAtLeastKConsonants(k + 1);
63}
64
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. Although there's a nested while loop, each character is added to the window once (when the right pointer moves) and removed from the window at most once (when the left pointer moves), resulting in amortized O(1)
operations per character. Therefore, each call to f(k)
takes O(n)
time, and calling it twice still results in O(2n) = O(n)
overall time complexity.
The space complexity is O(1)
. The algorithm uses a Counter
object cnt
to track 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. Additionally, the algorithm uses a few integer variables (ans
, l
, x
) which require constant space. Therefore, the total space complexity is O(1)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Window Shrinking Logic
Pitfall: A common mistake is shrinking the window too aggressively or not maintaining the correct invariant. Developers often write:
# INCORRECT: Shrinks window when we have a valid substring
while consonant_count >= min_consonants and len(vowel_counter) == 5:
# shrink window...
This appears correct but can lead to undercounting. The issue is that after shrinking, we immediately count left_pointer
substrings, but some of these substrings might not actually contain all vowels or enough consonants.
Solution: The current implementation is actually correct! The key insight is that we shrink the window while maintaining validity, and stop as soon as the window becomes invalid. At that point, all substrings from indices [0, left_pointer-1]
to the current right pointer are valid, which is exactly left_pointer
substrings.
2. Off-by-One Error in Counting
Pitfall: Confusing whether to add left_pointer
or left_pointer + 1
to the answer:
# INCORRECT: Adding left_pointer + 1 total_substrings += left_pointer + 1
Solution: The correct count is left_pointer
because:
- After shrinking,
left_pointer
points to the first position where the window is NO LONGER valid - Valid starting positions are from index 0 to
left_pointer - 1
(inclusive) - This gives us exactly
left_pointer
valid substrings
3. Forgetting to Handle Empty Counter
Pitfall: Not properly removing vowels from the counter when their count reaches zero:
# INCORRECT: Just decrementing without removing if left_char in "aeiou": vowel_counter[left_char] -= 1 # Missing: removal when count becomes 0
This causes len(vowel_counter)
to incorrectly report 5 even when some vowels have zero count.
Solution: Always remove the vowel from the counter when its count reaches zero:
if left_char in "aeiou": vowel_counter[left_char] -= 1 if vowel_counter[left_char] == 0: vowel_counter.pop(left_char) # Critical step!
4. Edge Case: k Larger Than Total Consonants
Pitfall: Not handling cases where k
is larger than the total number of consonants in the string:
# May cause issues if not handled properly word = "aeiou" # No consonants k = 1 # Should return 0, but implementation might break
Solution: The current implementation naturally handles this because:
- If
k
is larger than available consonants,consonant_count >= k
will never be true - The function will return 0, which is correct
- No special edge case handling needed due to the algorithm's design
5. Integer Overflow in Other Languages
Pitfall: In languages like C++ or Java, counting substrings of a long string might cause integer overflow:
// C++ example that might overflow int total_substrings = 0; // For a string of length n, maximum substrings is n*(n+1)/2
Solution: Use appropriate data types:
- In C++: use
long long
instead ofint
- In Java: use
long
instead ofint
- Python handles arbitrary precision integers automatically, so this isn't an issue
Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?
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!