一、 问题介绍
输入:皇后的数目,例4
输出:第1行至第N行皇后对应的列号,例(2,4,1,3)
1. 采用递归策略实现N皇后问题,测试能够在短时间内找到解的最大N;
2. 采用宽度优先算法实现N皇后问题,测试能够在短时间内找到解的最大N;
3. 采用深度优先算法实现N皇后问题,测试能够在短时间内找到解的最大N。
4. 利用位运算算法实现N皇后问题,测试能够在短时间内找到解的最大N。
5. 采用爬山法实现N皇后问题,测试能够在短时间内找到解的最大N。
二、 程序设计与算法分析
(一) 采用递归算法的实现思路为:
Backtrack(Data)
Data:当前状态
回溯搜索算法 返回值:从当前状态到目标状态的路径(以规则表的形式表示)
或Fail。
1. If Term(Data) ReturnNil;
2. If Deadend(Data)Return Fail;
3. Rules:=Apprules(Data);
4. Loop: If Null(Rules)Return Fail;
5. R:=First(Rules);
6. Rules:=Tail(Rules);
7. RData:=Gen(R, Data);
8. Path:=Backtrack(RData);
9. If Path=Fail Go Loop;
10. Return Cons(R, Path);
(二) 一般图的搜索方法的实现思路:
1. G=G0 (G0=s),Open:=(s);
2. Closed:=( );
3. Loop: If Open=( )Then Exit(Fail);
4. n:=First(Open),Remove(n, Open), Add(n, Closed);
5. If Goal(n),ThenExit(Success);
6. Expand(n)→{mi}, G:=Add(mi, G);
7. 标记和修改指针: Add(mj,Open), 并标记mj到n的指针; 计算是否要修改mk、ml到n的指针; 计算是否要修改ml到其后继节点的指针;
8.对Open中的节点按某种原则重新排序;
9.Go Loop;
这是对于一般图搜索的普适算法。不同的图搜,例如宽度优先算法和深度优先算法的区别主要体现在该算法中的第8条原则,依据不同的原则对open表中的数据进行排序。其中深度优先的排序方式为每次将待扩展节点插入open表的头部,而宽度优先搜索每次将待扩展的节点放到open表的尾部。
也就是深度优先搜索方法:
1. G := G0(G0=s), Open:= (s), Closed := ( );
2. Loop: If Open = ( )Then Exit (Fail);
3. n := First(Open);
4. If Goal(n) Then Exit(Success);
5. Remove(n, Open),Add(n, Closed);
6. If Depth(n) ≥ Dm Go Loop;
7. Expand(n) →{mi}, G := Add(mi,G);
8. If 目标在{mi}中 ThenExit(Success);
9. Add(mj, Open), 并标记mj到n的指针;
10. Go Loop;
深度优先搜索特点:
l 若存在多个目标状态,一般不能保证找到最优解
l 当深度限制不合理时,可能找不到解,可以将算法改为可变深度限制
l 最坏情况时,搜索空间等同于穷举
l 与回溯法的差别:图搜索
l 是一个通用的与问题无关的方法
宽度优先搜索方法的实现思路:
1. G := G0 (G0=s), Open:= (s), Closed := ( );
2. Loop: If Open = ( )Then Exit (Fail);
3. n := First(Open);
4. If Goal(n) Then Exit(Success);
5. Remove(n, Open),Add(n, Closed);
6. Expand(n) →{mi}, G:=Add(mi, G);
7. If 目标在{mi}中 ThenExit(Success);
8. Add(Open, mj), 并标记mj到n的指针;
9. Go Loop;
宽度优先搜索特点:
l 当问题有解时,一定能找到解
l 当问题为单位耗散值,且问题有多解时,一定能找到最优解
l 方法与问题无关,具有通用性
l 效率较低
l 属于图搜索方法
(三) 爬山法:
爬山法是A算法的一个特例。定义一个评价函数f,对当前的搜索状态进行评估,找出一个最有希望的节点来扩展。一般A算法中定义
l 评价函数的格式: f(n) = g(n)+ h(n)
l f(n):评价函数
l h(n):启发函数
而在爬山法中总是考虑与目标之间的差距
l g(n) = 0,仅考虑h(n)
爬山法具体算法:
1. n := s;
2. Loop: If Goal(n) ThenExit(Success);
3. Expand(n) →{mi}, 计算h(mi), nextn:= m (min h(mi) 的结点)
4. If h(n) < h(nextn)Then Exit(Fail);
5. n := nextn;
6. Go Loop;
(四) 位运算法:
严格来说这并不应该单独作为一个算法来讲,因为他同样是用的深度优先的算法思想,其运行效率因为位运算速度的提高虽然比起不同搜索算法略有提高,不过也不是什么质的提高。不过考虑到其实现的巧妙性还是在这里讲一下。
算法思路:
l 二进制的每一位就代表一个皇后。
l 从第一层开始往下一层迭代,最后为全1表示一种可行解。
l 当前层皇后摆放情况取反后1为表示可放皇后的地方。
l 利用位运算x&(-x)便可以很快的找到从右往左第一个1的位置,在这个位置摆放皇后,往下一层搜索。
三、 结果及其分析
1. 递归和深度优先在较短时间内能解决33规模的皇后问题。
2. 利用为运算能在较短时间内解决34规模的皇后问题。
3. 利用宽度优先算法能在较短时间内解决14规模的皇后问题。宽度优先算法效率较低的原因是他无法直接回溯,这导致它不能在不同节点共用同一个棋盘。为保存每个节点的状态信息不仅需要较大的内存开销,在每一个节点间传递棋盘信息时,也需要较大的数据拷贝的时间开销。
4. 爬山法效率较高,经测试10w规模的皇后问题耗时一般在5s左右,100w。这主要是因为爬山法不是漫无目的的搜索,而是利用启发函数,尽量朝着目标方向靠近,这减少了大量无意义的枚举。
各种版本实现代码:
递归法:
// 递归法解n皇后问题,n在30以内效率可以接受
import java.util.*;
public class Queen1{
private static int n;
private static boolean ok;
static int[] pos=new int[100];//第i行所放的棋子的列号
public static void main(String[] args){
Scanner in=new Scanner(System.in);
while(in.hasNextInt()){
n=in.nextInt();
ok=false;
dfs(0);
if(!ok)
System.out.println("no answer");
}
}
public static void dfs(int deep){
if(ok) return;
if(deep==n){
System.out.print("("+(pos[0]+1));
for(int i=1;i
// 迭代回溯,n皇后
import java.util.*;
public class Queen2{
public static void main(String[] args){
Scanner in=new Scanner(System.in);
int n;
while(in.hasNext()){
n=in.nextInt();
if(!queen(n))
System.out.println("no answer");
}
}
public static boolean queen(int n){
int deep=0;
int[] pos=new int[100];
label:
while(deep>=0){
while(pos[deep]
// bfs求解皇后问题
import java.util.*;
public class Queen4{
static int n;
static boolean ok=false;
public static void main(String[] args){
Scanner in=new Scanner(System.in);
while(in.hasNext()){
n=in.nextInt();
while(n>39){
System.out.println("Please be sure that n must less than 40. Try to input n again!");
n=in.nextInt();
}
ok=false;
bfs();
if(!ok)
System.out.println("No answer");
}
}
public static void bfs(){
Queue q=new LinkedList();
q.clear();
for(int i=0;i
#include "iostream"
using namespace std;
#include "time.h"
// sum用来记录皇后放置成功的不同布局数;upperlim用来标记所有列都已经放置好了皇后。
long sum = 0, upperlim = 1;
// 试探算法从最右边的列开始。
bool ok=false;
void test(long row, long ld, long rd)
{
if(ok) return;
if (row != upperlim)
{
// row,ld,rd进行“或”运算,求得所有可以放置皇后的列,对应位为0,
// 然后再取反后“与”上全1的数,来求得当前所有可以放置皇后的位置,对应列改为1
// 也就是求取当前哪些列可以放置皇后
long pos = upperlim & ~(row | ld | rd);
while (pos) // 0 -- 皇后没有地方可放,回溯
{
// 拷贝pos最右边为1的bit,其余bit置0
// 也就是取得可以放皇后的最右边的列
long p = pos & -pos;
// 将pos最右边为1的bit清零
// 也就是为获取下一次的最右可用列使用做准备,
// 程序将来会回溯到这个位置继续试探
pos -= p;
// row + p,将当前列置1,表示记录这次皇后放置的列。
// (ld + p) << 1,标记当前皇后左边相邻的列不允许下一个皇后放置。
// (ld + p) >> 1,标记当前皇后右边相邻的列不允许下一个皇后放置。
// 此处的移位操作实际上是记录对角线上的限制,只是因为问题都化归
// 到一行网格上来解决,所以表示为列的限制就可以了。显然,随着移位
// 在每次选择列之前进行,原来N×N网格中某个已放置的皇后针对其对角线
// 上产生的限制都被记录下来了
test(row + p, (ld + p) << 1, (rd + p) >> 1);
}
}
else
{
// row的所有位都为1,即找到了一个成功的布局,回溯
ok=true;
return;
}
}
int main(int argc, char *argv[])
{
time_t tm;
int n = 16;
if (argc != 1)
n = atoi(argv[1]);
tm = time(0);
// 因为整型数的限制,最大只能32位,
// 如果想处理N大于32的皇后问题,需要
// 用bitset数据结构进行存储
if ((n < 1) || (n > 32))
{
printf(" 只能计算1-32之间\n");
exit(-1);
}
printf("%d 皇后\n", n);
// N个皇后只需N位存储,N列中某列有皇后则对应bit置1。
upperlim = (upperlim << n) - 1;
test(0, 0, 0);
printf("共有%ld种排列, 计算时间%d秒 \n", sum, (int) (time(0) - tm));
system("pause");
return 0;
}
//爬山发解决皇后问题 #include#include #include #include using namespace std; const int size=1000000; int board[size],ru[size*2],rd[size*2],n; int f(){//统计对角线需要调整的皇后数 int i,r=0; memset(ru,0,sizeof(ru)); memset(rd,0,sizeof(rd)); for(i=0;i 1) r+=ru[i]-1; if(rd[i]>1) r+=rd[i]-1; } return r; } void randgen(){ // 随即生成全排列 for(int i=0;i >n){ if(n==2 || n==3){ cout<<"No answer"< 1) ++fnow; ++rd[board[i]+j]; if(rd[board[i]+j]>1) ++fnow; --ru[board[j]-j+n-1]; if(ru[board[j]-j+n-1]) --fnow; --rd[board[j]+j]; if(rd[board[j]+j]) --fnow; ++ru[board[j]-i+n-1]; if(ru[board[j]-i+n-1]>1) ++fnow; ++rd[board[j]+i]; if(rd[board[j]+i]>1) ++fnow; swap(board[i],board[j]); if(fnow