【学习笔记】 - 基础数据结构 :Link-Cut Tree(进阶篇)
前言
LCT没题写可以去写树剖和一些线段树合并的题练手
LCT
的概念
原本的树剖是对树进行剖分,剖分为重边和轻边
LCT则是对于树分为虚边和实边,特殊的,LCT可以没有虚边(例:银河英雄传说v2)
单独被包含在一个实链里的点称作孤立点
在树剖中,我们使用线段树/树状数组来维护重链
在Link-Cut Tree
里我们使用一种更灵活地数据结构splay
来维护这些实链
splay
维护的是所有实边路径的中序遍历
每个实链都是一颗splay
每条实链之间都有一条虚边将其连接起来
x
的子节点其实是 x
在 splay
里的后继节点,而 x
的父节点其实是 x
在 splay
里的前驱节点
注意,这里 splay
本身也有父节点和子节点,但是 splay
里的父节点和子节点与原树的父节点和子节点没有任何关系
虚边用每个 splay
的根节点来维护
如下图,假设这里的右侧包含 x
和 r
节点的整颗 splay
的根节点为 r
,则虚边应该在splay
中记录为 r
的父节点,而非 x
的父节点
我们发现,对于虚边而言,子节点能够指向父节点,父节点却不知道子节点是谁
也就是说,路径用 splay
来维护,而路径与路径的关系用 splay
的根节点来维护
我们可以非常简单的把一条实边删掉,换成一条虚边,只要把父节点的后继修改即可
LCT
的基本操作
这里可以直接接上前面的学习笔记了
这里是上一篇
发现树剖代码太长了,给我恶心坏了
学个代码短点的能写树剖题的数据结构吧
前置知识
树链剖分
简介以及优缺点介绍
Link-Cut Tree
,也就是LCT
,一般用于解决动态树问题
Link-Cut Tree
可用于实现重链剖分的绝大多数问题,复杂度为\(O(n \log n)\),看起来比树剖的\(O(n \log^2 n)\)复杂度更小,但则不然,基于splay
实现的Link-Cut Tree
常数巨大(约11
倍常数),往往表现不如树剖
Link-Cut Tree
的代码往往比树剖少一些
动态树问题
维护一个森林,支持删除某条边,连接某条边,并保证加边/删边之后仍是森林
同时维护这个森林的一些信息
实链剖分
回顾重链剖分
按子树大小剖分整棵树并重新标号
此时树上形成了一些以链为单位的连续区间,用线段树进行区间操作
我们发现,诶重剖怎么是按子树大小来剖的,这也不能搞动态树啊
显然我们需要让剖分的链是我们指定的链,以便利用来求解
实链剖分
对于一个点连向它所有儿子的边,我们自己选择一条边进行剖分,我们称被选择的边为实边,其他边则为虚边。
我们称实边所连接的儿子为实儿子,实边组成的链称之为实链
选择实链剖分的最重要的原因便是因为实链是我们选择的,灵活且可变
正是它的这种灵活可变性,用
Splay
来维护这些实链
Link-Cut Tree
我们可以把 LCT
理解为用一些 Splay
来维护动态树剖并实现动态树上的区间操作
每条实链都建一个 Splay
维护整个链的区间信息
辅助树
我们认为一些
Splay
共同构成了一颗辅助树,每个辅助树都维护了一颗树,所有的辅助树构成了Link-Cut Tree
,维护了整个森林辅助树有很多性质
辅助树由多棵
Splay
组成,每棵Splay
都维护了树中一条严格在原树中「从上到下」深度单调递增的路径,且中序遍历这棵Splay
得到的点的深度序列单调递增原本的树的每个节点与辅助树的
Splay
节点一一对应。辅助树各棵
Splay
间并不独立。在LCT
中每棵Splay
的根节点的父亲节点指向原树中这条链的父亲节点(即链最顶端的点的父亲节点)。特殊的,这里的儿子认父亲,父亲却不认儿子,对应原树的一条 虚边
故每个连通块恰好有一个点的父亲节点为空
维护任何操作都不需要维护原树
辅助树可以在任何情况下拿出一个唯一的原树
只需维护辅助树即可
这是一颗原树 \(\gets\)
这是建出的辅助树 \(\gets\)
代码实现
这里只有 LCT
特有的几个操作
数组定义
fa[x] //x的父亲节点
son[x][2] //x的左右儿子
sz[x] //x的子树大小
rev[x] //x是否需要对儿子进行翻转
splay
操作和正常
splay
不同的是LCT
的每次splay
影响的所有点都必须是当前splay
中的钱而且在
splay
操作前必须把它的所有祖先全都pushdown
,因为LCT
不一定把哪个点应用splay
操作代码
inline bool isroot(int x){
return ((son[fa[x]][0]==x)||(son[fa[x]][1]==x));
}
inline void splay(int x){
int y=x,z=0;
st[++z]=y;
while(isroot(y)){
st[++z]=y=fa[y];
}
while(z){
push_down(st[z--]);
}
while(isroot(x)){
y=fa[x],z=fa[y];
if(isroot(y))
rotate((son[y][0]==x)^(son[z][0]==y)?x:y);
rotate(x);
}
push_up(x);
}
access
操作LCT
最重要的操作,其他所有操作都要用到它含义是访问某节点,作用是对于访问的节点 \(x\) 打通一条从树根到 \(x\) 的实链
如果有其他实边与新的实链相连则改为轻边
可以理解为专门开辟一条从 \(x\) 到 \(root\) 的路径,用
splay
来维护这条路径实现方法
先把 \(x\) 旋转到所在
Splay
的根用 \(y\) 记录上一次的 \(x\) (初始化\(y=0\)),把 \(y\) 接到 \(x\) 的右儿子上
这样就把上一次的实链接到了当前实链下
它原来的右儿子(也就是
LCT
树中在 \(x\) 下方的点)与它所有的边自然变成了虚边记得
pushup
代码
inline void access(int x){
for(int y=0;x;x=fa[y=x])
splay(x),
rc=y,push_up(x);
}
换根操作
作用是把某个节点变成树根(这里的根指的是整颗
LCT
的根)再加上
access
操作就能方便的提取出LCT
上两点之间距离提取\(u\)到\(v\)的路径只需要
toroot(u),access(v)
,然后\(v\)所在的Splay
对应的链就是\(u\)到\(v\)的路径实现方法
先
access
一下,这样 \(x\) 就一路打通到了根,然后再splay(x)
,由于x
是这条实链最下面的点,所以 \(x\) 的splay
的右儿子是空的,左儿子是它上面所有点因为
splay
是支持区间翻转的,所以只要给x打个翻转标记就翻转到根了代码
inline void toroot(int x){
access(x);
splay(x);
reserve(x);
}
link
操作作用是链接两个辅助树,对于
link(u,v)
,表示 \(u\) 所在的辅助树和 \(v\) 所在的辅助树实现方法
只需要先
toroot(u)
,然后记fa[u]=v
就可以了,就是把一整颗辅助树连到另一个点上代码
inline void link(int x,int y){
toroot(x);
if(Find(y)!=x)
fa[x]=y;
}
cut
操作这个操作作用是切断某条边
实现方法
先分离出 \(x\) 到 \(y\) 的这条链
我们假设切断的点一定是相邻的(不相邻的特判掉),然后把 \(y\) 的左儿子(也就是
LCT
中 \(y\) 的父亲)与 \(y\) 的边断掉就好了代码
inline void split(int x,int y){
toroot(x);
access(y);
splay(y);
}
inline int Find(int x){
access(x);
splay(x);
while(lc)
push_down(x),x=lc;
splay(x);
return x;
}
inline void cut(int x,int y){
toroot(x);
if(Find(y)==x&&fa[y]==x&&!son[y][0]){
fa[y]=son[x][1]=0;
push_up(x);
}
}
完整代码
点击查看代码
#define lc son[x][0]
#define rc son[x][1]
int fa[N],son[N][2],val[N],ans[N],st[N];
bool rev[N];
inline bool isroot(int x){
return ((son[fa[x]][0]==x)||(son[fa[x]][1]==x));
}
inline void push_up(int x){
ans[x]=ans[lc]^ans[rc]^val[x];
}
inline void reserve(int x){
int t=lc;
lc=rc;rc=t;
rev[x]^=1;
}
inline void push_down(int x){
if(rev[x]){
if(lc)reserve(lc);
if(rc)reserve(rc);
rev[x]=0;
}
}
inline void rotate(int x){
int y=fa[x],z=fa[y],k=son[y][1]==x,w=son[x][!k];
if(isroot(y))
son[z][son[z][1]==y]=x;
son[x][!k]=y;
son[y][k]=w;
if(w)
fa[w]=y;
fa[y]=x;fa[x]=z;
push_up(y);
}
inline void splay(int x){
int y=x,z=0;
st[++z]=y;
while(isroot(y)){
st[++z]=y=fa[y];
}
while(z){
push_down(st[z--]);
}
while(isroot(x)){
y=fa[x],z=fa[y];
if(isroot(y))
rotate((son[y][0]==x)^(son[z][0]==y)?x:y);
rotate(x);
}
push_up(x);
}
inline void access(int x){
for(int y=0;x;x=fa[y=x])
splay(x),
rc=y,push_up(x);
}
inline void toroot(int x){
access(x);
splay(x);
reserve(x);
}
inline int Find(int x){
access(x);
splay(x);
while(lc)
push_down(x),x=lc;
splay(x);
return x;
}
inline void split(int x,int y){
toroot(x);
access(y);
splay(y);
}
inline void link(int x,int y){
toroot(x);
if(Find(y)!=x)
fa[x]=y;
}
inline void cut(int x,int y){
toroot(x);
if(Find(y)==x&&fa[y]==x&&!son[y][0]){
fa[y]=son[x][1]=0;
push_up(x);
}
}
signed main(){
int n,m;FastI>>n>>m;
for(int i=1;i<=n;++i)
FastI>>val[i];
while(m--){
int opt,x,y;
FastI>>opt>>x>>y;
if(opt==0){
split(x,y);
FastO<<ans[y]<<endl;
}
else if(opt==1){
link(x,y);
}
else if(opt==2){
cut(x,y);
}
else if(opt==3){
splay(x);
val[x]=y;
}
}
}
进阶一点的操作/配套题目
P1501 [国家集训队] \(\text{Tree II}\)
依然是链操作,但是有区间加法和区间乘法操作
这里参考了动态树大师
FlashHu
的题解区间加法
先用
spilt
操作把x
到y
的链分离出来然后整体直接加上
lazy
标记即可,并且在pushdown
操作时额外加入推平操作即可核心代码也很简单
inline void push_add(int x,int c){
(val[x]+=c*sz[x])%=51061;
(v[x]+=c)%=51061;
(lazy_add[x]]+=c)%=51061;
}
在
pushdown
操作翻转前加入以下代码if(lazy_add[x]){
push_add(lc,lazy_add[x]),
push_add(rc,lazy_add[x]),
lazy_add[x]=0;
}
区间乘法
也是先用
spilt
操作分离,然后挂lazy
标记,pushdown
操作加入乘法标记注意要先
pushdown
乘法的lazy
标记,再pushdown
加法的核心代码
inline void push_mul(int x,int c){
(val[x]*=c)%=51061;
(v[x]*=c)%=51061;
(lazy_mul[x]*=c)%=51061;
(lazy_add[x]*=c)%=51061;
}
在
pushdown
操作内pushdown
加法前加入以下代码if(lazy_mul[x]!=1){
push_mul(lc,lazy_mul[x]),
push_mul(rc,lazy_mul[x]),
lazy_mul[x]=1;
}
那么就非常好搞了
核心代码都在上面了
-
维护两点是否连通,但是包含了断边操作和连边操作
这样普通的并查集就不好维护了,但是可以考虑使用
LCT
来维护维护方法就是直接对两点进行
Find
,如果Find
的结果相同那就是联通的核心代码
if(opt=="Q"){
FastO<<((Find(x)==Find(y))?"Yes":"No")<<endl;
}
-
和上一题是双倍经验,基本区别不大
核心代码和上面一样
-
这道题与一般的
LCT
似乎有一点不同给的不是点权,那怎么办呢
错误思考示范
诶树剖的时候好像也有这个问题,是不是可以参考树剖
用儿子节点记录其到其父亲的边权,然后云云
但是这样有个问题,就是说
LCT
使用splay
维护的,splay
是会破坏原本的父子关系的那么这个做法宣告破产了wwwww
根据
FalshHu
的讲解,我们可以得知有两种解法把边置于
LCT
外,然后在LCT
节点中维护父边和重子边的编号,需要更新信息时从外部获取但是这种方法有一个问题:需要在
access
操作,Link
操作,Cut
操作都进行修改,很麻烦建立额外的节点
我们可以建立额外的表示边的节点,然后把表示点的节点的权值都设为\(0\)
这样我们就可以把原本的边权转化为点权来让
LCT
去维护此时普通的
Link
和Cut
都存在一些问题Link
和Cut
操作只能添加/删除一条边,而不能删除代表边权的边解决方法也很简单,直接
Link
/Cut
两次即可核心代码
link(a[i].x,a[i].id);
link(a[i].id,a[i].y);
我们通常选择建立代表边的节点,也就是第二种方法
在本题中建立边的方式如下
for(int i=1;i<n;++i){
FastI>>x>>y>>val[i+n];
link(x,i+n);
link(y,i+n);
}
这里的求
max
后取反如何维护呢?我们发现只要取min
即可,这样取相反数后的结果就是max
那么这道题就非常容易的做出来了
-
树剖板子题,不用LCT也能写但是这里用的LCT
需要边权转点权
-
- 不断加边,判环,取较优者。
LCT
动态维护生成树(边权的最大值最小),似乎不是很好维护删边(因为不能对于每次删除都进行一次最小生成树,不然复杂度爆炸了)但是本题只有删边操作,所以我们可以先对操作离线,然后倒过来变为加边
先把所有边都删掉,这里保证了任何时刻图都是联通的,所以就可以来离线完成
如何加入边呢
在加入一条边之后会形成一个环,此时从任意一点进入环,从另一点出环,可以从环上两个方向走,那么最优解总可以避开最长的一条边
我们先
split(x,y)
提取出x
到y
的最大权值,然后看加入的边,如果比原来的最大权值小就可以直接断掉原来的最大权值那条边这里直接平衡树查找就行,所以我觉得map更好做其实
在倒序跑完最小生成树之后就可以直接维护了
for(int i=q-1;i>=0;--i){
int x=vec[i].a,y=vec[i].b;
split(x,y);
if(vec[i].k==1){
sta.push(T[ans[y]].val);
}
else{
if(T[vec[i].num].val<T[ans[y]].val){
cut(T[ans[y]].s,j+n);
cut(ans[y]+n,T[ans[y]].son);
link(x,vec[i].num+n);
link(vec[i].num+n,y);
}
}
}
这里的
sta
用于记录答案 -
坏了,要维护子树的
siz
了,我们优秀的LCT只能比较容易的维护链信息而弱于子树信息但是还要动态加边所以要用LCT,不太能直接树剖
所以我们需要对LCT进行一定修改使其适于子树操作
我们按照对链剖分的方式把子树分为虚子树和实子树,其中实子树就是一条实链,可以直接通过
splay
获取那么瓶颈主要就在虚子树上(因为已知实子树的信息,只要知道虚子树的信息和就可以求出整个子树的信息了)
我们这里设虚子树
siz
为si[x]
,整棵子树的siz
为s[x]
考虑对原本的操作进行修改
splay
操作这一操作基于
splay
树且只会修改在splay
树中的相对位置,众所周知splay
树中的相对位置对于虚子树是不会有任何影响的,所以不需要修改access
操作access
操作的含义是作用是对于访问的节点 \(x\) 打通一条从树根到 \(x\) 的实链然后会修改实边虚边,所以会对虚子树产生影响,得到一个虚儿子,失去一个虚儿子
直接改就行
inline void access(int x){
for(int y=0;x;x=fa[y=x]){
splay(x);
si[x]+=s[rc];
si[x]-=s[y];
rc=y;
pushup(x);
}
}
和普通 access 操作的对比
这是普通的
access
inline void access(int x){
for(int y=0;x;x=fa[y=x]){
splay(x);
rc=y;
pushup(x);
}
}
不同点只是
si
加上原来右儿子的s
,再减去新的右儿子的s
toroot
操作换根,但是我们发现
toroot
只是在实链上的翻转,所以对虚子树没有影响不用改
findroot
操作这显然没影响,并没有改变树的形态
split
操作分离操作
这里实现时只是调用了
toroot(x),access(y),splay(y);
三个函数而有影响的
access
操作我们已经在前面改了,所以这个没有影响link
操作连边操作
当连接一条边时,虚子树的信息会发生改变(因为多了一个虚边)
那么
s[y]
和si[y]
都加上s[x]
就行但是这里只更新了
y
,y
的祖先没更新,所以会寄只要先把
y
转到根,这样y
就没祖先了inline void link(int x,int y){
toroot(x);access(y);splay(y);
fa[x]=y;si[y]+=s[x];
pushup(y);
}
cut
操作断边操作
cut
操作会断掉一条实边,不会影响虚子树,建议不理pushup
操作这个直接把虚子树和实子树加起来就行
inline void pushup(int x){
s[x]=s[lc]+s[rc]+si[x]+1;
}
回过头来看这道题,发现这是板子
对于操作2,询问的是断掉
(x,y)
之后x
和y
的子树大小乘积直接做就行
核心代码
if(opt=='A'){
link(x,y);
}
else{
cut(x,y);
FastO<<(s[x])*(s[y])<<endl;
link(x,y);
}
【学习笔记】 - 基础数据结构 :Link-Cut Tree(进阶篇)的更多相关文章
- 【学习笔记】LCT link cut tree
大概就是供自己复习的吧 1. 细节讲解 安利两篇blog: Menci 非常好的讲解与题单 2.模板 把 $ rev $ 和 $ pushdown $ 的位置记清 #define lc son[x][ ...
- C学习笔记-基础数据结构与算法
数据结构 数据(data)是对客观事物符号表示,在计算机中是指所有能输入的计算机并被计算机程序处理的数据总称. 数据元素(data element)是数据的基本单位,在计算机中通常做为一个整体进行处理 ...
- python学习笔记十三 JS,Dom(进阶篇)
JS介绍 JavaScript 是属于网络的脚本语言!JavaScript 被数百万计的网页用来改进设计.验证表单.检测浏览器.创建cookies,以及更多的应用:JavaScript 是因特网上最流 ...
- python 学习笔记八 进程和线程 (进阶篇)
什么是线程(thread)? 线程是操作系统能够进行运算调度的最小单位.它被包含在进程之中,是进程中的实际运作单位.一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执 ...
- Link Cut Tree学习笔记
从这里开始 动态树问题和Link Cut Tree 一些定义 access操作 换根操作 link和cut操作 时间复杂度证明 Link Cut Tree维护链上信息 Link Cut Tree维护子 ...
- 学习笔记:Link Cut Tree
模板题 原理 类似树链剖分对重儿子/长儿子剖分,Link Cut Tree 也做的是类似的链剖分. 每个节点选出 \(0 / 1\) 个儿子作为实儿子,剩下是虚儿子.对应的边是实边/虚边,虚实时可以进 ...
- LCT总结——概念篇+洛谷P3690[模板]Link Cut Tree(动态树)(LCT,Splay)
为了优化体验(其实是强迫症),蒟蒻把总结拆成了两篇,方便不同学习阶段的Dalao们切换. LCT总结--应用篇戳这里 概念.性质简述 首先介绍一下链剖分的概念(感谢laofu的讲课) 链剖分,是指一类 ...
- link cut tree 入门
鉴于最近写bzoj还有51nod都出现写不动的现象,决定学习一波厉害的算法/数据结构. link cut tree:研究popoqqq那个神ppt. bzoj1036:维护access操作就可以了. ...
- Link Cut Tree 总结
Link-Cut-Tree Tags:数据结构 ##更好阅读体验:https://www.zybuluo.com/xzyxzy/note/1027479 一.概述 \(LCT\),动态树的一种,又可以 ...
- Python学习笔记基础篇——总览
Python初识与简介[开篇] Python学习笔记——基础篇[第一周]——变量与赋值.用户交互.条件判断.循环控制.数据类型.文本操作 Python学习笔记——基础篇[第二周]——解释器.字符串.列 ...
随机推荐
- Gorm 入门介绍与基本使用
Gorm 入门介绍与基本使用 目录 Gorm 入门介绍与基本使用 一.ORM简介 1.1 什么是ORM 1.2 使用ORM的好处 1.2.1 避免直接操作SQL语句 1.2.2 提高代码的可维护性 1 ...
- TienChin 验证码响应结果分析&验证码生成接口分析
验证码响应结果分析 首先从前端开始进行分析,进入到登录页面,打开开发者工具(f12),找到 network,f5 刷新一下页面,然后,筛选一下,筛选内容为 Fetch/XHR: 你会发现列表中有两项内 ...
- LyScriptTools 调试控制类API接口手册
LyScriptTools模块中的DebugControl类主要负责控制x64dbg调试器的行为,例如获取或设置寄存器组,执行单步命令等,此类内的方法也是最常用的. 插件地址:https://gith ...
- vue-cropper 移动端上传图片压缩裁剪
头像裁剪压缩上传流程: 点击头像--判断是否为IOS端--若是--A,否则--BA:选择图片 --CB:弹框供用户选择从相册选择或者调用相机拍照--选择图片--CC:出现cropper裁剪框,裁剪框位 ...
- 性价比超频我都要 两大内存绝技带来20%性能提升!技嘉雪雕Z790 AORUS LITE AX-W主板评测
一.前言:主打性价比.两大内存绝技加持的技嘉Z790主板 要说现在最主流的装机方案,那必然是13代酷睿+700系主板.我们此前曾测试过技嘉的Z790钛雕主板,独有的顶级表现让人印象深刻,不过近6K的价 ...
- Pandas resample数据重采样
随机抽样,是统计学中常用的一种方法,它可以帮助我们从大量的数据中快速地构建出一组数据分析模型.在 Pandas 中,如果想要对数据集进行随机抽样,需要使用 sample() 函数. sample() ...
- 近五年的APIO
[APIO2018] 铁人两项 题意:给定一个张图,询问其中有多少个有序三元组 \((u,v,w)\),满足存在一条从 \(u\) 到 \(w\) 的简单路径,经过点 \(v\). 考虑建出原图的圆方 ...
- NC54586 小翔和泰拉瑞亚
题目链接 题目 链接:https://ac.nowcoder.com/acm/problem/54586 来源:牛客网 题目描述 小翔爱玩泰拉瑞亚 . 一天,他碰到了一幅地图.这幅地图可以分为 \(n ...
- Java图片加水印
采用Java自带的Image IO 废话不多说,上菜 1. 文字水印 1 import sun.font.FontDesignMetrics; 2 3 import javax.imageio.Im ...
- linux删除目录下指定文件方法
1.删除当前目录下文件名含有2013的文件 ls | grep 2013 | xargs rm --To be continue...