Facebook Pixel

3306. Count of Substrings Containing Every Vowel and K Consonants II

MediumHash TableStringSliding Window
Leetcode Link

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:

  1. The substring contains every vowel ('a', 'e', 'i', 'o', and 'u') at least once
  2. 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.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

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):

  1. Data Structures:

    • cnt: A Counter (hash table) to track the frequency of each vowel in the current window
    • x: Counter for the number of consonants in the current window
    • l: Left pointer of the sliding window
    • ans: Accumulator for the total count of valid substrings
  2. Sliding Window Algorithm:

    For each character c at position r (right pointer):

    • If c is a vowel, increment its count in cnt
    • If c is a consonant, increment x
  3. Shrinking the Window:

    When the window becomes valid (x >= k and len(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 from cnt if count becomes 0
      • If d is a consonant: decrement x
      • Increment l
    • Stop shrinking when the window becomes invalid
  4. 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 to ans
  5. Final Answer:

    Return f(k) - f(k+1) to get the count of substrings with exactly k 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 Evaluator

Example 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
  • Position 7: 'd' gives us 3 consonants
    • After shrinking, l = 2
    • Count: 2

Total for f(2) = 0 + 2 = 2

Final Answer: f(1) - f(2) = 5 - 2 = 3

The 3 substrings with exactly 1 consonant are:

  1. "aeioub" (all vowels + 'b')
  2. "eioub" (all vowels + 'b')
  3. "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 consonants
  • f(k+1) counts substrings with ≥k+1 consonants
  • f(k) - f(k+1) gives exactly k consonants
  • f(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
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which data structure is used to implement recursion?


Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More