暴力递归到动态规划

暴力递归到动态规划

之前刷动态规划的时候,总是很奇怪,他们教程总叫我找状态转移方程。但是我又总是找不到,奇怪他们是怎么找到的,除非是我刷到过的,才有可能做出来。但是在听了左程云老师的暴力递归到动态规划的修改后,我觉得这个思路才是符合逻辑的。左程云老师的课程通俗易懂,就算不刷算法题,单纯的学习解题思路也非常清楚

我一直觉得,算法题没用,但是在实际的刷了几题以后,才发现,写算法其实能锻炼个人的思维,能准确描述自己思路的才能学好算法,而你思维清晰,自然写法流畅了。

案例讲解

单纯的讲有点干,于是就搬了下左老师的课程讲解,复述下。

现在有这么一个赛道,从1到N的位置,有这么一个机器人,当前在S位置,机器人只能向左或者向右走,请问,机器人再走K步,走到E位置的各种走法有几种。

如果一下子让你写状态转移方程,我们肯定是写不出来的

try

最重要的是试的思路,这里很明显思路就是,当前位置,尝试向左或者右。

那什么时候停止呢,我们需要记录一下已经用了几步,当步数用完的时候,如果我们当前位置就是P点,那就是一种走法。

这里有一个问题,就是边界的话,只有一种走法。

那么我们可以简单的写出暴力递归

暴力递归

//一共是1-N这么多位置,固定参数
//最终的目标是E 固定参数
//S起始位置
//K步
public static int walkways(int N,int E,int S,int K){
    return f(N,E,K,S);
}
//一共是1-N这么多位置,固定参数
//最终的目标是E 固定参数
//还剩rest步要走
//当前在cur位置
//返回方法数
public static int f(int N,int E,int rest,int cur){
    if(rest==0){
        return cur==E?1:0;
    }
    //此时rest>0有路可走
    //在边界1时候
    if(cur==1){
        return f(N,E,rest-1,2);
    }
    //在边界N时候
    if(cur==N){
        return f(N,E,rest-1,N-1);
    }
    return f(N,E,rest-1,cur-1)+f(N,E,rest-1,cur+1);
}

这个就是简单的暴力递归。我们可以简单写出这种写法,但是这个写法,他的时间复杂度比较高。时间复杂度是o(2的k次方)

我们可以分析,N是固定参数,E是固定参数,rest是递减的。cur是变化的。

我们可以知道,其实参数固定后,结果是固定的,也就是说,如果我们设定函数f为f(rest,cur)。

那么其实就是求解这个函数的值而已。

那么我们可以举个例子,如1,2,3,4,5这么多位置,当前位置在2,目标位置是4,要走4步。

我们要求解的就是f(4,2)。而我们知道f(4,2)其实取决于f(3,1)和f(3,3)。因为是左走和右走。

那么就可以画出这么一幅图。

​ f(4,2)

​ f(3,1) f(3,3)

​ f(2,2) f(2,2) f(2,4)

​ f(1,1) f(1,3) f(1,1) f(1,3) f(1,3) f(1,5)

​ f(0,2) f(0,2) f(0,4) f(0,2) f(0,4) f(0,2)f(0,2) f(0,4) f(0,4)

我们可以发现,其实我们暴力递归的时候,其实有些结果,已经在之前的暴力递归中求出对应的值了,而我们暴力递归未记忆,还是重走了一遍。那么我们可以在这个基础上,加一个缓存。

因为主要变化的参数就是rest和cur,那么我们可以申请一个二维数组缓存,大小的话,因为rest最大是K,cur

最大是N。所以我们就申请K+1和N+1;

初始化都为-1,如果数据不为-1,那么就用这个结果。

记忆集(记忆化搜索)

public static int walkways2(int N,int E,int S,int K){
    int[][] dp=new int[K+1][N+1];
    for (int i = 0; i < K; i++) {
        for (int j = 0; j < N; j++) {
            dp[i][j]=-1;
        }

    }
    return f2(N,E,K,S,dp);
}
//一共是1-N这么多位置,固定参数
//最终的目标是E 固定参数
//还剩rest步要走
//当前在cur位置
//返回方法数
public static int f2(int N,int E,int rest,int cur,int[][] dp){
    if(dp[rest][cur]!=-1){
        return dp[rest][cur];
    }
    if(rest==0){
        dp[rest][cur]=cur==E?1:0;
        return dp[rest][cur];
    }
    //此时rest>0有路可走
    //在边界1时候
    if(cur==1){
        dp[rest][cur]=f2(N,E,rest-1,2,dp);

    }else if(cur==N){
        dp[rest][cur]=f2(N,E,rest-1,N-1,dp);
    }else{
        dp[rest][cur]=f2(N,E,rest-1,cur-1,dp)+f2(N,E,rest-1,cur+1,dp);
    }

    return dp[rest][cur];
}

我们可以看出,这个只是单纯加了一个缓存,无脑但是有效。因为我们申请的缓存是K+1,N+1的二维数据,每个数组 只算一次,所以我们的时间复杂度O(N*M)。

因为这个只是单纯的加了一个缓存集,但是我们也可以知道,其实我们只是要一个缓存集的数据,然后求缓存集的具体一个格子的数据,递归调用只是为了计算数据而已。

那么我们能不能不通过递归,直接得出格子内之间的关系呢?

动态规划

当然可以,我们翻译一下递归的各个case,其实就能得到具体的关系。

比如:,如1,2,3,4,5这么多位置,当前位置在2,目标位置是4,要走4步。

我们申请的是6*6的格子,横着是当前位置,竖着是剩余次数。我们其实要求的是,2,4这个位置的数据。

012345
0
1
2
3
4*
5

先看case1

 if(rest==0){
        dp[rest][cur]=cur==E?1:0;
        return dp[rest][cur];
    }

即当前位置是0的话,只有当前位置等于目标位置,才为1

当前位置为不会为0

那么

012345
0x00010
1x
2x
3x
4x*
5x
 if(cur==1){
        dp[rest][cur]=f2(N,E,rest-1,2,dp);

当前位置如果为1,那么结果就是当前位置为2,剩余次数为上一个的位置的数。

012345
0x00010
1x0
2x
3x
4x*
5x
}else if(cur==N){
        dp[rest][cur]=f2(N,E,rest-1,N-1,dp);

当前位置如果为N,那么结果就是当前位置为N-1,剩余次数为上一个的位置的数。

  dp[rest][cur]=f2(N,E,rest-1,cur-1,dp)+f2(N,E,rest-1,cur+1,dp);

当前位置的结果,取决于左上角一格和右上角一格的数据和。

那么表格就是

012345
0x00010
1x00100
2x
3x
4x*
5x

继续画下去的话就是

012345
0x00010
1x00101
2x01020
3x10302
4x04050
5x49905

那么我们就可以知道,4,2这个位置就是4.

public static int walkways3(int N,int E,int S,int K){
        int[][] dp=new int[K+1][N+1];
        for (int rest = 0; rest <= K; rest++) {
            for (int cur = 1; cur <= N; cur++) {
                if(rest==0){
                    dp[0][cur]=cur==E?1:0;
                    continue;
                }
                if(cur==1){
                    dp[rest][cur]=dp[rest-1][2];
                }else if(cur==N){
                    dp[rest][cur]=dp[rest-1][N-1];
                }else{
                    dp[rest][cur]=dp[rest-1][cur-1] +dp[rest-1][cur+1];
                }
            }

        }
        return dp[K][S];
    }

那么结果也就知道了,我们也可以按照这个思路,修改出对应的动态规划写法了。

那么我们也可以总结出套路来。

先根据一定的套路尝试,知道普遍的算法,从而写出递归的方法,要求参数尽可能短。(因为我们知道,可变参数联系这后续的动态规划表的大小。),然后优化的话加上缓存,写出记忆集。当然可以不写。

接下来,就是按照之前写的暴力递归的方法得出严格表依赖的动态规划,1.分析可变参数的变化范围(得到表的范围)。2.标出你要计算出答案的位置。3.根据basecase(例子就是当rest==0的时候,的值变化)得出固定值的。4.根据固定依赖得出各个格子间的关系。进行计算。

其实就是根据暴力递归的方法反推而已。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值