线段树技巧 Trick of Segment Tree
update 2026-07-07 21:16:加入可持久化线段树
update 2026-07-08 20:41:加入线段树分裂
update 2026-07-013->2026-07-014:加入线段树分治
本文的线段树在如下模版内定义:
View Code
const int maxn = 2e5 + 5;
struct Stucture {
ll value;
int leftSon, rightSon;
};
Stucture tree[maxn << 6];
#define leftson(index) tree[index].leftSon
#define rightson(index) tree[index].rightSon
int identity = 0, top = 0, rubbish[maxn << 6];
int NewNode() {
return top ? rubbish[top--] : ++identity;
}
void del(int& id) {
rubbish[++top] = id;
tree[id].value = tree[id].leftSon = tree[id].rightSon = 0;
id = 0;
}
线段树合并 | Segment Tree Merging
前言 | Preface
也是成功地学了,感觉药剂八万了
优雅的暴力 | Clever Brute Force
线段树合并其实就是一个暴力
普通的暴力思想是一步步的将线段树遍历一遍,合并叶子结点,然后一路 push up 上去
我们稍微修改一下,想到了启发式合并的思路,但是,由于这里的合并操作并不涉及到两个线段树的“秩”的相关概念,所以就变成了简化版本
但是,这样子改下来时候还是暴力,本质上还是没有改变,所以我们直接考虑这个暴力法的复杂度
(暴力的代码)
int merge(int id, int ano, int left, int right) {
if (!id || !ano) return id | ano;
if (left == right) {
tree[id].value += tree[ano].value;
return id;
}
int mid = (left + right) >> 1;
lson(id) = merge(lson(id), lson(ano), left, mid);
rson(id) = merge(rson(id), rson(ano), mid + 1, right);
pushup(id);
return id;
}
复 杂 度 分 析
这里直接使用势能法分析
定义 \(|S|\) 表示一个线段树森林的总结点数
定义势能函数 \(\Phi(i) = |S_i|\) 表示状态 \(i\) 下 \(S_i\) 的总结点数量
直接套上公式:
移一下项得到总复杂度:
由势能的性质得 \(\Phi(n) \ge 0\)
观察 update 操作,每次至多增加 \(\mathcal O(\log V)\) 的势能,因此:
得到
看起来十分的优秀,所以说它(笔者只能够想到这个词来形容了)的暴力,比莫队的 \(\mathcal O(n \sqrt n)\) 还要优雅
线段树分裂 | Segment Tree Splitting
既然线段树都可合并了,那么一棵线段树肯定可以表示成两棵线段树的“和”
至于具体的分裂方法,要视情况而定
至于分裂的作用,像P5494 【模板】线段树分裂题目要求了、P2824 [HEOI2016/TJOI2016] 排序为了实现ODT
Split By Value
void SplitByValue(int id, int &rest, int left, int right, ll k) {
if (!id) return ;
rest = NewNode();
if (left == right) {
if (left > k) {
tree[rest].value = tree[id].value; // 当前节点归 rest
tree[id].value = 0;
}
return ;
}
int mid = (left + right) >> 1;
if (k <= mid) {
rightson(rest) = rightson(id); // 右子树全部 > k,归 rest
rightson(id) = 0;
SplitByValue(leftson(id), leftson(rest), left, mid, k);
} else // 照常递归
SplitByValue(rightson(id), rightson(rest), mid + 1, right, k);
maintain(id);
maintain(rest);
}
Split By Rank
void SplitByRank(int id, int &rest, int left, int right, ll k) {
if (!id) return ;
rest = NewNode();
if (left == right) {
int save = min(k, tree[id].value); // 将 min(k, sum) 那一坨给 rest
tree[rest].value = tree[id].value - save;
tree[id].value = save;
return ;
}
int mid = (left + right) >> 1;
int iValue = tree[leftson(id)].value;
if (k > iValue) SplitByRank(rightson(id), rightson(rest), mid + 1, right, k - iValue);
else {
rightson(rest) = rightson(id); // 排名 > k 保存到 rest 上面
rightson(id) = 0;
SplitByRank(leftson(id), leftson(rest), left, mid, k);
}
maintain(id);
maintain(rest);
}
Split By Range
void SplitRange(int &id, int &rest, int left, int right, int L, int R) {
if (!id or right < L or left > R) return ;
if (L <= left and right <= R) { // 覆盖了,整棵转移
rest = id;
id = 0;
return ;
}
if (!rest) rest = NewNode();
int mid = (left + right) >> 1;
if (L <= mid) SplitRange(leftson(id), leftson(rest), left, mid, L, R);
if (R > mid) SplitRange(rightson(id), rightson(rest), mid + 1, right, L, R);
maintain(id);
maintain(rest);
}
例题
P5494 【模板】线段树分裂
就是一道纯模版,注意该开 long long 的时候就开
View Code
int kthElement(int id, ll k) {
if (tree[root[id]].value < k) return -1;
return kth_element(root[id], 1, n, k);
}
int tot = 0;
void solve() {
cin >> n >> m;
root[++tot] = NewNode();
for (int i = 1; i <= n; i++) {
cin >> arr[i];
update(root[tot], 1, n, i, arr[i]);
}
for (int i = 1; i <= m; i++) {
int opt;
cin >> opt;
if (opt == 0) {
int p, x, y;
cin >> p >> x >> y;
int newid = ++tot;
SplitRange(root[p], root[newid], 1, n, x, y);
} else if (opt == 1) {
int p, t;
cin >> p >> t;
root[p] = merge(root[p], root[t], 1, n);
root[t] = 0;
} else if (opt == 2) {
int p, x, q;
cin >> p >> x >> q;
update(root[p], 1, n, q, x);
} else if (opt == 3) {
int p, x, y;
cin >> p >> x >> y;
cout << query(root[p], 1, n, x, y) << "\n";
} else {
int p;
ll k;
cin >> p >> k;
cout << kthElement(p, k) << "\n";
}
}
}
P2824 [HEOI2016/TJOI2016] 排序
本题有两个解法
二分
二分 q 的位置上面的数字,假设是 x
将整个序列中 \(\le x\) 的数字改成 \(0\),而 \(\gt x\) 的改成 \(1\)
然后当 q 的位置上是 \(0\) 的时候缩小,反之则放大
在使用 \(0\) 和 \(1\) 来表示数字的方法下,排序变得很简单,只需要维护区间内 \(0\) 的个数就行了,自然使用上一个普通线段树就能够解决了
单调性
假设 \(x'\) 就是我们需要的数字,我们发现:
\(\forall n \lt x'\),有 \(a[q] = 1\)
而
\(\forall m \ge x'\),有 \(a[q] = 0\)
Old Driver Tree,ODT
有一大堆的别名,像珂朵莉树什么的
本质上是对于 set 的应用
我们将一整个序列分成很多根线段,然后我们的操作就可以在线段上进行了
具体的,对于本题
我们发现,直接进行排序的话,复杂度最低也是 \(\mathcal O({nm})\) (可以卡常卡进 \(\mathcal O(\frac{nm}{w})\),勉强通过)
因为基于决策树分析,任何基于比较的排序算法的复杂度下界为 \(\mathcal O(n\log_2n)\),而桶排序则为 \(\mathcal O(n)\),很明显都不行
所以我们不在额外考虑排序,仅仅将去要排序的区间打上一个标签,构建权值线段树,利用桶的性质,完成本题
具体的,对于区间 \([l,r]\),在存储了区间信息的 set 中查找 l、r 所在的块,若是所在的块不以 l、r 为端点,就将它们裂开在合并,具体的,便是将这两个块对应的线段树裂开再将区间内的合并
线段树分治
可撤销并查集
考虑构建一个叫做可撤销并查集的东西
本质上就是一个没有路径压缩的按秩合并并查集
在进行一个合并操作后,我们将这个操作放进一个栈中
随后就可以从栈中撤销出这一个操作
void deleteMerge() {
int x = rollback[top].x;
int y = rollback[top--].y;
father[y] = y;
siz[x] -= siz[y];
}
线段树分治
这更像是一种技巧而非算法
首先,线段树分治解决的是形如“xxx在某一时间点出现,持续到另一时间点的”这一类问题的
具体的思路就是将根据时间轴构建一颗线段树,将操作放在时间段内,相应的,这种操作就需要支持撤销
在遇到一个节点有标记的操作的时候,就进行一次操作,在回溯的时候进行撤销,对应就是它对于后面的时间点并没有作用
所以,如果觉得一道题是线段树分治,那么第一步就是找到值的效用范围,然后再想 dfs (也就是求答案的逻辑)
例题
由于删除操作一般很难,所以线段树分治将删除操作改成将除了这个值外的值加进去
ps:这是老师讲的,错了怪他
P5247 【模板】动态图连通性
这个傻子find(int id)return了father,没有递归
由于在线的做法是 LCT,这里仅考虑离线的情况
很明显,如果我们将每一条边的存在时间给列出来的话,会呈现出刚才我们提到的问题模版
所以考虑第一次遇到一条边的时候,用并查集连起来,在回溯的时候撤销该操作
很明显,这里借助了 DFS 的一些性质或者说实现
View Code
#include<bits/stdc++.h>
using namespace std;
using lf = double;
using ll = long long;
using ull = unsigned long long;
const int maxn = 5000 + 5;
const int maxm = 5e5 + 5;
int appear[maxn][maxn];
int answer[maxm];
int type[maxm];
pair<int, int> node[maxm];
namespace SegmentTree {
struct Stucture {
ll value;
vector<pair<int, int>> point;
} tree[maxm << 4];
int lson(int id) {return id << 1;}
int rson(int id) {return id << 1 | 1;}
int father[maxn], size[maxn];
struct dsunode {
int x, y;
} rollback[maxm];
int top = 0;
void init(int length) {
for (int i = 1; i <= length; i++) {
father[i] = i;
size[i] = 1;
}
}
int find(int index) {
if (father[index] == index) return index;
return find(father[index]);
}
void merge(int X, int Y) {
int x = find(X);
int y = find(Y);
if (size[x] < size[y]) swap(x, y);
father[y] = x;
size[x] += size[y];
rollback[++top] = {x, y};
}
void deleteMerge() {
int x = rollback[top].x;
int y = rollback[top--].y;
father[y] = y;
size[x] -= size[y];
}
void addEdge(int id, int u, int v) {
return tree[id].point.push_back({u, v}), void();
}
void add(int u, int v, int L, int R, int id, int left, int right) {
if (L <= left and right <= R) {
addEdge(id, u, v);
return ;
}
int mid = (left + right) >> 1;
if (L <= mid) add(u, v, L, R, lson(id), left, mid);
if (R > mid) add(u, v, L, R, rson(id), mid + 1, right);
}
void dfs(int id, int left, int right) {
int countMerge = 0;
for (pair <int, int> point : tree[id].point) {
int x = point.first, y = point.second;
x = find(x);
y = find(y);
if (find(x) != find(y)) {
merge(x, y);
++countMerge;
}
}
if (left == right) {
if (type[left] == 2)
answer[left] = (find(node[left].first) == find(node[left].second));
} else {
int mid = (left + right) >> 1;
dfs(lson(id), left, mid);
dfs(rson(id), mid + 1, right);
}
for (int i = 1; i <= countMerge; i++) deleteMerge();
}
};
int n, m;
using namespace SegmentTree;
void solve() {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int op, u, v;
cin >> op >> u >> v;
type[i] = op;
node[i] = {min(u, v), max(u, v)};
}
for (int i = 1; i <= m; i++) {
int op = type[i], u = node[i].first, v = node[i].second;
if (op == 0) appear[u][v] = i;
else if (type[i] == 1) {
add(u, v, appear[u][v], i - 1, 1, 1, m);
appear[u][v] = 0;
}
}
for (int i = 1; i <= n; i++)
for (int j = i + 1; j <= n; j++)
if (appear[i][j])
add(i, j, appear[i][j], m, 1, 1, m);
init(n);
dfs(1, 1, m);
for (int i = 1; i <= m; i++)
if (type[i] == 2)
cout << (answer[i] == 1 ? "Y" : "N") << "\n";
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cout.tie(0);
int T = 1;
// cin >> T;
while (T--) solve();
return 0;
}
P5787 【模板】线段树分治 / 二分图
这还是一个模版,甚至于上一道题的代码只用稍微改一下 dfs 的部分
既然是二分图,那么考虑每次将 x 和 y+n 以及 y 和 x+n 合并
这样子,每当出现了 find(u) == find(v) 就说明这不是一个二分图
View DFS Code
void dfs(int id, int left, int right) {
bool flag = true;
int countMerge = 0;
for (pair <int, int> point : tree[id].point) {
int x = point.first, y = point.second;
if (find(x) == find(y)) {
flag = false;
break;
} else {
merge(x, y + n);
merge(y, x + n);
countMerge += 2;
}
}
if (flag) {
if (left == right) {
answer[left] = true;
} else {
int mid = (left + right) >> 1;
dfs(lson(id), left, mid);
dfs(rson(id), mid + 1, right);
}
}
// else for (int i = left; i <= right; i++) answer[i] = false;
for (int i = 1; i <= countMerge; i++) deleteMerge();
}
CF601E A Museum Robbery
怎么是道紫
容易发现,题目中要求的最大值可以使用 0/1 背包来求
但是每一件展品都在某一段时间内出现,根据这一点,很容易想到线段树分治
撤回也十分的简单,只需要重新赋值成上一次的值就行了:
struct node{
struct object{
int weight, value;
};
vector<object> _array;
void append(const object &value) {
_array.push_back(value);
}
} tree[maxn << 2];
// --------
void add(node::object value, int L, int R, int id, int left, int right) {
if (L <= left and right <= R) {
tree[id].append(value);
return ;
}
int mid = (left + right) >> 1;
if (L <= mid) add(value, L, R, lson(id), left, mid);
if (R > mid) add(value, L, R, rson(id), mid + 1, right);
}
int dp[1000][maxn];
void dfs(int id, int left, int right, int depth) {
for (node::object object : tree[id]._array)
for (int i = k; i >= object.weight; i--)
dp[depth][i] = max(dp[depth][i], dp[depth][i - object.weight] + object.value);
if (left == right) {
if (query_index[left]) {
ll answer = 0, base = 1;
for (int i = 1; i <= k; i++) {
answer = ((answer + (base * dp[depth][i] % q + q) % q) % q + q) % q;
base = (base * p) % q;
}
cout << answer << "\n";
}
return ;
}
int mid = (left + right) >> 1;
for (int i = 1; i <= k; i++) dp[depth + 1][i] = dp[depth][i];
dfs(lson(id), left, mid, depth + 1);
for (int i = 1; i <= k; i++) dp[depth + 1][i] = dp[depth][i];
dfs(rson(id), mid + 1, right, depth + 1);
}
练习(题单)
- P4219 [BJOI2014] 大融合
- CF576E Painting Edges
- P5227 [AHOI2013] 连通图
- CF1681F Unique Occurrences
- CF981E Addition on Segments
可持久化线段树 | Persistent Segment Tree
可持久化数据结构 | Persistent Data Structure
可持久化数据结构 (Persistent data structure) 总是可以保留每一个历史版本,并且支持操作的不可变特
性 (immutable).——— OI Wiki
可持久化数据结构其实就像放一部电影,但是不能将所有的画面都存储起来,所以仅仅将相邻画面的不同给存储了下来
这就导致了这些数据结构之间的合并很像进行加法,而且这个“加法”和数据结构在某些情况下(数据结构本身没有顺序的限制,像堆这种就不行)构成了一个半群
等等,半群?这就意味着这些数据结构可以使用处理半群的数据结构,或者换句话说,可以在外面套上一层线段树或者树状数组
可持久化线段树 | Persistent Segment Tree
主席树 | Functional Segment Tree
其实是可持久化权值线段树的别称
要保留历史版本,就可以将本次要修改的 current 和历史版本 history 看成两个线段树合并,不过一定要新开一个节点
int update(int id, int left, int right, int position, int delta) {
int nowID = ++identity; // 新开一个节点
tree[nowID] = tree[id];
if (left == right) {
tree[nowID].value += delta;
return nowID;
}
int mid = (left + right) >> 1;
if (position <= mid) leftson(nowID) = update(leftson(nowID), left, mid, position, delta);
else rightson(nowID) = update(rightson(nowID), mid + 1, right, position, delta);
maintain(nowID);
return nowID;
}

浙公网安备 33010602011771号