线段树(区间问题)

目录

单点操作

Lazy标记

       1.集中更新和动态统计子序列中的数据。

       2.计算可见线段

       3.互不相交线段的更新与维护。


单点操作

#include <iostream>
using namespace std;

const int MAX_N=10010;
int s[4*MAX_N];

void up(int p){
    s[p]=s[p*2]+s[p*2+1];
}

void modify(int p,int l,int r,int x,int v){
    //节点x上的权值增加v;
	if(l==r){
		s[p]+=v;
        return ;
    }
    int mid=(l+r)/2;
    if(x<=mid){
        modify(p*2,l,mid,x,v);
    }else{
        modify(p*2+1,mid+1,r,x,v);
    }
    up(p);
}

int query(int p,int l,int r,int x,int y){
    //p,l,r为当前更新到的节点,左右端点,x,y为要查询的区间
    if(x<=l&&r<=y){
        return s[p];
    }
    int mid=(l+r)/2,res=0;
    if(x<=mid){
        res+=query(p*2,l,mid,x,y);
    }
    if(y>mid){
        res+=query(p*2+1,mid+1,r,x,y);
    }
    return res;
}

int main() {
	int n;
    cin>>n;
    for(int i=1;i<=n;i++){
        int d;
        cin>>d;
        modify(1,1,n,i,d);
    }
    int q;
    cin>>q;
    while(q--){
        int d,x,y;
        cin>>d>>x>>y;
        if(d==0){       //x处增加y
            modify(1,1,n,x,y);
        }else{//询问x-y的和
            cout<<query(1,1,n,x,y)<<endl;
        }
    }
    return 0;
}

Lazy标记

1.集中更新和动态统计子序列中的数据。

(1)子序列增加或者减少一个值

(2)子序列求和

例:POJ3468 

#include<cstdio>
#include<cstring>

using namespace std;

const int maxn=1e5+10;

typedef long long ll;

struct node{
    ll mark,sum;        //线段树,其中节点i的数和为sum,懒惰标记为mark
}tree[maxn*4];

int x[maxn];            //初始值序列。
int n,m;                //数字个数,操作次数。

void update(int l,int r,int i){                     //标记法维护线段树(根为i,对应区间为[l,r])
    if(!tree[i].mark) return;                       //若结点i未被标记,则退出,否则左右儿子对应的区间应该被完全覆盖,分别计算左右子区间的数和,并将节点i的标记下传给左右儿子。
    int mid=l+r>>1;
    tree[i<<1].sum+=tree[i].mark*(ll)(mid-l+1);
    tree[i<<1|1].sum+=tree[i].mark*(ll)(r-mid);
    tree[i<<1].mark+=tree[i].mark;
    tree[i<<1|1].mark+=tree[i].mark;
    tree[i].mark=0;                                 //撤去标记 
}

ll query(int tl,int tr,int l,int r,int i){          //计算线段树根为i,对应区间为[l,r]内子区间[tl,tr]的数字和。
    if(tl>r||tr<l) return 0;
    if(tl<=l&&r<=tr) return tree[i].sum;            //标记法维护
    update(l,r,i);
    int mid=l+r>>1;
    return query(tl,tr,l,mid,i<<1)+query(tl,tr,mid+1,r,i<<1|1);
}

void add_value(int tl,int tr,int l,int r,int i,int val){    //线段树根为i,对应区间为[l,r]内子区间[tl,tr]每个数+val
    if(tl>r||tr<l) return ;
    if(tl<=l&&r<=tr){
        tree[i].sum+=val*(ll)(r-l+1);
        tree[i].mark+=val;
        return;
    }
    update(l,r,i);
    int mid=(l+r)>>1;
    add_value(tl,tr,l,mid,i<<1,val);
    add_value(tl,tr,mid+1,r,i<<1|1,val);
    tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum;
}

void build_tree(int l,int r,int i){                          //构建以i为根,对应区间[1,n]的线段树。
    if(l==r){
        tree[i].sum=x[l];
        return;
    }
    int mid=l+r>>1;
    build_tree(l,mid,i<<1);
    build_tree(mid+1,r,i<<1|1);
    tree[i].sum=tree[i<<1].sum+tree[i<<1|1].sum;
}

int main()
{
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&x[i]);
    }
    memset(tree,0,sizeof tree);
    build_tree(1,n,1);
    while(m--){
        char ch;
        int l,r,v;
        scanf(" %c",&ch);
        if(ch=='Q'){                                    //询问[l,r]的区间和。
            scanf("%d%d",&l,&r);
            ll ans=query(l,r,1,n,1);
            printf("%lld\n",ans);
        }else{                                          //[l,r]中每个数均加上v
            scanf("%d%d%d",&l,&r,&v);
            add_value(l,r,1,n,1,v);
        }
    }
    return 0;
}

2.计算可见线段

被插入的线段按照先后顺序,后面覆盖前面的,计算最终线段并区间中的可见线段数。

注意可能用到离散化。

例POJ 2528

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int maxn=1e4+10;

//线段i表示颜色i
bool tab[maxn];             //颜色码k被使用的标志是tab[k]
int l[maxn],r[maxn],x[maxn*3],num[maxn*3],tree[maxn*12];
/*
对于第i张海报来说,不大于左边界的不同坐标数为l[i],不大于右边界的不同坐标数为r[i],左边界坐标为x[3*i-2],右边界坐标为x[3*i-1];
中间位置坐标为x[3*i],x[]排序后x[1...j]中不重复的坐标数为num[j];
线段树中的节点k标记为tree[k],即代表区间的颜色码。
*/

void update(int i){          //标记法维护线段树。
    if(!tree[i]) return ;
    tree[i<<1]=tree[i<<1|1]=tree[i];
    tree[i]=0;
}

void change(int tl,int tr,int l,int r,int i,int co){
    if(tr<l||tl>r) return;
    if(tl<=l&&r<=tr){
        tree[i]=co;
        return ;
    }
    update(i);
    int mid=l+r>>1;
    change(tl,tr,l,mid,i<<1,co);
    change(tl,tr,mid+1,r,i<<1|1,co);
}

int query(int l,int r,int i){
    if(tree[i]){
        if(!tab[tree[i]]){
            tab[tree[i]]=1;
            return 1;
        }
        return 0;
    }
    if(l==r) return 0;      //当前点未覆盖,则返回0
    int mid=l+r>>1;
    return query(l,mid,i<<1)+query(mid+1,r,i<<1|1);
}

int main()
{
    int T;
    scanf("%d",&T);
    while(T--){
        int n,cnt=0;
        scanf("%d",&n);
        for(int i=1;i<=n;i++){
            scanf("%d%d",&l[i],&r[i]);
            x[++cnt]=l[i],x[++cnt]=r[i],x[++cnt]=(l[i]+r[i])/2;
            //因为是区间,所以直接离散化的话可能导致可看见的区间数减少。所以添加一个中间值进行离散化。
        }
        sort(x+1,x+1+cnt);
        cnt=unique(x+1,x+cnt+1)-(x+1);
        memset(tree,0,sizeof tree);
        for(int i=1;i<=n;i++){
            int ll=lower_bound(x+1,x+1+cnt,l[i])-x;
            int rr=lower_bound(x+1,x+1+cnt,r[i])-x;
            change(ll,rr,1,cnt,1,i);
        }
        memset(tab,0,sizeof tab);
        int ans=query(1,3*n,1);
        printf("%d\n",ans);
    }
    return 0;
}

3.互不相交线段的更新与维护。

每次给出插入线段的长度l,若线段树中存在空位置数不小于l的子区间,则该线段插入(一般规定选择区间的优先级),可使树中满的线段是互不相交的,对删除操作,若线段树中存在被删线段的“满区间”,则该线段可被删除。

节点的懒惰标记一般包括:

(1)对应子区间的占据情况mark:分“全满”,“全空”,“部分占据”三部分。

(2)对应子序列的最长空区间lm,pos,即pos位置开始,长度为lm的区间为最长空区间。

(3)左端 最长空区间的长度ls,和右端最长空区间的长度rs,即跨越左右子区间的最长空区间的长度为ls+rs;

例如:POJ 3667

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;

const int maxn=5e4+10;

struct node{
    int ls,rs,ms,pos,mark;
}tree[maxn*4];

/*线段树,其中节点i的懒惰标记:对应区间为tree[i].mark(0为未定,1为全空,2为全满)
 *左端空区间的长度为tree[i].ls,右端区间的长度为tree[i].rs,最长子区间的长度为
 *tree[i].ms,开始区间为tree[i].pos.
 */

int n,m;        //房间数,请求数.

void build_tree(int l,int r,int i){
    tree[i].ls=tree[i].rs=tree[i].ms=r-l+1;
    tree[i].pos=l;
    if(l==r) return;
    int mid=l+r>>1;
    build_tree(l,mid,i<<1);
    build_tree(mid+1,r,i<<1|1);
}

bool all_space(int l,int r,int i){      //若结点i对应的区间[l,r]全空则返回1,否则返回0;
    if(tree[i].ls==r-l+1) return 1;
    return 0;
}

void update(int l,int r,int i){
    if(!tree[i].mark) return;   //若结点i的区间未定,则返回.
    if(tree[i].mark==1){        //节点i全空,则r-l+1个空位置均分给左右子树,左右子数设全空状态.
        int len=r-l+1;
        tree[i<<1].ls=tree[i<<1].rs=tree[i<<1].ms=len+1>>1;
        tree[i<<1].pos=l;
        tree[i<<1|1].ls=tree[i<<1|1].rs=tree[i<<1|1].ms=len>>1;
        tree[i<<1|1].pos=(l+r>>1)+1;
        tree[i<<1].mark=tree[i<<1|1].mark=1;
    }else{                       //全满,0个空位置传给左右子树,左右子数设为全满状态.
        tree[i<<1].ls=tree[i<<1].rs=tree[i<<1].ms=0;
        tree[i<<1].pos=l;
        tree[i<<1|1].ls=tree[i<<1|1].rs=tree[i<<1|1].ms=0;
        tree[i<<1|1].pos=(l+r>>1)+1;
        tree[i<<1].mark=tree[i<<1|1].mark=2;
    }
    tree[i].mark=0;             //设节点i的状态待定.
}

int query(int d,int l,int r,int i){     //若线段树(根为i,对应区间为[l,r])存在长度为d的空区间,则返回其左指针,否则返回0.
    update(l,r,i);                      //通过标记法维护线段树.
    if(tree[i].ms<d) return 0;
    if(tree[i].ms==d) return tree[i].pos;
    int mid=l+r>>1;
    if(tree[i<<1].ms>=d)                 //若左子树的空位不小于d,则递归左子树.
        return query(d,l,mid,i<<1);
    if(tree[i<<1].rs+tree[i<<1|1].ls>=d)    //若跨越中间点的空子区间的不小于d,则返回空子区间的左指针,否则递归右子树.
        return mid-tree[i<<1].rs+1;
    return query(d,mid+1,r,i<<1|1);
}

void change(int tl,int tr,int l,int r,int i,bool flag){
    //线段树(根为i,对应区间为[l,r])中插入或删除线段[tl,tr],插删标志为flag
    if(tl>r||tr<l) return;
    if(tl<=l&&r<=tr){       //线段[tl,tr]完全覆盖区间[l,r].
        if(flag){           //若为插入操作,则节点i代表的区间全满.
            tree[i].ls=tree[i].rs=tree[i].ms=0;
            tree[i].pos=l;
            tree[i].mark=2;
        }else{
            tree[i].ls=tree[i].rs=tree[i].ms=r-l+1;
            tree[i].pos=l;
            tree[i].mark=1;
        }
        return;
    }
    update(l,r,i);
    int mid=l+r>>1;
    change(tl,tr,l,mid,i<<1,flag);
    change(tl,tr,mid+1,r,i<<1|1,flag);
    //回溯往上进行修改.
    tree[i].ls=tree[i<<1].ls;
    if(all_space(l,mid,i<<1))              //如果左子树全为空
        tree[i].ls+=tree[i<<1|1].ls;
    tree[i].rs=tree[i<<1|1].rs;
    if(all_space(mid+1,r,i<<1|1))           //如果右子树全为空
        tree[i].rs+=tree[i<<1].rs;
    //节点i所代表的区间的最长空区间的长度.
    tree[i].ms=max(tree[i<<1].rs+tree[i<<1|1].ls,max(tree[i<<1].ms,tree[i<<1|1].ms));
    if(tree[i].ms==tree[i<<1].ms)           //最长空子区间在左子树.
        tree[i].pos=tree[i<<1].pos;
    else if(tree[i].ms==tree[i<<1].rs+tree[i<<1|1].ls)
        tree[i].pos=mid-tree[i<<1].rs+1;
    else tree[i].pos=tree[i<<1|1].pos;
}

int main()
{
    scanf("%d%d",&n,&m);
    memset(tree,0,sizeof tree);
    build_tree(1,n,1);
    while(m--){
        int kind;
        scanf("%d",&kind);
        if(kind==1){                //入住请求
            int d;
            scanf("%d",&d);         //输入入住的房间数
            int ans=query(d,1,n,1); //检查线段树(根为i,对应区间为[l,r])是否存在长度为d的空区间,存在返回其左指针,否则返回0.
            printf("%d\n",ans);
            if(ans){
                change(ans,ans+d-1,1,n,1,1);
            }
        }else{
            int x,d;
            scanf("%d%d",&x,&d);    //x开始的d间房间退房.
            change(x,x+d-1,1,n,1,0);
        }
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值