【树形结构】树链剖分

恶心至极

树链剖分

树链就是树上的路径。
将一棵树划分成若干条链,用数据结构(线段树,平衡树等)去维护每条链,复杂度为 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)

重链剖分

剖分有三种方法:盲目剖分、随机剖分、启发式剖分。
综合比较,启发式剖分是剖分时的最佳选择。

定义 s i z e ( x ) size(x) size(x)为以 x x x为根的子树的节点个数。
v v v u u u的儿子节点中 s i z e size size值最大的节点,那么边 ( u , v ) (u,v) (u,v)被称为重边,树中重边之外的边被称为轻边
我们称某条路径为重路径(也叫重链),当且仅当它全部由重边组成。
对轻边 ( u , v ) (u,v) (u,v) s i z e ( v ) ⩽ 1 2 s i z e ( u ) size(v)\leqslant\frac 12size(u) size(v)21size(u)
从根到某一点的路径上,不超过 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)条轻边,不超过 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)条重路径。

重链剖分的过程为2次dfs:

  • 第一步,找重边(图中的红边)。通过一次dfs,可记下所有的重边。
  • 第二步,连重边成重链。以根节点为起点,沿重边向下拓展,拉成重链。不在当前重链上的节点,都以该节点为起点向下重新拉一条重链。

记树中编号为 i i i的结点在用来维护的数据结构中的新编号为 i d [ i ] id[i] id[i],且 r n k [ i d [ i ] ] = i rnk[id[i]]=i rnk[id[i]]=i t o p top top记录每个结点所在重链的起点(深度最小的结点), d e p dep dep记录每个结点的深度。之后会用到。

int id[maxn],rnk[maxn];
struct edge{
  int to,pre;
  edge(int to=0,int pre=0):to(to),pre(pre){};
};
struct tree{
  edge e[maxn];
  int now[maxn],edgtot=0,idtot=0;
  int fa[maxn],son[maxn],dep[maxn],size[maxn],top[maxn];
  void insert(int u,int v){
    e[++edgtot]=edge(v,now[u]),now[u]=edgtot;
  }
  void find(int x,int cfa,int cdep){
    fa[x]=cfa,dep[x]=cdep,size[x]=1,son[x]=0;
    for(int i=now[x];i;i=e[i].pre){
      int cson=e[i].to;
      if(cson!=fa[x]){
        find(cson,x,cdep+1);size[x]+=size[cson];
        if(size[cson]>size[son[x]])son[x]=cson;
      }
    }
  }
  void connect(int x,int anc){
    id[x]=++idtot,rnk[idtot]=x,top[x]=anc;
    if(son[x])connect(son[x],anc);
    for(int i=now[x];i;i=e[i].pre){
      int cson=e[i].to;
      if(cson!=fa[x]&&cson!=son[x])connect(cson,cson);
    }
  }
};

修改查询结点

直接在数据结构上面修改/查询就行了。注意要使用新的结点编号 i d [ x ] id[x] id[x]

修改查询路径


剖分完之后,每条重链就相当于一段 i d id id编号连续的区间。
整体修改或查询点 u u u和点 v v v的路径上的权值:

  • 情况1:如果 u u u v v v在同一条重链上,直接用数据结构(代码中以线段树为例)修改/查询 i d [ u ] id[u] id[u] i d [ v ] id[v] id[v]这段区间的值。
  • 情况2:如果 u u u v v v不在同一条重链上,一边进行修改/查询,一边将 u u u v v v往同一条重链上靠(当然是让起点深度大的往上跳),然后就变成了情况1。
    • 情况A:若 f a [ t o p [ u ] ] fa[top[u]] fa[top[u]] v v v在同一条重链上,修改/查询点 u u u t o p [ u ] top[u] top[u]间的各权值,然后 u u u跳至 f a [ t o p [ u ] ] fa[top[u]] fa[top[u]],就变成了情况1。
    • 情况B:若 u u u向上经过若干条重链和轻边后才与 v v v在同一条重链上,不断地修改/查询当前 u u u t o p [ u ] top[u] top[u]间的各权值,再将 u u u跳至 f a [ t o p [ u ] ] fa[top[u]] fa[top[u]],直到 u u u v v v在同一条重链。
    • 情况C:若 u u u v v v都是向上经过若干条重链和轻边,到达同一条重链,每次在点 u u u和点 v v v中,选择 d e p [ t o p [ x ] ] dep[top[x]] dep[top[x]]较大的点 x x x,修改/查询 x x x t o p [ x ] top[x] top[x]间的各权值,再跳至 f a [ t o p [ x ] ] fa[top[x]] fa[top[x]],直到点 u u u和点 v v v在同一条重链。
...
segment_tree segT;
struct tree{
  edge e[maxn];
  int now[maxn],edgtot=0,idtot=0;
  int fa[maxn],son[maxn],dep[maxn],size[maxn],top[maxn];
  ...
  int query(int x,int y){
    int ans=0;
    while(top[x]!=top[y]){
      if(dep[top[x]]<dep[top[y]])swap(x,y);
      ans+=segT.query(1,id[top[x]],id[x]);
      x=fa[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    ans+=segT.query(1,id[x],id[y]);
    return ans;
  }
  void modify(int x,int y,int d){
    while(top[x]!=top[y]){
      if(dep[top[x]]<dep[top[y]])swap(x,y);
      segT.modify(1,id[top[x]],id[x],d);
      x=fa[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    segT.modify(1,id[x],id[y],d);
  }
};

修改查询子树


根据dfs序,同一棵子树上的结点的 i d id id编号也是连续的。表示这棵子树的区间就是以根节点 i i i的编号 i d [ i ] id[i] id[i]为起点、长度为 s i z e [ i ] size[i] size[i]的区间。

...
segment_tree segT;
struct tree{
  edge e[maxn];
  int now[maxn],edgtot=0,idtot=0;
  int fa[maxn],son[maxn],dep[maxn],size[maxn],top[maxn];
  ...
  int query_subtree(int x){
    return segT.query(1,id[x],id[x]+size[x]-1);
  }
  void modify_subtree(int x,int d){
    segT.modify(1,id[x],id[x]+size[x]-1,d);
  }
};

求LCA

若两个点处在同一条重链上,深度低的那个就是LCA。
如果不在同一条重链上,那么就跟前面的修改/查询操作一样,让两个点向上跳,直至它们在一条重链上。

...
segment_tree segT;
struct tree{
  edge e[maxn];
  int now[maxn],edgtot=0,idtot=0;
  int fa[maxn],son[maxn],dep[maxn],size[maxn],top[maxn];
  ...
  int Lca(int x,int y){
    while(top[x]!=top[y]){
      if(dep[top[x]]<dep[top[y]])swap(x,y);
      x=fa[top[x]];
    }
    if(dep[x]>dep[y])swap(x,y);
    return x;
  }
};
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值