1641. Count Sorted Vowel Strings
Problem Description
You need to count how many strings of length n
can be formed using only vowels (a
, e
, i
, o
, u
) where the characters are arranged in lexicographically sorted order.
A string is lexicographically sorted when each character is either the same as or comes alphabetically before the next character. For example:
"aaa"
is valid (all same characters)"aeu"
is valid (characters in alphabetical order)"aeiou"
is valid (characters in alphabetical order)"aea"
is NOT valid (e
comes aftera
, so we can't go back toa
)"uio"
is NOT valid (i
ando
come beforeu
alphabetically)
Given an integer n
representing the length of the string, return the total number of valid strings that can be formed.
For example:
- If
n = 1
, the valid strings are:"a"
,"e"
,"i"
,"o"
,"u"
→ answer is5
- If
n = 2
, valid strings include:"aa"
,"ae"
,"ai"
,"ao"
,"au"
,"ee"
,"ei"
,"eo"
,"eu"
,"ii"
,"io"
,"iu"
,"oo"
,"ou"
,"uu"
→ answer is15
The solution uses dynamic programming with memoization. The dfs(i, j)
function counts valid strings where:
i
represents the current position in the string being builtj
represents the index of the last vowel used (0 fora
, 1 fore
, 2 fori
, 3 foro
, 4 foru
)
At each position, we can only use vowels that come at or after the previous vowel to maintain lexicographical order.
Intuition
The key insight is recognizing this as a counting problem with constraints. Since we need lexicographically sorted strings, once we choose a vowel at position i
, all subsequent positions can only use that same vowel or vowels that come after it alphabetically.
Think of it like climbing stairs where you can only go up or stay at the same level, never go down. If we map vowels to numbers (a
=0, e
=1, i
=2, o
=3, u
=4), then a valid string is like a non-decreasing sequence.
This naturally leads to a recursive approach: at each position in the string, we make choices about which vowel to place. The choices available depend on what vowel we used previously. If we just placed an e
(index 1), our next character can only be e
, i
, o
, or u
(indices 1-4).
The problem exhibits optimal substructure - the number of valid strings of length n
starting with a certain vowel can be broken down into smaller subproblems of length n-1
. For instance, if we want to count all valid strings of length 3 that start with a
, we need to count all valid strings of length 2 that can follow a
.
We can formulate this recursively: dfs(i, j)
represents "how many valid strings can we form from position i
to the end, given that the last vowel used has index j
or higher". The base case is when we've filled all n
positions (i >= n
), which gives us one valid string.
The recurrence relation becomes: from position i
with last vowel j
, we sum up all possibilities by trying each vowel from j
to 4 (inclusive), and recursively solving for the remaining positions. Using memoization with @cache
prevents recalculating the same subproblems multiple times, making the solution efficient.
Learn more about Math, Dynamic Programming and Combinatorics patterns.
Solution Approach
The solution uses dynamic programming with memoization to efficiently count all valid vowel strings. Here's how the implementation works:
Function Structure:
def dfs(i, j):
return 1 if i >= n else sum(dfs(i + 1, k) for k in range(j, 5))
The dfs
function takes two parameters:
i
: current position in the string (0 to n-1)j
: index of the minimum vowel we can use (0='a', 1='e', 2='i', 3='o', 4='u')
Base Case:
When i >= n
, we've successfully built a complete string of length n
, so we return 1
to count this valid string.
Recursive Case:
For each position i
, we try all valid vowels from index j
to 4
. The range(j, 5)
ensures we only pick vowels that maintain lexicographical order. For each choice k
, we recursively solve for the next position with dfs(i + 1, k)
.
The Recursion Flow:
Starting with dfs(0, 0)
:
- At position 0, we can choose any vowel (indices 0-4)
- If we choose 'a' (index 0), at position 1 we can again choose any vowel
- If we choose 'e' (index 1), at position 1 we can only choose 'e', 'i', 'o', or 'u'
- This continues until we reach position
n
Memoization with @cache:
The @cache
decorator automatically memoizes the function results. When dfs(i, j)
is called with the same parameters again, it returns the cached result instead of recalculating. This reduces time complexity from exponential to polynomial.
Time Complexity: O(n * 5)
where we have at most n * 5
unique states (positions × vowel choices)
Space Complexity: O(n * 5)
for the memoization cache plus O(n)
for recursion stack
The sum aggregates all valid string counts from different branches, giving us the total number of lexicographically sorted vowel strings of length n
.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with n = 3
to see how the algorithm counts valid strings.
Starting with dfs(0, 0)
:
- Position 0, can use any vowel from index 0 ('a') onwards
- We sum:
dfs(1, 0) + dfs(1, 1) + dfs(1, 2) + dfs(1, 3) + dfs(1, 4)
Let's trace one branch: dfs(1, 0)
(chose 'a' at position 0):
- Position 1, can use any vowel from index 0 ('a') onwards
- We sum:
dfs(2, 0) + dfs(2, 1) + dfs(2, 2) + dfs(2, 3) + dfs(2, 4)
Following dfs(2, 0)
(chose 'a' at position 1):
- Position 2, can use any vowel from index 0 ('a') onwards
- We sum:
dfs(3, 0) + dfs(3, 1) + dfs(3, 2) + dfs(3, 3) + dfs(3, 4)
Each dfs(3, k)
hits the base case (3 >= 3) and returns 1.
So dfs(2, 0) = 1 + 1 + 1 + 1 + 1 = 5
This represents the strings: "aaa", "aae", "aai", "aao", "aau"
Similarly, dfs(2, 1) = 4
(strings starting "ae": "aee", "aei", "aeo", "aeu")
And dfs(2, 2) = 3
(strings starting "ai": "aii", "aio", "aiu")
And dfs(2, 3) = 2
(strings starting "ao": "aoo", "aou")
And dfs(2, 4) = 1
(strings starting "au": "auu")
Therefore: dfs(1, 0) = 5 + 4 + 3 + 2 + 1 = 15
Continuing this pattern:
dfs(1, 1) = 10
(strings starting with 'e')dfs(1, 2) = 6
(strings starting with 'i')dfs(1, 3) = 3
(strings starting with 'o')dfs(1, 4) = 1
(strings starting with 'u')
Final result: dfs(0, 0) = 15 + 10 + 6 + 3 + 1 = 35
The memoization ensures we never recalculate the same dfs(i, j)
twice. For instance, dfs(2, 2)
is needed when we have "ai", "ei", or "ii" as our first two characters, but it's only computed once and reused.
Solution Implementation
1class Solution:
2 def countVowelStrings(self, n: int) -> int:
3 from functools import cache
4
5 @cache
6 def dfs(current_position: int, start_vowel_index: int) -> int:
7 """
8 Recursively count lexicographically sorted vowel strings.
9
10 Args:
11 current_position: Current position in the string being formed (0 to n-1)
12 start_vowel_index: Index of vowel to start from (0='a', 1='e', 2='i', 3='o', 4='u')
13
14 Returns:
15 Number of valid strings that can be formed from this state
16 """
17 # Base case: reached the required string length
18 if current_position >= n:
19 return 1
20
21 # Recursive case: try all vowels from current vowel index to 'u' (index 4)
22 # This ensures lexicographical order is maintained
23 total_count = 0
24 for next_vowel_index in range(start_vowel_index, 5):
25 total_count += dfs(current_position + 1, next_vowel_index)
26
27 return total_count
28
29 # Start building strings from position 0, allowing all vowels initially
30 return dfs(0, 0)
31
1class Solution {
2 // Memoization table: dp[position][lastVowelIndex] stores the count of valid strings
3 private Integer[][] dp;
4 // Length of the string to form
5 private int stringLength;
6
7 /**
8 * Count the number of sorted vowel strings of length n.
9 * The vowels are: 'a', 'e', 'i', 'o', 'u' (indexed 0-4).
10 * Strings must be lexicographically sorted (non-decreasing).
11 *
12 * @param n the length of strings to count
13 * @return the total number of valid vowel strings
14 */
15 public int countVowelStrings(int n) {
16 this.stringLength = n;
17 // Initialize memoization table: n positions × 5 vowels
18 dp = new Integer[n][5];
19 // Start DFS from position 0 with minimum vowel index 0
20 return dfs(0, 0);
21 }
22
23 /**
24 * Depth-first search with memoization to count valid strings.
25 *
26 * @param position current position in the string being formed (0-indexed)
27 * @param minVowelIndex minimum vowel index that can be used (ensures sorted order)
28 * @return number of valid strings that can be formed from this state
29 */
30 private int dfs(int position, int minVowelIndex) {
31 // Base case: if we've filled all positions, we have one valid string
32 if (position >= stringLength) {
33 return 1;
34 }
35
36 // Check memoization table for already computed result
37 if (dp[position][minVowelIndex] != null) {
38 return dp[position][minVowelIndex];
39 }
40
41 // Count all possible valid strings from current state
42 int count = 0;
43 // Try each vowel from minVowelIndex to 4 (maintaining sorted order)
44 for (int vowelIndex = minVowelIndex; vowelIndex < 5; vowelIndex++) {
45 // Recursively count strings starting with chosen vowel
46 count += dfs(position + 1, vowelIndex);
47 }
48
49 // Store result in memoization table and return
50 dp[position][minVowelIndex] = count;
51 return count;
52 }
53}
54
1class Solution {
2public:
3 int countVowelStrings(int n) {
4 // dp[i][j] represents the number of valid strings of length (n-i)
5 // starting from the j-th vowel (0='a', 1='e', 2='i', 3='o', 4='u')
6 int dp[n][5];
7 memset(dp, 0, sizeof dp);
8
9 // Define recursive function with memoization
10 function<int(int, int)> dfs = [&](int currentLength, int startVowel) {
11 // Base case: if we've built a string of length n, return 1
12 if (currentLength >= n) {
13 return 1;
14 }
15
16 // Return memoized result if already computed
17 if (dp[currentLength][startVowel]) {
18 return dp[currentLength][startVowel];
19 }
20
21 // Calculate the number of valid strings
22 int count = 0;
23 // Try all vowels from startVowel to 'u' (maintaining lexicographical order)
24 for (int nextVowel = startVowel; nextVowel < 5; ++nextVowel) {
25 count += dfs(currentLength + 1, nextVowel);
26 }
27
28 // Store and return the result
29 return dp[currentLength][startVowel] = count;
30 };
31
32 // Start building strings from length 0 with vowel 'a' (index 0)
33 return dfs(0, 0);
34 }
35};
36
1function countVowelStrings(n: number): number {
2 // dp[i][j] represents the number of valid strings of length (n-i)
3 // starting from the j-th vowel (0='a', 1='e', 2='i', 3='o', 4='u')
4 const dp: number[][] = Array(n).fill(0).map(() => Array(5).fill(0));
5
6 /**
7 * Recursive function with memoization to count vowel strings
8 * @param currentLength - The current length of the string being built
9 * @param startVowel - The index of the vowel to start from (0-4)
10 * @returns The number of valid strings that can be formed
11 */
12 const dfs = (currentLength: number, startVowel: number): number => {
13 // Base case: if we've built a string of length n, return 1
14 if (currentLength >= n) {
15 return 1;
16 }
17
18 // Return memoized result if already computed
19 if (dp[currentLength][startVowel] !== 0) {
20 return dp[currentLength][startVowel];
21 }
22
23 // Calculate the number of valid strings
24 let count = 0;
25 // Try all vowels from startVowel to 'u' (maintaining lexicographical order)
26 for (let nextVowel = startVowel; nextVowel < 5; nextVowel++) {
27 count += dfs(currentLength + 1, nextVowel);
28 }
29
30 // Store and return the result
31 dp[currentLength][startVowel] = count;
32 return count;
33 };
34
35 // Start building strings from length 0 with vowel 'a' (index 0)
36 return dfs(0, 0);
37}
38
Time and Space Complexity
Time Complexity: O(n * 5) = O(n)
The function uses memoization with @cache
. The state space is defined by two parameters: i
(ranging from 0 to n) and j
(ranging from 0 to 4). This gives us at most n * 5
unique states. Each state is computed only once due to memoization. For each state (i, j)
, we iterate through at most 5 values (from j
to 4), performing constant work for each. Therefore, the total time complexity is O(n * 5 * 5) = O(25n) = O(n)
.
Space Complexity: O(n)
The space complexity consists of two components:
- Memoization cache: Stores at most
n * 5 = O(n)
unique states - Recursion call stack: The maximum depth of recursion is
n
(wheni
goes from 0 to n)
Therefore, the total space complexity is O(n) + O(n) = O(n)
.
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Parameter Order in Recursive Calls
A frequent mistake is confusing which parameter represents the position and which represents the vowel index, leading to incorrect recursive calls:
# WRONG: Swapping parameters
def dfs(vowel_index, position):
if position >= n:
return 1
return sum(dfs(k, position + 1) for k in range(vowel_index, 5))
# This will cause incorrect results because the logic is reversed
Solution: Always maintain consistent parameter order and use descriptive names:
def dfs(position, min_vowel_index): # Clear naming prevents confusion
if position >= n:
return 1
return sum(dfs(position + 1, k) for k in range(min_vowel_index, 5))
2. Off-by-One Errors in Base Case
Using the wrong comparison operator in the base case is a subtle but critical error:
# WRONG: Using > instead of >=
def dfs(i, j):
if i > n: # This builds strings of length n+1!
return 1
return sum(dfs(i + 1, k) for k in range(j, 5))
# WRONG: Using == with incorrect value
def dfs(i, j):
if i == n - 1: # This stops one position too early!
return 1
return sum(dfs(i + 1, k) for k in range(j, 5))
Solution: Use i >= n
or i == n
for the base case when building from position 0:
def dfs(i, j):
if i >= n: # Correct: we've filled all n positions (0 to n-1)
return 1
# ... rest of the code
3. Forgetting Memoization
Without memoization, the solution has exponential time complexity and will timeout for larger inputs:
# WRONG: No memoization
def countVowelStrings(self, n: int) -> int:
def dfs(i, j): # Missing @cache decorator
if i >= n:
return 1
return sum(dfs(i + 1, k) for k in range(j, 5))
return dfs(0, 0)
Solution: Always add memoization using either @cache
or manual dictionary:
# Option 1: Using @cache
from functools import cache
@cache
def dfs(i, j):
# ... implementation
# Option 2: Manual memoization
memo = {}
def dfs(i, j):
if (i, j) in memo:
return memo[(i, j)]
# ... calculate result
memo[(i, j)] = result
return result
4. Incorrect Range Boundaries
Using wrong range boundaries breaks the lexicographical constraint:
# WRONG: Starting from 0 instead of j
def dfs(i, j):
if i >= n:
return 1
return sum(dfs(i + 1, k) for k in range(0, 5)) # Allows going backwards!
# WRONG: Not including 5 in range
def dfs(i, j):
if i >= n:
return 1
return sum(dfs(i + 1, k) for k in range(j, 4)) # Misses 'u' (index 4)!
Solution: Always use range(j, 5)
to maintain order and include all valid vowels:
def dfs(i, j):
if i >= n:
return 1
return sum(dfs(i + 1, k) for k in range(j, 5)) # Correct range
5. Alternative Approach Pitfall: Combinatorial Formula
Some might try to use the stars and bars formula directly: C(n+4, 4), but implementing it incorrectly:
# WRONG: Incorrect formula application
from math import comb
def countVowelStrings(self, n: int) -> int:
return comb(n + 4, n) # Wrong parameters!
Solution: If using the combinatorial approach, the correct formula is:
from math import comb
def countVowelStrings(self, n: int) -> int:
return comb(n + 4, 4) # Correct: choosing 4 dividers among n+4 positions
In a binary min heap, the maximum element can be found in:
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!