首页 > 其他分享 >重链剖分

重链剖分

时间:2023-11-09 20:22:15浏览次数:40  
标签:剖分 int top dep dfn 重链

前置芝士:线段树,或树状数组,越熟练越好。

(当然这不是意味着你不会线段树就看不懂这篇博客。)

1. 何为树链剖分

树链剖分,简而言之,就是将树分成一条条链,然后用数据结构去维护这些链,以支持树上两点间的各种询问操作。

据我所知,树链剖分大约有三种,分别是重链剖分、长链剖分和实链剖分。其中的重链剖分最为常见,所以一般说树链剖分(简称树剖)就是指重链剖分。

注:重链剖分和长链剖分只能在有根树上用。

2. 重链剖分

  1. 预处理

这是普通 oi 题中的一颗树。这里我们钦定 1 为根。

这是一棵树

在重链剖分中,有如下几个定义:

重儿子:树上每个节点的儿子中子树大小最大的称为重儿子。图中用红圈标出。

轻儿子:不是重儿子的点是轻儿子。

重边:连接每个重儿子及其父节点的边称为重边。图中用蓝色标出。

轻边:不是重边的边是轻边。

重链:以轻儿子为起点,只经过重边与重儿子的极长链。如图中 1 - 2 - 4 - 5,10 - 8 - 11 等。特别地,单独一个轻叶子节点也可以算一条重链。

轻链:重链以外,都是轻链。

这样,我们就可以发现树上每个节点都恰好只被一条重链包含。

那接下来要考虑的问题自然就变成了如何求重链。由重链的定义可知,我们可以进行一次 dfs,先求出每个节点的重儿子,然后让重儿子继承当前节点所在的重链,再以所有轻儿子为起点各再往下拉一条重链。这样,我们就求出了树中每一条重链。

重链求完之后,就需要维护。由于要用数据结构来维护重链,所以应该尽量让重链上所有点的编号连续。显然原树上重链的编号是不一定连续的,所以需要给树上每一个节点重新编号。为了使编号连续,我们采用 dfs 序的方式来重编号。在递归每个节点时,先给该节点编上号,随后优先递归其重儿子,这样就能够保证同一条重链上的编号连续。

上代码:

int dfn[100005]; // dfs 序
int sz[100005]; // 子树大小
int dep[100005]; // 深度
int top[100005]; // 所在重链链顶
int son[100005]; // 重儿子
int f[100005]; // 父亲
void dfs1(int x, int fa, int d) {
    sz[x] = 1;
    f[x] = fa;
    dep[x] = d;
    for (int i = head[x]; i != 0; i = nxt[i]) {
        int v = to[i];
        if (v != fa) {
            dfs1(v, x, d + 1);
            sz[x] += sz[v];
            if (sz[v] > sz[son[x]]) // son[x] 代表 x 的重儿子
                son[x] = v;
        }
    }
}
void dfs2(int x, int t) { // t 表示当前重链的最上面的节点
    dfn[x] = ++ncnt; // dfs 序
    wt[ncnt] = w[x];
    top[x] = t;
    if (!son[x]) 
        return;
    dfs2(son[x], t); // 让重儿子继承当前重链并优先递归,确保编号连续
    for (int i = head[x]; i != 0; i = nxt[i]) {
        int v = to[i];
        if (v != son[x] && v != f[x]) 
            dfs2(v, v); // 以每个轻儿子为起点拉一条链
    }
}

这是普通 oi 题中一颗被重新编号过的树。

这是一棵树

可以看出,现在所有重链上的节点编号都连续了,可以用数据结构维护了。

2. 在线询问

对于一次询问的两个端点 \(u\) 和 \(v\),我们用跳链的方法将原本不在同一条重链上的两个点放到同一条重链上。每次让所在链顶深度较深的点往上跳,跳到其链顶节点的父亲,一边跳一边在数据结构中统计区间 [dfn[top[x]], dfn[x]] 的答案。当两点跳到同一条链上后,再最后统计一遍浅的节点到深的节点这一段区间的答案就好了。

这里以区间和为例。

int Query_path(int u, int v) {
    int ret = 0;
    while (top[u] != top[v]) { // 当两点不在同一条链上
        if (dep[top[u]] < dep[top[v]]) // 保证是链顶深度深的点在往上跳
            swap(u, v);
        ret += Query_sum(1, 1, N, dfn[top[u]], dfn[u]); // 统计被跳过区间的答案
        ret %= p;
        u = f[top[u]]; // 跳链
    }
    if (dep[u] > dep[v]) // 当两点在同一条链上时
        swap(u, v);
    ret += Query_sum(1, 1, N, dfn[u], dfn[v]); // 统计深度浅的节点到深的节点这段区间的答案
    return ret % p;
}

在线修改同理。

void Add_path(int u, int v, int z) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) 
            swap(u, v);
        Add(1, 1, N, dfn[top[u]], dfn[u], z); // 修改整条链上的答案
        u = f[top[u]];
    }
    if (dep[u] > dep[v]) 
        swap(u, v);
    Add(1, 1, N, dfn[u], dfn[v], z); // 最后再修改一次
}

现在,你已经会写树链剖分了。来打一个板子吧!

P3384 【模板】重链剖分/树链剖分

AC 代码:

#include <iostream>
#define int long long
using namespace std;
const int N = 262144;
int n, m, r, p;
int head[200005], nxt[200005], to[200005], cnt;
void add(int u, int v) { to[++cnt] = v, nxt[cnt] = head[u], head[u] = cnt; }
int dfn[100005], sz[100005], dep[100005], top[100005], son[100005], f[100005];
int wt[N * 4], w[N * 4];
int ncnt;
void dfs1(int x, int fa, int d) {
    sz[x] = 1;
    f[x] = fa;
    dep[x] = d;
    for (int i = head[x]; i != 0; i = nxt[i]) {
        int v = to[i];
        if (v != fa) {
            dfs1(v, x, d + 1);
            sz[x] += sz[v];
            if (sz[v] > sz[son[x]]) 
                son[x] = v;
        }
    }
}
void dfs2(int x, int t) {
    dfn[x] = ++ncnt;
    wt[ncnt] = w[x];
    top[x] = t;
    if (!son[x]) 
        return;
    dfs2(son[x], t);
    for (int i = head[x]; i != 0; i = nxt[i]) {
        int v = to[i];
        if (v != son[x] && v != f[x]) 
            dfs2(v, v);
    }
}
int sm[N * 4], tag[N * 4];
void pushdown(int o, int l, int r) {
    if (tag[o] == 0) 
        return;
    int t = tag[o];
    int mid = l + r >> 1;
    tag[o << 1] += t;
    tag[o << 1 | 1] += t;
    sm[o << 1] += (mid - l + 1) * t;
    sm[o << 1 | 1] += (r - mid) * t;
    tag[o] = 0;
}
void Build(int o, int l, int r) {
    if (l == r) {
        sm[o] = wt[l];
        return;
    }
    int mid = l + r >> 1;
    Build(o << 1, l, mid);
    Build(o << 1 | 1, mid + 1, r);
    sm[o] = sm[o << 1] + sm[o << 1 | 1];
}
void Add(int o, int l, int r, int L, int R, int k) {
    if (L <= l && r <= R) {
        sm[o] += (r - l + 1) * k;
        tag[o] += k;
        return;
    }
    pushdown(o, l, r);
    int mid = l + r >> 1;
    if (L <= mid) 
        Add(o << 1, l, mid, L, R, k);
    if (R > mid) 
        Add(o << 1 | 1, mid + 1, r, L, R, k);
    sm[o] = sm[o << 1] + sm[o << 1 | 1];
}
int Qsum(int o, int l, int r, int L, int R) {
    if (L <= l && r <= R) 
        return sm[o];
    pushdown(o, l, r);
    int mid = l + r >> 1, ret = 0;
    if (L <= mid) 
        ret += Qsum(o << 1, l, mid, L, R);
    if (R > mid) 
        ret += Qsum(o << 1 | 1, mid + 1, r, L, R);
    return ret;
}
void Add_path(int u, int v, int z) {
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) 
            swap(u, v);
        Add(1, 1, N, dfn[top[u]], dfn[u], z);
        u = f[top[u]];
    }
    if (dep[u] > dep[v]) 
        swap(u, v);
    Add(1, 1, N, dfn[u], dfn[v], z);
}
int Query_path(int u, int v) {
    int ret = 0;
    while (top[u] != top[v]) {
        if (dep[top[u]] < dep[top[v]]) 
            swap(u, v);
        ret += Qsum(1, 1, N, dfn[top[u]], dfn[u]);
        ret %= p;
        u = f[top[u]];
    }
    if (dep[u] > dep[v]) 
        swap(u, v);
    ret += Qsum(1, 1, N, dfn[u], dfn[v]);
    return ret % p;
}
void Add_sbt(int x, int y) { Add(1, 1, N, dfn[x], dfn[x] + sz[x] - 1, y % p); }
int Query_sbt(int x) { return Qsum(1, 1, N, dfn[x], dfn[x] + sz[x] - 1) % p; }
signed main() {
    cin >> n >> m >> r >> p;
    for (int i = 1; i <= n; i++) scanf("%lld", w + i);
    for (int i = 1, u, v; i <= n - 1; i++) {
        cin >> u >> v;
        add(u, v);
        add(v, u);
    }
    dfs1(r, 0, 1);
    dfs2(r, r);
    Build(1, 1, N);
    for (int i = 1, op, x, y, z; i <= m; i++) {
        cin >> op >> x;
        if (op == 1) {
            cin >> y >> z;
            Add_path(x, y, z);
        } else if (op == 2) {
            cin >> y;
            cout << Query_path(x, y) << "\n";
        } else if (op == 3) {
            cin >> y;
            Add_sbt(x, y);
        } else {
            cout << Query_sbt(x) << "\n";
        }
    }
    return 0;
}

树剖平均码量 100 多行,所以看到这么长的代码不要害怕。你会习惯的。

最后,来点小练习题:

P2590 树的统计

P3833 魔法树

P2146 软件包管理器

最后

虽然树剖代码长,但是很好打。如果认真打的话这些板子一遍过没有问题。实在不行可以照着我代码打(不要脸.jpg

最后的最后,一定要多练!!!多练习才能真正学会新东西。

标签:剖分,int,top,dep,dfn,重链
From: https://www.cnblogs.com/forgotmyhandle/p/17822724.html

相关文章

  • 点集合的三角剖分
    点集合的三角剖分是指如何将一些离散的点集合组合成不均匀的三角形网格,使得每个点成为三角网中三角面的顶点。这个算法的用处很多,一个典型的意义在于可以通过一堆离散点构建的TIN实现对整个构网区域的线性控制,比如用带高程的离散点构建的TIN来表达地形。在实际工作中,使用最多的三......
  • 树链剖分
    注意事项:线段树中\(modify(),query()\)中\(>=,<=,>,<\)以及\(l,r,L,R\)#include<bits/stdc++.h>#definelsp<<1#definersp<<1|1#definelsonls,l,mid#definersonrs,mid+1,r#defineroot1,1,nusingnamespacestd;constint......
  • 树链剖分 学习心得
    Bug都写在代码开头了,就不复述了。还有一个智障的错误是注释调试代码的时候把同一行的正式代码也给注释掉了(写得非常精彩。/*  bug:1.rev、id要分清!      2.mod()函数的情况不能写一半就跑路!      3.别忘了先给tree build()一下!      4.出界......
  • 重链剖分
    代码思路主体部分:初始化,剖分链,求LCA(也就是dfs1,dfs2,LCA三个函数)辅助部分:structPoint{//节点信息多的时候会习惯开个结构体来存 intdep,siz,son,top,fath; //点的深度子树大小重儿子所在重链的链头父亲节点 //没有重儿子则son=0 intid,l,r;//求lca不......
  • 树链剖分【产品说明书】
    一种暴论:树链剖分=多叉树上分块+线段树适用范围总之就是数据结构的基础问题。总的来说,树链剖分可以在\(O(m\logn)\)的时间复杂度中,解决大多数树上路径问题,包括其修改、维护和查询。例如这样的一道模板题又例如……(请直接跳到本文最后一章)产品简介树链剖分有两种:重......
  • 树链剖分
    树链剖分树链剖分常用于解决树上路径查询的问题。原理:对于树上两点之间的路径\(u\)->\(v\),根据某种策略,将之拆分成若干条链,然后利用线段树等数据结构单独维护这些子链,最后将答案合并。常用的剖分方法:轻重边划分。剖分树种的边可以分为两种边:重边和轻边。设\(size_u\)......
  • 算法:树链剖分
    去年就看过树链剖分的视频了,当时连树状数组,线段树都没学,对树的dfs也一知半解,所以基本完全听不懂。昨天又重新看了一般,感觉思路挺简单,应该比线段树简单吧,从用树链剖分求LCA来看确实是这样的,但是没有想到的是用线段树维护树链剖分。QAQ这应该是我打过最长的代码吧!(3K)树链剖分......
  • 浅谈树链剖分—轻重链剖分
    闲话似乎会有很多种树剖,什么长链剖分之类的,但是暂时只会轻重链剖分(可怜)。以前的版本在这里,但是感觉写的太粗糙了,所以决定重写一篇(我也不知道为什么要一直写树剖而不写点别的)。正文引入如果给出一个序列,有一些区间修改查询之类的操作,直接上线段树什么的就完事了,但是如果给出的......
  • 树链剖分
    前言:本人太弱了,看着题解调了一下午,才A了树剖的板子题,所以写篇博客纪念一下(其实是怕自己忘了树剖怎么做)。本文以板子题为例子定义:树链剖分,计算机术语,指一种对树进行划分的算法,它先通过轻重边剖分将树分为多条链,保证每个于且只属于一条链,然后再通过数据结构(树状数组、BST、SPL......
  • [学习笔记] 树链剖分
    叫复习笔记或许更好。树链剖分就是把树剖成链去解决一些问题。定义重子节点:子节点中子树大小最大的节点。轻子节点:除重子节点外的其他子节点。重边:到重子节点的边。轻边:到轻子节点的边。记号\(dfn[x]\):DFS序,也是在线段树中的编号。\(son[x]\):重子节点。\(dep[x]\)......