【学习笔记】线段树合并
前言
线段树合并是指对于多棵区间相同的线段树,将他们的信息合并到一棵线段树上。
每次合并两棵,合并的结果就附在其中一棵线段树上。
看起来挺神秘的,其实也不难。
前置知识
权值线段树
我们知道线段树维护的是区间上的信息,如最大值、最小值、区间和等信息。它维护的对象是数组,一般情况下,线段树维护的区间是数组下标对应的区间。
而权值线段树是指以值域为区间建立的线段树,它维护的区间是一段值域。所以称为权值线段树。
动态开点线段树
因为维护的是值域,而值域可能范围很大,且值域中许多位置是空的,没有对应的元素。我们只需要将存在的元素插入到线段树中即可。
此时,叶节点的数量就是有效的元素值的数量。每个叶节点的祖先最多 \(\log m\) 个,这棵线段树的节点数量也不超过 \(n\log m\)。其中 \(n\) 是元素个数,\(m\) 是值域大小。
当然,如果在此基础上使用离散化优化的话,常数可以变得小一些,即结点数量会从 \(n\log m\) 降到 \(n\log n\)。
动态开点线段树已经不符合完全二叉树的性质了,所以,不能再用 \(i\times 2\) 表示 \(i\) 的左儿子,\(i\times 2 + 1\) 表示 \(i\) 的右儿子了。
因此我们需要对每个结点另存两个变量:每个结点的左儿子和右儿子。
struct node {
int l, r, lc, rc, cnt;
// l, r 表示当前结点控制的区间 [l, r]
// lc, rc 表示当前结点的左儿子与右儿子
// cnt 表示当前结点的出现次数
};
那么到底应该如何去理解这个过程呢?下面这个例子或许可以帮到你。
给定一个 \(n\),请画出 \(1\sim 1,1\sim 2,1\sim 3,...,1\sim n\) 的线段树。
为方便理解,这里我们假设 \(n=4\)。
那么我们将会画出下面的图:

这里我们需要使用的结点其实只有 \(4\) 个,但是我们却耗费了 \(10\) 个结点的空间,非常浪费。
因此我们可以考虑在建立前一棵线段树的基础上再多加一条链,这样就可以避免空间的极度浪费了。
如图,这样就可以节省很多空间了:

线段树合并
一般情况
考虑将两棵线段树合并,具体而言,如下:
- 如果两颗线段树都有共同的一个结点,那么将两颗线段树在这个结点上的信息进行合并
- 如果两颗线段树中只有一棵线段树有一个结点,而另一个线段树没有,那么就将新的线段树连向这个结点
举个例子,这里的合并就用权值相加来理解:

复杂度分析
将两个线段树合并时,遇到空节点直接返回,也就是只有两个线段树的重合部分会被 Dfs 到。所以总的合并次数不会超过较小的线段树的节点数。即单次合并复杂度不会超过 \(O(n\log n)\)。
如果进行了很多次合并,那么时间复杂度依然是 \(O(n\log n)\),原因是如果将两个线段树合并,事实上不会增加新的节点,而较小的线段树永远的消失了,也就是对于每一棵线段树,在合并一次后,要么对方消失,要么自己消失。即所有的节点都只会被合并一次。那么无论合并多少次,总的时间复杂度就是 \(O(n\log n)\)。
由上可知,空间复杂度也为 \(O(n\log n)\),不过具体大小随题目的变换会有常数上的变动。
树上合并
和一般情况其实并没有什么区别。用树上前缀和的思想,将一个节点所有儿子对应的线段树合并,然后再和它自身合并即可。
本题解法
对于每一个节点,开一颗权值线段树,维护值域最大值。修改时利用树上差分进行修改。然后进行树上前缀和合并即可。
时空复杂度 \(O(n\log n)\),注意空间有四倍的常数。
代码实现
#include <bits/stdc++.h>
#define ll long long
#define lson tree[id].ls
#define rson tree[id].rs
#define mid ((l + r) / 2)
using namespace std;
const int N = 1e5 + 10;
const int R = 1e5;
int n, m;
int ccnt; // 动态开点计数器
int root[N * 80]; // 以 i 号节点为根的权值树的根节点编号
int deep[N], fa[N][20];
int ans[N];
vector<int> linker[N];
// 权值树节点结构
struct node {
int ls, rs; // 左右儿子
int sum, lei; // 最大值的大小和种类
} tree[N * 80]; // 权值树数组
void add(int u, int v) {
linker[u].push_back(v);
}
// LCA 预处理
void init(int now, int father) {
fa[now][0] = father;
deep[now] = deep[father] + 1;
for (int i = 1; i <= 18; i++)
fa[now][i] = fa[fa[now][i - 1]][i - 1];
for (int v : linker[now]) {
if (v == father) continue;
init(v, now);
}
return ;
}
int lca(int u, int v) {
if (deep[u] != deep[v]) {
if (deep[u] < deep[v]) swap(u, v);
for (int i = 18; i >= 0; i--)
if (deep[fa[u][i]] >= deep[v])
u = fa[u][i];
}
if (u == v) return u;
for (int i = 18; i >= 0; i--) {
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
// 向上更新
void pushup(int id) {
if (lson == 0) {
tree[id].sum = tree[rson].sum;
tree[id].lei = tree[rson].lei;
return ;
}
if (rson == 0) {
tree[id].sum = tree[lson].sum;
tree[id].lei = tree[lson].lei;
return ;
}
// 如果相等,则选左儿子,因为左儿子的类型绝对小于右儿子
if (tree[lson].sum >= tree[rson].sum) {
tree[id].sum = tree[lson].sum;
tree[id].lei = tree[lson].lei;
} else {
tree[id].sum = tree[rson].sum;
tree[id].lei = tree[rson].lei;
}
}
// 单点更新:某个节点上增加一袋 v 类型的救济粮
void change(int &id, int l, int r, int v, int val) {
if (!id) id = ++ccnt;
if (l == r) {
tree[id].sum += val; // l == r 时,此区间只有一种救济粮,可以直接相加
tree[id].lei = v; // 类型变化
return ;
}
if (v <= mid) change(lson, l, mid, v, val);
else change(rson, mid + 1, r, v, val);
pushup(id);
}
// 合并两棵权值树:将 v 合并到 u 上
int merge(int u, int v, int l, int r) {
if (!u || !v) return u + v;
if (l == r) {
tree[u].sum += tree[v].sum; // 信息合并
tree[u].lei = l; // l == r时,此区间只有一种救济粮,可以直接相加
return u;
}
// 递归合并左右子树
tree[u].ls = merge(tree[u].ls, tree[v].ls, l, mid);
tree[u].rs = merge(tree[u].rs, tree[v].rs, mid + 1, r);
pushup(u);
return u;
}
// 统计答案:后序遍历合并子树的权值树
void cacl(int id, int fa) {
for (int v : linker[id]) {
if (v == fa) continue;
cacl(v, id);
root[id] = merge(root[id], root[v], 1, R);
}
// 处理当前节点的答案
if (tree[root[id]].sum == 0) ans[id] = 0;
else ans[id] = tree[root[id]].lei;
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
cin >> n >> m;
// 建边
for (int i = 1, a, b; i < n; i++) {
cin >> a >> b;
add(a, b), add(b, a);
}
// LCA 预处理
init(1, 0);
// 处理修改操作(树上差分)
for (int i = 1, x, y, z; i <= m; i++) {
cin >> x >> y >> z;
change(root[x], 1, R, z, 1);
change(root[y], 1, R, z, 1);
change(root[lca(x, y)], 1, R, z, -1);
change(root[fa[lca(x, y)][0]], 1, R, z, -1);
}
// 统计答案
cacl(1, 0);
for (int i = 1; i <= n; i++)
cout << ans[i] << "\n";
return 0;
}
CF600E Lomsat gelral
题意:求以 \(1\) 为根时,每个子树中出现次数最多的颜色编号之和。

浙公网安备 33010602011771号