dfs序就是按照树的先序遍历的顺序,为每个点记录下进入/最后一次出去这个点的时间。
dfs序是维护一个树基本套路之一,有一些基本的用处(蒟蒻我知道的):
1.树结构线性化,主要用于确定子树的范围。比如例题:
(银牌题)ACM-ICPC 2018 沈阳赛区网络预赛 J - Ka Chang dfs时间戳+树状数组+二分+分块(比较综合的题目)
2.树链的划分,树链剖分中用于将重节连续标号转化为重链。(下面会有讲)
3.别的蒟蒻就不会了。
就不贴代码了,参考上面给的题解链接里的AC代码。
本篇重点是树链剖分。
百科上对树链剖分的描述:指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个点属于且只属于一条链,然后再通过数据结构(树状数组、SBT、SPLAY、线段树等)来维护每一条链。
最经典的例子就是动态修改节点值,查询从某个节点到另一节点路径上点权和。
网上有大量的讲解,但对我这种蒟蒻来说非常新手不友好。那么我来总结一下原理。
先给一个例题(HDU - 3966)
题意:给一棵树,并给定各个点权的值,然后有3种操作:
I C1 C2 K: 把C1与C2的路径上的所有点权值加上K
D C1 C2 K:把C1与C2的路径上的所有点权值减去K
Q C:查询节点编号为C的权值
数据量为5e4
(代码参考kuangbin的模板)
大家思考一下这个题怎么处理,然后下面进行详细讨论。
前驱知识点:dfs序、dfs、线段树/树状数组、前向星(大家都是用的前向星存图我也不知道为什么,我只好跟风了)
链式前向星 学习笔记
前缀和,线段树,树状数组讲解(入门)
树状数组 区间修改 区间查询 讲解
----------------------回到正题------------------
线段树、主席树都是利用二叉树的性质。区间和树都是比较完美的。
可是,如果给出一棵树不是二叉树(可能是任意的树),而常规的树(二叉树)都是利用深度是节点个数/叶子个数的高阶无穷小(logn)来简化计算量的。而任意的树可能度非常大,也可能深度特别大,树链不完美,线段树家族处理某条子链的能力不强,则事情就麻烦了。
轻、重节点:兄弟节点中,子树结点数目最多的结点,其他的兄弟节点都为轻节点
重链:将连续的重节点连接在一起就是一条重链,最上面的重链的父亲也算在重链内。
轻链:除了重节点以外的轻节点就是轻链,长度必为1且为叶子,我们将会在下图后面详细分析。
树链剖分的核心思想是:重链是引起树复杂(深度过大)的原因,以重链为单位建立线段树维护(或者其他数据结构),其他轻节点暴力向重链转移。这样就相当于在树上建立了重链组成的高铁,轻节点转移到重链上之后就可以搭高铁了(搭高铁的意思是,转移到重链之后,重链只需要查询(修改)一次就可以查询(修改)到结果)。
请允许我画一个丑陋的图
上图中红色为重节点,黑色为重链(也就是所谓的高铁),注意我们逻辑上重链要包含最上面的父亲。
有结论:
重链的个数不超过logn。
(网上说的是轻、重链的个数不超过logn,这至少在我们的定义中是错误的,举个反例就是深度为2,有100个叶子的树)
顺带把轻链必长度为1且为叶子一块解释了:
我们根据重链的定义可知:每个节点的孩子中必有一个重节点,这很好理解。而我们把重节点上面的父亲节点算作重链的开头第一个节点,所以:只要节点有子孙,则一定划归于重链,换句话说就是:不是叶子的节点,就是重链的一部分(要么是重节点,要么是重节点的父亲)。显然,不可能有两个叶子相连,则轻链长度只能为1.
重要的话再用红字写一遍,轻链长度为1。
证明重链的个数不超过logn:
(类似反证法)
我们经过分析发现对于一个非叶子节点:要么它是重链,要么在重链最上端,也就是说对每个非轻链的节点都存在一条经过它或者从它开始,直达一个叶子的重链。那么要使重链尽可能的多,必须要使得树不断分叉(如果不的话就只有一条直达叶子的重链),分叉越多层数就越少,而我们知道每组兄弟中,只有一个是重节点,则分叉不能太多,那么就是二叉,而对于平衡二叉树最多logn级别的,这是重链最多的情况,普通情况重链自然就小于logn了。
我们树链剖分核心思想就是重链用数据结构维护,轻链尽可能搭重链便车来简化运算,而轻链长度最多为1。这样,轻链只需一次就可以到达重链,而重链个数不超过logn,所以重链之间的辗转也就不超过logn次(如下图黄色为轻链向根节点的移动轨迹),所以总体时间复杂度为logn。
再重复一遍之前得到的结论:轻链只需一次就能移动到重链,重链之间辗转不超过logn次
我们接下来分析重链为什么能、如何加速整个数据结构。也就是重链内部是如何传递的。
所以要回答这个问题,就是要回答如何用数据结构维护重链。比如例题是对链进行加减查询操作,所以我们选择树状数组(或者线段树)来维护重链。(再把例题贴一遍:)
例题(HDU - 3966)
题意:给一棵树,并给定各个点权的值,然后有3种操作:
I C1 C2 K: 把C1与C2的路径上的所有点权值加上K
D C1 C2 K:把C1与C2的路径上的所有点权值减去K
Q C:查询节点编号为C的权值
数据量为5e4
(代码参考kuangbin的模板)
上面我们已经说了,轻链向重链靠拢,重链使用数据结构维护。要用树状数组维护重链,就要先将重链连续标号。也就是线性化。
这里使用dfs序将树上节点重新标号。从根节点出发,按照优先遍历重节点的顺序,先序遍历整棵树,记录下每个节点的时间戳。这样我们发现,由于是优先遍历重节点,则同一个重链内部一定是连续序号的。只要重链内部序号连续,就可以用树状数组维护区间特性了。
当然不同题目要求,我们采取的数据结构维护也不一样,但有一些基本操作。以上述例题为例。
定义数据和初始化:
#include
#include
using namespace std;
int const maxn=5e4+10;//数据量
/* 树的内存和结构 */
struct Edge{ //前向星数据结构(我按照邻接表理解的,下面我都按照邻接表来讲)
int to; //邻接表条目
int next; //指向下一个节点的指针。
}edge[maxn*2]; //邻接表内存池 ,要开足够大。
int head[maxn],tot; //head是邻接表头指针。tot是内存池分配指针,初始为0;/* 下面是节点、链的信息 */
int top[maxn]; //top[v] 记录节点v所在重链的顶端节点。顶端节点应为轻节点(重节点的父亲)
int fa[maxn]; //记录节点的父亲节点(前驱)
int deep[maxn]; //记录节点深度
int num[maxn]; //num[v]表示以v为根的子树节点数。
int p[maxn];//p[v]表示v对应的位置(节点对应的dfs序)
int fp[maxn];//与p数组相反。(dfs序对应的节点号)
int son[maxn]; //重儿子。int pos;
void init(){ //初始化
tot=0;
memset(head,-1,sizeof(head));
pos=1;//使用树状数组,编号从1开始
memset(son,-1,sizeof(son));
}
建立树:
首先我们要把树建立起来,这里选择用链式前向星(也就是内存池+邻接表)的方式存图。
/*不断添加边来建树*/
void addedge(int u,int v){ //头插法向u的邻接表里插入v,若无向图则要正反都添加
edge[tot].to=v; //从内测池edge中取一个节点空间,
edge[tot].next=head[u]; //将节点插入邻接表,头插法
head[u]=tot++; //将头节点重新指向链表头部,内存池计数变量加一
}
初始化信息:
第一次dfs得到的信息有:每个点的(深度、父亲、子树点的个数、重儿子)
void dfs1(int u,int pre,int d){ //当前节点,前驱节点,深度
deep[u]=d; //初始化深度
fa[u]=pre; //记录前驱
num[u]=1; //子树点个数统计,算上自己的1
for(int i=head[u]; i!=-1 ;i=edge[i].next){ //遍历u的所有儿子
int v=edge[i].to; //儿子
if(v!=pre){
dfs1(v,u,d+1); //递归
num[u]+=num[v]; //加上儿子的子树节点个数
if(son[u]==-1 || num[v]>num[son[u]]){ //寻找重儿子
son[u] = v;
}
}
}
}
设置dfs序并将重节点串成重链:
/*第二次dfs优先遍历重节点设置dfs序,连接重链,并寻找每个节点(如果在重链上)所在重链的头部*/
void getpos(int u,int sp){ //当前节点,所在重链头部。
top[u]=sp; //统计所在重链头部
p[u]=pos++; //记录节点号对应的dfs序
fp[p[u]]=u; //记录dfs序对应的节点号
if(son[u] == -1) //因为只有叶子没有重儿子,所以用来判断是否为叶子。
return;
getpos(son[u],sp);//优先递归遍历重儿子,重儿子重链头部跟自己一样,所以直接填sp
for(int i=head[u] ; i!=-1;i=edge[i].next){ //遍历轻儿子
int v=edge[i].to; //轻儿子
if(v!=son[u] && v!=fa[u]){ //确保是轻儿子
getpos(v,v); //轻儿子要么是轻链(轻链就不用管啦),要么是重链开头,所以sp填轻儿子本身。
}
}
}
至此树链剖分部分就完成了,接下来是用数据结构(树状数组)维护重链。
树状数组维护重链
(直接套一个裸的树状数组)
注意:柱状数组是建立在dfs序上的。
前缀和,线段树,树状数组讲解(入门)<-------传送门
/*-------------------树状数组-------------------*/
#define lowbit(x) (x&-x)
int c[maxn];//树
int n;
int sum(int i){//求前缀和
int s=0;
while(i>0){
s+=c[i];
i-=lowbit(i);
}
return s;
}
void add(int i,int val){//
while(i<=n){
c[i]+=val;
i+=lowbit(i);
}
}
题目解决部分
改变路径上点权
对于轻链直接暴力改变,对于重链使用树状数组区间更新。
/* 解决题目 */
/* u-->v的路径上点的值改变val */
void change(int u,int v,int val){
int f1=top[u],f2=top[v];//top是所在链起始端点(对于轻链就是本身喽)
int tmp=0;
while(f1 !=f2){ //直到u和v辗转到同一个重链后停止。 一段一段change
if(deep[f1]swap(f1,f2);
swap(u,v);
}
add(p[f1],val); //由于u深度大,所以先让u往 lca靠
add(p[u]+1,-val);//这里的add是后缀区间加值,所以这一句把多加的后缀区间减掉,就变成了重链上一个区间。
u=fa[f1]; //重链之间辗转
f1=top[u]; //重链之间辗转
}
if(deep[u]>deep[v]) //while结束之后u和v在同一重链上了,然后 把最后一段change掉
swap(u,v);
add(p[u],val); //两个点已经在同一个重链上了,直接区间改变即可。
add(p[v]+1,-val);
}
主函数
int a[maxn];
int main(){
#ifndef ONLINE_JUDGE
freopen("r.txt","r",stdin);
#endif
int m,q;
while(scanf("%d%d%d",&n,&m,&q)!=EOF){
int u,v;
int c1,c2,k;
char op[10];
init();
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
while(m--){
scanf("%d%d",&u,&v);
addedge(u,v);
addedge(v,u);//无向图双向都要加
}
dfs1(1,0,0);//根节点,根节点的father,根节点的深度
getpos(1,1);//根节点,根节点所在重链起始节点。
memset(c,0,sizeof(c));//树状数组清零
for(int i=1;i<=n;i++){
add(p[i],a[i]);
add(p[i]+1,-a[i]);
}
while(q--){
scanf("%s",op);
if(op[0]=='Q'){
scanf("%d",&u);
printf("%d\n",sum(p[u]));
}
else{
scanf("%d%d%d",&c1,&c2,&k);
if(op[0]=='D')
k=-k;
change(c1,c2,k);
}
}
}
}
全部AC代码
#include
#include
using namespace std;
int const maxn=5e4+10;//数据量
/* 树的内存和结构 */
struct Edge{ //前向星数据结构(我按照邻接表理解的,下面我都按照邻接表来讲)
int to; //邻接表条目
int next;//指向下一个节点的指针。
}edge[maxn*2];//邻接表内存池 ,要开足够大。
int head[maxn],tot;//head是邻接表头指针。tot是内存池分配计数指针,初始为0;/* 下面是节点、链的信息 */
int top[maxn];//top[v] 记录节点v所在重链的顶端节点。顶端节点应为轻节点(重节点的父亲)
int fa[maxn];//记录节点的父亲节点(前驱)
int deep[maxn];//记录节点深度
int num[maxn];//num[v]表示以v为根的子树节点数。
int p[maxn];//p[v]表示v对应的位置(节点对应的dfs序)
int fp[maxn];//与p数组相反。(dfs序对应的节点号)
int son[maxn];//重儿子。int pos;
void init(){
tot=0;
memset(head,-1,sizeof(head));
pos=1;//使用树状数组,编号从1开始
memset(son,-1,sizeof(son));
}/*添加边*/
void addedge(int u,int v){ //头插法向u的邻接表里插入v,若无向图则要正反都添加
edge[tot].to=v; //从内测池edge中取一个节点空间,
edge[tot].next=head[u]; //将节点插入邻接表,头插法
head[u]=tot++; //将头节点重新指向链表头部,内存池计数变量加一
}
void dfs1(int u,int pre,int d){ //当前节点,前驱节点,深度
deep[u]=d; //初始化深度
fa[u]=pre; //记录前驱
num[u]=1; //子树点个数统计
for(int i=head[u]; i!=-1 ;i=edge[i].next){ //遍历u的所有儿子
int v=edge[i].to; //儿子
if(v!=pre){
dfs1(v,u,d+1); //递归
num[u]+=num[v]; //加上儿子的子树节点个数
if(son[u]==-1 || num[v]>num[son[u]]){ //寻找重儿子
son[u] = v;
}
}
}
}
/*第二次dfs优先遍历重节点设置dfs序,连接重链,并寻找每个节点(如果在重链上)所在重链的头部*/
void getpos(int u,int sp){ //当前节点,所在重链头部。
top[u]=sp; //统计所在重链头部
p[u]=pos++; //记录节点号对应的dfs序
fp[p[u]]=u; //记录dfs序对应的节点号
if(son[u] == -1) //因为只有叶子没有重儿子,所以用来判断是否为叶子。
return;
getpos(son[u],sp);//优先递归遍历重儿子,重儿子重链头部跟自己一样,所以直接填sp
for(int i=head[u] ; i!=-1;i=edge[i].next){ //遍历轻儿子
int v=edge[i].to; //轻儿子
if(v!=son[u] && v!=fa[u]){ //确保是轻儿子
getpos(v,v); //轻儿子要么是轻链(轻链的起始就是本身喽),要么是重链开头,所以sp填轻儿子本身。
}
}
}/*-------------------树状数组-------------------*/
#define lowbit(x) (x&-x)
int c[maxn];//树
int n;
int sum(int i){
int s=0;
while(i>0){
s+=c[i];
i-=lowbit(i);
}
return s;
}
void add(int i,int val){
while(i<=n){
c[i]+=val;
i+=lowbit(i);
}
}/* 解决题目 */
/* u-->v的路径上点的值改变val */
void change(int u,int v,int val){
int f1=top[u],f2=top[v];//top是所在链起始端点(对于轻链就是本身喽)
int tmp=0;
while(f1 !=f2){ //直到u和v辗转到同一个重链后停止。 一段一段change
if(deep[f1]swap(f1,f2);
swap(u,v);
}
add(p[f1],val); //由于u深度大,所以先让u往 lca靠
add(p[u]+1,-val);//这里的add是后缀区间加值,所以这一句把多加的后缀区间减掉,就变成了重链上一个区间。
u=fa[f1]; //重链之间辗转
f1=top[u]; //重链之间辗转
}
if(deep[u]>deep[v]) //while结束之后u和v在同一重链上了,然后 把最后一段change掉
swap(u,v);
add(p[u],val); //两个点已经在同一个重链上了,直接区间改变即可。
add(p[v]+1,-val);
}int a[maxn];
int main(){
#ifndef ONLINE_JUDGE
freopen("r.txt","r",stdin);
#endif
int m,q;
while(scanf("%d%d%d",&n,&m,&q)!=EOF){
int u,v;
int c1,c2,k;
char op[10];
init();
for(int i=1;i<=n;i++)
scanf("%d",&a[i]);
while(m--){
scanf("%d%d",&u,&v);
addedge(u,v);
addedge(v,u);//无向图双向都要加
}
dfs1(1,0,0);//根节点,根节点的father,根节点的深度
getpos(1,1);//根节点,根节点所在重链起始节点。
memset(c,0,sizeof(c));//树状数组清零
for(int i=1;i<=n;i++){
add(p[i],a[i]);
add(p[i]+1,-a[i]);
}
while(q--){
scanf("%s",op);
if(op[0]=='Q'){
scanf("%d",&u);
printf("%d\n",sum(p[u]));
}
else{
scanf("%d%d%d",&c1,&c2,&k);
if(op[0]=='D')
k=-k;
change(c1,c2,k);
}
}
}
}