903. Valid Permutations for DI Sequence
Problem Description
Given a string s
of length n
, which consists of characters 'D' and 'I' representing decreasing and increasing respectively, we are tasked with generating a permutation perm
of integers from 0
to n
. This permutation must satisfy the condition where 'D' at index i
means perm[i]
should be greater than perm[i + 1]
, and 'I' means perm[i]
should be less than perm[i + 1]
. The goal is to find the total number of such valid permutations and return this number modulo 10^9 + 7
since the result could be very large.
Intuition
To solve this problem, we need to leverage dynamic programming (DP) because the straightforward backtracking approach would be too slow due to its exponential time complexity.
The intuition behind this DP approach is to keep track of the number of valid sequences at each step while building the permutation from left to right according to the given 'D' and 'I' constraints. We use an auxiliary array f
to represent the number of valid permutations ending with each possible number at each step.
We iterate over the input string s
, and for each character, we update the DP array based on whether it's 'D' or 'I'. For 'D', we want to accumulate the counts in reverse since we're adding a smaller number before a larger one. For 'I', we accumulate counts forward since we're adding a larger number after the smaller one. This leads us to construct a new array g
at each step, representing the new state of the DP.
At each step, g
gets the cumulative sum of values from f
depending on the 'D' or 'I' condition. By the end of the s
traversal, the f
array holds the counts of valid permutations ending with each number. The total number of distinct valid permutations would be the sum of all values in this DP array f
. We return this sum modulo 10^9 + 7
.
Learn more about Dynamic Programming and Prefix Sum patterns.
Solution Approach
The given Python solution employs dynamic programming. Let's elaborate on its implementation step by step:
-
mod = 10**9 + 7
ensures that we take the remainder after division with10^9 + 7
, satisfying the problem's requirement to return results under this modulo to handle large numbers. -
The list
f
is initialized with1
followed byn
zeros:f = [1] + [0] * n
. The1
represents the number of valid permutations when the permutation length is 0 (which is 1 since there's only an empty permutation), and the zeros act as placeholders for the future steps. -
The
for
loop over the enumerated strings
is the core of the dynamic programming:-
pre
is initialized to zero. It serves as a prefix sum that gets updated while iterating through the positions where a number could be placed. -
A temporary list
g
is initialized with zeros. It'll be populated with the number of valid permutations ending at each index for the current length of the permutation sequence. -
Inside this loop, depending on whether the current character
c
is 'D' or 'I', we iterate through the listf
to update the prefix sums.-
If
c
is "D": We iterate backward because we want to place a number that's smaller than the preceding one. As we iterate, we updatepre
by addingf[j]
modulomod
and assignpre
tog[j]
. -
If
c
is "I": We iterate forward because we want to place a number larger than the previous one. Here,g[j]
is assigned the value ofpre
, and thenpre
is updated to its value plusf[j]
, also modulomod
.
-
-
-
After processing either 'D' or 'I' for all positions, we replace
f
with the new stateg
. This assignment progresses our dynamic programming to the next step, wheref
now represents the state of the DP for sequences of lengthi + 1
. -
Once we finish going through the string
s
,f
will contain the number of ways to arrange permutations ending with each possible number for a sequence of lengthn
. This is because each iteration effectively builds upon the previous length, considering whether we're adding a number in a decremented ('D') or incremented ('I') fashion. -
The final result is the sum of all counts in
f
modulomod
, which gives us the total count of valid permutations that can be formed according to the input pattern strings
.
The use of a rolling DP array f
that gets updated at each step with g
and the accumulation of counts in forward or backward fashion depending on 'I' or 'D' characters are the lynchpins of this solution. This approach optimizes computing only the necessary states at each step without having to look at all permutations explicitly, which would be prohibitively expensive for large n
.
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 take a small example to illustrate the solution approach. Suppose the string s
is "DID"
. The length of s
is 3
, so our permutation perm
needs to be comprised of integers from 0
to 3
.
-
Initial State: We initialize
f
with1
plus3
zeros:f = [1, 0, 0, 0]
. The1
represents the singular empty permutation, and zeros are placeholders to be filled. -
First Character ('D'): The first character is 'D', so we want
perm[0] > perm[1]
. We iterate backward. Say, initially,f = [1, 0, 0, 0]
, and we are fillingg
.pre = 0
(prefix sum)- Start from the end of
f
,pre = pre + f[3] % mod = 0
andg[3] = pre = 0
pre = pre + f[2] % mod = 0
andg[2] = pre = 0
pre = pre + f[1] % mod = 0
andg[1] = pre = 0
pre = pre + f[0] % mod = 1
andg[0] = pre = 1
After this,g = [1, 0, 0, 0]
and we updatef
tog
.
-
Second Character ('I'): The second character is 'I', so we want
perm[1] < perm[2]
. We iterate forward.pre = 0
(reset prefix sum)g[0] = pre = 0
pre = pre + f[0] % mod = 1
andg[1] = pre = 1
pre = pre + f[1] % mod = 1
andg[2] = pre = 1
pre = pre + f[2] % mod = 1
andg[3] = pre = 1
After this step,g = [0, 1, 1, 1]
and we updatef
tog
.
-
Third Character ('D'): The third character is 'D', so
perm[2] > perm[3]
. We iterate backward again.pre = 0
pre = pre + f[3] % mod = 1
andg[3] = pre = 1
pre = pre + f[2] % mod = 2
andg[2] = pre = 2
pre = pre + f[1] % mod = 3
andg[1] = pre = 3
- We do not need to continue, as
f[0]
represents a state where the sequence is already longer than what the 'D' at last position can influence. After this,g = [0, 3, 2, 1]
andf
is updated tog
.
-
Final Result: We sum up the elements in
f
to find the total number of valid permutations:0 + 3 + 2 + 1 = 6
. The possible permutation patterns are "3210", "3201", "3102", "2103", "3012", and "2013". -
Return Value: We return this sum modulo
10^9 + 7
for the result. In this case, since6
is less than10^9 + 7
, the modulus operation is not necessary, and the result is simply6
.
The above walkthrough visualizes the dynamic programming method, with f
representing the state of the permutation counts at each step, updated according to the 'D' and 'I' constraints in the input string s
.
Solution Implementation
1class Solution:
2 def numPermsDISequence(self, sequence: str) -> int:
3 # Defining the modulus for the problem, as the permutations
4 # can be a large number and we need to take it modulo 10^9 + 7
5 modulus = 10**9 + 7
6
7 # Length of the input sequence
8 n = len(sequence)
9
10 # Initializing the dynamic programming table with the base case:
11 # There is 1 permutation of length 0
12 dp = [1] + [0] * n
13
14 # Looping through the characters in the sequence along with their indices
15 for i, char in enumerate(sequence, 1):
16 # 'pre' is used to store the cumulative sum that helps in updating dp table
17 pre = 0
18 # Initializing a new list to store the number of permutations for the current state
19 new_dp = [0] * (n + 1)
20
21 if char == "D":
22 # If the character is 'D', we count the permutations in decreasing order
23 for j in range(i, -1, -1):
24 pre = (pre + dp[j]) % modulus
25 new_dp[j] = pre
26 else:
27 # If the character is 'I', we do the same in increasing order
28 for j in range(i + 1):
29 new_dp[j] = pre
30 pre = (pre + dp[j]) % modulus
31
32 # Update the dynamic programming table with new computed values for the next iteration
33 dp = new_dp
34
35 # The result is the sum of all permutations possible given the entire DI sequence
36 return sum(dp) % modulus
37
1class Solution {
2 public int numPermsDISequence(String s) {
3 final int MODULO = (int) 1e9 + 7; // Define the modulo constant for avoiding overflow.
4 int n = s.length(); // Length of the input string.
5 int[] dp = new int[n + 1]; // dp array for dynamic programming, initialized for a sequence of length 0.
6 dp[0] = 1; // There's one permutation for an empty sequence.
7
8 // Iterate over the sequence.
9 for (int i = 1; i <= n; ++i) {
10 int cumulativeSum = 0; // To store the cumulative sum for 'D' or 'I' scenarios.
11 int[] newDp = new int[n + 1]; // Temporary array to hold new dp values for current iteration.
12
13 // If the character is 'D', we calculate in reverse.
14 if (s.charAt(i - 1) == 'D') {
15 for (int j = i; j >= 0; --j) {
16 cumulativeSum = (cumulativeSum + dp[j]) % MODULO;
17 newDp[j] = cumulativeSum;
18 }
19 } else { // Otherwise, we calculate in the forward direction for 'I'.
20 for (int j = 0; j <= i; ++j) {
21 newDp[j] = cumulativeSum;
22 cumulativeSum = (cumulativeSum + dp[j]) % MODULO;
23 }
24 }
25 // Assign the newly computed dp values to be used in the next iteration.
26 dp = newDp;
27 }
28
29 int ans = 0;
30 // Sum up all the possible permutations calculated in dp array.
31 for (int j = 0; j <= n; ++j) {
32 ans = (ans + dp[j]) % MODULO;
33 }
34
35 return ans; // Return the total permutations count.
36 }
37}
38
1class Solution {
2public:
3 int numPermsDISequence(string s) {
4 const int MOD = 1e9 + 7; // Constant to hold the modulus value for large numbers
5 int sequenceLength = s.size(); // The length of the sequence
6 vector<int> dp(sequenceLength + 1, 0); // Dynamic programming table
7 dp[0] = 1; // Base case initialization
8
9 // Iterate over the characters in the input string
10 for (int i = 1; i <= sequenceLength; ++i) {
11 int accumulated = 0;
12 vector<int> nextDP(sequenceLength + 1, 0); // Create a new DP array for the next iteration
13
14 // Check if the current character is 'D' for a decreasing relationship
15 if (s[i - 1] == 'D') {
16 // Fill in the DP table backwards for 'D'
17 for (int j = i; j >= 0; --j) {
18 accumulated = (accumulated + dp[j]) % MOD; // Update accumulated sum
19 nextDP[j] = accumulated; // Update the next DP table
20 }
21 } else {
22 // Else, this is an increasing relationship represented by 'I'
23 for (int j = 0; j <= i; ++j) {
24 nextDP[j] = accumulated; // Set the value for 'I'
25 accumulated = (accumulated + dp[j]) % MOD; // Update the accumulated sum
26 }
27 }
28 dp = move(nextDP); // Move the next DP table into the current DP table for the next iteration
29 }
30
31 // Sum all possibilities from the last DP table to get the final answer
32 int answer = 0;
33 for (int j = 0; j <= sequenceLength; ++j) {
34 answer = (answer + dp[j]) % MOD;
35 }
36
37 return answer; // Return the total number of permutations that match the DI sequence
38 }
39};
40
1// This function calculates the number of permutations of the sequence of 0...N
2// that satisfy the given "DI" (decrease/increase) sequence.
3// s: The input "DI" sequence as a string.
4// Returns the number of valid permutations modulo 10^9 + 7.
5function numPermsDISequence(s: string): number {
6 const sequenceLength = s.length;
7 let dp: number[] = Array(sequenceLength + 1).fill(0);
8 dp[0] = 1; // Base case: one permutation of an empty sequence
9 const MOD = 10 ** 9 + 7; // Define the modulo to prevent overflow
10
11 // Iterating over the characters in the sequence
12 for (let i = 1; i <= sequenceLength; ++i) {
13 let prefixSum = 0; // Initialize prefix sum for the number of permutations
14 let nextDp: number[] = Array(sequenceLength + 1).fill(0); // Temporary array to hold new dp values
15
16 // If the current character is 'D', count decreasing sequences
17 if (s[i - 1] === 'D') {
18 for (let j = i; j >= 0; --j) {
19 prefixSum = (prefixSum + dp[j]) % MOD;
20 nextDp[j] = prefixSum;
21 }
22 }
23 // If the current character is 'I', count increasing sequences
24 else {
25 for (let j = 0; j <= i; ++j) {
26 nextDp[j] = prefixSum;
27 prefixSum = (prefixSum + dp[j]) % MOD;
28 }
29 }
30
31 // Update the dp array for the next iteration
32 dp = nextDp;
33 }
34
35 // Sum up all the permutations stored in dp array to get the answer
36 let result = 0;
37 for (let j = 0; j <= sequenceLength; ++j) {
38 result = (result + dp[j]) % MOD;
39 }
40
41 // Return the total number of permutations modulo 10^9 + 7
42 return result;
43}
44
Time and Space Complexity
The given Python code defines a method to count the number of permutations that satisfy a certain "decreasing/increasing" (D/I) sequence requirement, following the rules specified by a given string s
.
Time Complexity
To analyze the time complexity, let's consider the size of the input string s
, denoted as n
. The code includes a main outer loop, which iterates over the input string s
, and two inner loops, which will both iterate at most i + 1
times (where i
ranges from 1 to n
inclusive).
The main loop runs exactly n
times: for i, c in enumerate(s, 1)
. In each iteration, depending on whether the character is a 'D' or not, it performs one of the two inner loops. These inner loops execute a maximum of i
iterations in their respective contexts (for j in range(i, -1, -1)
for 'D' and for j in range(i + 1)
for 'I'), which can be summarized as an arithmetic progression sum from 1
to n
. Arithmetically summing this progression, we get n * (n + 1) / 2
.
Consequently, the overall time complexity is O(n^2)
, since (n * (n + 1)) / 2
is within the same order of magnitude as n^2
.
Space Complexity
For space complexity, the script uses a list f
of size n + 1
to store the running totals of valid permutations and a temporary list g
with the same size to calculate the new values for the next iteration.
Since the largest data structure size is proportional to the input size n
, the space complexity is O(n)
, which is linear to the input size.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of the tree traversal order can be used to obtain elements in a binary search tree in sorted order?
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
LeetCode 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 we
Want a Structured Path to Master System Design Too? Don’t Miss This!