小時候玩過的移動拼圖遊戲。有一個3*3的棋盤,其中有0-8這9個數字,0表示空格,每次移動只能把空格旁邊的數字移到空格,即與0相鄰的數字可以和0交換位置。
求從初始狀態
2 3 0
7 1 6
5 8 4
變到目標狀態
1 2 3
4 5 6
7 8 0
的最佳移動方案。
今天學習了一下A*算法,並嘗試用其解決八數碼問題,搞了一下午終於搞定,性能確實比BFS好很多。
在上一篇博客BFS版的基礎上,爲每個結點添加g、h、f三個變量,g表示當前點到起點的代價,h表示當前點到終點的代價,總權值f=g+h。
維護兩個表,OPEN表和CLOSE表。這裏我用map實現,map<vector<int> ,Node*> CLOSE,OPEN; 結點的數據矩陣爲鍵,結點的指針爲值。便於判斷探索到的新結點是否在表中,同時也便於若探索到的結點在OPEN表中時,能夠通過數據矩陣獲取指針,從而進行OPEN表中結點的前驅及權值的修正。每次從OPEN表中選擇f最小的,放入CLOSE表中,並用其修正能夠得到的相鄰結點。此處的思想跟Dijkstra最短路徑一樣,不同之處在於Dijkstra是從OPEN表中選取g值最小的(若八數碼問題也這麼做,就是BFS),而A*是從OPEN表中選取f值最小的,更優。
/*2015.8.4cyq*/
//八數碼A*
#include <iostream>
#include <vector>
#include <map>
#include <time.h>
using namespace std;
const int MAX=2147483647;
struct Node{
vector<int> data;
int blankPos;//空格(用0表示)位置,用於計算出相鄰結點
int f,g,h; //f=g+h,g和h分別表示當前點到起點和到終點的代價
Node *parent;
};
//檢測結點是否相等
bool isEqual(Node *&a,Node *&b){
for(int i=1;i<9;i++)
if(a->data[i]!=b->data[i])
return false;
return true;
}
//檢測是否有解
bool canSolve(Node *&a,Node *&b){
int sum1=0,sum2=0;
for(int i=0;i<9;i++)
for(int j=i+1;j<9;j++){
if(a->data[i] > a->data[j]&&a->data[j]!=0)
sum1++;
if(b->data[i] > b->data[j]&&b->data[j]!=0)
sum2++;
}
return (sum1%2==sum2%2);//奇偶性相同纔有解
}
//顯示結點的數據矩陣
void Show(Node *&a){
if(a==nullptr)
return;
for(int i=0;i<9;i++){
if(a->data[i]==0)
cout<<" ";
else
cout<<a->data[i]<<" ";
if((i+1)%3==0)
cout<<endl;
}
}
//空格上移後的結點
Node* upNode(Node *&a){
if(a->blankPos<=2)
return nullptr;
else{
Node* tmp=new Node(*a);
swap(tmp->data[tmp->blankPos],tmp->data[tmp->blankPos-3]);
tmp->blankPos-=3;
return tmp;
}
}
//空格下移後的結點
Node* downNode(Node *&a){
if(a->blankPos>=6)
return nullptr;
else{
Node* tmp=new Node(*a);
swap(tmp->data[tmp->blankPos],tmp->data[tmp->blankPos+3]);
tmp->blankPos+=3;
return tmp;
}
}
//空格左移後的結點
Node* leftNode(Node *&a){
if(a->blankPos%3==0)
return nullptr;
else{
Node* tmp=new Node(*a);
swap(tmp->data[tmp->blankPos],tmp->data[tmp->blankPos-1]);
tmp->blankPos--;
return tmp;
}
}
//空格右移後的結點
Node* rightNode(Node *&a){
if(a->blankPos%3==2)
return nullptr;
else{
Node* tmp=new Node(*a);
swap(tmp->data[tmp->blankPos],tmp->data[tmp->blankPos+1]);
tmp->blankPos++;
return tmp;
}
}
//計算出兩個節點的距離
int nodeDistance(Node *&a,Node *&b){
int h1,h2;
int sum=0;
for(int k=1;k<=8;k++){//分別檢測數字0到8
for(int i=0;i<9;i++){
if(a->data[i]==k)
h1=i;
if(b->data[i]==k)
h2=i;
}
sum+=abs(h1/3-h2/3)+abs(h1%3-h2%3);
}
return sum;
}
int main(){
clock_t time1,time2;//用於計算程序運行時間
time1=clock();
//起始節點和目標結點的初始化
int ivec1[10]={2,3,0,
7,1,6,
5,8,4};//起始結點
//int ivec1[10]={0,8,7,
// 6,5,4,
// 3,2,1};//起始結點
int ivec2[10]={1,2,3,
4,5,6,
7,8,0};//目標結點
Node a,b;
for(int i=0;i<9;i++){//0代表空字符
a.data.push_back(ivec1[i]);
b.data.push_back(ivec2[i]);
}
for(int i=0;i<9;i++){//空格位置
if(a.data[i]==0)
a.blankPos=i;
if(b.data[i]==0)
b.blankPos=i;
}
Node* startNode=&a;
Node* targetNode=&b;
startNode->parent=nullptr;
startNode->h=nodeDistance(startNode,targetNode);
startNode->g=0;
startNode->f=startNode->h;
if(!canSolve(startNode,targetNode)){
cout<<"Can't solve!"<<endl;
return 0;
}
//OPEN表和CLOSE表,矩陣數據爲鍵,指針爲值,便於檢測矩陣是否訪問過
//同時也便於從矩陣獲取指針,進行OPEN表的修正
map<vector<int> ,Node*> CLOSE,OPEN;
OPEN[startNode->data]=startNode;
while(!OPEN.empty()){
//找到OPEN表中f最小的結點,移入CLOSE表
Node* root;
int fmin=MAX;
for(auto it=OPEN.begin();it!=OPEN.end();it++){
if((*it).second->f < fmin){
fmin=(*it).second->f;
root=(*it).second;
}
}
OPEN.erase(root->data);
CLOSE[root->data]=root;
if(isEqual(root,targetNode)){//找到目標結點,結束循環
targetNode->parent=root->parent;
break;
}
//用root對不在CLOSE表中的後繼結點進行修正,若下面的結點不在OPEN表,則加到OPEN表
//若下面的結點已經在OPEN表中,則進行修正,選取更優的路徑
Node* node1=upNode(root);
Node* node2=downNode(root);
Node* node3=leftNode(root);
Node* node4=rightNode(root);
if(node1!=nullptr&&CLOSE.find(node1->data)==CLOSE.end()){
node1->parent=root;
node1->g=root->g+1;
node1->h=nodeDistance(node1,targetNode);
node1->f=node1->g+node1->h;
if(OPEN.find(node1->data)==OPEN.end()){//新探索到的點
OPEN[node1->data]=node1;
}else{//該結點已經在OPEN表中,修正
Node* tmp=OPEN[node1->data];
if(tmp->g > node1->g){
tmp->g=node1->g;
tmp->parent=node1->parent;
tmp->h=node1->h;
tmp->f=node1->f;
}
}
}
if(node2!=nullptr&&CLOSE.find(node2->data)==CLOSE.end()){
node2->parent=root;
node2->g=root->g+1;
node2->h=nodeDistance(node2,targetNode);
node2->f=node2->g+node2->h;
if(OPEN.find(node2->data)==OPEN.end()){//新探索到的點
OPEN[node2->data]=node2;
}else{//該結點已經在OPEN表中,修正
Node* tmp=OPEN[node2->data];
if(tmp->g > node2->g){
tmp->g=node2->g;
tmp->parent=node2->parent;
tmp->h=node2->h;
tmp->f=node2->f;
}
}
}
if(node3!=nullptr&&CLOSE.find(node3->data)==CLOSE.end()){
node3->parent=root;
node3->g=root->g+1;
node3->h=nodeDistance(node3,targetNode);
node3->f=node3->g+node3->h;
if(OPEN.find(node3->data)==OPEN.end()){//新探索到的點
OPEN[node3->data]=node3;
}else{//該結點已經在OPEN表中,修正
Node* tmp=OPEN[node3->data];
if(tmp->g > node3->g){
tmp->g=node3->g;
tmp->parent=node3->parent;
tmp->h=node3->h;
tmp->f=node3->f;
}
}
}
if(node4!=nullptr&&CLOSE.find(node4->data)==CLOSE.end()){
node4->parent=root;
node4->g=root->g+1;
node4->h=nodeDistance(node4,targetNode);
node4->f=node4->g+node4->h;
if(OPEN.find(node4->data)==OPEN.end()){//新探索到的點
OPEN[node4->data]=node4;
}else{//該結點已經在OPEN表中,修正
Node* tmp=OPEN[node4->data];
if(tmp->g > node4->g){
tmp->g=node4->g;
tmp->parent=node4->parent;
tmp->h=node4->h;
tmp->f=node4->f;
}
}
}
}
//利用結點的parent指針回溯出路徑
Node* p=targetNode;
vector<Node*> result;
while(p!=nullptr){
result.push_back(p);
p=p->parent;
}
int count=0;
for(int i=result.size()-1;i>=0;i--){
cout<<"stage:"<<count++<<endl;
Show(result[i]);
}
time2=clock();
cout<<"程序運行耗費的毫秒數:"<<(time2-time1)<<endl;
return 0;
}
經過測試,求從初始狀態
2 3 0
7 1 6
5 8 4
變到目標狀態
1 2 3
4 5 6
7 8 0
花費83ms可以輸出具體步驟,經歷了14次變換,比上一篇博客BFS的2秒快很多。
而從初始狀態
0 8 7
6 5 4
3 2 1
變到目標狀態
1 2 3
4 5 6
7 8 0
僅僅耗費了327ms就輸出具體步驟,經歷了28次變換,完勝上一篇博客BFS的137秒。