线段树进阶学习笔记

线段树进阶学习笔记

线段树是一种常见的数据结构,除了基础用法,我们还需要掌握一些进阶。

线段树的合并与分裂

概念

线段树的合并通常是针对权值线段树的合并,将两棵线段树的信息合并起来保存在一棵线段树中。为了节省空间,我们通常将两棵线段树的信息直接保留在一棵线段树中。需要留意的是,初始节点个数为 n 的线段树,需要的空间复杂度是 O(nlogn)

int merge(int p, int q, int l, int r) {
	if (!p || !q) return p + q;
	if (l == r) return sm(p) += sm(q), p;
	int mid = (l + r) >> 1;
	lc(p) = merge(lc(p), lc(q), l, mid);
	rc(p) = merge(rc(p), rc(q), mid + 1, r);
	push_up(p);
	return p;
}

线段树的分裂则通常是将权值线段树前 k 小的保留到原线段树中,其它部分分裂成为新树。它的分裂方式和 FHQ-Treap 是类似的,分类讨论左儿子子树大小 xk 的大小关系,当 x<k 时,显然递归至右边分裂;x>k 时,右子树全部属于新树,在左边继续分裂;x=k 时,直接将右子树归为新树即可。

void split(int p, int &q, ll k) {
	if (!p) return;
	q = nw();
	ll num = sm(lc(p));
	if (k > num) split(rc(p), rc(q), k - num);
	else swap(rc(p), rc(q));
	if (k < num) split(lc(p), lc(q), k);
	sm(q) = sm(p) - k;
	sm(p) = k;
}

应用

线段树合并/分裂通常用来解决一类集合动态变化时权值的维护问题。其中线段树合并的应用范围较广,而线段树分裂常和 ODT 结合,来维护 ODT 的分裂操作。

线段树优化建图

当我们需要将一个点连向一个区间时,暴力连边的复杂度来到了 O(n),难以接受。如果将这个过程改用线段树维护,显然可以优化至 logn 级别。代码由于过于简单就不放了。

线段树分治

概念

线段树分治是离线维护带撤销问题的一种思想,常用于维护“某操作的有效时间是 [l,r]”,在各个时间处询问答案。在见到这类问题时,往往可以采用线段树分治。线段树分治对时间轴建立了一棵线段树,将每个区间的贡献拆到线段树上不超过 O(logn) 个节点上,在节点上增加贡献,继续遍历,在叶子节点处统计答案,离开这个节点时撤销贡献即可。这里给出代码框架:

void dfs(int p) {
    for (auto i : v(p)) add(i); // 增加贡献
    if (l(p) == r(p)) ans[l(p)] = ask(); // 记录答案
    else dfs(lc), dfs(rc);
    del(); // 撤销贡献
}

应用

由于线段树分治维护答案的可撤销性,其通常于可撤销并查集结合,维护动态加删边的连通性问题。例如动态加删边的二分图判定问题,我们采用线段树分治结合扩展域可撤销并查集来解决问题。同时对于一些规模较小的数据结构,如线性基,我们可以在 O(logV) 的时间里恢复原状,这同样是线段树分治的经典应用。

线段树二分

如果线段树维护的信息具有单调性,当我们固定了区间的右端点 r,要找出一个极小的左端点 l 时,我们可以用朴素二分 + 线段树 check 的方法做到 O(nlog2n)。然而事实上用线段树二分可以做到 O(nlogn)。做法是用在遍历线段树的过程中用维护的信息检查左子树是否合法,如合法就遍历左子树,反之遍历右子树。

值得一提的是,线段树求解 k 小值本身就是线段树二分的简单应用。

这里给出代码框架:

int fnd(int p) {
    if (l(p) == r(p)) return l(p);
    if (/*左子树满足条件*/) return fnd(lc);
    return fnd(rc);
}

吉司机线段树

吉司机线段树通常用来处理区间最值操作 & 区间历史最值问题。

区间最值

不带区间加减

给定长度为 n 的序列,需要支持区间取 min,求区间 max 和区间和。

对于区间取 min 操作本身线段树是不好维护的,但是考虑取 min 操作的实质是将大于 x 的数改为 x,于是我们维护区间最大值 mx,严格次大值 se,最大值出现次数 cnt 以及区间和 sum

  • mxx,直接返回;
  • se<xmxsumsum+cnt×(tmx),并将当前区间的最大值改为 x
  • tse,无法直接维护,需要继续向下递归。

通过势能分析的方法可以知道这样操作的时间复杂度是 O(nlogn)

带区间加减

P10639 BZOJ4695 最佳女选手

直接维护难以确定标记的次序。然而注意到区间取最大值的操作只会改变 [se,mx] 之间的部分,于是考虑将最大值/最小值的部分拆出来另外开标记维护,对于这一部分的信息我们单独来维护标记来修改,其它部分正常操作即可。这样一来取 min/max 同样可以采用加减标记维护。这种方法 [UKE_Automation](https://www.cnblogs.com/UKE-Automa tion) 称作数域划分

这样一来我们维护的标记有最大值,严格次大值,最小值,严格次小值,最大值个数,最小值个数,区间和,最大值的加法标记,最小值的加法标记,其它值的加法标记。

需要留意的是下传标记时要判断下传的子区间最大值是否是当前区间的最大值,若不是则应当下传其它值的标记。以及需要判断一个数既是最大值又是最小值时做贡献的情形。

给出代码:

#include <bits/stdc++.h>
#define N 500005
#define int long long
using namespace std;
const int inf = 1e15;
int n, m;
int a[N];
struct Node {
	int l, r, sm;
	int mx, nx, cx;
	int mn, nn, cn;
	int tgx, tgn, tgo;
} e[N << 2];
#define l(i) e[i].l
#define r(i) e[i].r
#define sm(i) e[i].sm
#define mx(i) e[i].mx
#define nx(i) e[i].nx
#define cx(i) e[i].cx
#define mn(i) e[i].mn
#define nn(i) e[i].nn
#define cn(i) e[i].cn
#define tgx(i) e[i].tgx
#define tgn(i) e[i].tgn
#define tgo(i) e[i].tgo
#define lc (p << 1)
#define rc (lc | 1)
void psup(int p) {
	sm(p) = sm(lc) + sm(rc);
	if (mx(lc) > mx(rc)) {
		mx(p) = mx(lc);
		nx(p) = max(nx(lc), mx(rc));
		cx(p) = cx(lc);
	}
	else if (mx(lc) < mx(rc)) {
		mx(p) = mx(rc);
		nx(p) = max(mx(lc), nx(rc));
		cx(p) = cx(rc);
	}
	else {
		mx(p) = mx(lc);
		nx(p) = max(nx(lc), nx(rc));
		cx(p) = cx(lc) + cx(rc);
	}
	if (mn(lc) < mn(rc)) {
		mn(p) = mn(lc);
		nn(p) = min(nn(lc), mn(rc));
		cn(p) = cn(lc);
	}
	else if (mn(lc) > mn(rc)) {
		mn(p) = mn(rc);
		nn(p) = min(mn(lc), nn(rc));
		cn(p) = cn(rc);
	}
	else {
		mn(p) = mn(lc);
		nn(p) = min(nn(lc), nn(rc));
		cn(p) = cn(lc) + cn(rc);
	}
}
void bud(int p, int l, int r) {
	l(p) = l, r(p) = r;
	if (l == r) {
		sm(p) = mx(p) = mn(p) = a[l];
		cx(p) = cn(p) = 1;
		nx(p) = -inf, nn(p) = inf;
		return;
	}
	int mid = (l + r) >> 1;
	bud(lc, l, mid);
	bud(rc, mid + 1, r);
	psup(p);
}
void tgdn(int p, int tgx, int tgn, int tgo) {
	if (mx(p) == mn(p)) {
		if (tgx == tgo) tgx = tgn;
		else tgn = tgx;
		sm(p) += cx(p) * tgx;
	}
	else sm(p) += cx(p) * tgx + cn(p) * tgn + (r(p) - l(p) + 1 - cx(p) - cn(p)) * tgo;
	if (nx(p) == mn(p)) nx(p) += tgn;
	else if (nx(p) != -inf) nx(p) += tgo;
	if (nn(p) == mx(p)) nn(p) += tgx;
	else if (nn(p) != inf) nn(p) += tgo;
	mx(p) += tgx;
	mn(p) += tgn;
	tgx(p) += tgx;
	tgn(p) += tgn;
	tgo(p) += tgo;
}
void psdn(int p) {
	int mx = max(mx(lc), mx(rc)), mn = min(mn(lc), mn(rc));
	tgdn(lc, mx == mx(lc) ? tgx(p) : tgo(p), mn == mn(lc) ? tgn(p) : tgo(p), tgo(p));
	tgdn(rc, mx == mx(rc) ? tgx(p) : tgo(p), mn == mn(rc) ? tgn(p) : tgo(p), tgo(p));
	tgx(p) = tgn(p) = tgo(p) = 0;
}
void updt(int p, int l, int r, int x) {
	if (l > r || l > r(p) || l(p) > r) return;
	if (l <= l(p) && r(p) <= r) return tgdn(p, x, x, x);
	psdn(p);
	updt(lc, l, r, x), updt(rc, l, r, x);
	psup(p);
}
void upmx(int p, int l, int r, int x) {
	if (mn(p) >= x || l > r || l > r(p) || l(p) > r) return;
	if (l <= l(p) && r(p) <= r && nn(p) > x) return tgdn(p, 0, x - mn(p), 0);
	psdn(p);
	upmx(lc, l, r, x), upmx(rc, l, r, x);
	psup(p);
}
void upmn(int p, int l, int r, int x) {
	if (mx(p) <= x || l > r || l > r(p) || l(p) > r) return;
	if (l <= l(p) && r(p) <= r && nx(p) < x) return tgdn(p, x - mx(p), 0, 0);
	psdn(p);
	upmn(lc, l, r, x), upmn(rc, l, r, x);
	psup(p);
}
int qusm(int p, int l, int r) {
	if (l > r || l > r(p) || l(p) > r) return 0;
	if (l <= l(p) && r(p) <= r) return sm(p);
	psdn(p);
	return qusm(lc, l, r) + qusm(rc, l, r);
}
int qumx(int p, int l, int r) {
	if (l > r || l > r(p) || l(p) > r) return -inf;
	if (l <= l(p) && r(p) <= r) return mx(p);
	psdn(p);
	return max(qumx(lc, l, r), qumx(rc, l, r));
}
int qumn(int p, int l, int r) {
	if (l > r || l > r(p) || l(p) > r) return inf;
	if (l <= l(p) && r(p) <= r) return mn(p);
	psdn(p);
	return min(qumn(lc, l, r), qumn(rc, l, r));
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n;
	for (int i = 1; i <= n; i++) cin >> a[i];
	bud(1, 1, n);
	cin >> m;
	while (m--) {
		int o, l, r;
		cin >> o >> l >> r;
		if (o == 1) {
			int x;
			cin >> x;
			updt(1, l, r, x);
		}
		else if (o == 2) {
			int x;
			cin >> x;
			upmx(1, l, r, x);
		}
		else if (o == 3) {
			int x;
			cin >> x;
			upmn(1, l, r, x);
		}
		else if (o == 4) cout << qusm(1, l, r) << '\n';
		else if (o == 5) cout << qumx(1, l, r) << '\n';
		else cout << qumn(1, l, r) << '\n'; 
	}
	return 0;
}

区间历史最值

不带区间修改最值

CPU 监控

题意:区间求 max,求历史版本区间 max,区间加,区间推平。

区间历史最值问题采用普通标记维护的方式是困难的。注意到我们实际上需要维护的是两个值:当前区间 max,历史区间 max,于是考虑采用广义矩阵乘法 (+,max) 来维护这个问题。

我们将上面两个值记作 [a,b],那么对于区间 +k 的操作,目标矩阵事实上就是 [a+k,max(b,a+k)]。那么构造出转移矩阵:

[a+kmax(b,a+k)]=[ab][kk0]

考虑区间覆盖如何解决,一般处理这种问题加一个辅助变量就可以解决。于是:

[kmax(b,k)0]=[ab0][0kk0]

区间加类似地修改为:

[a+kmax(b,a+k)0]=[ab0][kk00]

然而直接这样维护会有一个 m3 的常数。考虑到每次变换的形式总是

[ab0cd0]

且变换中总只有这四个值发生变化,因此只维护这四个值即可。需要留意的是 (+,max) 意义下的单位元是

[000]

然后直接维护就可以了。

带区间修改最值

P6242 【模板】线段树 3

就是还要求区间取 min/max

和上面的维护方式是相近的,数域划分一下维护最小值的标记就可以了。这里给出代码:

#include <bits/stdc++.h>
#define N 500005
#define int long long
using namespace std;
const int inf = 1e15;
int n, m;
int a[N];

struct mar {
	int a, b;
};
mar operator + (const mar &a, const mar &b) {
	return {a.a + b.a, max(a.a + b.b, a.b)};
}
struct Node {
	int l, r;
	int sm, ct;
	int mx, hmx;
	int nx, hnx;
	mar tm, tn;
} e[N << 2];
#define l(i) e[i].l
#define r(i) e[i].r
#define sm(i) e[i].sm
#define ct(i) e[i].ct
#define mx(i) e[i].mx
#define hmx(i) e[i].hmx
#define nx(i) e[i].nx
#define hnx(i) e[i].hnx
#define tm(i) e[i].tm
#define tn(i) e[i].tn
#define lc (p << 1)
#define rc (lc | 1)
void push_up(int p) {
	sm(p) = sm(lc) + sm(rc);
	mx(p) = max(mx(lc), mx(rc));
	hmx(p) = max(hmx(lc), hmx(rc));
	if (mx(lc) == mx(rc)) {
		ct(p) = ct(lc) + ct(rc);
		nx(p) = max(nx(lc), nx(rc));
		hnx(p) = max(hnx(lc), hnx(rc));
	}
	else if (mx(lc) > mx(rc)) {
		ct(p) = ct(lc);
		nx(p) = max(nx(lc), mx(rc));
		hnx(p) = max({hnx(lc), hnx(rc), mx(rc)});
	}
	else {
		ct(p) = ct(rc);
		nx(p) = max(mx(lc), nx(rc));
		hnx(p) = max({hnx(lc), hnx(rc), mx(lc)});
	}
}
void build(int p, int l, int r) {
	l(p) = l, r(p) = r;
	tm(p) = tn(p) = {0, -inf};
	if (l == r) {
		mx(p) = hmx(p) = sm(p) = a[l];
		ct(p) = 1;
		nx(p) = hnx(p) = -inf;
		return;
	}
	int mid = (l + r) >> 1;
	build(lc, l, mid);
	build(rc, mid + 1, r);
	push_up(p);
}
void tgdn(int p, mar tm, mar tn) {
	sm(p) += ct(p) * tm.a + (r(p) - l(p) + 1 - ct(p)) * tn.a;
	hmx(p) = max(hmx(p), mx(p) + tm.b);
	mx(p) += tm.a;
	if (nx(p) != -inf) hnx(p) = max(hnx(p), nx(p) + tn.b), nx(p) += tn.a;
	tm(p) = tm(p) + tm;
	tn(p) = tn(p) + tn;
}
void psdn(int p) {
	int mx = max(mx(lc), mx(rc));
	tgdn(lc, mx(lc) == mx ? tm(p) : tn(p), tn(p));
	tgdn(rc, mx(rc) == mx ? tm(p) : tn(p), tn(p));
	tm(p) = tn(p) = {0, -inf};
}
void updt(int p, int l, int r, int x) {
	if (l > r || l > r(p) || l(p) > r) return;
	if (l <= l(p) && r(p) <= r) return tgdn(p, {x, x}, {x, x});
	psdn(p);
	updt(lc, l, r, x), updt(rc, l, r, x);
	push_up(p);
}
void upmn(int p, int l, int r, int x) {
	if (x >= mx(p) || l > r || l > r(p) || l(p) > r) return;
	if (l <= l(p) && r(p) <= r && nx(p) < x) return tgdn(p, {x - mx(p), x - mx(p)}, {0, -inf});
	psdn(p);
	upmn(lc, l, r, x), upmn(rc, l, r, x);
	push_up(p);
}
int qsm(int p, int l, int r) {
	if (l > r || l > r(p) || l(p) > r) return 0;
	if (l <= l(p) && r(p) <= r) return sm(p);
	psdn(p);
	return qsm(lc, l, r) + qsm(rc, l, r);
}
int qmx(int p, int l, int r) {
	if (l > r || l > r(p) || l(p) > r) return -inf;
	if (l <= l(p) && r(p) <= r) return mx(p);
	psdn(p);
	return max(qmx(lc, l, r), qmx(rc, l, r));
}
int qnx(int p, int l, int r) {
	if (l > r || l > r(p) || l(p) > r) return -inf;
	if (l <= l(p) && r(p) <= r) return hmx(p);
	psdn(p);
	return max(qnx(lc, l, r), qnx(rc, l, r));
}

signed main() {
	ios::sync_with_stdio(0);
	cin.tie(0);
	cin >> n >> m;
	for (int i = 1; i <= n; i++) cin >> a[i];
	build(1, 1, n);
	while (m--) {
		int o, l, r;
		cin >> o >> l >> r;
		if (o == 1) {
			int x;
			cin >> x;
			updt(1, l, r, x);
		}
		else if (o == 2) {
			int x;
			cin >> x;
			upmn(1, l, r, x);
		}
		else if (o == 3) cout << qsm(1, l, r) << '\n';
		else if (o == 4) cout << qmx(1, l, r) << '\n';
		else cout << qnx(1, l, r) << '\n';
	}
	return 0;
}
posted @   长安19路  阅读(4)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示