1220. Count Vowels Permutation - Explanation
Prerequisites
Before attempting this problem, you should be comfortable with:
- Dynamic Programming - Breaking down problems into overlapping subproblems with memoization or tabulation
- State Transitions - Understanding how to model transitions between different states (vowels in this case)
- Modular Arithmetic - Handling large numbers with modulo operations to prevent overflow
- Matrix Exponentiation (Optional) - Using matrix multiplication for O(log n) solutions to linear recurrences
1. Recursion
Intuition
We need to count strings of length n where each vowel can only be followed by specific vowels. The rules are: 'a' can be followed by 'e'; 'e' can be followed by 'a' or 'i'; 'i' can be followed by any vowel except itself; 'o' can be followed by 'i' or 'u'; 'u' can be followed by 'a'. We can solve this by exploring all valid paths using recursion, starting from each vowel.
Algorithm
- Define the transition rules mapping each vowel to its valid successors.
- Create a recursive function that takes the current position and current vowel.
- Base case: if position equals
n, return1(found a valid string). - For each valid successor of the current vowel, recursively count strings.
- Sum up the counts from all successors.
- Start the recursion from position
1with each of the 5 vowels. - Return the total count modulo
10^9 + 7.
class Solution:
def countVowelPermutation(self, n: int) -> int:
MOD = 10**9 + 7
follows = {
'a': ['e'],
'e': ['a', 'i'],
'i': ['a', 'e', 'o', 'u'],
'o': ['i', 'u'],
'u': ['a']
}
def dfs(i, v):
if i == n:
return 1
total = 0
for nxt in follows[v]:
total = (total + dfs(i + 1, nxt)) % MOD
return total
res = 0
for vowel in ['a', 'e', 'i', 'o', 'u']:
res = (res + dfs(1, vowel)) % MOD
return resTime & Space Complexity
- Time complexity:
- Space complexity: for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The plain recursion recomputes the same subproblems many times. For example, counting strings starting with 'e' at position 5 is computed multiple times. We can add memoization to cache results for each (position, vowel) pair. This reduces the time complexity dramatically since there are only O(n * 5) unique states.
Algorithm
- Define the transition rules mapping each vowel to its valid successors.
- Create a memoization cache (2D array or hash map) for states
(position, vowel). - Create a recursive function that:
- Returns
1if position equalsn. - Returns cached result if the state was computed before.
- Otherwise, computes the sum of counts from all valid successors and caches it.
- Returns
- Start the recursion from position
1with each of the 5 vowels. - Return the total count modulo
10^9 + 7.
class Solution:
def countVowelPermutation(self, n: int) -> int:
MOD = 10**9 + 7
cache = {}
follows = {
'a': ['e'],
'e': ['a', 'i'],
'i': ['a', 'e', 'o', 'u'],
'o': ['i', 'u'],
'u': ['a']
}
def dfs(i, v):
if i == n:
return 1
if (i, v) in cache:
return cache[(i, v)]
total = 0
for nxt in follows[v]:
total = (total + dfs(i + 1, nxt)) % MOD
cache[(i, v)] = total
return total
res = 0
for vowel in ['a', 'e', 'i', 'o', 'u']:
res = (res + dfs(1, vowel)) % MOD
return resTime & Space Complexity
- Time complexity:
- Space complexity:
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of recursion with memoization, we can build the solution iteratively from the base case. We use a 2D DP table where dp[i][v] represents the count of valid strings of length i ending with vowel v. We start with length 1 (each vowel has count 1) and build up to length n by considering which vowels can precede each vowel.
Algorithm
- Create a 2D DP array of size
(n+1) x 5. - Initialize
dp[1][v] = 1for all vowels (there is one string of length1for each vowel). - For each length
ifrom2ton:- For each vowel
v, sum updp[i-1][prev]for all vowelsprevthat can precedev. - Note: we need to reverse the transition rules (find what can come before, not after).
- For each vowel
- Sum
dp[n][v]for all vowelsv. - Return the result modulo
10^9 + 7.
class Solution:
def countVowelPermutation(self, n: int) -> int:
MOD = 10**9 + 7
a, e, i, o, u = 0, 1, 2, 3, 4
dp = [[0] * 5 for _ in range(n + 1)]
dp[1] = [1, 1, 1, 1, 1]
for j in range(2, n + 1):
dp[j][a] = (dp[j - 1][e] + dp[j - 1][i] + dp[j - 1][u]) % MOD
dp[j][e] = (dp[j - 1][a] + dp[j - 1][i]) % MOD
dp[j][i] = (dp[j - 1][e] + dp[j - 1][o]) % MOD
dp[j][o] = dp[j - 1][i] % MOD
dp[j][u] = (dp[j - 1][i] + dp[j - 1][o]) % MOD
return sum(dp[n]) % MODTime & Space Complexity
- Time complexity:
- Space complexity:
4. Dynamic Programming (Space Optimized)
Intuition
In the bottom-up approach, we only need the previous row to compute the current row. This means we can reduce space from O(n) to O(1) by using just two arrays of size 5 (or even one array with careful updates). We alternate between the current and previous state arrays.
Algorithm
- Initialize an array
dpof size 5, all set to1(representing strings of length1). - For each length from
2ton:- Create a new array
next_dpof size 5. - For each vowel
v, computenext_dp[v]as the sum ofdp[prev]for all valid predecessors. - Replace
dpwithnext_dp.
- Create a new array
- Sum all values in
dp. - Return the result modulo
10^9 + 7.
class Solution:
def countVowelPermutation(self, n: int) -> int:
MOD = 10**9 + 7
follows = [
[1], # 'a' -> 'e'
[0, 2], # 'e' -> 'a', 'i'
[0, 1, 3, 4], # 'i' -> 'a', 'e', 'o', 'u'
[2, 4], # 'o' -> 'i', 'u'
[0] # 'u' -> 'a'
]
dp = [1] * 5
for _ in range(2, n + 1):
next_dp = [0] * 5
for v in range(5):
for nextV in follows[v]:
next_dp[v] = (next_dp[v] + dp[nextV]) % MOD
dp = next_dp
return sum(dp) % MODTime & Space Complexity
- Time complexity:
- Space complexity: since we used array of size .
5. Matrix Exponentiation
Intuition
The transition between states can be represented as a matrix multiplication. If we define a transition matrix T where T[i][j] = 1 if vowel j can follow vowel i, then multiplying the state vector by T gives the next state. To get the state after n-1 transitions, we compute T^(n-1). Matrix exponentiation allows us to compute this in O(log n) time instead of O(n).
Algorithm
- Define the 5x5 transition matrix based on the vowel succession rules.
- Implement matrix multiplication for 5x5 matrices.
- Implement matrix exponentiation using binary exponentiation.
- Compute
T^(n-1)whereTis the transition matrix. - The result is the sum of all elements in the resulting matrix (since we start with
1of each vowel). - Return the result modulo
10^9 + 7.
class Solution:
MOD = 10**9 + 7
class M:
def __init__(self, n):
self.a = [[0] * n for _ in range(n)]
def __mul__(self, other):
n = len(self.a)
product = Solution.M(n)
for i in range(n):
for j in range(n):
for k in range(n):
product.a[i][k] = (product.a[i][k] + self.a[i][j] * other.a[j][k]) % Solution.MOD
return product
def matrixExpo(self, base, exp):
n = len(base.a)
result = Solution.M(n)
for i in range(n):
result.a[i][i] = 1
while exp > 0:
if exp % 2 == 1:
result = result * base
base = base * base
exp //= 2
return result
def countVowelPermutation(self, n: int) -> int:
follows = [
[0, 1, 0, 0, 0], # 'a' -> 'e'
[1, 0, 1, 0, 0], # 'e' -> 'a', 'i'
[1, 1, 0, 1, 1], # 'i' -> 'a', 'e', 'o', 'u'
[0, 0, 1, 0, 1], # 'o' -> 'i', 'u'
[1, 0, 0, 0, 0] # 'u' -> 'a'
]
base = Solution.M(5)
base.a = follows
result = self.matrixExpo(base, n - 1)
return sum(sum(row) for row in result.a) % self.MODTime & Space Complexity
- Time complexity:
- Space complexity:
Where is the size of the matrix used in matrix exponentiation and is the length of the permutation.
Common Pitfalls
Confusing "Follows" vs "Precedes" Transitions
The problem states which vowels can follow each vowel, but the DP recurrence requires knowing which vowels can precede each vowel. Mixing up the direction leads to wrong transition rules.
# Incorrect - using "follows" direction for DP[i][v]
# This computes what can come AFTER v, not what can come BEFORE v
dp[i][a] = dp[i-1][a] # wrong: 'a' follows 'a'?
# Correct - 'a' can be preceded by 'e', 'i', or 'u'
dp[i][a] = dp[i-1][e] + dp[i-1][i] + dp[i-1][u]Forgetting Modulo in Intermediate Sums
With large n, intermediate sums overflow before taking the final modulo. Apply modulo after each addition to keep values within bounds.
# Incorrect - sum overflows before modulo
total = 0
for next in follows[v]:
total += dfs(i + 1, next)
return total % MOD
# Correct - apply modulo after each addition
total = 0
for next in follows[v]:
total = (total + dfs(i + 1, next)) % MOD
return totalIncorrect Matrix Exponentiation Base Case
When using matrix exponentiation, computing T^n instead of T^(n-1) is a common error. The initial state already represents strings of length 1, so only n-1 transitions are needed.
# Incorrect - one extra transition
result = matrix_expo(base, n)
# Correct - n-1 transitions from length 1 to length n
result = matrix_expo(base, n - 1)Not Handling n=1 Edge Case
For n=1, the answer is simply 5 (one for each vowel). Some implementations with matrix exponentiation or DP loops may fail if they do not handle this boundary correctly.
# Incorrect - matrix_expo with exp=0 may not return identity
def countVowelPermutation(n):
return sum_matrix(matrix_expo(base, n - 1)) # fails if n=1 and expo not handled
# Correct - either handle n=1 explicitly or ensure expo(M, 0) returns identity
def countVowelPermutation(n):
if n == 1:
return 5
return sum_matrix(matrix_expo(base, n - 1))Integer Overflow in Matrix Multiplication
In languages without arbitrary precision integers (like Java, C++), multiplying two large values before taking modulo causes overflow. Cast to a larger type or restructure the multiplication.
// Incorrect - integer overflow before modulo
product[i][k] = (product[i][k] + a[i][j] * b[j][k]) % MOD;
// Correct - use long long to prevent overflow
product[i][k] = (product[i][k] + (long long)a[i][j] * b[j][k]) % MOD;