线段树合并
适用位置
线段树的合并与分裂是线段树的常用技巧,常见于权值线段树维护可重集的场景。
例如,树上某些结点处有若干操作,如果需要自下而上地将子节点信息传递给亲节点,而单个结点处的信息又方便用线段树维护时,就可以应用线段树合并的技巧控制整体的复杂度。
例如下面这个模板题
P4556 [Vani有约会] 雨天的尾巴 /【模板】线段树合并
题目背景
深绘里一直很讨厌雨天。
灼热的天气穿透了前半个夏天,后来一场大雨和随之而来的洪水,浇灭了一切。
虽然深绘里家乡的小村落对洪水有着顽固的抵抗力,但也倒了几座老房子,几棵老树被连根拔起,以及田地里的粮食被弄得一片狼藉。
无奈的深绘里和村民们只好等待救济粮来维生。
不过救济粮的发放方式很特别。
题目描述
村落里一共有 \(n\) 座房屋,并形成一个树状结构。然后救济粮分 \(m\) 次发放,每次选择两个房屋 \((x, y)\),然后对于 \(x\) 到 \(y\) 的路径上(含 \(x\) 和 \(y\))每座房子里发放一袋 \(z\) 类型的救济粮。
然后深绘里想知道,当所有的救济粮发放完毕后,每座房子里存放的最多的是哪种救济粮。
输入格式
输入的第一行是两个用空格隔开的正整数,分别代表房屋的个数 \(n\) 和救济粮发放的次数 \(m\)。
第 \(2\) 到 第 \(n\) 行,每行有两个用空格隔开的整数 \(a, b\),代表存在一条连接房屋 \(a\) 和 \(b\) 的边。
第 \((n + 1)\) 到第 \((n + m)\) 行,每行有三个用空格隔开的整数 \(x, y, z\),代表一次救济粮的发放是从 \(x\) 到 \(y\) 路径上的每栋房子发放了一袋 \(z\) 类型的救济粮。
输出格式
输出 \(n\) 行,每行一个整数,第 \(i\) 行的整数代表 \(i\) 号房屋存放最多的救济粮的种类,如果有多种救济粮都是存放最多的,输出种类编号最小的一种。
如果某座房屋没有救济粮,则输出 \(0\)。
输入输出样例 #1
输入 #1
5 3
1 2
3 1
3 4
5 3
2 3 3
1 5 2
3 3 3
输出 #1
2
3
3
0
2
说明/提示
- 对于 \(20\%\) 的数据,保证 \(n, m \leq 100\)。
- 对于 \(50\%\) 的数据,保证 \(n, m \leq 2 \times 10^3\)。
- 对于 \(100\%\) 测试数据,保证 \(1 \leq n, m \leq 10^5\),\(1 \leq a,b,x,y \leq n\),\(1 \leq z \leq 10^5\)。
题意分析
这个题目要我们处理一个树上差分问题,但是现在有一个问题是,树上同一个节点需要维护多个差分数组,所以我们维护多个线段树,因为我们只需要找到最大的那一个,所以我们可以对线段树进行合并。
思路过程
分析一下变量
// 全局变量定义
i64 n, m; // n表示节点数,m表示操作数
i64 ans[100005]; // 存储每个节点的答案(出现次数最多的颜色)
vector<i64> t[200005]; // 存储树的邻接表
i64 fa[200005][63]; // 用于LCA算法的倍增表,fa[x][k]表示x的2^k倍祖先
i64 dep[200005]; // 存储每个节点的深度
i64 rt[200005]; // 每个节点对应的动态开点线段树根节点
// 动态开点线段树相关数组
i64 sum[5000005]; // 存储线段树节点对应的颜色出现次数
i64 cnt = 0; // 线段树节点计数器,用于动态分配节点
i64 res[500005]; // 存储线段树节点对应的出现次数最多的颜色
i64 ls[5000005]; // 线段树节点的左孩子
i64 rs[5000005]; // 线段树节点的右孩子
对于更新操作,我们让当前节点保存出现次数最多的颜色
// 更新线段树节点信息,确保当前节点保存的是出现次数最多的颜色
void update(i64 p) {
// 比较左右子树的颜色出现次数,取较大的一方
if (sum[ls[p]] < sum[rs[p]]) {
res[p] = res[rs[p]]; // 记录右子树中出现次数最多的颜色
sum[p] = sum[rs[p]]; // 记录最大出现次数
}
else {
res[p] = res[ls[p]]; // 记录左子树中出现次数最多的颜色
sum[p] = sum[ls[p]]; // 记录最大出现次数
}
}
首先我们需要,做一个树上差分,先写一个lca倍增算法
// 深度优先搜索,预处理LCA所需的倍增表和节点深度
void dfs(i64 x) {
// 预处理倍增表,计算x的2^k倍祖先
for (int i = 0; i <= 20; i++) {
fa[x][i + 1] = fa[fa[x][i]][i];
}
// 遍历x的所有邻接节点
for (auto it : t[x]) {
// 跳过父节点,避免重复访问
if (it != fa[x][0]) {
dep[it] = dep[x] + 1; // 设置子节点深度
fa[it][0] = x; // 设置子节点的直接父节点
dfs(it); // 递归处理子节点
}
}
}
// 计算两个节点x和y的最近公共祖先(LCA)
i64 lca(i64 x, i64 y) {
// 确保x的深度不小于y,方便后续处理
if (dep[x] < dep[y]) {
swap(x, y);
}
// 将x上移到与y相同的深度
for (i64 len = dep[x] - dep[y], w = 0; len; len >>= 1, w++) {
if (len & 1) { // 如果二进制第w位为1,则上移2^w步
x = fa[x][w];
}
}
// 如果x和y已经相同,说明此时x(y)就是LCA
if (x == y) {
return x;
}
// 从最大的步数开始尝试,找到LCA的前一步
for (i64 i = 20; i >= 0; i--) {
if (fa[x][i] != fa[y][i]) {
x = fa[x][i];
y = fa[y][i];
}
}
// 最终LCA是x的直接父节点
return fa[x][0];
}
然后我们动态开点建一个线段树,(为什么要用动态开点呢,因为我们要维护1e5个线段树,不然会爆内存)
我们用每一个节点代表这个救济粮的根节点。
// 向动态开点线段树中添加(或修改)颜色的出现次数
// 参数:p-当前节点,x,y-当前区间,co-颜色值,val-增减的数量
i64 add(i64 p, i64 x, i64 y, i64 co, i64 val) {
// 如果当前节点为空,动态创建一个新节点
if (!p) p = ++cnt;
// 如果到达叶子节点(对应具体颜色),更新该颜色的出现次数
if (x == y) {
sum[p] += val; // 更新出现次数
res[p] = co; // 记录当前颜色
return p; // 返回当前节点
}
// 计算中间点,划分左右区间
i64 mid = (x + y) >> 1;
// 根据颜色值决定递归左子树还是右子树
if (co <= mid) {
ls[p] = add(ls[p], x, mid, co, val);
}
else {
rs[p] = add(rs[p], mid + 1, y, co, val);
}
// 更新当前节点信息
update(p);
return p; // 返回当前节点
}
我们现在对于每一个颜色节点都建立了一颗树,并且动态的开出了他的子节点。
现在我们对这些线段树进行合并操作
// 合并两棵动态开点线段树,将y合并到x中
// 参数:x,y-待合并的两棵树的根节点,x1,x2-当前区间范围
i64 merge(i64 x, i64 y, i64 x1, i64 x2) {
// 如果其中一棵树为空,直接返回另一棵树
if (!x) return y;
if (!y) return x;
// 如果到达叶子节点,合并颜色出现次数
if (x1 == x2) {
sum[x] += sum[y]; // 累加出现次数
return x; // 返回合并后的节点
}
// 计算中间点,划分左右区间
i64 mid = (x1 + x2) >> 1;
// 递归合并左右子树
ls[x] = merge(ls[x], ls[y], x1, mid);
rs[x] = merge(rs[x], rs[y], mid + 1, x2);
// 更新当前节点信息
update(x);
return x; // 返回合并后的根节点
}
// 后序遍历计算每个节点的答案
void cacl(i64 x) {
// 遍历x的所有子节点
for (auto it : t[x]) {
if (it != fa[x][0]) { // 跳过父节点
cacl(it); // 递归处理子节点
// 将子节点的线段树合并到当前节点
rt[x] = merge(rt[x], rt[it], 1, 100000);
}
}
// 记录当前节点的答案(出现次数最多的颜色)
ans[x] = res[rt[x]];
// 如果没有颜色出现(或总次数为0),答案设为0
if (sum[rt[x]] == 0) ans[x] = 0;
}
AC代码
#include <iostream>
#include <string>
#include <vector>
#include <algorithm>
using namespace std;
// 定义64位整数类型,用于处理可能的大数值
using i64 = long long;
// 全局变量定义
i64 n, m; // n表示节点数,m表示操作数
i64 ans[100005]; // 存储每个节点的答案(出现次数最多的颜色)
vector<i64> t[200005]; // 存储树的邻接表
i64 fa[200005][63]; // 用于LCA算法的倍增表,fa[x][k]表示x的2^k倍祖先
i64 dep[200005]; // 存储每个节点的深度
i64 rt[200005]; // 每个节点对应的动态开点线段树根节点
// 动态开点线段树相关数组
i64 sum[5000005]; // 存储线段树节点对应的颜色出现次数
i64 cnt = 0; // 线段树节点计数器,用于动态分配节点
i64 res[500005]; // 存储线段树节点对应的出现次数最多的颜色
i64 ls[5000005]; // 线段树节点的左孩子
i64 rs[5000005]; // 线段树节点的右孩子
// 深度优先搜索,预处理LCA所需的倍增表和节点深度
void dfs(i64 x) {
// 预处理倍增表,计算x的2^k倍祖先
for (int i = 0; i <= 20; i++) {
fa[x][i + 1] = fa[fa[x][i]][i];
}
// 遍历x的所有邻接节点
for (auto it : t[x]) {
// 跳过父节点,避免重复访问
if (it != fa[x][0]) {
dep[it] = dep[x] + 1; // 设置子节点深度
fa[it][0] = x; // 设置子节点的直接父节点
dfs(it); // 递归处理子节点
}
}
}
// 计算两个节点x和y的最近公共祖先(LCA)
i64 lca(i64 x, i64 y) {
// 确保x的深度不小于y,方便后续处理
if (dep[x] < dep[y]) {
swap(x, y);
}
// 将x上移到与y相同的深度
for (i64 len = dep[x] - dep[y], w = 0; len; len >>= 1, w++) {
if (len & 1) { // 如果二进制第w位为1,则上移2^w步
x = fa[x][w];
}
}
// 如果x和y已经相同,说明此时x(y)就是LCA
if (x == y) {
return x;
}
// 从最大的步数开始尝试,找到LCA的前一步
for (i64 i = 20; i >= 0; i--) {
if (fa[x][i] != fa[y][i]) {
x = fa[x][i];
y = fa[y][i];
}
}
// 最终LCA是x的直接父节点
return fa[x][0];
}
// 更新线段树节点信息,确保当前节点保存的是出现次数最多的颜色
void update(i64 p) {
// 比较左右子树的颜色出现次数,取较大的一方
if (sum[ls[p]] < sum[rs[p]]) {
res[p] = res[rs[p]]; // 记录右子树中出现次数最多的颜色
sum[p] = sum[rs[p]]; // 记录最大出现次数
}
else {
res[p] = res[ls[p]]; // 记录左子树中出现次数最多的颜色
sum[p] = sum[ls[p]]; // 记录最大出现次数
}
}
// 向动态开点线段树中添加(或修改)颜色的出现次数
// 参数:p-当前节点,x,y-当前区间,co-颜色值,val-增减的数量
i64 add(i64 p, i64 x, i64 y, i64 co, i64 val) {
// 如果当前节点为空,动态创建一个新节点
if (!p) p = ++cnt;
// 如果到达叶子节点(对应具体颜色),更新该颜色的出现次数
if (x == y) {
sum[p] += val; // 更新出现次数
res[p] = co; // 记录当前颜色
return p; // 返回当前节点
}
// 计算中间点,划分左右区间
i64 mid = (x + y) >> 1;
// 根据颜色值决定递归左子树还是右子树
if (co <= mid) {
ls[p] = add(ls[p], x, mid, co, val);
}
else {
rs[p] = add(rs[p], mid + 1, y, co, val);
}
// 更新当前节点信息
update(p);
return p; // 返回当前节点
}
// 合并两棵动态开点线段树,将y合并到x中
// 参数:x,y-待合并的两棵树的根节点,x1,x2-当前区间范围
i64 merge(i64 x, i64 y, i64 x1, i64 x2) {
// 如果其中一棵树为空,直接返回另一棵树
if (!x) return y;
if (!y) return x;
// 如果到达叶子节点,合并颜色出现次数
if (x1 == x2) {
sum[x] += sum[y]; // 累加出现次数
return x; // 返回合并后的节点
}
// 计算中间点,划分左右区间
i64 mid = (x1 + x2) >> 1;
// 递归合并左右子树
ls[x] = merge(ls[x], ls[y], x1, mid);
rs[x] = merge(rs[x], rs[y], mid + 1, x2);
// 更新当前节点信息
update(x);
return x; // 返回合并后的根节点
}
// 后序遍历计算每个节点的答案
void cacl(i64 x) {
// 遍历x的所有子节点
for (auto it : t[x]) {
if (it != fa[x][0]) { // 跳过父节点
cacl(it); // 递归处理子节点
// 将子节点的线段树合并到当前节点
rt[x] = merge(rt[x], rt[it], 1, 100000);
}
}
// 记录当前节点的答案(出现次数最多的颜色)
ans[x] = res[rt[x]];
// 如果没有颜色出现(或总次数为0),答案设为0
if (sum[rt[x]] == 0) ans[x] = 0;
}
int main() {
// 加速输入输出
ios::sync_with_stdio(false);
// 读取节点数和操作数
cin >> n >> m;
// 读取树的边,构建邻接表
for (i64 i = 1; i < n; i++) {
i64 x, y;
cin >> x >> y;
t[x].push_back(y);
t[y].push_back(x);
}
// 初始化LCA相关信息(从根节点1开始)
dfs(1);
// 处理m次操作
for (i64 i = 1; i <= m; i++) {
i64 x, y, z;
cin >> x >> y >> z; // 读取路径端点和颜色
// 在x和y处增加颜色z的出现次数
rt[x] = add(rt[x], 1, 100000, z, 1);
rt[y] = add(rt[y], 1, 100000, z, 1);
// 计算x和y的LCA
i64 lc = lca(x, y);
// 在LCA处和LCA的父节点处减少颜色z的出现次数(差分思想)
rt[lc] = add(rt[lc], 1, 100000, z, -1);
rt[fa[lc][0]] = add(rt[fa[lc][0]], 1, 100000, z, -1);
}
// 计算每个节点的答案
cacl(1);
// 输出所有节点的答案
for (i64 i = 1; i <= n; i++) {
cout << ans[i] << endl;
}
return 0;
}

浙公网安备 33010602011771号