图的最短路径算法(Dijkstra,SPFA,Bellman_Ford,Floyd)(迪杰斯特拉算法,Spfa算法,贝尔曼-福特算法,弗洛伊德算法)(超详代码注释+例题)(使用C/C++)

目录

Dijkstra迪杰斯特拉算法

可行性证明

写法

时间复杂度

例题

描述

输入描述

输出描述

样例

输入用例

输出用例

写法

Spfa算法

写法

例题

描述

输入描述

输出描述

样例

输入用例

输出用例

写法

Bellman_Ford算法(贝尔曼-福特算法)

写法

例题

描述

输入描述

输出描述

样例

输入样例

输出样例

写法

Floyd算法(弗洛伊德算法)

写法

例题

描述

输入描述

输出描述

样例

输入描写

输出描写

写法

谢谢大家


Dijkstra迪杰斯特拉算法

迪杰斯特拉算法(Dijkstra)是由迪杰斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。

缺点:在负权图中无法使用(SPFA,Bellman_Ford皆可)

可行性证明

dis[i]表示点i到起点的距离。
队列中存在若干点,其中dis[i]最小。
假设i出队后,并不是最短路。即存在一点j使得dis[i]变得更小,则有:dis[i]>dis[j]+w。
又因为dis[j]>=dis[i],当w>0时,该不等式不成立。因此点i出队后,一定找到i到起点的最短路。

写法

核心:根据已知最短路径来更新未知点

在BFS中定义小根堆优先队列,并且利用pair建立两个变量(点n距离1的值,点n)

1. 初始化所有点到起点1的距离为正无穷,且dis[1]=0。
2. 将起点信息(0,1)入队进行bfs拓展,将起点相连点入队,即(3,2),(5,3)入队。更新点2,3的距离信息。
3. 继续拓展节点2,3,将(6,4),(9,4)以及(119,5)入队。此时队首为(5,4),将该节点出队,得到dis[4]=5。
4. 继续拓展,直到终点出队,BFS循环结束

时间复杂度

朴素写法:O(n^2)

优化写法:O((n+m) logm)

例题

描述

给定一个n个点m条边的有向图,图中可能存在重边和自环,所有边权均为正值(边权小于10000)。
请你求出1号点到n号点的最短距离。如果无法从1号点走到n号点,则输出-1。

输入描述

第一行包含整数n和m(n<=1e5,m<=2e5)。
接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出描述

输出一个整数,表示1号点到n号点的最短距离
如果路径不存在,则输出-1。

样例

输入用例
3 3
1 2 2
2 3 1
1 3 4
输出用例
3

写法

本题如果用朴素的dijkstra算法,按O(n^2)算的话,1e5*1e5一定会超时,故用优化算法

上优化后算法

#include<bits/stdc++.h>
using namespace std;
#pragma GCC s
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC fast
//优化程序运行效率 
void read(int &x) {
	x=0;
	int flag=1;
	char ch=getchar();
	for(; ch<'0'||ch>'9';) {
		if(ch=='-') flag=-1;
		ch=getchar();
	}
	for(; ch>='0'&&ch<='9'; ch=getchar()) x=x*10+ch-'0';
	x=x*flag;
}//快读进行常数优化 
const int N=1e5+5;
int dis[N],n,m;
int h[N],nx[N*2],v[N*2],w[N*2],tot;
bool vis[N];
//n*2原型:n*(n-1)=m,n为节点数,m为一个图的最大边数
priority_queue <pair <int,int> ,vector<pair <int,int> >,greater<pair <int,int> > > q;
/*
	优先队列大根堆写法:priority_queue < pair<int,int> > q; 
	优先队列小根堆写法:priority_queue <pair <int,int> ,vector<pair <int,int> >,greater<pair <int,int> > > q;
*/
void add(int x,int y,int z) {
	tot++;
	v[tot]=y;
	w[tot]=z;
	nx[tot]=h[x];
	h[x]=tot;
}//邻接表记录图的关系
void dijkstra(int x) {
	memset(dis,0x3f,sizeof dis);
    /*
	    1. 首先,memset()是按内存地址命名的,故用0x3f;
	    2. memset()中的0x3f赋完值后,dis[]中的数字是为0x3f3f3f3f,而int的最大值为0x7f7f7f7f,
	    为了防止两个标记的数组值相加导致爆int,故对半开为0x3f3f3f3f 
	*/ 
	dis[x]=0;//出发点到出发点的距离是0 
	q.push({0,x});//放入队列
	while(!q.empty()) {//队列为空时代表遍利完成 
		pair <int,int> tmp;
		tmp=q.top();//取得队列的第一位 
		q.pop();//弹出队列的第一位 
		vis[tmp.second]=1;//vis标记,防止二次入队,降低时间复杂度 
		for(int i=h[tmp.second]; i; i=nx[i]) {
			if(vis[v[i]]==0/*vis标记检查(==0代表没入队),防止二次入队,降低时间复杂度 */&&dis[v[i]]>dis[tmp.second]+w[i]) {
				dis[v[i]]=dis[tmp.second]+w[i];
				q.push({dis[v[i]],v[i]});//入队 
			}
		}
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
    //优化程序 
	read(n);
	read(m);
	for(int i=1; i<=m; i++) {
		int x;
		int y;
		int z;
		read(x);
		read(y);
		read(z);
		add(x,y,z);
	}
	dijkstra(1);//进行遍历
	if(dis[n]==0x3f3f3f3f) cout<<-1;//如果没有被访问证明不联通,故输出-1 
	else cout<<dis[n];
	return 0;
}

Spfa算法

SPFA就是由无权图的BFS转化而来。在无权图中,BFS首先到达的顶点所经历的路径一定是最短路(也就是经过的最少顶点数),所以此时利用数组记录节点访问可以使每个顶点只进队一次,但在带权图中,最先到达的顶点所计算出来的路径不一定是最短路。一个解决是放弃数组,此时所需时间自然就是指数的,所以我们不能放弃数组,而是在处理一个已经在队列中且当前所得的路径比原来更好的顶点时,直接更新最优解。

缺点:在菊花图(下图)中,时间复杂度飘升至O(mn)(m为边数,n为点数)与Bellman_Ford的时间复杂度相同,也不能无法解决限制变数的问题(Bellman_Ford 可以)

不过在正常情况下时间复杂度为O(km) (在稀疏图中一般小于等于2)

写法


在求解各点到起点的最小距离dis值时,若某点i产生一个更小的dis[i],那么节点i后续指向的节点都会重新更新,因此我们可以将该点i再次入队,重新更新即可。

例题

描述

给定一个n个点m条边(n<=1e5,m<=2e5)的有向图,图中可能存在重边和自环,边权绝对值小于104。数据保证图中不存在负权回路。

请你求出1号点到n号点的最短距离。如果无法从1号点走到n号点,则输出-1。

输入描述

第一行包含整数n和m(n<=1e5,m<=2e5)。

接下来m行每行包含三个整数x,y,z,表示存在一条从点x到点y的有向边,边长为z。

输出描述

输出一个整数,表示1号点到n号点的最短距离
如果路径不存在,则输出-1。

样例

输入用例
3 3
1 2 1
2 3 2
1 3 1
输出用例
1

写法

#include<bits/stdc++.h>
using namespace std;
#pragma GCC s
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC fast
//优化程序运行效率 
void read(int &x) {
	x=0;
	int flag=1;
	char ch=getchar();
	for(; ch<'0'||ch>'9';) {
		if(ch=='-') flag=-1;
		ch=getchar();
	}
	for(; ch>='0'&&ch<='9'; ch=getchar()) x=x*10+ch-'0';
	x=x*flag;
}//快读进行常数优化 
const int N=1e5+5;
int dis[N],n,m;
int h[N],nx[N*2],v[N*2],w[N*2],tot;
bool vis[N];
//n*2原型:n*(n-1)=m,n为节点数,m为一个图的最大边数
queue<int>q;
void add(int x,int y,int z) {
	tot++;
	v[tot]=y;
	w[tot]=z;
	nx[tot]=h[x];
	h[x]=tot;
}//邻接表记录图的关系 
void spfa(int x) {
	memset(dis,0x3f,sizeof dis);
	/*
	1. 首先,memset()是按内存地址命名的,故用0x3f;
	2. memset()中的0x3f赋完值后,dis[]中的数字是为0x3f3f3f3f,而int的最大值为0x7f7f7f7f,
	为了防止两个标记的数组值相加导致爆int,故对半开为0x3f3f3f3f 
	*/ 
	dis[x]=0;//出发点到出发点的距离是0 
	q.push(x);//放入队列 
	vis[x]=1;//vis标记,防止二次入队,降低时间复杂度
	while(!q.empty()) {
		int tmp=q.front();//取得队列的第一位 
		q.pop();//弹出队列的第一位 
		vis[tmp]=0;//vis出队标记
		for(int i=h[tmp];i; i=nx[i]) {
			if(dis[v[i]]>dis[tmp]+w[i]) {
				dis[v[i]]=dis[tmp]+w[i];
				if(vis[v[i]]==0) {//vis标记检查(==0代表没入队),防止二次入队,降低时间复杂度 
					vis[v[i]]=1;//vis入队标记
					q.push({v[i]});//入队 
				}
			}
		}
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	//优化程序 
	read(n);
	read(m);
	for(int i=1; i<=m; i++) {
		int x;
		int y;
		int z;
		read(x);
		read(y);
		read(z);
		add(x,y,z);
	}
	spfa(1);
	if(dis[n]==0x3f3f3f3f) cout<<-1;//如果没有被访问证明不联通,故输出-1 
	else cout<<dis[n];
	return 0;
}

Bellman_Ford算法(贝尔曼-福特算法)

遇到有限制边数的题可采用bellman_ford,从起点开始,每一次循环只更新一次,更新出新的最小值。

时间复杂度为O(nm)。

写法

如果只使用一个dis数组进行存储的话 ,在面对下图的时候,会更新多个节点,这不满足我们的需要。

而为了解决此问题,我们建立了一个dis_old数组进行存储,将dis数组第i轮的操作后得到的值,完整复制到dis_old数组,用此数组和dis数组进行处理,就可以避免一次循环更新多个节点的问题了。

下图为dis数组和dis_old数组用上图样例进行更新时候的过程。

——12345
tmp
dis0
————————————
tmp0
dis01
————————————
tmp01
dis013
————————————
tmp013
dis0136
————————————
tmp0136
dis013610
————————————

例题

描述

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible。

输入描述

第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x
到点 y 的有向边,边长为 z。

点的编号为 1∼n。

输出描述

输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible。

样例

输入样例
3 3 1
1 2 1
2 3 1
1 3 3
输出样例
3

写法

#include<bits/stdc++.h>
using namespace std;
#pragma GCC s
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC fast
//优化程序
void read(int &x) {
	x=0;
	int flag=1;
	char ch=getchar();
	for(; ch<'0'||ch>'9';) {
		if(ch=='-') flag=-1;
		ch=getchar();
	}
	for(; ch>='0'&&ch<='9'; ch=getchar()) x=x*10+ch-'0';
	x=x*flag;
}//快读进行常数优化 
int n,m,k,dis[505],tmp[505];//俩数组,用来记录和复制 
struct node {
	int x;
	int y;
	int z;
} t[10005];//记录边的两顶点,边权 
void bellman_ford(int x) {
	memset(dis,0x3f,sizeof(dis));
	/*
		优先队列大根堆写法:priority_queue < pair<int,int> > q; 
		优先队列小根堆写法:priority_queue <pair <int,int> ,vector<pair <int,int> >,greater<pair <int,int> > > q;
	*/
	dis[x]=0;//出发点到出发点的距离是0
	for(int i=0; i<k; i++) {//模拟边数 
		memcpy(tmp,dis,sizeof(tmp));//复制数组 
		for(int j=1; j<=m; j++) {//枚举边 
			int x=t[j].x;
			int y=t[j].y;
			int w=t[j].z;
			dis[y]=min(dis[y],tmp[x]+w);//找最小值 
		}
	}
}
int main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	//优化程序 
	read(n);
	read(m);
	read(k);
	for(int i=1; i<=m; i++) {
		read(t[i].x);
		read(t[i].y);
		read(t[i].z);
	}
	bellman_ford(1);
	if(dis[n]>=0x3f3f3f3f/2) cout<<"impossible";
	/*
		由于负边权的存在初始化的值可能被减小
		但题目中负边权的值一般不会小于 -5e8,故用 dis[n]>=0x3f3f3f3f/2
	*/
	else cout<<dis[n];
	return 0;
}

Floyd算法(弗洛伊德算法)

写法

因为Floyd算法的时间复杂度为O(n^3),故用邻接矩阵即可(之前的三种算法都要邻接表)

floyd算法核心原理:我们可以用一个二维矩阵表示两点之间的最短路径,即我们只需要维护图中最小距离的邻接矩阵。

只有在两点路径间插入一个中间点,才可能使得两点距离减小。如:3->4,距离为11,插入中间节点1后3->1->4,变成10。
因此,每加入一个中间点k,矩阵中有些点对的距离就有可能减小。同时也说明两点的最短路径链上就可能会增加该中间点k。
当所有点都作为中间点加入且完成矩阵的更新后,最终矩阵一定存储任意两点之间的距离最小值。

模拟3->4的最短路径

初始矩阵

终点1终点2终点3终点4
出发点1003f3f3f3f03f3f3f3f2        
出发点21003f3f3f3f6
出发点383011
出发点403f3f3f3f03f3f3f3f03f3f3f3f0

加入1当中点

终点1终点2终点3终点4
出发点1003f3f3f3f03f3f3f3f2        
出发点21003f3f3f3f3
出发点383010
出发点403f3f3f3f03f3f3f3f03f3f3f3f0

加入2当中间点

终点1终点2终点3终点4
出发点1003f3f3f3f03f3f3f3f2        
出发点21003f3f3f3f3
出发点34306
出发点403f3f3f3f03f3f3f3f03f3f3f3f0

最开始dis[3][4]=11。
当节点1作为中间点进行更新得到:
2->4 变为 2->1->4(dis[2][4]=3),而3->4 变为3->1->4(dis[3][4]=10)。

当节点2作为中间点更新得到:
3->1 变为 3->2->1(dis[3][1]=4)。而3->4变为3->2->4(dis[3][4]=6),而2->4的路径已经变成为2->1->4,故3->4的最短路径为3->2->1->4。

例题

描述

给定由n个点m条边的构成无向图,边权可能为负,图中可能存在重边,不存自环以及负权环。现在给定q组询问,请输出每组询问中节点i到j的最短距离及其路径。

输入描述

第一行 n,m,q,其中n≤500,m≤106,q≤104。接下来m行表示这两个点之间有边相连,边权绝对值小于10000。随后q行,代表接下来有q个询问,每个询问由ai​,bi​构成。

输出描述

一共输出q行,如果两点不存在路径则输出-1,否则每行第一个数字表示两点之间的最短距离,随后输出其路径(如果存在多条最短路径,则输出字典序最小的一个)

样例

输入描写
3 3 2
1 2 1
2 3 1
1 3 3
1 3
2 3
输出描写
2 1 2 3
1 2 3

写法

#include<bits/stdc++.h>
#pragma GCC s
#pragma GCC optimize(2)
#pragma GCC optimize(3)
#pragma GCC fast
//优化程序运行效率
using namespace std;
long long read() {
	int x=0,f=1;
	char c=getchar();
	while(c<'0'||c>'9') {
		if(c=='-') f=-1;
		c=getchar();
	}
	while(c>='0'&&c<='9') {
		x=x*10+c-'0';
		c=getchar();
	}
	return x*f;
}//快读进行常数优化
const int N=505;
int n,m,q,tot,g[N][N],fa[N][N];//g[N][N]记录边的两顶点,fa[N][N]记录父亲节点 
void prt(int x,int y) {
	if(fa[x][y]==0)	printf(" %d",y);
	else prt(x,fa[x][y]),prt(fa[x][y],y);
}//输出部分
void floyd() {
	for(int k=1; k<=n; k++) {//枚举加入的点
		for(int i=1; i<=n; i++) {//枚举起点 
			for(int j=1; j<=n; j++) {//枚举终点 
				if(i==j||i==k||j==k)	continue;
				//加入的点和起点或终点相同则无需更新,起点和终点相同也无需更新 
				if(g[i][j]>g[i][k]+g[k][j]) {//如果加入的新点可以减少路径长度,那么就更新 
					g[i][j]=g[i][k]+g[k][j];//更新路径 
					fa[i][j]=k;//记录父亲节点 
				}
			}
		}
	}
}
int main() {
	n=read();
	m=read();
	q=read();
	memset(g,0x3f,sizeof(g));
	/*
	    1. 首先,memset()是按内存地址命名的,故用0x3f;
	    2. memset()中的0x3f赋完值后,dis[]中的数字是为0x3f3f3f3f,而int的最大值为0x7f7f7f7f,
	    为了防止两个标记的数组值相加导致爆int,故对半开为0x3f3f3f3f
	*/
	for(int i=1; i<=m; i++) {
		int a,b,c;
		a=read();
		b=read();
		c=read();
		g[a][b]=g[b][a]=min(g[a][b],c);
		//min(g[a][b],c)是因为有可能重边,又因为本题求最小值,故用min求最小值
	}
	floyd();//进行遍历
	while(q--) {
		int a;
		int b;
		a=read();
		b=read();
		if(g[a][b]>=0x3f3f3f3f) {
			cout<<-1;
			continue;
		}//如果没有被访问证明不联通,故输出-1
		printf("%d %d",g[a][b],a);
		prt(a,b);
		cout<<endl;
	}
	return 0;
}

谢谢大家

(写了正文8380+标题94+文章摘要94(合计8572)个字,点个赞再走吧,谢谢了)

(累死个人啦)

  • 28
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值