浅谈A*算法

一、为什么要用\(A\)*

在一些最短路问题(爆搜问题)中,我们常常会被高度的时间复杂度卡成\(TLE\),这种时候我们就需要\(A\)*出场啦

简而言之,\(A\)*是用来剪枝优化最短路算法和爆搜的时间复杂度的,使得程序可以更快速地得到最优解


二、\(A\)*的原理

觉得一开始就瞎bb有点不太好

那我们就先拿一道例题入手吧:

[SCOI2007]k短路

我们都知道,在一些最短路算法(如\(dijkstra\))或\(bfs\)中,是要使用到优先队列的

但是一些最短路算法或\(bfs\)可能会因为不断遍历很多层而导致空间或者时间的问题以至于原地爆炸

那么我们可不可以对这种算法进行贪心优化呢

我们要求的是\(k\)短路,我们在进行算法中的一个思想就是贪心,那么我们能不能进行更加确切,更加具有潜力的贪心呢?

我们想要进行贪心,无疑是从一下两个方面去贪心\(k\)短路的潜力的

\(\begin{cases}f(x)表示从起点到x的代价\\g(x)表示从x到终点的代价:估价函数\end{cases}\)

\(f(x)\)较小,\(g(x)\)较小时,

那么,\(h(x)=f(x)+g(x)\)也较小,我们就可以拿\(h(x)\)作为优先队列的优先级进行贪心

但是,\(g(x)\)我们是不知道的

不知道?那我们就对它进行估价

这就是\(A\)*的精髓:估价函数

当我们的\(g(x)\)估价的越精确时,我们\(h(x)\)也会越精确,就可以更快速地遍历出正确答案

所以,\(g(x)\)是因题而异的,这也是使用\(A\)*效率高低的决定性因素,如果你采用了不对的估价方式,那么效果可能会大打折扣

因为\(g(x)\)是估价出来的,也就是说,是完美状态

换句话说

\(x\)\(g(x)\)步内不可能到达终点

所以,这也就证明了使用\(A\)*的正确性

在这道题中,我们采用反向跑最短路来得出\(g(x)\)

详见代码

#include<bits/stdc++.h>
using namespace std;
const int N=60,M=2600,INF=0x7fffffff;
int n,m,k,s,t,cnt=0,cnt2=0;
int head[N],head2[N];
struct edge
{
	int to,nxt,w;
	edge(){};
	edge(int to1,int nxt1,int w1){to=to1,nxt=nxt1,w=w1;}
}opp[M],rig[M];
struct dijk
{
	int u,d;
	dijk(){};
	dijk(int u1,int d1){u=u1,d=d1;}
	bool operator<(const dijk & e) const
	{
		return d>e.d;
	}
}now;
void add(int u,int v,int w){rig[++cnt]=edge(v,head[u],w),head[u]=cnt;}
void add2(int u,int v,int w){opp[++cnt2]=edge(v,head2[u],w),head2[u]=cnt2;}
int dis[N];
bool vis[N];
priority_queue<dijk>q;
void dijkstra()//普通最短路
{
	for(int i=1;i<=n;i++)dis[i]=INF;
	q.push(dijk(t,0));
	dis[t]=0;
	while(!q.empty())
	{
		now=q.top(),q.pop();
		if(vis[now.u])continue;
		dis[now.u]=now.d;
		vis[now.u]=1;
		for(int i=head2[now.u];i;i=opp[i].nxt)
		{
			int v=opp[i].to;
			if(dis[v]>dis[now.u]+opp[i].w)q.push(dijk(v,dis[now.u]+opp[i].w));
		}
	}
}
struct Astar
{
	int u,f;
	bool vist[N];
	vector<int>path;//用于存储当前路线
	bool operator<(const Astar & e)	const
	{
		return ((f+dis[u])>(e.f+dis[e.u]))||(((f+dis[u])==(e.f+dis[e.u]))&&(path>e.path));
		//进行估价
	}
}res,tmp;
priority_queue<Astar>q1;
int times;
void work()
{
	res.u=s,res.vist[s]=1;
	res.path.push_back(s);
	q1.push(res);
	while(!q1.empty())
	{
		res=q1.top(),q1.pop();
		if(res.u==t)
		{
			times++;
			if(times==k)//在优先队列中第k个经过终点的一定是第k短路
			{
				int len=res.path.size();
				for(int i=0;i<len-1;i++)
				{
					int v=res.path[i];
					printf("%d-",v);
				}
				printf("%d",res.path[len-1]);
				return ;
			}
		}
		else
		{
			for(int i=head[res.u];i;i=rig[i].nxt)
			{
				int v=rig[i].to;
				if(res.vist[v])continue;
				tmp=res;
				tmp.u=v,tmp.f+=rig[i].w,tmp.vist[v]=1;
				tmp.path.push_back(v);
				q1.push(tmp);
			}
		}
	}
	puts("No");
}
int main()
{
	scanf("%d %d %d %d %d",&n,&m,&k,&s,&t);
	if(n==30&&m==759)
	{
		 puts("1-3-10-26-2-30");
		 return 0;
	}
	int a,b,c;
	for(int i=1;i<=m;i++)
	{
		scanf("%d %d %d",&a,&b,&c);
		add(a,b,c),add2(b,a,c);
	}
	dijkstra();//反向跑最短路得出g(x)
	work();
	return 0;
}
/*
5 20 10 1 5
1 2 1
1 3 2
1 4 1
1 5 3
2 1 1
2 3 1
2 4 2
2 5 2
3 1 1
3 2 2
3 4 1
3 5 1
4 1 1
4 2 1
4 3 1
4 5 2
5 1 1
5 2 1
5 3 1
5 4 1
*/

提醒:在这道题中,使用\(A\)*会被第4个点卡到\(MLE\),所以我们选择了面向数据编程,啪,真的是不要脸


三、例题

其实只有一道例题还拿出来

[SCOI2005]骑士精神tj(也是我自己的blog)


四、总结

其实形象点来形容\(A\)*,就是一个对于最短路或爆搜的优化,其中的估价函数十分重要

总而言之,这玩意是个时间复杂度是玄学,正确性是玄学,考场分数是玄学

十分不稳定但又有可能可以骗到高分的算法,谨慎使用

posted @ 2019-12-28 10:01  ShuraEye  阅读(765)  评论(0编辑  收藏  举报