【数据结构】线段树
CR400BF_1145 · · 算法·理论
要想进行区间查询,我们可以使用前缀和。要想处理区间修改,我们可以使用差分。然而,如果我们需要同时进行区间查询与修改,这两种算法都无法高效地解决问题。
此时,我们就需要一种数据结构——线段树来帮助我们,它可以以
约定
本文中的代码仅作为示范,请根据题目实际情况选择合适的数据类型与数组大小。
简介
线段树是一种专门用来处理区间问题的数据结构,一般是完全二叉树。它基于分治思想将原序列递归地进行划分,处理并记录各个子区间的信息。查询时,我们将目标区间分解为
| ::cute-table | 40 | < | < | < | < | < | < | < |
|---|---|---|---|---|---|---|---|---|
| 21 | < | < | < | 19 | < | < | < | |
| 9 | < | 12 | < | 11 | < | 8 | < | |
| 1 | 8 | 3 | 9 | 7 | 4 | 2 | 6 |
这是一棵维护区间和的线段树示例。最底层的就是原序列,它向上结合出了不同的子节点。
假设要查询区间
| ::cute-table | 40 | < | < | < | < | < | < | < |
|---|---|---|---|---|---|---|---|---|
| 21 | < | < | < | 19 | < | < | < | |
| 9 | < | 12 | < | 11 | < | 8 | < | |
| 1 | 8 | 3 | 9 | 7 | 4 | 2 | 6 |
线段树一般占用
由于目标区间由子区间的信息组合而成,线段树通常只用于维护符合结合律的操作。
建树
以维护区间和为例,我们首先需要建立线段树。可以考虑先从顶部出发向下直到底部,然后递归自底向上地建树。
首先,我们定义结构体node。
struct node
{
long long data,lazy=0;
}tree[400060];
里面的data就是该节点存储的数据,lazy的含义之后会讲。
接下来为了简洁,我们实现一个函数pushup(idx),它通过其子节点的信息维护当前节点的信息。
void pushup(int idx)
{
tree[idx].data=tree[idx<<1].data+tree[idx<<1|1].data;
return;
}
函数中,idx<<1是左孩子,idx<<1|1是右孩子。由于左移之后二进制的个位肯定是 idx<<1按位或
然后,我们实现建树函数build(l,r,idx)。如果区间的长度为
void build(int l,int r,int idx)
{
if(l==r)
{
tree[idx].data=a[l];
return;
}
int mid=l+((r-l)>>1);
build(l,mid,idx<<1);
build(mid+1,r,idx<<1|1);
pushup(idx);
return;
}
建树的操作是
build(1,n,1);
此时,idx中填写的就是根节点的编号,也就是 build(0,n-1,0),但并不推荐这样做。
更新
我们首先实现更新,不过区间更新操作是有很多种类型的,我们这里先假设更新就是给区间
对于第一种情况,我们直接返回就行了,不需要执行任何操作。第二种情况,如果更新到了叶子节点,我们就对其进行更新并返回,否则继续递归,不然下面的节点没有更新到,会出现数据不一致。最后一种情况,我们也继续递归,并在递归完成后进行pushup()操作。
void update(int L,int R,int l,int r,int idx,long long k)
{
if(l>R||r<L) return;
if(l>=L&&r<=R)
{
if(l==r)
{
tree[idx].data+=k;
return;
}
}
int mid=l+((r-l)>>1);
update(L,R,l,mid,idx<<1,k);
update(L,R,mid+1,r,idx<<1|1,k);
pushup(idx);
return;
}
查询
实现了建树与更新后,我们处理查询。我们需要实现query(L,R,l,r,idx),它负责查询目标区间
同样有三种情况,这里不再列出。对于第一种情况,我们返回一个对结果无影响的值,这里是 INT_MIN等)。第二种情况,我们直接返回当前节点的信息。最后一种情况,我们进行递归,将递归函数返回的值相加并返回。
long long query(int L,int R,int l,int r,int idx)
{
if(l>R||r<L) return 0;
if(l>=L&&r<=R) return tree[idx].data;
int mid=l+((r-l)>>1);
return query(L,R,l,mid,idx<<1)+query(L,R,mid+1,r,idx<<1|1);
}
懒惰标记
我们成功实现了一棵线段树。然而,它的效率并不如预期,没有通过 P3372。
我们发现在更新时,当前的代码会直接更新到叶子节点。然而,底部的一些节点在之后的查询中可能并没有被访问,这不就白更新了吗?这下,我们在结构体中声明的lazy就有用了。我们可以将修改操作的变化值存储在lazy中,只在必要时才将lazy下发给子节点并真正更新当前节点。由于这可以说是“懒得更新之后的节点”,它被称为懒惰标记。
我们首先实现pushdown(idx,l,r),它负责处理当前节点的懒惰标记,并将其下发到子节点。
void pushdown(int idx,int l,int r)
{
if(!tree[idx].lazy) return;
int mid=l+((r-l)>>1);
tree[idx<<1].lazy+=tree[idx].lazy,tree[idx<<1|1].lazy+=tree[idx].lazy;
tree[idx<<1].data+=tree[idx].lazy*(mid-l+1),tree[idx<<1|1].data+=tree[idx].lazy*(r-mid);
tree[idx].lazy=0;
return;
}
为什么还要传入当前区间呢?很简单,因为懒惰标记中存储的是区间中每个元素的增量,必须乘上区间长度才能得到区间增量。
接下来,我们在处理查询与更新操作时,如果需要向下递归,先pushdown()即可处理懒惰标记。然后,对于更新操作中的第二种情况,我们可以高效地解决而不必再继续递归。
void update(int L,int R,int l,int r,int idx,long long k)
{
if(l>R||r<L) return;
if(l>=L&&r<=R)
{
tree[idx].data+=k*(r-l+1),tree[idx].lazy+=k;
return;
}
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
update(L,R,l,mid,idx<<1,k);
update(L,R,mid+1,r,idx<<1|1,k);
pushup(idx);
return;
}
到此,我们成功写出了一棵线段树。
#include<bits/stdc++.h>
using namespace std;
int n,m;
long long a[100005];
struct node
{
long long data,lazy=0;
}tree[400060];
void pushup(int idx)
{
tree[idx].data=tree[idx<<1].data+tree[idx<<1|1].data;
return;
}
void pushdown(int idx,int l,int r)
{
if(!tree[idx].lazy) return;
int mid=l+((r-l)>>1);
tree[idx<<1].lazy+=tree[idx].lazy,tree[idx<<1|1].lazy+=tree[idx].lazy;
tree[idx<<1].data+=tree[idx].lazy*(mid-l+1),tree[idx<<1|1].data+=tree[idx].lazy*(r-mid);
tree[idx].lazy=0;
return;
}
void build(int l,int r,int idx)
{
if(l==r)
{
tree[idx].data=a[l];
return;
}
int mid=l+((r-l)>>1);
build(l,mid,idx<<1);
build(mid+1,r,idx<<1|1);
pushup(idx);
return;
}
long long query(int L,int R,int l,int r,int idx)
{
if(l>R||r<L) return 0;
if(l>=L&&r<=R) return tree[idx].data;
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
return query(L,R,l,mid,idx<<1)+query(L,R,mid+1,r,idx<<1|1);
}
void update(int L,int R,int l,int r,int idx,long long k)
{
if(l>R||r<L) return;
if(l>=L&&r<=R)
{
tree[idx].data+=k*(r-l+1),tree[idx].lazy+=k;
return;
}
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
update(L,R,l,mid,idx<<1,k);
update(L,R,mid+1,r,idx<<1|1,k);
pushup(idx);
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,n,1);
for(int i=1,cz,x,y;i<=m;i++)
{
cin>>cz>>x>>y;
if(cz==1)
{
long long k;
cin>>k;
update(x,y,1,n,1,k);
}
else cout<<query(x,y,1,n,1)<<endl;
}
return 0;
}
不同类型的区间更新
除了全都加上
区间乘法
如果我们的修改操作是将区间中的元素乘上
- 必须将
lazy初始化为1 。 - 在
update()、pushdown()时直接乘上k 就行了,区间乘法的更新对区间长度并没有需求。
代码就不放了,很简单。
直接修改
如果修改操作直接将区间中的元素修改为 bool变量来表示当前节点是否有待更新的懒惰标记。
struct node
{
long long data,lazy=0;
bool have=false;
}tree[400060];
相应的pushdown()函数如下。
void pushdown(int idx,int l,int r)
{
if(!tree[idx].have) return;
int mid=l+((r-l)>>1);
tree[idx<<1].lazy=tree[idx<<1|1].lazy=tree[idx].lazy;
tree[idx<<1].data=tree[idx].lazy*(mid-l+1),tree[idx<<1|1].data=tree[idx].lazy*(r-mid);
tree[idx<<1].have=tree[idx<<1|1].have=true;
tree[idx].have=false;
return;
}
update()函数同理,就不放出来了。
混合修改
在 P3373 中,我们需要同时处理区间加、乘法并对某个模数取模。我们可以给加法、乘法各开一个单独的懒惰标记。
struct node
{
long long data,add=0,mul=1;
}tree[400060];
不过这会导致一个问题——在下传懒惰标记时,是先处理加法还是乘法的懒惰标记呢?
不妨设
所以,新的
其中
也就是说,新的
void pushdown(int idx,int l,int r)
{
if(tree[idx].mul==1&&!tree[idx].add) return;
int mid=l+((r-l)>>1);
tree[idx<<1].data=(tree[idx<<1].data*tree[idx].mul%MOD+tree[idx].add*(mid-l+1)%MOD)%MOD;
tree[idx<<1].mul=tree[idx<<1].mul*tree[idx].mul%MOD;
tree[idx<<1].add=(tree[idx<<1].add*tree[idx].mul%MOD+tree[idx].add)%MOD;
tree[idx<<1|1].data=(tree[idx<<1|1].data*tree[idx].mul%MOD+tree[idx].add*(r-mid)%MOD)%MOD;
tree[idx<<1|1].mul=tree[idx<<1|1].mul*tree[idx].mul%MOD;
tree[idx<<1|1].add=(tree[idx<<1|1].add*tree[idx].mul%MOD+tree[idx].add)%MOD;
tree[idx].add=0,tree[idx].mul=1;
return;
}
当然,在pushdown()里,处理乘法、加法懒惰标记的代码顺序没有什么关系。
在区间乘法更新时,我们只需要给区间中的数据以及加法、乘法懒惰标记都乘上
#include<bits/stdc++.h>
using namespace std;
int n,q;
long long MOD,a[100005];
struct node
{
long long data,add=0,mul=1;
}tree[400060];
void pushup(int idx)
{
tree[idx].data=(tree[idx<<1].data+tree[idx<<1|1].data)%MOD;
return;
}
void pushdown(int idx,int l,int r)
{
if(tree[idx].mul==1&&!tree[idx].add) return;
int mid=l+((r-l)>>1);
tree[idx<<1].data=(tree[idx<<1].data*tree[idx].mul%MOD+tree[idx].add*(mid-l+1)%MOD)%MOD;
tree[idx<<1].mul=tree[idx<<1].mul*tree[idx].mul%MOD;
tree[idx<<1].add=(tree[idx<<1].add*tree[idx].mul%MOD+tree[idx].add)%MOD;
tree[idx<<1|1].data=(tree[idx<<1|1].data*tree[idx].mul%MOD+tree[idx].add*(r-mid)%MOD)%MOD;
tree[idx<<1|1].mul=tree[idx<<1|1].mul*tree[idx].mul%MOD;
tree[idx<<1|1].add=(tree[idx<<1|1].add*tree[idx].mul%MOD+tree[idx].add)%MOD;
tree[idx].add=0,tree[idx].mul=1;
return;
}
void build(int l,int r,int idx)
{
if(l==r)
{
tree[idx].data=a[l]%MOD;
return;
}
int mid=l+((r-l)>>1);
build(l,mid,idx<<1);
build(mid+1,r,idx<<1|1);
pushup(idx);
return;
}
long long query(int L,int R,int l,int r,int idx)
{
if(l>R||r<L) return 0;
if(l>=L&&r<=R) return tree[idx].data%MOD;
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
return (query(L,R,l,mid,idx<<1)+query(L,R,mid+1,r,idx<<1|1))%MOD;
}
void update(int L,int R,int l,int r,int idx,long long k,bool ismul)
{
if(l>R||r<L) return;
if(l>=L&&r<=R)
{
if(ismul)
{
tree[idx].data=tree[idx].data*k%MOD;
tree[idx].add=tree[idx].add*k%MOD;
tree[idx].mul=tree[idx].mul*k%MOD;
}
else
{
tree[idx].data=(tree[idx].data+k*(r-l+1))%MOD;
tree[idx].add=(tree[idx].add+k)%MOD;
}
return;
}
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
update(L,R,l,mid,idx<<1,k,ismul);
update(L,R,mid+1,r,idx<<1|1,k,ismul);
pushup(idx);
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>q>>MOD;
for(int i=1;i<=n;i++) cin>>a[i];
build(1,n,1);
for(int i=1,cz,x,y;i<=q;i++)
{
cin>>cz>>x>>y;
if(cz==1)
{
long long k;
cin>>k;
update(x,y,1,n,1,k,true);
}
else if(cz==2)
{
long long k;
cin>>k;
update(x,y,1,n,1,k,false);
}
else cout<<query(x,y,1,n,1)<<endl;
}
return 0;
}
动态开点线段树
我们知道,线段树进行一次更新操作的时间复杂度是
struct node
{
long long data,lazy=0;
int left=0,right=0;
}tree[400060];
由于需要动态开点,原本的build()已经不再适用。现在,对于每个新的输入,我们都进行一次单点更新。这样做的时间复杂度是
每次进行pushdown()操作时,我们都检查当前节点的子节点。如果没有,就创建它。其他的地方,动态开点线段树则几乎与普通线段树别无二致。
#include<bits/stdc++.h>
using namespace std;
int n,m,cnt;
long long a[100005];
struct node
{
long long data,lazy=0;
int left=0,right=0;
}tree[400060];
int newnode()
{
tree[++cnt]={0,0,0,0};
return cnt;
}
void pushup(int idx)
{
tree[idx].data=tree[tree[idx].left].data+tree[tree[idx].right].data;
return;
}
void pushdown(int idx,int l,int r)
{
if(!tree[idx].lazy) return;
if(!tree[idx].left) tree[idx].left=newnode();
if(!tree[idx].right) tree[idx].right=newnode();
int mid=l+((r-l)>>1);
tree[tree[idx].left].data+=tree[idx].lazy*(mid-l+1);
tree[tree[idx].left].lazy+=tree[idx].lazy;
tree[tree[idx].right].data+=tree[idx].lazy*(r-mid);
tree[tree[idx].right].lazy+=tree[idx].lazy;
tree[idx].lazy=0;
return;
}
long long query(int L,int R,int l,int r,int idx)
{
if(l>R||r<L) return 0;
if(l>=L&&r<=R) return tree[idx].data;
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
return query(L,R,l,mid,tree[idx].left)+query(L,R,mid+1,r,tree[idx].right);
}
void update(int L,int R,int l,int r,int idx,long long k)
{
if(l>R||r<L) return;
if(l>=L&&r<=R)
{
tree[idx].data+=k*(r-l+1),tree[idx].lazy+=k;
return;
}
pushdown(idx,l,r);
int mid=l+((r-l)>>1);
update(L,R,l,mid,tree[idx].left,k);
update(L,R,mid+1,r,tree[idx].right,k);
pushup(idx);
return;
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin>>n>>m;
newnode();
for(int i=1;i<=n;i++) cin>>a[i],update(i,i,1,n,1,a[i]);
for(int i=1,cz,x,y;i<=m;i++)
{
cin>>cz>>x>>y;
if(cz==1)
{
long long k;
cin>>k;
update(x,y,1,n,1,k);
}
else cout<<query(x,y,1,n,1)<<endl;
}
return 0;
}
标记永久化
在前面,我们采用pushdown()对懒惰标记进行下传。但对于懒惰标记的处理,实际上还有另一种方法——标记永久化。相比于下传的方法,标记永久化的常数会更小。如它的名字,懒惰标记一旦打上就再也不会消失,而是在查询与更新操作中直接计算。
更具体的,在标记永久化中,我们不再进行pushdown()操作。我们依旧有之前的三种情况。对于情况二,就表明当前区间内所有子节点都受到了修改,可以直接打上懒惰标记,修改data并返回。对于情况三,只有一部分节点需要修改,我们不能直接修改当前节点的懒惰标记,于是只能修改data,并继续向下递归处理子节点。
查询时,我们需要累加路径上所有节点的懒惰标记,才能计算出当前区间的真实值。
long long query(int L,int R,int l,int r,int idx,long long t)
{
if(l>R||r<L) return 0;
if(l>=L&&r<=R) return tree[idx].data+t*(r-l+1);
int mid=l+((r-l)>>1);
return query(L,R,l,mid,idx<<1,t+tree[idx].lazy)+query(L,R,mid+1,r,idx<<1|1,t+tree[idx].lazy);
}
void update(int L,int R,int l,int r,int idx,long long k)
{
if(l>R||r<L) return;
tree[idx].data+=k*(min(R,r)-max(L,l)+1);
if(l>=L&&r<=R)
{
tree[idx].lazy+=k;
return;
}
int mid=l+((r-l)>>1);
update(L,R,l,mid,idx<<1,k);
update(L,R,mid+1,r,idx<<1|1,k);
return;
}