1. 程式人生 > 其它 >【Lintcode】1702. Distinct Subsequences II(配數學證明)

【Lintcode】1702. Distinct Subsequences II(配數學證明)

技術標籤:# 貪心、動態規劃與記憶化搜尋

題目地址:

https://www.lintcode.com/problem/distinct-subsequences-ii/description

給定一個長 n n n的字串 s s s,問其所有不同的非空子序列一共多少個。

思路是動態規劃。容易想到按照子序列的結尾分類,這裡的關鍵是如何去重。我們先寫出 f f f的遞推式,然後解釋其正確性: f [ i ] = 1 + ∑ s [ j ] ≠ s [ i ] , j = 0 , 1 , . . . , i − 1 f [ j ] f[i]=1+\sum_{s[j]\ne s[i],j=0,1,...,i-1}f[j]

f[i]=1+s[j]=s[i],j=0,1,...,i1f[j]最後答案就是 ∑ f \sum f f。這裡累加的時候,略過了與 s [ i ] s[i] s[i]相等的字元對應的 f f f。如果 ∄ j < i , s [ j ] = s [ i ] \nexists j<i,s[j]=s[i] j<i,s[j]=s[i],那 f [ i ] = 1 f[i]=1 f[i]=1,這裡的 1 1 1計數的就是 s [ 0 : i ] s[0:i] s[0:i]這個字串;設 s [ i ] s[i] s[i] s [ 0 : i − 1 ] s[0:i-1] s[0:i
1]
出現的所有位置分別是 j 1 , . . . , j k j_1,...,j_k j1,...,jk j k < . . . < j 1 j_k<...<j_1 jk<...<j1,那麼對於 j 1 < k < i j_1<k<i j1<k<i,累加 f [ k ] f[k] f[k]所計數的子序列就是 f [ k ] f[k] f[k]所計數的子序列後面接上 s [ i ] s[i] s[i],而對於 j 2 < k < j 1 j_2<k<j_1 j2<k<j1
,累加 f [ k ] f[k] f[k]所計數的子序列就是 f [ k ] f[k] f[k]所計數的子序列後面接上 s [ i ] × 2 s[i]\times 2 s[i]×2,以此類推。舉例如下:對於 s = a b a b s=abab s=abab,那麼 f [ 0 ] = 1 f[0]=1 f[0]=1對應的是 a a a f [ 1 ] = 2 f[1]=2 f[1]=2對應的是 b b b a b ab ab,而 f [ 2 ] = 1 + f [ 1 ] = 3 f[2]=1+f[1]=3 f[2]=1+f[1]=3對應,這裡 1 1 1指的是 a a aa aa,而 f [ 1 ] f[1] f[1]累加的是 b a ba ba a b a aba aba,接著 f [ 3 ] = 1 + f [ 2 ] + f [ 0 ] = 5 f[3]=1+f[2]+f[0]=5 f[3]=1+f[2]+f[0]=5,這裡 1 1 1指的是 b b bb bb,而 f [ 2 ] f[2] f[2]累加的是之前 f [ 2 ] f[2] f[2]所代表的 a a aa aa b a ba ba a b a aba aba後面接上 s [ 3 ] = b s[3]=b s[3]=b,即 a a b aab aab b a b bab bab a b a b abab abab,而 f [ 0 ] f[0] f[0]累加的是之前 f [ 0 ] f[0] f[0]所代表的 a a a後面接上 b × 2 b\times 2 b×2,即 a b b abb abb。列在下面:
f [ 0 ] f[0] f[0] a a a
f [ 1 ] f[1] f[1] b b b a b ab ab
f [ 2 ] f[2] f[2] a a aa aa b a ba ba a b a aba aba
f [ 3 ] f[3] f[3] b b bb bb a a b aab aab b a b bab bab a b a b abab abab a b b abb abb
最後答案就是 11 11 11

我們證明一下上面的演算法的正確性:

首先容易看出,任何一個子序列一定會被列舉到(這裡可以這樣看,任何一個子序列都會在它第一次出現的位置被列舉到,這裡的“第一次”出現指的是每個字元都取儘量左邊的那個)。接下來只需要證明沒有重複即可。這一點可以用數學歸納法證明。對 s s s的長度做歸納。我們要證明的結論是,每個子序列會在它第一次出現的位置被列舉到(這句話的意思是它第一次出現的時候,它的最後一個字元所在位置 i i i對應的 f [ i ] f[i] f[i]會對其進行計數),並且之後不會重複列舉。當長度為 1 , 2 1,2 1,2時結論正確。設長度小於 n n n的情況下結論也正確,當長度等於 n n n的時候。如果有重複,接下來分類討論。如果最後一個重複的位置是相同的,由歸納假設,前面部分只會在第一次出現的時候列舉到,這就矛盾了;如果最後一個重複的位置不同,一個在 c = s [ n − 1 ] c=s[n-1] c=s[n1],另一個在 c = s [ m ] c=s[m] c=s[m]並且 m < n − 1 m<n-1 m<n1,那麼將兩個子序列同時去掉最後一個字元,前面的部分,由歸納假設,都是在第一次出現的位置被列舉的,但是前面部分的最後一個字元的下標是小於 m m m的,在算 f [ n − 1 ] f[n-1] f[n1]的時候列舉不到這個子序列,因為算 f [ n − 1 ] f[n-1] f[n1]的時候列舉到的子序列事實上是上面子序列去掉最後一個字元後後面至少接兩個 c c c,這就矛盾了。所以結論正確。

事實上上面的 f [ i ] f[i] f[i]存的就是,所有以 s [ i ] s[i] s[i]結尾的,並且是第一次出現的子序列的個數。

程式碼如下:

public class Solution {
    /**
     * @param S: The string s
     * @return: The number of distinct, non-empty subsequences of S.
     */
    public int distinctSubseqII(String S) {
        // Write your code here
        int res = 0, MOD = (int) (1E9 + 7);
        int[] dp = new int[S.length()];
        for (int i = 0; i < S.length(); i++) {
            dp[i] = 1;
            for (int j = 0; j < i; j++) {
                if (S.charAt(j) != S.charAt(i)) {
                    dp[i] += dp[j];
                    dp[i] %= MOD;
                }
            }
        }
        
        for (int i : dp) {
            res += i;
            res %= MOD;
        }
        
        return res;
    }
}

時間複雜度 O ( n 2 ) O(n^2) O(n2),空間 O ( n ) O(n) O(n)