zoukankan      html  css  js  c++  java
  • 【算法学习】【洛谷】树链剖分 & P3384 【模板】树链剖分 P2146 软件包管理器

    刚学的好玩算法,AC2题,非常开心。

    其实很早就有教过,以前以为很难就没有学,现在发现其实很简单也很有用。

    更重要的是我很好调试,两题都是几乎一遍过的。


    介绍树链剖分前,先确保已经学会以下基本技巧:

    DFS序列,线段树/树状数组,LCA(最近公共祖先)

    DFS序列确保你能听懂以下环节,线段树/树状数组是维护序列的有力工具,而LCA涉及树上的很多基本问题。


    经常会遇到这样的题目:

    对于一棵树,给x到y的路径上的点/边都做一个操作,并且查询x到y的路径上的点/边的值。

    如果不是x到y的路径,而是节点x的子树,这可以方便地用DFS序列维护。

    因为一棵子树在一个DFS序列中是连续的一段区间。

    用类似的思想,我们尝试把树上的链也变成一段连续区间。

    然而这是不可能的,因为链上是有分支的。

    于是我们退而求其次,把一个链分成有限个区间,这边是有可能实现的,应运而生的算法就是树链剖分。


    对于一棵树上的每个节点(x),我们定义(siz(x))为以(x)为根的子树的节点个数。

    对于一棵树上的每一个非叶子节点,我们找到它的儿子们中(siz)最大的那一个,并且标记它到这个儿子的这条边。

    那么对树上所有点都进行这个操作之后,被标记的边会形成若干条链,并且覆盖每一个节点。

    我们把这样的边叫做重边,其他的边叫做轻边,把连续重边形成的链叫做重链,一个点也可以组成一条长度为1的重链。


    定理①:每个点属于且只属于一个重链。

    定理②:从一个点到根的路径中,重链个数不超过(O(log_2n))个,(n)表示树大小。

    证明:从一个点向它的父亲走,如果这条边是轻边,那么子树的节点数至少翻倍,重链个数加一。

       那么显然轻边个数不超过(O(log_2n)),而不是轻边的就组成重链,于是重链个数也不超过(O(log_2n))个。


    好了,我们知道了重链的定义,性质,如何使用它呢?

    因为一个点到根不超过(O(log_2n))条重链,那么任意两点间也不会超过(O(log_2n))条重链。

    而每条重链都是一段连续的区间,那么链上问题就变成了区间问题的并了。

    这就实现了链到区间的转化。


    要想使用树链剖分,需要在代码中实现它,我们需要做些什么?

    先考虑用使用重链,求出任意两点之间的路径吧,或者,求出它们的LCA。

    考虑初始时(x,y)两点,我们发现每一条重链都能直接用它的最顶端的节点表示,记作(top)节点。

    那么先看(x,y)属不属于同一条重链:(top(x)=top(y))?

    只要不属于,一定要有一个跑出它的重链,实际上,必然是(top(x),top(y))中深度更深的那个跳出其重链。

    不妨设其为(x),那么(x o y)的路径上就多了一条(top_x o x)的路径,之后执行(x=faz(top(x))),(faz(x))表示(x)的父亲节点。

    不断重复这个操作,直到(top(x)=top(y)),那么这时(x,y)的LCA就是(x,y)中深度小的那个了。

    这样的时间复杂度是(O(log_2n))的。


    在做这些操作前,我们需要先求出(top(x))、(siz(x))、(dep(x))、(faz(x))等关键数组。

    如何求出?其实一次DFS就够了。

    DFS(u){
    	dep[u] = dep[faz[u]] + 1
    	Heavy = 0, siz[u] = 1
    	for ( i = u.sons ) {
    		faz[i] = u
    		DFS(i)
    		siz[u] = siz[u] + siz[i]
    		if ( siz[Heavy] < siz[i] )
    			Heavy = i
    	}
    	son[u] = Heavy
    	for ( i = u.sons ) {
    		if ( i != Heavy ) {
    			j=i
    			while ( j != NULL )
    				top[j] = i, j = son[j]
    		}
    	}
    }

    这样就能处理好了,这里的(son[x])表示(x)的重儿子。

    还有一个小问题就是:我们没有把树中的每个节点映射到序列中的某个编号上。

    在通常的DFS序列中这个编号就是(dfn)数组,在树链剖分中,我们也可以使用(dfn)数组,但是要满足链上的点在一个区间中。

    做到这一点也不难,直接在里面一样做就好,也不用第二个DFS。

    问题在于,有的题目不止要你改链上的点,还要子树修改,而一棵树的总重链数是可以到(O(n))级别的,自然不能也用链做。

    然而子树修改是个很简单的东西,只要满足子树内有序就好了,但是要同时满足的话……

    也不是不可能,再写一个DFS就好啦。就和平常的求DFS序列的DFS一样,不过要注意先遍历到重儿子,这样可以保证重链上的点的编号连续。


    总结:

    树链剖分,DFS序列,本质上都是一样的,相当于一个树到数组的映射,把分支的结构变成连续的结构,并且满足一些性质。

    DFS序列只要满足子树的节点是连续的即可,而树链剖分还要满足特定树链的节点连续。

    这必然使树链剖分有更强的操作性,树链剖分是很重要的静态在线树上算法,通过预处理,一切操作都可以在线地完成。


    接下来放两道例题:

    洛谷 P3384 【模板】树链剖分

    直接的树链剖分模板,使用线段树维护区间。

    // luogu-judger-enable-o2
    #include<cstdio>
    #define F(i,a,b) for(int i=a;i<=(b);++i)
    #define F2(i,a,b) for(int i=a;i<(b);++i)
    #define eF(i,u) for(int i=h[u];i;i=nxt[i])
    #define swap(a,b) tmp=a, a=b, b=tmp
    
    int n,q,R,P,a[100001],b[100001],tmp;
    int h[100001],to[200001],nxt[200001],tot;
    inline void ins(int x,int y){nxt[++tot]=h[x];to[tot]=y;h[x]=tot;}
    int top[100001],son[100001],siz[100001],dep[100001],faz[100001],dfn[100001],cnt;
    int dat[262145],laz[262145];
    
    inline void Ex(int u){
        for(int i=u;i;i=son[i]) top[i]=u;
    }
    
    void DFS(int u,int f){
        siz[u]=1; dep[u]=dep[faz[u]]+1;
        int Heavy=0;
        eF(i,u) if(to[i]!=f){
            DFS(to[i],faz[to[i]]=u);
            siz[u]+=siz[to[i]];
            if(siz[Heavy]<siz[to[i]]) Heavy=to[i];
        }
        son[u]=Heavy;
        eF(i,u) if(to[i]!=f) if(to[i]!=Heavy) Ex(to[i]);
    }
    
    void DFS2(int u,int f){
        dfn[u]=++cnt; b[cnt]=a[u];
        if(son[u]) DFS2(son[u],u);
        eF(i,u) if(to[i]!=f) if(to[i]!=son[u]) DFS2(to[i],u);
    }
    
    void build(int i,int l,int r){
        if(l==r) {dat[i]=b[l]%P; return;}
        int mid=l+r>>1;
        build(i<<1,l,mid), build(i<<1|1,mid+1,r);
        dat[i]=(dat[i<<1]+dat[i<<1|1])%P;
    }
    
    inline void push_down(int i,int len){
        int len2=len>>1;
        dat[i<<1]=(dat[i<<1]+(len-len2)*laz[i])%P;
        dat[i<<1|1]=(dat[i<<1|1]+len2*laz[i])%P;
        laz[i<<1]=(laz[i<<1]+laz[i])%P;
        laz[i<<1|1]=(laz[i<<1|1]+laz[i])%P;
        laz[i]=0;
    }
    
    void Add(int i,int l,int r,int a,int b,int x){
        if(r<a||b<l) return;
        if(a<=l&&r<=b) {dat[i]=(dat[i]+x*(r-l+1))%P; laz[i]=(laz[i]+x)%P; return;}
        push_down(i,r-l+1);
        int mid=l+r>>1;
        Add(i<<1,l,mid,a,b,x); Add(i<<1|1,mid+1,r,a,b,x);
        dat[i]=(dat[i<<1]+dat[i<<1|1])%P;
    }
    
    int Qur(int i,int l,int r,int a,int b){
        if(r<a||b<l) return 0;
        if(a<=l&&r<=b) return dat[i];
        push_down(i,r-l+1);
        int mid=l+r>>1;
        return (Qur(i<<1,l,mid,a,b)+Qur(i<<1|1,mid+1,r,a,b))%P;
    }
    
    inline void Add(int x,int y,int z){
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            Add(1,1,n,dfn[top[x]],dfn[x],z);
            x=faz[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        Add(1,1,n,dfn[x],dfn[y],z);
    }
    
    inline int Qur(int x,int y){
        int ans=0;
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            ans=(ans+Qur(1,1,n,dfn[top[x]],dfn[x]))%P;
            x=faz[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        return (ans+Qur(1,1,n,dfn[x],dfn[y]))%P;
    }
    
    int main(){
        scanf("%d%d%d%d",&n,&q,&R,&P);
        F(i,1,n) scanf("%d",a+i);
        int x,y; F2(i,1,n) scanf("%d%d",&x,&y), ins(x,y), ins(y,x);
        dep[R]=1; DFS(R,0); Ex(R);
        DFS2(R,0);
        build(1,1,n);
        F(i,1,q){
            int opt,x,y,z;
            scanf("%d",&opt);
            if(opt==1) scanf("%d%d%d",&x,&y,&z), Add(x,y,z);
            if(opt==2) scanf("%d%d",&x,&y), printf("%d
    ",Qur(x,y));
            if(opt==3) scanf("%d%d",&x,&z), Add(1,1,n,dfn[x],dfn[x]+siz[x]-1,z);
            if(opt==4) scanf("%d",&x), printf("%d
    ",Qur(1,1,n,dfn[x],dfn[x]+siz[x]-1));
        }
        return 0;
    }
    

    洛谷 P2146 软件包管理器

    NOI2015的题目,要在树上进行清零和置一的操作。有链上和子树上的操作。

    #include<cstdio>
    #include<algorithm>
    #define F(i,a,b) for(int i=a;i<=(b);++i)
    #define F2(i,a,b) for(int i=a;i<(b);++i)
    #define eF(i,u) for(int i=h[u];i;i=nxt[i])
    using namespace std;
    
    int n,q,a[100005],b[100005],tmp;
    int h[100005],to[100005],nxt[100005],tot;
    inline void ins(int x,int y){nxt[++tot]=h[x];to[tot]=y;h[x]=tot;}
    int top[100005],son[100005],siz[100005],dep[100005],faz[100005],dfn[100005],cnt;
    int dat[262145],laz[262145];
    
    inline void Ex(int u){
        for(int i=u;i;i=son[i]) top[i]=u;
    }
    
    void DFS(int u){
        siz[u]=1; dep[u]=dep[faz[u]]+1;
        int Heavy=0;
        eF(i,u){
            DFS(to[i]);
            siz[u]+=siz[to[i]];
            if(siz[Heavy]<siz[to[i]]) Heavy=to[i];
        }
        son[u]=Heavy;
        eF(i,u) if(to[i]!=Heavy) Ex(to[i]);
    }
    
    void DFS2(int u){
        dfn[u]=++cnt; b[cnt]=a[u];
        if(son[u]) DFS2(son[u]);
        eF(i,u) if(to[i]!=son[u]) DFS2(to[i]);
    }
    
    inline void push_down(int i,int len){
        if(!laz[i]) return;
        int len2=len>>1;
        laz[i<<1]=laz[i<<1|1]=laz[i];
        if(laz[i]==1) dat[i<<1]=dat[i<<1|1]=0;
        else dat[i<<1]=len-len2, dat[i<<1|1]=len2;
        laz[i]=0;
    }
    
    void Mdf(int i,int l,int r,int a,int b,int x){
        if(r<a||b<l) return;
        if(a<=l&&r<=b) {dat[i]=x?r-l+1:0; laz[i]=x+1; return;}
        push_down(i,r-l+1);
        int mid=l+r>>1;
        Mdf(i<<1,l,mid,a,b,x); Mdf(i<<1|1,mid+1,r,a,b,x);
        dat[i]=dat[i<<1]+dat[i<<1|1];
    }
    
    int Qur(int i,int l,int r,int a,int b){
        if(r<a||b<l) return 0;
        if(a<=l&&r<=b) return dat[i];
        push_down(i,r-l+1);
        int mid=l+r>>1;
        return Qur(i<<1,l,mid,a,b)+Qur(i<<1|1,mid+1,r,a,b);
    }
    
    inline void Mdf(int y){
        int x=1;
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            Mdf(1,1,n,dfn[top[x]],dfn[x],1);
            x=faz[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        Mdf(1,1,n,dfn[x],dfn[y],1);
    }
    
    inline int Qur(int y){
        int x=1,ans=0;
        while(top[x]!=top[y]){
            if(dep[top[x]]<dep[top[y]]) swap(x,y);
            ans=ans+Qur(1,1,n,dfn[top[x]],dfn[x]);
            x=faz[top[x]];
        }
        if(dep[x]>dep[y]) swap(x,y);
        return ans+Qur(1,1,n,dfn[x],dfn[y]);
    }
    
    int main(){
        scanf("%d",&n);
        int x; F2(i,1,n) scanf("%d",&x), ins(x+1,i+1), faz[i+1]=x+1;
        ++n;
        dep[1]=1; DFS(1); Ex(1);
        DFS2(1);
        scanf("%d",&q);
        F(i,1,q){
            char s[10];
            int x,y;
            scanf("%s%d",s,&x); ++x;
            if(s[0]=='i') y=Qur(x), Mdf(x), printf("%d
    ",Qur(x)-y);
            else printf("%d
    ",Qur(1,1,n,dfn[x],dfn[x]+siz[x]-1)), Mdf(1,1,n,dfn[x],dfn[x]+siz[x]-1,0);
        }
        return 0;
    }
  • 相关阅读:
    《软件安全系统设计最佳实践》课程大纲
    《基于IPD流程的成本管理架构、方法和管理实践》培训大纲
    《技术规划与路标开发实践 》内训在芜湖天航集团成功举办!
    年终总结,究竟该写什么?
    Docker安装RabbitMQ
    Ubuntu18安装docker
    Ubuntu18.04安装MySQL
    Windows常用CMD命令
    ping不通域名的问题OR请求找不到主机-问题
    JMeter 函数用法
  • 原文地址:https://www.cnblogs.com/PinkRabbit/p/8503611.html
Copyright © 2011-2022 走看看