基本介绍
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上。问有多少种摆法。
八皇后问题,是一个古老而著名的问题,是回溯算法的典型案例。该问题是国际西洋棋棋手马克斯·贝瑟尔于1848年提出。 高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。计算机发明后,有多种计算机语言可以解决此问题。
问题分析
设八个皇后为xi,分别在第i行( I = 1,2,3,4……,8)。
问题的解状态:可以用
(1,x1),(2,x2),……,(8,x8)
表示8个皇后的位置。由于行号固定,可简单记为:
(x1,x2,x3,x4,x5,x6,x7,x8)
所以我们可以直接用一个长度为8的数组,来表示八皇后问题的一组解。
问题的解空间:
(x1,x2,x3,x4,x5,x6,x7,x8),1≤xi≤8(i=1,2,3,4……,8)
共88个状态。
约束条件:八个皇后位置 (1,x1),(2,x2),……,(8,x8)不在同一行、同一列和同一对角线上。
方法一:暴力穷举
每一行放一个皇后,可以放在第 1 列,第 2 列,……,直到第8列。穷举所有的可能,检验皇后之间是否会相互攻击。
毫无疑问,这种方法是非常低效率的,因为它并不是哪里有冲突就调整哪里,而是盲目地按既定顺序枚举所有的可能方案。
代码如下:
public class EightQueens {
// 方法一:暴力枚举
public List<int[]> eightQueens1(){
ArrayList<int[]> result = new ArrayList<>();
// 用一个数组保存一组解
int[] solution = new int[8];
// 遍历解空间
for (solution[0] = 0; solution[0] < 8; solution[0]++){
for (solution[1] = 0; solution[1] < 8; solution[1]++){
for (solution[2] = 0; solution[2] < 8; solution[2]++){
for (solution[3] = 0; solution[3] < 8; solution[3]++){
for (solution[4] = 0; solution[4] < 8; solution[4]++){
for (solution[5] = 0; solution[5] < 8; solution[5]++){
for (solution[6] = 0; solution[6] < 8; solution[6]++){
for (solution[7] = 0; solution[7] < 8; solution[7]++){
if (check(solution))
result.add(Arrays.copyOf(solution, 8));
}
}
}
}
}
}
}
}
return result;
}
// 定义一个判定有效的方法
private boolean check(int[] a){
// 任意两个皇后位置比较
for (int i = 0; i < 7; i++){
for (int j = i + 1; j < 8; j++){
if (a[i] == a[j] || Math.abs(a[i] - a[j]) == j - i )
return false;
}
}
return true;
}
}
复杂度分析
- 时间复杂度:O(N^N)。这里N为皇后数量,即n皇后问题的维度,本题N=8。 八皇后问题如果用穷举法,需要尝试88 =16,777,216种情况。而这里的check方法,又需要C28 = 28次比较。
方法二:回溯法
回溯算法优于穷举法。首先,将第一行的皇后放在第一列。之后第二行的皇后,也从放在第一列开始判断,这时已经发生冲突。于是调整第二行的的皇后到第二列,继续冲突就放第三列,直到不冲突为止。
如此可依次放下后续每一行的皇后。当发现某一行的皇后无处放置时,就回溯到上一行,将皇后位置向后继续调整到另一个不冲突的地方。如果上一行也无处放置,继续向上回溯。直到每一行都无法继续放置,遍历结束。
判断冲突的方法改进
我们发现,一个皇后是否跟其它有冲突,主要取决于当前位置所在的横、纵、斜4条线。
目前我们直接考虑每一行放置一个皇后,那么横向直线就不用考虑了,只需要考虑其它三条线。纵向比较简单,只要判断同一列是否有皇后就可以了;而对斜向仔细研究规律,可以发现,同一斜线上的格子,横纵坐标是有规律的:
- 方向一的斜线为从左上到右下方向,同一条斜线上的每个位置满足行下标与列下标之差相等
- 方向二的斜线为从右上到左下方向,同一条斜线上的每个位置满足行下标与列下标之和相等
所以为了在代码中快速判断,当前某个皇后位置是否有效,可以增加三个辅助集合: - col:记录某一列上是否出现过皇后;
- diag1:记录某一左上-右下方向的斜线上,是否出现过皇后;
- diag2:记录某一右上-左下方向的斜线上,是否出现过皇后。
每次放置皇后时,对于每个位置判断其是否在三个集合中,如果三个集合都不包含当前位置,则当前位置是可以放置皇后的位置。
代码如下:
// 定义辅助集合
HashSet<Integer> cols = new HashSet<>();
HashSet<Integer> diags1 = new HashSet<>();
HashSet<Integer> diags2 = new HashSet<>();
// 方法二:回溯法
public List<int[]> eightQueens(){
ArrayList<int[]> result = new ArrayList<>();
int[] solution = new int[8];
Arrays.fill(solution, -1); // 初始填充-1
// 传入行号0,开始调用
backtrack(result, solution, 0);
return result;
}
// 定义一个回溯方法
private void backtrack(ArrayList<int[]> result, int[] solution, int row){
if (row >= 8){
result.add(Arrays.copyOf(solution, 8));
} else {
// 遍历每一列,考察可能的皇后位置
for (int column = 0; column < 8; column ++){
if (cols.contains(column))
continue;
int diag1 = row - column;
if (diags1.contains(diag1))
continue;
int diag2 = row + column;
if (diags2.contains(diag2))
continue;
solution[row] = column; // 当前位置可以放置皇后
cols.add(column);
diags1.add(diag1);
diags2.add(diag2);
// 递归调用,找下一行的皇后
backtrack(result, solution, row + 1);
// 回溯状态
solution[row] = -1;
cols.remove(column);
diags1.remove(diag1);
diags2.remove(diag2);
}
}
}
复杂度分析
- 时间复杂度:O(N!),其中 N 是皇后数量。回溯的过程,其实就是N的一个全排列。
- 空间复杂度:O(N),其中 N是皇后数量。空间复杂度主要取决于递归调用层数、记录每行放置的皇后的列下标的数组以及三个集合,递归调用层数不会超过 N,数组的长度为 N,每个集合的元素个数都不会超过N。