线段树

一、线段树

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点,如图下:

二、线段树的基础实现

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;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值