LCT

Upd:

一个细节:假如我们要修改某个节点的数据,那么要先把它makeroot再修改,改完之后pushup。

LCT是一种维护森林的数据结构,本质是用Splay维护实链剖分。

实链剖分大概是这样的:每个节点往一个儿子连实边,其它的儿子连虚边。

而我们用Splay维护实链剖分后的每一条实链。

因此LCT有一些基本的性质:

\(1.\)每一棵Splay维护树上一条直上直下的实链,且其中序遍历的点的序列的深度递增。

\(2.\)每个节点包含且仅包含于一棵Splay。

\(3.\)实边包含于Splay中,而虚边则连接两棵Splay。对于每个节点而言,它会记录它在Splay中的父亲,而play的根节点的父亲则是原树中这棵Splay代表的链的链顶的父亲。但是每个节点只会记录它在Splay中的左右儿子,并不会记录由虚边连接的儿子。(认父不认子

下面如果没有特殊说明,我们默认我们所说的树为Splay而非原树。

然后我们先来说几个预备的基本操作:

nrooot

nroot实现判断一个节点是否为该节点所在Splay的根节点。

根据认父不认子的特性,我们只需要判断该节点的父亲是否有它这个儿子即可。

int nroot(int x){return ch[fa[x]][0]==x||ch[fa[x]][1]==x;}

pushrev

pushrev实现把一个子树翻转。

在后面的操作需要用到。

具体为交换左右儿子,并给左右儿子打上翻转标记。

void pushrev(int x){swap(lc,rc),rev[x]^=1;}

pushdown

pushdown实现下放标记。

在后面的操作中我们有翻转整棵子树的标记。

有的题目可能还会有其它的标记。

void pushdown(int x){if(!rev[x])return ;rev[x]=0,pushrev(lc),pushrev(rc);}

pushall

pushall实现把某个点到该点所在Splay根节点路径上的标记全部下放。

在后面的操作需要用到。

可以用栈实现不过函数堆栈更加方便。

void pushall(int x){if(nroot(x))pushall(fa[x]);pushdown(x);}

然后我们来分析一下Splay的基本操作:

这里我们需要支持的基本上就只有rotate和splay两个操作了。

rotate

和一般的Splay没有什么较大的差别。

判断一下当前节点的父亲是否为当前Splay的根,免得破坏认父不认子的特性。

void rotate(int x)
{
int y=fa[x],z=fa[y],k=ch[y][1]==x;
if(nroot(y)) ch[z][ch[z][1]==y]=x;
fa[x]=z,fa[y]=x,fa[ch[x][!k]]=y,ch[y][k]=ch[x][!k],ch[x][!k]=y,pushup(y);
}

splay

和一般的Splay没有什么较大的差别。不过我们现在只需要旋转到根节点了。

不过在splay之前需要pushall一下,典型的查询前pushdown

void splay(int x)
{
pushall(x);
for(int y;nroot(x);rotate(x)) if(nroot(y=fa[x])) rotate((ch[fa[y]][0]==y)^(ch[y][0]==x)? y:x);
pushup(x);
}

然后就是LCT的基本操作了。

LCT所有的操作都依赖于一个核心操作:access。

access

access实现将原树中某个点到根的路径变为一条实链,单独拿出来做一棵Splay。

假设我们要拉的是\(x\)点。

首先我们把\(x\)Splay一下。

这时\(x\)的左子树中的点都会在这条实链上,而右子树的点都不在。所以我们把\(rc\)置为\(0\)。(注意认父不认子的特性)

然后我们跳到\(y=fa_x\)(根据上面的性质,我们跳到的实际上就是原树中\(x\)当前实链链顶的父亲。),把y\(Splay\)一下。

那么此时\(y\)的左子树中的点还是都在这条实链上,而右子树的点都不在。所以我们把刚才的\(x\)接在\(y\)的右儿子处。

注意因为修改后pushup的原则,我们需要在更新右儿子的时候pushup一下。

这样一直做到原树的根为止即可。

void access(int x){for(int y=0;x;x=fa[y=x])splay(x),rc=y,pushup(x);}

makeroot

相比access,makeroot更进一步地实现了把一个节点\(x\)转成原树中的根节点。

原理还是很简单的。我们先把\(x\)access一下,然后把\(x\)splay到它所在Splay的根节点。

此时因为它是原树中这条链上深度最大的点,所以它没有右儿子。

为了让它变成原树中深度最小的点,我们把它的左右子树交换一下,这样他就没有左儿子了,就变成了原树中深度最小的点。

交换子树可以通过打标记的方法来完成。

void makeroot(int x){access(x),splay(x),pushrev(x);}

findroot

findroot实现找到一个节点\(x\)所在原树中的根节点。

和makeroot类似,我们先access、splay\(x\)。

那么此时\(x\)所在原树中的根就是它左儿子中一直跳左儿子最后到的点。

根据查询前pushdown的原则我们要一边跳左儿子一边pushdown。

Upd:根据xzz的说法这里由于我们pushdown的写法可以不用pushdown。

最后可以顺便把查询到的根节点给splay一下。

int findroot(int x){access(x),splay(x);while(lc)x=lc;return splay(x),x;}

split

split实现把原树中一条链\((x,y)\)单独拿出来做一条实链。

很轻松地,我们先让\(x\)做原树的根,然后拉一条\((x,y)\)的实链出来,再让\(y\)做这条实链的Splay的根。这样我们在过程中通过pushup和pushdown就会让这条实链(这棵Splay)上的信息反映到这棵Splay的根节点\(y\)上。

然后如果我们要查询一条路径的信息,就可以快乐地split然后查\(y\)的信息了。

void split(int x,int y){makeroot(x),access(y),splay(y);}

下面则是一些真正意义上LCT能做而树剖做不了的东西了。

link

link实现在两点\(x,y\)之间新连一条边\((x,y)\)。(如果\(x,y\)连通就不连)

保证\(x,y\)不连通:

直接让\(x\)做\(x\)所在原树的根,然后把\(fa_x\)置为\(y\)即可。

void link(int x,int y){makeroot(x),fa[x]=y;}

不保证\(x,y\)不连通:

还要判一下连通性。即\(x\)成为原树所在的根之后,判断\(y\)所在原树的根是否为\(x\)。

void link(int x,int y){makeroot(x);if(findroot(y)^x)fa[x]=y;}

cut

cut实现断掉边\((x,y)\)。(如果不存在边\(x,y\)就不断)。

保证\((x,y)\)存在:

先把\((x,y)\)给split出来。

然后根据split的写法,此时\(y\)一定是该Splay的根,\(x\)一定是该原树的根,所以\(x\)一定是\(y\)的左儿子,那么我们直接断就完事了。

记得pushup。

void cut(int x,int y){split(x,y),fa[x]=ch[y][0]=0,pushup(y);}

不保证\((x,y)\)存在:

先判断连通性。

由于我们makeroot了,所以此时\(x\)一定为原树的根。

又由于我们findroot了,所以此时\(x\)一定是这条链的Splay的根。

那么如果此时\(y\)是\(x\)的右儿子,\(x\)是\(y\)的父亲,且\(y\)没有左儿子(\(x,y\)之间没有其它深度的点),那么\((x,y)\)就是存在的。

事实上如果满足了\(x\)是\(y\)的父亲,\(y\)没有左儿子,那么\((x,y)\)就是存在的。

因为\(x\)不可能有左儿子。

void cut(int x,int y){makeroot(x);if(findroot(y)==x&&fa[y]==x&&!ch[y][0])fa[y]=rc=0,pushup(x);}

这样我们就成功完成了LCT的基本操作。

然后就是一些技巧性的东西了:

LCT维护e-dcc:

删边似乎不太好做?只考虑加边(虽然树剖也能做)。

假如加入的边的两端不连通就直接加入,否则我们就会形成一个e-dcc,此时缩点即可。

LCT维护v-dcc:

圆方树?溜了溜了。

LCT维护边权:

一般而言我们最基本的思路是把边权放到其深度更大的那一端。

但是由于LCT会改变父子关系所以这东西没办法做了。

因此我么考虑拆点,把一条边拆成一个点,向其两端连边。

然后就可以做了。

LCT维护子树:

这是个大头。

如果会Top Tree的话似乎会轻松很多。

对于某些较容易维护的信息,我们可以考虑开一个新的数组来记录每个点的所有轻儿子所在子树的信息和,那么这个点的子树信息和就是两个重儿子的子树信息和加上所有轻儿子的子树信息和了。

(下面的\(sum\)表示该节点所有轻儿子的子树信息和,\(Sum\)表示该节点子树的信息和)

这样对于pushup操作,我们要多加一点东西。

void pushup(int x){Sum[x]=Sum[lc]+Sum[rc]+sum[x]+1;}

对于改变了轻重关系的操作,我们需要实时维护这个东西。比如access。

void access(int x){for(int y=0;x;x=fa[y=x])splay(x),sum[x]+=Sum[rc],sum[x]-=Sum[rc=y];}

link:

\(y\)多了一个轻儿子,所以要进行更改。

所以\(y\)在Splay中的祖先也要跟着更改,这太麻烦了。

我们把\(y\)旋转到它所在Splay的根,就不用了改\(y\)的祖先了。

保证\(x,y\)不连通:

void link(int x,int y){split(x,y),sum[fa[x]=y]+=Sum[x],pushup(y);}

这里的split是一个偷懒的写法。

不保证\(x,y\)不连通:

void link(int x,int y){makeroot(x);if(findroot(y)^x)sum[fa[x]=y]+=Sum[x],pushup(y);}

cut:

断掉的一定是重边,因为我们pushup了所以并没有特殊的影响。

LCT维护染色连通块:

可以直接开一个LCT,把同色的点连起来,不过很辣鸡。

更加优秀一点的是每个颜色开一个LCT。

还有一些诸如把点的颜色赋给其父边的技巧。

动态树(LCT、Top Tree、ETT)的更多相关文章

  1. hdu 5398 动态树LCT

    GCD Tree Time Limit: 5000/2500 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Su ...

  2. hdu 5002 (动态树lct)

    Tree Time Limit: 16000/8000 MS (Java/Others)    Memory Limit: 65536/65536 K (Java/Others)Total Submi ...

  3. HDU 4718 The LCIS on the Tree (动态树LCT)

    The LCIS on the Tree Time Limit: 6000/3000 MS (Java/Others)    Memory Limit: 65535/65535 K (Java/Oth ...

  4. [模板] 动态树/LCT

    简介 LCT是一种数据结构, 可以维护树的动态加边, 删边, 维护链上信息(满足结合律), 单次操作时间复杂度 \(O(\log n)\).(不会证) 思想类似树链剖分, 因为splay可以换根, 用 ...

  5. 动态树LCT小结

    最开始看动态树不知道找了多少资料,总感觉不能完全理解.但其实理解了就是那么一回事...动态树在某种意思上来说跟树链剖分很相似,都是为了解决序列问题,树链剖分由于树的形态是不变的,所以可以通过预处理节点 ...

  6. 动态树LCT(Link-cut-tree)总结+模板题+各种题目

    一.理解LCT的工作原理 先看一道例题: 让你维护一棵给定的树,需要支持下面两种操作: Change x val:  令x点的点权变为val Query x y:  计算x,y之间的唯一的最短路径的点 ...

  7. bzoj2049-洞穴勘测(动态树lct模板题)

    Description 辉辉热衷于洞穴勘测.某天,他按照地图来到了一片被标记为JSZX的洞穴群地区.经过初步勘测,辉辉发现这片区域由n个洞穴(分别编号为1到n)以及若干通道组成,并且每条通道连接了恰好 ...

  8. SPOJ OTOCI 动态树 LCT

    SPOJ OTOCI 裸的动态树问题. 回顾一下我们对树的认识. 最初,它是一个连通的无向的无环的图,然后我们发现由一个根出发进行BFS 会出现层次分明的树状图形. 然后根据树的递归和层次性质,我们得 ...

  9. BZOJ 2002: [Hnoi2010]Bounce 弹飞绵羊 (动态树LCT)

    2002: [Hnoi2010]Bounce 弹飞绵羊 Time Limit: 10 Sec  Memory Limit: 259 MBSubmit: 2843  Solved: 1519[Submi ...

  10. HDU 5002 Tree(动态树LCT)(2014 ACM/ICPC Asia Regional Anshan Online)

    Problem Description You are given a tree with N nodes which are numbered by integers 1..N. Each node ...

随机推荐

  1. Miller Robin大素数判定

    Miller Robin算法 当要判断的数过大,以至于根n的算法不可行时,可以采用这种方法来判定素数. 用于判断大于2的奇数(2和偶数需要手动判断),是概率意义上的判定,因此需要做多次来减少出错概率. ...

  2. Work Queues(工作队列)

    1.模型 2.创建生产者 package com.dwz.rabbitmq.work; import java.io.IOException; import java.util.concurrent. ...

  3. 使用EasyPrint实现不预览直接打印功能_非JS打印

    插件地址 github 下载插件,安装后将在注册表中添加EasyPrint的协议 随后可以在开始->运行中输入EasyPrint://1&test 进行测试  参数分为两部分使用[&am ...

  4. "并发用户数量"的正确英文表示

    并发用户数量the number of concurrent users 最佳并发用户数量the optimum number of concurrent users 最大并发用户数量 the max ...

  5. IntelliJ IDEA 设置检查 serialVersionUID

    IntelliJ IDEA 设置检查 serialVersionUID 背景介绍 我们在使用IntelliJ IDEA创建Java类的时候,有时候需要实现序列化接口 implements Serial ...

  6. python与正则

    想了解正则的使用,请点击:正则表达式.每种编程语言有一些独特的匹配方式,python也不例外: 语法 含义 表达实例 完整匹配匹配的字符串 \A 仅匹配字符串开头 \Aabc abc \Z 仅匹配字符 ...

  7. 【DataBase】mysql连接错误:Cannot get hostname for your address

    问题 环境:win7 + 64Bit + 本地mysql5.6 问题:navicat连接本地mysql数据库,提示“Cannot get hostname for your address”,但是连接 ...

  8. docker遇到防火墙报错问题解决方法

    -- 报错信息[root@localhost docker]# docker run -d -p 5000:5000 training/webapp python app.pycc61442060cb ...

  9. np的concatenate和pandas的groupby

    1. concatenate concatenate函数可以实现对两个张量进行拼接,这个张量可以实一维向量,二维矩阵等等 1. 首先定义四个列表,然后用concatenate把他们拼接起来,这里我设a ...

  10. 《0day安全-软件漏洞分析技术》实验笔记2

    实验 3.4 通用shellcode 工具 Windows XP SP3 Visual C++ 6.0 OD IDA 源代码 见随书代码,参考:https://github.com/jas502n/0 ...