图论——最短路径

博客围绕求从s到t权值和最小的路径展开,介绍了Floyd - Warshall、Dijkstra、Bellman - Ford三种算法。Floyd可处理负边,Dijkstra适用于无负边情况,Bellman - Ford能检查负回路。还给出各算法模板及例题,并对算法思想进行了对比。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

求从s到t权值和最小的路径

Floyd-Warshall算法

  • 求任意两点之间的最短路径(可以是负边)
  • 时间复杂度 O ( n 3 ) O(n^{3}) O(n3)
  • 该算法的思想十分简单, k k k表示中间桥梁,暴力遍历所有可能性,如果点 i i i到点 j j j的距离大于点 i i i到点 k k k的距离加上点 k k k到点 j j j的距离,那么就将 d [ i ] [ j ] d[i][j] d[i][j]更新为 d [ i ] [ k ] + d [ k ] [ j ] d[i][k]+d[k][j] d[i][k]+d[k][j],例如想要从昆明到重庆,如果从昆明到成都,再从成都到重庆的时间小于从昆明到重庆,那么显然使用时间少的来选择路径
模板:
const int MAX_V = 1005;
const int INF = 0x3f3f3f3f;
int dis[MAX_V][MAX_V];//dis[u][v]表示顶点u和v之间的最短路径,其中若没有连接则dis[u][v] = INF, u=v时,dis[u][v] = 0 
int v;

void Floyd_Warshall() {
	for(int k = 1; k <= v; k++) 
		for(int i = 1; i <= v; i++) 
			for(int j = 1; j <= v; j++) 
				if(i != j && i != k && j != k)
					dis[i][j] = min(dis[i][j], dis[i][k] + dis[k][j]);
}
Floyd例题

问题描述:
现在有一无向连通加权图,它有 N N N个顶点和 M M M条边,该图不包含自环以及重边,
其中第 i ( 1 &lt; = i &lt; = M ) i(1&lt;=i&lt;=M) i1<=i<=M条边连接顶点 a i a_i ai b i b_i bi,距离为 c i c_i ci
a i = b i a_i = b_i ai=bi的边表示自环,重边是两条边 ( ( a i , b i ) = ( a j , b j ) ((a_i,b_i)=(a_j,b_j) ((ai,bi)=(aj,bj)或者 ( a i , b i ) = ( b j , a j ) ) ( 1 &lt; = i &lt; = j &lt; = M ) (a_i,b_i)=(b_j,a_j))(1&lt;=i&lt;=j&lt;=M) (ai,bi)=(bj,aj))1<=i<=j<=M
连通图是指每对不同顶点之间都有一条路径的图。
请找到不包含在任意两点之间的最短路径中的边的数量。

输入

第一行输入 n , m n, m n,m
接下来 m m m行,每行 a &ThinSpace; b &ThinSpace; c a\, b\, c abc, 表示顶点 a a a与顶点 b b b距离为 c c c

输出

不包含在任意两点之间的最短路径中的边的数量

样例输入

3 3
1 2 1
1 3 1
2 3 3

样例输出

1

策略分析:

用两个邻接矩阵记录一下图,一个用来跑floyd算法,另一个用来做比较,如果某两条边更新距离之后不和之前的距离相等,说明这条边不包含在最短路径之中
#include <cstdio>
#include <algorithm>
#define MAXN 1005
#define INF 0x3f3f3f3f 

int d[MAXN][MAXN], be[MAXN][MAXN];
int V, E;
void floyd() {
    for(int k = 1; k <= V; k++)
        for(int i = 1; i <= V; i++)
            for(int j = 1; j <= V; j++)
                d[i][j] = std::min(d[i][j], d[i][k] + d[k][j]);
}

int main() {
    int a, b, c, cnt = 0;
    scanf("%d %d", &V, &E);
    for(int i = 1; i <= V; i++) { //初始化 
        for(int j = 1; j <= V; j++) {
            if(i == j) {
                d[i][j] = 0;
                be[i][j] = 0;
            }else {
            	d[i][j] = INF;
            	be[i][j] = INF;
			} 
        }
    }

    for(int i = 0; i < E; i++) {
        scanf("%d %d %d", &a, &b, &c);
        d[a][b] = c;
        d[b][a] = c;
        be[a][b] = c;
        be[b][a] = c;
    }

    floyd();
    for(int i = 1; i <= V; i++)
        for(int j = i+1; j <= V; j++)
            if(be[i][j] != INF && d[i][j] != be[i][j])
                cnt++;
    printf("%d\n", cnt);
    return 0;
}

Dijkstra算法

  • 无负边的情况下使用
  • 时间复杂度 O ( E l o g ( V ) ) O(Elog(_V)) O(Elog(V))
  • 找到最短距离确定的顶点,从该顶点出发更新相邻顶点的最短距离
普通模板
#define MAX_V 10001
#define INF 0x3f3f3f3f
int dis[MAX_V]; //dis[i]表示从顶点s出发到达i的最短路径,若不存在则为INF
int cost[MAX_V][MAX_V]; // 邻接矩阵
bool used[MAX_V]; //表示该点是否使用过
inline void init() {  //初始化
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= n; j++) 
			if(i != j) 
				cost[i][j] = INF;
}
void dijkstra(int s) {
	for(int i = 1; i <= n; i++)
		dis[i] = INF;
	dis[s] = 0;
	while(1) {
		int v = -1;
		for(int i = 1; i <= n; i++) 	// 找到最下一次能够扩展的最近的顶点v
			if(!used[i] && (v == -1 || dis[v] > dis[i])) 
				v = i;
		if(v == -1) break;	
		used[v] = 1;
		for(int i = 1; i <= n; i++) 	//松弛通过v顶点能够到达的所有最短路径
			dis[i] = min(dis[i], dis[v] + cost[v][i]);
	}
}
Dijkstra例题

问题描述:
罗老师被邀请参加一个舞会,是在城市n,而罗老师当前所处的城市为1,附近还有很多城市 2 ∼ n − 1 2\sim n-1 2n1,有些城市之间没有直接相连的路,有些城市之间有直接相连的路,这些路都是双向的,当然也可能有多条。
现在给出直接相邻城市的路长度,罗老师想知道从城市 1 1 1到城市 n n n,最短多少距离。

输入

输入 n , m n, m n,m,表示 n n n个城市和 m m m条路;
接下来 m m m行,每行 a &ThinSpace; b &ThinSpace; c a\, b\, c abc, 表示城市 a a a与城市 b b b有长度为 c c c的路。

输出

输出 1 1 1 n n n的最短路。如果 1 1 1到达不了 n n n,就输出 − 1 -1 1

样例输入

5 5
1 2 20
2 3 30
3 4 20
4 5 20
1 5 100

样例输出

90
#include <cstdio>
#include <algorithm>
#define MAX_V 10001
#define INF 0x3f3f3f3f
using namespace std;
int dis[MAX_V];
int cost[MAX_V][MAX_V];
bool used[MAX_V] = {0};
int n, m, s;
inline void init() {
	for(int i = 1; i <= n; i++)
		for(int j = 1; j <= n; j++) 
			if(i != j) 
				cost[i][j] = INF;
}
void dijkstra(int s) {
	for(int i = 1; i <= n; i++)
		dis[i] = INF;
	dis[s] = 0;
	while(1) {
		int v = -1;
		for(int i = 1; i <= n; i++) 
			if(!used[i] && (v == -1 || dis[v] > dis[i])) 
				v = i;
		if(v == -1) break;	
		used[v] = 1;
		for(int i = 1; i <= n; i++) 
			dis[i] = min(dis[i], dis[v] + cost[v][i]);
	}
}
int main() {
	int u, v, c;
	scanf("%d%d", &n, &m);
	init();
	for(int i = 1; i <= m; i++) {
		scanf("%d%d%d", &u, &v, &c);
		cost[u][v] = min(cost[u][v], c); //如果u,v之间有多条路,只考虑最短的一条即可
		cost[v][u] = cost[u][v];
	}
	dijkstra(1);
	if(dis[n] != INF)			
		printf("%d\n", dis[n]);
	else
		printf("%d\n", -1);
	return 0;
}
链式前向星优化
struct Edge {
	int to, next, w;
}edge[MAXX];
int cnt, n, m, s, head[MAXN], dis[MAXN]; //表示n个顶点,m条边,要初始化head,dis
bool vst[MAXN];
inline void addEdge(int u, int v, int w) {
	edge[++cnt].to = v;
	edge[cnt].w = w;
	edge[cnt].next = head[u];
	head[u] = cnt;
}
void dijkstra(int s) {
	dis[s] = 0;
	int cur = s;
	while(!vst[cur]) {
		vst[cur] = 1;
		for(int i = head[cur]; i != -1; i = edge[i].next) {
			if(!vst[edge[i].to] && dis[edge[i].to] > dis[cur] + edge[i].w)
				dis[edge[i].to] = dis[cur] + edge[i].w;
		}
		int mn = INF;
		for(int i = 1; i <= n; i++) {
			if(!vst[i] && mn > dis[i]) {
				mn = dis[i];
				cur = i;
			}
		}
	}
}
链式前向星 + 堆优化

模板题
问题描述:
给定一个 N N N 个点, M M M 条有向边的带非负权图,请你计算从 S S S 出发,到每个点的距离。
数据保证你能从 S S S 出发到任意点。

输入

第一行为三个正整数 N , M , S N, M, S N,M,S。 第二行起 M M M 行,每行三个非负整数 u i , v i , w i u_i, v_i, w_i ui,vi,wi,表示从 u i ​ u_i​ ui v i v_i vi有一条权值为 w i w_i wi的边。其中
1 ≤ N ≤ 100000 ; 1\leq N\leq 100000; 1N100000
1 ≤ M ≤ 200000 1 \leq M \leq 200000 1M200000
输出

输出一行 N N N个空格分隔的非负整数,表示 S S S 到每个点的距离。

样例输入

4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4

样例输出

0 2 4 3
#include <iostream>
#include <cstring>
#include <queue> 
#define MAXN 100005
#define MAXX 200005
#define INF 0x3f3f3f3f 
#define P pair<int, int>
using namespace std;

struct Edge {
	int next, to, cost;
}edge[MAXX];

bool vst[MAXN];
int dis[MAXN], head[MAXN], n, m, s, cnt;
priority_queue <P, vector<P >, greater<P > > q; //从小到大排
void add_edge(int u, int v, int cost) {	//链式前向星存边
	edge[++cnt].to = v;
	edge[cnt].next = head[u];
	edge[cnt].cost = cost;
	head[u] = cnt;
}

void dijkstra() {
	dis[s] = 0;
	q.push(P(0, s));
	while(!q.empty()) {
		int cur = q.top().second;
		q.pop();
		if(!vst[cur]) {
			vst[cur] = 1;
			for(int i = head[cur]; i != -1; i = edge[i].next) { //访问当前顶点cur能到达的所有顶点
				int u = edge[i].to;
				if(dis[u] > dis[cur] + edge[i].cost) {
					dis[u] = dis[cur] + edge[i].cost;//松弛操作
					q.push(P(dis[u], u));//加入堆中
				}
			}
		}	
	}
}

int main() {
	ios::sync_with_stdio(0);
	int u, v, w;
	cin >> n >> m >> s;
	memset(dis, INF, sizeof(dis)); //一定要初始化
	memset(head, -1, sizeof(head));
	for(int i = 1; i <= m ; i++) {
		cin >> u >> v >> w;
		add_edge(u, v, w);
	}
	dijkstra();
	for(int i = 1; i <= n; i++)
		cout << dis[i] << " ";
	return 0;
}

Bellman-Ford算法

  • 用于求解固定顶点到其他顶点的的最短路径问题
  • 时间复杂度 O ( E V ) O(EV) O(EV)
  • 可用来检查负回路(如果存在一个环从自己出发又回到自己,而且这个环上的权值之和为负数,就叫负权环或负权回路)
模板
const int maxx = 200;
const int INF = 999999;
struct edge {
	int from;//来自哪个顶点
	int to;//指向哪个顶点
	int cost;//权值
};
edge es[maxx];//存储边的信息的数组
int d[maxx];//存储距离的数组
int v, e;//顶点和边

//求解从顶点s出发到所有其他的最短距离
void shortese_path(int s) {
	for(int i = 0; i < v; i++)  d[i] = INF;//初始化距离
	d[s] = 0;//起点到起点的距离为0
	while(1) {//循环最多执行v-1次,每执行一次,更新顶点到所有可连接的顶点的最短路径
		bool update = false;
    	for(int i = 0; i < e; i++) {//遍历所有可能的边,进行松弛
    		edge edg = es[i];
    		if(d[edg.from] != INF && d[edg.to] > d[edg.from] + edg.cost) {//如果新的顶点的距离大于当前路径,则更新为短的当前路径
    			d[edg.to] = d[edg.from] + edg.cost;
        		update = true;
      		}
    	}
    	if(!update) //当所有的路径无法再更新的时候
    		break;
	}
}
//求解从顶点s出发是否存在负圈
bool find_negative_loop(int s) {
	memset(d, 0, sizeof(d));//将所有距离置为0
	for(int i = 0; i < v; i++) {//从第i个顶点出发
    	for(int j = 0; j < e; j++) {//每个顶点遍历所有的边
    		edge edg = es[j];
    		if(d[edg.to] > d[edg.from] + edg.cost) {
    			d[edg.to] = d[edg.from] + edg.cost;
    			if(i == v-1)//循环执行了v次
    			return true;//有负圈
    		}
    	}
	}
	return false;//无负圈
}

同时还有队列优化的Bellman-Ford算法(SPFA)就不写了(多用dijkstra)

算法思想对比

首先简单进行下对比:

算法floyddijkstraBellman-Ford
空间复杂度 O ( n 2 ) O(n^2) O(n2) O ( E ) O(E) O(E) O ( E ) O(E) O(E)
时间复杂度 O ( n 3 ) O(n^3) O(n3) O ( ( V + E ) l o g V ) O((V+E)logV) O((V+E)logV) O ( E V ) O(EV) O(EV)
使用稠密图,顶点较少稠密图稀疏图
可否处理负权可以不能可以
可否判断负权不能不能可以

对于这三种基本的算法

  • floyd算法就是通过循环的方式,暴力枚举每两个点之间的路,是否存在着更短的路径,因此效率比较低。
  • dijkstra算法的思想是一种贪心的思想,每次都从剩余未访问过的顶点找到距离起点最近的点,加入队列,然后更新所有能够通过这个点访问到的顶点的距离,并且把这个点标记成已经访问过,后面不再访问它。(这样做是因为每一步都是从起点出发,并且以最短的路径来更新的,所以已经访问过的点不需要再次访问,起点到它的距离一定是最短的。)然后重复这个过程,直到所有点都被访问过结束。
  • Bellman-Ford算法会进行 V − 1 V-1 V1次松弛(松弛就是用所有节点更新距离最短)来找到最短路径,每次都利用所有的顶点来进行松弛操作,直到无法松弛为止。而这个算法能够判负环的原因就在于,每一次松弛至少会确定一个顶点的最短路径,但不知道是哪一个顶点,因此最多进行 V − 1 V-1 V1次松弛,如果松弛的次数超过了这个值,就可以说明有负环(对于负环来讲,环上权值之和为负,必然会无限的迭代下去)

对于这三种算法,针对不同的情况使用,dijkstra算法可扩展性强,效率较高。Bellman_Ford可以判断负环,而floyd算法很好写,针对特定情况很方便。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值