树链剖分
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)
总结
至此树剖入门就结束了!恭喜完成树剖入门。下面有几道题不妨一做!
快餐式的预备知识
- PS:这只是很粗略的讲解,踏踏实实还是自己去找资料学习qwq
dfs序
所谓dfs序,就是按照dfs的顺序给每个点一个新编号(id[ ])。
它的性质:一棵子树的新编号是连续的(强烈建议画个图理解)
它的作用:那么就可以把一棵子树表达成一个区间。
? 假设子树的根节点为x,则左端点为id[x], 右端点为id[x] + 子树大小 - 1
? 那么就可以把树转成区间来处理了!
如何写:dfs的时候给每个结点赋新编号就行了
LCA
本文并不讲解LCA算法,因为内容过多。只说一下LCA思想原理
- 给你一棵树,要你找一对点的最近公共祖先。你怎么做?
- 暴力当然可以。但更好的是倍增式的往上跳,找到最近公共祖先
那么在跳的过程中,我们实则就找了这两点间的路径,那么就可以跳的过程中对这条路径进行修改或查询
线段树
- 本文并不讲解线段树,因为内容过多。它就是一个维护区间的数据结构
以上是关于树链剖分的主要内容,如果未能解决你的问题,请参考以下文章