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. js保留二位小数

    js保留小数常用有三种方法,可根据实际情况选择 方法一:使用toFixed保留两位小数 自带四舍五入与补位 var num1 = 55.3715; console.log(num1.toFixed(2 ...

  2. Eclipse改变成炫酷黑色主题

    有一个款Eclipse插件EclipseColorTheme,其官网地址是http://eclipsecolorthemes.org/,可以直接使用大量内置的样式主题模板,如果还不能满足你自己的个性化 ...

  3. C++入门经典-例6.21-比较string字符串,比较两个字符串

    1:使用“>”.“!=”.“>=”等比较运算符可以比较两个字符串的内容.比较的方法是将两个string字符串从头开始比较每一个字符,直到出现两者不一致.比较这两个不相同的字符的字面值,得出 ...

  4. Nginx事件管理之概念描述

    1. Nginx事件管理概述 首先,Nginx定义了一个核心模块ngx_events_module,这样在Nginx启动时会调用ngx_init_cycle方法解析配置项,一旦在 nginx.conf ...

  5. java加密算法相关

    简介 RSA公钥加密算法是1977年由罗纳德·李维斯特(Ron Rivest).阿迪·萨莫尔(Adi Shamir)和伦纳德·阿德曼(Leonard Adleman)一起提出的.1987年首次公布,当 ...

  6. 解决Git 报错:warning: LF will be replaced by CRLF

    Ruby命令 $ git init $ git add . 系统出现如下错误:warning: LF will be replaced by CRLF 原因分析:CRLF -- Carriage-Re ...

  7. 微服务一键启动脚本shell没有环境变量的

    #!/bin/bash#######################################################export JAVA_HOME=/root/data/app/jd ...

  8. yum源安装mysql数据库 添加密码

    学习mysql数据库时,安装的问题一直很烦恼,linux的不同版本的安装方式都可能不同,这里是我学习时的一些总结.也是刚刚开始学习,大佬勿喷,谢谢啦!!! 1.查看是否已经安装 2.如果没安装 yum ...

  9. [ubuntu]android SDK 与Gradle环境的安装与配置|搭建android基础开发/构建环境

    系统环境: linux:ubuntu18 已配置jdk 环境变量 切换到root账户 sudo su 安装Android-sdk (0)准备工作 切换到/usr/local目录: /usr/local ...

  10. 小D课堂 - 零基础入门SpringBoot2.X到实战_第14节 高级篇幅之SpringBoot多环境配置_59、SpringBoot多环境配置介绍和项目实战

    笔记 1.SpringBoot多环境配置介绍和项目实战(核心知识)     简介:SpringBoot介绍多环境配置和使用场景 1.不同环境使用不同配置         例如数据库配置,在开发的时候, ...