线段树浅析
线段树是一种二叉搜索树,与树状数组相似,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。 然后我们可以以log级别的时间复杂度进行区间修改与查询。相对于树状数组,线段树区间修改的难度远低于树状数组加差分的区间修改。所以我们有必要来研究一下这个算法。
一、这棵树的思想
我们可以将一个数组建立起一棵按照编码排列的完全二叉树,那么我们很容易可以想到,一个非叶节点n的左儿子编码为 2 × n 2\times n 2×n,右儿子的编码为$ 2 \times n+1$,这样的话我们很容易就可以做到递归查询整一棵树。
那么我们让数组中的根节点存储[1…n]的值。令mid=(l+r)/2
,那么我们可以知道,任何一个存储了[l…r]的非叶节点的左儿子存储了[1…mid],右儿子存储了[mid+1…r]的值。这样我们在进行操作时可以一边递归一边修改,压缩了时间复杂度。
那么我们怎么进行修改操作呢?
先来看单点修改:我们可以从根节点出发,判断这个点是位于这个节点的左儿子还是右儿子,然后往指定方向进行递归处理,当查询到了这个点的时候,对这个节点进行修改操作。在递归回溯到根节点的过程中,对沿途包含这个被修改节点的值也进行修改。
当然查询操作也是一样,不过不需要修改罢了。
二、建树
在建立这一棵线段树时,我们其实可以认为我们对这一棵树的每一个叶节点进行了单点修改操作,然后不断回传、回传,维护这一棵树。这里我们采用区间最大值的例子,下面就是可爱的代码了QAQ。
void bulid(int p,int l,int r){
t[p].l=l;t[p].r=r;//以p为编号的节点维护的区间为l到r
if(l==r){//l=r的话,这个区间就只有一个数,直接让区间维护的值等于a[i]
t[p].pre=a[l];
return;
}//否则维护的值等于左儿子加右儿子
int mid=l+r>>1;
bulid(p*2,l,mid);
bulid(p*2+1,mid+1,r);
t[p].pre=max(t[p*2].pre,t[p*2+1].pre);
}
这其实就是一个单点修改的集合……吧。
三、Lazy Tag的应用
这个东西呢,中文名称“懒标记”,我们通常把它应用于线段树的区间修改。
如果我们对一段区间的修改还是参照上方单点修改的模式,就是说当一个节点所概括的区间与修改区间有交集时,我们就对这个节点,以及以这个节点为根节点的子树全部进行修改。这样我们的时间复杂度就会上升,所以我们就可以开始进行一些优化。于是就有大神捣鼓出了这个鬼东西。
Lazy Tag的主要思想就是当一个节点所代表的一整段区间都被修改时,我们就为这个节点打上一个懒标记,表示着这个节点的子树均未修改。
当我们进行查询操作时遍历到达这个节点的时候,我们可以修改这个节点并将这个懒标记消除,然后为它的两个子节点打上懒标记,直到到达叶节点为止。
这样的话我们可以避免很多很多重复的遍历操作,将许多东西放在一起处理,大大节省了时间复杂度。
四、算法分析
线段树算法,作为一个树形结构,它需要一个更大的空间,也就是所谓的以空间换时间,一般我们将树数组开到元素个数的四倍。然后这个算法在竞赛里经常会被变态的出题人卡常数,这个时候我们就不得不用带有差分的树状数组来优化查询了。
五、友好的代码
下面贴上高清无码的代码呦~
我们就以某谷P3372为例:(又是P的逃)
var
n,m,p,x,y,k:int64;
i:longint;
a,tree,tag:array[0..2000001]of int64;//tree是线段树数组,tag是懒标记数组
function ls(x:int64):int64;//查询左儿子
begin exit(x<<1); end;
function rs(x:int64):int64;//查询右儿子
begin exit((x<<1) or 1); end;
procedure push_up(p:int64);//向上汇总操作
begin tree[p]:=tree[ls(p)]+tree[rs(p)]; end;
procedure build(p,l,r:int64);//建树
var mid:int64;
begin
if l>r then exit;
tag[p]:=0;
if l=r then
begin
tree[p]:=a[l];
exit;
end;
mid:=(l+r) div 2;
build(ls(p),l,mid);
build(rs(p),mid+1,r);
push_up(p);
end;
procedure f(p,l,r,k:int64);//懒标记的下放(下一层的)
begin
inc(tag[p],k);
inc(tree[p],k*(r-l+1));
end;
procedure push_down(p,l,r:int64);//懒标记的下放(原层)
var mid:int64;
begin
mid:=(l+r)>>1;
f(ls(p),l,mid,tag[p]);
f(rs(p),mid+1,r,tag[p]);
tag[p]:=0;
end;
procedure update(nl,nr,l,r,p,k:int64);//区间修改
var mid:int64;
begin
if (nl<=l)and(nr>=r) then
begin
inc(tree[p],k*(r-l+1));
inc(tag[p],k);
exit;
end;
push_down(p,l,r);
mid:=(l+r)>>1;
if (nl<=mid) then
update(nl,nr,l,mid,ls(p),k);
if (nr>mid) then
update(nl,nr,mid+1,r,rs(p),k);
push_up(p);
end;
function query(q_x,q_y,l,r,p:int64):int64;//区间查询
var res,mid:int64;
begin
res:=0;
if (q_x<=l)and(q_y>=r) then
exit(tree[p]);
mid:=(l+r) >>1;
push_down(p,l,r);
if (q_x<=mid) then
inc(res,query(q_x,q_y,l,mid,ls(p)));
if (q_y>mid) then
inc(res,query(q_x,q_y,mid+1,r,rs(p)));
exit(res);
end;
begin
readln(n,m);
for i:=1 to n do
read(a[i]);
build(1,1,n);
for i:=1 to m do
begin
read(p);
if p=1 then
begin
readln(x,y,k);
update(x,y,1,n,1,k);
end else
begin
readln(x,y);
writeln(query(x,y,1,n,1));
end;
end;
end.
P.S.:这玩意儿实在是太长了,调代码能调好久的呢……