三种算法求解经典N皇后问题
【问题描述】
八皇后问题是一个古老而著名的问题,是回溯算法的典型例题。该问题是十九世纪著名的数学家高斯1850年提出:在8X8格的国际象棋上摆放八个皇后,使其不能互相攻击,即任意两个皇后都不能处于同一行、同一列或同一斜线上,问有多少种摆法。
高斯认为有76种方案。1854年在柏林的象棋杂志上不同的作者发表了40种不同的解,后来有人用图论的方法解出92种结果。
要求扩展到任意的皇后数的情况,同时对源程序给出详细的注释。
看到n皇后问题首先想到的就是回溯法求解,这是一个非常经典的解法,另一个方案就是递归的思想。
至于第三种解法,算法思想也是采用了递归,但在数据结构方面,使用了非常少见的机器码存储,各种操作、判断的实现采用的是位运算,因此单独列出,详见下文。
【数据存储】
一维数组Q[],下标表示第i个皇后,也是当前放置的皇后所处行数,数组元素值Q[i]表示列的位置,所以棋盘上的点阵坐标可表示为
(
i
,
Q
[
i
]
)
(i, Q[i])
(i,Q[i])。
1、递归求解n皇后
【算法思想】
首先判定当前位置
(
i
,
Q
[
i
]
)
(i, Q[i])
(i,Q[i])是否可以放置皇后,要保证同列、对角线上没有被放置其他皇后,(这里不去判断同行的原因是,在寻找位置时是按行顺序进行的,所以每一个新皇后同行必然不存在其他皇后),按照坐标判断
Q
[
j
]
=
=
Q
[
i
]
Q[j]==Q[i]
Q[j]==Q[i],
a
b
s
(
Q
[
j
]
−
Q
[
i
]
)
=
=
a
b
s
(
j
−
i
)
abs(Q[j]-Q[i])==abs(j-i)
abs(Q[j]−Q[i])==abs(j−i) 即可。
然后进行递归求解,遍历每一行,只要满足放置条件,则可以进行下一个皇后的放置,n次放置即可求出一个解。
注: 完整代码在下面回溯法中一同贴出
2、 非递归回溯探测
【算法思想】
可用位置判定与上一个方案相同,依旧是先判断当前位置是否满足条件,满足就进入下一个行皇后的放置;遍历下一行满足条件的位置,如果当前行中所有位置都不满足条件,那么进行回溯,在上一行继续向后寻找位置重新放置皇后。
判断结束的标志,当回溯向前遍历了所有行,则结束程序。
public class Item_04 {
static final int N = 8; //皇后的数量
static int[] Q = new int[N+1];
static int count = 0; //记录解的个数
public static void main(String[] args) {
long start = System.currentTimeMillis();
NQueens(N); //非递归
// NQueensTrace(1, N); //递归
System.out.println(N+"皇后问题解的个数: "+count);
long end = System.currentTimeMillis();
System.out.println("程序运行时间:"+(end-start)+"ms");
}
/**
* 判断位置(i,Q[i])是否可以放皇后
* @param i
* @return
*/
private static boolean isPlace(int i) {
int j = 1;
if(i==1) {
return true; //第一个皇后
}
while (j<i) { //j=1~i-1是已放置了皇后的行
//该皇后是否与以前皇后同列,位置(j,Q[j])与(i,Q[i])是否同对角线
if((Q[j]==Q[i]) || (Math.abs(Q[j]-Q[i])==Math.abs(j-i)))
return false;
j++;
}
return true;
}
/**
* 非递归求解
* @param n 皇后的数量
*/
private static void NQueens(int n) {
int i = 1;
Q[i] = 0; //第一个皇后从(1,1)开始
while(i>=1) { //回溯是否结束
Q[i]++;
while(!isPlace(i) && Q[i]<=n) {
Q[i]++; //判断(i,Q[i])是否可以放置第i个皇后,若此行无法放置,则Q[i]循环结束后为n+1
}
if(Q[i]<=n) {
if(i==n) {
count++;
out();
}else {
i++;
Q[i]=0; //位置(i,Q[i])满足条件,转向下一行,放置第i+1个皇后
}
}else {
i--; //Q[i]循环结束后为n+1,回溯
}
}
}
/**
* 递归求解n皇后问题
* @param tmp 当前需要排序的皇后所在行
* @param n 皇后的数量
*/
private static void NQueensTrace(int tmp, int n) {
if(tmp>n){ //一次放置结束
count++;
out();
}
else{
for(int i=1;i<=n;i++){ //遍历当前行
Q[tmp]=i;
if(isPlace(tmp)){ //该位置可以放置
NQueensTrace(tmp+1,n); //放置下一个皇后
}
}
}
}
/**
* 输出m皇后的解
*/
private static void out() {
System.out.println();
System.out.println("第"+count+"个解:");
for(int i=1;i<=N;i++) {
System.out.print("("+i+","+Q[i]+")" + " ");
}
System.out.println();
}
}
3、位运算求解
前两种方案算法思想较为常见,思路也比较简单,但是当n的数值较大时,无论是递归求解还是非递归求解,都将花费大量的时间,8皇后问题仅仅92种解法递归就消耗了3毫秒,到16皇后问题时,14772512个解消耗时间就达到了5分钟左右。
就这个问题,我查找了资料,发现了位运算的方法,求解16皇后的程序在我的电脑上运行仅仅需要19秒左右。以下是这个算法的详细介绍。
【算法思想】
和普通算法一样,这是一个递归过程,程序一行一行地寻找可以放皇后的地方。过程带三个参数,row、ld和rd,表示三个方向(列、斜对角)可用位置,把它们三个并起来,得到该行所有的禁位,取反后就得到所有可以放的位置(用pos来表示)。以下是图解row、ld、rd在寻找合适位置pos时判断的情况。
此时,
r
o
w
=
00001000
,
l
d
=
00000000
,
r
d
=
01000000
row=00001000,ld=00000000,rd=01000000
row=00001000,ld=00000000,rd=01000000,pos=upperlim&~(row | ld | rd)
取反后POS在计算机中以补码形式存在,按位与后,得到最右边的1,也就是当前要放皇后的位置。递归调用时,每个参数都加上了一个禁位,但两个对角线方向的禁位对下一行的影响需要平移一位。最后,如果递归到某个时候发现row=11111111了,说明八个皇后全放进去了,此时程序从第1行跳到第11行,找到的解的个数加一。
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
long sum = 0, upperlim = 1;
void test(long row, long ld, long rd) //row、ld、rd分别表示列和两条对角线上的皇后冲突
{
if (row != upperlim) //row为11111111这样即结束一次求解
{
long pos = upperlim & ~(row | ld | rd); //三个约束先或运算获得三个方向都为0(都没有冲突)的状态,取反,再进行与运算,
//得到的值pos就是所有可以放置皇后的位置
while (pos)
{
long p = pos & -pos; // 取反后POS在计算机中以补码形式存在,按位与后,得到最右边的1,也就是当前要放皇后的位置
pos -= p; //把这个1从POS中取出
test(row + p, (ld + p) << 1, (rd + p) >> 1); //三个方向的约束全部加上p,表示p位置被标记已放置皇后,在后来的操作中将被禁用
}
} else
sum++; //寻找到一个解
}
int main(int argc, char *argv[])
{
time_t tm;
int n = 16;
if (argc != 1)
n = atoi(argv[1]);
tm = time(0);
if ((n < 1) || (n > 32))
{
printf(" 只能计算1-32之间\n");
exit(-1);
}
printf("%d 皇后\n", n);
upperlim = (upperlim << n) - 1;
test(0, 0, 0);
printf("共有%ld种排列, 计算时间%d秒 \n", sum, (int) (time(0) - tm));
}
三种算法运行时间截图(因电脑状态不同,不同电脑可能会有细微差距,有兴趣的话可以将代码复制下来在自己电脑上跑一遍)
- 递归16皇后
- 非递归16皇后
- 位运算
部分内容参考Matrix大佬的文章,这里贴上文章链接,有兴趣的话可以看一看。