线段树学习笔记

时间:2022-05-14
本文章向大家介绍线段树学习笔记,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

0.什么是线段树

线段树有着比分块小的时间复杂度,也有着比树状数组大的通用性,有着比平衡树更小的常数和代码难度,实在是一种非常牛逼的东西。

1.线段树入门

我们要了解线段树,首先得明白它长什么样。
此处放一张图:

如图,一个节点代表一个区间,从中点切开这个区间,就成为了左右区间(不一定严谨,听着吧)。
由于每切一次,区间都减少一半,因此树高为\(\log n\)级别。
叶节点的值直接由序列得出,其它节点有左右节点合并得出。
为方便,将\(p\)的左右节点分别设为\(p\times2\)\(p\times2+1\)(可以证明这样节点不会重复,只是牺牲了一点空间)。
词穷了,直接开始上代码吧。

//以下为加减,求和操作
void pushup(int p){
	val[p]=val[p<<1]+val[p<<1|1];
}
void build(int p,int l,int r){//建树
	if(l==r){
		val[p]=v[l];//v[l]为序列第l项
	}
	int mid=(l+r>>1);
	build(p<<1,l,mid);build(p<<1|1,mid+1,r);
	pushup(p);//将左右节点的信息合并
}

学会了建树,接下来就应该是操作了。
操作1:单点查询
简单到不知道如何解释了,直接上代码。

int query(int p,int l,int r,int pos){//pos为查询位置
	if(l==r)return val[p];
	int mid=(l+r>>1);
	if(pos<=mid)return query(p<<1,l,mid,pos);
	else return query(p<<1|1,mid+1,r,pos);
}

操作2:单点修改
自上而下到叶节点进行修改,再自下而上更新

void modify(int p,int l,int r,int pos,int x){
	if(l==r){
		val[p]+=x;
		return;
	}
	int mid=(l+r>>1);
	if(pos<=mid)modify(p<<1,l,mid,pos);
	else modify(p<<1,mid+1,r,pos);
}

以上操作暴力都可以做到,现在就要介绍暴力做不到的:区间操作。
首先上简单点的:区间查询。
我们想一想,维护非叶节点有什么用?进行区间操作。
先上代码:

void query(int p,int l,int r,int L,int R){
	if(L<=l&&r<=R)return val[p];//当前区间被完全包含,直接返回
	int mid=(l+r>>1),ans=0;
	if(L<=mid)ans+=query(p<<1,l,mid,L,R);//左儿子与询问区间有交集
	if(R>mid)ans+=query(p<<1|1,mid+1,r,L,R);//右儿子与询问区间有交集
}

易证,信息肯定是不重不漏的。
下面进行时间复杂度证明:
只有包含端点的区间才能一直递归下去。
未包含端点有两种情况:完全在询问区间内,完全在询问区间外,这些都是直接返回。
而端点只有两个,树高为\(\log n\)级别,因此查询复杂度也为\(\log n\)

接下来是区间修改。
询问可以不到叶子,因为只要获取信息。但修改不行,因为修改要将信息更新。
好像思路进入了僵局。
但线段树有一个强大的功能:懒标记。
有一个标记,代表此节点已被更新,但子节点未被更新。
如果区间被修改区间完全包含,打一个标记,并更新它的信息。
如果以后需要对此区间的儿子进行操作,就将标记下传,更新儿子节点的信息,将儿子节点打上标记,并将此节点的标记去除。
标记下传复杂度理论为\(O(1)\),因此只是常数大了那么一点(不是亿点)。

void pushdown(int p,int l,int r){
	if(tag[p]){
		int mid=(l+r>>1);
		val[p<<1]+=(mid-l+1)*tag[p];
		val[p<<1|1]+=(r-mid)*tag[p];
		tag[p<<1]+=tag[p];tag[p<<1|1]+=tag[p];
		tag[p]=0;
	}
}
void modify(int p,int l,int r,int L,int R,int x){
	if(L<=l&&r<=R){
		val[p]+=(r-l+1)*x;tag[p]+=x;return;
	}
	pushdown(p,l,r);int mid=(l+r>>1);
	if(L<=mid)modify(lc,l,mid,L,R,x);
	if(R>mid)modify(rc,mid+1,r,L,R,x);
}

P3372代码如下:

#include<bits/stdc++.h>
using namespace std;
int n,m,a[100001];
struct tree{
	int l,r;
	long long sum,add;
}tree[400001];
void label(int p){
	if(tree[p].add){
		tree[p*2].sum+=(tree[p*2].r-tree[p*2].l+1)*tree[p].add;
		tree[p*2+1].sum+=(tree[p*2+1].r-tree[p*2+1].l+1)*tree[p].add;
		tree[p*2].add+=tree[p].add;
		tree[p*2+1].add+=tree[p].add;
		tree[p].add=0;
	}
}
void bulid(int l,int r,int p){
	tree[p].l=l,tree[p].r=r;
	if(l==r){
		tree[p].sum=a[l];
		return;
	}
	int mid=(l+r)/2;
	bulid(l,mid,p*2);
	bulid(mid+1,r,p*2+1);
	tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
}
void change(int l,int r,int p,int num){
	if(tree[p].l>=l&&tree[p].r<=r){
		tree[p].sum+=1ll*num*(tree[p].r-tree[p].l+1);
		tree[p].add+=num;
		return;
	}
	label(p);
	int mid=(tree[p].l+tree[p].r)/2;
	if(l<=mid)change(l,r,p*2,num);
	if(r>mid)change(l,r,p*2+1,num);
	tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
}
long long ask(int l,int r,int p){
	long long ans=0;
	if(tree[p].l>=l&&tree[p].r<=r){
		return tree[p].sum;
	}
	label(p);
	int mid=(tree[p].l+tree[p].r)/2;
	if(l<=mid)ans+=ask(l,r,p*2);
	if(r>mid)ans+=ask(l,r,p*2+1);
	return ans;
}
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++){
		scanf("%d",a+i);
	}
	bulid(1,n,1);
	while(m--){
		int op,l,r;
		scanf("%d%d%d",&op,&l,&r);
		if(op==1){
			int x;
			scanf("%d",&x);
			change(l,r,1,x);
		}
		else printf("%lld\n",ask(l,r,1));
	}
	return 0;
}

其它操作例如区间最值,其实是大同小异的。

原文地址:https://www.cnblogs.com/andy-lin102/p/16269642.html