这是我参与11月更文挑战的第12天,活动详情查看:2021最后一次更文挑战」
115.不同的子序列
原题目:
115. 不同的子序列
给定一个字符串 s 和一个字符串 t ,计算在 s 的子序列中 t 出现的个数。
字符串的一个 子序列 是指,通过删除一些(也可以不删除)字符且不干扰剩余字符相对位置所组成的新字符串。(例如,"ACE" 是 "ABCDE" 的一个子序列,而 "AEC" 不是)
暴力解法
对于这个题目,最简单的方法就是:
-
我们只考虑第s第M个字符和T的第N个字符匹配的情况:
- 如果匹配了,我们就开一个递归接下去考虑第M+1个字符和第N+1个字符匹配的情况
- 然后不管匹配与否,在本方法中,考虑第M+1个字符和N的匹配情况
- 边界条件就是,如果N+1越界了,那么就代表我们匹配结束了
这样,我们就可以把所有情况都考虑到。
那么,我们可以通过递归写出以下暴力代码:
public int numDistinct(String s, String t) {
return numDistinctFrom(s,t,0,0);
}
public int numDistinctFrom(String s,String t,int nxtIdx,int cur){
if(nxtIdx>t.length()-1) return 1;
if(cur>s.length()-1) return 0;
int res = 0;
for (int i = cur; i < s.length(); i++) {
if(s.charAt(i)==t.charAt(nxtIdx)) res+= numDistinctFrom(s,t,nxtIdx+1,i+1);
}
return res;
}
结果:超时
做匹配情况的记录
我们深入分析下s和t的匹配情况:
- 如果发生了匹配,我们可以从任何位置和t的第一个字符匹配的地方开始;更换匹配结果和匹配开始位置,这个结论都是成立的
- 任何不同的选择都让结果+1
其实我们就可以这样认为:
-
如果到S的第M个字符,和t的第N个字符匹配,且在M之前和t的第N-1个字符匹配的不同情况有w个,那么:
-
到S的第M个字符为止,和T的第N个字符匹配的不同情况,也为W个
-
例如:
假设:我们到第m个字符可以匹配1个字符的是2次,2个字符的是1次 那么到第m+1个字符,如果m+1=t(2),此时可以匹配第二个字符的有(2+自身匹配)=3种可能,以此类推
-
那么我们就可以根据这个推论,来设计算法:
-
顺序遍历S字符串,并记录到遍历节点为止,和t到第n个字符为止的匹配次数为a[n]。
-
我们维护一个当前匹配最大字符数的值为peek且peek<t字符串长度,方便我们在匹配未结束之前减小我们的计算次数
-
每一次遍历中假设S的字符为M,我们只需要判断t在[0,peek]之间的n,M是否等于t(n)
-
如果发生了相等,就意味着:
- S从0到到M-1,和取t的前n-1个字符比对,可以有a[n-1]种方法搞到相同的子串,此时又发生了匹配,那么意味着,此时a[n]就多了a[n-1]种选择。
最后,我们返回a[t.length]即可。
把上面的逻辑写成代码如下:
public int numDistinct2(String s, String t) {
int[] n = new int[t.length()+1];
int peek = 0;
//初始化
n[0] = 1;
for (int i = 0; i < s.length(); i++) {
//这里需要倒着来防止重复计算
for (int i1 = peek; i1 >= 0; i1--) {
if(s.charAt(i) == t.charAt(i1)){
n[i1+1]+=n[i1];
}
}
if(peek<t.length()-1 && n[peek+1]!=0){
peek++;
}
}
return n[t.length()];
}
结果:
执行用时:6 ms, 在所有 Java 提交中击败了92.92%的用户
内存消耗:36.6 MB, 在所有 Java 提交中击败了93.87%的用户
和官方的题解相比,只用了一维数组来记录计算结果,因此内存消耗要更低一些。