廣度優先搜尋演算法(BFS)
使用計算機求解的問題中,有許多問題是無法用數學公式進行計算推導採用模擬方法來找出答案的。這樣的問題往往需要我們根據問題所給定的一些條件,在問題的所有可能解中用某種方式找出問題的解來,這就是所謂的搜尋法或搜尋技術。
通常用搜索技術解決的問題可以分成兩類:一類問題是給定初始結點,要求找出符合約束條件的目標結點;另一類問題是給出初始結點和目標結點,找出一條從初始結點到達目標結點的路徑。
常見的搜尋演算法有列舉法、廣度優先搜尋法、深度優先搜尋法、雙向廣度優先搜尋法,A*演算法、回溯法、分支定界法等。這裡來討論一下廣度優先搜尋法。
一.廣度優先搜尋演算法
1. 問題的特徵
可以採用搜尋演算法解決的這類問題的特點是:
1)有一組具體的狀態,狀態是問題可能出現的每一種情況。全體狀態所構成的狀態空間是有限的,問題規模較小。
2)在問題的解答過程中,可以從一個狀態按照問題給定的條件,轉變為另外的一個或幾個狀態。
3)可以判斷一個狀態的合法性,並且有明確的一個或多個目標狀態。
4)所要解決的問題是:根據給定的初始狀態找出目標狀態,或根據給定的初始狀態和結束狀態,找出一條從初始狀態到結束狀態的路徑。
2.廣度優先搜尋演算法解題的步驟
1)定義一個狀態結點
採用廣度優先搜尋演算法解答問題時,需要構造一個表明狀態特徵和不同狀態之間關係的資料結構,這種資料結構稱為結點。不同的問題需要用不同的資料結構描述。
2)確定結點的擴充套件規則
根據問題所給定的條件,從一個結點出發,可以生成一個或多個新的結點,這個過程通常稱為擴充套件。結點之間的關係一般可以表示成一棵樹,它被稱為解答樹。搜尋演算法的搜尋過程實際上就是根據初始條件和擴充套件規則構造一棵解答樹並尋找符合目標狀態的結點的過程。
廣度優先搜尋演算法中,解答樹上結點的擴充套件是沿結點深度的“斷層”進行,也就是說,結點的擴充套件是按它們接近起始結點的程度依次進行的。首先生成第一層結點,同時檢查目標結點是否在所生成的結點中,如果不在,則將所有的第一層結點逐一擴充套件,得到第二層結點,並檢查第二層結點是否包含目標結點,...對長度為n+1的任一結點進行擴充套件之前,必須先考慮長度為n的結點的每種可能的狀態。因此,對於同一層結點來說,求解問題的價值是相同的,我們可以按任意順序來擴充套件它們。這裡採用的原則是先生成的結點先擴充套件。
結點的擴充套件規則也就是如何從現有的結點生成新結點。對不同的問題,結點的擴充套件規則也不相同,需要按照問題的要求確定。
3)搜尋策略
為了便於進行搜尋,要設定一個表儲存所有的結點。因為在廣度優先搜尋演算法中,要滿足先生成的結點先擴充套件的原則,所以儲存結點的表一般設計成佇列的資料結構。
搜尋的步驟一般是:
(1)從佇列頭取出一個結點,檢查它按照擴充套件規則是否能夠擴充套件,如果能則產生一個新結點。
(2)檢查新生成的結點,看它是否已在佇列中存在,如果新結點已經在佇列中出現過,就放棄這個結點,然後回到第(1)步。否則,如果新結點未曾在佇列中出現過,則將它加入到佇列尾。
(3)檢查新結點是否目標結點。如果新結點是目標結點,則搜尋成功,程式結束;若新結點不是目標結點,則回到第(1)步,再從佇列頭取出結點進行擴充套件......。
最終可能產生兩種結果:找到目標結點,或擴充套件完所有結點而沒有找到目標結點。
如果目標結點存在於解答樹的有限層上,廣度優先搜尋演算法一定能保證找到一條通向它的最佳路徑,因此廣度優先搜尋演算法特別適用於只需求出最優解的問題。當問題需要給出解的路徑,則要儲存每個結點的來源,也就是它是從哪一個節點擴充套件來的。
3.廣度優先搜尋演算法的演算法框架
對於廣度優先搜尋法來說,問題不同則狀態結點的結構和結點擴充套件規則是不同的,但搜尋的策略是相同的,因此演算法框架也基本相同。
struct tnode{ //定義一個結點資料型別
.... //根據具體問題確定所需的資料型別
}state[maxn]; //定義tnode型別的陣列作為儲存結點的佇列
void init(); //初始化函式
bool extend(); //判斷結點是否能擴充套件,如果能則產生新結點
bool repeat(); //檢查新結點是否在佇列中已經出現
bool find() //檢查新結點是否目標結點
void outs(); //輸出結點狀態
void printpath(); //輸出路徑
void bfs(){ //BFS演算法主程式
tnode temp; //tnode型臨時結點
int head=0,tail=0; //佇列頭指標和尾指標
while(head<=tail && tail //根據具體問題確定一個結點擴充套件規則
temp=state[head]; //取佇列頭的結點
if(extend()){ //如果該結點可以擴充套件則產生一個新結點
if(!repeat()){ //如果新結點未曾在佇列中出現過則
tail++; // 將新結點加入佇列尾
state[tail] =temp;
state[tail].last=head; //記錄父結點標識
if(find()){ // 如果新結點是目標結點
hail++; // 將佇列尾結點的父結點指標指向佇列尾
state[tail] =tail-1;
printpath(); //輸出路徑
break; //退出程式
}
}
}
head++; //佇列頭的結點擴充套件完後出隊,取下一結點擴充套件
}
}
對於不同的問題,用廣度優先搜尋法的演算法基本上都是一樣的。但表示問題狀態的結點資料結構、新結點是否目標結點和是否重複結點的判斷等方面則有所不同,對具體的問題需要進行具體分析,這些函式要根據具體問題進行編寫。
二.廣度優先搜尋演算法的例子
下面來看幾個簡單的例子:
1.分油問題
一個一斤的瓶子裝滿油,另有一個七兩和一個三兩的空瓶,再沒有其它工具。只用這三個瓶子怎樣精確地把一斤油分成兩個半斤油。
選擇廣度優先演算法來求解分油問題可以得到通過最少步驟完成分油的最優解。
1)定義狀態結點
分油過程實際上就是將油從一個油瓶倒入另一個油瓶。分油過程中,各個油瓶中的油在不斷變化,因此需要記錄各個油瓶在不同狀態所裝油的多少。這裡用一個數組bottle[3]存放當前油瓶中所裝油的多少,不同油瓶用陣列下標區分,陣列元素bottle[0]是一斤油瓶中的油,bottle[1]是七輛油瓶中的油,而bottle[2]是三兩油瓶中的油。
此外,結點中用變數last還要記錄每個狀態是從哪一個狀態變化來的,就是擴展出該結點的父結點編號。
2)擴充套件規則
很明顯,油瓶中必須有油才能把油倒出,同樣油瓶必須不滿才能將油倒入。分油過程中,將油從一個油瓶倒入另一個油瓶,可能的情形用變數i表示,一共只有6種,每種情形的序號與油瓶編號的關係如下表所示:
分油情形 i 0 1 2 3 4 5
倒出油的油瓶 i/2 0 0 1 1 2 2
倒入油的油瓶 (i+3)/2 Mod 3 1 1 2 0 0 1
3)重複結點和目標結點的判斷
結點是否相同只需比較油瓶的狀態。對於重複結點,需要將佇列中的結點逐一檢查,目標結點的判斷則比較簡單。
4)程式程式碼如下(VC6.0下編譯通過):
#include
#include
const maxn=100;
struct tnode{
int bottle[3]; //當前油瓶裝的油
int last; //父結點
int souc; //源瓶
int dest; //目標瓶
}state[maxn]; //狀態佇列
int capacity[3]; //油瓶容量
void init(){ //初始化
state[0].bottle[0]=10;
state[0].bottle[1]=0;
state[0].bottle[2]=0;
state[0].last=0;
state[0].souc=0;
state[0].dest=0;
capacity[0]=10;
capacity[1]=7;
capacity[2]=3;
}
bool expand(tnode& temp,int i,int j){ //擴充套件結點
if(temp.bottle[i]>0 && capacity[j]>temp.bottle[j]){ //如果源瓶中有油且目標瓶不滿
if(temp.bottle[i]>=capacity[j]-temp.bottle[j]){ //源瓶的油大於目標瓶空餘容量
temp.bottle[i]=temp.bottle[i]-capacity[j]+temp.bottle[j];//源瓶有餘
temp.bottle[j]=capacity[j]; //目標瓶滿
}
else{ //否則
temp.bottle[j]=temp.bottle[j]+temp.bottle[i];
temp.bottle[i]=0; //源瓶空
}
temp.souc=i; //記錄源瓶於目標瓶
temp.dest=j;
return 1; //可擴充套件,返回1
}
return 0;
}
bool repeat(tnode state[],tnode temp,int tail){ //重複結點判斷
for(int i=0;i<=tail;i++){
for(int j=0;j<3;j++)
if(temp.bottle[j]!=state[i].bottle[j])break;
if(j==3)return 1;
}
return 0;
}
bool finds(tnode temp){ //目標結點判斷
if(temp.bottle[1]==5||temp.bottle[0]==5)return 1;
return 0;
}
void printpath(tnode state[],int tail){ //輸出路徑
if(tail>0){
tail=state[tail].last;
printpath(state,tail);
cout< cout<"< for(int i=0;i<3;i++)
cout< cout< }
}
void bfs(){ //搜尋主程式
tnode temp;
int head=0,tail=0,i;
while(head<=tail && tail for(i=0;i<6;i++){
temp=state[head];
if(expand(temp,i/2,(i+3)/2%3)){
if(repeat(state,temp,tail))continue;
state[++tail]=temp;
state[tail].last=head;
if(finds(temp)){ //找到目標結點
tail++; //新增一個結點
state[tail].last=tail-1; //最後結點的父結點是目標結點
printpath(state,tail); //輸出搜尋路徑
return; //找到最優解。退出程式
}
}
}
head++;
}
}
void main()
{
init();
bfs();
}
執行結果為:
0 0 0-->0 10 0 0
1 0 0-->1 3 7 0
4 1 1-->2 3 4 3
6 4 2-->0 6 4 0
8 6 1-->2 6 1 3
10 8 2-->0 9 1 0
12 10 1-->2 9 0 1
14 12 0-->1 2 7 1
16 14 1-->2 2 5 3
因為這裡只求最優解,所以在找到解後,就退出程式,如果要求全部解,可將語句goto end換成break,並去掉語句end:;。
2.移動球的問題一
10個盒子排成一列,前面兩個是空的,後面盒子裡相間放著4個紅球和4個白球,若每次可移動任意兩個相鄰的球進入空盒,移動時兩球不得更動原來次序。目標是將4個紅球連在一起,而空盒位置不限。試程式設計,求出一種方案並輸出每一次移動後的球的狀態。下面是一種球放置的最初狀態,其中O表示空盒子,A表示紅球,B表示白球。
O O A B A B A B A B
1)定義狀態結點
用一個數組ball[10]存放球的放置狀態,變數last和spac分別儲存父結點編號和第一個空盒的位置。
2)擴充套件規則
因為只能同時移動兩個球並且不改變順序,因此球移動的目標是兩個相連的空盒,否則不能移動。移動球后狀態改變。
3)重複結點與目標結點的判斷
比較簡單,只需順次檢查盒子狀態即可。
4)程式程式碼如下(VC6.0下編譯通過):
#include
#include
const maxn=250;
struct tnode{ //狀態結點
char ball[10]; //盒子裡球的狀態
int last; //父結點
int spac; //第一個空格位置(從左起順序為0、1...)
}state[maxn]; //狀態佇列
void init(){ //初始化
state[0].ball[0]=''O'';
state[0].ball[1]=''O'';
state[0].ball[2]=''A'';
state[0].ball[3]=''B'';
state[0].ball[4]=''A'';
state[0].ball[5]=''B'';
state[0].ball[6]=''A'';
state[0].ball[7]=''B'';
state[0].ball[8]=''A'';
state[0].ball[9]=''B'';
state[0].spac=0;
}
bool expand(tnode& temp, int i){ //擴充套件結點
if((i-1==temp.spac)||(i==temp.spac)||(i+1==temp.spac))return 0;
//如果被移動的兩個盒子包含空盒,則不能移動
temp.ball[temp.spac]=temp.ball[i]; //球放入空盒
temp.ball[temp.spac+1]=temp.ball[i+1];
temp.ball[i]=''O'';
temp.ball[i+1]=''O'';
temp.spac=i; //記錄新的空盒位置
return 1;
}
bool rept(tnode state[],tnode temp,int k){ //判斷重複結點
for(int i=0;i for(int j=0;j<10;j++)
if(state[i].ball[j]!=temp.ball[j])break;
if(j==10)return 1;
}
return 0;
}
bool find(tnode temp){ //判斷目標結點
int i=0,j=0;
while(i<10){
if(temp.ball[i++]==''A''){
j++;
if(j==4)return 1;
}
else
if(j>0)break;
}
return 0;
}
void printpath(tnode state[], int tail){ //輸出搜尋路徑
if(tail>0){
tail=state[tail].last;
printpath(state,tail);
cout< for(int i=0;i<10;i++)
cout< cout< }
}
void bfs(){ //搜尋主程式
tnode temp;
int head=0,tail=0;
while(head<=tail && tail for(int i=0;i<9;i++){
temp=state[head];
if(expand(temp,i)){
if(rept(state,temp,tail))continue;
state[++tail]=temp;
state[tail].last=head;
if(find(temp)){
tail++;
state[tail].last=tail-1;
printpath(state,tail);
cout< break;
}
}
}
head++;
}
}
void main(){
init();
bfs();
}
3.移動球的問題二
n個盒子被放成一圈,每個盒子按順時針編號為1到n,每個盒子裡都有一些球,且各個盒子裡球的總數不超過n。這些球要按如下的方式移動:每一步可以將一個球從盒子中取出,放入一個相鄰的盒子中。目標是用盡量少的移動使得所有的盒子中球的個數都不超過1。
要最少移動次數,可採用廣度優先搜尋演算法。
1)定義狀態結點
需要儲存的狀態是每個盒子裡所放的球,以10個球為例,可用一個數組ball[10]存放盒子裡放置球的狀態,變數last儲存父結點編號,而變數souc和dest則存放被移動的球移動前後所在盒子的編號。
2)結點擴充套件規則
如果一個盒子裡的球多於1個就可以將一個球移到相鄰的盒子裡去,既可向前(順時針)移動,也可向後(逆時針)移動。球可以移到空盒子中,也可移到有球的盒子中。
3)程式程式碼(VC6.0下編譯通過):
#include
#include
const maxn=500;
struct tnode{ //狀態結點
int ball[10];
int last;
int souc;
int dest;
}state[maxn];
void init(){ //初始化
state[0].ball[0]=6;
state[0].ball[1]=0;
state[0].ball[2]=0;
state[0].ball[3]=0;
state[0].ball[4]=1;
state[0].ball[5]=0;
state[0].ball[6]=0;
state[0].ball[7]=0;
state[0].ball[8]=0;
state[0].ball[9]=2;
}
bool repeat(tnode state[],tnode temp,int k){ //重複結點判斷
for(int i=0;i<=k;i++){
for(int j=0;j<10;j++)
if(temp.ball[j]!=state[i].ball[j])break;
if(j==10) return 1;
}
return 0;
}
bool find(tnode temp){ //目標結點判斷
for(int i=0;i<10;i++)
if(temp.ball[i]>1)break;
if(i==10) return 1;
else
return 0;
}
void printpath(tnode state[],int tail){ //輸出搜尋路徑
if(tail>0){
tail=state[tail].last;
printpath(state,tail);
cout<"
< for(int i=0;i<10;i++)
cout< cout< }
}
void bfs(){ //搜尋主程式
tnode temp;
int head=0,tail=0,i;
while(head<=tail && tail for(i=0;i<10;i++){
int d=-1; //順時針移動球
for(int j=0;j<=1;j++){
temp=state[head];
if(temp.ball[i]>1){
temp.ball[i]--;
temp.ball[(i+d+10)%10]++;
temp.souc=i;
temp.dest=(i+d+10)%10;
d=-d; //逆時針移動球
if(repeat(state,temp,tail))continue;
state[++tail]=temp;
state[tail].last=head;
if(find(temp)){
tail++;
state[tail].last=tail-1;
printpath(state,tail);
cout< break;
}
}
}
}
head ++;
}
}
void main(){
init();
bfs();
}
三.雙向廣度優先搜尋演算法
廣度優先演算法需要從初始結點出發,逐步擴展才能找到問題的解。如果問題有解,它總是處在解答樹的某一層上。解答樹在擴充套件過程中,隨著層次的增加,結點就越來越多,搜尋的量也就迅速擴大,很容易產生資料溢位,導致搜尋失敗。下面來看一個例子:八數碼問題(Eight-puzzle)。
在3×3的棋盤上,擺有 8個棋子,在每個棋子上標有1~8中的某一數字。棋盤中留有一個空格。空格周圍的棋子可以移到空格中。要求解的問題是,給出一種初始佈局(初始狀態)和目標佈局(目標狀態),找到一種最少步驟的移動方法,實現從初始佈局到目標佈局的轉變。設初始狀態和目標狀態如下:
初始狀態 1 2 3 目標狀態2 8 3
8 0 4 1 6 4
7 6 5 7 0 5
問題分析 :由於題目要找的解是達到目標的最少步驟,因此可以採用BFS。以初始狀態為搜尋的出發點,把棋子移動一步後的佈局全部找到,檢查是否有達到目標的佈局,如果沒有,再從這些棋子移動一步的佈局出發,找出棋子移動兩步後的所有佈局,再判斷是否有達到目標的。依此類推,一直到某佈局為目標狀態為止,輸出結果。由於是按棋子移動步數從少到多產生新佈局的,所以找到的第一個目標一定是棋子移動步數最少的一個,也就是最優解。
1.定義結點
用3×3的二維陣列來表示棋盤的佈局比較直觀,如果將棋盤上的格子從左上角到右下角按0到8編號,就可用一維陣列nums[9]來順序表示棋盤上棋子的數字,空格用0表示,陣列元素的下標是格子編號。狀態結點除了棋子佈局的陣列外,還包括該佈局的空格位置spac和該佈局的父結點標識last。因此在程式中,定義狀態結點為結構資料型別:
2.結點擴充套件規則
棋子向空格移動實際上是空格向向相反方向移動。設空格當前位置是spac,則結點的擴充套件規則為:
(1)空格向上移動,spac=spac-3;空格向左移動,spac=spac-1;空格向右移動,spac=spac+1;空格向下移動,spac=spac+3;
如果設向上移動k=1;向左移動k=2;向右移動k=3;向下移動k=4,則上述規則可歸納為一條: 空格移動後的位置為spac=spac-5+2*k。
(2) 空格的位置<3,不能上移;空格的位置>5,不能下移;空格的位置是3的倍數,不能左移;空格的位置+1是3的倍數,不能右移;
廣度優先搜尋的過程中,新產生的結點放在佇列後面,當前擴充套件的結點從佇列前面選取。所以結點是按產生的先後次序進行擴充套件,深度大(步數多)的結點後擴充套件。程式中設定兩個指標:佇列的頭指標Head和佇列的尾指標tail,分別指向佇列頭和對列尾。
3. 搜尋策略
1)從佇列頭取一個佈局,按照向上、向右、向下和向左的順序,檢查移動空格後是否可以產生新的佈局。
2)如果移動空格後有新佈局產生,則檢查新佈局是否已在佇列中出現過,是則放棄,返回1)。
3)如果新佈局未在佇列中出現過,就將它加入佇列,再檢查新佈局是否目標佈局,如果是,則找到解,程式結束。否則返回1)。
4.程式程式碼(VC6.0下編譯通過):
#include
#include
struct tnode{
int nums[9];
int last;
int spac;
}state[10000];
tnode goal;
void init(){ //初始化
state[0].nums[0]=1; //初始狀態
state[0].nums[1]=2;
state[0].nums[2]=3;
state[0].nums[3]=8;
state[0].nums[4]=0;
state[0].nums[5]=4;
state[0].nums[6]=7;
state[0].nums[7]=6;
state[0].nums[8]=5;
state[0].last=0;
state[0].spac=4; //空格位置
goal.nums[0]=2; //目標狀態
goal.nums[1]=8;
goal.nums[2]=3;
goal.nums[3]=1;
goal.nums[4]=6;
goal.nums[5]=4;
goal.nums[6]=7;
goal.nums[7]=0;
goal.nums[8]=5;
goal.spac=6;
}
bool expand(tnode& temp,int k){ //擴充套件結點
if(k==2 && (temp.spac==3 || temp.spac==6))return 0;
if(k==3 && (temp.spac==2 || temp.spac==5))return 0;
int i=temp.spac-5+2*k;
if(i<0 || i>8)return 0;
temp.nums[temp.spac]=temp.nums[i];
temp.nums[i]=0;
temp.spac=i;
return 1;
}
bool repeat(tnode state[],tnode temp,int tail){ //判斷重複結點
int i,j;
for(i=0;i<=tail;i++){
for(j=0;j<9;j++)
if(state[i].nums[j]!=temp.nums[j])break;
if(j==9)return 1;
}
return 0;
}
bool find(tnode temp){ //判斷目標結點
if(temp.spac==goal.spac){
for(int i=0;i<9;i++)
if(temp.nums[i]!=goal.nums[i])return 0;
return 1;
}
return 0;
}
void printpath(tnode state[],int tail){ //輸出路徑
if(tail>0){
tail=state[tail].last;
printpath(state,tail);
cout< for(int i=0;i<9;i++){
cout< if((i+1)%3==0)cout< }
cout< }
}
void bfs(){ //搜尋主程式
tnode temp;
int head=0,tail=0,i;
while(head<=tail && tail<10000){
for(i=1;i<5;i++){
temp=state[head];
if(expand(temp,i)){
if(repeat(state,temp,tail))continue;
state[++tail]=temp;
state[tail].last=head;
if(find(temp)){
tail++;
state[tail].last=tail-1;
printpath(state,tail);
cout< return;
}
}
}
head++;
}
}
void main(){
init();
bfs();
}
程式執行結果為:
0 2 7 15 26 48
1 2 3 1 2 3 0 2 3 2 0 3 2 8 3 2 8 3
8 0 4 --> 0 8 4 --> 1 8 4 --> 1 8 4 --> 1 0 4 --> 1 6 4
7 6 5 7 6 5 7 6 5 7 6 5 7 6 5 7 0 5
結果表明,找到解總共搜尋了48個結點。但實際上這裡的目標狀態是經過仔細選擇的,如果更換一個目標狀態,就會發現需要搜尋的節點數量大大增加,例如目標狀態是1 7 2 4 6 3 0 8 5,則需要搜尋5496個結點才找到解,對於隨機生成的初始狀態和目標狀態,大多數都搜尋失敗。
應當如何來減少搜尋量的擴張呢?一個辦法是採用雙向廣度優先搜尋法。
如果將上述問題中的初始狀態和目標狀態對換,仍然可以用廣度優先搜尋法找到解答,也就是說,搜尋過程時可逆的。因為解答樹靠近根部的層次結點少,所以如果從初始狀態和目標狀態同時向對方搜尋,可以大大減少搜尋結點數量,這正是雙向廣度優先搜尋演算法能夠減少搜尋量的依據。
雙向廣度優先搜尋演算法的步驟如下:
1)定義狀態結點
與廣度優先搜尋演算法相同。
2)確定結點擴充套件規則
與廣度優先搜尋演算法也相同。但需要定義兩個佇列,一個儲存(從初始結點向目標結點)正向擴充套件的結點,另一個儲存(從目標結點向初始結點)反向擴充套件的結點。
3)搜尋策略
(1)從正向擴充套件的佇列頭取出一個結點,檢查它按照擴充套件規則是否能夠擴充套件,如果能則產生一個新結點。
(2)檢查新生成的結點,看它是否已在正向擴充套件的佇列中存在,如果新結點已經在佇列中出現過,就放棄這個結點。否則,如果新結點未曾在佇列中出現過,則將它加入到佇列尾。
(3)檢查新結點是否在反向擴充套件的佇列中出現過,如果是,則兩個佇列在新結點處相遇,搜尋成功,程式結束;若新不是,則繼續。
(4)對反向擴充套件的佇列按(1)、(2)、(3)的步驟進行相同的處理。
(5)未找到目標結點(兩個佇列位相遇)時回到第(1)步,再從佇列頭取出結點進行擴充套件......。
4)程式程式碼如下(VC6.0下編譯通過):
#include
#include
struct tnode{
int nums[9];
int last;
int spac;
};tnode statr[250],statb[250];
void init(){ //初始化
statr[0].nums[0]=1; //初始狀態
statr[0].nums[1]=2;
statr[0].nums[2]=3;
statr[0].nums[3]=8;
statr[0].nums[4]=0;
statr[0].nums[5]=4;
statr[0].nums[6]=7;
statr[0].nums[7]=6;
statr[0].nums[8]=5;
statr[0].last=0;
statr[0].spac=4;
statb[0].nums[0]=1; //目標狀態
statb[0].nums[1]=7;
statb[0].nums[2]=2;
statb[0].nums[3]=4;
statb[0].nums[4]=6;
statb[0].nums[5]=3;
statb[0].nums[6]=0;
statb[0].nums[7]=8;
statb[0].nums[8]=5;
statb[0].spac=6;
}
bool expand(tnode& temp,int k){ //判斷結點是否可擴充套件,可則擴充套件
if(k==2 && (temp.spac==3 || temp.spac==6))return 0;
if(k==3 && (temp.spac==2 || temp.spac==5))return 0;
int i=temp.spac-5+2*k;
if(i<0 || i>8)return 0;
temp.nums[temp.spac]=temp.nums[i];
temp.nums[i]=0;
temp.spac=i;
return 1;
}
bool repeat(tnode state[],tnode temp,int tail){ //重複結點判斷
int i,j;
for(i=0;i<=tail;i++){
for(j=0;j<9;j++)
if(state[i].nums[j]!=temp.nums[j])break;
if(j==9)return 1;
}
return 0;
}
bool find(tnode state[],tnode temp,int& tail){ //判斷目標結點(是否在另一佇列中)
int i,j;
for(i=0;i<=tail;i++){
for(j=0;j<9;j++)
if(state[i].nums[j]!=temp.nums[j])break;
if(j==9){
tail=i; //另一佇列中相同結點編號
return 1;
}
}
return 0;
}
void printpath(tnode statr[],int tair,tnode statb[],int taib){ //輸出路徑
int p[20],k=0;
while(tair>0){ //將正向擴充套件佇列逆轉
p[k++]=tair;
tair=statr[tair].last;
}
p[k]=0;
for(int i=k;i>=0;i--){ //輸出正向路徑
cout< cout< for(int j=0;j<9;j++){
cout< if((j+1)%3==0)cout< }
cout< }
while(taib>0){ //輸出逆向路徑
taib=statb[taib].last;
cout< cout< for(int i=0;i<9;i++){
cout< if((i+1)%3==0)cout< }
cout< }
}
void bfs(){ //搜尋主程式
tnode temp;
int hear=0,tair=0,heab=0,taib=0,i;
while(hear<=tair && heab<=taib){
for(i=1;i<5;i++){ //正向擴充套件佇列
temp=statr[hear];
if(expand(temp,i)){
if(repeat(statr,temp,tair))continue;
statr[++tair]=temp;
statr[tair].last=hear;
if(find(statb,statr[tair],taib)){
printpath(statr,tair,statb,taib);
return;
}
}
}
hear++;
for(i=1;i<5;i++){ //反向擴充套件佇列
temp=statb[heab];
if(expand(temp,i)){
if(repeat(statb,temp,taib))continue;
statb[++taib]=temp;
statb[taib].last=heab;
if(find(statr,statb[taib],tair)){
printpath(statr,tair,statb,taib);
return;
}
}
}
heab++;
}
}
void main(){
init();
bfs();
}
執行結果如下:
0 0 3 10 18 32 60
1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3 1 2 3
8 0 4—> 8 4 0—> 8 4 5—> 8 4 5—> 8 4 5—> 0 4 5—> 4 0 5—> 7 6 5 7 6 5 7 6 0 7 0 6 0 7 6 8 7 6 8 7 6
114(182) 42(74) 24 11 5 2 0 0
1 2 3 1 2 3 1 2 3 1 2 0 1 0 2 1 7 2 1 7 2 1 7 2
4 7 5--> 4 7 5—> 4 7 0—> 4 7 3 —> 4 7 3—> 4 0 3—> 4 6 3—> 4 6 3
8 0 6 8 6 0 8 6 5 8 6 5 8 6 5 8 6 5 8 0 5 0 8 5
結果表明,兩個方向的搜尋,需要擴充套件的節點數量大大減少,總共不到300個結點(括弧中是結點本身的編號,即它在佇列中的位置)。