整理:线段树
关于线段树的整理
1.什么是线段树
\(OI\ Wiki\)
线段树是算法竞赛中常用的用来维护 区间信息 的数据结构。
线段树可以在 \(O(nlogn)\) 的时间复杂度内实现单点修改,区间修改,区间查询(区间求和,求区间最大值,求区间最小值)等操作。
2.线段树与其他类似数据结构的对比
| 时间复杂度 | 空间复杂度 | 适用范围 | 码量 | |
|---|---|---|---|---|
| \(分块\) | \(O(n\sqrt n)\) | \(O(n\sqrt n)\) | 巨大 | 大 |
| \(树状数组\) | \(O(nlogn)(常数小)\) | \(O(n)\) | 小 | 小 |
| \(线段树\) | \(O(nlongn)(常数大)\) | \(O(n)\) | 大 | 巨大 |
3.线段树的基本实现
我们首先来考虑线段树的基本操作,合并两个小区间的信息得到更大的区间信息。
void pushup(int p){c[p]=cal(c[ls],c[rs]);}
其中 \(cal\) 表示合并操作。
下面是建树过程。
void build(int p,int l,int r){
if(l==r){
c[p]=val;//val 表示初始值
return ;
}
int mid=l+r>>1;
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
}
为什么线段树要这样建树呢?
我们将整段需要维护的区间尽可能平均的分成两份,可以简单看出,线段树应当有 \(\log n\) 层,这使得我们下面的修改和查询都是 \(\log n\) 的时间复杂度。
而这样,线段树将是一棵满二叉树,当区间大小为 \(2\) 的正整数幂时,线段树是一颗完全二叉树,那么我们就可以使用数组记录线段树,对于节点 \(p\),左儿子为 \(2\times p\),右儿子为 \(2\times p +1\)。
对于修改操作,我们可以分为两种,一种为单点修改,另一种为区间修改。
我们先来考虑单点修改,以区间加为例,假设修改的位置为 \(x\)。
void change(int p,int l,int r,int x,int v){
if(l==r){//到达叶子节点,此时叶子节点管理的区间一定为 x 这单个点
c[p]+=v;
return ;
}
int mid=l+r>>1;
if(mid>=x)change(ls,l,mid,x,v);//mid>=x 表示需要修改的点在左儿子的区间内
else change(rs,mid+1,r,x,v);//在右儿子的区间内
pushup(p);//此时它的儿子的信息改变,需要重新合并获得正确的信息
}
那么区间修改呢?
这里就引入了一个重要概念,懒惰标记。
何为懒惰标记?对于修改,我们可以进行一定的延后,这时对于本需要对儿子节点修改但是还没有修改的节点打上标记,记录修改。
具体什么意思呢?
我们假设修改区间为 \([L,R]\),此时访问的节点管辖区间为 \([l,r]\),那么当 \(L\le l\ \land\ r\le R\) 时,修改区间完全包含了 \([l,r]\) 区间,我们可以直接修改当前节点的值,然后打上标记,最后结束递归。
为什么这样做是对的呢?
我们可以先看一下线段树的区间查询操作的一部分。
Typename query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return c[p];
//do something......
}
我们发现,当询问区间完全包含当前区间时,我们直接返回了当前节点的值,而不访问其子节点。
我们回到上面的打标记问题。
我们直接修改了当前节点的值,然后打上标记,将修改操作延后,当查询时,如果询问区间完全包含当前区间,我们可以通过返回当前节点的值来得到正确的区间信息。
而这衍生出了另一个问题,也就是标记下传。
我们发现,并不是每一次,询问区间都可以完全包含当前区间,这时需要访问子节点,但是由于懒惰标记的存在,子节点并没有被更新,此时我们需要将延后的操作落实,更新子节点的值,并且下传标记,对子节点打上标记,表示将子节点的子节点的操作延后。
注意,无论是修改还是查询,只要访问了子节点,就应当将标记下传。
下传操作和上面的上传操作一样,都是线段树的基本操作。
void pushdown(int p){
Tag(ls,tag[p]);
Tag(rs,tag[p]);
tag[p]=0;
}
其中 \(Tag\) 操作表示打标记和修改的过程,注意清空当前节点的标记。
那么区间修改和区间查询的代码可以写出,这里区间修改以区间加为例。
void change(int p,int l,int r,int L,int R,int v){
if(L<=l&&r<=R){
c[p]+=v;
tag[p]+=v;//懒惰标记
return ;
}
pushdown(p);//下传标记
int mid=l+r>>1;
if(mid>=L)change(ls,l,mid,L,R,v);
if(mid<R)change(rs,mid+1,r,L,R,v);
pushup(p);//注意重新上传区间
}
Typename query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return c[p];
pushdown(p);//下传标记
if(mid>=L&&mid<R)return cal(query(ls,l,mid,L,R),query(rs,mid+1,r,L,R));
if(mid>=L)return query(ls,l,mid,L,R);
return query(rs,mid+1,r,L,R);
}
但是懒惰标记难道是万能的吗?
显然不是,懒惰标记维护的操作要满足操作可加性,也就说,标记的先后顺序与最后导出的结果无关,比如加,乘,或者覆盖。
但是,如果不满足可加性的操作就不能用懒惰标记了吗?
当然也可以,我们把修改函数略微更改。
void change(int p,int l,int r,int L,int R,int v){
if(L<=l&&r<=R){
pushdown(p);
Tag(p);
return ;
}
pushdown(p);//下传标记
int mid=l+r>>1;
if(mid>=L)change(ls,l,mid,L,R,v);
if(mid<R)change(rs,mid+1,r,L,R,v);
pushup(p);//注意重新上传区间
}
我们在下一次修改来时直接将上一次的标记下传就解决了顺序问题。
这样,我们就得到了线段树的基本代码,这里以维护区间和,支持区间加,单点加的线段树为例。
struct Segment_tree{
int c[N<<2],tag[N<<2];
#define ls p<<1
#define rs p<<1|1
#define mid (l+r>>1)
void pushup(int p){c[p]=c[ls]+c[rs];}
void Tag(int p,int v){
c[p]+=v;
tag[p]+=v;
}
void pushdown(int p){
if(!tag[p])return ;
Tag(ls,tag[p]);
Tag(rs,tag[p]);
tag[p]=0;
}
void build(int p,int l,int r){
if(l==r)return void(c[p]=0);
build(ls,l,mid),build(rs,mid+1,r);
pushup(p);
}
void change(int p,int l,int r,int x,int v){
if(l==r)return void(c[p]+=v);
pushdown(p);
if(mid>=x)change(ls,l,mid,x,v);
else change(rs,mid+1,r,x,v);
pushup(p);
}
void change(int p,int l,int r,int L,int R,int v){
if(L<=l&&r<=R)return Tag(p,v);
pushdown(p);
if(mid>=L)change(ls,l,mid,L,R,v);
if(mid<R)change(rs,mid+1,r,L,R,v);
pushup(p);
}
int query(int p,int l,int r,int L,int R){
if(L<=l&&r<=R)return c[p];
pushdown(p);
if(mid>=L&&mid<R)return query(ls,l,mid,L,R)+query(rs,mid+1,r,L,R);
if(mid>=L)return query(ls,l,mid,L,R);
return query(rs,mid+1,L,R);
}
}
我们可以发现,码量十分之大。
相比之下,树状数组就十分简短,所以可以写树状数组的场合尽量写树状数组吧。
4.线段树的时空复杂度分析
在上面的代码中我们可以发现,储存线段树的数组我们开了四倍空间,为什么呢?
因为在最优情况下,线段树是一颗满二叉树,此时只需要两倍空间,但是在其他情况下,完全二叉树就需要四倍空间储存了。
单次操作时间复杂度显然为 \(O(\log n)\),但是带一个 \(4\) 的巨大常数。
为什么呢?
以查询操作为例,我们发现最劣情况为,每一次操作区间都需要分成两份,右儿子被完全包含,左儿子没有被完全包含,这样,我们需要访问 \(O(4\times\log n)\) 个节点才能获得完整信息。

浙公网安备 33010602011771号