【图论】最短路算法:Dijkstra、bellman-ford、spfa、Floyd 和拓扑排序

是AcWing算法基础课关于基本图论算法的笔记。图片和引用来自给出原链接的“参考”。
AcWing 永远滴神!

图来自这里
在这里插入图片描述

Dijkstra的使用条件是:边权非负即可

朴素版Dijkstra-AcWing 849. Dijkstra求最短路 I(稠密图)

参考

步骤:

  1. 初始化,dis[1]=0,dis[n]=0x3f3f3f3f.——起点到起点的距离为0,到其他点的距离为正无穷(这里1是起点)
  2. 迭代:n次迭代确定每个点到起点的最小值
  3. 输出答案

其中:

dist[n]表示起点到点n的距离
st[n]表示点n到起点的最短距离是否已经确定

每次找还没有确定最短距离的点中的最短的那个进行更新:
可以看这里的演示

如:
(举例子的图都来自上面的链接!)

有一个图,我们想知道家到学校的距离:
在这里插入图片描述
1.进行初始化:不与家相连的都为无穷大。(也可以全都初始化为无穷大,一样的)
在这里插入图片描述

2.此时所有的点都是未确定最短路的点。 我们选择当前从起点出发最短路径的点。
是 家->V2.
在这里插入图片描述

此时确定的集合:括号内是路径长度

V2(3)

不确定的集合:

V1(4),V3(8),V6(15),V4(INF),V5(INF),学校(INF)

3.然后是家->V1
在这里插入图片描述

确定的集合:

V2(3),V1(4)

不确定的集合:

V3(7),V6(15),V4(12),V5(INF),学校(INF)

4.然后是家->V3:
在这里插入图片描述

确定的:

V2(3),V1(4),V3(7)

不确定的:

V6(15),V4(13),V5(22),学校(INF)

…以此类推。大概就是这个意思。

其中,如果确定了某个点已经有到起点x的最短路了,我们就让st[x]=1;
然后用这个点去更新其他所有点的距离。

后更新的不会影响先更新的点,因为后更新的点就算与先更新的点有路,也会比它大。

注意:若有重边,要保留最短的边。

模板题:
在这里插入图片描述

代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500+10;
int n,m;
int g[N][N];//存图 
int st[N];//点N是否确定了最短距离 
int dist[N];//从起点到点N的最短距离 
void dij()
{
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	
	//st[1]=1;
	//这里万万不能st[1]=1;
	//因为如果=1了,在后面的更新中就不会从1这个起点开始;
	//而只有1这个起点的dist[1]=0是最小的,其他都是正无穷 
	
	for(int i=1;i<=n;i++)
	{
		//找到当前未确定最短距离的点——找当前距离最小的
		int t=-1;
		
		for(int j=1;j<=n;j++)
		{
			//t==-1表示还没找到t 第二个条件是找最短的t
			if(st[j]==0&&(t==-1||dist[t]>dist[j])) 
			{
				t=j;
			} 
		} 
		
		st[t]=1;
		for(int j=1;j<=n;j++)
		{
			dist[j]=min(dist[j],dist[t]+g[t][j]);
			//dist[t]+g[t][j]就是用点t去更新所有相连的点 
		}
	}
}
int main()
{
	cin>>n>>m;
	memset(g,0x3f,sizeof(g));//先初始化为最大 
	while(m--)
	{
		int x,y,z;
		cin>>x>>y>>z;
		g[x][y]=min(g[x][y],z);//如果重边,留最短的 
	}
	
	dij();
	
	if(dist[n]==0x3f3f3f3f) cout<<-1;
	else cout<<dist[n];
	return 0;
}

堆优化版Dijkstra-AcWing 850. Dijkstra求最短路 II(稀疏图)

引用和参考来源:参考1参考2
关于如何判断稠密/稀疏图:m是n^2级别的是稠密图,是n级别的是稀疏图。
思路:

  1. 一号点的距离dist[1]=0,其他初始化为0x3f3f3f3f
  2. 将一号点放入堆中
  3. 不断循环,直到堆空。每一次循环的操作为:弹出堆顶(与朴素版找到S外距离最短的点相同,并标记该点的最短路径已经确定),用该点更新临界点的距离,更新成功就加入堆中。

在这里插入图片描述
关于时间复杂度:

时间复杂度 O(mlogn)
每次找到最小距离的点沿着边更新其他的点,若dist[j] > distance + w[i],表示可以更新dist[j],更新后再把j点和对应的距离放入小根堆中。由于点的个数是n,边的个数是m,在极限情况下(稠密图m=n(n−1)2)最多可以更新m回,每一回最多可以更新n个点(严格上是n - 1个点),有m回,因此最多可以把n2个点放入到小根堆中,因此每一次更新小根堆排序的情况是O(log(n2)),一共最多m次更新,因此总的时间复杂度上限是O(mlog((n2)))=O(2mlogn)=O(mlogn)

堆优化版的Dijkstra用邻接表来存,就无所谓重边,因为算法会保证我们得到最短的边。
关于用数组模拟邻接表的引用和参考:数组模拟邻接表

h数组的下标为结点的编号,e,w,nxt数组的下标为边的编号,eidx为边的编号
h[u]表示u这个结点的第一条边的编号
e[eidx]表示边的终点,w[eidx]表示边的权重,nxt[eidx]表示下一条边。

所以模板是:

const int N = 1010, M = 1010;

int h[N], e[M], w[M], nxt[M], eidx;

void add(int u, int v, int weight)   // 添加有向边 u->v, 权重为weight
{
    e[eidx] = v;        // 记录边的终点
    w[eidx] = weight;   // 记录边的权重
    nxt[eidx] = h[u];   // 将下一条边指向结点u此时的第一条边
    h[u] = eidx;        // 将结点u的第一条边的编号改为此时的eidx
    eidx++;             // 递增边的编号edix, 为将来使用
}

邻接表添加边之前要全初始化为-1(邻接表的性质)。

题:
在这里插入图片描述
代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;

const int N=150000+10,M=150000+10;

int h[N],w[M],e[M],ne[M],idx;
//点对应的边,边的权重,边结束的点,边的下一条边,一个编号 
void add(int a,int b,int c)//往邻接表里加点
{
	w[idx]=c;//存权重 
	e[idx]=b;//存边的结尾 
	ne[idx]=h[a];//存下一条边:头插法,当前的下一条边其实是之前的该点的第一条边 
	h[a]=idx++;//idx要更新 
} 

typedef pair<int,int>PII;
int dist[N],st[N];
int n,m;
void dijkstra()
{
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	priority_queue<PII,vector<PII>,greater<PII>>heap;//小根堆
	//heap的数据类型是PII,first用来存权重,second用来存点
	//pair的排序排的是first,所以必须要权重放在前面
	heap.push({0,1}); 
	while(heap.size())
	{
		PII temp=heap.top();
		heap.pop();
		int distance=temp.first,id=temp.second;
		
		if(st[id]) continue;//该点已经找到了最短路
		
		st[id]=1;
		for(int i=h[id];i!=-1;i=ne[i])//遍历该点的所有边
		{
			int j=e[i];
			if(dist[j]>distance+w[i])
			{
				dist[j]=distance+w[i];
				heap.push({dist[j],j});
			}
		} 
	}
}
int main()
{
	memset(h,-1,sizeof(h));
	cin>>n>>m;
	
	while(m--)
	{
		int a,b,c;
		cin>>a>>b>>c;
		add(a,b,c);//存图 
	} 
	
	dijkstra();
	if(dist[n]==0x3f3f3f3f) cout<<-1;
	else cout<<dist[n];
	
	return 0;
}

bellman-ford-AcWing 853. 有边数限制的最短路

擅长解决有边数限制的最短路问题

参考和引用:
参考1参考2
为什么dijkstra不能解决负边权的最短路问题?
在这里插入图片描述
如图所示:
最左边的点到其右下的点,它只会记录1的距离,而不会记录其到右上方100的距离,就自然不会记录-200的距离。
因此无法解决负边权的最短路问题。

什么是bellman-ford算法:

Bellman - ford 算法是求 含负权图的单源最短路径 的一种算法,效率较低,代码难度较小。其原理为连续进行松弛, 在每次松弛时把每条边都更新一下,若在 n-1 次松弛后还能更新,则说明图中有负环,因此无法得出结果,否则就完成。
(通俗的来讲就是:假设 1 号点到 n 号点是可达的,每一个点同时向指向的方向出发,更新相邻的点的最短距离,通过循环 n-1 次操作,若图中不存在负环,则 1 号点一定会到达 n 号点,若图中存在负环,则在 n-1 次松弛后一定还会更新)

一些名词:
松弛操作:dist[b]=min(dist[b],dist[a]+w)
三角不等式:dist[b]<=dist[a]+w;

若是图中存在负权边,如图:那么从1点到2点的距离会变成负无穷——12342这样一直转。
在这里插入图片描述
也就是说:如果能通过bellman-ford求出最短路,则路径中是没有负权回路的(负环不在最短路径中就不影响,可以在其他无所谓的地方)。
(一个对比,spfa是一定要求图中不含负环

因此,迭代的次数会有实际意义:假如迭代了k次,那么意义就是从1号点,经过不超过k条边,走到每个点的最短距离。
也就是说,如果在第n次迭代的时候又更新了,那么存在一条最短路径,它上面有n条边,即意味着有n+1个点,则这条路径上一定存在环,且是负环(因为更新过了,不是负的怎么会往回走呢)。

所以bellman-ford算法可以用来找负环(但时间复杂度较高),不过一般都用spfa来找。
时间复杂度:O(nm)

伪代码:

for n次  //n
	for 所有边 a,b,w (松弛操作)  //m
		dist[b] = min(dist[b],back[a] + w)

back数组是对dist数组的备份。如果不加back数组,可能会发生串联
如图:
在这里插入图片描述
开始的时候:

     1  2  3
dist 0  INF INF 

迭代一次:3号点是根据2号点得来的,所以得到了2,但实际它应该是3(1->3为3)。这里的2相当于迭代了2次(2条边)

     1  2  3
dist 0  1  2 

这里的错误就是串联。
所以需要一个back数组来存放迭代前的dist数组。

关于边数限制的含义:如上图,如果边数限制是1,则1-3的最短路是3.

关于判断是否能达到终点n:是否能到达n号点的判断中需要进行if(dist[n] > INF/2)判断,而并非是if(dist[n] == INF)判断,原因是INF是一个确定的值,并非真正的无穷大,会随着其他数值而受到影响,dist[n]大于某个与INF相同数量级的数即可
如图:dist[x]不等于0x3f3f3f3f,但是还是到不了。
所以我们的判断条件就变为:if(dist[n] > INF/2) cout<<-1;
在这里插入图片描述
题:
在这里插入图片描述
代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=500+10,M=10000+10;
struct node
{
	int a,b,c;
}nodes[M];//M条边 
int dist[N];
int back[N];
int n,m,k;

void bellman_ford()
{
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	
	for(int i=0;i<k;i++)//k次循环 
	{
		memcpy(back,dist,sizeof(dist));
		//把dist的全部复制给back
		for(int j=0;j<m;j++)
		{
			int a=nodes[j].a,b=nodes[j].b,c=nodes[j].c;
			dist[b]=min(dist[b],back[a]+c);
			//这里是back[a]+c,即这一次迭代前的dist[a],否则会串联 
		} 
	}
}
int main()
{
	cin>>n>>m>>k;
	for(int i=0;i<m;i++)
	{
		int a,b,c;
		cin>>a>>b>>c;
		nodes[i]={a,b,c};
	}
	
	bellman_ford();
	
	if(dist[n]>0x3f3f3f3f/2) cout<<"impossible";
	else cout<<dist[n];
	return 0;
}

spfa

AcWing 851. spfa求最短路

参考和引用:12

spfa算法是对bellman-ford算法的优化。
bellman-ford会更新所有的边,实际上没有必要。我们只需要遍历那些到起点的距离变小的点所相连的边即可。
因此,做法:创建一个队列,每次放入距离被更新的结点。

注意:
st数组的作用
标记是否已经在队列中。如果已经在队列中,就无需再push进队列,只需更新值。

spfa与dijkstra的不同

  1. dijkstra中的st[x]=1表示,点x的最短路已经确定,因此x点的dist值不会改变;而spfa的st[x]=1表示,x点在队列里,但dist[x]还是可能更新的。
  2. dijkstra中使用优先队列保存当前未确定的点中的最短路,而spfa用的队列,是保存当前发生过更新的点。

spfa与bellman-ford的不同:判断没有最短路条件
bellman-ford中表示没有最短路(也就是不连通)的条件是if(dist[n]>0x3f3f3f3f/2),原因是所有的路径bellman-ford都会遍历,所以尽管不连通,也会参与计算,0x3f3f3f3f可能会改变,判断条件就不能是if(dist[n]==0x3f3f3f3f).
而spfa中表示没有最短路的条件仍是if(dist[n]==0x3f3f3f3f),原因是,若不连通,则不会发生更新,0x3f3f3f3f不会改变。

spfa与bellman-ford的不同:关于负环
bellman-ford可以存在负权回路,因为它会限制路径长度,不会产生死循环。
spfa不行,它用队列存储,只要更新就不断入队,会死循环。

时间复杂度
spfa由bellman-ford优化而来,最坏情况下的时间复杂度跟bellman-ford一样,O(nm)。

求负环一般使用spfa

能用dijkstra尽量别用spfa,因为spfa的时间复杂度带有常数,比dijkstra更容易TLE

题:
在这里插入图片描述
代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=100010,M=100010;
int h[N],e[M],w[M],ne[M],idx;
void add(int a,int b,int c)
{
	e[idx]=b;
	w[idx]=c;
	ne[idx]=h[a];//联想一下头插法就好理解 
	h[a]=idx++;
}

int n,m;
int dist[N],st[N];

void spfa()
{
	memset(dist,0x3f,sizeof(dist));
	dist[1]=0;
	
	queue<PII>q;
	q.push({0,1});//first是w,second是结点编号
	st[1]=1;
	while(q.size())
	{
		PII temp=q.front();
		q.pop();
		int id=temp.second;
		st[id]=0;
		
		//这里是更新所有与队头元素相连的边
		for(int i=h[id];i!=-1;i=ne[i])
		{
			int j=e[i];
			
			//是w[i],不是temp.first 后者是固定不变的 
			if(dist[j]>dist[id]+w[i])
			{
				dist[j]=dist[id]+w[i];
				if(!st[j])
				{
					st[j]=1;
					q.push({dist[j],j});
				}
			}
		}
	} 
}
int main()
{
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof(h));//邻接表要初始化为-1
	 
	while(m--)
	{
		int a,b,c;scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);
	}
	
	spfa();
	
	if(dist[n]==0x3f3f3f3f) cout<<"impossible";
	else cout<<dist[n];
	return 0;
}

AcWing 852. spfa判断负环

参考和引用:这里

用spfa算法解决是否存在负环有两种方法:

  1. 统计每个点的入队次数,如果某个点入队次数为n次,则说明存在环
  2. 统计当前每个点的最短路中所包含的边数,如果某点的最短路所包含的边数大于等于n,则说明存在环。

一般用方法2,因为方法1可能会超时。

spfa求负环与spfa求最短路的区别:
不需要初始化为0x3f
因为不是求距离。而且,如果存在负环,不管初始化为多少,都会被更新(至无穷小)。

要把所有的点都放进去
求是否存在负环,但负环可能没法从1号点得到。所以开始的时候要把所有点放进去。就当作:

在原图的基础上建立一个虚拟源点,从该点向其他所有点连一条权值为0的有向边。

dist[x]记录虚拟源点到x点的距离
cnt[x]记录当前x点到虚拟源点的最短路边数。若出现cnt[x]>=n,说明存在负环。(因为只有n个点,而1-n的点的个数应该最多只有n-1个才是,多了说明有环,而因为求的是最短路,如果环是正的,就不会往回拐,所以一定是负环)

时间复杂度 一般:O(m) 最坏:O(nm)

题:
在这里插入图片描述
代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
typedef pair<int,int> PII;
const int N=2010,M=10010;
int h[N],e[M],w[M],ne[M],idx;
void add(int a,int b,int c)
{
	e[idx]=b;
	w[idx]=c;
	ne[idx]=h[a];//联想一下头插法就好理解 
	h[a]=idx++;
}

int n,m;
int dist[N],st[N],cnt[N];
int flag=0;//判断是否有负环 

void spfa()
{		
	queue<int>q;
	for(int i=1;i<=n;i++)
	{
		q.push(i);
		st[i]=1;
	}
	while(q.size())
	{
		int id=q.front();
		q.pop();
		
		st[id]=0;
		
		//这里是更新所有与队头元素相连的边
		for(int i=h[id];i!=-1;i=ne[i])
		{
			int j=e[i];
			
			//是w[i],不是temp.first 后者是固定不变的 
			if(dist[j]>dist[id]+w[i])
			{
				dist[j]=dist[id]+w[i];
				cnt[j]=cnt[id]+1;
				if(cnt[j]>=n) 
				{
					flag=1;
					return;
				}
				if(!st[j])
				{
					st[j]=1;
					q.push(j);
				}
			}
		}
	} 
}
int main()
{
	scanf("%d%d",&n,&m);
	memset(h,-1,sizeof(h));//邻接表要初始化为-1
	 
	while(m--)
	{
		int a,b,c;scanf("%d%d%d",&a,&b,&c);
		add(a,b,c);
	}
	
	spfa();
	
	if(flag==1) puts("Yes");
	else puts("No");
	
	return 0;
}

Floyd-AcWing 854. Floyd求最短路

参考和引用:12

要点全在这里:
在这里插入图片描述
这个算法要注意的点:

状态转移
d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
i到j的距离为min(自己,i到k的距离+k到j的距离)。

判断是否存在最短路的条件

if(d[x][y]>0x3f3f3f3f/2) 

因为Floyd算的是所有点的距离,会把没法连到起点的点也算进去。所以要这样判断。

注意全初始化为无穷大和d[x][x]=0

题:
在这里插入图片描述
代码:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=200+10;
int d[N][N];//d[i][j]表示从i到j的最短距离 
int n,m,k;

void Floyd()
{
	for(int k=1;k<=n;k++)//k相当于跳板
		for(int i=1;i<=n;i++)
			for(int j=1;j<=n;j++)
			{
				d[i][j]=min(d[i][j],d[i][k]+d[k][j]);
			} 
}
int main()
{
	cin>>n>>m>>k;
	memset(d,0x3f,sizeof(d));
	for(int i=1;i<=n;i++) d[i][i]=0;
	while(m--)
	{
		int a,b,c;cin>>a>>b>>c;
		d[a][b]=min(d[a][b],c);
	}
	Floyd();
	while(k--)
	{
		int x,y;cin>>x>>y;
		if(d[x][y]>0x3f3f3f3f/2) puts("impossible");
		else cout<<d[x][y]<<endl;
	}
	return 0;
}

拓扑排序

看这里

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

karshey

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值