• 【模板时间】◆模板·II◆ 树链剖分


    【模板·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
    ",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
    ",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
    ",QueryRoad(u,v));
    175     }
    176     return 0;
    177 }

     The End

    Thanks for reading!

    - Lucky_Glass

    (Tab:如果我有没讲清楚的地方可以直接在邮箱lucky_glass@foxmail.com email我,在周末我会尽量解答并完善博客~)
  • 相关阅读:
    linux所有命令失效的解决办法
    第一章 网络基础知识
    RNQOJ 数列
    RNQOJ Jam的计数法
    RNQOJ 开心的金明
    RQNOJ 明明的随机数
    分类讨论的技巧
    Unity 碰撞检测
    Unity --yield return
    Unity 移动方式总结
  • 原文地址:https://www.cnblogs.com/LuckyGlass-blog/p/9371618.html
Copyright © 2020-2023  润新知