树链剖分笔记

摘要

树链剖分是用来维护静态树上路径信息的数据结构,静态即树的形态不能改变(增删点或者换根,改变了构造)。
其思想是将树上的路径分为重路径和轻边,然后为每个节点分配一个编号,重路径上节点的编号是连续的;接着对这些编号建立一棵线段树,这样就可以将重路径当作区间来维护了;利用了“每个点到根的路径上都有不超过O(logN)条轻边和O(logN)条重路径”结论来保证时间复杂度。

算法简介

树链剖分通常用于解决一类维护静态树上路径信息的问题,例如,给定一棵点带权的树,接下来每次操作会修改某条路径上所有点的权值(修改为同一个值或是加上同一个值等),以及询问某条路径上所有点的权值和。

当这棵树是一条链时,这个问题实际上就是一个序列上区间修改、区间询问的问题,可以用线段树等数据结构解决。

对于其他情况,由于树的形态是不变的,因此树链剖分的策略是将这些点按某种方式组织起来,剖分成若干条链,每条链就相当于一个序列。这样,操作的路径可以拆分为某几条链,也就是若干个完整序列或是某个序列上的一段区间,此时可利用线段树等处理序列上区间操作的数据结构解决问题。

树链剖分的核心是如何恰当的将树剖分为若干条链。当链的划分方式确定后,我们只要将它们看作是一个个序列,将所有序列按顺序拼接起来,每条链就成了一段区间,而序列上的区间问题是我们所熟悉和擅长的。

算法流程

划分轻重边

下面以点带权的树为例,结合路径权值修改、路径询问权值和问题,介绍常用的剖分方法,该方法是轻、重边剖分。
我们将树中的边分为两种:轻边 和 重边。如图1所示,图中红色的边是重边,其余的是轻边。
在这里插入图片描述

图1:节点数字代表子树大小(size)
我们可以以任意点为根,然后记sz( u )为以u为根的子树的节点个数,令 v 为 u 所有儿子中sz 值最大的一个,则(u,v)是重边,v 称为 u 的重儿子。u 到其余儿子的边为轻边。

轻重边的性质

轻重边有如下几个性质:

  1. 如果(u , v)为轻边,则sz( v ) <= sz( u )/2。
  2. 从根到某一点v的路径上的轻边个数不多于O(logN)。
  3. 我们称某条路径为重路径(链),当且仅当它全部由重边组成(特殊地,一个点也算一条重路径)。那么对于每个点到根的路径上都有不超过O(logN)条轻边和O(logN)条重路径。

同时我们也容易发现,一个点在且只在一条重路径上,而每条重路径一定是一条从根节点方向向叶节点方向延申的深度递增的路径(因为一个非叶节点有且只有一个重儿子)。

如何处理树上的路径

对树进行轻、重边剖分后,操作所要处理的路径(u , v),我们可以分别处理u,v两个点到其最近公共祖先的路径。根据性质3,路径可以分解为最多O(logN)条的重路径和最多O(logN)的轻边,那么现在我们只考虑如何维护这两种对象。

对于重路径,它们此时相当于一个序列,因此我们只需要用线段树维护。而对于轻边,我们可以直接跳过,访问下一条重路径,因为轻边的两端点一定在某两条重路径上。这两种操作的时间复杂度分别为 O ( l o g 2 N ) O(log^2N) O(log2N)和O(logN),因此总的时间复杂度为 O ( l o g 2 N ) O(log^2N) O(log2N)

剖分方法

轻、重边剖分的过程可以用两次dfs实现,有时为了防止递归过深而导致栈溢出,也可以用bfs实现。
剖分过程中主要计算如下7个值:
par[x] : x在树中的父亲。
deep[x]:x在树中的深度。
siz[x]:x的子树节点数(子树大小)。
son[x]:x的重儿子,即u->son[u]为重边。
top[x]:x所在重路径的顶部节点(深度最小)。
seg[x]:x所在线段树中的位置(下标)。
rev[x]:线段树中第x个位置对应的树中节点编号,即rev[seg[x] ] = x。

第一遍 dfs 时可以计算前 4 个值,第二遍 dfs 可以计算后 3 个值。而计算 seg 时,同一条重路径上的点需要按顺序排在连续的一段位置,也就是一段区间。

将一条路径(u ,v)拆分为若干条重路径的过程,实际上就是一个寻找最近公共祖先的过程。考虑“暴力”的做法,我们会选择u,v中深度较大的点向上走一步,直到u = v。现在有了重路径,由于我们记录了重路径的顶部节点top[x],还记录了每个点在序列中的位置,因此我们不需要一步步走。假定top[u]和top[v]不同,那么它们的最近公共祖先可能在其中一条重路径上,也可能在其他的重路径上,因为LCA显然不可能在top深度较大的那条重路径上,所以我们先处理top深度较大的。假设u是深度较大的,则可以直接条到par[top[u]]处,且跳过的这一段,在线段树中是一段区间,若我们按照深度从小到大来存储点,则这段区间为:[seg[top[x]] , seg[x]]。当u,v的top相同时,说明它们走到了同一条重路径上,这时它们之间的路径也是序列上的一段区间,且u,v中深度较小的那点是原路径的LCA。

这样我们就可以将给出的任意路径拆分成若干条重路径,也就是若干个区间,并用线段树等数据结构处理操作。

例题模板

原题来自:ZJOI 2008

一树上有 n 个节点,编号分别为 1 到 n,每个节点都有一个权值 w。我们将以下面的形式来要求你对这棵树完成一些操作:
1.CHANGE u t :把节点 u 权值改为 t;
2.QMAX u v :询问点 u 到点 v 路径上的节点的最大权值;
3.QSUM u v :询问点 u 到点 v 路径上的节点的权值和。
注意:从点 u 到点 v 路径上的节点包括 u 和 v 本身。

模板使用说明:

  • 将树拆分成若干条链,为树上每个点分配一个编号,同一条链上的编号连续,然后在这些编号上建立线段树维护。seg[x]是树上的节点x在线段树的下标,rev[y]是线段树下标为 y 节点在树中的编号。
  • 树的节点编号要从1开始,不能从0。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int N = 3e4+10;
const int M = 2*N;
int head[N],ver[M],edge[M],nex[M],tot = 1;
void addEdge(int x,int y,int z){
    ver[++tot] = y, edge[tot] = z;
    nex[tot] = head[x], head[x] = tot;
}
int n,q,a[N];
/*父亲,深度,子树大小,重儿子,重路径顶部节点,
    树中节点在线段树中下标,线段树中节点对应树中位置*/
int par[N],deep[N],size[N],son[N],top[N],seg[N],rev[N];
int vis[N];
void dfs1(int x,int fa){
    /*利用深搜更新par,size,deep,son数组*/
    vis[x] = true; size[x] = 1;
    par[x] = fa; deep[x] = deep[fa]+1;
    for(int i = head[x];i ;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(y == fa || vis[y]) continue;
        dfs1(y,x);
        size[x] += size[y]; //累加子树大小
        if(size[y] > size[son[x]]) son[x] = y;//求重儿子
    }
}
void dfs2(int x,int fa){
    if(son[x]){ //先走重儿子,使得重路径在线段树中连续
        seg[son[x]] = ++seg[0];//0位置用不到,利用来计数
        top[son[x]] = top[x];
        rev[seg[0]] = son[x];
        dfs2(son[x],x);
    }
    for(int i = head[x];i;i = nex[i]){
        int y = ver[i], z = edge[i];
        if(top[y]) continue;
        /*若y没有被遍历过,即y不是x的重儿子或者父亲*/
        seg[y] = ++seg[0]; rev[seg[0]] = y;
        top[y] = y; dfs2(y,x);
        /*如果x-->y是轻边,那么y就是其所在重路径顶部节点*/
    }
}
struct SegmentTree{
    int l,r;
    ll mx, sum;
    #define l(x) t[x].l
    #define r(x) t[x].r
    #define mx(x) t[x].mx
    #define sum(x) t[x].sum
}t[N*4];
void BuildTree(int rt,int l,int r){
    l(rt) = l, r(rt) = r;
    if(l == r){
        mx(rt) = sum(rt) = a[rev[l]]; //线段树上l节点对应着树上rev[l]点
        return;
    }
    int mid = l+r>>1;
    BuildTree(rt*2,l,mid); BuildTree(rt*2+1,mid+1,r);
    mx(rt) = max(mx(rt<<1),mx(rt<<1|1));
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
void preHandle(){
    dfs1(1,0); //我们以1号节点为根
    /*根节点所在重路径的顶部节点也是根节点,赋初值*/
    seg[0] = seg[1] = top[1] = rev[1] = 1;
    dfs2(1,0);
    BuildTree(1,1,seg[0]);
}
ll tmx,tsum;//利用全局变量同时统计2个答案
void query(int rt,int l,int r){
    /*将以rt为根的区间内属于[l,r]部分的和累加到tsum上,并更新tmx*/
    if(l <= l(rt) && r(rt) <= r){
        tsum += sum(rt); tmx = max(tmx,mx(rt));
        return ;
    }
    int mid = l(rt)+r(rt)>>1;
    if(l <= mid) query(rt<<1,l,r);
    if(r > mid) query(rt<<1|1,l,r);
}
void ask(int x,int y){
    /*返回x与y之间路径上权值最大点的权值*/
    int fx = top[x] , fy = top[y];
    while(fx != fy){//先将x和y条整到同一个重链上
        if(deep[fx] < deep[fy]) swap(x,y),swap(fx,fy);
        query(1,seg[fx],seg[x]);
        x = par[fx]; fx = top[x];
    }
    if(deep[x] > deep[y]) swap(x,y);//路径浅的编号小
    query(1,seg[x],seg[y]); //再更新一次
}
void change(int rt,int x,int val){
    /*把线段树节点x的权值改为val*/
    if(l(rt) == r(rt)){
        mx(rt) = sum(rt) = val;
        return;
    }
    int mid = l(rt) + r(rt) >> 1;
    if(x > mid) change(rt<<1|1,x,val);
    else change(rt<<1,x,val);
    mx(rt) = max(mx(rt<<1),mx(rt<<1|1));
    sum(rt) = sum(rt<<1) + sum(rt<<1|1);
}
int main(){
    #ifdef LOCAL
        freopen("123.txt","r",stdin);
        freopen("222.txt","w",stdout);
    #endif
    scanf("%d",&n);
    for(int i = 1,x,y;i < n;i++){
        scanf("%d%d",&x,&y);
        addEdge(x,y,1); addEdge(y,x,1);
    }
    for(int i = 1;i <= n;i++) scanf("%d",a+i);
    scanf("%d",&q);
    preHandle();//树链剖分预处理
    char op[20];
    for(int i = 1,x,y;i <= q;i++){
        scanf("%s%d%d",op,&x,&y);
        if(op[0] == 'C') change(1,seg[x],y);
        else{
            tmx = -INF; tsum = 0;
            ask(x,y);//同时更新最大值与路径和
            if(op[1] == 'M') printf("%lld\n",tmx);
            else printf("%lld\n",tsum);
        }
    }
    return 0;
}


已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 程序猿惹谁了 设计师:白松林 返回首页