返回顶部

树链剖分总结

树链剖分总结

参考自:OI Wiki

树链剖分

树链剖分用于将树分割成若干条链的形式,以维护树上路径的信息。

具体来说,将整棵树剖分为若干条链,使它组合成线性结构,然后用其他的数据结构维护信息。

树链剖分(树剖/链剖)有多种形式,如 重链剖分长链剖分 和用于 Link/cut Tree 的剖分(有时被称作“实链剖分”),大多数情况下(没有特别说明时),“树链剖分”都指“重链剖分”。

重链剖分可以将树上的任意一条路径划分成不超过  条连续的链,每条链上的点深度互不相同(即是自底向上的一条链,链上所有点的 LCA 为链的一个端点)。

重链剖分还能保证划分出的每条链上的节点 DFS 序连续,因此可以方便地用一些维护序列的数据结构(如线段树)来维护树上路径的信息。

如:

  1. 修改 树上两点之间的路径上 所有点的值。
  2. 查询 树上两点之间的路径上 节点权值的 和/极值/其它(在序列上可以用数据结构维护,便于合并的信息)

除了配合数据结构来维护树上路径信息,树剖还可以用来 O(log n)(且常数较小)地求 LCA。在某些题目中,还可以利用其性质来灵活地运用树剖。

重链剖分

我们给出一些定义:

定义 重子节点 表示其子节点中子树最大的子结点。如果有多个子树最大的子结点,取其一。如果没有子节点,就无重子节点。

定义 轻子节点 表示剩余的所有子结点。

从这个结点到重子节点的边为 重边

到其他轻子节点的边为 轻边

若干条首尾衔接的重边构成 重链

把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。

如图:

实现:两次dfs

我们先给出一些定义:

  • fa[] 表示节点  在树上的父亲。
  •  d[]表示节点  在树上的深度。
  •  si[]表示节点  的子树的节点个数。
  • son[] 表示节点  的 重儿子
  •  top[]表示节点  所在 重链 的顶部节点(深度最小)。
  •  id[]表示节点  的 DFS 序,也是其在线段树中的编号。
  •  rev[]表示 DFS 序所对应的节点编号,有 。

我们进行两遍 DFS 预处理出这些值,其中第一次 DFS 求出 fa,d,son,si,第二次 DFS 求出 top,id,rev。

void dfs1(int x,int y)//树链剖分第一次dfs,处理所有节点的深度、父节点、子树大小,并找到重儿子。 
{
    d[x]=d[y]+1;
    fa[x]=y;
    si[x]=1;
    for(int i=fi[x];i;i=ne[i])
    {
        int v=to[i];
        if(v==y) continue;
        dfs1(v,x);
        si[x]+=si[v];
        if(si[v]>si[son[x]]) son[x]=v;
    }
}

void dfs2(int x,int t)//树链剖分第二次dfs,处理每个节点的编号、链顶、编号对应的值 
{
    top[x]=t;
    id[x]=++num;
    w[num]=a[x];
    if(son[x]) dfs2(son[x],t);
    for(int i=fi[x];i;i=ne[i])
    {
        int v=to[i];
        if(v==fa[x]||v==son[x]) continue;
        dfs2(v,v);
    }
}

重链剖分的性质

树上每个节点都属于且仅属于一条重链

重链开头的结点不一定是重子节点(因为重边是对于每一个结点都有定义的)。

所有的重链将整棵树 完全剖分

在剖分时 重边优先遍历,最后树的 DFN 序上,重链内的 DFN 序是连续的。按 DFN 排序后的序列即为剖分后的链。

一颗子树内的 DFN 序是连续的。

可以发现,当我们向下经过一条 轻边 时,所在子树的大小至少会除以二。

因此,对于树上的任意一条路径,把它拆分成从 lca分别向两边往下走,分别最多走 O(log n) 次,因此,树上的每条路径都可以被拆分成不超过 O(log n) 条重链。

常用维护:(线段树&树状数组)

此处线段树举例:

void pu(int x)//下传懒标记 
{
    if(la[x])
    {
        tr[x<<1].w=(tr[x<<1].w%p+la[x]*(tr[x<<1].r-tr[x<<1].l+1)%p)%p;
        tr[x<<1|1].w=(tr[x<<1|1].w%p+la[x]*(tr[x<<1|1].r-tr[x<<1|1].l+1)%p)%p;
        la[x<<1]=(la[x<<1]+la[x])%p;
        la[x<<1|1]=(la[x<<1|1]+la[x])%p;
        la[x]=0;
    }
}

void bu(int x,int l,int r)//建线段树 
{
    tr[x].l=l,tr[x].r=r;
    if(l==r)
    {
        tr[x].w=w[l];
        return ;
    }
    int mid=(l+r)>>1;
    bu(x<<1,l,mid),bu(x<<1|1,mid+1,r);
    tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p;
}

void u(int x,int l,int r,ll k)//线段树区间修改 
{
    pu(x);
    if(tr[x].l>=l&&tr[x].r<=r)
    {
        tr[x].w=(tr[x].w%p+k*(tr[x].r-tr[x].l+1)%p)%p;
        la[x]=(la[x]+k)%p;
        return;
    }
    int mid=(tr[x].l+tr[x].r)>>1;
    if(l<=mid) u(x<<1,l,r,k);
    if(r>mid) u(x<<1|1,l,r,k);
    tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p;
}

ll q(int x,int l,int r)//线段树区间查询 
{
    pu(x);
    if(tr[x].l>=l&&tr[x].r<=r)
        return tr[x].w;
    int mid=(tr[x].l+tr[x].r)>>1;
    ll cnt=0;
    if(l<=mid) cnt=(cnt+q(x<<1,l,r))%p;
    if(r>mid) cnt=(cnt+q(x<<1|1,l,r))%p;
    return cnt%p;
}

建好线段树:

    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    dfs1(root,0);
    dfs2(root,root);
    bu(1,1,n);

修改和查询区间时,不断将区间端点向一条重链靠,边靠近边修改和查询:

void up(int x,int y,ll k)// 原树上点权修改 
{
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]]) swap(x,y);
        u(1,id[top[x]],id[x],k);
        x=fa[top[x]];
    }
    if(d[x]>d[y]) swap(x,y);
    u(1,id[x],id[y],k);
}

ll qu(int x,int y)// 原树上路径点权和 
{
    ll cnt=0;
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]]) swap(x,y);
        cnt=(cnt+q(1,id[top[x]],id[x]))%p;
        x=fa[top[x]];
    }
    if(d[x]>d[y]) swap(x,y);
    cnt=(cnt+q(1,id[x],id[y]))%p;
    return cnt;
}

例题:

 P3384 【模板】轻重链剖分/树链剖分 - 洛谷 | 计算机科学教育新生态 

代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=2e5+5;
int n,m,root,p,tot,num;
int a[N],fa[N],si[N],son[N],d[N],top[N];
int id[N],w[N],la[N];
int fi[N],ne[N*2],to[N*2];
struct xiao
{
    int l,r;
    ll w;
}tr[N*4];

void add(int x,int y)//建树 
{
    ne[++tot]=fi[x];
    fi[x]=tot;
    to[tot]=y;
}

void pu(int x)//下传懒标记 
{
    if(la[x])
    {
        tr[x<<1].w=(tr[x<<1].w%p+la[x]*(tr[x<<1].r-tr[x<<1].l+1)%p)%p;
        tr[x<<1|1].w=(tr[x<<1|1].w%p+la[x]*(tr[x<<1|1].r-tr[x<<1|1].l+1)%p)%p;
        la[x<<1]=(la[x<<1]+la[x])%p;
        la[x<<1|1]=(la[x<<1|1]+la[x])%p;
        la[x]=0;
    }
}

void dfs1(int x,int y)//树链剖分第一次dfs,处理所有节点的深度、父节点、子树大小,并找到重儿子。 
{
    d[x]=d[y]+1;
    fa[x]=y;
    si[x]=1;
    for(int i=fi[x];i;i=ne[i])
    {
        int v=to[i];
        if(v==y) continue;
        dfs1(v,x);
        si[x]+=si[v];
        if(si[v]>si[son[x]]) son[x]=v;
    }
}

void dfs2(int x,int t)//树链剖分第二次dfs,处理每个节点的编号、链顶、编号对应的值 
{
    top[x]=t;
    id[x]=++num;
    w[num]=a[x];
    if(son[x]) dfs2(son[x],t);
    for(int i=fi[x];i;i=ne[i])
    {
        int v=to[i];
        if(v==fa[x]||v==son[x]) continue;
        dfs2(v,v);
    }
}

void bu(int x,int l,int r)//建线段树 
{
    tr[x].l=l,tr[x].r=r;
    if(l==r)
    {
        tr[x].w=w[l];
        return ;
    }
    int mid=(l+r)>>1;
    bu(x<<1,l,mid),bu(x<<1|1,mid+1,r);
    tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p;
}

void u(int x,int l,int r,ll k)//线段树区间修改 
{
    pu(x);
    if(tr[x].l>=l&&tr[x].r<=r)
    {
        tr[x].w=(tr[x].w%p+k*(tr[x].r-tr[x].l+1)%p)%p;
        la[x]=(la[x]+k)%p;
        return;
    }
    int mid=(tr[x].l+tr[x].r)>>1;
    if(l<=mid) u(x<<1,l,r,k);
    if(r>mid) u(x<<1|1,l,r,k);
    tr[x].w=(tr[x<<1].w+tr[x<<1|1].w)%p;
}

void up(int x,int y,ll k)// 原树上点权修改 
{
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]]) swap(x,y);
        u(1,id[top[x]],id[x],k);
        x=fa[top[x]];
    }
    if(d[x]>d[y]) swap(x,y);
    u(1,id[x],id[y],k);
}

ll q(int x,int l,int r)//线段树区间查询 
{
    pu(x);
    if(tr[x].l>=l&&tr[x].r<=r)
        return tr[x].w;
    int mid=(tr[x].l+tr[x].r)>>1;
    ll cnt=0;
    if(l<=mid) cnt=(cnt+q(x<<1,l,r))%p;
    if(r>mid) cnt=(cnt+q(x<<1|1,l,r))%p;
    return cnt%p;
}

ll qu(int x,int y)// 原树上路径点权和 
{
    ll cnt=0;
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]]) swap(x,y);
        cnt=(cnt+q(1,id[top[x]],id[x]))%p;
        x=fa[top[x]];
    }
    if(d[x]>d[y]) swap(x,y);
    cnt=(cnt+q(1,id[x],id[y]))%p;
    return cnt;
}

void us(int x,ll k)//原树节点的子树修改 
{
    u(1,id[x],id[x]+si[x]-1,k);
}

ll qs(int x)//原树节点的子树点权和查询 
{
    return q(1,id[x],id[x]+si[x]-1)%p;
}

int main()
{
    scanf("%d%d%d%d",&n,&m,&root,&p);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<n;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y),add(y,x);
    }
    dfs1(root,0);
    dfs2(root,root);
    bu(1,1,n);
    while(m--)
    {
        int op,x,y,z;
        scanf("%d",&op);
        if(op==1)
        {
            scanf("%d%d%d",&x,&y,&z);
            up(x,y,z);
        }
        if(op==2)
        {
            scanf("%d%d",&x,&y);
            printf("%lld\n",qu(x,y));
        }
        if(op==3)
        {
            scanf("%d%d",&x,&z);
            us(x,z);
        }
        if(op==4)
        {
            scanf("%d",&x);
            printf("%lld\n",qs(x));
        }
    }
}
posted @ 2022-10-06 09:34  光暗之影x  阅读(5)  评论(0编辑  收藏  举报