淺談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\)*,就是一個對於最短路或爆搜的優化,其中的估價函數十分重要

總而言之,這玩意是個時間復雜度是玄學,正確性是玄學,考場分數是玄學

十分不穩定但又有可能可以騙到高分的算法,謹慎使用


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM