树链剖分

树链剖分

引入:

如果我们需要建立一棵树,而且需要维护树上路径的信息,较为方便的做法就是树链剖分。

形式:

树剖主要有两种形式:

  1. 重链剖分:利用 \(size\) 定义重节点。
  2. 长链剖分:利用 \(depth\) 定义重节点。

一般来说,树链剖分指的是重链剖分。本篇博客主要讲的也是重链剖分。

作用:

几乎所有树上问题都可以用树链剖分解决,这里给出一些作用:

  1. 修改树上两点之间值
  2. 查询树上两点之间权值的和/极值/........
  3. \(lca\)
  4. 路径长度
  5. ......

定义:

\(son[x]\) :重子节点 表示 \(x\) 子节点重子树最大的节点。
\(dfn[x]\) : \(dfs\)
\(rk[x]\) :表示该节点\(dfs\) 序对应的节点。
\(sizes[x]\): 子节点个数。
\(f[x]\) : 父亲节点。
\(d[x]\) :节点深度。

基础的就这六个,此外可能还有定义的边权值,就根据题意即可。

代码部分:

基础代码:

树链剖分最基础的部分就是两次\(dfs\)

首先看代码:

void dfs1(int x,int last){
    d[x]=d[last]+1;//深度
    sizes[x]=1;//节点本身也是其子树
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i];
        if(y==last) continue;
        fa[y]=x;//父亲
        dfs1(y,x);
        sizes[x]+=sizes[y];
        if(sizes[y]>sizes[son[x]]||!son[x])  son[x]=y;
        //对于节点x,其子树最大的儿子就是其重儿子
        //如果儿子节点子树大小相同,那么重儿子随便哪个都可以
    }
}

void dfs2(int x,int topfather){
    dfn[x]=++cnt;//dfs序
    top[x]=topfather;//这个点所在重链的顶端,对于求lca和链有极大帮助
    rk[cnt]=x;//dfs序所对应的节点编号
    if(!son[x]) return;
        dfs2(son[x],topfather);//我们首先进入重儿子来保证一条重链上各个节点dfs序连续
    for(int i=head[x];i;i=nxt[i]){
        int y=ver[i];
        if(y!=son[x]&&y!=fa[x]) dfs2(y,y);//位于轻链底端,top为本身
    }
}

对于每一部分都有解释,就不再多说了。

\(lca\) 部分:

我们已经知道了该树的重链,以及\(dfs\) 序,那么在 \(O(\log n)\) 的时间里我们就可以求出 \(x,y\) 节点的 \(lca\) 。(没有学过倍增 \(lca\) 建议先学倍增)

我们的思路是要把 \(x,y\) 两个节点弄到同一条重链上,因此我们需要将深度深的节点不断蹦到其对应重链顶端的父亲上。

当在同一条重链上时,比较两点深度,深度浅的就是其公共父亲。

代码:

int lca(int x,int y){
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]) swap(x,y);
        x=fa[top[x]];
    }
    if(d[x]>d[y]) swap(x,y);
    return x;
}

数据结构:

因为树剖在树上,所以用线段树存储比较方便。

对应的操作也大多是跟线段树相同的操作:

struct tree{
    int l,r,sum,lazy;
}t[N];
int len(int x){
    return t[x].r-t[x].l+1;
}
void pushdown(int x){//懒标记
    if(t[x].lazy){
        int lz=t[x].lazy,ls=x<<1,rs=x<<1|1;
        t[ls].lazy=(t[ls].lazy+lz)%mod;
        t[rs].lazy=(t[rs].lazy+lz)%mod;
        t[ls].sum=(t[ls].sum+lz*len(ls))%mod;
        t[rs].sum=(t[rs].sum+lz*len(rs))%mod;
        t[x].lazy=0;
    }
}
void pushup(int x){
    t[x].sum=(t[x<<1].sum+t[x<<1|1].sum)%mod;
}
void update(int l,int r,int c,int x){//区间修改
    if(t[x].l>=l&&t[x].r<=r){
        t[x].lazy=(t[x].lazy+c)%mod;
        t[x].sum=(t[x].sum+len(x)*c)%mod;
        return;
    }
    pushdown(x);
    int mid=t[x].l+t[x].r>>1;
    if(mid>=l) update(l,r,c,x<<1);
    if(mid<r) update(l,r,c,x<<1|1);
    pushup(x);
}
void build(int l,int r,int x){
    t[x].l=l;t[x].r=r;t[x].sum=0;
    if(l==r){
        t[x].sum=val[rk[l]];//给这个节点赋值
        return;
    }
    int mid=l+r>>1;
    build(l,mid,x<<1);
    build(mid+1,r,x<<1|1);
    pushup(x);
}

例题部分:

P3384 【模板】轻重链剖分/树链剖分

模板题,包含更改路径权值,求路径权值,更改点权值,求子树点值和。

更改路径权值:

void updatetree(int x,int y,int c){//对于树上一个节点到另一个节点增加值
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]) swap(x,y);//根据深度进行枚举
        update(dfn[top[x]],dfn[x],c,1); 
        x=fa[top[x]];
    }
    if(dfn[x]>dfn[y]) swap(x,y);
    update(dfn[x],dfn[y],c,1); 
}

求路径权值:

int sum(int x,int y){//两点之间的值
    int res=0;
    while(top[x]!=top[y]){//两者链的顶端不同
        if(d[top[x]]<d[top[y]]) swap(x,y);//先跳深度大的
        res=(res+query(dfn[top[x]],dfn[x],1))%mod;
        x=fa[top[x]];
    }
    if(dfn[x]>dfn[y]) swap(x,y);
    return (res+query(dfn[x],dfn[y],1))%mod;//两者在同一条链上,直接求就行
}

给子树加权值:

else if(x==3){
    scanf("%lld%lld",&l,&z);
    update(dfn[l],dfn[l]+sizes[l]-1,z,1);//子树区间的右端点
    //在之前求过以l为根的子树大小,因此直接加上就是子树。
}

求子树点权和:

int query(int l,int r,int x){//线段树上加和
    if(t[x].l>=l&&t[x].r<=r) return t[x].sum;
    pushdown(x);
    int mid=t[x].l+t[x].r>>1,res=0;
    if(mid>=l) res+=query(l,r,x<<1);
    if(mid<r) res+=query(l,r,x<<1|1);
    return res%mod;
}
scanf("%lld",&l);
printf("%lld\n",query(dfn[l],dfn[l]+sizes[l]-1,0));

P2590 [ZJOI2008]树的统计

又是一道模板题,操作涉及:更改点权值,求路径最大权值,求路径权值

int query_sum(int l,int r,int x){
    int ans=0;
    if(l<=t[x].l&&r>=t[x].r) return t[x].sum;
    int mid=t[x].l+t[x].r>>1;
    if(l<=mid) ans+=query_sum(l,r,x<<1);
    if(r>mid) ans+=query_sum(l,r,x<<1|1);
    return ans;
}
int query_max(int l,int r,int x){
    int ans=0;
    if(l<=t[x].l&&r>=t[x].r) return t[x].maxn;
    int mid=t[x].l+t[x].r>>1;
    if(l<=mid) ans=max(ans,query_max(l,r,x<<1));
    if(r>mid) ans=max(ans,query_max(l,r,x<<1|1));
    return ans;
}
int tree_sum(int x,int y){
    int ans=0;
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]) swap(x,y);
        ans+=query_sum(dfn[top[x]],dfn[x],1);
        x=f[top[x]];
    }
    if(dfn[x]>dfn[y]) swap(x,y);
    ans=max(ans,query_sum(dfn[x],dfn[y],1));
    return ans;
}
int tree_max(int x,int y){
    int ans=0;
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]) swap(x,y);
        ans=max(ans,query_max(dfn[top[x]],dfn[x],1));
        x=f[top[x]];
    }
    if(dfn[x]>dfn[y]) swap(x,y);
    ans=max(ans,query_max(dfn[x],dfn[y],1));
    return ans;
}

P2486 [SDOI2011]染色

这道题其实也是一道模板题。

我们需要记录线段树里的左儿子的颜色和右儿子的颜色,如果两个区间中间颜色相同,那么答案需要减少。

#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=4e5+5;
int nxt[N],head[N],ver[N],tot;
int Lc,Rc;//记录序列左边和右边的颜色
struct node{
    int l,r,sum,lc,rc;
    int lazy;
}t[N];
int n,m;
int dfn[N],rk[N],d[N],f[N],sizes[N],cnt,son[N],top[N];
int col[N];
void add(int x,int y){
    ver[++tot]=y;
    nxt[tot]=head[x];
    head[x]=tot;
}
void build(int l,int r,int x){
    t[x].l=l,t[x].r=r,t[x].sum=0;
    if(l==r) return;
    int mid=l+r>>1;
    build(l,mid,x<<1);
    build(mid+1,r,x<<1|1);
}
void pushdown(int x){
    if(t[x].lazy){
        int L=x<<1,R=x<<1|1;
        t[L].lazy=t[R].lazy=t[x].lazy;
        t[L].sum=t[R].sum=1;
        t[L].lc=t[L].rc=t[x].lc;
        t[R].lc=t[R].rc=t[x].lc;
        t[x].lazy=0;
    }
}
void pushup(int x){
    t[x].lc=t[x<<1].lc;
    t[x].rc=t[x<<1|1].rc;
    int res=t[x<<1].sum+t[x<<1|1].sum;
    // cout<<res<<endl;
    if(t[x<<1].rc==t[x<<1|1].lc) res--;//中间颜色相同
    t[x].sum=res;
}
.....dfs过程
void update(int l,int r,int x,int color){
    if(t[x].l==l&&t[x].r==r){
        t[x].sum=t[x].lazy=1;
        t[x].lc=t[x].rc=color;
        return;
    }
    pushdown(x);
    int mid=t[x].l+t[x].r>>1;
    if(r<=mid) update(l,r,x<<1,color);
    else if(l>mid) update(l,r,x<<1|1,color);
    else{
        update(l,mid,x<<1,color);
        update(mid+1,r,x<<1|1,color);
    }
    pushup(x);
}
int query(int l,int r,int L,int R,int x){
    if(t[x].l==L) Lc=t[x].lc;//获取边界值
    if(t[x].r==R) Rc=t[x].rc;
    if(t[x].l==l&&t[x].r==r) return t[x].sum;
    pushdown(x);
    int mid=t[x].l+t[x].r>>1;
    if(r<=mid) return query(l,r,L,R,x<<1);
    else if(l>mid) return query(l,r,L,R,x<<1|1);
    else{
        int ans=query(l,mid,L,R,x<<1)+query(mid+1,r,L,R,x<<1|1);
        if(t[x<<1].rc==t[x<<1|1].lc) ans--;
        return ans;
    }
    pushup(x);
}

void solve1(int x,int y,int c){
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]) swap(x,y);
        update(dfn[top[x]],dfn[x],1,c);
        x=f[top[x]];
    }
    if(d[x]>d[y]) swap(x,y);
    update(dfn[x],dfn[y],1,c);
}
int solve2(int x,int y){
    int ans=0,ans1=-1,ans2=-1;
    while(top[x]!=top[y]){
        if(d[top[x]]<d[top[y]]){
            swap(x,y);swap(ans1,ans2);
        }

        ans+=query(dfn[top[x]],dfn[x],dfn[top[x]],dfn[x],1);
        if(Rc==ans1) ans--;
        ans1=Lc;
        x=f[top[x]];
    }

    if(d[x]<d[y]) swap(x,y),swap(ans1,ans2);
    ans+=query(dfn[y],dfn[x],dfn[y],dfn[x],1);//忘写了:(
    if(Rc==ans1) ans--;
    if(Lc==ans2) ans--;
    return ans;
}
signed main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) scanf("%lld",&col[i]);
    for(int i=1,x,y;i<n;i++){
        scanf("%lld%lld",&x,&y);add(x,y);add(y,x);
    }
    cnt=0;dfs1(1,0);dfs2(1,1);
    cnt=1;build(1,n,1);
    for(int i=1;i<=n;i++){
        update(dfn[i],dfn[i],1,col[i]);
    }
    while(m--){
        char op[3];int a,b,c;
        scanf("%s",op);//字符串读入问题
        // cout<<op<<endl;
        if(op[0]=='C'){
            scanf("%lld%lld%lld",&a,&b,&c);
            solve1(a,b,c);
        }
        else{
            scanf("%lld%lld",&a,&b);
            printf("%lld\n",solve2(a,b));
        }
    }
    system("pause");
    return 0;
}

P2146 [NOI2015] 软件包管理器

算是半个模板题:

对于操作一,我们可以统计\(x\)到根节点未安装的软件包的个数,然后区间修改为已安装。

对于操作二,我们可以统计\(x\)所在子树已安装软件包的个数,然后将子树修改为未安装。

代码:

int sum(int x){
    int ans=0;
    while(top[x]){
        ans+=dfn[x]-dfn[top[x]]-query(dfn[top[x]],dfn[x],rt)+1;
        update(dfn[top[x]],dfn[x],1,rt);
        x=fa[top[x];
    }
    ans+=dfn[x]-dfn[0]-query(dfn[0],dfn[x],rt)+1;
    update(dfn[0],dfn[x],1,rt);
    return ans;
}
{   if(op=="install") printf("%lld\n",sum(x));
    else if(op=="uninstall"){
        printf("%lld\n",query(dfn[x],dfn[x]+sizes[x]-1,rt));
        update(dfn[x],dfn[x]+sizes[x]-1,0,rt);
    }  
}

P2680 [NOIP2015 提高组] 运输计划

这题还是比较有难度的。

我们看见最长路径最短时间这类题目,都可以想到二分答案来解决。

如果成立,那么就比 \(mid\) 答案小,如果不行,那么比 \(mid\) 大。

我们先把树建好,然后依次求两点之间的路径,根据 \(lca\) 求。

\[t[i].dis=dis[t[i].l]+dis[t[i].r]-2*dis[t[i].lca]; \]

那么二分答案的范围是:\([R-maxL,R+1]\) ,其中 \(R\) 是两点之间长度最大值,\(maxL\) 是边权值最大。

在二分答案中,我们首先判断所有路径是否大于\(mid\),如果大于,说明我们不能把这条路径删除。

利用 树上差分 记录当前路径走了多少次,再记录一下当前路走过的个数。

增加一些判断就可以解决题目。一些细节在代码里。

bool check(int x){
    int res=0;memset(C,0,sizeof(C));
    for(int i=1;i<=m;i++)//只判断大路径
        if(t[i].dis>x){//大于路径,因此我们不能把其删除
            C[t[i].l]++;C[t[i].r]++;//树上差分,记录
            C[t[i].lca]-=2; 
            res++;
        }
    for(int i=n;i>=1;i--){
        C[f[rk[i]]]+=C[rk[i]];//每次差分值累加到父亲节点,记录经过次数
        if(val[rk[i]]>=R-x//这条边权值减去合适
            &&C[rk[i]]==res)//被所有大路径经过————表示这些路径的距离比二分值大
            return 1;
    }
    return 0;
}

易错点:

  1. \(dfs\)\(build\) 过程中,我们需要保证根节点是一致的。
  2. 对于记录 \(dfs\) 序的时候,我们要注意 \(cnt\) 的初始量。
  3. 注意一下求 \(lca\) 的顺序,不要写错了。
posted @ 2021-06-30 21:03  Evitagen  阅读(82)  评论(0)    收藏  举报