树链剖分---详解

更好的阅读体验点击跳转

更更好的阅读体验点击跳转

导言

你会DFS序吗?

我想,你肯定会说会.不会,欢迎点击搜索和DFS序学习

你会线段树吗?不会,欢迎点击暂无

我想,身为巨佬的你肯定会.

既然巨佬你会DFS序,会线段树.那么接下来的树链剖分,你也一定会.

接下来的学习,您必备的算法知识有,DFS序,线段树.

您可以选修的知识点有树上差分,最近公共祖先


初学算法

适用范围
  1. 将树从x到y结点最短路径上所有节点的值都加上z

我们很容易发现,这个算法就是树上差分算法.

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

Lca大佬们,很容易发现这个其实就是Lca算法,然后通过之前我们所介绍的算法性质.

dis[x]+dis[y]dis[Lca(x,y)]dis[x]+dis[y]−dis[Lca(x,y)]
的老编号,也就是老名字.

总而言之,这就是我们的目标数组了,看上去好多啊,但其实只要两次DFS就解决完毕了.


代码解析
void dfs1(int x,int father)//father是x的父亲节点
{
    size[x]=1;//刚开始子树为1,也就是直接
    for(int i=head[x]; i; i=Next[i])//访问所有出边
    {
        int y=edge[i];//儿子节点
        if (y==father)//访问到唯一一个不是儿子节点的父亲节点去了
            continue;//当然不可以,直接跳过
        deep[y]=deep[x]+1;//深度+1,儿子节点是父亲节点深度+1
        fa[y]=x;//y的父亲节点是x
        dfs1(y,x);//y的父亲节点是x
        size[x]+=size[y];//加上儿子贡献的子树
        if (size[y]>size[wson[x]])//如果这个节点比当前重儿子,子树还要多(还要重) ,那么营养过剩的重儿子就是他了.
            wson[x]=y;//wson[x]表示x节点的重儿子
    }
}

这一层DFS,就让我们求解出来了前四个数组,那么后三个数组呢?

void dfs2(int x,int tp)//x表示当前节点,tp表示链头
{
    dfn[x]=++cnt;//重新编辑编号
    pre[cnt]=x;//存储老编号
    top[x]=tp;//存储链头
    if (wson[x]) //优先处理重儿子
        dfs2(wson[x],tp);
    for(int i=head[x]; i; i=Next[i])//访问所有的轻链
    {
        int y=edge[i];//存储出边
        if (y==wson[x] || y==fa[x])//重儿子已经处理过了,父亲节点不可以抵达
            continue;
        dfs2(y,y);//每一个轻链的链头,都是自己,而且重儿子的开头都是轻链.
    }
}

算法拓展

咱们知道,树链剖分是LCA+树上差分的合并增强版,那么他们两个有的操作,他当然也得有.

正如同某名言.你的是我的,我的还是我的.

先来看LCA的操作,其实就是其名的求两点的最近公共祖先.

我们对于上面所述的top数组,也就是链头数组,比较懵逼,觉得他似乎没有什么用处啊.

实际上它的用处非常之大,是树链剖分的核心数组之一.

通俗理解的故事,又来了.以下内容纯属瞎编,只为了更好的理解.

AcwingAcwing.

那么我们得出.

因为A小组和B小组是两个没有交集的小组.(也就是互相都不认识)

那么显然A小组这一串人,都不可能成为答案.

同理B小组这一伙人,也不可能成为答案.

明知没有用,何必浪费时间.

所以,我们迅速跨越中间的所有节点.

a=top[a];//直接爬到链头
b=top[b];//直接爬到链头

这样我们发现效率直线上升.

aO(len(a)).本来我们a节点爬到这条链需要花费O(len(a)).

注意点

还有请注意,每次跳跃的话.
比如说a,b节点要跳跃.
我们每次是将链头深度深的节点往上跳跃,而不本节点的深度.
为了避免出现下面评论所说的情况.
两个节点,跳过去了,跳到了LCA节点的上方去了.

拓展代码
int Query_sum(int a,int b)//a,b这条路径上的权值和
{
    int ans=0;//初始化为0
    while(top[a]!=top[b]) //此时a,b不在同一条重链上面
    {
        if (deep[top[a]]<deep[top[b]])//我们这里默认a是深度在下面的链
            swap(a,b);
        ans+=t.Query_sum(1,1,n,dfn[top[a]],dfn[a]);//访问这条重链,dfn[top[a]]是链头,dfn[a]是当前节点,dfn[top[a]]<dfn[a]因为a后访问.
        a=fa[top[a]];//往上面访问,也就是往上面爬一步
    }
    if (dfn[a]<dfn[b])//保证a深度比b深度,深.保证后面查询l<r
        swap(a,b);
    ans+=t.Query_sum(1,1,n,dfn[b],dfn[a]);//他们已经在同一条重链上面了
    return ans;
}
链上结构
路径操作

看到上面的代码,我们发现t是什么东西.

t其实就是我们的线段树数据结构.

根据树上差分的操作们,得知我们的树链剖分需要复制一大波操作.

  1. 将树从x到y结点最短路径上所有节点的值都加上z

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

根据上面这些操作,我们不难发现,每条链也需要资瓷这些操作.

综上所述得出.

  1. 一条链上,需要资瓷区间修改.
  2. 一条链上,需要支持区间查询.

总而言之,区间操作多多,因此我们使用线段树.

当然了巨佬们,肯定喜欢使用平衡树等等高大上,上档次,有内涵的数据结构,但是线段树的代码复杂度对于考试而言,是最好不过的结构了.

代码复杂度是考试的时候,极为重要的复杂度. by Acwing站长,校长,集训队大佬yxc总裁

总而言之,言而总之,我们树链剖分的代码量,成功的增加了1k.

子树操作

我们再来几个操作.

  1. 将以x为根节点的子树内所有节点值都加上z

  2. 求以x为根节点的子树内所有节点值之和

看到子树操作,有点不知所措,线段树似乎不支持这个鬼东西吧,难道代码量又要翻倍处理.

事实上,我们的代码只需要增加八行代码,也就核心两句话.

我们的DFS遍历,在这里起到了决定性,关键性,核心性的作用.(语文老师:捕捉到了病句,起到了,什么的作用,句式杂糅)

我们发现DFS序列的一个重点,就是.一颗子树,它的DFS序,是有序的.

树链剖分11.png

还是这张图,我们发现熊熊助教这颗子树.

其实就是[2,7][2,7]这个区间,就达到了修改以2为根的树修改.

因此代码如下.

void Update(int x,int v)//修改子树的值
{
    t1.Update(1,1,n,dfn[x],dfn[x]+size[x]-1,v);//DFS序是有序的数列
}

同理,查询操作也就如下所示了.

int Query_sum(int x)//查询子树的和
{
    return t1.Query_sum(1,1,n,dfn[x],dfn[x]+size[x]-1);//统计子树和,其实和修改差不多
}
树链模板
struct line_tree//线段树
{
#define mid (l+r>>1)//二分中点 
#define Lson (rt<<1),l,mid//左儿子 
#define Rson (rt<<1 | 1),mid+1,r//右儿子 
#define Len (r-l+1)//区间长度
    void Push_down(int rt,int l,int r)
    //这里的懒惰标记只适合区间修改,把[l,r]区间都变成v.如果是都+v,则必须Lazy标记+=,而不是下面的=
    {
        if (Lazy[rt]!=-1)//当前节点有Lazy标记
        {
            Lazy[rt<<1]=Lazy[rt<<1 | 1]=Lazy[rt];//懒惰标记下传
            sum[rt<<1]=Lazy[rt]*(mid-l+1);//左儿子的区间长度
            sum[rt<<1 |1]=Lazy[rt]*(r-(mid+1)+1);//右儿子的区间长度
            Lazy[rt]=-1;//此时懒惰标记已经下传完毕了,那么可以全部清空了
        }
    }
    void build(int rt,int l,int r)
    {
        Lazy[rt]=-1;
        if (l==r)//叶子节点
        {
            sum[rt]=1;//sum数组
            return ;
        }
        build(Lson);//左儿子
        build(Rson);//右儿子
        sum[rt]=sum[rt<<1]+sum[rt<<1 |1];//左儿子节点+右儿子节点
    }
    void Update(int rt,int l,int r,int L,int R,int v)//将[L,R]区间统统修改成为v,然后当前区间[l,r]
    {
        if (L<=l && r<=R)//当前区间被包括了
        {
            Lazy[rt]=v;//懒惰标记修改
            sum[rt]=v*Len;//全部修改完毕
            return ;
        }
        Push_down(rt,l,r);//向下传递Lazy标记
        if (L<=mid)//在左儿子身上
            Update(Lson,L,R,v);//左儿子
        if (R>mid)//在右儿子身上
            Update(Rson,L,R,v);//右儿子
        sum[rt]=sum[rt<<1]+sum[rt<<1 |1];//左儿子+右儿子
    }
    int Query_sum(int rt,int l,int r,int L,int R)//查询[L,R]区间和,当前区间[l,r]
    {
        int ans=0;
        if (L<=l && r<=R)//当前区间被包括了
            return sum[rt];
        if (L<=mid)//左儿子上面
            ans+=Query_sum(Lson,L,R);
        if (R>mid)//右儿子上面
            ans+=Query_sum(Rson,L,R);
        sum[rt]=sum[rt>>1]+sum[rt>>1 |1];
        return ans;//返回
    }
} t1;
struct Tree_Chain//树链剖分
{
    void add_edge(int a,int b)//添加边函数
    {
        edge[++tot]=b;//出边节点
        Next[tot]=head[a];//链表链接
        head[a]=tot;//上一个节点
    }
    void dfs1(int x,int father)//x节点,和他的父亲节点father
    {
        size[x]=1;//刚开始就自己这一个节点
        for(int i=head[x]; i; i=Next[i]) //开始遍历所有的出边
        {
            int y=edge[i];//出边节点
            if (y==father)//儿子节点是不可以等于父亲节点的
                continue;
            deep[y]=deep[x]+1;//儿子的深度,是父亲深度+1
            fa[y]=x;//y的父亲节点是x
            dfs1(y,x); //开始遍历儿子节点
            size[x]+=size[y];//儿子节点贡献子树大小
            if (size[y]>size[wson[x]]) //发现当前的儿子节点,比之前的重儿子,还要重(胖),那么更新重儿子
                wson[x]=y;//更新
        }
        return ;//华丽结束
    }
    void dfs2(int x,int tp)//x节点,以及x节点所在链的链头
    {
        dfn[x]=++cnt;//当前节点的新编号,也就是DFS序编号
        pre[cnt]=x;//老编号,虽然在整道题目中没有用处,但是树链剖分板子打一遍也是好的
        top[x]=tp;//链头存储一下
        if (wson[x])//有重儿子,那么一定先访问重儿子
            dfs2(wson[x],tp); //访问节点,此时重儿子一定在重链上,所以还是tp
        for(int i=head[x]; i; i=Next[i]) //访问所有的轻儿子
        {
            int y=edge[i];//出边
            if (y==wson[x] || y==fa[x])//轻儿子节点不能是重儿子,也不能是父亲节点
                continue;
            dfs2(y,y);//每一个轻儿子,他的链头其实都是自己,而且重链的开头也得是轻儿子
        }
    }
    void Update(int x,int v)//修改子树的值
    {
        t1.Update(1,1,n,dfn[x],dfn[x]+size[x]-1,v);//DFS序是有序的数列
    }
    int Query_sum(int x)//查询子树的和
    {
        return t1.Query_sum(1,1,n,dfn[x],dfn[x]+size[x]-1);//统计子树和,其实和修改差不多
    }
    long long Query_sum2(int a,int b)//a,b这条路径上的权值和
    {
        long long ans=0;//初始化为0
        while(top[a]!=top[b]) //此时a,b不在同一条重链上面
        {
            if (deep[top[a]]<deep[top[b]])//我们这里默认a是深度在下面的链
                swap(a,b);
            now_ans=0;
            t1.Query_sum(1,1,n,dfn[top[a]],dfn[a]);
            ans+=now_ans;//访问这条重链,dfn[top[a]]是链头,dfn[a]是当前节点,dfn[top[a]]<dfn[a]因为a后访问.
            a=fa[top[a]];//往上面访问,也就是往上面爬一步
        }
        if (deep[a]<deep[b])//保证a深度比b深度,深.保证后面查询l<r
            swap(a,b);
        now_ans=0;
        t1.Query_sum(1,1,n,dfn[top[a]],dfn[a]);
        ans+=now_ans;//他们已经在同一条重链上面了
        return ans;
    }
    int Update2(int a,int b) //链上修改 
    {
        while(top[a]!=top[b])//也就是两个点还在不同的重链上
        {
            if (deep[top[a]]<deep[top[b]])//我们默认a节点是深度比b节点深一些
                swap(a,b);//交换一下就好了
            t1.Update(1,1,n,dfn[top[a]],dfn[a],0);//在爬的过程中,也帮忙修改一下
            a=fa[top[a]];//往上面爬一下
        }
        if (deep[a]<deep[b])//保证a深度比b深度,深.保证后面查询l<r
            swap(a,b);
        t1.Update(1,1,n,dfn[b],dfn[a],0);//同一条链了,那么修改这条链上在[a,b]之间的点 
    }
    //请注意,本模板是很多题目的操作合并而成,可能有问题,但是单独没有问题.如果有问题请艾特博主.
    //所有代码风格为博主风格,所以操作之间都是通用的,应该不会出现问题.
} t2;

经典选讲

第一题 模板题[HAOI2015]树上操作

题目描述

有一棵点数为 NN ,之后接这个操作的参数( x 或者 x a ) 。

输出格式:

对于每个询问操作,输出该询问的答案。答案之间用换行隔开。

输入输出样例
输入样例#1:
5 5
1 2 3 4 5
1 2
1 4
2 3
2 5
3 3
1 2 1
3 5
2 1 2
3 3
输出样例#1:
6
9
13
数据范围

对于 100100

题意理解

一棵树上,要求支持单点修改,子树修改,路径修改操作.

思路解析

学会了树连剖分的我们,很轻松的发现,这就是一个树链剖分的模板题目.

所以说,我们几乎是可以轻松解决这道题目了.

树链剖分超级详解,可以媲美全网第一通俗树链剖分讲解了

于是我们惊奇的发现一道省选题目,就这样轻松愉快地AC了.

代码超多中文解释,绝对看得懂.

代码解析
#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+200;
int sum[N<<2],Lazy[N<<2];//处理区间和;懒惰标记
int head[N<<1],edge[N<<1],ver[N<<1],Next[N<<1],tot;
int pre[N],deep[N],size[N],a[N],fa[N],cnt;
int wson[N],dfn[N],top[N],n,m,now_ans;
struct line_tree
{
#define mid ((l+r)>>1) //二分值 
#define lson root<<1,l,mid //左儿子 
#define rson root<<1|1,mid+1,r //右儿子 
#define len (r-l+1) //区间长度 
    void build(int root,int l,int r)
    {
        if (l==r)//抵达叶子节点,也就是单个节点
        {
            sum[root]=a[pre[l]];
            return ;
        }
        build(root<<1,l,mid);//左儿子
        build(root<<1 | 1,mid+1,r);//右儿子
        sum[root]=sum[root<<1]+sum[root<<1 | 1];//左右儿子都访问完毕了,可以更新父亲节点了.
    }
    inline void Push_down(int x,int lenn)
    {
        Lazy[x<<1]+=Lazy[x];//左儿子得到,父亲节点的懒惰标记下传
        Lazy[x<<1 | 1]+=Lazy[x];//右儿子得到,父亲节点的懒惰标记下传
        sum[x<<1]+=Lazy[x]*(lenn-(lenn>>1));//懒惰标记的值叠加
        sum[x<<1 | 1]+=Lazy[x]*(lenn>>1);//懒惰标记的值叠加
        Lazy[x]=0;//懒惰标记已经下传
    }
    inline void Query_sum(int root,int l,int r,int L,int R)//目标区间ql,qr,当前区间[l,r],当前节点是root
    {
        if(L<=l&&r<=R)//目标区间包含了当前区间
        {
            now_ans+=sum[root];//累加
            return;//返回
        }
        else
        {
            if(Lazy[root])//有标记
                Push_down(root,len);//先走标记
            if(L<=mid)//左儿子上有
                Query_sum(lson,L,R);
            if(R>mid)//右儿子上
                Query_sum(rson,L,R);
        }
    }
    inline void Update(int root,int l,int r,int L,int R,int k)//目标区间[L,R]全部要加上K,当前区间[l,r]
    {
        if (L<=l && r<=R)//[L,R]包括了当前区间[l,r]
        {
            Lazy[root]+=k;//叠加懒惰标记
            sum[root]+=k*len;
            return ;
        }
        if (Lazy[root])//有懒惰标记
            Push_down(root,len);//下传标记
        if (L<=mid)
            Update(lson,L,R,k);//左儿子
        if (R>mid)
            Update(rson,L,R,k);//右儿子
        sum[root]=sum[root<<1]+sum[root<<1 |1];
    }
} t1;
struct Tree_Chain
{
    inline void add_edge(int a,int b)
    {
        edge[++tot]=b;
        Next[tot]=head[a];
        head[a]=tot;
    }
    void dfs1(int x,int father)//fa是x的父亲节点
    {
        size[x]=1;//刚开始子树为1,也就是直接
        for(int i=head[x]; i; i=Next[i])//访问所有出边
        {
            int y=edge[i];//儿子节点
            if (y==father)//访问到唯一一个不是儿子节点的父亲节点去了
                continue;//当然不可以,直接跳过
            deep[y]=deep[x]+1;//深度+1,儿子节点是父亲节点深度+1
            fa[y]=x;//y的父亲节点是x
            dfs1(y,x);//y的父亲节点是x
            size[x]+=size[y];//加上儿子贡献的子树
            if (size[y]>size[wson[x]])//如果这个节点比当前重儿子,子树还要多(还要重) ,那么营养过剩的重儿子就是他了.
                wson[x]=y;//wson[x]表示x节点的重儿子
        }
    }
    void dfs2(int x,int tp)//x表示当前节点,tp表示链头
    {
        dfn[x]=++cnt;//重新编辑编号
        pre[cnt]=x;//存储老编号
        top[x]=tp;//存储链头
        if (wson[x]) //优先处理重儿子
            dfs2(wson[x],tp);
        for(int i=head[x]; i; i=Next[i])//访问所有的轻链
        {
            int y=edge[i];//存储出边
            if (y==wson[x] || y==fa[x])//重儿子已经处理过了,父亲节点不可以抵达
                continue;
            dfs2(y,y);//每一个轻链的链头,都是自己,而且重儿子的开头都是轻链.
        }
    }
    long long Query_sum(int a,int b)//a,b这条路径上的权值和
    {
        long long ans=0;//初始化为0
        while(top[a]!=top[b]) //此时a,b不在同一条重链上面
        {
            if (deep[top[a]]<deep[top[b]])//我们这里默认a是深度在下面的链
                swap(a,b);
            now_ans=0;
            t1.Query_sum(1,1,n,dfn[top[a]],dfn[a]);
            ans+=now_ans;//访问这条重链,dfn[top[a]]是链头,dfn[a]是当前节点,dfn[top[a]]<dfn[a]因为a后访问.
            a=fa[top[a]];//往上面访问,也就是往上面爬一步
        }
        if (deep[a]<deep[b])//保证a深度比b深度,深.保证后面查询l<r
            swap(a,b);
        now_ans=0;
        t1.Query_sum(1,1,n,dfn[top[a]],dfn[a]);
        ans+=now_ans;//他们已经在同一条重链上面了
        return ans;
    }
    inline void update_son(int x,int k)
    {
        t1.Update(1,1,n,dfn[x],dfn[x]+size[x]-1,k);
        //因为DFS所以子树其实是有序的,而且一个子树大小是size[x],那么dfn+size-1就可以遍历整棵树
    }
} t2;
signed main()//其实就是int main()
{
//  freopen("stdin.in","r",stdin);
//  freopen("a.out","w",stdout);
    scanf("%lld%lld",&n,&m);
    for(int i=1; i<=n; i++)
        scanf("%lld",&a[i]);
    for(int i=1; i<n; i++)
    {
        int a,b;
        scanf("%lld%lld",&a,&b);
        t2.add_edge(a,b);
        t2.add_edge(b,a);//无向图建立边
    }
    t2.dfs1(1,0);//第一次DFS,求size,deep,fa
    t2.dfs2(1,1);//第二DFS求dfn,top
    t1.build(1,1,n);
    while(m--)
    {
        int k,a,b,c;
        scanf("%lld",&k);
        if (k==1)
        {
            scanf("%lld%lld",&a,&b);
            t1.Update(1,1,n,dfn[a],dfn[a],b);//虽然是区间修改[dfn[a],dfn[a]],但是其实是单点修改.
        }
        if (k==2)
        {
            scanf("%lld%lld",&a,&b);
            t2.update_son(a,b);//修改a以及子树,统统增加b
        }
        if (k==3)
        {
            scanf("%lld",&a);
            printf("%lld\n",t2.Query_sum(1,a));
        }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值