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]
.
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 forl
innums
, which gives us the index of the first element innums
that is >=l
. This is our starting position. -
bisect_right(nums, r)
: Finds the insertion point forr
innums
, which gives us the index after the last element innums
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 EvaluatorExample 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 thewords
arraym
is the length of thequeries
array
Breaking down the time complexity:
- Creating the
nums
list requires iterating through alln
words and checking if each word starts and ends with vowels:O(n)
- For each of the
m
queries, we perform:bisect_right(nums, r)
:O(log n)
binary searchbisect_left(nums, l)
:O(log n)
binary search- Total for all queries:
O(m × log n)
- 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 mostn
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.
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
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!