leetcode 903. DI 序列的有效排列

题目描述

给定一个长度为 n 的字符串 s ,其中 s[i] 是:
“D” 意味着减少,或者
“I” 意味着增加
有效排列 是对有 n + 1 个在 [0, n] 范围内的整数的一个排列 perm ,使得对所有的 i:
如果 s[i] == ‘D’,那么 perm[i] > perm[i+1],以及;
如果 s[i] == ‘I’,那么 perm[i] < perm[i+1]。
返回 有效排列 perm的数量 。因为答案可能很大,所以请返回你的答案对 109 + 7 取余。
示例 1:
输入:s = “DID”
输出:5
解释:
(0, 1, 2, 3) 的五个有效排列是:
(1, 0, 3, 2)
(2, 0, 3, 1)
(2, 1, 3, 0)
(3, 0, 2, 1)
(3, 1, 2, 0)
示例 2:
输入: s = “D”
输出: 1
提示:
n == s.length
1 <= n <= 200
s[i] 不是 ‘I’ 就是 ‘D’

分析

这是一道不错的练习动态规划的题目,leetcode官方题解写的比较难懂。题意比较简洁,但是难度的确是hard。

首先我们还是用减而治之的思想来分析本题。s的长度是n,可以生成的排列长度是n + 1,如果知道了前i - 1个数的排列方案数,如何求出前i个数的排列方案数?有两个信息是我们要知道的,第一个是第i - 1个数是什么,因为我们要根据s对应的D和I来确定下一个数,只有知道上一个数是什么,才知道下一个数的范围。还有前i-1个数已经使用的数我们也要知道,因为我们要从剩下的数中选择满足条件的数放到第i个位置。因此这题的状态表示可以是f[i][j][st]表示前i个数的排列的最后一个数是j,已经选择的数的状态是st的有效排列方案数。可以使用状态压缩来表示数的使用状态,但是这题n的范围是200,2200个状态显然时间和空间都不能接受,我们只能去掉状态这一维度。

使用f[i][j]来表示以j结尾的前i个数的排列的有效方案数,使用一个实例来分析下状态要如何转移。

s = “DID”,根据题目的用例可以知道n = 3时候的方案数,如果再加上一个I使得s = “DIDI”,那么新的方案数是多少呢?前四个数已经形成合法排列了,s的增长引入了第五个数4,并且排列的最后两个数是递增的,也就是第五个数大于第四个数。

此时,n = 4,排列的数可以从0~4中选择,那么我们枚举第五个数时就可以选择0到4中的任意一个。

如果第五个数就是4,那么将其加到任意一个合法的DID排列中都是合法的,因为4必然大于上一个数,也就是f[4][4] = f[3][0] + f[3][1] + f[3][2] + f[3][3],也就是第四个数取0,1,2,3都可以,注:这里i = 0表示第1个数。

如果第五个数不是4呢?比如第五个数是3,第五个数需要大于第四个数,第四个数只能是0,1,2,假设第四个数取2,那么前三个数就需要是0,1,4这三个数的合法排列了,准确说是前四个数要取0,1,2,4这四个数的合法排列,我们已知的是0,1,2,3的合法排列,把4映射成3即可解决问题,因为所有数的相对位置不需要随着3变成4而发生改变,所以这两个排列数的方案数是相等的。

再来考虑下s = “DIDD”的情况,第五个数要小于第四个数,如果第五个数是3,那么前一个数只能是4,是否可以写f[4][3] = f[3][4]呢?显然是不可以的因为我们求f[3][j]时是排列数中没有4这个数的存在,遍历到s的最后一个位置才引入4的,最后一个数是3,相当于把前面排列里的3换成4了,比如前面合法的排列是1032,引入4后必然存在合法方案数1042,所以f[4][3] = f[3][3],相当于把之前排列位置中的3映射成我们需要的4了。

如果第五个数是2,前一个数可以是3,4,而3和4作为剩下排列数中最大的两个数,必然要对应于原四个数排列中的最大的两个数2,3。一般的说,第五个数是j,那么前一个数应该是j + 1,j + 2,…,4,由于4在前四个数的序列中不存在,所以要将所有数都向前移动一下。也就是本来前四个数是0,1,2,3的排列,现在把2调到末尾了,前四个数依次是0,1,3,4,按照大小一一映射得到3映射到2,4映射到3。映射的原理就是0,1,2,3构成的以3位末尾的合法排列方案数等于0,1,3,4构成的以4位末尾的合法排列方案数。而小于2的数0,1在新的序列里依旧维持第1和第2小,所以无需映射。

使用映射方法的代码如下,时间复杂度是立方级别的。

class Solution {
public:
    int numPermsDISequence(string s) {
        int n = s.size();
        const int MOD = 1e9 + 7;
        vector<vector<int> > f(n + 1,vector<int>(n + 1,0));
        if(s[0] == 'D') f[0][0] = 1;//10
        else    f[0][1] = 1;//01
        for(int i = 1;i < n;i++){
            for(int j = 0;j <= i + 1;j++){
                if(s[i] == 'D'){
                    for(int k = j;k <= i;k++){//将每个比j大的数都映射成减去1的数
                        f[i][j] = (f[i][j] + f[i-1][k]) % MOD;
                    }
                }
                else{
                    for(int k = 0;k < j;k++){//小于j的数相对位置不变不用映射
                        f[i][j] = (f[i][j] + f[i-1][k]) % MOD;
                    }
                }
            }
        }
        int res = 0;
        for(int i = 0;i <= n;i++)   res = (res + f[n-1][i]) % MOD;
        return res;
    }
};

上面DP算法在进行状态转移过程中存在冗余状态,在AcWing 3 完全背包问题里我们介绍了消除冗余的优化办法,同样可以用在本题。

s[i] == 'D’时,如果最后一个数是3,那么前面一个数可以是4(映射得到3),如果最后一个数是2,前面一个数可以是3,4(映射得到2,3),最后一个数是1,前面一个数可以是2,3,4(映射得到1,2,3),可以发现f[i][j]使用到的状态仅比f[i][j+1]用到的状态多上了一个f[i-1][j],因此我们从大到小枚举j,就可以免去对前一个数的枚举的循环了,时间复杂度降低到平方级别了。

s[i] == 'I’也是一样,最后一个数是1,前面一个数数可以是0,最后一个数是2,前面一个数可以是0,1,最后一个数是3,前面一个数可以是0,1,2,因此f[i][j]也只需要用到f[i][j-1]和f[i-1][j-1]的状态,自小到大枚举j即可。

O(n2)的代码如下:

class Solution {
public:
    int numPermsDISequence(string s) {
        int n = s.size();
        const int MOD = 1e9 + 7;
        vector<vector<int> > f(n + 1,vector<int>(n + 1,0));
        if(s[0] == 'D') f[0][0] = 1;
        else    f[0][1] = 1;
        for(int i = 1;i < n;i++){
            if(s[i] == 'D'){
                for(int j = i;j >= 0;j--){
                    f[i][j] = (f[i][j + 1] + f[i-1][j]) % MOD;
                }
            }
            else{
                for(int j = 1;j <= i + 1;j++){
                    f[i][j] = (f[i][j-1] + f[i-1][j-1]) % MOD;
                }
            }
        }
        int res = 0;
        for(int i = 0;i <= n;i++)   res = (res + f[n-1][i]) % MOD;
        return res;
    }
};
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值