【区间和线段树】洛谷 P3372 【模板】线段树 1
前言
线段树简介
线段树是一种二叉搜索树,主要用于高效地处理数组或线性序列上的区间查询和区间更新问题。它能够在 \(O(logn)\) 时间内完成单点更新、区间查询(如求和、最大值、最小值等)以及区间更新(配合懒惰标记)。线段树将每个区间划分为若干个更小的子区间,并通过树形结构存储这些子区间的信息,从而实现对任意区间的快速操作。
线段树的基本思想与结构
线段树基于分治思想,将一个长度为 \(n\) 的数组 \(a [1..n]\)(下标从1开始)递归地划分为若干个长度尽可能相等的子区间,形成一棵二叉树:
- 根节点代表整个区间 \([1,n]\);
- 每个非叶子节点代表一个区间 \([l,r]\),其左孩子代表区间 \([l,mid]\),右孩子代表区间 \([mid+1,r]\),其中 \(mid=⌊(l+r)/2⌋\);
- 叶子节点代表长度为 1 的区间 \([i,i]\),存储数组元素 \(a[i]\) 的信息。
每个节点存储的信息取决于具体需求,例如区间和、最大值、最小值等。通过维护这些信息,我们可以快速合并左右孩子的信息来得到父节点的信息。
题目
https://www.luogu.com.cn/problem/P3372
题解
本题为区间和线段树经典模板题,下面向大家详细介绍一下区间和线段树的设计过程:
根据前言所述,定义以下数据结构信息表示线段树的一个节点:
struct node {
int l, r;// 区间 [l, r]
ll sum;// 区间和:区间 [l, r] 所有元素之和
ll lazy;// 懒标记:代表当前节点的所有子节点都需要加的值
} tr[N<<2];
建立线段树
根据线段树的基本思想与结构,建立线段树可以按照以下算法步骤进行:
- 执行函数 \(build(int p, int l, int r)\)
- 完成对编号为 \(p\) 的节点的初步初始化 \(tr[p] = {l, r, a[l]/*sum*/, 0LL/*lazy*/}\)
- 判断区间左端点 \(l\) 与区间右端点 \(r\) 是否相等
- 相等:叶子节点,只需要初始化完节点信息即可退出
- 不相等:非叶子节点,需要递归 \(build(p<<1, l, m)\) 生成左子树和递归 \(build(p<<1|1,m+1,r)\) 生成右子树,其中 \(m=(l+r)/2\)
- 由于左右子树(若存在)均已初始化完成,因此可以根据左右子树的区间和信息更新当前节点的区间和 \(tr[p].sum = tr[p<<1].sum + tr[p<<1|1].sum\)
建立线段树的过程,是采取分治的思想,不断的将区间 \([1, n]\) 二分分配给二叉树中的每一个节点,并维护区间上的信息,例如区间和、区间左右端点以及懒标记(由于初始化时没有任何修改,因此都初始化为 \(0\))。
算法时间复杂度 \(O(4n)\)
更新线段树
更新线段树是指对区间 \([l, r]\) 上的每个数都加上 \(k\),这个操作就要涉及到线段树的精华操作懒修改,而懒修改正是依托于懒标记实现的。
试想,若更新的区间大小为 \(1\),那么一次更新只需要从树的一个叶子节点更新到根节点,时间复杂度为 \(O(logn)\);若更新的区间大小为 \(n\),那么每次整棵树的节点就都需要被更新到,时间复杂度为 \(O(n)\)。
若更新次数为 \(m\) 次,则最差时间复杂度为 \(O(m \times n)\),当 \(n, m\) 都较大(例如 \(10^6\))时,明显是无法接受的。
观察我们设计的数据结构,我们很容易发现每次更新操作,被更新的节点都介于左端点所在叶子节点到根节点的树链和右端点所在叶子节点到根节点的树链之间,那么就有这么一个特点:在左树链左边或右树链右边的节点都不需要被更新,否则需要被更新。那么如果当前节点 \(p\) 整个区间都介于要更新的区间范围 \([l,r]\) 之间,则直接在当前节点为其所有子节点打上懒标记 \(tr[p].lazy += k\)。这里需要注意的是,更新的懒标记是当前节点的懒标记,但是标记的内容是代表所有的子节点未来都需要加上的值。而整棵线段树恰好是根据二分法生成的,因此当前节点 \(p\) 没有整个区间都含于要更新的区间范围 $[l,r] 之间时,可以先计算出区间的中间位置 \(m = (tr[p].l+tr[p].r)/2\),随后判断左子树维护的区间 \([tr[p].l,m]\) 是否和更新的区间范围 \([l,r]\) 有交集,即判断 \(l \leq m\) 是否成立,若成立则需递归左子树,否则不需要;然后判断右子树维护的区间 \([m+1,tr[p],r]\) 是否和更新的区间范围 \([l,r]\) 有交集,即判断 \(m<r\) 是否成立,若成立则需递归右子树,否则不需要。
这里要注意的是,在更新操作的递归过程中,二分的始终是当前节点维护的区间范围,从而判断出当前节点的左右子树所维护区间是否和所需更新区间范围 \([l, r]\) 存在交集。
算法时间复杂度 \(O(m \times logn)\)
查询区间和
查询区间和是指求区间 \([l, r]\) 上的全部元素之和,这个操作同样也会涉及到线段树的精华操作懒修改,但与更新线段树操作不同的是,查询区间和操作若遍历的节点 \(p\) 维护的区间完全含于区间 \([l, r]\),则需要直接返回 \(tr[p].sum\),而更新线段树操作则需要更新自身懒标记信息以及区间和信息。
此操作与更新操作类似,当节点 \(p\) 没有整个区间都含于区间 \([l, r]\) 时,同样先计算出区间的中间位置 \(m = (tr[p].l+tr[p].r)/2\),随后判断左子树维护的区间 \([tr[p].l,m]\) 是否和查询的区间范围 \([l,r]\) 有交集,即判断 \(l \leq m\) 是否成立,若成立则需递归左子树,否则不需要;然后判断右子树维护的区间 \([m+1,tr[p],r]\) 是否和查询的区间范围 \([l,r]\) 有交集,即判断 \(m<r\) 是否成立,若成立则需递归右子树,否则不需要。
算法时间复杂度 \(O(m \times logn)\)
参考代码
#include<bits/stdc++.h>
#define lc(x) x<<1
#define rc(x) x<<1|1
using namespace std;
using ll = long long;
constexpr int N = 1e5 + 7;
int n, m, w, x, y;
ll k;
ll a[N];
struct node {
int l, r;// 区间 [l, r]
ll sum;// 区间和:区间 [l, r] 所有元素之和
ll lazy;// 懒标记:代表当前节点的所有子节点都需要加的值
} tr[N<<2];
void pushUp(int p) {
// 在节点 p 的左右子节点都更新完区间和以后,相加得到节点 p 的区间和
tr[p].sum = tr[lc(p)].sum + tr[rc(p)].sum;
}
void pushDown(int p) {
if (tr[p].lazy) {// 节点 p 有懒标记,因此向下传导时需要清空
int le = lc(p), ri = rc(p);
// 左子树的根节点为 le,区间元素个数为 tr[le].r - tr[le].l + 1
tr[le].sum += (tr[le].r - tr[le].l + 1) * tr[p].lazy;// 计算左子树的区间和
// 左子树的根节点已经更新过区间和,但是左子树除根节点以外的其它节点未更新,因此需要更新左子树懒标记
tr[le].lazy += tr[p].lazy;// 更新左子树懒标记
// 右子树的根节点为 ri,区间元素个数为 tr[ri].r - tr[ri].l + 1
tr[ri].sum += (tr[ri].r - tr[ri].l + 1) * tr[p].lazy;// 计算右子树的区间和
// 右子树的根节点已经更新过区间和,但是右子树除根节点以外的其它节点未更新,因此需要更新右子树懒标记
tr[ri].lazy += tr[p].lazy;// 更新右子树懒标记
tr[p].lazy = 0LL;// 当前节点 p 的左右子节点都已更新,清空当前节点 p 的懒标记
}
}
/*建立线段树*/
void build(int p/*线段树节点编号*/, int l/*该节点管理的区间左端点*/, int r/*该节点管理的区间右端点*/) {
tr[p] = {l, r, a[l], 0LL};// 节点 p 维护区间 [l, r] 的信息
if (l == r) return ;// 若已经是叶子节点,无需再次递归到子节点,可以直接返回
int m = l + r >> 1;// 二分线段 [l, r],将线段划分为左子树 [l, m] 和右子树 [m + 1, r]
build(lc(p), l, m);// 建立左子树
build( rc(p), m + 1, r);// 建立右子树
pushUp(p);// 根据左右子树信息,更新当前节点信息
}
/*更新线段树:为区间 [l, r] 上的每个元素都加上 k*/
void update(int p, int l, int r, const ll& k) {
if (l <= tr[p].l && tr[p].r <= r) {// 节点 p 的管辖区间 [tr[p].l, tr[p].r] 位于 [l, r] 之间
tr[p].lazy += k;// 更新懒标记信息,记录子节点需要更新的数据
tr[p].sum += (tr[p].r - tr[p].l + 1) * k;// 更新当前节点的总和
return ;
}
pushDown(p);// 将懒标记信息传给左右子节点
int m = tr[p].l + tr[p].r >> 1;// 二分区间 [tr[p].l, tr[p].r],划分为 [tr[p].l, m] 和 [m + 1, tr[p],r]
if (l <= m) update(lc(p), l, r, k);// 如果左子树维护的区间 [tr[lc(p).l, tr[lc(p)].r] 和 [l, r] 有交集则递归左子树
if (m < r) update(rc(p), l, r, k);// 如果右子树维护的区间 [tr[rc(p).l, tr[rc(p)].r] 和 [l, r] 有交集则递归右子树
pushUp(p);// 左右子树更新完,需要更新当前节点信息
}
/*查询区间和*/
ll query(int p, int l, int r) {
if (l <= tr[p].l && tr[p].r <= r) return tr[p].sum;// 节点 p 的管辖区间 [tr[p].l, tr[p].r] 位于 [l, r] 之间
ll sum = 0LL;
pushDown(p);// 将懒标记信息传给左右子节点
int m = tr[p].l + tr[p].r >> 1;// 二分区间 [tr[p].l, tr[p].r],划分为 [tr[p].l, m] 和 [m + 1, tr[p],r]
if (l <= m) sum += query(lc(p), l, r);// 如果左子树维护的区间 [tr[lc(p).l, tr[lc(p)].r] 和 [l, r] 有交集则递归左子树
if (m < r) sum += query(rc(p), l, r);// 如果右子树维护的区间 [tr[rc(p).l, tr[rc(p)].r] 和 [l, r] 有交集则递归右子树
return sum;
}
int main() {
ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);
cin >> n >> m;
for (int i = 1; i <= n; ++ i) cin >> a[i];
build(1, 1, n);// 建立线段树
while (m --) {
cin >> w >> x >> y;
if (w == 1) {// 将区间 [x,y] 内每个数加上 k
cin >> k;
update(1, x, y, k);
} else {// 输出区间 [x,y] 内每个数的和
cout << query(1, x, y) << '\n';
}
}
return 0;
}
浙公网安备 33010602011771号