从暴力递归到记忆递归再到动态规划(从超时到超越100%的历程)

在这里插入图片描述
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];
    }

在这里插入图片描述

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值