算法训练 || 八皇后问题(递归回溯)
首先说明一下递归和回溯的区别:
- 递归是一种数据结构;
- 回溯是一种算法思想,也可理解为一个解决问题的工具。
一、回溯算法
回溯算法,简单的说,就是一条路走到黑,无路可走了就回头,从分叉路口找另一条路继续走到黑,如此反复,直到所有路都走完。
这个算法思想跟跟深度优先搜索算法(DFS)类似,但两者也有不同之处,体现在:
- 访问序
回溯算法对每一个元素的访问都有一个序,不能使用 visited 记录,因为不仅仅是记录 “是否被访问过” 这么简单;而DFS算法对访问的“序”没有提出要求,只要访问过该结点(元素),就用visited记录下即可。 - 适用场景
回溯是遍历问题空间寻找解决方案的方法,而DFS是一种遍历实际图或者树结构然后求值的方法。
二、八皇后问题
问题描述:
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯・贝瑟尔于 1848 年提出:
在 8×8 格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
这个问题的难点在于一个皇后放置之后会对下一个皇后位置的放置点产生影响(道路堵塞),下面用表格简单描述一下这一场景,
- 放一个皇后及它皇后所引起的道路堵塞(用黑色填充表示被堵塞的道路)
- 假设在第3行的空白格放置下一个皇后(用蓝色填充表示被堵塞的道路),则出现
可见每放置一个皇后对下一个皇后位置放置的影响是很大的。
在这里可能有些小伙伴会想到用暴力算法求解,但是暴力算法在这里的思路虽然行得通,但是由于数据量太大,在程序竞赛时往往容易造成超时,而且暴力算法的效率较低,所以这里采用一种效率更高效的算法:回溯算法。通过递归函数的调用,很快就能计算出问题的答案。
具体代码如下:
public class 八皇后回溯递归 {
static int count=0;
static int [][]arr=new int[8][8];
public static void main(String[] args) {
search(0);
System.out.println(count);
}
//检查
public static boolean check(int row,int col) {
//行检查
for(int j=0;j<7;j++) {
if(arr[row][j]==1) {
return false;
}
}
//列检查
for(int i=0;i<7;i++) {
if(arr[i][col]==1) {
return false;
}
}
//左对角线检查(检查左上角,左下方不需要检查)
for(int i=row-1,j=col-1;i>=0 && j>=0;i--,j--) {
if(arr[i][j]==1) {
return false;
}
}
//右对角线检查(检查右上角,右下方不需要检查)
for(int i=row-1,j=col+1;i>=0 && j<8;i--,j++) {
if(arr[i][j]==1) {
return false;
}
}
return true;
}
//递归回溯
public static void search(int row){
if(row==8) { //row为0到7,row==8表示遍历完整个棋盘
count++;
return ;
}
else {
for(int col=0;col<8;col++) {//列
if(check(row,col)) {//调用检查方法
arr[row][col]=1;
search(row+1);
arr[row][col]=0;//将标志信号置0,避免回溯时误判断
}
}
}
}
}
下面对代码一些关键点作简单解析:
- 递归回溯search函数中,通过遍历棋盘的每一行(row),对每一行进行每一列的for循环遍历。若8行全部走完,表示走完了整个棋盘,此时进入if中将count递增1并退出函数;若在某一行中对于所有列都没找到合适的放置点时,回溯到上一行,改变上一行皇后的位置。即从第一行(row=0)开始,通过check 方法之后,在 if 里面会进行一次递归调用,进入下一行(row+1)。第二行的皇后进入 for 循环的时候,会出现两种情况:
- for 循环没走完时就有通过 check 方法的了,这样就成功地往下走,然后再继续向下递归调用,row 再加 1(第三行),以此类推。
- for 循环走完了都没有通过 check 方法的col,说明第二行所有道路都被堵塞,无法放置皇后,所以也触发不了递归,此时就用到了回溯的思想,回到第一行,把皇后位置的 for 循环 col 加 1,即第一行的皇后往后移一格,即摆在第一行第二列的位置上,如果位置符合要求就继续往下走,如果不符合,继续回溯并移动上一行(第一行)的皇后位置,其他位置情况以此类推。
- check检查函数中,对左对角线和右对角线的检查有一点点技巧,因为我们在摆放皇后的时候是从上向下摆的,所有摆皇后的时候只需要考虑自己的左上方,右上方,正上方,同一行的每一个位置有无被堵塞的,若有,则当前位置无法放置,反之,可放置。而其左下角和右下角是不需要检查的,具体做法在代码有体现。
若代码有需要优化的地方,欢迎大家指正,大家一起交流学习,谢谢。