2559. Count Vowel Strings in Ranges

In this engineering deep dive, we solve LeetCode 2559: Count Vowel Strings in Ranges. We examine Prefix Sum preprocessing, evaluate $O(1)$ static range query answering against dynamic tree representations, and benchmark bitmask vowel checks in C++ and TypeScript.

Problem Statement & Formal Constraints

You are given a 0-indexed array of strings words and a 2D integer array queries.

Each query queries[i] = [l_i, r_i] asks us to find the number of strings of present in the range from index l_i to r_i (both inclusive) that start and end with a vowel.

Return an array of integers ans of size queries.length, where ans[i] is the answer to the $i^{ ext{th}}$ query.

Vowel letters are 'a', 'e', 'i', 'o', and 'u'.

Formal Constraints: 1 ≤ words.length, queries.length ≤ 10^5. 1 ≤ words[i].length ≤ 10. 0 ≤ l_i ≤ r_i < words.length. Strings consist exclusively of lowercase English letters.

Example Walkthroughs:

Example 1: words = ["aba","bcb","ece","aa","e"], queries = [[0,2],[1,4],[1,1]] → Output: [2,3,0]
Explanation: The strings starting and ending with vowels are "aba", "ece", "aa", and "e" (indices 0, 2, 3, 4).
Query [0,2]: words at 0, 1, 2 → "aba" and "ece" (count = 2).
Query [1,4]: words at 1, 2, 3, 4 → "ece", "aa", "e" (count = 3).
Query [1,1]: word at 1 is "bcb" (count = 0).

The Range Query Bottleneck: Naive Scan vs. Prefix Sums

A brute-force scan answers each query by iterating from $l_i$ to $r_i$. Because both $words$ and $queries$ can contain up to $10^5$ items, a naive approach requires $O(Q imes N)$ operations ($10^5 imes 10^5 = 10^{10}$ calculations), exceeding the standard 1-second CPU execution limit by orders of magnitude and causing Time Limit Exceeded (TLE).

Because the underlying array $words$ is static and immutable (no insertions or deletions occur between queries), we can precompute cumulative vowel counts using a 1D Prefix Sum Array in $O(N)$ time. Once precomputed, any arbitrary range query $[l, r]$ is answered in strict $O(1)$ constant time:

$$ ext{Count}(l, r) = ext{Prefix}[r + 1] - ext{Prefix}[l]$$

Approach 1: C++ Implementation with Fast Vowel Bitmasking

Instead of multiple conditional string comparisons or hash-set lookups, we evaluate vowel status using a compact 32-bit integer bitmask:

#include <vector>
#include <string>

class Solution {
public:
    std::vector<int> vowelStrings(
        const std::vector<std::string>& words, 
        const std::vector<std::vector<int>>& queries
    ) {
        const int n = words.size();
        // 1-indexed prefix sum array of size n + 1
        std::vector<int> prefix(n + 1, 0);

        // Bitmask representing vowel character offsets: (1 << ('a'-'a')) | ...
        constexpr int VOWEL_MASK = (1 << 0) | (1 << 4) | (1 << 8) | (1 << 14) | (1 << 20);

        auto isVowel = [](char c) constexpr -> bool {
            return (VOWEL_MASK & (1 << (c - 'a'))) != 0;
        };

        for (int i = 0; i < n; ++i) {
            const auto& w = words[i];
            bool matches = isVowel(w.front()) && isVowel(w.back());
            prefix[i + 1] = prefix[i] + (matches ? 1 : 0);
        }

        std::vector<int> results;
        results.reserve(queries.size());

        for (const auto& q : queries) {
            int l = q[0];
            int r = q[1];
            results.push_back(prefix[r + 1] - prefix[l]);
        }

        return results;
    }
};

Approach 2: TypeScript Prefix Sum with Int32Array

In TypeScript, using typed arrays avoids V8 object boxing and ensures contiguous 32-bit integer storage in memory:

/**
 * Solves LeetCode 2529 using 1-indexed Int32Array prefix sum buffer.
 * Time Complexity: O(N + Q)
 * Space Complexity: O(N) prefix array buffer
 */
export function vowelStrings(words: string[], queries: number[][]): number[] {
  const n = words.length;
  const prefix = new Int32Array(n + 1);

  const vowels = new Set(['a', 'e', 'i', 'o', 'u']);

  for (let i = 0; i < n; i++) {
    const w = words[i];
    const isMatch = vowels.has(w[0]) && vowels.has(w[w.length - 1]);
    prefix[i + 1] = prefix[i] + (isMatch ? 1 : 0);
  }

  const qLen = queries.length;
  const ans = new Array<number>(qLen);

  for (let j = 0; j < qLen; j++) {
    const [l, r] = queries[j];
    ans[j] = prefix[r + 1] - prefix[l];
  }

  return ans;
}

Complexity Analysis & Data Structure Tradeoffs

The table below compares static Prefix Sums against alternative range-query architectures:

Architecture Preprocessing Time Query Time Space Complexity Ideal Use Case & Tradeoff
Brute Force Scan $O(1)$ none $O(N)$ per query $O(1)$ Causes TLE when $Q, N ge 10^5$.
Prefix Sum (Optimal) $O(N)$ linear $O(1)$ constant $O(N)$ Optimal for immutable static datasets; single subtraction per query.
Segment Tree / Fenwick Tree $O(N)$ $O(log N)$ $O(N)$ to $O(4N)$ Necessary only if elements mutate dynamically during execution.

Real-World Full-Stack Engineering Applications

Prefix Sum range query architectures power several production infrastructure systems:

  • Time-Series Metrics Aggregation: Answering telemetry queries like "how many HTTP 5xx errors occurred between timestamp $t_1$ and $t_2$" in Prometheus or Grafana.
  • Geospatial 2D Bounding Box Filters: Summed-area tables (2D prefix sums) calculate pixels or user density inside GPS bounding polygons in $O(1)$ time.
  • Financial Audit Trail Intervals: Rapidly verifying transactional credit balances between discrete fiscal statement close dates.

Frequently Asked Questions

Why use a 1-indexed prefix array of size N + 1?

A 1-indexed array avoids edge-case branch checks when $l = 0$. Because $ ext{Prefix}[0] = 0$, evaluating $ ext{Prefix}[r + 1] - ext{Prefix}[l]$ cleanly computes the sum from index 0 without needing an explicit ternary conditional.

Why is a bitmask faster than checking characters with if statements?

The bitwise operation (VOWEL_MASK & (1 << (c - 'a'))) translates to two CPU instructions (shift and bitwise AND) without branch prediction penalties, avoiding branch mispredictions in pipelined processors.

Can this approach support dynamic string modifications?

No. If words are modified after initialization, updating a standard prefix sum requires $O(N)$ recomputation. In dynamic environments, a Binary Indexed Tree (Fenwick Tree) updates and queries in $O(log N)$ time.

What is the total memory footprint for 100,000 strings?

An Int32Array(100001) consumes exactly $100,001 imes 4 ext{ bytes} approx 400 ext{ KB}$ of memory, which easily resides in standard L2/L3 CPU cache hierarchy.

Conclusion & Performance Takeaways

LeetCode 2559 illustrates the power of trade-offs in algorithm design: spending a linear $O(N)$ upfront preprocessing step permanently reduces query latency from $O(N)$ to $O(1)$, yielding an optimal total execution time of $O(N + Q)$.

Full-Stack Algorithm & Systems Architecture

Explore high-performance React, NodeJS, and algorithmic design patterns for enterprise web applications on WebDesigner.LA.

Schedule Technical Consultation →