LCT

\(\quad\)以模板题为例,P3690 【模板】动态树(Link Cut Tree),具体讲讲每个操作

#define lc c[x][0]
#define rc c[x][1]
const int N=3e5+10;
int n,m,v[N],f[N],c[N][2],s[N],r[N],st[N];

\(\quad\)这是一些定义,v 是价值,f 是父亲, c 是孩子,s 是splay子树异或和,r 是反转标记,st 是栈。

inline bool nroot(int x){
    return c[f[x]][0]==x||c[f[x]][1]==x;
}//x 是不是一个 splay 的根

\(\quad\)如果自己父亲的两个儿子之一是自己,那么自己不是根,这个显然

\(\quad\)事实上这个就体现了 0 节点的重要性,所以在打代码时一定要注意不要动 0 节点,比如判断一下当前是不是根。

inline void pushup(int x){
    s[x]=s[lc]^s[rc]^v[x];
}
inline void pushr(int x){
    swap(lc,rc);
    r[x]^=1;
}
inline void pushdown(int x){
    if(r[x]){
        if(lc) pushr(lc);
        if(rc) pushr(rc);
        r[x]=0;
    }
}

\(\quad\)更新子节点

inline void rotate(int x){
    int 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(w) f[w]=y;f[y]=x;f[x]=z;
    pushup(y);
    pushup(x);
}//旋转一次,和splay差不多,注意不要涉及 0 即可

\(\quad\)rotate 的前提是 \(y,x\) 都存在,也就是我们注意 \(z,w\) 是否为 0

inline void splay(int x){
    int y=x,z=0;
    st[++z]=y;
    while(nroot(y)) st[++z]=y=f[y];//用栈存下来一到根的路径,然后从上到下 pushdown
    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);
}

\(\quad\)先用栈从上到下的 pushdown ,然后再做 splay 操作,注意判根。

inline void access(int x){
    for(int y=0;x;x=f[y=x]){
        splay(x);rc=y;pushup(x);
    }
}//打通 x 到根的路径,并且最后 x 是 splay 中中序遍历最大的,x 到根是一条链

\(\quad\)注意打通后,x 到根的路径一定是一条链

inline void makeroot(int x){
    access(x);splay(x);
    pushr(x);
}//换根,打通到根的路,转上去,打标记

\(\quad\)打通后,自己就是 splay 中中序遍历最大的,那么转上去后没有右儿子,将子树反转一下,就变成了没有左儿子,也就是深度最小的点,也就是根

inline int findroot(int x){
    access(x);splay(x);
    while(lc) pushdown(x),x=lc;
    splay(x);
    return x;
}//找"真实"的树中的根

\(\quad\)树中的根一定在 splay 根所在的树中

inline void split(int x,int y){
    makeroot(x);
    access(y);
    splay(y);
}//提取路径,将 x 换根,打通 y 到 x 的路径,再将 y 转上去成为根

\(\quad\)这样的话此时树中只有 x , y 之间的点。

inline void link(int x,int y){
    makeroot(x);
    if(findroot(y)!=x) f[x]=y;
}//连边,判断连通性
inline void cut(int x,int y){
    makeroot(x);
    if(findroot(y)==x && f[y]==x && !c[y][0]){
        f[y]=c[x][1]=0;
        pushup(x);
    }
}//判断的是否有边的存在,然后删除边

\(\quad\)首先将 x 变成根,然后判断是否在同一颗树中,此时 x 是没有左子树,那么 y 只有是 x 的右儿子,并且 y 没有左儿子(也就是 x y 之间没有别的节点)时,它们才可以说有一条边相连。然后再双向的删去这个实边,这样才算真正意义上的删除。

\(\quad\)所有的基础操作都在上面了,下面是一个封装了的 LCT。

namespace LCT{
    #define lson ch[x][0]
    #define rson ch[x][1]
    int s[N],st[N],ch[N][2],r[N],f[N];
    inline void pushup(int x) {s[x]=s[lson]^s[rson]^a[x];}
    inline void pushr(int x) {r[x]^=1;swap(lson,rson);}
    inline void pushdown(int x) {if(r[x]) {if(lson) pushr(lson);if(rson) pushr(rson);r[x]=0;}}
    inline int noroot(int x) {return (ch[f[x]][0]==x)||(ch[f[x]][1]==x);}
    inline void rotate(int x){
        int y=f[x],z=f[y],k=ch[x][!(ch[y][1]==x)],w1=(ch[z][1]==y),w2=(ch[y][1]==x);
        if(noroot(y)) ch[z][w1]=x;
        f[x]=z;f[y]=x;ch[x][!w2]=y;ch[y][w2]=k;
        if(k) f[k]=y;
        pushup(y);pushup(x);
    }
    inline void splay(int x){
        int y=x,z=0;
        while(noroot(y)) st[++z]=y,y=f[y];
        st[++z]=y;
        while(z) pushdown(st[z--]);
        while(noroot(x)){
            y=f[x],z=f[y];
            if(noroot(y)) rotate((ch[z][1]==y)^(ch[y][1]==x)?x:y);
            rotate(x);
        }
        pushup(x);
    }
    inline void access(int x){
        for(int y=0;x;x=f[y=x]){
            splay(x);rson=y;pushup(x);
        }
    }
    inline void makeroot(int x){
        access(x);
        splay(x);
        pushr(x);
    }
    inline int findroot(int x){
        access(x);splay(x);
        pushdown(x);
        while(lson) x=lson,pushdown(lson);
        splay(x);
        return x;
    }
    inline void split(int x,int y){
        makeroot(x);
        access(y);
        splay(y);
    }
    inline void link(int x,int y){
        makeroot(x);
        if(findroot(y)!=x) f[x]=y;
    }
    inline void cut(int x,int y){
        makeroot(x);
        if(findroot(y)==x && !ch[y][0] && f[y]==x){
            f[y]=ch[x][1]=0;
            pushup(x);
            return ;
        }
    }
    #undef lson
    #undef rson
}using namespace LCT;

\(\quad\)LCT 要注意是:可以维护一个森林的信息,支持边动态变化,实际 LCT 形态和树的形态是有区别的。每一个 splay 维护的都是原树的一个链,满足每一个 splay 中序遍历节点在原树的深度是严格递增的。

维护链信息

P1501 [国家集训队]Tree II

\(\quad\)注意要判是否联通。

P3203 [HNOI2010]弹飞绵羊

\(\quad\)建一个虚拟节点表示被弹飞了,那么就是一个裸的 LCT 。

P2486 [SDOI2011]染色

\(\quad\)裸的 LCT ,就是 spaly 要维护一下相应的东西。

P4332 [SHOI2014]三叉神经树

\(\quad\)\(f(x)\) 表示节点具有 1 的输入节点数,假设是变 0 到 1 (另一种是一样的),那么如果它的父节点 \(f(x)=1\) ,那么它的父节点就会变成 1 ,以此类推一直延申到根,最后可能会停在中间的某个节点,如果我们能找到这个节点,那么它下去到叶子节点一条链一起修改一下 \(f(x)\) 就可以了。

\(\quad\)我们可以维护一下每一个节点子树内 \(f(x)\neq 1\) 的深度最深的节点,这样就可以直接找了。

动态维护连通性&双连通分量

P2147 [SDOI2008] 洞穴勘测

P3950 部落冲突

P2542 [AHOI2005] 航线规划

\(\quad\)如果是静态的,那么很明显就是缩点,然后搞一搞。既然是动态的,那么考虑 LCT 。删边似乎不是很好维护,考虑离线下来加边维护。那么每一次加边,如果两个点已经联通,那么就把这些边的权值赋为 0 ,答案就是两个点之间边的权值和,不够这种方法的额扩展性不够广。或者也可以用并查集维护一下,就像下面说的一样。

【BZOJ4998】星球联盟

\(\quad\)每次判断一下是否联通,如果联通,那么就用并查集缩点,这里可以直接 dfs 下去,把一条链全部缩了,均摊的复杂度应该是 \(O(nlogn)\)

【BZOJ2959】长跑

\(\quad\)仍然是一个 LCT 和 并查集 维护的题,注意每次对于一个节点一定要先 \(find\_fa\) ,这才代表在树中真正的节点。

注意:如果用并查集缩点一定要注意每次用节点的时候先 find 出真实的缩后的节点。

维护边权(生成树)

\(\quad\)常用的技巧是将边权拆成点,向端点连边,那么就可以转换成点权问题。

P4172 [WC2006]水管局长

\(\quad\)首先离线下来,倒序处理。这个题就是要维护动态最小生成树,那么和静态一样,每次加边形成环后判断一下是否替换其中的边即可。

P4234 最小差值生成树

\(\quad\)将权值从小到大扫一遍,假设当前最大值为 k ,那么我们需要最小值最大,也就是维护一个动态最大生成树,如果替换边后能连成一个联通块,那么就可以统计答案了。

P2387 [NOI2014] 魔法森林

\(\quad\)因为每一条边有两个边权,所以我们首先确定一个边权为标准,从小到大排序,然后从小到大扫一遍,每一次将满足当前权值的边插入维护的森林中,如果有环,就撤掉最大的边,否则就直接连上去,最后求一下路径最大值即可。

维护子树信息

\(\quad\)一般是会专门维护虚边的贡献,向上合并答案的时候合并到节点上,似乎有的题专门为虚边开平衡树来维护,我目前还没遇到这种题。

注意

\(\quad\)那么在更新一个节点的儿子时,一定要注意同时要修改维护虚边的数组。也意味着如果 splay 树结构没有被破坏(rotate,splay),那么就不用特殊考虑虚边儿子的变化。

P4219 [BJOI2014]大融合

\(\quad\)维护虚边连接的儿子节点数即可,入门题。

#558. 「Antileaf's Round」我们的 CPU 遭到攻击

P4299 首都

\(\quad\)首先要知道这就是要动态维护树的重心,可以用并查集记录树的重心,Lct 来维护。然后需要知道一个性质就是两棵树合并后的重心一定在原来两颗树的重心的路径上。那么我们将路径提取出来,还需要知道一个结论:树的重心满足所有的子树节点数都不超过树的节点数的一半,我们根据这个东西来判断树的重心。肯定不可能一个个的判断,这个时候我们需要用类似树上二分的东西来搞,也就是每次根据左右子树和之前记录的长度,选择树的重心会出现在哪个子树,然后进入这个子树,一直递归下去,那么这样找的均摊复杂度就是 \(O(logn)\)。如果节点数是奇数,那么树的重心固定,这是一个剪枝。

U19482 山村游历(Wander)

\(\quad\)题意的走法就是将边随机成一个排列,然后按照顺序走直到走不了回头。

\(\quad\)关于 S 到 T 的路径一定是一个简单路径加上简单路径上每个节点的一些子树组成的。如果走进了一个子树那么只有就将子树走完才会回来。分开考虑走每一个子树的期望。也就是考虑这个点到子树的边在排列中的要比出边早出现的概率,而这个概率就是 \(\frac{1}{2}\) ,因为每一个排列反过来也对应着另一个排列。

\(\quad\)那么链上维护一下子树的节点数即可。

P4115 Qtree4

\(\quad\)仍然考虑对于虚边进行特殊的维护。首先将边权变成点权,给儿子,接着维护这些东西:

    int ch[N][2],f[N],sum[N];
    int len[N],lmx[N],rmx[N],Mx[N],w[N];
//“点权”,节点所在链最浅的节点的和白点的最远距离,节点所在链最深的节点的和白点的最远距离,节点所维护的子树包括虚儿子白色点对的最远距离,是否是白点

\(\quad\)这么维护后就好 pushup 了,但是每次我们需要拿出虚树链的最长值和次长值,这个就搞个 set 维护一下,但是如果总是进进出出,常数就会非常大,所以我们搞个标记,维护一下:

    int lfir[N],lsec[N],pfir[N];
    multiset<int> l[N],p[N];

\(\quad\)看一看 pushup 的代码应该就能懂了

    inline void pushup(int x) {
        sum[x]=sum[lson]+sum[rson]+len[x];
        //权值和
        int xushu=max(w[x],lfir[x]);
        //虚链能延长出去的最大值,包括自己本身
        int L=max(xushu,rmx[lson]+len[x]);
        //自己能从左边或者虚链走得到的最大值
        int R=max(xushu,lmx[rson]);
        //同上
        lmx[x]=max(lmx[lson],sum[lson]+len[x]+R);
        //不走 x ,走 x 
        rmx[x]=max(rmx[rson],L+sum[rson]);
        //应该也比较好懂
        
        Mx[x]=max(rmx[lson]+len[x]+R,lmx[rson]+L);
        //固定左/右实链,求最大值
        Mx[x]=max(max(Mx[lson],Mx[rson]),Mx[x]);
        //直接通过左右子树更新最大值
        Mx[x]=max(Mx[x],lfir[x]+lsec[x]);
        //通过最大虚链和次大虚链更新最大值
        Mx[x]=max(Mx[x],pfir[x]);
        //直接通过虚儿子更新最大值
        if(!w[x]) Mx[x]=max(0,max(lfir[x],Mx[x]));
        //如果本身是白点,那么自己就可以做端点,拿一个最长虚链出来更新即可。
    }

SP2939 QTREE5 - Query on a tree V

\(\quad\)这个题目是上面的弱化版,对于每一个节点维护离自己最近的白节点即可。

维护树上染色联通块

\(\quad\)目前见过的套路:

  1. 每个颜色开 LCT 去维护
  2. 每一个 splay 维护一种颜色

P2173 [ZJOI2012]网络

\(\quad\)因为颜色很少,那么直接为每一种颜色开 LCT 维护

P3703 [SDOI2017]树点涂色

\(\quad\)这个肯定不可能为每一种颜色开 LCT ,考虑用第二种套路,这就意味着我们不能随便改变结构了,也就是虚实链的变化。那么操作 1 就是 access ,操作是要求虚链的个数,那么我们考虑改成树上差分,也就是类似 \(F(x)+F(y)-2\cdot F (LCA)\) 的形式。那么我们需要找 LCA ,并且对于每一个点维护 \(F\) ,然后我们发现操作 3 就是对于子树要维护最大的 \(F\) 。那么我们考虑维护 \(F\) ,但是 LCT 不支持修改操作,而修改操作似乎是一个子树都要修改,也就是 access 的地方,而询问操作似乎也是子树询问,那么搞一个 dfn 序和线段树维护一下。

SP16549 QTREE6 - Query on a tree VI

\(\quad\)这个考虑第一种套路,对于黑白点分开考虑。

\(\quad\)这里有一个小 trick 就是我们将边权换成点权,意思就是对于子节点的颜色赋给父边,然后再连边,维护子树大小。

SP16580 QTREE7 - Query on a tree VII

\(\quad\)仍然像上一题一样维护,对于最大值得搞个 multiset 去维护。

[BZOJ3914] Jabby's shadows

\(\quad\)暂时不会,先咕着。。。

特殊题型

P5354 [Ynoi2017] 由乃的 OJ

\(\quad\)之前用线段树过了,现在怎么还要用 LCT 搞啊。。总体思路是一样的,每一个节点维护自己在这个子树中序遍历的 \(f_0\)\(f_1\) ,那么查询的话就直接 split ,直接查就可以了。

UOJ #207. 共价大爷游长沙

\(\quad\)一个显然的想法是覆盖几何中所有的路径,但是似乎很难维护。我们再考虑一个边如果是必经边,那么两个端点两边的子树各都要包含所有路径的一个端点,那么我们判断这个就可以了。应该可以给每一个端点一个值,剩下的节点值为 0 ,这样维护一下子树和就可以判断了。也就是我们要进行的操作是

  1. 维护子树和,这个随便搞一搞
  2. 加入/删除点对,那就是修改权值,转到根上修改一下
  3. 查询,split 一下,然后判断子树和是否满足条件即可

P3348 [ZJOI2016]大森林

\(\quad\)这个题目是真的恶心。首先你发现空间不足以为每一个点开一棵树,时间也不允许每次给每一棵树加点,那么考虑只用一棵树来维护整个题。那么一想法就是把操作都离线下来,扫描线,跟那个二次离线莫队似乎差不多,直接从前面节点所成型的树加加减减一些记录过的贡献,然后就可以变成自己树的形态,因为你会发现这个题没有断边这种操作,也就是完全可以把整个树的形态直接转完后,再询问。

\(\quad\)那么现在考虑怎么转移。首先我们需要好好读题,操作 1 并不是说每次新加的点的标号是取决那颗树,事实上题目的意思是这取决于当前添加节点的次数。对于操作 0 ,我们可以直接想着为所有的树都加上这个标号的节点,这样一定不会影响到答案,因为所有的操作一定不会涉及到这个节点,也就是说这个节点只会接在虚拟节点下面并且不会有子节点。对于操作 1 就是最恶心的了,我们对每一个操作 1 新建一个虚拟节点,然后从这个位置一直到下个操作 1 之间的所有新的节点都接在这个虚拟节点上,这样假如一个位置要换根,那么我们直接将这个虚拟节点和父亲断边,然后移植过去就可以了。

\(\quad\)考虑区间 \([l,r]\) ,那么对于转移到 \(l\) 的时候就需要换根了,那么我们就记录一下这一次的虚拟节点和新根,扫描线扫到这里的时候,就根据信息换根,对于 \(r+1\) ,是不换根的,也就是我们要将之前换了的根换回去,因为下面的结构都是根据之后的顺序定的,所以不影响之前的换根操作(这个需要好好思考一下),那么我们记录下这一次的虚拟节点和上一次的虚拟节点,将这一次的虚拟节点接回上一次的虚拟节点下面。

\(\quad\)当树的形态已经用 LCT 维护好了,我们就可以查询了。用 LCT 查询公共祖先的方法就是先 access(x) ,然后 access(y)并且记录在最后一个(根所在)的平衡树所相遇的节点,这个点就是 LCA 。因为这个题目不能破坏树的结构,所以我们只能通过树上差分的形式来求距离,也就是我们需要知道节点到根节点的距离,也就是要记录每一个平衡树的子树大小。

P4338 [ZJOI2018]历史

不会,先咕着...

posted @ 2022-06-18 18:12  Kzos_017  阅读(68)  评论(0编辑  收藏  举报