算法系列——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皇后时,符合条件的解集已经如此庞大了。而数组的存储及加法运算来求解已经不能适应当前的运算。
六.算法改进
程序中的所有数在计算机内存中都是以二进制的形式储存的,而位运算就是直接对整数在内存中的二进制位进行操作,所以速度快,效率高。因此我们选择用位运算来改进运算速度。
算法思想用图列应该更好解释:
如上图所示,假设一个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 }
实验结果:
改进后算法的不足:虽然运算速度及效率提高了很多倍,但是由于上N大于等于20后,运算量太大,改进运算方式不能从本质上解决问题,所以我们继续跟进。
七.算法改进二
改进思路,对于不同的皇后问题,使用不同的方法计算,
如,对于除2、3、8、9、14、15、26、27、38、39之外的任意N值皇后,可以用分治法,如:
如图,我们可以用类比法来推算除去上述特殊值的N皇后问题,但是其推导公式过于复杂,分类运算考虑的情况及排列组合的公式还没完全推导出,所以这个算法还只是停留在我们的思路中。
多聚旅游 聚游宝 学友网
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架