1. 左偏树

1.1. 前置定义

  • 外节点:当且仅当这个节点的左子树和右子树中的一个是空节点;

  • 距离:它子树中离它 最近的外节点 到这个节点的距离。

1.2. 性质

首先,左偏树保证堆性质。其次,左偏树任一节点的左儿子的距离不小于右儿子的距离。

Property 1.\(\quad\)任一节点的距离为其右儿子的距离加一。

Proof 1.\(\quad\)显然。这里只是为了格式好看(。


Property 2.\(\quad\)\(n\) 个点的左偏树,其最大距离不超过 \(\log n\) 级别。

Proof 2.\(\quad\)如果一个节点的距离为 \(d\),说明以它为根的子树至少包含一棵高度为 \(d\) 二叉树。这就要求节点数至少为 \(2^d\) 级别。

1.3. \(\rm merge()\) 操作

假设维护大根堆,令 \(y\)\(x,y\) 中权值较小的节点,将 \(y\)\(x\) 的右儿子合并。返回时维护 "左儿子的距离不小于右儿子的距离" 这一性质,然后将 \(x\) 的距离更新。

考虑复杂度。由于 \(x,y\) 在合并时是交叉的,我们不妨定义 \(t=d_u+d_v\),其中 \(u,v\) 分别是 \(x,y\) 子树目前合并到的点。由于每次向右儿子走,\(t\) 在一次递归中必定减一,而它初始时是 \(\log size_x+\log size_y\) 范围的,所以单次合并 \(\mathcal O(\log n)\).

int merge(int x,int y) {
	if(!x || !y) return x|y;
	if(c[x]<c[y]) swap(x,y);
	f[rs[x]=merge(rs[x],y)] = x;
	if(d[ls[x]]<d[rs[x]]) swap(ls[x],rs[x]);
	d[x] = d[rs[x]]+1; return x;
}

1.4. 建树

for(int i=1;i<=n;++i) q.push(i);
while(q.size()>1) {
    int x=q.front(); q.pop();
    int y=q.front(); q.pop();
    q.push(merge(x,y));
}

2. 杂题题解

罗马游戏

支持找到左偏树的根,然后将其删除。首先用路径压缩并查集判断是否在一棵左偏树,删除了根 \(rt\) 之后算出新的根,然后将 \(rt\) 的父亲置为它本身即可。\(\text{Link.}\)

[JLOI 2015] 城池攻占

对于每个城池维护一个左偏树(小根堆),用来存放可以来到这个城池的骑士(并不一定能活着出去),那么就可以边删根边统计城池的死亡骑士数和骑士到哪座城池。

关于战斗力的修改,把左偏树打两个 \(\text{mul},\text{add}\) 用于延迟修改。注意要考虑挺到最后的骑士。

一次 merge() 会减少一个骑士,所以复杂度是 \(\mathcal O(m\log m)\) 的。\(\text{Link.}\)

[SDOI 2010] 魔法猪学院

\(\text{Update on 2022.8.15}\):突然发现自己之前写的文章烂得像一坨屎 & 错得离谱。关键是写完的时候还觉得写得清晰透彻,非常开心地给劳委观赏!难怪她最后什么都没说 qwq.

题目转化成求第 \(k\) 短路。设反图为 \(G'\),首先在 \(G'\) 上从 \(t\) 开始跑最短路,加入最短路上的边,这样就构成了一棵树 \(T\)。不过事实上,最短路并不是唯一的,对于点 \(v\) 只需要选择一个为它贡献最短路的点 \(u\) 作为它的父亲即可。

如何构造 \(k\) 短路?以一条从 \(1\)\(n\) 的最短路为基础(也即最短路树上 \(n\)\(1\) 的路径),接着以一种顺序将树边替换成非树边(接下来均在原图上讨论)。考虑使用非树边 \(e(v,u)\) 来替换这条从 \(1\)\(n\) 的路径的贡献

\[\delta_{e(v,u)}=val_{e(v,u)}-(d_v-d_u) \]

其中 \(d_u\) 就是反着跑的最短路值。我们发现,对于 \(1\)\(n\) 路径的点集 \(P\),只有满足 \(v\in P\)\(e(v,u)\) 才能替换此路径上的边。用左偏树(小根堆)来维护非树边的选择,注意需要可持久化。具体来说,把形如 \(e(v,)\) 的边挂在 \(v\) 的左偏树内。初始时每个点都被合并上自己在最短路树上的祖先。接下来考虑两种拓展:

  • 直接用当前属于 \(P\) 的点的左偏树集合进行替换。为了避免算重,如果一条路径新被替换了 \(e(v,u)\),那么接下来选取 \(e(v',u')\) 时要保证 \(v'\)\(u\)\(n\) 的最短路树上;

  • 撤销上次新替换上的 \(e(v,u)\),选取另一条可行非树边。

左偏树一次合并是 \(\mathcal O(\log m)\) 的,一共有 \(n+m\) 次合并,正好和 \(\rm dijkstra\) 的复杂度一样。点数每次合并增加 \(\log m\) 个点,所以也是可过的。


另外再说明一下为什么 merge() 函数中当 \(x,y\) 有一个为 \(0\) 时可以不新增节点:因为此时并没有点的信息被更改。好简单啊可是我又想了很久

需要注意题目要求到达 \(n\) 就结束了,所以我们要删去 \(n\) 的所有出边。\(\text{Link.}\)

彩蛋:还是给大家放放原版让大家看看什么是狗屁不通 ——

Click me!!!

题目转化成求第 \(k\) 短路。设反图为 \(G'\),首先在 \(G'\) 上从 \(t\) 开始跑最短路,加入最短路上的边,这样就构成了一棵树 \(T\)。不过事实上这可能不是一棵树,我们对于点 \(v\),只需要选择一个为它贡献最短路的点 \(u\) 作为它的父亲即可。至于为什么,下文会有说明。

如何构造 \(k\) 短路?一个基本的想法是把树边替换成非树边。假设一条非树边 \(e(u,v)\) 连接树上存在 祖孙关系 的两点 \(u,v\)(不妨设 \(u\)\(v\) 的祖先),令 \(u\)\(e(u,v)\) 的初点,\(v\) 为末点,那么就有

\[\delta_{e(u,v)}=val_{e(u,v)}-(d_v-d_u) \]

具体地,令 \(P'\) 为路径 \(P\) 去掉树边的边集。用优先队列维护 \(\sum_{e\in P'}\delta_e\)(它代表了 \(P'\)),每次找到最小值与其对应的终点 \(u\)(终点定义为 \(P'\) 中某条边的起点,它满足是 \(P'\) 中起点在 \(T\)\(n\)\(1\) 的那条树上路径中离 \(n\) 最近的点),我们可以进行两种扩展:

  • 还是假设在 \(P'\) 中终点 \(u\) 代表了非树边 \(e(u,v)\)。现在考虑在 \(e(u,v)\) 后面再添加一条非树边。因为树上路径只能走 \(v\rightarrow u\) 方向,所以这次需要枚举末点是 \(u\) 的祖先及本身 的非树边进行替换。同理,我们要选择这些非树边中最小的非树边来替换。

可以用左偏树(小根堆)来维护非树边的选择,注意需要可持久化。另外这也可以解答最开始的问题:如果建出以 \(t\) 为根的树,就可以保证扩展方向一定是向 \(t\) 的,而不会行进到不合法的点。

  • 假设在 \(P'\) 中终点 \(u\) 代表了非树边 \(e(u,v)\)。现在考虑将 \(e(u,v)\) 换成另一条非树边。因为它由第一种扩展得来,\(e(u,v)\) 相当于在 \(v\) 号点的左偏树中选择的边,所以还要选择在其中的边进行替换。具体而言,假设 \(e(u,v)\) 在左偏树中的节点为 \(o\),我们选择 \(o\) 的左右儿子。

左偏树一次合并是 \(\mathcal O(\log m)\) 的,一共有 \(n+m\) 次合并,正好和 \(\sf Dijkstra\) 的复杂度一样。点数每次合并增加 \(\log m\) 个点,所以也是可过的。


另外再说明一下为什么 merge() 函数中当 \(x,y\) 有一个为 \(0\) 时可以不新增节点:因为此时并没有点的信息被更改。好简单啊可是我又想了很久

posted on 2020-04-01 12:30  Oxide  阅读(175)  评论(0编辑  收藏  举报