當前位置:編程學習大全網 - 源碼下載 - 求八數碼問題算法,並說明下該算法優缺點,要算法,不是源代碼(可以沒有)。

求八數碼問題算法,並說明下該算法優缺點,要算法,不是源代碼(可以沒有)。

八數碼問題

壹.八數碼問題

八數碼問題也稱為九宮問題。在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.八數碼問題的基類

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

#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);

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,這是目標狀態。

三.線性表

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

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類的壹個子類。其類的結構和成員函數的實現如下:

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.廣度優先搜索法的缺點

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

五、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*算法的類聲明如下:

class TAstar:public TEight

{

public:

TAstar(){} //構造函數

TAstar(char *fname); //帶參數構造函數

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 *fname):TEight(fname)

{

for(int i=0;i<Num;)

{

r[p[i]]=i; //存儲初始狀態個個數字的位置

s[q[i]]=i++; //存儲目標狀態個個數字的位置

}

ifstream fin;

fin.open("eight_dis.txt",ios::in); //打開數據文件

if(!fin)

{

cout<<"不能打開數據文件!"<<endl;

return;

}

for(int 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();

int i;

for(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();

int i;

for(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*算法的測試:

int main()

{

TAstar aStar("eight.txt");

aStar.Search();

system("pauze");

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

eight_Result.txt中的結果(運行後得到的結果)

七、算法運行結果

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是奇數,所以他奇排列。

因此,可以在運行程序前檢查初始狀態和目標狀態的窘是否相同,相同則問題可解,應當能搜索到路徑。否則無解。

PS:整理自網絡

  • 上一篇:股票KDJ中J值最大、最小理論值是多少?實際是多少?
  • 下一篇:VC++是什麽
  • copyright 2024編程學習大全網