机器人从轨道长N上的起点M出发,走到P点,必须走K步,请问有多少种走法

机器人从轨道长N上的起点M出发,走到P点,必须走K步,请问有多少种走法?

提示:阿里巴巴的面试原题!

互联网大厂的动态规划题目中的四种经典暴力递归尝试模型:
(1)DP1:从左往右的尝试模型,关注i位置结尾,或者i位置开头的情况,或者看i联合i+1,i+2的情况,填表往往是上到下,或者下到上,左到右,右到左。
(2)DP2:从L–R范围上的尝试模型,关注L和R的情况,填表格式非常固定,主对角,副对角,倒回来填
(3)DP3:多样本位置对应的尝试模型,2个样本,一个样本做行,一个样本做列,关注i和j对应位置的情况,先填边界,再填中间
(4)DP4:业务限制类的尝试模型,比如走棋盘,固定的几个方向可以走,先填边界,再填中间。

——本题是DP4:业务限制类的尝试模型


题目

轨道长N,即1–N是轨道编号
机器人从起点M出发,走到P点,必须走K步,请问有多少种走法?
1<=M<=N
1<=P<=N
当机器人来到1点,只能逆转反向往右走,即走到2
当机器人来到N点,只能逆转反向往左走,即走到N-1


一、审题

示例:
机器人从轨道长N=7上的起点M=3出发,走到P=2点,必须走K=3步,请问有多少种走法?
在这里插入图片描述
从3出发,必须走3步,最后得走到2去
那么三种走法,绿色路径那3种,就是咱的答案数量。


暴力递归求方案数量

咱们这么定义,让起始点M和必须要走K步做变量,N轨道长度固定,P终点要去的地方固定
**定义f(N,M,K,P)**为机器人,从N长轨道的当前M起点出发,必须走K步,最终到达P点的走法有多少种?

递归里面怎么处理,拿到走法数量返回呢?

(1)不妨设此刻K=0了,如果你当前位置M不等于P的话,不要意思你没法完成任务,返回0方案数,无效!如果M=P的话,OK,恰好完成任务,返回有效方案数量1种。
在这里插入图片描述

还有剩余步数K>0的话,还得继续走,因为机器人必须走K步,那就继续走呗!
(2)当机器人来到1点,只能逆转反向往右走,即下一次走到2,返回f(N,2,K-1,P)
在这里插入图片描述
(3)当机器人来到N点,只能逆转反向往左走,即下一次走到N-1,返回f(N,N-1,K-1,P)
(4)上面条件都没中,OK,处于中间位置,好说,往左试试p1种可能性,往右试试p2种可能性,返回p1+p2,
其中:p1=返回f(N,M-1,K-1,P),p2=返回f(N,M+1,K-1,P)
在这里插入图片描述

主函数怎么调用?
就调用f(N,M,K,P)
是不是很简单,其实这个阿里的题目,还真的就非常非常简单,你只要学会动态规划的知识,知道用哪些尝试模型,就能轻易把递归函数搞出来,关键就在这!!

手撕代码:

//复习:
    //**定义f(N,M,K,P)**为机器人,从N长轨道的当前M起点出发,必须走K步,最终到达P点的走法有多少种?
    public static int f(int N, int M, int K, int P){
        //(1)不妨设此刻K=0了,如果你当前位置M不等于P的话,不要意思你没法完成任务,返回0方案数,无效!
        // 如果M=P的话,OK,恰好完成任务,返回有效方案数量1种。
        if (K == 0) return M == P ? 1 : 0;
        //还有剩余步数K>0的话,还得继续走,因为机器人必须走K步,那就继续走呗!
        //(2)当机器人来到1点,只能逆转反向往右走,即下一次走到2,返回f(N,2,K-1,P)
        if (M == 1) return f(N, 2, K - 1, P);
        //(3)当机器人来到N点,只能逆转反向往左走,即下一次走到N-1,返回f(N,N-1,K-1,P)
        if (M == N) return f(N, N - 1, K - 1, P);
        //(4)上面条件都没中,OK,处于中间位置,好说,往左试试p1种可能性,往右试试p2种可能性,返回p1+p2,
        //其中:p1=返回f(N,**M-1**,K-1,P),p2=返回f(N,**M+1**,K-1,P)
        return f(N, M - 1, K - 1, P) + f(N, M + 1, K - 1, P);
    }
    //主函数怎么调用?
    //就调用f(N,M,K,P)
    public static int robotSteps(int N, int M, int K, int P){
        if (N < 1 || M < 1 || M > N || P < 1 || P > N) return 0;

        return f(N, M, K, P);
    }

    public static void test(){
        System.out.println(findWalks(7, 2, 3, 3));
        System.out.println(robotSteps(7, 3, 3, 2));
    }
测试结果:

```java
3
3

这暴力递归,如果范围太大,自然速度就很慢了
变量是M起点,和剩余步数K必须走
如果我们求f(M=3,K=4)
如下图所示,在递归过程中,咱们可以往左走,可以往右走
f(M=3,K=4)依赖f(M=2,K=3)【向左走1步】,f(M=4,K=3)【向右走1步】
其中,f(M=2,K=3)依赖f(M=1,K=2)【向左走1步】,f(M=3,K=2)【向右走1步】
其中,f(M=4,K=3)依赖f(M=3,K=2)【向左走1步】,f(M=5,K=2)【向右走1步】
在这里插入图片描述
发现了吗???重复去递归求f(M=3,K=2)了
这是暴力递归存在的缺陷,中间重复求很多相同的值,浪费时间!!!!


笔试AC解,暴力递归改傻缓存填表dp

为了避免递归函数,在中间重复求很多相同的值,浪费时间
咱们可以很简单解决这个问题,也是笔试AC的经典解决方案,用一个傻缓存dp表,把它记录在M行,K列,就得了!!!
下次在遇到同一个M和K,直接拿结果,不要再去递归求了!!!!

这就是笔试过程中,加速算法的最好解决方案
既然用空间换时间
用傻缓存在中间过程保存值,这就是动态规划!!!
动态保存结果。

咱们在f中带一个参数,dp表
dp表,M做行,K做列,M下一次能去哪里就四种情况,遇到1右走,遇到N左走,任意位置左右两个方向走,因此本题叫做DP4:业务限制类的尝试模型!!每个业务方向是限制死的。
这里的话,把M做行,K做列,类似于样本位置对应的模型,但是DP2:样本位置对应模型的本质,是2个不同的样本对应:x样本的i位置和y样本的j位置对应。这里只能是起点M和K步的组合,还不是多样本位置对应。

f(N, M, K, P, dp) 代表从M起点出发,要求K步,最终走到P的走法有多少种方案,放入 dp[M][K]

咱们完全根据上面暴力递归的代码来改写这个傻缓存的代码
首先,咱们在主函数中,准备一个dp表,里面先全部放-1,认为没有求过结果
M可以取0–N,那就是N+1长度
K可以取0–K,那就是K+1长度
所以dp表,是一个二维的N+1 × K+1的表格
每一个格子dp[M][K]代表从M起点出发,要求K步,最终走到P的走法有多少种方案
在这里插入图片描述
OK,咱们dp[M][K]=-1,就是没求过,需要递归求,如果进到f,发现dp[M][K]!=-1,那就求过了,直接返回dp[M][K],别再递归浪费时间了!!!
手撕代码:

//复习:
    //**定义f(N,M,K,P)**为机器人,从N长轨道的当前M起点出发,必须走K步,最终到达P点的走法有多少种?
    public static int fDP(int N, int M, int K, int P, int[][] dp){
        //dp[M][K]=-1,就是没求过,需要递归求,如果进到f,发现dp[M][K]!=-1,那就求过了,直接返回dp[M][K]
        if (dp[M][K] != -1) return dp[M][K];
        //(1)不妨设此刻K=0了,如果你当前位置M不等于P的话,不要意思你没法完成任务,返回0方案数,无效!
        // 如果M=P的话,OK,恰好完成任务,返回有效方案数量1种。
        if (K == 0) {
            dp[M][K] = M == P ? 1 : 0;
            return dp[M][K];
        }
        //还有剩余步数K>0的话,还得继续走,因为机器人必须走K步,那就继续走呗!
        //(2)当机器人来到1点,只能逆转反向往右走,即下一次走到2,返回f(N,2,K-1,P)
        if (M == 1) {
            dp[M][K] = fDP(N, 2, K - 1, P, dp);
            return dp[M][K];
        }
        //(3)当机器人来到N点,只能逆转反向往左走,即下一次走到N-1,返回f(N,N-1,K-1,P)
        if (M == N) {
            dp[M][K] = fDP(N, N - 1, K - 1, P, dp);
            return dp[M][K];
        }
        //(4)上面条件都没中,OK,处于中间位置,好说,往左试试p1种可能性,往右试试p2种可能性,返回p1+p2,
        //其中:p1=返回f(N,**M-1**,K-1,P),p2=返回f(N,**M+1**,K-1,P)
        dp[M][K] = fDP(N, M - 1, K - 1, P, dp) + fDP(N, M + 1, K - 1, P, dp);
        return dp[M][K];
    }
    //主函数怎么调用?
    //就调用f(N,M,K,P)
    public static int robotStepsDP(int N, int M, int K, int P){
        if (N < 1 || M < 1 || M > N || P < 1 || P > N) return 0;

        int[][] dp = new int[N + 1][K + 1];
        for (int i = 0; i < N + 1; i++) {
            for (int j = 0; j < K + 1; j++) {
                dp[i][j] = - 1;//标记为没求过
            }
        }
        return fDP(N, M, K, P, dp);
    }

    public static void test2(){
        System.out.println(findwk(117, 113, 112, 3));
        System.out.println(robotStepsDP(117, 112, 3, 113));
    }

测试结果:

3
3

你会发现傻缓存的速度那叫一个快!!


面试精华解,暴力递归改动态规划填表dp

上面的笔试AC在笔试过程中,编写代码速度快,而且能AC,即可
面试还需要精细化改为DP动态规划的填表过程
把转移方程搞出来,直接填一个二维表dp,然后返回我们要的值
dp表,i做行,j做列,i下一次能去哪里就四种情况,遇到1右走,遇到N左走,任意位置左右两个方向走,因此本题叫做DP4:业务限制类的尝试模型!!每个业务方向是限制死的。

和笔试AC解填写傻缓存一样
咱们完全根据上面暴力递归的代码来改写精细化动态规划的代码
首先,咱们在主函数中,准备一个dp表,里面先全部放-1,认为没有求过结果
i做行,就是之前的M,可以取0–N,那就是N+1长度
j做列,就是之前的K,可以取0–K,那就是K+1长度
所以dp表,是一个二维的N+1 × K+1的表格
每一个格子 dp[i][j]
代表从i起点出发(当前来到了i位置),要求必须走j步(或者理解为剩余j步必须要走),最终走到P的走法有多少种方案

咱们直接根据暴力递归的代码:整转移方程:

//(1)不妨设此刻K=0了,如果你当前位置M不等于P的话,不要意思你没法完成任务,返回0方案数,无效!
        // 如果M=P的话,OK,恰好完成任务,返回有效方案数量1种。
        if (K == 0) return M == P ? 1 : 0;

咱们把j=0列填一下,根据M与P的关系填写,就是表格中的绿色问号,都能填好哦!
在这里插入图片描述

//(2)当机器人来到1点,只能逆转反向往右走,即下一次走到2,返回f(N,2,K-1,P)
        if (M == 1) return f(N, 2, K - 1, P);
        //(3)当机器人来到N点,只能逆转反向往左走,即下一次走到N-1,返回f(N,N-1,K-1,P)
        if (M == N) return f(N, N - 1, K - 1, P);

然后,咱吧i=1行,填一下,发现dp[1][j]依赖dp[2][j -1],也就是依赖1 j格子的左下角那个格子,发现没
同理,咱把i=N行,填一下,发现dp[N][j]依赖dp[N-1][j -1],也就是依赖N j格子的左上角那个格子,发现没
图中用橘色标记的依赖
在这里插入图片描述

//(4)上面条件都没中,OK,处于中间位置,好说,往左试试p1种可能性,往右试试p2种可能性,返回p1+p2,
        //其中:p1=返回f(N,**M-1**,K-1,P),p2=返回f(N,**M+1**,K-1,P)
        return f(N, M - 1, K - 1, P) + f(N, M + 1, K - 1, P);

任意位置dp[i][j] = dp[i - 1][j-1] + dp[i + 1][j-1],也就是粉色那个位置i j,依赖左上角,左下角的俩格子
在这里插入图片描述
因为被依赖的那些位子,已经填好了,所以后续所有的格子,都能根据这个依赖填完
后续,从左往右,一列一列地调度填表

咱们最后要啥结果呢?
dp[M][K]
图中五角星那个,就机器人从M出发,必须走K步,能有多少种走法?
这不就是咱要的结果吗?

手撕代码——一定是源于暴力递归的代码!

//复习精细化改DP
    public static int robotStepsDP2(int N, int M, int K, int P){
        if (N < 1 || M < 1 || M > N || P < 1 || P > N) return 0;

        //每一个格子 **dp[i][j]代表从i起点出发,要求j步,最终走到P的走法有多少种方案**
        int[][] dp = new int[N + 1][K + 1];

        //(1)不妨设此刻K=0了,如果你当前位置M不等于P的话,不要意思你没法完成任务,返回0方案数,无效!
        // 如果M=P的话,OK,恰好完成任务,返回有效方案数量1种。
        for (int i = 0; i < N + 1; i++) {
            dp[i][0] = i == P ? 1 : 0;
        }

        //任意位置if
        for (int j = 1; j < K + 1; j++) {
            //从左往右填
            for (int i = 1; i < N + 1; i++) {//每次从上往下一行
                //还有剩余步数K>0的话,还得继续走,因为机器人必须走K步,那就继续走呗!
                //(2)当机器人来到1点,只能逆转反向往右走,即下一次走到2,返回f(N,2,K-1,P)
                if (i == 1) dp[i][j] = dp[2][j - 1];
                //(3)当机器人来到N点,只能逆转反向往左走,即下一次走到N-1,返回f(N,N-1,K-1,P)
                else if (i == N) dp[i][j] = dp[N - 1][j - 1];
                //(4)上面条件都没中,OK,处于中间位置,好说,往左试试p1种可能性,往右试试p2种可能性,返回p1+p2,
                //其中:p1=返回f(N,**M-1**,K-1,P),p2=返回f(N,**M+1**,K-1,P)
                else dp[i][j] = dp[i - 1][j - 1] + dp[i + 1][j - 1];
                //上面三种只能有其一
            }
        }
        
        return dp[M][K];
    }
    
    public static void test3(){
        //上面填表这玩意不好填,我看记忆搜索法就很好!!,先不管了,准备组会的事情
        System.out.println(finddpwk(7,3,2,3));
        System.out.println(robotStepsDP2(7,2,3,3));
    }
   
    public static void main(String[] args) {
//        test();
//        test2();
        test3();
    }

填好表之后,是这样的
在这里插入图片描述
咱们要的结果就是dp[M][K]=3
结果:3

里面求dp[i][j]的各个式子,就是咱要的转移方程,这些转移方程,不是什么数学高手推到的数学公式!
就是咱们根据当初暴力递归的代码里面的条件转移,搞成dp依赖了,仅此而已……
没有什么高端大气上档次,认为这个转移方程非常困难的说法,绝对不是必须要天才才能推导的那种难于登天的数学公式!!!

本题,一定捋清楚了,就知道动态规划,其实非常非常容易,根本不需要天才,
就是踏踏实实掌握咱们准备的4个DP尝试模型,用其中一个模型一定就能拿下大厂的动态规划题目【我们只说互联网大厂的笔试面试题目,不是比赛,不是学术研究


总结

提示:重要经验:

1)任何动态规划的题目,关键在于暴力递归的尝试模型,有了递归函数,就能知晓变量有几个,最终填几维dp表。
2)笔试AC解,暴力递归改傻缓存填表dp,一般来讲,能改为傻缓存就足矣,有些暴力递归也没法改为精细化dp表,或者笔试就可以这样了。
3)面试的话,一定要捋清楚dp表ij位置的依赖关系,先填边界,再填中间位置,最后得到我们想要的最终解。
4)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

冰露可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值