前言:

  Link-Cut Tree简称LCT是解决动态树问题的一种数据结构,可以说是我见过功能最强大的一种树上数据结构了。在此与大家分享一下LCT的学习笔记。提示:前置知识点需要树链剖分和splay。

引例:

  在讲LCT之前先来看一道题:给一棵树,每个点有一个点权,多次操作,操作包含1、修改路径上点权2、查询路径上点权和。这道题显然用树链剖分+线段树就能做,但现在再加两个操作:3、删除树上的一条边4、连接两个点,保证连接后的联通块是一棵树。树的形态发生了改变,树链剖分+线段树这种静态数据结构显然做不了,我们要应用一些动态的数据结构来维护树上信息——平衡树(因为是区间操作所以采用splay,当然也可以用非旋转treap,不过比较麻烦),那么能否沿用树链剖分来解决呢?答案是可以的,但并不是上述的树链剖分方法而是沿用树链剖分的思想。因此可以将LCT看做是树链剖分+splay。

LCT的构建:

  对于一棵树上的一个点,我们依旧选出它的一个子节点作为它的重儿子(这里的重儿子不是根据子树大小决定的),每个点与重儿子之间的边为重边,重边连成的链为重链。因为是动态树,所以重儿子是可变的。对于每一条重链,我们用一棵splay来维护链的信息,splay的key值为每个点的深度。每棵splay的根节点指向这棵splay维护的链的链头的父节点,这里注意是单方向指向,splay根节点能找到指向的链头父节点,但从这个父节点找不到这棵splay的根节点。通俗点说就是父亲不认儿子,儿子认父亲。我们将这些splay组成的树成为辅助树。总的来说,对于原树的一个节点:和它的重儿子在同一棵splay中,被它的轻儿子所在splay的根节点指向。注意辅助树与原树的结构并不相同。如下图所示,无向边是splay中的边,有向边是上述说的儿子认父亲的指向边。

LCT的基本操作:

先声明一下变量:s[x][0/1]代表x的左右子节点,f[x]表示x的父节点,st[]代表splay时用到的栈,r[x]代表旋转标记

is_root

用途:判断一个点是否是它所在的splay的根。

实现:因为splay的根与其父亲之间是单向指向的边,所以只要判断它的父节点的左右子节点都不是它就好了。

补充:LCT中有许多splay,因此判断splay根的操作与通常splay略有不同。

int is_root(int rt)
{
return s[f[rt]][0]!=rt&&s[f[rt]][1]!=rt;
}

splay

用途:将一个点旋到它所在splay的根。

实现:先将这个点到splay的根路径上的点都记录下来,从上往下下传标记后按正常splay那样旋到根即可。

补充:因为后续有一个操作需要区间翻转,而在splay之前可能在所旋点到根路径上还存有标记。

void splay(int rt)
{
int top=0;
st[++top]=rt;
for(int i=rt;!is_root(i);i=f[i])
{
st[++top]=f[i];
}
for(int i=top;i>=1;i--)
{
pushdown(st[i]);
}
for(int fa;!is_root(rt);rotate(rt))
{
if(!is_root(fa=f[rt]))
{
rotate(get(rt)==get(fa)?fa:rt);
}
}
}

access

用途:将原树中一个点到根的路径变成一条重链并将这个点与它子节点间的重链断开。也就是将这个点到根路径上的所有点放到同一个splay中。

实现:假设要操作点是x,那么x一定是这条到根路径上深度最深的,将x的右子树设为0即切断了与子节点间的链(因为右子树中的点都是深度比他大的),再将x旋到当前splay的根处,然后将x跳到它的父节点(也就是它指向的节点),重复上述操作,但要记录上一次splay的节点,每次splay之后,将当前splay的节点的右儿子设为上次splay的节点。

补充:这是LCT中最重要的操作之一,也是查询路径信息时所必须的一步操作。

void access(int rt)
{
for(int x=0;rt;x=rt,rt=f[rt])
{
splay(rt);
s[rt][1]=x;
pushup(rt);
}
}

reverse

用途:将一个点旋成原树中的根。

实现:假设操作点为x,先将原树中x到根路径变成一条链access(x),再将x旋到它所在splay的根splay(x),这时x没有右儿子,它到原树根路径上的点都在它的左子树中,只要给x打一个旋转标记,这样它就没有了左子树,也就是没有深度比它小的点,它就成为了根。

补充:当询问路径(x,y)上的信息时,通常是先将x旋到原树的根reverse(x),再将y到根(也就是x)路径变成一条链access(y),这时y所在splay中的所有节点就都是原树x到y路径上的节点,只要再把y旋到splay的根O(1)查询即可。

void reverse(int rt)
{
access(rt);
splay(rt);
r[rt]^=1;
}

link

用途:连接两个点。

实现:例如连接x,y两个点,先将x旋到原树的根(因为只有这时x才没有父亲,可以指向),直接将它指向y即可。

补充:连接后只是x单向指向y。

void link(int x,int y)
{
reverse(x);
f[x]=y;
}

cut

用途:切断两个点之间的边。

实现:例如切断x,y两个点之间的边,先将x旋到原树的根reverse(x),再将y到原树根的路径变为一条链access(y),然后将y旋到所在splay的根splay(y),因为x,y之间有边,所以x一定是y的左子节点,将x的父亲及y的左儿子置0即可。

补充:有些题不保证x,y之间有边,因此要有一些特判(代码中有实现)。

void cut(int x,int y)
{
reverse(x);
access(y);
splay(y);
if(s[x][1]||f[x]!=y)
{
return ;
}
s[y][0]=f[x]=0;
}

find

用途:找到一个点所在原树的根节点。

实现:假设查找点为x,先将x到根路径变成一条链access(x),再将x旋到splay的根splay(x),这时因为根节点深度最小,所以根在x所在splay中最左子树中,直接一直找左子树,直到当前点没有左子节点为止,此时的点就是根。

补充:这个操作通常用于判断两个点的连通性。

int find(int rt)
{
access(rt);
splay(rt);
while(s[rt][0])
{
rt=s[rt][0];
}
return rt;
}

LCT的时间复杂度:

观察上述几个操作发现除了access之外易证其他操作单次都是均摊O(logn)。那么探究LCT的时间复杂度就在于探究access操作的时间复杂度。因为一次access的路径上指向的边有logn条,所以也就有logn次splay操作,那么这些splay操作是均摊logn的。具体证明参考杨哲的论文《QTREE 解法的一些研究》。

LCT维护原树子树信息:

上面只讲了LCT维护原树路径信息,那么LCT能否维护原树子树信息呢?答案是可以的。我们定义一个点的左右子节点为实儿子,指向它的点是它的虚儿子。那么原树路径信息就是实儿子子树信息之和,而原树子树信息其实就是实儿子和虚儿子子树信息之和。那么我们每个点维护两个信息,一个是总儿子信息,也就是原树中子树信息,一个是虚儿子的信息,上传直接像上述那样合并就好了。但能发现虚儿子信息不是一直不变的,观察在哪里改变了虚儿子信息。一个是在access时,另一个是在link时,access时每次往上爬都会将原来的右儿子变成虚儿子,将上次splay的点变成新的右儿子,这里要更新虚儿子信息,当然不管怎样他们都是这个点的儿子,因此总儿子信息不变。link时会把x指向y,y会多一个虚儿子,因此要更新y的虚儿子信息。这里注意严格意义上一个点在原树上的子树信息只包含虚儿子子树信息及实儿子中右儿子的子树信息(因为左儿子子树信息是这个点所在重链中比它深度浅的点的信息和),但因为我们每次查询时都将查询点变为原树的根,所以这个点在LCT上不存在左儿子,因此可以像上述那样维护信息。

以维护原树子树节点数为例,其中sum代表总儿子信息,size代表虚儿子信息。

access

void access(int rt)
{
for(int x=0;rt;x=rt,rt=f[rt])
{
splay(rt);
size[rt]+=sum[s[rt][1]]-sum[x];
s[rt][1]=x;
pushup(rt);
}
}

link

void link(int x,int y)
{
reverse(x);
reverse(y);
f[x]=y;
size[y]+=sum[x];
pushup(y);
}

LCT维护原树边上信息:

通过上述讲解可以发现LCT上的边并不是原树上的边,那么如果题目要求维护原树边上信息该怎么做呢?我们将原树上的边在LCT上也建立一个点来维护这条边的信息,例如:原树上有一条边为(x,y),我们新建一个点z来维护这条边的信息,当原树(x,y)这条边被连接上时,原本在LCT上应该link(x,y),现在改为link(x,z)和link(z,y),同样在删边时也要cut(x,z)和cut(z,y)。因为有删边操作,所以要记录原树每条边的两个端点。

TopTree:

上面说到了如何维护原树子树信息即维护LCT上轻儿子信息,那么如何修改原树的子树信息呢?因为一个点的轻儿子数量是不固定的,如果只是单纯的记录每个点的轻儿子并打标记下传的话,那么就无法保证下传的时间复杂度,所以我们引入了一种新的数据结构——TopTree。TopTree就是对于LCT上每个点,建立一个splay(即新建一些点组成一个splay),将splay的根作为这个点的轻儿子,而这个点原先所有的轻儿子则按一定顺序连到splay的每个节点下面作为他们的轻儿子(轻儿子顺序因题而异,且轻儿子顺序决定了新建splay的key值),TopTree的结构如图所示。

其中带箭头指向的是轻儿子,左边是LCT,右边是TopTree。1~6号节点为原树点,7、8号节点为用来管理轻儿子的建立的splay上的点。可以发现整棵TopTree分为两部分,维护一条重链的splay即原LCT上的splay和维护一个点轻儿子的splay即新建的splay。这样在维护轻儿子的splay上移动即可实现在一个点的不同轻儿子间移动,同样对于原树子树修改也可以通过下传到维护轻儿子的splay中再进一步下传到对应轻儿子所在重链的splay中来实现。因为LCT中的splay和TopTree中新建的splay作用不同,所以对于splay的所有操作都要在两种splay中分别实现即写两种splay的操作,代码量巨大且及其难调。因为每个点只有被当作轻儿子时才会在上面新建一个节点来维护他的信息,而每个点被当作轻儿子一次,所以新建节点数为$O(n)$。

LCT的练习题:

BZOJ2049[Sdoi2008]洞穴勘探(LCT模板题,只有link和cut)

BZOJ3282Tree(LCT模板题,单点修改,求路径异或和)

BZOJ2631tree(LCT模板题,路径加,路径乘,求路径点权和)

BZOJ2002[Hnoi2002]弹飞绵羊(LCT练习题,重点在于如何转化成LCT)

BZOJ3669[Noi2014]魔法森林(LCT经典题,利用LCT解决二维最小生成树)

BZOJ4530[Bjoi2014]大融合(LCT维护子树信息)

BZOJ3091城市旅行(LCT区间信息合并)

Link-Cut Tree(LCT)&TopTree讲解的更多相关文章

  1. 洛谷P3690 [模板] Link Cut Tree [LCT]

    题目传送门 Link Cut Tree 题目背景 动态树 题目描述 给定n个点以及每个点的权值,要你处理接下来的m个操作.操作有4种.操作从0到3编号.点从1到n编号. 0:后接两个整数(x,y),代 ...

  2. BZOJ 3282 Link Cut Tree (LCT)

    题目大意:维护一个森林,支持边的断,连,修改某个点的权值,求树链所有点点权的异或和 洛谷P3690传送门 搞了一个下午终于明白了LCT的原理 #include <cstdio> #incl ...

  3. Luogu 3690 Link Cut Tree

    Luogu 3690 Link Cut Tree \(LCT\) 模板题.可以参考讲解和这份码风(个人认为)良好的代码. 注意用 \(set\) 来维护实际图中两点是否有直接连边,否则无脑 \(Lin ...

  4. LCT总结——概念篇+洛谷P3690[模板]Link Cut Tree(动态树)(LCT,Splay)

    为了优化体验(其实是强迫症),蒟蒻把总结拆成了两篇,方便不同学习阶段的Dalao们切换. LCT总结--应用篇戳这里 概念.性质简述 首先介绍一下链剖分的概念(感谢laofu的讲课) 链剖分,是指一类 ...

  5. LuoguP3690 【模板】Link Cut Tree (动态树) LCT模板

    P3690 [模板]Link Cut Tree (动态树) 题目背景 动态树 题目描述 给定n个点以及每个点的权值,要你处理接下来的m个操作.操作有4种.操作从0到3编号.点从1到n编号. 0:后接两 ...

  6. Link Cut Tree学习笔记

    从这里开始 动态树问题和Link Cut Tree 一些定义 access操作 换根操作 link和cut操作 时间复杂度证明 Link Cut Tree维护链上信息 Link Cut Tree维护子 ...

  7. P3690 【模板】Link Cut Tree (动态树)

    P3690 [模板]Link Cut Tree (动态树) 认父不认子的lct 注意:不 要 把 $fa[x]$和$nrt(x)$ 混 在 一 起 ! #include<cstdio> v ...

  8. Link Cut Tree 总结

    Link-Cut-Tree Tags:数据结构 ##更好阅读体验:https://www.zybuluo.com/xzyxzy/note/1027479 一.概述 \(LCT\),动态树的一种,又可以 ...

  9. 【刷题】洛谷 P3690 【模板】Link Cut Tree (动态树)

    题目背景 动态树 题目描述 给定n个点以及每个点的权值,要你处理接下来的m个操作.操作有4种.操作从0到3编号.点从1到n编号. 0:后接两个整数(x,y),代表询问从x到y的路径上的点的权值的xor ...

随机推荐

  1. 九,ESP8266 判断是断电上电(强制硬件复位)之后运行的内部程序还是内部软件复位之后运行的程序(基于Lua脚本语言)

    现在我有一个需求,WIFI模块控制一个继电器,我要做的是如果内部程序跑乱了,造成了内部程序复位重启,那么控制继电器的状态不能改变 如果是设备断电了,然后又来电了,我需要的是继电器一定要是断开才好.不能 ...

  2. Oracle 在函数或存储过程中执行一条插入语句并返回主键ID值

    有时,我们需要往一张表插入一条记录,同时返回主键ID值. 假定主键ID的值都是通过对应表的SEQUENCE来获得,然后进行ID赋值 这里有几种情况需要注意: 1)如果建表语句含有主键ID的触发器,通过 ...

  3. Oracle 在函数或存储过程中执行sql查询字符串并将结果值赋值给变量

    请看黄色部分 --区县指标 THEN TVALUE_SQL := 'SELECT TO_CHAR(' || CUR_ROW.MAIN_FIELD || ') FROM ' || CUR_ROW.END ...

  4. 把DataTable转换为List<T>

    前一篇有学习过<把List<T>转换为DataTable>http://www.cnblogs.com/insus/p/8043173.html 那此篇,将是学习反向,把Dat ...

  5. java jdk 配置

    1.配置 C:\Program Files\Java\jdk1.8.0_131\bin 路径 到环境变量 Path

  6. python 常见矩阵运算

    python 的 numpy 库提供矩阵运算的功能,因此我们在需要矩阵运算的时候,需要导入 numpy 的包. 1.numpy 的导入和使用 from numpy import *;#导入numpy的 ...

  7. cgroup.conf系统初始配置

    # Slurm cgroup support configuration file # # See man slurm.conf and man cgroup.conf for further # i ...

  8. PMO在组织中实现价值应做的工作

    PMO在组织中实现价值应做的工作 研发人员及项目经理常常对PMO有反感情绪,认为其不熟悉业务流程与技术.经常要求项目经理和研发人员提交形式化的材料,只审批和监控,不能为项目提供良好的服务.在很多企业, ...

  9. 快速零配置迁移 API 适配 iOS 对 IPv6 以及 HTTPS 的要求

    本文快速分享一下快速零配置迁移 API 适配 iOS 对 IPv6 以及 HTTPS 的要求的方法,供大家参考. 原文发表于我的技术博客 零配置方案 最新的苹果审核政策对 API 的 IPv6 以及 ...

  10. Nextcloud私有云盘在Centos7下的部署笔记

    搭建个人云存储一般会想到ownCloud,堪称是自建云存储服务的经典.而Nextcloud是ownCloud原开发团队打造的号称是“下一代”存储.初一看觉得“口气”不小,刚推出来就重新“定义”了Clo ...