线段树技巧 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\) 的总结点数量

直接套上公式:

\[\begin{equation} \begin{split} \sum \hat c_{i} &= \sum c_i + \Phi(n) - \Phi(0) + \sum \Delta \Phi(i)\\ &= \sum c_i + \Phi(n) \end{split} \end{equation} \]

移一下项得到总复杂度:

\[\sum \Delta \Phi(i) - \Phi(n) \]

由势能的性质得 \(\Phi(n) \ge 0\)

观察 update 操作,每次至多增加 \(\mathcal O(\log V)\) 的势能,因此:

\[\sum \Delta \Phi(i) = \mathcal O(n \log V) \]

得到

\[T(n) \le \sum \Delta \Phi(i) = \mathcal O(n \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 中查找 lr 所在的块,若是所在的块不以 lr 为端点,就将它们裂开在合并,具体的,便是将这两个块对应的线段树裂开再将区间内的合并

线段树分治

可撤销并查集

考虑构建一个叫做可撤销并查集的东西

本质上就是一个没有路径压缩的按秩合并并查集

在进行一个合并操作后,我们将这个操作放进一个栈中

随后就可以从栈中撤销出这一个操作

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 的部分

既然是二分图,那么考虑每次将 xy+n 以及 yx+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);
}

练习(题单)

可持久化线段树 | 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;
}
posted @ 2026-07-06 21:21  Yangyihao  阅读(12)  评论(0)    收藏  举报