「笔记」Link Cut Tree
写在前面
世界上怎么会有这么优美的数据结构!
这是一篇 Rewrite,「笔记」LCT 的 1.0 版本已经被削除了。
本文是具有主观性质的一些理解,用于自我总结复习,Link Cut Tree 初学者请移步其它博客。作者水平有限,若有不当之处请不吝赐教。
以及不要太在意 Tarjan 是怎么想到它的= =
动态树问题
给定一片森林,要求在线支持下列操作:
- 修改路径权值。
- 查询路径权值和。
- 断开并连接一些边,保证仍是一片森林。
要求单次操作复杂度 \(O(\log n)\) 级别。
若给定的森林是静态的,即不存在操作 3,该问题可以通过对原树的树链剖分,将树上路径/子树问题转化为基于 dfs 序的序列问题,从而使用序列静态数据结构维护。
加入动态加/删边操作后,我们仍然考虑对原树进行树链剖分来维护路径信息。在静态问题中我们使用了静态数据结构维护树链。在这里可以考虑使用易于修改的 Splay 动态维护树链信息。
换句话说,LCT 是通过 Splay 动态维护树链剖分结构,从而将树上问题转化为 Splay 上的序列问题的一种数据结构。
定义
实链剖分
实链剖分是一种对原树的树链剖分的方式,它将树划分成了多条实链:
- 每个节点能且仅能存在于一条实链中。实链是节点深度递增的一条树链,实链与实链间通过虚边连接。
- 实链中的边称为实边,称一条实边的儿子是父亲的实儿子。显然一个节点至多有一个实儿子,但可能有多个虚儿子。
- 在实链剖分中,对于每个节点,仅记录其父亲 与 实儿子的信息,即“认父不认子”。
确定边的虚实的标准是人的主观能动,即可以灵活地“随意划分”,以达到灵活修改剖分结构的功能。以下是一个实链剖分的例子:
![](https://oi-wiki.org/ds/images/lct9.png)
Link-Cut Tree
LCT 使用 Splay 来维护实链。每一条实链中的节点构成一棵 Splay。Splay 具有以下性质:
- 每棵 Splay 维护原树中的一条实链,且中序遍历这棵 Splay 得到的点序列,从前到后对应原树自上到下的这条实链。
- 原树每个节点与 Splay 节点一一对应。
- 对于每一棵对应实链的 Splay,与一般 Splay 不同的是,其根节点也存在父亲节点,指向原树中其父亲节点。这条边对应着原树中两条实链间的虚边。所有 Splay 节点中仅有原树根对应节点的父节点为空。
- 所有实链的 Splay 的父子关系构了一棵树,称为辅助树。由以上性质,辅助树可以体现原树中的所有父子关系,两棵原树的辅助树不同,两棵原树就不同。维护操作时仅需维护辅助树即可。
下图展示了上述实链剖分对应的一种合法的辅助树:
小结
- 实链剖分是一种树链剖分的方式。实链是节点深度递增的一条树链,实链与实链间通过虚边连接。
- 对于原树的实链:每棵 Splay 维护一条实链,其中序遍历对应自上到下的这条实链。
- 对于原树的虚边:子节点是所在 Splay 的根,且其父亲指向虚边的父节点。但父节点的儿子不指向子节点。
- 辅助树可以在满足辅助树、Splay 的性质下任意换根。
分步实现
以下以 P1501 Tree II 为例。
给定一 \(n\) 个节点的树,初始点权为 \(1\)。给定 \(m\) 次操作:
- 路径加。
- 路径乘。
- 删去一条边,再加入一条边,保证操作完之后仍然是一棵树。
- 查询路径和,答案对 \(51061\) 取模。
\(1\le n,m\le 10^5\)。
2S,512MB。
定义及声明
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode]; //Splay 的结构信息
LL siz[kMaxNode], sum[kMaxNode], tagplus[kMaxNode], tagprod[kMaxNode]; //子树大小,子树和,两种标记
bool tagrev[kMaxNode]; //Splay 的子树反转标记
Splay
void Pushup(int now_) { //维护子树和 和 子树大小
sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
siz[now_] = siz[ls] + siz[rs] + 1;
}
void PushReverse(int now_) { //子树反转标记,使得 Splay 节点的中序遍历反向。若原 splay 表示一条自顶向下的链,反转相当于将 splay 表示的链的边反向,父子关系互换。
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void PushPlus(int now_, LL val_) { //子树加
if (!now_) return;
val[now_] = (val[now_] + val_) % mod;
sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
tagplus[now_] = (tagplus[now_] + val_) % mod;
}
void PushProd(int now_, LL val_) { //子树乘
if (!now_) return;
val[now_] = val[now_] * val_ % mod;
sum[now_] = sum[now_] * val_ % mod;
tagplus[now_] = tagplus[now_] * val_ % mod;
tagprod[now_] = tagprod[now_] * val_ % mod;
}
void Pushdown(int now_) { //注意下放顺序
LL plus = tagplus[now_], prod = tagprod[now_], rev = tagrev[now_];
if (prod != 1) PushProd(ls, prod), PushProd(rs, prod);
if (plus) PushPlus(ls, plus), PushPlus(rs, plus);
if (rev) PushReverse(ls), PushReverse(rs);
tagprod[now_] = 1, tagplus[now_] = 0, tagrev[now_] = 0;
}
bool IsRoot(int now_) { //判断 now_ 是否为当前 Splay 的根
return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
return son[f][1] == now_;
}
void Rotate(int now_) {
int fa_ = f, w = WhichSon(now_);
if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) { //将 Splay 路径上的所有标记下放
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
}
Access 构造实链
上面提到,LCT 使用 Splay 来维护实链,可以将实链信息转化为 Splay 的子树信息。由此我们可以考虑把需要维护的对象拿出来,构成一个 Splay,再对该 Splay 进行操作即可。
Access 函数的作用,是将当前辅助树的根到指定节点的路径上所有点,构造成一个 Splay。即使得原树中由根到指定节点的链成为实链。
下图展示 Access(F)
后辅助树的变化。
![](https://img.imgdb.cn/item/600ce18e3ffa7d37b322c174.png)
实现时自下向上构建,舍弃父亲的原有右实儿子,更新为深度更深的链的根即可。
void Access(int now_) { //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,满足 Splay 的中序遍历深度递减的性质。
//自下向上构建,舍弃父亲的原有右儿子,换成 -> last 的链。
for (int last_ = 0; now_; last_ = now_, now_ = f) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
MakeRoot 换根
使指定节点成为原树的根。
画张图可以发现,换根相当于将根到指定节点的路径反向。
![](https://img.imgdb.cn/item/600ce18e3ffa7d37b322c171.png)
于是可以先使原树中由根到指定节点的链成为实链,再使它成为 Splay 的根。
此时的 Splay 表示一条自顶向下的链,路径反向等价于将 splay 表示的链的边反向,父子关系互换。这又等价于反转 Splay 的中序遍历,直接在 Splay 的根上打反转标记即可。
void MakeRoot(int now_) { //使 now 成为原树的根
Access(now_); //先使得树中由根->now 的链成为实链,构造出由它们组成的 Splay。
Splay(now_); //使 now 成为 splay 的根节点
PushReverse(now_); //将根->now 的链反转,使 now 成为原树的根。原理参考 PushReverse 函数的注释。
}
Find 找到指定节点所在原树的根
构造出根到指定节点的 Splay 后,根据实链节点深度递增的性质,Splay 中序遍历的第一个元素即为原树的根。
int Find(int now_) { //找到 now_ 所在原树的根
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls; //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,再找到 Splay 中序遍历的第一个元素,即为原树的根。
Splay(now_); //为了下一步操作,把根再转回去
return now_;
}
Split 构造由路径组成的 Splay
使 \(x\) 成为根,构造出根到 \(y\) 的 Splay 即得。此时 Splay 根 \(y\) 的子树信息即为路径信息。
通过该操作可以取出指定的原树的一条路径放入 Splay 中,从而将路径问题转化为序列数据结构问题。
void Split(int x_, int y_) { //构造由路径 x->y 组成的 Splay
MakeRoot(x_); //使 x 成为根,构造出根 -> y 的 Splay 即得,Splay 根的子树信息即为路径信息。
Access(y_);
Splay(y_);
}
Link 连边
使 \(x\) 成为根,将根的父亲指向 \(y\) 即可。注意判断两点之间是否已经连通。
void Link(int x_, int y_) { //加边 (x,y)
MakeRoot(x_); //使 x 成为根,再给根一个父亲
if (Find(y_) != x_) fa[x_] = y_;
}
Cut 删边
先使 \(x\) 成为根。再使用 Find
判断两点是否连通。
之后需要判断两点间是否有直接连边。
在 Find
中 Access(y)
之后,\(y\) 所在的 Splay 由 \(x\rightarrow y\) 的链构成。又 \(x\) 是 Splay 的根,\(y\) 的中序遍历恰好是 \(x\) 之后的一个,则若边 \((x,y)\) 存在,则 \(y\) 的位置只有一种可能:
\(y\) 是 \(x\) 的右儿子,且 \(y\) 没有左儿子,按照下述代码判断即可。
void Cut(int x_, int y_) { //删边 (x,y)
MakeRoot(x_); //使 x 成为根
//Find(y_) != x_ 保证 y 与 x 连通
//在 Find 函数中 Access(y) 之后,y 所在的 splay 由 x->y 的链构成。又 x 是 splay 的根,y 的中序遍历在 x 后,则若边 (x,y) 存在,则 y 的位置只有一种可能:
//y 是 x 的右儿子,且 y 没有左儿子。从而保证 y 在中序遍历中是 x 的后一个。
if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
fa[y_] = son[x_][1] = 0; //断绝父子关系
Pushup(x_);
}
路径修改 和 路径查询
Child's play。
void Modify(int x_, int y_, int val_, int type) { //路径修改
Split(x_, y_); //给路径构成的 Splay 打标记
if (!type) PushPlus(y_, val_);
else PushProd(y_, val_);
}
LL Query(int x_, int y_) { //路径查询
Split(x_, y_); //返回路径构成的 Splay 的信息
return sum[y_];
}
封装模板
//知识点:LCT
/*
By:Luckyblock
*/
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode]; //Splay 的结构信息
LL siz[kMaxNode], sum[kMaxNode], tagplus[kMaxNode], tagprod[kMaxNode]; //子树大小,子树和,两种标记
bool tagrev[kMaxNode]; //Splay 的子树反转标记
void Pushup(int now_) { //维护子树和 和 子树大小
sum[now_] = (sum[ls] + sum[rs] + val[now_]) % mod;
siz[now_] = siz[ls] + siz[rs] + 1;
}
void PushReverse(int now_) { //子树反转标记,使得 Splay 节点的中序遍历反向。若原 splay 表示一条自顶向下的链,反转相当于将 splay 表示的链的边反向,父子关系互换。
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void PushPlus(int now_, LL val_) {
if (!now_) return;
val[now_] = (val[now_] + val_) % mod;
sum[now_] = (sum[now_] + siz[now_] * val_ % mod) % mod;
tagplus[now_] = (tagplus[now_] + val_) % mod;
}
void PushProd(int now_, LL val_) {
if (!now_) return;
val[now_] = val[now_] * val_ % mod;
sum[now_] = sum[now_] * val_ % mod;
tagplus[now_] = tagplus[now_] * val_ % mod;
tagprod[now_] = tagprod[now_] * val_ % mod;
}
void Pushdown(int now_) { //注意下放顺序
LL plus = tagplus[now_], prod = tagprod[now_], rev = tagrev[now_];
if (prod != 1) PushProd(ls, prod), PushProd(rs, prod);
if (plus) PushPlus(ls, plus), PushPlus(rs, plus);
if (rev) PushReverse(ls), PushReverse(rs);
tagprod[now_] = 1, tagplus[now_] = 0, tagrev[now_] = 0;
}
bool IsRoot(int now_) { //判断 now_ 是否为当前 Splay 的根
return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
return son[f][1] == now_;
}
void Rotate(int now_) {
int fa_ = f, w = WhichSon(now_);
if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) { //将 Splay 路径上的所有标记下放
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
}
void Access(int now_) { //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,满足 Splay 的中序遍历深度递减的性质。
//自下向上构建,舍弃父亲的原有右儿子,换成 -> last 的链。
for (int last_ = 0; now_; last_ = now_, now_ = f) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) { //使 now 成为原树的根
Access(now_); //先使得树中由根->now 的链成为实链,构造出由它们组成的 Splay。
Splay(now_); //使 now 成为 splay 的根节点
PushReverse(now_); //将根->now 的链反转,使 now 成为原树的根。原理参考 PushReverse 函数的注释。
}
int Find(int now_) { //找到 now_ 所在原树的根
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls; //使得树中由根->now 的链成为实链,构造出由它们组成的 Splay,再找到 Splay 中序遍历的第一个元素,即为原树的根。
Splay(now_); //为了下一步操作,把根再转回去
return now_;
}
void Split(int x_, int y_) { //构造由路径 x->y 组成的 Splay
MakeRoot(x_); //使 x 成为根,构造出根 -> y 的 Splay 即得,Splay 根的子树信息即为路径信息。
Access(y_);
Splay(y_);
}
void Link(int x_, int y_) { //加边 (x,y)
MakeRoot(x_); //使 x 成为根,再给根一个父亲
if (Find(y_) != x_) fa[x_] = y_;
}
void Cut(int x_, int y_) { //删边 (x,y)
MakeRoot(x_); //使 x 成为根
//Find(y_) != x_ 保证 y 与 x 连通
//在 Find 函数中 Access(y) 之后,y 所在的 splay 由 x->y 的链构成。又 x 是 splay 的根,y 的中序遍历在 x 后,则若边 (x,y) 存在,则 y 的位置只有一种可能:
//y 是 x 的右儿子,且 y 没有左儿子。从而保证 y 在中序遍历中是 x 的后一个。
if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
fa[y_] = son[x_][1] = 0; //断绝父子关系
Pushup(x_);
}
void Modify(int x_, int y_, int val_, int type) { //路径修改
Split(x_, y_); //给路径构成的 Splay 打标记
if (!type) PushPlus(y_, val_);
else PushProd(y_, val_);
}
LL Query(int x_, int y_) { //路径查询
Split(x_, y_); //返回路径构成的 Splay 的信息
return sum[y_];
}
}
复杂度
上述 Splay
和 Access
操作的复杂度都是均摊 \(O(\log n)\) 的,可得其它所有操作的复杂度都是均摊 \(O(\log n)\) 的。
小技巧
一次 Find
操作中进行了 1 次 Access
和 两次 Splay
,并进行了一次 Splay 上的向下查询操作。这使得此操作复杂度较高。
在某些不带删除的题目中可以直接使用并查集维护树根,避免调用 Find
函数。
在保证加删边合法时(例如本题),就不需要在 Link
和 Cut
操作中判断是否连通了。两个函数可改写为下述简单形式。同时避免了 Find
函数的调用,降低了复杂度。
void Link(int x_, int y_) {
MakeRoot(x_);
fa[x_] = y_;
}
void Cut(int x_, int y_) {
Split(x_, y_);
fa[x_] = son[y_][0] = 0; //在 Splay 中 x 和 y 有直接连边,x 在中序遍历中是 y 的前一个,Split 后 Splay 的根是 y,则 x 一定是 y 的左儿子。
Pushup(y_);
}
例题
我相信有能力学习 LCT 的读者并不需要过多对代码的解释了= =
「SDOI2008」洞穴勘测
维护连通性。
给定 \(n\) 个点,给定 \(m\) 次操作:
- 断开一条边,保证断开的边存在。
- 连接一条边,保证操作后是一片森林。
- 查询两点的连通性。
\(1\le n\le 10^4\),\(1\le m\le 2\times 10^5\)。
1S,128MB。
板题,又保证操作合法,套板子即可。
P3690 【模板】Link Cut Tree (动态树)
维护树链信息。
给定 \(n\) 个点以及每个点的权值,给定 \(m\) 次操作:
- 询问路径上的点的权值异或和。
- 单点权值修改。
- 断开一条边,若边不存在则跳过。
- 连接一条边,若两点已连通则跳过。
\(1\le n\le 10^5\),\(1\le m\le 3\times 10^5\)。
2S,128MB。
保证操作后仍是森林,Splay 维护子树节点异或和即可。注意判断操作不合法。可以用如下方法简单实现单点修改:
void Modify(int now_, int val_) {
Splay(now_); //先转到根,修改后不影响其它子树的 xor 和
val[now_] = val_;
Pushup(now_);
}
「HNOI2010」弹飞绵羊
模型的建立。
下述题意是抽象过的,建议先阅读原题面。
给定一长度为 \(n\) 的正整数数列 \(k\)。对于点 \(1\sim n\),从点 \(i\) 向点 \(i + k_i\) 连有向边,若 \(i + k_i >n\) 不连边。
给定 \(m\) 次操作:
- 给定点 \(i\),查询以它为起点的有向路径上点的数量。
- 给定参数 \(j,k'\),表示将 \(k_j\) 修改为 \(k'\)。
\(1\le n\le 2\times 10^5\),\(1\le m\le 10^5\)。
1S,128MB。
考虑建立虚拟节点 \(n + 1\)。对于节点 \(i\),若满足 \(i + k_i>n\),则令 \(i\) 连向 \(n +1\)。发现每个节点只会向编号比它大的一个节点连一条边,构成了一个树状结构。
在这棵树上,询问的答案即为 \(n+1\) 到 \(i\) 链的长度 \(- 1\)。修改相当于动态删边加边操作。Splay 维护子树大小即可。总复杂度 \(O(m\log n)\) 级别。
bzoj 4998 星球联盟
加边,维护边双连通分量。
时空限制是在 Mina! 看的 = =
给定一 \(n\) 个点 \(m\) 条边的无向图,给定 \(q\) 次操作。
每次操作加入一条边,查询加入这条边后这条边的两端点是否在一个边双连通分量中,若在则输出该分量内节点个数。
\(1< n,m,q< 2\times 10^5\),。
1S,256MB。
无向图边双缩点后一定是一棵树,考虑使用 LCT 维护加边操作。
具体地,使用并查集维护每个点所在的边双(注意不是连通性)。加边时分类讨论。
- 若两点已经在一个边双中,跳过操作。
- 两点不连通,直接连边即可。
- 两点已连通,考虑把两点路径上所有点取出,把它们合并成一个点。新点代表它们所在的边双。
并查集中维护 size。每次加边后考察两点是否在同一个边双内,若在则输出对应祖先的 size。
具体实现上:对于需要合并成一个点的路径,将该路径取出后遍历其中所有点,并在并查集中把它们合并。之后每次需要调用一个 LCT 节点时,都找到该节点在并查集中的祖先,然后调用它的祖先。由于事先 Access(u,v)
了,这样做显然不会破坏 LCT 中原有的各 Splay 的父子关系,可以保证正确性。
考虑复杂度。每个原图中的节点都只会被合并一次,合并一个节点均摊 \(O(1)\)。则总复杂度为常数飞天的 \(O((m+q)\log n)\) 级别。
//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace UF { //Union-Find data structure
const int kMaxNode = kN;
int fa[kMaxNode], siz[kMaxNode];
int Find(int x_) {
int y = fa[x_];
return fa[x_] == x_ ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int x_, int y_) {
x_ = Find(x_), y_ = Find(y_);
if (x_ != y_) {
fa[x_] = y_;
siz[y_] += siz[x_];
}
}
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], siz[kMaxNode];
bool tagrev[kMaxNode];
void Pushup(int now_) {
now_ = UF::Find(now_);
siz[now_] = siz[ls] + siz[rs] + 1;
}
void PushReverse(int now_) {
now_ = UF::Find(now_);
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void Pushdown(int now_) {
now_ = UF::Find(now_);
if (!now_) return ;
if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
tagrev[now_] = 0;
}
bool IsRoot(int now_) {
int x = f, y = UF::Find(f);
return son[UF::Find(f)][0] != now_ && son[UF::Find(f)][1] != now_;
}
bool WhichSon(int now_) {
return son[UF::Find(f)][1] == now_;
}
void Rotate(int now_) {
now_ = UF::Find(now_);
int fa_ = UF::Find(f), w = WhichSon(now_);
if (!IsRoot(fa_)) son[UF::Find(fa[fa_])][WhichSon(fa_)] = now_;
f = UF::Find(fa[fa_]);
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) {
now_ = UF::Find(now_);
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
now_ = UF::Find(now_);
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(UF::Find(f))) Rotate(WhichSon(UF::Find(f)) == WhichSon(now_) ? UF::Find(f) : now_);
}
}
void Access(int now_) {
now_ = UF::Find(now_);
for (int last_ = 0; now_; last_ = now_, now_ = UF::Find(f)) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) {
now_ = UF::Find(now_);
Access(now_);
Splay(now_);
PushReverse(now_);
}
int Find(int now_) {
now_ = UF::Find(now_);
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls;
Splay(now_);
return now_;
}
void Split(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
MakeRoot(x_);
Access(y_);
Splay(y_);
}
void Delete(int now_, int fa_) { //递归地遍历 Splay,合并原节点
now_ = UF::Find(now_);
if (!now_) return;
UF::Union(now_, fa_);
Delete(ls, fa_), Delete(rs, fa_);
}
bool Link(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
if (x_ == y_) return true;
MakeRoot(x_);
if (Find(y_) != x_) {
fa[x_] = y_;
return false;
}
Delete(son[x_][1], x_); //合并原节点
son[x_][1] = 0; //x 即合并后的得到的代表边双的节点。将它的儿子置零。
Pushup(x_);
return true;
}
int Query(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
Split(x_, y_);
return siz[y_] - 1;
}
}
//=============================================================
int main() {
int n = read(), m = read(), p = read();
for (int i = 1; i <= n; ++ i) {
UF::fa[i] = i;
LCT::siz[i] = UF::siz[i] = 1;
}
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read();
LCT::Link(u_, v_);
}
for (int i = 1; i <= p; ++ i) {
int u_ = read(), v_ = read();
if (LCT::Link(u_, v_)) {
printf("%d\n", UF::siz[UF::Find(v_)]);
} else{
printf("No\n");
}
}
return 0;
}
「AHOI2005」航线规划
加边,维护边双连通分量。
给定一 \(n\) 个点 \(m\) 条边的无向图,给定 \(q\) 次操作:
- 断开一条边,保证断开的边存在。
- 查询给定两点间所有路径的必经边的数量。
\(1< n< 3\times 10^4\),\(1< m< 10^5\),\(0\le q\le 4\times 10^4\)。
1S,128MB。
先离线反向操作,将删边转化为加边。
容易发现查询的答案即原图边双连通分量缩点后指定两点间的边数,同上题使用并查集维护节点所在边双。查询时取出指定路径,答案即路径上的点数 \(-1\),维护 Splay 子树大小即可。
总复杂度 \(O((m+q)\log n)\) 级别。
上述做法通用性较强。对于此题来说,还有另外一种解法。
同样离线反向操作,先进行边双缩点,在新图上考虑边转点。将代表边的点的权值赋为 1,其它点权值为 0。边权为 1 的实际含义是该边不在一个环中。
若一次加边后会出现环,则将这条路径上所有点的点权都置为 0 即可。查询的答案即指定两点间路径的权值和。
此时树是静态的,甚至可以直接树剖维护,常数要小很多。
LCT 代码
//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <map>
#define LL long long
#define pr std::pair
#define mp std::make_pair
const int kN = 5e4 + 10;
//=============================================================
int n, m, e_num, u[kN << 1], v[kN << 1];
int q_num, q[kN][3], ans[kN];
std::map <pr <int, int>, bool> del;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace UF { //Union-Find data structure
const int kMaxNode = kN;
int fa[kMaxNode];
int Find(int x_) {
int y = fa[x_];
return fa[x_] == x_ ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int x_, int y_) {
x_ = Find(x_), y_ = Find(y_);
if (x_ != y_) fa[x_] = y_;
}
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], siz[kMaxNode];
bool tagrev[kMaxNode];
void Pushup(int now_) { //维护 size
now_ = UF::Find(now_);
siz[now_] = siz[ls] + siz[rs] + 1;
}
void PushReverse(int now_) {
now_ = UF::Find(now_);
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void Pushdown(int now_) {
now_ = UF::Find(now_);
if (!now_) return ;
if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
tagrev[now_] = 0;
}
bool IsRoot(int now_) {
int x = f, y = UF::Find(f);
return son[UF::Find(f)][0] != now_ && son[UF::Find(f)][1] != now_;
}
bool WhichSon(int now_) {
return son[UF::Find(f)][1] == now_;
}
void Rotate(int now_) {
now_ = UF::Find(now_);
int fa_ = UF::Find(f), w = WhichSon(now_);
if (!IsRoot(fa_)) son[UF::Find(fa[fa_])][WhichSon(fa_)] = now_;
f = UF::Find(fa[fa_]);
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) {
now_ = UF::Find(now_);
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
now_ = UF::Find(now_);
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(UF::Find(f))) Rotate(WhichSon(UF::Find(f)) == WhichSon(now_) ? UF::Find(f) : now_);
}
}
void Access(int now_) {
now_ = UF::Find(now_);
for (int last_ = 0; now_; last_ = now_, now_ = UF::Find(f)) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) {
now_ = UF::Find(now_);
Access(now_);
Splay(now_);
PushReverse(now_);
}
int Find(int now_) {
now_ = UF::Find(now_);
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls;
Splay(now_);
return now_;
}
void Split(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
MakeRoot(x_);
Access(y_);
Splay(y_);
}
void Delete(int now_, int fa_) { //递归地遍历 Splay,合并原节点
now_ = UF::Find(now_);
if (!now_) return;
UF::Union(now_, fa_);
Delete(ls, fa_), Delete(rs, fa_);
}
void Link(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
if (x_ == y_) return;
MakeRoot(x_);
if (Find(y_) != x_) {
fa[x_] = y_;
return ;
}
Delete(son[x_][1], x_); //合并原节点
son[x_][1] = 0; //x 即合并后的得到的代表边双的节点。将它的儿子置零。
Pushup(x_);
}
int Query(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
Split(x_, y_);
return siz[y_] - 1;
}
}
//=============================================================
signed main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
UF::fa[i] = i;
LCT::siz[i] = 1;
}
for (int i = 1; i <= m; ++ i) {
int u_ = read(), v_ = read();
u[++ e_num] = u_, v[e_num] = v_;
if (u_ > v_) std::swap(u[e_num], v[e_num]);
}
while(true) {
int opt = read();
if (opt == -1) break;
int u_ = read(), v_ = read();
if (u_ > v_) std::swap(u_, v_);
q[++ q_num][0] = opt, q[q_num][1] = u_, q[q_num][2] = v_;
if (!opt) del[mp(u_, v_)] = true;
}
for (int i = 1; i <= m; ++ i) {
int u_ = u[i], v_ = v[i];
if (del[mp(u_, v_)]) continue;
LCT::Link(u_, v_);
}
for (int i = q_num; i; -- i) { //注意反向
int opt = q[i][0], u_ = q[i][1], v_ = q[i][2];
if (!opt) LCT::Link(u_, v_);
if (opt) ans[i] = LCT::Query(u_, v_);
}
for (int i = 1; i <= q_num; ++ i) {
if (q[i][0]) printf("%d\n", ans[i]);
}
return 0;
}
bzoj 2959 长跑
加边,维护边双连通分量。
给定一 \(n\) 个点的无向图,点有点权,初始图中没有边。给定 \(m\) 次操作:
- 连接一条无向边。
- 单点权值修改。
- 给定节点 \(u,v\),要求把所有边定向,最大化从 \(u\) 到 \(v\) 的所有路径中,经过的点权值和的最大值(经过多次权值也只计算一次),输出该最大值。
\(n\le 1.5 \times 10^5\),\(1\le m\le 5n\),点权非负。
1S,256MB。
点权非负,显然应该尽量增加经过的点数。
考虑这个查询操作。对于一个边双连通分量,显然可通过构造使得可以在边双里绕圈圈,从而令每个节点都可以被经过并获得其权值,且可以从任意一个节点离开边双。
考虑对原图进行边双缩点,新点的权值为内部所有节点的权值和,查询的答案即两点路径上所有节点的权值和,使用 LCT 维护加边、点权修改即可。
总复杂度 \(O(m\log n)\)。
//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 2e5 + 10;
//=============================================================
int origin[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
namespace UF { //Union-Find data structure
const int kMaxNode = kN;
int fa[kMaxNode];
int Find(int x_) {
int y = fa[x_];
return fa[x_] == x_ ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int x_, int y_) {
x_ = Find(x_), y_ = Find(y_);
if (x_ != y_) fa[x_] = y_;
}
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode];
LL sum[kMaxNode];
bool tagrev[kMaxNode];
void Pushup(int now_) {
now_ = UF::Find(now_);
sum[now_] = sum[ls] + sum[rs] + val[now_];
}
void PushReverse(int now_) {
now_ = UF::Find(now_);
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void Pushdown(int now_) {
now_ = UF::Find(now_);
if (!now_) return ;
if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
tagrev[now_] = 0;
}
bool IsRoot(int now_) {
return son[UF::Find(f)][0] != now_ && son[UF::Find(f)][1] != now_;
}
bool WhichSon(int now_) {
return son[UF::Find(f)][1] == now_;
}
void Rotate(int now_) {
now_ = UF::Find(now_);
int fa_ = UF::Find(f), w = WhichSon(now_);
if (!IsRoot(fa_)) son[UF::Find(fa[fa_])][WhichSon(fa_)] = now_;
f = UF::Find(fa[fa_]);
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) {
now_ = UF::Find(now_);
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
now_ = UF::Find(now_);
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(UF::Find(f))) Rotate(WhichSon(UF::Find(f)) == WhichSon(now_) ? UF::Find(f) : now_);
}
}
void Access(int now_) {
now_ = UF::Find(now_);
for (int last_ = 0; now_; last_ = now_, now_ = UF::Find(f)) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) {
now_ = UF::Find(now_);
Access(now_);
Splay(now_);
PushReverse(now_);
}
int Find(int now_) {
now_ = UF::Find(now_);
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls;
Splay(now_);
return now_;
}
void Split(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
MakeRoot(x_);
Access(y_);
Splay(y_);
}
void Delete(int now_, int fa_) { //递归地遍历 Splay,合并原节点
now_ = UF::Find(now_);
if (!now_) return;
UF::fa[now_] = fa_;
val[fa_] += val[now_];
Delete(ls, fa_), Delete(rs, fa_);
}
void Link(int x_, int y_) {
x_ = UF::Find(x_), y_ = UF::Find(y_);
if (x_ == y_) return;
MakeRoot(x_);
if (Find(y_) != x_) {
fa[x_] = y_;
return;
}
Delete(son[x_][1], x_); //合并原节点
son[x_][1] = 0; //x 即合并后的得到的代表边双的节点。将它的儿子置零。
Pushup(x_);
}
void Modify(int x_, int val_) { //单点修改
Splay(UF::Find(x_));
val[UF::Find(x_)] -= origin[x_];
origin[x_] = val_;
val[UF::Find(x_)] += origin[x_];
Pushup(UF::Find(x_));
}
LL Query(int x_, int y_) { //查询路径权值和
x_ = UF::Find(x_), y_ = UF::Find(y_);
MakeRoot(x_);
if (Find(y_) != x_) return -1; //不连通
Split(x_, y_);
return sum[y_];
}
}
//=============================================================
int main() {
int n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
UF::fa[i] = i;
origin[i] = LCT::val[i] = LCT::sum[i] = read();
}
for (int i = 1; i <= m; ++ i) {
int opt = read(), x = read(), y = read();
if (opt == 1) LCT::Link(x, y);
if (opt == 2) LCT::Modify(x, y);
if (opt == 3) printf("%lld\n", LCT::Query(x, y));
}
return 0;
}
P4234 最小差值生成树
LCT 维护生成树。
给定一 \(n\) 个点,\(m\) 条边的无向图,求边权最大值与最小值的差值最小的生成树,输出最小的差值。图可能存在自环。
\(1\le n\le 5\times 10^4\),\(1\le m\le 2\times 10^5\),\(1\le w\le 10^4\)。
3S,256MB。
考虑按照权值升序枚举边,并加入生成树中,以固定边权值最大的边。生成树中权值最小的边的权值应尽可能小,考虑使用 LCT 动态维护生成树中最小的边。
若加入一条边 \((u,v)\) 后图上会产生环,为保证树的形态,则需删掉原树中路径 \((u,v)\) 中任意一条边。显然应贪心地删去权值最小的边,LCT 维护最小边权即可。只要加入新边后构成生成树就统计贡献。
由于树的形态不定,边权并不好直接维护,考虑边转点,对每条边都新建一个虚拟节点并储存边权。
总复杂度 \(O(m\log n)\) 级别。
//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#include <set>
#define LL long long
const int kN = 5e5 + 10;
const int kInf = 1e9 + 2077;
//=============================================================
struct Edge {
int u, v, w;
} e[kN];
int ans = kInf;
std::multiset <int> edge;
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
bool CMP(Edge fir_, Edge sec_) {
return fir_.w < sec_.w;
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], val[kMaxNode];
int minval[kMaxNode], node[kMaxNode];
bool tagrev[kMaxNode];
void Pushup(int now_) {
minval[now_] = val[now_], node[now_] = now_;
if (ls && minval[ls] < minval[now_]) {
minval[now_] = minval[ls], node[now_] = node[ls];
}
if (rs && minval[rs] < minval[now_]) {
minval[now_] = minval[rs], node[now_] = node[rs];
}
}
void PushReverse(int now_) {
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void Pushdown(int now_) {
if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
tagrev[now_] = 0;
}
bool IsRoot(int now_) {
return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
return son[f][1] == now_;
}
void Rotate(int now_) {
int fa_ = f, w = WhichSon(now_);
if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) {
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
}
void Access(int now_) {
for (int last_ = 0; now_; last_ = now_, now_ = f) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) {
Access(now_);
Splay(now_);
PushReverse(now_);
}
int Find(int now_) {
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls;
Splay(now_);
return now_;
}
void Split(int x_, int y_) {
MakeRoot(x_);
Access(y_);
Splay(y_);
}
void Link(int x_, int y_) {
MakeRoot(x_);
if (Find(y_) != x_) fa[x_] = y_;
}
void Cut(int x_, int y_) {
MakeRoot(x_);
if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
fa[y_] = son[x_][1] = 0;
Pushup(x_);
}
}
//=============================================================
int main() {
int n = read(), m = read();
for (int i = 1; i <= n; ++ i) {
LCT::val[i] = LCT::minval[i] = kInf;
LCT::node[i] = i;
}
for (int i = 1; i <= m; ++ i) e[i] = (Edge) {read(), read(), read()};
std::sort(e + 1, e + m + 1, CMP);
for (int i = 1, tot = 0; i <= m; ++ i) {
int u_ = e[i].u, v_ = e[i].v, w_ = e[i].w;
if (u_ == v_) continue;
LCT::val[n + i] = LCT::minval[n + i] = w_;
LCT::node[n + i] = n + i;
if (LCT::Find(u_) != LCT::Find(v_)) { //加边后没有环
++ tot;
} else { //加边后产生环
LCT::Split(u_, v_);
int minval = LCT::minval[v_], node = LCT::node[v_]; //找到最小边
edge.erase(edge.find(minval)); //削除
LCT::Cut(e[node - n].u, node), LCT::Cut(e[node - n].v, node);
}
LCT::Link(u_, n + i), LCT::Link(v_, n + i); //连边
if (tot == n - 1) { //同级生成树的贡献
if (!edge.size()) Chkmin(ans, e[i].w);
else Chkmin(ans, e[i].w - (*edge.begin()));
}
edge.insert(w_);
}
printf("%d\n", ans);
return 0;
}
「NOI2014」魔法森林
LCT 维护生成树
给定一 \(n\) 个节点 \(m\) 条边的无向图,每条边有两个属性 \((a_i, b_i)\)。
对于一棵生成树 \(T\),最小化并输出:\[\left(\max_{e_i\in T} a_i\right) + \left(\max_{e_i\in T} b_i\right) \]\(2\le n\le 5\times 10^4\),\(0\le m\le 10^5\),\(1\le a_i,b_i\le 5\times 10^4\)。
1S,256MB。
二元最小生成树问题。
考虑把所有边按 \(a_i\) 升序排序,按顺序枚举加入生成树中,固定树中各边 \(a_i\) 的最大值。同上题的套路,先边转点,若加入新边后产生环,删去原路径上 \(b\) 最大的边以最小化 \(b\)。只要加入新边后构成生成树就 Split(1,n)
来统计贡献。
LCT 分别维护两个属性的最大值即可。总复杂度 \(O(m\log n)\)。
加入的边不一定影响 \(1\rightarrow n\),为什么这样可以保证正确性?可以发现上面实际上就是在模拟 Kruskal 构建最小瓶颈树的过程,结合 Kruskal 的性质可知这样是正确的。
//知识点:LCT
/*
By:Luckyblock
*/
#include <algorithm>
#include <cctype>
#include <cstdio>
#include <cstring>
#define LL long long
const int kN = 5e5 + 10;
const int kInf = 2e9 + 2077;
//=============================================================
struct Edge {
int u, v, w1, w2;
} e[kN];
int n, m, ans = kInf, fa[kN];
//=============================================================
inline int read() {
int f = 1, w = 0;
char ch = getchar();
for (; !isdigit(ch); ch = getchar())
if (ch == '-') f = -1;
for (; isdigit(ch); ch = getchar()) w = (w << 3) + (w << 1) + (ch ^ '0');
return f * w;
}
void Chkmax(int &fir, int sec) {
if (sec > fir) fir = sec;
}
void Chkmin(int &fir, int sec) {
if (sec < fir) fir = sec;
}
bool CMP(Edge fir_, Edge sec_) {
return fir_.w1 < sec_.w1;
}
int Find(int x_) {
return fa[x_] == x_ ? x_ : fa[x_] = Find(fa[x_]);
}
void Union(int x_, int y_) {
x_ = Find(x_), y_ = Find(y_);
if (x_ != y_) fa[x_] = y_;
}
namespace LCT {
#define f fa[now_]
#define ls son[now_][0]
#define rs son[now_][1]
const int kMaxNode = kN;
int fa[kMaxNode], son[kMaxNode][2], vala[kMaxNode], valb[kMaxNode];
int mxa[kMaxNode], posa[kMaxNode], mxb[kMaxNode], posb[kMaxNode];
bool tagrev[kMaxNode];
void Init(int now_, int vala_, int valb_) {
vala[now_] = mxa[now_] = vala_;
valb[now_] = mxb[now_] = valb_;
posa[now_] = posb[now_] = now_;
}
void Pushup(int now_) {
mxa[now_] = vala[now_], mxb[now_] = valb[now_];
posa[now_] = posb[now_] = now_;
if (ls && mxa[ls] > mxa[now_]) {
mxa[now_] = mxa[ls], posa[now_] = posa[ls];
}
if (rs && mxa[rs] > mxa[now_]) {
mxa[now_] = mxa[rs], posa[now_] = posa[rs];
}
if (ls && mxb[ls] > mxb[now_]) {
mxb[now_] = mxb[ls], posb[now_] = posb[ls];
}
if (rs && mxb[rs] > mxb[now_]) {
mxb[now_] = mxb[rs], posb[now_] = posb[rs];
}
}
void PushReverse(int now_) {
if (!now_) return;
std::swap(ls, rs);
tagrev[now_] ^= 1;
}
void Pushdown(int now_) {
if (tagrev[now_]) PushReverse(ls), PushReverse(rs);
tagrev[now_] = 0;
}
bool IsRoot(int now_) {
return son[f][0] != now_ && son[f][1] != now_;
}
bool WhichSon(int now_) {
return son[f][1] == now_;
}
void Rotate(int now_) {
int fa_ = f, w = WhichSon(now_);
if (!IsRoot(f)) son[fa[f]][WhichSon(f)] = now_;
f = fa[f];
son[fa_][w] = son[now_][w ^ 1];
fa[son[fa_][w]] = fa_;
son[now_][w ^ 1] = fa_;
fa[fa_] = now_;
Pushup(fa_), Pushup(now_);
}
void Update(int now_) {
if (!IsRoot(now_)) Update(f);
Pushdown(now_);
}
void Splay(int now_) {
Update(now_);
for (; !IsRoot(now_); Rotate(now_)) {
if (!IsRoot(f)) Rotate(WhichSon(f) == WhichSon(now_) ? f : now_);
}
}
void Access(int now_) {
for (int last_ = 0; now_; last_ = now_, now_ = f) {
Splay(now_), rs = last_;
Pushup(now_);
}
}
void MakeRoot(int now_) {
Access(now_);
Splay(now_);
PushReverse(now_);
}
int Find(int now_) {
Access(now_);
Splay(now_);
while (ls) Pushdown(now_), now_ = ls;
Splay(now_);
return now_;
}
void Split(int x_, int y_) {
MakeRoot(x_);
Access(y_);
Splay(y_);
}
void Link(int x_, int y_) {
MakeRoot(x_);
if (Find(y_) != x_) fa[x_] = y_;
}
void Cut(int x_, int y_) {
MakeRoot(x_);
if (Find(y_) != x_ || fa[y_] != x_ || son[y_][0]) return ;
fa[y_] = son[x_][1] = 0;
Pushup(x_);
}
int Query(int x_, int y_) {
Split(x_, y_);
return mxa[y_] + mxb[y_];
}
}
//=============================================================
int main() {
n = read(), m = read();
for (int i = 1; i <= n; ++ i) fa[i] = i;
for (int i = 1; i <= m; ++ i) {
e[i].u = read(), e[i].v = read();
e[i].w1 = read(), e[i].w2 = read();
}
std :: sort(e + 1, e + m + 1, CMP);
for (int i = 1; i <= m; ++ i) {
if (e[i].u == e[i].v) continue;
int u_ = e[i].u, v_ = e[i].v, w1_ = e[i].w1, w2_ = e[i].w2;
LCT::Init(i + n, w1_, w2_);
if (Find(u_) != Find(v_)) {
Union(u_, v_);
LCT::Link(u_, i + n), LCT::Link(v_, i + n);
} else {
LCT::Split(u_, v_);
int mxb = LCT::mxb[v_], posb = LCT::posb[v_];
if (mxb <= w2_) continue;
LCT::Cut(e[posb - n].u, posb), LCT::Cut(e[posb - n].v, posb);
LCT::Link(i + n, u_), LCT::Link(i + n, v_);
}
if (Find(1) == Find(n)) Chkmin(ans, LCT::Query(1, n));
}
printf("%d\n", ans == kInf ? -1 : ans);
return 0;
}
//*我写 kruscal 骗分,好吗?
//评测机:*不!
//5pts
写在最后
鸣谢
Link Cut Tree - OI Wiki
杨哲《QTREE 解法的一些研究》
Link-Cut-Tree - PoPoQQQ
Link-Cut Tree(LCT)&TopTree讲解 - The_Virtuoso