一、线段树
线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点,如图下:
二、线段树的基础实现
1、建立线段树
由图可知,我们给与线段树的每个节点一个下标值,暂记为rt,同时我们可以记录每个节点的左右儿子为L(Left)与R(Right),表示L到R区间。
由此,我们可以定义一个结构体来存储这些变量:
const int maxn = 5e5 + 10;
struct Node{
int L, R, num, lazy;
}tree[maxn << 4];
至于为什么要开4倍区间,这里推荐一篇博客:线段树及空间开4倍
接下来,我们就要建树了
我们可以通过简单递归得到这棵初始线段树:即用build(l, r, rt)表示当前要构建区间[l, r]的线段树,rt表示该区间的标号值,如果l == r,则表明我们已经找到线段树最底端的节点,它的值就为ai;否则我们新建节点,它的两个儿子就分别由Build(L, mid, rt << 1)和Build(mid + 1, R, rt << 1 | 1)递归得到。
void Build(int L, int R, int rt){
tree[rt].L = L, tree[rt].R = R;
if(L == R){//边界条件
tree[rt].num = a[L];//将a数组赋值到tree中
return;
}
int mid = (L + R) >> 1;
Build(L, mid, rt << 1);
Build(mid + 1, R, rt << 1 | 1);//继续向下建树
tree[rt].num = tree[rt << 1].num + tree[rt << 1 | 1].num;//递归返回每个节点的权值
}
2、线段树的单点改值
既然是单点改值,我们就必须找到“单点”的位置进行更改,但由于线段树子节点更改后会对其父节点产生影响,所以我们会对“单点”进行更改后重新计算其父节点一直到根节点,因此单点改值的思路也是使用递归。
设我们要把第pos位的数增加val,则由图可知,我们必须在线段树中先找到pos的位置,再依次递归返回值到树顶:
void update(int pos, int val, int rt){
if(tree[rt].L == tree[rt].R){//找到“单点”
tree[rt].num += val;//更新单点的值
return;
}
int mid = (tree[rt].L + tree[rt].R) >> 1;
if(mid < pos) update(pos, val, rt << 1 | 1);
else update(pos, val, rt << 1);//向下递归找到“单点”
tree[rt].num = tree[rt << 1].num + tree[rt << 1 | 1].num;//递归到“单点”后更新其父亲的权值
}
3、线段树的区间改值
其实区间改值和单点改值的定义差不多,就是在一个区间中进行更改,而区间改值才能完美地体现线段树的作用,先贴个代码,不慌,我们慢慢讲解:
void update1(int L, int R, int val, int rt){
if(tree[rt].L >= L && tree[rt].R <= R){
tree[rt].num += (tree[rt].R - tree[rt].L + 1) * val;
tree[rt].lazy = val;
return;
}
if(tree[rt].lazy) pushdown(rt);
int mid = (tree[rt].R + tree[rt].L) >> 1;
if(mid < L) update1(L, R, val, rt << 1 | 1);
else if(mid >= R) update1(L, R, val, rt << 1);
else{
update1(L, R, val, rt << 1);
update1(L, R, val, rt << 1 | 1);
}
tree[rt].num = tree[rt << 1].num + tree[rt << 1 | 1].num;
}
首先区间改值的思想和单点差不多,就是用递归;
函数需要的变量:我们想要得到区间的左右边界、要更改的值和递归的初始节点;(分别对应L, R, val和rt)
然后我们看向代码:第一个if语句便是判断是否递归到了我们想要进行更改的区间,图像如下:
这表明线段树的这个区间在我们要改值的区间内
如果递归到了
(1)更改线段树节点值,这里因为是区间改值所以区间内每个数都要加val
(2)更新当前节点的lazy值,用于更新其子节点的lazy值
(3)返回
————————————————————————————————————————————————————
接下来我们判断递归到的节点是否有lazy值
如果已经被更新过lazy,则将其子节点的lazy值也更新,这里我们写了一个函数:
void pushdown(int rt) {
tree[rt << 1].num += (tree[rt << 1].R - tree[rt << 1].L + 1) * tree[rt].lazy;
//更新左节点的权值
tree[rt << 1 | 1].num += (tree[rt << 1 | 1].R - tree[rt << 1 | 1].L + 1) * tree[rt].lazy;
//更新右节点的权值
tree[rt << 1].lazy += tree[rt].lazy;
//更新左节点的lazy值
tree[rt << 1 | 1].lazy += tree[rt].lazy;
//更新右节点的lazy值
tree[rt].lazy = 0;//父节点的lazy返回为零,防止下次递归到这个点时多次叠加
}
注:为此引入线段树的延迟标记概念,也叫 lazy
延迟标记:节点结构体中新增一个标记,记录这个节点是否会进行某种修改,对于任意区间的修改,我们先按照区间查询的方式将其划分成线段树中的节点,然后修改这些节点的信息,并给这些节点打上标记。在修改和查询的时候,如果我们到了一个节点 P,并且要继续查看其子节点,那么我们就要看看节点 P 是否被标记,如果有,则需要按照其标记首先修改子节点的信息,并且给子节点都打上相同的标记,同时取消节点 P 的标记,这一操作称为标记下放,也叫 pushDown。
可以这么理解,假设爷爷要给两个孙女压岁钱,所以爷爷就先把总的压岁钱给自己的儿子,让儿子给女儿
,但是儿子觉得自己的女儿还太小了,暂时用不到,于是就先保存着。突然有一天爷爷准备要问孙女拿到压岁钱了没有,此时爸爸着急了,就赶紧把压岁钱给了女儿。
注解摘自mathor的博客
————————————————————————————————————————————————————
lazy值更新后,便是我们日常的递归了,这张图也许能向你们展现递归为什么有三个if语句:
最后是更新每个父节点的权值。
四、线段树的区间求和
区间求和其实与区间改值相差无几,就只是在if语句中把改值换为返回答案罢了,这里就不过多解释,上代码:
int query(int L, int R, int rt){//求区间和
if(tree[rt].L >= L && tree[rt].R <= R){//如果tree的区间在要求的区间中的话,则返回此节点的值
return tree[rt].num;
}
if(tree[rt].lazy) pushdown(rt);
int mid = (tree[rt].L + tree[rt].R) >> 1;
if(mid < L) return query(L, R, rt << 1 | 1);
else if(mid >= R) return query(L, R, rt << 1);
else return query(L, R, rt << 1) + query(L, R, rt <<1 | 1);
}
最后的最后,贴一下完整代码吧
#include<cstdio>
#include<iostream>
#include<algorithm>
#define debug(x) cout <<#x <<" = " <<x <<endl
using namespace std;
const int maxn = 5e5 + 10;
struct Node{
int L, R, num;
int lazy;
}tree[maxn << 2];
int a[maxn];
void Build(int L, int R, int rt){//建树
tree[rt].L = L, tree[rt].R = R, tree[rt].lazy = 0;
if(L == R){
tree[rt].num = a[L];
return;
}
int mid = (L + R) >> 1;
Build(L, mid, rt << 1);
Build(mid + 1, R, rt << 1 | 1);
tree[rt].num = tree[rt << 1].num + tree[rt << 1 | 1].num;
}
void pushdown(int rt){
tree[rt << 1].num += (tree[rt << 1].R - tree[rt << 1].L + 1) * tree[rt].lazy;
tree[rt << 1 | 1].num += (tree[rt << 1 | 1].R - tree[rt << 1 | 1].L + 1) * tree[rt].lazy;
tree[rt << 1].lazy += tree[rt].lazy;
tree[rt << 1 | 1].lazy += tree[rt].lazy;
tree[rt].lazy = 0;
}
//区间求和
int query(int L, int R, int rt){
if(tree[rt].L >= L && tree[rt].R <= R){
return tree[rt].num;
}
if(tree[rt].lazy) pushdown(rt);
int mid = (tree[rt].L + tree[rt].R) >> 1;
if(mid < L) return query(L, R, rt << 1 | 1);
else if(mid >= R) return query(L, R, rt << 1);
else return query(L, R, rt << 1) + query(L, R, rt <<1 | 1);
}
//区间更新
void update1(int L, int R, int val, int rt){
if(tree[rt].L >= L && tree[rt].R <= R){
tree[rt].num += (tree[rt].R - tree[rt].L + 1) * val;
tree[rt].lazy = val;
return;
}
if(tree[rt].lazy) pushdown(rt);
int mid = (tree[rt].R + tree[rt].L) >> 1;
if(mid < L) update1(L, R, val, rt << 1 | 1);
else if(mid >= R) update1(L, R, val, rt << 1);
else{
update1(L, R, val, rt << 1);
update1(L, R, val, rt << 1 | 1);
}
tree[rt].num = tree[rt << 1].num + tree[rt << 1 | 1].num;
}
//单点改值
void update(int pos, int val, int rt){
if(tree[rt].L == tree[rt].R){
tree[rt].num += val;
return;
}
int mid = (tree[rt].L + tree[rt].R) >> 1;
if(mid < pos) update(pos, val, rt << 1 | 1);
else update(pos, val, rt << 1);
tree[rt].num = tree[rt << 1].num + tree[rt << 1 | 1].num;
}
int main(){
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
int n, m; cin >> n >> m;
for(int i = 1; i <= n; i++) cin >> a[i];
Build(1, n, 1);
while(m--){
int f, l, r;
cin >> f >> l >> r;
if(f == 2) cout << query(l, r, 1) << endl;
else {
int x;
cin >> x;
update1(l, r, x, 1);
}
}
return 0;
}