树链剖分学习小记

前言

很早以前就听说过这个算法,但是一直没怎么学习过,这一次好不容易有机会学习到了这个算法。
其实这个算法还是很简单的,只要好好学,十分钟不到就能学明白。

简介

树链剖分,又叫轻重链剖分,是一种对树进行划分的方法。
被划分后的每个点只会属于一条链上。
这有什么作用呢?
这就相当于把一个树上问题映射到了区间上,变成了一个在一个序列上的问题。
树链剖分往往和树状数组、线段树一起使用。
下面通过引入一道例题来讲解这个算法。

例题

【ZJOI2008】树的统计

Description

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

Input

  输入文件的第一行为一个整数n,表示节点的个数。
  接下来n – 1行,每行2个整数a和b,表示节点a和节点b之间有一条边相连。
  接下来n行,每行一个整数,第i行的整数wi表示节点i的权值。
  接下来1行,为一个整数q,表示操作的总数。
  接下来q行,每行一个操作,以“CHANGE u t”或者“QMAX u v”或者“QSUM u v”的形式给出。

Output

  对于每个“QMAX”或者“QSUM”的操作,每行输出一个整数表示要求输出的结果。

Sample Input

4
1 2
2 3
4 1
4 2 1 3
12
QMAX 3 4
QMAX 3 3
QMAX 3 2
QMAX 2 3
QSUM 3 4
QSUM 2 1
CHANGE 1 5
QMAX 3 4
CHANGE 3 6
QMAX 3 4
QMAX 2 4
QSUM 3 4

Sample Output

4
1
2
2
10
6
5
6
5
16

Data Constraint

【数据说明】
  对于100%的数据,保证1<=n<=30000,0<=q<=200000;中途操作中保证每个节点的权值w在-30000到30000之间。

分析

如果这是一个区间问题的话,那就是个大水题了,直接用线段树乱搞一下就可以了。
然而这是在树上的,单单使用线段树显得有些乏力,那么我们就使用树链剖分把他转化为区间问题。
下面开始讲解树链剖分。

讲解

先定义一个概念。
重儿子——就是一个结点子结点最多的儿子,连向儿子的边称为重边
轻儿子——即除了重儿子以外的儿子,连向轻儿子的边称为轻边
因此,树剖又叫轻重链剖分
该算法的核心即是:把重儿子与父亲划分到一条链上,轻儿子则自己单独开辟一条新的链。
下面来讲讲该算法的实现。
谈一谈数组的设置。
top[x]表示x结点所在的链的开始结点
s[x],即size[x]的缩写,表示x结点的子节点的个数
h[x],即x结点的深度
fa[x],即x的父亲结点
tr[x]即重儿子优先得到的dfs序
pr[x]即tr[x]的反函数
son[x]即x的重儿子
我们可以通过两遍dfs来算出这些数组

void dfs1(int x){
    s[x]=1;
    int i,j;
    for(i=he[x];i;i=nx[i]){
        j=b[i];
        if (j==fa[x]) continue;
        d[j]=d[x]+1;
        fa[j]=x;
        dfs1(j);
        s[x]+=s[j];
        if (!son[x]||s[son[x]]<s[j]) son[x]=j;}
}
void dfs2(int x,int y){
    int i;
    top[pre[tr[x]=++num]=x]=y;
    if (!son[x]) return;
    dfs2(son[x],y);
    for(i=he[x];i;i=nx[i])
      if (b[i]!=fa[x]&&b[i]!=son[x]) dfs2(b[i],b[i]);
}

这个代码应该非常好理解
有了这个之后,上面的例题在套用线段树应该就可以得到解决了吧

有些毒瘤题比较恶心,用dfs很可能会爆栈,不想打人工栈的同学可以打bfs
下面提供一个bfs版本的树剖
变量名和dfs版本略有出入

void bfs(){
    int l,r,i,j,t,res;
    l=0,r=1,q[r]=1;
    while (l<r){
        x=q[++l];
        for(i=he[x];i;i=nx[i]){
            j=b[i];
            if (j==fa[x]) continue;
            fa[j]=x; h[j]=h[x]+1;
            q[++r]=j;}
    }
    fo(i,1,n) top[i]=i,s[i]=1;
    fa[1]=0;
    fd(i,n,1) s[fa[q[i]]]+=s[q[i]];
    dfn[1]=1; s[0]=0;
    fo(i,1,n) {
        x=q[i],t=0,res=dfn[x];
        for(j=he[x];j;j=nx[j]) if (s[b[j]]>s[t]&&b[j]!=fa[x]) t=b[j]; 
        if (t) son[x]=t,top[t]=top[x],dfn[t]=res+1,res+=s[t];
        for(j=he[x];j;j=nx[j]) 
          if (b[j]!=t&&b[j]!=fa[x]) dfn[b[j]]=res+1,res+=s[b[j]];}
    fo(i,1,n) seq[dfn[i]]=i;
}

dfn数组相当于dfs版本的tr数组
seq数组相当于dfs版本的pre数组

最近树剖的题目做了不少,有时间的话会把博客写出来和大家一起分享
Enjoy coding life!

下面还有一道板子题

jzoj4604 树

题意就是给定一棵树,给出两种操作
一种是给树中的某个点打标记
另一种是询问
询问离某个点最近的打了标记的点(一开始只有一号点打了标记)

Solution

很裸的树剖
可以用来练练熟练度和码力

Code

#include <cstdio>
#include <cstring>
#include <algorithm>
#define fo(i,a,b) for(i=a;i<=b;i++)
#define fd(i,a,b) for(i=a;i>=b;i--)
#define L rt<<1
#define R (rt<<1)+1 
using namespace std;
const int N=100025;
int n,i,x,y,tot,num,seq[N],tr[N<<2],qq; char ss[20];
int nx[N<<1],b[N<<1],he[N],s[N],dfn[N],top[N],h[N],fa[N],son[N],q[N];
int read(){
    int sum=0;
    char c=getchar();
    while (c<'0'||c>'9') c=getchar();
    while (c>='0'&&c<='9'){
        sum=sum*10+c-'0';
        c=getchar();}
    return sum;
}
inline void add(int x,int y){
    nx[++tot]=he[x];he[x]=tot;b[tot]=y;
}
void bfs(){
    int l,r,i,j,t,res;
    l=0,r=1,q[r]=1;
    while (l<r){
        x=q[++l];
        for(i=he[x];i;i=nx[i]){
            j=b[i];
            if (j==fa[x]) continue;
            fa[j]=x; h[j]=h[x]+1;
            q[++r]=j;}
    }
    fo(i,1,n) top[i]=i,s[i]=1;
    fa[1]=0;
    fd(i,n,1) s[fa[q[i]]]+=s[q[i]];
    dfn[1]=1; s[0]=0;
    fo(i,1,n) {
        x=q[i],t=0,res=dfn[x];
        for(j=he[x];j;j=nx[j]) if (s[b[j]]>s[t]&&b[j]!=fa[x]) t=b[j]; 
        if (t) son[x]=t,top[t]=top[x],dfn[t]=res+1,res+=s[t];
        for(j=he[x];j;j=nx[j]) 
          if (b[j]!=t&&b[j]!=fa[x]) dfn[b[j]]=res+1,res+=s[b[j]];}
    fo(i,1,n) seq[dfn[i]]=i;
}
inline void update(int rt){ tr[rt]=max(tr[L],tr[R]);}
inline void change(int rt,int l,int r,int x){
    if (l==r) {
        tr[rt]=l;
        return;}
    int mid=(l+r)>>1;
    if (x<=mid) change(L,l,mid,x);
      else change(R,mid+1,r,x);
    update(rt);
}
inline int query(int rt,int l,int r,int x,int y){
    if (l==x&&r==y) return tr[rt];
    int mid=(l+r)>>1;
    if (y<=mid) return query(L,l,mid,x,y);
      else if (x>mid) return query(R,mid+1,r,x,y);
        else return max(query(L,l,mid,x,mid),query(R,mid+1,r,mid+1,y)); 
}
inline void lca(int x){
    int ans=0;
    while (x>=1&&ans==0) {
        int f1=top[x];
        ans=query(1,1,n,dfn[f1],dfn[x]);
        x=fa[f1];}
    printf("%d\n",seq[ans]);
}
int main(){
//  freopen("1.in","r",stdin);
//  freopen("1.out","w",stdout);
    n=read(); qq=read();
    fo(i,2,n) add(x=read(),y=read()),add(y,x);
    s[1]=0,fa[1]=1;
    bfs();
    fa[1]=1;
    change(1,1,n,1);
    while (qq){
        qq--;
        scanf("%s",ss+1); x=read();
        if (ss[1]=='C') change(1,1,n,dfn[x]);
          else lca(x);  
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值