//悲观者永远正确,乐观者永远前行。|

ccrui

园龄:2年1个月粉丝:2关注:4

2023-01-16 16:25阅读: 48评论: 0推荐: 0

线段树学习笔记


线段树简介

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

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

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

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

image

image

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

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

线段树特性:

  • 长度是 n 的序列构造线段树,这颗线段树有 2n1 个节点(同二叉树叶子节点与所有节点数量的关系),高度为 logn
  • 存线段树的数组要开 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

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

[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

[3,8]

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

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

image

[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

本文作者:ccrui

本文链接:https://www.cnblogs.com/ccr-note/p/stree.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   ccrui  阅读(48)  评论(0编辑  收藏  举报
评论
收藏
关注
推荐
深色
回顶
收起
点击右上角即可分享
微信分享提示