算法小讲堂之最短路算法(Floyd+bellman+SPFA+Dijkstra)

Posted MangataTS

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了算法小讲堂之最短路算法(Floyd+bellman+SPFA+Dijkstra)相关的知识,希望对你有一定的参考价值。

前言

如果你对图论相关知识一点也没有,那么建议您先去了解这些知识:https://acmer.blog.csdn.net/article/details/122310835,然后就可以快乐的学习最短路算法啦

视频中绘图软件:https://csacademy.com/app/graph_editor/

配套讲解视频:https://www.bilibili.com/video/BV1Fa411C7wX/

如果哪里讲的有问题欢迎在评论区指出,感谢支持!

一、Floyd算法

1.1简介

Floyd算法算是最简单的算法,没有之一。适用于任何图

不管有向无向,边权正负,但是最短路必须存在。

基于动态规划的思想

1.2复杂度

1.2.1时间复杂度

O ( N 3 ) O(N^3) O(N3)

1.2.2空间复杂度

O ( N 2 ) O(N^2) O(N2)

1.3优缺点

1.3.1优点

常数小,容易实现,思路简单,能处理大部分图

1.3.2缺点

复杂度较高、不能处理负环图

1.4算法原理

我们定义一个三维数组 f [ k ] [ u ] [ v ] f[k][u][v] f[k][u][v]表示的是允许经过 [ 1 , k ] [1,k] [1,k]的点的 u u u v v v的最小距离,换句话说从 1 1 1 k k k这些点可以作为 u u u v v v中间节点,当然没也可以不经过,很显然我们如果要求解 u u u v v v的最小距离那么就是 f [ n ] [ u ] [ v ] f[n][u][v] f[n][u][v](假设当前的图中有n个点的话),那么我们考虑怎么来维护这个关系呢,首先初始化来说, f [ 0 ] [ u ] [ v ] f[0][u][v] f[0][u][v]先初始化为INF,如果有边连接的话,那么我们取一个min就好,还有就是如果u和v相等的话应该初始化为0,那么我们就能推出这个状态是如何转移的:

f [ k ] [ u ] [ v ] = m i n ( f [ k − 1 ] [ u ] [ v ] , f [ k − 1 ] [ u ] [ k ] + f [ k − 1 ] [ k ] [ v ] ) f[k][u][v] = min(f[k-1][u][v],f[k-1][u][k] + f[k-1][k][v]) f[k][u][v]=min(f[k1][u][v],f[k1][u][k]+f[k1][k][v])

我们对经过k点和不经过k点去一个min,那么我们的状态转移方程就构造好啦,下面给出代码

void Floyd()
    for(int k = 1;k <= n; ++k)
        for(int i = 1;i <= n; ++i)
            for(int j = 1;j <= n; ++j)
                f[k][i][j] = min(f[k-1][i][j],f[k-1][i][k]+f[k-1][k][j]);

我们发现我们这个第一维的k其实最多能用到当前这一层以及上一层的状态,那么我们可以通过滚动数组优化将其去掉,那么新的代码即为:

void Floyd()
    for(int k = 1;k <= n; ++k)
        for(int i = 1;i <= n; ++i)
            for(int j = 1;j <= n; ++j)
                f[i][j] = min(f[i][j],f[i][k]+f[k][j]);

关于第一维对结果无影响的证明:

我们注意到如果放在一个给定第一维 k 二维数组中,f[x][k]f[k][y] 在某一行和某一列。而 f[x][y] 则是该行和该列的交叉点上的元素。

现在我们需要证明将 f[k][x][y] 直接在原地更改也不会更改它的结果:我们注意到 f[k][x][y] 的涵义是第一维为 k-1 这一行和这一列的所有元素的最小值,包含了 f[k-1][x][y],那么我在原地进行更改也不会改变最小值的值,因为如果将该三维矩阵压缩为二维,则所求结果 f[x][y] 一开始即为原 f[k-1][x][y] 的值,最后依然会成为该行和该列的最小值。

故可以压缩。

模板题:多源最短路

代码实现

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

const int N = 2e2+10;
const int INF = 0x3f3f3f3f;

int n,m,k;
int f[N][N];

void Floyd()
	for(int k = 1;k <= n; ++k)
		for(int i = 1;i <= n; ++i)
			for(int j = 1;j <= n; ++j)
				f[i][j] = min(f[i][j],f[i][k]+f[k][j]);


int main()

	cin>>n>>m>>k;
	int u,v,w;

	for(int i = 1;i <= n; ++i)
		for(int j = 1;j <= n; ++j)
			f[i][j] = i==j?0:INF;

	for(int i = 1;i <= m; ++i)
		cin>>u>>v>>w;
		f[u][v] = min(f[u][v],w);
	
	Floyd();
	while(k--)
		cin>>u>>v;
		if(f[u][v] > INF / 2) cout<<"impossible"<<endl;
		else cout<<f[u][v]<<endl;
	


	return 0;

二、Bellman-Ford 算法

2.1简介

B e l l m a n − F o r d Bellman-Ford BellmanFord 算法是一种基于松弛( r e l a x relax relax)操作的最短路算法,可以求出有负权的图的最短路,并可以对最短路不存在的情况进行判断。当然你可能没听过这个算法,但是应该听过另一个算法 S P F A SPFA SPFA 算法, S P F A SPFA SPFA算法其实就是加入了队列优化的 B e l l m a n − F o r d Bellman-Ford BellmanFord

2.2复杂度

2.2.1时间复杂度

O ( N M ) O(NM) O(NM)

2.2.2空间复杂度

邻接矩阵: O ( N 2 ) O(N^2) O(N2)

邻接表: O ( M ) O(M) O(M)

2.3优缺点

2.3.1优点

能够处理负权图、能处理边数限制的最短路

2.3.2缺点

复杂度不太理想,很容易被卡

2.4算法原理

2.4.1松弛操作

在介绍该算法前,先来介绍一下松弛操作,对于一个边 ( u , v ) (u,v) (u,v),松弛操作对应下面的式子: d i s [ v ] = m i n ( d i s [ v ] , d i s [ u ] + w ( u , v ) ) dis[v]=min(dis[v],dis[u]+w(u,v)) dis[v]=min(dis[v],dis[u]+w(u,v))。也就是我们将源点到v点的距离更新的一个操作

也就是开始可能源点 S S S v v v的路径为 S − > v S->v S>v,如果说经过 u u u点后再到 v v v的权值比直接到v小那么我们就更新一下路径最小值,这就是松弛操作

2.4.2 具体流程

Bellman算法要做的事就是对于图中所有的边,我们都进行一次松弛操作,那么完成这整个操作的复杂度大概在 O ( M ) O(M) O(M),然后我们就一直循环的进行这个操作,直到我们不能进行松弛操作为止,就说明我们的单源最短路以及全部求完,那么我们需要多少次这样的完整操作呢,在最短路存在的情况下,由于一次松弛操作会使最短路的边数至少+1 ,而最短路的边数最多为 N − 1 N-1 N1 ,因此整个算法最多执行 N − 1 N-1 N1轮松弛操作。故总时间复杂度为 O ( N M ) O (NM) O(NM)

2.4.3 负环问题

上面提到了我们在求最短路存在的情况最多执行 N − 1 N-1 N1轮松弛操作,如果数据中出现了负环,那么我们在第N轮操作的时候也会更新

注意一点

S S S点为源点跑 Bellman-Ford 算法时,如果没有给出存在负环的结果,只能说明从 S S S点出发不能抵达一个负环,而不能说明图上不存在负环。因为这个图可能是不连通的,那么对于不连通的图我们应该建一个虚点或者称之为超级源点,让这个点连向每一个其他的点并且权值为0,然后再来跑 b e l l m a n _ f o r d bellman\\_ford bellman_ford

2.4.4 算法图解

第x轮松弛操作本轮松弛操作
1dis[2] =1,dis[3]=4,dis[4]=6
2dis[4]=3
3无操作

模板题:https://ac.nowcoder.com/acm/contest/27274/E

2.5代码实现

#include<algorithm>
#include<cstring>
#include<iostream>
#include<cstdio>
#include<vector>

const int INF = 0x3f3f3f3f;
const int N = 10000+10;

using namespace std;

struct Node
	int u,v,w;
;
vector<Node> E; 
int n,m,s,t;
int dis[N];

void bellman_ford(int s)
	for(int i = 1;i <= n; ++i) dis[i] = INF;
	dis[s] = 0;
	
	for(int i = 1;i <= n; ++i)
		for(int j = 0;j < 2 * m; ++j) 
			int u = E[j].u,v = E[j].v,w = E[j].w;
			if(dis[v] > dis[u] + w)
				dis[v] = dis[u] + w;
		


int main()

	cin>>n>>m>>s>>t;
	int u,v,w;
	for(int i = 1;i <= m; ++i) 
		cin>>u>>v>>w;
		E.push_back(u,v,w);
		E.push_back(v,u,w);
	
		
	bellman_ford(s);
	if(dis[t] >= INF / 2) cout<<"-1"<<endl;
	else cout<<dis[t]<<endl;
	

2.6判负环实现

如果我们发现第 N N N轮操作也更新了那么说明存在负权回路

#include<iostream>
#include<algorithm>
#include<cstring>

using namespace std;
#define INF 0x3f3f3f3f

const int N = 2e6+10;

int n,m,q,k;
struct Edge
	int u,v,w;
E[N];
int dis[N]四大算法解决最短路径问题(Dijkstra+Bellman-ford+SPFA+Floyd)

单源最短路(bellman-ford算法+dijkstra算法)+任意两点最短路(floyd-warshall算法)

最短路问题(Bellman/Dijkstra/Floyd)

最短路径算法总结(floyd,dijkstra,bellman-ford)

最短路径之Dijkstra算法和Floyd-Warshall算法

求最短路径的三种算法: Ford, Dijkstra和Floyd