树链剖分

Posted bigyellowdog

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树链剖分相关的知识,希望对你有一定的参考价值。

树链剖分

  • write by:BigYellowDog、lsy263

预备知识

  • 预备知识:DFS序、LCA原理、线段树
  • 线段树的作用是为了维护区间,所以这里不一定是线段树,还可以是其它维护区间的数据结构
  • PS:如果你大致掌握了上述知识,树剖10分钟就可以理解。当然,如果没掌握,树剖还是有点难理解的。所以如果不懂上述知识的,请直接看到文章最后:”快餐式的预备知识“后再来阅读正文

引入

  • 先来回顾两个问题:

  • 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)

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

  • (引用自洛谷日报)

概念及定义

技术分享图片

如图,这是一棵树。

我们如果要按上面引入提到的内容操♂ 作它,咋办呢?

差分?LCA?

这些似乎都不好做,TLE,MLE欢迎你

所以,我们的树剖就闪亮登场了!它的好处就在于:把树简化为一条条的链,通过数据结构维护这一条条链

没错这就是树剖的概念,神奇海螺时间结束!

对于树剖算法,我们需要知道一些基本的数组及变量含义:

重儿子 一个结点的所有儿子中拥有最多子树的儿子
轻儿子 一个结点的所有儿子中不是重儿子的儿子
重边 父亲与重儿子的连边
轻边 父亲与轻儿子的连边
重链 一堆重边连接而成的链
轻链 一堆轻边连接而成的链

如上图,用黑线连接的结点都是重儿子,其余均是轻儿子,2-11就是重链,2-5就是轻链

如果你掌握了上面这些的含义,恭喜你,树剖入门完成50%!

题目

了解了基本概念之后,我们来看一道模版题以此来继续下面的讲解... ...

如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z

操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和

操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z

操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

输入格式:

第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。

接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)

接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:

操作1: 1 x y z

操作2: 2 x y

操作3: 3 x z

操作4: 4 x

输出格式:

输出包含若干行,分别依次表示每个操作2或操作4所得的结果(对P取模

  • 那么下面正式开始讲解如何如何实现树剖!

0. 预处理

  • 预处理有两个Dfs函数 + 线段树

  • 第一个dfs的作用:寻找 重儿子结点的子树大小父亲深度

  • 第二个dfs的作用:给每个结点赋上新编号新点权

  • 线段树作用:把树表示成一个区间

  • 有些人可能会问?为什么要找这些东西?

  • 先大致回答:

  • Dfs1算出后面会用到的东西

  • Dfs2是把树剖成了一个可以用线段树表达出来的区间(想一想,为什么?跟dfs序有关)

Dfs1

void dfs1(int x, int fa, int dep) //当前点编号, 父亲, 深度(初始值:root, 0, 1)
{
    d[x]=dep, size[x]=1, fat[x]=fa; //d[]:深度  fat[]:父亲  size[]:子树大小
    int maxSon=0; //重儿子的子树大小
    for(int i=head[x];i!=0;i=edge[i].next)
    {
        if(edge[i].to==fa) continue;
        dfs1(edge[i].to, x, dep+1);
        size[x]+=size[edge[i].to];
        if(size[edge[i].to]>maxSon)
            maxSon=size[edge[i].to], son[x]=edge[i].to; //son[]:重儿子编号
    }
}

技术分享图片

  • 如图是Dfs1函数的模拟,可手动模拟助于理解

Dfs2

void dfs2(int x,int tp) //x当前节点,tp当前链的最顶端的节点 
{
    id[x]=++index;      //标记每个点的新编号 
    val[index]=w[x];    //把每个点的初始值赋到新编号上来 
    top[x]=tp;          //这个点所在链的顶端(重点数组!)
    if(!son[x]) return; //如果没有儿子则返回
    dfs2(son[x],tp);    //按先处理重儿子,再处理轻儿子的顺序递归处理 
    for(int i=head[x];i!=0;i=edge[i].next)
    {
        int y=to[i];
        if(y==fa[x]||y==son[x])continue;
        dfs2(y,y);      //对于每一个轻儿子都有一条从它自己开始的链 
    }
}

线段树

  • 裸奔模版就不贴代码了。主函数里建树build(root, 1, n)

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

  • 学会LCA的同学这个操作就很容易理解了。它只是用top数组加速代替了倍增
void update_link(int x, int y, int add)
{
    while(top[x]!=top[y]) //当两点所处的链顶端的点不是一个点时,就一直往上跳
    {
        if(d[top[x]]<d[top[y]]) swap(x, y); //记住:是深的往上跳
        update(1, id[top[x]], id[x], add);  //更新
        x=fat[top[x]]; //跳到它链顶端的爸爸那
    }
    if(d[x]>d[y]) swap(x, y); //为了保证id[x]<id[y]
    update(1, id[x], id[y], add);
}

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

  • 跟操作1一模一样,只是改了一点地方
int ask_link(int x, int y)
{
    int ans=0; //记录答案用
    while(top[x]!=top[y])
    {
        if(d[top[x]]<d[top[y]]) swap(x, y); //原理同上
        ans+=ask(1, id[top[x]], id[x]); //累加
        x=fat[top[x]];
    }
    if(d[x]>d[y]) swap(x, y);
    ans+=ask(1, id[x], id[y]); //记得最后这还要加一次
    return ans;
}

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

  • 这时候dfs序的作用就发挥出来了!
  • dfs序作用:每个子树的新编号都是连续的
  • 所以要更新一个子树x,它自己的新编号为id[x],那么它的子树中新编号最大的是id[x]+size[x]-1
  • (想一想,为什么?)

直接输出:update(1, id[x], id[x]+size[x]-1, add);

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

  • 同样利用dfs序!
  • 查询id[x] - id[x]+size[x]-1 范围即可

直接输出:ask(1, id[x], id[x]+size[x]-1)

总结

至此树剖入门就结束了!恭喜完成树剖入门。下面有几道题不妨一做!

模版

[NOI2015]真题

思维题


快餐式的预备知识

  • PS:这只是很粗略的讲解,踏踏实实还是自己去找资料学习qwq

dfs序

  • 所谓dfs序,就是按照dfs的顺序给每个点一个新编号(id[ ])。

  • 它的性质:一棵子树的新编号是连续的(强烈建议画个图理解)

  • 它的作用:那么就可以把一棵子树表达成一个区间。

    ? 假设子树的根节点为x,则左端点为id[x], 右端点为id[x] + 子树大小 - 1

    ? 那么就可以把树转成区间来处理了!

  • 如何写:dfs的时候给每个结点赋新编号就行了

LCA

  • 本文并不讲解LCA算法,因为内容过多。只说一下LCA思想原理

  • 给你一棵树,要你找一对点的最近公共祖先。你怎么做?
  • 暴力当然可以。但更好的是倍增式的往上跳,找到最近公共祖先
  • 那么在跳的过程中,我们实则就找了这两点间的路径,那么就可以跳的过程中对这条路径进行修改或查询

线段树

  • 本文并不讲解线段树,因为内容过多。它就是一个维护区间的数据结构

以上是关于树链剖分的主要内容,如果未能解决你的问题,请参考以下文章

树链剖分小结

树链剖分详解

树链剖分

树链剖分 入门

树链剖分

树链剖分(轻/重链剖分学习笔记)