動態規劃——不同的子序列(二)
問題來源: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
-
如果子串 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k−1] 中沒有出現過字元 S [ k ] S[k] S[k],那麼字元 S [ k ] S[k] S[k] 可以接在子串 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k−1] 的所有子序列的後面,且還可以加上字元 S [ k ] S[k] S[k] 自身,所以:
d p [ k ] = d p [ k − 1 ] ∗ 2 + 1 dp[k] = dp[k-1] * 2 + 1
-
如果子串 S [ 0 , . . . , k − 1 ] S[0,...,k-1] S[0,...,k−1] 中出現過字元 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[k−1]∗2−dp[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,...,k−1] 中沒有字元與
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,...,k−1] 中有字元與
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]∗2−dp[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];
}
};