2559. Count Vowel Strings in Ranges - Explanation

Problem Link



Prerequisites

Before attempting this problem, you should be comfortable with:

  • Prefix Sum - Precomputing cumulative sums to answer range queries in O(1) time
  • Hash Set - Using sets for O(1) character lookup when checking vowels
  • Bit Manipulation (Optional) - Using bitmasks as an alternative to hash sets for membership testing

1. Brute Force

Intuition

A word is a "vowel string" if it starts and ends with a vowel (a, e, i, o, u). For each query, we need to count how many words in the given range satisfy this condition. The straightforward approach is to iterate through the range for each query and check each word.

Algorithm

  1. Create a set of vowels for O(1) lookup.
  2. Initialize an empty result list.
  3. For each query (start, end):
    • Initialize a counter to 0.
    • For each word from index start to end:
      • Check if the first and last characters are both vowels.
      • If so, increment the counter.
    • Append the counter to the result list.
  4. Return the result list.
class Solution:
    def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]:
        vowels = set("aeiou")
        res = []

        for start, end in queries:
            cnt = 0
            for i in range(start, end + 1):
                if words[i][0] in vowels and words[i][-1] in vowels:
                    cnt += 1
            res.append(cnt)

        return res

Time & Space Complexity

  • Time complexity: O(nm)O(n * m)
  • Space complexity:
    • O(1)O(1) extra space.
    • O(m)O(m) space for the output list.

Where nn is the number of words, and mm is the number of queries.


2. Prefix Sum + Hash Set

Intuition

The brute force approach recomputes the count for overlapping or similar ranges repeatedly. We can precompute a prefix sum array where prefix[i] stores the count of vowel strings from index 0 to i-1. Then any range query (l, r) can be answered in O(1) time as prefix[r+1] - prefix[l].

Algorithm

  1. Create a set of vowels for O(1) lookup.
  2. Build a prefix sum array of size n+1, initialized to 0.
  3. For each word at index i:
    • Set prefix[i+1] = prefix[i].
    • If the word starts and ends with a vowel, increment prefix[i+1].
  4. For each query (l, r):
    • The answer is prefix[r+1] - prefix[l].
  5. Return the results.
class Solution:
    def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]:
        vowel_set = set("aeiou")
        prefix_cnt = [0] * (len(words) + 1)
        prev = 0

        for i, w in enumerate(words):
            if w[0] in vowel_set and w[-1] in vowel_set:
                prev += 1
            prefix_cnt[i + 1] = prev

        res = [0] * len(queries)
        for i, q in enumerate(queries):
            l, r = q
            res[i] = prefix_cnt[r + 1] - prefix_cnt[l]

        return res

Time & Space Complexity

  • Time complexity: O(n+m)O(n + m)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(m)O(m) space for the output list.

Where nn is the number of words, and mm is the number of queries.


3. Prefix Sum + Bitmask

Intuition

Instead of using a hash set to check vowels, we can use a bitmask. Since there are only 5 vowels and 26 letters, we can represent which characters are vowels using a single integer where bit i is set if the character at position i ('a' + i) is a vowel. Checking if a character is a vowel becomes a simple bitwise AND operation.

Algorithm

  1. Create a bitmask for vowels by setting bits for 'a', 'e', 'i', 'o', 'u'.
  2. Build a prefix sum array.
  3. For each word:
    • Check if the first character's bit is set in the vowel mask.
    • Check if the last character's bit is set in the vowel mask.
    • If both are true, increment the prefix sum.
  4. For each query (l, r):
    • The answer is prefix[r+1] - prefix[l].
  5. Return the results.
class Solution:
    def vowelStrings(self, words: List[str], queries: List[List[int]]) -> List[int]:
        vowels = sum(1 << (ord(c) - ord('a')) for c in "aeiou")
        prefix = [0]
        for w in words:
            prefix.append(prefix[-1])
            if (1 << (ord(w[0]) - ord('a'))) & vowels and (1 << (ord(w[-1]) - ord('a'))) & vowels:
                prefix[-1] += 1
        return [prefix[r + 1] - prefix[l] for l, r in queries]

Time & Space Complexity

  • Time complexity: O(n+m)O(n + m)
  • Space complexity:
    • O(n)O(n) extra space.
    • O(m)O(m) space for the output list.

Where nn is the number of words, and mm is the number of queries.


Common Pitfalls

Off-by-One Error in Prefix Sum Query

A classic mistake is using prefix[r] - prefix[l] instead of prefix[r + 1] - prefix[l]. The prefix array is 1-indexed to allow easy range queries, so the right endpoint needs the +1 offset.

# Incorrect - excludes the element at index r
res[i] = prefix[r] - prefix[l]

# Correct - includes the element at index r
res[i] = prefix[r + 1] - prefix[l]

Checking Only First or Last Character

A vowel string must start AND end with a vowel. Checking only one condition will count words that should not be included.

# Incorrect - only checks starting character
if words[i][0] in vowels:
    cnt += 1

# Correct - checks both first and last characters
if words[i][0] in vowels and words[i][-1] in vowels:
    cnt += 1

Hardcoding Vowels Incorrectly

When using a bitmask or set for vowels, omitting one of the five vowels (a, e, i, o, u) leads to incorrect results. This is especially common when manually constructing the bitmask.

# Incorrect - missing 'u'
vowels = set("aeio")

# Correct - all five vowels included
vowels = set("aeiou")