写的很乱,可能只有自己看得懂ORZ
对于区间修改,单点查找问题以及单点修改,区间查找问题使用树状数组可以在O(logN)的时间复杂度上很好解决,但是对于区间修改,区间查找时,就不好解决了。因此使用到了一种叫做线段数的数据结构,类似于堆(结点的编号为n,则左孩子编号为2n,右孩子的编号为2n+1)。原数组为A[1~10]。线段树的结构如下图,图片来自洛谷。
根据上图可以看到是根节点表示的是t[1]表示A[1]+A[2]+…+A[10]
根节点的左节点表示t[2]=A[1]+…+A[mid] (mid = (1+10)/2)
根节点的右节点表示t[3]=A[mid+1]+…+A[10]
因此,当为了求区间(nl,nr)内的数字和时,可以从根结点开始查找。
区间累加的步骤如下:
如果当前结点包含的区间在(nl,nr)之间,则可以直接返回该结点的值。否则进行拆分,递归找他的左右孩子。可以很轻松的写出递归代码。
单点修改的步骤如下:
如果需要单点修改则只能从根节点一直遍历到该点对应的叶子结点,修改这条路径上的所有结点。
区间修改
但是如果需要区间修改则复杂度会不止O(logN)。
这时如果给树加上一个懒标记(tag数组,每个结点都有一个懒标记的值)就可以很好的解决区间修改问题。下面解释懒标记的作用。
- 示例
- 第一步:当我们需要给A[1~10] (nl=1,nr=10)增加2的时候,我们从根节点(结点编号1)开始遍历,根节点的范围被(nl,nr)包含了,这时直接c[1]增加2*10(因为c[1]表示的是A[1-10]的和),再令tag[1]加2,表示了该结点范围的数字都需要增加2。结束遍历。如下图。
- 第二步:当需要给A[1~6] (nl=1,nr=6)增加3的时候,因为nl,nr无法包含结点1的范围[1-10],这时就需要从结点1向结点2和3遍历。这时候进行的操作是,先把tag[1]的结点传递(乘以下一个结点元素的个数)给结点2(1-5)和结点3(6-10),再把tag[1]置为0。再进行上面描述的第一步判断。对于结点2,因为[1-6]包含了结点2的范围[1-5].因此,直接给结点2再增加,并且令tag[2]=3即可,用tag[2]先存着下面结点需要加的数。跳出对结点2的递归。在判断结点3的过程中,则需要一直遍历到最下面的6结点才能让其被(nl,nr)包含。在递归遍历两个孩子结点结束的时候,需要返回两个孩子结点的和增加到父结点上,即t[1] = t[2]+t[3] (此时tag[1]已经被清零了)进行回溯。
- tag结点的含义:tag[1]=4 表示,该节点(1)包含范围的所有元素[1-10]都需要加4。
区间查找
解决了区间修改的问题后,区间查找的步骤只需要稍微修改。
在遍历到的结点不能被(nl,nr)包含时,即需要分割时,先把懒标记传递下去,再进行递归孩子即可。
例题1
P3372 【模板】线段树 1
AC代码如下
#include<cstdio>
#define ll long long
const int maxn = 300005; //树的结点要至少多开一倍
//用一个存结点范围和值的结构体似乎也不错!
ll t[maxn]; //线段树
ll tag[maxn]; //为了加快区间插入的的速度,引入懒标记
void f(int node,int left,int right,ll v){
//node区间的每个元素都加v
tag[node]+=v;
t[node]+=v*(right-left+1);
return;
}
void push_down(int node,int left,int right){
//把node的懒标记传递到子节点
int mid = (left+right)>>1;
f(node*2 ,left ,mid ,tag[node]);
f(node*2+1,mid+1,right,tag[node]);
tag[node] = 0;
return;
}
void update(int node,int left,int right,int nl,int nr,ll v){ //区间修改[nl~nr]都加v
//node:当前结点;left,right结点的取值范围;nl,nr原数组的范围
if(nl<=left&&right<=nr){
t[node] += v*(right-left+1);
tag[node] += v;
return;
}
push_down(node,left,right); //把node结点的懒标记传递下去
int mid = (left+right)>>1;
if(nl<=mid) update(node*2,left,mid,nl,nr,v);
if(mid+1<=nr) update(node*2+1,mid+1,right,nl,nr,v);
t[node] = t[node*2]+t[node*2+1];//回溯
return;
}
ll getsum(int node,int left,int right,int nl,int nr){ //区间求和
if(nl<=left&&right<=nr)
return t[node]; //该结点范围被需要求和的区间覆盖
int mid=(left+right)>>1;
push_down(node,left,right); //把懒标记传递下去
ll res=0;
if(nl<=mid)
res+=getsum(node*2,left,mid,nl,nr);
if(mid+1<=nr)
res+=getsum(node*2+1,mid+1,right,nl,nr);
return res;
}
int n,m;//数字个数,操作个数
int main(){
scanf("%d %d",&n,&m);
int o,x,y;
ll k;
for(int i=1;i<=n;i++){
scanf("%lld",&k);
update(1,1,n,i,i,k);//nums[i]加上k
}
for(int i=1;i<=m;i++){
scanf("%d",&o);
if(o==1){
scanf("%d %d %lld",&x,&y,&k);//[x~y]内的加上k
update(1,1,n,x,y,k);
}else{
scanf("%d %d",&x,&y);
printf("%lld\n",getsum(1,1,n,x,y));
}
}
return 0;
}
例题2
P3373 【模板】线段树 2
观察题目,发现只需要设置两个懒标记表示乘法和加法即可。之后要规定一个优先级,规定方法如下:
①加法优先,即规定好segtree[root2].value=((segtree[root2].value+segtree[root].add)segtree[root].mul)%p,问题是这样的话非常不容易进行更新操作,假如改变一下add的数值,mul也要联动变成奇奇怪怪的分数小数损失精度,我们内心是很拒绝的;
②乘法优先,即规定好segtree[root2].value=(segtree[root2].valuesegtree[root].mul+segtree[root].add*(本区间长度))%p,这样的话假如改变add的数值就只改变add,改变mul的时候把add也对应的乘一下就可以了,没有精度损失,看起来很不错。 —zhuwanman
因此选择乘法优先。按照线段树例题1的例子可以稍微修改即可。
AC代码。
#include<cstdio>
#define ll long long
const int maxn = 300005; //树的结点要至少多开一倍
//用一个存结点范围和值的结构体似乎也不错!
ll t[maxn]; //线段树
ll tag_mul[maxn]; //乘懒标记
ll tag_add[maxn]; //加懒标记
int p;
//加法优先,即规定好segtree[root*2].value=((segtree[root*2].value+segtree[root].add)*segtree[root].mul)%p,
//问题是这样的话非常不容易进行更新操作,假如改变一下add的数值,mul也要联动变成奇奇怪怪的分数小数损失精度,我们内心是很拒绝的;
//乘法优先,即规定好segtree[root*2].value=(segtree[root*2].value*segtree[root].mul+segtree[root].add*(本区间长度))%p,
//这样的话假如改变add的数值就只改变add,改变mul的时候把add也对应的乘一下就可以了,没有精度损失,看起来很不错。
//https://www.luogu.com.cn/problemnew/solution/P3373根据分析选择先乘再加
void f(int node,int left,int right,ll v_mul,ll v_add){ //接受乘和加懒标记
//node区间的每个元素都先乘以用乘懒标记再用加懒标记
tag_add[node] = (tag_add[node] * v_mul+v_add)%p;
tag_mul[node] = (tag_mul[node] * v_mul)%p;
t[node] = (v_mul*t[node] +v_add*(right-left+1))%p;
return;
}
void push_down(int node,int left,int right){
//把node的懒标记传递到子节点
int mid = (left+right)>>1;
f(node*2 ,left ,mid ,tag_mul[node],tag_add[node]);
f(node*2+1,mid+1,right,tag_mul[node],tag_add[node]);
tag_add[node] = 0;
tag_mul[node] = 1;
return;
}
void update_add(int node,int left,int right,int nl,int nr,ll v){ //区间修改[nl~nr]都加v
//node:当前结点;left,right结点的取值范围;nl,nr原数组的范围
if(nl<=left&&right<=nr){
t[node] =(t[node] + v*(right-left+1))%p;
tag_add[node] += v;
return;
}
push_down(node,left,right); //把node结点的懒标记传递下去
int mid = (left+right)>>1;
if(nl<=mid) update_add(node*2,left,mid,nl,nr,v);
if(mid+1<=nr) update_add(node*2+1,mid+1,right,nl,nr,v);
t[node] = (t[node*2]+t[node*2+1])%p;//回溯
return;
}
void update_mul(int node,int left,int right,int nl,int nr,ll v){ //区间修改[nl~nr]都乘以v
//node:当前结点;left,right结点的取值范围;nl,nr原数组的范围
if(nl<=left&&right<=nr){
t[node] =(t[node]*v)%p;
tag_add[node] *= v;
tag_mul[node] *= v;
return;
}
push_down(node,left,right); //把node结点的懒标记传递下去
int mid = (left+right)>>1;
if(nl<=mid) update_mul(node*2,left,mid,nl,nr,v);
if(mid+1<=nr) update_mul(node*2+1,mid+1,right,nl,nr,v);
t[node] = (t[node*2]+t[node*2+1])%p;//回溯
return;
}
ll getsum(int node,int left,int right,int nl,int nr){ //区间求和
if(nl<=left&&right<=nr)
return t[node]; //该结点范围被需要求和的区间覆盖
int mid=(left+right)>>1;
push_down(node,left,right); //把懒标记传递下去
ll res=0;
if(nl<=mid)
res+=getsum(node*2,left,mid,nl,nr);
if(mid+1<=nr)
res+=getsum(node*2+1,mid+1,right,nl,nr);
return res%p;
}
int n,m;//数字个数,操作个数
int main(){
for(int i=1;i<=maxn;i++)
tag_mul[i] = 1;
scanf("%d %d %d",&n,&m,&p);
int o,x,y;
ll k;
for(int i=1;i<=n;i++){
scanf("%lld",&k);
update_add(1,1,n,i,i,k);//nums[i]加上k
}
for(int i=1;i<=m;i++){
scanf("%d",&o);
if(o==1){
scanf("%d %d %lld",&x,&y,&k);//[x~y]内的加上k
update_mul(1,1,n,x,y,k);
}else if(o==2){
scanf("%d %d %lld",&x,&y,&k);//[x~y]内的加上k
update_add(1,1,n,x,y,k);
}else{
scanf("%d %d",&x,&y);
printf("%lld\n",getsum(1,1,n,x,y));
}
}
return 0;
}