线段树专题一:基本应用

线段树专题一:基本应用

——第四次智商危机实录

古有全排列模拟一夜而完全不解,今有线段树课程两天而一分未懂

一 线段树初步中的初步

1 开始前:为什么,是什么

线段树,顾名思义,就是把一个线段拆分成一棵树。
更具体点,是一棵二叉树。
图源:百度百科
那么这么做的意义是什么?
很明显,如果这个序列变成这种形式,根据倍增的基础知识,我们找到一个特定点、一个特定区间的次数都会变成logn次,同时我们修改一个点或一个区间所需要修改的次数也会变成logn次。而从n到logn节省的时间是非常巨大的(指数级优化),所以线段树在区间维护、区间查询的问题上能够省下非常多的时间。
这样一来,线段树的应用范围和功能也就明确了:区间维护、区间查询
在这一部分中,就将会围绕线段树的基本功能进行讨论。

2 开始:区间查改

区间查改是最能体现出线段树优势的地方,毕竟一般的线段单点查改是O(1)的,其实比线段树还快(废话)。既然如此,那肯定是要从区间查改开始。

先来一个最最基本的:区间加,区间求和
T1 线段树1 (洛谷P3372,难度1)

分析一下完成这个任务需要的功能模块。
第一个,我们一开始有一个给定的序列,那肯定要有建树;
第二,我们要区间查改,那肯定还要有修改和查询。
结束了。
具体研究一下这三个东西怎么写。
首先根据这个结构来看,肯定要递归的操作。由于每一个段都是由左右两边的数据(以下称为左右儿子)组合出来的,所以肯定要递归左右儿子。至于左右儿子的编号,根据二叉树的理论,就k << 1和k << 1 | 1好了。

友情提示:k | 1不等于k+1,这里之所以能用位运算替代,是因为左移之后最右面一位一定是0.

再考虑一下边界。建树实质上就是单点修改,所以边界就是l==r;区间操作的话,如果当前查询的区间包括在目标区间当中,那么就直接返回结果,不必再查了;否则,我们根据左右儿子的特点来看,如果查询的区间与目标区间左半边有交集,就查询左儿子,另一侧同理。
这样一来,基本上线段树的雏形就出来了。
考虑一下有没有更优的处理方法:由于我们查区间改区间,如果查询区间已经包含在目标区间内了,对于更小的区间,我们不一定要改。例如我们给1 ~ 5的每个数加3,又查询1 ~ 5,那我们就不需要处理1 ~ 2,3 ~ 5以及更小的区间。这样一来我们就可以把要更新的内容打个标记,说明某个修改停留在某个位置,如果我们将要继续查询儿子,再把这个修改更新给儿子。这个标记,被称为懒标记。
很显然,懒标记传递也是一个功能模块。这样一来,此题涉及的线段树就有4个模块:建树、标记传递、修改、查询(从上到下)。此题就完成了。
注意一个细节:与线段树有关的数组要开到4n。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define mid (l + r >> 1)//注意:括号打在外面
using namespace std;
long long a[1000001];
struct yjx{
   
	long long tre[1000001],laz[1000001];
	void build(int k,int l,int r){
   
		if(l == r){
   
			tre[k] = a[l];
			return;
		}
		build(k << 1,l,mid),build(k << 1 | 1,mid + 1,r);
		tre[k] = tre[k << 1] + tre[k << 1 | 1];
	}
	void push(int k,int l,int r){
   
		laz[k << 1] += laz[k],laz[k << 1 | 1] += laz[k];
		tre[k << 1] += laz[k] * (mid - l + 1),tre[k << 1 | 1] += laz[k] * (r - mid);
		laz[k] = 0;//一定要记得清空
	}
	void change(int k,int l,int r,int x,int y,long long c){
   
		if(x <= l && r <= y){
   
			tre[k] += (r - l + 1) * c;
			laz[k] += c;
			return;
		}
		push(k,l,r);
		if(x <= mid) change(k << 1,l,mid,x,y,c);
		if(y > mid) change(k << 1 | 1,mid + 1,r,x,y,c);
		tre[k] = tre[k << 1] + tre[k << 1 | 1];
	}
	long long query(int k,int l,int r,int x,int y){
   
		long long ret = 0;
		if(x <= l && r <= y) return tre[k];
		push(k,l,r);
		if(x <= mid) ret += query(k << 1,l,mid,x,y);
		if(y > mid) ret += query(k << 1 | 1,mid + 1,r,x,y);
		return ret;
	}
}STr;
int main(){
   
	int m,n,i,x,y,z;
    long long w;
	scanf("%d %d",&n,&m);
	for(i = 1;i <= n;i++) scanf("%lld",&a[i]);
	STr.build(1,1,n);
	for(i = 1;i <= m;i++){
   
		scanf("%d",&z);
		if(z == 1){
   
			scanf("%d %d %lld",&x,&y,&w);
			STr.change(1,1,n,x,y,w);
		}
		if(z == 2){
   
			scanf("%d %d",&x,&y);
			printf("%lld\n",STr.query(1,1,n,x,y));
		}
	}
	return 0;
}

(注:结构体内引用结构体内的函数不加结构体前缀)

3 懒标记的运用

懒标记的本质其实就是维护。随着线段树问题的不同,懒标记肯定也会不同。那么懒标记的运用有没有局限性呢?
为了整明白这个问题,我们需要考虑懒标记如何传递。传递的对象无非就是两个:左右儿子和左右儿子的懒标记。
不妨举几个例子:
(1) 没有任何的修改操作,懒标记明显没用 (没有查询是不可能的,不然写了个寂寞)
(2)单点修改,直接改就行了,不需要懒标记。
(3)区间取模,区间开平方,…根本无法维护或者是个定值没有维护的必要。
综上所述,懒标记适用于区间修改当中变量与变量之间的互动,例如五则基本运算。其余的要寻求其他的优化方式,或者直接暴力修改。
那如果好几个操作都需要打标记咋办呢?

T2 线段树2(洛谷P3373,难度2.5)
区间加,区间乘,区间求和。

这题操作本身没什么难度,关键就在于懒标记怎么传递。
首先,懒标记可以一起传递,没必要写俩push函数。
其次,如果要给儿子更新乘法和加法的懒标记,那肯定是先乘后加。这里我们没必要纠结万一先来个区间加后来个区间乘怎么办,因为之前的标记该传的早就传完了,我们只需要考虑四则运算本身的优先级。
再次,如果我们要传递左右儿子的懒标记,要注意懒标记是接下来将要传递的信息,所以单纯的懒标记传递当中,懒标记之间也是要运算的。 例如,如果先后给同一个区间+3和*2,那么乘法的时候原来的+3标记就应该变成+6。
这样一来,我们可以总结出,懒标记在传递标记和更新的过程中是遵循优先级进行运算的,懒标记之间会有互动(高级影响低级)。
代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#define mid (l + r >> 1)
using namespace std;
long long p,a[1000001];
struct yjx{
   
	long long tre[1000001],laz1[1000001],laz2[1000001];
	void build(int k,int l,int r){
   
		laz2[k] = 1;
		if(l == r){
   
			tre[k] = a[l] % p;
			return;
		}
		build(k << 1,l,mid),build(k << 1 | 1,mid + 1,r);
		tre[k] = (tre[k << 1] + tre[k << 1 | 1]) % p;
	}
	void push(int k,int l,int r){
   
		tre[k << 1] = (tre[k << 1] * laz2[k] + laz1[k] * (mid - l + 1)) % p;
		tre[k << 1 | 1] = (tre[k << 1 | 1] * laz2[k] + laz1[k] * (r - mid)) % p;
		laz1[k << 1] = (laz1[k << 1] * laz2[k] + laz1[k]) % p;
		laz1[k << 1 | 1] = (laz1[k << 1 | 1] * laz2[k] + laz1[k]) % p;
		laz2[k << 1] = (laz2[k << 1] * laz2[k]) % p;
		laz2[k << 1 | 1] = (laz2[k << 1 | 1] * laz2[k]) % p;
		laz1[k] = 0;
		laz2[k] = 1;
	}
	void change1(int k,int l,int r,int x,int y,int c){
   
		if(x <= l && r <= y){
   
			tre[k] = (tre[k] + (r - l + 1) * c) % p;
			laz1[k] = (laz1[k] + c) % p;
			return;
		}
		push(k,l,r);
		if(x <= mid) change1(k << 1,l,mid,x,y,c);
		if(y > mid) change1(k << 1 | 1,mid + 
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值