A*
简介
A*搜索算法(英文:A*search algorithm,A*读作 A-star),简称 A*算法,是一种在图形平面上,对于有多个节点的路径求出最低通过成本的算法。它属于图遍历(英文:Graph traversal)和最佳优先搜索算法(英文:Best-first search),亦是 BFS 的改进。
定义起点 s s s ,终点 t t t ,从起点(初始状态)开始的距离函数 g ( x ) g(x) g(x),到终点 (最终状态)的距离函数 h ( x ) h(x) h(x) , h ′ ( x ) h'(x) h′(x) ,以及每个点的估价函数 f ( x ) = g ( x ) + h ( x ) f(x)=g(x)+h(x) f(x)=g(x)+h(x) 。
A*算法每次从优先队列中取出一个 f f f 最小的元素,然后更新相邻的状态。
如果 h ≤ h ′ h \leq h' h≤h′ ,则 A*算法能找到最优解。
上述条件下,如果 h h h 满足三角形不等式,则 A*算法不会将重复结点加入队列。且 h h h 越大,效果越好,当然,如果 h h h 过大的话可能会直接跳过最优解。
当 h = 0 h=0 h=0 时,A*算法变为 DFS ;当 h = 0 h=0 h=0 并且边权为 1 1 1 时变为 BFS。
典型例题(15数码)
思路分析
首先通过分析我们可以知道,使用 b f s bfs bfs 或者是普通的 d f s dfs dfs 剪枝肯定会TLE,所以考虑A*,我们先把普通的状态搜索 b f s bfs bfs 的板子打出来。这里定义两类结构体 M a t r i x Matrix Matrix 和 S t a t e State State , M a t r i x Matrix Matrix 结构体是普通 b f s bfs bfs 板子中的状态量结点,这里我们给它增加一个权值 e s t est est,表示一个预估量,预估的是从当前结点到目标状态所需的最小步数。 S t a t e State State 结构体总共有两个成员: M a t r i x Matrix Matrix 和 e s t est est ;它的作用是在出队入队操作时作为一个标准。因为 A*的剪枝策略实际上时对 起点到当前位置的成本+当前位置到目标状态的推测值 的最小值进行优先选择,所以原先 b f s bfs bfs 中的队列要改成堆式结构,也就是优先队列,这里就是对 S t a t e State State 进行操作了。具体实现见下方代码。
Codes (A*)
#include<bits/stdc++.h>
using namespace std;
typedef struct Matrix{ //存16宫格的棋型
int x,y; //0所在的坐标
int cost; //状态迭代过程中的实际花费
int f[17]; //将二维宫格用一维序列存储
int est; //表示此棋型号还需至少est步才能到达目标状态
bool operator < (const Matrix &k) const{
for (int i=1; i<=16; i++)
if (f[i]==k.f[i]) continue;
else
return f[i]>k.f[i];
return false;
}
};
typedef struct State{ //搜索过程中的状态结点
Matrix u; //该状态16宫格的具体内容
int est; //预计该状态总共至少还剩est步才能到达目标
bool operator < (const State &s) const{
return est>s.est;
}
};
int dx[4]={1,-1,0,0};
int dy[4]={0,0,1,-1};
int main(){
priority_queue<State> q;
map<Matrix,bool> vis; //表示每个棋型状态是否到达过
int x,cnt=0,cost_sum=0;
int estimate[25][25][25]={0}; //estimate[i][j][k]表示从位置(i,j)到目标答案中k所在位置的曼哈顿距离
for (int i=0; i<=15; i++){
for (int j=1; j<=16; j++){
if (i==0){
if (j%4!=0){
estimate[(j-1)/4+1][j%4][0]=abs((j-1)/4+1-4)+abs(j%4-4);
}
else{
estimate[j/4][4][0]=abs(j/4-4)+abs(4-4);
}
}
else{
if (j%4!=0){
if (i%4!=0){
estimate[(j-1)/4+1][j%4][i]=abs((j-1)/4+1-(i-1)/4-1)+abs((j%4-i%4));
}
else{
estimate[(j-1)/4+1][j%4][i]=abs((j-1)/4+1-i/4)+abs(j%4-4);
}
}
else{
if (i%4!=0){
estimate[j/4][4][i]=abs(j/4-(i-1)/4-1)+abs(4-i%4);
}
else{
estimate[j/4][4][i]=abs(j/4-i/4)+abs(4-4);
}
}
}
}
}
Matrix uu;
for (int i=1; i<=4; i++)
for (int j=1; j<=4; j++){
scanf("%d",&x); //读入初始的每个棋盘位置上的值
if (x==0)
uu.x=i,uu.y=j;
uu.f[++cnt]=x;
}
for (int i=1; i<=16; i++){ //统计初始状态所有需要的est值
if (i%4!=0){
cost_sum+=estimate[(i-1)/4+1][i%4][uu.f[i]];
}
else{
cost_sum+=estimate[i/4][4][uu.f[i]];
}
}
State start; //定义初始状态start
start.est=uu.est=cost_sum;
start.u=uu;
q.push(start);
vis[start.u]=1; start.u.cost=0;
while (!q.empty()){ //用bfs进行搜索
State u=q.top(); q.pop();
Matrix uu=u.u,u1;
if (uu.est==0){ //如果预计还需0步(也就是已经到了目标状态了),输出实际花费,退出。
cout<<uu.cost<<endl;
return 0;
}
vis[uu]=1; //标记状态uu已经访问过
int x1,y1;
for (int i=0; i<4; i++){
u1=uu; //建立新状态u1
x1=uu.x+dx[i]; y1=uu.y+dy[i];
if (x1>=1 && x1<=4 && y1>=1 && y1<=4){ //如果(x1,y1)可以移动到0所在位置
/*更新u1状态的总est*/
u1.est+=estimate[x1][y1][0]-estimate[uu.x][uu.y][0]+estimate[uu.x][uu.y][uu.f[(x1-1)*4+y1]]-estimate[x1][y1][uu.f[(x1-1)*4+y1]];
int tmp;
tmp=u1.f[(uu.x-1)*4+uu.y];
u1.f[(uu.x-1)*4+uu.y]=u1.f[(x1-1)*4+y1];
u1.f[(x1-1)*4+y1]=tmp;
//=uu.f[(x1-1)*4+y1];
u1.x=x1; u1.y=y1;
if (vis[u1]!=1){
u1.cost++;
State news;
news.est=u1.est+u1.cost;
news.u=u1;
q.push(news);
}
}
}
}
return 0;
}
但实际上,A*的实际效果并不是很好(虽然相对于普通的 b f s bfs bfs 它已经很强了),它只能过22/28个点。最后的六个点全部MLE了(不过A*能很好地处理八数码难题),所以我们考虑另一种方法来处理空间消耗过度的问题,那就是IDA*(迭代加深搜索)
IDA*
简介
IDA*,即采用迭代加深的 A*算法。相对于 A*算法,由于 IDA*改成了深度优先的方式,所以 IDA*更实用:
1.不需要判重,不需要排序;
2.空间需求减少。
迭代加深搜索
迭代加深是一种 每次限制搜索深度的深度优先搜索。
它的本质还是深度优先搜索,只不过在搜索的同时带上了一个深度 d d d ,当 d d d 达到设定的深度时就返回,一般用于找最优解。如果一次搜索没有找到合法的解,就让设定的深度加一,重新从根开始。
既然是为了找最优解,为什么不用 BFS 呢?我们知道 BFS 的基础是一个队列,队列的空间复杂度很大,当状态比较多或者单个状态比较大时,使用队列的 BFS 就显出了劣势。事实上,迭代加深就类似于用 DFS 方式实现的 BFS,它的空间复杂度相对较小。
当搜索树的分支比较多时,每增加一层的搜索复杂度会出现指数级爆炸式增长,这时前面重复进行的部分所带来的复杂度几乎可以忽略,这也就是为什么迭代加深是可以近似看成 BFS 的。
具体步骤
首先设定一个较小的深度作为全局变量,进行 DFS。每进入一次 DFS,将当前深度加一,当发现 d d d 大于设定的深度 l i m i t limit limit 就返回。如果在搜索的途中发现了答案就可以回溯,同时在回溯的过程中可以记录路径。如果没有发现答案,就返回到函数入口,增加设定深度,继续搜索。