线段树不要唱歌1

再唱歌给你分开在拼起来,等着吧!哈哈哈哈!!!!!!!!!!!!!!!!!!!!

合并

顾名思义,将两颗线段树合并起来。

两种做法:

1.暴力

很简单,枚举每个节点,两个节点的值按需要 pushup 即可

2.优雅的枚举

我们假设现在有两棵线段树 \(a\)\(b\)

如果当前枚举到的节点如果为叶子就直接合并。

如果以当前节点为子树的节点 \(a\)左子树\(b\)右子树,那么直接指向对应的即可。

反过来也一样。

两种建树方法:

  1. 直接在原树上合并,但会丢失原树的信息。优点就是省空间
  2. 再开一个树来合并,缺点空间浪费。

复杂度:

\(\Phi\) = 所有线段树的节点总数

初始时无任何节点:\(\Phi_0 = 0\)

每次 insert 沿着值域链新建 \(O(log V)\) 个节点 → \(\Phi\) 增加 \(O(log V)\)

每次 merge 递归合并一对重合节点:

  • 叶节点:两节点合为一 → \(\Phi\) 减少 1
  • 内部节点:递归处理子节点,\(\Phi\) 不变

每次递归调用 merge 时,要么合并两个叶节点使 \(\Phi\) 减 1,要么追踪到空节点直接返回\(\Phi\) 不变)。因此:

\[\text{总递归调用次数} = (\text{叶节点合并次数}) + (\text{空节点返回次数}) \]

其中叶节点合并次数 = 所有 merge 中真正合并的叶节点对数。每次叶节点合并使 \(\Phi\) 减 1,所以:

\[\text{叶节点合并次数} = \Phi_{\text{初始}} + \sum\Delta\Phi_{\text{insert}} - \Phi_{\text{最终}} \]

代入 \(\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\) 的线段树」合并的结果。

而主席树可以复用旧节点。

总结:

  1. 可持久化数据结构

在计算机科学中,可持久化是指一种在进行修改操作时,能够保留其所有历史版本的数据结构。

  • 非可持久化:普通的修改操作会覆盖旧状态,导致无法查询过去的数据。

  • 可持久化:每次修改都会生成一个新的独立版本,且旧版本依然完全可用。从线段树合并的角度来看,它本质上是利用了“新建节点代替修改”的原则,使得每次操作后的新树与旧树独立共存。

  1. 核心优化思想:状态复用

要在极高效率下实现可持久化,不能每次都全盘复制(否则会导致 MLE 和 TLE)。其核心巧妙之处在于共用节点

\(i\) 棵树(插入前缀 [1, i])相对于第 \(i-1\) 棵树,仅仅多插入了一个元素 a[i]

由于线段树的高度为 \(\log N\),一次单点插入最多只会改变一条从根到叶子的路径上的 \(O(\log N)\) 个节点。

对于没有发生改变的分支,新树直接指向前一个版本(树 \(i-1\))的对应节点。这样每次新增版本的空间和时间开销都被严格控制在 \(O(\log N)\)

  1. 差分

为了解决任意区间 \([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];
	}
}
posted @ 2026-07-06 21:05  OiLight  阅读(23)  评论(2)    收藏  举报