1. 程式人生 > 其它 >動態規劃——不同的子序列(二)

動態規劃——不同的子序列(二)

技術標籤:動態規劃演算法動態規劃

問題來源:leetcode 940

不同的子序列(二)

給定一個字串 S,計算 S 的不同非空子序列的個數。

因為結果可能很大,所以返回答案模 10^9 + 7.

示例 1:

輸入:"abc"
輸出:7
解釋:7 個不同的子序列分別是 "a", "b", "c", "ab", "ac", "bc", 以及 "abc"。

示例 2:

輸入:"aba"
輸出:6
解釋:6 個不同的子序列分別是 "a", "b", "ab", "ba", "aa" 以及 "aba"。

示例 3:

輸入:"aaa"
輸出:3
解釋:3 個不同的子序列分別是 "a", "aa" 以及 "aaa"。

提示:

  • S 只包含小寫字母。
  • 1 <= S.length <= 2000

動態規劃

優化子結構就是證明原問題的最優解包含子問題的最優解,或者可以通過子問題的最優解構造原問題的最優解,下面說明如何由子問題的解來構造原問題的解

優化子結構:設 d p [ k ] dp[k] dp[k] 表示字串 S [ 0 , . . . , k ] S[0,...,k] S[0,...,k] 的不同子序列的個數,對於每一個位置 k k

k d p [ k ] dp[k] dp[k] 可以由子問題的解構造得到:

  • 如果子串 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k1] 中沒有出現過字元 S [ k ] S[k] S[k],那麼字元 S [ k ] S[k] S[k] 可以接在子串 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k1] 的所有子序列的後面,且還可以加上字元 S [ k ] S[k] S[k] 自身,所以:

    d p [ k ] = d p [ k − 1 ] ∗ 2 + 1 dp[k] = dp[k-1] * 2 + 1

    dp[k]=dp[k1]2+1

  • 如果子串 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k1] 中出現過字元 S [ k ] S[k] S[k],假如說最靠近 S [ k ] S[k] S[k] 且與 S [ k ] S[k] S[k] 相等的字元是 S [ j ] S[j] S[j] j < k j < k j<k S [ j ] = S [ k ] S[j]=S[k] S[j]=S[k]),那麼對於加上 S [ j ] S[j] S[j] 之前的子序列來說,加上字元 S [ k ] S[k] S[k] 會生成部分與加上 S [ j ] S[j] S[j] 相同的子序列,所以需要把這部分重複的子序列去掉,注意此時 S [ k ] S[k] S[k] 自身不可以再作為一個新的子序列加入進來,不需要再加 1:

    d p [ k ] = d p [ k − 1 ] ∗ 2 − d p [ l a s t [ S [ k ] ] − 1 ] dp[k] = dp[k-1] * 2\ -\ dp[last[S[k]]-1] dp[k]=dp[k1]2dp[last[S[k]]1]

該問題的優化子結構不容易證明,但從子問題的解構造原問題的解的角度去思考還是比較容易的。

重疊子問題:存在。

遞迴地定義最優解的值:優化子結構是通過子問題,考慮到在有重複字元出現時,下標 d p [ l a s t [ S [ k ] ] − 1 ] dp[last[S[k]]-1] dp[last[S[k]]1] 可能是負數,因此這裡初始化 d p dp dp 陣列大小為 n + 1 n+1 n+1,其中 n n n 表示字串的字元個數, d p [ k + 1 ] dp[k+1] dp[k+1] 表示字串 S [ 0 , . . . , k ] S[0,...,k] S[0,...,k] 中不同子序列的個數:

  • 如果 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k1] 中沒有字元與 S [ k ] S[k] S[k] 相等,那麼
    d p [ k + 1 ] = d p [ k ] ∗ 2 + 1 dp[k+1]=dp[k]*2+1 dp[k+1]=dp[k]2+1
  • 如果 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k1] 中有字元與 S [ k ] S[k] S[k] 相等,其下標為 l a s t [ k ] last[k] last[k],那麼
    d p [ k + 1 ] = d p [ k ] ∗ 2 − d p [ l a s t [ S [ k ] ] ] dp[k+1]=dp[k] * 2 - dp[last[S[k]]] dp[k+1]=dp[k]2dp[last[S[k]]]

自底向上地計算最優解的值:從左向右掃描字串的每個字元 S [ k ] S[k] S[k],便可以保證計算每一個 d p [ k + 1 ] dp[k+1] dp[k+1] 時,其相關的子問題 d p [ k ] dp[k] dp[k] 以及 d p [ l a s t [ S [ k ] ] ] dp[last[S[k]]] dp[last[S[k]]] 均已經被計算了出來。

class Solution {
public:
    int distinctSubseqII(string S) {
        int n = S.size();
        int M = 1000000007;
        vector<int> dp(n + 1);
        dp[0] = 0;

        vector<int> last(26, -1);
        for(int i=0; i<n; i++) {
            int ch = S[i] - 'a';
            dp[i+1] = (2 * dp[i] + 1) % M;
            if(last[ch] >= 0) {
                dp[i+1] -= (dp[last[ch]] + 1);
            }
            dp[i+1] %= M;
            last[ch] = i;
        }
        return dp[n] < 0 ? dp[n] + M : dp[n];
    }
};