最短路径算法总结(floyd,dijkstra,bellman-ford)
继续复习数据结构和算法,总结一下求解最短路径的一些算法。
弗洛伊德(floyd)算法
弗洛伊德算法是最容易理解的最短路径算法,可以求图中任意两点间的最短距离,但时间复杂度高达\(O(n^3)\),主要思想就是如果想缩短从一个点到另一个点的距离,就必须借助一个中间点进行中转,比如A点到B点借助C点中转的话AB的距离就可以更新为\(D(a,b)=Min(D(a,b),D(a,c)+D(c,b))\),这样我们用每一个结点作为中转结点,尝试对另每两个结点进行距离更新,总共需要三层循环进行遍历。
核心代码如下,图存储在邻接矩阵G中。
for (int i = 0; i < n; i++) { for (int j = 0; j < n; j++) { for (int k = 0; k < n; k++) { G[j][k] = min(G[j][k], G[j][i] + G[i][k]); } } }
迪杰斯特拉(Dijkstra)算法
迪杰斯特拉算法是一种求解单源最短路径的算法,给定一个结点,可以求出图上各个结点到该结点最短距离。
没学过的话推荐看看这个视频:https://www.bilibili.com/video/av21376839?p=13,从8分钟开始。看完之后基本上就明白了Dijkstra算法的运行过程。总结一下就是不断寻找离源点最近的点并将其作为新的源点去更新其他点到目标点的距离。
代码如下,nowIndex代表当前源点编号,minDis是当前源点到其他点的最短距离,用于选择下一个源点,dis数组存储每个点到最终目标点的距离,也就是结果,mark数组用于标记结点是否被当作源点过。
#include<iostream> #include<algorithm> using namespace std; #define inf 100000000 int G[10][10]; int dis[10]; bool mark[10]; int n, m; void dijkstra(int nowIndex) { mark[nowIndex] = true; for (int i = 1; i <= n; i++)//先将跟源点直接相连的结点更新一遍 dis[i] = min(dis[i], G[nowIndex][i]); for (int i = 1; i < n; i++)//循环n-1次,因为源点已经更新过了 { int minDis = inf; for (int j = 1; j <= n; j++)//找离当前源点最近的点 { if (!mark[j] && dis[j] < minDis) { minDis = dis[j]; nowIndex = j; } } mark[nowIndex] = true; for (int j = 1; j <= n; j++)//用当前源点去更新 dis[j] = min(dis[j], dis[nowIndex] + G[nowIndex][j]); } } int main() { cin >> n >> m;//输入顶点数和边数 int u, v, w; for (int i = 1; i <= n; i++) for (int j = 1; j <= n; j++) if (i != j) G[i][j] = inf; else G[i][j] = 0; for (int i = 1; i <= n; i++) dis[i] = inf; for (int i = 0; i < m; i++) { cin >> u >> v >> w;//输入无向边 G[u][v] = w; G[v][u] = w; } dijkstra(1);//以1号结点为源点 for (int i = 1; i <= n; i++) { cout << dis[i] << ' '; } return 0; }
邻接表实现
使用邻接表存储图能大大降低空间复杂度,代码如下:
#include<iostream> #include<algorithm> using namespace std; #define inf 100000000 #define maxN 10000 int value[maxN], to[maxN], nextL[maxN]; int head[maxN], total; int dis[maxN]; bool mark[maxN]; int n, m; void dijkstra(int nowIndex) { for (int i = 0; i <= n; i++)dis[i] = inf; dis[nowIndex] = 0; mark[nowIndex] = true; for (int i = head[nowIndex]; i; i = nextL[i]) { dis[to[i]] = min(dis[to[i]], dis[nowIndex] + value[i]); } for (int i = 1; i < n; i++)//循环n-1次,因为源点已经更新过了 { int minDis = inf; for (int j = 1; j <= n; j++)//找离当前源点最近的点 { if (!mark[j] && dis[j] < minDis) { minDis = dis[j]; nowIndex = j; } } mark[nowIndex] = true; for (int j = head[nowIndex]; j; j = nextL[j]) { dis[to[j]] = min(dis[to[j]], dis[nowIndex] + value[j]); } } } void AddLine(int a, int b, int c) { total++; to[total] = b; value[total] = c; nextL[total] = head[a]; head[a] = total; } int main() { cin >> n >> m; for (int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; AddLine(a, b, c); } dijkstra(1); for (int i = 1; i <= n; i++) cout << dis[i] << " "; return 0; }
堆优化
普通的Dijkstra时间复杂度为\(O(n^2)\),但可以通过优化达到\(O(nlogn)\),注意在上面的循环中我们每次都要取出离当前源点最近的点,所以可以用优先级队列来优化。每次搜索将修改过dis的点进队,然后每次取队首就是最近的点。
代码如下:
#include<iostream> #include<algorithm> #include<queue> #include<vector> using namespace std; #define inf 100000000 #define maxN 10010 int s; int value[500001], to[500001], nextL[500001]; int head[maxN], total; int dis[maxN]; bool mark[maxN]; int n, m; typedef pair<int, int> disID; priority_queue<disID,vector<disID>,greater<disID>> q; void dijkstra(int nowIndex) { for (int i = 0; i <= n; i++)dis[i] = inf; dis[nowIndex] = 0; q.push(disID(0, nowIndex)); while (!q.empty()) { int t = q.top().second; q.pop(); if (mark[t])continue; mark[t] = true; for (int i = head[t]; i ; i=nextL[i]) { if (dis[to[i]] > dis[t] + value[i]) { dis[to[i]] = dis[t] + value[i]; q.push(disID(dis[to[i]], to[i])); } } } } void AddLine(int a, int b, int c) { total++; to[total] = b; value[total] = c; nextL[total] = head[a]; head[a] = total; } int main() { cin >> n >> m >> s; for (int i = 1; i <= m; i++) { int a, b, c; cin >> a >> b >> c; AddLine(a, b, c); } dijkstra(s); for (int i = 1; i <= n; i++) { cout << dis[i] << ' '; } return 0; }
Bellman-ford算法
上面的Dijkstra算法存在一个问题就是不能处理存在负权边的情况,只要有边的权值是负数就不能用,这时可以用Bellman-ford算法解决。
Bellman-ford算法的思想是这样的,我们将每条边的起点、权值、终点存储为三个数组from[i],val[i],to[i],然后扫描每一条边,看能不能通过走这条边来使dis[to[i]]减少。
代码很简单如下:
#include<iostream> #include<algorithm> #include<queue> #include<vector> using namespace std; #define inf 100000000 int from[10000], val[10000], to[10000]; int dis[10000]; int n, m; void Bellman_ford(int u) { for (int i = 0; i <= n; i++)dis[i] = inf; dis[u] = 0; while (true) { bool update = false; for (int i = 1; i <= m; i++) { if (dis[from[i]] != inf && dis[to[i]] > dis[from[i]] + val[i]) { dis[to[i]] = dis[from[i]] + val[i];//更新 update = true; } } if (!update)break;//直到每一条边都不能使dis减少 } } int main() { cin >> n >> m ; for (int i = 1; i <= m; i++) { cin >> from[i] >> to[i] >> val[i]; } Bellman_ford(1); for (int i = 1; i <= n; i++) cout << dis[i] << ' '; return 0; }
算法中的while循环最多循环n-1次,所以Bellman-ford的时间复杂度是\(O(mn)\),不仅能处理负权边,而且在稀疏图(顶点数远多于边数)当中比Dijkstra快。