线段树 SEGMENT TREE
HYLW
·
2024-03-15 21:46:24
·
科技·工程
前言-INTRODUCTION
线段树,是一种功能更全面的树状数组,可以做到更多复杂的操作,然而时间空间复杂度均有提高。
一、线段树基本思想-THE BASIC IDEA OF THE SEGMENT TREE
简单来讲就是分治 ,分而治之,当然,这里的线段树是二叉树,你也可以看做二分(然而二分也是分治)。
我们按照长度固定为 n 的数列依次确立 n 个节点,每个节点表示一个区间的信息,节点间当然有根叶关系,我们规定节点 k 代表区间 [l,r] ,其左右节点分别为 2k [l,\lfloor\frac{(r+l)}{2}\rfloor ] 与 2k+1 [\lfloor \frac{(r+l)}{2}\rfloor+1,r] 。
可以发现,这里使用了堆的存储方式 。那么一个节点 k 满足:
其父节点:\lfloor\frac{k}{2}\rfloor ——k>>1
其左儿子:2k ——k<<1
其右儿子:2k+1 ——k<<1|1
其中,根节点编号 k=1 表示区间 [1,n] ,所有表示区间 [i,i] 的节点称为元线段,此时可知,所有的非元线段对应节点有左右子节点。
例如一个长度为 10 的数列,得到的线段树如图。
假如你看过这个视频,那么你应该会比较理解。
这里就可以看出线段树的第一个问题:空间复杂度。显然,对于一个长度为 n 的“线段”,其所需空间为 O(x)=O(1+2+4+8+……+\frac{n}{2}+n)\approx O(2n) ,然而,可以看见,在上述例子中 2n 的空间还不够,要开到 4n 。再加上线段树常用结构体,这就很\texttt{逆天} 。
下面开始讲操作,这才是核心,我们将顺便看清线段树的时间复杂度。
二、线段树的基本操作-THE BASIC OPERATION OF SEGMENT TREE
一般来讲,最基础的线段树有五个操作——
由于线段树适用极广,我们用例题来示范一下:luogu P3372 【模板】线段树 1
动手。
0.初始化-INIT
一般我们把线段树设为结构体,线段树的每个节点应该包含它的范围、性质信息、懒标记。线段树数组开 4 倍。
当然根据题意也有其他设置。开long long。
const int N = 100100;
typedef long long ll;
int n,m;
struct node{
int l,r;//范围:左右边界
ll sum;//性质信息:区间和
ll k;//懒标记
#define l(u) tr[u].l
#define r(u) tr[u].r
#define sum(u) tr[u].sum
}tr[N*4];//线段树数组:开 4 倍
1.\texttt{build()} 操作-\texttt{build()} OPERATION
建立一棵线段树,我们从根节点开始分别向左右儿子深搜。
```cpp
void build(int u,int l,int r){
tr[u].l=l,tr[u].r=r;//左右边界初始化
tr[u].k=0;//懒标记初始化
if(l==r){//当为整棵数的叶子节点
tr[u].sum=(ll)a[l];//性质(区间和)初始化
return ;
}
//如果不是叶子节点
int mid=(l+r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);//左右儿子更新节点
pushup(u);//儿子更新完后更新爸爸,保证根节点的正确性
}
```
这样的时间复杂度是 $O(4n)$ 。
实际上,$\texttt{build()}$ 操作在每次建树时只会用到一次,所以实际上你也可以写成这样:
```cpp
void build(int u,int l,int r){
tr[u].l=l,tr[u].r=r;//左右边界初始化
tr[u].k=0;//懒标记初始化
if(l==r){//当为整棵数的叶子节点
return ;
}
//如果不是叶子节点
int mid=(l+r)>>1;
build(u<<1,l,mid),build(u<<1|1,mid+1,r);
//左右儿子更新节点
}
……//若干操作
int main(){
scanf("%d",&n);
build(1,1,n);//建树
for(int i=1;i<=n;i++){
ll a;
//根据题目,其和有可能爆int,数据类型都用long long
scanf("%lld",&a);
modify(1,i,i,a);//修改操作
}
}
```
### 2.$\texttt{pushup()}$ 操作-$\texttt{pushup()}$ OPERATION
也就是用儿子节点的性质信息来更新父节点的性质信息。
在例子中体现为“父区间的和为子区间的和的和”。
```cpp
void pushup(int u){
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
```
### 3.$\texttt{pushdown()}$ 操作- $\texttt{pushdown()}$ OPERATION
也就是用父节点更新儿子节点,主要是懒标记下放。每次 $\texttt{pushdown()}$ 只下放一层。
我们需要先把懒标记下放,再更新儿子节点。
```cpp
void pushdown(int u){
//左儿子
tr[u<<1].sum+=tr[u].k*(tr[u<<1].r-tr[u<<1].l+1);
//注意这里不是用tr[u<<1].k,
//我们保证每个有懒标记的节点首先有其自身的正确性,即处理过懒标记
tr[u<<1].k+=tr[u].k;
//右儿子
tr[u<<1|1].sum+=tr[u].k*(tr[u<<1|1].r-tr[u<<1|1].l+1);
tr[u<<1|1].k+=tr[u].k;
//懒标记清空
tr[u].k=0;
}
```
### 4.$\texttt{query()}$ 操作-$\texttt{query()}$ OPERATION
线段树的两大难点之一便是 $\texttt{query()}$ 操作。
单点询问很简单,一直搜就可以了。这里就不展示了。
区间询问是懒标记的核心之一。
在每次询问时,我们从根节点开始,当当前节点有用时,便返回,同时注意懒标记。
根据这道题具体来讲,从根节点 $[1,n]$ 开始,但凡有交集,就追问下去,并下放懒标记;当完全包含,就返回性质信息,由于追问时已经下放了懒标记,这里是正确的,无需再考虑懒标记。
```cpp
ll query(int u,int l,int r){//返回区间和有可能爆int
if(l<=tr[u].l && tr[u].r<=r){//完全包含
return tr[u].sum;
//由于追问时已经处理了懒标记,直接获取性质信息
}else{
pushdown(u);//下放懒标记保证正确性
int mid=(tr[u].l+tr[u].r)>>1;
ll v=0;
if(l<=mid) v+=query(u<<1,l,r);
//左半边有交集,获取右边的查询信息
if(r>mid) v+=query(u<<1|1,l,r);
//右半边有交集 获取左边的查询信息
return v;
}
}
```
时间复杂度是 $O(2\log n)$ 。
### 5.$\texttt{modify()}$ 操作-$\texttt{modify()}$ OPERATION
线段树操作的两大难点之一。
修改分为单点修改和区间修改,单点修改很简单(~~懒得写~~),这里只展示区间修改。
同样的,从根结点开始,当当前区间完全包含于目标区间时,直接给懒标记更新,同时保证当前节点的正确性,处理懒标记;当当前区间仅仅有交集时,假如有懒标记未下放,那么就先下放,这样才能保证修改子节点时的正确性,然后追问子节点,直到完全包含。
```cpp
void modify(int u,int l,int r,ll v){
if(l<=tr[u].l && tr[u].r<=r){
tr[u].k+=v;//懒标记
tr[u].sum+=v*(tr[u].r-tr[u].l+1);
}else{
if(tr[u].k) pushdown(u);
//保证下放懒标记前子节点的正确性
int mid=(tr[u].l+tr[u].r)>>1;
if(mid>=l) modify(u<<1,l,r,v);
//与左半边有交集,追下去
if(r>mid) modify(u<<1|1,l,r,v);
//与右半边有交集,追下去
pushup(u);
//子节点已修改,更新父节点
}
}
```
时间复杂度是 $O(2\log n)$ 。
至此,线段树的所有通用操作均已说明,可以找题练练了。
当然,这里也有:
- [`luogu P3373 【模板】线段树 2`](https://www.luogu.com.cn/problem/P3373)$\textcolor{darkred}{\textbf{attention}}$:线段树里套模拟,模拟“拆括号”
- [`luogu P1253 扶苏的问题`](https://www.luogu.com.cn/problem/P1253)$\textcolor{darkred}{\textbf{attention}}$:线段树里套模拟
- [`P11289 【MX-S6-T1】「KDOI-11」打印`](https://www.luogu.com.cn/problem/P11289):正解不是线段树,但是对于线段树来讲是水题。
[我在`线段树`中调啊调啊调](https://www.luogu.com.cn/training/1416#information)
## 三、李超线段树-$\text{Li Chao}$ SEGMENT TREE
[博客](https://blog.csdn.net/fzyyyyyyyy/article/details/133186426)