Part I.Introduction
线段树是一棵二叉树,其每个节点表示一个区间[a,b]。
线段树的作用是进行区间加、修改、询问等操作。
若一个节点不是叶子节点,则其左儿子表示区间[a,mid],其右儿子表示区间[mid+1,r]。
由于这是一棵二叉树,我们可以采用一个数组记录一棵线段树。设某个节点数组下标为i,则其左儿子数组下标为i*2,右儿子数组下标为i*2+1。
我们可以发现,线段树除去最后一层可以看做是一棵满二叉树。由二叉树的性质可得,我们需开的空间为4*n。
统一起见,本文中讨论的操作为区间加和区间询问最大值。
我们可以使用以下的定义:
#define LeftSon(x) ((x)*2)
#define RightSon(x) ((x)*2+1)
struct Segment_Tree{
int Max,add;
}tree[maxn*4];
如图即为一棵[1,3]的线段树。
Part II.Build
我们可以用递归的方法建立一棵线段树。
void build(int k,int lc,int rc){
if (lc==rc){
tree[k].Max=seq[lc];
return;
}
int mid=(lc+rc)/2;
build(LeftSon(k),lc,mid);
build(RightSon(k),mid+1,rc);
update(k);
}
Part III.Lazy
Lazy思想是一种应用广泛的思想。
在线段树中,如果我们要对一个节点所代表的区间进行修改,我们只需修改这个节点本身,不必再修改其左右子树,而是待下次操作时将标记下传至左右儿子即可。
统一起见,我们定义的Lazy标记的含义为该节点的信息已经修改的情况、但还未下传的Lazy标记。
Update操作的作用是用一个节点的2个儿子的信息来更新其信息。
Update操作可以写为:
void update(int k){
tree[k].Max=max(tree[LeftSon(k)].Max,tree[RightSon(k)].Max);
}
Part V.Download
Download操作的作用是把一个节点的Lazy标记下传至其左右儿子。
需要注意的是,下传标记时我们需要更改左右儿子的答案标记和左右儿子的Lazy标记。
Download操作可以写为:
void download(int k){
if (tree[k].add){
tree[LeftSon(k)].add+=tree[k].add;
tree[RightSon(k)].add+=tree[k].add;
tree[LeftSon(k)].Max+=tree[k].add;
tree[RightSon(k)].Max+=tree[k].add;
tree[k].add=0;
}
}
Part VI.Add
我们可以发现线段树的一个性质:一个区间的信息可以由若干个子区间的信息合并得到。(*)
由此,Add操作可以不断将一个区间分解为左右2个子区间,直到一个区间恰好为一个节点所代表的区间。然后我们将这些节点的lazy标记加上需要加的值就可以了。
Add操作可以写为:
void add(int k,int lc,int rc,int l,int r,int d){
if (lc==l && rc==r){
tree[k].Max+=d;
tree[k].add+=d;
return;
}
download(k);
int mid=(lc+rc)/2;
if (r<=mid) add(LeftSon(k),lc,mid,l,r,d);
else if (l>mid) add(RightSon(k),mid+1,rc,l,r,d);
else{
add(LeftSon(k),lc,mid,l,mid,d);
add(RightSon(k),mid+1,rc,mid+1,r,d);
}
update(k);
}
Part VII.Ask
由Part VI中的(*)性质,对于一段区间的询问可以由几个子区间的询问合并得到。
由于是运用了同一个性质,我们可以发现Ask操作和Add操作的代码很相似。
Ask操作的代码可以写为:
int ask(int k,int lc,int rc,int l,int r){
if (lc==l && rc==r) return tree[k].Max;
download(k);
int mid=(lc+rc)/2;
if (r<=mid) return ask(LeftSon(k),lc,mid,l,r);
if (l>mid) return ask(RightSon(k),mid+1,rc,l,r);
return max(ask(LeftSon(k),lc,mid,l,mid),ask(RightSon(k),mid+1,rc,mid+1,r));
}
Part VIII.Analysis
在上面我们已经知道了线段树除去最下面一层是一棵满二叉树。
而各种操作的最坏时间复杂度是O(h),由二叉树的性质,也就是O(log n)。
Part IX.Exercise
至此,线段树的基本操作就介绍完了。
下面推荐2题BZOJ上的模板题:BZOJ 1012 & BZOJ 1798
以上2题的题解均在本Blog中。
Part X.Code
/*
* Algorithm:Segment_Tree
* Author:PYC
*/
#include <cstdio>
#define maxn 1000000
using namespace std;
int n,q,a[maxn],tree[maxn*4],lazy[maxn*4];
inline int max(int a,int b){if (a>b) return a;return b;}
void build(int k,int l,int r){
if (l==r){tree[k]=a[l];return;}
int mid=(l+r)/2;
build(k*2,l,mid);
build(k*2+1,mid+1,r);
tree[k]=max(tree[k*2],tree[k*2+1]);
}
void down(int x){
lazy[x*2]+=lazy[x];
lazy[x*2+1]+=lazy[x];
tree[x*2]+=lazy[x];
tree[x*2+1]+=lazy[x];
lazy[x]=0;
}
void up(int x){tree[x]=max(tree[x*2],tree[x*2+1]);}
int ask(int k,int lc,int rc,int l,int r){
if (lc==l && rc==r) return tree[k];
int mid=(lc+rc)/2;
down(k);
if (r<=mid) return ask(k*2,lc,mid,l,r);
if (l>mid) return ask(k*2+1,mid+1,rc,l,r);
return max(ask(k*2,lc,mid,l,mid),ask(k*2+1,mid+1,rc,mid+1,r));
}
void add(int k,int lc,int rc,int l,int r,int d){
if (lc==l && rc==r){lazy[k]+=d;tree[k]+=d;return;}
int mid=(lc+rc)/2;
down(k);
if (r<=mid){add(k*2,lc,mid,l,r,d);up(k);return;}
if (l>mid){add(k*2+1,mid+1,rc,l,r,d);up(k);return;}
add(k*2,lc,mid,l,mid,d);
add(k*2+1,mid+1,rc,mid+1,r,d);
up(k);
}
int main(){
scanf("%d%d",&n,&q);
for (int i=1;i<=n;++i) scanf("%d",&a[i]);
build(1,1,n);
for (int i=1;i<=q;++i){
int x;
scanf("%d",&x);
if (x==1){
int l,r;
scanf("%d%d",&l,&r);
printf("%d\n",ask(1,1,n,l,r));
}
else{
int l,r,d;
scanf("%d%d%d",&l,&r,&d);
add(1,1,n,l,r,d);
}
}
return 0;
}
Part XI.Thank You!
Thank you for reading!
By Charlie Pan
Feb 19,2014