【笔记】树链(熟练)剖分

/*参考 https://www.cnblogs.com/LoveYayoi/p/6745541.html

https://www.cnblogs.com/George1994/p/7821357.html

以及我校学长的优秀讲解。

*/

树链剖分可以将树剖分成一条一条的链,形成一条新链,然后通过一些维护线性数据的数据结构(比如线段树)来维护树上的信息,例如 树上求两点间边or点权和、边权or点权max or min、修改边or点权 等。

P.S.来自另一位大佬(LoveYayoi)的解释:给定一棵形态不变的树,要求支持查询一条链,修改一条链。

对于“给定一棵形态变化的树,要求支持查询和修改一条链”,需要用到LCT。

一、概念:

重儿子:u的所有子节点中,子树最大的一个。特别的,根节点自己是一个重儿子。

轻儿子:u的所有子节点中,除了重儿子的其他儿子。

重边:u与其重儿子所连的边。

轻边:u与其轻儿子所连的边。

重链:重边所形成的链。

轻链:轻边所形成的链。

dep[i]:点i在树上的深度。

siz[i]:以点i为根的子树(包括点i)的大小(点数之和)。

fa[i]:点i的父节点。

son[i]:点i的重儿子。

top[i]:点i所在的链的顶端。对于轻链,其顶端为点i本身。

dfn[i]:点i在DFS序中的编号。

rnk[i]:DFS中编号为i的点在树上的编号。

二、性质:

1.一棵树被剖分后所形成的重链数与轻链数不会超过logn条

->这样每蹦一次操作一次,共操作O(重链数)=O(logn)次;每次操作的复杂度是O(logn);

总共的时间复杂度是O(qlogn^2)

2.若一条边(u,v)是一条轻边,则size(v)<size(u)/2。

->若一条边(u,v)是一条轻边,即点v是轻儿子,则点u至少有两个子节点;若点v是轻儿子,其子节点最多不会超过size(u)/2,否则v就成了重儿子。

三、代码:

(一)预处理

第一遍DFS,预处理出dep[]、fa[]、siz[]。

void dfs1(int u,int f,int d){
    dep[u]=d;
    siz[u]=1;
    fa[u]=f;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]) continue;
        dfs1(v,u,d+1);
        siz[u]+=siz[v];
        if(!son[u]||siz[v]>siz[son[u]]) son[u]=v;
    }
}

第二遍DFS,预处理出tid[]、rnk[]、top[]。

int idx;
void dfs2(int u,int tp){
    dfn[u]=++idx;//u的DFS序编号为idx 
    rnk[idx]=u;//在DFS序上编号为idx的点在树上的编号为u 
    top[u]=tp;
    if(!son[u]) return;
    dfs2(son[u],tp);//处理重链 
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]||v==son[u]) continue;
        dfs2(v,v);//处理轻链 
    }
}

(二)链上查询修改问题

①LCA(u,v)例:P3379 【模板】最近公共祖先(LCA)

对于树上两点u与v间路径的一系列问题,可以将其拆分为u->lca(u,v)和v->lca(u,v)两个子问题。类似于倍增求LCA,树链剖分也是类似的方法。对于u和v两点的LCA,我们让所在链的深度(即dep[top[]])大的一点跳到另一点所在的链上,当两点同在一链时,深度小的一点就是LCA(u,v)

int lca(int u,int v){
    int f1=top[u],f2=top[v];
    //令所在链的深度大的一点跳到另一点所在的链上 
    while(f1!=f2){
        //令u为所在链的深度大的一点 
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v); 
        }
        u=fa[f1];
        //u跳到所在链的顶端f1=top[u]的父节点,从而进入下一条重链。 
        f1=top[u];
        //同样所在链顶端f1也要修改 
    }
    //当两点同处一链时,深度小的为LCA 
    int lca;
    if(dep[u]>=dep[v]) {
        lca=v;
    }
    else lca=u;
    return lca;
}

②链上点权和、点权max... 例:P2590 [ZJOI2008]树的统计

在求LCA时通过线段树随时更新ans即可。注意查询时查询的是点的DFS序

 ->链上点权和

int lca_sum(int u,int v){
    int ans=0;
    int f1=top[u],f2=top[v];
    while(f1!=f2){
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v);
        }
        ans+=ask_sum(1,dfn[f1],dfn[u]);//线段树区间求和 
        u=fa[f1];
        f1=top[u];
    }
    int lca,tmp;
    if(dep[u]>=dep[v]) {
        lca=v,tmp=u;
    }
    else lca=u,tmp=v;
    ans+=ask_sum(1,dfn[lca],dfn[tmp]);
    return ans;
}

->链上最大点权

int lca_max(int u,int v){
    int ans=-(1<<30);
    int f1=top[u],f2=top[v];
    while(f1!=f2){
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v);
        }
        ans=max(ans,ask_max(1,tid[f1],tid[u]));//线段树区间max 
        u=fa[f1];
        f1=top[u];
    }
    int lca,tmp;
    if(dep[u]>=dep[v]) {
        lca=v,tmp=u;
    }
    else lca=u,tmp=v;
    ans=max(ans,ask_max(1,tid[lca],tid[tmp]));
    return ans;
}

P.S.

1.求链上边权和、链上边权max...时,可以将边权下放为点权。此时注意查询的边界问题:由于u向上蹦时,会从u蹦到fa[top[u]]。事实上我们只经过了top[u]~u之间的边,因此查询的边界为dfn[top[u]]~dfn[u],而不包括dfn[fa[top[u]]]的信息,因为没有经过其保存的边。

 2.建树时,已知的是线段树上的编号,即DFS序编号。所以需要将线段树编号转化为树上的编号,例如tree[u].sum=val[rnk[l]],其中val[]表示点权,rnk[l]表示DFS编号为l的点在树上的编号为dfn[l]。

(三)子树查询修改问题 例:P3384 【模板】树链剖分

 对于以u为根的子树,其DFS序为一段有序且连续的序列,所以我们记录点u的DFS序的范围,对于修改子树的操作直接修改DFS序的两端即可。

(代码有点长。。。就折叠了)

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int N=100000+100;
int n,m,r,p;
//邻接表 
struct ed{
    int to,nxt;
}e[N<<1];
int head[N],tot;
void add(int u,int v){
    e[++tot]=(ed){v,head[u]};
    head[u]=tot;
}
//线段树
int dep[N],siz[N],fa[N],top[N],son[N],dfn[N],rnk[N],left[N],right[N],val[N];
struct node{
    int l,r;
    long long sum;
    long long add;
}tree[N<<2];
void updata(int u){
    tree[u].sum=tree[u<<1].sum+tree[u<<1|1].sum;
    tree[u].sum%=p;
}
void build(int u,int l,int r){
    tree[u].l=l,tree[u].r=r,tree[u].add=0;
    if(l==r){
        tree[u].sum=val[rnk[l]];
        return ;
    }
    int mid=(l+r)>>1;
    build(u<<1,l,mid);build(u<<1|1,mid+1,r);
    updata(u);
}
void pushdown(int u){
    int l=tree[u].l,r=tree[u].r,k=tree[u].add;
    if(k){
        tree[u<<1].add+=k;
        tree[u<<1].sum+=(tree[u<<1].r-tree[u<<1].l+1)*k;
        tree[u<<1].sum%=p;
        tree[u<<1|1].add+=k;
        tree[u<<1|1].sum+=(tree[u<<1|1].r-tree[u<<1|1].l+1)*k;
        tree[u<<1|1].sum%=p;
        tree[u].add=0;
    }
}
void modify(int u,int ql,int qr,int k){
    int l=tree[u].l,r=tree[u].r;
    if(ql<=l&&qr>=r){
        tree[u].sum+=(r-l+1)*k;
        tree[u].sum%=p;
        tree[u].add+=k;
        return ;
    }
    pushdown(u);
    int mid=(l+r)>>1;
    if(ql<=mid) modify(u<<1,ql,qr,k);
    if(qr>mid) modify(u<<1|1,ql,qr,k);
    updata(u);
}
long long ask_sum(int u,int ql,int qr){
    long long ans=0;
    int l=tree[u].l,r=tree[u].r;
    if(ql<=l&&qr>=r) return tree[u].sum%p;
    if(ql>r||qr<l) return 0;
    pushdown(u);
    int mid=(l+r)>>1;
    if(ql<=mid) {
        ans+=ask_sum(u<<1,ql,qr);
        ans%=p;
    }
    if(qr>mid) {
        ans+=ask_sum(u<<1|1,ql,qr);
        ans%=p;
    }
    return ans;
}
//树链剖分 
void dfs1(int u,int f,int d){
    dep[u]=d;
    fa[u]=f;
    siz[u]=1;
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]) continue;
        dfs1(v,u,d+1);
        siz[u]+=siz[v];
        if(!son[u]||siz[v]>siz[son[u]]) son[u]=v;
    }
}
int idx;
void dfs2(int u,int tp){
    top[u]=tp;
    dfn[u]=++idx;
    rnk[idx]=u;
    left[u]=right[u]=idx;
    if(!son[u]) return;
    dfs2(son[u],tp);
    for(int i=head[u];i;i=e[i].nxt){
        int v=e[i].to;
        if(v==fa[u]||v==son[u]) continue;
        dfs2(v,v);
    }
    right[u]=idx;
}
void lca_modify(int u,int v,int k){
    int f1=top[u],f2=top[v];
    while(f1!=f2){
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v);
        }
        modify(1,dfn[f1],dfn[u],k);
        u=fa[f1];
        f1=top[u];
    }
    int lca,tmp;
    if(dep[u]>=dep[v]) lca=v,tmp=u;
    else lca=u,tmp=v;
    modify(1,dfn[lca],dfn[tmp],k);
    return ;
}
long long lca_sum(int u,int v){
    int f1=top[u],f2=top[v];
    long long ans=0;
    while(f1!=f2){
        if(dep[f1]<dep[f2]){
            swap(f1,f2);
            swap(u,v);
        }
        ans+=ask_sum(1,dfn[f1],dfn[u]);
        ans%=p;
        u=fa[f1];
        f1=top[u];
    }
    int lca,tmp;
    if(dep[u]>=dep[v]) lca=v,tmp=u;
    else lca=u,tmp=v;
    ans+=ask_sum(1,dfn[lca],dfn[tmp]);
    ans%=p;
    return ans;
}
int main(){
    scanf("%d%d%d%d",&n,&m,&r,&p);
    for(int i=1;i<=n;i++) scanf("%d",&val[i]);
    for(int i=1;i<n;i++){
        int u,v;
        scanf("%d%d",&u,&v);
        add(u,v);
        add(v,u);
    }
    dfs1(r,0,1);
    dfs2(r,r);
    build(1,1,n);
    for(int i=1;i<=m;i++){
        int op,x,y,z;
        scanf("%d",&op);
        if(op==1){//链上修改 
            scanf("%d%d%d",&x,&y,&z);
            lca_modify(x,y,z);
        }
        else if(op==2){//链上查询 
            scanf("%d%d",&x,&y);
            printf("%lld\n",lca_sum(x,y));
        }
        else if(op==3){//子树修改 
            scanf("%d%d",&x,&z);
            modify(1,left[x],right[x],z);
        }
        else if(op==4){//子树查询 
            scanf("%d",&x);
            printf("%lld\n",ask_sum(1,left[x],right[x]));
        }
    }
    return 0;
} 
P3384 【模板】树链剖分

 

posted @ 2018-06-02 20:02  dprswdr  阅读(301)  评论(0)    收藏  举报