Want Help Cracking FAANG?

(Then click this)

×
Back to Question Bank

903. Valid Permutations for DI Sequence - Leetcode Solution

Problem Description

Given a string S consisting only of the characters 'D' (for "decrease") and 'I' (for "increase"), you are to return the number of valid permutations of the integers 0 to n (where n = len(S)) such that for each character S[i]:

  • If S[i] == 'I', then the i-th element of the permutation is less than the (i+1)-th element.
  • If S[i] == 'D', then the i-th element of the permutation is greater than the (i+1)-th element.

Each permutation must use every number from 0 to n exactly once. Return the total number of valid permutations modulo 10^9 + 7.

Constraints:

  • 1 <= S.length <= 200
  • Each character in S is either 'I' or 'D'

Thought Process

At first glance, this problem looks like a permutation generation task with constraints, which might suggest generating all possible permutations and filtering them. However, with n up to 200, this approach is computationally impossible, as the number of permutations grows factorially.

Instead, we can think recursively: for each position, we want to know how many ways we can fill it, given the previous choices and the current 'I' or 'D' requirement. This leads us to dynamic programming, where we keep track of the number of valid ways to fill the first i positions ending with a certain number.

By building up solutions for smaller subproblems and reusing them, we can avoid redundant work and solve the problem efficiently.

Solution Approach

We use dynamic programming (DP) to efficiently count the number of valid permutations. Here's how:

  1. Define DP State:
    • Let dp[i][j] represent the number of ways to arrange the first i numbers, ending with the j-th smallest unused number.
  2. Initialization:
    • At the start (when i = 0), there is only one way to pick the first number, so dp[0][j] = 1 for all j in 0...n.
  3. Transition:
    • For each position i and for each possible j (which represents the count of smaller numbers left), update dp[i][j] based on whether S[i-1] is 'I' or 'D'.
    • If S[i-1] == 'I', then the current number must be greater than the previous one, so sum up dp[i-1][k] for k = 0 to j-1.
    • If S[i-1] == 'D', then the current number must be less, so sum up dp[i-1][k] for k = j to i-1.
  4. Result:
    • The answer is the sum of all dp[n][j] for j from 0 to n.
  5. Optimization:
    • We can use prefix sums to speed up the summation in the transitions.
    • We only need two rows for DP at a time, reducing memory usage.

Example Walkthrough

Let's take S = "DID" as an example. Here, n = 3, so we use numbers from 0 to 3.

  1. Initialization:
    • At position 0, dp[0][j] = 1 for j = 0,1,2,3.
  2. First character 'D':
    • For each j, dp[1][j] = sum(dp[0][k]) for k = j to 0 (since we want a decrease).
    • So, dp[1][0] = dp[0][0]+dp[0][1]+dp[0][2]+dp[0][3]=4, dp[1][1]=3, dp[1][2]=2, dp[1][3]=1.
  3. Second character 'I':
    • For each j, dp[2][j] = sum(dp[1][k]) for k = 0 to j-1 (since we want an increase).
    • Calculate each dp[2][j] using the prefix sum of previous row.
  4. Continue for all positions.
  5. Sum all dp[3][j] to get the answer.

In this example, the answer is 5.

Code Implementation

MOD = 10 ** 9 + 7

class Solution:
    def numPermsDISequence(self, S: str) -> int:
        n = len(S)
        dp = [1] * (n + 1)
        for i in range(1, n + 1):
            new = [0] * (n + 1)
            if S[i - 1] == 'I':
                curr = 0
                for j in range(i):
                    curr = (curr + dp[j]) % MOD
                    new[j] = curr
            else:  # 'D'
                curr = 0
                for j in range(i - 1, -1, -1):
                    curr = (curr + dp[j + 1]) % MOD
                    new[j] = curr
            dp = new
        return sum(dp) % MOD
      
#include <vector>
#include <string>
using namespace std;

class Solution {
public:
    int numPermsDISequence(string S) {
        const int MOD = 1e9 + 7;
        int n = S.size();
        vector<int> dp(n + 1, 1);
        for (int i = 1; i <= n; ++i) {
            vector<int> new_dp(n + 1, 0);
            if (S[i - 1] == 'I') {
                int curr = 0;
                for (int j = 0; j < i; ++j) {
                    curr = (curr + dp[j]) % MOD;
                    new_dp[j] = curr;
                }
            } else {
                int curr = 0;
                for (int j = i - 1; j >= 0; --j) {
                    curr = (curr + dp[j + 1]) % MOD;
                    new_dp[j] = curr;
                }
            }
            dp = new_dp;
        }
        int ans = 0;
        for (int x : dp) ans = (ans + x) % MOD;
        return ans;
    }
};
      
class Solution {
    public int numPermsDISequence(String S) {
        int MOD = 1000000007;
        int n = S.length();
        int[] dp = new int[n + 1];
        for (int i = 0; i <= n; ++i) dp[i] = 1;
        for (int i = 1; i <= n; ++i) {
            int[] newDp = new int[n + 1];
            if (S.charAt(i - 1) == 'I') {
                int curr = 0;
                for (int j = 0; j < i; ++j) {
                    curr = (curr + dp[j]) % MOD;
                    newDp[j] = curr;
                }
            } else {
                int curr = 0;
                for (int j = i - 1; j >= 0; --j) {
                    curr = (curr + dp[j + 1]) % MOD;
                    newDp[j] = curr;
                }
            }
            dp = newDp;
        }
        int ans = 0;
        for (int x : dp) ans = (ans + x) % MOD;
        return ans;
    }
}
      
var numPermsDISequence = function(S) {
    const MOD = 1e9 + 7;
    const n = S.length;
    let dp = new Array(n + 1).fill(1);
    for (let i = 1; i <= n; ++i) {
        let newDp = new Array(n + 1).fill(0);
        if (S[i - 1] === 'I') {
            let curr = 0;
            for (let j = 0; j < i; ++j) {
                curr = (curr + dp[j]) % MOD;
                newDp[j] = curr;
            }
        } else {
            let curr = 0;
            for (let j = i - 1; j >= 0; --j) {
                curr = (curr + dp[j + 1]) % MOD;
                newDp[j] = curr;
            }
        }
        dp = newDp;
    }
    return dp.reduce((a, b) => (a + b) % MOD, 0);
};
      

Time and Space Complexity

  • Brute-force approach:
    • Would generate all (n+1)! permutations, checking each for validity, which is infeasible for large n.
    • Time Complexity: O((n+1)!), Space Complexity: O(n) for recursion stack.
  • Optimized DP approach:
    • We fill a DP table of size (n+1) x (n+1), but with rolling arrays, we use only O(n) space.
    • Each DP update for i involves O(i) work, so total time is O(n^2).
    • Space Complexity: O(n) (with rolling DP rows).

Summary

The key insight for solving the "Valid Permutations for DI Sequence" problem is to model the constraints using dynamic programming, breaking the problem into subproblems based on the current state and previous choices. By using prefix sums and a compact DP structure, we efficiently count the number of valid permutations without generating them all. This approach is both elegant and practical, scaling easily to the problem's constraints.