题目地址:
https://leetcode.com/problems/distinct-subsequences-ii/
给定一个长 n n n的字符串 s s s,问 s s s的本质不同的非空子序列的个数。答案模 1 0 9 + 7 10^9+7 109+7后返回。题目保证 s s s只含英文小写字母。
法1:动态规划。设 f [ k ] f[k] f[k]是 s s s的长 k k k的前缀的本质不同的子序列的个数(含空)。那么 f [ 0 ] = 1 f[0]=1 f[0]=1,接下来考虑怎么由 f [ k ] f[k] f[k]得出 f [ k + 1 ] f[k+1] f[k+1]。以下 s [ i ] s[i] s[i]表示 s s s的第 i i i个字符。先假设已经得出所有的 f [ k ] f[k] f[k]个不同的子序列,那么在 f [ k + 1 ] f[k+1] f[k+1]中,它们仍然是合法的子序列,应该包含在内;此外,它们可以后面接上 s [ k + 1 ] s[k+1] s[k+1],成为另外 f [ k ] f[k] f[k]个不同的子序列,当中可能有重复的部分,如果 s [ l ] s[l] s[l]是 s s s中在 s [ k + 1 ] s[k+1] s[k+1]之前的最后一次出现,那么对于 f [ l − 1 ] f[l-1] f[l−1]的那些不同子序列,后面接上 s [ l ] s[l] s[l]和接上 s [ k + 1 ] s[k+1] s[k+1]是等价的,所以此时 f [ k + 1 ] = 2 f [ k ] − f [ l − 1 ] f[k+1]=2f[k]-f[l-1] f[k+1]=2f[k]−f[l−1]。当然如果 s [ k + 1 ] s[k+1] s[k+1]在之前没出现过,则 f [ k + 1 ] = 2 f [ k ] f[k+1]=2f[k] f[k+1]=2f[k]。每个字符的最后一次出现的位置可以用一个哈希表记录。代码如下:
public class Solution {
public int distinctSubseqII(String s) {
int n = s.length(), MOD = (int) (1e9 + 7);
// 记录每个字母出现的最后位置,下标从1开始
int[] pos = new int[26];
long[] f = new long[n + 1];
f[0] = 1;
for (int i = 1; i <= n; i++) {
char ch = s.charAt(i - 1);
f[i] = f[i - 1] * 2 % MOD;
if (pos[ch - 'a'] > 0) {
f[i] = (f[i] - f[pos[ch - 'a'] - 1] + MOD) % MOD;
}
pos[ch - 'a'] = i;
}
return (int) (f[n] - 1);
}
}
时空复杂度 O ( n ) O(n) O(n)。
法2:序列自动机 + 记忆化DFS。设
s
[
i
]
s[i]
s[i]是
s
s
s的第
i
i
i个字母,此处
i
i
i从
1
1
1开始计数,
s
[
0
]
s[0]
s[0]定义为空字符。构造
s
s
s的序列自动机,即
A
[
u
]
[
c
]
A[u][c]
A[u][c]指的是
s
[
u
]
s[u]
s[u]之后字母
c
c
c第一次出现的位置,如果未出现则定义为
0
0
0。
f
[
u
]
f[u]
f[u]是
s
[
u
+
1
:
]
s[u+1:]
s[u+1:]的本质不同子序列的个数(含空序列)。那么
f
[
u
]
f[u]
f[u]两种可能,或者空序列,或者是接上
s
[
u
+
1
:
]
s[u+1:]
s[u+1:]之后的第一个'a', 'b'
等等如果有的话,然后再接
s
[
A
[
u
]
[
c
]
]
s[A[u][c]]
s[A[u][c]]之后的本质不同子序列,其个数是
f
[
A
[
u
]
[
c
]
]
f[A[u][c]]
f[A[u][c]],即
f
[
u
]
=
1
+
∑
A
[
u
]
[
c
]
>
0
f
[
A
[
u
]
[
c
]
]
f[u]=1+\sum_{A[u][c]>0} f[A[u][c]]
f[u]=1+A[u][c]>0∑f[A[u][c]]代码如下:
public class Solution {
public int distinctSubseqII(String s) {
int n = s.length(), MOD = (int) (1e9 + 7);
// 构造序列自动机
int[][] dfa = new int[n + 1][26];
for (int i = n - 1; i >= 0; i--) {
for (int j = 0; j < 26; j++) {
dfa[i][j] = dfa[i + 1][j];
}
dfa[i][s.charAt(i) - 'a'] = i + 1;
}
// 还要把空序列减掉
return dfs(0, dfa, MOD, new int[n + 1]) - 1;
}
private int dfs(int u, int[][] dfa, int MOD, int[] f) {
if (f[u] > 0) {
return f[u];
}
f[u] = 1;
for (int i = 0; i < 26; i++) {
if (dfa[u][i] > 0) {
f[u] = (f[u] + dfs(dfa[u][i], dfa, MOD, f)) % MOD;
}
}
return f[u];
}
}
时空复杂度 O ( n ) O(n) O(n)。
法3:动态规划。还有另一种思路,设
f
[
i
]
[
c
]
f[i][c]
f[i][c]是
s
s
s前
i
i
i个字母中的以字母
c
c
c为结尾的不同子序列的个数。那么答案就是
∑
c
=
0
26
f
[
n
]
[
c
]
\sum_{c=0}^{26} f[n][c]
∑c=026f[n][c](我们用
0
0
0指代字母a
,以此类推)。考虑
f
[
i
]
[
c
]
f[i][c]
f[i][c],设
s
s
s下标从
1
1
1开始,如果
c
≠
s
[
i
]
c\ne s[i]
c=s[i],那么就不可能以
s
[
i
]
s[i]
s[i]结尾,所以不同的方案数就是
f
[
i
−
1
]
[
c
]
f[i-1][c]
f[i−1][c];否则,
f
[
i
]
[
c
]
f[i][c]
f[i][c]的任意一个方案都可以将最后一个字母看成是使用了
s
[
i
]
s[i]
s[i],这样可以考虑倒数第二个字母是什么,可以不存在倒数第二个,那么方案数就是
1
1
1,也可以存在倒数第二个字母,方案数为
∑
c
=
0
26
f
[
i
−
1
]
[
c
]
\sum_{c=0}^{26} f[i-1][c]
∑c=026f[i−1][c],所以总方案数即为
1
+
∑
c
=
0
26
f
[
i
−
1
]
[
c
]
1+\sum_{c=0}^{26} f[i-1][c]
1+∑c=026f[i−1][c]。一路递推出
f
[
n
]
f[n]
f[n]即可。代码如下:
class Solution {
public:
int distinctSubseqII(string s) {
int MOD = 1e9 + 7;
int n = s.size(), f[n + 1][26];
memset(f, 0, sizeof f);
for (int i = 1; i <= n; i++) {
f[i][s[i - 1] - 'a'] = 1;
for (int j = 0; j < 26; j++)
if (s[i - 1] - 'a' != j)
f[i][j] = f[i - 1][j];
else
for (int k = 0; k < 26; k++) f[i][j] = (f[i][j] + f[i - 1][k]) % MOD;
}
int res = 0;
for (int x : f[n]) res = (res + x) % MOD;
return res;
}
};
时空复杂度 O ( n ) O(n) O(n)。
我们发现,对于 f [ i ] f[i] f[i],除了 f [ i ] [ s [ i ] ] f[i][s[i]] f[i][s[i]],其余的 f [ i ] [ c ] = f [ i − 1 ] [ c ] f[i][c]=f[i-1][c] f[i][c]=f[i−1][c],代码可以优化:
class Solution {
public:
int distinctSubseqII(string s) {
int MOD = 1e9 + 7;
int n = s.size(), f[26] = {};
for (int i = 1; i <= n; i++) {
int t = 1;
for (int k = 0; k < 26; k++) t = (t + f[k]) % MOD;
f[s[i - 1] - 'a'] = t;
}
int res = 0;
for (int x : f) res = (res + x) % MOD;
return res;
}
};
时间复杂度一样,空间 O ( 1 ) O(1) O(1)。