树上数据结构——LCT

目录

  • 树上数据结构——LCT

    • 概述
    • 基本概念
    • 核心操作
    • 其他操作
  • 完整模板

树上数据结构——LCT

概述

LCT是一种强力的树上数据结构,支持以下操作:

  1. 链上求和
  2. 链上求最值
  3. 链上修改
  4. 子树修改
  5. 子树求和
  6. 换根
  7. 断开树上一条边
  8. 连接两个点,保证连接后仍然是一棵树。

基本概念

LCT是对树的实链剖分,即把所有边划分为实边和虚边

类似于重链剖分,每个点连向子节点中的实链至多只会有一条,把这条实边连向的儿子叫做实儿子

把一些实边连接的点构成的链叫做实链,容易发现实链之间没有共同点

需要注意的是一个不在实边上的点(一些叶节点)也视为一条没有实边的实链

于是实链之间一定是用虚边链接的

要涉及动态删连边操作,于是使用splay来维护一条实链,splay是LCT的辅助树

此处splay的深度按中序遍历严格递增

由于用splay维护,LCT的实边是动态的,可以改变

核心操作

? access(x):让x到根节点的所有边均为实边,并且x没有实儿子

这个推荐flash_hu的博客,简单易懂

稍微说一下,每次操作先把当前要连的点splay到当前splay的根,由于splay中深度按中序遍历递增,此时根的右儿子一定是之前连的实链,需要去掉

于是把之前的点连到当前根的右儿子就行了

注意此时一些\(fa,son,isroot\)之类的信息改变了,需要\(push\)_\(up\)

void access(int x){
    for(int y=0;x;y=x,x=fa[x]){ //y是之前的根,x是当前需要连的点
        splay(x); ch[x][1]=y;
        push_up(x);
    }
}

其他操作

  1. makeroot

    换根操作

    access(x)之后x是深度最大的点

    所以splay(x)之后,x在splay中一定没有右子树,这个时候翻转整个splay,所有点的深度就都倒过来了,x成为深度最小的点,即为根节点

    void pushr(int x){
        swap(ch[x][0],ch[x][1]);
        r[x]^=1;
    }
    void makeroot(int x){
        access(x); splay(x);
        pushr(x);
    }
  2. findroot

    找所在树的树根,可以用来判断两点之间的连通性(两点所在树相同则有唯一相同根

    int findroot(int x){
        access(x);splay(x);
        while(c[x][0]) push_down(x),x=ch[x][0];//寻找深度最小的点,此处push_down是为了x到跟的标记放完,好判连通性
        splay(x);//多多splay有益健康
        return 0;
    }
  3. split

    把一条路径拉成一个splay

    void spilt(int x,int y){
        makeroot(x);access(y);
        splay(y);
    }
  4. link

    连一条边,保证连完还是一棵树

    不保证合法:

    int link(int x,int y){
        makeroot(x);
        if(findroot(y)==x) return 0;
        fa[x]=y; //把x作为y的儿子
        return 1;
    }

    保证合法:

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

    此处连的边是虚边(感受到实链剖分的方便了罢

  5. cut

    断边

    保证存在:

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

    不存在此边的时候是什么情况呢?

    先把x给\(makeroot\)到根

    1. x和y不连通 (\(findroot\))
    2. 在同一splay中而没有直接连边 (\(f[y]==x\)且\(!c[y][0]\))

      (考虑其他的点在哪里,在findroot之后x到了根节点,如果x和y之间有点,只能是在y到根的路径上或者y的左儿子上)

    int cut(int x,int y){
        makeroot(x);
        if(findroot(y)!=x||fa[y]!=x||ch[y][0]) return 0;
        fa[y]=ch[x][1]=0;
        push_up(x);
        return 1;
    }
  6. nroot

    naiive的操作,判断此点是否不是当前splay的根节点

    int nroot(int x){
        return (ch[fa[x]][1]==x||ch[fa[x]][0]==x);
    }
  7. splay 的特殊性

    此处splay的标记一定要从上往下放,也就是先开个栈把标记放完再旋转

完整模板

#include<bits/stdc++.h>
#define R register int
#define I inline void
#define G if(++ip==ie)if(fread(ip=buf,1,SZ,stdin))
#define lc c[x][0]
#define rc c[x][1]
using namespace std;
const int SZ=1<<19,N=3e5+9;
char buf[SZ],*ie=buf+SZ,*ip=ie-1;
inline int in(){
    G;while(*ip<'-')G;
    R x=*ip&15;G;
    while(*ip>'-'){x*=10;x+=*ip&15;G;}
    return x;
}
int f[N],c[N][2],v[N],s[N],st[N];
bool r[N];
inline bool nroot(R x){//判断节点是否为一个Splay的根(与普通Splay的区别1)
    return c[f[x]][0]==x||c[f[x]][1]==x;
}//原理很简单,如果连的是轻边,他的父亲的儿子里没有它
I pushup(R x){//上传信息
    s[x]=s[lc]^s[rc]^v[x];
}
I pushr(R x){R t=lc;lc=rc;rc=t;r[x]^=1;}//翻转操作
I pushdown(R x){//判断并释放懒标记
    if(r[x]){
        if(lc)pushr(lc);
        if(rc)pushr(rc);
        r[x]=0;
    }
}
I rotate(R x){//一次旋转
    R y=f[x],z=f[y],k=c[y][1]==x,w=c[x][!k];
    if(nroot(y))c[z][c[z][1]==y]=x;c[x][!k]=y;c[y][k]=w;//额外注意if(nroot(y))语句,此处不判断会引起致命错误(与普通Splay的区别2)
    if(w)f[w]=y;f[y]=x;f[x]=z;
    pushup(y);
}
I splay(R x){//只传了一个参数,因为所有操作的目标都是该Splay的根(与普通Splay的区别3)
    R y=x,z=0;
    st[++z]=y;//st为栈,暂存当前点到根的整条路径,pushdown时一定要从上往下放标记(与普通Splay的区别4)
    while(nroot(y))st[++z]=y=f[y];
    while(z)pushdown(st[z--]);
    while(nroot(x)){
        y=f[x];z=f[y];
        if(nroot(y))
            rotate((c[y][0]==x)^(c[z][0]==y)?x:y);
        rotate(x);
    }
    pushup(x);
}
/*当然了,其实利用函数堆栈也很方便,代替上面的手工栈,就像这样
I pushall(R x){
    if(nroot(x))pushall(f[x]);
    pushdown(x);
}*/
I access(R x){//访问
    for(R y=0;x;x=f[y=x])
        splay(x),rc=y,pushup(x);
}
I makeroot(R x){//换根
    access(x);splay(x);
    pushr(x);
}
int findroot(R x){//找根(在真实的树中的)
    access(x);splay(x);
    while(lc)pushdown(x),x=lc;
    splay(x);
    return x;
}
I split(R x,R y){//提取路径
    makeroot(x);
    access(y);splay(y);
}
I link(R x,R y){//连边
    makeroot(x);
    if(findroot(y)!=x)f[x]=y;
}
I cut(R x,R y){//断边
    makeroot(x);
    if(findroot(y)==x&&f[y]==x&&!c[y][0]){
        f[y]=c[x][1]=0;
        pushup(x);
    }
}
int main()
{
    R n=in(),m=in();
    for(R i=1;i<=n;++i)v[i]=in();
    while(m--){
        R type=in(),x=in(),y=in();
        switch(type){
        case 0:split(x,y);printf("%d\n",s[y]);break;
        case 1:link(x,y);break;
        case 2:cut(x,y);break;
        case 3:splay(x);v[x]=y;//先把x转上去再改,不然会影响Splay信息的正确性
        }
    }
    return 0;
}

之后可能会补自己做的LCT题(咕



在创作本文的过程中,参考了以下文章:

原文地址:https://www.cnblogs.com/lcyfrog/p/11391899.html

时间: 2024-11-01 23:03:46

树上数据结构——LCT的相关文章

ZROI 19.08.01 树上数据结构

1.总览 LCT 链分治(树剖) 点/边分治 2.点分治 一棵树,点有\(0/1\),多次修改,询问最远的两个\(1\)距离. 建出点分树,每个子树用堆维护:①最远的\(1\)距离:②它的每个儿子的①堆顶: 全局维护每棵子树②堆最大的两个值,每次修改暴力改就可以. 时间复杂度\(O(n \log^2n)\)(基本动态点分都是\(\log^2\)以上的复杂度--然而仍然随便过\(10^5\),小常数可能性微存?). 一棵树,点有点权,每次询问距离一个点不超过\(k\)的点权和. 每个点维护一个深度

【BZOJ-2555】SubString 后缀自动机 + LinkCutTree

2555: SubString Time Limit: 30 Sec  Memory Limit: 512 MBSubmit: 1936  Solved: 551[Submit][Status][Discuss] Description 懒得写背景了,给你一个字符串init,要求你支持两个操作    (1):在当前字符串的后面插入一个字符串    (2):询问字符串s在当前字符串中出现了几次?(作为连续子串)    你必须在线支持这些操作. Input 第一行一个数Q表示操作个数    第二行一

回文树或者回文自动机,及相关例题

回文树简述 在大部分说法中,回文树与回文自动机指的是一个东西: 回文树是对一个字符串,基于自动机思想构建的处理回文问题的树形结构: 回文树是对着一个单串建立的: 于是他主要用于计数(回文子串种类及个数) 基本建立思路是先建立其前缀的回文树,然后每加上一个字符,统计影响: 回文树存在fail指针但一般不承接字符串匹配问题: (回文树大概可以判定一个回文串是不是一个串的子串,但KMP之类的可以做得更好) 构建好的回文树,是这样的: (好难看) 可看出: 存在两个树结构,分别记录奇数|偶数长度的回文:

51nod 1600 Simplr KMP(后缀自动机+维护树上的数据结构)

题意:对于每个位置,统计有多少个相同的字串. 分析:按照题目的意思,把fail树画出来就会发现,对于第i个字符:ans[i] = ans[i-1] + (ans[i-1]-ans[i-1]) + cal(i); cal(i)是计算s[1…i-1]所有子串与s[1…i]的最长公共后缀的和.换句话说,根据后缀自动机性质,沿着parent树往上走可以知道对于后缀s[1…i]的所有位置的公共后缀长度以及个数(right集合的大小).很容易可以计算出cal(i), 只要每次新增一个字符的时候,在paren

【bzoj3779】重组病毒 LCT+树上倍增+DFS序+树状数组区间修改区间查询

题目描述 给出一棵n个节点的树,每一个节点开始有一个互不相同的颜色,初始根节点为1. 定义一次感染为:将指定的一个节点到根的链上的所有节点染成一种新的颜色,代价为这条链上不同颜色的数目. 现有m次操作,每次为一下三种之一: RELEASE x:对x执行一次感染: RECENTER x:把根节点改为x,并对原来的根节点执行一次感染: REQUEST x:询问x子树中所有节点感染代价的平均值. 输入 输入的第一行包含两个整数n和m,分别代表局域网中计算机的数量,以及操作和询问的总数.接下来n-1行,

洛谷P3348 [ZJOI2016]大森林(LCT,虚点,树上差分)

洛谷题目传送门 思路分析 最简单粗暴的想法,肯定是大力LCT,每个树都来一遍link之类的操作啦(T飞就不说了) 考虑如何优化算法.如果没有1操作,肯定每个树都长一样.有了1操作,就来仔细分析一下对不同树的影响. 假设有一个2操作形如\(l\ r\ x\),那么从微观来看差异,我们只关注第l-1棵树和第l棵树.再假设以后都没有了1操作,那么我们可以认为,第l棵树是第l-1棵树把这个1操作以后嫁接在原来生长节点上的所有节点(以及所有子树)全部转而嫁接到x上.再看第r棵树和第r+1棵树,是不是可以认

【luoguP3676】 小清新数据结构题 推式子(LCT维护)

挺有趣的一个数据结构题,不必用 LCT 维护,只不过 LCT 比较好写 ~ code: #include <cstdio> #include <string> #include <cstring> #include <algorithm> #define N 200008 #define ll long long #define lson s[x].ch[0] #define rson s[x].ch[1] using namespace std; ll a

树形DP——动态规划与数据结构的结合,在树上做DP

本文始发于个人公众号:TechFlow,原创不易,求个关注 今天是算法与数据结构的第15篇,也是动态规划系列的第4篇. 之前的几篇文章当中一直在聊背包问题,不知道大家有没有觉得有些腻味了.虽然经典的文章当中背包一共有九讲,但除了竞赛选手,我们能理解到单调优化就已经非常出色了.像是带有依赖的背包问题,和混合背包问题,有些剑走偏锋,所以这里不多做分享.如果大家感兴趣可以自行百度背包九讲查看,今天我们来看一个有趣的问题,通过这个有趣的问题,我们来了解一下在树形结构当中做动态规划的方法. 这个问题题意很

数据结构&#183;树链剖分+LCT

于是两个一起搞了... 怎么说,写的是P党风格的C++,短也不会短到哪里去,跑起来也不快,常数大成狗OTL BZOJ 1036 树链的经典题吧,点修改+路经询问 [Code] BZOJ 2243 路径修改+路径询问 [Code] BZOJ 3083 路径修改+子树询问(根可变),这道题要用树链剖分求DFS序中某一段区间的值(DFS序可查子树,链剖可修改路径,两者相结合就行了) [Code] BZOJ 2049 用LCT维护森林形态 [Code] BZOJ 2631 路径修改+路径询问+形态可变