浅谈树状数组和RMQ问题

有这样一个问题:给定一个n个元素的数组{A},你的任务是设计一个数据结构,支持一下操作:

ADD(x,d):让Ax增加d

Query(L,R)求A[L]+A[L+1]+...+A[R]的和


很显然这可以用线段树求解,但这里说的不是这种数据结构,我们要讨论的是树状数组,一个利用位运算来进行操作的数据结构,相对于上述问题,树状数组速度略微会快一点,而且写起来也简单一些,当然线段树的用处更多。

首先需要知道这样一个结论:x&(-x)表示的是x二进制表示最右边的1所对应的权值,如6&(-6)=2,这是因为计算机中数字使用补码表示,6取负后各位取反,然后末尾加1,那么原先末尾连续的0变成1后加1又变成0,直至末尾第一个1的位置,其余位置相与为0,。我们记lowbit[i]=(i&(-i));


树状数组的示意图如下




从图上易知如果某一节点编号为i,那么其父亲节点编号为i+lowbit[i],我们建立一个辅助数组C[i],其中C[i]=A[i-lowbit[i]+1]+...+A[i],即C[i]表示{A}的某一段和是哪一段呢?即是图上C[i]统治的左边部分,如C[4]为A[1]+A[2]+A[3]+A[4],C[3]为A[3],那么有了C[i],如何求某一段和呢?很简单,我们用S[i]表示前缀和,即是S[i]=A[1]+...+A[i],则sum(L,R)=S[R]-S[L-1],

求S[i]即可。而S[i]可以这样来求:从C[i]开始往左上走,如求S[7],顺序为C[7],C[6],C[4],加上某一个数时,同样如A[3]+d,那么顺序更新C[i]数组,往右上走C[3],C[4],C[8],看出来了吧,更新其实就是按照i+lowbit[i]和i-lowbit[i]的标号来更新的,那么代码也就呼之欲出了:

C++代码:

int sum(int x){
    int ret=0;
    while(x>0){
        ret+=C[x];x-=lowbit(x);
    }
    return ret;
}

void add(int x,int d){
    while(x<=n){
        C[x]+=d;x+=lowbit(x);
    }
}

问题举例:

有n个乒乓球爱好者,他们的技术被量化为数字,他们排成顺序的一列,如果要进行比赛,必须有三个人,切这三个人的顺序和序列中的顺序要一致,即不能调换相对顺序,中间的人的值必须在左右两人之间,求这样的比赛个数。

很容易想到的是对于每一个人,枚举在他左边有多少个小于他的值,右边有多少个小于他的值,那么即可求出这个人当裁判的比赛场数,每一个人都这样处理即可求出最后解。关键是如何求左边或者右边小于他的个数。我们这样来考虑,对于某一个人,记他的技术值为x,如果在他左边有小于他的技术值得人,那么值必然在1~x中,这很容易想到一个数组标记它,于是利用一个数组x[j],x[j]记为从扫描开始到现在是否存在ai=j的标记,那么小于第i个人的技术值得人的个数就为x[1]+x[2]+...+x[a[i]-1],记前缀和,这就可以利用树状数组来处理,而右边一样重新扫描一遍即可得出右边小于该人技术值的个数,答案也就呼之欲出了。


RMQ问题(范围最小问题)

给出一个n个元素的数组A1,A2,...,An,求min{A[L],A[L+1],...,A[R]}

怎么办呢,我们可以先设d(i,j)表示i开头的长度为2^j的一段元素中的最小值,那么地推关系式有d(i,j)=min(d(i,j-1),d(i+2^(j-1),j-1))

那么我们可以这样初始化d(i,j)数组

C++代码:

void RMQ_init(const vector<int>& A){
    int n=A.size();
    for(int i=0;i<n;i++)d[i][0]=A[i];
    for(int j=1;(1<<j)<=n;j++){
        for(int i=0;i+(1<<j)-1<n;i++)
            d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);
    }
}

查询可以这样查询:

int RMQ(int L

int RMQ(int L,int R){
    int k=0;
    while((1<<(k+1))<<(R-L+1))k++;//找到最大的k使得2^k<=(R-L+1)
    return min(d[L][k],d[R-(1<<k)+1][k]);
}

问题举例:

给出一个非降序的序列a1,a2,...,an,询问(i,j)这一段中出现次数最多的值所出现的次数。

应该注意到的是这是一个非降序的序列,那么我们就可以将这个序列写成(a[k1],k1),(a[k2],k2)...的形式,其中ki表示其出现的次数,那么序列就被分为几个段,对于询问(i,j)我们需要知道这样几个信息来解题,位置i所在的段num[i],位置i所在段的右端点right[i],位置j所在的段num[j],位置j所在段的左端点left[j],那么答案就很容易求出了,即为段num[i]+1~num[j]-1的每个段中的最大值,right[i]-i+1,j-left[j]+1三者中的较大值,而第一个值得求解即是我们上述讨论的RMQ问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值