Loading

图 - 最短路

<目录

在开始之前……

本文使用:
\(\sf P_n\) 表示点 \(\sf n\)
\(\sf E_{u,v}\) 表示从 \(\sf P_u\) 点到 \(\sf P_v\) 点的边长,
\(\sf Dis(f,t)\) 表示从 \(\sf P_f\) 点到 \(\sf P_t\) 点的最短路长度。

\(\sf E_{a,b}\)\(\sf Dis(a,b)\) 的值为 \(\sf\infty\) 表示 \(\sf a\) 点和 \(\sf b\) 点没有直接的边相连或不联通。
在代码中使用 0x3fffffff ( 1073741823 )表示 \(\sf\infty\)

最短路

最短路,是在带权图上的的两个点之间最小的距离。
看似很简单的问题,可是要解决他却很难。
怎么办呢?
既然图是由点和边构成的,那么,我们先从点入手吧……

单源最短路径

顾名思义,就是只有一个原点,可以计算 \(\sf Dis(s,m)\) ( \(\sf P_s\) 为定点 )。

\(\sf Dijksta\)

对于一个点 \(\sf P_m\) 那么:

\[\sf Dis(s,m)=\min\limits_{\forall E_{i,m}\not=\infty}\{Dis(s,i)+E_{i,m}\} \]

很好理解吧?
\(\sf P_m\) 所有的入点与原点的最短路加上入边的边权的最小值,就是这个点的答案。
我们把他转成递推,这样就能优化了。
为了防止重复计算某些点,我们需要一个 \(\sf Visit\) 记录这个点是否已经被计算过了。

如何确定计算顺序也是个大麻烦。
每次没被计算过的点中最小的那个一定不会再受别的点的更新答案了,
所以,我们取最小的一个点确定下来,更新别的点的答案,直到所有的点都被计算完成。

代码

#include<cstdio>
int Min(int a,int b){return a<b?a:b;}
bool Point[10005];
int Ans[10005];
int Graph[10005][10005];
int M,N,S,o1,o2,o3;
void Init()
{
	scanf("%d%d",&N,&M);
	S=1;
	for(int i=0;i<=N;i++)
	{
		for(int j=0;j<=N;j++)
			Graph[i][j]=0x3fffffff;
		Ans[i]=0x3fffffff;
	}
	Ans[S]=0;
	while(M--)
	{
		scanf("%d%d%d",&o1,&o2,&o3);
		Graph[o1][o2]=Min(Graph[o1][o2],o3);
	}
}
void Fill()
{
	for(int i=1;i<=N;i++)
	{
		int Minn=0x7fffffff,Minj=-1;
		for(int j=1;j<=N;j++)
			if(!Point[j]&&Ans[j]<Minn)
				Minn=Ans[j],Minj=j;
		if(Minj==-1)	break;
		Point[Minj]=true;
		for(int j=1;j<=N;j++)
			if(!Point[j]&&Graph[Minj][j]!=0x7fffffff)
				Ans[j]=Min(Ans[j],Ans[Minj]+Graph[Minj][j]);
	}
	return;
}
void Print()
{
	printf("%d ",Ans[N]);
}
int main()
{
	Init();
	Fill();
	Print();
	return 0;
}

先把所有的 \(\sf Dis()\) 全部标记为 0x3fffffff ,然后把起点标记为 0 (因为自己到自己距离为 0 )。

\(\textsf{Heap-Dijksta}\)

我们发现,在找最小点的时候花了大部分的时间。
何不用堆来帮助我们呢?
(再加一点链式前向星)

#include<cstdio>
#include<queue>
#define INF 0x7fffffff
using std::priority_queue;
struct Line{int To,W,Next;}Edge[200020];
struct Point
{
	int Dis,To;
	bool operator< (Point a)const
		{return Dis>a.Dis;}
};
priority_queue<Point> Search;
int Head[100005],G=1;
void Add(int From,int To,int W)
{
	Edge[G].To=To;
	Edge[G].W=W;
	Edge[G].Next=Head[From];
	Head[From]=G++;
}
int N,M,S,Ans[100005],u,v,w;
bool Visit[100005];
void Init()
{
	scanf("%d%d%d",&N,&M,&S);
	for(int i=0;i<=N;i++)
		Ans[i]=INF,Head[i]=-1;
	Ans[S]=0;
	while(M--)
		scanf("%d%d%d",&u,&v,&w),Add(u,v,w);
}
void Fill()
{
	Search.push((Point){0,S});
	while(!Search.empty())
	{
		const Point Sl=Search.top();
		Search.pop();
		if(Visit[Sl.To])	continue;
		Visit[Sl.To]=true;
		for(int j=Head[Sl.To];j!=-1;j=Edge[j].Next)
		{
			const int NTo=Edge[j].To;
			if(Visit[NTo])	continue;
			if(Ans[NTo]>Ans[Sl.To]+Edge[j].W)
				Ans[NTo]=Ans[Sl.To]+Edge[j].W,
				Search.push((Point){Ans[NTo],NTo});
		}
	}
	return;
}
void PrintAns()
{
	for(int i=1;i<=N;i++)
		printf("%d ",Ans[i]);
}
int main()
{
	Init();
	Fill();
	PrintAns();
	return 0;
}

既然我可以从点去更新答案,边呢……

\(\textsf{Bellman-Ford}\)

让我们来基于边做——
用已确定的点,按照边权更新未确定的点的答案。
如果新答案比老答案小,就替换老答案。
我们把其称作 \(\sf\color{red}松弛\)

如何确定一个点是否已经“确定”了呢?
其实我们不需要确定一个点是否“确定”。
管你确不确定,我松弛就对了!

因为一个点如果没有被“确定”,
那么他所松弛的答案到最后一定不会保留,
等他“确定”以后还可以更新更小的答案。

按照思路,写出……

代码

#include<cstdio>
#define INF 0x3fffffff
#define Min(a,b) ((a)<(b)?(a):(b))
#define Max(a,b) ((a)>(b)?(a):(b))
struct Line{int F,T,W;}E[20010];
int A[105],Fans,N,M;
int main()
{
	scanf("%d%d",&N,&M);M*=2;
	for(int i=2;i<=N;i++)	A[i]=INF;
	for(int i=0;i<M;i++)
		scanf("%d%d%d",&E[i].F,&E[i].T,&E[i].W),i++,
		E[i].T=E[i-1].F,E[i].F=E[i-1].T,E[i].W=E[i-1].W;
	for(int i=1;i<N;i++)
		for(int j=0;j<M;j++)
			A[E[j].T]=Min(A[E[j].T],A[E[j].F]+E[j].W);
	for(int i=1;i<=N;i++)
		Fans=Max(Fans,A[i]);
	if(Fans>=INF)		printf("-1");
	else				printf("%d",Fans);
	return 0;
}

因为一个点如果没有被“确定”,
那么他所松弛的答案到最后一定不会保留,
等他“确定”以后还可以更新更小的答案。

这不就浪费了吗?
如果这个点被修改之后,他才可以去松弛别的点。
我想,我们需要队列的帮助。
于是,我们想到了……

\(\textsf{SPFA}\color{grey}\overset{\textsf{Shortest Path Faster Algorithm}}{\textsf{Queue-Bellman-Ford}}\)

如果我们已经把某个点入队了,那就不能让他同时入两次队。
写出我们的:

代码

#include<cstdio>
#include<queue>
using std::queue;
using std::vector;
struct Edge{int To,Dis;};
vector<Edge> E[10010];
int Ans[10010],N,M,S,o1,o2,o3;
bool InQueue[10010];
queue<int> Search;
int main()
{
	scanf("%d %d %d",&N,&M,&S);
	for(int i=1;i<=N;i++)
		Ans[i]=0x3fffffff;
	for(int i=0;i<M;i++)
		scanf("%d %d %d",&o1,&o2,&o3),
		E[o1].push_back((Edge){o2,o3});
	Ans[S]=0,InQueue[S]=true,Search.push(S);
	while(!Search.empty())
	{
		const int Fr=Search.front();
		Search.pop(),InQueue[Fr]=false;
		for(Edge i:E[Fr])
		{
			const int To=i.To,Dis=i.Dis;
			if(Ans[Fr]+Dis<Ans[To])
			{
				Ans[To]=Ans[Fr]+Dis;
				if(!InQueue[To])
					InQueue[To]=true,
					Search.push(To);
			}
		}
	}
	for(int i=1;i<=N;i++)
		if(Ans[i]>=0x3fffffff)
			printf("%d ",0x7fffffff);
		else
			printf("%d ",Ans[i]);
	return 0;
}

全源最短路

我可不可以一下子把所有的 \(\sf Dis(u,v)\) 一下子全算出来呢?
答案是可以的。

\(\textsf{Floyed}\)

\[\sf Dis(i,j)=\min\limits_{k\in P}\{Dis(i,k)+Dis(k,j)\} \]

只需要读这个式子,其余的就不言而喻了。

上……

代码

#include<cstdio>
#define INF 0x3fffffff
#define Min(a,b) ((a)<(b)?(a):(b))
int Ans[205][205],N,M,K,u,v,w;
int main()
{
	scanf("%d%d%d",&N,&M,&K);
	for(int i=1;i<=N;i++)
		for(int j=1;j<=N;j++)
			Ans[i][j]=INF;
	while(M--)
	{
		scanf("%d%d%d",&u,&v,&w);
		Ans[u][v]=w;
	}
	for(int k=1;k<=N;k++)
		for(int i=0;i<=N;i++)
			for(int j=0;j<=N;j++)
				Ans[i][j]=Min(Ans[i][j],Ans[i][k]+
											Ans[k][j]);
	while(K--)
	{
		scanf("%d%d",&u,&v);
		if(Ans[u][v]>=INF)	printf("impossible\n");
		else				printf("%d\n",Ans[u][v]);
	}
	return 0;
}

\(\sf N\times Dijkstra\)

当然,跑 \(\sf N\)\(\sf Dijkstra\) 也不是什么问题……

\(\sf Johnson\ \color{grey}N\times\textsf{Bellman-Ford}\)

当然,跑 \(\sf N\)\(\textsf{Bellman-Ford}\) \(也\) 不是什么问题……

总结一下

Floyd Bellman-Ford Dijkstra
全源最短路 单源最短路 单源最短路
任意图 非负权图 任意图
能检测负环 不能检测负环 能检测负环
\ \(\sf O(NM)\rarr O(会被卡)\) \(\sf O(N^2)\rarr O(M\log M)\)
\(\sf O(N^3)\) \(\sf O(N^2)\) \(\sf O(NM\log M)\)

完结散……
打住!
负边权还没解决呢!

未解决的问题

为什么 \(\sf Dijkstra\) 不能解决负边权呢?
因为他基于贪心。
负边权会导致后效性。

那如何判断负权环呢?
负权环会导致最短路为 \(\sf -\infty\) ,所以,如果我们发现循环已经结束但还可以小下去,说明本图带有负权环。


真的完结散花。

posted @ 2022-11-19 11:04  PCwqyy  阅读(17)  评论(0)    收藏  举报  来源