浅谈树链剖分

mac2026-03-12  3

定义

(不严谨的口胡)

树链剖分将一棵树按照某种方式划分成多条链,再利用一系列数据结构对链上信息进行维护。

有什么用?

详见洛谷模板P3384

支持:

求\(LCA\)路径信息维护子树信息维护...

算法实现

怎么实现呢? 因为博主太菜了不会长链剖分,所以这里讲轻重链剖分 据说也有名字叫启发式剖分?

在轻重链剖分中,每个非叶节点有且仅有一个重儿子,其余子节点为轻儿子。轻重儿子划分的依据是子树大小,对于每个非叶节点, 其子树\(size\)最大的子节点成为它的重儿子,其余节点成为它的轻儿子。

放一张图

红色边为重边,黑色边为轻边

性质:

每个点在且仅在一条重链上 证明显然对于任意轻边\(edge(u \rightarrow v)\),有\(size[v] < \frac{size[u]}{2}\) 证明显然一棵树从叶节点走到根节点,经过的重链至多有\(log_2(n)\)条 证明:可以发现,经过树剖之后的图是由一些重链和连接它们的轻边组成的(必要时,一条重链也可退化成边甚至是点)。从底向上,当我们每一次走过一条重链并跨过轻边到另一条重链时,所在节点的子树大小都至少翻了一倍(见性质\((2)\)),那么最多翻\(log_2(n)\)次子树大小就会变成\(n\),即跳到根节点。 这条性质的证明实际也是树剖复杂度为严格\(O(nlog_2(n))\)的证明

实现:

两遍dfs 第一遍\(dfs\)维护每个节点的深度\(dep\)、父亲\(fa\)、子树大小\(size\)(回溯时更新),重儿子\(son\)(在更新\(siz\)时顺便维护)

void dfs1(int cur, int fa) { father[cur] = fa, size[cur] = 1, dep[cur] = dep[fa] + 1; for (register int i = first[cur]; i; i = nxt[i]) { int v = to[i]; if (v == fa) continue; dfs1(v, cur); size[cur] += size[v]; if (size[son[cur]] < size[v]) son[cur] = v; } }

第二遍\(dfs\)基于第一遍已经得到的信息对剩下的信息进行维护,得到每条重链的顶点\(top\),\(dfs\)序\(num\),每个节点的\(dfs\)序对应的原点编号\(idx\)

void dfs2(int cur, int tp) { top[cur] = tp, num[cur] = ++ind, idx[num[cur]] = cur; if (son[cur]) dfs2(son[cur], tp); for (register int i = first[cur]; i; i = nxt[i]) { int v = to[i]; if (!num[v]) dfs2(v, v); } }

用途

求\(LCA\)

关于树剖和倍增的常数这个问题,看了不少神仙打架,有说树剖小的,有说倍增小的,这里为了不误人子弟就不下结论了。

树剖和倍增的实质都是通过一定合理的方式把向上一步一步跳变成向上跳很多很多步,类比倍增的方式是跳\(2^k\)个顶点,树剖的方式是每次向上跳一条重链。

贴一段代码:

int lca(int u, int v) { while (top[u] != top[v]) { if (dep[top[u]] < dep[top[v]]) swap(u, v); u = fa[top[u]]; } if (dep[u] < dep[v]) swap(u, v); return v; }

……好吧,原谅我才疏学浅,不带数据结构维护重链的树剖就我所知用的多的也就是求一个\(LCA\),当然没事把树剖一剖也不是不行,你高兴就好—— wlj

\(UPD\):据\(fsy\)说可以拿来做\(dsu\ on\ tree\),因为我太菜不会所以不讲

于是接下来是数据结构维护重链上信息。

关于数据结构维护

冷静分析一下发现上面的过程是有道理的,在\(dfs2\)中我们优先进了重儿子的递归分支,然后再回来处理其他轻儿子。这样做一方面是为了\(top\)信息能够很好地继承下去,另一方面则是为了保证一条重链上的\(dfs\)序(即上述代码中的\(num\)数组)是连续的。

由此,以\(dfs\)序作为下标,我们可以很容易地用数据结构维护链上信息。

看一道例题:

如题,已知一棵包含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为根节点的子树内所有节点值之和

简化一下题意:维护路径和子树信息。

树剖之后,对于\(dfs\)序建立一棵线段树,维护每条重链上的信息,对于链加链求和的信息,我们在每一条重链上分段处理。上文已经说过,一条重链上的\(dfs\)序是连续的,所以只需要在线段树上正常区间操作就可以了。对于子树加,子树求和的信息,可以知道,一棵子树的\(dfs\)序一定是一段连续区间(考虑\(dfs\)过程容易得到)。而子树根节点的\(dfs\)序一定是最小的(不经过它无法进入这棵子树),最大的\(dfs\)序则是根节点\(dfs\)序\(+\)子树\(size-1\),于是也可以通过线段树上区间操作解决。根据上面的证明,一次操作最多需要跳\(log\)次重链,而线段树区间维护单次操作也是\(log\)次的,故总复杂度为\(O(nlog^2(n))\)

放一下代码:

#include<bits/stdc++.h> #define N (200000+5) using namespace std; inline int read() { int cnt = 0, f = 1; char c = getchar(); while (!isdigit(c)) {if (c == '-') f = -f; c = getchar();} while (isdigit(c)) {cnt = (cnt << 3) + (cnt << 1) + (c ^ 48), c = getchar();} return cnt * f; } int n, m, r, nxt[N], first[N], to[N], tot = 0, mod, x, y, opr, z; void Add(int x, int y) {nxt[++tot] = first[x], first[x] = tot, to[tot] = y;} int father[N], size[N], dep[N], num[N], son[N], top[N], a[N], idx[N], ind; struct node { int l, r; long long sum, add; #define l(p) tree[p].l #define r(p) tree[p].r #define add(p) tree[p].add #define sum(p) tree[p].sum } tree[N << 2]; inline void pushup(int p) {sum(p) = sum(p << 1) + sum(p << 1 | 1), sum(p) %= mod;} inline void pushadd(int p, int d) {sum(p) += d * (r(p) - l(p) + 1) % mod, add(p) += d % mod;} inline void pushdown(int p) {pushadd(p << 1, add(p)), pushadd(p << 1 | 1, add(p)), add(p) = 0;} void build(int p, int l, int r) { l(p) = l, r(p) = r; if (l == r) {sum(p) = a[idx[l]]; return;} int mid = (l + r) >> 1; build(p << 1, l, mid), build(p << 1 | 1, mid + 1, r); pushup(p); } void modify(int p, int l, int r, int d) { if (l <= l(p) && r >= r(p)) {pushadd(p, d); return;} pushdown(p); int mid = (l(p) + r(p)) >> 1; if (l <= mid) modify(p << 1, l, r, d); if (r > mid) modify(p << 1 | 1, l, r, d); pushup(p); } long long query(int p, int l, int r) { if (l <= l(p) && r >= r(p)) return sum(p); pushdown(p); long long val = 0; int mid = (l(p) + r(p)) >> 1; if (l <= mid) val += query(p << 1, l, r), val %= mod; if (r > mid) val += query(p << 1 | 1, l, r), val %= mod; return val % mod; } //pre_work void dfs1(int cur, int fa) { father[cur] = fa, size[cur] = 1, dep[cur] = dep[fa] + 1; for (register int i = first[cur]; i; i = nxt[i]) { int v = to[i]; if (v == fa) continue; dfs1(v, cur); size[cur] += size[v]; if (size[son[cur]] < size[v]) son[cur] = v; } } void dfs2(int cur, int tp) { top[cur] = tp, num[cur] = ++ind, idx[num[cur]] = cur; if (son[cur]) dfs2(son[cur], tp); for (register int i = first[cur]; i; i = nxt[i]) { int v = to[i]; if (!num[v]) dfs2(v, v); } } //chain-operation void chain_add(int u, int v, int d) { while (top[u] != top[v]) { if (dep[top[u]] < dep[top[v]]) swap(u, v); modify(1, num[top[u]], num[u], d); u = father[top[u]]; } if (dep[u] < dep[v]) swap(u, v); modify(1, num[v], num[u], d); } int chain_query(int u, int v) { long long ans = 0; while (top[u] != top[v]) { if (dep[top[u]] < dep[top[v]]) swap(u, v); ans += query(1, num[top[u]], num[u]) % mod; u = father[top[u]]; } if (dep[u] < dep[v]) swap(u, v); ans += query(1, num[v], num[u]); return ans % mod; } //subtree-operation inline void subtree_add(int p, int d) {modify(1, num[p], num[p] + size[p] - 1, d);} inline long long subtree_query(int p) {return query(1, num[p], num[p] + size[p] - 1); } int main() { n = read(); m = read(); r = read(); mod = read(); for (register int i = 1; i <= n; ++i) a[i] = read(); for (register int i = 1; i < n; ++i) x = read(), y = read(), Add(x, y), Add(y, x); dfs1(r, 0), dfs2(r, r), build(1, 1, n); for (register int i = 1; i <= m; ++i) { opr = read(), x = read(); if (opr == 1) {y = read(), z = read(); chain_add(x, y, z);} if (opr == 2) {y = read(); printf("%lld\n", chain_query(x, y));} if (opr == 3) {z = read(); subtree_add(x, z);} if (opr == 4) printf("%lld\n", subtree_query(x)); } return 0; }

例题

SDOI2001 染色 博客链接:染色

LNOI2014 LCA 博客链接:LCA

最新回复(0)