前引:继上篇我们讲到暴力递归的过程,这一篇blog我们将继续对从暴力递归到动态规划的实现过程,与上篇类似,我们依然采用题目的方式对其转化过程进行论述。
上篇博客:https://blog.csdn.net/m0_65431718/article/details/129604874?spm=1001.2014.3001.5502
一.n皇后问题
八皇后问题是十九世纪著名的数学家高斯于1850年提出的。问题是:在n×n的棋盘上摆放n个皇后,使任意两个皇后都不能处于同一行、同一列或同一斜线上。
我们的解题思路如下:采用暴力递归,既然要求任意两个皇后不能在同一行和同一列和同一斜线,我们依次对这三者进行讨论:①同一行:每一层递归代表一行,我们只要保证在每一层递归中只放置一个皇后即可②同一列:按照题目的要求,我们在访摆放第n层的皇后时,要保证它和前面n-1等皇后都不冲突,这就意味着我们在进行下一层递归的时候仍能有方法访问前面皇后摆放的位置:我们的第一考虑对象当然是数组,但是比较巧妙的是它是一个n*n的棋盘,第一个n代表行,第二个n代表列,我们只需要建立一个长度为n的一维数组,下标代表第几行,下标对应的数组元素代表列,作为参数带入到下一层递归中,斜线也是如此,我们详细展开说说列和斜线的要求:对于列来说,我们要遍历缓存,使前面的缓存和当前列不相等即为不冲突,对于斜线的要求来说,对于一个正方形棋盘,我们其实很容易想到的是直线的斜率为1,也就是说两个元素行的变化如果等于列的变化,我们可以说在同一条斜线上。我们根据思路给出code:
public class NEmpress {
public static void main(String[] args) {
//创建date
int n=4;
int[]data=new int[n];
System.out.println(process(data, 0, n));
}
//创建递归过程
public static int process(int[]data,int i,int n){
//判断出口条件:共有n个元素,一旦当前行越过了n-1,则说明成功
if(i==n){
return 1;
}
//循环处理每一列
//如果没结束
int res=0;
for(int j=0;j<n;++j){
//判断当前元素是否有效
if(isValid(data,i,j)){
data[i]=j;
//进入下一层递归
res+= process( data, i+1, n);
}
}
return res;
}
private static boolean isValid(int[] data, int i, int j) {
//在data中检验是否存在
for(int k=0;k<i;++k){
//第一个是判断从0到i-1行中列的元素是否相等,第二个是判断是否在同一斜线
if(data[k]==j||(Math.abs(i-k)==Math.abs(j-data[k]))){
return false;
}
}
return true;
}
}
如果不改变问题的实现思路,很难去实现大的效率提升,但是考虑不同的方法仍能在一定程度上提升效率(常数级提升):采用位运算,总体的实现逻辑和之前的暴力递归完全相同,但是就具体细节做出了一定的改动,我们给出递归的核心代码,并改动进行解释说明:
limit:是位数限制,对于行列数为N的棋盘,limit的限制是:对于前N个二进制位数均为1,对于N行列的棋盘而言,前N个二进制位代表棋盘的每一行(第一个二进制位代表第一行,第二个代表第二行........)
①col:对于每次摆放个皇后,就将这个二进制位置变为1,表示这个二进制位不能摆放皇后了
②leftLim:左斜线限制,如果leftLim为1,代表对于当前行来说,这个位置不能摆放皇后了。
③RightLim:右斜线限制,如果RightLim为1,同样对于当前行来说,这个位置不能摆放皇后了。
④limit==col:代表col前N个二进制位都是1,表示N个皇后都已经摆放好了,游戏结束,退出递归
⑤limit&(~(col|leftLim|rightLim)):pos是在每一行中能选择的列
⑥ mostRight=pos&(~pos+1):取出最右边的一位
⑦ pos-=mostRight:将最右边的位置从可选择的位数中去除,使当前行不能放置皇后
⑧while(pos!=0) 循环当前行中能选择的位置
⑨res+= process2(limit,col|mostRight,(leftLim|mostRight)<<1, (rightLim|mostRight)>>>1):循环下一层
public static int process2(int limit,int col,int leftLim,int rightLim){
//递归出口
if(limit==col){
return 1;
}
//计算能放的位置:
int pos= limit&(~(col|leftLim|rightLim));
int mostRight=0;
int res=0;
//检验是否能递归
while(pos!=0){
//找最右的位置
mostRight=pos&(~pos+1);
pos-=mostRight;
res+= process2(limit,col|mostRight,(leftLim|mostRight)<<1, (rightLim|mostRight)>>>1);
}
return res;
我们对代码中的几个点进行解释说明:
三.机器人走路问题:(从暴力递归到动态规划的实践)
问题要求如下:
1.假设有排成一行的n个位置记为1-N,N一定大于等于2
2.开始时机器人在m位置上,m一定是1-N中的一个
3.机器人要在规定的步数到达指定的终点,计算到达指定终点的路线有多少条
4.如果机器人来到1位置只能往右来到2位置
5.如果机器人来到N位置只能往左来到N-1位置
6.如果机器人在其他位置,则机器人可以往右也可以往左
对于暴力递归,实现思路就相对比较简单:对于当前位置而言,如果位置是1,他只能选择2,如果在2-N-1的位置,他可以向左和向右走,如果在N位置,只能往N-1的位置走,不断走,直到剩余步数为0,判断是不是要求的位置,然后返回结果。
我们给出关于暴力递归的代码:
public int walking(int N,int cur,int rest,int P){
//编写递归出口
if(rest==0){
if(cur==P){
return 1;
}else {
return 0;
}
}
//排除特殊情况,在0位置处:只能往后走
if(cur==1){
return walking(N, cur+1, rest-1, P);
}
//在最后一个位置,只能往前走
if(cur==N){
return walking(N, cur-1, rest-1, P);
}
//在中间,可以往前往后走
return walking(N, cur-1, rest-1, P)+walking(N, cur+1, rest-1, P);
}
为什么说这是从暴力递归到动态规划的实践开始呢?我们对此进行解释:
我们能在暴力递归的基础上修改为动态规划,什么是动态规划呢?是将暴力递归中重复计算的过程转化为缓存,从而降低暴力递归中重复计算的次数,转而从相关缓存中获取,是一种典型的空间换时间的策略,对于动态规划而言,其实最难的部分是写出关于动态规划的转移方程。
对这道题来说,这种动态规划的类型是记忆性搜索:如果这个位置有缓存,就直接返回缓存结果,否则递归。
动态规划的的code如下:
public int walkCache(int N,int cur,int rest ,int [][]dp,int P){
if(dp[cur][rest]!=-1){
return dp[cur][rest];
}
if(rest==0){
dp[cur][rest]=cur==P?1:0;
return dp[cur][rest];
}
if(cur==1){
dp[cur][rest]=walkCache(N, cur+1, rest-1, dp,P);
return dp[cur][rest];
}
if(cur==N){
dp[cur][rest]=walkCache(N, cur-1, rest-1, dp,P);
return dp[cur][rest];
}
return dp[cur][rest]=walkCache(N, cur-1, rest-1, dp,P)+walkCache(N, cur+1, rest-1,dp, P);
}
}
四.零和博弈问题:
问题描述:
思路:对于A而言,作为先手,他一定在纵观全局后选择对自己最有利的计划,而B作为后手,只能在A 选择之后在此基础上选择对自己最友好的计划和策略,换句话说,B选择的只能是A选择剩下的。
所以我们需要设计两个函数,一个是先手函数,选择其中相对自己而言最优的策略,即为选择自己能选的棋中的最大值,而同样需要设计一个后手函数,它的作用是:在后手参与下选择相对较小的选择(只能选择A选剩下的)
我们给出code:
package violencerecursion;
/**
* @author tongchen
* @create 2023-03-21 16:09
*/
public class GameWin {
public static void main(String[] args) {
GameWin gameWin = new GameWin();
int[]arr={1,100,1};
System.out.println(gameWin.win(arr));
}
public int win(int[]arr){
//零和博弈问题,解题思路:先手的人拿最优的选择,后手的人只能被迫接收最差的结果
int left=0;
int right= arr.length-1;
return Math.max(f(arr, 0, arr.length-1),s(arr, 0, arr.length-1));
}
private int f(int[] arr, int left, int right) {
//递归出口
if(left==right){
return arr[left];
}
//选择已知策略中最优的选择
return Math.max(arr[left]+s(arr,left+1,right),arr[right]+s(arr,left,right-1));
}
private int s(int[] arr, int left, int right) {
if(right==left){
return 0;
}
//B相当于从第二个棋子先选择(因为第一个棋子肯定被A选走了,B先手选第二个棋子)
//但是这种情况下B只能选择在A纵观全局后选择最优策略之后被迫选择劣的选择(即最小值)
return Math.min(f(arr, left+1, right),f(arr, left, right-1));
}
}
后续会更新关于动态规划的转移方程的编写思路过程,希望大家能持续关注捏~