首页 > 代码库 > 线段树

线段树

    线段树与BST不同,它维护的是区间信息,树高越低,区间范围越大,而最后一层就是单点信息。线段树的价值于其维护的区间信息,如果不能有效利用,那么线段树就是一颗废树。

 

一、单点更新

线段树按照结点更新方式的不同,分为单点更新和成段更新。单点更新是线段树最简单的结构。通常由Push_Up, Build,Update,Query四个操作构成。其中Push_Up代码最简单,但却是线段树的核心,它负责区间信息的向上更新,换言之,如果设计不 出有效的Push_Up,那么线段树就是颗废树。

 
#include "iostream"#include "string"#include "vector"#include "cstring"#include "fstream"#include "cstdio"using namespace std;#define M 50001#define lson l,mid,root<<1#define rson mid+1,r,root<<1|1int sum[M<<2];void PushUp(int root){    sum[root]=sum[root<<1]+sum[root<<1|1];}void build(int l,int r,int root){    if(l==r)    {        scanf("%d",&sum[root]);        return;    }    int mid=(l+r)>>1;    build(lson);    build(rson);    PushUp(root);}void update(int p,int value,int l,int r,int root){    if(l==r)    {        sum[root]=value;        return;    }    int mid=(l+r)>>1;    if(p<=mid) update(p,value,lson);    else update(p,value,rson);    PushUp(root);}int qurry(int L,int R,int l,int r,int root){    if(L<=l&&r<=R) return sum[root];    int mid=(l+r)>>1;    int ret=0;    if(L<=mid) ret+=qurry(L,R,lson);    if(R>mid) ret+=qurry(L,R,rson);    return ret;}
单点更新的基本操作

上面的线段树风格来自Hdu的神犇NotOnlySuccess,他的那篇线段树总结写的很好,不过个人主页经常挂。orz。

线段树是大量数据时代,首先得抛弃使用cin的坏习惯。Windows下,不关sync的cin时间是scanf的10倍,关了也有2~3倍,100%会超时。Linux下关了sync的cin倒是比scanf快。

根据Push_Up操作,大致可以分为以下几类。

 
1. 区间求和
 sum[root]=sum[root<<1]+sum[root<<1|1]
 

@练习题

HDU 1166, 线段树求和模板题,更新之后查询即可。

XCOJ 1019, 阉割掉Update的线段树,其实用树状数组做更好。坑的地方是有重复点,要叠加。

 

2. 区间RMQ(范围最值查询)

RMQ[root]=max(RMQ[root<<1],RMQ[root<<1|1]);

 

@练习题

HDU 1754,线段树RMQ模板题,Query操作记得改下。

HDU 2795, 很有趣的一个题,利用的是RMQ的性质,去放东西。如果大小<=左子树区间最大值,那么就往左走。左子树放不了,再考虑右子树。亮点是Update和Query一体化,这也是非裸线段树的一大难点,一般我们认为这题是线段树首先看有没有明显Update、Query,但是这题藏得很隐蔽。
 
3. 区间覆盖(HASH)
 
特殊类问题,不需要Push_Up,但是依赖于离散数据结构。

@练习题
 
POJ 2828,排队插队问题。维护一个区间剩余容量。按顺序给出的是入队后的位置(可能原始位置,可能插过队)。我一开是顺序考虑,如果那个位置有人就把原来的人后移,但是万一后面也有人呢?得整体后移,但是如果有空位的又不能整体后移,得把空位填掉,实在太麻烦。后来看题解,才明白应该逆序处理,逆序之后update,只要不重叠,那就是最终位置。如果重叠,那就利用pos-左右区间剩余容量确定新的空位置,因为逆序,只要这个位置是空的,就是最终位置,不用考虑邻近点偏移了。维护(update)区间容量的时候,每次到一个区间都要-1(100%会插在这个区间)。由于逆序,要记录每次插的位置,最后顺序输出。
 
 

二、成段更新

成段更新的Update操作发生了变化,对指定L..R区间进行更新。由于点更新量可能是单点更新的几十倍,若对每个点采取自下而上的Push_Up,那么在Update超庞大情况下势必会完蛋。所以成段更新使用了Lazy思想,即暂时不更新,做个可以叠加更新的标记,到不得不更新的情况下再更新,不过这次是往下更新(上面已经被更新过了),这个操作称为Push_Down。 Lazy标记的使用使得线段树的结构异常复杂,尤其在有多个操作时(如加减乘除运算混合),因为其涉及到操作的合并问题,如何设计Lazy标记成为难点。
 
void build(int l,int r,int root){    col[root]=-1; //多组数据,标记清除}void PushDown(int root,int m){    if(col[root]!=-1)    {    col[root<<1]=col[root<<1|1]=col[root];    sum[root<<1]=col[root]*(m-(m>>1));    sum[root<<1|1]=col[root]*(m>>1);    col[root]=-1;    }}void update(int L,int R,int c,int l,int r,int root){    if(L<=l&&r<=R)    {        col[root]=c;        sum[root]=c*(r-l+1);        return;    }    PushDown(root,r-l+1);    int mid=(l+r)>>1;    if(L<=mid) update(L,R,c,lson);    if(R>mid) update(L,R,c,rson);    PushUp(root);}int query(int L,int R,int l,int r,int root){    if(L<=l&&r<=R) return sum[root];    PushDown(root,r-l+1); //这里的PushDown很重要    //下同单点更新}
成段更新的基本操作
 
1. 区间求和

@练习题
 
HDU 1698, 求和裸题,注意Push_Down和Update修改sum[root]的时候得算上整个区间。

POJ 3468,Lazy标记从覆盖变成了叠加,注意PushDown左右子标记的时候+=col[root],而不是覆盖,对待sum也一样。

XCOJ 1025,安徽OI 09年省选题,加和乘两种优先级不同的运算符给这题的PushDown带来了巨大麻烦。全程的mod让人蛋疼。首先明确一点,乘法标记mul优先级大于加法标记add,所以必然是sum[root]=sum[root]*mul[root]+add[root]*区间,更新add左右标记的时候,要先乘以mul标记再加,更新mul标记则直接乘。mul标记重置是1,add标记重置是0
 
2. 区间覆盖(HASH)
 
@练习题
 
POJ2528, 首先得清楚常规线段树的叶子结点容量大概是10^6,这题达到了10^7,所以我们要将离散的区间“压缩”。

通俗点说,离散化就是压缩区间,使原有的长区间映射到新的短区间,但是区间压缩前后的覆盖关系不变。举个例子:

有一条1到10的数轴(长度为9),给定4个区间[2,4] [3,6] [8,10] [6,9],覆盖关系就是后者覆盖前者,每个区间染色依次为 1 2 3 4。

现在我们抽取这4个区间的8个端点,2 4 3 6 8 10 6 9

然后删除相同的端点,这里相同的端点为6,则剩下2 4 3 6 8 10 9

对其升序排序,得2 3 4 6 8 9 10

然后建立映射

2     3     4     6     8     9   10

↓     ↓      ↓     ↓     ↓     ↓     ↓

1     2     3     4     5     6     7

那么新的4个区间为 [1,3] [2,4] [5,7] [4,6],覆盖关系没有被改变。新数轴为1到7,即原数轴的长度从9压缩到6,显然构造[1,7]的线段树比构造[1,10]的线段树更省空间,搜索也更快,但是求解的结果却是一致的。

离散化时有一点必须要注意的,就是必须先剔除相同端点后再排序,这样可以减少参与排序元素的个数,节省时间。


压缩完区间后,按照每张海报的id对其区域编号修改(update)。但是在Query的时候,作出了很大修改。

void query(int *col,bool *Hash,int l,int r,int root){    if (col[root])    {        if (!Hash[col[root]]) cnt++;        Hash[col[root]] = true;        return;    }    PushDown(col,root,r-l+1);    if (l==r) return ;    int mid = (l + r) >> 1;    query(col,Hash,lson);    query(col,Hash,rson);}
Query的操作

注意这里利用线段树中保留的Lazy标记来HASH,首先你得明白,叶子结点必然保留着标记,但是如果只是对叶子结点HASH,那么线段树就没有存在意义了。幸运的是,叶子结点上层存在着大量未被消除的线段Lazy标记,只要把这些标记用起来,我们就没必要下到每个叶子结点进行HASH了。这就是线段树的神奇之处。

线段树