树链剖分(入门学习)

学习博客:https://www.cnblogs.com/ivanovcraft/p/9019090.html

先来回顾两个问题:
1,将树从x到y结点最短路径上所有节点的值都加上z

这也是个模板题了吧

我们很容易想到,树上差分可以以O(n+m)的优秀复杂度解决这个问题

2,求树从x到y结点最短路径上所有节点的值之和

lca大水题,我们又很容易地想到,dfs O(n)预处理每个节点的dis(即到根节点的最短路径长度)

然后对于每个询问,求出x,y两点的lca,利用lca的性质distance ( x , y ) = dis ( x ) + dis ( y ) - 2 * dis ( lca )求出结果

时间复杂度O(mlogn+n)

现在来思考一个bug:
如果刚才的两个问题结合起来,成为一道题的两种操作呢?

刚才的方法显然就不够优秀了(每次询问之前要跑dfs更新dis)



树链剖分华丽登场
树剖是通过轻重边剖分将树分割成多条链,然后利用数据结构来维护这些链(本质上是一种优化暴力)

首先明确概念:

重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;

轻儿子:父亲节点中除了重儿子以外的儿子;

重边:父亲结点和重儿子连成的边;

轻边:父亲节点和轻儿子连成的边;

重链:由多条重边连接而成的路径;

轻链:由多条轻边连接而成的路径;

比如上面这幅图中,用黑线连接的结点都是重结点,其余均是轻结点,

2-11就是重链,2-5就是轻链,用红点标记的就是该结点所在重链的起点,也就是下文提到的top结点,

还有每条边的值其实是进行dfs时的执行序号。


变量声明:

复制代码
const int maxn=1e5+10;
struct edge{
    int next,to;
}e[2*maxn];
struct Node{
    int sum,lazy,l,r,ls,rs;
}node[2*maxn];
int rt,n,m,r,a[maxn],cnt,head[maxn],f[maxn],d[maxn],size[maxn],son[maxn],rk[maxn],top[maxn],id[maxn];
复制代码
名称 解释
f[u] 保存结点u的父亲节点
d[u] 保存结点u的深度值
size[u] 保存以u为根的子树节点个数
son[u] 保存重儿子
rk[u] 保存当前dfs标号在树中所对应的节点
top[u] 保存当前节点所在链的顶端节点
id[u] 保存树中每个节点剖分以后的新编号(DFS的执行顺序)


我们要做的就是(树链剖分的实现):


1,对于一个点我们首先求出它所在的子树大小,找到它的重儿子(即处理出size,son数组),
解释:比如说点1,它有三个儿子2,3,4

2所在子树的大小是5

3所在子树的大小是2

4所在子树的大小是6

那么1的重儿子是4

ps:如果一个点的多个儿子所在子树大小相等且最大

那随便找一个当做它的重儿子就好了

叶节点没有重儿子,非叶节点有且只有一个重儿子


2,在dfs过程中顺便记录其父亲以及深度(即处理出f,d数组),操作1,2可以通过一遍dfs完成

复制代码
void dfs1(int u,int fa,int depth)    //当前节点、父节点、层次深度
{
    f[u]=fa;
    d[u]=depth;
    size[u]=1;    //这个点本身size=1
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v==fa)
            continue;
        dfs1(v,u,depth+1);    //层次深度+1
        size[u]+=size[v];    //子节点的size已被处理,用它来更新父节点的size
        if(size[v]>size[son[u]])
            son[u]=v;    //选取size最大的作为重儿子
    }
}
//进入
dfs1(root,0,1);
复制代码

dfs跑完大概是这样的,大家可以手动模拟一下


3,第二遍dfs,然后连接重链,同时标记每一个节点的dfs序,并且为了用数据结构来维护重链,我们在dfs时保证一条重链上各个节点dfs序连续(即处理出数组top,id,rk)

复制代码
void dfs2(int u,int t)    //当前节点、重链顶端
{
    top[u]=t;
    id[u]=++cnt;    //标记dfs序
    rk[cnt]=u;    //序号cnt对应节点u
    if(!son[u])
        return;
    dfs2(son[u],t);
/*我们选择优先进入重儿子来保证一条重链上各个节点dfs序连续,
一个点和它的重儿子处于同一条重链,所以重儿子所在重链的顶端还是t*/
    for(int i=head[u];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v!=son[u]&&v!=f[u])
            dfs2(v,v);    //一个点位于轻链底端,那么它的top必然是它本身
    }
}
复制代码

dfs跑完大概是这样的,大家可以手动模拟一下


4,两遍dfs就是树链剖分的主要处理,通过dfs我们已经保证一条重链上各个节点dfs序连续,那么可以想到,我们可以通过数据结构(以线段树为例)来维护一条重链的信息
回顾上文的那个题目,修改和查询操作原理是类似的,以查询操作为例,其实就是个LCA,不过这里使用了top来进行加速,因为top可以直接跳转到该重链的起始结点,轻链没有起始结点之说,他们的top就是自己。需要注意的是,每次循环只能跳一次,并且让结点深的那个来跳到top的位置,避免两个一起跳从而插肩而过。

复制代码
int sum(int x,int y)
{
    int ans=0,fx=top[x],fy=top[y];
    while(fx!=fy)    //两点不在同一条重链
    {
        if(d[fx]>=d[fy])
        {
            ans+=query(id[fx],id[x],rt);    //线段树区间求和,处理这条重链的贡献
            x=f[fx],fx=top[x];    //将x设置成原链头的父亲结点,走轻边,继续循环
        }
        else
        {
            ans+=query(id[fy],id[y],rt);
            y=f[fy],fy=top[y];
        }
    }
    //循环结束,两点位于同一重链上,但两点不一定为同一点,所以我们还要统计这两点之间的贡献
    if(id[x]<=id[y])
        ans+=query(id[x],id[y],rt);
    else
        ans+=query(id[y],id[x],rt);
    return ans;
}
复制代码

大家如果明白了树链剖分,也应该有举一反三的能力(反正我没有),修改和LCA就留给大家自己完成了


5,树链剖分的时间复杂度
树链剖分的两个性质:

1,如果(u, v)是一条轻边,那么size(v) < size(u)/2;

2,从根结点到任意结点的路所经过的轻重链的个数必定都小于logn;

可以证明,树链剖分的时间复杂度为O(nlog^2n)

 

下面看一道板子题:题目链接:https://www.luogu.org/problemnew/show/P3384

代码:

#include<iostream>
#include<cstdio>
using namespace std;
const int maxn=1e5+50;
int N,M,R,mod;
int cnt=0;
int v[maxn];
int Size[maxn],deep[maxn],fa[maxn],son[maxn],id[maxn],top[maxn],head[maxn],rk[maxn];//
struct edge
{
    int to,next;//e[i].to代表第i条边的终点 e[i].next代表与第i条边同起点的下一条边的终点
}e[maxn<<1];
struct node
{
    int l,r,ls,rs,sum,lazy;
}a[maxn<<1];
void add(int x,int y)//链式前向星存储
{
    e[++cnt].to=y;
    e[cnt].next=head[x];
    head[x]=cnt;
}
int len(int rt)
{
    return a[rt].r-a[rt].l+1;
}

void push_up(int rt)
{
    a[rt].sum=(a[a[rt].ls].sum+a[a[rt].rs].sum)%mod;
}
void Push_down(int rt)
{
    a[a[rt].ls].lazy=(a[a[rt].ls].lazy+a[rt].lazy)%mod;
    a[a[rt].rs].lazy=(a[a[rt].rs].lazy+a[rt].lazy)%mod;
    a[a[rt].ls].sum=(a[a[rt].ls].sum+len(a[rt].ls)*a[rt].lazy)%mod;
    a[a[rt].rs].sum=(a[a[rt].rs].sum+len(a[rt].rs)*a[rt].lazy)%mod;
    a[rt].lazy=0;
}
void dfs1(int rt)
{
    Size[rt]=1,deep[rt]=deep[fa[rt]]+1;
    for(int i=head[rt];i;i=e[i].next)
    {
        int v=e[i].to;
        if(v!=fa[rt])
        {
            fa[v]=rt;
            dfs1(v);
            Size[rt]+=Size[v];
            if(Size[v]>Size[son[rt]]) son[rt]=v;
        }
    }
}
void dfs2(int R,int rt)
{
    id[R]=++cnt;
    rk[cnt]=R;
    top[R]=rt;
    if(son[R])
    dfs2(son[R],rt);//优先走重儿子
    for(int i=head[R];i;i=e[i].next)
    {
//        cout<<"***"<<endl;
        int v=e[i].to;
        if((v!=son[R])&&(v!=fa[R])) dfs2(v,v);//轻儿子的重儿子就是本身
    }
}
void Build(int l,int r,int rt)
{
    if(l==r)
    {
        a[rt].l=l;
        a[rt].r=r;
        a[rt].sum=v[rk[l]];
        return ;
    }
    int mid=(l+r)>>1;
    a[rt].ls=++cnt;
    a[rt].rs=++cnt;
    a[rt].l=l,a[rt].r=r;
//    cout<<a[rt].ls<<" "<<a[rt].rs<<endl;
//    cout<<a[rt].l<<" "<<a[rt].r<<endl;
    Build(l,mid,a[rt].ls);
    Build(mid+1,r,a[rt].rs);
    push_up(rt);
}

void update(int x,int y,int c,int rt)
{
//    cout<<x<<" "<<y<<" "<<rt<<endl;
//    cout<<"*"<<endl;
    if(x<=a[rt].l&&a[rt].r<=y)
    {
        a[rt].lazy=(a[rt].lazy+c)%mod;
        a[rt].sum=(a[rt].sum+len(rt)*c)%mod;
        return ;
    }
    Push_down(rt);
    int mid=(a[rt].l+a[rt].r)>>1;
    if(x<=mid) update(x,y,c,a[rt].ls);
    if(y>mid) update(x,y,c,a[rt].rs);
    push_up(rt);
}
void updates(int x,int y,int c)
{
    while(top[x]!=top[y])
    {
        if(deep[top[x]]<deep[top[y]]) swap(x,y);//统一跳x
        update(id[top[x]],id[x],c,0);//线段树区间更新
        x=fa[top[x]];
    }
    //在同一条重链上
    if(id[x]>id[y]) swap(x,y);
    update(id[x],id[y],c,0);
}
int query(int x,int y,int rt)
{
    int tot=0;
    if(x<=a[rt].l&&a[rt].r<=y)
    {
        return a[rt].sum;
    }
    Push_down(rt);
    int mid=(a[rt].l+a[rt].r)>>1;
    if(x<=mid) tot+=query(x,y,a[rt].ls);
    if(y>mid) tot+=query(x,y,a[rt].rs);
    tot%=mod;
//    push_up(rt);
    return tot;
}
int sum(int x,int y)
{
    int ret=0;
    while(top[x]!=top[y])//不在同一条重链上
    {
        if(deep[top[x]]<deep[top[y]]) swap(x,y);
        ret+=query(id[top[x]],id[x],0);
        ret%=mod;
        x=fa[top[x]];
    }
    if(id[x]>id[y]) swap(x,y);
    ret+=query(id[x],id[y],0);
    ret%=mod;
    return ret;
}
int main()
{
    cnt=0;
    scanf("%d%d%d%d",&N,&M,&R,&mod);
    for(int i=1;i<=N;i++) scanf("%d",&v[i]);
    for(int i=1;i<N;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        add(x,y);//双向边
        add(y,x);

    }
    /**
    dfs1求出原树中每个结点为根的树大小 原树中每个结点的深度 每个结点的父亲 重儿子
    */
    cnt=0;
    dfs1(R);//从根节点开始
//    cout<<"**"<<endl;
    cnt=0;
    /**
    dfs2求出新树中每个结点的编号 每个编号对应原树中的值 所在重链的根节点
    */
    dfs2(R,R);
    cnt=0;
//    cout<<"*"<<endl;
    Build(1,N,0);//以得到的新编号来建树
    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);
            updates(x,y,z);
        }
        else if(op==2)
        {
            scanf("%d%d",&x,&y);
            printf("%d
",sum(x,y));
        }
        else if(op==3)
        {
            scanf("%d%d",&x,&z);
            update(id[x],id[x]+Size[x]-1,z,0);
        }
        else
        {
            scanf("%d",&x);
            printf("%d
",query(id[x],id[x]+Size[x]-1,0));
        }
    }
    return 0;
}
当初的梦想实现了吗,事到如今只好放弃吗~
原文地址:https://www.cnblogs.com/caijiaming/p/10821835.html