代码随想录第59天 | 最短路算法:dijkstra和Bellman_ford
目标:
了解原理,能照着代码写出来,理解即可。
最短路:
最短路是图论中的经典问题——
给出一个有向图,一个起点,一个终点,问起点到终点的最短路径
而求最短路径的算法是——
dijkstra算法:在有权图(权值非负数的基础上)中求从起点到其他节点的最短路径的算法
dijkstra朴素版
这个算法其实和prim算法很类似,都是贪心的思路,每次都找最近的节点。
下面是dijkstra算法的三部曲:
1. 选源点(开始节点)到哪个节点近且该节点未被访问过
2. 把这个最近节点(刚被选择过的)标记为——访问过
3. 更新非访问节点到源点的距离(即更新minDist数组)——这个更新的是谁与刚才选择的节点连接了,则谁就更新(更新是与之前的minDist元素比较,谁小选谁)
直到所有节点都被访问过,则说明到达终点。
minDist数组 用来记录 每一个节点距离源点(起始点)的最小路径距离
visited数组 用来记录每个节点是否被访问过
代码:
#include <iostream>
#include <vector>
#include <climits>//用于整型数据类型的极限值using namespace std;int main() {int n,m,p1,p2,val;cin>>n>>m;vector<vector<int>> grid(n + 1, vector<int>(n + 1, INT_MAX));//表示图,使用邻接矩阵的方式,因为边和点数量差不多for(int i=0;i<m;i++){cin>>p1>>p2>>val;grid[p1][p2] = val;}int start = 1;int end = n;//表示开始节点(源点)和结束节点//两个重要的数组,分别记录每个节点到源点的最短举例,每个节点是否被访问过vector<int> minDist(n + 1, INT_MAX);vector<bool> visited(n + 1, false);minDist[start] = 0; // 起始点到自身的距离为0,其余全设置为maxfor (int i=1;i<=n;i++) { // 遍历所有节点int minval = INT_MAX;//用于记录到源点最短的距离int cur = 1;//记录当前被访问的节点//步骤1,选择未被访问且最近的for (int v=1;v<=n;++v) {if (!visited[v] && minDist[v] < minval) {minval = minDist[v];cur = v;}}visited[cur] = true; // 步骤2,标记该节点为已访问// 步骤3,更新非访问节点(与cur连接的节点)到源点的距离(即更新minDist数组),更新需要是新的值与原来的值比较。for (int v = 1; v <= n; v++) {if (!visited[v] && grid[cur][v] != INT_MAX && minDist[cur] + grid[cur][v] < minDist[v]) {minDist[v] = minDist[cur] + grid[cur][v];}}}if (minDist[end] == INT_MAX) cout << -1 << endl; // 不能到达终点else cout << minDist[end] << endl; //输出结果}
dijkstra和prim算法的区别在于:minDist数组的含义不同,一个表示到达源点的最小路径距离,一个表示到达生成树的最小路径
其次prim可以用于负值的权值,且不涉及单一路径。
dijkstra(堆优化版)精讲
dijkstra(堆优化版)精讲
朴素版时间复杂度:O(n²)
-
当 n = 10⁵ 时,n² = 10¹⁰,会超时
-
主要瓶颈:每次都要遍历所有节点找最小值
那优化就是:用堆来快速完成"选择最近节点"这一步
1. 选择邻接表而非邻接矩阵
2. 两个数组minDist和visited不变
3. 引入优先队列——什么是优先队列呢?
priority_queue<PII, vector<PII>, greater<PII>> pq; pq.push({0, start}); // {距离, 节点}优先队列(小顶堆)是——自动按从小到大排序,总是取出最小的
优化三部曲:
while 循环开始(对应朴素版的 for 循环)用于遍历选择
步骤1:选择最近的未访问节点
auto [minDist, cur] = pq.top(); // 堆顶就是当前最小距离节点
pq.pop();if (visited[cur]) continue; // 如果已经访问过就跳过 对比朴素版:
-
朴素版:遍历n个节点找最小值 → O(n)
-
堆优化:直接取堆顶 → O(1)
步骤2:标记节点为已访问
visited[cur] = true; // 完全相同! 步骤3:更新邻接节点的距离
for (auto [neighbor, weight] : graph[cur]) {if (!visited[neighbor]) {int newDist = dist[cur] + weight;if (newDist < dist[neighbor]) {dist[neighbor] = newDist;pq.push({newDist, neighbor}); // 关键:把更新后的节点加入堆}}
} 对比朴素版:
-
朴素版:遍历所有n个节点
-
堆优化:只遍历当前节点的邻居(更高效)
示例:
-
节点3第一次加入堆:距离=5
-
后来发现更短路径,节点3再次加入堆:距离=3
-
当从堆中取出距离=3的节点3时,标记为已访问
-
之后如果取出距离=5的节点3,直接跳过(因为已经有更优解)
代码
#include <iostream>
#include <vector>
#include <queue>
#include <climits>using namespace std;typedef pair<int, int> PII; // {节点编号, 距离}int main() {int n, m, p1, p2, val;cin >> n >> m;// 邻接表存储:graph[p1] = {邻居节点, 权重}vector<vector<PII>> graph(n + 1);for (int i = 0; i < m; i++) {cin >> p1 >> p2 >> val;graph[p1].push_back({p2, val}); // {邻居节点, 权重}}int start = 1, end = n;vector<int> dist(n + 1, INT_MAX);vector<bool> visited(n + 1, false);// 优先队列:{距离, 节点} ← 注意:这里要按距离排序,所以距离放前面!!priority_queue<PII, vector<PII>, greater<PII>> pq;dist[start] = 0;pq.push({0, start}); // {距离, 节点} ← 修正这里!while (!pq.empty()) {// 步骤1:取堆顶auto [minDist, cur] = pq.top(); // 现在第一个是距离,第二个是节点pq.pop();if (visited[cur]) continue;// 步骤2:标记为已访问visited[cur] = true;// 步骤3:更新邻居for (auto [neighbor, weight] : graph[cur]) { // {邻居节点, 权重}if (!visited[neighbor]) {int newDist = dist[cur] + weight;if (newDist < dist[neighbor]) {dist[neighbor] = newDist;pq.push({newDist, neighbor}); // {距离, 节点}}}}}if (dist[end] == INT_MAX) cout << -1 << endl;else cout << dist[end] << endl;
} Bellman_ford 算法精讲(权值为负的最短路径
Bellman_ford 算法精讲
这个算法解决:权值存在负时的单源最短路问题
思路是:对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路
⭐基本概念:松弛操作
什么是松弛操作呢?对于节点v而言,有多个路径可以到达,若从u节点通过权值为weight的边到达v节点,则其dist需要和v节点之前的dist比较,谁更小选择谁。
//核心操作
if (dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;
} 意思:如果通过u到达v比当前已知的到v的路径更短,就更新
松弛操作的注意事项:
对所有的边进行n-1轮松弛操作(n为节点数)
第一轮松弛后:确定了从起点出发,经过最多1条边能到达的所有节点的最短距离
第二轮松弛后:确定了从起点出发,经过最多2条边能到达的所有节点的最短距离
第k轮松弛后:确定了从起点出发,经过最多k条边能到达的所有节点的最短距离
示例:
1 → 2 (权重1) 1 → 4 (权重10) 2 → 3 (权重1) 3 → 4 (权重1)
第1轮松弛:
1→2: dist[2] = 1 1→4: dist[4] = 10 2→3: dist[3] = 2 3→4: 还不能更新,因为dist[3]还没确定第1轮后:
dist = [0, 1, 2, 10]第2轮松弛:
3→4: dist[3]+1=3 < 10 → dist[4] = 3 ← 发现更短路径!第2轮后:
dist = [0, 1, 2, 3]看!节点4在第1轮是10,第2轮才找到更优的3
——⭐⭐第一轮找到的是从开始节点出发,只经过1条边,到达所有节点的最短距离⭐⭐
⭐则从上推测——需要n-1轮来确认从开始节点到达所有节点的最短路径(无论经过多少边)⭐
-
在最坏情况下,最短路径最多包含n-1条边
-
每轮松弛至少确定一个节点的最短路径
算法详细步骤
第一步:初始化
vector<int> dist(n + 1, INT_MAX);
dist[start] = 0; // 起点到自己的距离为0 第二步:进行n-1轮松弛
for (int i = 1; i <= n - 1; i++) {for (每条边(u, v, weight)) {if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {dist[v] = dist[u] + weight;}}
} 第三步:检测负权环(可选)
for (每条边(u, v, weight)) {if (dist[u] != INT_MAX && dist[u] + weight < dist[v]) {// 存在负权环!}
} 完整代码实现
#include <iostream>
#include <vector>
#include <climits>using namespace std;struct Edge {int from;int to;int weight;
};int main() {int n, m;cin >> n >> m;vector<Edge> edges;for (int i = 0; i < m; i++) {int u, v, w;cin >> u >> v >> w;edges.push_back({u, v, w});}int start = 1, end = n;vector<int> dist(n + 1, INT_MAX);dist[start] = 0;// 进行n-1轮松弛for (int i = 1; i <= n - 1; i++) {//n-1轮for (auto& edge : edges) {//遍历边!!!!int u = edge.from;//从uint v = edge.to;//到vint w = edge.weight;//权值为w// 松弛操作if (dist[u] != INT_MAX && dist[u] + w < dist[v]) {//更新v为最小的,且前提是u不算maxdist[v] = dist[u] + w;}}}if (dist[end] == INT_MAX) {cout << "unconnected" << endl; // 不可达} else {cout << dist[end] << endl;}
} 实例演示
示例1:正常情况
输入: 4 5 1 2 2 1 3 4 2 3 1 2 4 7 3 4 3起点:1,终点:4
执行过程:
-
初始化:
dist = [0, max, max, max] -
第1轮松弛后:
dist = [0, 2, 4, max] -
第2轮松弛后:
dist = [0, 2, 3, 6](通过2->3更新了3) -
第3轮松弛后:
dist = [0, 2, 3, 6](无变化)
结果:6
示例2:含负权边
输入: 3 3 1 2 3 1 3 5 2 3 -2起点:1,终点:3
执行过程:
-
初始化:
dist = [0, max, max] -
第1轮:
dist = [0, 3, 5] -
第2轮:通过2->3(-2)更新:
dist = [0, 3, 1]
结果:1(比直接1->3的5更短)
有关生成树和最小路径算法选择指南
根据图类型选择:
稠密图(边数 ≈ n²):
-
使用朴素版 Dijkstra
-
时间复杂度:O(n²)
稀疏图(边数 ≈ n):
-
使用堆优化版 Dijkstra
-
时间复杂度:O((n+m)logn)
根据问题需求选择:
单源最短路径:
-
正权图:Dijkstra(朴素/堆优化)
-
负权图:Bellman-Ford / SPFA
所有点对最短路径:
-
Floyd 算法
最小生成树:
-
稠密图:Prim
-
稀疏图:Kruskal
