堆优化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×105,
图中涉及边长均不小于 0,且不超过 10000。
数据保证:如果最短路存在,则最短路的长度不超过 109。输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3 -
题目来源:https://www.acwing.com/problem/content/852/
题目分析:
- 点数边数同量级,稀疏图,邻接表
- 单源最短路径 + 正边权 = Dijkstra
- 朴素Dijkstra时间复杂度O(2·n2)达到1010
- 下面介绍堆优化Dijkstra算法
大家可以猜猜,朴素Dijkstra算法中哪一步可以优化?
算法原理:
模板算法:
- 传送门:朴素Dijkstra
堆优化Dijkstra:
1. 优化哪里?
-
回顾朴素Dijkstra,三大步骤:择点 & 松弛 & 循环迭代
-
必然是择点
从所有不在确定集s[N]中的点中选择距离单源最近的点
使用小根堆存储所有点到单源的距离,就可以O(1)找到最近的点
-
堆的节点:
由于择点目的还是找到一个图中节点,所以堆本身除了存储距离,还需要存储距离对应的节点,这一点可以使用PII 的堆实现
-
入堆:
由于dis[N]初始为无穷,依靠入st[N]的节点来更新其余节点到单源的距离
所以下一次的最近点必然在这一轮中发生dis[]变短的节点中
故每次只需要将dis[]变短的节点加入堆中即可
2. 存储形式:
- 图:邻接表,邻接表五件套:idx h[节点] val[idx] ne[idx] memset(h, -1, sizeof(h));外加边长数组w[idx]
- 堆:堆本身为STL的priority_queue<>
堆中节点为pair<int, int> - 确定集:st[N]
- 距离集:dis[N]
- 初始化:
图初始设定不连通,为无穷
确定集无元素,为0
确定集无元素,dis[N]为无穷
3. 三大步骤:
- 其实和朴素Dijkstra一样,只不过将择点的O(n)换到了O(1)
- 择点
- 松弛
- 循环迭代
4. 重边 & 自环:
- 非负自环不影响结果,况且本题所有边都是正数
- 重边亦不影响结果
所有以节点i作为起点,经过边到达的终点j都加入了h[i]拉开的邻接表中
dis[j] = min(dis[j], dis[j]+w[i->j]);
纵使有很多w[i->j],dis[j]只选取其中最短的一条
5. 时间复杂度:
- 循环重数:n轮,不变
- 择点:查找O(1),出堆O(logn)
- 松弛:O(1),n个点,n条边,每个点平均只有一个出边
- 总的时间负责度O(n logn),不到1.5*105*20 == 3*106,cpp1s内107~108
- 看出来了吗?前提是稀疏图,边数等于点数
代码实现:
#include <iostream>
#include <cstring>
#include <queue>
#include <algorithm>
using namespace std;
const int N = 150010;
int n, m;
int h[N], val[N*2], ne[N*2], w[N*2], idx;
void insert(int x, int y, int z){
val[idx] = y;
w[idx] = z;
ne[idx] = h[x];
h[x] = idx++;
}
bool st[N];
int dis[N];
typedef pair<int, int> PII;
int dijkstra(int start){
memset(dis, 0x3f, sizeof(dis));
priority_queue<PII, vector<PII>, greater<PII>> que;
que.push({0, start});
dis[start] = 0;
while(que.size()){
int t = que.top().second;
que.pop();
if (st[t]) continue;
st[t] = 1;
for(int i = h[t]; i!=-1; i=ne[i]){
if(dis[val[i]] > dis[t]+w[i]){
dis[val[i]] = dis[t]+w[i];
que.push({dis[val[i]], val[i]});
}
}
}
if (dis[n] > 0x3f3f3f3f/2) return -1;
else return dis[n];
}
int main(){
cin >>n >>m;
memset(h, -1, sizeof(h));
while(m--){
int x, y, z;
cin >>x >>y >>z;
insert(x, y, z);
}
int start = 1;
cout <<dijkstra(start);
}
代码误区:
1. 节点何时入堆
- 当节点x的dis[x]变小时,节点入堆
- 下一次就从所有dis[]中选择最小者作为出发点
- 毕竟dis[x]不变小,堆顶就算是原本的dis[x],也不是他
对,这是由于heap本身就像哈希一样是拷贝
2. 节点何时入确定集st[N]?
- 当节点从堆中top()出来时,该节点对应图中节点就是最近的
- 最近节点加入st[N],表示该节点到单源的最短距离确定
- 之后就从该节点出发,更新其余节点到单源的最短距离
3. 为什么heap中的PII节点first存储距离,second存储图中节点?
- 因为heap内部也有比较函数,默认按照PII的first排序的
- 我们利用堆也是为寻找距离最小点加入st[]
本篇感想:
- 看完本篇博客,恭喜已登 《筑基境-中期》
距离登仙境不远了,加油