跳马问题:马走日,请问马从0,0位置出发,走到x,y目标点,还必须走k步,有多少种走法?
提示:这个题目,被字节跳动改编了,然后放笔试里面考!!
所以好好联系这些题目,都是大厂爱考的算法原型
提示:互联网大厂的经典动态规划题
解决动态规划问题的重要知识!暴力递归的4种经典尝试模型
最关键的就是这个递归函数,解题的核心在这。
互联网大厂的动态规划题目中的四种经典暴力递归尝试模型:
(1)DP1:从左往右的尝试模型,关注i位置结尾,或者i位置开头的情况,或者看i联合i+1,i+2的情况,填表往往是上到下,或者下到上,左到右,右到左。
(2)DP2:从L–R范围上的尝试模型,关注L和R的情况,填表格式非常固定,主对角,副对角,倒回来填
(3)DP3:多样本位置对应的尝试模型,2个样本,一个样本做行,一个样本做列,关注i和j对应位置的情况,先填边界,再填中间
(4)DP4:业务限制类的尝试模型,比如走棋盘,固定的几个方向可以走,先填边界,再填中间。
本题是——DP4:业务限制类的尝试模型
题目
跳马问题:10*10的棋盘放好,马只能在棋盘中走日的几个方向,
请问马从0,0位置出发,走到棋盘中的x,y目标点,还必须走k步,有多少种走法?
一、审题
示例:
10*10的棋盘放好,马只能走8个方向,日形状走,比如下面在xy处(绿色点)的马,下一次只能选择粉色点这8个方向试试:
他们分别是:
x - 2,y + 1
x - 1,y + 2
x + 1,y + 2
x + 2,y +1
x + 2,y - 1
x + 1,y - 2
x - 1,y - 2
x - 2,y - 1
现在从00出发,去xy,必须走k步,有多少种方案?
暴力递归函数的尝试:DP4业务限制模型
看示例那个棋盘,显然有三个变量,咱们这么想
从00去xy,等价于从xy去00,走法一样对吧?
所有,从xy出发,必须走k步,去00点,有多少种走法?可以定义为暴力递归函数:
f(arr,x,y,k)
arr是棋盘,xy是当前位置,还剩余k步,去00点,有多少种走法?
(0)一旦进f,请问你此刻x,y还合法吗???不合法,你这个方案非法,返回0种,没用
(1)当k=0了,迈不动了,如果此刻真的在x=0,y=0处,OK,成功来了,算合法方案,返回1,否则就是非法的方案,返回0
(2)当然在任意xy处,还有k步必须走,那就走:马走日,8个方向全部试探一下,能成功的话,他们返回来的方案数累加和就是咱要的结果,返回ans。
这个解题思路还是很简单的吧!!!
手撕代码:
//复习:
//所有,从xy出发,必须走k步,去00点,有多少种走法?可以定义为暴力递归函数:
//f(arr,x,y,k)
//arr是棋盘,xy是当前位置,还剩余k步,去00点,有多少种走法?
public static int f(int x, int y, int k){
//(0)一旦进f,请问你此刻x,y还合法吗???不合法,你这个方案非法,返回0种,没用
if (x < 0 || x > 9 || y < 0 || y >= 9) return 0;
//(1)当k=0了,迈不动了,如果此刻真的在x=0,y=0处,
// OK,成功来了,算合法方案,返回1,否则就是非法的方案,返回0
if (k == 0) return x == 0 && y == 0 ? 1 : 0;
int ans = 0;
//(2)当然在任意xy处,还有k步必须走,那就走:马走日,8个方向全部试探一下,
//x - 2,y + 1
//x - 1,y + 2
//x + 1,y + 2
//x + 2,y + 1
//x + 2,y - 1
//x + 1,y - 2
//x - 1,y - 2
//x - 2,y - 1
// 能成功的话,他们返回来的方案数累加和就是咱要的结果,返回ans。
ans += f(x - 2,y + 1, k - 1) +
f(x - 1,y + 2, k - 1) +
f(x + 1,y + 2, k - 1) +
f(x + 2,y + 1, k - 1) +
f(x + 2,y - 1, k - 1) +
f(x + 1,y - 2, k - 1) +
f(x - 1,y - 2, k - 1) +
f(x - 2,y - 1, k - 1);
return ans;
}
public static void test(){
System.out.println(go(2, 3, 3));
System.out.println(f(2, 3, 3));
}
这很容易就想到了
尝试,这是8个方向的业务,限制好的
所谓DP4:业务限制类型的尝试模型!
笔试AC解:根据暴力递归,改傻缓存dp表跟随暴力递归,记忆化搜索方法
看得出来,暴力递归函数中,xy和k仨是3个变量,这是咱们目前遇到的最复杂的变量,3个呢
但是也很简单
xy是棋盘平面,k是z轴,好说
就是要填下面这么个三维表dp而已
xy取0–9
k取0–k
咱们准备这个表dp,跟对f,求过了,返回
没求过,重新求,放入dp存起来,动态规划已然OK
手撕代码!
//复习:记忆化搜索法:傻缓存dp跟随f
//所有,从xy出发,必须走k步,去00点,有多少种走法?可以定义为暴力递归函数:
//f(arr,x,y,k)
//arr是棋盘,xy是当前位置,还剩余k步,去00点,有多少种走法?
public static int fDP(int x, int y, int k, int[][][] dp){
//(0)一旦进f,请问你此刻x,y还合法吗???不合法,你这个方案非法,返回0种,没用
if (x < 0 || x > 9 || y < 0 || y >= 9) return 0;
if (dp[x][y][k] != -1) return dp[x][y][k];//求过了
//(1)当k=0了,迈不动了,如果此刻真的在x=0,y=0处,
// OK,成功来了,算合法方案,返回1,否则就是非法的方案,返回0
if (k == 0) {
dp[x][y][k] = x == 0 && y == 0 ? 1 : 0;
return dp[x][y][k];
}
int ans = 0;
//(2)当然在任意xy处,还有k步必须走,那就走:马走日,8个方向全部试探一下,
//x - 2,y + 1
//x - 1,y + 2
//x + 1,y + 2
//x + 2,y + 1
//x + 2,y - 1
//x + 1,y - 2
//x - 1,y - 2
//x - 2,y - 1
// 能成功的话,他们返回来的方案数累加和就是咱要的结果,返回ans。
ans += fDP(x - 2,y + 1, k - 1, dp) +
fDP(x - 1,y + 2, k - 1, dp) +
fDP(x + 1,y + 2, k - 1, dp) +
fDP(x + 2,y + 1, k - 1, dp) +
fDP(x + 2,y - 1, k - 1, dp) +
fDP(x + 1,y - 2, k - 1, dp) +
fDP(x - 1,y - 2, k - 1, dp) +
fDP(x - 2,y - 1, k - 1, dp);
dp[x][y][k] = ans;
return dp[x][y][k];
}
public static int fDPwithdp(int x, int y, int k){
if (x < 0 || x > 9 || y < 0 || y >= 9) return 0;
int[][][] dp = new int[10][10][k + 1];
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
for (int l = 0; l < k + 1; l++) {
dp[i][j][l] = -1;//初始化
}
}
}
return fDP(x, y, k, dp);
}
public static void test(){
System.out.println(go(2, 3, 3));
System.out.println(f(2, 3, 3));
System.out.println(fDPwithdp(2, 3, 3));
}
测试问题不大:
4
4
4
这样的话,笔试快速AC!
面试精华解:根据暴力递归,改动态规划表!
老样子,咱们要根据暴力递归改写动态规划的转移方程,不要用暴力递归求了,直接填表dp返回dp
dp是这样的:在xy处剩余k步,去00有多少种走法?
xy取0–9
k取0–k
咱们最后要dp[x][y][k],图中红色五角星那个格子!
手撕代码,完全根据暴力递归改写转移方程,所以上面暴力递归尝试最重要!!!
根据:f(arr,x,y,k)arr是棋盘10*10,xy是当前位置,还剩余k步,去00点,有多少种走法?
(0)一旦进f,请问你此刻x,y还合法吗???不合法,你这个方案非法,返回0种,没用
(1)当k=0了,迈不动了,如果此刻真的在x=0,y=0处,OK,成功来了,算合法方案,返回1,否则就是非法的方案,返回0
if (k == 0) return x == 0 && y == 0 ? 1 : 0;
咱们可以把0 0 0原点格子填好为1
(2)当然在任意xy处,还有k步必须走,那就走:马走日,8个方向全部试探一下,能成功的话,他们返回来的方案数累加和就是咱要的结果,返回ans。
ans += f(x - 2,y + 1, k - 1) +
f(x - 1,y + 2, k - 1) +
f(x + 1,y + 2, k - 1) +
f(x + 2,y + 1, k - 1) +
f(x + 2,y - 1, k - 1) +
f(x + 1,y - 2, k - 1) +
f(x - 1,y - 2, k - 1) +
f(x - 2,y - 1, k - 1);
其余任意位置dp[x][y][k]依赖8个方向,当然,那些方向必须要是合法的格子
所以咱们先整一个填格子的函数
如果xy不越界,咱们获取那个方向的值回来
public static int getValue(int x, int y, int k, int[][][] dp){
//控制越界
if (x < 0 || x > 9 || y < 0 || y > 8) return 0;
return dp[x][y][k];//不越界就返回下一次的值
}
OK,咱慢来手撕代码填写dp!!!宏观调度是啥呢?xy是一个面,咱们要调度,不同的k,也就是不同的z轴,相同的xy有啥情况
所以,先从k调度
再填写xy
//其余任意点
for (int l = 1; l < k + 1; l++) {//注意,k=0有了,求k一面一面增
for (int i = 0; i < 10; i++) {
for (int j = 1; j < 10; l++) {
整体代码:
//面试精华解:根据暴力递归,改动态规划表!
public static int horseGoDp(int x, int y, int k){
if (x < 0 || x > 9 || y < 0 || y > 9) return 0;
int[][][] dp = new int[10][10][k + 1];
//完全根据暴力递归改写
//(1)当k=0了,迈不动了,如果此刻真的在x=0,y=0处,
// OK,成功来了,算合法方案,返回1,否则就是非法的方案,返回0
dp[0][0][0] = 1;//原点就是1,因为xyk都为0--这个面,其他的就是0
//其余任意点
for (int l = 1; l < k + 1; l++) {//注意,k=0有了,求k一面一面增
for (int i = 0; i < 10; i++) {
for (int j = 0; j < 10; j++) {
//暴力递归怎么搞,咱就怎么改写
int ans = 0;
//(2)当然在任意xy处,还有k步必须走,那就走:马走日,8个方向全部试探一下,
//x - 2,y + 1
//x - 1,y + 2
//x + 1,y + 2
//x + 2,y + 1
//x + 2,y - 1
//x + 1,y - 2
//x - 1,y - 2
//x - 2,y - 1
// 能成功的话,他们返回来的方案数累加和就是咱要的结果,返回ans。
ans += getValueValid(i - 2,j + 1, l - 1, dp) +
getValueValid(i - 1,j + 2, l - 1, dp) +
getValueValid(i + 1,j + 2, l - 1, dp) +
getValueValid(i + 2,j + 1, l - 1, dp) +
getValueValid(i + 2,j - 1, l - 1, dp) +
getValueValid(i + 1,j - 2, l - 1, dp) +
getValueValid(i - 1,j - 2, l - 1, dp) +
getValueValid(i - 2,j - 1, l - 1, dp);
dp[i][j][l] = ans;
}
}
}
return dp[x][y][k];
}
public static int getValueValid(int x, int y, int k, int[][][] dp){
//合法才能拿到dp那些位置的值
if (x < 0 || x > 9 || y < 0 || y > 9) return 0;//不合法
return dp[x][y][k];
}
public static void test2(){
System.out.println(go2(2, 3, 3));
System.out.println(horseGoDp(2, 3, 3));
}
public static void main(String[] args) {
// test();
test2();
}
结果OK:
4
4
总结
提示:重要经验:
1)任何动态规划,都有一个DP模型,最关键的就是尝试模型了,把暴力递归搞出来
2)后续改记忆化搜索方法也好,改精细化动态规划填表也罢,都非常容易
3)笔试求AC,可以不考虑空间复杂度,但是面试既要考虑时间复杂度最优,也要考虑空间复杂度最优。