众所周知,dijkstra 算法有一个叫做堆优化的东西,不难写出下面这份代码:
memset(dis, 0x3f, sizeof dis);
dis[s] = 0;
q.emplace(0, s);
while (q.size()) {
int u = q.top().second;
q.pop();
if (cfg[u]) continue;
else cfg[u] = 1;
for (auto [v, w] : G[u])
if (dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
q.emplace(-dis[v], v);
}
}
这份代码的时间复杂度网上广为流传的是 O((n+m)logn),不过事实上可能应该也许大概是 O((n+m)logm)。
卡满的构造方法也不难想,就是先搞个菊花图,其中一条的边 (s,v) 为 1,其它边权为 1145141919810,对于 v 同理,一条为 1,其它为 1145141919800,这样可以保证每一次走一条出边后,新的点都会扔到堆里。
但我在前天翻 tourist 的代码时,突然发现一种 set 写法。
set<pair<int, int>> q;
memset(dis, 0x3f, sizeof dis);
dis[s] = 0;
q.emplace(0, s);
while (q.size()) {
int u = q.begin()->second;
q.erase(q.begin());
if (cfg[u]) continue;
else cfg[u] = 1;
for (auto [v, w] : G[u])
if (dis[v] > dis[u] + w) {
q.erase({ dis[v], v }); // 主要的区别在这
dis[v] = dis[u] + w;
q.emplace(dis[v], v);
}
}
不难发现对于这份代码,每个点在 set 种只会保留一个值,所以是严格 O((n+m)logn) 的。
一个奇妙的现象是在不开 O2 的情况下,set 写法跑得比 priority_queue 快,而在开 O2 的情况下反之。
怎么会是呢?原因可能有以下几个:
priority_queue 写法。(但卡了也没有意义)set 和 priority_queue 操作速度有差异。或者 O2 对 priority_queue 比较友好。本着闲的蛋疼严谨求知的精神,我们跑 30 组数据,分别取平均值后比较。
插电笔记本,CPU 是 AMD Ryzen 5 5600H。
编译命令 g++ (-O2) -std=c++20 -Wall -Wl,--stack=2610612736 -Wextra
| 操作类型 | set | priority_queue | set(O2) | priority_queue(O2) |
|---|---|---|---|---|
| 插入删除 107 次,保证元素有 106 个 | 死了 | 4.73s | 7.93s | 0.44s |
| 完全图(n=2000,m=2n(n−1)) | 3.43s | 3.56s | 1.83s | 1.61s |
| 随机(稀疏)图(随机性由 CYaRon(luogu-dev) 保证) | 0.235s | 0.217s | 0.165s | 0.156 |
从上表可以看出,set 带来的优越性(指 logn 和 logm 差的 21 的系数)远没有其本身操作自带的大常数优秀,瑜不掩瑕。只有在特殊图且不开 O2 的情况下能和 priority_queue 比肩。
所以 set 写法不是好文明,建议还是通通写 priority_queue。
Q:那这篇讨论到底有什么意义?到底想表达什么?
A:没有意义,闲的蛋疼,现在你浪费了人生中宝贵的两分钟。
测试用到的所有源代码可以在这里找到。