最短路算法

Floyd Dijkstra Bellman-Ford Johnson
每对结点之间的最短路 单源最短路 单源最短路 每对结点之间的最短路
没有负环的图 非负权图 任意图(可以判定负环是否存在) 没有负环的图
O ( n 3 ) O(n^3) O(n3) O ( m log ⁡ m ) O(m \log m) O(mlogm) O ( n m ) O(nm) O(nm) O ( n m log ⁡ m ) O(nm \log m) O(nmlogm)

*注:这里的 Dijkstra 指单调队列优化的,未优化的 Dijkstra 时间复杂度为 O ( n 2 ) O(n^2) O(n2)

最短路

  • Floyd
  • Dijkstra
    • 链式前向星 + 单调队列 优化
  • Bellman-Ford
    • 队列优化:SPFA
      • 关于SPFA
    • 其他优化
  • Johnson
  • 输出路径
  • 参考

Floyd

// Floyd 邻接矩阵
#include
#include
#define N 302
using namespace std;
int dis[N][N];
int n, m;
int u, v, w;
int main() {
     
	memset(dis,63,sizeof(dis));
	cin >> n >> m;
	for(int i = 1; i <= m; ++i) {
     
		cin >> u >> v >> w;
		dis[u][v] = w;
	}
	
	for(int i = 1; i <= n; ++i) {
     
		dis[i][i] = 0;
	}
	
	for(int i = 1; i <= n; ++i) {
     
		for(int j = 1; j <= n; ++j) {
     
			cout << dis[i][j] << " ";
		}
		cout << endl;
	}
	cout << endl;
	for(int k = 1; k <= n; ++k) {
     
		for(int i = 1; i <= n; ++i) {
     
			for(int j = 1; j<= n; ++j) {
     
				dis[i][j] = min(dis[i][j],dis[i][k]+dis[k][j]);
			}
		}
	}
	
	for(int i = 1; i <= n; ++i) {
     
		for(int j = 1; j <= n; ++j) {
     
			cout << dis[i][j] << " ";
		}
		cout << endl;
	}
}
/*
4 5
1 2 12
1 3 16
1 4 14
2 3 7
3 4 9
*/

Dijkstra

//Luogu P3371 Dijkstra 邻接表
#include
#include
#include
#define N 10004
const int inf = 0x7f7f7f7f; 
using namespace std;
int n, m, s;
int u, v, w;
struct Edge {
     
	int v, w;
	Edge(int v_, int w_):v(v_), w(w_) {
     }
};
vector<Edge> g[N];
int dis[N];
bool vis[N];
void dijkstra(int st = 1) {
     
	for(int i=1;i<=n;i++) {
     
		dis[i]=0x7fffffff;
	}
	dis[st] = 0;
	memset(vis,false,sizeof(vis));
	for(int i = 1; i <= n; ++i) {
     
		int minn = inf;
		for(int j = 1; j <= n; ++j) {
     
			if(!vis[j] && (minn == inf || dis[j] < dis[minn])) {
     
				minn = j;
			}
		}
		if(minn == inf) {
     
			return ;
		}
		vis[minn] = true;
		for(vector<Edge>::iterator iter = g[minn].begin(); iter != g[minn].end(); iter++) {
     
			if(!vis[iter->v]) {
     
				dis[iter->v] = min(dis[iter->v], dis[minn] + iter->w);
			}
		}
	}
}
int main() {
     
	cin >> n >> m >> s;
	for(int i = 1; i <= m; ++i) {
     
		cin >> u >> v >> w;
		g[u].push_back(Edge(v,w));
	}
	dijkstra(s);
	for(int i = 1; i <=n; ++i) {
     
		cout << dis[i] << " ";
	}
	return 0;
}

链式前向星 + 单调队列 优化

//Luogu P4779 Dijkstra + priority_queue 链式前向星
#include
#include
#include
#define M 200005
#define N 100005
using namespace std;
int n, m, s;
int u, v, w;
struct Edge {
     
	int v, w, next;
} e[M];
int head[N], cnt=0;
struct Node {
     
	int u, d;
	bool operator <(const Node& x) const {
     
		return d > x.d;
	}
};
int dis[N];
void addedge(int u, int v, int w) {
     
	e[++cnt].v = v;
	e[cnt].w = w;
	e[cnt].next = head[u];
	head[u] = cnt;
}
void Dijkstra(int s) {
     
	for(int i = 1; i <= n; ++i) dis[i] = 2147483647;
	dis[s]=0;
	priority_queue<Node> q;
	q.push((Node){
     s,0});
	while(!q.empty()) {
     
		Node tmp = q.top();
		q.pop();
		int u = tmp.u, d = tmp.d;
		if(d != dis[u]) continue;
		for(int i = head[u]; i; i = e[i].next) {
     
			int v = e[i].v, w = e[i].w;
			if(dis[v] > dis[u] + w) {
     
				dis[v] = dis[u] + w;
				q.push((Node){
     v,dis[v]});
			}
		}
	}
}
int main() {
     
	scanf("%d%d%d",&n,&m,&s);
	for(int i = 1; i <= m; ++i) {
     
		scanf("%d%d%d",&u,&v,&w);
		addedge(u, v, w);
	}
	Dijkstra(s);
	for(int i = 1; i <= n; ++i) {
     
		printf("%d ",dis[i]);
	}
	return 0;
}

Bellman-Ford

//Bellman-Ford
#include
const int INF = 0x3f3f3f3f;
using namespace std;
int n, m;
int u[100], v[100], w[100];
int dis[100];
int main() {
     
	cin >> n >> m;
	for(int i = 1; i <= m; i++) {
     
		cin >> u[i] >> v[i] >> w[i];
	}
	for(int i = 1; i <= n; i++) {
     
		dis[i] = INF;
	}
	dis[1] = 0;
	for(int k = 1; k <= n - 1; k++) {
     
		for(int i = 1; i <= m; i++) {
     
			if(dis[v[i]] > dis[u[i]] + w[i]) {
     
				dis[v[i]] = dis[u[i]] + w[i];
			}
		}
	}
	for(int i = 1; i <= n; i++) {
     
		cout << dis[i] << " ";
	}
	return 0;
}
/*
5 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
*/

优化
如果在新一轮的松弛中数组 dis 没有发生变化,则可以提前跳出循环。

#include
const int INF = 0x3f3f3f3f;
using namespace std;
int n, m;
int u[100], v[100], w[100];
int dis[100];
int main() {
     
	cin >> n >> m;
	for(int i = 1; i <= m; i++) {
     
		cin >> u[i] >> v[i] >> w[i];
	}
	for(int i = 1; i <= n; i++) {
     
		dis[i] = INF;
	}
	dis[1] = 0;
	for(int k = 1; k <= n - 1; k++) {
     
		int check = 0; // 用来标记本轮松弛操作中数组 dis 是否会发生更新 
		for(int i = 1; i <= m; i++) {
     
			if(dis[v[i]] > dis[u[i]] + w[i]) {
     
				dis[v[i]] = dis[u[i]] + w[i];
				check = 1;
			}
		}
		if(check == 0) {
     
			break;
		}
	}
	int flag = 0;
	for(int i = 1; i <= m; i++) {
     
		if(dis[v[i]] > dis[u[i]] + w[i]) {
     
			flag = 1;
		}
	}
	if(flag == 1) {
     
		cout << "此图有负权回路\n";
	} else {
     
		for(int i = 1; i <= n; i++) {
     
			cout << dis[i] << " ";
		}
	}
	return 0;
}

队列优化:SPFA

即 Shortest Path Faster Algorithm。

关于SPFA

  • 它死了

知乎 - 如何看待 SPFA 算法已死这种说法?

//SPFA 链式前向星
#include
#include
#include
using namespace std;
const int N = 100005;
const int M = 200005;
int n, m;
int s, t;
struct Edge {
     
	int to, next, w;
} e[M];
int head[N], cnt;
int dis[N];
int In[N];
bool vis[N];
void add_edge(int u, int v, int w) {
     
	e[++cnt].to = v;
	e[cnt].next = head[u];
	e[cnt].w = w;
	head[u] = cnt;
}
bool Spfa(int s) {
     
	memset(vis,false,sizeof(vis));
	memset(dis,0x3f,sizeof(dis));
	memset(In,0,sizeof(In));
	queue<int> q;
	q.push(s);
	vis[s] = true;
	dis[s] = 0;
	while(!q.empty()) {
     
		int u = q.front();
		q.pop();
		vis[u] = false;
		for(int i = head[u]; i; i = e[i].next) {
     
			int v = e[i].to;
			if(dis[v] > dis[u] + e[i].w) {
     
				dis[v] = dis[u] + e[i].w;
				if(!vis[v]) {
     
					q.push(v);
					vis[v] = true;
					if(++In[v] > n) return false;
				}
			}
		}
	}
	return true;
}
int main() {
     
	cin >> n >> m;
	cin >> s >> t;
	for(int i = 1; i <= m; ++i) {
     
		int u, v, w;
		cin >> u >> v >> w;
		add_edge(u, v, w);
	}
	if(!Spfa(s)) {
     
		cout << "FAIL!" << endl;
	} else {
     
		cout << dis[t] << endl;
	}
	return 0;
}
/*
5 5
1 5
2 3 2
1 2 -3
1 5 5
4 5 2
3 4 3
*/
/*
2 2
1 2
1 2 1
2 1 -2
*/

其他优化

除了队列优化(SPFA)之外,Bellman-Ford 还有其他形式的优化,这些优化在部分图上效果明显,但在某些特殊图上,最坏复杂度可能达到指数级。

  • 堆优化:将队列换成堆,与 Dijkstra 的区别是允许一个点多次入队。在有负权边的图可能被卡成指数级复杂度。
  • 栈优化:将队列换成栈(即将原来的 BFS 过程变成 DFS),在寻找负环时可能具有更高效率,但最坏时间复杂度仍然为指数级。
  • LLL 优化:将普通队列换成双端队列,每次将入队结点距离和队内距离平均值比较,如果更大则插入至队尾,否则插入队首。
  • SLF 优化:将普通队列换成双端队列,每次将入队结点距离和队首比较,如果更大则插入至队尾,否则插入队首。

SPFA + DFS

//P3385 SPFA dfs (TLE on #9) [洛谷 - P3385 【模板】负环](https://www.luogu.com.cn/problem/P3385)
#include
#include
#include
using namespace std;
const int INF = 0x3f3f3f3f;
const int N = 2003;
const int M = 6003;
int T;
int n, m;
struct Edge {
     
	int to, next, w;
} e[M];
int head[N], cnt;
int dis[N];
bool instack[N];
bool flag;
void add_edge(int u, int v, int w) {
     
	e[++cnt].to = v;
	e[cnt].next = head[u];
	e[cnt].w = w;
	head[u] = cnt;
}
void Spfa(int u) {
     
	instack[u]=true;
	for(int i = head[u]; i; i=e[i].next) {
     
		int v = e[i].to;
		if(dis[v] > dis[u] + e[i].w) {
     
			dis[v] = dis[u] + e[i].w;
			if(!instack[v]) {
     
				Spfa(v);
			} else {
     
				flag = true;
				return;
			}
		}
	}
	instack[u]=false;
}
int main() {
     
	cin >> T;
	while(T--) {
     
		flag = false;
		memset(instack,false,sizeof(instack));
		memset(e,0,sizeof(e));
		memset(head,0,sizeof(head));
		cnt = 0;
		cin >> n >> m;
		for(int i = 1; i <= m; ++i) {
     
			int u, v, w;
			cin >> u >> v >> w;
			if(w >= 0) {
     
				add_edge(u, v, w);
				add_edge(v, u, w);
			} else {
     
				add_edge(u, v, w);
			}
		}
		for(int i = 1; i <= n; i++) {
     
			dis[i] = INF;
		}
		dis[1] = 0;
		Spfa(1);
		if(flag) {
     
			cout << "YES" << endl;
		} else {
     
			cout << "NO" << endl;
		}
	}
	return 0;
}

Johnson

新建一个虚拟节点(设它的编号为 0 0 0),从这个点向其他所有点连一条边权为 0 0 0 的边。
用 Bellman-Ford 算法求出从 0 0 0 号点到其他所有点的最短路,记为 h i h_i hi

假如存在一条从 u u u 点到 v v v 点,边权为 w w w 的边,则将该边的边权重新设置为 w + h u − h v w+h_u-h_v w+huhv

接下来以每个点为起点,跑 n n n 轮 Dijkstra 算法即可求出任意两点间的最短路了。

//P5905 Johnson [洛谷 - P5905 【模板】Johnson 全源最短路](https://www.luogu.com.cn/problem/P5905)
#include
#include
#include
#define INF 1e9
using namespace std;
const int N = 5005;
const int M = 10005;
struct edge {
     
	int v, w, next;
} e[M];
struct node {
     
	int dis, id;
	bool operator < (const node& a) const {
     
		return dis > a.dis;
	}
	node(int d, int x) {
     
		dis = d, id = x;
	}
};
int head[N], vis[N], t[N];
int cnt, n, m;
long long h[N], dis[N];
void add_edge(int u, int v, int w) {
     
	e[++cnt].v = v;
	e[cnt].w = w;
	e[cnt].next = head[u];
	head[u] = cnt;
}
bool spfa(int s) {
     
	queue<int> q;
	memset(h,63,sizeof(h));
	h[s] = 0, vis[s] = 1;
	q.push(s);
	while(!q.empty()) {
     
		int u = q.front();
		q.pop();
		vis[u] = 0;
		for(int i = head[u]; i; i = e[i].next) {
     
			int v = e[i].v;
			if(h[v] > h[u] + e[i].w) {
     
				h[v] = h[u] + e[i].w;
				if(!vis[v]) {
     
					vis[v] = 1;
					q.push(v);
					t[v]++;
					if(t[v] == n + 1) return false;
				}
			}
		}
	}
	return true;
}
void Dijkstra(int s) {
     
	priority_queue<node> q;
	for(int i = 1; i <= n; ++i) {
     
		dis[i] = INF;
	}
	memset(vis,0,sizeof(vis));
	dis[s] = 0;
	q.push(node(0,s));
	while(!q.empty()) {
     
		int u = q.top().id;
		q.pop();
		if(vis[u]) continue;
		vis[u] = 1;
		for(int i = head[u]; i; i = e[i].next) {
     
			int v = e[i].v;
			if(dis[v] > dis[u] + e[i].w) {
     
				dis[v] = dis[u] + e[i].w;
				if(!vis[v]) q.push(node(dis[v],v));
			}
		}
	}
	return ;
}
int main() {
     
	cin >> n >> m;
	for(int i = 1; i <= m; ++i) {
     
		int u, v, w;
		cin >> u >> v >> w;
		add_edge(u, v, w);
	}
	for(int i = 1; i <= n; ++i) {
     
		add_edge(0, i, 0);
	}
	if(!spfa(0)) {
     
		cout << -1 << endl;
		return 0;
	}
	for(int u = 1; u <= n; ++u) {
     
		for(int i = head[u]; i; i = e[i].next) {
     
			e[i].w += h[u] - h[e[i].v];
		}
	}
	for(int i = 1; i <= n; ++i) {
     
		Dijkstra(i);
		long long ans = 0;
		for(int j = 1; j <= n; ++j) {
     
			if(dis[j] == INF) ans += j * INF;
			else ans += j * (dis[j] + h[j] - h[i]);
		}
		cout << ans << endl;
	}
	return 0;
}

输出路径

开一个 pre 数组,在更新距离的时候记录下来后面的点是如何转移过去的。最后递归地输出路径即可。

比如 Floyd 就要记录 p r e [ i ] [ j ] = k pre[i][j] = k pre[i][j]=k,Bellman-Ford 和 Dijkstra 一般记录 p r e [ v ] = u pre[v] = u pre[v]=u

/*......*/
int pre[N];
/*......*/
void print_road(int s) {
     
	if(pre[s] != -1) {
     
		print_road(pre[s]);
	}
	else {
     
		return;
	}
	printf("->%d", s);
}
/*......*/
	memset(pre,-1,sizeof(pre));
/*......*/
	printf("%d",s);
	print_road(t);
	printf("\n");
/*......*/

参考

https://oi-wiki.org/graph/shortest-path/

你可能感兴趣的:(图论,算法,dijkstra,算法,SPFA,floyd,最短路)