树链剖分
概念
树链剖分是把一颗树分割成若干条链,用来维护链上信息的一种算法,常配合线段树,树状数组,平衡树使用。
重链剖分
基本定义
为了更好的理解,我们先给出一些定义:
- 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;
}
长链剖分
与重链剖分类似,把重儿子的定义换为某节点子结点中子树深度最大的子结点,其余类似,不在赘述

浙公网安备 33010602011771号