We are given `S`, a length `n` string of characters from the set `{'D', 'I'}`. (These letters stand for "decreasing" and "increasing".)

valid permutation is a permutation P[0], P[1], ..., P[n] of integers {0, 1, ..., n}, such that for all i:

  • If S[i] == 'D', then P[i] > P[i+1], and;
  • If S[i] == 'I', then P[i] < P[i+1].

How many valid permutations are there?  Since the answer may be large, return your answer modulo 10^9 + 7.

Example 1:

Input: "DID"
Output: 5
Explanation:
The 5 valid permutations of (0, 1, 2, 3) are:
(1, 0, 3, 2)
(2, 0, 3, 1)
(2, 1, 3, 0)
(3, 0, 2, 1)
(3, 1, 2, 0)

Note:

  1. 1 <= S.length <= 200
  2. S consists only of characters from the set {'D', 'I'}.

这道题给了我们一个长度为n的字符串S,里面只有两个字母D和I,分别表示下降 Decrease 和上升 Increase,意思是对于 [0, n] 内所有数字的排序,必须满足S的模式,比如题目中给的例子 S="DID",就表示序列需要先降,再升,再降,于是就有那5种情况。题目中提示了结果可能会是一个超大数,让我们对 1e9+7 取余,经验丰富的刷题老司机看到这里就知道肯定不能递归遍历所有情况,编译器估计都不允许,这里动态规划 Dynamic Programming 就是不二之选,但是这道题正确的动态规划解法其实是比较难想出来的,因为存在着关键的隐藏信息 Hidden Information,若不能正确的挖掘出来(山东布鲁斯特挖掘机专业了解一下?),是不太容易解出来的。首先来定义我们的 DP 数组吧,这里大家的第一直觉可能是想着就用一个一维数组 dp,其中 dp[i] 表示范围在 [0, i] 内的字符串S的子串能有的不同序列的个数。这样定义的话,就无法写出状态转移方程,像之前说的,我们忽略了很关键的隐藏信息。先来想,序列是升是降到底跟什么关系最大,答案是最后一个数字,比如我们现在有一个数字3,当前的模式是D,说明需要下降,所以可能的数字就是 0,1,2,但如果当前的数字是1,那么还要下降的话,那么貌似就只能加0了?其实也不一定,因为这道题说了只需要保证升降模式正确就行了,数字之间的顺序关系其实并不重要,举个例子来说吧,假如我们现在已经有了一个 "DID" 模式的序列 1032,假如我们还想加一个D,变成 "DIDD",该怎么加数字呢?多了一个模式符,就多了一个数字4,显然直接加4是不行的,实际是可以在末尾加2的,但是要先把原序列中大于等于2的数字都先加1,即 1032 -> 1043,然后再加2,变成 10432,就是 "DIDD" 了。虽然我们改变了序列的数字顺序,但是升降模式还是保持不变的。同理,也是可以加1的,1032 -> 2043 -> 20431,也是可以加0的,1032 -> 2143 -> 21430。但是无法加3和4,因为 1032 最后一个数字2很很重要,所有小于等于2的数字,都可以加在后面,从而形成降序。那么反过来也是一样,若要加个升序,比如变成 "DIDI",猜也猜的出来,后面要加大于2的数字,然后把所有大于等于这个数字的地方都减1,比如加上3,1032 -> 1042 -> 10423,再比如加上4,1032 -> 1032 -> 10324。

通过上面的分析,我们知道了最后一个位置的数字的大小非常的重要,不管是要新加升序还是降序,最后的数字的大小直接决定了能形成多少个不同的序列,这个就是本题的隐藏信息,所以我们在定义 dp 数组的时候必须要把最后一个数字考虑进去,这样就需要一个二维的 dp 数组,其中 dp[i][j] 表示由范围 [0, i] 内的数字组成且最后一个数字为j的不同序列的个数。就拿题目中的例子来说吧,由数字 [0, 1, 2, 3] 组成 "DID" 模式的序列,首先 dp[0][0] 是要初始化为1,如下所示(括号里是实际的序列):

dp[0][0] = 1 (0)

然后需要加第二个数字,由于需要降序,那么根据之前的分析,加的数字不能大于最后一个数字0,则只能加0,如下所示:

加0:  ( dp[1][0] = 1 )
0 -> 1 -> 10

然后需要加第三个数字,由于需要升序,那么根据之前的分析,加的数字不能小于最后一个数字0,那么实际上可以加的数字有 1,2,如下所示:

加1:  ( dp[2][1] = 1 )
10 -> 20 -> 201 加2: ( dp[2][2] = 1 )
10 -> 10 -> 102

然后需要加第四个数字,由于需要降序,那么根据之前的分析,加的数字不能大于最后一个数字,上一轮的最后一个数字有1或2,那么实际上可以加的数字有 0,1,2,如下所示:

加0:  ( dp[3][0] = 2 )
201 -> 312 -> 3120
102 -> 213 -> 2130 加1: ( dp[3][1] = 2 )
201 -> 302 -> 3021
102 -> 203 -> 2031 加2: ( dp[3][2] = 1 )
102 -> 103 -> 1032

这种方法算出的 dp 数组为:

1 0 0 0
1 0 0 0
0 1 1 0
2 2 1 0

最后把 dp 数组的最后一行加起来 2+2+1 = 5 就是最终的结果,分析到这里,其实状态转移方程已经不难得到了,根据前面的分析,当是降序时,下一个数字不小于当前最后一个数字,反之是升序时,下一个数字小于当前最后一个数字,所以可以写出状态转移方程如下所示:

if (S[i-1] == 'D')    dp[i][j] += dp[i-1][k]    ( j <= k <= i-1 )
else dp[i][j] += dp[i-1][k] ( 0 <= k < j )

解法一:

class Solution {
public:
int numPermsDISequence(string S) {
int res = 0, n = S.size(), M = 1e9 + 7;
vector<vector<int>> dp(n + 1, vector<int>(n + 1));
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 - 1; ++k) {
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % M;
}
} else {
for (int k = 0; k <= j - 1; ++k) {
dp[i][j] = (dp[i][j] + dp[i - 1][k]) % M;
}
}
}
}
for (int i = 0; i <= n; ++i) {
res = (res + dp[n][i]) % M;
}
return res;
}
};

我们还可以换一种形式 DP 解法,这里的 dp 数组在定义上跟之前的略有区别,还是用一个二维数组,这里的 dp[i][j] 表示由 i+1 个数字组成且第 i+1 个数字(即序列中的最后一个数字)是剩余数字中(包括当前数字)中第 j+1 小的数字。比如 dp[0][0],表示序列只有1个数字,且该数字是剩余数字中最小的,那就只能是0。再比如,dp[1][2] 表示该序列有两个数字,且第二个数字是剩余数字中第三小的,那么序列只能是 32,因为剩余数字为 0,1,2(包括最后一个数字),这里2就是第三小的。有些情况序列不唯一,比如 dp[1][1] 表示该序列有两个数字,且第二个数字是剩余数字中第二小的,此时的序列就有 31(1是 0,1,2 中第二小的)和 21(1是 0,1,3 中第二小的)两种情况。搞清楚了 dp 的定义之后,再来推导状态转移方程吧,对于 dp[0][j] 的情况,十分好判断,因为只有一个数字,并不存在升序降序的问题,所以 dp[0][j] 可以都初始化为1,如下所示(括号里是实际的序列):

dp[0][3] = 1  (3)
dp[0][2] = 1 (2)
dp[0][1] = 1 (1)
dp[0][0] = 1 (0)

然后需要加第二个数字,由于需要降序,那么根据之前的分析,新加的数字不可能是第四小的,所以不可能出现 dp[1][3] 为正数,因为这表示有两个数字,且第二个数字是剩余数字中的第四小,总共就四个数字,第四小的数字就是最大数字,由于是降序,所以第二个数字要小于第一个数字,这里就矛盾了,所以 dp[1][3] 一定为0,而其余的确实可以从上一层递推过来,具体来说,对于 dp[1][j],需要累加 dp[0][k] ( j < k <= n-1 ):

dp[1][2] = dp[0][3] = 1  (32)
dp[1][1] = dp[0][3] + dp[0][2] = 2 (31, 21)
dp[1][0] = dp[0][3] + dp[0][2] + dp[0][1] = 3 (30, 20, 10)

然后需要加第三个数字,由于需要升序,那么根据之前的分析,此时已经有两个数字了,新加的第三个数字只可能是剩余数字的第一小和第二小,即只有 dp[2][0] 和 dp[2][1] 会有值,跟上面相似,其也是由上一层累加而来,对于 dp[2][j],需要累加 dp[1][k] ( 0 <= k <= j ):

dp[2][1] = dp[1][1] + dp[1][0] = 5  (312, 213, 302, 203, 103)
dp[2][0] = dp[1][0] = 3 (301, 201, 102)

最后再加第四个数字,由于需要降序,那么根据之前的分析,此时已经有三个数字了,新加的第四个数字只可能是剩余数字的第一小,即只有 dp[3][0] 会有值,跟上面相似,其也是由上一层累加而来,对于 dp[3][j],需要累加 dp[2][k] ( j< k <= n-1 ),这里有值的只有 dp[2][1]:

dp[3][0] = dp[2][1] = 5 (3120, 2130, 3021, 2031, 1032)

这种方法算出的 dp 数组为:

1 1 1 1
3 2 1 0
3 5 0 0
5 0 0 0

这种方法算出的最终结果一定是保存在 dp[n][0] 中的,分析到这里,其实状态转移方程已经不难得到了,如下所示:

if (S[i] == 'D')    dp[i+1][j] = sum(dp[i][k])    ( j < k <= n-1 )
else dp[i+1][j] = sum(dp[i][k]) ( 0 <= k <= j )

解法二:

class Solution {
public:
int numPermsDISequence(string S) {
int n = S.size(), M = 1e9 + 7;
vector<vector<int>> dp(n + 1, vector<int>(n + 1));
for (int j = 0; j <= n; ++j) dp[0][j] = 1;
for (int i = 0; i < n; ++i) {
if (S[i] == 'I') {
for (int j = 0, cur = 0; j < n - i; ++j) {
dp[i + 1][j] = cur = (cur + dp[i][j]) % M;
}
} else {
for (int j = n - 1 - i, cur = 0; j >= 0; --j) {
dp[i + 1][j] = cur = (cur + dp[i][j + 1]) % M;
}
}
}
return dp[n][0];
}
};

Github 同步地址:

https://github.com/grandyang/leetcode/issues/903

参考资料:

https://leetcode.com/problems/valid-permutations-for-di-sequence/

https://leetcode.com/problems/valid-permutations-for-di-sequence/discuss/168278/C%2B%2BJavaPython-DP-Solution-O(N2)

https://leetcode.com/problems/valid-permutations-for-di-sequence/discuss/196939/Easy-to-understand-solution-with-detailed-explanation

https://leetcode.com/problems/valid-permutations-for-di-sequence/discuss/168612/Top-down-with-Memo-greater-Bottom-up-DP-greater-N3-DP-greater-N2-DP-greater-O(N)-space

[LeetCode All in One 题目讲解汇总(持续更新中...)](https://www.cnblogs.com/grandyang/p/4606334.html)

[LeetCode] 903. Valid Permutations for DI Sequence DI序列的有效排列的更多相关文章

  1. 903. Valid Permutations for DI Sequence

    We are given S, a length n string of characters from the set {'D', 'I'}. (These letters stand for &q ...

  2. [Swift]LeetCode903. DI 序列的有效排列 | Valid Permutations for DI Sequence

    We are given S, a length n string of characters from the set {'D', 'I'}. (These letters stand for &q ...

  3. 动态规划——Valid Permutations for DI Sequence

    We are given S, a length n string of characters from the set {'D', 'I'}. (These letters stand for &q ...

  4. LeetCode 422. Valid Word Square

    原题链接在这里:https://leetcode.com/problems/valid-word-square/ 题目: Given a sequence of words, check whethe ...

  5. [LeetCode] 549. Binary Tree Longest Consecutive Sequence II 二叉树最长连续序列之 II

    Given a binary tree, you need to find the length of Longest Consecutive Path in Binary Tree. Especia ...

  6. [LeetCode] Longest Valid Parentheses

    第一种方法,用栈实现,最容易想到,也比较容易实现,每次碰到‘)’时update max_len,由于要保存之前的‘(’的index,所以space complexity 是O(n) // 使用栈,时间 ...

  7. [Leetcode][Python]46: Permutations

    # -*- coding: utf8 -*-'''__author__ = 'dabay.wang@gmail.com' 46: Permutationshttps://leetcode.com/pr ...

  8. [LeetCode] 036. Valid Sudoku (Easy) (C++)

    指数:[LeetCode] Leetcode 解决问题的指数 (C++/Java/Python/Sql) Github: https://github.com/illuz/leetcode 036. ...

  9. LeetCode:36. Valid Sudoku,数独是否有效

    LeetCode:36. Valid Sudoku,数独是否有效 : 题目: LeetCode:36. Valid Sudoku 描述: Determine if a Sudoku is valid, ...

随机推荐

  1. Python连载19-装饰器

    一.检视一个函数相同的另一种方法 利用属性:函数._name def hello(): print("我是一个测试程序") f = hello print(f.__name__) ...

  2. javascript播放图片序列帧

    javascript播放图片序列帧1 先预加载<pre>var load_img = [];for(k=0;k<=16;k++){load_img.push( '/cjsxy/ima ...

  3. GV900 Political Explanation

    GV900 Political Explanation, 2017/201830 October, 2018Homework assignment 2Due Week 7 (13 November)W ...

  4. 【题解】Typesetting [Hdu6107]

    [题解]Typesetting [Hdu6107] 传送门:\(\text{Typesetting}\) \(\text{[Hdu6107]}\) [题目描述] 有一篇行数无限宽度 \(MaxW\) ...

  5. 聊聊springboot2的embeded container的配置改动

    本文主要研究下springboot2的embeded container的配置改动 springboot 1.x import org.apache.catalina.connector.Connec ...

  6. git 使用 tortoisegit 解冲突

    git 解冲突需要注意的问题 弄清除冲突双向的修改意图,并在解决冲突时,同时处理两边的意图. 举例说明 A.txt 文件, 在 master 分支上,有一行文字(代码)是这样: 这是一段在 maste ...

  7. 初识Android App自动化测试框架--Unittest

    1.为什么需要使用框架实现自动化测试 作为测试工程师,可能在代码能力上相比开发工程师要弱一点,所以我们在写脚本的时候就会相对容易的碰到更多的问题,如果有一个成熟的框架供给我们使用的话,可以帮助我们避免 ...

  8. 三.基础部分+asp网站搭建

    渗透测试流程:更全面地找出服务器的问题,更倾向保护 明确目标-->信息收集-->漏洞探测-->漏洞验证-->信息分析-->获取所需-->信息整理-->形成报告 ...

  9. ios开发之--为父view上的子view添加阴影

    项目中碰到一个问题,在tableview的headerview里面有很一个子view,设计师的要求是在下方添加一个阴影,效果如下: 以前的实现思路就是,代码如下: 添加阴影 调用视图的 layer C ...

  10. Android自定义圆角矩形进度条2

    效果图: 或 方法讲解: (1)invalidate()方法 invalidate()是用来刷新View的,必须是在UI线程中进行工作.比如在修改某个view的显示时, 调用invalidate()才 ...