1641. Count Sorted Vowel Strings
Problem Description
The goal is to find the number of strings of a given length n
that are composed solely of vowels (a
, e
, i
, o
, u
) and are lexicographically sorted. A lexicographically sorted string means that each character in the string is in non-decreasing alphabetical order. For example, the string "aei" is sorted but "aie" is not because i
comes before e
alphabetically. Given that the strings can only contain vowels, and they must be in sorted order, we have to count all the possible combinations that meet these criteria for a string of length n
.
Intuition
To understand the solution, let's consider a simple case with n = 1
. There are obviously 5 strings that can be formed, each with one character: a
, e
, i
, o
, or u
. Now consider n = 2
. We can add each vowel to each of the strings of length 1, but because our strings must remain sorted, every character we add can only be the same or come after the character in our existing string. So for 'a', we can add a
, e
, i
, o
, or u
, but for 'e', we can only add e
, i
, o
, or u
, and so on. This results in a total of 15 strings.
Intuitively, you can see that the problem is related to the concept of combinations with repetitions allowed. The code provided uses a dynamic programming approach to build a solution for a string of length n
based on the solutions for strings of length n-1
. Here's how the implementation works:
-
We start by initializing a list
f
with length 5, filled with 1s. This list keeps track of the number of ways to end a string of lengthn
with each vowel. Initially, for the case wheren
= 1, we have 1 way to end a string with each vowel. -
For each additional character in our string (from 2 to
n
), we iterate through our listf
, and for each position, we calculate the cumulative sum up to the current position. This cumulative sum represents the number of ways we can form a string of the current length that ends with the vowel corresponding to the current position. -
Why does this work? The cumulative sum works because it adds all the ways we could form strings ending with vowels that can lexicographically precede the current vowel. This is exactly what we need because earlier vowels can always be followed by later vowels to maintain the sorted order.
-
After we have iterated through all the characters (
n - 1
times), we sum up our listf
to get the total number of sorted vowel strings of lengthn
.
The elegance of the solution lies in the way it builds from simpler subproblems, exploiting the fact that every subproblem is related to smaller instances of itself; hence, building up to the solution rather than trying to compute it from scratch.
Learn more about Math, Dynamic Programming and Combinatorics patterns.
Solution Approach
The solution uses dynamic programming, a common technique for solving problems by breaking them down into simpler subproblems. The Python code's structure demonstrates how we can smartly leverage the combinatorial nature of the problem with a bottom-up approach. Here's a step-by-step walkthrough of how the algorithm operates:
-
Initialize the State: A list
f
of length 5 is initialized with ones,f = [1, 1, 1, 1, 1]
. This represents the number of ways to form a lexicographically sorted string withn = 1
for each vowel. Essentially, these are the base cases. -
Iterate Over String Length: We iterate from 2 up to
n
(sincen = 1
is our base case). For each new length, we are trying to compute the number of sorted vowel strings of that length. -
Update State with Cumulative Sums: For each vowel indexed by
j
, we updatef[j]
with the sum of counts up toj
. In other words,s
is a running total which is added to eachf[j]
. This represents all the ways we can end a string with the vowel atf[j]
while maintaining lexicographic order.- For example, if
f
currently equals[1, 2, 3, 4, 5]
(for 'a', 'e', 'i', 'o', 'u' respectively), for the next length, 'a' can be followed by any vowel so it takes the sum of all possibilities[1+2+3+4+5]
. 'e' cannot be followed by 'a', so it gets[2+3+4+5]
, and so on.
- For example, if
-
Final Summation: Once all iterations for lengths are completed, we sum up all the numbers in
f
to get the total count of valid lexicographically sorted strings of lengthn
.
This solution effectively applies the principles of counting sort by accumulating the number of valid previous options and using them to form new strings. By the end of the iterations, sum(f)
gives us the desired output — the number of lexicographically sorted strings of length n
.
Here's how the code encapsulates this logic:
class Solution:
def countVowelStrings(self, n: int) -> int:
f = [1] * 5 # Base case initialization
for _ in range(n - 1): # Iterating over string length
s = 0 # Reset cumulative sum
for j in range(5): # Iterating over vowels
s += f[j] # Cumulative sum represents the combinatorial options
f[j] = s # Store the new count back in f
return sum(f) # Final result is the sum of the last state
The algorithm's time and space complexity are both O(N), where N is the length of the string, making it efficient even for larger strings as it only requires iterating and updating a 5-element list N-1 times.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach with an example where n = 3
, i.e., we want to find the count of all lexicographically sorted strings that can be formed using vowels and have a length of 3.
-
Initialize the State: We start by setting
f = [1, 1, 1, 1, 1]
, which represents there is 1 way to create a string of length 1 ending with each of the vowels 'a', 'e', 'i', 'o', 'u'. -
Iterate Over String Length:
-
For
n = 2
:- We start by initializing a cumulative sum
s = 0
. - We then update this sum in a loop for each element in
f
:- For 'a' (
f[0]
), the sum becomess = 0 + 1 = 1
; we updatef[0] = 1
. - For 'e' (
f[1]
), the sum becomess = 1 + 1 = 2
; we updatef[1] = 2
. - For 'i' (
f[2]
), the sum becomess = 2 + 1 = 3
; we updatef[2] = 3
. - For 'o' (
f[3]
), the sum becomess = 3 + 1 = 4
; we updatef[3] = 4
. - For 'u' (
f[4]
), the sum becomess = 4 + 1 = 5
; we updatef[4] = 5
.
- For 'a' (
- Now,
f = [1, 2, 3, 4, 5]
after the first pass which accounts for strings of length 2.
- We start by initializing a cumulative sum
-
For
n = 3
:- Again, we initialize the cumulative sum
s = 0
. - We repeat the loop, updating
f
cumulatively:- For 'a', 's' becomes
s = 0 + 1 = 1
; 'a' can precede any vowels, so we keepf[0] = 1
. - For 'e', 's' becomes
s = 1 + 2 = 3
; 'e' can only be followed by 'e', 'i', 'o', 'u', sof[1] = 3
. - For 'i', 's' becomes
s = 3 + 3 = 6
; 'i' can only be followed by 'i', 'o', 'u', sof[2] = 6
. - For 'o', 's' becomes
s = 6 + 4 = 10
; 'o' can only be followed by 'o', 'u', sof[3] = 10
. - For 'u', 's' becomes
s = 10 + 5 = 15
; 'u' can only be followed by 'u', sof[4] = 15
.
- For 'a', 's' becomes
- Our array
f
is now[1, 3, 6, 10, 15]
.
- Again, we initialize the cumulative sum
-
-
Final Summation: After completing iteration for
n = 3
, we calculate the sum of all elements inf
to obtain the total count of sorted vowel strings of lengthn
:sum = 1 + 3 + 6 + 10 + 15 = 35
Therefore, the number of lexicographically sorted strings of length 3 that can be made using the vowels 'a', 'e', 'i', 'o', 'u' is 35
. This example demonstrates how the dynamic programming approach simplifies the counting process by breaking it down into manageable steps, building upon the result for each previous length to gradually find the total count for a string of length n
.
Solution Implementation
1class Solution:
2 def countVowelStrings(self, n: int) -> int:
3 # Initialize a list `vowel_count` with 5 elements all set to 1.
4 # This represents the count for each vowel: a, e, i, o, u
5 vowel_count = [1] * 5
6
7 # We loop over `n - 1` times because we already initialized the first set of counts.
8 for _ in range(n - 1):
9 # Initialize a running sum to build the counts for the current position.
10 running_sum = 0
11 # Loop through the counts for each vowel.
12 for j in range(5):
13 # Add the count for the current vowel to the running sum.
14 running_sum += vowel_count[j]
15 # Update the count for the current vowel with the running sum.
16 # This step is based on the dynamic programming principle, where
17 # the count of vowel strings ending with the current vowel is equal to
18 # all vowel strings ending with any vowel up to the current one.
19 vowel_count[j] = running_sum
20
21 # Return the sum of all counts, which represents all possible vowel strings of length `n`.
22 return sum(vowel_count)
23
1import java.util.Arrays; // Import the Arrays class for the sum method
2
3class Solution {
4
5 // This method counts the number of strings consisting of vowels that can be formed of length `n`.
6 public int countVowelStrings(int n) {
7 // An array to represent the counts of vowel strings for each vowel ending
8 // f[0] for 'a', f[1] for 'e', f[2] for 'i', f[3] for 'o', f[4] for 'u'
9 int[] vowelCounts = {1, 1, 1, 1, 1};
10
11 // Loop over the length n times, first length is already initialized, so we start from 0 to n-2
12 for (int i = 0; i < n - 1; ++i) {
13 // A temporary variable to keep track of the cumulative sum
14 int cumulativeSum = 0;
15 // This loop calculates the new counts by adding up counts from all previous vowels
16 for (int j = 0; j < 5; ++j) {
17 // Update cumulative sum with the current vowel count
18 cumulativeSum += vowelCounts[j];
19 // Update the count for the current vowel with the new cumulative sum
20 vowelCounts[j] = cumulativeSum;
21 }
22 }
23 // Sum up all the counts and return the result
24 return Arrays.stream(vowelCounts).sum();
25 }
26}
27
1#include <numeric> // Required for std::accumulate
2
3class Solution {
4public:
5 int countVowelStrings(int n) {
6 // Initialize an array with 5 elements corresponding to the 5 vowels
7 // and assign them the base case value, which is 1 for each vowel.
8 int vowelCounts[5] = {1, 1, 1, 1, 1};
9
10 // Run the loop for 'n - 1' times starting from 0 since the base case
11 // for the first element is already set.
12 for (int i = 0; i < n - 1; ++i) {
13 // Initialize the sum which holds the running number of strings
14 // ending with the current vowel.
15 int runningSum = 0;
16
17 // Iterate through the vowelCounts to update the counts for each vowel.
18 // To prevent the new updates for vowels as we progress through the arrays,
19 // we utilize the runningSum to keep track of the updated counts.
20 for (int j = 0; j < 5; ++j) {
21 // Add the current vowel's count to the running sum.
22 runningSum += vowelCounts[j];
23
24 // Update the count for the current vowel to be the running sum,
25 // which is effectively the count of vowel strings ending with the
26 // current vowel or before.
27 vowelCounts[j] = runningSum;
28 }
29 }
30
31 // Return the sum of all the vowel string counts which we have
32 // accumulated in our vowelCounts array for 'n' positions.
33 // std::accumulate performs a fold operation over the entire range
34 // [first, last) and the initial sum value of 0.
35 return std::accumulate(vowelCounts, vowelCounts + 5, 0);
36 }
37};
38
1/**
2 * Count the number of strings of length `n` composed of vowels (a, e, i, o, u)
3 * that are lexicographically sorted.
4 *
5 * @param {number} n - The length of the vowel strings to count.
6 * @returns {number} - The total count of lexicographically sorted vowel strings.
7 */
8function countVowelStrings(n: number): number {
9 // Initialize an array with 5 elements corresponding to the counts for a, e, i, o, u.
10 let vowelCounts: number[] = [1, 1, 1, 1, 1];
11
12 // Loop for 'n - 1' times since we start the counts with the base case of length 1.
13 for (let i = 0; i < n - 1; i++) {
14 // Initialize the running sum, which holds the cumulative counts.
15 let runningSum = 0;
16
17 // Update the counts for each position in the vowel string.
18 for (let j = 0; j < 5; j++) {
19 // Update the running sum with the current vowel count.
20 runningSum += vowelCounts[j];
21
22 // Assign the running sum as the new vowel count for the current vowel.
23 // This represents the total count of strings ending with this vowel or before.
24 vowelCounts[j] = runningSum;
25 }
26 }
27
28 // Calculate and return the total count of vowel strings of length `n`.
29 // Use the reduce function to sum all elements in `vowelCounts`.
30 return vowelCounts.reduce((acc, count) => acc + count, 0);
31}
32
Time and Space Complexity
The given Python code is used to count the number of strings of length n
that consist only of vowels (a
, e
, i
, o
, u
), where each string is lexicographically greater than the previous one (i.e., we can consider each vowel as a number, and these strings represent non-decreasing sequences).
Time Complexity
To analyze the time complexity, we observe the following steps in the code:
-
An initial list
f
is created with 5 elements, all of which are set to1
. This represents the number of vowel strings of length1
. -
The outer loop runs
(n - 1)
times which isO(n)
sincen
is the length of the string to be formed. -
Inside the outer loop, there is an inner loop that iterates over the 5 vowels.
-
For each vowel, it accumulates the values from the previous counts in the list
f
, effectively computing the cumulative sum.
Since there are 5 vowels, the inner loop runs at constant time, O(5)
, which simplifies to O(1)
. The cumulative sum in the inner loop does not alter the time complexity.
Thus, combining the complexity of the outer and inner loop, the final time complexity comes out to be O(n) * O(1) = O(n)
.
Space Complexity
The space complexity is determined by the storage utilized by the algorithm which remains constant irrespective of the input size:
-
A list
f
containing 5 elements is used to store the count of each type of vowel strings ending in a particular vowel, which takes up constant spaceO(5)
. -
The variable
s
is used to keep the cumulative sum within the inner loop. This is just an integer, which also uses constant spaceO(1)
.
Therefore, the space complexity of the algorithm is O(5) + O(1)
, which simplifies to O(1)
indicating constant space complexity.
Learn more about how to find time and space complexity quickly using problem constraints.
How would you design a stack which has a function min
that returns the minimum element in the stack, in addition to push
and pop
? All push
, pop
, min
should have running time O(1)
.
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!