线段树的原理与模板(上)

昨天训练第一次做到了线段树的题,琢磨了两个小时代码,大概看懂了,今天特地来整理总结记录一下原理与模板
昨天看的一篇很不错的博文,也推荐给大家:https://blog.youkuaiyun.com/iwts_24/article/details/81484561
下面都是自己语言描述整理的

首先
为什么要使用线段树?
假设医院有8种药,每次开药进药都会引起响应库存数量的变化,也需要记录方便管理,我们都会想到用数组去存储

int a[8]={1,2,3,4,5,6,7,8};

如果进药(比如都是八盒),那就是很常规的O(n)遍历循环

for(int i=0;i<8;i++)
{
	a[i]+=8;
}

然后要求[a,b]的和,也当然是遍历,O(b-a)

线性的时间复杂度还是不太好的如果在比赛中,m个数据n个操作,可能会有t的可能
所以,使用线段树这种数据结构来存储和操作数据,降低时间复杂度

什么是线段树?
一种完全二叉树,除了最下层和次下层,其余层都放满,一定条件下会成为满二叉树,每个节点代表一个区间一种范围,树根节点代表最大的区间1-n,在医院这个例子中就是[1,8]
在这里插入图片描述树根节点,代表a[1-8]的和

接下来就是代码部分了,一段一段剖析着记录吧

线段树的操作-建树
大致的数据结构了解了,首先我们需要创建一个结构去存储,易看出线段树非常耗费空间,所以不管是结构体还是数组,都应该开大点空间,比如四倍

#define MAX 200000
 
int tree[4*MAX];
 
void init(){
    memset(tree,0,sizeof(tree));
}

建树开始

void build(int node,int l,int r){
	if(l == r){ // 到达叶子节点,赋值
		cin >> tree[node];
		return;
	}
	int mid = (l+r)/2;
	build(node*2,l,mid); // 进入子树开始递归
	build(node*2+1,mid+1,r);
	tree[node] = tree[node*2] + tree[node*2 + 1]; // 回溯
}

一开始调用为build(rt,1,8)进入函数(rt为树根节点),叶子节点赋值,通过二叉递归回溯给一系列父亲节点代入数据,创建好这棵树

线段树的操作-单点修改
单点操作比区间操作方便快捷的多,代码也简单,没有要用到lazy数组,单点更新很类似二分查找,二分递归找到更新点并更新,回溯后自然更新了上层一系列区间节点

// 单点更新,n为更新值,index为更新点,lr为更新范围
void update(int n,int index,int l,int r,int node){
	if(l == r) {
		tree[node] += n; // 更新方式,可以自由改动
		return;
	}
	int mid = (l+r) / 2;
	// push_down(node,mid-l+1,r-mid); 若既有点更新又有区间更新,需要这句话
	if(index <= mid){
		update(n,index,l,mid,node*2);
	}else{
		update(n,index,mid+1,r,node*2+1);
	}
	tree[node] = tree[node*2] + tree[node*2 + 1]; // 回溯过程,需要将线段树上层数据更新
}

和建树过程的回溯思想是一样的,很方便

懒惰标记
懒惰标记的作用呢,就是省去了改一次更新一次的繁琐,将修改的积累在一起,等到更新时统一更新线段树,理论上建一个跟线段树同样大小的数组,称为懒惰数组,表示每个节点的懒惰标记,有如下的操作:
1、修改数据的时候,每次递归到某节点,修改数据以后将数据的变化添加到数组中。
2、当使用到这个节点的时候,发现对应的懒惰标记存在,那就更新该节点,及以下所有的节点
简而言之就是不用时积累,使用时统一更新

那么懒惰数组的更新非常简单,对线段树更新的时候就可以添加到懒惰标记,但是在使用的时候,我们需要用一个函数来完成懒惰标记的下传操作,也就是更新积累的值。代码:

void push_down(int node,int l,int r){
	if(lz[node]){
		int mid = (l+r) / 2;
		lz[node*2] += lz[node];
		lz[node*2 + 1] += lz[node];
        // 注意线段树的数据更新方式要一致
		tree[node*2] += 1LL*(mid - l + 1)*lz[node];
		tree[node*2 + 1] += 1LL*(r - mid)*lz[node];
		lz[node] = 0;//lz标记清零
	}
}

当lz[rt]存在值时,我在使用这个节点,以下的节点就需要更新了,就利用二叉向下传递lz,同时也要更新数据,最终记得标记清零
注意,下推的时候不是一直更新到叶子节点,而是只更新当前节点以及2个子树,因为实际操作的时候,只要碰到对某节点的操作就要调用push_down()函数,所以每次只用下推一层即可。

线段树的操作-区间更新

单点更新类似二分查找,更新的时候对经过的路径进行操作就可以了。但是区间更新需要考虑整个区间。线段树除了叶子节点,都表示了一段区间的值,那么就要配合懒惰标记在整个区间上进行操作。先看代码:

// 区间更新,lr为更新范围,LR为线段树范围,add为更新值
void update_range(int node,int l,int r,int L,int R,int add){
	if(l <= L && r >= R){
		lz[node] += 1LL*add;
		tree[node] += 1LL*(R - L + 1)*add; // 更新方式
		return;
	}
	push_down(node,L,R);
	int mid = (L+R) / 2;
	if(mid >= l) update_range(node*2,l,r,L,mid,add);
	if(mid < r) update_range(node*2 + 1,l,r,mid+1,R,add);
	tree[node] = tree[node*2] + tree[node*2 + 1];
}

单点更新是自下而上更新,而区间更新可以看做自上而下,找到区间,配合懒惰标记更新下一层

线段树的操作-区间查找

区间查找和区间更新有一定的类似之处,值得注意的是,每次查询到点时一定要调用一次push_down更新值及下一层节点的lazy值

// 区间查找
LL query_range(int node,int L,int R,int l,int r){
	if(l <= L && r >= R) return tree[node];
	push_down(node,L,R);
	int mid = (L+R) / 2;
	LL sum = 0;
	if(mid >= l) sum += query_range(node*2,L,mid,l,r);
	if(mid < r) sum += query_range(node*2 + 1,mid+1,R,l,r);
	return sum;
}

理解懒惰数组及其模板后,确实有些胸有成竹,但具体到题目中的话,对模板进行修改就会增加难度,不易修改,所以要不要使用lazy数组得看具体情况而定,等下篇博客再记录.

接下来是整个模板,引用那篇博客里的模板,做个记录

#include<iostream>
#include<string>
#define LL long long
#define MAX 1001
 
using namespace std;
 
int tree[MAX]; // 线段树
int lz[MAX]; // 延迟标记
 
void init(){
	memset(tree,0,sizeof(tree));
	memset(lz,0,sizeof(lz));
}
 
// 创建线段树
void build(int node,int l,int r){
	if(l == r){
		cin >> tree[node];
		return;
	}
	int mid = (l+r)/2;
	build(node*2,l,mid);
	build(node*2+1,mid+1,r);
	tree[node] = tree[node*2] + tree[node*2 + 1];
}
 
// 单点更新,n为更新值,index为更新点,lr为更新范围
void update(int n,int index,int l,int r,int node){
	if(l == r) {
		tree[node] = n; // 更新方式,可以自由改动
		return;
	}
	int mid = (l+r) / 2;
	// push_down(node,mid-l+1,r-mid); 若既有点更新又有区间更新,需要这句话
	if(index <= mid){
		update(n,index,l,mid,node*2);
	}else{
		update(n,index,mid+1,r,node*2+1);
	}
	tree[node] = tree[node*2] + tree[node*2 + 1];
}
 
void push_down(int node,int l,int r){
	if(lz[node]){
		int mid = (l+r) / 2;
		lz[node*2] += lz[node];
		lz[node*2 + 1] += lz[node];
		tree[node*2] += 1LL*(mid - l + 1)*lz[node];
		tree[node*2 + 1] += 1LL*(r - mid)*lz[node];
		lz[node] = 0;
	}
}
 
// 区间更新,lr为更新范围,LR为线段树范围,add为更新值
void update_range(int node,int l,int r,int L,int R,int add){
	if(l <= L && r >= R){
		lz[node] += 1LL*add;
		tree[node] += 1LL*(R - L + 1)*add; // 更新方式
		return;
	}
	push_down(node,L,R);
	int mid = (L+R) / 2;
	if(mid >= l) update_range(node*2,l,r,L,mid,add);
	if(mid < r) update_range(node*2 + 1,l,r,mid+1,R,add);
	tree[node] = tree[node*2] + tree[node*2 + 1];
}
 
// 区间查找
LL query_range(int node,int L,int R,int l,int r){
	if(l <= L && r >= R) return tree[node];
	push_down(node,L,R);
	int mid = (L+R) / 2;
	LL sum = 0;
	if(mid >= l) sum += query_range(node*2,L,mid,l,r);
	if(mid < r) sum += query_range(node*2 + 1,mid+1,R,l,r);
	return sum;
}
 
int main() {
	init();
	build(1,1,8);
 
	system("pause");
	return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值