图的最短路径算法
1.不带权值的最短路径
对于不带权值的最短路径而言,我们可以采用广度优先遍历的方法,同时在遍历的过程中记录其上一个节点即可。如下图所示,我们找寻从 A 顶点到 H 顶点的最短路径:

从上图中可以看到,在广度优先遍历到第 2 层时,已经找到了 H 节点,此时直接返回即可。
2.Dijkstra算法
迪杰斯特拉(Dijkstra)算法是典型的单源最短路径算法,用于计算一个节点到其它所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。
算法思想:设 是一个带权有向图,把图中顶点集合 V 分成两组,第一组为已求出最短路径的顶点集合(用 S 表示,初始时 S 中只有一个源点,以后每求得一条最短路径,就将加入到集合 S 中,直到全部顶点都加入到 S 中,算法就结束了),第二组为其余未确定最短路径的顶点集合(用 U 表示),按最短路径长度的递增次序依次把第二组的顶点加入 S 中。在加入的过程中,总保持从源点 v 到 S 中各顶点的最短路径长度不大于从源点 v 到 U 中任何顶点的最短路径长度。此外,每个顶点对应一个距离,S 中的顶点的距离就是从 v 到此顶点的最短路径长度,U 中的顶点的距离,就是从 v 到此顶点只包括 S 中的顶点为中间顶点的当前最短路径长度。
Dijkstra 算法其实是贪心算法在单源最短路径问题上的典型应用场景。
其算法步骤如下:
- 初始时,S 只包含源点,即 ,v 的距离为 0。U 包含除 v 外的其它顶点,即 ,若 v 与 U 中顶点 u 有边,则 正常有权值,若 u 不是 v 的出边邻接点,则 权值为 ;
- 从 U 中选取一个距离 v 最小的顶点 k,把顶点 k 加入到 S 中(该选定的距离就是 v 到 k 的最短路径长度);
- 以 k 为新的中间点,修改 U 中各顶点的距离,若从源点 v 到顶点 u 的距离(经过顶点 k)比原来距离(不经过顶点 k)短,则修改顶点 u 的距离值,修改后的距离值的顶点 k 的距离加上边上的权;
- 重复步骤 2 和 3 直到所有顶点都包含在 S 中;
例如,现在对于如下图所示的有权无向图:

开始时,以 A 点为源点,此时集合 S 中只有 A,且最短路径为 A->A=0,然后以 A 为中间点,从 A 开始找最短路径。而集合 U 则包含 B、C、D、E、F,其中,A->B=6,A->C=3,而到其余顶点的距离则为 ,可以看到 A->C=3 的权值最小:

于是将顶点 C 加入到集合 S 中,并以顶点 C 作为新的源点,此时集合 U 中包含 B、D、E、F,由于 A->C->B=3+2=5,其值要小于 A->B=6,因此更新顶点 B 的路径,同样地 A->C->D=3+3=6,A->C->E=3+4=7,A->C->D=3+3=6,可以看到,A->C->B 的权值最小:

选取顶点 B 加入到集合 S 中,然后将顶点 B 作为新的源点,此时集合 U 中包含 D、E、F,由于 A->C->B->D=3+2+5=10,其值要大于上一步中的 A->C->D=3+3=6,因此 D 值不变,而顶点 B 到顶点 E、F 的距离为 ,因此也不更新。可以看到 A->C->D 的权值最小:

选取顶点 D 加入到集合 S 中,并以顶点 D 作为新的源点,此时集合 U 中包含 E 和 F,由于 A->C->D->E=3+3+2=8,大于第二步中的 A->C->E=3+4=7,而 A->C->D->F=3+3+3=9。可以看到,A->C->E 的权值最小:

选取顶点 E 加入到集合 S 中,并将顶点 E 作为新的源点,此时集合 U 中仅剩下顶点 F,由于 A->C->E->F=3+4+5=12,大于第四步中的 A->C->D->F=3+3+3=9,可以看到 A->C->D->F 的权值最小:

将顶点 F 加入到集合 S 中,此时集合 U 已空,查找完毕:

注意,该算法无法处理负权变,有可能无法得到最短的路径,比如对于如下图所示的有权无向图:

开始时,选取 A 顶点作为源点,由于 A->B=4,A->C=6,所以选择顶点 B 加入到集合 S 中,但是 A->C->B=6-3=3,其值要小于 A->B=4,所以开始选取的 A->B 并非为最短路径。
3.Floyd算法
Floyd 算法又称为插点法,是一种利用动态规划算法的思想寻找给定的加权图中多源点之间最短路径的算法,其主要思想是:
- 从第 1 个点到第 n 个点依次加入图中,每个点加入后进行试探是否有路径长度被更改。具体方法为遍历图中每一个点(通过 i,j 双重循环),判断每一个点对距离是否因为加入的点而发生最小距离变化。如果发生改变,更新两点(i,j)的距离。
- 重复上述直到最后插点试探完成。
其中更新距离的状态转移方程为:
其中 的意思可以理解为 x 到 y 的最短路径。 为 i 到 k 的最短路径, 为 k 到 j 的最短路径。
例如,对于如下图所示的带权无向图:

将其转化为邻接矩阵后如下所示,其中 INF 表示两点间没有直接相连的路径:

首先,我们选择加入 A 顶点,那么就需要遍历邻接矩阵,看其他点经过 A 顶点中转后的距离能否小于其直接到达另一个点的距离。注意,下图中的绿色部分是不需要进行变化的,一方面对角线元素是自己到自己的距离,而除去对角线元素的其它绿色部分是 A 顶点无需中转而直接到达其它顶点的最短路径,所以,需要观察的只有其中的橙色部分:

那么对于以顶点 B 为起点的路径而言,如果存在更短的路径,则进行更新:
- B->A->C=6+3=9 > B->C=2
- B->A->D=INF > B->D=5
- B->A->E=INF = B->E=INF
- B->A->F=INF = B->F=INF
因此上述邻接矩阵中 B 行是无需发生任何改变的。然后以同样的方式观察其余的 C、D、E、F 顶点,完成后,再加入 B 顶点,以 B 顶点为中转顶点,重复上述过程即可。
所以,Floyd 算法的时间复杂度为 ,但是相比于 Dijkstra 算法而言,它可以用来计算负权值的最短路径。
4.代码实现
4.1 不带权值的最短路径
利用广度优先遍历实现不带权值的最短路径算法的代码实现如下:
void shortPath(int start, int end) { vector<bool> visited(vertics.size(), false); queue<int> que; // 记录顶点在遍历过程中的前后遍历关系 vector<int> path(vertics.size(), 0); que.push(start); visited[start] = true; while (!que.empty()) { int cur_no = que.front(); if (cur_no == end) { // 找到end末尾节点 break; } que.pop(); for (auto no : vertics[cur_no].adjList_) { if (!visited[no]) { que.push(no); visited[no] = true; // 当前节点处,记录是从哪一个节点过来的 path[no] = cur_no; } } } if (!que.empty()) { // 存在一条最短路径 while (end != 0) { cout << vertics[end].data_ << " <= "; end = path[end]; } } else { cout << "不存在有效的最短路径!" << endl; } cout << endl; }
4.2 Dijkstra算法实现
Dijkstra 算法的实现如下:
int dijkstra(const vector<vector<uint>>& graph, int start, int end) { // 存储各个顶点的最短路径 vector<uint> dis(graph.size(), 0); vector<bool> used(graph.size(), false); used[start] = true; // 初始化start到其它U集合顶点权值 for (int i = 0; i < graph.size(); ++i) { dis[i] = graph[start][i]; } for (int i = 1; i < graph.size(); ++i) { // 从U集合选择权值最小的顶点 int idx = -1; int min = INT_MAX; for (int j = 0; j < graph.size(); ++j) { if (!used[j] && min > dis[j]) { min = dis[j]; idx = j; } } if (idx == -1) { break; } // 将选择的顶点加入到S集合中 used[idx] = true; // 更新U集合中剩下顶点的权值信息 for (int j = 0; j < graph.size(); ++j) { if (!used[j] && min + graph[idx][j] < dis[j]) { dis[j] = min + graph[idx][j]; } } } return dis[end]; }
4.3 Dijkstra算法优化
因为在每一次都需要遍历 U 集合,找寻权值最小的点,因此,可以使用小根堆数据结构来进行优化,其代码实现如下:
using MinHeap = priority_queue<pair<uint, int>, vector<pair<uint, int>>, greater<pair<uint, int>>>; int dijkstra_plus(const vector<vector<uint>>& graph, int start, int end) { // 存储各个顶点的最短路径 vector<uint> dis(graph.size(), 0); vector<bool> used(graph.size(), false); MinHeap que; used[start] = true; // 初始化start到其它U集合顶点权值 for (int i = 0; i < graph.size(); ++i) { dis[i] = graph[start][i]; // 将除start顶点之外的其余顶点全部放入到小根堆中 if (i != start) { que.emplace(graph[start][i], i); } } while (!que.empty()) { // 从U集合选择权值最小的顶点 auto elem = que.top(); que.pop(); if (elem.first == INT_MAX) { break; } int idx = elem.second; int min = elem.first; if (used[idx]) continue; // 将选择的顶点加入到S集合中 used[idx] = true; // 更新U集合中剩下顶点的权值信息 for (int j = 0; j < graph.size(); ++j) { if (!used[j] && min + graph[idx][j] < dis[j]) { dis[j] = min + graph[idx][j]; que.emplace(dis[j], j); } } } return dis[end]; }
4.4 Floyd算法实现
Floyd 算法的实现如下:
void floyd(vector<vector<uint>>& graph) { // 依次加入每个点 for (int k = 0; k < graph.size(); ++k) { // 遍历邻接矩阵 for (int i = 0; i < graph.size(); i++) { for (int j = 0; j < graph.size(); j++) { graph[i][j] = min(graph[i][j], graph[i][k] + graph[k][j]); } } } }
本文作者:鼠键不离 码不停蹄
本文链接:https://www.cnblogs.com/tuilk/p/17136936.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步