移動拼圖遊戲(八數碼問題)A*版

小時候玩過的移動拼圖遊戲。有一個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秒。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章