线段树基础

书接上文树状数组,区间修改与区间查询用线段树实现

线段树的拓展性较强,一般被当做处理区间的工具,所以熟练的掌握线段树尤为重要

线段树的结构分析

线段树的本质是一颗二叉树,这意味着我们开数组时往往需要4倍n(原数组长)的空间。

 通过上图可以发现,每个线段树的节点都有左右端点(毕竟是线段嘛),节点存放的值就是在这个区间内线段树需要维护的值。

加法线段树的维护与查询

线段树的修改

假设我们要令区间 (x,y)增加 ,从树的根开始找,很明显根的左右节点为(1,n) ,进行递归查询,定义每次查找到的节点为p,区间为(l,r),mid=(l+r)/2,根据二叉树方式递归

现在就会出现三种情况

1、区间(x,y)完全包括区间(l,r),此时就可以直接令当前节点增加k*(r-l+1)

2、(x,y)与(l,mid)有交集,递归左儿子

3、(x,y),与(mid+1,r)有交集,递归右儿子

这是线段树维护和查询的基本框架,

是不是觉得很简单,但我们还举回上面(1,4)区间的例子

假设我更新了(1,3)区间,现在请你按照上述方式推一下,会发现只有(1,2)和(3,3)节点被更新了

但如果此时我查询区间(2,4),我搜索到节点(2,2)时,理论上它应该被更新,但实际上并没有

针对这样的漏洞,你会怎么解决?

难道每次都把所有包括的节点都更新吗,显然那样线段树根本起不到优化作用(甚至>暴力)

所以lazytag懒标记就显得尤为关键,懒标记的思想就是增加一个线段树维护的值

每次修改时对于当前节点的懒标记增加k,在修改与查询的时候,我们在递归左右儿子的时候不断下传懒标记给儿子,这样既不会增加时间,也能解决上述问题

要注意的是懒标记下传时当前节点懒标记要清0(不然就重复累加了)

线段树的查询

查询函数的基本逻辑与修改一样,返回左右儿子之和作为答案,此时也需要下传懒标记

代码及注释

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=1e5+10;
unsigned ll n,m,a[MAXN],ans[MAXN*4],la[MAXN*4];
inline ll ls(ll x)
{
    return x*2;
}
inline ll rs(ll x)
{
    return x*2+1;
}
//求子节点 
void cr()
{
    scanf("%lld%lld",&n,&m);
}
inline void push_up(ll p)//求和 
{
    ans[p]=ans[ls(p)]+ans[rs(p)];
}
void build(ll p,ll l,ll r)//按原数据建树 
{
    la[p]=0;
    if(l==r){ans[p]=a[l];return ;}
    ll mid=(l+r)>>1;
    build(ls(p),l,mid);
    build(rs(p),mid+1,r);//分治思想 
    push_up(p);//回溯时求和 
} 
inline void f(ll p,ll l,ll r,ll k)
{
    la[p]=la[p]+k;
    ans[p]=ans[p]+k*(r-l+1);
}
inline void push_down(ll p,ll l,ll r)
{
    ll mid=(l+r)>>1;
    f(ls(p),l,mid,la[p]);//将懒标记加入子节点 
    f(rs(p),mid+1,r,la[p]);
    la[p]=0;
    //懒标记归零 
}
inline void update(ll nl,ll nr,ll l,ll r,ll p,ll k)
{
    if(nl<=l&&r<=nr)
    {
        ans[p]+=k*(r-l+1);
        la[p]+=k;
        return ;//要求区间完全包裹被查询区间就直接更新
    }
    push_down(p,l,r);//下传懒标记
    ll mid=(l+r)>>1;
    if(nl<=mid)update(nl,nr,l,mid,ls(p),k);
    if(nr>mid) update(nl,nr,mid+1,r,rs(p),k);
    //要求区间与左右任意区间有重合就更新
    push_up(p);//求和 
}
ll query(ll q_x,ll q_y,ll l,ll r,ll p)
{
    ll res=0;
    if(q_x<=l&&r<=q_y)return ans[p];//要求区间完全包裹被查询区间
    ll mid=(l+r)>>1;
    push_down(p,l,r);//下传懒标记 
    if(q_x<=mid)res+=query(q_x,q_y,l,mid,ls(p));
    if(q_y>mid) res+=query(q_x,q_y,mid+1,r,rs(p));//左右儿子
    return res;
}
int main()
{
    ll a1,x1,y1,z1;
    cr();
    build(1,1,n);
    while(m--)
    {
        scanf("%lld",&a1);
        if(a1==0){
            scanf("%lld%lld%lld",&x1,&y1,&z1);
            update(x1,y1,1,n,1,z1);//更改区间x1,y1 查询区间 1~n  根节点1   增加z1 
        }
        if(a1==1) {
            scanf("%lld%lld",&x1,&y1);
            printf("%lld\n",query(x1,y1,1,n,1));
        }
    }
    return 0;
}

加乘线段树的维护

加乘优先级

如果这个线段树只有乘法,那么直接将lazytag的加法变成乘,然后ans[p]*=k就好了。但是,如果我们是又加又乘,那就不一样了。

此时线段树的维护有两种操作:令区间(x,y)+k   、  令区间(x,y)*k

那么引申出一个问题,lazytag先加再乘还是先乘再加

而所谓先乘后加就是在做乘法的时候把加法标记也乘上这个数,在后面做加法的时候直接加

简单举个例子

考虑1~3的线段树,现将1~3加2,再将1~3乘上3,最后让1~3加4

求1~3的和

自己纸笔根据先加再乘和先乘再加运算一下

答案应该是

sum=(a[1]+2)*3+(a[2]+2)*3+(a[3]+2)*3;

先加再乘

sum=(a[1]+2+4)*3+(a[2]+2+4)*3+(a[3]+2+4)*3;
   =(a[1]+2)*3+4*3+(a[2]+2)*3+4*3+(a[3]+2)*3+4*3;

显然两者不等价

而先乘后加

sum=(a[1]*3+2*3+4)+(a[2]*3+2*3+4)+(a[3]*3+2*3+4);

是正确的

所以我们使用先乘再加

push_down逻辑结构

此时我们需要两个lazytag,mlz和add,我们可以写一个结构体将lazytag和ans数组合并。

定义结构体数组tree,分析两个lazytag的处理方式

mlz很简单,pushdown时直接乘父亲的mlz就可以了

而add,我们需要的add*父亲的mlz再加上父亲的add

这就是push_down 函数的逻辑结构

代码及注释

我们需要分别写出维护乘和加的函数,两个函数的逻辑与求和线段树差别不大,根据代码注释自行理解即可

tips:乘法用longlong哦

代码对应题目洛谷P3373

#include<bits/stdc++.h>
using namespace std;
int mod;
long long a[100010];
struct tree{
	long long v,mlz,add;//和,乘法lazytag,加法lazytag
}tr[400040];
void build(int root,int l,int r){//建树
	tr[root].mlz=1;//乘法应初始化为1
	tr[root].add=0;
	if(l==r) tr[root].v=a[l];
	else{
		int mid=(l+r)>>1;
		build(root*2,l,mid);
		build(root*2+1,mid+1,r);
		tr[root].v=tr[root*2].v+tr[root*2+1].v;
	}
	tr[root].v%=mod;
}
void pushdown(int root,int l,int r){
	int mid=(l+r)>>1;
	//根据我们规定的优先度,儿子的值=此刻儿子的值*爸爸的乘法lazytag+儿子的区间长度*爸爸的加法lazytag
	tr[root*2].v=(tr[root*2].v*tr[root].mlz+tr[root].add*(mid-l+1))%mod;
	tr[root*2+1].v=(tr[root*2+1].v*tr[root].mlz+tr[root].add*(r-mid))%mod;
	
	tr[root*2].mlz=(tr[root*2].mlz*tr[root].mlz)%mod;
    tr[root*2+1].mlz=(tr[root*2+1].mlz*tr[root].mlz)%mod;
	
    tr[root*2].add=(tr[root*2].add*tr[root].mlz+tr[root].add)%mod;
    tr[root*2+1].add=(tr[root*2+1].add*tr[root].mlz+tr[root].add)%mod;

    tr[root].mlz=1;
    tr[root].add=0;
}
void cheng(int root,int nl,int nr,int l,int r,long long k){
	if(r<nl||nr<l){
        return ;
    }//越界返回
    if(l<=nl&&nr<=r){
        tr[root].v=(tr[root].v*k)%mod;
        tr[root].mlz=(tr[root].mlz*k)%mod;
        tr[root].add=(tr[root].add*k)%mod;
        return ;
    }//乘法时也要维护加法lazytag
    pushdown(root,nl,nr);//传递lazytag
    int mid=(nl+nr)>>1;
    cheng(root*2,nl,mid,l,r,k);
    cheng(root*2+1,mid+1,nr,l,r,k);
    tr[root].v=(tr[root*2].v+tr[root*2+1].v)%mod;
}
void jia(int root,int nl,int nr,int l,int r,long long k){
	if(r<nl||nr<l){
        return ;
    }//越界
    if(l<=nl&&nr<=r){
        tr[root].add=(tr[root].add+k)%mod;
        tr[root].v=(tr[root].v+k*(nr-nl+1))%mod;
        return ;
    }//加法不变
    pushdown(root,nl,nr);
    int mid=(nl+nr)>>1;
    jia(root*2,nl,mid,l,r,k);
    jia(root*2+1,mid+1,nr,l,r,k);
    tr[root].v=(tr[root*2].v+tr[root*2+1].v)%mod;
}
long long query(int root,int nl,int nr,int l,int r){
    if(r<nl||nr<l){
        return 0;
    }
    if(l<=nl&&nr<=r){
        return tr[root].v;
    }
    pushdown(root,nl,nr);
    int mid=(nl+nr)>>1;
    return (query(root*2,nl,mid,l,r)+query(root*2+1,mid+1,nr,l,r))%mod;
}
int main(){
	int n,m;
	scanf("%d%d%d",&n,&m,&mod);
	for(int i=1;i<=n;i++){
		scanf("%lld",&a[i]);
	}
	build(1,1,n);
	while(m--){
		int sb;
		scanf("%d",&sb);
		if(sb==1){
			int x,y;
			long long k;
			scanf("%d%d%lld",&x,&y,&k);
			cheng(1,1,n,x,y,k);
		}
		if(sb==2){
			int x,y;
			long long k;
			scanf("%d%d%lld",&x,&y,&k);
			jia(1,1,n,x,y,k);
		}
		if(sb==3){
			int x,y;
			scanf("%d%d",&x,&y);
			printf("%lld\n",query(1,1,n,x,y));
		}
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值