1.暴力递归:设有S和T两个字符串,我们一个一个字符从前往后来看:设i为在S中的位置,j为在T中的位置。初始时i,j为0,在两个字符串往后匹配的过程中分为两种情况:
①S[i]==T[j]
当前有匹配,可以都向后移一位,也可以只把i向后移一位。
②S[i]!=T[j]
当前无匹配,i向后移一位.
直到i=S.length(说明这条路没有匹配,返回0)或者j=T.length(说明这条路匹配了一次,返回1)
如图,递归的本质就是一个压栈出栈的树形遍历过程,最终能到达目的节点的路径即为一次成功的匹配。
代码:
public int numDistinct(String s, String t) {
return helper(s,t,0,0);
}
public int helper(String s,String t,int i,int j){
if (j==t.length())return 1;//到达终点返回结果
if (i==s.length())return 0;
int a= 0;
a=helper(s,t,i+1,j);//无论相等不相等都有的操作
if (s.charAt(i)==t.charAt(j))
a+=helper(s,t,i+1,j+1);//相等特有的操作
return a;
}
2.优化带记忆的递归:
通过上图我们不难发现,有许多递归的路径都指向的同一节点
这就造成了重复计算(不仅仅是一个节点的重复计算,而是重复节点后的所有可能路径的重复计算!)
所以我们想办法把已经计算出的结果保留下来,以便下次计算需要此结果时直接取用。
整个问题无非是i,j的变化,所以我们可以维护一个二维数组储存已经计算好的结果(也可以是一个hashmap<i&j,value>)
dp[i][j]表示i,j进行到当前后续的结果(i,j)这个结点后续所有可能路径结果的集合。(结合图示,本质还是在i,j二维数组上跑)
0代表还没遍历到,-1代表已经遍历到但是没有匹配结果(0),非0代表有结果。
int[][] dp;
public int numDistinct(String s, String t) {
dp=new int[s.length()][t.length()];
return helper(s,t,0,0);
}
public int helper(String s,String t,int i,int j){
if (j==t.length())return 1;
if (i==s.length())return 0;
if (dp[i][j]==-1)return 0;//已经遍历过没结果
if (dp[i][j]>0)return dp[i][j];//已经遍历过有结果直接返回结果
int a= 0;
a=helper(s,t,i+1,j);
if (s.charAt(i)==t.charAt(j))
a+=helper(s,t,i+1,j+1);
dp[i][j]=a==0?-1:a;//记录当前递归返回的结果
return a;
}
3.动态规划
到这里,我们发现,不过是在和二维数组打交道而已!严格来说是因为只有i,j两个标在运动所以我们使用二维数组,其他问题还可能是一维或者三维的。
我们把上面的二维数组抽出来,不在用递归填表了。
还是那张表,这里表的意义有些变了(本质还是一样)dp[i][j]代表T的前j个字符组成的子串在S的前i个字符组成的子串的不同匹配方式。
按照递归的两种情况分析:
①S[i]==T[j]
当前有匹配,可以都向后移一位,也可以只把i向后移一位。
则T(0,j)在S(0,i)中的匹配方式的数量可表示为:dp[i][j]=dp[i-1][j-1]+dp[i-1][j]
②S[i]!=T[j]
当前无匹配,i向后移一位.
dp=dp[i-1][j]
整个填表的过程如下(是否和递归神似呢?)
到了重复的地方便可以直接取用表中数据
初始条件:T为空串在S中匹配次数都设为1
public int numDistinct(String s, String t) {
//动态规划
int s_length=s.length();
int t_length=t.length();
int[][] dp=new int[s_length+1][t_length+1];
for (int i=0;i<s.length();i++){
dp[i][0]=1;
}
for (int i=0;i<s_length;i++){
for (int j=0;j<t_length;j++){
if (s.charAt(i)==t.charAt(j))
dp[i+1][j+1]=dp[i][j]+dp[i][j+1];
else dp[i+1][j+1]=dp[i][j+1];
}
}
return dp[s_length][t_length];
}
4.动态规划的优化
空间优化:二维变一维:
在二维数组中,我们只用到当前位置上方的元素([i-1][j])与左上方的元素[i-1][j-1],所以我们可以只用一个长度为s.length的一维数组来保存当前行的元素已经上一行的元素(当前行元素会覆盖上一行元素),用pre保存左上方的元素。
public int numDistinct(String s, String t) {
//动态规划
int s_length = s.length();
int t_length = t.length();
int[] dp = new int[s_length + 1];
for (int i=0;i<=s_length;i++)
dp[i]=1;
for (int j = 0; j < t_length; j++) {
int pre=dp[0];
dp[0]=0;
for (int i = 0; i < s_length; i++) {
int temp=dp[i+1];
if (s.charAt(i)==t.charAt(j))
dp[i+1]=dp[i]+pre;
else dp[i+1]=dp[i];
pre=temp;
}
}
return dp[s_length];
}
再优化:
倒序计算,从左往右填每一行,这样就省去了pre
public int numDistinct(String s, String t) {
//动态规划
int s_length = s.length();
int t_length = t.length();
int[] dp = new int[t_length + 1];
dp[0]=1;
for (int i=0;i<s_length;i++){
for (int j=t_length-1;j>=0;j--){
if (s.charAt(i)==t.charAt(j))
dp[j+1]+=dp[j];//从后往前覆盖上一行的数据
}
}
return dp[s_length];
}
这就到极限了?
继续优化:
填表的过程中,我们每次都要对T进行遍历,只是为了寻找与S[i]的T[j]。所以为了避免遍历T,可以把T的每个字符的索引保存在map里,比较的时候直接取用。用一个map[128]数组保存字符索引(因为字符最多也不过128个)。这里考虑重复元素,用nexts[t.length]来储存重复元素的索引,比如
T=rabbit,则nexts={-1,-1,-1,2,-1,-1} 若T=rabbbit则为{-1,-1,-1,2,3,-1,-1} -1代表单一元素。
public int numDistinct(String s, String t) {
//动态规划
int s_length = s.length();
int t_length = t.length();
int[] dp = new int[t_length + 1];
dp[0]=1;
int[] map=new int[128];//建立索引,字符可以用两个字节表示
Arrays.fill(map,-1);
int[] nexts=new int[t.length()];//记录重复字符的下一个索引
for (int i=0;i<t.length();i++){
int c=t.charAt(i);
nexts[i]=map[c];
map[c]=i;
}
for (int i:nexts)
System.out.print(i);
for (int i=0;i<s_length;i++){
char c=s.charAt(i);
for (int j=map[c];j>=0;j=nexts[j])
dp[j+1]+=dp[j];
}
return dp[t_length];
}