算法学习笔记(25)——Dijikstra算法(单源最短路)
Dijkstra 算法
Dijkstra算法用于在所有边权都非负的图上,求单源点最短路。
设 \(n\) 是图上结点的数量,\(m\) 是边的数量。
- 朴素Dijkstra算法的时间复杂度是 \(O(n^2)\) ,适合稠密图(点少边多);
- 堆优化版的Dijkstra算法的时间复杂度是 \(O(m\log n)\) ,适合稀疏图(点多边少)。
如果是稠密图,那么边的数量 \(m\) 和点数的平方 \(n^2\) 基本是一个规模的。如果用堆优化版的Dijkstra,那么复杂度就可以视为 \(O(n^2\log n)\) ,这个时候是不如朴素版Dijkstra的。
如果是稀疏图,那么边的数量 \(m\) 和点的数量 \(n\) 基本是一个规模的,题目数据可能给到 \(10^5\) 。如果用朴素版的Dijkstra,那么规模就是 \(10^{10}\) 了,肯定会超时。
Dijkstra算法属于贪心算法。
朴素Dijkstra
例如求1号点到n号点的最短距离
- 初始化每个点到起点的距离。
dist[1]=0
表示1号点到起点(自己)的距离是0,其余的dist[i]=正无穷
。 - 集合
S[]
存储当前已经确定了最短距离的点,初始化为空集。 - 由于第一个我们默认最短距离为0,所以只需要对剩下未确定的n-1个点循环操作
- 每次找到不在集合中的距离最小的点,并将这个点加入到集合里面
- 用新加入的点
t
更新到其他点的最短距离。更新的原则是,如果从起点到某一点j
的距离dist[j]
大于从起点先到t
,再到该点的距离dist[t] + g[t][j]
,那么就更新该点到起点的最短距离。(新加入的点当作中介,看看经过他
#include <iostream>
#include <cstring>
using namespace std;
const int N = 510;
int n, m;
int g[N][N]; // 邻接矩阵存图
int dist[N]; // 存储每一点到起点的最短距离
bool st[N]; // 标记每一点是否确定了最短距离
void dijkstra()
{
// 初始化距离数组
memset(dist, 0x3f, sizeof dist);
// 初始化起点的距离
dist[1] = 0;
// 对剩下n-1个点循环操作
for (int i = 0; i < n - 1; i ++ ) {
// 找到不在集合中的距离最短的点
int t = -1;
for (int j = 1; j <= n; j ++ )
if (!st[j] && (t == -1 || dist[j] < dist[t]))
t = j;
// 将距离最短的点加入集合,确定了该点到起点的最短距离
st[t] = true;
// 利用新加入的点更新其他点到起点的距离
for (int j = 1; j <= n; j ++ )
dist[j] = min(dist[j], dist[t] + g[t][j]);
}
}
int main()
{
memset(g, 0x3f, sizeof g); // 图里的每条边都初始化为正无穷,表示不可达
cin >> n >> m;
for (int i = 0; i < m; i ++ ) {
int x, y, z;
cin >> x >> y >> z;
g[x][y] = min(g[x][y], z); //只存储最短的那一条边
}
dijkstra();
if (dist[n] == 0x3f3f3f3f) puts("-1"); // 如果n号点不可达,输出-1
else cout << dist[n] << endl;
return 0;
}
堆优化Dijkstra
如果是稀疏图,题目所给的节点数很大,使用朴素Dijkstra会超时(\(O(n^2)\)),所以需要考虑优化。
朴素Dijkstra的算法思想如下:
循环n - 1次
找一个结点t,是不在S中的距离最近的点
将t加入到S里
用t更新其它点的距离
通过分析,第一个操作是每次找出距离最近的点,这一步骤的时间复杂度是 \(O(n^2)\) ,第二个操作是将找出来的点加入确定了最小距离的点的集合,时间复杂度是 \(O(1)\) ,第三个操作是用t更新其他点的距离,本质上就是遍历了所有的边,所以时间复杂度是 \(O(m)\) 。
综上,主要的时间复杂的的瓶颈在于第一步找出最小值的操作,而在一堆数里面每次找出一个最小值,我们可以通过小根堆以 \(O(1)\) 的时间复杂度来实现,在 \(n-1\) 次循环总共就是 \(O(n)\) 的时间复杂度。
如果用堆存储每个点到起点的距离,则第三个更新到其他点的距离这一操作,实际上就是修改堆中一个元素,时间复杂度为 \(O(\log n)\) ,一共要修改 \(m\) 条边,所以总的时间复杂度是 \(O(m\log n)\) 。
所以堆优化版的Dijkstra算法的时间复杂度在于第三个步骤,与朴素版的Dijkstra有区别。所以堆优化Dijkstra算法的时间复杂度就是 \(O(m\log n)\)。
另外, 通过手写模拟堆来做,才能支持修改堆里的任意一个元素,但是手写堆很麻烦。
我们选择用STL的优先队列来做,但是优先队列不支持修改任意一个元素,这里使用的解决方案就是用冗余。即每次都直接加入新的元素,这样做的话,堆里总共的元素个数要达到 \(m\) 个,这样实现的堆优化Dijkstra算法的时间复杂度就变 \(O(m\log m)\)。
由于 \(m < n^2\) ,所以 \(m\log m < m\log(n^2) < 2m\log n\),它和 \(m\log n\) 实际也是一个级别的。另外因为是稀疏图,所以 \(m\) 实际远比 \(n^2\) 小,所以这个常数也比 \(2\) 小很多了,就是一点几,这样写方便而且速度也够。
由于这样做的堆里存在很多冗余,所以找到的最小值可能之前已经确定过了,这里就用st数组来判断,如果是已经确定过的,就把它扔掉再重新找就可以了。
#include <iostream>
#include <cstring>
#include <queue>
using namespace std;
const int N = 150010;
typedef pair<int, int> PII;
int n, m;
int h[N], e[N], ne[N], w[N], idx; // 带边权的邻接表
int dist[N]; // 距离数组
bool st[N]; // 标记数组
// 邻接表存储图加边
void add(int a, int b, int c)
{
e[idx] = b, ne[idx] = h[a], w[idx] = c, h[a] = idx ++;
}
void dijkstra()
{
// 初始化距离数组
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
// 堆中元素按第一关键字由小到大排序,按照{距离,点}的格式存储,即按边递增排序
priority_queue<PII, vector<PII>, greater<PII>> heap;
heap.push({0, 1});
// 枚举每一条边
while (heap.size()) {
// 取出堆顶元素,就是距离最近的点
auto t = heap.top();
heap.pop();
// 如果该点已经计算过最短距离,则跳过(冗余)
int ver = t.second, distance = t.first;
if (st[ver]) continue;
// 该点还没有确定最短距离,则将它加入集合S
st[ver] = true;
// 更新该点邻接的点的距离
for (int i = h[ver]; i != -1; i = ne[i]) {
int j = e[i];
if (dist[j] > dist[ver] + w[i]) {
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});
}
}
}
}
int main()
{
memset(h, -1, sizeof h);
cin >> n >> m;
for (int i = 0; i < m; i ++ ) {
int x, y, z;
cin >> x >> y >> z;
add(x, y, z);
}
dijkstra();
if (dist[n] == 0x3f3f3f3f) puts("-1");
else cout << dist[n] << endl;
return 0;
}