题目2:隐式图的搜索问题(A*算法解决八数码)

数据结构课程实践系列

题目1:学生成绩档案管理系统(实验准备)

题目2:隐式图的搜索问题(A*算法解决八数码)

题目3:文本文件单词的检索与计数(实验准备)

声明

题目要求

看过这次实验要求之后在这里插入图片描述

总结:利用A*来解决八数码问题,状态很好找,每次移动空格就会形成一种新的状态,

何为八数码?

八数码游戏包括一个3X3的棋盘,棋盘上摆放着8个数字的棋子,留下一个空位。与空位相邻的棋子可以滑动到空位中。游戏的目的是要达到一个特定的目标状态。标注的形式化如下(举例):

23
184
765

(初始状态)

123
84
765

(目标状态)

状态如何表示

  1. 每个状态都用3*3的数组表示,但是BFS中需要入队出队,比较麻烦而且空间占用较大
  2. 状态压缩,采用一个整数保存状态的数字序列,例如状态1表示为203184765,状态2表示为123804765

所需知识

优先队列BFS算法,因为A*算法就是带有估价函数的优先队列BFS

导出所需知识

优先队列BFS算法缺陷

该算法维护了一个优先队列(二叉堆),不断从堆中取出“当前代价最小”的状态(堆顶)去进行扩展。但是它得到只是初态到该状态的最小代价并没有去考虑从该状态出发到目标状态的情况)

举例说明:
如果给定一个“目标状态”,需要求出从初态到目标状态的最小代价,优先队列BFS显然不行。因为一个状态的当前代价最小,只能说明从起始状态到该状态的代价很小,而在未来的探索中,从该状态到目标状态可能会花费很大的代价另外一些状态虽然当前代价略大,但是未来到目标状态的代价可能会很小,于是从起始状态到目标状态的总代价反而更优。

A*搜索算法

于是,我们为了解决上面的问题:可以对未来可能产生的代码进行预估。详细地讲,我们去设计一个**“估价函数”**,以任意状态为输入,计算出从该状态到目标状态所需代价的估计值。在搜索之中,仍然维护了一个堆,不断从堆中取出“当前代价+未来估价”最小的状态来进行扩展。

为了保证第一次从堆中取出目标状态时得到的就是最优解,我们设计的估价函数需要满足一个基本准则:

设当前状态state到目标状态所需代价的估计值为f(state);设在未来的探索中,实际求出的从当前状态state到目标状态的最小代价为g(state)
对于任意的state,应该有f(state)<=g(state)

也就是说,估价函数的估值不能大于未来实际代价,估价比实际代价更优。估价函数f(n)=g(n)+h(n)

总结:

这种带有估价函数的优先队列BFS就成为A* 算法。只要保证对于任意状态state,都有f(state)<=g(state),A* 算法就一定能在目标状态第一次从堆中被取出时得到最优解,并且在搜索过程中每个状态只需要扩展一次(之后再被取出就可以直接忽略 )。估价f(state)越准确,越接近g(state),A*算法的效率越高。如果估价始终为0,就等于普通的优先队列BFS。

实际操作

搜索过程描述

A算法又称为启发式搜索算法。对启发式搜索算法,又可根据搜索过程中选择扩展节点的范围,将其分为全局择优搜索算法局部择优搜索算法

在全局择优搜索中,每当需要扩展节点时,总是从 Open 表的所有节点中选择一个估价函数值最小的节点进行扩展。其搜索过程可能描述如下:

  1. 把初始节点 S0 放入 Open 表中, f(S0)=g(S0)+h(S0)
  2. 如果 Open 表为空,则问题无解,失败退出;
  3. 把 Open 表的第一个节点取出放入 Closed 表,并记该节点为 n ;
  4. 考察节点 n 是否为目标节点。若是,则找到了问题的解,成功退出;
  5. 若节点 n 不可扩展,则转到第 (2) 步;
  6. 扩展节点 n ,生成子节点 ni ( i =1,2, …… ) ,计算每一个子节点的估价值 f( ni ) ( i =1,2, …… ) ,并为每一个子节点设置指向父节点的指针,然后将这些子节点放入 Open 表中;
  7. 根据各节点的估价函数值,对 Open 表中的全部节点按从小到大的顺序重新进行排序;
  8. 转第 (2) 步。

这里采用的启发式策略为:f(n) = g(n) + h(n),其中g(n)为从初始节点到当前节点的步数(层数),h(n)为 当前节点 “不在位 ”的方块数(也就是说不在位的方块数越少,那么临目标状态越近)例如下图中的h(n)=5,有的讲解的是不包含空格,我这里是包含了的,经测试只要前后标准一致,包不包含空格都一样。
g(n)为已经消耗的实际代价,即已经走了的步数
h(n)为预测路径,即还有几个数字待走

在这里插入图片描述
h(n)=5

启发式策略工作工程:红圈数字表示扩展顺序

在这里插入图片描述

代码实现

Map+BFS+A*

#include<cstdio>
#include<queue>
#include<map>
using namespace std;
char arr[10], brr[10] = "123804765";
struct node {
	int num, step, cost, zeroPos;
	bool operator<(const node& a)const {
		return cost > a.cost;
	}
	node(int n, int s, int p) {
		num = n, step = s, zeroPos = p;
		setCost();
	}
	void setCost() {
		char a[10];
		int c = 0;
		sprintf(a, "%09d", num);
		for (int i = 0; i < 9; i++)
			if (a[i] != brr[i])
				c++;
		cost = c + step;
	}
};
int des = 123804765;
int changeId[9][4] = { {-1,-1,3,1},{-1,0,4,2},{-1,1,5,-1},
					{0,-1,6,4},{1,3,7,5},{2,4,8,-1},
					{3,-1,-1,7},{4,6,-1,8},{5,7,-1,-1} };
map<int, bool>mymap;
priority_queue<node> que;//优先级队列 
void swap(char* ch, int a, int b) { char c = ch[a]; ch[a] = ch[b]; ch[b] = c; }
int bfsHash(int start, int zeroPos) {
	char temp[10];
	node tempN(start, 0, zeroPos);//创建一个节点 
	que.push(tempN);//压入优先级队列 
	mymap[start] = 1;//标记开始节点被访问过 
	while (!que.empty()) {
		tempN = que.top();
		que.pop();//弹出一个节点 
		sprintf(temp, "%09d", tempN.num);
		int pos = tempN.zeroPos, k;
		for (int i = 0; i < 4; i++) {
			if (changeId[pos][i] != -1) {
				swap(temp, pos, changeId[pos][i]);
				sscanf(temp, "%d", &k);
				if (k == des)return tempN.step + 1;
				if (mymap.count(k) == 0) {
					node tempM(k, tempN.step + 1, changeId[pos][i]);
					que.push(tempM);//创建一个新节点并压入队列 
					mymap[k] = 1;
				}
				swap(temp, pos, changeId[pos][i]);
			}
		}
	}
}
int main() {
	int n, k, b;
	scanf("%s", arr);
	for (k = 0; k < 9; k++)
		if (arr[k] == '0')break;
	sscanf(arr, "%d", &n);
	b = bfsHash(n, k);
	printf("%d步即可变换完成", b);
	return 0;
}

在这里插入图片描述
所得结果跟我们上图分析结果一样,

changeId[9][4] = { {-1,-1,3,1},{-1,0,4,2},{-1,1,5,-1},
					{0,-1,6,4},{1,3,7,5},{2,4,8,-1},
					{3,-1,-1,7},{4,6,-1,8},{5,7,-1,-1} };

这个数组也就代表九个位置中四个方向的可置换的数组元素下标(-1表示该方向不能交换),注意:这里的四个方向的话,是逆时针,(上左下右)

Hash+BFS+A*

#include<cstdio>
#include<queue>
using namespace std;
char arr[10],brr[10]="123804765";
struct node{
	int num,step,cost,zeroPos;
	bool operator<(const node &a)const{
		return cost>a.cost;
	}
	node(int n,int s,int p){
		num=n,step=s,zeroPos=p;
		setCost();
	}
	void setCost(){
		char a[10];
		int c=0;
		sprintf(a,"%09d",num);
		for(int i=0;i<9;i++)
			if(a[i]!=brr[i])
				c++;
		cost=c+step;
	}
};
int changeId[9][4]={{-1,-1,3,1},{-1,0,4,2},{-1,1,5,-1},
					{0,-1,6,4},{1,3,7,5},{2,4,8,-1},
					{3,-1,-1,7},{4,6,-1,8},{5,7,-1,-1}};
const int M=2E+6,N=1000003;//362897;
int hashTable[M];//hashtable中key为hash值,value为被hash的值 
int Next[M],des=123804765;//next表示如果在某个位置冲突,则冲突位置存到hashtable[next[i]] 
priority_queue<node> que;//优先级队列 
int Hash(int n){
	return n%N; 
}
bool tryInsert(int n){
	int hashValue=Hash(n);
	while(Next[hashValue]){//如果被hash出来的值得next不为0则向下查找 
		if(hashTable[hashValue]==n)//如果发现已经在hashtable中则返回false 
			return false; 
		hashValue=Next[hashValue];
	}//循环结束hashValue指向最后一个hash值相同的节点 
	if(hashTable[hashValue]==n)//再判断一遍 
		return false; 
	int j=N-1;//在N后面找空余空间,避免占用其他hash值得空间造成冲突 
	while(hashTable[++j]);//向后找一个没用到的空间 
	Next[hashValue]=j;
	hashTable[j]=n;
	return true; 
}
void swap(char* ch,int a,int b){char c=ch[a];ch[a]=ch[b];ch[b]=c;}
 
int bfsHash(int start,int zeroPos){
	char temp[10];
	node tempN(start,0,zeroPos); 
	que.push(tempN);
	while(!que.empty()){
		tempN=que.top();
		que.pop();
		sprintf(temp,"%09d",tempN.num);
		int pos=tempN.zeroPos,k;
		for(int i=0;i<4;i++){
			if(changeId[pos][i]!=-1){
				swap(temp,pos,changeId[pos][i]);
				sscanf(temp,"%d",&k);
				if(k==des)return tempN.step+1;
				if(tryInsert(k)){//插入新状态成功,则说明新状态没有被访问过 
					node tempM(k,tempN.step+1,changeId[pos][i]);
					que.push(tempM);
				}
				swap(temp,pos,changeId[pos][i]);
			}
		}
	}
}
int main(){
	int n,k,b=0;
	scanf("%s",arr);
	for(k=0;k<9;k++)
		if(arr[k]=='0')break;
	sscanf(arr,"%d",&n);
	if(n!=des)
		b=bfsHash(n,k);
	printf("%d步即可变换完成",b);	
	return 0;
}

在这里插入图片描述

GAMEOVER

include using namespace std; struct node{ int nodesun[4][4]; int pre; //上一步在队列中的位置 int flag ; //步数标识,表示当前的步数为有效的 int value; //与目标的差距 int x,y; //空格坐标 }queue[1000]; //移动方向数组 int zx[4]={-1,0,1,0}; int zy[4]={0,-1,0,1}; //当前步数 int top; int desti[4][4];//目标状态 int detect(struct node *p)//检查是否找到 {int i,j; for(i=1;i<4;i++) for(j=1;jnodesun[i][j]!=desti[i][j]) return 0; return 1; } //打印 void printlj() {int tempt; int i,j; tempt=top; while(tempt!=0) { for(i=1;i<4;i++) for(j=1;j<4;j++) {cout<<queue[tempt].nodesun[i][j]; if(j==3) cout<<" "<<endl; } tempt=queue[tempt].pre; } } //现在状态与目标状态有多少个不同位置 int VALUE(struct node *p) {int count=0; int i,j; for(i=1;i<4;i++) for(j=1;jnodesun[i][j]!=desti[i][j]) count++; return count; } void main() { //初始化 int i,j,m,n,f; int min=10; int temp,find=0,minnumber; top=1; for(i=1;i<4;i++) for(j=1;j<4;j++) {cout<<"请输入第"<<i<<"行"<<"第"<<j<<"列的值"<>temp; queue[1].nodesun[i][j]=temp; } cout<<"请输入初始状态的空格的位置(行)"<>temp; queue[1].x=temp; cout<<"请输入初始状态的空格的位置(列)"<>temp; queue[1].y=temp; queue[1].value=VALUE(&queue[1]); queue[1].pre=0; //上一步在队列中的位置 queue[1].flag=0; //目标状态 for(i=1;i<4;i++) for(j=1;j<4;j++) {cout<<"请输入目标状态第"<<i<<"行"<<"第"<<j<<"列的值"<>temp; desti[i][j]=temp; } //根据估价函数 while(!find&&top>0) { for(i=1;i<=top;i++) //////////////////////////////////////////// //min为上一中与目标有多少个元素不相同,queue[i]为当前与目标有多少个元素不相同通过这两个数的比较,就可以得出当前较之上一向目标接近同时把当前的i记录下来进行下一步比较 {if(queue[i].value<min&&queue[i].flag==0) {minnumber=i;// min=queue[i].value; //还有多少不同的位数 } } queue[minnumber].flag=1; //表示此位有效 ////////////////////////////////////// // for(f=0;f=1&&i=1&&j<=3) {top++; ///////////////////////////////////////////// //位置交换 queue[top]=queue[minnumber]; queue[top].nodesun[m][n]=queue[minnumber].nodesun[i][j]; queue[top].nodesun[i][j]=0; /////////////////////////////////////// //空格移动方向 queue[top].x=i; queue[top].y=j; /////////////////////////////////////// queue[top].pre=minnumber; //上一步在队列中的位置 queue[top].value=VALUE(&queue[top]); //有多少位与目标不同 queue[top].flag=0; //标识位初始化 if(detect(&queue[top])) //检查是否为目标 {printlj(); //打印 find=1; //设找到标识位 break; } } } } }
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

寻梦&之璐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值