八數碼問題的A*演算法
問題描述:八數碼問題即是找出一條狀態路徑,使初始狀態(start)轉換到目標狀態(end),一般選取目標狀態為:1 2 3 4 5 6 7 8 0(0代表空格)
0 1 2 1 2 3
3 4 5 ——> 4 5 6
6 7 8 7 8 0
下面通過幾個問題來對求解策略進行討論。
(Q1)任意給定初態和終態,是否存在路徑(可解性)?
(Q2)如何儲存狀態節點?
採用3*3或者1*9矩陣來儲存狀態節點,筆者也不例外,因為該儲存結構方便計算評價函式值以及列印路徑等。但是在此基礎上,筆者添加了一個mark域(一個int型),sizeof(int)=4*8=8*4,即能表示8個十六進位制數,則能夠標誌唯一一個狀態,這樣兩個狀態是否相同的判斷就從9個int型資料的比較變為1個int型的比較,因此能夠節省大量的時間(雖然說,計算標誌時也相當於一次矩陣比較,但是卻能做到“一勞永逸”)。
(Q3)為什麼選擇鏈式儲存結構?
與靜態儲存結構相比,鏈式儲存結構是“量體裁衣”,不會造成空間的浪費;另一方面,對於不同的初態,搜尋的深度不知,靜態表的長度也不能確定一個合適的值(既不浪費也能夠用);最重要的一點,搜尋過程,經常要進行節點的刪除,新增,若用靜態連結串列儲存必定會浪費大量的時間。
(Q4)搜尋方法如何選取?
八數碼問題的解空間樹屬排列樹,用於排列樹搜尋的方法主要有兩大類:一類是盲目搜尋,如深度優先搜尋DFS和廣度優先搜尋BFS;另一類是啟發式搜尋,如A*演算法。對於八數碼問題,深度優先搜尋的狀態空間搜尋樹的深度可能很深,或者可能至少要比某個可接受的解答序列的已知深度上限還要深。應用此策略很可能得不到解。寬度優先搜尋的優勢在於當問題有解時,一定能找到解,且能找到最優解。但其搜尋時需要儲存所有的待擴充套件結點,這樣很容易擴充套件那些沒有用的結點,造成狀態的指數增長,甚至"組合爆炸"。這對寬度優先搜尋很不利。這兩種搜尋方法的共同缺點是結點排序雜亂無章,往往在搜尋了大量無關結點後才能得到目的結點,只適合於複雜度較低的問題的求解。啟發式搜尋利用特定問題自身所攜帶的啟發資訊在一定程度上避免了盲目搜尋的不足。
(Q5)如何選取評價函式?
對於f(n)的考慮最簡單的便是比較每個狀態與目標狀態相比錯位的個數。這個啟發意味著如果其他條件相同,那麼錯位的個數最少的狀態可能最接近目標狀態。然而這個啟發沒有使用從棋盤格局中可以得到的所有資訊,因為它沒有把數字必要的移動距離納入考慮。一個“更好一點”的啟發是對錯位的牌必須要移動的距離求和,為了達到這個目的,每張牌必須要移動的每個方格算為一個距離單位。本文采用後者。
#include <stdio.h> #include <stdlib.h> #include <malloc.h> /***************構造資料結構******************/ typedef struct Node { int a[9]; //棋盤的狀態 int mark; //該狀態的標誌(為比較兩狀態是否相同節省時間) int dx; //深度 int fx; //評價函式值 struct Node* parent; //標誌父親節點(為列印路徑) struct Node* prior; //指示前驅 struct Node* next; //方便查表 }Node,*SNode; //棋盤狀態節點 typedef struct StateQueue { SNode front; //頭指標 SNode rear; //尾指標 }StateQueue; //以棋盤狀態為節點的狀態佇列 StateQueue closed,open; //closed 表和 open表 /************相關子函式申明***************/ void copySNode(SNode &t,SNode s); void initSQueue(StateQueue &Q , SNode s); void markState(SNode s); SNode searchSQueue(StateQueue Q,SNode s); void exSNode(SNode t); void childSolve(SNode s); void evaluate(SNode s); void insertOpen(SNode s); void eightPuzzle(); void printPath(SNode s); void openClosed(SNode s); bool isOK(); SNode start,end; /***************主函式****************/ int main() { printf("/****************八數碼遊戲**************/\n"); printf("問題描述:從給定初態達到目標狀態:\n"); printf("1 2 3 \n"); printf("4 5 6 \n"); printf("7 8 0\n" ); printf("請給出求解路徑!\n"); printf("/***************************************/"); printf("\n\n請輸入初始狀態:\n\n"); int i=0; start=(SNode)malloc(sizeof(Node)); end=(SNode)malloc(sizeof(Node)); //freopen("in.txt","r",stdin); for(i=0;i<9;++i) { scanf("%d",&start->a[i]); end->a[i]=i+1; } end->a[8]=0; start->dx=0; if(!isOK()) { printf("不能達到目的狀態!!!\n"); return 0; } evaluate(start); markState(start); markState(end); start->next=NULL; start->parent=NULL; start->prior=NULL; eightPuzzle(); return 0; } /**************相關子函式*****************/ /** *SNode節點的賦值 */ void copySNode(SNode &t,SNode s) { int i; t=(SNode)malloc(sizeof(Node)); for(i=0;i<9;++i) { t->a[i]=s->a[i]; } t->dx=s->dx; t->fx=s->fx; t->mark=s->mark; t->next=s->next; t->parent=s->parent; t->prior=s->prior; } /** *初始化StateQueue */ void initSQueue(StateQueue &Q , SNode s) { Q.front=s; Q.rear=s; } /** *為棋盤狀態mark賦值 */ void markState(SNode s) { int i,mark=0; s->mark=0; for(i=0;i<8;++i) { mark=s->a[i]; s->mark+=mark<<(4*(7-i)); } } /** *查StateQueue表,判斷s節點是否存在並返回查到節點 */ SNode searchSQueue(StateQueue Q,SNode s) { SNode q=Q.front; if(!q) return NULL; do { if(q->mark==s->mark) return q; q=q->next; }while(q); return NULL; } /** *擴充套件節點s並交由childSolve函式處理子節點 */ void exSNode(SNode t) { int i; SNode s; for(i=0;i<9;++i) //尋找空格位置 { if(!t->a[i]) break; } if(i%3) //空格向左移動 { copySNode(s,t); s->dx++; s->parent=t; s->a[i]=s->a[i-1]; s->a[i-1]=0; markState(s); childSolve(s); //處理擴充套件的子節點 } if(i%3!=2) //空格向右移動 { copySNode(s,t); s->dx++; s->parent=t; s->a[i]=s->a[i+1]; s->a[i+1]=0; markState(s); childSolve(s); //處理擴充套件的子節點 } if(i/3) //空格向上移動 { copySNode(s,t); s->dx++; s->parent=t; s->a[i]=s->a[i-3]; s->a[i-3]=0; markState(s); childSolve(s); //處理擴充套件的子節點 } if(i/3!=2) //空格向下移動 { copySNode(s,t); s->dx++; s->parent=t; s->a[i]=s->a[i+3]; s->a[i+3]=0; markState(s); childSolve(s); //處理擴充套件的子節點 } } /** *處理子節點(即是,判斷是否加入open表,插入位置) */ void childSolve(SNode s) { SNode p; if(p=searchSQueue(open,s)) //在open表中 { if(p->dx>s->dx) //若子狀態沿著更短的路徑 { s->fx=p->fx-p->dx+s->dx; s->mark=p->mark; if(!p->prior) //表頭 { p->next->prior=NULL; open.front=p->next; } else if(!p->next) //表尾 { p->prior->next=NULL; open.rear=p->prior; } else { p->next->prior=p->prior; p->prior->next=p->next; } free(p); } else return ; //若子狀態路徑更長,不加入open表 } else if(p=searchSQueue(closed,s)) //在closed表中 { if(p->dx>s->dx) //若子狀態沿著更短路徑, { s->fx=p->fx-p->dx+s->dx; s->mark=p->mark; if(!p->prior) //表頭 { p->next->prior=NULL; closed.front=p->next; } else if(!p->next) //表尾 { p->prior->next=NULL; closed.rear=p->prior; } else { p->next->prior=p->prior; p->prior->next=p->next; } free(p); } else return ; //否則,不加入open表 } else //既不在open表,也不在closed表中 { evaluate(s); //評價該狀態 } insertOpen(s); //把s插入open表中 } /** *evaluate評價該狀態,即為求fx的值 */ void evaluate(SNode s) { int i,value=0; s->fx=s->dx; for(i=0;i<9;++i) { if(!s->a[i]) value=8-i; else value=s->a[i]-i-1; if(value>0) { s->fx+=value; } else s->fx-=value; } } /** *insertOpen插入open表中 */ void insertOpen(SNode s) { SNode p=open.front; while(p->next&&p->fx<=s->fx) { p=p->next; } if(p->fx>s->fx) { s->prior=p->prior; //更改前驅及後繼指標 s->next=p; if(p->prior) p->prior->next=s; else open.front=s; p->prior=s; } else { p->next=s; s->prior=p; s->next=NULL; open.rear=s; } } /** *除錯程式是用到的輔助函式 void print() { SNode p; p=open.front; printf("\nopen:\n"); while(p) { printf("%.8x\n",p->mark); p=p->next; } p=closed.front; printf("\nclosed:\n"); while(p) { printf("%.8x\n",p->mark); p=p->next; } } **/ /** *八數碼問題求解策略 */ void eightPuzzle() { initSQueue(open,start); initSQueue(closed,NULL); SNode s=open.front; while(s->mark!=end->mark) { exSNode(s); openClosed(s); //把s從open表移到closed表的隊尾 s=open.front; } printPath(s); } /** *把s從open表移到closed表的隊尾 */ void openClosed(SNode s) { if(!s->prior) //表頭 { s->next->prior=NULL; open.front=s->next; } else if(!s->next) //表尾 { s->prior->next=NULL; } else { s->next->prior=s->prior; s->prior->next=s->next; } if(!closed.front) { closed.front=s; closed.rear=s; s->next=NULL; s->prior=NULL; } else { if(!closed.front->next) closed.front->next=s; closed.rear->next=s; s->prior=closed.rear; s->next=NULL; closed.rear=s; } } /** *打印出移動路徑 */ void printPath(SNode s) { SNode p=s; int i,j; p->next=NULL; while(p->parent) { p->parent->next=p; p=p->parent; } while(p) { for(i=0;i<3;++i) { for(j=i*3;j<3*(i+1);++j) { printf("%d ",p->a[j]); } printf("\n"); } printf("\n\n"); p=p->next; } } /** *判斷初始狀態能否到達目標狀態 */ bool isOK() { int i,j,count=0; //count記錄逆序數和 for(i=0;i<9;++i) { if(start->a[i]) { for(j=i+1;j<9;++j) { if(start->a[j]&&start->a[j]<start->a[i]) { count++; } } } } if(!(count%2)) return true; else return false ; }
不足之處:1)評價函式可以更加合理,提高搜尋效率;2)本質是找出一系列狀態,從start->end,end狀態也可以隨意變化。
注:改進方法還是比較簡單的,希望看到此博文的人可以自行練習。