树链剖分问题
定义
树链剖分是一种对树进行划分的算法,将树分为多条链子,保证每个点只属于一条链,然后再通过数据结构(树状数组,BST,SPLAY,线段树等)来维护每一条链。
分类
一般分为两种,重链剖分和长链剖分
重链剖分:
定义
重儿子:在该节点的儿子中,子数数量最多的节点(如果存在多个最大的,选其一即可)(叶子节点无重儿子)
轻儿子:在该节点的儿子中,除重儿子之外的节点
重边:节点和其重儿子连成的边
轻边:节点和其轻儿子连成的边
重链:多条重边连成的路径
轻链:多条轻边连成的路径
把落单的结点也当作重链,那么整棵树就被剖分成若干条重链。
如下图所示

实现
那么如何实现剖分成重链的过程呢
我们首先进行一次dfs,记录标记每个点的深度dep,[]标记每个点的父亲fa[]
标记每个非叶子节点的子树大小(含它自己),标记每个非叶子节点的重儿子编号son[]
代码如下:
void dfs(i64 now , i64 f , i64 deep)//分别表示现在在哪个节点,父亲是谁,深度为多少
{
dep [ now ] = deep;//记录深度
fa [ now ] = f;//记录父节点
siz [ now ] = 1;//初始化子树大小
i64 maxson = -1;
for (auto it : t [ now ]) {
if (it != f) {
dfs(it , now , deep + 1);
siz [ now ] += siz [ it ];//计算子树大小
if (siz [ it ] > maxson) {
son[now]=it;//找到重儿子
maxson = siz [ it ];
}
}
}
}
接下来我们还得进行一次dfs,用我们之前记录的信息去构造重链
完成
1.标记每个点的新编号
2.赋值每个点的初始值到新编号上
3.处理每个点所在链的顶端
4.处理每条链
代码如下
void dfss(i64 now,i64 ttop) //now表示现在的点,ttop表示链顶
{
id [now ] = ++cnt;//更新点的编号
top [ now ] = ttop;//更新top数组
neww [ cnt ] = w [ now ];//将原点的值赋值给新点
if (!son [ now ])return;//如果重儿子不存在,直接返回
dfss(son [ now ] , ttop);
for (auto it : t [ now ]) {
if (it == fa [ now ] || it == son [ now ])
continue;
dfss(it , it);//所有的其他儿子都是链的top,继续递归
}
}
现在我们应该用我们构造的重链去解决某些问题,以优化时间复杂度
模板题https://www.luogu.com.cn/problem/P3384
P3384 【模板】重链剖分/树链剖分
题目描述
如题,已知一棵包含 \(N\) 个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作:
-
1 x y z,表示将树从 x 到 y结点最短路径上所有节点的值都加上 z。 -
2 x y,表示求树从 x 到 y 结点最短路径上所有节点的值之和。 -
3 x z,表示将以 x 为根节点的子树内所有节点值都加上 z。 -
4 x,表示求以 x 为根节点的子树内所有节点值之和。
题目要求我们完成这几个任务
总结一下我们刚刚构造链所得到的信息
1.因为顺序是先重再轻,所以每一条重链的新编号是连续的
2.因为是dfs,所以每一个子树的新编号也是连续的

当处理第一个和第二个问题时
我们要处理任意两点间路径
设所在链顶端的深度更深的那个点为x点
ans加上x点到x所在链顶端 这一段区间的点权和
把x跳到x所在链顶端的那个点的上面一个点,不停执行这两个步骤,直到两个点处于一条链上,这时再加上此时两个点的区间和即可
如下图:假如现在要处理4和9,那么9的top深度更深,然后往上跳一个点,所以我们计算[2,4]的区间和与[6,9]的区间和就是我们的答案

这时我们注意到,我们所要处理的所有区间均为连续编号(新编号),于是想到线段树,用线段树处理连续编号区间和
每次查询时间复杂度为O(log^2n)
i64 qrange(i64 x , i64 y) {
i64 ans = 0;
while (top [ x ] != top [ y ]) {
if (dep [ x ] < dep [ y ])swap(x , y);//找到深度低的
res = 0;
query(1 , 1 , n , id [ top [ x ] ] , id [ x ]);//计算区间和
ans += res;
ans %= mod;
x = fa [ top [ x ] ];//更新x
}
if (dep [ x ] > dep [ y ])swap(x , y);//如果还没相等在计算最后一次
res = 0;
query(1 , 1 , n , id [ x ] , id [ y ]);
ans += res;
return ans%=mod;//返回查询结果
}
当处理第三个和第四个问题时、我们要处理一点及其子树的点权和:想到记录了每个非叶子节点的子树大小(含它自己),并且每个子树的新编号都是连续的,于是直接线段树区间查询即可时间复杂度为O(logn)
i64 qSon(i64 x) {
return query(1, 1, n, id[x], id[x] + siz[x] - 1);//查找树上代表[id[x], id[x] + siz[x] - 1]的区间的节点值是多少
}
区间修改:
void updRange(i64 x, i64 y, i64 k) {
k %= mod;
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
update(1, 1, n, id[top[x]], id[x], k);//不断更新深度大的到他的top这一条路径上的区间
x = fa[top[x]];
}
if (dep[x] > dep[y]) swap(x, y);
update(1, 1, n, id[x], id[y], k);//最后更新一次
}
子树修改:
void updSon(i64 x, i64 k) {
k %= mod;
update(1, 1, n, id[x], id[x] + siz[x] - 1, k); //更新[id[x],id[x]+siz[x]-1]这一段区间
}
于是我们就可以通过这道题目
查看AC代码
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
using i64 = long long; // 使用长整型防止溢出
const i64 MAXN = 500005; // 最大节点数
i64 n, m, r, p; // n-节点数, m-操作数, r-根节点, p-模数
i64 w[MAXN]; // 原始节点权值
i64 neww[MAXN]; // 重编号后的节点权值
i64 id[MAXN]; // 节点的新编号(DFS序)
i64 top[MAXN]; // 节点所在重链的顶部节点
i64 son[MAXN]; // 节点的重儿子
i64 siz[MAXN]; // 以节点为根的子树大小
i64 fa[MAXN]; // 节点的父节点
i64 dep[MAXN]; // 节点的深度
vector<i64> t[MAXN]; // 树的邻接表表示
// 线段树相关变量
i64 a[MAXN * 4]; // 线段树节点值
i64 laz[MAXN * 4]; // 线段树懒标记
i64 mod; // 模数
i64 cnt = 0; // DFS序计数器
// 线段树: 下推懒标记
void pushdown(i64 rt, i64 len) {
if (laz[rt] == 0) return;
// 将懒标记下推到左右孩子
laz[rt << 1] = (laz[rt << 1] + laz[rt]) % mod;
laz[rt << 1 | 1] = (laz[rt << 1 | 1] + laz[rt]) % mod;
// 更新左右孩子的值
a[rt << 1] = (a[rt << 1] + laz[rt] * (len - (len >> 1))) % mod;
a[rt << 1 | 1] = (a[rt << 1 | 1] + laz[rt] * (len >> 1)) % mod;
laz[rt] = 0; // 清除当前节点的懒标记
}
// 线段树: 建树
void build(i64 rt, i64 l, i64 r) {
if (l == r) {
a[rt] = neww[l] % mod; // 叶子节点赋值
return;
}
i64 mid = (l + r) >> 1;
build(rt << 1, l, mid); // 递归构建左子树
build(rt << 1 | 1, mid + 1, r); // 递归构建右子树
a[rt] = (a[rt << 1] + a[rt << 1 | 1]) % mod; // 合并左右子树信息
}
// 线段树: 区间查询
i64 query(i64 rt, i64 l, i64 r, i64 L, i64 R) {
if (L <= l && r <= R) {
return a[rt]; // 当前区间完全包含在查询区间内
}
pushdown(rt, r - l + 1); // 下推懒标记
i64 mid = (l + r) >> 1;
i64 res = 0;
if (L <= mid) res = (res + query(rt << 1, l, mid, L, R)) % mod; // 查询左子树
if (R > mid) res = (res + query(rt << 1 | 1, mid + 1, r, L, R)) % mod; // 查询右子树
return res;
}
// 线段树: 区间更新
void update(i64 rt, i64 l, i64 r, i64 L, i64 R, i64 k) {
if (L <= l && r <= R) {
// 当前区间完全包含在更新区间内
laz[rt] = (laz[rt] + k) % mod; // 更新懒标记
a[rt] = (a[rt] + k * (r - l + 1)) % mod; // 更新当前节点值
return;
}
pushdown(rt, r - l + 1); // 下推懒标记
i64 mid = (l + r) >> 1;
if (L <= mid) update(rt << 1, l, mid, L, R, k); // 更新左子树
if (R > mid) update(rt << 1 | 1, mid + 1, r, L, R, k); // 更新右子树
a[rt] = (a[rt << 1] + a[rt << 1 | 1]) % mod; // 合并左右子树信息
}
// 第一次DFS: 计算子树大小、深度、父节点和重儿子
void dfs1(i64 now, i64 f, i64 deep) {
dep[now] = deep; // 记录深度
fa[now] = f; // 记录父节点
siz[now] = 1; // 初始化子树大小为1(自己)
i64 maxson = -1; // 用于寻找重儿子
for (auto it : t[now]) {
if (it != f) {
dfs1(it, now, deep + 1); // 递归处理子节点
siz[now] += siz[it]; // 累加子树大小
if (siz[it] > maxson) {
son[now] = it; // 更新重儿子
maxson = siz[it];
}
}
}
}
// 第二次DFS: 进行重链剖分,分配新编号
void dfs2(i64 now, i64 ttop) {
id[now] = ++cnt; // 分配新编号
top[now] = ttop; // 记录所在重链的顶部
neww[cnt] = w[now]; // 记录新编号对应的权值
if (!son[now]) return; // 没有重儿子(叶子节点)则返回
dfs2(son[now], ttop); // 优先处理重儿子(保证重链编号连续)
for (auto it : t[now]) {
// 处理轻儿子(开启新的重链)
if (it == fa[now] || it == son[now]) continue;
dfs2(it, it); // 轻儿子的顶部是自己
}
}
// 查询树上两点路径上的权值和
i64 qRange(i64 x, i64 y) {
i64 ans = 0;
// 当两点不在同一条重链上时
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y); // 让x所在重链顶部更深
// 查询当前重链的贡献
ans = (ans + query(1, 1, n, id[top[x]], id[x])) % mod;
x = fa[top[x]]; // 跳到上一条重链
}
// 两点已在同一条重链上
if (dep[x] > dep[y]) swap(x, y); // 保证x是更浅的节点
ans = (ans + query(1, 1, n, id[x], id[y])) % mod; // 查询最后一段
return ans;
}
// 查询子树权值和
i64 qSon(i64 x) {
// 子树在DFS序上是连续的[id[x], id[x]+siz[x]-1]
return query(1, 1, n, id[x], id[x] + siz[x] - 1);
}
// 更新树上两点路径上的权值
void updRange(i64 x, i64 y, i64 k) {
k %= mod;
// 类似查询操作,分段更新
while (top[x] != top[y]) {
if (dep[top[x]] < dep[top[y]]) swap(x, y);
update(1, 1, n, id[top[x]], id[x], k); // 更新当前重链
x = fa[top[x]]; // 跳到上一条重链
}
if (dep[x] > dep[y]) swap(x, y);
update(1, 1, n, id[x], id[y], k); // 更新最后一段
}
// 更新子树权值
void updSon(i64 x, i64 k) {
k %= mod;
// 子树在DFS序上是连续的[id[x], id[x]+siz[x]-1]
update(1, 1, n, id[x], id[x] + siz[x] - 1, k);
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
// 输入数据
cin >> n >> m >> r >> p;
mod = p;
for (int i = 1; i <= n; ++i) {
cin >> w[i]; // 节点权值
}
for (int i = 1; i < n; ++i) {
i64 x, y;
cin >> x >> y;
t[x].push_back(y); // 构建树结构
t[y].push_back(x);
}
// 树链剖分预处理
dfs1(r, 0, 1); // 第一次DFS
dfs2(r, r); // 第二次DFS
build(1, 1, n); // 构建线段树
// 处理操作
while (m--) {
i64 op, x, y, z;
cin >> op;
if (op == 1) {
// 路径更新: x到y路径上所有节点加z
cin >> x >> y >> z;
updRange(x, y, z);
} else if (op == 2) {
// 路径查询: x到y路径上所有节点权值和
cin >> x >> y;
cout << qRange(x, y) << '\n';
} else if (op == 3) {
// 子树更新: x的子树中所有节点加z
cin >> x >> z;
updSon(x, z);
} else if (op == 4) {
// 子树查询: x的子树中所有节点权值和
cin >> x;
cout << qSon(x) << '\n';
}
}
return 0;
}

浙公网安备 33010602011771号