树链剖分

概念

树链剖分是把一颗树分割成若干条链,用来维护链上信息的一种算法,常配合线段树,树状数组,平衡树使用。

重链剖分

基本定义

为了更好的理解,我们先给出一些定义:

  • 1.重儿子 :某节点的子节点中子树大小最大的一个节点
  • 2.轻儿子 :某节点除去重儿子的所有节点。
  • 3.重边 :某节点到其重儿子的边
  • 4.轻边: 某节点到其轻儿子的边
  • 5.重链:连续的重边首位相接,形成重链

构造方法

需要两个 dfs。

第一个 dfs 用来记录树上父亲,深度,子树大小等基本信息,同时找到重儿子,其他都很简单,找重儿子的话就是比较子树大小就行了

第二个 dfs 用来构成链,dfs 传参时传一个链顶的标号,优先处理重儿子,同时记录 dfn (dfs 序)。

为什么要这么做

因为我们要维护啊一条链上的信息,但是可能原来的标号不是连续的,所以我们优先处理重儿子就可以保证每条重链上的 dfn 是连续的,记录链顶标号是为了记录链的起点

代码实现

第一个 dfs:

void dfs(int u, int f) {
	dep[u] = dep[f] + 1, siz[u] = 1, fa[u]= f;
	for (int i =head[u]; ~i; i = edge[i].nxt) {
		int v = edge[i].to;
		if (v == f)
			continue;
		dfs(v, u);
		siz[u] += siz[v];
		if (siz[son[u]] < siz[v])
			son[u] = v;
	}
}

第二个 dfs:

void dfs2(int u, int tp) {
    dfn[u] = ++dfncnt;
    top[u] = tp;
    if (!son[u]) return;
    dfs2(son[u], tp);
    for (int i = head[u]; ~i; i = edge[i].nxt) {
        int v = edge[i].to;
        if (v != fa[u] && v != son[u]) {
            dfs2(v, v);
        }
    }
}

解释:

fa[x] 表示节点 x 的父亲
siz[x] 表示节点 x 的子树大小
son[x] 表示节点 x 的重儿子
dep[x] 表示节点 x 的深度
top[x] 表示节点 x 所在重链的的链顶
dfn[x] 表示节点 x 的 dfs 序(即 dfn)
是重儿子节点的重链顶端是他父亲所在重链(我们将每一条轻边看做一条重链)的顶端,否则即为他本身

应用

求 LCA (最近公共祖先)

为什么可以?

因为我们沿着重链一直往上跳, 最终一定会跳到同一条重链上

方法:沿着重链一直向上跳,当跳到同一条重链上时,深度较小的即为最小公共祖先

Code:

int lca(int x, int y) {
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]])
			swap(x, y);
        x = fa[top[x]];
    }
    return dep[x] > dep[y] ? y : x;
}

维护路径信息

比如需要将某个路径上的边权(点权)进行区间加,便可以使用使用树链剖分转化到链上的区间加操作,至于如何区间加,可以使用线段树或者树状数组进行维护

代码与求 LCA 的相似

Code:(线段树代码不展示)

区间修改:

关于 update 函数的定义为 : 当前节点, 节点的左边界,节点的右边界, 要修改区间的左边界,右边界, 值

void update(int x, int y) {
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]])
            swap(x, y);
        update(1, 1, n, dfn[top[x]], dfn[x], 1);
        x = fa[top[x]];
    }
    if (dep[x] < dep[y])
        swap(x, y);
    update(1, 1, n, dfn[y], dfn[x], 1);
}

区间查询:

int query(int x, int y) {
    int res = 0;
    while (top[x] != top[y]) {
        if (dep[top[x]] < dep[top[y]])
            swap(x, y);
        res = res + query(1, 1, n, dfn[top[x]], dfn[x]);
        x = fa[top[x]];
    }
    if (dep[x] < dep[y]) swap(x, y);
    res = res + query(1, 1, n, dfn[y], dfn[x]);
    return res;
}

时间复杂度

预处理时间复杂度为 \(\Theta(n)\)
对于每次查询、更新
跳重链的总复杂度为 \(\Theta(\log n)\), 因为重链的总条数不超过 \(\log n\)

证明

简要证明:每走到一个轻儿子,子树大小至少减少一半

线段树操作为 \(\Theta(\log n)\)

所以复杂度为 \(\Theta(log ^ 2 n)\)

提醒:虽然树剖带两个 \(\log\) 但是由于常数小且两个 \(\log\) 通常跑不满,所以实际可能快于某些带单 \(\log\) 的算法

例题

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

code:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1e5 + 5;
int n, m;
int root, mod;
int tot;
int cnt;
struct segtree
{
    int sum;
    int lazy_tag;
} tr[MAXN << 2];
struct ed
{
    int to, nxt;
} edge[MAXN << 1];
int w[MAXN];
int tw[MAXN];
int a[MAXN << 3];
int ind[MAXN];
int head[MAXN];
int dep[MAXN];
int son[MAXN];
int siz[MAXN];
int father[MAXN];
int top[MAXN];

void add_edge(int u, int v) { edge[++tot].to = v, edge[tot].nxt = head[u], head[u] = tot; }
//---线段树start---//

void push_up(int id) { tr[id].sum = (tr[id << 1].sum + tr[id << 1 | 1].sum) % mod; }

void push_down(int id, int s, int t)
{
    int mid = s + t >> 1;
    tr[id << 1].sum += tr[id].lazy_tag * (mid - s + 1), tr[id << 1].sum %= mod;
    tr[id << 1 | 1].sum += tr[id].lazy_tag * (t - mid), tr[id << 1 | 1].sum %= mod;
    tr[id << 1].lazy_tag += tr[id].lazy_tag, tr[id << 1 | 1].lazy_tag += tr[id].lazy_tag;
    tr[id << 1].lazy_tag %= mod, tr[id << 1 | 1].lazy_tag %= mod;
    tr[id].lazy_tag = 0;
}

void build(int id, int l, int r)
{
    if (l == r)
    {
        tr[id].sum = tw[l];
        return;
    }
    int mid = l + r >> 1;
    build(id << 1, l, mid), build(id << 1 | 1, mid + 1, r);
    push_up(id);
}

void update(int id, int l, int r, int s, int t, int val)
{
    if (l <= s && r >= t)
    {
        tr[id].sum += (t - s + 1) * val, tr[id].sum %= mod;
        tr[id].lazy_tag += val, tr[id].lazy_tag %= mod;
        return;
    }
    int mid = s + t >> 1;
    if (tr[id].lazy_tag)
        push_down(id, s, t);
    if (mid >= l)
        update(id << 1, l, r, s, mid, val);
    if (mid < r)
        update(id << 1 | 1, l, r, mid + 1, t, val);
    push_up(id);
}

int get_sum(int id, int l, int r, int s, int t)
{
    if (l <= s && r >= t)
    {
        return tr[id].sum;
    }
    int mid = s + t >> 1;
    if (tr[id].lazy_tag)
        push_down(id, s, t);
    int sum = 0;
    if (mid >= l)
        sum = get_sum(id << 1, l, r, s, mid);
    if (mid < r)
        sum += get_sum(id << 1 | 1, l, r, mid + 1, t);
    return sum % mod;
}

//---线段树end---//
//---树链剖分start---//

void path_add(int x, int y, int val)
{
    val %= mod;
    while (top[x] != top[y])
    {
        if (dep[top[x]] < dep[top[y]])
            swap(x, y);
        update(1, ind[top[x]], ind[x], 1, n, val);
        x = father[top[x]];
    }
    if (dep[x] > dep[y])
        swap(x, y);
    update(1, ind[x], ind[y], 1, n, val);
}

int query_path_sum(int x, int y)
{
    int sum = 0;
    while (top[x] != top[y])
    {
        if (dep[top[x]] < dep[top[y]])
            swap(x, y);
        sum += get_sum(1, ind[top[x]], ind[x], 1, n);
        x = father[top[x]];
    }
    if (dep[x] > dep[y])
        swap(x, y);
    sum += get_sum(1, ind[x], ind[y], 1, n);
    return sum % mod;
}

void tree_add(int u, int val)
{
    update(1, ind[u], ind[u] + siz[u] - 1, 1, n, val % mod);
}

int sum_tree(int u)
{
    return get_sum(1, ind[u], ind[u] + siz[u] - 1, 1, n) % mod;
}

void dfs1(int u, int fa)
{
    father[u] = fa;
    int max_size = 0;
    siz[u] = 1, dep[u] = dep[fa] + 1;
    for (int i = head[u]; ~i; i = edge[i].nxt)
    {
        int v = edge[i].to;
        if (v == fa)
            continue;
        dfs1(v, u);
        siz[u] += siz[v];
        if (siz[v] > max_size)
            max_size = siz[v], son[u] = v;
    }
}

void dfs2(int u, int tp, int fa)
{
    ind[u] = ++cnt, tw[cnt] = w[u], top[u] = tp;
    if (!son[u])
        return;

    dfs2(son[u], tp, u);
    for (int i = head[u]; ~i; i = edge[i].nxt)
    {
        int v = edge[i].to;
        if (v == fa || v == son[u])
            continue;
        dfs2(v, v, u);
    }
}

//---树链剖分---end//
signed main()
{
    memset(head, -1, sizeof(head));
    cin >> n >> m >> root >> mod;
    for (int i = 1; i <= n; i++)
        cin >> w[i];
    for (int i = 1; i < n; i++)
    {
        int u, v;
        cin >> u >> v;
        add_edge(u, v), add_edge(v, u);
    }
    dep[0] = -1;
    dfs1(root, 0);
    dfs2(root, root, 0);
    build(1, 1, n);
    for (int i = 1; i <= m; i++)
    {
        int op, x, y, z;
        cin >> op;
        if (op == 1)
        {
            cin >> x >> y >> z;
            path_add(x, y, z);
        }
        else if (op == 2)
        {
            cin >> x >> y;
            cout << query_path_sum(x, y) << "\n";
        }
        else if (op == 3)
        {
            cin >> x >> z;
            tree_add(x, z);
        }
        else if (op == 4)
        {
            cin >> x;
            cout << sum_tree(x) << "\n";
        }
    }
    return 0;
}

长链剖分

与重链剖分类似,把重儿子的定义换为某节点子结点中子树深度最大的子结点,其余类似,不在赘述

posted @ 2025-10-23 20:05  孤独的Bochi  阅读(6)  评论(0)    收藏  举报