[LeetCode] 903. Valid Permutations for DI Sequence DI序列的有效排列



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)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM