【暖*墟】 #初级数据结构# 线段树

【概念引入】

用途:单点修改,区间查询 && 区间修改,单点查询 && 区间修改,区间查询 。

线段树是二叉树,每个节点对应的是序列的一段区间。根节点对应整个区间。

每个节点对应区间为 [ 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;
}

                                               ——时间划过风的轨迹,那个少年,还在等你。

猜你喜欢

转载自blog.csdn.net/flora715/article/details/81865167