903. Valid Permutations for DI Sequence
Problem Description
You are given a string s
of length n
consisting of characters 'D'
and 'I'
:
'D'
means the next number should be decreasing'I'
means the next number should be increasing
You need to find how many valid permutations exist for n + 1
integers from the range [0, n]
.
A permutation perm
is considered valid if:
- When
s[i] == 'D'
, thenperm[i] > perm[i + 1]
(decreasing) - When
s[i] == 'I'
, thenperm[i] < perm[i + 1]
(increasing)
For example, if s = "DID"
:
- The permutation has 4 numbers (from 0 to 3)
- Position 0 to 1 should be decreasing (D)
- Position 1 to 2 should be increasing (I)
- Position 2 to 3 should be decreasing (D)
- A valid permutation could be
[2, 0, 3, 1]
because2 > 0
(D),0 < 3
(I), and3 > 1
(D)
The task is to count the total number of valid permutations. Since this number can be very large, return the result modulo 10^9 + 7
.
The dynamic programming solution uses f[i][j]
to represent the number of valid permutations for the first i
characters where the element at position i
has rank j
among all elements up to position i
. The transitions depend on whether the current character is 'D'
or 'I'
:
- For
'D'
: The current element must be greater than the next, so we sum contributions from higher-ranked previous positions - For
'I'
: The current element must be less than the next, so we sum contributions from lower-ranked previous positions
Intuition
The key insight is to think about relative rankings rather than absolute values. When we place numbers in a permutation, what matters is not the specific values but their relative order.
Consider building the permutation step by step. At each position, we need to decide which "rank" the current number should have among all numbers placed so far. For example, if we've placed 3 numbers, the new number could be the smallest (rank 0), second smallest (rank 1), third smallest (rank 2), or largest (rank 3) among these 4 numbers.
Why does this ranking approach work? When we insert a new number into an existing valid sequence:
- If we need a decrease (
'D'
), the new number must be smaller than the previous one - If we need an increase (
'I'
), the new number must be larger than the previous one
The brilliant observation is that when we insert a new number with rank j
, all existing numbers with rank ≥ j
get their ranks shifted up by 1. This shift preserves the relative ordering between existing numbers while establishing the correct relationship with the new number.
For a 'D'
transition: If the previous number had rank k
and we insert a new number with rank j
where j < k
, then after insertion, the previous number's rank becomes k+1
. Since k+1 > j
, we maintain the decreasing property. This works when j ≤ k
.
For an 'I'
transition: If the previous number had rank k
and we insert a new number with rank j
where j > k
, then the previous number keeps rank k
. Since k < j
, we maintain the increasing property. This works when j > k
.
This leads us to define f[i][j]
as the number of ways to arrange the first i+1
numbers where the number at position i
has rank j
among these i+1
numbers. The transitions naturally follow:
- For
'D'
: Sum allf[i-1][k]
wherek ≥ j
(which becomesk ∈ [j, i-1]
after adjustment) - For
'I'
: Sum allf[i-1][k]
wherek < j
(which isk ∈ [0, j-1]
)
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
We implement the dynamic programming solution using a 2D array f[i][j]
where:
i
represents we've processed the firsti
characters of strings
j
represents the rank of the element at positioni
among alli+1
elements
Initialization:
- Create a 2D array
f
of size(n+1) × (n+1)
initialized with zeros - Set
f[0][0] = 1
as the base case (one element with rank 0)
State Transitions:
For each character s[i-1]
at position i
(1-indexed):
-
When
s[i-1] == 'D'
(Decreasing):- For each possible rank
j
of the current position (from 0 toi
) - Sum contributions from previous states where rank
k ∈ [j, i-1]
- Formula:
f[i][j] = Σ(f[i-1][k])
fork
fromj
toi-1
- This ensures the previous element (after rank adjustment) will be greater
- For each possible rank
-
When
s[i-1] == 'I'
(Increasing):- For each possible rank
j
of the current position (from 0 toi
) - Sum contributions from previous states where rank
k ∈ [0, j-1]
- Formula:
f[i][j] = Σ(f[i-1][k])
fork
from0
toj-1
- This ensures the previous element keeps a smaller rank
- For each possible rank
Implementation Details:
for i, c in enumerate(s, 1): # Process each character, i starts from 1
if c == "D":
for j in range(i + 1): # j can be from 0 to i
for k in range(j, i): # Sum from j to i-1
f[i][j] = (f[i][j] + f[i-1][k]) % mod
else: # c == "I"
for j in range(i + 1): # j can be from 0 to i
for k in range(j): # Sum from 0 to j-1
f[i][j] = (f[i][j] + f[i-1][k]) % mod
Final Answer:
- Sum all possible ranks at the final position:
Σ(f[n][j])
forj
from0
ton
- Apply modulo
10^9 + 7
to handle large numbers
Time Complexity: O(n³)
due to three nested loops
Space Complexity: O(n²)
for the 2D DP array
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with s = "DI"
:
We need to find valid permutations of [0, 1, 2]
(3 numbers for string length 2).
Step 1: Initialize DP table
- Create
f[3][3]
array (since we have n=2, we need n+1=3 rows) - Set
f[0][0] = 1
(base case: one number at rank 0)
Initial state:
f[0] = [1, 0, 0] f[1] = [0, 0, 0] f[2] = [0, 0, 0]
Step 2: Process first character 'D' (i=1)
For 'D', we need decreasing transition. For each rank j
at position 1:
j=0
: Sumf[0][k]
for k ∈ [0, 0] →f[1][0] = f[0][0] = 1
j=1
: Sumf[0][k]
for k ∈ [1, 0] → No valid k, sof[1][1] = 0
After processing 'D':
f[0] = [1, 0, 0] f[1] = [1, 0, 0] f[2] = [0, 0, 0]
This means after placing 2 numbers with 'D' constraint, we have 1 way where the second number has rank 0 (is the smaller of the two).
Step 3: Process second character 'I' (i=2)
For 'I', we need increasing transition. For each rank j
at position 2:
j=0
: Sumf[1][k]
for k ∈ [0, -1] → No valid k, sof[2][0] = 0
j=1
: Sumf[1][k]
for k ∈ [0, 0] →f[2][1] = f[1][0] = 1
j=2
: Sumf[1][k]
for k ∈ [0, 1] →f[2][2] = f[1][0] + f[1][1] = 1 + 0 = 1
Final state:
f[0] = [1, 0, 0] f[1] = [1, 0, 0] f[2] = [0, 1, 1]
Step 4: Calculate final answer
Sum all values in the last row: f[2][0] + f[2][1] + f[2][2] = 0 + 1 + 1 = 2
Verification: The 2 valid permutations for "DI" are:
[2, 0, 1]
: 2 > 0 (D), 0 < 1 (I) ✓[1, 0, 2]
: 1 > 0 (D), 0 < 2 (I) ✓
Our DP solution correctly found 2 valid permutations!
Understanding the rank interpretation:
- When
f[2][1] = 1
: This represents permutations where the last element has rank 1 (middle value) among all 3 elements - When
f[2][2] = 1
: This represents permutations where the last element has rank 2 (largest value) among all 3 elements - Both contribute to valid permutations satisfying the "DI" pattern
Solution Implementation
1class Solution:
2 def numPermsDISequence(self, s: str) -> int:
3 MOD = 10**9 + 7
4 n = len(s)
5
6 # dp[i][j] represents the number of valid permutations of length i+1
7 # where the last element has rank j among all i+1 elements
8 # (rank 0 means smallest, rank i means largest)
9 dp = [[0] * (n + 2) for _ in range(n + 2)]
10
11 # Base case: one way to have a single element with rank 0
12 dp[0][0] = 1
13
14 # Process each character in the DI sequence
15 for i, char in enumerate(s, start=1):
16 if char == "D":
17 # For 'D' (decrease): current element must be smaller than previous
18 # If previous had rank k among i elements, and current has rank j among i+1 elements,
19 # then we need j <= k (since adding current element shifts ranks)
20 for j in range(i + 1):
21 for k in range(j, i):
22 dp[i][j] = (dp[i][j] + dp[i - 1][k]) % MOD
23 else: # char == "I"
24 # For 'I' (increase): current element must be larger than previous
25 # If previous had rank k among i elements, and current has rank j among i+1 elements,
26 # then we need j > k
27 for j in range(i + 1):
28 for k in range(j):
29 dp[i][j] = (dp[i][j] + dp[i - 1][k]) % MOD
30
31 # Sum all valid permutations of length n+1 regardless of the last element's rank
32 total = sum(dp[n][j] for j in range(n + 1)) % MOD
33 return total
34
1class Solution {
2 public int numPermsDISequence(String s) {
3 final int MOD = 1_000_000_007;
4 int n = s.length();
5
6 // dp[i][j] represents the number of valid permutations of length i+1
7 // where the last element has rank j among all i+1 elements
8 // (rank 0 means smallest, rank i means largest)
9 int[][] dp = new int[n + 1][n + 1];
10
11 // Base case: one way to arrange a single element (at position 0, rank 0)
12 dp[0][0] = 1;
13
14 // Build up the DP table for each position
15 for (int position = 1; position <= n; position++) {
16 char constraint = s.charAt(position - 1);
17
18 if (constraint == 'D') {
19 // 'D' means decreasing: previous element must be larger
20 // So we need to sum from ranks >= current rank in previous position
21 for (int currentRank = 0; currentRank <= position; currentRank++) {
22 // Previous rank must be >= currentRank (after adjustment)
23 // In the previous position with (position-1) elements,
24 // ranks range from 0 to position-1
25 for (int previousRank = currentRank; previousRank < position; previousRank++) {
26 dp[position][currentRank] = (dp[position][currentRank] +
27 dp[position - 1][previousRank]) % MOD;
28 }
29 }
30 } else {
31 // 'I' means increasing: previous element must be smaller
32 // So we need to sum from ranks < current rank in previous position
33 for (int currentRank = 0; currentRank <= position; currentRank++) {
34 // Previous rank must be < currentRank (after adjustment)
35 for (int previousRank = 0; previousRank < currentRank; previousRank++) {
36 dp[position][currentRank] = (dp[position][currentRank] +
37 dp[position - 1][previousRank]) % MOD;
38 }
39 }
40 }
41 }
42
43 // Sum up all possible ending ranks for the complete permutation
44 int totalPermutations = 0;
45 for (int finalRank = 0; finalRank <= n; finalRank++) {
46 totalPermutations = (totalPermutations + dp[n][finalRank]) % MOD;
47 }
48
49 return totalPermutations;
50 }
51}
52
1class Solution {
2public:
3 int numPermsDISequence(string s) {
4 const int MOD = 1e9 + 7;
5 int n = s.size();
6
7 // dp[i][j] represents the number of valid permutations of length i+1
8 // where the last element has rank j among all i+1 elements
9 // (rank 0 means smallest, rank i means largest)
10 vector<vector<int>> dp(n + 1, vector<int>(n + 1, 0));
11
12 // Base case: single element permutation has only one way
13 dp[0][0] = 1;
14
15 // Process each position in the DI sequence
16 for (int i = 1; i <= n; ++i) {
17 // Check if we need a decrease or increase at position i-1
18 if (s[i - 1] == 'D') {
19 // For 'D' (decrease): the new element must be smaller than previous
20 // If new element has rank j, previous element must have had rank >= j
21 for (int j = 0; j <= i; ++j) {
22 for (int k = j; k < i; ++k) {
23 dp[i][j] = (dp[i][j] + dp[i - 1][k]) % MOD;
24 }
25 }
26 } else {
27 // For 'I' (increase): the new element must be larger than previous
28 // If new element has rank j, previous element must have had rank < j
29 for (int j = 0; j <= i; ++j) {
30 for (int k = 0; k < j; ++k) {
31 dp[i][j] = (dp[i][j] + dp[i - 1][k]) % MOD;
32 }
33 }
34 }
35 }
36
37 // Sum up all valid permutations ending at any rank
38 int result = 0;
39 for (int j = 0; j <= n; ++j) {
40 result = (result + dp[n][j]) % MOD;
41 }
42
43 return result;
44 }
45};
46
1/**
2 * Counts the number of valid permutations based on a DI sequence pattern
3 * @param s - String containing 'D' (decrease) and 'I' (increase) characters
4 * @returns Number of valid permutations modulo 10^9 + 7
5 */
6function numPermsDISequence(s: string): number {
7 const sequenceLength: number = s.length;
8 const MOD: number = 10 ** 9 + 7;
9
10 // dp[i][j] represents the number of permutations of length i+1
11 // where the last element has rank j among all i+1 elements
12 const dp: number[][] = Array(sequenceLength + 1)
13 .fill(0)
14 .map(() => Array(sequenceLength + 1).fill(0));
15
16 // Base case: one way to arrange a single element
17 dp[0][0] = 1;
18
19 // Fill the DP table based on the DI sequence
20 for (let position = 1; position <= sequenceLength; position++) {
21 const currentChar: string = s[position - 1];
22
23 if (currentChar === 'D') {
24 // For 'D' (decrease): new element must be smaller than previous
25 // Sum contributions from all valid previous ranks
26 for (let currentRank = 0; currentRank <= position; currentRank++) {
27 for (let previousRank = currentRank; previousRank < position; previousRank++) {
28 dp[position][currentRank] = (dp[position][currentRank] + dp[position - 1][previousRank]) % MOD;
29 }
30 }
31 } else {
32 // For 'I' (increase): new element must be larger than previous
33 // Sum contributions from all valid previous ranks
34 for (let currentRank = 0; currentRank <= position; currentRank++) {
35 for (let previousRank = 0; previousRank < currentRank; previousRank++) {
36 dp[position][currentRank] = (dp[position][currentRank] + dp[position - 1][previousRank]) % MOD;
37 }
38 }
39 }
40 }
41
42 // Sum all valid permutations at the final position
43 let totalPermutations: number = 0;
44 for (let rank = 0; rank <= sequenceLength; rank++) {
45 totalPermutations = (totalPermutations + dp[sequenceLength][rank]) % MOD;
46 }
47
48 return totalPermutations;
49}
50
Time and Space Complexity
The time complexity is O(n³)
, where n
is the length of the string s
.
This is because:
- The outer loop iterates through each character in string
s
, runningn
times - For each iteration, we have nested loops with indices
j
andk
- The
j
loop runs from0
toi
, which can be at mostn+1
iterations - The inner
k
loop runs either fromj
toi
(for "D") or from0
toj-1
(for "I"), contributing at mostO(n)
iterations - Overall:
O(n) × O(n) × O(n) = O(n³)
The space complexity is O(n²)
.
This is because:
- We create a 2D array
f
with dimensions(n+1) × (n+1)
- This requires
O((n+1)²) = O(n²)
space - The additional variables use only
O(1)
space - Therefore, the total space complexity is
O(n²)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Confusion About Rank Interpretation
The most common pitfall is misunderstanding what "rank" means in the DP state. Many developers incorrectly interpret dp[i][j]
as the actual value at position i
rather than its relative rank among the first i+1
elements.
Wrong Understanding:
- Thinking
j
represents the actual number (0, 1, 2, ..., n) at positioni
- This leads to incorrect state transitions since the actual values don't directly determine the validity
Correct Understanding:
j
represents the rank (0-indexed) of the element at positioni
when considering only the firsti+1
positions- Rank 0 means smallest among those
i+1
elements, ranki
means largest
2. Incorrect Range Boundaries in Transitions
Another critical error is using wrong loop boundaries when accumulating from previous states.
Common Mistakes:
# WRONG: Off-by-one errors
if char == "D":
for k in range(j + 1, i + 1): # Should be range(j, i)
dp[i][j] += dp[i-1][k]
# WRONG: Including invalid indices
if char == "I":
for k in range(0, j): # This is correct, but often written as range(0, j+1)
dp[i][j] += dp[i-1][k]
Solution: Always remember:
- For 'D': Sum from
k = j
tok = i-1
(inclusive) - For 'I': Sum from
k = 0
tok = j-1
(inclusive)
3. Forgetting Modulo Operations
Since the result can be very large, forgetting to apply modulo at each step can cause integer overflow in languages with fixed integer sizes, or incorrect results due to precision issues.
Wrong:
dp[i][j] = dp[i][j] + dp[i-1][k] # Missing modulo
Correct:
dp[i][j] = (dp[i][j] + dp[i-1][k]) % MOD
4. Space Optimization Pitfall
When attempting to optimize space from O(n²) to O(n) using rolling arrays, a common mistake is updating values in-place without preserving the previous row's values properly.
Wrong Space Optimization:
# Using single array incorrectly
dp = [0] * (n + 2)
dp[0] = 1
for i, char in enumerate(s, 1):
for j in range(i + 1):
# This modifies dp while still reading from it!
if char == "D":
dp[j] = sum(dp[k] for k in range(j, i)) % MOD
Correct Space Optimization:
# Use two arrays and swap
prev = [0] * (n + 2)
curr = [0] * (n + 2)
prev[0] = 1
for i, char in enumerate(s, 1):
curr = [0] * (n + 2) # Reset current row
if char == "D":
for j in range(i + 1):
for k in range(j, i):
curr[j] = (curr[j] + prev[k]) % MOD
else:
for j in range(i + 1):
for k in range(j):
curr[j] = (curr[j] + prev[k]) % MOD
prev, curr = curr, prev # Swap for next iteration
result = sum(prev[j] for j in range(n + 1)) % MOD
5. Index Confusion Between String and DP Array
The string s
has length n
, but we're creating permutations of n+1
elements. This off-by-one relationship often causes indexing errors.
Key Points to Remember:
- String
s
has indices from0
ton-1
- DP array
dp[i]
represents states after processingi
characters (soi
goes from0
ton
) - When at
dp[i]
, we look ats[i-1]
to determine the transition - The final answer sums
dp[n][j]
for all validj
Which data structure is used in a depth first search?
Recommended Readings
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
Prefix Sum The prefix sum is an incredibly powerful and straightforward technique Its primary goal is to allow for constant time range sum queries on an array What is Prefix Sum The prefix sum of an array at index i is the sum of all numbers from index 0 to i By
Coding Interview Patterns Your Personal Dijkstra's Algorithm to Landing Your Dream Job The goal of AlgoMonster is to help you get a job in the shortest amount of time possible in a data driven way We compiled datasets of tech interview problems and broke them down by patterns This way
Want a Structured Path to Master System Design Too? Don’t Miss This!