博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
LCT 模板及套路总结
阅读量:5918 次
发布时间:2019-06-19

本文共 6895 字,大约阅读时间需要 22 分钟。

这一个月貌似已经考了无数次\(LCT\)了.....

保险起见还是来一发总结吧.....

A. LCT 模板

\(LCT\) 是由大名鼎鼎的 \(Tarjan\) 老爷发明的。

主要是用来维护树上路径问题的。 它的神奇之处在于可以直接把一条路径抠出来维护。
其实就是维护树链剖分中的重链与轻链。
网上相关教程很多,直接给板子(其实是我懒得打E_E)

0. Splay代码:

\(LCT\)是以\(Splay\) 为实现基础的:

IL bool Son(RG int x){return ch[fa[x]][1] == x;}IL bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }IL void Rot(RG int x){    RG int y = fa[x] , z = fa[y] , c = Son(x);    if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;    ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;    ch[x][!c] = y; fa[y] = x; PushUp(y);}IL void Splay(RG int x){    RG int top = 0; stk[++top] = x;    for(RG int i = x; !Isroot(i); i = fa[i])stk[++top] = fa[i];    while(top)PushDown(stk[top--]);    for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])        if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);    PushUp(x); return;}

1. Access(x) 操作

作用是把 \(x\) 到根节点的这条路径变为重链。

void Access(int x){     for(RG int y=0;x;y=x,x=fa[x])         Splay(x) , ch[x][1] = y , PushUp(x);     }}

2. MakeRoot(x) 操作

作用是使 \(x\) 节点成为原树中的根。

IL void Makeroot(RG int x){Access(x); Splay(x); Reverse(x);}

对应的辅助函数:

IL bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}
IL void PushDown(RG int x){    if(rev[x])        rev[x] ^= 1 ,        Reverse(ch[x][0]) , Reverse(ch[x][1]) ;}

3. FindRoot(x) 操作

作用是找到\(x\)所在原树的根节点。

IL int FindRoot(RG int x){    Access(x); Splay(x);     while(ch[x][0])x=ch[x][0]; return x;}

4. Split(x,y) 操作

作用是分离出\(x\)\(y\)这条路径。

IL void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}

那么此时路径信息都存在节点 \(y\) 上了,要查询什么直接查即可。

5. Link(x,y) 操作

作用是连接两个结点\(x\) , \(y\)

IL void Link(RG int x,RG int y){if((!x)||(!y))return; MakeRoot(x); fa[x] = y; }

6. Cut(x,y) 操作

作用是删除\(x\) , \(y\) 之间的连边

IL void Cut(RG int x,RG int y){if((!x)||(!y))return; Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}

7. PushUp(x)  PushDown(x) 操作

其实是与线段树一样的,直接维护题目所求即可。

代码略,以实际题目为准(注意翻转左右儿子的\(PushDown\)是必须有的)。

8.贴一发完整的\(LCT\)模版

namespace Link_Cut_Tree{        bool Son(RG int x){return ch[fa[x]][1] == x;}    bool Isroot(RG int x){return ch[fa[x]][1]!=x && ch[fa[x]][0]!=x; }    bool Reverse(RG int x){swap(ch[x][1],ch[x][0]); rev[x] ^= 1;}    void PushUp(RG int x){ ....... }    void PushDown(RG int x)        {if(rev[x])Reverse(ch[x][0]) , Reverse(ch[x][1]) , rev[x] ^= 1;}    void Rot(RG int x){        RG int y = fa[x] , z = fa[y] , c = Son(x);        if(!Isroot(y))ch[z][Son(y)] = x; fa[x] = z;        ch[y][c] = ch[x][!c]; fa[ch[y][c]] = y;        ch[x][!c] = y; fa[y] = x; PushUp(y);    }    void Splay(RG int x){        RG int top = 0; stk[++top] = x;        for(RG int i = x; !Isroot(i) ; i = fa[i])stk[++top] = fa[i];        while(top)PushDown(stk[top--]);        for(RG int y = fa[x]; !Isroot(x) ; Rot(x) , y = fa[x])            if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);        PushUp(x); return;    }        void Access(RG int x)        { for(RG int y = 0; x; y = x,x = fa[x])Splay(x),ch[x][1] = y,PushUp(x); }    void MakeRoot(RG int x){Access(x); Splay(x); Reverse(x);}    int FindRoot(RG int x){Access(x); Splay(x); while(ch[x][0])x=ch[x][0]; return x;}    void Split(RG int x,RG int y){MakeRoot(x); Access(y); Splay(y);}    void Link(RG int x,RG int y){if((!x)||(!y))return; MakeRoot(x); fa[x] = y;  }    void Cut(RG int x,RG int y){if((!x)||(!y))return; Split(x,y); ch[y][0] = fa[x] = 0; PushUp(y);}}

B. LCT 维护 边权 信息

具体的实现方法有几种,这里只讲一种最简单、最常用的方法。

对于每一条边,新建一个点,然后连向对应的两个原树节点。
那么常见的维护方法是 代表点的节点不赋值,只有代表边的节点才赋值。
具体可以见这一道非常经典的题目:
伪代码为:
\(Link\)操作直接进行即可。

IL void Link(Node u,Node v)   NewNode(val_edge) ---(give)---> code_edge   Link(u,code_edge);  Link(v,code_edge);

\(Cut\)操作则需要找到这条边对应的两个节点,然后直接删除即可。

IL void Cut(Edge e)    Edge e ---(find)--> Node u1,u2 ;      Delete(u1,code_e);  Delete(u2,code_e);

这里注意 :

一定要找到对应的节点,而不是code_e的左右儿子!! ,不然保准你 WA 的怀疑人生。
然后一个非常经典的应用 就是 动态求图的割边(桥):
具体见这题: ;;;;;;;;

C. LCT 维护 子树 信息

理论上来说\(LCT\)是不适合维护子树信息的,但总有一些毒瘤的出题人**..

给一篇写的非常详细的博客:
怎么搞呢?
先明确一下概念。
\(LCT\) 链剖后,一些路径变为了 单独的 一棵\(Splay\)
我们类似树链剖分,把节点划为实儿子、虚儿子。

实儿子就是在同一棵\(Splay\)里的那个儿子,其它则为虚儿子。

那么实儿子对应的子树就为实子树,虚儿子对应的子树就为虚子树。
.
其实我们原来的\(LCT\)就是维护了实子树对吧?
所以我们在维护子树信息时,只要再维护一下虚子树的信息即可。
以求子树大小为例。
我们假设\(sz[u]\)表示\(u\)虚子树大小 , \(sum[u]\)表示整个子树大小,
那么显然,我们\(PushUp\)时,只需要更新\(sum\) , \(sz\)我们手动维护。

IL void PushUp(RG int x){ sum[x] = sum[ch[x][0]]+sum[ch[x][1]]+sz[x]+1; }

那么考虑什么情况下会改变虚子树的大小,其实只有两个:

1. Access'(x) 操作

我们会把 \(x\) 原来的实儿子变为虚儿子,把另一个虚儿子变为实儿子。

我们对应的修改一下\(x\)\(sz\)值,然后\(PushUp\)修改\(sum\)值即可。

IL void Access(RG int x){    for(RG int y = 0; x; y = x,x = fa[x]){        Splay(x);        sz[x] += sum[ch[x][1]] - sum[y];        ch[x][1] = y; PushUp(x);     }return;}

2. Link'(x,y) 操作

我们把\(x\)变为\(y\)的儿子。

那么显然\(x\)以及其所有祖先的\(sz\)值都会收到影响。
解决办法非常简单,把\(y\)\(MakeRoot\) 一下即可使得\(x\)只影响 \(y\)
注意一下由于我们修改了\(y\)\(sz\)值,所以要\(PushUp\)一下\(y\)节点。

IL void Link(RG int x,RG int y){        if((!x)||(!y))return;     MakeRoot(x); MakeRoot(y);    fa[x] = y; sz[y] += sum[x]; PushUp(y); }

3.应用

应用一般就是求解会不断换根的子树信息问题。

最经典的一道题目是:

D.LCT 维护图上信息

理论上来说\(LCT\)也是不适合维护图上信息的,但总有一些毒瘤的出题人..

图上信息的维护是不支持删边操作的。
其实图与树的差别就是可能有重边、环之类的。
那么既然我们只需要支持加边操作,那么只要用并查集维护缩点即可。
怎么维护呢? 这其实也是维护双联通分量的套路了。
.
开两个并查集:
(1)\(bzj[u][1]\),用于维护连通性
每次\(Link\)前,先查找一下两个点是否联通,如果不连通,则直接相连即可。
如果联通,则需要用到第二个并查集:
(2)\(bzj[u][2]\),用于维护双联通性
如果两个点已经双联通了,那么再加边其实对双联通性没有影响。
如果两个点的\(bzj2\)未相连,那么把两个的\(bzj2\)相连,然后缩点。
如果两个点的\(bzj2\)已相连,那么说明这两个点已经缩在一起了,所以无需连边。
.
那么此时\(bzj2\)其实就是每个点对应的缩点后的点\(id\)了。
然后是实现,实现的时候,想一想\(LCT\)的性质,我们可以发现:
只有向上跳父亲的时候,缩点才会对操作有影响
为了方便描述,定义一个\(F\)函数用于找到一个点\(u\)的对应\(id\)

int Find(RG int x,RG int od){return(bzj[x][od]==x)?x:Find(bzj[x][od],od);}int F(RG int x){return Find(x,2); }

然后正常的\(LCT\)操作都遵循上面那个结论,找父亲是\(Find\)一下即可。

\(Splay\)为例(什么\(Access\)之类的都是一样的):

IL void Splay(RG int x){    RG int top = 0; stk[++top] = x;    for(RG int i = x; !Isroot(i); i = F(fa[i]))stk[++top] = F(fa[i]);     while(top)PushDown(stk[top--]);    for(RG int y = F(fa[x]); !Isroot(x); Rot(x),y = F(fa[x]))        if(!Isroot(y))Son(x) ^ Son(y) ? Rot(x) : Rot(y);    PushUp(x);  return;}

关键:\(Add(x,y)\) 操作

关键在于链接两个点时的操作,这时候是不能直接执行\(Link\)操作的。

先给代码:

IL void Add(RG int u,RG int v){    RG int x = F(u) , y = F(v);    RG int f1 = Find(x,1) , f2 = Find(y,1);    if(f1 != f2){        bzj[f1][1] = f2;        Link(x,y);  return;    }    else{        if(x == y)return;        Split(x,y); Merge(y,y);   //缩点        ch[y][0] = ch[y][1] = 0;  //!!!!!!!!    }return;}

显然就是按照上面的并查集维护原则连边。

然后给一下缩点的代码:

IL void Merge(RG int u,RG int rt){    if(u ^ rt)        sum[rt] += sum[u], bzj[F(u)][2] = rt, sum[u] = fig[u] = 0;    if(ch[u][0])Merge(ch[u][0],rt);    if(ch[u][1])Merge(ch[u][1],rt);}

其中\(sum\)为结点自身的信息(本身值),\(fig\)为子树信息(计算值)。

应用:

这个就依题目而定吧。

请记住一个最明显的特征:只支持加边,不支持删边!
\(LCT\)维护图上信息最经典的一题为: , 友情提示注意卡常。

有根LCT

自行yy,唯一一个需要注意的地方:

Cut(u,Fa) 的时候,必须把\(fa\)转到\(Splay\)顶端!(不能把\(u\)转到顶端),然后剪切。

IL void Cut(int u , int Fa) {    Access(u) ; Splay(Fa) ;    ch[1][Fa] = fa[u] = 0 ; PushUp(Fa) ; return ; }

因为如果转\(u\)到顶端的话,剪切后整棵树的根就会产生变化,转移到\(u\)所属\(Splay\)中。

转载于:https://www.cnblogs.com/GuessYCB/p/8330024.html

你可能感兴趣的文章
高性能优化Web前端
查看>>
Google研究员Ilya Sutskever:成功训练LDNN的13点建议
查看>>
【转】Java并发编程:volatile关键字解析
查看>>
Sublime Text 格式化代码快捷键
查看>>
C#百度关键字指数查询Socket实现
查看>>
JVM系列五:JVM监测&工具[整理中]
查看>>
疯狂的 Web 应用开源项目
查看>>
实用CSS3属性之 :target伪类实现Tab切换效果
查看>>
开涛spring3(6.9) - AOP 之 6.9 代理机制
查看>>
hdu 4775 Infinite Go(暴力)
查看>>
查看 SELinux状态及关闭SELinux
查看>>
程序员辞职申请 [职业生涯的第一份辞职书]
查看>>
程序员全国不同地区,微信(面试 招聘)群。
查看>>
HADOOP集群MAPREDUCE原理篇
查看>>
聊聊host中ip/域名映射记录的解析规则
查看>>
180620-mysql之数据库导入导出
查看>>
【干货】界面控件DevExtreme视频教程大汇总!
查看>>
在Fedora 15上使用Vmware Server 2.0.2
查看>>
oracle 按每天,每周,每月,每季度,每年查询统计数据
查看>>
正则表达式
查看>>