八數碼問題及A*算法


一.八數碼問題
八數碼問題也稱為九宮問題。在3×3的棋盤,擺有八個棋子,每個棋子上標有1至8的某一數字,不同棋子上標的數字不相同。棋盤上還有一個空格,與空格相鄰的棋子可以移到空格中。要求解決的問題是:給出一個初始狀態和一個目標狀態,找出一種從初始轉變成目標狀態的移動棋子步數最少的移動步驟。
所謂問題的一個狀態就是棋子在棋盤上的一種擺法。棋子移動后,狀態就會發生改變。解八數碼問題實際上就是找出從初始狀態到達目標狀態所經過的一系列中間過渡狀態。
八數碼問題一般使用搜索法來解。
搜索法有廣度優先搜索法、深度優先搜索法、A*算法等。這里通過用不同方法解八數碼問題來比較一下不同搜索法的效果。

二.搜索算法基類
1.八數碼問題的狀態表示
八數碼問題的一個狀態就是八個數字在棋盤上的一種放法。每個棋子用它上面所標的數字表示,並用0表示空格,這樣就可以將棋盤上棋子的一個狀態存儲在一個一維數組p[9]中,存儲的順序是從左上角開始,自左至右,從上到下。也可以用一個二維數組來存放。
2.結點
搜索算法中,問題的狀態用結點描述。結點中除了描述狀態的數組p[9]外,還有一個父結點指針last,它記錄了當前結點的父結點編號,如果一個結點v是從結點u經狀態變化而產生的,則結點u就是結點v的父結點,結點v的last記錄的就是結點u的編號。在到達目標結點后,通過last 可以找出搜索的路徑。
3.類的結構
在C++中用類來表示結點,類將結點有關的數據操作封裝在一起。
不同的搜索算法具有一定共性,也有各自的個性,因此這里將不同搜索算法的共有的數據和功能封裝在一個基類中,再通過繼承方式實現不同的搜索算法。
4.結點擴展規則
搜索就是按照一定規則擴展已知結點,直到找到目標結點或所有結點都不能擴展為止。
八數碼問題的結點擴展應當遵守棋子的移動規則。按照棋子移動的規則,每一次可以將一個與空格相鄰棋子移動到空格中,實際上可以看作是空格作相反移動。空格移動的方向可以是右、下、左、上,當然不能移出邊界。
棋子的位置,也就是保存狀態的數組元素的下標。空格移動后,它的位置發生變化,在不移出界時,空格向右、下、左和上移動后,新位置是原位置分別加上1、3、-1、-3,如果將空格向右、下、左和上移動分別用0、1、2、3表示,並將-3、3、-1、1放在靜態數組d[4]中,空格位置用spac表示,那么空格向方向i移動后,它的位置變為spac+d[i]。
空格移動所產生的狀態變化,反映出來則是將數組p[]中,0的新位置處的數與0交換位置。
5.八數碼問題的基類

八數碼問題的基類及其成員函數的實現如下:

View Code
#define Num 9   
class TEight  
{  
public:  
    TEight(){}  
    TEight(char *fname);  
    virtual void Search()=0;  
protected:  
    int p[Num];  
    int last,spac;  
    static int q[Num],d[],total;  
    void Printf();  
    bool operator==(const TEight &T);  
    bool Extend(int i);  
};  
int TEight::q[Num];  
int TEight::d[]={1,3,-1,-3};  
int TEight::total=0;  
  
TEight::TEight(char *fname)  
{  
    ifstream fin;  
    fin.open(fname,ios::in | ios::nocreate);  
    if(!fin)  
    {  
        cout<<"不能打開數據文件!"<<endl;  
        return;  
    }  
    int i;  
    for(i=0;i<Num;)  
        fin>>p[i++];  
    fin>>spac;  
    for(i=0;i<Num;)  
        fin>>q[i++];  
    fin.close();  
    last=-1;  
    total=0;  
}  
  
void TEight::Printf()  
{  
    ofstream fout;  
    fout.open("Eight_result.txt",ios::ate|ios::app);  
    fout<<total++<<"t";  
    for(int i=0;i<Num;)  
        fout<<"  "<<p[i++];  
    fout<<endl;  
    fout.close();  
}  
  
bool TEight::operator==(const TEight &T)  
{  
    for(int i=0;i<Num;)  
        if(T.p[i]!=p[i++])  
            return 0;  
    return 1;  
}  
  
bool TEight::Extend(int i)  
{  
    if(i==0 && spac%3==2 || i==1 && spac>5   
        || i==2 && spac%3==0 || i==3 && spac<3)  
        return 0;  
    int temp=spac;  
    spac+=d[i];  
    p[temp]=p[spac];  
    p[spac]=0;  
    return 1;  
}  

數據文件的結構:
一共三行,第一行是用空格隔開的九個數字0~8,這是初始狀態。第二行是一個數字,空格(數字0)的位置,第三行也是用空格隔開的九個數字0~8,這是目標狀態。

三.線性表
搜索法在搜索過程中,需要使用一個隊列存儲搜索的中間結點,為了在找到目標結點后,能夠找到從初始結點到目標結點的路徑,需要保留所有搜索過的結點。另一方面,不同問題甚至同一問題的不同搜索方法中,需要存儲的結點數量相差很大,所以這里采用鏈式線性表作為存儲結構,同時,為適應不同問題,線性表設計成類模板形式。

View Code
template<class Type> class TList;            //線性表前視定義   
  
template<class Type> class TNode            //線性表結點類模板   
{  
       friend class TList<Type>;  
  public:  
       TNode(){}  
       TNode(const Type& dat);  
private:  
       TNode<Type>* Next;  
       Type Data;  
};  
  
template<class Type> class TList  
{  
public:  
       TList(){Last=First=0;Length=0;}   //構造函數   
       int Getlen()const{return Length;}   //成員函數,返回線性表長度   
       int Append(const Type& T);           //成員函數,從表尾加入結點   
       int Insert(const Type& T,int k);       //成員函數,插入結點   
       Type GetData(int i);                         //成員函數,返回結點數據成員   
       void SetData(const Type& T,int k); //成員函數,設置結點數據成員      
private:  
       TNode<Type> *First,*Last;             //數據成員,線性表首、尾指針     
       int Length;                                         //數據成員,線性表長度   
};  
  
template<class Type> int TList<Type>::Append(const Type& T)  
{  
       Insert(T,Length);  
       return 1;  
}  
  
template<class Type> int TList<Type>::Insert(const Type& T,int k)  
{  
       TNode<Type> *p=new TNode<Type>;  
       p->Data=T;  
       if(First)  
       {  
              if(k<=0)  
              {  
                     p->Next=First;  
                     First=p;  
              }  
              if(k>Length-1)  
              {  
                     Last->Next=p;     
                     Last=Last->Next;  
                     Last->Next=0;  
              }  
              if(k>0 && k<Length)  
           {  
              k--;  
              TNode<Type> *q=First;  
              while(k-->0)  
              q=q->Next;  
              p->Next=q->Next;  
              q->Next=p;  
           }  
       }  
       else  
       {  
              First=Last=p;  
              First->Next=Last->Next=0;  
       }  
       Length++;  
       return 1;  
}  
  
template<class Type> Type TList<Type>::GetData(int k)  
{  
       TNode<Type> *p=First;  
       while(k-->0)  
              p=p->Next;  
       return p->Data;  
}  
  
template<class Type> void TList<Type>::SetData(const Type& T,int k)  
{  
       TNode<Type> *p=First;  
       while(k-->0)  
              p=p->Next;  
       p->Data=T;  
}  

線性表單獨以頭文件形式存放。

四.廣度優先搜索法
在搜索法中,廣度優先搜索法是尋找最短路經的首選。
1.廣度優先搜索算法的基本步驟
1)建立一個隊列,將初始結點入隊,並設置隊列頭和尾指針
2)取出隊列頭(頭指針所指)的結點進行擴展,從它擴展出子結點,並將這些結點按擴展的順序加入隊列。 
3)如果擴展出的新結點與隊列中的結點重復,則拋棄新結點,跳至第六步。
4)如果擴展出的新結點與隊列中的結點不重復,則記錄其父結點,並將它加入隊列,更新隊列尾指針。
5)如果擴展出的結點是目標結點,則輸出路徑,程序結束。否則繼續下一步。
6)如果隊列頭的結點還可以擴展,直接返回第二步。否則將隊列頭指針指向下一結點,再返回第二步。
2.搜索路徑的輸出
搜索到目標結點后,需要輸出搜索的路徑。每個結點有一個數據域last,它記錄了結點的父結點,因此輸出搜索路徑時,就是從目標結點Q出發,根據last找到它的父結點,再根據這個結點的last找到它的父結點,....,最后找到初始結點。搜索的路徑就是從初始結點循相反方向到達目標結點的路徑。
3.廣度優先搜索法TBFS類的結構
廣度優先搜索法TBFS類是作為TEight類的一個子類。其類的結構和成員函數的實現如下:

View Code
class TBFS:public TEight  
{  
public:  
    TBFS(){}  
    TBFS(char *fname):TEight(fname){}  
    virtual void Search();  
private:  
    void Printl(TList<TBFS> &L);  
    int Repeat(TList<TBFS> &L);  
    int Find();  
};  
  
void TBFS::Printl(TList<TBFS> &L)  
{  
    TBFS T=*this;  
    if(T.last==-1)  
        return;  
    else  
    {  
        T=L.GetData(T.last);  
        T.Printl(L);  
        T.Printf();  
    }  
}  
  
int TBFS::Repeat(TList<TBFS> &L)  
{  
    int n=L.Getlen();  
    int i;  
    for(i=0;i<n;i++)  
        if(L.GetData(i)==*this)  
            break;  
    return i;  
}  
  
int TBFS::Find()  
{  
    for(int i=0;i<Num;)  
        if(p[i]!=q[i++])  
            return 0;  
    return 1;  
}  
  
void TBFS::Search()  
{  
    TBFS T=*this;  
    TList<TBFS> L;  
    L.Append(T);  
    int head=0,tail=0;  
    while(head<=tail)  
    {  
        for(int i=0;i<4;i++)  
        {  
            T=L.GetData(head);  
            if(T.Extend(i) && T.Repeat(L)>tail)  
            {  
                T.last=head;  
                L.Append(T);  
                tail++;  
            }  
            if(T.Find())  
            {  
                T.Printl(L);  
                T.Printf();  
                return;  
            }  
        }  
        head++;  
    }  
}  

4.廣度優先搜索法的缺點
廣度優先搜索法在有解的情形總能保證搜索到最短路經,也就是移動最少步數的路徑。但廣度優先搜索法的最大問題在於搜索的結點數量太多,因為在廣度優先搜索法中,每一個可能擴展出的結點都是搜索的對象。隨着結點在搜索樹上的深度增大,搜索的結點數會很快增長,並以指數形式擴張,從而所需的存儲空間和搜索花費的時間也會成倍增長。

五.雙向廣度優先搜索法
1.雙向廣度優先搜索法
八數碼問題具有可逆性,也就是說,如果可以從一個狀態A擴展出狀態B,那么同樣可以從狀態B擴展出狀態A,這種問題既可以從初始狀態出發,搜索目標狀態,也可以從目標狀態出發,搜索初始狀態。對這類問題如果采用雙向廣度優先搜索法,將可以大大節省搜索的時間。
所謂雙向廣度優先搜索法,是同時從初始狀態和目標狀態出發,采用廣度優先搜索的策略,向對方搜索,如果問題存在解,則兩個方向的搜索會在中途相遇,即搜索到同一個結點。將兩個方向的搜索路徑連接起來,就可以得到從初始結點到目標結點的搜索路徑。
2.雙向廣度優先搜索算法
雙向廣度優先搜索算法的基本步驟如下:
1)建立兩個隊列,一個是正向搜索的隊列,另一個是反向搜索的隊列。將初始結點放入正向隊列,將目標結點放入反向隊列,並設置兩個隊列的頭和尾指針。
2)從正向隊列取出隊列頭(頭指針所指)的結點進行擴展。
3)如果擴展出的新結點與隊列中的結點重復,則拋棄新結點,跳至第六步。
4)如果擴展出的新結點與隊列中的結點不重復,則記錄其父結點,並將它加入隊列,更新隊列尾指針。
5)檢查擴展出的結點是否在另一方向的隊列中,如果是則兩個方向的搜索相遇,顯示搜索路徑,程序結束。否則繼續下一步。
6)如果隊列頭的結點還可以擴展,直接返回第二步。否則將隊列頭指針指向下一結點,然后對另一方向搜索的隊列,按照第二步開始的同樣步驟處理。
3.雙向廣度優先搜索法的優勢
廣度優先搜索法搜索時,結點不斷擴張,深度越大,結點數越多。如果從兩個方向向對方搜索,就會在路徑中間某個地方相會,這樣,雙方的搜索的深度都不大,所搜索過的結點數就少得多,搜索時間也就節省不少。
從理論上說,如果每一結點可擴展的子結點數為m,廣度優先搜索的搜索樹就是一顆m叉樹,也就是每個結點都由m個分支。按完全m叉樹計算,如果目標結點在第n層,廣度優先搜索就必須在搜索樹上擴展完n-1層的所有結點,擴展的結點數為m(mn-1)/(m-1)。對於雙向廣度優先搜索來說,如果兩個方向的搜索在第i層生成同一子結點,那么正向搜索擴展的結點數為m(mi-1)/(m-1),反向搜索擴展的結點數為m(mn-i-1)/(m-1),搜索的結點總數為m(mi+mn-i-1)/(m-1)(其中n是最優解路徑長度,i=(m+1) div 2,)。設n為偶數(n=2*i),廣度優先雙向搜索擴展的結點數約是廣度優先搜索的2/(mi/2+1)*100%,相對減少(mi/2-1)/(mi/2+1)*100%。
4.判斷兩個方向的搜索相遇
在雙向廣度優先搜索法中,如何判斷兩個方向的搜索相遇呢?只要我們在生成結點的同時,判斷該結點是否出現在相反方向的搜索樹上即可,也就是說,在某個方向搜索中擴展出一個新結點,如果它與另一個方向已擴展出的結點重復,也就找到了解。
5.雙向廣度優先搜索法的TDBFS類結構
雙向廣度優先搜索法的TDBFS和廣度優先搜索法類似,也是TEight類的子類,類結構及其成員函數的實現如下:

View Code
class TDBFS:public TEight  
{  
public:  
    TDBFS(){}  
    TDBFS(char *fname):TEight(fname){}  
    virtual void Search();  
private:  
    void Printp(TList<TDBFS> &L);  
    void Printb(TList<TDBFS> &L);  
    int Repeat(TList<TDBFS> &L);    
};  
  
void TDBFS::Printp(TList<TDBFS> &L)  
{  
    TDBFS T=*this;  
    if(T.last==-1)  
        return;  
    else  
    {  
        T=L.GetData(T.last);  
        T.Printp(L);  
        T.Printf();  
    }  
}  
  
void TDBFS::Printb(TList<TDBFS> &L)  
{  
    TDBFS T=*this;  
    while(T.last>-1)  
    {  
        T=L.GetData(T.last);  
        T.Printf();  
    }   
}  
  
int TDBFS::Repeat(TList<TDBFS> &L)  
{  
    int n=L.Getlen();  
    int i;  
    for(i=0;i<n;)  
        if(L.GetData(i++)==*this)  
            break;  
    return i;  
}  
  
void TDBFS::Search()  
{  
    TDBFS T1=*this;  
    TDBFS T2;  
    for(int i=0;i<Num;i++)  
    {  
        T2.p[i]=q[i];  
        if(q[i]==0)  
            T2.spac=i;  
    }  
    T2.last=-1;  
    TList<TDBFS> L1,L2;  
    L1.Append(T1);  
    L2.Append(T2);  
    int head1=0,tail1=0,head2=0,tail2=0;  
    while(head1<=tail1 || head2<=tail2)  
    {  
        for(int i=0;i<4;i++)  
        {  
            T1=L1.GetData(head1);  
            if(T1.Extend(i) && T1.Repeat(L1)>tail1)  
            {  
                T1.last=head1;  
                L1.Append(T1);  
                tail1++;  
            }  
            int m=T1.Repeat(L2);  
            if(m<tail2)  
            {  
                T1.Printp(L1);  
                T1.Printf();  
                T2=L2.GetData(m);  
                T2.Printb(L2);  
                return;  
            }  
        }  
        head1++;  
        for(i=0;i<4;i++)  
        {  
            T2=L2.GetData(head2);  
            if(T2.Extend(i) && T2.Repeat(L2)>tail2)  
            {  
                T2.last=head2;  
                L2.Append(T2);  
                tail2++;  
            }  
            int m=T2.Repeat(L1);  
            if(m<tail1)  
            {  
                T1=L1.GetData(m);  
                T1.Printb(L1);  
                T1.Printf();  
                T2.Printp(L2);  
  
                return;  
            }  
        }  
        head2++;  
    }  
}  

六.A*算法
1.啟發式搜索
廣度優先搜索和雙向廣度優先搜索都屬於盲目搜索,這在狀態空間不大的情況下是很合適的算法,可是當狀態空間十分龐大時,它們的效率實在太低,往往都是在搜索了大量無關的狀態結點后才碰到解答,甚至更本不能碰到解答。
搜索是一種試探性的查尋過程,為了減少搜索的盲目性引,增加試探的准確性,就要采用啟發式搜索了。所謂啟發式搜索就是在搜索中要對每一個搜索的位置進行評估,從中選擇最好、可能容易到達目標的位置,再從這個位置向前進行搜索,這樣就可以在搜索中省略大量無關的結點,提高了效率。
2.A*算法
A*算法是一種常用的啟發式搜索算法。
在A*算法中,一個結點位置的好壞用估價函數來對它進行評估。A*算法的估價函數可表示為: 
f'(n) = g'(n) + h'(n) 
這里,f'(n)是估價函數,g'(n)是起點到終點的最短路徑值(也稱為最小耗費或最小代價),h'(n)是n到目標的最短路經的啟發值。由於這個f'(n)其實是無法預先知道的,所以實際上使用的是下面的估價函數:
f(n) = g(n) + h(n) 
其中g(n)是從初始結點到節點n的實際代價,h(n)是從結點n到目標結點的最佳路徑的估計代價。在這里主要是h(n)體現了搜索的啟發信息,因為g(n)是已知的。用f(n)作為f'(n)的近似,也就是用g(n)代替g'(n),h(n)代替h'(n)。這樣必須滿足兩個條件:(1)g(n)>=g'(n)(大多數情況下都是滿足的,可以不用考慮),且f必須保持單調遞增。(2)h必須小於等於實際的從當前節點到達目標節點的最小耗費h(n)<=h'(n)。第二點特別的重要。可以證明應用這樣的估價函數是可以找到最短路徑的。
3.A*算法的步驟
A*算法基本上與廣度優先算法相同,但是在擴展出一個結點后,要計算它的估價函數,並根據估價函數對待擴展的結點排序,從而保證每次擴展的結點都是估價函數最小的結點。
A*算法的步驟如下:
1)建立一個隊列,計算初始結點的估價函數f,並將初始結點入隊,設置隊列頭和尾指針。
2)取出隊列頭(隊列頭指針所指)的結點,如果該結點是目標結點,則輸出路徑,程序結束。否則對結點進行擴展。 
3)檢查擴展出的新結點是否與隊列中的結點重復,若與不能再擴展的結點重復(位於隊列頭指針之前),則將它拋棄;若新結點與待擴展的結點重復(位於隊列頭指針之后),則比較兩個結點的估價函數中g的大小,保留較小g值的結點。跳至第五步。
4)如果擴展出的新結點與隊列中的結點不重復,則按照它的估價函數f大小將它插入隊列中的頭結點后待擴展結點的適當位置,使它們按從小到大的順序排列,最后更新隊列尾指針。
5)如果隊列頭的結點還可以擴展,直接返回第二步。否則將隊列頭指針指向下一結點,再返回第二步。
4.八數碼問題的A*算法的估價函數
估價函數中,主要是計算h,對於不同的問題,h有不同的含義。那么在八數碼問題中,h的含意是各什么?八數碼問題的一個狀態實際上是數字0~8的一個排列,用一個數組p[9]來存儲它,數組中每個元素的下標,就是該數在排列中的位置。例如,在一個狀態中,p[3]=7,則數字7的位置是3。如果目標狀態數字3的位置是8,那么數字7對目標狀態的偏移距離就是3,因為它要移動3步才可以回到目標狀態的位置。
八數碼問題中,每個數字可以有9個不同的位置,因此,在任意狀態中的每個數字和目標狀態中同一數字的相對距離就有9*9種,可以先將這些相對距離算出來,用一個矩陣存儲,這樣只要知道兩個狀態中同一個數字的位置,就可查出它們的相對距離,也就是該數字的偏移距離:
       0 1 2 3 4 5 6 7 8
0      0 1 2 1 2 3 2 3 4
1      1 0 1 2 1 2 3 2 3
2      2 1 0 3 2 1 4 3 2
3      1 2 3 0 1 2 1 2 3
4      2 1 2 1 0 1 2 1 2
5      3 2 1 2 1 0 3 2 1
6      2 3 4 1 2 3 0 1 2
7      3 2 3 2 1 2 1 0 1
8      4 3 2 3 2 1 2 1 0
例如在一個狀態中,數字8的位置是3,在另一狀態中位置是7,那么從矩陣的3行7列可找到2,它就是8在兩個狀態中的偏移距離。
估價函數中的h就是全體數字偏移距離之和。
顯然,要計算兩個不同狀態中同一數字的偏移距離,需要知道該數字在每個狀態中的位置,這就要對數組p[9]進行掃描。由於狀態發生變化,個數字的位置也要變化,所以每次計算h都沿線掃描數組,以確定每個數字在數組中的位置。為了簡化計算,這里用一個數組存儲狀態中各個數字的位置,並讓它在狀態改變時隨着變化,這樣就不必在每次計算h時,再去掃描狀態數組。
例如,某個狀態中,數字5的位置是8,如果用數組r[9]存儲位置,那么就有r[5]=8。
現在用數組r[9]存儲當前狀態的數字位置,而用s[9]存儲目標狀態的數字位置,那么當前狀態數字i對目標狀態的偏移距離就是矩陣中r[i]行s[i]列對應的值。
5.A*算法的類結構
A*算法的類聲明如下:

View Code
class TAstar:public TEight  
{  
public:  
    TAstar(){}                           //構造函數   
    TAstar(char *fname1,char *fname2);   //帶參數構造函數    
    virtual void Search();               //A*搜索法   
private:  
    int f,g,h;                               //估價函數     
    int r[Num];                              //存儲狀態中各個數字位置的輔助數組   
    static int s[Num];                       //存儲目標狀態中各個數字位置的輔助數組   
    static int e[];                            //存儲各個數字相對距離的輔助數組      
    void Printl(TList<TAstar> L);       //成員函數,輸出搜索路徑    
    int Expend(int i);                        //成員函數,A*算法的狀態擴展函數    
    int Calcuf();                             //成員函數,計算估價函數   
    void Sort(TList<TAstar>& L,int k);  //成員函數,將新擴展結點按f從小到大順序插入待擴展結點隊列   
    int Repeat(TList<TAstar> &L);       //成員函數,檢查結點是否重復   
};  
  
int TAstar::s[Num],TAstar::e[Num*Num];  
  
TAstar::TAstar(char *fname1,char *fname2):TEight(fname1)  
{   
    for(int i=0;i<Num;)  
    {  
        r[p[i]]=i;   //存儲初始狀態個個數字的位置   
        s[q[i]]=i++; //存儲目標狀態個個數字的位置   
    }  
    ifstream fin;                       
    fin.open(fname2,ios::in | ios::nocreate);//打開數據文件   
    if(!fin)  
    {  
        cout<<"不能打開數據文件!"<<endl;  
        return;  
    }  
    for(i=0;i<Num*Num;i++)   //讀入各個數字相對距離值     
        fin>>e[i];  
    fin.close();  
    f=g=h=0;       //估價函數初始值     
}  
  
void TAstar::Printl(TList<TAstar> L)   
{  
    TAstar T=*this;  
    if(T.last==-1)  
        return;  
    else  
    {  
        T=L.GetData(T.last);  
        T.Printl(L);  
        T.Printf();  
    }  
}  
  
int TAstar::Expend(int i)                        
{  
    if(Extend(i))   //結點可擴展   
    {  
        int temp=r[p[r[0]]];   //改變狀態后數字位置變化,存儲改變后的位置    
        r[p[r[0]]]=r[0];  
        r[0]=temp;  
        return 1;  
    }  
    return 0;  
}  
  
int TAstar::Calcuf()  
{  
    h=0;  
    for(int i=0;i<Num;i++)  //計算估價函數的h   
        h+=e[Num*r[i]+s[i]];  
    return ++g+h;                                       
}  
  
void TAstar::Sort(TList<TAstar>& L,int k)  
{  
    int n=L.Getlen();  
    for(int i=k+1;i<n;i++)  
    {  
        TAstar T=L.GetData(i);  
        if(this->f<=T.f)  
            break;  
    }  
    L.Insert(*this,i);  
}  
  
int TAstar::Repeat(TList<TAstar> &L)  
{  
    int n=L.Getlen();  
    for(int i=0;i<n;i++)  
        if(L.GetData(i)==*this)  
            break;  
    return i;  
}  
  
void TAstar::Search()  
{  
    TAstar T=*this;              //初始結點   
    T.f=T.Calcuf();               //初始結點的估價函數   
    TList<TAstar> L;           //建立隊列   
    L.Append(T);                 //初始結點入隊   
    int head=0,tail=0;           //隊列頭和尾指針   
    while(head<=tail)           //隊列不空則循環   
    {  
        for(int i=0;i<4;i++)            //空格可能移動方向   
        {  
            T=L.GetData(head);   //去隊列頭結點   
            if(T.h==0)                   //是目標結點   
            {     
                T.Printl(L);//輸出搜索路徑   
                T.Printf();    //輸出目標狀態   
                return;        //結束   
            }  
            if(T.Expend(i))          //若結點可擴展   
            {  
                int k=T.Repeat(L); //返回與已擴展結點重復的序號        
                if(k<head)               //如果是不能擴展的結點   
                    continue; //丟棄   
                T.last=head;      //不是不能擴展的結點,記錄父結點   
                T.f=T.Calcuf(); //計算f   
                if(k<=tail)         //新結點與可擴展結點重復   
                {  
                    TAstar Temp=L.GetData(k);   
                    if(Temp.g>T.g)         //比較兩結點g值   
                        L.SetData(T,k); //保留g值小的   
                    continue;  
                }      
                T.Sort(L,head) ;            //新結點插入可擴展結點隊列        
                tail++;        //隊列尾指針后移    
            }  
        }  
        head++; //一個結點不能再擴展,隊列頭指針指向下一結點   
    }  
}  

七.測試程序

A*算法的測試:

View Code
int main()  
{  
    TAstar aStar("eight.txt","eight_dis.txt");  
    aStar.Search();  
    return 0;  
}  

eight.txt文件中的數據(初始態和目標態):
一共三行,第一行是用空格隔開的九個數字0~8,這是初始狀態。第二行是一個數字,空格(數字0)的位置,第三行也是用空格隔開的九個數字0~8,這是目標狀態。

8 3 5 1 2 7 4 6 0
8
1 2 3 4 5 6 7 8 0

eight_dis.txt中的數據(估計函數使用):
0 1 2 1 2 3 2 3 4
1 0 1 2 1 2 3 2 3
2 1 0 3 2 1 4 3 2
1 2 3 0 1 2 1 2 3
2 1 2 1 0 1 2 1 2
3 2 1 2 1 0 3 2 1
2 3 4 1 2 3 0 1 2
3 2 3 2 1 2 1 0 1

4 3 2 3 2 1 2 1 0

八.算法運行結果
1.BFS算法只能適用於到達目標結點步數較少的情況,如果步數超過15步,運行時間太長,實際上不再起作用。
2.對於隨機生成的同一個可解狀態,BFS算法最慢,DBFS算法較慢,A*算法較快。但在15步以內,DBFS算法與A*算法相差時間不大,超過15步后,隨步數增加,A*算法的優勢就逐漸明顯,A*算法要比DBFS算法快5倍以上,並隨步數增大而增大。到25步以上,DBFS同樣因運行時間過長而失去價值。
3.一般來說,解答的移動步數每增加1,程序運行時間就要增加5倍以上。由於八數碼問題本身的特點,需要檢查的節點隨步數增大呈指數形式增加,即使用A*算法,也難解決移動步數更多的問題。

九.問題可解性
八數碼問題的一個狀態實際上是0~9的一個排列,對於任意給定的初始狀態和目標,不一定有解,也就是說從初始狀態不一定能到達目標狀態。因為排列有奇排列和偶排列兩類,從奇排列不能轉化成偶排列或相反。
如果一個數字0~8的隨機排列871526340,用F(X)表示數字X前面比它小的數的個數,全部數字的F(X)之和為Y=∑(F(X)),如果Y為奇數則稱原數字的排列是奇排列,如果Y為偶數則稱原數字的排列是偶排列。
例如871526340這個排列的
Y=0+0+0+1+1+3+2+3+0=10
10是偶數,所以他偶排列。871625340
Y=0+0+0+1+1+2+2+3+0=9
9是奇數,所以他奇排列。
因此,可以在運行程序前檢查初始狀態和目標狀態的窘是否相同,相同則問題可解,應當能搜索到路徑。否則無解。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM