【概念引入】
用途:单点修改,区间查询 && 区间修改,单点查询 && 区间修改,区间查询 。
线段树是二叉树,每个节点对应的是序列的一段区间。根节点对应整个区间。
每个节点对应区间为 [ l , r ]。l=r时,是叶节点,没有左右儿子;
否则有两个儿子,令mid=(l+r) / 2,则左儿子对应区间为 [ l , mid ],右儿子 [ mid+1 , r ]。
线段树前h-1层是满二叉树,最后一层可能不满。(比如上图中只有7,没有8)
【应用实例】
一. 单点修改,区间查询(min)
1.初始建树
用 build(k,l,r) 表示建造区间 [l,r] 的线段树,k表示区间对应的标号。
若 l!=r,则新建一个(非叶子)节点,
它的两个子节点可以通过 build(k*2,l,mid) , build(k*2+1,mid+1,r) 递归得到。
它的区间最小值就是两个儿子的区间最小值中的较小者。
*注意* 线段树数组要开到4*n的级别(标号数可能超过2*n)。
//建树
void build(int k,int l,int r){ //标号+区间
if(l==r){ minn[k]=v; return; } //叶节点
int mid=(l+r)/2;
build(k*2,l,mid); build(k*2+1,mid+1,r); //递归左右子树
minn[k]=min(minn[k*2],minn[k*2+1]); //自下而上更新min
}
2.区间查询
初始访问根节点,然后从上到下寻找区间,进行匹配。
&& 可能出现的情况:
(1)当前区间与询问区间完全无交集。
(子节点肯定也无交集)直接返回一个不影响答案的极大值,退出递归。
(2)询问区间完全包含当前区间。
直接返回当前区间所维护的最小值。在返回过程中判断询问区间的最值是否需要更新。
(3)当前区间与询问区间有一部分交集。
递归左右儿子,直到满足(1)或(2)。
//区间查询
int query_min(int k,int l,int r,int x,int y){
if(y<l||x>r) return 极大值; //完全无交集
if(x<=l&&r<=y) return minn[k]; //完全包含(还要判断是否需要更新询问区间的最值)
int mid=(l+r)/2;
return min(query_min(k*2,l,mid,x,y),query_min(k*2+1,mid+1,r,x,y));
}
3.单点修改
更新 ai 的值时,需要对所有包含 i 这个位置的节点的值重新计算。
//单点修改
void change(int k,int l,int r,int x,int v){
//x是修改的点在原序列中的位置,v为修改后的值
if(r<x||l>x) return;
if(l==r&&l==x){ //当前节点为对应的[叶子节点]
minn[k]=v; return; //修改叶子节点
}
int mid=(l+r)/2;
change(k*2,l,mid,x,v);
change(k*2+1,mid+1,r,x,v);
minn[k]=min(minn[k*2],minn[k*2+1]);
}
4.总代码模板(求区间和)
//模板:长度为n的全0序列,m次操作:修改;求区间和。
#include <cstdio>
#include <algorithm>
using namespace std;
inline int read(){ //读入优化
char ch;
while((ch=getchar())<'0'&&ch>'9'); //排除非数字元素
int res=ch-48; //变成0~9
while((ch=getchar())>='0'&&ch<='9') //接下来的每一位
res=res*10+ch-48;
return res;
}
const int maxn=1e5+5;
int n,m; ll sum[maxn*4]; //求和数组
//建树(此题目初始为0,不需要建树这一步的操作)
void build(int k,int l,int r){ //标号+区间
if(l==r){ minn[k]=v; return; } //叶节点
int mid=(l+r)/2;
build(k*2,l,mid); build(k*2+1,mid+1,r); //递归左右子树
minn[k]=min(minn[k*2],minn[k*2+1]); //自下而上更新min
}
//单点修改
void change(int k,int l,int r,int x,int v){
//x是修改的点在原序列中的位置,v为修改后的值
if(r<x||l>x) return;
if(l==r&&l==x){ //当前节点为对应的[叶子节点]
minn[k]=v; return; //修改叶子节点
}
int mid=(l+r)/2;
change(k*2,l,mid,x,v);
change(k*2+1,mid+1,r,x,v);
sum[k]=sum[k*2]+sum[k*2+1];
}
//区间查询
ll query(int k,int l,int r,int x,int y){
if(y<l||x>r) return 0; //完全不重叠(对区间求和无影响)
if(x<=l&&r<=y) return sum[k]; //完全包含(直接加上)
int mid=(l+r)/2; ll res=0;
res=query(k*2,l,mid,x,y);
res+=query(k*2+1,mid+1,r,x,y);
return res;
}
int main(){
n=read(),m=read();
for(int i=1;i<=m;i++){
int steps=read(),x=read(),y=read();
if(steps==0) change(1,1,n,x,y);
else printf("%lld\n",query(1,1,n,x,y));
}
return 0;
}
标准代码(结构体版):
const int N=200004;
struct SegmentTree{
int l,r,sum; //管理区间+区间和
}tree[4*N];
void build(int l,int r,int k){ //【建树】
tree[k].l=l; tree[k].r=r; //建立标号与区间的关系
if(l==r){ scanf("%d",&tree[k].sum); return; } //叶子节点
int mid=(l+r)/2;
build(l,mid,k*2); build(mid+1,r,k*2+1);//右孩子
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;//此结点的sum=两孩子的sum之和
}
void query(int k){ //【单点查询】
if(tree[k].l==tree[k].r){ //当前结点的左右端点相等,是叶子节点
ans=tree[k].sum; return;
}
int m=(tree[k].l+tree[k].r)/2;
if(x<=m) query(k*2); //目标位置比中点靠左,就递归左孩子
else query(k*2+1); //反之,递归右孩子
}
void add(int k){ //【单点修改】
if(tree[k].l==tree[k].r){ //找到目标叶子的位置
tree[k].sum+=y; return;
}
int mid=(tree[k].l+tree[k].r)/2;
if(x<=mid) add(k*2);
else add(k*2+1);
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;//所有包含结点k的结点状态更新
}
void sum(int k){ //【区间查询求和】
if(tree[k].l>=x&&tree[k].r<=y){ //完全包含
ans+=tree[k].sum; return;
}
int mid=(tree[k].l+tree[k].r)/2;
if(x<=mid) sum(k*2); //区间部分重叠,递归左右
if(y>mid) sum(k*2+1);
}
二. 延迟标记(区间修改,单点查询)
思路:当我们需要用到这些子节点信息时,再进行更新。
把所有需要修改的值,先求和记录在根到叶子路径上的某节点处。
即标记【该节点曾经被修改,但其子节点尚未更新】,只记录了上层管理数组的变化。
等到询问时,再将所有上方的影响加和计算。(从上到下传递信息)
//延迟标记(区间修改,单点查询)
//单点查询
int query(int k,int l,int r,int p){ //p为所求的叶子节点标号
if(l==r) return addsum[k];
//↑↑已经遍历到达叶子结点,返回此时的叶子节点修改总和
int mid=(l+r)/2;
if(p<=mid) return query(k*2,l,mid,p)+addsum[k]; //在左子树
else return query(k*2+1,mid+1,r,p)+addsum[k];
//根节点到叶子节点p的路径上所有点的addsum之和就是此时的叶子节点对应值
}
//区间修改,处理出addsum数组(修改的最大整区间)
void modify(int k,int l,int r,int x,int y,int v){ //区间[x,y]内所有数加上v
if(l>y||r<x) return; //完全无交集
if(x<=l&&r<=y){ addsum[k]+=v; return; } //完全包含,打标记
int mid=l+r>>1; //‘>>’优先级低于‘+’
modify(k*2,l,mid,x,y,v);
modify(k*2+1,mid+1,r,x,y,v); //部分重叠,递归左右子树
}
三. 标记下传和标记永久化
1.下传操作
①当前节点的懒标记累积到子节点的懒标记中。
②修改子节点状态。在引例中,就是原状态+子节点区间点的个数*父节点传下来的懒标记。
这就有疑问了,既然父节点都把标记传下来了,为什么还要乘父节点的懒标记,乘自己的不行吗?
因为自己的标记可能是父节点多次传下来的累积,每次都乘自己的懒标记造成重复累积
③父节点懒标记清0。这个懒标记已经传下去了,不清0后面再用这个懒标记时会重复下传。
void down(int k){ //标记下传
tree[k*2].f+=tree[k].f;
tree[k*2+1].f+=tree[k].f;
tree[k*2].sum+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
tree[k*2+1].sum+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
tree[k].f=0;
}
完整的区间修改+标记下移操作:
void down(int k){ //标记下传
tree[k*2].f+=tree[k].f;
tree[k*2+1].f+=tree[k].f;
tree[k*2].sum+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
tree[k*2+1].sum+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
tree[k].f=0;
}
void add(int k){ //区间[a,b]上+x
if(tree[k].l>=a&&tree[k].r<=b){ //当前区间全部对要修改的区间有用
tree[k].sum+=(tree[k].r-tree[k].l+1)*x; //(r-1)+1区间点的总数*每处修改
tree[k].f+=x; return;
}
if(tree[k].f) down(k); //k处有延迟标记,延迟标记下传
int mid=(tree[k].l+tree[k].r)/2;
if(a<=mid) add(k*2);
if(b>mid) add(k*2+1);
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;//更改区间状态
}
此时的单点查询:
void ask(int k){ //单点查询
if(tree[k].l==tree[k].r){
ans=tree[k].sum; return;
}
if(tree[k].f) down(k); //懒标记下传,唯一需要更改的地方
int m=(tree[k].l+tree[k].r)/2;
if(x<=m) ask(k*2);
else ask(k*2+1);
}
此时的区间查询:
void sum(int k){ //区间查询
if(tree[k].l>=x&&tree[k].r<=y) {
ans+=tree[k].sum; return;
}
if(tree[k].f) down(k) //懒标记下传,唯一需要更改的地方
int m=(tree[k].l+tree[k].r)/2;
if(x<=m) sum(k*2);
if(y>m) sum(k*2+1);
}
2.标记永久化
https://www.cnblogs.com/Hallmeow/p/8004676.html
原理就是: 在路过该节点的时候把修改对答案的影响加上,来省去标记下放的过程。
实现起来: 线段树的每个节点维护 sum 与 add 两个标记。
四. 具体代码实现
【例题1】luogu p3372 (poj 3468)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【洛谷p3372】线段树模板题
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x; 2.求出某区间每一个数的和。*/
const int N=200004;
struct SegmentTree{
ll l,r,sum,f; //管理区间+区间和+标记f
}tree[4*N];
ll a,b,t,x,y,w[N],ans=0;
void build(ll l,ll r,ll k){ //【建树】
tree[k].l=l; tree[k].r=r; //建立标号与区间的关系
if(l==r){ tree[k].sum=w[l]; return; } //叶子节点
ll mid=(l+r)/2;
build(l,mid,k*2); build(mid+1,r,k*2+1);//右孩子
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;//此结点的sum=两孩子的sum之和
}
void down(ll k){ //标记下传
tree[k*2].f+=tree[k].f;
tree[k*2+1].f+=tree[k].f;
tree[k*2].sum+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
tree[k*2+1].sum+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
tree[k].f=0;
}
void adds(ll k){ //【区间修改】区间[a,b]上+x
if(tree[k].l>=a&&tree[k].r<=b){ //当前区间全部对要修改的区间有用
tree[k].sum+=(tree[k].r-tree[k].l+1)*t; //(r-1)+1区间点的总数*每处修改
tree[k].f+=t; return;
}
if(tree[k].f) down(k); //k处有延迟标记,延迟标记下传
ll mid=(tree[k].l+tree[k].r)/2;
if(a<=mid) adds(k*2);
if(b>mid) adds(k*2+1);
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;//更改区间状态
}
void sums(ll k){ //【区间查询求和】
if(tree[k].l>=x&&tree[k].r<=y){ //完全包含
ans+=tree[k].sum; return;
}
if(tree[k].f) down(k); //k处有延迟标记,延迟标记下传
ll mid=(tree[k].l+tree[k].r)/2;
if(x<=mid) sums(k*2); //区间部分重叠,递归左右
if(y>mid) sums(k*2+1);
}
int main(){
ll n,m; scanf("%lld%lld",&n,&m);
for(ll i=1;i<=n;i++) scanf("%lld",&w[i]);
build(1,n,1);
for(ll i=1;i<=m;i++){
char p; cin>>p;
ans=0; //←←←记得这个地方ans要清零!!!
if(p=='1'){ scanf("%lld%lld%lld",&a,&b,&t); adds(1); } //区间修改
else{
scanf("%lld%lld",&x,&y);//区间查询
sums(1); printf("%lld\n",ans);
}
}
return 0;
}
【例题2】SPOJ GSS3
#include<cstdio>
#include<iostream>
#define lc k<<1
#define rc k<<1|1
using namespace std;
/*【SPOJ-GSS3】最大连续子段和
在询问一段数列的连续最大自序列和的基础上,同时进行单点修改。*/
//维护【GSS】,【LEFT-GSS】,【RIGHT-GSS】和【整段和sum】
const int M=1e5+5,N=M<<2;
struct sgtment{
int sum,gss,lgss,rgss;
}tr[N];
int n,m,a[N];
void updata(int k){
tr[k].sum=tr[lc].sum+tr[rc].sum;
tr[k].lgss=max(tr[lc].lgss,tr[lc].sum+tr[rc].lgss);
tr[k].rgss=max(tr[rc].rgss,tr[rc].sum+tr[lc].rgss);
tr[k].gss=max(max(tr[lc].gss,tr[rc].gss),tr[lc].rgss+tr[rc].lgss);
}
void build(int k,int l,int r){
if(l==r){
tr[k].sum=tr[k].gss=tr[k].lgss=tr[k].rgss=a[l];
return;
}
int mid=l+r>>1;
build(lc,l,mid); build(rc,mid+1,r);
updata(k);
}
void change(int k,int l,int r,int pos,int val){
if(l==r){
tr[k].sum=tr[k].gss=tr[k].lgss=tr[k].rgss=val;
return;
}
int mid=l+r>>1;
if(pos<=mid) change(lc,l,mid,pos,val);
else change(rc,mid+1,r,pos,val);
updata(k);
}
sgtment query(int k,int l,int r,int x,int y){
if(l==x&&r==y) return tr[k];
int mid=l+r>>1;
if(y<=mid) return query(lc,l,mid,x,y);
else if(x>mid) return query(rc,mid+1,r,x,y);
else{
sgtment left,right,result;
left=query(lc,l,mid,x,mid);
right=query(rc,mid+1,r,mid+1,y);
result.sum=left.sum+right.sum;
result.lgss=max(left.lgss,left.sum+right.lgss);
result.rgss=max(right.rgss,right.sum+left.rgss);
result.gss=max(max(left.gss,right.gss),left.rgss+right.lgss);
return result; //返回结构体类型
}
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
build(1,1,n); scanf("%d",&m);
for(int i=1,opt,x,y;i<=m;i++){
scanf("%d%d%d",&opt,&x,&y);
if(opt) printf("%d\n",query(1,1,n,x,y).gss);
else change(1,1,n,x,y);
}
return 0;
}
【例题3】bzoj 1012 最大数
(1.单调队列做法)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【bzoj 1012】维护一个数列,要求提供以下两种操作:
1.查询操作。查询当前数列中末尾L个数中的最大的数,并输出。
2.插入操作。在序列后添加一个数,序列长度+1。
注意:初始时数列是空的,没有一个数。*/
int m,d,a[200001],t,max[200001],l=0,p;
char q[1];
int main(){ //单调队列
scanf("%d%d",&m,&d);
while(m--){
scanf("%s%d",q,&p);
if(q[0]=='A'){
a[++t]=(l+p)%d;
for(int i=t;i;i--)
if(max[i]<a[t]) max[i]=a[t];
else break;
}
else printf("%d\n",l=max[t-p+1]);
}
return 0;
}
(2.线段树做法)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【bzoj 1012】维护一个数列,要求提供以下两种操作:
1.查询操作。查询当前数列中末尾L个数中的最大的数,并输出。
2.插入操作。在序列后添加一个数,序列长度+1。
注意:初始时数列是空的,没有一个数。*/
const int inf=0x7fffffff;
int m,mod,last,cnt,x;
struct data{
int l,r,mx;
}t[800005];
void build(int k,int l,int r){
t[k].l=l; t[k].r=r; t[k].mx=-inf;
if(l==r) return;
int mid=(l+r)>>1;
build(k*2,l,mid);
build(k*2+1,mid+1,r);
}
int ask(int k,int x,int y){
int l=t[k].l,r=t[k].r;
if(l==x&&r==y) return t[k].mx;
int mid=(l+r)>>1;
if(y<=mid) return ask(k<<1,x,y);
else if(x>mid) return ask(k<<1|1,x,y);
else return max(ask(k<<1,x,mid),ask(k<<1|1,mid+1,y));
}
void insert(int k,int x,int y){
int l=t[k].l,r=t[k].r;
if(l==r){ t[k].mx=y; return; }
int mid=(l+r)>>1;
if(x<=mid) insert(k<<1,x,y);
else insert(k<<1|1,x,y);
t[k].mx=max(t[k<<1].mx,t[k<<1|1].mx);
}
int main(){
scanf("%d%d",&m,&mod);
build(1,1,m);
for(int i=1;i<=m;i++){
char ch[5]; scanf("%s",ch);
if(ch[0]=='A'){
cnt++; scanf("%d",&x);
x=(x+last)%mod; insert(1,cnt,x);
}
else{
scanf("%d",&x);
last=ask(1,cnt-x+1,cnt);
printf("%d\n",last);
}
}
return 0;
}
【例题4】bzoj 3211 花神游历各国
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
/*【bzoj 3211】一个正整数数列。
1是求一段数的和,2是对一段数中每个数都开方(下取整)。*/
/*【分析】开方操作的标记是不能合并的。
但每个数最大都是1e12,开一次方成了1e6,然后1e3…可以看出来下降的十分迅速。
用线段树记录最大值,每次递归左右儿子时,若最大值大于1,则递归。
每次修改区间时暴力修改,没几次这个线段树就基本不递归了。*/
ll a[100001]; int n,m;
struct data{
int l,r; ll sum;
bool flag;
}tr[400001];
void build(int k,int s,int t){
tr[k].l=s; tr[k].r=t;
if(s==t){
tr[k].sum=a[s]; //填入节点对应数值
if(a[s]==1||a[s]==0) tr[k].flag=1;
return; //↑↑不会再变化
}
int mid=(s+t)>>1;
build(k*2,s,mid); build(k*2+1,mid+1,t);
tr[k].sum=tr[k*2].sum+tr[k*2+1].sum;
tr[k].flag=tr[k*2].flag&tr[k*2+1].flag;
}
void change(int k,int x,int y){
if(tr[k].flag) return;
int l=tr[k].l,r=tr[k].r;
if(l==r){ //叶子节点修改
tr[k].sum=(ll)sqrt(tr[k].sum);
if(tr[k].sum==1||tr[k].sum==0) tr[k].flag=1;
return;
}
int mid=(l+r)>>1; //管理节点修改
if(mid>=y) change(k*2,x,y);
else if(mid<x) change(k*2+1,x,y);
else{
change(k*2,x,mid);
change(k*2+1,mid+1,y);
}
tr[k].sum=tr[k*2].sum+tr[k*2+1].sum;
tr[k].flag=tr[k*2].flag&tr[k*2+1].flag;
}
ll ask(int k,int x,int y){
int l=tr[k].l,r=tr[k].r;
if(l==x&&r==y) return tr[k].sum;
int mid=(l+r)>>1;
if(mid>=y) return ask(k*2,x,y);
else if(mid<x) return ask(k*2+1,x,y);
else return ask(k*2,x,mid)+ask(k*2+1,mid+1,y);
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
build(1,1,n); scanf("%d",&m);
for(int i=1;i<=m;i++){
int k,x,y; scanf("%d%d%d",&k,&x,&y);
if(x>y) swap(x,y);
if(k==2) change(1,x,y); //区间修改
else printf("%lld\n",ask(1,x,y)); //区间查询
}
return 0;
}
【例题5】bzoj 1798 维护序列 (luogu 3373)
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
//输入方面按照洛谷p3373
/*【bzoj 1798】序列a1,a2,…,aN。有如下三种操作形式:
(1)把数列中的一段数全部乘一个值; (2)把数列中的一段数全部加一个值;
(3)询问数列中的一段数的和。由于答案可能很大,你只需输出这个数模P的值。*/
#define MAXN 100005
struct tree_type{
int l,r;
LL tmp_1,tmp_2,sum;
}t[MAXN*4];
int n,m,p,opt,L,R,c,a[MAXN];
void build(int num,int l,int r){ //建树
t[num].l=l; t[num].r=r;
t[num].tmp_1=0; t[num].tmp_2=1;
if(l==r){ t[num].sum=a[l]%p; return; }
int mid=(l+r)/2; build(num*2,l,mid); build(num*2+1,mid+1,r);
t[num].sum=(t[num*2].sum+t[num*2+1].sum)%p;
return;
}
void push_down(int num){ //标记下移
if(t[num].tmp_2==1&&t[num].tmp_1==0) return; //+0并且*1,不影响
t[num*2].tmp_1=(t[num*2].tmp_1*t[num].tmp_2+t[num].tmp_1)%p; //加法标记
t[num*2+1].tmp_1=(t[num*2+1].tmp_1*t[num].tmp_2+t[num].tmp_1)%p;
t[num*2].tmp_2=(t[num*2].tmp_2*t[num].tmp_2)%p; //乘法标记
t[num*2+1].tmp_2=(t[num*2+1].tmp_2*t[num].tmp_2)%p;
t[num*2].sum=(t[num*2].sum*t[num].tmp_2+
(t[num*2].r-t[num*2].l+1)*t[num].tmp_1)%p;
t[num*2+1].sum=(t[num*2+1].sum*t[num].tmp_2+
(t[num*2+1].r-t[num*2+1].l+1)*t[num].tmp_1)%p; //标记记入总和中
t[num].tmp_1=0; t[num].tmp_2=1; return; //记得归零
}
void add(int num,int l,int r,int x){ //加法
if(t[num].l>r||t[num].r<l) return;
if(t[num].l>=l&&t[num].r<=r){
t[num].tmp_1=(t[num].tmp_1+x)%p; //标记
t[num].sum=(t[num].sum+(t[num].r-t[num].l+1)*x)%p;
return;
}
push_down(num); //标记下移
add(num*2,l,r,x); add(num*2+1,l,r,x);
t[num].sum=(t[num*2].sum+t[num*2+1].sum)%p;
return;
}
void multiply(int num,int l,int r,int x){ //乘法
if(t[num].l>r||t[num].r<l) return;
if(t[num].l>=l&&t[num].r<=r){
t[num].tmp_1=(t[num].tmp_1*x)%p;
t[num].tmp_2=(t[num].tmp_2*x)%p;
t[num].sum=(t[num].sum*x)%p;
return;
}
push_down(num); //标记下移
multiply(num*2,l,r,x); multiply(num*2+1,l,r,x);
t[num].sum=(t[num*2].sum+t[num*2+1].sum)%p;
return;
}
LL ask(int num,int l,int r){ //区间查询
if(t[num].l>r||t[num].r<l) return 0; //无重叠
if(t[num].l>=l&&t[num].r<=r) return t[num].sum%p; //全包含
push_down(num); //标记下移
return (ask(num*2,l,r)+ask(num*2+1,l,r))%p; //递归左右子树
}
int main(){
scanf("%d%d%d",&n,&m,&p);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
build(1,1,n);
for(int i=1;i<=m;i++){
scanf("%d",&opt);
if(opt==1){
scanf("%d%d%d",&L,&R,&c);
multiply(1,L,R,c); //乘法
}
if(opt==2){
scanf("%d%d%d",&L,&R,&c);
add(1,L,R,c); //加法
}
if (opt==3){
scanf("%d%d",&L,&R);
printf("%lld\n",ask(1,L,R)%p); //求和
}
}
return 0;
}
【例题6】Interval GCD
#include <cmath>
#include <iostream>
#include <cstdio>
#include <string>
#include <cstring>
#include <vector>
#include <algorithm>
#include <stack>
#include <queue>
using namespace std;
typedef long long LL;
typedef unsigned long long ull;
/*【Interval GCD】
1.C l r d 区间修改操作; 2.Q l r 求区间gcd。*/
//维护区间gcd,转化为单点修改。
//根据求gcd的辗转相减法,gcd(x,y,z)=gcd(x,y-x,z-y),可以推广到多个数。
//所以可以求原序列[差分]后的gcd,这样就可以做到单点修改。
//注意:用一个树状数组维护原序列。
const int Maxn=2000010;
LL n,m,a[Maxn],b[Maxn],c[Maxn];
void add(int x,LL y){ //树状数组建立
for(;x<=n;x+=(x&-x)) c[x]+=y;
}
LL get(int x){ //树状数组查询
LL ans=0;
for(;x;x-=(x&-x)) ans+=c[x];
return ans;
}
LL gcd(LL a,LL b){
if(!a) return b; //递归出口a=0
return gcd(b%a,a);
}
struct Seg{
int l,r,lc,rc; LL gcds;
}tr[Maxn<<1]; //tr范围:最大值*4
int len=0;
void build(int l,int r){
int t=++len; tr[t].l=l;tr[t].r=r;
if(l==r){ tr[t].gcds=b[l]; return; } //填入数值
int mid=l+r>>1; //记录编号,递归左右
tr[t].lc=len+1; build(l,mid);
tr[t].rc=len+1; build(mid+1,r);
tr[t].gcds=gcd(tr[tr[t].lc].gcds,tr[tr[t].rc].gcds);
}
void modify(int x,int p,LL d){ //单点修改
if(tr[x].l==tr[x].r){ tr[x].gcds+=d; return; }
int mid=tr[x].l+tr[x].r>>1;
int lc=tr[x].lc,rc=tr[x].rc;
if(p<=mid) modify(lc,p,d);
else modify(rc,p,d);
tr[x].gcds=gcd(tr[lc].gcds,tr[rc].gcds);
}
LL query(int x,int l,int r){
if(l>r) return 1;
if(tr[x].l==l&&tr[x].r==r) return tr[x].gcds;
int mid=tr[x].l+tr[x].r>>1,lc=tr[x].lc,rc=tr[x].rc;
if(r<=mid) return query(lc,l,r);
else if(l>mid) return query(rc,l,r);
else return gcd(query(lc,l,mid),query(rc,mid+1,r));
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%lld",&a[i]);
for(int i=1;i<n;i++) b[i]=a[i+1]-a[i];
add(1,a[1]); //差分数组由树状数组储存:便于单点修改
for(int i=2;i<=n;i++) add(i,b[i-1]);
build(1,n-1);
while(m--){
char op[2]; scanf("%s",op);
int l,r; LL d; scanf("%d%d",&l,&r);
if(op[0]=='Q') printf("%lld\n",abs(gcd(get(l),query(1,l,r-1))));
else{
scanf("%lld",&d); add(l,d);
if(l>1) modify(1,l-1,d);
if(r<n) add(r+1,-d),modify(1,r,-d);
}
}
return 0;
}
——时间划过风的轨迹,那个少年,还在等你。