上次我们讲过了线段树的点修改,它非常简单。
所以,有些基础的东西(包括变量的含义)我可能不再赘述,可以参考我的文章:线段树点修改
今天我们再讲稍微复杂一些的线段树的区间修改,以及区间和、最大值、最小值的查询。
这里的区间修改有不同的修改方式,首先是将某一区间都加上某一个值,或是将一个区间全部改成某一个值,还有一种更高级的,算是彩蛋,就是将一个区间的数对应加上一个等差数列的某一项。当然我们还能使某一个区间的每个数都乘上某一个数,都行,这看具体的题目,去灵活改动。
下面我们就开始给大家介绍几种情况。
一、区间加值
1、区间和查询
更新结点附加值
void update(int rt){
sum[rt]=sum[rt<<1]+sum[rt<<1|1];
}
这是一个简单的更新,没什么好讲的,就是左子结点和加上右子结点和。
打标记
void color(int l,int r,int rt,int a) {
sum[rt]=sum[rt]+a\*(r-l+1);
add[rt]+=a;
}
什么叫打标记?
这是区间修改需要引进的一样新东西。
首先我们考虑正常情况下,我们会怎么进行区间加值。
答案就是,讨论,往左右子树走,直到叶子结点。
那么我们会发现我们修改的复杂度比直接修改原数组还高,
特别是我们修改整个区间时,每个结点都要访问一遍,这样的话会降低线段树的效率。
所以,我们可以通过打标记来提高效率。
怎么做呢?
就是一旦修改区间包含当前区间,我们就在当前区间记录下要加的值,这就是add数组,等到我们需要查询该结点的子结点时,我们在将标记下放,再对子结点的sum进行修改。
如该段代码,打标记时,先把本身加上这个值,再记录下要给子结点加值,这样就完成了打标记的步骤。
如果还没明白标记怎么用,可以继续把代码看完。
标记下放
void push_col(int l,int r,int rt) {
if (add[rt]) {
int m=(l+r)>>1;
color(l,m,rt<<1,add[rt]);
color(m+1,r,rt<<1|1,add[rt]);
add[rt]=0;
}
}
这一步就是上面说的标记下放的函数,我们可以用这个函数将当前结点的标记下放至子结点。
先判断是否有标记,没有就返回,有的话,就二分区间,给两个子结点染色(打标记),然后把当前标记清空即可。
建树过程
void build(int l,int r,int rt){
if (l==r) {
sum[rt]=z[l];
add[rt]=0;
return;
}
int m=(l+r)>>1;
build(l,m,rt<<1);
build(m+1,r,rt<<1|1);
update(rt);
}
这个函数是用来在初始时,搭建线段树的。
比如有的题目会给出一个数列的初始值,这样的话我们就可以用这个函数来构建该数列的线段树。
注释:z表示原始数组。
也不难理解,我简单解释一下。
首先判断l和r是否相等,若相等,说明这是单点区间,那么直接把他的sum设置为这个单点。
若不是,则递归建立左右子树,然后update一下,求得当前区间的sum
区间增值修改
void modify(int l,int r,int rt,int nowl,int nowr,int c) {
if (nowl<=l && r<=nowr) {
color(l,r,rt,c);
return;
}
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m) modify(l,m,rt<<1,nowl,nowr,c);
if (m<nowr) modify(m+1,r,rt<<1|1,nowl,nowr,c);
update(rt);
}
这里就是修改的核心代码了。
首先判断修改区间是否完全包含此区间,
包含的话,就直接打标记。
如果不是,那么就下放一次标记,然后再看是否覆盖到了左右子结点。
对对应的子结点修改。
最后再update一下即可。
区间和查询
int query(int l,int r,int rt,int nowl,int nowr) {
if (nowl<=l && r<=nowr) return sum[rt];
push_col(l,r,rt);
int m=(l+r)>>1,ans=0;
if (nowl<=m) ans+=query(l,m,rt<<1,nowl,nowr);
if (m<nowr) ans+=query(m+1,r,rt<<1|1,nowl,nowr);
return ans;
}
区间和查询就更好理解了。
还是,若覆盖,直接返回该区间的和。
否则查询对应的子结点。
最后把结果相加即可。
2、区间最大值查询
更新结点最大值
void update(int rt){
maxn[rt]=max(maxn[rt<<1],maxn[rt<<1|1]);
}
没什么大的变化,只是求和变成了取左右子结点的最大值。
打标记
void color(int l,int r,int rt,int a) {
maxn[rt]=maxn[rt]+a;
add[rt]+=a;
}
记录标记的函数值的更改没有变化,只是附加值发生了变化。
如果一个区间每个数都加上了一个值,那么这段区间的最大值也会加上这个值。
所以直接把最大值加上这个值就行了。
下放标记
void push_col(int l,int r,int rt) {
if (add[rt]) {
int m=(l+r)>>1;
color(l,m,rt<<1,add[rt]);
color(m+1,r,rt<<1|1,add[rt]);
add[rt]=0;
}
}
好像没有任何变化。
建树过程
void build(int l,int r,int rt){
if (l==r) {
maxn[rt]=z[l];
add[rt]=0;
return;
}
int m=(l+r)>>1;
build(l,m,rt<<1);
build(m+1,r,rt<<1|1);
update(rt);
}
依然没有任何变化。
修改区间值
void modify(int l,int r,int rt,int nowl,int nowr,int c) {
if (nowl<=l && r<=nowr) {
color(l,r,rt,c);
return;
}
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m) modify(l,m,rt<<1,nowl,nowr,c);
if (m<nowr) modify(m+1,r,rt<<1|1,nowl,nowr,c);
update(rt);
}
依然没变化。
查询区间最大值
int query(int l,int r,int rt,int nowl,int nowr) {
if (nowl<=l && r<=nowr) return maxn[rt];
push_col(l,r,rt);
int m=(l+r)>>1,ans=-1;
if (nowl<=m) ans=max(ans,query(l,m,rt<<1,nowl,nowr));
if (m<nowr) ans=max(ans,query(m+1,r,rt<<1|1,nowl,nowr));
return ans;
}
变化依然很小,只是求和变成了取左右最大值。
阶段性总结
最小值我就不写了,和最大值一样。
相信到这里,大家应该发现不同版本的线段树其实变化并不大,所以
大家在遇到不同的题目时一定要会灵活处理,要能举一反三。
下面我们来讲另一种修改方式——区间改成某一个数。
二、区间改值
1、区间和查询
这里,如果我把代码完整地给出,你会发现和区间加值的区间和查询几乎一模一样,
只有染色的代码不一样。
所以我这里就只给出染色的代码,其他的就参考区间加值的代码。
打标记
void color(int l,int r,int rt,int a) {
sum[rt]=a*(r-l+1);
change[rt]=a;
}
这里简单解释一下。
由于我们把区间内的值全部改成了a,所以该区间的值就会变成区间长度乘上a,直接赋值即可。
还有注意,change这个标记数组在修改时,要直接换掉。
2、区间最大值查询
一样的,我们只展现打标记的函数,其他函数还是参考区间加值的求最大值代码。
打标记
void color(int l,int r,int rt,int a) {
maxn[rt]=a;
change[rt]=a;
}
如果一个区间全部改成某一个值,那么很明显这个区间的最大值也是这个值,所以直接把该区间的最大值直接改成这个值即可。
change数组一样,还是直接改掉,将之前的区间覆盖掉。
3、阶段性总结
还是很简单对吧?所以只要深入理解了之后,你就可以随心所欲的玩线段树了。
还有区间乘上某一个值的,这个我就不讲了,大家自己想想。
我接下来就来讲彩蛋部分,就是对一个区间加上一个等差数列。
三、区间加等差数列
我先解释一下,这是什么意思。
直接举例子,比如有区间[1,5],我们要给它加上以2为首项,以3为公差的等差数列。
那么我们设原数组为A,那么该操作就是:
A[1]+=2,A[2]+=2+3,A[3]+=2+3+3,A[4]+=2+3+3+3,A[5]+=2+3+3+3+3
现在你应该明白了这是怎么一回事了,下面我们开始讲怎么实现。
1、区间和查询
区间更新我就不写了,和以前一样。
建树过程
void build(int l,int r,int rt){
if (l==r) {
sum[rt]=z[l];
shouxiang[rt]=0;
gongcha[rt]=0;
return;
}
int m=(l+r)>>1;
build(l,m,rt<<1);
build(m+1,r,rt<<1|1);
update(rt);
}
和其他的区间和查询的代码没有什么大的差别,但是我们能看到两个非常中文化的数组:gongcha和shouxiang。我想我不解释,大家都知道这是什么,这里只是做了一个初始化。
区间修改
void modify(int l,int r,int rt,int nowl,int nowr,int a,int b) {
if (nowl<=l && r<=nowr) {
color(l,r,rt,a,b);
return;
}
push_col(l,r,rt);
int m=(l+r)>>1;
if (nowl<=m) modify(l,m,rt<<1,nowl,nowr,a,b);
if (m<nowr) modify(m+1,r,rt<<1|1,nowl,nowr,a,b);
update(rt);
}
这里我们是加上以a为首项,b为公差的数列,看起来没有什么差别。
区间和查询
int query(int l,int r,int rt,int nowl,int nowr) {
if (nowl<=l && r<=nowr) return sum[rt];
push_col(l,r,rt);
int m=(l+r)>>1,ans=0;
if (nowl<=m) ans+=query(l,m,rt<<1,nowl,nowr);
if (m<nowr) ans+=query(m+1,r,rt<<1|1,nowl,nowr);
return ans;
}
区间和查询,依然没有什么重点的修改。
下面就是重点了,那就是如何打标记,先上代码;
打标记
void color(int l,int r,int rt,int a,int b) {
sum[rt]=sum[rt]+a*(r-l+1)+b*(r-l)*(r-l+1)/2;
shouxiang[rt]+=a;
gongcha[rt]+=b;
}
这里a和b的含义和前面一样。
两个修改值维护的数组的修改还是一样的。
那么重点就是对区间和的修改,那后面的一大串是什么东西(晕)?
其实就是等差数列的求和公式,不知道的话自己查一下,我这里不再赘述。
所以这样就可以给区间加上等差数列了。
下放标记
void push_col(int l,int r,int rt) {
if (shouxiang[rt] || gongcha[rt]) {
int m=(l+r)>>1;
color(l,m,rt<<1,shouxiang[rt],gongcha[rt]);
color(m+1,r,rt<<1|1,shouxiang[rt]+gongcha[rt]*(m-l+1),gongcha[rt]);
shouxiang[rt]=0,gongcha[rt]=0;
}
}
这次下放标记有了很大的变动,我们可以看到,首先,判断条件发生了小的变化,但我相信大家都能理解,就不说了。
下面我们来中点看一下给右结点染色的代码,我们注意到它的首项是一个很长的式子,那么这是一个什么样的式子呢?为什么要这么写呢?
其实很简单,因为这段区间为于当前区间的右边,所以它开始加的一定不是真正的首项,比如我们在开头举的例子,A[3]加的就是8,而不是2,所以我们要把右区间的第一个数加上的数字作为首项,这样就能顺利的实现这个功能。
3、阶段性总结
其实通过这几个例子,我们也能看出线段树变来变去,变的主要还是标记部分,所以遇到题目时,要有意识的去设计标记,以达到适应题目的效果。
四、结束语
线段树现在也讲完了,接下来我会找些题目给大家练习,敬请期待。