针对hdu1043,来说一下A* 搜索。这道题不一定用A* 算法,还可以用双向bfs。但是A*搜索更快,在人工智能方面应用也很广泛。
A* 搜索不是像深度优先搜索算法和广度优先搜索算法一样的傻瓜式的埋头搜索,它是先对当前的情况进行分析,得到最有可能的一个分支,然后在该分支上进行扩展,然后将扩展的结果放在之前的大环境中进行比较,再选取最有可能的分支进行扩展,直到找到最终状态。A* 算法的核心是估价函数的选取(通俗的说就是对当前情况的评价方式的选取,通过什么方式选取的分支才是最有可能离最终状态最近的分支)。A* 搜索得到的路径不一定是最优的路径,但是它得到这条路径的时间一定是最小的。换句话说,A* 搜索不论是在扩展节点还是在生成节点方面都要优于深度优先搜索和广度优先搜索。
八数码问题
八数码游戏包括一个3 * 3的棋盘,棋盘上摆放着8个数字的棋子,留下一个空位。与空位相邻的棋子可以滑动到空位中。游戏的目的是要达到一个特定的目标状态。假定目标状态如下
对于广度优先搜索来说,我们要从初始状态转移到目标状态,所做的就是要判断空格移动方向以及空格的位置。广搜得到的路径一定是最短的。但是这样盲目的扩展和生成节点,如果数据量大了,就有可能MLE或者TLE。究其原因,我们用广搜的时候,没有什么限制,什么状态都可以生成节点。
与之相比,A*搜索则会设置一个估价函数,针对估价函数的值,去判断哪种状态可以优先生成。
估价函数是搜索特性的一种数学表示,是指从问题树根节点到达目标节点所要耗费的全部代价的一种估算,记为f(n)。估价函数通常由两部分组成,其数学表达式为:f(n)=g(n)+h(n)
其中f(n) 是节点n从初始点到目标点的估价函数,g(n) 是在状态空间中从初始节点到n节点的实际代价,h(n)是从n到目标节点最佳路径的估计代价。保证找到最短路径(最优解)的条件,关键在于估价函数h(n)的选取。估价值h(n)<= n到目标节点的距离实际值,这种情况下,搜索的点数多,搜索范围大,效率低。但能得到最优解。如果估价值>实际值, 搜索的点数少,搜索范围小,效率高,但不能保证得到最优解。
h(n)在这里表示的数从当前状态到目标状态的曼哈顿距离。
曼哈顿距离:就是表示两个点在标准坐标系上的绝对轴距之和
算法思想
对于八数码问题来说,我们首先要判断的是它能否得到目标状态。八数码问题又叫数字华容道。数字华容道的有解条件为:
列数为奇数,(逆序数)与(目标状态的逆序数)奇偶性相同时必然有解。
列数为偶数,(逆序数)与(当前空格所在的行数和初始空格所在的行数之差)的和为偶数时必然有解。
针对八数码问题的各个状态,由于数据量并不大,根据康托展开,我们可以用400000以内的数字来判断它的各个状态。这样我们就可以将各个状态装换为数字,这样的话,我们来判断是否到达目标状态的时候,就容易了一些。
八数码问题A*搜索代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxx=400001;
struct node{
int ma[3][3];
int h,g;//两个估价函数,g代表着从初始状态到当前状态的花费。h代表着从当前状态到目标状态的曼哈顿距离
int x,y;//空位的位置
int hash;
bool operator<(const node &a)const{//估价函数1 874ms
if(h!=a.h) return h>a.h;
else return g>a.g;
}
// bool operator<(const node &a)const{//估价函数2 1450ms
// return (h+g)>(a.h+a.g);
// }第一个估价函数更优化于第二个估价函数
bool check()
{
if(x>=0&&x<3&&y>=0&&y<3) return 1;
return 0;
}
}s;
int Hash[]={1,1,2,6,24,120,720,5040,40320};//Hash值利用康拓展开
int destination=322560;//目标的hash值
int vis[maxx];
int pre[maxx];
int d[][2]={{0,1},{0,-1},{1,0},{-1,0}};
char str[50];int tz,sc;
inline int get_hash(node tmp)//获得hash值
{
int a[9],k=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++) a[k++]=tmp.ma[i][j];
int cnt=0;
int jj;
for(int i=0;i<9;i++)
{
jj=0;
for(int j=0;j<i;j++) if(a[j]>a[i]) jj++;
cnt+=Hash[i]*jj;
}
return cnt;
}
inline bool Isok(node tmp)//判断逆序对,判断是否有解
{
int a[9],k=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++) a[k++]=tmp.ma[i][j];
int sum=0;
for(int i=0;i<9;i++)
{
for(int j=i+1;j<9;j++)
{
if(a[j]&&a[i]&&a[i]>a[j]) sum++;
}
}
return !(sum&1);//目标解的逆序数是偶数,所以现在状态的解是偶数才有可能到达目标状态.
}
inline int get_h(node tmp)//估价函数H ,计算当前值与目标值的曼哈顿距离
{
int ans=0;
for(int i=0;i<3;i++)
for(int j=0;j<3;j++)
if(tmp.ma[i][j])
ans+=abs(i-(tmp.ma[i][j]-1)/3)+abs(j-(tmp.ma[i][j]-1)%3);
return ans;
}
inline void bfs()
{
priority_queue<node> q;
q.push(s);
while(q.size())
{
node u=q.top();
q.pop();
tz++;
for(int i=0;i<4;i++)
{
node v=u;
v.x+=d[i][0];
v.y+=d[i][1];
if(v.check())
{
sc++;
swap(v.ma[v.x][v.y],v.ma[u.x][u.y]);
v.hash=get_hash(v);
if(vis[v.hash]==-1&&Isok(v))
{
vis[v.hash]=i;//保存方向
v.g++;//已经画掉的代价
pre[v.hash]=u.hash;//记录路径
v.h=get_h(v);//取得达到目标状态的总花费
q.push(v);
}
if(v.hash==destination) return ;//达到目标状态就直接退出
}
}
}
}
inline void dfs(int x)//还原空格移动位置
{
char ss;
if(pre[x]==-1) return ;
dfs(pre[x]);
if(vis[x]==0) ss='r';
else if(vis[x]==1) ss='l';
else if(vis[x]==2) ss='d';
else if(vis[x]==3) ss='u';
putchar(ss);
}
inline void init()//初始化数组
{
memset(vis,-1,sizeof(vis));
memset(pre,-1,sizeof(pre));
}
int main()
{
while(gets(str)!=NULL)
{
init();
int k=0;
tz=sc=0;
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
if((str[k]<='9'&&str[k]>='0')||str[k]=='x')
{
if(str[k]=='x')
{
s.ma[i][j]=0;
s.x=i;
s.y=j;
}
else s.ma[i][j]=str[k]-'0';
}
else j--;
k++;
}
}
if(!Isok(s)) puts("unsolvable");
else
{
s.hash=get_hash(s);
if(s.hash==destination) puts("");
else
{
vis[s.hash]=-2;
s.g=0;s.h=get_h(s);
bfs();
dfs(destination);
puts("");
printf("拓展的节点数目为:%d,生成的节点数目为:%d\n",tz,sc);
}
}
}
return 0;
}
对于15数码问题,大部分是一样的。但是判断是否可行的条件不一样。因为15数码是4*4的,判断条件复杂一些。除此之外,由于15数码利用康托展开后太大,所以用数组无法表示。我们可以利用map,将各个状态hash之后的值再进行hash。这样就可以表示了。
15数码代码如下:
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int maxx=3e6+100;
struct node{
int ma[4][4];
int h,g;//两个估价函数,g代表着从初始状态到当前状态的花费。h代表着从当前状态到目标状态的曼哈顿距离
int x,y;//空位的位置
int hash;
bool operator<(const node &a)const{//估价函数1
if(h!=a.h) return h>a.h;
else return g>a.g;
}
// bool operator<(const node &a)const{//估价函数2
// return (h+g)>(a.h+a.g);
// }
bool check()
{
if(x>=0&&x<4&&y>=0&&y<4) return 1;
return 0;
}
}s;
ll Hash[]={1,1,2,6,24,120,720,5040,40320,322560,3225600,35481600,425779200,5535129600,7749184400,116237766000};//Hash值利用康拓展开
ll destination=1743566490000;//目标的hash值
int vis[maxx];
int pre[maxx];
map<ll,int> mp;//由于原始的Hash值太大,数组开不了。所以我们用map将Hash值映射为一个较小的数
int d[][2]={{0,1},{0,-1},{1,0},{-1,0}};
int tz,sc;int cnt=0;
inline ll get_hash(node tmp)//获得hash值
{
int a[20],k=0;
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++) a[k++]=tmp.ma[i][j];
}
ll cnt=0;
int jj;
for(int i=0;i<16;i++)
{
jj=0;
for(int j=0;j<i;j++) if(a[j]>a[i]) jj++;
cnt+=Hash[i]*jj*1ll;
}
return cnt;
}
inline bool Isok(node tmp)//判断逆序对,判断是否有解
{
int a[20],k=0;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++) a[k++]=tmp.ma[i][j];
int sum=0;
for(int i=0;i<16;i++)
{
for(int j=i+1;j<16;j++)
{
if(a[j]&&a[i]&&a[i]>a[j]) sum++;
}
}
if(sum%2==(4-tmp.x-1)%2) return 1;
return 0;//由于15数码是4*4的,4位偶数。所以判断是否有解的时候不像奇数那样简单。列数为偶数,(逆序数)与(当前空格所在的行数和初始空格所在的行数之差)的和为偶数时必然有解。
}
inline int get_h(node tmp)//估价函数H ,计算当前值与目标值的曼哈顿距离
{
int ans=0;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
if(tmp.ma[i][j])
ans+=abs(i-(tmp.ma[i][j]-1)/4)+abs(j-(tmp.ma[i][j]-1)%4);
return ans;
}
inline void bfs()
{
priority_queue<node> q;
q.push(s);
while(q.size())
{
node u=q.top();
q.pop();
tz++;
for(int i=0;i<4;i++)
{
node v=u;
v.x+=d[i][0];
v.y+=d[i][1];
if(v.check())
{
sc++;
swap(v.ma[v.x][v.y],v.ma[u.x][u.y]);
ll zz=get_hash(v);
if(mp[zz]) continue;
else mp[zz]=++cnt;
v.hash=mp[zz];
if(vis[v.hash]==-1&&Isok(v))
{
vis[v.hash]=i;//保存方向
v.g++;//已经画掉的代价
pre[v.hash]=u.hash;//记录路径
v.h=get_h(v);//取得达到目标状态的总花费
q.push(v);
}
if(zz==destination) return ;//达到目标状态就直接退出
}
}
}
}
inline void dfs(int x)//还原空格移动位置
{
char ss;
if(pre[x]==-1) return ;
dfs(pre[x]);
if(vis[x]==0) ss='r';
else if(vis[x]==1) ss='l';
else if(vis[x]==2) ss='d';
else if(vis[x]==3) ss='u';
putchar(ss);
}
inline void init()//初始化数组
{
memset(vis,-1,sizeof(vis));
memset(pre,-1,sizeof(pre));
}
int str[20];
int main()
{
for(int i=0;i<16;i++) scanf("%d",&str[i]);
init();
int k=0;
tz=sc=cnt=0;
for(int i=0;i<4;i++)
{
for(int j=0;j<4;j++)
{
if(str[k]==0)
{
s.x=i;
s.y=j;
s.ma[i][j]=0;
}
else s.ma[i][j]=str[k];
k++;
}
}
if(!Isok(s)) puts("unsolvable");
else
{
ll zz=get_hash(s);
mp[zz]=++cnt;
s.hash=mp[zz];
if(zz==destination) puts("");
else
{
vis[s.hash]=-2;
s.g=0;s.h=get_h(s);
bfs();
dfs(mp[destination]);
puts("");
printf("拓展的节点数目为:%d,生成的节点数目为:%d\n",tz,sc);
}
}
return 0;
}//3 4 8 12 1 2 7 15 5 6 11 14 9 13 10 0
多理解多思考。
努力加油a啊,(o)/~