线段树详解之单点修改,区间查询

Part 1 引入

先看下面一个例子:
题面
给定一个长度为 n n n 数组的 a a a 进行 q q q 次询问。
每次询问有两种操作:
操作1:给定 l , r l,r l,r,将 a l a_{l} al 的值改为 r r r
操作2:给定 l , r l,r l,r,求区间 l , r {l,r} l,r 内最大值。
输入格式
第一行:两个正整数 n , q n,q n,q
第二行: n n n 个整数第 i i i 个数表示 a i a_{i} ai 的值
接下来 q q q 行:输入三个正整数 o p , l , r op,l,r op,l,r o p op op 表示操作几。
输出格式
对于每一个操作2,输出一个整数表示答案。
数据范围
a i ≤ 1 0 9 , n ≤ 1 0 5 , q ≤ 1 0 5 a_{i} \le 10^9,n \le 10^5,q \le 10^5 ai109,n105,q105
时限: 1000 m s 1000ms 1000ms
对于这个题目,我们的普通暴力已经无能为力了.
这时线段树就要出现了!!

Part 2 实现原理(Segment tree)

具体的线段树咋实现呢?
看这组数据:
输入样例:

8 4
1 2 3 4 5 6 7 8
2 1 2
2 1 8

输出样例:

2
8

话不多说,先上图。
在这里插入图片描述

我们可以发现区间 [ 1 , 8 ] [1,8] [1,8] 的答案就是区间 [ 1.2 ] [1.2] [1.2] 的答案与区间 [ 3 , 8 ] [3,8] [3,8] 的答案的最大值。
于是我们可以将区间 [ 1 , 8 ] [1,8] [1,8] 分成区间 [ 1 , 4 ] [1,4] [1,4] 和区间 [ 5 , 8 ] [5,8] [5,8]
同样区间 [ 1 , 4 ] [1,4] [1,4] 同样可以拆成区间 [ 1 , 2 ] [1,2] [1,2] 和区间 [ 3 , 4 ] [3,4] [3,4]
对于区间 [ l , r ] [l,r] [l,r],都可以分为区间 [ l , m i d ] [l,mid] [l,mid] 和区间 [ m i d + 1 , r ] [mid + 1,r] [mid+1,r] 其中 m i d mid mid
l + r 2 \dfrac{l +r} 2 2l+r
并且这个区间的答案就是两个子区间的答案的最大值。

Part 3 建树(build)

那么具体如何实现呢?就像前文说的一样,我们可以使用递归解决,每次传入一个区间,表示当前函数处理的区间。

根据递归的原理,本函数的边界值就是当当前区间长度为 1 时,不再需要继续递归,直接将当前节点的值初始化,在本题中就是记为 a i a_i ai

而每次递推,就是将当前区间 [ l , r ] [l,r] [l,r],推到 [ l , m i d ] [l,mid] [l,mid] [ m i d + 1 , r ] [mid+1,r] [mid+1,r],然后更新当前节点的值,在本题中即为取子节点的最大值。

最后,我们建树时直接传入区间 [ 1 , n ] [1,n] [1,n] 即可,时间复杂度 O ( n ) O(n) O(n),证明留给读者。

代码

void build(int u, int l, int r) {
	if (l == r) {
		maxv[u] = a[l];
		return;
	}
	int mid = l + r >> 1;
	build(u << 1, l, mid);
	build(u << 1 | 1, mid + 1, r);
	maxv[u] = max(maxv[u << 1], maxv[u << 1 | 1]);
}

Part 4 修改(update)

修改与建树类似,但是因为这里只需要修改实际上的一个点,即使在线段树中,也只对应 l o g 2 n log_2n log2n 级别的点。

所以我们不再需要在每次递推过程中同时递推左右两边的区间,因为显然目标点只会在其中一个区间内,我们只需要递推一个即可。

边界值也和建树一样,但是因为当前位置的 a i a_i ai 已被修改,所以我们可以直接赋上新的值。同样需要记得更新上面的节点。

代码

void update(int u, int l, int r) {
	if (l > r) return;
	if (l == r && l == x) {
		maxv[u] = v;
		return;
	}
	int mid = l + r >> 1;
	if (mid >= x) update(u << 1, l, mid);
	if (mid < x) update(u << 1 | 1, mid + 1, r);
	maxv[u] = max(maxv[u << 1], maxv[u << 1 | 1]);
}

Part 5 查询(query)

查询就是线段树的精髓所在了。

如果我们也像建树和修改一样,那么时间复杂度是查询区间的长度乘上一只 l o g log log,比暴力的常数还大。

注意到,如果递推到当前节点时,当前区间已经是查询区间的子区间时,我们根本不需要再下传,为什么呢?

因为此时我们已经处理过了整个区间的总值(在本题中即区间最大值),所以我们直接返回当前区间节点存储的值即可。

此时我们本质上就是将查询区间拆分成了 l o g 2 n log_2 n log2n 级别个小区间,因为每个小区间已经处理过,所以每个小区间都是 O ( 1 ) O(1) O(1) 查询的。当查询区间长度与 n n n 同阶时,单次查询时间复杂度 O ( l o g 2 n ) O(log_2 n) O(log2n)

代码

int query(int u, int l, int r) {
	if (l > r) return 0;
	if (L <= l && r <= R) {
		return maxv[u];
	}
	int ret = 0;
	int mid = l + r >> 1;
	if (mid >= L) ret = max(ret, query(u << 1, l, mid));
	if (mid < R) ret = max(ret, query(u << 1 | 1, mid + 1, r));
	return ret;
}

Part 6 习题&小结(problems)

看完这么多,我们可以发现线段树解决的就是区间问题,其本质就是将一个区间,拆分为两个区间。但是当我们需要求的无法简单拆分时,这个方法就行不通了,以下是一些例子。

线段树不可行:区间和为 x x x 的数对个数,区间逆序对数,区间最长不讲子序列长度等。

线段树可行:区间和,区间积,区间异或和等。

习题
P1531
P3374
P1198
2022 提高组 T2(P8818)

附例题主函数

int main() {
	int n, q;
	cin >> n >> q;
	for (int i = 1; i <= n; ++i) cin >> a[i];
	build(1, 1, n);
	while (q--) {
		int op;
		cin >> op;
		if (op == 1) {
			cin >> x >> v;
			update(1, 1, n);
		} else {
			cin >> L >> R;
			cout << query(1, 1, n) << endl;
		}
	}
	return 0;
}
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值