线段树基础+例题

一、问题——一定要开四倍空间啊啊啊

1)query过程中区间的情况讨论

在这里插入图片描述

T l , T r T_l,T_r Tl,Tr为线段树中的区间左右端点, l , r l,r l,r为查询的区间左右端点

  • [ l , r ] ⊃ [ T l , T r ] [l,r]\supset[T_l,T_r] [l,r][Tl,Tr],直接返回,不再进行往下搜索——由于这一步的剪枝可以时query操作控制在== O ( 4 ∗ l o g N ) O(4*logN) O(4logN)==的复杂度

  • [ l , r ] ∩ [ T l , T r ] ≠ ∅ [l,r]\cap[T_l,T_r]\neq\emptyset [l,r][Tl,Tr]=

    1. T l < = l < = T r < = r , T m i d = T l + T r > > 1 T_l<=l<=T_r<=r,T_{mid}=T_l+T_r>>1 Tl<=l<=Tr<=r,Tmid=Tl+Tr>>1

      l > T m i d l>T_{mid} l>Tmid,递归右区间 [ T m i d + 1 , T r ] [T_{mid+1},T_r] [Tmid+1,Tr]

      l < = T m i d l<=T_{mid} l<=Tmid,递归左区间、右区间

    2. l < = T l < = r < = T r l<=T_l<=r<=T_r l<=Tl<=r<=Tr

    3. T l < = l < = r < = T r , T m i d = T l + T r > > 1 T_l<=l<=r<=T_r,T_{mid}=T_l+T_r>>1 Tl<=l<=r<=Tr,Tmid=Tl+Tr>>1

      r < = T m i d r<=T_{mid} r<=Tmid,递归左区间

      l > T m i d l>T_{mid} l>Tmid,递归右区间

      其他情况,递归左区间、右区间

  • [ l , r ] ∩ [ T l , T r ] = ∅ [l,r]\cap[T_l,T_r]=\emptyset [l,r][Tl,Tr]=——不存在,因为query操作是从根结点向下遍历的,每次递归都只会涉及由交集的区间,所以不会产生交集为空集的情况

2)线段树数组tr应开到点个数的四倍大小

当线段树时满二叉树时,显然结点个数时 2 ∗ n − 1 2*n-1 2n1,若不是满二叉树,则结点编号有可能会超过 2 ∗ n − 1 2*n-1 2n1(因为最后一层不一定是按照完全二叉树的状态存点的,所以编号可能会很大,超过 2 ∗ n − 1 2*n-1 2n1

3)注意区间左右端点有可能会需要互换

(谁知道出题人会不会恶心人呢)

4)懒标记、pushdown操作

只有对区间进行加减可以利用差分的技巧维护线段树,就不用懒标记了

懒标记是一个从上往下的操作,类似于query操作的区间查找,复杂度在== O ( 4 ∗ l o g N ) O(4*logN) O(4logN)==

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TQVUhbs6-1652749238520)(C:\Users\19322\Pictures\Saved Pictures\懒标记.png)]

懒标记含义是若当前结点有懒标记,则对当前结点为根的子树中的每一个结点进行操作(add),且不包含根节点

我们维护的懒标记,是方便在querymodify过程中对儿子结点进行信息的更新

pushdown

  • query过程中,我们枚举线段树中对应的区间时需要将懒标记传到下方
  • modify过程中,我们也需要将之前modify所添加的懒标记传给下方结点——若不执行pushdown操作的话,不能对线段树中结点信息进行及时修改,若进行查询操作,则返回的仍是未修改的旧值

在这里插入图片描述

pushup

  • modify过程中,修改完信息之后需要将信息更新到父节点

5)权值线段树

在这里插入图片描述

6)主席树

二、例题:

最大数

题意:

单点修改,查询区间最大值

题解:

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+10;

int m,p;
struct Node{
    int l,r;
    int v;
}tr[N*4];

void pushup(int u){
    tr[u].v=max(tr[u<<1].v,tr[u<<1|1].v);
}

void build(int u,int l,int r){
    tr[u]={l,r};
    if(l==r) return;
    int mid=l+r>>1;
    build(u<<1,l,mid),build(u<<1|1,mid+1,r);
    pushup(u);
}

int query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r) return tr[u].v;
    
    int mid=tr[u].l+tr[u].r>>1;
    int v=0;
    if(l<=mid) v=query(u<<1,l,r);
    if(r>mid) v=max(v,query(u<<1|1,l,r));
    return v;
}

void modfiy(int u,int x,int v){
    if(tr[u].l==x&&tr[u].r==x) tr[u].v=v;
    else{
        int mid=tr[u].l+tr[u].r>>1;
        if(x<=mid) modfiy(u<<1,x,v);
        else modfiy(u<<1|1,x,v);
        pushup(u);
    }
}
int main(){
    int n=0,last=0;
    scanf("%d%d",&m,&p);
    build(1,1,m);
    
    char op[2];
    int x;
    while(m--){
        scanf("%s%d",op,&x);
        if(*op=='Q'){
            last=query(1,n-x+1,n);
            printf("%d\n",last);
        }
        else{
            modfiy(1,n+1,((ll)x+last)%p);
            n++;
        }
    }
    return 0;
}

你能回答这些问题吗

题意:

单点修改,查询一个区间中连续的最大连续子段和

题解:

区间最值有可能横跨两个子区间,所以需要维护前缀最值和后缀最值

在更新前缀后缀最值是需要知道区间和,所以还需要维护区间和

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=5e5+10;
struct P{
    int l,r;
    int sum,lmax,rmax,tmax;//区间和,前缀最值,后缀最值,区间最值
}tr[4*N];
int n,m;
int w[N];
void pushup(P& u,P& l,P& r){//由儿子结点更新而来,所以不能与之前的值取max
    u.sum=l.sum+r.sum;
    u.tmax=max(max(l.tmax,r.tmax),l.rmax+r.lmax);//区间最值为左儿子的区间最值、右儿子的区间最值、左右儿子相连接的部分三者取最大值
    u.lmax=max(l.sum+r.lmax,l.lmax);
    u.rmax=max(r.sum+l.rmax,r.rmax);
}
void pushup(int u){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
    if(l==r) tr[u]={l,r,w[l],w[l],w[l],w[l]};
    else{
        tr[u]={l,r};//注意非叶子节点也要传值
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
P query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
    int mid=tr[u].l+tr[u].r>>1;
    if(r<=mid) return query(u<<1,l,r);
    else if(l>mid) return query(u<<1|1,l,r);
    else{
        P root,left,right;
        left=query(u<<1,l,r);
        right=query(u<<1|1,l,r);
        pushup(root,left,right);
        return root;
    }
}
void modify(int u,int pos,int v){
    if(tr[u].l==pos&&tr[u].r==pos) tr[u]={pos,pos,v,v,v,v};//修改时修改结点的所有信息
    else{
        int mid=tr[u].l+tr[u].r>>1;
        if(pos<=mid) modify(u<<1,pos,v);
        else modify(u<<1|1,pos,v);
        pushup(u);
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];
    build(1,1,n);
    while(m--){
        int x,l,r; cin>>x>>l>>r;
        if(x==1){
            if(l>r) swap(l,r);
            auto t=query(1,l,r);
            cout<<t.tmax<<endl;
        }
        else modify(1,l,r);
    }
    return 0;
}

区间最大公约数

题意:

区间加减,查询区间的最大公约数

题解:

g c d ( a 1 , a 2 , . . . , a n ) = g c d ( a 1 , a 2 − a 1 , . . . , a n − a n − 1 ) gcd(a_1,a_2,...,a_n)=gcd(a_1,a_2-a_1,...,a_n-a_{n-1}) gcd(a1,a2,...,an)=gcd(a1,a2a1,...,anan1)

证明:

  • 证明: g c d ( a 1 , a 2 , . . . , a n ) < = g c d ( a 1 , a 2 − a 1 , . . . , a n − a n − 1 ) gcd(a_1,a_2,...,a_n)<=gcd(a_1,a_2-a_1,...,a_n-a_{n-1}) gcd(a1,a2,...,an)<=gcd(a1,a2a1,...,anan1)

d d d为左式的最大公约数,显然有 d ∣ a 1 , d ∣ a 2 d|a_1,d|a_2 da1,da2,所以有 d ∣ ( a 2 − a 1 ) d|(a_2-a_1) d(a2a1),所以d一定是右式的一个公约数

  • 证明: g c d ( a 1 , a 2 , . . . , a n ) > = g c d ( a 1 , a 2 − a 1 , . . . , a n − a n − 1 ) gcd(a_1,a_2,...,a_n)>=gcd(a_1,a_2-a_1,...,a_n-a_{n-1}) gcd(a1,a2,...,an)>=gcd(a1,a2a1,...,anan1)

d d d为右式的最大公约数,显然有 d ∣ a 1 , d ∣ ( a 2 − a 1 ) d|a_1,d|(a_2-a_1) da1,d(a2a1),所以有 d ∣ ( a 2 − a 1 + a 1 )   ⇒   d ∣ a 2 d|(a_2-a_1+a_1)\ \Rightarrow \ d|a_2 d(a2a1+a1)  da2,所以 d d d一定是左式的一个公约数

所以可以将查询区间最大公约数变为:

查询 a [ l ] a[l] a[l] b [ l + 1 ] b[l+1] b[l+1]~ b [ r ] b[r] b[r]的最大公约数(b为原序列a的差分数组)

所以只需要用线段树维护b的区间和以及区间最大公约数即可

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;//注意数据都要ll
const int N=5e5+10;
struct P{
    int l,r;
    ll sum,gcd;
}tr[4*N];
int n,m;
ll w[N];
ll gcd(ll a,ll b){
    if(!b) return a;
    return gcd(b,a%b);
}
void pushup(P &u,P &l,P &r){
    u.sum=l.sum+r.sum;
    u.gcd=gcd(l.gcd,r.gcd);
}
void pushup(int u){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
    if(l==r) tr[u]={l,r,w[l],w[r]};
    else{
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
P query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r) return tr[u];
    int mid=tr[u].l+tr[u].r>>1;
    if(r<=mid) return query(u<<1,l,r);
    else if(l>mid) return query(u<<1|1,l,r);
    else{
        P root,left,right;
        left=query(u<<1,l,r);
        right=query(u<<1|1,l,r);
        pushup(root,left,right);
        return root;
    }
}
void modify(int u,int pos,ll v){
    if(tr[u].l==pos&&tr[u].r==pos){
        v+=tr[u].sum;
        tr[u]={pos,pos,v,v};
    }
    else{
        int mid=tr[u].l+tr[u].r>>1;
        if(pos<=mid) modify(u<<1,pos,v);
        else modify(u<<1|1,pos,v);
        pushup(u);
    }
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];
    for(int i=n;i>=1;i--) w[i]-=w[i-1];
    build(1,1,n);
    while(m--){
        string s;int l,r;
        cin>>s>>l>>r;
        if(s=="C"){
            ll d; cin>>d;
            modify(1,l,d);
            if(r+1<=n) modify(1,r+1,-d);
        }
        else{
            P a=query(1,1,l),b={0,0,0,0};
            if(l+1<=r) b=query(1,l+1,r);
            cout<<abs(gcd(a.sum,b.gcd))<<endl;
        }
    }
    return 0;
}

亚特兰蒂斯(扫描线+离散化+线段树)

题意:

求n个可能有交集的矩形的面积和

题解:

从左往右枚举坐标x,用线段树维护坐标y,扫描线分块求每个小块的面积和

1)线段树叶子节点的是一段区间

2)我们每次取的是根节点的区间长度,所以不需要query操作

3)为何不需要懒标记?

  • query操作只查询根节点,不会用到pushdown
  • 由于扫描线的特殊性,每次区间查询都会自行抵消,所以modify操作可以不用进行pushdown
#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;

//存储线段的信息
struct P1{
    double x,y1,y2;
    int k;
    bool operator<(const P1 &t)const{
        return x<t.x;
    }
}g[N*2];

// 线段树的每个节点 保存的为线段,0号点为y[0]到y[1],以此类推
struct P2{
    int l,r;
    double len;//区间长度
    int cnt;//判断是否算作长度
}tr[8*N];

//离散化后的y坐标位置
vector<double>ve;
int n;
int find(double x){
    return lower_bound(ve.begin(),ve.end(),x)-ve.begin();
}
void pushup(int u){
    if(tr[u].cnt) tr[u].len=ve[tr[u].r+1]-ve[tr[u].l];//如果cnt>0,注意不能+1(区间长度)
    else if(tr[u].l!=tr[u].r){//如果不是叶子节点
        tr[u].len=tr[u<<1].len+tr[u<<1|1].len;
    }
    else tr[u].len=0;
}
void build(int u,int l,int r){
    if(l==r) tr[u]={l,r,0,0};
    else{
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
    }
}
void modify(int u,int l,int r,int v){
    if(tr[u].l>=l&&tr[u].r<=r){
        tr[u].cnt+=v;
        pushup(u);//u节点cnt的改变可能会导致len的改变,所以需要在这个地方pushup,在之前的题目中,u的值只与他的子孙节点有关,而本题中len可能由自身cnt的取值更新。
    }
    else{
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid) modify(u<<1,l,r,v);
        if(r>mid) modify(u<<1|1,l,r,v);
        pushup(u);
    }
}
int main(){
    int T=1;
    while(cin>>n,n){
        ve.clear();
        for(int i=0,j=0;i<n;i++){
            double x1,x2,y1,y2; cin>>x1>>y1>>x2>>y2;
            g[j++]={x1,y1,y2,1};
            g[j++]={x2,y1,y2,-1};
            ve.push_back(y1),ve.push_back(y2);//离散化存放y轴坐标
        }
        //离散化
        sort(ve.begin(),ve.end());
        ve.erase(unique(ve.begin(),ve.end()),ve.end());
        
        build(1,0,ve.size()-2);//每个叶子节点tr[i]代表一个区间[ve[i],vr[i+1]]
        
        //对x轴排序
        sort(g,g+2*n);
        
        double ans=0;
        for(int i=0;i<2*n;i++){
            if(i) ans+=(g[i].x-g[i-1].x)*tr[1].len;//判断i点之前的那个区间的面积
            modify(1,find(g[i].y1),find(g[i].y2)-1,g[i].k);//注意find(g[i].y2)-1,因为线段树的叶子节点代表的是一个区间
        }
        printf("Test case #%d\n",T++);
        printf("Total explored area: %.2lf\n\n",ans);
    }
    return 0;
}

一个简单的整数问题2

题意:

区间修改,查询区间和

题解:

1)线段树含懒标记
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct P{
    int l,r;
    ll sum,add;
}tr[4*N];
int n,m;
int w[N];
void pushdown(int u){
    auto &root=tr[u],&left=tr[u<<1],&right=tr[u<<1|1];
    if(root.add){
        left.sum+=(left.r-left.l+1)*root.add,left.add+=root.add;
        right.sum+=(right.r-right.l+1)*root.add,right.add+=root.add;
        root.add=0;
    }
}
void pushup(int u){
    tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void build(int u,int l,int r){
    if(l==r) tr[u]={l,r,w[l],0};
    else{
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int l,int r,int v){
    if(tr[u].l>=l&&tr[u].r<=r){
        tr[u].sum+=(ll)(tr[u].r-tr[u].l+1)*v;
        tr[u].add+=v;
    }
    else{
        pushdown(u);//向下修改时要pushdown
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid) modify(u<<1,l,r,v);
        if(r>mid) modify(u<<1|1,l,r,v);
        pushup(u);
    }
}
ll query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
    
    pushdown(u);//向下查询时要pushdown
    int mid=tr[u].l+tr[u].r>>1;
    ll d=0;
    if(l<=mid) d=query(u<<1,l,r);
    if(r>mid) d+=query(u<<1|1,l,r);
    return d;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];
    build(1,1,n);
    while(m--){
        string s;int l,r,d;
        cin>>s>>l>>r;
        if(s=="C"){
            cin>>d;
            modify(1,l,r,d);
        }
        else cout<<query(1,l,r)<<endl;
    }
    return 0;
}

2)线段树无懒标记——类似树状数组做法
#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct P{
    int l,r;
    ll sumb,suma;
}tr[4*N];
int n,m;
int w[N];

void pushup(P &root,P &left,P &right){
    root.sumb=left.sumb+right.sumb;
    root.suma=left.suma+right.suma;
}
void pushup(int u){
    pushup(tr[u],tr[u<<1],tr[u<<1|1]);
}
void build(int u,int l,int r){
    if(l==r) tr[u]={l,r,(ll)w[l]*l,w[l]};
    else{
        tr[u]={l,r};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}
void modify(int u,int pos,int v){
    if(tr[u].l==pos&&tr[u].r==pos){
        tr[u].sumb+=(ll)v*pos;
        tr[u].suma+=v;
    }
    else{
        int mid=tr[u].l+tr[u].r>>1;
        if(pos<=mid) modify(u<<1,pos,v);
        else modify(u<<1|1,pos,v);
        pushup(u);
    }
}
P query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r) return tr[u];

    int mid=tr[u].l+tr[u].r>>1;
    if(r<=mid) return query(u<<1,l,r);
    else if(l>mid) return query(u<<1|1,l,r);
    else{
        P root,left,right;
        left=query(u<<1,l,r);
        right=query(u<<1|1,l,r);
        pushup(root,left,right);
        return root;
    }
}
ll res(int x){
    return query(1,1,x).suma*(x+1)-query(1,1,x).sumb;
}
int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++) cin>>w[i];
    for(int i=n;i>=1;i--) w[i]-=w[i-1];
    build(1,1,n);
    while(m--){
        string s;int l,r,d;
        cin>>s>>l>>r;
        if(s=="C"){
            cin>>d;
            modify(1,l,d);
            if(r+1<=n) modify(1,r+1,-d);
        }
        else{
            ll ans=res(r);
            if(l-1>=1) ans-=res(l-1);
            cout<<ans<<endl;
        } 
    }
    return 0;
}

维护序列

题意:

区间修改(加、乘),查询区间和

题解:

在这里插入图片描述

如何计算modify之后的值?(计算顺序)
  • 若采用先加后乘的方式,即需要将每一个节点变成 ( x + a d d ) ∗ m u l (x+add)*mul (x+add)mul的形式,若之后加的值不能被 m u l mul mul整除,则不好维护成 ( x + a d d ) ∗ m u l (x+add)*mul (x+add)mul的形式

  • 若采用先乘后加的方式,即需要将每一个节点变成 x ∗ m u l + a d d x*mul+add xmul+add,若后面加一个数,可将式子变为 x ∗ m u l + ( a d d 1 + a d d 2 ) x*mul+(add^1+add^2) xmul+(add1+add2)的形式,若乘一个数可变为 x ∗ m u l 1 ∗ m u l 2 + a d d ∗ m u l 2 x*mul^1*mul^2+add*mul^2 xmul1mul2+addmul2的形式

#include<bits/stdc++.h>
#define endl '\n'
using namespace std;
typedef long long ll;
const int N=1e5+10;
struct P{
    int l,r;
    int sum,add,mul;
}tr[4*N];
int n,m,mod;
int w[N];

void work(P &t,int add,int mul){//更新
    t.sum=((ll)t.sum*mul+(ll)(t.r-t.l+1)*add)%mod;
    t.add=((ll)t.add*mul+add)%mod;
    t.mul=(ll)t.mul*mul%mod;
}

void pushdown(int u){//维护懒标记
    work(tr[u<<1],tr[u].add,tr[u].mul);
    work(tr[u<<1|1],tr[u].add,tr[u].mul);
    tr[u].add=0,tr[u].mul=1;
}

void pushup(int u){
    tr[u].sum=((ll)tr[u<<1].sum+tr[u<<1|1].sum)%mod;
}
void build(int u,int l,int r){
    if(l==r) tr[u]={l,r,w[l],0,1};//注意非叶子节点乘的懒标记也要记为1
    else{
        tr[u]={l,r,0,0,1};
        int mid=l+r>>1;
        build(u<<1,l,mid),build(u<<1|1,mid+1,r);
        pushup(u);
    }
}

void modify(int u,int l,int r,int add,int mul){//可以同时维护加和乘
    if(tr[u].l>=l&&tr[u].r<=r) work(tr[u],add,mul);
    else{
        pushdown(u);
        int mid=tr[u].l+tr[u].r>>1;
        if(l<=mid) modify(u<<1,l,r,add,mul);
        if(r>mid) modify(u<<1|1,l,r,add,mul);
        pushup(u);
    }
}

int query(int u,int l,int r){
    if(tr[u].l>=l&&tr[u].r<=r) return tr[u].sum;
    
    pushdown(u);
    int mid=tr[u].l+tr[u].r>>1;
    int res=0;
    if(l<=mid) res=query(u<<1,l,r);
    if(r>mid) res=(res+query(u<<1|1,l,r))%mod;
    return res;
}

int main(){
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    
    cin>>n>>mod;
    for(int i=1;i<=n;i++) cin>>w[i];
    build(1,1,n);
    
    cin>>m;
    while(m--){
        int f,l,r,d;
        cin>>f>>l>>r;
        if(f==3) cout<<query(1,l,r)<<endl;
        else if(f==1){
            cin>>d;
            modify(1,l,r,0,d);
        }
        else{
            cin>>d;
            modify(1,l,r,d,1);
        }
    }
    return 0;
}
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值