浅谈A*算法

一、为什么要用A*

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

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


二、A*的原理

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

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

[SCOI2007]k短路

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

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

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

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

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

{f(x)xg(x)x:

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

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

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

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

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

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

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

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

换句话说

xg(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 @   ShuraEye  阅读(773)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示