题目
官方思路(动态规划)
用 f [ i ] f[i] f[i]表示以 s [ i ] s[i] s[i]为最后一个字符的子序列的数目。
情况一
如果子序列中只有 s [ i ] s[i] s[i]一个字符, f [ i ] = 1 f[i]=1 f[i]=1
情况二
如果子序列中至少有两个字符,那么可以枚举倒数第二个字符来进行状态转移。可以想到的是:倒数第二个字符可以选择 s [ 0 ] , s [ 1 ] , . . . , s [ i − 1 ] s[0],s[1],...,s[i-1] s[0],s[1],...,s[i−1]中的某一个,这样就可以得到如下的状态转移方程:
f [ i ] = f [ 0 ] + f [ 1 ] + . . . + f [ i − 1 ] f[i]=f[0]+f[1]+...+f[i-1] f[i]=f[0]+f[1]+...+f[i−1]
然而,这样做会产生重复的计数。
如果 s [ j 0 ] s[j_0] s[j0]和 s [ j 1 ] s[j_1] s[j1]这两个字符不相同,那么 f [ j 0 ] f[j_0] f[j0]和 f [ j 1 ] f[j_1] f[j1]对应的子序列是两个不相交的集合。
【我的理解】因为 s [ j 0 ] s[j_0] s[j0]和 s [ j 1 ] s[j_1] s[j1]这两个字符不相同,所以以 s [ j 0 ] s[j_0] s[j0]和 s [ j 1 ] s[j_1] s[j1]这两个字符结尾的所有子序列也都不可能相同
但是,如果 s [ j 0 ] s[j_0] s[j0]和 s [ j 1 ] s[j_1] s[j1]这两个字符相同,那么 f [ j 0 ] f[j_0] f[j0]和 f [ j 1 ] f[j_1] f[j1]对应的子序列会包含重复的项。最简单的一个重复项就是只包含一个字符的子序列 s [ j 0 ] s[j_0] s[j0]或者 s [ j 1 ] s[j_1] s[j1]本身
【我的理解】因为 s [ j 0 ] s[j_0] s[j0]和 s [ j 1 ] s[j_1] s[j1]这两个字符相同,假设 j 0 < j 1 j_0 < j_1 j0<j1,那么以 s [ j 1 ] s[j_1] s[j1]结尾的所有可能的子序列一定包含了以 s [ j 0 ] s[j_0] s[j0]结尾的所有可能的子序列
如何防止重复计数?
可以发现,如果 j 0 < j 1 j_0 < j_1 j0<j1,那么 f [ j 0 ] f[j_0] f[j0]一定是 f [ j 1 ] f[j_1] f[j1]的真子集,这是因为:
每一个以 s [ j 0 ] s[j_0] s[j0]为最后一个字符的子序列,都可以把这个字符改成完全相同的 s [ j 1 ] s[j_1] s[j1],计入到 f [ j 1 ] f[j_1] f[j1]中。
【我的理解】就是把 f [ j 0 ] f[j_0] f[j0]的值作为 f [ j 1 ] f[j_1] f[j1]的一部分,加到 f [ j 1 ] f[j_1] f[j1]中
因此,对于每一种字符,我们只需要找到其最后一次出现的位置(并且在位置i之前),并将其对应的 f f f值累加进 f [ i ] 即可 f[i]即可 f[i]即可。
由于本题中字符串只包含小写字母,我们可以用 l a s t [ k ] last[k] last[k]记录第 k ( 0 ≤ k < 26 ) k(0 \leq k < 26) k(0≤k<26)个小写字母最后一次出现的位置。如果它还没出现过,那么 l a s t [ k ] = − 1 last[k]=-1 last[k]=−1。这样正确的状态转移方程是:
f [ i ] = ∑ 0 ≤ k < 26 , l a s t [ k ] ≠ − 1 f [ l a s t [ k ] ] f[i] = \sum_{0 \leq k <26,last[k] \neq -1} f[last[k]] f[i]=0≤k<26,last[k]=−1∑f[last[k]]
将两种情况合并在一起
最终的状态转移方程为:
f [ i ] = 1 + ∑ 0 ≤ k < 26 , l a s t [ k ] ≠ − 1 f [ l a s t [ k ] ] f[i] = 1 + \sum_{0 \leq k <26,last[k] \neq -1} f[last[k]] f[i]=1+0≤k<26,last[k]=−1∑f[last[k]]
再计算完f[i]后,需要记得更新对应的last项。最终答案即为:
∑ 0 ≤ k < 26 , l a s t [ k ] ≠ − 1 f [ l a s t [ k ] ] \sum_{0 \leq k <26,last[k] \neq -1} f[last[k]] 0≤k<26,last[k]=−1∑f[last[k]]
Java代码
Java中int的取值范围是 [ − 2 31 , 2 31 − 1 ] [-2^{31},2^{31}-1] [−231,231−1]
所以,用int定义要取余的数是肯定可以的
import java.util.Arrays;
class Solution {
public int distinctSubseqII(String s) {
final int MOD = 1000000007;
//last数组初始化
int[] last = new int[26];
Arrays.fill(last, -1);
//获取字符串的长度
int n = s.length();
//f[i]表示以s[i]为最后一个字符的子序列的数目
int[] f = new int[n];
//如果每个子序列,只有一个字符,那么只有一种方案,所以将f的每个元素初始化为1
Arrays.fill(f, 1);
for (int i = 0; i < n; i++) {
for (int j = 0; j < 26; j++) {
//这里不等于-1的last[j],j对应的26个字母,一定是出现在s[i]之前的
if (last[j] != -1) {
f[i] = (f[i] + f[last[j]]) % MOD;
}
}
last[s.charAt(i) - 'a'] = i;
}
int ans = 0;
for (int i = 0; i < 26; i++) {
if (last[i] != -1) {
ans = (ans + f[last[i]]) % MOD;
}
}
return ans;
}
}
举例说明
用具体实例,结合代码,结合理论,这样理解的更快
输入:“abc”
last数组的长度是26,初始化每个元素为-1
f数组,f[i]表示以s[i]为最后一个字符的子序列数目
进入两层for循环:
- i = 0,这时内层for循环j从0到25,所有的last[j]都是-1,所以不会进入if条件判断,last[0] = 0,表示字母a(last数组中对应的下标0)的最后出现的位置在0
以s[0]结尾的子序列所有可能的情况:
- a
以s[0]结尾的子序列的数目是1
- i=1,这时内层for循环j从0到25,除了last[0]=0之外,其余的都是-1,说明s[1]之前的字符种类只有一个(是字符种类,而不是字符个数),所以,以s[1]结尾的子序列的数目f[1]需要更新,除了原有的只有s[1]一个字符这一种情况以外,还需要加上以s[0]作为字符串结尾的子序列的数目,f[1]=f[1]+f[last[0]],其中last[0]=0,所以,f[1]=f[1]+f[0]=1+1=2,最后更新last[1]=1,表示字母b最后出现的位置在1
以s[1]结尾的子序列所有可能的情况:
- b —> 对应着刚初始化以后的f[1]=1
- ab —> 对应着f[1] = f[last[0]] + f[1],其中last[0]=0,也就是f[1] = f[0] + f[1] = 1 + 1 = 2
以s[1]结尾的子序列的数目是2
- i=2,这时内存for循环j从0到25,除了last[0]=0,last[1]=1之外,其余都是-1,说明,s[2]之前的字符种类只有两个(是字符种类,而不是字符个数),所以,以s[2]结尾的子序列的数目f[2]需要更新,除了原有的s[2]一个字符这一种情况以外,还需要加上以s[1]和s[0]作为字符串结尾的子序列的数目,f[2]=f[2]+f[last[0]]+f[last[1]],其中last[0]=0,last[1]=1,所以,f[2]=f[2]+f[0]+f[1]=1+1+2=4,最后更新last[2]=2,表示字母c最后出现在位置2
以s[2]结尾的子序列所有可能的情况
- c —> 对应着刚初始化以后的f[2]=1
- ac —> 对应着以s[0]结尾的子序列数目f[last[0]]=f[0]
- bc、abc —> 对应着以s[1]结尾的子序列数目f[last[1]]=f[1]
以s[2]结尾的子序列数目为f[2]=f[2]+f[0]+f[1]=1+1+2=4
输入:“aba”
-
i=0,i=1的过程和上面一个输入的情况是一样的,这里不在赘述
-
i=2,f[2]也是按照之前的思路进行更新,f[2] = f[2] + f[last[0]] + f[last[1]] = f[2] + f[0] + f[1] = 1 + 1 + 2 = 4,最后更新last[0]=2【这时和上一个输入不一样的地方】,表示字母a最后出现在位置2
以s[2]结尾的子序列所有可能的情况
- a —> 对应着刚初始化以后的f[2]=1
- aa —> 对应着以s[0]结尾的子序列数目f[last[0]]=f[0]
- ba、aba —> 对应着以s[1]结尾的子序列数目f[last[1]]=f[1]
以s[2]结尾的子序列数目为f[2]=f[2]+f[0]+f[1]=1+1+2=4
因为有两个相同的a,last[0]由原来的等于0,现在修改为等于2,这样至少在我们能想到的以a结尾的单个字符只会被计算一遍,所以在最后计算所有可能的子串的数量时,只会加一次字母a在f数组中对应的值