【LeetCode】DI 序列的有效排列de 动态规划解法

题目

我们给出 S,一个源于 {‘D’, ‘I’} 的长度为 n 的字符串 。(这些字母代表 “减少” 和 “增加”。)
有效排列 是对整数 {0, 1, …, n} 的一个排列 P[0], P[1], …, P[n],使得对所有的 i:

  • 如果 S[i] == ‘D’,那么 P[i] > P[i+1],以及 ;
  • 如果 S[i] == ‘I’,那么 P[i] < P[i+1]。

有多少个有效排列?因为答案可能很大,所以请返回你的答案模 10^9 + 7.

来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/valid-permutations-for-di-sequence
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。


这里主要想和大家讨论一下动态规划的相关思路。

思路一

题目中有这么一句话:

  • 如果 S[i] == ‘D’,那么P[i] > P[i+1],
  • 如果 S[i] == ‘I’,那么 P[i] < P[i+1]。

其实这就暗示了本题的一种解决思路,S[ i ] 决定的只是有效排列 P 中的相对大小,排列 P[ i + 1 ] 的值仅由 S[ i ] 和 P[ i ] 决定。
如果把整数范围 i 作为 dp 数组的一维,将P[ i ] 以 j 的方式( j = P[ i ] )作为第二维,那么我们可以大胆尝试一下,初步写出一个 不成熟的 状态转移方程:

  • dp[ i ][ j ] = dp[ i - 1 ][ j + 1] + dp [ i - 1][ j + 2 ] + … + dp[ i - 1][ i - 1 ]     if (S[ i - 1 ] == ‘D’)

  • dp[ i ][ j ] = dp[ i - 1 ][ j - 1] + dp [ i - 1][ j - 2 ] + … + dp[ i - 1][ 0 ]            if (S[ i - 1 ] == ‘I’)

意思是 S[ i - 1] = 'D’时,整数范围 i 并以 j 为末尾的有效排列数量 ,是所有 整数范围 i - 1 并以大于 j 结尾的排列数量的总和。另一种情况,同理。

但很明显!这是错误的方程,因为对于子问题 i - 1,上述方程漏掉了dp[ i - 1][ j ]。 想要知道 dp[ i - 1][ j ] 应该加在哪里,就必须弄清 随着 i 的增加,P 排列是如何增加数字的。接下来我们开始正面分析问题。

输入 “DID” ,字符串的长度n = 3。

当 n = 2 时,一共有两种有效排列 【1 0 2】 和 【2 0 1】,当 n = 3 时 ,以【1 0 2】 为例,我们想要在后面添加一个数字。可加的数字有 3,2,1,0 ,我们可以依次看一下如何添加。

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述
在这里插入图片描述添加 k ,就让之前大于等于k都加1。折线图很好的反应了相对大小关系,以及这个规则下产生的排列是唯一的。然后,我们综合起来看。
在这里插入图片描述
我们可以发现,n = 3 时,以 1 结尾的有【2 0 3 1】和【 3 0 2 1】两个,可以分别由 n = 2 中 大于等于 1 结尾的排列得到。再比如,n = 3 时,以 2 结尾的【1 0 3 2】只有一个,因为 n = 2 中 大于等于 2 结尾的只有【1 0 2】一项。
所以 dp[ i - 1] [ j ] 应该加在 下降 ‘D’ 的条件中,下面给出 真正的 状态转移方程:

dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp [ i - 1][ j + 1 ] + ... + dp[ i - 1][ i - 1 ]      if (S[ i - 1 ] == 'D')   

dp[ i ][ j ] = dp[ i - 1 ][ j - 1] + dp [ i - 1][ j - 2 ] + ... + dp[ i - 1][ 0 ]       if (S[ i - 1 ]  == 'I')

考虑边界 i = 0时 ,数字范围只有0,只有一种排法,初始化 dp[ 0 ][ 0 ] = 1。

    int numPermsDISequence(string S) {
    
    
         int n = S.length();
        vector<vector<int>> dp(n + 1,vector<int>(n + 1, 0));
        int mod = 1000000007;
        dp[0][0] = 1;
        for(int i = 1 ; i <= n; i++){
    
    
            for(int j = 0 ; j <= i; j++){
    
    
                if(S[i- 1] == 'D'){
    
    
                    for(int k = j; k < i ; k++){
    
    
                        dp[i][ j] += dp[i - 1][k] ;
                        dp[i][ j] = dp[i][ j]%mod;
                    }
                }else{
    
    
                    for(int k = 0 ; k < j ; k++){
    
    
                        dp[i ][ j] += dp[i - 1][k];
                        dp[i ][ j] = dp[i ][ j]%mod;
                    }
                }
            }
        }
        int ans = 0;
        for(int j = 0; j <= n ; j++){
    
    
            ans += dp[n][j];
            ans = ans % mod;
        }
        return ans;
    }

复杂度分析

  • 时间复杂度: O ( N 3 ) O(N^3) O(N3)
  • 空间复杂度: O ( N 2 ) O(N^2) O(N2)

优化

我们再次上述的状态转移方程,

(1) 当S[ i - 1 ] == ‘D’。
   dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp [ i - 1 ][ j + 1 ] + … + dp[ i - 1 ][ i - 1 ]
   dp[ i ][ j + 1] = dp[ i - 1 ][ j + 1] + dp [ i - 1 ][ j + 2 ] + … + dp[ i - 1 ][ i - 1 ]

(2) 当S[ i - 1 ] == ‘I’。
   dp[ i ][ j ] = dp[ i - 1 ][ j - 1] + dp [ i - 1 ][ j - 2 ] + … + dp[ i - 1 ][ 0 ]
   dp[ i ][ j - 1 ] = dp[ i - 1 ][ j - 2] + dp [ i - 1 ][ j - 3 ] + … + dp[ i - 1 ][ 0 ]

可以发现当 S [ i - 1] 为 ‘D’ 时,dp[ i ][ j ] 只比 dp[ i ][ j + 1 ] 多出 dp[ i - 1 ][ j ] , 而当S[ i - 1 ]为‘I’,dp[ i ][ j ] 比dp[ i ][ j - 1] 多出了 dp[ i - 1 ][ j - 1 ]。状态转移可以修改为:

dp[ i ][ j ] = dp[ i ][ j - 1 ] + dp[ i - 1 ][ j - 1]    if S[i - 1] == 'I'
dp[ i ][ j ] = dp[ i ][ j + 1 ] + dp[ i - 1 ][ j ]       if S[i - 1] == 'D'

考虑边界同理dp[ 0 ][ 0 ] 为1,并为了方便 把 dp 第二维扩大到 n +2。

class Solution {
    
    
public:
    int numPermsDISequence(string S) {
    
    
         int n = S.length();
        vector<vector<int>> dp(n + 1,vector<int>(n + 2));
        int mod = 1000000007;

		dp[0][0] = 1;
        
        for(int i = 1 ; i <= n; i++){
    
    

            if(S[i- 1] == 'D'){
    
    
                for(int j = i - 1; j >= 0 ;j--){
    
    
                    dp[i][ j] = dp[i][j + 1] + dp[i - 1][j ];
                    dp[i][ j] = dp[i][ j]%mod;
                }
            }else{
    
    
                for(int j = 1 ; j <= i ; j++){
    
    
                    dp[i ][ j] = dp[i ][j - 1] + dp[i - 1][j - 1];
                    dp[i ][ j] = dp[i ][ j]%mod;
                }
            }
        }
        int ans = 0;
        for(int j = 0; j <= n ; j++){
    
    
            ans += dp[n][j];
            ans = ans % mod;
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2)
  • 空间复杂度: O ( N 2 ) O(N^2) O(N2),每次循环只涉及前后两组状态,设置滚动数组,空间复杂度还可以进一步优化为 O ( N ) O(N) O(N),这里不再展开。

思路二

接下来,我们换一种思路来思考这个问题。

这里不再逐步扩大数字选择范围 来解决此问题。而是一开始我们就能选择从0 到 n 的数字。为了方便理解,我们发挥点想象力,可以将数字从小到大,如同木块堆叠起来。每次就从中抽出一块放入排列P中。

这里我们还是以“DID” 为例,每次抽出一个数字块,这个块上的其他块掉下来补齐,整个过程保持从上到下,由大到小。

在这里插入图片描述
那么这里我们可以思考一个问题。如果 我们第2次 抽取 第1层的木块,第1次应该抽取哪一层才能 构成降序‘D’。

在这里插入图片描述
我们罗列出所有情况。
在这里插入图片描述
我们观察可以发现一个规律,想要构成‘ D ’,即 p[ i - 1 ] > p[ i ],就必须让上次抽取的层数 大于 本次抽取的层数;想要构成‘ I ’,即p[ i - 1 ] < p[ i ] ,就必须让上次抽取的层数 小于等于 本次抽取的层数。将那些层数的方案数 加起来 就是本次抽取层的方案数。

让 i 代表已经排了 i 个数字,现在应该排p[ i ] ,让 j 代表选择第 j 层数字(或 选择剩余未排序数字中第 j + 1小的数字),dp[ i ][ j ] 为方案数量。我们就可以写出状态转移方程了。

dp[ i ][ j ] = dp[i - 1][ j + 1] + dp [i - 1][j + 2] + ... + dp[i - 1][ n - i ]      if (S[ i - 1 ] == 'D')   

dp[ i ][ j ] = dp[ i - 1 ][ j ] + dp [ i - 1][ j - 1 ] + ... + dp[ i - 1][ 0 ]       if (S[ i - 1 ]  == 'I')

我们可以发现,这与未优化的思路一的 方程相似。不同的是,两个思路的 j 的限制范围不同,一个是逐步扩大,另一个是逐步减小;思路一的最终有效排列数量是dp[ n ] 的总和,而思路二 则直接就是dp[ n ][ 0 ]。

直接按照这个方程书写的时间复杂度是 O ( N 3 ) O(N^3) O(N3),与未优化的思路一程序结构相似,避免啰嗦,直接优化。

优化

同理,我们分别对比 dp[ i ][ j ] 和 dp[ i ][ j + 1]、dp[ i ][ j ] 和 dp[ i ][ j - 1],获得优化后的方程。

dp[ i ][ j ] = dp[ i ][ j + 1 ] + dp[ i - 1 ][ j + 1 ]       if S[i - 1] == 'D'
dp[ i ][ j ] = dp[ i ][ j - 1 ] + dp[ i - 1 ][ j ]              if S[i - 1] == 'I'

考虑边界,i = 0时,可以选择范围为n 的数字,选择每种数字的方案数分别为1,所以初始化dp[ 0 ] 为1。
并且引入变量cur 处理 j 的边界问题。

    int numPermsDISequence(string S) {
    
    
        int n = S.length();
        vector<vector<int>> dp(n + 1,vector<int>(n + 1));
        int mod = 1000000007;
        int cur = 0;
        for (int j = 0; j <= n; j++) dp[0][j] = 1;
        for (int i = 1; i <= n;i++)
        {
    
    
            cur = 0;
            if(S[i - 1] == 'D')
            {
    
    
            	// 注意与思路一的范围区别
                for (int j = n - i; j >= 0; j-- )
                {
    
    
                	// 这里用cur 避免了一下边界问题
                    // 实际上就是 dp[i][j] = dp[i][j+1] + dp[i-1][j+1];
                    cur = ( cur + dp[i - 1][j + 1] ) %mod; 
                    dp[i][j] = cur ;
                } 
            }
            else
            {
    
    
                for(int j = 0 ; j <= n - i; j++)
                {
    
    
                    //  dp[i][j] = dp[i][j-1] + dp[i-1][j];
                    cur =  (cur + dp[i - 1][j])%mod ;
                    dp[i][j] = cur;
                } 
            }
        }
        return dp[n][0];
    }

复杂度分析

  • 时间复杂度: O ( N 2 ) O(N^2) O(N2)
  • 空间复杂度: O ( N 2 ) O(N^2) O(N2),设置滚动数组,空间复杂度还可以进一步优化为 O ( N ) O(N) O(N)

水平有限,若有错误,请多包涵 (Θ3Θ)


参考

[1] https://leetcode-cn.com/problems/valid-permutations-for-di-sequence/solution/di-xu-lie-de-you-xiao-pai-lie-by-leetcode/
[2] https://leetcode.com/problems/valid-permutations-for-di-sequence/discuss/168278/C%2B%2BJavaPython-DP-Solution-O(N2)

猜你喜欢

转载自blog.csdn.net/zigzagbomb/article/details/104450888