树链剖分 (应该还是比较好懂的)

前言

把九个分块做完了,终于开始了高级数据结构的学习.
树链剖分是最简单的高级数据结构,没有之一.

注:我这里高级数据结构的定义是模板难度在提高以上的数据结构.
为什么说树链剖分简单呢?因为你只要会dfs和线段树就可以学习了.
所以在这之前,你要先学会的是:
dfs序,线段树
那我默认你会了.我想点进来的人想必也是有一定了解的,因此我们就不多说了,直接进入游戏.

介绍

那么,树链剖分究竟是怎样的一种数据结构呢?
它是一种能让你代码量莫名其妙增加100行的数据结构.
这个梗被玩烂了.
它是一种能把树上的操作转化到链上的数据结构.
众所周知,如果树退化成链,里面的很多询问都会变得很简单.
比如说:询问最近公共祖先,你只需要比较两个节点的深度大小就可以了.
是不是?
以前我认为树链剖分是一个多高深的东西,现在看看不过如此.
首先看”树链剖分”4个字.很显然,这个可以把树剖成很多链状的结构,我们询问的时候就可以把几条链组合起来处理.
我们来看一个样例.最上面的节点是根.
这里写图片描述
接下来我们对它标号并剖成一些链.但是剖是有方法的,如果剖得不够优越,效率会非常低.
链上的边称为重边,它就像高速公路,一个结点到另一个结点经过一整条重链才花费1.
而链与链之间的边称为轻边,它就是农村的水泥路,经过一条轻边就要花费1.

这个时候我直接告诉你应该怎么开高速公路.
定义一个结点的重儿子是它的儿子中子树大小最大的一个儿子.
连接的时候某个结点所剖成的链中应当含有它的重儿子.如果有多个重儿子就随意选一个.

根据上面的说法我们就可以把上面的树剖成一些链了.重边我们用红色表示.
这样我们能够保证在该树的所有点对u,v中,每一对u->v的花费(重链和轻边的花费都是1)最少.
我不负责证明.
然后这棵树被分成了7条链.用绿色表示.

代码实现

看起来非常高深,但是实际上只要两个dfs,你就可以把树剖成一条一条的链了.神奇吧.

const int yuzu=1e5;//树的点数.
typedef int fuko[yuzu|10];//定义fuko这种类型是yuzu|10这么大的int数组,接下来代码会舒服很多,因为有大量数组出现.
vector<int> lj[yuzu|10];//vector代替邻接表
fuko fa,son,sz,dfn,top,ord,dep;int cnt;
/*
fa是节点的父亲,son是重儿子,sz是以该节点为根的子树大小,dfn是给节点重新编上的序号.
ord是与dfn相反的数组,表示标到的这个号表示的原节点,top是目前节点所在链的顶端.
dep是结点的深度,cnt是目前标到的号码.
可能数组多了一点,不过让我解释解释.
*/
void dfs1(int u,int f){//第一个dfs,目前搜索到的节点和它的父亲
sz[u]=1;               //不知道结点的子树有多大,目前只有它自己一个.
fa[u]=f;               //父亲节点赋值不用解释吧
dep[u]=dep[f]+1;       //这些都是老东西了.
for (int v:lj[u]) if (v^f){//v不是u的父亲
  dfs1(v,u);
  sz[u]+=sz[v];        //u节点的子树大小包括它的儿子
  if (sz[v]>sz[son[u]]) son[u]=v;
  /*v的子树大小比u的重儿子大,v就是重儿子.这样找出了u的重儿子.*/
  }
}

void dfs2(int u,int _top){//目前节点,它所在链的顶端
  top[u]=_top;            //top的赋值
  dfn[u]=++cnt;           //重新标号.
  ord[cnt]=u;             //表示目前标到的号是u.
  /*你可能会问为什么需要dfn和ord两个相反的数组,你马上就可以知道了*/
  if (son[u]) dfs2(son[u],_top);
  /*这一句是因为一条链上的点它们的top应该是相等的,而且它们的编号应该是连续的,这样比较好维护.*/ 
  for (int v:lj[u]){
    if (v^fa[u]&&v!=son[u]) dfs2(v,v);
    /*其他情况,v就应该是自己所在链的顶端,所以dfs(v,v)*/
    }
  }

以上是剖分.可是光剖分没有用啊,你要维护这些链来解题.下面就让我们使用这种方法来解决一些问题.

例题

这是树链剖分的裸题中比较简单的一道.
bzoj 1036 ZJOI2008 树的统计
树上每个节点有个权值,维护树上两点间的最大值,权值和,或者单点修改.
这样的结构以及最大值和区间和让我们不禁可以想到能够维护以上两值的数据结构:线段树.
在重新标号以后,同一条链上的节点的编号是连续的,这样就可以用线段树维护区间和和最大值.

/*
修改是单点的比较好搞定.
问题是如何维护两点间的权值和(最大值亦同理).
首先我们想如果这两个点在一条链上就好了.
那么我们可以不断地像求最近公共祖先一样往上跳.
每一次先修改一下从它链顶端到它的数据,然后跳到它链顶端的父亲这个地方.
两个点让谁来跳?我们比较一下两点的链顶端的深度,让深度大的那个先跳.
这样能保持两点深度平衡,不让其中一个跳到根节点以外.
*/
#include<bits/stdc++.h> //Ithea Myse Valgulious
namespace chtholly{
typedef long long ll;
#define re0 register int
#define rec register char
#define rel register ll
#define gc getchar
#define pc putchar
#define p32 pc(' ')
#define pl puts("")
/*By Citrus*/
inline int read(){
  re0 x=0,f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  return x*(f?1:-1);
  }
inline void read(rel &x){
  x=0;re0 f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  x*=f?1:-1;
  }
template <typename mitsuha>
inline int write(mitsuha x){
  if (!x) return pc(48);
  if (x<0) x=-x,pc('-');
  re0 bit[20],i,p=0;
  for (;x;x/=10) bit[++p]=x%10;
  for (i=p;i;--i) pc(bit[i]+48);
  }
inline char fuhao(){
  rec c=gc();
  for (;isspace(c);c=gc());
  return c;
  }
}using namespace chtholly;
using namespace std;
const int yuzu=3e4;
typedef int fuko[yuzu|10];
vector<int> lj[yuzu|10];
int n,m;

namespace shu_lian_pou_fen{
fuko fa,sz,son,dfn,dep,top,a,ord;int cnt;
/*由于bzoj不支持c++11,不能用:遍历vector.*/
void dfs1(int u,int f){
  fa[u]=f,sz[u]=1,dep[u]=dep[f]+1;
  for (int i=0;i<lj[u].size();++i){
    int v=lj[u][i];
    if (v^f){
      dfs1(v,u),sz[u]+=sz[v];
      if (sz[v]>sz[son[u]]) son[u]=v;
      }
    }
  }
void dfs2(int u,int _top){
  top[u]=_top,dfn[u]=++cnt,ord[cnt]=u; 
  if (son[u]) dfs2(son[u],_top); 
  for (int i=0;i<lj[u].size();++i){
    int v=lj[u][i];
    if (v^fa[u]&&v!=son[u]) dfs2(v,v);
    }
  }

typedef int yuki[yuzu<<2|13];
/*然后是单点修改,区间询问的线段树,这些都是基本了.不过我和当时那个写博客时候的我已经不是同一个人了.码风完全变了.*/
struct segtree{
#define le rt<<1
#define ri le|1
#define ls le,l,mid
#define rs ri,mid+1,r
yuki val,da;
void pushup(int rt){
  val[rt]=val[le]+val[ri];
  da[rt]=max(da[le],da[ri]);
  }
void build(int rt=1,int l=1,int r=n){
  /*有一个需要注意的地方就在这里.这里后面是ord[l].*/
  if (l==r) val[rt]=da[rt]=a[ord[l]];
  else{
    int mid=l+r>>1;
    build(ls),build(rs);
    pushup(rt);
    } 
  }
void update(int u,int v,int rt=1,int l=1,int r=n){
  if (l>u||r<u) return;
  if (l==r) val[rt]=da[rt]=v;
  else{
    int mid=l+r>>1;
    update(u,v,ls),update(u,v,rs);
    pushup(rt);
    }
  }
int q_max(int ql,int qr,int rt=1,int l=1,int r=n){
  if (ql>r||qr<l) return -30001;
  if (ql<=l&&qr>=r) return da[rt];
  int mid=l+r>>1;
  return max(q_max(ql,qr,ls),q_max(ql,qr,rs));
  }
int q_sum(int ql,int qr,int rt=1,int l=1,int r=n){
  if (ql>r||qr<l) return 0;
  if (ql<=l&&qr>=r) return val[rt];
  int mid=l+r>>1;
  return q_sum(ql,qr,ls)+q_sum(ql,qr,rs);
  }
}my_;

void update(int u,int v){my_.update(dfn[u],v);}
/*update的时候注意用的是dfn不是ord.*/

/*这里两个询问是一样的.*/
int q_max(int u,int v){
  int ans=-30001;
  for (;top[u]!=top[v];u=fa[top[u]]){
    if (dep[top[u]]<dep[top[v]]) swap(u,v);//top深度换一换.
    ans=max(ans,my_.q_max(dfn[top[u]],dfn[u]));//询问这条链.
    }
  if (dep[u]>dep[v]) swap(u,v);
  return max(ans,my_.q_max(dfn[u],dfn[v]));//最后两点终于在同一条链上了,直接询问.
  }

int q_sum(int u,int v){
  int ans=0;
  for (;top[u]!=top[v];u=fa[top[u]]){
    if (dep[top[u]]<dep[top[v]]) swap(u,v);
    ans+=my_.q_sum(dfn[top[u]],dfn[u]);
    }
  if (dep[u]>dep[v]) swap(u,v);
  return ans+my_.q_sum(dfn[u],dfn[v]);
  }

int main(){
  re0 i;
  n=read();
  for (i=1;i<n;++i){
    int u=read(),v=read();
    lj[u].push_back(v);
    lj[v].push_back(u);
    }
  for (i=1;i<=n;++i) a[i]=read();
  dfs1(1,0),dfs2(1,1);
  my_.build();
  char c[9];
  for (m=read();m--;){
    scanf("%s",c);
    int u=read(),v=read();
    if (c[1]=='M'){
      write(q_max(u,v)),pl;
      }
    else if (c[1]=='S'){
      write(q_sum(u,v)),pl;
      }
    else{
      update(u,v);
      }
    }
  }
}

int main(){
shu_lian_pou_fen::main();
}

洛谷的模板.
luogu p3384 [模板]树链剖分
这题就比较难了.它的操作非常的多.
我想两点之间的操作刚才你会了.
现在我们来研究一下一个结点的子树怎么修改.

/*
写这题的时候我的码风和现在不是很一样.你看头文件就知道这个namespace chtholly是老版的,
而且我定义的常量是karen.
对于一个结点的子树来说,其实比两点间好写多了.
根据dfs序,一个结点u和它的子树的所有点的标号是连续的.
而且u和u的子树的大小我们也已经知道了,就是sz[u].
所以更新的时候update(dfn[u],dfn[u]+sz[u]-1)就可以了.
*/
#include<bits/stdc++.h>
namespace chtholly{
typedef long long ll;
#define re0 register int
#define rec register char
#define rel register ll
#define gc getchar
#define pc putchar
#define p32 pc(' ')
#define pl puts("")
inline int read(){
  re0 x=0,f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  return x*(f?1:-1);
  }
inline void read(rel &x){
  x=0;re0 f=1;rec c=gc();
  for (;!isdigit(c);c=gc()) f^=c=='-';
  for (;isdigit(c);c=gc()) x=x*10+c-'0';
  x*=f?1:-1;
  }
inline int write(rel x){
  if (!x) return pc(48);
  if (x<0) x=-x,pc('-');
  re0 bit[20],i,p=0;
  for (;x;x/=10) bit[++p]=x%10;
  for (i=p;i;--i) pc(bit[i]+48);
  }
}
using namespace chtholly;
using namespace std;
const int karen=1e5;
typedef int fuko[karen|10];
int n=read(),m=read(),rt=read(),mod=read();

namespace shu_lian_pou_fen{
fuko sz,fa,son,top,dep,w,ord,a;
vector<int> lj[karen|10];
int cnt;
void dfs(int u){
  dep[u]=dep[fa[u]]+1,sz[u]=1;
  for (int v:lj[u]) if (v^fa[u]){
    fa[v]=u,dfs(v);
    sz[u]+=sz[v];
    if (!~son[u]||sz[v]>sz[son[u]]) son[u]=v;
    }
  }
void dfs2(int u){
  w[u]=++cnt,ord[cnt]=u;
  if (u==son[fa[u]]) top[u]=top[fa[u]];
  else top[u]=u;
  if (~son[u]) dfs2(son[u]);
  for (int v:lj[u]){
    if (v^fa[u]&&v!=son[u]) dfs2(v);
    }
  }

namespace segtree{
#define le rt<<1
#define ri le|1
#define ls le,l,mid
#define rs ri,mid+1,r
#define mo(u) if (u>=mod) u%=mod 
int val[karen<<2],lazy[karen<<2];
void pushup(int rt){
  val[rt]=val[le]+val[ri];
  mo(val[rt]);
  }
void build(int rt,int l,int r){
  if (l==r) val[rt]=a[ord[l]];
  else{
    int mid=l+r>>1;
    build(ls),build(rs);
    pushup(rt);
    }
  }
void push_down(int rt,int l,int r){
  if (lazy[rt]){
    int mid=l+r>>1;
    lazy[le]+=lazy[rt];
    lazy[ri]+=lazy[rt];
    mo(lazy[le]);mo(lazy[ri]);
    val[le]+=(mid-l+1)*lazy[rt];
    val[ri]+=(r-mid)*lazy[rt];
    mo(val[le]);mo(val[ri]);
    lazy[rt]=0;
    }
  }
void update(int rt,int l,int r,int ql,int qr,int k){
  if (l>=ql&&r<=qr){
    lazy[rt]+=k;
    val[rt]+=(r-l+1)*k;
    mo(val[rt]);
    }
  else{
    int mid=l+r>>1;
    push_down(rt,l,r);
    if (ql<=mid) update(ls,ql,qr,k);
    if (qr>mid) update(rs,ql,qr,k);
    pushup(rt);
    }
  }
int query(int rt,int l,int r,int ql,int qr){
  if (l>=ql&&r<=qr) return val[rt];
  int mid=l+r>>1,res=0;
  push_down(rt,l,r);
  if (ql<=mid) res+=query(ls,ql,qr);
  mo(res);
  if (qr>mid) res+=query(rs,ql,qr);
  return res>=mod?res%mod:res;
  }
}
using namespace segtree;

void upd_path(int l,int r,int k){
  for (;top[l]!=top[r];l=fa[top[l]]){
    if (dep[top[l]]<dep[top[r]]) swap(l,r);
    update(1,1,n,w[top[l]],w[l],k);
    }
  if (dep[l]>dep[r]) swap(l,r);
  update(1,1,n,w[l],w[r],k);
  }
int query_path(int l,int r){
  int val=0;
  for (;top[l]!=top[r];l=fa[top[l]]){
    if (dep[top[l]]<dep[top[r]]) swap(l,r);
    val+=query(1,1,n,w[top[l]],w[l]);
    mo(val);
    }   
  if (dep[l]>dep[r]) swap(l,r);
  return (val+query(1,1,n,w[l],w[r]))%mod;
  }
int main(){
  re0 i;
  memset(son,-1,sizeof son);
  for (i=1;i<=n;++i) a[i]=read();
  for (i=1;i<n;++i) {
    re0 u=read(),v=read();
    lj[u].push_back(v);
    lj[v].push_back(u);
    }
  dfs(rt),dfs2(rt);
  build(1,1,n);
  re0 t,u,v,k;
  for (;m--;){
    t=read(),u=read();
    if (t==1) v=read(),k=read(),upd_path(u,v,k);
    if (t==2) v=read(),write(query_path(u,v)),pl;
    if (t==3) v=read(),update(1,1,n,w[u],w[u]+sz[u]-1,v);
    if (t==4) write(query(1,1,n,w[u],w[u]+sz[u]-1)),pl;
    }
  }
}

int main()
{
shu_lian_pou_fen::main();
}

让我再去刷几道题.我就先讲到这里了.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值