线段树学习笔记
0.什么是线段树
线段树有着比分块小的时间复杂度,也有着比树状数组大的通用性,有着比平衡树更小的常数和代码难度,实在是一种非常牛逼的东西。
1.线段树入门
我们要了解线段树,首先得明白它长什么样。
此处放一张图:

如图,一个节点代表一个区间,从中点切开这个区间,就成为了左右区间(不一定严谨,听着吧)。
由于每切一次,区间都减少一半,因此树高为\(\log n\)级别。
叶节点的值直接由序列得出,其它节点有左右节点合并得出。
为方便,将\(p\)的左右节点分别设为\(p\times2\)和\(p\times2+1\)(可以证明这样节点不会重复,只是牺牲了一点空间)。
词穷了,直接开始上代码吧。
//以下为加减,求和操作
void pushup(int p){
val[p]=val[p<<1]+val[p<<1|1];
}
void build(int p,int l,int r){//建树
if(l==r){
val[p]=v[l];//v[l]为序列第l项
}
int mid=(l+r>>1);
build(p<<1,l,mid);build(p<<1|1,mid+1,r);
pushup(p);//将左右节点的信息合并
}
时间复杂度:\(O(n)\)
学会了建树,接下来就应该是操作了。
操作1:单点查询
简单到不知道如何解释了,直接上代码。
int query(int p,int l,int r,int pos){//pos为查询位置
if(l==r)return val[p];
int mid=(l+r>>1);
if(pos<=mid)return query(p<<1,l,mid,pos);
else return query(p<<1|1,mid+1,r,pos);
}
时间复杂度:\(O(\log n)\)
操作2:单点修改
自上而下到叶节点进行修改,再自下而上更新
void modify(int p,int l,int r,int pos,int x){
if(l==r){
val[p]+=x;
return;
}
int mid=(l+r>>1);
if(pos<=mid)modify(p<<1,l,mid,pos);
else modify(p<<1,mid+1,r,pos);
}
时间复杂度:同样 \(O(\log n)\)
以上操作暴力都可以做到,现在就要介绍暴力做不到的:区间操作。
首先上简单点的:区间查询。
我们想一想,维护非叶节点有什么用?进行区间操作。
先上代码:
void query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return val[p];//当前区间被完全包含,直接返回
int mid=(l+r>>1),ans=0;
if(L<=mid)ans+=query(p<<1,l,mid,L,R);//左儿子与询问区间有交集
if(R>mid)ans+=query(p<<1|1,mid+1,r,L,R);//右儿子与询问区间有交集
}
易证,信息肯定是不重不漏的。
下面进行时间复杂度证明:
只有包含端点的区间才能一直递归下去。
未包含端点有两种情况:完全在询问区间内,完全在询问区间外,这些都是直接返回。
而端点只有两个,树高为\(\log n\)级别,因此查询复杂度也为\(\log n\)
接下来是区间修改。
询问可以不到叶子,因为只要获取信息。但修改不行,因为修改要将信息更新。
好像思路进入了僵局。
但线段树有一个强大的功能:懒标记。
有一个标记,代表此节点已被更新,但子节点未被更新。
如果区间被修改区间完全包含,打一个标记,并更新它的信息。
如果以后需要对此区间的儿子进行操作,就将标记下传,更新儿子节点的信息,将儿子节点打上标记,并将此节点的标记去除。
标记下传复杂度理论为\(O(1)\),因此只是常数大了那么一点(不是亿点)。
void pushdown(int p,int l,int r){
if(tag[p]){
int mid=(l+r>>1);
val[p<<1]+=(mid-l+1)*tag[p];
val[p<<1|1]+=(r-mid)*tag[p];
tag[p<<1]+=tag[p];tag[p<<1|1]+=tag[p];
tag[p]=0;
}
}
void modify(int p,int l,int r,int L,int R,int x){
if(L<=l&&r<=R){
val[p]+=(r-l+1)*x;tag[p]+=x;return;
}
pushdown(p,l,r);int mid=(l+r>>1);
if(L<=mid)modify(lc,l,mid,L,R,x);
if(R>mid)modify(rc,mid+1,r,L,R,x);
}
P3372代码如下:
#include<bits/stdc++.h>
using namespace std;
int n,m,a[100001];
struct tree{
int l,r;
long long sum,add;
}tree[400001];
void label(int p){
if(tree[p].add){
tree[p*2].sum+=(tree[p*2].r-tree[p*2].l+1)*tree[p].add;
tree[p*2+1].sum+=(tree[p*2+1].r-tree[p*2+1].l+1)*tree[p].add;
tree[p*2].add+=tree[p].add;
tree[p*2+1].add+=tree[p].add;
tree[p].add=0;
}
}
void bulid(int l,int r,int p){
tree[p].l=l,tree[p].r=r;
if(l==r){
tree[p].sum=a[l];
return;
}
int mid=(l+r)/2;
bulid(l,mid,p*2);
bulid(mid+1,r,p*2+1);
tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
}
void change(int l,int r,int p,int num){
if(tree[p].l>=l&&tree[p].r<=r){
tree[p].sum+=1ll*num*(tree[p].r-tree[p].l+1);
tree[p].add+=num;
return;
}
label(p);
int mid=(tree[p].l+tree[p].r)/2;
if(l<=mid)change(l,r,p*2,num);
if(r>mid)change(l,r,p*2+1,num);
tree[p].sum=tree[p*2].sum+tree[p*2+1].sum;
}
long long ask(int l,int r,int p){
long long ans=0;
if(tree[p].l>=l&&tree[p].r<=r){
return tree[p].sum;
}
label(p);
int mid=(tree[p].l+tree[p].r)/2;
if(l<=mid)ans+=ask(l,r,p*2);
if(r>mid)ans+=ask(l,r,p*2+1);
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",a+i);
}
bulid(1,n,1);
while(m--){
int op,l,r;
scanf("%d%d%d",&op,&l,&r);
if(op==1){
int x;
scanf("%d",&x);
change(l,r,1,x);
}
else printf("%lld\n",ask(l,r,1));
}
return 0;
}
习题:P5057 P4588 P1908 P1637
其它操作例如区间最值,其实是大同小异的。
请注意:前方进入可持久化,请做好准备!
可持久化线段树,简单来说就是修改时将涉及到的点不直接修改,而是开一个新点。
这导致我们只能维护左右子节点的编号,而不是直接偷懒。
由于可持久化线段树的特殊性,我们不能再使用传统下传标记的方式,这导致只能使用标记永久化,或是不支持区间修改。反正这种题又不多()
代码与普通线段树大同小异。
习题:P3919 P3834

浙公网安备 33010602011771号