【数据结构】树链剖分

百度百科

Definition

在处理树上的链上修改与询问问题时,如果朴素地采用LCA的手段,那么询问的复杂度是\(O(logn)\),但是修改的复杂度会成为朴素地\(O(n)\),这在大部分题目中是难以接受的。用于处理树上两点间简单路径上权值和与单点子树权值和的修改以及其查询问题的数据结构与处理方法,被叫做树链剖分。

Solution

考虑在一位的数列上做区间查询与修改,可以使用线段树做到每次修改\(O(logn)\)的复杂度。那么考虑在树上能否使用线段树。最简单的思想是给每个节点一个DFS序,即在DFS时给每个点打上时间戳,那么可以将时间戳对应到线段树的区间上,对其进行操作。但是考虑时间戳是散乱的,对于树上的长度为\(n\)一段链,他们的dfs序可能会最多对应\(n\)个区间。对每个区间进行暴力查询与修改的复杂度会达到每次\(O(nlogn)\),对于一个有\(n\)次操作的问题,复杂度会高达\(O(n^2logn)\),这也是无法接受的。那么考虑减少链上对应的区间。可以使用一种方法将区间的个数减小至与\(logn\)同阶,这样的方法就是树链剖分。

下面给出一些定义:

重节点:在一个节点所有儿子中,子树最大的儿子是它的重儿子,这个儿子是一个重节点
轻节点:所有不是重节点的节点是轻节点。
重边:链接两个重节点的边是重边。
重链:将相邻的重边连起来可以形成重链。
规定所有的重链以轻节点为开头。
链:将所有不是重链的边可相连的相连,形成的集合与重链的集合的并集是一棵树上链的集合。
特别的,对于叶子节点,有一条起始于该节点结束与该节点的链。
链头:一个点是链头当且仅当他在自身所在的链上是深度最低的点
链的深度:链的深度指链上深度最低的点(链头)的深度。

考虑如下性质:

设\(size(u)\)为\(u\)的子树大小。那么对于一个节点的重儿子\(u\)和轻儿子\(v\),一定满足
\(size(v)~\leq~size(u)\)。

根据上面的性质可以证明:

对于一个点到根节点的简单路径中,重链条数不超过\(logn\)条,轻链条数不超过\(logn\)条。
那么考虑根据这个性质,如果将每条链上的时间戳作为定为连续的,那么分区间查询与修改的次数与\(logn\)同阶。加上线段树的复杂度,每次操作的复杂度会成为\(O(log^2n)\),对于操作次数与\(n\)同阶的题目,操作的题目的总复杂度会降低至\(O(nlog^2n)\)。

考虑实现

首先显然可以通过一次dfs处理处每个点的子树大小,以及重儿子。同时习惯上我在第一次dfs时递推出每个点的深度和父节点,以备查询应用。

void DFS(ci u,ci ft) {
    sz[u]=1;
    deepth[u]=deepth[ft]+1;
    fa[u]=ft;
    for(rg int i=hd[u];i;i=edge[i].nxt) {
        int &to=edge[i].to;
        if(sz[to]) continue;
        DFS(to,u);sz[u]+=sz[to];
        if(sz[to]>sz[son[u]]) son[u]=to;
    }
}

然后考虑对每个点打上时间戳。同一条链上时间戳一定是需要连续的。所以对于每个点先向他的重儿子递归打上时间戳,然后递归轻儿子。这样保证了对于每条链的时间戳都是连续的。下面使用\(top\)数组记录每个节点所在链的链头(即深度最低的节点)的节点编号。使用\(dfn\)数组记录每个点的时间戳。这样在查询的时候查询对应节点\(u\)到链头的区间就是\([dfn_{top_u},dfn_u]\)。同时使用\(remap\)数组将时间戳映射回对应的节点。

void dfs(ci u,ci tp) {
    if(!u) return;
    top[u]=tp;
    dfn[u]=++vistime;
    remap[vistime]=u;
    dfs(son[u],tp);
    for(rg int i=hd[u];i;i=edge[i].nxt) {
        int &to=edge[i].to;
        if(dfn[to]) continue;
        if(to == son[u]) continue;
        dfs(to,to);
    }
}

考虑查询。
对于两点不在一条链上的情况,显然他们的公共祖先在他们深度较深的链上方。那么可以直接查询该节点到链头,然后跳到链头的父节点。在同一个链时,直接在链上查询即可。
具体代码如下:

int ask_l(int a,int b) {
    int _ans=0;
    while(top[a] != top[b]) {
        if(deepth[top[a]] < deepth[top[b]]) mswap(a,b);
        _ans=(0ll+_ans+ask(1,n,1,dfn[top[a]],dfn[a]))%MOD;
        a=fa[top[a]];
    }
    if(deepth[a] < deepth[b]) mswap(a,b);
    _ans=(0ll+_ans+ask(1,n,1,dfn[b],dfn[a]))%MOD;
    return _ans;
}

考虑对子树的操作:
因为一棵树的子树显然编号是连续的。所以以\(u\)为根的子树在区间上对应的区间是\(dfn[u],dfn[u]+sz[u]-1\)。修改与查询次数都收\(O(1)\)。
这样,对一棵树进行剖分,进行链上信息查询的复杂度就被降低为\(O(mlog^2n)\),其中\(m\)代表操作次数。

Example

P3384 【模板】树链剖分

Description

如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:

操作1: 格式: 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z

操作2: 格式: 2 x y 表示求树从x到y结点最短路径上所有节点的值之和

操作3: 格式: 3 x z 表示将以x为根节点的子树内所有节点值都加上z

操作4: 格式: 4 x 表示求以x为根节点的子树内所有节点值之和

Input

第一行包含4个正整数N、M、R、P,分别表示树的结点个数、操作个数、根节点序号和取模数(即所有的输出结果均对此取模)。

接下来一行包含N个非负整数,分别依次表示各个节点上初始的数值。

接下来N-1行每行包含两个整数x、y,表示点x和点y之间连有一条边(保证无环且连通)

接下来M行每行包含若干个正整数,每行表示一个操作,格式如下:

操作1: 1 x y z

操作2: 2 x y

操作3: 3 x z

操作4: 4 x

Output

输出包含若干行,分别依次表示每个操作2或操作4所得的结果(对P取模)

Sample Input

5 5 2 24
7 3 7 8 0
1 2
1 5
3 1
4 1
3 4 2
3 2 2
4 5
1 5 1 3
2 1 3

Sample Output

2
21

Hint

对于100%的数据: $ N \leq {10}^5, M \leq {10}^5 $

其实,纯随机生成的树LCA+暴力是能过的,可是,你觉得可能是纯随机的么233

Solution

板子题要啥solution

Code

#include<cstdio>
#define rg register
#define ci const int
#define cl const long long int

typedef long long int ll;

namespace IO {
    char buf[90];
}

template<typename T>
inline void qr(T &x) {
    char ch=getchar(),lst=' ';
    while(ch>'9'||ch<'0') lst=ch,ch=getchar();
    while(ch>='0'&&ch<='9') x=(x<<1)+(x<<3)+(ch^48),ch=getchar();
    if(lst=='-') x=-x;
}

template<typename T>
inline void write(T x,const char aft,const bool pt) {
    if(x<0) x=-x,putchar('-');
    int top=0;
    do {
        IO::buf[++top]=x%10+'0';
        x/=10;
    } while(x);
    while(top) putchar(IO::buf[top--]);
    if(pt) putchar(aft);
}

template<typename T>
inline T mmax(const T a,const T b) {if(a>b) return a;return b;}
template<typename T>
inline T mmin(const T a,const T b) {if(a<b) return a;return b;}
template<typename T>
inline T mabs(const T a) {if(a<0) return -a;return a;}

template<typename T>
inline void mswap(T &a,T &b) {
    T temp=a;a=b;b=temp;
}

const int maxn = 100010;
const int maxm = 200010;
const int maxt = 400010;

struct Edge {
    int to,nxt;
};
Edge edge[maxm];int hd[maxn],ecnt;
inline void cont(ci from,ci to) {
    Edge &e=edge[++ecnt];
    e.to=to;e.nxt=hd[from];hd[from]=ecnt;
}

int n,m,r,MOD,a,b,c,vistime;
int MU[maxn],sz[maxn],son[maxn],dfn[maxn],remap[maxn],fa[maxn],top[maxn],deepth[maxn],frog[maxt],lazy[maxt];

void DFS(ci,ci);
void dfs(ci,ci);
int ask_t(ci);
void change_t(ci,ci);
int ask_l(int,int);
void build(ci,ci,ci);
void Free(ci,ci,ci,ci,ci,ci);
void change_l(int,int,ci);
int ask(ci,ci,ci,ci,ci);
void change(ci,ci,ci,ci,ci,ci);

int main() {
    qr(n);qr(m);qr(r);qr(MOD);deepth[0]=-1;
    for(rg int i=1;i<=n;++i) qr(MU[i]);
    for(rg int i=1;i<n;++i) {
        a=b=0;qr(a);qr(b);
        cont(a,b);cont(b,a);
    }
    DFS(r,0);dfs(r,r);
    build(1,n,1);
    while(m--) {
        a=0;qr(a);
        switch(a) {
            case 1:
                a=b=c=0;qr(a);qr(b);qr(c);
                change_l(a,b,c);break;
            case 2:
                a=b=0;qr(a);qr(b);write(ask_l(a,b),'\n',true);
                break;
            case 3:
                a=b=0;qr(a);qr(b);
                change_t(a,b);break;
            case 4:
                a=0;qr(a);
                write(ask_t(a),'\n',true);
                break;
        }
    }
    return 0;
}

void DFS(ci u,ci ft) {
    sz[u]=1;
    deepth[u]=deepth[ft]+1;
    fa[u]=ft;
    for(rg int i=hd[u];i;i=edge[i].nxt) {
        int &to=edge[i].to;
        if(sz[to]) continue;
        DFS(to,u);sz[u]+=sz[to];
        if(sz[to]>sz[son[u]]) son[u]=to;
    }
}

void dfs(ci u,ci tp) {
    if(!u) return;
    top[u]=tp;
    dfn[u]=++vistime;
    remap[vistime]=u;
    dfs(son[u],tp);
    for(rg int i=hd[u];i;i=edge[i].nxt) {
        int &to=edge[i].to;
        if(dfn[to]) continue;
        if(to == son[u]) continue;
        dfs(to,to);
    }
}

void build(ci l,ci r,ci p) {
    if(l > r) return;
    if(l == r) {frog[p]=MU[remap[l]];return;}
    int mid=(l+r)>>1,dp=p<<1,ddp=dp|1;
    build(l,mid,dp);build(mid+1,r,ddp);
    frog[p]=(frog[dp]+frog[ddp])%MOD;
}

int ask_l(int a,int b) {
    int _ans=0;
    while(top[a] != top[b]) {
        if(deepth[top[a]] < deepth[top[b]]) mswap(a,b);
        _ans=(0ll+_ans+ask(1,n,1,dfn[top[a]],dfn[a]))%MOD;
        a=fa[top[a]];
    }
    if(deepth[a] < deepth[b]) mswap(a,b);
    _ans=(0ll+_ans+ask(1,n,1,dfn[b],dfn[a]))%MOD;
    return _ans;
}

inline int ask_t(ci u) {
    int r=dfn[u]+sz[u]-1;
    return ask(1,n,1,dfn[u],r);
}

void change_l(int a,int b,ci v) {
    while(top[a] != top[b]) {
        if(deepth[top[a]] < deepth[top[b]]) mswap(a,b);
        change(1,n,1,dfn[top[a]],dfn[a],v);
        a=fa[top[a]];
    }
    if(deepth[a] < deepth[b]) mswap(a,b);
    change(1,n,1,dfn[b],dfn[a],v);
}

void change_t(ci u,ci v) {
    int r=dfn[u]+sz[u]-1;
    change(1,n,1,dfn[u],r,v);
}

void change(ci l,ci r,ci p,ci aiml,ci aimr,ci v) {
    if(l > r) return;
    if((l > aimr) || (r < aiml)) return;
    if((l >= aiml) && (r <= aimr)) {lazy[p]+=v;frog[p]=((1ll*(r-l+1)%MOD)*v+frog[p])%MOD;return;}
    int mid=(l+r)>>1,dp=p<<1,ddp=dp|1;
    Free(l,r,mid,p,dp,ddp);
    change(l,mid,dp,aiml,aimr,v);change(mid+1,r,ddp,aiml,aimr,v);
    frog[p]=(frog[dp]+frog[ddp])%MOD;
}

void Free(ci l,ci r,ci mid,ci p,ci dp,ci ddp) {
    frog[dp]=(1ll*(mid-l+1)%MOD*lazy[p]+frog[dp])%MOD;
    frog[ddp]=(1ll*(r-mid)%MOD*lazy[p]+frog[ddp])%MOD;
    lazy[dp]=(lazy[dp]+lazy[p])%MOD;
    lazy[ddp]=(lazy[ddp]+lazy[p])%MOD;
    lazy[p]=0;
}

int ask(ci l,ci r,ci p,ci aiml,ci aimr) {
    if(l > r) return 0;
    if((l > aimr) || (r < aiml)) return 0;
    if((l >= aiml) && (r <= aimr)) return frog[p];
    int mid=(l+r)>>1,dp=p<<1,ddp=dp|1;
    Free(l,r,mid,p,dp,ddp);
    return (ask(l,mid,dp,aiml,aimr)+ask(mid+1,r,ddp,aiml,aimr))%MOD;
}

原文地址:https://www.cnblogs.com/yifusuyi/p/9583033.html

时间: 2024-10-10 22:12:03

【数据结构】树链剖分的相关文章

数据结构&#183;树链剖分+LCT

于是两个一起搞了... 怎么说,写的是P党风格的C++,短也不会短到哪里去,跑起来也不快,常数大成狗OTL BZOJ 1036 树链的经典题吧,点修改+路经询问 [Code] BZOJ 2243 路径修改+路径询问 [Code] BZOJ 3083 路径修改+子树询问(根可变),这道题要用树链剖分求DFS序中某一段区间的值(DFS序可查子树,链剖可修改路径,两者相结合就行了) [Code] BZOJ 2049 用LCT维护森林形态 [Code] BZOJ 2631 路径修改+路径询问+形态可变

数据结构(树链剖分):BZOJ 4034: [HAOI2015]T2

Description 有一棵点数为 N 的树,以点 1 为根,且树点有边权.然后有 M 个 操作,分为三种: 操作 1 :把某个节点 x 的点权增加 a . 操作 2 :把某个节点 x 为根的子树中所有点的点权都增加 a . 操作 3 :询问某个节点 x 到根的路径中所有点的点权和. Input 第一行包含两个整数 N, M .表示点数和操作数. 接下来一行 N 个整数,表示树中节点的初始权值. 接下来 N-1 行每行三个正整数 fr, to , 表示该树中存在一条边 (fr, to) . 再

数据结构(树链剖分):COGS 2109. [NOIP2015] 运输计划

2109. [NOIP2015] 运输计划 ★★★   输入文件:transport.in   输出文件:transport.out   简单对比时间限制:1 s   内存限制:256 MB [题目描述] 公元 2044 年,人类进入了宇宙纪元. L 国有 n 个星球,还有 n-1 条双向航道,每条航道建立在两个星球之间,这 n-1 条航道连通了 L 国的所有星球. 小 P 掌管一家物流公司,该公司有很多个运输计划,每个运输计划形如:有一艘物 流飞船需要从 ui 号星球沿最快的宇航路径飞行到 v

数据结构(树链剖分,堆):HNOI 2016 network

2215. [HNOI2016]网络 ★★★☆   输入文件:network_tenderRun.in   输出文件:network_tenderRun.out   简单对比时间限制:2 s   内存限制:128 MB [题目描述] [输入格式] [输出格式] [样例输入1] 13 23 1 2 1 3 2 4 2 5 3 6 3 7 4 8 4 9 6 10 6 11 7 12 7 13 2 1 0 8 13 3 0 9 12 5 2 9 2 8 2 2 0 10 12 1 2 2 1 3 2

浅析树链剖分Orz

本文思路参考自何开大佬 引子 相信各位大佬一定会线段树这种非常实用的数据结构 那么如果我们要维护一棵树上的链的权值的时候怎么办 就比如说BZOJ1036树的统计这道题目 可能诸位草率地想想线段树是可以口头AC的,But 这是在一棵树上,线段树支持的连续的区间操作 在这棵树上,如果链的编号断断续续,那么我们的线段树就和暴力没有什么区别有一点点区别了 概念 所以这里就需要用到树链剖分,这种可以支持树上链操作的数据结构 树链剖分有很多高大上的名词需要我们去记 我们先定义一些概念东东 size[u]表示

数据结构之树链剖分

首先了解一下基本概念: 重儿子:siz[u]为v的子节点中siz值最大的,那么u就是v的重儿子.      轻儿子:v的其它子节点.      重边:点v与其重儿子的连边.      轻边:点v与其轻儿子的连边.      重链:由重边连成的路径.      轻链:轻边. 剖分后的树有如下性质:      性质1:如果(v,u)为轻边,则siz[u] * 2 < siz[v]:      性质2:从根到某一点的路径上轻链.重链的个数都不大于logN. 树链剖分,如其字面意思,就是将一棵树按照轻重

数据结构(并查集||树链剖分):HEOI 2016 tree

[注意事项] 为了体现增强版,题目限制和数据范围有所增强: 时间限制:1.5s 内存限制:128MB 对于15% 的数据,1<=N,Q<=1000. 对于35% 的数据,1<=N,Q<=10000. 对于50% 的数据,1<=N,Q<=100000,且数据均为官方数据. 对于100% 的数据,1<=N,Q<=1000000. 请注意常数因子对于程序运行的影响. 并查集很简单,并查集就是倒序处理,表示删除一个点的标记,删除后不会再加回来,删完后,合并当前点与其

【数据结构】——树链剖分

树链剖分——简单而强大的数据维护方法 只是放个板子而已. 用我的码风覆盖了的. 1 #include<bits/stdc++.h> 2 using namespace std; 3 //------------------------------------------------------ 4 inline int read(){ 5 int f=1,x=0; 6 char c=getchar(); 7 while(!isdigit(c)){ 8 if(c=='-') f=-1; 9 c=

树链剖分简(单)介(绍)

树链剖分可以算是一种数据结构(一大堆数组,按照这个意思,主席树就是一大堆线段树).将一棵树分割成许多条连续的树链,方便完成一下问题: 单点修改(dfs序可以完成) 求LCA(各种乱搞也可以) 树链修改(修改任意树上两点之间的唯一路径) 树链查询 (各种操作)  前两个内容可以用其他方式解决,但是下面两种操作倍增.st表,dfs序就很难解决(解决当然可以解决,只是耗时长点而已).下面开始步入正题. 树链剖分的主要目的是分割树,使它成一条链,然后交给其他数据结构(如线段树,Splay)来进行维护.常