Facebook Pixel

2559. Count Vowel Strings in Ranges

Problem Description

You are given an array of strings words (0-indexed) and a 2D array of integer pairs called queries.

Each query is represented as queries[i] = [li, ri], where li and ri are indices. For each query, you need to count how many strings in the range from index li to index ri (inclusive) in the words array both start AND end with a vowel letter.

The vowel letters are: 'a', 'e', 'i', 'o', and 'u'.

Your task is to return an array ans where ans[i] contains the count of strings that start and end with a vowel for the i-th query.

For example:

  • If words = ["apple", "orange", "ice"] and you have a query [0, 2], you would check all three words:
    • "apple" starts with 'a' (vowel) and ends with 'e' (vowel) ✓
    • "orange" starts with 'o' (vowel) and ends with 'e' (vowel) ✓
    • "ice" starts with 'i' (vowel) and ends with 'e' (vowel) ✓
    • The answer for this query would be 3

The solution uses preprocessing to identify all indices where words start and end with vowels, stores them in a sorted list nums, then uses binary search (bisect_right and bisect_left) to efficiently count how many valid indices fall within each query range [l, r].

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

Intuition

The naive approach would be to iterate through each query and check every word in the given range, counting those that start and end with vowels. However, this would be inefficient if we have many queries, as we'd be repeatedly checking the same words.

The key insight is that we can preprocess the data once to make all subsequent queries faster. Since we're only interested in words that start and end with vowels, we can identify these words upfront and store their indices.

Once we have a list of indices where valid words exist (let's call it nums), the problem transforms into: "How many indices in nums fall within the range [l, r]?"

For example, if nums = [0, 2, 3, 5, 7] and we have a query [2, 5], we need to count how many values in nums are between 2 and 5 inclusive. The answer would be 3 (indices 2, 3, and 5).

This is a classic range counting problem that can be solved efficiently with binary search:

  • Find the leftmost position where we could insert l in the sorted array (this gives us the first valid index >= l)
  • Find the rightmost position where we could insert r (this gives us the count of all valid indices <= r)
  • The difference between these two positions gives us the count

Using Python's bisect_left(nums, l) finds the first index >= l, and bisect_right(nums, r) finds the position after the last index <= r. The difference bisect_right(nums, r) - bisect_left(nums, l) gives us exactly how many valid indices fall within our query range.

This preprocessing approach reduces the time complexity from O(queries × words) to O(words + queries × log(valid_words)), which is much more efficient when dealing with multiple queries.

Learn more about Prefix Sum patterns.

Solution Approach

The solution implements the preprocessing and binary search approach in three main steps:

Step 1: Identify Valid Words

vowels = set("aeiou")

First, we create a set of vowels for O(1) lookup time when checking if a character is a vowel.

Step 2: Preprocess and Store Valid Indices

nums = [i for i, w in enumerate(words) if w[0] in vowels and w[-1] in vowels]

We iterate through the words array once using enumerate() to get both the index i and the word w. For each word, we check:

  • w[0] in vowels: Is the first character a vowel?
  • w[-1] in vowels: Is the last character a vowel?

If both conditions are true, we add the index i to our nums list. This list comprehension creates a sorted array of indices where valid words (starting and ending with vowels) are located.

Step 3: Process Queries with Binary Search

return [bisect_right(nums, r) - bisect_left(nums, l) for l, r in queries]

For each query [l, r], we use binary search to count valid indices:

  • bisect_left(nums, l): Finds the insertion point for l in nums, which gives us the index of the first element in nums that is >= l. This is our starting position.

  • bisect_right(nums, r): Finds the insertion point for r in nums, which gives us the index after the last element in nums that is <= r. This is our ending position (exclusive).

  • The difference between these two positions gives us the count of valid indices within the range [l, r].

For example, if nums = [0, 2, 3, 5, 7] and query is [2, 5]:

  • bisect_left(nums, 2) returns 1 (index where 2 is located)
  • bisect_right(nums, 5) returns 4 (index after 5)
  • Count = 4 - 1 = 3, meaning indices 2, 3, and 5 are valid

The list comprehension processes all queries and returns the results as a list.

Time Complexity: O(n + m × log k) where n is the length of words, m is the number of queries, and k is the number of valid words.

Space Complexity: O(k) for storing the nums array.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's walk through a concrete example to illustrate the solution approach.

Given:

  • words = ["aba", "bcb", "ece", "aa", "e"]
  • queries = [[0, 2], [1, 4], [1, 1]]

Step 1: Identify which words start and end with vowels

Let's check each word:

  • Index 0: "aba" → starts with 'a' (vowel), ends with 'a' (vowel) ✓
  • Index 1: "bcb" → starts with 'b' (not vowel), ends with 'b' (not vowel) ✗
  • Index 2: "ece" → starts with 'e' (vowel), ends with 'e' (vowel) ✓
  • Index 3: "aa" → starts with 'a' (vowel), ends with 'a' (vowel) ✓
  • Index 4: "e" → starts with 'e' (vowel), ends with 'e' (vowel) ✓

Step 2: Create the preprocessed array nums

Store the indices of valid words: nums = [0, 2, 3, 4]

This sorted array contains all indices where words start and end with vowels.

Step 3: Process each query using binary search

Query 1: [0, 2] - Count valid words from index 0 to 2

  • bisect_left(nums, 0) = 0 (insertion point for 0 in [0, 2, 3, 4])
  • bisect_right(nums, 2) = 2 (insertion point after 2 in [0, 2, 3, 4])
  • Count = 2 - 0 = 2
  • Valid indices in range: 0 and 2

Query 2: [1, 4] - Count valid words from index 1 to 4

  • bisect_left(nums, 1) = 1 (insertion point for 1 in [0, 2, 3, 4])
  • bisect_right(nums, 4) = 4 (insertion point after 4 in [0, 2, 3, 4])
  • Count = 4 - 1 = 3
  • Valid indices in range: 2, 3, and 4

Query 3: [1, 1] - Count valid words from index 1 to 1

  • bisect_left(nums, 1) = 1 (insertion point for 1 in [0, 2, 3, 4])
  • bisect_right(nums, 1) = 1 (insertion point after 1 in [0, 2, 3, 4])
  • Count = 1 - 1 = 0
  • No valid indices in range (index 1 contains "bcb" which doesn't start/end with vowels)

Final Answer: [2, 3, 0]

This approach efficiently handles all queries by preprocessing once and using binary search, avoiding the need to repeatedly check the same words for different queries.

Solution Implementation

1from typing import List
2from bisect import bisect_left, bisect_right
3
4class Solution:
5    def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]:
6        # Define the set of vowels for quick lookup
7        vowels = {'a', 'e', 'i', 'o', 'u'}
8      
9        # Collect indices of words that start and end with vowels
10        # These indices represent positions in the original words array
11        vowel_word_indices = []
12        for index, word in enumerate(words):
13            if word[0] in vowels and word[-1] in vowels:
14                vowel_word_indices.append(index)
15      
16        # Process each query to count vowel strings in the given range
17        result = []
18        for left, right in queries:
19            # Use binary search to find how many vowel word indices fall within [left, right]
20            # bisect_right(nums, right) finds position after last index <= right
21            # bisect_left(nums, left) finds position of first index >= left
22            count = bisect_right(vowel_word_indices, right) - bisect_left(vowel_word_indices, left)
23            result.append(count)
24      
25        return result
26
1class Solution {
2    // List to store indices of words that start and end with vowels
3    private List<Integer> vowelWordIndices = new ArrayList<>();
4
5    /**
6     * Counts the number of strings that start and end with vowels within given query ranges.
7     * @param words Array of strings to check
8     * @param queries Array of query ranges [left, right] inclusive
9     * @return Array containing count of vowel strings for each query
10     */
11    public int[] vowelStrings(String[] words, int[][] queries) {
12        // Define the set of vowels
13        Set<Character> vowels = Set.of('a', 'e', 'i', 'o', 'u');
14      
15        // Collect indices of words that start and end with vowels
16        for (int i = 0; i < words.length; i++) {
17            char firstChar = words[i].charAt(0);
18            char lastChar = words[i].charAt(words[i].length() - 1);
19          
20            // Check if both first and last characters are vowels
21            if (vowels.contains(firstChar) && vowels.contains(lastChar)) {
22                vowelWordIndices.add(i);
23            }
24        }
25      
26        // Process each query
27        int queryCount = queries.length;
28        int[] result = new int[queryCount];
29      
30        for (int i = 0; i < queryCount; i++) {
31            int leftBound = queries[i][0];
32            int rightBound = queries[i][1];
33          
34            // Count vowel words in range [leftBound, rightBound]
35            // Uses binary search to find positions
36            result[i] = search(rightBound + 1) - search(leftBound);
37        }
38      
39        return result;
40    }
41
42    /**
43     * Binary search to find the first position where vowelWordIndices[pos] >= target.
44     * Returns the number of elements less than target.
45     * @param target The value to search for
46     * @return Index of first element >= target (or size if all elements < target)
47     */
48    private int search(int target) {
49        int left = 0;
50        int right = vowelWordIndices.size();
51      
52        // Binary search for lower bound
53        while (left < right) {
54            int mid = (left + right) >> 1;  // Equivalent to (left + right) / 2
55          
56            if (vowelWordIndices.get(mid) >= target) {
57                // Target is in left half or at mid
58                right = mid;
59            } else {
60                // Target is in right half
61                left = mid + 1;
62            }
63        }
64      
65        return left;
66    }
67}
68
1class Solution {
2public:
3    vector<int> vowelStrings(vector<string>& words, vector<vector<int>>& queries) {
4        // Define the set of vowels for quick lookup
5        unordered_set<char> vowelSet = {'a', 'e', 'i', 'o', 'u'};
6      
7        // Store indices of words that start and end with vowels
8        vector<int> validWordIndices;
9      
10        // Check each word to see if it starts and ends with a vowel
11        for (int i = 0; i < words.size(); ++i) {
12            char firstChar = words[i][0];
13            char lastChar = words[i].back();
14          
15            // If both first and last characters are vowels, store the index
16            if (vowelSet.count(firstChar) && vowelSet.count(lastChar)) {
17                validWordIndices.push_back(i);
18            }
19        }
20      
21        // Process each query and store results
22        vector<int> result;
23      
24        for (auto& query : queries) {
25            int leftBound = query[0];
26            int rightBound = query[1];
27          
28            // Count valid words in the range [leftBound, rightBound] using binary search
29            // lower_bound finds the first index >= leftBound
30            // upper_bound finds the first index > rightBound
31            // The difference gives us the count of valid indices in the range
32            int count = upper_bound(validWordIndices.begin(), validWordIndices.end(), rightBound) - 
33                       lower_bound(validWordIndices.begin(), validWordIndices.end(), leftBound);
34          
35            result.push_back(count);
36        }
37      
38        return result;
39    }
40};
41
1/**
2 * Counts the number of strings that start and end with vowels within query ranges
3 * @param words - Array of strings to check
4 * @param queries - Array of query ranges [left, right] inclusive
5 * @returns Array of counts for each query range
6 */
7function vowelStrings(words: string[], queries: number[][]): number[] {
8    // Set of vowel characters for efficient lookup
9    const vowelSet = new Set<string>(['a', 'e', 'i', 'o', 'u']);
10  
11    // Array to store indices of words that start and end with vowels
12    const vowelWordIndices: number[] = [];
13  
14    // Iterate through all words to find those starting and ending with vowels
15    for (let index = 0; index < words.length; index++) {
16        const currentWord = words[index];
17        const firstChar = currentWord[0];
18        const lastChar = currentWord[currentWord.length - 1];
19      
20        // Check if both first and last characters are vowels
21        if (vowelSet.has(firstChar) && vowelSet.has(lastChar)) {
22            vowelWordIndices.push(index);
23        }
24    }
25  
26    /**
27     * Binary search to find the leftmost position where value >= target
28     * @param target - The target value to search for
29     * @returns The index of the first element >= target
30     */
31    const binarySearch = (target: number): number => {
32        let left = 0;
33        let right = vowelWordIndices.length;
34      
35        // Binary search for lower bound
36        while (left < right) {
37            const mid = (left + right) >> 1; // Equivalent to Math.floor((left + right) / 2)
38          
39            if (vowelWordIndices[mid] >= target) {
40                // Target might be at mid or to the left
41                right = mid;
42            } else {
43                // Target is definitely to the right
44                left = mid + 1;
45            }
46        }
47      
48        return left;
49    };
50  
51    // Process each query and calculate the count of vowel strings in range
52    return queries.map(([leftBound, rightBound]) => {
53        // Find count of vowel words in range [leftBound, rightBound]
54        // by finding positions in the sorted vowelWordIndices array
55        const rightCount = binarySearch(rightBound + 1);
56        const leftCount = binarySearch(leftBound);
57        return rightCount - leftCount;
58    });
59}
60

Time and Space Complexity

The time complexity is O(n + m × log n), where:

  • n is the length of the words array
  • m is the length of the queries array

Breaking down the time complexity:

  1. Creating the nums list requires iterating through all n words and checking if each word starts and ends with vowels: O(n)
  2. For each of the m queries, we perform:
    • bisect_right(nums, r): O(log n) binary search
    • bisect_left(nums, l): O(log n) binary search
    • Total for all queries: O(m × log n)
  3. Overall: O(n) + O(m × log n) = O(n + m × log n)

The space complexity is O(n):

  • The vowels set is constant size: O(1)
  • The nums list can contain at most n elements (if all words start and end with vowels): O(n)
  • The result list stores m integers but this is required output, not auxiliary space
  • Overall auxiliary space: O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

1. Off-by-One Errors with Binary Search Functions

A common mistake is confusing when to use bisect_left vs bisect_right for the query boundaries, leading to incorrect counts.

Pitfall Example:

# WRONG: Using bisect_right for both boundaries
count = bisect_right(nums, r) - bisect_right(nums, l)  # Misses elements equal to l

# WRONG: Using bisect_left for both boundaries  
count = bisect_left(nums, r) - bisect_left(nums, l)  # Misses elements equal to r

Why This Happens:

  • bisect_left(nums, x) returns the leftmost position where x should be inserted (before any existing x values)
  • bisect_right(nums, x) returns the rightmost position where x should be inserted (after any existing x values)

Correct Solution:

count = bisect_right(nums, r) - bisect_left(nums, l)

This ensures we include all indices from l to r inclusive.

2. Empty String Handling

The code assumes all words have at least one character when accessing w[0] and w[-1]. Empty strings will cause an IndexError.

Pitfall Example:

words = ["apple", "", "ice"]  # Empty string causes IndexError
if w[0] in vowels and w[-1] in vowels:  # Crashes on empty string

Solution:

vowel_word_indices = []
for index, word in enumerate(words):
    if word and word[0] in vowels and word[-1] in vowels:
        vowel_word_indices.append(index)

Adding the if word check ensures we skip empty strings.

3. Case Sensitivity Issues

The current solution only checks lowercase vowels. If the input contains uppercase letters, they won't be recognized as vowels.

Pitfall Example:

words = ["Apple", "ORANGE", "Ice"]  # All start/end with vowels but uppercase
vowels = {'a', 'e', 'i', 'o', 'u'}  # Only lowercase
# Result: None of these words will be counted

Solution:

vowels = {'a', 'e', 'i', 'o', 'u', 'A', 'E', 'I', 'O', 'U'}
# OR convert to lowercase when checking:
if word[0].lower() in vowels and word[-1].lower() in vowels:

4. Performance Degradation with Inefficient Implementation

Using a brute force approach for each query instead of binary search significantly impacts performance.

Pitfall Example:

# INEFFICIENT: O(m × n) time complexity
result = []
for left, right in queries:
    count = 0
    for i in range(left, right + 1):
        if words[i][0] in vowels and words[i][-1] in vowels:
            count += 1
    result.append(count)

Why Binary Search is Better:

  • Preprocessing: O(n) time to build the indices list
  • Each query: O(log k) time where k is the number of valid words
  • Total: O(n + m × log k) vs O(m × n) for brute force

The binary search approach is especially beneficial when the number of queries is large or when the valid words are sparse in the array.

Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

What's the output of running the following function using the following tree as input?

1def serialize(root):
2    res = []
3    def dfs(root):
4        if not root:
5            res.append('x')
6            return
7        res.append(root.val)
8        dfs(root.left)
9        dfs(root.right)
10    dfs(root)
11    return ' '.join(res)
12
1import java.util.StringJoiner;
2
3public static String serialize(Node root) {
4    StringJoiner res = new StringJoiner(" ");
5    serializeDFS(root, res);
6    return res.toString();
7}
8
9private static void serializeDFS(Node root, StringJoiner result) {
10    if (root == null) {
11        result.add("x");
12        return;
13    }
14    result.add(Integer.toString(root.val));
15    serializeDFS(root.left, result);
16    serializeDFS(root.right, result);
17}
18
1function serialize(root) {
2    let res = [];
3    serialize_dfs(root, res);
4    return res.join(" ");
5}
6
7function serialize_dfs(root, res) {
8    if (!root) {
9        res.push("x");
10        return;
11    }
12    res.push(root.val);
13    serialize_dfs(root.left, res);
14    serialize_dfs(root.right, res);
15}
16

Recommended Readings

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

Load More