1220. Count Vowels Permutation - Explanation

Problem Link



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

  1. Define the transition rules mapping each vowel to its valid successors.
  2. Create a recursive function that takes the current position and current vowel.
  3. Base case: if position equals n, return 1 (found a valid string).
  4. For each valid successor of the current vowel, recursively count strings.
  5. Sum up the counts from all successors.
  6. Start the recursion from position 1 with each of the 5 vowels.
  7. 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 res

Time & Space Complexity

  • Time complexity: O(4n)O(4 ^ n)
  • Space complexity: O(n)O(n) 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

  1. Define the transition rules mapping each vowel to its valid successors.
  2. Create a memoization cache (2D array or hash map) for states (position, vowel).
  3. Create a recursive function that:
    • Returns 1 if position equals n.
    • Returns cached result if the state was computed before.
    • Otherwise, computes the sum of counts from all valid successors and caches it.
  4. Start the recursion from position 1 with each of the 5 vowels.
  5. 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 res

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Create a 2D DP array of size (n+1) x 5.
  2. Initialize dp[1][v] = 1 for all vowels (there is one string of length 1 for each vowel).
  3. For each length i from 2 to n:
    • For each vowel v, sum up dp[i-1][prev] for all vowels prev that can precede v.
    • Note: we need to reverse the transition rules (find what can come before, not after).
  4. Sum dp[n][v] for all vowels v.
  5. 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]) % MOD

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(n)O(n)

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

  1. Initialize an array dp of size 5, all set to 1 (representing strings of length 1).
  2. For each length from 2 to n:
    • Create a new array next_dp of size 5.
    • For each vowel v, compute next_dp[v] as the sum of dp[prev] for all valid predecessors.
    • Replace dp with next_dp.
  3. Sum all values in dp.
  4. 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) % MOD

Time & Space Complexity

  • Time complexity: O(n)O(n)
  • Space complexity: O(1)O(1) since we used array of size 55.

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

  1. Define the 5x5 transition matrix based on the vowel succession rules.
  2. Implement matrix multiplication for 5x5 matrices.
  3. Implement matrix exponentiation using binary exponentiation.
  4. Compute T^(n-1) where T is the transition matrix.
  5. The result is the sum of all elements in the resulting matrix (since we start with 1 of each vowel).
  6. 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.MOD

Time & Space Complexity

  • Time complexity: O(m3logn)O(m ^ 3 \log n)
  • Space complexity: O(m2)O(m ^ 2)

Where mm is the size of the matrix used in matrix exponentiation (5X5)(5 X 5) and nn 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 total

Incorrect 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;