线段树不要唱歌1
再唱歌给你分开在拼起来,等着吧!哈哈哈哈!!!!!!!!!!!!!!!!!!!!
合并
顾名思义,将两颗线段树合并起来。
两种做法:
1.暴力
很简单,枚举每个节点,两个节点的值按需要 pushup 即可
2.优雅的枚举
我们假设现在有两棵线段树 \(a\) 和 \(b\)
如果当前枚举到的节点如果为叶子就直接合并。
如果以当前节点为子树的节点 \(a\) 有左子树, \(b\) 有右子树,那么直接指向对应的即可。
反过来也一样。
两种建树方法:
- 直接在原树上合并,但会丢失原树的信息。优点就是省空间
- 再开一个树来合并,缺点空间浪费。
复杂度:
设 \(\Phi\) = 所有线段树的节点总数。
初始时无任何节点:\(\Phi_0 = 0\) 。
每次 insert 沿着值域链新建 \(O(log V)\) 个节点 → \(\Phi\) 增加 \(O(log V)\)。
每次 merge 递归合并一对重合节点:
- 叶节点:两节点合为一 → \(\Phi\) 减少 1
- 内部节点:递归处理子节点,\(\Phi\) 不变
每次递归调用 merge 时,要么合并两个叶节点使 \(\Phi\) 减 1,要么追踪到空节点直接返回(\(\Phi\) 不变)。因此:
其中叶节点合并次数 = 所有 merge 中真正合并的叶节点对数。每次叶节点合并使 \(\Phi\) 减 1,所以:
代入 \(\Phi_{\text{初始}} = 0\)、\(\sum\Delta\Phi_{\text{insert}} = \text{O}(n \log V)\)、\(\Phi_{\text{最终}} \ge 0\),得总 merge 开销 = \(O(n log V)\)。
分裂
掰开
线段树分裂是线段树合并的逆操作——将一棵权值线段树按值域或按排名拆分为两棵。
核心思想
将一棵权值线段树按某个分界值或分界排名分裂成两棵:
- 一棵包含值 \(\le k\) 的元素
- 另一棵包含值 \(> k\) 的元素(或按排名分裂)
三种分裂操作
按值分裂
给定阈值 \(k\),将树 \(id\) 分裂为 \(id\)(值 \(\le k\))和 \(v\)(值 \(> k\))。
关键点:
- 递归每一层只走一个方向,复杂度 \(O(\log V)\)
- 当子树全部在一侧时,直接转让指针 \(O(1)\),不递归子树内部
- 跨越分界点的节点才需要"掰开",新建节点也在这些位置
按排名分裂
给定排名 \(k\)(前 \(k\) 小的元素),将树分裂为 \(id\)(前 \(k\) 小)和 \(v\)(剩余部分)。
使用左子树的大小决定递归方向,类似权值线段树的 \(k-th\) 操作 + 分裂。
按值域区间分裂
给定区间 \([ql, qr]\),将 \(id\) 中值在 \([ql, qr]\) 内的部分全部剥离出来成为新树 \(v\)。
- 完全在区间内 \(\to\) 整棵子树直接转移
- 部分重叠 \(\to\) 递归分裂左右
- 完全不在区间内 \(\to\) 保持不动
可持久化线段树(主席树)略解
别人都是详解,那我来波略解。
今天我带你 2 hours 苏桐主席树。
例题:区间第 \(k\) 小
题目描述:
给定一个长度为 \(N\) 的数组 \(A\),\(M\) 次询问区间 \([l, r]\) 里第 \(k\) 小的数。
我们想到权值线段树可以查第 \(k\) 小!那我们能否利用一下呢?
我们参考前缀和思想:求出 \([1,r]\) 区间又能求出 \([1,l]\) 区间,所以就能求出 \([l,r]\) 了!
接下来重点考虑如何建立每个前缀的权值线段树!
暴力建树会导致MLE!QwQ
注意到:树 \(i\) 和树 \(i-1\) 其实就差一个 \(a_i\)。可以把树 \(i\) 看成是「树 \(i-1\)」和「只包含 \(a_i\) 的线段树」合并的结果。
而主席树可以复用旧节点。
总结:
- 可持久化数据结构
在计算机科学中,可持久化是指一种在进行修改操作时,能够保留其所有历史版本的数据结构。
-
非可持久化:普通的修改操作会覆盖旧状态,导致无法查询过去的数据。
-
可持久化:每次修改都会生成一个新的独立版本,且旧版本依然完全可用。从线段树合并的角度来看,它本质上是利用了“新建节点代替修改”的原则,使得每次操作后的新树与旧树独立共存。
- 核心优化思想:状态复用
要在极高效率下实现可持久化,不能每次都全盘复制(否则会导致 MLE 和 TLE)。其核心巧妙之处在于共用节点:
第 \(i\) 棵树(插入前缀 [1, i])相对于第 \(i-1\) 棵树,仅仅多插入了一个元素 a[i]。
由于线段树的高度为 \(\log N\),一次单点插入最多只会改变一条从根到叶子的路径上的 \(O(\log N)\) 个节点。
对于没有发生改变的分支,新树直接指向前一个版本(树 \(i-1\))的对应节点。这样每次新增版本的空间和时间开销都被严格控制在 \(O(\log N)\)。
- 差分
为了解决任意区间 \([L, R]\) 的查询问题,引入了类似“前缀和”的思想:
因为权值线段树维护的是每个值域区间内数字出现的频次(Count/Sum),这种频次满足可减性。要想知道原数组区间 \([L, R]\) 内各个数字的分布情况,只需要用“第 \(R\) 个历史版本(即前缀 \([1, R]\))的线段树”减去“第 \(L-1\) 个历史版本(即前缀 \([1, L-1]\))的线段树”即可。
剩下的就非常 easy 了!
戳我看 code 喵~
int n, m;
int a[N], b[N];
int rt[N];
struct Tree {
int ls, rs, val;
}tree[N << 5];
int tot;
#define ls(p) tree[p].ls
#define rs(p) tree[p].rs
int build(int p, int l, int r) {
p = ++tot;
if (l == r) {
tree[p].val = a[l];
return p;
}
int mid = l + r >> 1;
ls(p) = build(ls(p), l, mid), rs(p) = build(rs(p), mid + 1, r);
return p;
}
int update(int p, int l, int r, int pos, int val) {
int X = ++tot; tree[X] = tree[p];
if (l == r) {
tree[X].val = val;
return X;
}
int mid = l + r >> 1;
if (pos <= mid) ls(X) = update(ls(X), l, mid, pos, val);
else rs(X) = update(rs(X), mid + 1, r, pos, val);
return X;
}
int query(int p, int l, int r, int k) {
if (l == r) return tree[p].val;
int mid = l + r >> 1;
if (k <= mid) return query(ls(p), l, mid, k);
else return query(rs(p), mid + 1, r, k);
}
signed main() {
int n = re, m = re;
for (int i = 1; i <= n; i++) a[i] = re;
rt[0] = build(0, 1, n);
for (int i = 1; i <= m; i++) {
int ver = re, op = re, pos = re;
if (op == 1) rt[i] = update(rt[ver], 1, n, pos, re);
else wr(query(rt[ver], 1, n, pos)), endl, rt[i] = rt[ver];
}
}

浙公网安备 33010602011771号