书接上文树状数组,区间修改与区间查询用线段树实现
线段树的拓展性较强,一般被当做处理区间的工具,所以熟练的掌握线段树尤为重要
线段树的结构分析
线段树的本质是一颗二叉树,这意味着我们开数组时往往需要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));
}
}
}