⭐️前言⭐️
动态规划是一个很难的模块,如果一道动态规划的题目直接去推出动态转移方程来解题,是很难的,所以应该先想出暴力解决的方法,再去用空间换时间优化,得出动态规划的解法。
🍉欢迎点赞 👍 收藏 ⭐留言评论 📝私信必回哟😁
🍉博主将持续更新学习记录收获,友友们有任何问题可以在评论区留言
🍉博客中涉及源码及博主日常练习代码均已上传GitHub
📍内容导读📍
- 🍅机器人走路
- 🍅拿牌游戏
🍅机器人走路
题目:
假设有排成一行的N个位置记为1~N,N一定大于或等于2
开始时机器人在其中的M位置上(M一定是1~N中的一个)
如果机器人来到1位置,那么下一步只能往右来到2位置;
如果机器人来到N位置,那么下一步只能往左来到N-1位置;
如果机器人来到中间位置,那么下一步可以往左走或者往右走;
规定机器人必须走K步,最终能来到P位置(P也是1~N中的一个)的方法有多少种
给定四个参数 N、M、K、P,返回方法数
题解思路1:
暴力递归,列举所有可能性,确定结束递归的条件,把所有的可能性加起来就是结果。
代码实现:
public class RobotWalk {
/**
* @param N 有1~N个位置
* @param start 起始位置
* @param aim 目标位置
* @param k 要走k步
* @return
*/
public static int ways(int N,int start,int aim,int k) {
return process(start,k,aim,N);
}
/**
* @param cur 目前所在位置
* @param rest 剩余步数
* @param aim 目标位置
* @param N 有N个位置
* @return
*/
public static int process(int cur,int rest,int aim,int N) {
if(rest==0) {
return cur==aim?1:0;
}
if(cur==1) {
return process(2,rest-1,aim,N);
}
if(cur==N) {
return process(N-1,rest-1,aim,N);
}
return process(cur-1,rest-1,aim,N)+process(cur+1,rest-1,aim,N);
}
}
题解思路2:
记忆化搜索、从顶向下的动态规划
假设一开始在位置3,剩余4步,那么下一步可能走到位置2,剩余3步;或者位置4,剩余3步;这两个位置都能走到位置3,剩余2步,位置3到达aim的可能性只由该位置决定,与怎样到达位置3的没关系,所以为了减少重复计算,可以增加缓存表,把之前的结果记录下来,当后续其他分支又走到该位置时,可以直接使用不需要再计算。
代码实现:
public class RobotWalk {
public static int ways2(int N,int start,int aim,int k) {
int[][] dp=new int[N+1][k+1];
for (int i = 0; i <=N; i++) {
for (int j = 0; j <=k; j++) {
dp[i][j]=-1;
}
}
return process2(start,k,aim,N,dp);
}
public static int process2(int cur, int rest, int aim, int N, int[][] dp) {
if(dp[cur][rest]!=-1) {
return dp[cur][rest];
}
int ans=0;
if(rest==0) {
ans=cur==aim?1:0;
}else if(cur==1) {
ans=process2(2,rest-1,aim,N,dp);
}else if(cur==N) {
ans=process2(N-1,rest-1,aim,N,dp);
}else {
ans=process2(cur-1,rest-1,aim,N,dp)+process2(cur+1,rest-1,aim,N,dp);
}
dp[cur][rest]=ans;
return ans;
}
}
题解思路3:
动态规划最终版本,假设共有5个位置,2为起始位置,4为目标位置,需要走6步,那动态转移结果如下:
即求cur=2,rest=6位置的结果,cur=1时,结果依赖于与cur=2,rest-1位置的结果;
cur=5时,结果依赖于cur=4,rest-1位置的结果;中间位置的结果,依赖于(cur-1,rest-1)+(cur+1,rest-1)位置的结果。根据以上的规则,来完成dp表的填写,最终返回(2,6)位置的结果即可。
代码实现:
public class RobotWalk {
public static int ways3(int N,int start,int aim,int k) {
int[][] dp=new int[N+1][k+1];
dp[aim][0]=1;
for (int rest = 1; rest <=k ; rest++) {
dp[1][rest]=dp[2][rest-1];
for (int cur = 2; cur < N; cur++) {
dp[cur][rest]=dp[cur-1][rest-1]+dp[cur+1][rest-1];
}
dp[N][rest]=dp[N-1][rest-1];
}
return dp[start][k];
}
}
结果测试:
🍅拿牌游戏
题目:
给定一个整型数组arr,代表数值不同的纸牌排成一条线
玩家A和玩家B依次拿走每张纸牌
规定玩家A先拿,玩家B后拿
但是每个玩家每次只能拿走最左或最右的纸牌
玩家A和玩家B都绝顶聪明
请返回最后获胜者的分数
题解思路1:
拿牌有先手和后手两种姿态:
先手拿牌,所能拿到的最大分数为
1.arr[L]+后手在[L+1,R]范围取到的最小值(因为是后手所以取到的只能是最小值)
2.arr[R]+后手在[L,R-1]范围内取到的最小值
1、2中的最大值。
如果只剩一张牌,那就直接拿走。
后手拿牌,所能拿到的最大分数为
1.先手在[L+1,R]范围内的最大值
2.先手在[L,R-1]范围内的最大值
1、2中的最小值,因为为后手取牌,所以得到的结果肯定为较小的结果。
如果只剩一张牌,那么只能拿到0。
代码实现:
public class CardsInLine {
// 根据规则,返回获胜者的分数
public static int win1(int[] arr) {
if(arr==null||arr.length==0) {
return 0;
}
int first=f1(arr,0,arr.length-1);
int second=g1(arr,0,arr.length-1);
return Math.max(first,second);
}
// arr[L..R] 先手获得的最好分数返回
public static int f1(int[] arr, int L, int R) {
if(L==R) {
return arr[L];
}
int p1=arr[L]+g1(arr,L+1,R);
int p2=arr[R]+g1(arr,L,R-1);
return Math.max(p1,p2);
}
// arr[L..R] 后手获得的最好分数返回
public static int g1(int[] arr, int L, int R) {
if(L==R) {
return 0;
}
int p1=f1(arr,L+1,R); // 对手拿走了L位置的数
int p2=f1(arr,L,R-1); // 对手拿走了R位置的数
return Math.min(p1,p2);
}
}
题解思路2:
由下图的依赖关系可得,还是会有重复的计算,所以可以通过缓存来减少重复的计算。
代码实现:
public class CardsInLine {
public static int win2(int[] arr) {
if(arr==null||arr.length==0) {
return 0;
}
int N= arr.length;
int[][] fmap=new int[N][N];
int[][] gmap=new int[N][N];
for (int i = 0; i < N; i++) {
for (int j = 0; j < N; j++) {
fmap[i][j]=-1;
gmap[i][j]=-1;
}
}
int first=f2(arr,0,arr.length-1,fmap,gmap);
int second=g2(arr,0,arr.length-1,fmap,gmap);
return Math.max(first,second);
}
public static int f2(int[] arr, int L, int R, int[][] fmap, int[][] gmap) {
if(fmap[L][R]!=-1) {
return fmap[L][R];
}
int ans=0;
if(L==R) {
ans=arr[L];
}else {
int p1=arr[L]+g2(arr,L+1,R,fmap,gmap);
int p2=arr[R]+g2(arr,L,R-1,fmap,gmap);
ans=Math.max(p1,p2);
}
fmap[L][R]=ans;
return ans;
}
public static int g2(int[] arr, int L, int R, int[][] fmap, int[][] gmap) {
if(gmap[L][R]!=-1) {
return gmap[L][R];
}
int ans=0;
if(L!=R) {
int p1=f2(arr,L+1,R,fmap,gmap); // 对手拿走了L位置的数
int p2=f2(arr,L,R-1,fmap,gmap); // 对手拿走了R位置的数
ans=Math.min(p1,p2);
}
gmap[L][R]=ans;
return ans;
}
}
题解思路3:
状态转移表,根据暴力递归解法,来推断出表格中的结果。
假设一组数据为[7,4,16,15,1],那么对应的两张表的状态如下:
在f表中,如果L == R,即返回该位置的值,而在g表中,作为后手姿态,L==R时,只能获得0.
因为L<=R,所以表格中不合法区域直接用×排除
最终即判断f表中(0,4)与g表中(0,4)两个位置的结果大小,返回较大的即可。
其余位置的填写规则,可以由暴力递归的解法来得出。
比如g(0,1)位置,其结果依赖于f(1,1)与f(0,0)两个位置的结果;f(0,1)位置,其结果依赖于g(1,1)与g(0,0)两个位置的结果;即可得出规则,填写结果依赖于另一张表对应位置的左、下位置的结果。
代码实现:
public class CardsInLine {
public static int win3(int[] arr) {
if(arr==null||arr.length==0) {
return 0;
}
int N=arr.length;
int[][] fmap=new int[N][N];
int[][] gmap=new int[N][N];
for (int i = 0; i < N; i++) {
fmap[i][i]=arr[i];
}
for (int i = 1; i < N; i++) {
int L=0;
int R=i;
while (R<N) {
fmap[L][R]=Math.max(arr[L]+gmap[L-1][R],arr[R]+gmap[L][R-1]);
gmap[L][R]=Math.min(fmap[L-1][R],gmap[L][R-1]);
L++;
R++;
}
}
return Math.max(fmap[0][N-1],gmap[0][N-1]);
}
}
⭐️最后的话⭐️
总结不易,希望uu们不要吝啬你们的👍哟(^U^)ノ~YO!!如有问题,欢迎评论区批评指正😁