线段树 SEGMENT TREE

· · 科技·工程

前言-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 满足:

  1. 其父节点:\lfloor\frac{k}{2}\rfloor——k>>1
  2. 其左儿子:2k——k<<1
  3. 其右儿子: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)