LCT

动态树

树上查询问题是指,给定一个图论中的树结构,需要对树上的子树或者链进行一系列增删改查的问题。
和序列问题中一般常说的“动态”和“静态”不同。
动态树问题一般指树结构发生改变。
*注意:一般对于纯换根(change root)操作,不视为是动态树问题。

LCT结构

前置知识1:链分解

对于一颗树,可以用若干个互不交叉的连续序列分解一棵树。
序列的下标按照树深度递增的顺序。
然后对于每一条链中的节点,我们用“缩点”的思想可以把它们看成一个节点,这样就做出了一颗“结构树”。
首先你要去画这么一颗“结构树”,因为这就是宏观上LCT的样子

把一条树链当成一个点,把轻边当成树上的边,我们可以的得到一颗结构树。这个地方和树链剖分相同,事实上link-cut tree总是能在log级的时间内把需要查询的树上的简单路径构造到同一条链中。
(在刚刚的例子中,对于当前的一个树链,类比树链剖分你也可以继续叫他“重边、重链、重儿子”,其实有的地方管他们叫“偏好边/路径/儿子”,其实这里就是一个翻译的习惯。)

前置知识2:splay维护序列

首先复习一下如何使用splay维护序列。
splay是一个排序二叉树结构,它是一个Key-Val结构。Key是数组下标,Val是维护的信息。
中序遍历,从左至右是它的key,表示数组下标。

结构

LCT的底层原理叫做,用splay维护动态的链分解。
然后我们可以用splay维护单链,这些链很方便的可以做掉拼接和拆分。
(也就是merge和splite,在lct中我们习惯叫他们link cut)

LCT相对于其他数据结构属于大型数据结构。
原因在于它其实是有层次的,在学习时建议把LCT理解成“结构树套平衡树”以这种方式去理解。
如何去切入呢,就是说分成宏观和微观。
宏观:只关注结构树树链分解的变化。
微观:只关注某一天树链内部splay维护信息的变化。

以上部分就是对于LCT结构上的学习。
这里提出一个小问题:
给定一颗LCT,如何查找当前LCT维护树结构的根节点。

宏观:根节点一定位于LCT外层结构树的根部。
微观:因为LCT中每个节点都是一颗splay,key是相对深度,根节点是相对深度最小的节点,所以是中序遍历的第一个点。

LCT基本操作

\(access(x)(不换根,将根节点到x节点构造到同一条链中)\)
\(change/make/set root(x)(将节点x设置成整棵树的根)\)
\(query path(x,y)(将x,y构造到同一条链中,且链的收尾为x,y)\)
\(link(x,y)(将x,y连接到LCT中)\)
\(cut(将x,y从LCT中断开)\)
\(lca(x,y,z)(查询以x为根,y和z的lca)\)
\(findroot(x)(查询x节点所在树结构的树根是谁,类比并查集)\)

例题

1.【模板】动态树(Link Cut Tree)

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int w[maxn];
struct LCT{
    int ch[maxn][2],fa[maxn],rev[maxn];

    int sum[maxn];  //按题目要求的信息
    /*
    */

    int get(int x){return ch[fa[x]][1]==x;}
    int isr(int x){return ch[fa[x]][get(x)]!=x;}
    //同一条重链上的所有点才会被放在一棵𝑠𝑝𝑙𝑎𝑦中
    //isr(x)判断x是否是所在splay(重链)上的根(链中深度最浅的)

    void update(int x){
        //更新信息
        /*

        */
        sum[x]=sum[ch[x][1]]^sum[ch[x][0]]^w[x];
        return ;
    }
    
    void pushdown(int x){
        if(rev[x]){
            rev[x]=0;swap(ch[x][0],ch[x][1]);
            rev[ch[x][0]]^=1;rev[ch[x][1]]^=1;
        }
        /* 
        其他标记信息
        if(){

        }
        */
        return ;
    }

    void Rotate(int x){
        int y=fa[x],z=fa[y],k=get(x);
        if(!isr(y))ch[z][get(y)]=x;fa[x]=z;
        ch[y][k]=ch[x][k^1];
        if(ch[y][k])fa[ch[y][k]]=y;
        ch[x][k^1]=y;fa[y]=x;
        update(y); return ; 
    }

    int top,st[maxn];
    void splay(int x){

        top=0;st[++top]=x;
        for(int i=x;!isr(i);i=fa[i])st[++top]=fa[i];
        for(int i=top;i;--i)pushdown(st[i]);
        //链信息下传

        for(;!isr(x);Rotate(x)){
            if(!isr(fa[x]))Rotate(get(fa[x])==get(x)?fa[x]:x);  
        }
        update(x);
        return;
    }

    int access(int x){
        int pre=0;
        for(;x;pre=x,x=fa[x]){
            splay(x);ch[x][1]=pre;update(x);
        }
        return pre;
    }

    int lca(int root,int x,int y){
        //以root为根的lca(x,y)
        makeroot(root);
        access(x);
        return access(y);
    }

    int findroot(int x){
        access(x);splay(x);pushdown(x);
        while(ch[x][0]){
            pushdown(x);
            x=ch[x][0];
        }
        splay(x);
        return x;
    }

    void makeroot(int x){
        access(x);splay(x);
        rev[x]^=1;
        return ;
    }

    void link(int x,int y){
        makeroot(x);
        if(findroot(y)!=x)fa[x]=y;
        //如果x和y没有联通就连接
        return;
    }

    void cut(int x,int y){
        makeroot(x);access(y);splay(y);
        if(findroot(y)==x && fa[y]==x && !ch[y][0]){
             //findroot(y)后splay(x)
             ch[x][1]=fa[y]=0;
             update(x);
        }
        return ;
    }

    int query_path(int x,int y){
        makeroot(x);
        access(y);splay(y);
      /*
        题目要求内容
      */
        return sum[y];
    }
}L;
int n,m;
int main(){
    n=read();m=read();
    for(int i=1;i<=n;i++)w[i]=L.sum[i]=read();
    for(int i=1;i<=m;i++){
        int opt=read(),x=read(),y=read();
        if(opt==0){
            printf("%d\n",L.query_path(x,y));
        }
        else if(opt==1)L.link(x,y);
        else if(opt==2)L.cut(x,y);
        else L.splay(x),w[x]=y,L.update(x);
    }
    return 0;
}

2. [HNOI2010]BOUNCE 弹飞绵羊
首先按照初始弹力系数来进行连边,并将弹出的点连向点 ,修改的时候先cut再 link,每个点再记录下size即可

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int w[maxn];
struct LCT{
    int ch[maxn][2],fa[maxn],rev[maxn];

    int sum[maxn];  //按题目要求的信息
    /*
    */

    int get(int x){return ch[fa[x]][1]==x;}
    int isr(int x){return ch[fa[x]][get(x)]!=x;}
    //同一条重链上的所有点才会被放在一棵𝑠𝑝𝑙𝑎𝑦中
    //isr(x)判断x是否是所在splay(重链)上的根(链中深度最浅的)

    void update(int x){
        //更新信息
        /*

        */
        sum[x]=sum[ch[x][1]]+sum[ch[x][0]]+1;
        return ;
    }
    
    void pushdown(int x){
        if(rev[x]){
            rev[x]=0;swap(ch[x][0],ch[x][1]);
            rev[ch[x][0]]^=1;rev[ch[x][1]]^=1;
        }
        /* 
        其他标记信息
        if(){

        }
        */
        return ;
    }

    void Rotate(int x){
        int y=fa[x],z=fa[y],k=get(x);
        if(!isr(y))ch[z][get(y)]=x;fa[x]=z;
        ch[y][k]=ch[x][k^1];
        if(ch[y][k])fa[ch[y][k]]=y;
        ch[x][k^1]=y;fa[y]=x;
        update(y); return ; 
    }

    int top,st[maxn];
    void splay(int x){

        top=0;st[++top]=x;
        for(int i=x;!isr(i);i=fa[i])st[++top]=fa[i];
        for(int i=top;i;--i)pushdown(st[i]);
        //链信息下传

        for(;!isr(x);Rotate(x)){
            if(!isr(fa[x]))Rotate(get(fa[x])==get(x)?fa[x]:x);  
        }
        update(x);
        return;
    }

    int access(int x){
        int pre=0;
        for(;x;pre=x,x=fa[x]){
            splay(x);ch[x][1]=pre;update(x);
        }
        return pre;
    }

    int lca(int root,int x,int y){
        //以root为根的lca(x,y)
        makeroot(root);
        access(x);
        return access(y);
    }

    int findroot(int x){
        access(x);splay(x);pushdown(x);
        while(ch[x][0]){
            pushdown(x);
            x=ch[x][0];
        }
        splay(x);
        return x;
    }

    void makeroot(int x){
        access(x);splay(x);
        rev[x]^=1;
        return ;
    }

    void link(int x,int y){
        makeroot(x);
        if(findroot(y)!=x)fa[x]=y;
        //如果x和y没有联通就连接
        return;
    }

    void cut(int x,int y){
        makeroot(x);access(y);splay(y);
        if(findroot(y)==x && fa[y]==x && !ch[y][0]){
             //findroot(y)后splay(x)
             ch[x][1]=fa[y]=0;
             update(x);
        }
        return ;
    }

    int query_path(int x,int y){
        makeroot(x);
        access(y);splay(y);
        return sum[y];
    }
}L;
int n,m;
int main(){
    n=read();
    for(int i=1;i<=n;i++){
        w[i]=read();
        L.link(i,min(n+1,i+w[i]));
    }
    m=read();
    while(m--){
        int i=read(),j=read()+1;
        if(i==1){
            printf("%d\n",L.query_path(n+1,j)-1);
        }
        else {
            int k=read();
            L.cut(j,min(n+1,j+w[j]));
            w[j]=k;
            L.link(j,min(n+1,j+w[j]));
        }
    }
    return 0;
}

3.魔法森林
这道题的题意就是按照给定的边建一颗树,树上每条边的权值x,y,一条路径的价值为 这条路径上最小的x+最小的y的和,要求从1到n的路径的权值最小
显然最小生成树 的从1到n路径的权值最小。
普通的最小生成树的边只有一个权值,而现在有两个,那我们可以先对x进行从大到小排序,这样就只剩y了。
每次加边,如果两点不联通,则link,否则判断这两点路径的最大值和这条边的y值比较,如果y值小则cut最大值的边,link当前边
因为是边有权值,所以要将边化为点

点击查看代码
#include<functional>
#include<algorithm>
#include<iostream>
#include<cstdlib>
#include<cstring>
#include<complex>
#include<string>
#include<cstdio>
#include<vector>
#include<cmath>
#include<queue>
#include<deque>
#include<stack>
#include<map>
#include<set>
#define ll long long 
#define pa pair<int,int>
#define mp make_pair
#define pb push_back
using namespace std;
const int maxn=1e6+101;
const int MOD=998244353;
const int inf=2147483647;
const double eps=1e-12;

ll read(){
    ll x=0,f=1;char ch=getchar();
    for(;!isdigit(ch);ch=getchar())if(ch=='-')f=-1;
    for(;isdigit(ch);ch=getchar())x=x*10+ch-'0';
    return x*f;
}
int w[maxn];
struct LCT{
    int ch[maxn][2],fa[maxn],rev[maxn];

    int sum[maxn];     //最大值
    int pos[maxn];     //最大值的边的编号
    /*
    */

    int get(int x){return ch[fa[x]][1]==x;}
    int isr(int x){return ch[fa[x]][get(x)]!=x;}
    //同一条重链上的所有点才会被放在一棵𝑠𝑝𝑙𝑎𝑦中
    //isr(x)判断x是否是所在splay(重链)上的根(链中深度最浅的)

    void update(int x){
        //更新信息
        /*

        */
        pos[x]=x;sum[x]=w[x];
        if(sum[ch[x][1]]>sum[x])sum[x]=sum[ch[x][1]],pos[x]=pos[ch[x][1]];
        if(sum[ch[x][0]]>sum[x])sum[x]=sum[ch[x][0]],pos[x]=pos[ch[x][0]];
        return ;
    }
    
    void pushdown(int x){
        if(rev[x]){
            rev[x]=0;swap(ch[x][0],ch[x][1]);
            rev[ch[x][0]]^=1;rev[ch[x][1]]^=1;
        }
        /* 
        其他标记信息
        if(){

        }
        */
        return ;
    }

    void Rotate(int x){
        int y=fa[x],z=fa[y],k=get(x);
        if(!isr(y))ch[z][get(y)]=x;fa[x]=z;
        ch[y][k]=ch[x][k^1];
        if(ch[y][k])fa[ch[y][k]]=y;
        ch[x][k^1]=y;fa[y]=x;
        update(y); return ; 
    }

    int top,st[maxn];
    void splay(int x){

        top=0;st[++top]=x;
        for(int i=x;!isr(i);i=fa[i])st[++top]=fa[i];
        for(int i=top;i;--i)pushdown(st[i]);
        //链信息下传

        for(;!isr(x);Rotate(x)){
            if(!isr(fa[x]))Rotate(get(fa[x])==get(x)?fa[x]:x);  
        }
        update(x);
        return;
    }

    int access(int x){
        int pre=0;
        for(;x;pre=x,x=fa[x]){
            splay(x);ch[x][1]=pre;update(x);
        }
        return pre;
    }

    int lca(int root,int x,int y){
        //以root为根的lca(x,y)
        makeroot(root);
        access(x);
        return access(y);
    }

    int findroot(int x){
        access(x);splay(x);pushdown(x);
        while(ch[x][0]){
            pushdown(x);
            x=ch[x][0];
        }
        splay(x);
        return x;
    }

    void makeroot(int x){
        access(x);splay(x);
        rev[x]^=1;
        return ;
    }

    void link(int x,int y){
        makeroot(x);
        if(findroot(y)!=x)fa[x]=y;
        //如果x和y没有联通就连接
        return;
    }

    void cut(int x,int y){
        makeroot(x);access(y);splay(y);
        if(findroot(y)==x && fa[y]==x && !ch[y][0]){
             //findroot(y)后splay(x)
             ch[x][1]=fa[y]=0;
             update(x);
        }
        return ;
    }

    int query_path(int x,int y){
        makeroot(x);
        access(y);splay(y);
        return pos[y];
    }
}L;
int n,m;
struct wzq{
    int x,y,a,b;
}c[maxn];
int main(){
    n=read();m=read();
    for(int i=1;i<=m;i++){
        c[i].x=read();c[i].y=read();
        c[i].a=read();c[i].b=read();
    }
    sort(c+1,c+m+1,[](wzq i,wzq j){return i.a<j.a;});
    int ans=inf;
    for(int i=1;i<=m;i++){
        int x=c[i].x,y=c[i].y;
        w[i+n]=c[i].b;      //边化点
        if(L.findroot(x)!=L.findroot(y)){
            L.link(x,i+n);L.link(i+n,y);
        }
        else {
            int now=L.query_path(x,y);
            if(w[now]>c[i].b){
                L.cut(c[now-n].x,now);L.cut(now,c[now-n].y);
                L.link(x,i+n);L.link(i+n,y);
            }
        }
        if(L.findroot(1)==L.findroot(n)){
            ans=min(ans,c[i].a+w[L.query_path(1,n)]);
        }
    }
    printf("%d\n",(ans==inf)?-1:ans);
    return 0;
}
posted @ 2022-07-19 15:02  I_N_V  阅读(36)  评论(0编辑  收藏  举报