首页 > 技术文章 > 【模板时间】◆模板·II◆ 树链剖分

LuckyGlass-blog 2018-07-26 21:28 原文

【模板·II】树链剖分

学长给我讲树链剖分,然而我并没有听懂,还是自学有用……另外感谢一篇Blog +by 自为风月马前卒+


一、算法简述

树链剖分可以将一棵普通的多叉树转为线段树计算,不但可以实现对一棵子树的操作,还可以实现对两点之间路径的操作,或是求 LCA(看起来很高级)。

其实树链剖分不算什么特别高难的算法,它建立在 LCA、线段树、DFS序 的基础上(如果不了解这些算法的还是先把这些算法学懂再看树链剖分吧 QwQ)。又因为树链剖分的基础算法不难,树链剖分的题也逐渐被引入 OI 赛中。

不得不说,它的代码很长……尽管很多都是模板,但是还是必须理解清楚每一个模板的含义,不然容易混淆。

二、原理

 (1)基本定义

重儿子:对于每一个非叶子节点u的儿子vi,若以vi为根的子树的节点数是u的所有儿子的子树的节点数中最大的,则 vi 是u的重儿子;

重边:对于每一个非叶子节点u,它与其重儿子的连边为重边;

重链:树中只包含重边的一条链,这里我们把单个元素也看成重链;

轻儿子:非重儿子的节点;轻边:非重边;

举个例子:

(2)简单定理

  • 重链的起点一定是轻点;
  • 一条轻边一定连接重链上的一点和另一条重链的起点(重链的起点算入重链);
  • 任何节点都只属于一条重链;
  • 除了叶子节点,其他所有点一定有重儿子;

(3)计算重儿子

  siz[u]: 以u节点为根的子树的节点数量(包含u);

  dep[u]:u的深度(根节点深度为1);

  fa[u]:节点u的父亲,一般来说根节点父亲为0;

  heavy[u]:u的重儿子,没有为0;

  val[u]:u的点权;

(以上是我自己定义的名字,可能不太规范)

可以直接用一个DFS从根节点开始,用递归的方式求出每一棵子树的节点数,并找到有最大节点数的儿子,作为heavy[],不要在意那些节点数相同的情况,如果出现,则只选取其中一个作为heavy[]就可以了。

具体如何实现?我还是奉上代码吧 @( ◕ x ◕ )@:

 1 void DFS1(int u,int Fa,int Dep)
 2 {
 3     dep[u]=Dep;fa[u]=Fa; //更新深度、父节点
 4      siz[u]=1; //将u本身计入siz
 5      int MAX_siz=0; //最大子树大小
 6      for(int i=0;i<lnk[u].size();i++)
 7           if(lnk[u][i]!=Fa) //避免重复错误
 8           {
 9                 int v=lnk[u][i];
10                 DFS1(v,u,Dep+1);
11                 siz[u]+=siz[v]; //统计大小
12                 if(siz[v]>MAX_siz)
13                      MAX_siz=siz[v],heavy[u]=v; //求重儿子
14           }
15 }

 

(4)计算DFN序以及重链

上一步的DFS1为求重链打下了基础。

新添几个定义: ID[u]:u的DFS序(或者可以叫dfn);Top[u]:u所在的重链的起始点(即深度最小的点);fval[x]:DFS序为x的点的点权;

这次DFS,我们需要改变搜索顺序,不能按输入顺序遍历点——先搜索重儿子,再搜索其它儿子,如果没有重儿子,说明是叶子节点,结束搜索后回溯。这样我们保证了对于每一个非叶子节点u和它的重儿子v,ID[u]和ID[v]是连续的,按照这个规律,我们可以发现,在同一条重链中,相邻元素的ID总是连续的。这是一个非常重要的性质,正因为有这个性质,我们可以应用线段树来计算(等会解释)。

举个例子吧:

如何求 Top[u] 呢?

我们可以通过下传参数实现——下传一个参数 topf ,表示当前节点属于的重链起始于 topf。我们优先访问u的重儿子v,即使优先访问u所在的重链,此时u、v仍然属于同一个重链,因此topf不变,可以下传。而当我们搜索u的其他儿子v时,相当于经过了一条轻边,因此我们就到达了另一条重链的起点("简单定理"中第2条),所以将v作为topf继续下传。

 1 void DFS2(int u,int topf)
 2 {
 3      ID[u]=++ID_cnt;fval[ID_cnt]=val[u]; //ID_cnt 就是当前的DFS序;同时给fval赋值
 4      Top[u]=topf;
 5      if(!heavy[u]) return; //叶子节点
 6      DFS2(heavy[u],topf);
 7      for(int i=0;i<lnk[u].size();i++)
 8      {
 9           int v=lnk[u][i];
10           if(ID[v]) continue; //避免访问重复
11           DFS2(v,v); //v是另一条重链的起点
12      }
13 }

 

(5)线段树

这才是重头戏……

线段树最明显的优点就是区间修改、区间查询,但是这一切的前提就是它修改、查询的是一个连续的区间!这就是为什么要让一条重链上的ID成为连续的一串。在这棵线段树上,区间是ID值,即我们储存的是连续一段ID值所包含的信息。

但是这仅仅是第一步……谁也不知道线段树的强大功能到底还有哪些……

◆ 修改、查询一棵子树

一个最简单的性质 —— 以u为根的子树中 DFS 序(ID)是连续的长度为siz[u]的序列。当我们知道 ID[u] 时,我们可以马上知道这棵子树中ID最大的节点ID为 (ID[u]+siz[u]-1),又因为这是一个连续区间,线段树就可以发挥它的用途了!

以u为根的一棵子树在线段树上对应区间为 [ID[u],ID[u]+siz[u]-1]。

◆ 修改、查询从u到v的一条路径(当然也是唯一路径)

我们先分三种情况(在下面的描述中,dep[u]≤dep[v]):

① u、v本来就在一条重链上

由于重链是连续一段,且 dep[u]≤dep[v] ,所以 u->v 是连续的区间 [ID[u],ID[v]] ,这样用线段树求解比较容易吧。

② u、v分别属于的重链之间隔了一条轻边

还记得之前的“简单定理”吗?既然两条重链隔了一条轻边,则该轻边一定连接了一条轻边的顶点 Top。不妨设 dep[Top[u]]≤dep[Top[v]] ,那么我们可以得到 fa[Top[v]] 属于 u 所在重链。就像求LCA一样,我们把v上移到 fa[Top[v]] ,又归属于情况①,而 v 上移的一段也是一条重链,这段重链所属区间为 [ID[Top[v]],ID[v]] (ID[Top[v]] ≤ ID[v],因为Top[v]的深度一定小于等于v)。

这样答案就变成了两个区间 [ID[Top[v]],ID[v]] 和 [ID[fa[Top[v]]],ID[u]] 。

③ 普通情况

过程越发的像LCA了——我们的目的越发清晰——将u、v移动到同一条重链上。

我们不断的重复将点 x 做下列操作:

移动到 Top[x] -> 线段树区间处理 -> 移动到 fa[x] -> 移动到 Top[x]....

给一个伪代码把:

while(u,v不在同一个重链)

{

  保证 Top[u]的深度>Top[v]的深度 //我们固定移动u,所以一定要让u移动后不会离v的重链越来越远

  记录答案/修改区间 [ID[Top[u]],ID[u]] 

  u 移动到 fa[Top[u]]

}

//现在u、v在同一个重链了

转换为问题①

如何判断u、v是否属于同一个重链?还记得 Top吗? 同一个重链上的点的 Top 一定相同啊,所以只需要判断 Top 是否相同就行了。 QwQ

举个例子模拟一下:

三、一道板板题 +洛谷 3384 树链剖分模板题+

就是两种问题——对子树以及对路径。大家可以看看代码,先熟悉一下……

  1 /*Lucky_Glass*/
  2 #include<cstdio>
  3 #include<cstring>
  4 #include<algorithm>
  5 #include<vector>
  6 using namespace std;
  7 const int MAXN=int(1e5);
  8 int n,t,rt,mod;
  9 vector<int> lnk[MAXN+5];
 10 int val[MAXN+5];
 11 int dep[MAXN+5],fa[MAXN+5],heavy[MAXN+5],siz[MAXN+5];
 12 void DFS1(int u,int Fa,int Dep)
 13 {
 14     dep[u]=Dep;fa[u]=Fa; //更新深度、父节点
 15      siz[u]=1; //将u本身计入siz
 16      int MAX_siz=0; //最大子树大小
 17      for(int i=0;i<lnk[u].size();i++)
 18           if(lnk[u][i]!=Fa) //避免重复错误
 19           {
 20                 int v=lnk[u][i];
 21                 DFS1(v,u,Dep+1);
 22                 siz[u]+=siz[v]; //统计大小
 23                 if(siz[v]>MAX_siz)
 24                      MAX_siz=siz[v],heavy[u]=v; //求重儿子
 25           }
 26 }
 27 int ID[MAXN+5],fval[MAXN+5],Top[MAXN+5],ID_cnt;
 28 void DFS2(int u,int topf)
 29 {
 30      ID[u]=++ID_cnt;fval[ID_cnt]=val[u]; //ID_cnt 就是当前的DFS序;同时给fval赋值
 31      Top[u]=topf;
 32      if(!heavy[u]) return; //叶子节点
 33      DFS2(heavy[u],topf);
 34      for(int i=0;i<lnk[u].size();i++)
 35      {
 36           int v=lnk[u][i];
 37           if(ID[v]) continue; //避免访问重复
 38           DFS2(v,v); //v是另一条重链的起点
 39      }
 40 }
 41 struct TREE{
 42      int l,r,val,siz,lazy;
 43      TREE(){}
 44      TREE(int fl,int fr){
 45           l=fl,r=fr;siz=r-l+1,lazy=0;
 46      }
 47 }tree[MAXN*4+5];
 48 //上传更新总和
 49 void Update(int x){tree[x].val=((tree[x<<1].val+tree[x<<1|1].val)%mod+mod)%mod;}
 50 //下传懒标记
 51 void PushDown(int x)
 52 {
 53      tree[x<<1].val=(tree[x<<1].val+tree[x<<1].siz*tree[x].lazy)%mod;
 54      tree[x<<1|1].val=(tree[x<<1|1].val+tree[x<<1|1].siz*tree[x].lazy)%mod;
 55      tree[x<<1].lazy=(tree[x<<1].lazy+tree[x].lazy)%mod;
 56      tree[x<<1|1].lazy=(tree[x<<1|1].lazy+tree[x].lazy)%mod;
 57      tree[x].lazy=0;
 58 }
 59 //构造线段树
 60 void Build(int l,int r,int x)
 61 {
 62      tree[x]=TREE(l,r);
 63      if(l==r) {tree[x].val=fval[l];return;}
 64      int mid=(l+r)>>1;
 65      Build(l,mid,x<<1);
 66      Build(mid+1,r,x<<1|1);
 67      Update(x);
 68 }
 69 //给[L,R]的每个元素加上add
 70 void Add(int L,int R,int add,int x)
 71 {
 72      if(L>tree[x].r || R<tree[x].l) return;
 73      if(L<=tree[x].l && tree[x].r<=R)
 74      {
 75           tree[x].val+=tree[x].siz*add;
 76           tree[x].lazy+=add;
 77           return;
 78      }
 79      PushDown(x);
 80      Add(L,R,add,x<<1);
 81      Add(L,R,add,x<<1|1);
 82      Update(x);
 83 }
 84 //求[L,R]中元素的和
 85 int Sum(int x,int L,int R)
 86 {
 87      if(L>tree[x].r || R<tree[x].l)
 88           return 0;
 89      if(L<=tree[x].l && tree[x].r<=R)
 90           return tree[x].val;
 91      PushDown(x);
 92      return (Sum(x<<1,L,R)+Sum(x<<1|1,L,R))%mod;
 93 }
 94 //求路径 u->v 的总和
 95 int Road(int u,int v)
 96 {
 97      int ret=0;
 98      while(Top[u]!=Top[v])
 99      {
100           if(dep[Top[u]]<dep[Top[v]]) swap(u,v); //保证Top[u]在Top[v]下
101           ret=(ret+Sum(1,ID[Top[u]],ID[u]))%mod; //更新答案
102           u=fa[Top[u]]; //移动u
103      }//此时u,v应该属于同一条重链了
104      if(dep[u]>dep[v]) swap(u,v);
105      ret=(ret+Sum(1,ID[u],ID[v]))%mod; //同一重链中计算
106      return ret;
107 }
108 //修改路径 u->v
109 void Insert(int u,int v,int add)
110 {
111      while(Top[u]!=Top[v])
112      {
113           if(dep[Top[u]]<dep[Top[v]]) swap(u,v);
114           Add(ID[Top[u]],ID[u],add,1); //利用线段树修改一段重链
115           u=fa[Top[u]];
116      }
117      if(dep[u]>dep[v]) swap(u,v);
118      Add(ID[u],ID[v],add,1); //最后一段u->v
119 }
120 int main()
121 {
122      scanf("%d%d%d%d",&n,&t,&rt,&mod);
123      for(int i=1;i<=n;i++)
124           scanf("%d",&val[i]);
125      for(int i=1;i<n;i++)
126      {
127           int u,v;scanf("%d%d",&u,&v);
128           lnk[u].push_back(v);lnk[v].push_back(u);
129      }
130      DFS1(rt,0,1);
131      DFS2(rt,rt);
132      Build(1,n,1);
133      while(t--)
134      {
135           int cmd;scanf("%d",&cmd);
136           int x,y,z;
137           switch(cmd)
138           {
139                 case 1: scanf("%d%d%d",&x,&y,&z);Insert(x,y,z%mod);break;
140                 case 2: scanf("%d%d",&x,&y);printf("%d\n",Road(x,y));break;
141                 case 3: scanf("%d%d",&x,&y);Add(ID[x],ID[x]+siz[x]-1,y%mod,1);break;
142                 case 4: scanf("%d",&x);printf("%d\n",Sum(1,ID[x],ID[x]+siz[x]-1));break;
143           }
144      }
145      return 0;
146 }
View Code
其他的题目:

(补充:某dalao嫌我写的太少了 Orz)

四、奇怪的区间操作 +BZOJ 2243 染色+

还记得最初写线段树的时候,有一类题是修改区间颜色,然后查询一个区间中颜色段的数量。这就涉及到如果两个相邻区间的边缘颜色相同,则它们的边缘颜色将合并为一个,这个时候就需要把总区间的颜色段个数减一(比如区间[l,r],若[l,mid]的颜色段数量为a,[mid+1,r]的颜色段数量为b,但是mid颜色和mid+1颜色相同,则[l,r]颜色段数量为 a+b-1)。现在又在树链剖分有缘重逢……

现在查询的不是一个区间了,而是一个路径!按照之前的思路,我们可以把路径拆成多个重链,也就是多个区间,这就产生了一个问题——如何判断相邻区间的左右端点颜色是否相同?

于是……一个美妙的想法从我的脑间闪过——改变函数返回值!

之前的线段树查询都只是返回一个int的答案,但这次我返回了答案以及答案对应区间的左右端点颜色(用struct打包一下),这样我就可以判断左右端点颜色是否一样了!!(o^^o)♪

最后注意一下当查询的路径的两个端点已经移动到同一条重链上时,要判断该重链的两端点是否与之前的颜色相同,及时删减答案!

源代码

  1 /*Lucky_Glass*/
  2 #include<cstdio>
  3 #include<cstring>
  4 #include<algorithm>
  5 #include<vector>
  6 using namespace std;
  7 const int MAXN=int(1e5);
  8 int n,m;
  9 int col[MAXN+5],dep[MAXN+5],fa[MAXN+5],siz[MAXN+5],hvy[MAXN+5],ID[MAXN+5],top[MAXN+5],typ[MAXN+5];
 10 //col 原节点颜色,dep 深度,fa 父亲,siz 子树大小,hvy[u] u的重儿子,ID[u] u的编号(DFS序,用于线段树),top[u] u所在重链的起点,typ[x] ID为x的节点的颜色
 11 vector<int> lnk[MAXN+5];
 12 void DFS1(int u,int _fa,int _dep)
 13 {
 14     fa[u]=_fa;dep[u]=_dep;siz[u]=1; //更新父亲,深度,子树大小初始化为1(因为根节点也算入大小)
 15     for(int i=0;i<lnk[u].size();i++)
 16     {
 17         int v=lnk[u][i];
 18         if(v==_fa) continue;
 19         DFS1(v,u,_dep+1);
 20         siz[u]+=siz[v];
 21         if(siz[hvy[u]]<siz[v]) //求得重儿子
 22             hvy[u]=v;
 23     }
 24 }
 25 int ID_cnt;
 26 void DFS2(int u,int _top)
 27 {
 28     ID[u]=++ID_cnt;top[u]=_top;typ[ID[u]]=col[u];
 29     if(!hvy[u]) return; //没有根节点的节点是叶子节点
 30     DFS2(hvy[u],_top); //同一条重链
 31     for(int i=0;i<lnk[u].size();i++)
 32     {
 33         int v=lnk[u][i];
 34         if(ID[v]) continue;
 35         DFS2(v,v); //另一条重链的起点
 36     }
 37 }
 38 struct TREE{
 39     int l,r,lc,rc,tot,col;
 40     //左右端点,左右端点颜色,颜色段总数,区间颜色(不为纯色值为-1)
 41 }tree[MAXN*5+5];
 42 void Update(int u) //上传参数
 43 {
 44     tree[u].lc=tree[u<<1].lc;
 45     tree[u].rc=tree[u<<1|1].rc;
 46     tree[u].tot=tree[u<<1].tot+tree[u<<1|1].tot-(int)(tree[u<<1].rc==tree[u<<1|1].lc);
 47     if(tree[u<<1].col==tree[u<<1|1].col && tree[u<<1].col!=-1)
 48         tree[u].col=tree[u<<1].col;
 49     else
 50         tree[u].col=-1;
 51 }
 52 void Build(int L,int R,int u) //构建线段树
 53 {
 54     tree[u].l=L;tree[u].r=R;
 55     if(L==R) {tree[u].lc=tree[u].rc=tree[u].col=typ[L];tree[u].tot=1;return;}
 56     int mid=(L+R)>>1;
 57     Build(L,mid,u<<1);Build(mid+1,R,u<<1|1);
 58     Update(u);
 59 }
 60 void PushDown(int u) //下传懒标记
 61 {
 62     if(tree[u].col==-1) return;
 63     tree[u<<1].lc=tree[u<<1].rc=tree[u<<1].col=tree[u].col;
 64     tree[u<<1|1].lc=tree[u<<1|1].rc=tree[u<<1|1].col=tree[u].col;
 65     tree[u<<1].tot=tree[u<<1|1].tot=1;
 66     tree[u].col=-1;
 67 }
 68 void Modify(int L,int R,int val,int u) //修改区间[L,R]为val
 69 {
 70     if(tree[u].r<L || R<tree[u].l) return;
 71     if(L<=tree[u].l && tree[u].r<=R)
 72     {
 73         tree[u].lc=tree[u].rc=tree[u].col=val;
 74         tree[u].tot=1;
 75         return;
 76     }
 77     PushDown(u);
 78     Modify(L,R,val,u<<1);
 79     Modify(L,R,val,u<<1|1);
 80     Update(u);
 81 }
 82 void ModifyRoad(int u,int v,int val) //修改路径u->v为val
 83 {
 84     while(top[u]!=top[v])
 85     {
 86         if(dep[top[u]]<dep[top[v]]) swap(u,v);
 87         Modify(ID[top[u]],ID[u],val,1);
 88         u=fa[top[u]];
 89     }
 90     if(dep[u]<dep[v]) swap(u,v);
 91     Modify(ID[v],ID[u],val,1);
 92 }
 93 struct RETURN{
 94     int lc,rc,tot; //左右端点颜色,颜色段数量
 95     RETURN(){}
 96     RETURN(int _lc,int _rc,int _tot)
 97     {
 98         lc=_lc;rc=_rc;tot=_tot;
 99     }
100 };
101 RETURN Query(int L,int R,int u) //查询区间[L,R]颜色段数量
102 {
103     if(tree[u].r<L || R<tree[u].l) return RETURN(-1,-1,0);
104     if(L<=tree[u].l && tree[u].r<=R)
105         return RETURN(tree[u].lc,tree[u].rc,tree[u].tot);
106     PushDown(u); //*************************************必须有这一步
107     RETURN resl=Query(L,R,u<<1),
108                       resr=Query(L,R,u<<1|1);
109     RETURN ret;
110     ret.tot=resl.tot+resr.tot;
111     if(resl.rc==resr.lc) ret.tot--; //合并
112     if(!resl.tot) ret.lc=resr.lc,ret.rc=resr.rc;
113     else if(!resr.tot) ret.lc=resl.lc,ret.rc=resl.rc;
114     else ret.lc=resl.lc,ret.rc=resr.rc;
115     Update(u);
116     return ret;
117 }
118 int QueryRoad(int u,int v) //查询路径u->v
119 {
120     int cu=-1,cv=-1,ret=0;
121     while(top[u]!=top[v])
122     {
123         if(dep[top[u]]>dep[top[v]])
124         {
125             RETURN res=Query(ID[top[u]],ID[u],1);
126             ret+=res.tot;
127             if(res.rc==cu) ret--;
128             cu=res.lc;
129             u=fa[top[u]];
130         }
131         else
132         {
133             RETURN res=Query(ID[top[v]],ID[v],1);
134             ret+=res.tot;
135             if(res.rc==cv) ret--;
136             cv=res.lc;
137             v=fa[top[v]];
138         }
139     }
140     if(dep[u]>dep[v])
141     {
142         RETURN res=Query(ID[v],ID[u],1);
143         ret+=res.tot;
144         if(res.lc==cv) ret--; //判断两端点
145         if(res.rc==cu) ret--;
146     }
147     else
148     {
149         RETURN res=Query(ID[u],ID[v],1);
150         ret+=res.tot;
151         if(res.lc==cu) ret--;
152         if(res.rc==cv) ret--;
153     }
154     return ret;
155 }
156 int main()
157 {
158     scanf("%d%d",&n,&m);
159     for(int i=1;i<=n;i++)
160         scanf("%d",&col[i]),col[i]++; //由于输入颜色有0,强制转换为正整数
161     siz[0]=-1; //重儿子hvy数组的初始值是0,而为0时,siz[0]<0,就可以转换到其他的节点!
162     for(int i=1,u,v;i<n;i++)
163         scanf("%d%d",&u,&v),
164         lnk[u].push_back(v),
165         lnk[v].push_back(u);
166     DFS1(1,0,1);
167     DFS2(1,1);
168     Build(1,n,1);
169     while(m--)
170     {
171         int u,v,x;char cmd[2]="";
172         scanf("%s%d%d",cmd,&u,&v);
173         if(cmd[0]=='C') scanf("%d",&x),x++,ModifyRoad(u,v,x);
174         else printf("%d\n",QueryRoad(u,v));
175     }
176     return 0;
177 }

 


 The End

Thanks for reading!

- Lucky_Glass

(Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)

 

推荐阅读