目录
前言
这是一篇蒟蒻的博客,可能有许多错误或不详细的地方,欢迎大佬们指出。
这篇文章主要参考了这篇博文:http://blog.csdn.net/zearot/article/details/48299459
什么是线段树
线段树,是一种 二叉搜索树 。它将一段区间划分为若干 单位区间 ,每一个节点都储存着一个区间。它 功能强大 ,支持区间求和,区间最大值,区间修改,单点修改等操作。
线段树的思想和分治思想很相像。
线段树的每一个节点都储存着一段区间
[
L
.
.
R
]
[L..R]
[L..R] 的信息,其中 叶子节点
L
=
R
L=R
L=R 。它的大致思想是:将一段大区间平均地划分成
2
2
2 个小区间,每一个小区间都再平均分成
2
2
2 个更小区间……以此类推,直到每一个区间的
L
L
L 等于
R
R
R (这样这个区间仅包含一个节点的信息,无法被划分)。通过对这些区间进行修改、查询,来实现对大区间的修改、查询。
这样一来,每一次单点修改、单点查询的时间复杂度都只为
O
(
log
2
n
)
O(\log_2n)
O(log2n) 。
但是,可以用线段树维护的问题必须满足 区间加法 ,否则是不可能将大问题划分成子问题来解决的。
什么是区间加法
一个问题满足区间加法,仅当对于区间
[
L
,
R
]
[L,R]
[L,R] 的问题的答案可以由
[
L
,
M
]
[L,M]
[L,M] 和
[
M
+
1
,
R
]
[M+1,R]
[M+1,R] 的答案合并得到。
经典的区间加法问题有:
- 区间求和( ∑ i = L R a i = ∑ i = L M a i + ∑ i = M + 1 R a i , L ≤ M < R \sum_{i=L}^Ra_i=\sum_{i=L}^Ma_i+\sum_{i=M+1}^Ra_i\quad ,L\leq M<R ∑i=LRai=∑i=LMai+∑i=M+1Rai,L≤M<R);
- 区间最大值( max i = L R a i = max ( max i = L M a i , max i = M + 1 R a i ) , L ≤ M < R \max_{i=L}^Ra_i=\max(\max_{i=L}^Ma_i,\max_{i=M+1}^Ra_i)\quad ,L\leq M<R maxi=LRai=max(maxi=LMai,maxi=M+1Rai),L≤M<R)。
不满足区间加法的问题有:
- 区间的众数;
- 区间的最长不下降子序列。
线段树的原理及实现
注意:如果我没有特别申明的话,这里的询问全部都是区间求和
线段树主要是把一段大区间 平均地划分 成两段小区间进行维护,再用小区间的值来更新大区间。这样既能保证正确性,又能使时间保持在
log
\log
log 级别(因为这棵线段树是平衡的)。也就是说,一个
[
L
,
R
]
[L,R]
[L,R] 的区间会被划分成
[
L
,
⌊
L
+
R
2
⌋
]
\left[L,\left\lfloor\frac{L+R}{2}\right\rfloor\right]
[L,⌊2L+R⌋] 和
[
⌊
L
+
R
2
⌋
+
1
,
R
]
\left[\left\lfloor\frac{L+R}{2}\right\rfloor+1,R\right]
[⌊2L+R⌋+1,R] 这两个小区间进行维护,直到
L
=
R
L=R
L=R 。
下图就是一棵
[
1
,
10
]
[1,10]
[1,10] 的线段树的分解过程(相同颜色的节点在同一层)

可以发现,这棵线段树的最大深度不超过 ⌊ log 2 ( n − 1 ) ⌋ + 2 \lfloor\log_2(n-1)\rfloor+2 ⌊log2(n−1)⌋+2 。
储存方式
通常用的都是 堆式储存法 ,即编号为
k
k
k 的节点的左儿子编号为
2
k
2k
2k ,右儿子编号为
2
k
+
1
2k+1
2k+1 ,父节点编号为
⌊
k
2
⌋
\left\lfloor\frac{k}{2}\right\rfloor
⌊2k⌋ ,用 位运算 优化一下,以上的节点编号就变成了 k<<1
, k<<1|1
, k>>1
。其它储存方式请见 指针储存和动态开点 。
通常,每一个线段树上的节点储存的都是这几个变量:区间左边界,区间右边界,区间的答案(这里为区间元素之和)
注意:线段树的大小其实是 5n 左右的。
下面是线段树的定义:
struct node
{
int l/*区间左边界*/,r/*区间右边界*/,sum/*区间元素之和*/,lazy/*懒惰标记,此处默认是区间加,下文会提到*/;
node(){l=r=sum=lazy=0;}//给每一个元素赋初值
}a[N];//N为总节点数
inline void update(int k)//更新节点k的sum
{
a[k].sum=a[k*2].sum+a[k*2+1].sum+a[k].lazy;
//很显然,一段区间的元素和等于它的子区间的元素和
//如果有懒惰标记的话要相应地改变(记得加上懒惰标记的值!!!)感谢duck_master的指正
}
初始化
常见的做法是遍历整棵线段树,给每一个节点赋值,注意要递归到线段树的叶节点才结束。
void build(int k/*当前节点的编号*/,int l/*当前区间的左边界*/,int r/*当前区间的右边界*/)
{
a[k].l=l,a[k].r=r;
if(l==r)//递归到叶节点
{
a[k].sum=number[l];//其中number数组为给定的初值
return;
}
int mid=(l+r)/2;//计算左右子节点的边界
build(k*2,l,mid);//递归到左儿子
build(k*2+1,mid+1,r);//递归到右儿子
update(k);//记得要用左右子区间的值更新该区间的值
}
单点修改
当我们要把下标为 k k k 的数字修改(加减乘除、赋值运算等)时,可以直接在根节点往下DFS。如果当前节点的左儿子包含下标为k的数(即对于左儿子区间 [ L l s o n , R l s o n ] [L_{lson},R_{lson}] [Llson,Rlson] , L l s o n ≤ k ≤ R r s o n L_{lson}\leq k\leq R_{rson} Llson≤k≤Rrson ),那么就走到左儿子,否则走到右儿子(右儿子一定包含下标为 k k k 的数,因为根节点一定包含这个数,而从根节点往下走,能到达的点也一定包含这个数),直到 L = R L=R L=R 。这时就走到了只包含 k k k 的那个节点,只需要把这个点修改即可(这个点就相当于线段树中唯一只储存着 k k k 的信息的节点)。最后记得在回溯的时候把沿途经过的所有的点的值全部修改一下。
void change(int k/*当前节点的编号*/,int x/*要修改节点的编号*/,int y/*要把编号为x的数字修改成y*/)
{
if(a[k].l==a[k].r){a[k].sum=y;return;}
//如果当前区间只包含一个元素,那么该元素一定就是我们要修改的。
//由于该区间的sum一定等于编号为x的数字,所以直接修改sum就可以了。
int mid=(a[k].l+a[k].r)/2;//计算下一层子区间的左右边界
if(x<=mid) change(k*2,x,y);//递归到左儿子
else change(k*2+1,x,y);//递归到右儿子
update(k);//记得更新点k的值,感谢qq_36228735提出此错误
}
区间修改
其实如果会了 单点修改 的话,区间修改就不会太难理解了。
区间修改大体可以分为两步:
- 找到区间中全部都是要修改的点的线段树中的区间;
- 修改这一段区间的所有点。
先来解决第一步:
我们先从根节点出发(根节点一定包含所有的点,包括被修改区间),一直往下走,直到当前区间中的元素全部都是被修改元素。
当左区间包含整个被修改区间时,我们就递归到左区间;
当右区间包含整个被修改区间时,我们就递归到右区间;
否则,情况一定就如下图所示:
怎么办?这种情况似乎有些难了。
不过,通过思考,我们可以发现,被修改区间中的元素间,两两之间都不会产生影响。
所以,我们可以把被修改区间分解成两段,使得其中的一段完全在左区间,另一端完全在右区间。
很明显,直接在
m
i
d
mid
mid 的位置将该区间切开是最好的。如下图所示:
通过一系列的 玄学 操作,我们成功地把修改区间分解成一段一段的。但问题来了:我们怎样修改这些区间呢?
最暴力的做法是每一次都像建树一样,遍历区间内的所有节点,一一修改。但是这样的时间复杂度显然比暴力还多了个 4~5 的常数,我要这线段树有何用?
这里就要引入一样新的神奇的东西——懒惰标记!
懒惰标记
标记的含义:本区间已经被更新过了,但是子区间却没有被更新过,被更新的信息是什么(区间求和只用记录有没有被访问过,而区间加减乘除等多种操作的问题则要记录进行的是哪一种操作)。
这里再引入两个很重要的东西: 相对标记 和 绝对标记 。
相对标记和绝对标记
相对标记指的是可以共存的标记,且打标记的顺序与答案无关,即标记可以叠加。 比如说给一段区间中的所有数字都 + a +a +a ,我们就可以把标记叠加一下,比如上一次打了一个 + 1 +1 +1 的标记,这一次要给这一段区间 + 5 +5 +5 ,那么就把 + 1 +1 +1 的标记变成 + 6 +6 +6 。
绝对标记是指不可以共存的标记,每一次都要先把标记下传,再给当前节点打上新的标记。这些标记不能改变次序,否则会出错。 比如说给一段区间的数字重新赋值,或是给一段区间进行多种操作。
有了 懒惰标记 这种神奇的东西,我们区间修改时就可以偷一下懒,先修改当前节点,然后直接把信息挂在节点上就可以了!
如下面这棵线段树,当我们要修改区间
[
1
,
4
]
[1,4]
[1,4] ,将元素赋值为
1
1
1 时,我们可以先找到所有的整个区间都要被修改的节点,显然是储存区间
[
1
,
3
]
[1,3]
[1,3] 和
[
4
,
4
]
[4,4]
[4,4] 的这两个节点。我们就可以先把
[
1
,
3
]
[1,3]
[1,3] 的
s
u
m
sum
sum 改为
(
3
−
1
+
1
)
∗
1
=
3
(3-1+1)*1=3
(3−1+1)∗1=3 ,把
[
4
,
4
]
[4,4]
[4,4] 的
s
u
m
sum
sum 改为
(
1
−
1
+
1
)
∗
1
=
1
(1-1+1)*1=1
(1−1+1)∗1=1 ,然后给它们打上值为
1
1
1 的懒惰标记,然后就可以了。
这样一来,我们每一次修改区间时只要找到目标区间就可以了,不用再向下递归到叶节点。
下面是区间
+
x
+x
+x 的代码:
void changeSegment(int k,int l,int r,int x)
//当前到了编号为k的节点,要把[l..r]区间中的所有元素的值+x
{
if(a[k].l==l&&a[k].r==r)//如果找到了全部元素都要被修改的区间
{
a[k].sum+=(r-l+1)*x;
//更新该区间的sum
a[k].lazy+=x;return;
//懒惰标记叠加
}
int mid=(a[k].l+a[k].r)/2;
if(r<=mid) changeSegment(k*2,l,r,x);
//如果被修改区间完全在左区间
else if(l>mid) changeSegment(k*2+1,l,r,x);
//如果被修改区间完全在右区间
else changeSegment(k*2,l,mid,x),changeSegment(k*2+1,mid+1,r,x);
//如果都不在,就要把修改区间分解成两块,分别往左右区间递归
update(k);
//记得更新点k的值
}
请注意:某些题目的懒惰标记属于 绝对标记 (如维护区间 平方和 ),一定要先 下传标记 ,再向下递归。
下传标记
碰到 相对标记 这种容易欺负的小朋友,我们只用打一下懒惰标记就可以了。
但是,遇到 绝对标记 ,或是下文提到的 区间查询 ,简单地打上懒惰标记就明显GG了。毕竟, 懒惰标记 只是简单地在节点挂上一个信息而已,遇到复杂的情况可是不行的啊!
于是,懒惰标记的 下传操作 就诞生了。
顾名思义, 下传标记 就是把一个节点的懒惰标记传给它的左右儿子,再把该节点的懒惰标记删去。
我们先来回顾一下标记的含义:
标记的含义:本区间已经被更新过了,但是子区间却没有被更新过,被更新的信息是什么。
显然,父区间是包含子区间的,也就是对于父区间的标记和子区间是有联系的。在大多数情况下,父区间和子区间的标记是 相同的 。因此,我们可以由父区间的标记推算出子区间应当是什么标记。
注意:以下所说的问题都是指区间赋值,除非有什么特别的申明。
如果要给一个节点中的所有元素重新赋值为
x
x
x ,那么它的儿子也必定要被赋值成
x
x
x 。所以,我们直接在子节点处修改
s
u
m
sum
sum 值,再把子节点的标记改变一下就可以了(由于区间赋值要用 绝对标记 ,因此当子节点已经有标记时,要先下传子节点的标记,再下传该节点的标记。但是区间赋值会覆盖掉子节点的值,因此在这个问题中,直接修改标记就可以了)
下传区间
+
x
+x
+x 标记的代码如下:
void pushdown(int k)//将点k的懒惰标记下传
{
if(a[k].l==a[k].r){a[k].lazy=0;return;}
//如果节点k已经是叶节点了,没有子节点,那么标记就不用下传,直接删除就可以了
a[k*2].sum+=(a[k*2].r-a[k*2].l+1)*a[k].lazy;
a[k*2+1].sum+=(a[k*2+1].r-a[k*2+1].l+1)*a[k].lazy;
//给k的子节点重新赋值
a[k*2].lazy+=a[k].lazy;
a[k*2+1].lazy+=a[k].lazy;
//下传点k的标记
a[k].lazy=0;//记得清空点k的标记
}
那么区间赋值就很容易解决了。我们直接修改当前节点的 s u m sum sum ,再打上标记就可以了。在大多数问题中,我们要先下传当前节点的标记,再打上标记。但由于这个问题的特殊性,我们就不用先下传标记了。
区间查询
上面我们很轻松地解决了修改的问题,于是我们就维护了一个完整的在线线段树了。但是光有维护是没用的,我们还要处理询问的问题。最常见的莫过于区间查询了,如询问区间
[
l
.
.
r
]
[l..r]
[l..r] 中所有数的和。
这其实和 区间修改 是类似的。我们也分类讨论:
- 当查找区间在当前区间的左子区间时,递归到左子区间;
- 当查找区间在当前区间的右子区间时,递归到右子区间;
- 否则,这个区间一定是跨越两个子区间的,我们就把它切成2块,分在两个子区间查询。最后把答案合起来处理就可以了(如查询区间和时就把两块区间的和加起来,查询最大值时就返回两个区间的最大值的最大值)
最后强调一个细节: 记得在查询之前下传标记!!!
下面是查询区间和的代码:
int query(int k,int l,int r)
//当前到了编号为k的节点,查询[l..r]的和
{
if(a[k].lazy) pushdown(k);
//如果当前节点被打上了懒惰标记,那么就把这个标记下传,这一句其实也可以放在下一语句的后面
if(a[k].l==l&&a[k].r==r) return a[k].sum;
//如果当前区间就是询问区间,完全重合,那么显然可以直接返回
int mid=(a[k].l+a[k].r)/2;
if(r<=mid) return query(k*2,l,r);
//如果询问区间包含在左子区间中
if(l>mid) return query(k*2+1,l,r);
//如果询问区间包含在右子区间中
return query(k*2,l,mid)+query(k*2+1,mid+1,r);
//如果询问区间跨越两个子区间
}
指针储存和动态开点
上面我们用的都是 堆式储存法 。这种方法能快速地找出当前节点的父节点、子节点,但节点数很多,而无用节点也较多时就没有用了。我们可以用 指针储存和动态开点 解决这个问题。当然,也可以用 离散化 解决问题。
这其实就是用指针额外记录当前节点的子节点(有时可能还要记录父节点),且要用到节点时才新建节点。这样能大大地节省空间。
下面是结构体的定义:
struct node
{
int l/*区间左边界*/,r/*区间右边界*/,sum/*区间元素之和*/,lazy/*懒惰标记,下文会提到*/;
node *lson/*左儿子*/,*rson/*右儿子*/;
//这两个指针初始值为NULL,当儿子指针为NULL时表明它没有值
node(){l=r=sum=lazy=0;lson=rson=NULL;}//给每一个元素赋初值
};
node *root=new node;//根节点
inline void setroot()//根节点初始化
{
root->l=1,root->r=n;
}
inline void update(node *k)//更新节点k的sum
{
k->sum=0;
if(k->lson) k.sum+=k->lson->sum;
if(k->rson) k.sum+=k->rson->sum;
//注意要判断左右子节点是否存在
}
单点修改:
void change(node *k/*当前节点*/,int x/*要修改节点的编号*/,int y/*要把编号为x的数字修改成y*/)
{
if(k->l==k->r){k->sum=y;return;}
//如果当前区间只包含一个元素,那么该元素一定就是我们要修改的。
//由于该区间的sum一定等于编号为x的数字,所以直接修改sum就可以了。
int mid=(k->l+k->r)/2;//计算下一层子区间的左右边界
if(x<=mid)
{
if(!k->lson)//如果左儿子不存在,就新建一个
{
k->lson=new node;
k->lson->l=k->l;
k->lson->r=mid;
}
change(k->lson,x,y);//递归到左儿子
}
else
{
if(!k->rson)//如果右儿子不存在,就新建一个
{
k->rson=new node;
k->rson->l=mid+1;
k->rson->r=k->r;
}
change(k->rson,x,y);//递归到右儿子
}
}
其他操作相应地改一下就可以了,这里留给读者自己思考。
P.S:询问操作并 不用 新建节点。
其实动态开点不一定要用指针,也可以先开一个节点数组,每次新建节点时给它分配一个下标。不过个人觉得用指针方便一些(虽然常数巨大)。
关于模板
由于以前的代码可读性比较差,现在又一直没有时间,这里就不放模板了。
如果是急需模板的话,可以把上面的代码段复制下来使用(测试过没有问题的),注意有 区间修改 操作时 update
函数也要相应地加上 懒惰标记 的值(或者在每个 changeSegment
函数的开头都 pushdown
一下)。
扩展及应用
权值线段树
权值线段树 其实就相当于一个 桶 ,它维护了每一个数的出现次数。它可以解决许多问题(废话)。
下面就来看一道我脑补的题目(大佬勿喷):
给你一个长度为 n n n 的数组 a a a ,以及 m m m 个操作,每一个询问的格式为 [ x , l , r ] [x,l,r] [x,l,r] , x = 1 x=1 x=1 表示查询数组中值在区间 [ l . . r ] [l..r] [l..r] 中的元素的和, x = 2 x=2 x=2 表示将第 l l l 个数加 r r r 。每个数的取值范围: 0 ≤ a i ≤ 1 0 6 , n ≤ 2 ∗ 1 0 4 0\leq a_i\leq 10^6,n\leq 2*10^4 0≤ai≤106,n≤2∗104
这题显然可以用 权值线段树 做,其中线段树中区间 [ l , r ] [l,r] [l,r] 维护的是数值 l ≤ a i ≤ r l\leq a_i\leq r l≤ai≤r 的 a i a_i ai 的个数。由于节点数较多,要用动态开点。然后到了修改操作的时候,我们就把第 l l l 个数所对应的值单点修改(在 权值线段树 中将所对应的位置的值减一),再把第 r r r 个位置的值加一。
这其实就相当于一个 桶 ,很好理解的。
可持久化线段树(主席树)
主席树其实就是给线段树记录了 历史版本 。
给你一个长度为 n n n 的数组 a a a ,以及 m m m 个询问,每一个询问的格式为 [ l , r , k ] [l,r,k] [l,r,k] ,表示查询区间 [ l . . r ] [l..r] [l..r] 中第 k k k 大的数。
碰到这种情况,排序什么的就无能为力了。这就要用到 主席树 了。
我们可以开
n
n
n 棵 权值线段树 ,第
i
i
i 棵表示
[
1
,
i
]
[1,i]
[1,i] 中每一个数出现的次数(先不要担心空间的问题)。这种方法其实类似于 前缀和 ,询问时把第
r
r
r 棵线段树减去第
l
−
1
l-1
l−1 棵线段树(对应位置的值相减),再在得出的线段树中查找第
k
k
k 大的数。
假设现在的数列是
2
,
4
,
1
2,4,1
2,4,1 ,离散化为
2
,
3
,
1
2,3,1
2,3,1 。
那么这几棵线段树就会长成这个样子(壮观):
观察这个图,可以发现有不少信息相同的子树,我们可以把它们合并:
这就是 主席树 。
思路很简单,如果第
i
i
i 棵线段树的某子树和第
i
−
1
i-1
i−1 棵的那一个子树相同,那么就不用新建子树了,两棵线段树共用一个子树。在不用记录子树的父节点的情况下,这种方法是可行的。
观察上图,发现每一棵线段树都只有一条从根到底的路径是新建的,其余全部都是由以前的线段树得到的。因此我们可以证明这种方法能极大地优化空间。
还有一点比较坑的,在查询区间
[
l
,
r
]
[l,r]
[l,r] 的时候,我们不用把第
r
r
r 棵线段树的所有节点减去第
l
−
1
l-1
l−1 棵线段树的所有节点,一边递归一边减就可以了。最后用减出来的结果判断一下,看一看接下来递归到哪一个儿子即可。
但万一要需要修改怎么办?可以用类似于 树状数组 的方法,这里留给读者思考 其实是我不会 。
下面给出 gmoj.1011 的标程
#include<cstdio>
#include<algorithm>
using namespace std;
#define N 100010
struct tree
{
int sum,lson,rson;
tree(){lson=rson=sum=0;}
}node[N*30];
int a[N],id[N],b[N],root[N],s;
bool cmp(int x,int y){return a[x]<a[y];}
void insert(int p,int q,int l,int r,int k)
{
node[p].sum=node[q].sum+1;
if(l<r)
{
int mid=(l+r)/2;
if(k<=mid)
{
node[p].lson=++s;
if(node[q].rson) node[p].rson=node[q].rson;
else node[q].rson=++s;
insert(node[p].lson,node[q].lson,l,mid,k);
}
else
{
if(node[q].lson) node[p].lson=node[q].lson;
else node[q].lson=++s;
node[p].rson=++s;
insert(node[p].rson,node[q].rson,mid+1,r,k);
}
}
}
int find(int p,int q,int l,int r,int k)
{
if(l==r) return l;
int mid=(l+r)/2;
if(node[node[p].lson].sum-node[node[q].lson].sum>=k)
return find(node[p].lson,node[q].lson,l,mid,k);
return find(node[p].rson,node[q].rson,mid+1,r,
k-node[node[p].lson].sum+node[node[q].lson].sum);
}
int main()
{
int n,m,i,j,k,max=1;
scanf("%d%d",&n,&m);
for(i=1;i<=n;i++) scanf("%d",&a[i]),id[i]=i,root[i]=++s;
sort(id+1,id+n+1,cmp);
for(i=2;i<=n;i++)
{
if(a[id[i]]!=a[id[i-1]]) b[++max]=a[id[i]];
a[id[i]]=max;
}
b[1]=a[id[1]],a[id[1]]=1;
root[0]=0;
for(i=1;i<=n;i++) insert(root[i],root[i-1],1,max,a[i]);
while(m--)
{
scanf("%d%d%d",&i,&j,&k);
printf("%d\n",b[find(root[j],root[i-1],1,max,k)]);
}
return 0;
}
李超树
不知道李超树是什么?你应该听说过LCT吧,没错,李超树就是大名鼎鼎的LCT!
李超树是一种高效地维护平面内线段的数据结构。
给出一道例题吧(【HEOI2013】Segment):
要求在平面直角坐标系下维护两个操作:
- 在平面上加入一条线段。记第 i i i 条被插入的线段标号为 i i i 。
- 给定一个正整数 k k k ,询问与直线 x = k x=k x=k 相交的线段中,交点最靠上的线段的编号。
其中操作数是 1 0 5 10^5 105 级别的,横坐标是 3 × 1 0 4 3\times 10^4 3×104 级别的。
这题强制在线。
如果把所有线段都扔进线段树里维护,显然叶子节点
[
L
,
L
]
[L,L]
[L,L] 储存的就是与直线
x
=
L
x=L
x=L 相交的线段中,交点最靠上的线段的编号。
那么相应的,一个区间
[
L
,
R
]
[L,R]
[L,R] 上就要维护与直线
x
=
m
i
d
x=mid
x=mid 相交的线段中,交点最靠上的线段的编号。
现在考虑往区间
[
L
,
R
]
[L,R]
[L,R] 中插入一条新的线段
y
=
k
x
+
b
y=kx+b
y=kx+b (假如线段上的最大值大于等于
R
R
R ,最小值小于等于
L
L
L ):
- 若当前线段与 x = m i d x=mid x=mid 的交点比原来这个区间上的线段的交点更高时,如果原线段不是整段都比当前线段低(即 L L L 或 R R R 上的值比当前线段大),就把原线段下传。然后把当前线段挂到当前节点上。
- 否则,如果当前线段线段不是整段都比原线段低(即 L L L 或 R R R 上的值比原线段大),就把原线段下传。
下传的时候,要看一下这条线段在左区间优还是右区间优。假设要下传的线段是 y = k 1 x + b 1 y=k_1 x+b_1 y=k1x+b1 ,区间 [ L , R ] [L,R] [L,R] 上的线段是 y = k 2 x + b 2 y=k_2 x+b_2 y=k2x+b2 。如果 k 1 L + b 1 > k 2 L + b 2 k_1 L+b_1>k_2 L+b_2 k1L+b1>k2L+b2 ,那么就下传到左区间;如果 k 1 R + b 1 > k 2 R + b 2 k_1 R+b_1>k_2 R+b_2 k1R+b1>k2R+b2 ,下传到右区间。
下面是 【HEOI2013】Segment 的标程。
#include<cstdio>
using namespace std;
#define lson k<<1
#define rson k<<1|1
#define P2 1000000000
#define P1 39989
#define N 100005
inline char gc()
{
static char buf[100005],*l=buf,*r=buf;
return l==r&&(r=(l=buf)+fread(buf,1,100005,stdin),l==r)?EOF:*l++;
}
inline void read(int &k)
{
char ch;
while(ch=gc(),ch<'0'||ch>'9');k=ch-'0';
while(ch=gc(),ch>='0'&&ch<='9') k=k*10+ch-'0';
}
int tag[500005],ans;double top;
struct line{int l,r;double k,b;}a[N];
inline void swap(int &x,int &y){int z=x;x=y,y=z;}
inline double calc(int k,int x){return x*a[k].k+a[k].b;}
void modify(int k,int l,int r,int x)
{
if(a[x].l<=l&&r<=a[x].r)
{
if(!tag[k]) tag[k]=x;
else
{
bool b1=calc(tag[k],l)<calc(x,l),b2=calc(tag[k],r)<calc(x,r);
if(b1&&b2) tag[k]=x;
else if(b1||b2)
{
int mid=l+r>>1;
if(calc(tag[k],mid)<calc(x,mid)) swap(tag[k],x);
if(calc(tag[k],l)<calc(x,l)) modify(lson,l,mid,x);
else modify(rson,mid+1,r,x);
}
}
}
else
{
int mid=l+r>>1;
if(a[x].l<=mid) modify(lson,l,mid,x);
if(a[x].r>mid) modify(rson,mid+1,r,x);
}
}
void qry(int k,int l,int r,int x)
{
if(tag[k]&&(top<calc(tag[k],x)||top==calc(tag[k],x)&&tag[k]<ans))
top=calc(tag[k],x),ans=tag[k];
if(l==r) return;
int mid=l+r>>1;
if(x<=mid) qry(lson,l,mid,x);
else qry(rson,mid+1,r,x);
}
int main()
{
int n,i,j,cnt=0,opt,x0,y0,x1,y1,x;
double k;
read(n);
for(i=1;i<=n;++i)
{
read(opt);
if(opt==0)
{
read(x),x=(x+ans-1)%P1+1;
top=0,ans=0,qry(1,1,P1,x);
printf("%d\n",ans);
}
else
{
read(x0),read(y0),read(x1),read(y1);
x0=(x0+ans-1)%P1+1,y0=(y0+ans-1)%P2+1;
x1=(x1+ans-1)%P1+1,y1=(y1+ans-1)%P2+1;
if(x0>x1) swap(x0,x1),swap(y0,y1);
if(x0==x1) a[++cnt]=(line){x0,x0,0,double(y0>y1?y0:y1)};
else
{
k=(y1-y0)/(double)(x1-x0);
a[++cnt]=(line){x0,x1,k,y0-x0*k};
}
modify(1,1,P1,cnt);
}
}
return 0;
}
非递归式线段树(ZKW线段树)
前言
其实这个东西在竞赛中作用不大,感兴趣的读者就看一下吧。
介绍
非递归式线段树(ZKW线段树,张昆玮线段树),是清华大学的张昆玮在ppt 《统计的力量》 中提出的。
这种线段树最大的特点就是很重口味不用递归实现,因此常熟小、且码量不长,便于调试和卡常数。
它之所以与普通线段树不同,主要是因为它是一颗 自底到根 的线段树。
总的来说,它相对于线段树而言,主要有以下优点:
- 常数(时间)更小
- 代码长度更短
- 调试复杂度更低(?)
- 空间更小
- 学习难度更低
- 解决复杂问题的难度更低(如区间修改)
储存方式
ZKW线段树采用的是 堆式储存法 ,上文已经有所提及了。但是ZKW线段树与普通线段树的 堆式储存 又有点不同——它是一棵 满二叉树 ,以方便定位叶节点的位置。
也就是说,它无论如何都要开到
2
2
2 的幂次这么大,因此ZKW线段树的空间就是
2
⌈
log
2
n
⌉
+
1
−
1
2^{\lceil\log_2n\rceil+1}-1
2⌈log2n⌉+1−1 。为什么要
+
1
+1
+1 、
−
1
-1
−1 ?因为线段树除了储存最基础信息的
2
⌈
log
2
n
⌉
2^{\lceil\log_2n\rceil}
2⌈log2n⌉ 个叶节点外,还要有
2
⌈
log
2
n
⌉
−
1
2^{\lceil\log_2n\rceil}-1
2⌈log2n⌉−1 个管理左右子节点的父节点;而且有
k
k
k 层的线段树大小是
2
k
−
1
2^k-1
2k−1 的。
总的来说,zkw线段树长得还是很像一棵普通的线段树的。
建树
可以定义
M
M
M 表示最后一层的叶节点数,显然
M
≥
n
M\geq n
M≥n ,非叶节点有
M
−
1
M-1
M−1 个。但是我们在区间修改时,需要多
2
2
2 个节点
0
0
0 和
n
+
1
n+1
n+1 (实际上可以证明
n
+
1
n+1
n+1 是没有必要的),因此
M
≥
n
+
1
M\ge n+1
M≥n+1 ,第 i 个叶节点编号就为M+i。
那么程序就好写了:
inline void build()
{
for(M=1;M<=n;M<<=1);//计算M的值,注意M>=n+1
for(int i=1;i<=n;i++) scanf("%d",&max[M+i]);//输入叶节点的值
for(int i=M-1;i;i--) max[i]=mymax(max[i<<1],max[i<<1|1]);
//更新所有非叶节点的值,此处以最大值为例
}
单点查询
接下来就可以解决一个小怪——单点查询了。
显然,如果我们要查询第
i
i
i 个数的话,直接输出线段树中
M
+
i
M+i
M+i 号节点的值就可以了。
时间复杂度是
O
(
1
)
O(1)
O(1) ,和普通线段树的
O
(
log
2
n
)
O(\log_2n)
O(log2n) 对比起来,顿感高大上!
inline int qry_single(int k)
{
return max[M+k];
}
单点修改
和 单点查询 一样,我们先修改线段树中
M
+
i
M+i
M+i 号节点。
很明显,我们修改了一个节点后,只会影响到它到根节点的那一条路径上的点。
这样的时间复杂度也变成了
O
(
log
2
M
)
O(\log_2M)
O(log2M) 了,似乎与普通线段树一般了。但是我们的常数还是相当优越的——几乎整整小了一倍!
代码:
inline void change_single(int x,int y)//把第x个节点改成y
{
max[x+=M]=y;
while(x>>=1) max[x]=mymax(max[x<<1],max[x<<1|1]);
}
后记
此博客中的脑残错误比较多,若各位读者有发现,欢迎指出,以免误人子弟。