谈笑风生线段树(区间修改)

上次我们讲过了线段树的点修改,它非常简单。
所以,有些基础的东西(包括变量的含义)我可能不再赘述,可以参考我的文章:线段树点修改
今天我们再讲稍微复杂一些的线段树的区间修改,以及区间和、最大值、最小值的查询。
这里的区间修改有不同的修改方式,首先是将某一区间都加上某一个值,或是将一个区间全部改成某一个值,还有一种更高级的,算是彩蛋,就是将一个区间的数对应加上一个等差数列的某一项。当然我们还能使某一个区间的每个数都乘上某一个数,都行,这看具体的题目,去灵活改动。
下面我们就开始给大家介绍几种情况。

一、区间加值

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、阶段性总结

其实通过这几个例子,我们也能看出线段树变来变去,变的主要还是标记部分,所以遇到题目时,要有意识的去设计标记,以达到适应题目的效果。

四、结束语

线段树现在也讲完了,接下来我会找些题目给大家练习,敬请期待。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值