递归回溯
八皇后问题是递归回溯中非常经典的问题,这个问题甚至在计算机产生前就已经存在了
它到底是什么意思呢
八皇后
国际象棋中的皇后,可以横向、纵向、斜向移动。
如何在一个8X8的棋盘上放置8个皇后,使得任意两个皇后都不在同一条横线、竖线、斜线方向上?
让我们来举个栗子,下图的绿色格子是一个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子:
下图的绿色格子是两个皇后在棋盘上的“封锁范围”,其他皇后不得放置在这些格子:
以高斯为代表的许多数学家先后研究过这个问题。
后来,当计算机问世,通过计算机程序的运算可以轻松解出这个问题。
所谓递归回溯,本质上是一种枚举法。这种方法从棋盘的第一行开始尝试摆放第一个皇后,摆放成功后,递归一层,再遵循规则在棋盘第二行来摆放第二个皇后。如果当前位置无法摆放,则向右移动一格再次尝试,如果摆放成功,则继续递归一层,摆放第三个皇后……
如果某一层看遍了所有格子,都无法成功摆放,则回溯到上一个皇后,让上一个皇后右移一格,再进行递归。如果八个皇后都摆放完毕且符合规则,那么就得到了其中一种正确的解法。
图解这个过程
- 第一层递归,尝试在第一行摆放第一个皇后:
- 第二层递归,尝试在第二行摆放第二个皇后(前两格被第一个皇后封锁,只能落在第三格):
- .第三层递归,尝试在第三行摆放第三个皇后(前四格被第一第二个皇后封锁,只能落在第五格):
- 第四层递归,尝试在第四行摆放第四个皇后(第一格被第二个皇后封锁,只能落在第二格):
- 第五层递归,尝试在第五行摆放第五个皇后(前三格被前面的皇后封锁,只能落在第四格):
- 由于所有格子都“绿了”,第六行已经没办法摆放皇后,于是进行回溯,重新摆放第五个皇后到第八格。:
第六行仍然没有办法摆放皇后,第五行也已经尝试遍了,于是回溯到第四行,重新摆放第四个皇后到第七格。:
继续摆放第五个皇后,以此类推……
解决八皇后问题,可以分为两个层面:
1.找出第一种正确摆放方式,也就是深度优先遍历。
2.找出全部的正确摆放方式,也就是广度优先遍历。
之前的文章已经分析过dfs与bfs的适用范围,
dfs适用于判定是否存在可能的解
bfs适用于找到全局最优解
自然的。dfs会找到第一个符合条件的解,而bfs会找到所有存在的解
这里我们用dfs实现
1.表示棋盘
int size=8;//棋盘8*8
int num=8;//8个皇后
//创建棋盘0代表没有落子,1代表该位置有皇后
//比如chessBoard[3][4]代表四行五列
int chessBoard[][]=new int[size][size];
2.判断皇后落点是否合法
public boolean check(int x,int y){
for (int i = 0; i < y; i++) {
//纵向检查
if (chessBoard[x][i]==1) {
return false;
}
//检查左横向
if (x-1-i>=0&&chessBoard[x-1-i][y-1-i]==1) {
return false;
}
//检查右横向
if (x+1+i<size &&chessBoard[x+1+i][y-1-i]==1) {
return false;
}
}
return true;
}
3.如何回溯递归,这是本算法的核心
public boolean settleQueen(int n){
//base case 行数超过8说明已经找到答案,跳出
if (n==num) {
return true;
}
//遍历当前行,逐一格子验证
for (int i = 0; i < chessBoard.length; i++) {
//将当前行清0,避免回溯的时候出现脏数据
for (int j = 0; j < chessBoard.length; j++) {
chessBoard[j][n]=0;
}
//检查合法性,符合则修改元素并进行下一次递归
if (check(i, n)) {
chessBoard[i][n]=1;
//递归如果返回true说明已经找完了 没有就进行下一步递归
if (settleQueen(n+1)) {
return true;
}
}
}
return false;//遍历完都未找到满足条件的解
}
4.输出结果
很简单,直接把二维数组打印
public static void print(){
for (int i = 0; i < chessBoard.length; i++) {
for (int j = 0; j < chessBoard[i].length; j++) {
System.out.print(chessBoard[i][j]);
}
System.out.println();
}
}
5.组合程序,就是这个样子
public class EightQueen {
static int size=8;//棋盘8*8
static int num=8;//8个皇后
//创建棋盘0代表没有落子,1代表该位置有皇后
//比如chessBoard[3][4]代表四行五列
static int chessBoard[][]=new int[size][size];
public static void main(String[] args) {
settleQueen(0);//从第一行开始遍历
print();//打印结果
}
//这里直接用笨办法遍历了,其实这种不断重复的遍历可以通过预处理数组实现优化
public static boolean check(int x,int y){
for (int i = 0; i < y; i++) {
//纵向检查
if (chessBoard[x][i]==1) {
return false;
}
//检查左横向
if (x-1-i>=0&&chessBoard[x-1-i][y-1-i]==1) {
return false;
}
//检查右横向
if (x+1+i<size &&chessBoard[x+1+i][y-1-i]==1) {
return false;
}
}
return true;
}
public static boolean settleQueen(int n){
//base case 行数超过8说明已经找到答案,跳出
if (n==num) {
return true;
}
//遍历当前行,逐一格子验证
for (int i = 0; i < chessBoard.length; i++) {
//将当前行清0,避免回溯的时候出现脏数据
for (int j = 0; j < chessBoard.length; j++) {
chessBoard[j][n]=0;
}
//检查合法性,符合则修改元素并进行下一次递归
if (check(i, n)) {
chessBoard[i][n]=1;
//递归如果返回true说明已经找完了 没有就进行下一步递归
if (settleQueen(n+1)) {
return true;
}
}
}
return false;//遍历完都未找到满足条件的解
}
public static void print(){
for (int i = 0; i < chessBoard.length; i++) {
for (int j = 0; j < chessBoard[i].length; j++) {
System.out.print(chessBoard[i][j]);
}
System.out.println();
}
}
}
其中一种结果
10000000
00000010
00001000
00000001
01000000
00010000
00000100
00100000