RMQ、树状数组、线段树

1、RMQ(区间最大值、区间最小值)

RMQ(Range Minimum/Maximum Query),即区间最值查询

O(nlogn)时间内进行预处理,然后在O(1)时间内回答每个查询

①预处理

设A[i]是要求区间最值的数列,F[i,j]表示从第i个数起连续2^j个数中的最大值。(DP的状态)

例如:

A数列为:3 2 4 5 6 8 1 2 9 7

F[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。

同理F[1,1]=max(3,2)=3 , F[1,2]=max(3,2,4,5)=5 , F[1,3]=max(3,2,4,5,6,8,1,2)=8;

并且我们可以容易的看出F[i,0]就等于A[i]。(DP的初始值)

我们把F[i,j]平均分成两段(因为F[i,j]包括2^j个数,偶数个数字;

从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))

于是我们得到了状态转移方程F[i,j]=max(F[i,j-1],F[i+2^(j-1),j-1])


F[i,0]=A[i]

……

F[1,1]=max( F[1,0], F[2,0] )

F[2,1]=max( F[2,0], F[3,0] )

F[9,1]=max( F[9,0], F[10,0] )

……

F[1,2]=max( F[1,1], F[3,1] )

……..

F[1,3]=max( F[1,2], F[5,2] )

②查询

假如我们需要查询的区间为(i,j),那么我们需要找到覆盖这个闭区间(左边界取i,右边界取j)的最小幂

(可以重复,比如查询1,2,3,4,5,我们可以查询1234和2345)

因为这个区间的长度为j-i+1,所以我们可以取k=log2(j-i+1)

则有:RMQ(i,j)=max{F[i,k],F[j-2^k+1,k]} (两段F取覆盖要求的区间)

举例说明,要求区间[1,5]的最大值,k=log2(5-1+1)=2,即求max(F[1,2],F[5-2^2+1,2])=max(F[1,2],F[2,2])

举例说明,要求区间[1,7]的最大值,k=log2(7-1+1)=2,即求max(F[1,2],F[7-2^2+1,2])=max(F[1,2],F[3,2])

举例说明,要求区间[1,8]的最大值,k=log2(8-1+1)=3,即求max(F[1,3],F[8-2^3+1,3])=max(F[1,3],F[1,3])

RMQ(1,10) --> k=log(10-1+1)=3

RMQ=max(F[1,3],F[10-2^3+1,3])

RMQ=max(F[ s ,k] , F[ - 2<<k+1,k])

③首先是预处理,如上图所示,F[3,3]为边界,开始下标为3,长度为2^3;其次再进行查询,查询是判断哪两个子区间合并可以得到要查询的区间,如上公式所示,如果刚好覆盖,则两个F相等(区间[1,8]),若不是刚好覆盖,一个从前到后覆盖2^k,一个从后到前预留2^k。

代码

void ST(int n) {
    for (int i=1;i<=n;i++)
        dp[i][0]=A[i];
    for (int j =1;(1<<j)<=n;j++) {
        for(int i=1;i+(1<<j)-1<=n;i++) {
            dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
        }
    }
}
int RMQ(int l, int r){
    int k=0;
    while((1<<(k+1))<=r-l+1) k++;
    //k=log2(r-l+1)
    return max(dp[l][k], dp[r-(1<<k)+1][k]);
}

2、树状数组(区间求和,点更新)

树状数组其实是一个简单的索引表,a[]数组表示输入数据,c[]数组构建树状数组,c[i]都会存储a[i]本身,但是还会存储前面的其它值。lowbit(x)=x&(-x)是求取最低为1的位置(值为1,2,4,8,16)。

比如:对于一个a[i],有哪些c[j]会包含a[i] ?第一个肯定是j=i,依次j=i+lowbit(j).

和二进制有关,是一个神奇的操作。a[1],包含a[1]的有c[1]、c[2]、c[4]、c[8]...........

0001---->0010---->0100--->1000(看和最低位1的位置有关),可以得到数组树状,但是c[i]不是前i个值的和。

如何求前i个值的和:sum+=c[i]、i-=lowbit(i) ,为什么要减,判断c[i]的覆盖范围。

综述:x+=lowbit(x),x的最低位1一直前移,对应c[x]都要包含a[x]。

          x-=lowbit(x),x的最低位1,消去。一下回到解放前,也就是c[x]的覆盖范围,x的最低为1可以由前移得到,也就是覆盖范围,如a[1]要加到c[8]上,c[8]的覆盖范围肯定到a[1]。

其实也可以这样理解:预处理得到c数组和求和分开理解,预处理就理解为规则需要加,求和就理解为lowbit(x)表示当前c[x]覆盖多少个数组a的值。

总而言之:第一步预处理,第二步求和、判断覆盖范围。


#include<stdio.h>                            //树状数组,单点修改的区间求和问题
#define N 10005
int a[N],c[N];
int lowbit(int x){
    return x&(-x);
}
void add(int i,int n,int value){
    a[i]+=value;
    while(i<=n){
        c[i]+=value;
        i+=lowbit(i);
    }
}
int Query(int i){
    int sum=0;
    while(i>0){
        sum+=c[i];
        i-=lowbit(i);
    }
    return sum;
}
int main()
{
    int n,m,value,x1,x2;
    char str[5];
    while(~scanf("%d%d",&n,&m)){
        for(int i=1;i<=n;i++){
            scanf("%d",&value);
            add(i,n,value);
        }
        while(m--){
            scanf("%s%d%d",str,&x1,&x2);
            if(str[0]=='Q'){ //查询
                printf("%d\n",Query(x2)-Query(x1-1));
            }else{ //点修改
                int tmp=x2-a[x1];   //原数a[x1]改为x2
                add(x1,n,tmp);
            }
        }
    }
    return 0;
}
3、线段树(区间求和、区间最大值、区间最小值,点更新,区间更新)

何为线段树?就是树节点包含覆盖范围,和范围内你想要得到的值。注意强调一点,线段树不一定事完全二叉树。点更新可以直接找到叶子节点向上更新即可。但是区间更新,找到相应区间需要向上更新和向下更新,为了节省不必要的时间,需要用到延迟更新,就是在节点上加一个标记,需要查询子节点才向下更新。也就是是在查询的时候需要判断一下,是否向下更新。


代码:

#include<stdio.h>    //poj3468
#define N 100010
struct node{
    int l,r;
    long long sum;
    long long add;
}p[N*4];   //为什么一定要开4倍空间
int a[N];
long long ans;
void build(int o,int l,int r){
    p[o].l=l;
    p[o].r=r;
    p[o].add=0;
    if(l==r){   //叶子节点
        p[o].sum=a[l];
        return;
    }
    int mid=(l+r)/2;
    build(o*2,l,mid);
    build(o*2+1,mid+1,r);
    p[o].sum=p[o*2].sum+p[o*2+1].sum;
}
void show(int o,int l,int r){
    if(l==r){   //叶子节点
        printf("%d ",p[o].sum);
        return;
    }
    int mid=(l+r)/2;
    show(o*2,l,mid);
    show(o*2+1,mid+1,r);
}
//这个是非常难的,如果是单点更新,找到叶子节点即可
//区间更新,找到的是满足区间
void push_down(int o){
    //左子树,和需要更新,以及add标记
    p[o*2].sum += (p[o*2].r-p[o*2].l+1)*p[o].add;
    p[o*2].add += p[o].add;
    //同理右子树
    p[o*2+1].sum += (p[o*2+1].r-p[o*2+1].l+1)*p[o].add;
    p[o*2+1].add += p[o].add;
    p[o].add = 0;
 }
void update(int o,int l,int r,long long value){
    if(p[o].l>r || p[o].r<l) return ;
    if(l<=p[o].l&&p[o].r<=r){  //待查区间大于节点覆盖区间
        p[o].sum+=(p[o].r-p[o].l+1)*value;
        p[o].add+=value;
        return ;
    }
    if(p[o].add) push_down(o);   //延迟更新
    update(o*2,l,r,value);
    update(o*2+1,l,r,value);
    //向上更新
    p[o].sum=p[o*2].sum+p[o*2+1].sum;
}
long long Query(int o,int l,int r){
    if(p[o].l==l&&p[o].r==r){
        return p[o].sum;
    }
    if(p[o].add)     push_down(o);   //延迟更新
    if(r<=p[o*2].r){   //递归到子节点查询
        return Query(o*2,l,r);
    }else if(l>=p[o*2+1].l){
        return Query(o*2+1,l,r);
    }else{
        return Query(o*2,l,p[o*2].r)+Query(o*2+1,p[o*2+1].l,r);
    }
}
int main(){
    int n,m;
    int x1,x2,x3;
    char str[5];
    while(~scanf("%d%d",&n,&m)){
        for(int i=1;i<=n;i++){
            scanf("%d",&a[i]);
        }
        build(1,1,n);
        while(m--){
            scanf("%s%d%d",str,&x1,&x2);
            if(str[0]=='Q'){
                ans=0;

                printf("%lld\n",Query(1,x1,x2));
            }else{
                scanf("%d",&x3);
                update(1,x1,x2,x3);
            }
        }
        //show(1,1,n);
    }
    return 0;
}
注意:sum大于int范围正常,注意add累加也可能大于int范围。update和Query实现中可笼统点也可以具体点。具体点的结束条件是待查询和节点覆盖范围相等,切割待查询区间。笼统点的结束条件是节点覆盖范围只要在待查询区间内即可,不切割待查询区间。本代码在update使用的是笼统方法,在Query中使用的具体方法。具体方法只有一个递归结束,但是笼统方法有一个符合条件结束,还有一个异常结束(因为待查区间不切分,待查区间不在覆盖区间范围内,直接结束递归)
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值