数据结构——八皇后
题目介绍
八皇后问题是一个古老而又著名的问题,是学习回溯算法的一个经典案例。今天我们就一起来探究一下吧!
时间退回到1848年,国际西洋棋棋手马克斯·贝瑟尔提出了这样的一个问题,
在8×8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问一共有多少种摆法。
大数学家高斯认为一共有76种摆法,1854年在柏林的象棋杂志上不同的作者发表了共计40种不同的见解,后来还有人利用图论的方法得出共有92种摆法。
而如今,通过我们的计算机以及编程语言我们可以轻松的解决这个问题。
最直接的也是最容易想到的一种解法便是暴力法,我们可以在8×8的格子中任选8个皇后,选定后看是否满足任意两个皇后都不处于同行同列同斜线的条件,若满足则累计满足条件的方案。学习过排列组合的我们发现64取8这个数字达到了40亿,显然是令人难以接受的。
但我们根据这个条件,我们可以人为地做出一些选择,比如根据条件我们可知每行每列最多都只能有一个皇后,这样可以在一定程度上缩减问题的规模。在第一行的某一列选择放置一个皇后,共有8种不同的选择,而第二行只能选择剩下的7列,也就是7种选择,剩下每一行的选择都会递减1,那么总共可供选择的方案有8的阶乘种,已经是一种远优于暴力解法的解法,但是这个阶乘的时间复杂度恐怕也难以令人接受,还有更优的解法吗?
那是自然的,这便是递归回溯的方法
当我们选择了第一个皇后的位置之后,与其处于同行同列同斜线的位置便都无法被选择,第二个皇后只能放在未被第一个皇后所辐射到的位置上,接着放置第三个皇后,同样不能放在被前两个皇后辐射到的位置上,若此时已经没有未被辐射的位置能够被选择,也就意味着这种摆法是不可行的,我们需要回退到上一步,给第二个皇后重新选择一个未被第一个皇后辐射的位置,再来看是否有第三个皇后可以摆放的位置,如还是没有则再次回退至选择第二个皇后的位置,若第二个皇后也没有更多的选择则回退到第一个皇后,重新进行位置的选择。
思路分析
1、第一个皇后放在第一行第一列
2、第二和皇后放在第二行第一列,然后判断是否合乎规则,如果正确,则继续放第三个皇后,如果不正确,则继续放在第二列、第三列……,直到找到一个适合的。
3、继续放置第三个,还是按照同样的方法,……直到第8个皇后也能放在一个不冲突的位置,算是找到了一个正确解。
4、当得到一个正确解之后,在退回到上一个,就会开始回溯,即将第一个皇后,放到第一列正确的所有正确解。
5、然后,接续第一个皇后放第二列,后面继续执行1,2,3,4的步骤。
说明: 理论上,应该创建一个二维数组表示棋盘,但是实际上可以通过算法,使用一个一维数组就可以解决问题。
如: arr[8] = {0, 4, 7, 5, 2, 6, 1, 3}
上个数组就是该问题的一个正确解。在这里有一个规律:该数组的下标【0……7】表示了该皇后所处的行号,下标的值val 表示了该皇后所处的列号。如0表示的意思就是:第一皇后的位置是第一行的第一列,同样的4表示的意识是:第二个皇后的位置是第2行的第5列
代码实现
public class Queen {
private int max = 8;
//用一维数组来表示二维数组
//{0 4 7 5 2 6 1 3 },数组下标i表示第i行,val 表示第val列。
private int[] array = new int[max];
private static int count = 0;
public static void main(String[] args) {
Queen queen = new Queen();
queen.check(0);
System.out.println();
System.out.println("解法一共有"+count);
}
private void check(int n)
{
if(n == max)
{
print();
return;
}
//循环的是列
for(int i = 0;i < max;i++)
{
//把当前的Queen放在该行的第一列上
array[n] = i;
//进行判断
if(judge(n))
{
check(n+1);
}
}
}
//当放置第n个位置的皇后是,判断改皇后是否冲突
private boolean judge(int n)
{
for(int i = 0;i < n;i++)
{
// array[i] == array[n]: 由于在一维数组中 val表示Queen所处的列,
//由于其规则决定了,在数组中不能存在相同的值,也就是不用处在同一列上。
// Math.abs(n - i) == Math.abs(array[n] - array[i])
//由于在一维数组中 下标表示Queen所处的行,
// Math.abs(n - i)表示了 二者之间的行所差的数量,
// Math.abs(array[n] - array[i]) 表示了二者之间列所差的数量。如果相同,则说明在同义斜线上
if(array[i] == array[n] || Math.abs(n - i) == Math.abs(array[n] - array[i]))
{
return false;
}
}
return true;
}
//输出8皇后的位置
private void print()
{
count++;
for(int i = 0;i < max;i++)
{
System.out.print(array[i]+" ");
}
System.out.println();
}
}
代码是比较简单的,接下来是说如何实现的递归和回溯
对于java这个语言来说,一个类中的方法,其实虚拟机栈中的一个个的栈帧。当当前的方法执行完毕之后,就会弹出栈顶,继续执行下一个栈帧。
内存结构图大致如图所示,通过递归不断调用时,就会生成一个个的栈帧,通过8次的递归调用,当第八个皇后的8次for循环执行完毕之后,就会弹出栈顶,继续执行第7个皇后的剩余的for循环。只到第1个皇后的8次for循环执行完毕。通过这样的方式就产生了回溯。