算法基础篇:常见图论最短路算法(Bellman-Ford→SPFA→Dijkstra Floyd-Warshall )入门以及代码解析

单源最短路

单源最短路的意思就是一个起点,算出到其他点的最短路,这里介绍三种算法,bellman-ford,spfa和dijkstra

BF算法

算法思想

BF是bellman_ford的简称,算法的想法非常简单,进行|V|−1次操作,每次操作对所有的边松弛。
松弛可以形象的理解为更新值,比如有一条从u
v的边,如果u上的值加上边的长度小于v上的值,那么就更新v上的值。
为什么这样做是对的呢?我们可以回顾整个过程,第一次操作,我们一定能将起点出去的点中的某一个点更新为最小值(这个点以后不会被更新),关于这点可以用反证法证明,如果没有一个点是更新完毕的,那么从起点到这个点必然存在一条比起点到这个点的边更短的路径,与假设矛盾。故而每次我们至少能确定一个点,所以总共需要|V|−1次(起点不用更新)。
最开始的图,0是起点


初始化

第一次松弛更新了1,2,3三个点

最后全部更新好了

上述的专业解析是引用一篇博文的,自己敲了一下Bellman-Ford最短路的算法,并附上比较详细的注释,方便记忆和理解(图的数据依旧引用上文,方便测试):
#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int M = 1001;
const int Maxn = 1000001;
int d[M],V,n;//V为最大点的数值(也就是总点数-1,因为还包括0),n为有向边的总条数
struct edge {
	int to;
	int cost;
	edge() {};//默认构造函数+重设构造函数赋值
	edge(int a,int b) {
		to = a;
		cost = b;
	}//相当于edge(int tt,int cc):to(tt),cost(cc){}
};
vector<edge>G[M];
void Bellman_Ford(int s) {
	fill(d , d +M, Maxn);//将s到各个点的距离初始化(为寻找最短路,若一条边不存在,默认距离为无穷大)
	d[s] = 0;//s到s自身的距离为0
	for (int i = 0; i < V - 1; i++) {  //总共最多需要更新V-1次(有V个点)
		for (int u = 0; u < V; u++) {
			for (int j = 0; j < G[u].size(); j++) {//G[u]里面存放的就是所有以u为起点的有向边edge(to,cost)
				int u_to_distance = G[u][j].cost;//u_to_distance为以u为起点的这条边的长度
				int nextpoint = G[u][j].to;//nextpoint为以u为起点的边的终点
				if (u_to_distance + d[u] < d[nextpoint]) {
					d[nextpoint] = u_to_distance + d[u];
				}//if目标点s→nextpoint(不经过u)的距离大于s→u→nextpoint的距离,
				//那么更新s→nextpoint(不经过u)的距离为s→u→nextpoint的距离(因为更短)
			}
		}
	}
}
int main() {
	int a, b, c;//设为每条边的起点、终点和长度
	while (~scanf("%d%d", &V,&n)) {//<span style="font-family: Arial, Helvetica, sans-serif;">V为最大点的数值(也就是总点数-1,因为还包括0),n为有向边的总条数</span>

		for (int i = 0; i < n; i++) {
			scanf("%d%d%d", &a, &b, &c);
			G[a].push_back(edge(b, c));
		}
		Bellman_Ford(1);//这个起始点可以修改测试
		for (int i =0; i <=V; i++)
			cout << d[i] << " ";
	}
	system("pasue");
}

复杂度分析

复杂度很明显是O(V∗E),在完全图的时候,会变成 O(V3)



输入输出如上,最大点为4,有0,1,2,3,4五个点,共有7条有向边
分别输入起点+终点+边长,得到从起始点1到各个点的最短路长度:
1→0 没有路,距离d=100001(默认无穷大)
1→1 d=0;1→2没有路 d=100001;1→3 最短路d=3;1→4最短路d=6.

接下来谈一下SPFA算法:

SPFA算法又叫做bellman-ford队列优化,至于为啥叫SPFA我也不知道。

算法思想

纵观整个BF算法,我们可以用宽度优先搜索来代替V-1次的暴力松弛,每次用队列头部的元素去更新其他点,如果将一个点更新成功,就将这个点加入队列,直到更新不动为止。
可以证明,这样做和BF本质上是一样的。但这样做有个好处,这个好处就在于我们可以记录一个点是否在队列中,如果这个点在队列中,那么我们知道这个点一定会被拿出来松弛,那么当这个点被更新的时候,我们就不用讲他加入队列了。如此,就大大改进了BF算法。

SPFA代码就不重新打了,因为可以直接在Bellman-Ford算法的基础上加上队列的运用,但是可以得到很大大的效率优化,代码及注释如下:



#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int M = 1001;
const int Maxn = 1000001;
int d[M],V,n;
queue<int>pp;//声明一个队列放入被更新的点,下一次更新就以这些点为基础对其它跟这个点有关联的边进行更新
//这里先不用优先队列,到后面更加优化的Dijkstra算法才用
bool inqueue[M];//用于判断队列中是否已经存在这个点
struct edge {
	int to;
	int cost;
	edge() {};//默认构造函数+重设构造函数赋值
	edge(int a,int b) {
		to = a;
		cost = b;
	}//相当于edge(int tt,int cc):to(tt),cost(cc){}
};
vector<edge>G[M];
void SPFA(int s) {
	while (!pp.empty()) {
		pp.pop();
	}//初始化队列,将里面的数据清空(规范)
	fill(d , d +M, Maxn);//将s到各个点的距离初始化(为寻找最短路,若一条边不存在,默认距离为无穷大)
	d[s] = 0;//s到s自身的距离为0
	pp.push(s);//此时队列中只有一个点,就是起始点s
	inqueue[s] = 1;
	while (!pp.empty()) {  //总共最多需要更新V-1次(有V个点)
		int u = pp.front();
		pp.pop(); //每次拿出一个点,用初始点s到改点的距离更新与该点有关联的其它边的距离,同时把该点移除队列
		inqueue[u] = 0;//去标记
		for (int j = 0; j < G[u].size(); j++) {//G[u]里面存放的就是所有以u为起点的有向边edge(to,cost)
			int u_to_distance = G[u][j].cost;//u_to_distance为以u为起点的这条边的长度
			int nextpoint = G[u][j].to;//nextpoint为以u为起点的边的终点
			if (u_to_distance + d[u] < d[nextpoint]) {
				d[nextpoint] = u_to_distance + d[u];
				if (!inqueue[nextpoint]) {//没有在队列里面,可以放进去
					pp.push(nextpoint);
					inqueue[nextpoint] = 1;
				}
			}//if目标点s→nextpoint(不经过u)的距离大于s→u→nextpoint的距离,
			//那么更新s→nextpoint(不经过u)的距离为s→u→nextpoint的距离(因为更短)
		}
	}
}
int main() {
	int a, b, c;//设为每条边的起点、终点和长度
	while (~scanf("%d%d", &V,&n)) {
		for (int i = 0; i < n; i++) {
			scanf("%d%d%d", &a, &b, &c);
			G[a].push_back(edge(b, c));
		}
		SPFA(1);
		for (int i =0; i <=V; i++)
			cout << d[i] << " ";
	}
	system("pasue");
}


输出的答案是一样的。但复杂度远比Bellman-Ford降低许多。

算法复杂度

这个算法的复杂度是个迷,据传是O(k∗E),其中 k是个不大的常数。


接下来再谈一下 Dijkstra算法

算法思想

我们依旧用宽度优先搜索的角度思考BF算法,我们发现,如果我们每次都将最小的值从队列中拿出,那么拿出来的一定是已经更新好了的(同样可以用反证法证明),所以只要我们将队列变成堆,就能快速处理最短路了。
还是刚刚那个图,最开始将0号节点加入堆

现在从堆中拿出最小的数0,用他更新1,2,3,并把1,2,3加入堆

现在堆里面最小的数是2号节点,说明2号节点已经更新好了,那么用2号节点去更新其他节点

最短的都更新好了

Dijkstra的代码要多敲几次加深理解,有几点需要注意一下,看一下注释:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;
const int M = 1001;
const int Maxn = 1000001;
int d[M],V,n;//V为最大点的数值(因为还包括0,所以总点数是V+1),n为有向边的总条数
struct edge {
	int to;
	int cost;
	edge() {};//默认构造函数+重设构造函数赋值
	edge(int a,int b) {
		to = a;
		cost = b;
	}//相当于edge(int tt,int cc):to(tt),cost(cc){}
};
struct node {
	int point;
	int d;  //node是用来储存 (更新完的点point+s到point的距离d)
	node() {};
	node(int a, int b) {
		point = a;
		d = b;
	}
	bool operator<(const node &a) const{
		return d > a.d;//(比较运算符的重载,是优先队列原本系统默认的从大到小的排列顺序
	}//变成从小到大的排序,方便直接用nn.top()直接取队首的元素,也就是最小的
};
vector<edge>G[M];
priority_queue<node>nn;//声明一个名为nn的优先队列
void Dijkstra(int s) {
	fill(d , d +M, Maxn);//将s到各个点的距离初始化(为寻找最短路,若一条边不存在,默认距离为无穷大)
	d[s] = 0;//s到s自身的距离为0
	while (!nn.empty())
		nn.pop();
	nn.push(node(s, 0));//同样,将初始数据放进优先队列
	while(!nn.empty()){
		node Next = nn.top();
		nn.pop();
		int u = Next.point;
		int dd = Next.d;
		if (d[u] < dd)continue;
			for (int j = 0; j < G[u].size(); j++) {//G[u]里面存放的就是所有以u为起点的有向边edge(to,cost)
				int u_to_distance = G[u][j].cost;//u_to_distance为以u为起点的这条边的长度
				int nextpoint = G[u][j].to;//nextpoint为以u为起点的边的终点
				if (u_to_distance + d[u] < d[nextpoint]) {
					d[nextpoint] = u_to_distance + d[u];
					nn.push(node(nextpoint, d[nextpoint]));//把更新完的点和距离放进队列里
				}//if目标点s→nextpoint(不经过u)的距离大于s→u→nextpoint的距离,
				//那么更新s→nextpoint(不经过u)的距离为s→u→nextpoint的距离(因为更短)
			}
		}
}
int main() {
	int a, b, c;//设为每条边的起点、终点和长度
	while (~scanf("%d%d", &V,&n)) {
		for (int i = 0; i < n; i++) {
			scanf("%d%d%d", &a, &b, &c);
			G[a].push_back(edge(b, c));
		}
		Dijkstra(1);//这个起始点可以修改测试
		for (int i =0; i <=V; i++)
			cout << d[i] << " ";
	}
	system("pasue");
}


算法复杂度

由于我们至少将所有边遍历一次,所以我们需要O(E)的时间,堆优化使得我们在搜索时将一个 V 的复杂度降到 logV,所以时间复杂度为  O(E+VlogV)

根据《挑战程序设计竞赛》上的内容,Dijkstra算法的复杂度是O(|E|*log|V|),

需要注意的一点是,在图中存在负边的情况下,Dijkstra算法无法正确求解问题,还是需要使用Bellman-Ford和它的优化SPFA算法。

接下来是  多源最短路 Floyd-Warshall算法


由于这个算法比较简单,在此处就直接引用源码了,不再由自己敲了,注释应该不需要,理解一下就好


如果我们想知道任意两个点之间的最短路,这时候可以暴力跑n次单源最短路,也可以用floyd算法

Floyd算法

这个算法的特点就是非常好写

算法思想

Floyd算法运用了动态规划的思想,对于一个最短路径(u,v)而言,我们一定是从u到了k,再从k到v。所以我们只要知道了(u,k)和(k,v)的最短路,那么我们就能得到(u,v)的最短路。
所以暴力枚举就好

算法分析

这个算法呢?是用来求任意两点间的最短路问题。可以试着用DP来求解任意两点间的最短路问题。只使用顶点0~k和i,j的情况下,记i到j的最短路长度为d[k+1][i][j]。k=-1时,认为只使用i和j,所以d[0][i][j]=cost[i][j]。接下来让我们把只使用顶点0~k的问题归约到只使用0~k-1的问题上。

只使用0~k时,我们分i到j的最短路正好经过顶点k一次和完全不经过顶点k两种情况来讨论。不经过顶点k的情况下,d[k][i][j]=d[k-1][i][j]。通过顶点k的情况下,d[k][i][j]=d[k-1][i][k]+d[k-1][k][j]。合起来就得到了d[k][i][j]=min(d[k-1][i][j],d[k-1][i][k]+d[k-1][k][j])。这个DP也可以使用同一个数组,不断进行d[i][j]=min(d[i][j],d[i][k]+d[k][j])的更新来实现。


算法实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#include <iostream>
#include <cstring>
#include <algorithm>
#define MAX_V 102
#define INF 1008611
using namespace std;

int d[MAX_V][MAX_V];
int V,E;

int main(){
	cin>>V>>E;
	for(int i=1;i<=V;i++)
		for(int j=1;j<=V;j++)
			d[i][j]=(i==j?0:INF);
	while(E--){
		int u,v,c;
		cin>>u>>v>>c;
		d[u][v]=c;
	}
	for(int k=1;k<=V;k++)
		for(int i=1;i<=V;i++)
			for(int j=1;j<=V;j++)
				d[i][j]=min(d[i][k]+d[k][j],d[i][j]);
	for(int i=1;i<=V;i++)
		for(int j=1;j<=V;j++)
			cout<<i<<" "<<j<<" "<<d[i][j]<<endl;
	return 0;
}

算法复杂度

很明显,复杂度是 O(V3)

时间复杂度还是很高的,但如果复杂度在可以承受的范围之内,可以使用这个实现起来非常简单的算法。


个人觉得,Dijkstra是SPFA的进一步优化,把SPFA中依靠普通队列中的点全部出列更新的操作转换为依靠优先队列(从小到大的预设)的第一个点(即那条更新后的最短边)来进行更新的操作,可以证明最短边是一定所属那个点更新完毕后所拥有的,这样一来就可以省去很多不必要的点继续拓展更新,快速找到最短路。

而SPFA又是Bellman-Ford的进一步优化,直接将O(V^3)的复杂度降低为用队列后的O(k*E),这是很大的一个优化。通过判断是否是被更新过的那些点,继续以S到这个被更新的点的距离(也就是更新后的更短的一条路)为基础对其它与它关联的更长的边进行更新。

Dijkstra用heap(堆)和链式前向星优化后几乎是最快的,但缺点就是不能求有负权的最短路与判断负环




评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值