线段树(2)——其实是总题解
借这道挺难的模板题浅谈线段树入门必须的几种技巧
1.基础的建树
理解:sum数组用来存这棵树,前边分段的操作可以看作搭建树的建构,把树对应的每个坐标的格子空下用来装值。挡拆到单点时将需要进行树状处理的数组填进去,然后按照自己的需要上推所需要的值,线段树就构造完成了
代码大致如下(十分简单):
void build(int x,int l,int r)
{
if(l==r)
{
sum[x]=a[l];//找到单点
return;//记得return!!!!!!!!!!!!!要不然会毒瘤
}
int mid=(l+r)/2;
build(x*2,l,mid);//拆分
build(x*2+1,mid+1,r);
push_up(x);//往上推回溯
}
2.基础的查询
先去做I HATE IT和模板1,涉及简单的查询
理解:
(1)单点查找【替换】
最简单的理解是: 二分查找!
与建树的过程大致相同:二分在左右儿子中找需要的找到需要的单点时对其进行操作即可
代码
void max_updata(int x,int l,int r,int a,int b)//把a的成绩改为b,b是个数字
{
if(l==r)//找到单点a
{
sum[x]=max(sum[x], b);//替换
return;
}
int mid=(l+r)/2;//标准且经典的二分查找
if(a<=mid) max_updata(x*2,l,mid,a,b);
else max_updata(x*2+1,mid+1,r,a,b);
push_up(x);//别忘了在改动最下边的值之后pushup回溯改变上边的值
}
(2)最大最小值查询
这个代码就更弟弟了,二分查找左右儿子中最大/最小的那个
特别说明:1. 求最大最小值时,因为选择方法的改变,上推条件变成了比大小,push_up函数需要修改;2. 求最小值时,不在界内最好返回一个极大的值,不要随手返回0(本人在这个点上卡了一个半小时)
代码
int min_update(int x,int l,int r,int lx,int ly)
{
if(l>ly||r<lx) return 0x7f7f7f7f;///又见毒瘤
// printf("%d %d %d %d %d %d\n",x,l,r,lx,ly,sum[x]);
if(l>=lx&&r<=ly) return sum[x];
int mid=(l+r)/2;
int t= min(min_update(x*2,l,mid,lx,ly),min_update(x*2+1,mid+1,r,lx,ly));/////////
// printf(" %d\n",t);
return t;
}
(3)区间查找求和
这里要引进一个个人一直用的(暴力的)讨论方式:三段分类讨论
1.当前区间完全不在要求的区间中,当前区间的SUM值完全与要求的没有关系,返回0; 2.当前区间完全在要求的区间之内,即使还没求完也不用当前的区间管了,之间返回当前sum值就好。 3.如果当前区间超出了要求的区间,那么就二分拆分它直到拆到在界内为止【期间必定就舍去了界外的部分】
代码
int query(int x,int l,int r,int lx,int ly)//////////
{
if(l>ly||r<lx) return 0;
if(lx<=l&&r<=ly) return sum[x];
int mid=(l+r)/2;
//push_down_multiply(x,l,r);//没区间替换不用下移lazy_tag
//push_down_add(x,l,r);
int summ=query(x*2,l,mid,lx,ly)+query(x*2+1,mid+1,r,lx,ly);
return summ;
}
3.基础的区间修改
这里涉及一个lazy_tag的概念,其他dalao已经讲的很清楚了,只简单提一下:
lazy_tag与之前的push_up正好相反,修改区间时还修改儿子,那为啥还要用线段树?!跑得比暴力还慢。。。于是我们修改爸爸,利用爸爸前溯修改儿子,就可以大大减少操作次数。
那么lazy_tag到底是啥呢?就是对爸爸(其实就包括了儿子)的操作标记,每次下移一层(!!)就像传家宝一样,一代一代往下传,越攒越多(别忘了儿子自己也有tag),传到最后一代就完成了修改。
代码(注意事项在其中)
void push_down(int x,int l,int r)
{
if(add[x])
{
int leftson=x*2,rightson=x*2+1;
int mid=(l+r)/2;
add[leftson]+=add[x];//注意这里的+=
add[rightson]+=add[x];
sum[leftson]+=add[x]*(mid-l+1);//同时修改总值,标记*长度即可
sum[rightson]+=add[x]*(r-(mid+1)+1);
add[x]=0;//归零要不然回溯的时候会多加一遍
return;
}
}
最后讲讲这道题需要注意的地方:
难点在于:1.多了一个乘法标记,这种修改是牵一发而动全身的,在下移乘法标记的时候要先更新加法标记 2.须知乘法结合律,
(a+b)c!=ac+b;所以做区间乘法时要先下移乘法标记,再下移加法标记; 3.这道题数字会非常大,所以在计算中千万记得要把该取模的都取上,要不然也会毒瘤 4.multiply标记是 乘法!!在下以后要归为1!!!在初始时也要先赋成1!!不然也会毒瘤!!
AC代码附上:
///ORZ ZWH
#include<bits/stdc++.h>
#define int long long
#define MAXN 100010
using namespace std;
int n,m,p;
int a[MAXN],tree[MAXN*4],sum[MAXN*4],add[MAXN*4],multiply[MAXN*4];
void push_up(int x)//////////
{
sum[x]=sum[x*2]+sum[x*2+1];
}
void push_down_add(int x,int l,int r)/////////
{
if(add[x])
{
int leftson=x*2,rightson=x*2+1;
int mid=(l+r)/2;
add[leftson]=(add[leftson]%p+add[x]%p)%p;
add[rightson]=(add[rightson]%p+add[x]%p)%p;
sum[leftson]=(sum[leftson]%p+add[x]%p*(mid-l+1)%p)%p;
sum[rightson]=(sum[rightson]%p+add[x]%p*(r-(mid+1)+1)%p)%p;
add[x]=0;
return;
}
}
void push_down_multiply(int x,int l,int r)
{
if(multiply[x]!=1)
{
int leftson=x*2,rightson=x*2+1;
multiply[leftson]=(multiply[leftson]*multiply[x])%p;
multiply[rightson]=(multiply[rightson]*multiply[x])%p;
add[leftson]=(add[leftson]*multiply[x])%p;
add[rightson]=(add[rightson]*multiply[x])%p;
sum[leftson]=(sum[leftson]*multiply[x])%p;
sum[rightson]=(sum[rightson]*multiply[x])%p;
multiply[x]=1;
return;
}
}
void build(int x,int l,int r)/////////////
{
if(l==r)
{
sum[x]=a[l];
return;//记得return!!!!!!!!!!!!!要不然会毒瘤
}
int mid=(l+r)/2;
build(x*2,l,mid);
build(x*2+1,mid+1,r);
push_up(x);
}
void add_update(int x,int l,int r,int lx,int ly,int k)
{
if(l>ly||r<lx) return;
if(l>=lx&&r<=ly)
{
sum[x]+=k*(r-l+1)%p;
add[x]+=k;
return;
}
push_down_multiply(x,l,r);
push_down_add(x,l,r);
int mid=(l+r)/2;
add_update(x*2,l,mid,lx,ly,k);
add_update(x*2+1,mid+1,r,lx,ly,k);
push_up(x);
return;
}
void multiply_update(int x,int l,int r,int lx,int ly,int k)
{
if(l>ly||r<lx) return;
if(l>=lx&&r<=ly)
{
sum[x]=(sum[x]*k)%p;
add[x]=(add[x]*k)%p;
multiply[x]=(multiply[x]*k)%p;
return;
}
push_down_multiply(x,l,r);
push_down_add(x,l,r);
int mid=(l+r)/2;
multiply_update(x*2,l,mid,lx,ly,k);
multiply_update(x*2+1,mid+1,r,lx,ly,k);
push_up(x);
return;
}
int query(int x,int l,int r,int lx,int ly)
{
if(l>ly||r<lx) return 0;
if(lx<=l&&r<=ly) return sum[x];
int mid=(l+r)/2;
push_down_multiply(x,l,r);//没区间替换不用下移lazy_tag
push_down_add(x,l,r);
int summ=query(x*2,l,mid,lx,ly)+query(x*2+1,mid+1,r,lx,ly);
return summ;
}
signed main()
{
fill(multiply,multiply+MAXN*4,1);//
cin>>n>>p;
for(int i=1;i<=n;i++)
{
cin>>a[i];
}
build(1,1,n);
cin>>m;
while(m--)
{
int z,x,y,k;
cin>>z;
if(z==1)
{
cin>>x>>y>>k;
multiply_update(1,1,n,x,y,k);
}
if(z==2)
{
cin>>x>>y>>k;
add_update(1,1,n,x,y,k);
}
if(z==3)
{
cin>>x>>y;
cout<<query(1,1,n,x,y)%p<<endl;
}
}
return 0;
}
结语
这是一道其实很毒瘤的题,但其实只要避过了那些易错的bug点,这道题就也只是模板一多了个tag而已。线段树的基本操作大概也就这么多,仅为个人理解,一定与书上不同,不喜勿喷
最后祝大家debug愉快【手动滑稽】