机器人从轨道长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,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。