Facebook Pixel

3305. Count of Substrings Containing Every Vowel and K Consonants I

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. 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.

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

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 exactly k consonants + Substrings with exactly k+1 consonants + Substrings with exactly k+2 consonants + ...
  • Substrings with at least k+1 consonants = Substrings with exactly k+1 consonants + Substrings with exactly k+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:

  1. Data Structures:

    • cnt: A Counter (hash table) to track occurrences 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
  2. Expanding the Window (right pointer):

    • Iterate through each character c in word
    • If c is a vowel, add it to cnt
    • If c is a consonant, increment x
  3. Contracting the Window (left pointer):

    • When we have a valid window (x >= k and len(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 in cnt
        • If count becomes 0, remove it from cnt
      • If word[l] is a consonant, decrement x
      • Increment l
    • The loop stops when either x < k or we don't have all 5 vowels
  4. 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 to ans (there are exactly l such valid substrings)
  5. Final Answer:

    • Return f(k) - f(k + 1) to get substrings with exactly k consonants

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 Evaluator

Example 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 of int
  • In Java: use long instead of int
  • Python handles arbitrary precision integers automatically, so this isn't an issue
Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

Which of the two traversal algorithms (BFS and DFS) can be used to find whether two nodes are connected?


Recommended Readings

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

Load More