线段树学习笔记

线段树简介

线段树是一种二叉搜索树,与区间树相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。
使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为 \(O(logN)\) 。而未优化的空间复杂度为 \(2N\) ,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。
——百度百科

线段树是一种基于分治思想的二叉树结构,用于区间信息统计

线段树对比树状数组有如下好处:

  1. 每个节点都代表一个区间
  2. 具有唯一根节点,代表 \([1,N]\)
  3. 每个叶子节点代表一个元素
  4. 可以用二叉树的方式存储(详见下面)

image

\[\scriptsize\color{grey}{区间视角} \]

image

\[\scriptsize\color{grey}{树视角} \]

因为线段树接近完全二叉树,故可以用如下表示方法:

  • 根节点编号为 \(1\)
  • 编号为 \(X\) 的节点左子节点编号为 \(2X\)
  • 编号为 \(X\) 的节点右子节点编号为 \(2X+1\)

线段树特性:

  • 长度是 \(n\) 的序列构造线段树,这颗线段树有 \(2n-1\) 个节点(同二叉树叶子节点与所有节点数量的关系),高度为 \(logn\)
  • 存线段树的数组要开 \(\mathbf{4}\) 倍空间

线段树实现

1.建树


先定义一个线段树的结构体:

struct tree{
	int l,r;//区间信息
	int sum,tag;//区间和及标记
   //其他变量的定义参考题目
}t[40010];

从上到下构建线段树,并从下往上传值,可用递归实现

void build(int x,int l,int r){
	t[x].l=l,t[x].r=r;//传递区间[l,r] 
	if(l==r){ 
		t[x].sum=a[l];
		return;
	}//此点如是叶子节点则结束递归 
	int mid=(l+r)>>1;//区间中点 
	build(x*2,l,mid);//构造左子树 
	build(x*2+1,mid+1,r);//构造右子树 
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}

调用入口:build(1,1,n);

2.基础修改与查询


1.单点修改与查询

线段树从根节点开始执行指令,我们可以通过递归找到要修改的节点,然后从下往上更新经过的所有节点(时间复杂度 \(O(logn)\)

我们如果更改点 \(1\) ,需要更改的节点如图红圈部分:
image

\[\scriptsize\color{grey}{更改点1} \]

void change(int x,int u,int a){
	if(t[x].l==t[x].r){//找到目标更改 
		t[x].sum=a;
		return;
	}
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	if(mid>=u)change(x*2,u,a);//u属于左半部分 
	else change(x*2+1,u,a);//u属于右半部分 
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值 
}

调用入口:change(1,x,a);

单点查询同理,只是不用回溯

int ask(int x,int u){
	if(t[x].l==t[x].r)return t[x].sum;//找到目标返回值 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	if(mid>=u)ask(x*2,u);//u属于左半部分 
	else ask(x*2+1,u);//u属于右半部分 
}

调用入口:ask(1,x);

2.区间查询(基础)

区间查询其实并不难,只要递归执行以下步骤:

  1. \([l,r]\) 完全覆盖了整个区间,立刻回溯
  2. 若左子节点与 \([l,r]\) 有重叠部分,递归访问左子节点
  3. 若右子节点与 \([l,r]\) 有重叠部分,递归访问右子节点

我们如果查询区间 \([2,7]\) ,需要查询的节点如图红圈部分:
image

\[\scriptsize\color{grey}{查询区间[2,7]} \]

int ask(int x,int l,int r){
	if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	int sum=0; 
	if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分 
	if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分 
	return sum;
}

调用入口:ask(1,l,r);

学了这么多,练习一下吧!

例题1

luogu P3374 【模板】树状数组 1
别看这道题题目是树状数组,其实用线段树单点修改,区间查询也是可以的

点击查看题目
#include<bits/stdc++.h>
using namespace std;
int n,m,a[4000010];
struct tree{
	int l,r;//区间信息
	int sum,tag;//区间和及标记
   //其他变量的定义参考题目
}t[4000010];
void build(int x,int l,int r){
	t[x].l=l,t[x].r=r;//传递区间[l,r] 
	if(l==r){ 
		t[x].sum=a[l];
		return;
	}//此点如是叶子节点则结束递归 
	int mid=(l+r)>>1;//区间中点 
	build(x*2,l,mid);//构造左子树 
	build(x*2+1,mid+1,r);//构造右子树 
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}
void change(int x,int u,int a){
	if(t[x].l==t[x].r){//找到目标增加 
		t[x].sum+=a;
		return;
	}
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	if(mid>=u)change(x*2,u,a);//u属于左半部分 
	else change(x*2+1,u,a);//u属于右半部分 
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值 
}
int ask(int x,int l,int r){
	if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	int sum=0; 
	if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分 
	if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分 
	return sum;
}
int main(){
	cin>>n>>m;
	for(int i=1;i<=n;i++)cin>>a[i];
	build(1,1,n);//建树 
	for(int i=1;i<=m;i++){
		int f,x,y;
		cin>>f>>x>>y;
		if(f==1){
			change(1,x,y);//单点修改
		}else{
			cout<<ask(1,x,y)<<endl;//区间查询
		}
	}
	return 0;
}

延时标记

1.简介

一般区间修改是这样的:

for(int i=l;i<=r;i++){
    change(1,i,a);
}

如果我们更改区间 \([3,8]\) ,要更改的点如下图,其中红圈表示循环中直接更改的点,绿圈表示递归中要更改的点
image

\[\scriptsize\color{grey}{更改区间[3,8]} \]

是不是要更改很多点,这样做时间复杂度都退化到 \(O(n logn)\) 了,比模拟法都慢

我们可以先不全修改,打上标记以后修改,但用的时候怎么办呢?我们可以到用的时候再更新,这样就可以把时间复杂度降到 \(O(logn)\)
如下图,我们只要修改红圈部分,并在完全覆盖的地方(绿圈部分)打上标记

image

\[\scriptsize\color{grey}{更改区间[3,8]} \]

2.实现

1.建树

建树函数没有变化

void build(int x,int l,int r){
	t[x].l=l,t[x].r=r;//传递区间[l,r] 
	if(l==r){ 
		t[x].sum=a[l];
		return;
	}//此点如是叶子节点则结束递归 
	int mid=(l+r)>>1;//区间中点 
	build(x*2,l,mid);//构造左子树 
	build(x*2+1,mid+1,r);//构造右子树 
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}

调用入口:build(1,1,n);

2.下传标记

下传标记其实很简单,都不用递归,只有更改下面左右子树的值和标记,再刷新自己的值就行了

void down(int x){
  	if(t[x].tag){//如果有标记 
  		t[x*2].tag+=t[x].tag;//下传左子树 
  		t[x*2+1].tag+=t[x].tag;//下传右子树 
  		t[x*2].sum+=(t[x*2].r-t[x*2].l+1)*t[x].tag;//左子树和增加 
		t[x*2+1].sum+=(t[x*2+1].r-t[x*2+1].l+1)*t[x].tag;//右子树和增加 
  		t[x].tag=0;//清空标记 
  	}
}

调用入口:down(x);

3.更改

更改也没很大差别,区别是到所有点如果完全包含就标记,叶子节点直接返回(因为没有要下传子节点了),两者都不成立就下传标记并继续递归
增加代码:

if(t[x].l>=l&&t[x].r<=r){//完全包含 
	t[x].tag+=a;//标记区间 
	t[x].sum+=(t[x].r-t[x].l+1)*a;//区间和增加 
	return;
}
if(t[x].l==t[x].r)return;//到叶子节点直接返回 
down(x);//这时还没有操作就需下传标记 

全代码:

void change(int x,int l,int r,int a){
	if(t[x].l>=l&&t[x].r<=r){//完全包含 
		t[x].tag+=a;//标记区间 
		t[x].sum+=(t[x].r-t[x].l+1)*a;//区间 和增加 
		return;
	}
	if(t[x].l==t[x].r)return;//到叶子节点直接返回 
	down(x);//这时还没有操作就需下传标记 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	if(mid>=l)change(x*2,l,r,a);//访问左半部分 
	if(mid<r)change(x*2+1,l,r,a);//访问右半部分
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值
}

调用入口:build(1,1,n);

4.查询

查询基本没变化,只要在不完全包含时下传标记再递归就行了

int ask(int x,int l,int r){
	if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含 
	down(x);//只多了一个下传标记 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	int sum=0; 
	if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分 
	if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分 
	return sum;
}

区间查询就这样结束了,做做题练练手吧!

例题2

luogu P3372 【模板】线段树 1
这道题用线段树区间修改,区间查询就行了

点击查看题目
#include<bits/stdc++.h>
using namespace std;
int a[100010],n;
struct stree{
	long long l,r;
	long long sum,tag;
}t[400010];
void build(int x,int l,int r){
	t[x].l=l,t[x].r=r;//传递区间[l,r] 
	if(l==r){ 
		t[x].sum=a[l];
		return;
	}//此点如是叶子节点则结束递归 
	int mid=(l+r)>>1;//区间中点 
	build(x*2,l,mid);//构造左子树 
	build(x*2+1,mid+1,r);//构造右子树 
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//从下往上传值
}
void down(int x){
  	if(t[x].tag){//如果有标记 
  		t[x*2].tag+=t[x].tag;//下传左子树 
  		t[x*2+1].tag+=t[x].tag;//下传右子树 
  		t[x*2].sum+=(t[x*2].r-t[x*2].l+1)*t[x].tag;//左子树和增加 
		t[x*2+1].sum+=(t[x*2+1].r-t[x*2+1].l+1)*t[x].tag;//右子树和增加 
  		t[x].tag=0;//清空标记 
  	}
}
void change(int x,int l,int r,int a){
	if(t[x].l>=l&&t[x].r<=r){//完全包含 
		t[x].tag+=a;//标记区间 
		t[x].sum+=(t[x].r-t[x].l+1)*a;//区间和增加 
		return;
	}
	if(t[x].l==t[x].r)return;//到叶子节点直接返回 
	down(x);//这时还没有操作就需下传标记 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	if(mid>=l)change(x*2,l,r,a);//访问左半部分 
	if(mid<r)change(x*2+1,l,r,a);//访问右半部分
	t[x].sum=t[x*2].sum+t[x*2+1].sum;//刷新值
}
long long ask(int x,int l,int r){
	if(l<=t[x].l&&r>=t[x].r)return t[x].sum;//完全包含 
	down(x);//只多了一个下传标记 
	int mid=(t[x].l+t[x].r)>>1;//区间中点 
	long long sum=0; 
	if(mid>=l)sum+=ask(x*2,l,r);//访问左半部分 
	if(mid<r)sum+=ask(x*2+1,l,r);//访问右半部分 
	return sum;
}
int main(){
  	int n,m;
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
  	build(1,1,n);//建树 
  	while(m--){
  		string op;
  		int a,b,c;
  		cin>>op>>a>>b;
  		if(op=="1"){
  			cin>>c;
  			change(1,a,b,c);//区间修改 
  		}else{
  			cout<<ask(1,a,b)<<endl;//区间查询 
  		}
  	}
  	return 0;
}

提示:开long long

例题3

luogu P3372 【模板】线段树 2

点击查看代码

顺序:加->乘->加

#include<bits/stdc++.h>
#define int long long
using namespace std;
int a[100010],n,p;
//线段树结构体,sum表示此时的答案,tagc表示乘法意义上的lazytag,tag是加法意义上的
struct stree{
	long long l,r;
	long long sum,tag,tagc;
}t[400010];
//buildtree
void build(int x,int l,int r){
	t[x].tag=0;
  	t[x].tagc=1;
	t[x].l=l,t[x].r=r;
	if(l==r){ 
		t[x].sum=a[l]%p;
		return;
	}
	int mid=(l+r)>>1;
	build(x*2,l,mid);
	build(x*2+1,mid+1,r); 
	t[x].sum=(t[x*2].sum+t[x*2+1].sum)%p;
}
void down(int x){
  	t[x*2].sum=(t[x*2].sum*t[x].tagc%p+(t[x*2].r-t[x*2].l+1)*t[x].tag%p)%p;
	t[x*2+1].sum=(t[x*2+1].sum*t[x].tagc%p+(t[x*2+1].r-t[x*2+1].l+1)*t[x].tag%p)%p;
  	t[x*2].tagc=t[x*2].tagc*t[x].tagc%p;
  	t[x*2+1].tagc=t[x*2+1].tagc*t[x].tagc%p;
  	t[x*2].tag=(t[x*2].tag*t[x].tagc%p+t[x].tag)%p; 
  	t[x*2+1].tag=(t[x*2+1].tag*t[x].tagc%p+t[x].tag)%p;
  	t[x].tag=0;
  	t[x].tagc=1;
}
//加 
void change(int x,int l,int r,int a){
	if(r<t[x].l||t[x].r<l)return ;
	if(t[x].l>=l&&t[x].r<=r){
		t[x].tag=(t[x].tag+a%p)%p; 
		t[x].sum=(t[x].sum+(t[x].r-t[x].l+1)*a%p)%p;
		return;
	}
	if(t[x].l==t[x].r)return;
	down(x);
	int mid=(t[x].l+t[x].r)>>1;
	if(mid>=l)change(x*2,l,r,a);
	if(mid<r)change(x*2+1,l,r,a);
	t[x].sum=(t[x*2].sum+t[x*2+1].sum)%p;
}
//乘 
void changech(int x,int l,int r,int a){
	if(r<t[x].l||t[x].r<l)return;
	if(t[x].l>=l&&t[x].r<=r){
		t[x].tagc=t[x].tagc*a%p;
		t[x].tag=t[x].tag*a%p;
		t[x].sum=t[x].sum*a%p;
		return;
	}
	if(t[x].l==t[x].r)return;
	down(x);
	int mid=(t[x].l+t[x].r)>>1;
	if(mid>=l)changech(x*2,l,r,a);
	if(mid<r)changech(x*2+1,l,r,a);
	t[x].sum=(t[x*2].sum+t[x*2+1].sum)%p;
}
//访问
long long ask(int x,int l,int r){
	if(r<t[x].l||t[x].r<l)return 0;	
	if(l<=t[x].l&&r>=t[x].r)return t[x].sum%p;
	down(x);
	int mid=(t[x].l+t[x].r)>>1;
	long long sum=0; 
	if(mid>=l)sum+=ask(x*2,l,r)%p;
	sum%=p;
	if(mid<r)sum+=ask(x*2+1,l,r)%p;
	sum%=p;
	return sum;
}
signed main(){
	//freopen("P3373_2.in","r",stdin);
	//freopen("s.txt","w",stdout);
  	int n,m,k;
	cin>>n>>m>>p;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		a[i]%=p;
	}
  	build(1,1,n);
  	while(m--){
  		int op;
  		int a,b,c;
  		cin>>op>>a>>b;
  		if(op==1){
  			cin>>c;
  			changech(1,a,b,c); 
  		}else if(op==2){
  			cin>>c;
  			change(1,a,b,c); 
  		}else{
  			cout<<ask(1,a,b)%p<<endl;
  		}
  	}
  	return 0;
}

线段树拓展

\[这个人颓废去了,N年后再更 \]

posted @ 2023-01-16 16:25  ccrui  阅读(64)  评论(0)    收藏  举报