线段树入门
LITFLYR
·
·
算法·理论
概念
更国际化的阅读方式
线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
线段树可以在 O(log N) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间最大值,求区间最小值)等操作。
出自:OI Wiki
注:后面会讲复杂度的证明。
举例:现在有 n 个数,我需要对其进行如下操作:
- 修改其中一个数。
- 对其中连续的若干个数求和(求最大,最小等)。
- 对其中连续的若干个数进行修改。
在使用数组存储时,对于操作1我们可以直接进行 O(1) 修改。而执行操作2或者操作3时,我们需要把整个数组扫一遍进行修改,这样的时间复杂度是 O(n) 的,而对于 k 次操作,复杂度可以达到 O(kn),这样一旦数据范围过大,就很容易超时。所以这个时候,这种叫做线段树的数据结构就可以帮助我们达到 O(k log n) 的复杂度,在运行程序时速度就会大大加快。
首先,在学习线段树之前,我们需要对他的结构有所了解:
如图所示,线段树是一颗二叉树(每个节点最多有两个儿子),一颗线段树存储的是一段区间,如图所示他的根节点(图上节点1)表示整个区间,他的左儿子(图上节点2)和右儿子(图上节点3)将整个区间分成两段进行存储,直到不能再分为止。
观察图上,我们可以发现规律:
对于任意一个区间 [l,r],有 mid=\lfloor \frac{l+r}{2} \rfloor,则该节点的左儿子表示区间 [l,mid],右儿子表示区间 [mid+1,r]。
再观察节点编号,可以发现不是按照 1-n 连续存储,这是因为我们在记录线段树时通常使用一维数组,我们需要准确定位每个节点的左儿子和右儿子,因此他的存储具有一定规律性:
对于一个节点标号为 x 的点,他左儿子的标号为 2x,右儿子的标号为 2x+1。
代码实现
将区间输入之后,执行 build(1, 1, n)函数。其中第一个 1 表示当前节点标号,因为我们要从根节点开始操作故最开始节点标号为 1,随后的 1,n 表示区间的 l,r,因为是根节点所以记录整个区间。
在建树过程中,我们一定会走到叶子节点(没有儿子的节点),这个时候我们无法再继续向下建树,那我们该怎么返回呢?
其实只要在函数最开始做一个判断该节点(l==r)就好。
如果该节点不是叶子节点,我们需要继续建树,这时就需要用到递归的思想,将一个大区间分为两个小区间再处理。
#define ls(f) (f*2)//将代码中出现的 ls(f) 替换为 f*2
#define rs(f) (f*2+1)
void push_up(int f){//因为每次改变一个区间的时候,他的所有祖先都会被影响,所以需要向上更新。
//视情况而定,如果求和就更新 tre[f]=tre[ls(f)]+tre[rs(f)]
//也有可能是求最大值,最小值之类
}
void build(int f,int l,int r){
if(l==r){
tre[f]=a[l];//记录节点的值
return ;
}
int mid=(l+r)>>1;//或写为 int mid=(l+r)/2; 效果相同
build(ls(f),l,mid);//在这个区间没办法赋值,那我们就递归到可以赋值。
build(rs(f),mid+1,r);
push_up(f);
}
int main(){
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
build(1,1,n);
}
其实单点查询就是查询一个长度为 1 的区间,所以放在一起讲。
定义查询函数 query(1,1,n,x,y) ,分别表示当前节点,当前区间的边界,想要查询的区间。
如图,如果我要查询区间 [4,6],可以发现他在树中是以 [4,5], [6,6] 的方式记录,所以在查询过程中我们需要把多段合在一起处理(相加,取最大、最小等)。在我们查询过程中,因为 [4,5] 已经包含在我们想要查询的区间里,就没有必要再递归 [4,4], [5,5] 了,所以这个函数的返回条件是 (x<=l&&r<=y)。其余思想与建树过程基本一致。
#define ls(f) (f*2)//将代码中出现的 ls(f) 替换为 f*2
#define rs(f) (f*2+1)
void push_up(int f){//因为每次改变一个区间的时候,他的所有祖先都会被影响,所以需要向上更新。
tre[f]=tre[ls(f)]+tre[rs(f)];//这里拿求和举例。
}
int query(int f,int l,int r,int x,int y){
if(x<=l&&r<=y){
return tre[f];
}
int mid=(l+r)>>1,ans=0;
if(x<=mid) ans+=query(ls(f),l,mid,x,y);//如果要查询的区间没有覆盖当前区间的左儿子就没必要查询了
if(mid<y) ans+=query(rs(f),mid+1,r,x,y);
return ans;
}
int main(){
cin >> n;
for(int i=1;i<=n;i++) cin >> a[i];
build();
cin >> x >> y;
cout << query(1,1,n,x,y);
}
复杂度证明:
---
- **代码实现:区间修改(单点修改)**
单点修改其实基本思路和单点查询一样,但是在找到这个点之后进行修改,并且不要忘记修改之后需要将改变后的状态上传,进行更新。
难点主要在区间修改,如果对区间内个每个数逐个进行修改,复杂度是 $O(nlogn)$ 的,明显不优,因此我们可以使用一种标记,记录我们未完成的更改,只更新最大的在更改范围内的区间,等到我们需要使用未更改的节点的时候在进行更新。这种标记就叫 **懒标记**(lazy tag)。
此时我们就需要一种函数来更新懒标记,与 $pushup()$ 函数不同,这种函数向下更新,所以我们一般将其命名为 $pushdown()$。
```cpp
void push_down(int f,int x,int y){
int mid=(x+y)>>1;
lazy_tag[ls(f)]=lazy_tag[ls(f)]+lazy_tag[f];//下放懒标记,存懒标记是存在已经完成修改的节点
lazy_tag[rs(f)]=lazy_tag[rs(f)]+lazy_tag[f];//需要修改的是存懒标记的节点的后代,自身不进行更改!
tre[ls(f)]+=lazy_tag[f]*(mid-x+1);//区间内每个数都要加,所以要乘上该区间内数的个数!
tre[rs(f)]+=lazy_tag[f]*(y-mid);//更新一定要使用父节点的懒标记
lazy_tag[f]=0;//使用完的懒标记要清空
}
void pus(int f,int l,int r,int x,int y,int k){
if(x<=l&&y>=r){
lazy_tag[f]+=k;//修改自身并且记录懒标记
tre[f]+=(r-l+1)*k;
return ;
}
push_down(f,l,r);//一定记得要下放懒标记!
int mid=(l+r)>>1;
if(x<=mid) pus(ls(f),l,mid,x,y,k);
if(y>mid) pus(rs(f),mid+1,r,x,y,k);
push_up(f);
}
```
---
## 例题
- [P3372](https://www.luogu.com.cn/problem/P3372)
这题是线段树模板,包含上述内容中的区间查询和区间修改,代码如下,细节看注释。
```cpp
#include<bits/stdc++.h>
using namespace std;
#define ls(x) (x<<1)
#define rs(x) (x<<1|1)
#define int long long
int n,m,a[100005],x,y,k,b;
int tre[400005],lazy_tag[400005];
void push_up(int f){
tre[f]=tre[ls(f)]+tre[rs(f)];
}
void push_down(int f,int x,int y){
int mid=(x+y)>>1;
lazy_tag[ls(f)]=lazy_tag[ls(f)]+lazy_tag[f];
lazy_tag[rs(f)]=lazy_tag[rs(f)]+lazy_tag[f];
tre[ls(f)]+=lazy_tag[f]*(mid-x+1);
tre[rs(f)]+=lazy_tag[f]*(y-mid);
lazy_tag[f]=0;
}
void build(int f,int l,int r){
lazy_tag[f]=0;
if(l==r){tre[f]=a[l];return ;}
int mid=(l+r)>>1;
build(ls(f),l,mid);
build(rs(f),mid+1,r);
push_up(f);
}
void pus(int f,int l,int r,int x,int y,int k){
if(x<=l&&y>=r){
lazy_tag[f]+=k;
tre[f]+=(r-l+1)*k;
return ;
}
push_down(f,l,r);//查询前下放懒标记!
int mid=(l+r)>>1;
if(x<=mid) pus(ls(f),l,mid,x,y,k);
if(y>mid) pus(rs(f),mid+1,r,x,y,k);
push_up(f);
}
int query(int f,int l,int r,int x,int y){
if(x<=l&&y>=r){return tre[f];}
int mid=(l+r)>>1;int ans=0;
push_down(f,l,r); //查询前下放懒标记!
if(x<=mid) ans+=query(ls(f),l,mid,x,y);
if(y>mid) ans+=query(rs(f),mid+1,r,x,y);
return ans;
}
signed main(){
cin >> n >> m;
for(int i=1;i<=n;i++){
cin >> a[i];
}
build(1,1,n);
while(m--){
cin >> b;
if(b==1){
cin >> x >> y >> k;
pus(1,1,n,x,y,k);
}
else if(b==2){
cin >> x >> y;
int ans=query(1,1,n,x,y);
cout << ans << endl;
}
}
return 0;
}
```
**好题推荐**
1. P3373 【模板】线段树 2
2. P2357 守墓人
3. P2574 XOR的艺术
4. P4513 小白逛公园
5. P4344 [SHOI2015] 脑洞治疗仪
完结撒花!