一、 問題介紹
輸入:皇后的數目,例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;++i)
System.out.print(","+(pos[i]+1));
System.out.println(")");
ok=true;
return;
}
for(int i=0;i<n;++i){
pos[deep]=i;
if(judge(deep))
dfs(deep+1);
}
}
public static boolean judge(int deep){
// boolean flag=true;
for(int i=0;i<deep;++i){
if(pos[i]==pos[deep] || Math.abs(deep-i)==Math.abs(pos[i]-pos[deep]))
return false;
}
return true;
}
}
// 迭代回溯,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]<n){
if(judge(deep,pos)){
++deep;
pos[deep]=0;
if(deep==n){
for(int i=0;i<n;++i)
System.out.printf("("+(i+1)+","+(pos[i]+1)+")\t");
System.out.println();
return true;
}
continue label;
}
++pos[deep];
}
if(pos[deep]==n){
--deep;
if(deep<0)
break;
++pos[deep];
pos[deep+1]=0;
}
}
return false;
}
public static boolean judge(int deep,int[] pos){
for(int i=0;i<deep;++i){
if(pos[i]==pos[deep] || Math.abs(i-deep)==Math.abs(pos[i]-pos[deep]))
return false;
}
return true;
}
}
寬度優先搜索:
// 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<ChessBoard> q=new LinkedList<ChessBoard>();
q.clear();
for(int i=0;i<n;++i){
ChessBoard cb=new ChessBoard();
cb.pos[0]=i;
q.add(cb);
}
while(!q.isEmpty()){
ChessBoard now=q.remove();
for(int i=0;i<n;++i){
ChessBoard next=new ChessBoard(now);
++next.deep;
next.pos[next.deep]=i;
boolean flag=true;
for(int j=0;j<next.deep;++j){
if(next.pos[j]==next.pos[next.deep] || Math.abs(j-next.deep)==Math.abs(next.pos[j]-next.pos[next.deep])){
flag=false;
break;
}
}
if(flag){
if(next.deep==n-1){
for(int j=0;j<n;++j)
System.out.print("("+(j+1)+","+(next.pos[j]+1)+")\t");
System.out.println();
ok=true;
return;
}
q.add(next);
}
}
}
}
}
class ChessBoard{
public int[] pos=new int[40];// 第i行所放棋子的列數
public int deep;
public ChessBoard(){
Arrays.fill(pos,0);
deep=0;
}
public ChessBoard(ChessBoard r){
pos=Arrays.copyOf(r.pos,40);
deep=r.deep;
}
}
位運算法:
#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;
}
爬山法:
</pre><pre name="code" class="cpp">//爬山發解決皇后問題
#include<string.h>
#include<stdlib.h>
#include<time.h>
#include<iostream>
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<n;i++){
ru[board[i]-i+n-1]++; //負對角線
rd[board[i]+i]++; //正對角線
}
for(i=0;i<2*n-1;i++){
if(ru[i]>1) r+=ru[i]-1;
if(rd[i]>1) r+=rd[i]-1;
}
return r;
}
void randgen(){ // 隨即生成全排列
for(int i=0;i<n;++i)
board[i]=i;
for(int i=0;i<n;++i){
int pos=rand()%n;
swap(board[i],board[pos]);
}
}
int main(){
srand((unsigned)time(NULL));
int i,j,temp,now,t1,t2,fnow;
// n=200000;
while(cin>>n){
if(n==2 || n==3){
cout<<"No answer"<<endl;
continue;
}
while(1){
randgen();
now=f();
label:
if(now==0)
break;
int times=n*n;
if(times<0) times=100000000;
while(times--){
int fnow=now,i=rand()%n,j=rand()%n;
if(i==j) continue;
--ru[board[i]-i+n-1];
if(ru[board[i]-i+n-1])
--fnow;
--rd[board[i]+i];
if(rd[board[i]+i])
--fnow;
++ru[board[i]-j+n-1];
if(ru[board[i]-j+n-1]>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<now){
now=fnow;
goto label;
}
swap(board[i],board[j]);
--rd[board[j]+i];
--ru[board[j]-i+n-1];
++rd[board[j]+j];
++ru[board[j]-j+n-1];
--rd[board[i]+j];
--ru[board[i]-j+n-1];
++rd[board[i]+i];
++ru[board[i]-i+n-1];
}
}
cout<<"("<<board[0]+1;
for(int i=1;i<n;++i){
cout<<","<<board[i]+1;
}
cout<<")"<<endl;
}
return 0;
}
博客主頁