10.16 闲话-k 短路
Part.1 左偏树
左偏树是一个堆,支持 \(O(\log n)\) 合并。
假设 \(dist_i\) 表示离 \(i\) 最近的儿子数量不为 \(2\) 的儿子的距离 + 1,孤立点设为 0。
容易发现 \(dist_i \le \log n\) 。
证明考虑如果有 \(dist_i\) 则树高一定大于了 \(2^{dist_i-1}\) 。
那么如果钦定每一个节点的右儿子 \(dist\) 小于左儿子 \(dist\) ,那么这棵树就是左偏树。
不难发现此时 \(dist_i=dis_{rs_i}+1\) 。
故左偏树任意节点右链是 \(O(\log n)\) 的!
此时就很暴力了,合并时考虑将两个树树根较小的作为新的跟,然后将另一个跟和右儿子合并作为新的右儿子,注意此时可能破坏左偏结构,需要判断是否要交换左右儿子。
发现复杂度是 \(O(\log n)\) 。
int Merge(int x, int y)
{if (!x || !y) return x | y;if (val[x] > val[y]) swap(x, y);rs[x] = Merge(rs[x], y);fa[rs[x]] = x;if (dis[rs[x]] > dis[ls[x]]) swap(ls[x], rs[x]);dis[x] = dis[rs[x]] + 1;return x;
}
删除也很暴力,直接合并左右儿子,然后自底向上更新 \(dis\),由于可能发生更新的都是右儿子,而右链长度是 \(O(\log n)\) 的,所以复杂度是 \(O(\log n)\) 的。
void PushUp(int x)
{if (!x) return ;if (dis[x] != dis[rs[x]] + 1){dis[x] = dis[rs[x]] + 1;PushUp(fa[x]);}
}void Delete(int x)
{int y = Merge(ls[x], rs[x]);fa[y] = fa[x];if (ls[fa[x]] == x) ls[fa[x]] = y;if (rs[fa[x]] == x) rs[fa[x]] = y;if (dis[rs[fa[x]]] > dis[ls[fa[x]]]) swap(ls[fa[x]], rs[fa[x]]);PushUp(fa[y]);
}
然后其实就没了,会了这些,加上一些高端的并查集应用,就可以过穿可并堆的题目了。
Part.2 可持久化可并堆
发现上述过程完美符合可持久化操作,只需要 Merge 时新建一个节点即可。
int Merge(int x, int y)
{if (!x || !y) return x | y;if (val[x] > val[y]) swap(x, y);int p = ++cnt;ls[p] = ls[x], val[p] = val[x];rs[p] = Merge(rs[x], y);fa[rs[p]] = p;if (dis[rs[p]] > dis[ls[p]]) swap(ls[p], rs[p]);dis[p] = dis[rs[p]] + 1;return p;
}
Part.3 k 短路
会了一些前置知识后就可以开始学习 k 短路了。
其实我们可以发现一件事情就是一条更劣的路一定是经过了一些最短路的边,然后经过一些不在最短路上的边。
那么可以先建出到终点的最短路树(假设每个点到终点的距离为 \(h_i\)),然后将不在树边上的边成为偏离边。
这时发现经过一个偏离边(假设边权为 \(w\))的增加的代价其实就是 \(w+h_{to}-h_{from}\)
由于这是有向图,那么可以得知,对于一个路径,相邻的非树边中上一条的终点一定是这条边起点的儿孙。
那么便可以将一条边终点不变的添加到起点的子树中,将问题转化为统计从 \(s\) 到任意节点的 \(k\) 短路。
这时又注意到,对于每个要去转移的节点,如果其边越小转移出去越优,那么每次只更新最小的边,然后将这条边删去,发现这是一个堆能干的(每次拿出一个堆,弹掉栈顶)。
此时就发现,原本放到子树中的那一步操作竟然可以可持久化可并堆。
这样就愉快的通过了 k 短路。
复杂度 \(O((n+m)\log m+k\log k)\)
code
#include <iostream>
#include <cassert>
#include <vector>
#include <cstring>
#include <queue>
#include <climits>using namespace std;const int N = 2e6 + 10, M = 6000;#define fi first
#define se second
#define emp emplace_backusing ld = double;
using pii = pair <int, int>;
const ld inf = 1e300, eps = 1e-9;int cnt, ls[N], rs[N], rt[M], to[N], d[N], pre[M];
ld val[N];int Merge(int x, int y)
{if (!x || !y) return x | y;if (val[x] > val[y]) swap(x, y);int p = ++cnt;ls[p] = ls[x], val[p] = val[x], to[p] = to[x];rs[p] = Merge(rs[x], y);if (d[ls[p]] < d[rs[p]]) swap(ls[p], rs[p]);d[p] = d[rs[p]] + 1;return p;
}int New(ld v, int _to) {++cnt; val[cnt] = v, to[cnt] = _to; return cnt;}void Insert(int &x, ld v, int to)
{x = Merge(x, New(v, to));
}int n, m;
ld dis[M], pw[M];
vector <pair <int, ld>> E[M], T[M], NT[M];void dfs(int x, int fa)
{for (auto [to, w] : NT[x]) Insert(rt[x], w, to);rt[x] = Merge(rt[x], rt[fa]);for (auto [to, w] : T[x]){if (to == fa) continue;dfs(to, x);}
}int Solve(int s, ld k)
{priority_queue <pair <ld, int>> q;q.push({-(dis[s] + val[rt[s]]), rt[s]});int timer = 0;++timer, k -= dis[s];if (k < 0) return 0;while (q.size()){int x = q.top().se;ld y = -q.top().fi; q.pop();if (!x) continue;k -= y, ++timer;if (k < 0) return timer - 1;q.push({-(y + val[rt[to[x]]]), rt[to[x]]});q.push({-(y - val[x] + val[ls[x]]), ls[x]});q.push({-(y - val[x] + val[rs[x]]), rs[x]});}return timer;
}bool vis[M];
void dij(int s)
{for (int i = 1; i <= n; i++) dis[i] = inf;priority_queue <pair <ld, int>> q;q.push({0, s});dis[s] = 0;while (q.size()){int x = q.top().se; q.pop();if (vis[x]) continue;vis[x] = 1;for (auto [to, w] : E[x]){if (dis[to] > dis[x] + w){pre[to] = x, pw[to] = w;dis[to] = dis[x] + w;q.push({-dis[to], to});}}}
}bool flag[M];signed main()
{// freopen("data.in", "r", stdin); freopen("data.out", "w", stdout);ios :: sync_with_stdio(false), cin.tie(0), cout.tie(0);ld k; cin >> n >> m >> k;for (int i = 1; i <= m; i++){int x, y; ld w; cin >> x >> y >> w;if (x == n) continue;E[y].emp(x, w);}dij(n);for (int i = 1; i <= n; i++){for (auto [to, w] : E[i]){if (pre[to] == i && abs(pw[to] - w) < eps && !flag[to]) flag[to] = 1, T[i].emp(to, w);else NT[to].emp(i, w + dis[i] - dis[to]);}}dfs(n, 0);cout << Solve(1, k) << '\n';return 0;
}