1. 程式人生 > 實用技巧 >LeetCode——不同的子序列 II

LeetCode——不同的子序列 II

Q:給定一個字串 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"。

A:
雖然解決這題的程式碼很短,但它的演算法並不是很容易設計。我們會用動態規劃先求出包括空序列的所有子序列,再返回答案之前再減去空序列。
我們用 dp[k] 表示 S[0 .. k] 可以組成的不同子序列的數目。如果 S 中的所有字元都不相同,例如 S = "abcx",那麼狀態轉移方程就是簡單的 dp[k] = dp[k-1] * 2,例如 dp[2] = 8,它包括 ("", "a", "b", "c", "ab", "ac", "bc", "abc") 這 8 個不同的子序列,而 dp[3] 在這些子序列的末尾增加 x,就可以得到額外的 8 個不同的子序列,即 ("x", "ax", "bx", "cx", "abx", "acx", "bcx", "abcx"),因此 dp[3] = 8 * 2 = 16。
但當 S 中有相同字母的時候,就要考慮重複計數的問題了,例如當 S = "abab" 時,我們有:

  • dp[0] = 2,它包括 ("", "a");
  • dp[1] = 4,它包括 ("", "a", "b", "ab");
  • dp[2] = 7,它包括 ("", "a", "b", "aa", "ab", "ba", "aba");
  • dp[3] = 12,它包括 ("", "a", "b", "aa", "ab", "ba", "bb", "aab", "aba", "abb", "bab", "abab")。

當從 dp[2] 轉移到 dp[3] 時,我們只會在 dp[2] 中的 ("b", "aa", "ab", "ba", "aba") 的末尾增加 b,而忽略掉 ("", "a"),因為它們會得到重複的子序列。我們可以發現,這裡的 ("", "a") 剛好就是 dp[0],也就是上一次增加 b 之前的子序列集合。因此我們就得到了如下的狀態轉移方程:
dp[k] = 2 * dp[k - 1] - dp[last[S[k]] - 1]
即在計算 dp[k] 時,首先會將 dp[k - 1] 對應的子序列的末尾新增 S[k] 得到額外的 dp[k - 1] 個子序列,並減去重複出現的子序列數目,這個數目即為上一次新增 S[k] 之前的子序列數目 dp[last[S[k]] - 1]。

    public int distinctSubseqII(String S) {
        int MOD = 1_000_000_007;
        int N = S.length();
        int[] dp = new int[N+1];
        dp[0] = 1;

        int[] last = new int[26];
        Arrays.fill(last, -1);

        for (int i = 0; i < N; ++i) {
            int x = S.charAt(i) - 'a';
            dp[i+1] = dp[i] * 2 % MOD;
            if (last[x] >= 0)
                dp[i+1] -= dp[last[x]];
            dp[i+1] %= MOD;
            last[x] = i;
        }

        dp[N]--;
        if (dp[N] < 0) dp[N] += MOD;
        return dp[N];
    }