【学时·I】A*算法


■基本策略■

——A*(A Star)无非就是BFS的升级,当BFS都超时的时候……

同样以队列为基础结构,BFS使用FIFO队列(queue),而A*则使用优先队列(priority_queue)。与BFS的优化极其相似,但一般的BFS优化只是相当于使用了一个最优性剪枝,偶尔不会起到足够的优化所以就TLE了。

所以A*算法改进了其优先级的判定方法,使用了一个启发函数(没错,就是这么水的名字),它可以“乐观”地预估出一个从当前状态到达目标状态的代价,且此预估值必然小于等于实际值,否则A*算法就会出错。


■一般的搜索题■

这是A*的解题范围和其他搜索算法可以实现的解题范围的重叠域

◆永恒的经典◆ 八数码问题

· 以下引用自POJ 1077

The 15-puzzle has been around for over 100 years; even if you don't know it by that name, you've seen it. It is constructed with 15 sliding tiles, each with a number from 1 to 15 on it, and all packed into a 4 by 4 frame with one tile missing. Let's call the missing tile 'x'; the object of the puzzle is to arrange the tiles so that they are ordered as:

 1  2  3  4

5 6 7 8

9 10 11 12

13 14 15 x

where the only legal operation is to exchange 'x' with one of the tiles with which it shares an edge. As an example, the following sequence of moves solves a slightly scrambled puzzle:

 1  2  3  4    1  2  3  4    1  2  3  4    1  2  3  4

5 6 7 8 5 6 7 8 5 6 7 8 5 6 7 8

9 x 10 12 9 10 x 12 9 10 11 12 9 10 11 12

13 14 11 15 13 14 11 15 13 14 x 15 13 14 15 x

r-> d-> r->

The letters in the previous row indicate which neighbor of the 'x' tile is swapped with the 'x' tile at each step; legal values are 'r','l','u' and 'd', for right, left, up, and down, respectively.



Not all puzzles can be solved; in 1870, a man named Sam Loyd was famous for distributing an unsolvable version of the puzzle, and frustrating many people. In fact, all you have to do to make a regular puzzle into an unsolvable one is to swap two tiles (not counting the missing 'x' tile, of course).



In this problem, you will write a program for solving the less well-known 8-puzzle, composed of tiles on a three by three arrangement.

· 解析

这道题显然是一道搜索题,不过有一点变形……就难了许多。当然,这道题用BFS或者双向BFS能过,但是A*算法实际运行时间更优。

1.启发函数

作为整个A*算法的精华,这个函数显得十分重要,那么下面给出2个思路:

思路A: H()-当前状态有多少个元素不在正确位置; G()-当前搜索的深度,即用了的操作数; 最后一次操作将会在一次操作中将2个元素归位,所以启发函数F()=H()-1+G();

评价: 这个启发函数一定正确,但是通常与真实值相差过大,不是特别优;

思路B: H()-当前状态中位置不对的元素(不包含空位子)与正确位置的曼哈顿距离之和; G()-当前搜索的深度,即用了的操作数; 由于将一个元素归位至少需要H()次操作;

评价: 估测值与真实值较为接近,且不会超过真实值;

2.优先队列的定义

优先队列是A*算法所依赖的数据结构。STL的priority_queue能够实现自动排序,但有一个缺点——当需要输出路径时,STL的队列并没有提供访问删除了的元素的函数,因此无法通过记录“父亲”状态来输出路径,这时候就需要手写优先队列……QwQ

现在暂时不考虑这种尴尬情况,就当是只输出最短方案操作次数。那么我们需要一个结构体:

struct Node
{
int pri,code,whe,dep;
//优先级H(),八数码数字表示,空位的位置,搜索深度
}
bool operator <(Node A,Node B) {return A.pri+A.dep>B.pri+B.dep;}

这样定义了优先级过后就可以利用STL直接排序了~

3.如果代码不懂可以看这篇Blog:

Eight 八数码问题

· 源代码

仅供参考,如有不足(我知道写得复杂了点)请评论指出

/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
#define MOD 1000003 struct Node {int pri,code,whe,dep;}Push;
bool operator <(Node A,Node B) {return A.pri+A.dep>B.pri+B.dep;}
int num[10];
int MOve[4]={-3,-1,3,1};
long long ten_n[]={1,10,100,1000,10000,100000,1000000,10000000,100000000,1000000000,10000000000};
vector<int> vis[MOD]; inline int H(int n)
{
int res=0;
for(int i=9;i>0;i--,n/=10)
{
int v=n%10==0? 9:n%10;
int x1=(i-1)/3+1,y1=i%3==0? 3:i%3;
int x2=(v-1)/3+1,y2=v%3==0? 3:v%3;
res+=abs(x1-x2)+abs(y1-y2);
}
return res;
}
inline long long change(int n,int a,int b)
{
long long A=n%ten_n[9-a+1]/ten_n[9-a],B=n%ten_n[9-b+1]/ten_n[9-b],Ret;
Ret=n-A*ten_n[9-a]-B*ten_n[9-b];
Ret=Ret+B*ten_n[9-a]+A*ten_n[9-b];
return Ret;
}
inline bool Find(int n)
{
int m=n%MOD;
for(int i=0;i<vis[m].size();i++)
if(vis[m][i]==n)
return true;
vis[m].push_back(n);
return false;
} int main()
{
// freopen("in.txt","r",stdin);
priority_queue<Node> que;
for(int i=0,j=0;i<9;i++)
{
scanf("%d",&num[j]);
Push.code=Push.code*10+num[j];
if(num[j]) j++;
else Push.whe=i+1;
}
int End=0;
for(int i=0,x;i<9;i++)
scanf("%d",&x),End=10*End+x;
Push.pri=H(Push.code);
que.push(Push);
while(!que.empty())
{
Node Top=que.top();que.pop();
for(int i=0;i<4;i++)
{
Push=Top;
Push.whe+=MOve[i];Push.dep++;
if(Push.whe<=0 || Push.whe>9 || (i%2 && (Push.whe-1)/3!=(Top.whe-1)/3)) continue;
Push.code=(int)change(Push.code,Push.whe,Top.whe);
if(Find(Push.code)) continue;
if(Push.code==End)
{
printf("%d",Push.dep);
return 0;
}
Push.pri=H(Push.code);
que.push(Push);
}
}
puts("-1");
return 0;
}

◆真正的难题◆ 15数码问题

BFS真的过不了了……

UVA 15-Puzzle Problem

· 解析

这两道题唯一的区别就是数据规模,8数码的可能情况不超过9!种,但是15数码的可能情况是在16!种以内!所以只加上一般的判重并不能起到什么优秀的作用,所以用到了A*算法,启发函数和原来一样。但是……

人无完人,A*算法也是有缺陷的 QwQ

正如双向BFS,A*算法虽然在一般情况下较快,而这一般情况就是有解的情况。如果无解,双向搜索会退化为两个不相交的圆,既浪费空间又浪费时间;而A*算法会退化为普通的BFS(需要搜索完所有解),且比普通BFS慢——每次插入的时间复杂度为 O(log siz)。15数码仍然有无解的情况,所以为了避免超时(枚举出所有情况肯定会超时啊),我们需要预判:

bool If_ans(int brd[][4])
{
int sum=0,siz=0,x,y,tmp[17]={};
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
{
tmp[siz++]=brd[i][j];
if(!brd[i][j]) x=i,y=j;
}
for(int i=0;i<16;i++)
for(int j=i+1;j<16;j++)
if(tmp[j]<tmp[i] && tmp[j])
sum++;
if((sum+x)%2==0) return false;
return true;
}

上面这段代码的意思就是——将4*4的15数码矩阵除去0后,每行相接形成一个链状表(tmp),求出逆序对的个数(sum),如果加上0的行数(行数从0开始)的和是二的倍数,则无解,否则有解。其实就是下面这样:



最为权威的英文证明:Workshop Java - Solvability of the Tiles Game

· 源代码

这段代码并不算优秀,实际上利用了 Uva 的数据漏洞,真正的解是 IDA*,之后再说

/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<set>
#include<queue>
#include<iostream>
using namespace std; const int mov[4][2]={{0,1},{0,-1},{1,0},{-1,0}};
const int fin[18][2]={{3,3},{0,0},{0,1},{0,2},{0,3},{1,0},{1,1},{1,2},{1,3},{2,0},{2,1},{2,2},{2,3},{3,0},{3,1},{3,2},{3,3}};
const char chr[]="RLDU"; struct state
{
int pri,dep,x,y,brd[4][4];
string ans;
bool operator <(const state &cmp)const {return pri+dep>cmp.pri+cmp.dep;}
}; inline int Dis(int x,int y,int fx,int fy){return abs(x-fx)+abs(y-fy);}
int Get_pri(int brd[][4])
{
int sum=0;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
{
if(!brd[i][j] || (i==fin[brd[i][j]][0] && j==fin[brd[i][j]][1])) continue;
sum+=Dis(i,j,fin[brd[i][j]][0],fin[brd[i][j]][1]);
}
return 4*sum;
}
bool If_ans(int brd[][4])
{
int sum=0,siz=0,x,y,tmp[17]={};
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
{
tmp[siz++]=brd[i][j];
if(!brd[i][j]) x=i,y=j;
}
for(int i=0;i<16;i++)
for(int j=i+1;j<16;j++)
if(tmp[j]<tmp[i] && tmp[j])
sum++;
if((sum+x)%2==0) return false;
return true;
}
bool A_Star(state srt)
{
priority_queue<state> que;
state Top,Pus;
que.push(srt);
while(!que.empty())
{
Top=que.top();que.pop();
for(int i=0;i<4;i++)
{
Pus=Top;Pus.x+=mov[i][0];Pus.y+=mov[i][1];
if(Pus.x<0 || Pus.x>3 || Pus.y<0 || Pus.y>3) continue;
swap(Pus.brd[Pus.x][Pus.y],Pus.brd[Top.x][Top.y]);
Pus.dep++;
if(Pus.dep>50) continue;
Pus.ans+=chr[i];Pus.pri=Get_pri(Pus.brd);
if(!Pus.pri) {cout<<Pus.ans<<endl;return true;}
if(Pus.ans.size()>=2)
{
int f1=Pus.ans.size()-1,f2=Pus.ans.size()-2;
if((Pus.ans[f1]=='U' && Pus.ans[f2]=='D') || (Pus.ans[f1]=='D' && Pus.ans[f2]=='U') || (Pus.ans[f1]=='L' && Pus.ans[f2]=='R') || (Pus.ans[f1]=='R' && Pus.ans[f2]=='L'))
continue;
}
que.push(Pus);
}
}
return false;
} int main()
{
int t;scanf("%d",&t);
while(t--)
{
state srt;
for(int i=0;i<4;i++)
for(int j=0;j<4;j++)
{
scanf("%d",&srt.brd[i][j]);
if(!srt.brd[i][j]) srt.x=i,srt.y=j;
}
if(!If_ans(srt.brd))
{
printf("This puzzle is not solvable.\n");
continue;
}
srt.dep=0;srt.pri=0;
if(!A_Star(srt)) printf("This puzzle is not solvable.\n");
}
return 0;
}

■k-短路问题■

其他的搜索拿这个题型没辙了 (`・ω・´)

◆一道版题◆ k-th shortest POJ 2449

(题目长在超链接里)

· 解析

其实就是标准的k短路。如果是一般的搜索肯定会TLE的,那么为什么A*可以完成呢?原因如下:

  1. A*算法利用优先队列,所以它按顺序找到的第k条路径一定是第k短的路径;
  2. 它最高的时间复杂度仅比BFS多 O(\log siz) ,所以时间复杂度并不高;
  3. 我们可以通过k次最短的路径查找只进行一次BFS;

与之前的算法不同,我们在原来的A*算法中会有判重,并舍弃优先级低的情况;但是由于K短路中,同一条路径可能走多次,以达到第k短的路径。比如下面:



但是这样也造成了一些麻烦,有时候是无解的:

然后回到A*算法——由于我们按优先级排序,所以我们当前的队头元素一定是现在整个队列中最优的情况(前提是启发函数写对了);所以当我们第n次搜索到终点时,当前的路径就是第n短路径,为了避免意外,我们还可以先把到达终点的情况push到队列里,当整个操作完毕再次以到达终点的情况为队头时,说明它的确是第n短的。

根据这一性质,我们可以在队头元素是终点时,记录是第几次访问到终点,如果恰好是第n次,则返回答案——注意:此时虽然访问到终点,但还要把这种情况push进队列,否则反例见上图

别急着写代码,有一个小坑。由于我们是判断队头是否为终点,所以当起点终点重合时,按题意应该不算最短路径,但当程序进入BFS时会记录一次。所以我们给出特判——当起点终点重合时,k++。

·源代码

/*Lucky_Glass*/
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
#include<vector>
using namespace std; #define POI 1000 struct Line{int v,len;};
struct state
{
int u,dep,pri;
bool operator <(const state cmp)const
{
if(cmp.pri==pri) return cmp.dep<dep;
else return cmp.pri<pri;
}
};
vector<Line> lec[POI+5],dis_lec[POI+5];
int n_poi,n_edg,srt,fin,kth,INF;
int dis[POI+5]; inline Line Make_Line(int v,int l){return Line{v,l};}; void SPFA()
{
bool vis[POI+5]={};
memset(dis,0x3f,sizeof dis);INF=dis[0];
queue<int> que;
dis[fin]=0;que.push(fin);
while(!que.empty())
{
int Fro=que.front();que.pop();
vis[Fro]=false;
for(int i=0;i<dis_lec[Fro].size();i++)
{
int Pus=dis_lec[Fro][i].v;
if(dis[Pus]>dis[Fro]+dis_lec[Fro][i].len)
{
dis[Pus]=dis[Fro]+dis_lec[Fro][i].len;
if(!vis[Pus]) que.push(Pus),vis[Pus]=true;
}
}
}
}
int A_Star()
{
if(srt==fin) kth++;
if(dis[srt]==INF) return -1;
priority_queue<state> que;
que.push(state{srt,0,dis[srt]});
int tot=0;
while(!que.empty())
{
state Top=que.top();que.pop();
if(Top.u==fin)
{
tot++;
if(tot==kth) return Top.dep;
}
for(int i=0;i<lec[Top.u].size();i++)
{
state Pus=Top;
Pus.dep+=lec[Top.u][i].len;
Pus.u=lec[Top.u][i].v;
Pus.pri=dis[Pus.u]+Pus.dep;
que.push(Pus);
}
}
return -1;
} int main()
{
scanf("%d%d",&n_poi,&n_edg);
for(int i=0,u,v,l;i<n_edg;i++)
scanf("%d%d%d",&u,&v,&l),lec[u].push_back(Line{v,l}),dis_lec[v].push_back(Line{u,l});
scanf("%d%d%d",&srt,&fin,&kth);
SPFA();
printf("%d\n",A_Star());
return 0;
}

The End

Thanks for reading!

-Lucky_Glass

【学时总结】 ◆学时 · I◆ A*算法的更多相关文章

  1. 【学时总结】 ◆学时·II◆ IDA*算法

    [学时·II] IDA*算法 ■基本策略■ 如果状态数量太多了,优先队列也难以承受:不妨再回头看DFS-- A*算法是BFS的升级,那么IDA*算法是对A*算法的再优化,同时也是对迭代加深搜索(IDF ...

  2. springMVC文件上传与下载(六)

    1..文件上传 在springmvc.xml中配置文件上传解析器 <!-- 上传图片配置实现类,id必须为这个 --> <bean id="multipartResolve ...

  3. 如何使用和关闭onbeforeunload 默认的浏览器弹窗事件

    Onunload,onbeforeunload都是在刷新或关闭时调用,可以在<script>脚本中通过 window.onunload来指定或者在<body>里指定.区别在于o ...

  4. 教师表(TEACHER.DBF)

    20-27题使用的数据如表1和表2所示. 表1 教师表(TEACHER.DBF) 教师号 姓名 性别 籍贯 职称 年龄 工资/元 0001 王吉兵 男 江苏 讲师 27 2003.50 0002 张晓 ...

  5. 【学时总结】 ◆学时·III◆ 二分图

    [学时·III] 二分图 ■基本策略■ 其实本质是图论中的网络流 二分图是两个由多个点组成的集合(上部和下部,且没有重叠),两个集合中的点不与该集合内其他的点连通,但和另一个集合内的点连通.我们称这两 ...

  6. 【学时总结&模板时间】◆学时·10 & 模板·3◆ AC自动机

    ◇学时·10 & 模板·3◇ AC自动机 跟着高中上课……讲AC自动机的扩展运用.然而连KMP.trie字典树都不怎么会用的我一脸懵逼<(_ _)> 花一上午自学了一下AC自动机 ...

  7. 【学时总结】◆学时·IX◆ 整体二分

    ◆学时·IX◆ 整体二分 至于我怎么了解到这个算法的……只是因为发现一道题,明显的二分查找,但是时间会爆炸,被逼无奈搜题解……然后就发现了一些东西QwQ ◇ 算法概述 整体二分大概是把BFS与二分查找 ...

  8. 【学时总结】◆学时·VIII◆ 树形DP

    ◆学时·VIII◆ 树形DP DP像猴子一样爬上了树……QwQ ◇ 算法概述 基于树的模型,由于树上没有环,满足DP的无后效性,可以充分发挥其强大统计以及计算答案的能力. 一般来说树形DP的状态定义有 ...

  9. 【学时总结】◆学时·VII◆ 高维DP

    ◆学时·VII◆ 高维DP 自学之余,偶遇DP…… ◇ 算法概述 顾名思义——一种处理多方面状态的DP,这种DP特点是……每一维的大小都不算太大(不然用dp数组存储下来内存会炸),而且枚举时容易超时… ...

随机推荐

  1. OpenCV获取与设置像素点的值的几个方法

    Title: OpenCV OpenCV像素值的获取与设置 Fn 1 : 使用 Mat 中对矩阵元素的地址定位的知识 (参考博文:OpenCV中对Mat里面depth,dims,channels,st ...

  2. 什么是J2EE

    什么是J2EE 一.准备篇 1 什么是J2EE?它和普通的Java有什么不同? 答:J2EE全称为Java2 Platform Enterprise Edition. "J2EE平台本质上是 ...

  3. BottomNavigationView结合ViewPager

    BottomNavigationView是Google推出的底部导航栏组件,在没有这些底部导航组件之前,Android开发者多使用的是RadioGroup,在上一个项目开发中我们使用了Google的B ...

  4. (Stanford CS224d) Deep Learning and NLP课程笔记(一):Deep NLP

    Stanford大学在2015年开设了一门Deep Learning for Natural Language Processing的课程,广受好评.并在2016年春季再次开课.我将开始这门课程的学习 ...

  5. kettle 合并记录

    转自: http://blog.itpub.net/post/37422/464323 看到别人的脚本用到 合并记录 步骤,学下下. 该步骤用于将两个不同来源的数据合并,这两个来源的数据分别为旧数据和 ...

  6. [转发]CPU个数、CPU核心数、CPU线程数

    我们在选购电脑的时候,CPU是一个需要考虑到核心因素,因为它决定了电脑的性能等级.CPU从早期的单核,发展到现在的双核,多核.CPU除了核心数之外,还有线程数之说,下面文本就来解释一下CPU的核心数与 ...

  7. August 24th 2017 Week 34th Thursday

    If you have choices, choose the best. If you have no choice, do the best. 如果有选择,那就选择最好的:如果没有选择,那就努力做 ...

  8. August 21st 2017 Week 34th Monday

    In fact, the happiest fairy tale is no more than the simple days we have together. 其实全世界最幸福的童话,也比不上我 ...

  9. codeforces 1000F One Occurrence(线段树、想法)

    codeforces 1000F One Occurrence 题意 多次询问lr之间只出现过一次的数是多少. 题解 将查询按照左端点排序,对于所有值维护它在当前位置后面第二次出现是什么时候,那么查询 ...

  10. 1001.A+B Format (20)解题描述

    1. 作业链接 2. 解题的思路过程 首先这是道简单的计算题,要求计算a+b的值. 看初值条件,将a和b的取值限制在一个区间内. 本题难点和重点是如何把输出值形成题目要求的格式. 因为负数可通过在前面 ...