常用的最短路径问题方法总结如下:
1.Dijkstra算法
适用场景:单源最短路—要求没有负权边
Dijkstra本质是贪心的思想,每次选择当前到起点距离最短的边。包括朴素Dijkstra和堆优化版的Dijkstra
朴素Dijkstra适用于稠密图(边的数量远大于结点数量),堆优化版的Dijkstra适用于稀疏图
Dijkstra算法演示流程:
假设有如下的图,起始结点为a,设结果集和为s,初始时s为空,当最终所有的结点都加入到s中时,算法结束
Dijskstra需要维护一个距离数组dist[N],表示某个点到起点的最短距离,初始状态如下:
a | b | c | d | e | f |
+∞ | +∞ | +∞ | +∞ | +∞ | +∞ |
首先更新起点,将起点a的dist数组的值更新为0,并将起点a加入到结果集和s,更新后的dist数组状 态如下:
a | b | c | d | e | f |
0 | +∞ | +∞ | +∞ | +∞ | +∞ |
由集和s内的点,更新其它点到起点的距离,与a相邻的点有b和d,最短距离分别为6和4,选择最短的距离的点d加入结果集和,更新dist数组
a | b | c | d | e | f |
0 | +∞ | +∞ | 4 | +∞ | +∞ |
此时结果集和内有a和d,再用这两个点更新其它点到起点的距离,起点可以到的点有b,f,c,最短距离分别为6,7,5,选择c加入结果集合,更新dist数组
a | b | c | d | e | f |
0 | +∞ | 5 | 4 | +∞ | +∞ |
此时s内有a,c,d,起点可以到的点有b,f,e,最短距离分别为6,7,12,将b加入结果集和s
a | b | c | d | e | f |
0 | 6 | 5 | 4 | +∞ | +∞ |
此时s内有a,b,c,d,起点可以到的点有e和f,最短距离分别为12和7,将f加入结果集合s
a | b | c | d | e | f |
0 | 6 | 5 | 4 | +∞ | 7 |
此时s内有a,b,c,d,起点可以到的点有e,最短距离为9,将e加入结果集和s
a | b | c | d | e | f |
0 | 6 | 5 | 4 | 9 | 7 |
此时所有结点已全部加入集和s,更新完成
时间复杂度分析:
Dijstra算法包括:
for i 1→ n 遍历n次(假设有n个点),每次将一个点加入结果集和s
for j 1→n 找到当前距离起点最近的结点加入集和
for k 1→n 利用新加入的结点更新起点到其他所有结点的距离
所以朴素Dijstra的时间复杂度为O(2n²)=O(n²)
堆优化版的Dijkstra主要对寻找最近的结点这一步进行优化,采用最小堆(优先队列)来提高时间效率
下面是两道Dijkstra算法的问题,分别基于朴素Dijkstra和对堆优化版的Dijkstra
朴素Dijkstra
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤10^5,
图中涉及边长均不超过10000。
输入样例
3 3
1 2 2
2 3 1
1 3 4
输出样例
3
代码
#include<iostream>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;
const int N = 510;
int n, m;
int g[N][N];
int dist[N];
bool st[N];
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for(int i = 0; i < n; i ++){
int min_id = -1;
for(int j = 1; j <= n; j ++){
//寻找到起始点最短距离的点, 加入结果集和
if(!st[j] && (min_id == -1 || dist[j] < dist[min_id]))
min_id = j;
}
st[min_id] = true;
for(int j = 1; j <= n; j ++){
dist[j] = min(dist[j], dist[min_id] + g[min_id][j]);
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(g, 0x3f, sizeof g);
for(int i = 0; i < m; i ++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
g[x][y] = min(g[x][y], z); //有重边的情况,只保留两个结点间的最短边
}
cout << dijkstra() << endl;
return 0;
}
说明
题目中n和m的范围可以看出这道题是稠密图
dist数组维护起点到每个点的最短距离,st数组记录每个结点是否被选过,防止在更新最近距离点的时候更新到已选过的节点
需要注意的是,这道题可能会有重边和还,处理方式是环不考虑(无负权边),重边只保留最短的那条边
同时要注意memset的用法(按字节赋值)
堆优化版的Dijkstra
问题描述
给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。
输入格式
第一行包含整数 n 和 m。
接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1 号点到 n 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n,m≤1.5×10^5,
图中涉及边长均不小于 00,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 10^9。
输入样例
3 3
1 2 2
2 3 1
1 3 4
输出样例
3
代码
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using namespace std;
typedef pair<int, int> PII;
const int N = 150010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
bool st[N];
void add(int a, int b, int c){
e[idx] = b;
w[idx] = c;
ne[idx] = h[a];
h[a] = idx ++;
}
int dijkstra(){
memset(dist, 0x3f, sizeof dist);
priority_queue<PII, vector<PII>, greater<PII>> heap;
dist[1] = 0;
heap.push({0, 1});
while(heap.size()){
auto t = heap.top();
heap.pop();
int distance = t.first, id = t.second;
if(st[id]) continue;
st[id] = true;
for(int i = h[id]; i != -1; i = ne[i]){
int j = e[i];
if(dist[j] > distance + w[i]){
dist[j] = distance + w[i];
heap.push({dist[j], j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main(){
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
for(int i = 0; i < m; i ++){
int x, y, z;
scanf("%d%d%d", &x, &y, &z);
add(x, y, z);
}
cout << dijkstra() << endl;
return 0;
}
说明
C++中小根堆和大根堆的定义如下:
priority_queue<int> maxHeap; // 大根堆
priority_queue<int, vector<int>, greater<int>> minHeap; // 小根堆
这里采用pair存储的原因是,对于一个点的所有路径可能都会存入堆中,默认取堆顶到该点的最短路径,其他的自动忽略即可,因为堆不涉及删除、覆盖操作,没有像朴素Dijkstra一样先筛选最短的加入集和,而是所有可行的路径全加入堆中(pair默认按第一个值排序)
时间复杂度:O(mlogn)