[学习笔记]树链剖分

树链剖分真的不难,基础够了,简直秒懂

1、前置知识

线段树、树状数组等,dfs序,lca

2、引入

首先先放树剖模板题
四个操作:

  • 1 x y z 表示将树从x到y结点最短路径上所有节点的值都加上z
  • 2 x y 表示求树从x到y结点最短路径上所有节点的值之和
  • 3 x z 表示将以x为根节点的子树内所有节点值都加上z
  • 4 x 表示求以x为根节点的子树内所有节点值之和

先倒退一下难度,如果只有询问呢?是不是树上倍增就可以了
但是加上修改呢?st数组就没能力了,所以我们需要一种更优的算法来解决
那就是,树链剖分

引入概念

重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
轻儿子:父亲节点中除了重儿子以外的儿子;
重边:父亲结点和重儿子连成的边;
轻边:父亲节点和轻儿子连成的边;
重链:由多条重边连接而成的路径;
轻链:由多条轻边连接而成的路径;

如图,黄色为重儿子,红色为重边
下图中,
重链为:3->20;4->12;11->11
轻链为:1->2;7->7;5->5;9->9;15->15;16->16;19->19

这样一颗树,就是树链剖分的树

那么,怎么弄出这样一颗树呢

3、变量

有一些经常使用的数组

  • size[]数组,用来保存以x为根的子树节点个数
  • son[]数组,用来保存重儿子
  • fa[]数组,用来保存当前节点的父亲
  • d[]数组,用来保存当前节点的深度
  • id[]数组,用来保存树中每个节点dfs序中的新编号
  • top[]数组,用来保存当前节点的所在链的顶端节点
  • rk[]数组,用来保存当前节点在线段树中的位置

4、dfs

重点,树剖必备dfs,跑两遍

  • 第一遍,处理出size,son,d,fa
void dfs1(int u, int pre){//pre记录父亲
    d[u] = d[pre] + 1, fa[u] = pre;//处理d、fa数组
    size[u] = 1, son[u] = -1;//初始只有一个点,没有重儿子
    for (int i = head[u]; i; i = edge[i].next){//枚举儿子
        int v = edge[i].to;
        if (v != pre){
            dfs1(v, u);
            size[u] += size[v];//统计size
            if (son[u] == -1 || son[u] != -1 && size[v] > size[son[u]]) son[u] = v;//更新son数组
        }
    }
}
  • 第二遍,处理出id,top,rk数组
void dfs2(int u, int x){//x为链上顶端点
    id[u] = ++cnt, wt[cnt] = w[u] % qy, top[u] = x;//id数组dfs序,这里用wt数组存新编号对应原来的点的值
    if (son[u] == -1) return;//没有重儿子,退出
    dfs2(son[u], x);//首先遍历重儿子
    for (int i = head[u]; i; i = edge[i].next){
        int v = edge[i].to;
        if (v != fa[u] && v != son[u]) dfs2(v, v);//轻儿子所在的链的顶端是自己
    }
}

5、用树剖求lca

可以用树剖求lca,单纯的树剖求lca比树上倍增快得多
具体采取跳重链的做法
点x,y,求lca(x,y)

  • 若x、y不在同一条重链上,把两个点中top的深度更深的那个点往上跳,跳过它属于的那条链,跳到它属于的那条链的顶端的父亲
    不断跳链,直到两个点属于同一条重链为止
  • 若x、y属于同一条重链,那么lca就是两个点中深度更浅的那个

具体Code(手码):

while (top[x] != top[y]){//两点不在同一条重链上
	if (d[top[x]] < d[top[y]]) swap(x, y);//x为top的深度更深的那个点
	x = fa[top[x]];//x跳到它属于的那条链的顶端的父亲
}
lca = d[x] < d[y] ? x : y;//lca就是两个点中深度更浅的那个

6、维护数据

我们用数据结构来维护数据
这里用线段树

假如维护两点最短路径上的点进行区间加
仿照树剖求lca的做法
点跳链之前,先把该点到该点属于的重链的顶端那一条小路径的值区间加,然后再跳链

Code:

void update_range(int x, int y, int delta){
    delta %= qy;
    while (top[x] != top[y]){
        if (d[top[x]] < d[top[y]]) swap(x, y);
        update(1, id[top[x]], id[x], delta);
        //至于为什么可以直接进行区间维护,这就和dfs2有关了,
        //因为我们总是优先遍历重儿子,所以一条重链上的点的dfs序(id数组)一定是连续的
        x = fa[top[x]];
    }
    if (d[x] < d[y]) swap(x, y);
    update(1, id[y], id[x], delta);
}

那么怎么维护子树的区间加呢
发现从某个点开始,遍历它的儿子,从这个点开始的id数组总是连续的
这个点的id是id[x],那么以该点为根的子树中的最后一个节点的id一定是id[x]+size[x]-1
因为我们并不知道那个点到底是谁,但是我们可以通过x算出来
所以,
Code:

void update_size(int x, int delta){//没啥好解释了吧
    delta %= qy;
    update(1, id[x], id[x] + size[x] - 1, delta);
}

求和操作呢,只要把update改成query就行了

7、例题讲解

树剖模板题
四大操作,码量较大
得注意时刻取模

Code:

#include <bits/stdc++.h>
#define maxn 100010
#define ls rt << 1
#define rs rt << 1 | 1
using namespace std;
struct Edge{
    int to, next;
}edge[maxn << 1];
struct Seg{
    int l, r, sum, tag;
}seg[maxn << 2];
int n, m, r, qy, num, cnt, head[maxn << 1], d[maxn], fa[maxn], size[maxn], son[maxn], top[maxn], id[maxn], w[maxn], wt[maxn];

inline int read(){
    int s = 0, w = 1;
    char c = getchar();
    for (; !isdigit(c); c = getchar()) if (c == '-') w = -1;
    for (; isdigit(c); c = getchar()) s = (s << 1) + (s << 3) + (c ^ 48);
    return s * w;
}

void add_edge(int x, int y){ edge[++num].to = y; edge[num].next = head[x]; head[x] = num; }

void pushup(int rt){ seg[rt].sum = (seg[ls].sum + seg[rs].sum) % qy; }

void pushdown(int rt){
    seg[ls].sum = (seg[ls].sum + (seg[ls].r - seg[ls].l + 1) * seg[rt].tag) % qy;
    seg[rs].sum = (seg[rs].sum + (seg[rs].r - seg[rs].l + 1) * seg[rt].tag) % qy;
    seg[ls].tag = (seg[ls].tag + seg[rt].tag) % qy;
    seg[rs].tag = (seg[rs].tag + seg[rt].tag) % qy;
    seg[rt].tag = 0;
}

void build(int rt, int l, int r){
    seg[rt].l = l, seg[rt].r = r;
    if (l == r){
        seg[rt].sum = wt[l];
        return;
    }
    int mid = (l + r) >> 1;
    build(ls, l, mid); build(rs, mid + 1, r);
    pushup(rt);
}

void update(int rt, int l, int r, int delta){
    if (seg[rt].l > r || seg[rt].r < l) return;
    if (seg[rt].l >= l && seg[rt].r <= r){
        seg[rt].sum = (seg[rt].sum + (seg[rt].r - seg[rt].l + 1) * delta) % qy;
        seg[rt].tag = (seg[rt].tag + delta) % qy;
        return;
    }
    if (seg[rt].tag) pushdown(rt);
    update(ls, l, r, delta); update(rs, l, r, delta);
    pushup(rt);
}

int query(int rt, int l, int r){
    if (seg[rt].l > r || seg[rt].r < l) return 0;
    if (seg[rt].l >= l && seg[rt].r <= r) return seg[rt].sum;
    if (seg[rt].tag) pushdown(rt);
    return (query(ls, l, r) + query(rs, l, r)) % qy;
}

void update_range(int x, int y, int delta){
    delta %= qy;
    while (top[x] != top[y]){
        if (d[top[x]] < d[top[y]]) swap(x, y);
        update(1, id[top[x]], id[x], delta);
        x = fa[top[x]];
    }
    if (d[x] < d[y]) swap(x, y);
    update(1, id[y], id[x], delta);
}

int query_range(int x, int y){
    int ans = 0;
    while (top[x] != top[y]){
        if (d[top[x]] < d[top[y]]) swap(x, y);
        ans = (ans + query(1, id[top[x]], id[x])) % qy;
        x = fa[top[x]];
    }
    if (d[x] < d[y]) swap(x, y);
    return (ans + query(1, id[y], id[x])) % qy;
}

void update_size(int x, int delta){
    delta %= qy;
    update(1, id[x], id[x] + size[x] - 1, delta);
}

int query_size(int x){ return query(1, id[x], id[x] + size[x] - 1); }

void dfs1(int u, int pre){
    d[u] = d[pre] + 1, fa[u] = pre;
    size[u] = 1, son[u] = -1;
    for (int i = head[u]; i; i = edge[i].next){
        int v = edge[i].to;
        if (v != pre){
            dfs1(v, u);
            size[u] += size[v];
            if (son[u] == -1 || son[u] != -1 && size[v] > size[son[u]]) son[u] = v;
        }
    }
}

void dfs2(int u, int x){
    id[u] = ++cnt, wt[cnt] = w[u] % qy, top[u] = x;
    if (son[u] == -1) return;
    dfs2(son[u], x);
    for (int i = head[u]; i; i = edge[i].next){
        int v = edge[i].to;
        if (v != fa[u] && v != son[u]) dfs2(v, v);
    }
}

int main(){
    n = read(), m = read(), r = read(), qy = read();
    for (int i = 1; i <= n; ++i) w[i] = read();
    for (int i = 1; i < n; ++i){
        int x = read(), y = read();
        add_edge(x, y); add_edge(y, x);
    }
    dfs1(r, 0);
    dfs2(r, r);
    build(1, 1, n);
    while (m--){
        int opt = read();
        if (opt == 1){
            int x = read(), y = read(), z = read();
            update_range(x, y, z);
        }
        if (opt == 2){
            int x = read(), y = read();
            printf("%d\n", query_range(x, y));
        }
        if (opt == 3){
            int x = read(), y = read();
            update_size(x, y);
        }
        if (opt == 4){
            int x = read();
            printf("%d\n", query_size(x));
        }
    }
    return 0;
}

8、复杂度分析

时间复杂度

两个dfs,O(n),太小,不考虑
总共m个询问,每个询问中,跑到lca需要跳链,平均每次跳 l o g 2 n log_2n log2n跳链,同时线段树维护O(logn)
所以总复杂度 O ( m l o g 2 2 n ) O(mlog_2^2n) O(mlog22n)

空间复杂度

空间主要是线段树和链式前向星的空间,不多

9、心得

维护边权题型

分析树剖性质,发现线段树维护的是点权,那么万一有题目需要维护边权呢
采用点代边的方法
用一条边的两端点中深度更大的那个点代表这条边(感性理解一下,没毛病)

线段树骚操作

树剖其实都很裸。。。敲多了发现真的非常简单。。
主要是线段树怎么维护,怎么pushup,怎么pushdown,这还是要有点技术在的

10、练习题

我貌似把我做的所有树剖题都写了题解,都是裸题。。
维护边权题/qtree系列:Qtree1
不是特别裸,要思考一下:Qtree3
ZJOI2008真题,不过真的裸:树的统计
小思考,NOI题:软件包管理器
pushdown见真章,细节注意:月下“毛景树”
弱省省选,裸题:[HEOI2016/TJOI2016]树
弱省省选,裸题:[SHOI2012]魔法树
弱省省选,裸题:[HAOI2015]树上操作
维护边权异或和:让我们异或吧
动态开点线段树+树剖:[SDOI2014]旅行

11、后记

树剖到这讲完了吧,水平有限,文笔不佳,若是没懂可以去看看别的博,我把我目前对树剖的理解都写上了,撒花~~

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值