0017: 动态树LCT简述

· · 算法·理论

0017: 动态树LCT简述

1. 简介

1.1 一些定义与 LCT 辅助树

动态树问题: 维护一个森林, 支持加入或删除一些边, 维护一些信息. LCT(Link Cut Tree), 就是一种常用的动态树.

回顾树链剖分, 我们将一棵树剖分成若干重链, 用线段树等数据结构维护重链上的信息. 而在 LCT 中, 由于我们在连边时需要使某个点成为根, 所以选用 Splay 实现的文艺平衡树.

由于我们需要合并一些链, 根据子树大小划分的重链不再适用, 而是使用实链. 在 LCT 的原树(如上图)上, 每个非叶节点有一个实儿子(父亲认的), 连接实儿子的边叫实边(上图中实线), 还有若干个虚儿子(父亲不认的), 连接的叫虚边(虚线). 若干条实边连起来, 构成一条实链, 我们用 Splay 维护. Splay 的中序遍历对应这条实链从上到下, Splay 的根的父亲是该实链顶端(显然是个虚儿子)的父亲. 由此, 多棵 Splay 构成了一棵辅助树, 如下图:

1.2 变量声明

接下来 Splay 节点定义的一般变量如下:

与维护权值有关的变量(以维护路径和为例, 下列代码中不给出与此有关的部分):

还有一些宏定义:

2. 函数声明

2.1 对树形态无改变的操作

isrt(x)

判断该点是否为所在 Splay 的根.

bool isrt(int x)
{
    return ch[fa[x]][0]!=x&&ch[fa[x]][1]!=x;
}

get(x)

判断该点是父亲的哪个儿子.

int get(int x)
{
    return ch[fa[x]][1]==x;
}

reverse(x)

打区间翻转标记.

void reverse(int x)
{
    rev[x]^=1;
    swap(ls,rs);
}

pushup(x)

上推标记.

void pushup(int x)
{
    siz[x]=siz[ls]+siz[rs]+1;
}

pushdown(x)

下传标记.

void pushdown(int x)
{
    if(rev[x])
    {
        reverse(ls);
        reverse(rs);
        rev[x]=0;
    }
}

update(x)

对该点到所在 Splay 的根的路径上的点从上到下下传标记.

void update(int x)
{
    if(!isrt(x))update(fa[x]);
    pushdown(x);
}

2.2 Splay 原有的操作

rotate(x)

将该点向上旋转一层.

void rotate(int x)
{
    int y=fa[x],z=fa[y],p=get(x);
    if(!isrt(y))ch[z][get(y)]=x;
    ch[y][p]=ch[x][!p];
    fa[ch[x][!p]]=y;
    ch[x][!p]=y;
    fa[y]=x;
    fa[x]=z;
    pushup(y);
    pushup(x);
}

splay(x)

将该点旋转到所在 Splay 的根.

void splay(int x)
{
    update(x);
    for(int f=fa[x];!isrt(x);rotate(x),f=fa[x])
        if(!isrt(f))rotate(get(x)==get(f)?f:x);
}

2.3 新操作

access(x)

构造一条以原树的根开始, 以 x 结束的链.

p 为当前已构造出的链构成的 Splay 的根, 初始时为 0. 我们要将 xp 各自所在的链合并, 只需将 x Splay 到根, 将 p 作为 x 的右儿子, x 原来的右儿子就成为了 x 的一个虚儿子. 此时, 以 x 为根的 Splay 表示了我们已构成的链, 因此 p\leftarrow x, x\leftarrow fa_x. 如此循环往复, 直到 p 成为原树的根.

int access(int x)
{
    int p;
    for(p=0;x;p=x,x=fa[x])
    {
        splay(x);
        rs=p;
        pushup(x);
    }
    return p;
}

这里的 access 操作还有一个返回值, 即最后一次合并时的 x, 它有两个含义:

makeroot(x)

使 x 成为原树的根.

我们将原树看做一个由父亲向儿子连边的 DAG, 可以发现, 只需要翻转 x 到根的路径上的边即可使 x 成为新的根.

void makeroot(int x)
{
    x=access(x);
    reverse(x);
}

find(x)

寻找 x 所在原树的根.

```cpp int find(int x) { access(x); splay(x); pushdown(x); while(ls)x=ls,pushdown(x); splay(x); return x; } ``` ### `split(x,y)` 构造一棵 Splay, 包含且仅包含 $x$ 到 $y$ 的链. ```cpp void split(int x,int y) { makeroot(x); access(y); splay(y); } ``` ### `link(x,y)` 将 $x$ 到 $y$ 连边. 首先我们需要利用 find 操作判断 $x$ 和 $y$ 是否联通. 其次, 为了在 cut 操作时判断是否有边, 我们利用一个 `map<pair<int,int>,int>` 来存储边. 连边时, 我们使 $x$ 同时成为所在原树和 Splay 的根, 然后 $fa_x\leftarrow y$ 使 $x$ 成为 $y$ 的虚儿子. ```cpp void link(int x,int y) { if(find(x)==find(y))return; if(x>y)swap(x,y); mp[make_pair(x,y)]=1; makeroot(x); splay(x); fa[x]=y; } ``` ### `cut(x,y)` 删除 $x$ 到 $y$ 的边. 我们首先利用 map 判断该边的存在, 然后利用 split 操作提取 $x$ 到 $y$ 的链(其实就是一条边), 然后双向断开即可. ```cpp void cut(int x,int y) { if(x>y)swap(x,y); if(!mp.count(make_pair(x,y)))return; mp.erase(make_pair(x,y)); split(x,y); ch[y][0]=fa[x]=0; } ``` # 3. 应用&例题 ## 3.1 模板题 模板:[「BZOJ 3282」Tree](https://hydro.ac/d/bzoj/p/3282) - [「HNOI2010」弹飞绵羊](https://www.luogu.com.cn/problem/P3203) ## 3.2 维护树链信息 通过 split 操作, 我们可以快速提取出树上的一条链, 从而利用 Splay 进行树链的快速修改和查询 1. [P3690 【模板】动态树(LCT)](https://www.luogu.com.cn/problem/P3690) [实现](https://www.luogu.com.cn/paste/hut69wtz) 2. [P1501 [国家集训队] Tree II](https://www.luogu.com.cn/problem/P1501) 使用与维护线段树类似的方法处理 Splay, 每个节点存储乘标记, 加标记, 子树大小(仅当前 Splay 中的节点), 下传标记时先下传乘标记再下传加标记. [实现](咕了) 3. [P2486 [SDOI2011] 染色](https://www.luogu.com.cn/problem/P2486) 也使用与线段树类似的方法, 维护区间(Splay 的一个子树)颜色段个数, 左右端点颜色(事实上我是用树剖+线段树过的qwq). 4. [P4332 [SHOI2014] 三叉神经树](https://www.luogu.com.cn/problem/P4332) 设 $dp_{x,0/1}$ 表示以 $x$ 为根的子树代表的链的最深节点的实儿子取 $0/1$ 时, 该链最浅节点的取值, 同时记录每个点虚儿子 $1$ 的个数, 用于更新. 每次询问时将 $1$ 到修改的输入的父亲 $x$ 这条链提取出来, 对 $x$ 进行 splay, 然后修改并更新, $dp_{x,0}$ 即为所求. 注意 access 时要减去/加上连接或断开的实链的 $dp_{x,0}$ 值(假设连接或断开的实链的顶端是 $x$). [实现](https://www.luogu.com.cn/paste/erif77nb) 其余例题及应用先咕了qwq ## 参考资料 [OI-Wiki](https://oi-wiki.org/ds/lct/#%E7%AE%80%E4%BB%8B)