算法系列——N皇后问题

常规N皇后解决问题过程

一.问题描述

运用回溯法解题通常包含以下三个步骤:
(1)针对所给问题,定义问题的解空间;
(2)确定易于搜索的解空间结构;
(3)以深度优先的方式搜索解空间,并且在搜索过程中用剪枝函数避免无效搜索;

 

通过上述的基本思路,我们可以将问题描述为:X(j)表示一个解的空间,j表示行数,里面的值表示可以放置在的列数,抽象约束条件得到能放置一个皇后的约束条件(1)X(i)!=X(k);(2)abs(X(i)-X(k))!=abs(i-k)。应用回溯法,当可以放置皇后时就继续到下一行,不行的话就返回到第一行,重新检验要放的列数,如此反复,直到将所有解解出。

也就是对于N×N的棋盘,选择出N个符合i!=r∧j!=s∧|i-r|!=|j-s|∨(i+r)!=(j+s)的点的排列总数。

 

二.伪代码:

判断点是否符合要求:
 place(k, X)

   I=1

   While i<k do

      If x[i]==x[k] or abs(x[i]-x[k])==abs(i-k) then

         Return false

      I=i+1

      Return true

 

求问题的所有解:

Nqueens(n, X)

Sum=0 , X[1]=0 , k=1

While k>0 do

   X[k]=X[k]+1

   While X[k]<=n and !(place(k,x))

      X[k]=X[k]+1

If X[k]<=n then

   Sum=Sum+1

Else

   K=K+1 ,X[k]=0

Else

   K=K-1

Print sum

 

三.代码实现

 1 #include <iostream>
 2 using namespace std;
 3 #include <math.h>
 4 
 5 /*检查可不可以放置一个新的皇后*/
 6 bool place(int k, int *X)
 7 {
 8 
 9     int i;
10     i=1;
11     while(i<k)
12     {
13        if((X[i]==X[k])||(abs(X[i]-X[k])==abs(i-k)))
14            return false;
15        i++;
16     }
17     return true;
18 }
19 
20 /*求解问题的所有解的总数,X存放列数*/
21 void Nqueens(int n,int *X)
22 {
23     int k,sum=0;
24     X[1]=0;
25     k=1;
26     while(k>0)
27     {
28        X[k]=X[k]+1;
29 
30        while((X[k]<=n)&&(!place(k, X)))
31            X[k]=X[k]+1;
32 
33        if(X[k]<=n)
34            if(k==n)
35            {
36               for(int i=1;i<=n;i++)
37                   cout<<X[i]<<" ";
38               cout<<endl;
39               sum++;
40            }
41            else
42            {
43               k=k+1;
44               X[k]=0;
45            }
46            else
47               k=k-1;
48      }
49      cout<<"解的总数为:"<<sum<<endl;
50 }
51 
52 int main()
53 {
54     int n;
55     int *X;
56     cout<<"请输入皇后的个数:";
57     cin>>n;
58     X=new int[n];
59     cout<<"问题的解如下:"<<endl;
60     Nqueens(n,X);
61     return 0;
62 }

 

四.实验结果

实验结果

五.存在的问题

当皇后个数N大于等于16以上,程序对棋盘的扫描次数大到惊人:

维基百科截图

从维基百科列出的结果不难看出,在25皇后时,符合条件的解集已经如此庞大了。而数组的存储及加法运算来求解已经不能适应当前的运算。

六.算法改进

程序中的所有数在计算机内存中都是以二进制的形式储存的,而位运算就是直接对整数在内存中的二进制位进行操作,所以速度快,效率高。因此我们选择用位运算来改进运算速度。

算法思想用图列应该更好解释:

程序2

如上图所示,假设一个8*8的棋盘,那么第一次我们在棋盘第一个位置放置一个皇后,则此时,第二列最靠右可放棋子的位置是3。假设第二个放到第二列3的位置,则此时,第三列最靠右能放棋子的位置是5...我们用蓝色线代表向右边斜的线,用橙色代表向左边斜的线,用红色代表向下边的线,而同一行,我们不需判断,因为棋子不能放置同一行的位置。这样,我们画了上面的图,所有被红,橙,蓝穿过的格都不能放置皇后。那么从图上,我们很容易的推出第四行第几个位置能放皇后(从右往左算是2,7,8)。

我们用0代表没有被线穿过,用1代表被线穿过,用row代表竖方向,ld代表左斜线,rd代表右斜线。假设每次放皇后我们都先放最靠右边的。

则放第一个皇后时:

Row=0000 0001, Ld=0000 0001, Rd=0000 1001

放置第二个皇后时:

Row=0000 0101, Ld=0000 0110, Rd=0000 0100

放置第三个皇后时:

Row=0001 0101, Ld=0001 1100, Rd=0001 0010

按照图,我们可以标识出有没有被线穿过的格子,那么我们要在上面放皇后,当然要放置在没有被线穿过的位置:也就是说Row 或者 Ld 或者 Rd上有被线穿过的格子都是不符合要求的,用数学描述为:

(row|ld|rd),因为数学上经常以1为是,0为否,所以我们将式子改为:~(row|ld|rd)

而初始时,某一行还没有线的限制,所以都是可以放置皇后的,对于8皇后,初始时,我们可以定义upperlimit=1111 1111来表示。

则要判断当前行那些位置可以放置皇后,我们可以用:

Pos=upperlimit&~(row|ld|rd)

一直放置,直到无可放置的位置或者扫描完一次棋盘为止。

对于无可放置位置这种情况,我们则要回溯到上一步,然后再找上一行可放置皇后的另一个点,如果不存在该点,则再继续向上回溯…重复直到找出所有解。

而对于扫描完成,我们如何判断呢?从上图,我们很容易直到,每次放置一个皇后,row则会多一个1,所以,只要到row=upperlimit时,说明棋盘扫描结束,则我们找到符合结果的总数sum就要加一。

 

程序清单:

 1 #include <iostream>
 2 using namespace std;
 3 #include <math.h>
 4 
 5 int sum = 0;
 6 int upperlimit = 1;
 7 void compare(int row,int ld,int rd)
 8 {
 9     if(row!=upperlimit)
10     {
11         int pos = upperlimit&~(row|ld|rd);
12         while(pos!=0)
13         {
14             int p=pos&-pos;
15             pos-=p;
16             compare(row+p,(ld+p)<<1,(rd+p)>>1);
17         }
18     }
19     else{
20         sum++;
21     }
22 }
23 
24 int main()
25 {
26     int n;
27     cout<<"请输入皇后的个数:";
28     cin>>n;
29    
30     upperlimit = (upperlimit<<n)-1;
31     compare(0,0,0);
32     cout<<"问题的解如下:"<<sum<<endl;
33     return 0;
34 }

 

实验结果:

程序2结果

改进后算法的不足:虽然运算速度及效率提高了很多倍,但是由于上N大于等于20后,运算量太大,改进运算方式不能从本质上解决问题,所以我们继续跟进。

 

七.算法改进二

改进思路,对于不同的皇后问题,使用不同的方法计算,

如,对于除2、3、8、9、14、15、26、27、38、39之外的任意N值皇后,可以用分治法,如: 

类推

如图,我们可以用类比法来推算除去上述特殊值的N皇后问题,但是其推导公式过于复杂,分类运算考虑的情况及排列组合的公式还没完全推导出,所以这个算法还只是停留在我们的思路中。

 

posted @ 2011-12-09 20:30  三度空间  阅读(8733)  评论(0编辑  收藏  举报