参考链接 http://kmplayer.iteye.com/blog/575725
http://blog.163.com/zhaohai_1988/blog/static/209510085201263011135062/
RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j]里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题
主要方法及复杂度(处理复杂度和查询复杂度)如下:
1.朴素(即搜索) O(n)-O(n) (可以优化成大小桶算法)
2.线段树(segment tree) O(n)-O(qlogn)
3.ST(实质是动态规划) O(nlogn)-O(1)
线段树方法:
线段树能在对数时间内在数组区间上进行更新与查询。
定义线段树在区间[i, j] 上如下:
第一个节点维护着区间 [i, j] 的信息。
if i<j , 那么左孩子维护着区间[i, (i+j)/2] 的信息,右孩子维护着区间[(i+j)/2+1, j] 的信息。
可知 N 个元素的线段树的高度 为 [logN] + 1(只有根节点的树高度为0) .
下面是区间 [0, 9] 的一个线段树:
线段树和堆有一样的结构, 因此如果一个节点编号为 x ,那么左孩子编号为2*x 右孩子编号为2*x+1.
例题:http://blog.csdn.net/gk1988/article/details/8920814
Sparse-Table算法
首先是预处理,用动态规划(DP)解决。设A[i]是要求区间最值的数列,F[i, j]表示从第i个数起连续2^j个数中的最大值。例如数列3 2 4 5 6 8 1 2 9 7,F[1,0]表示第1个数起,长度为2^0=1的最大值,其实就是3这个数。 F[1,2]=5,F[1,3]=8,F[2,0]=2,F[2,1]=4……从这里可以看出F[i,0]其实就等于A[i]。这样,DP的状态、初值都已经有了,剩下的就是状态转移方程。我们把F[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。F[i,j]就是这两段的最大值中的最大值。于是我们得到了动态规划方程F[i, j]=max(F[i,j-1], F[i + 2^(j-1),j-1])。
然后是查询。取k=[log2(j-i+1)],则有:RMQ(A, i, j)=min{F[i,k],F[j-2^k+1,k]}。 举例说明,要求区间[2,8]的最大值,总共2到8是7个元素,所以k=2,那么就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最大值我们可以直接由f[2,2]和f[5,2]得到。
具体如下图所示:
//初始化 INIT_RMQ
//max[i][j]中存的是重i开始的2^j个数据中的最大值,最小值类似,num中存有数组的值
for i : 1 to n
max[i][0] = num[i]
for j : 1 to log(n)/log(2)
for i : 1 to (n+1-2^j)
max[i][j] = max(max[i][j-1], max[i+2^(j-1)][j-1])
//查询 RMQ(i, j)
k = log(j-i+1) / log(2)
return MAX(max[i][k], max[j-2^k+1][k])
大小桶算法是在朴素比较算法的基础上优化,减小了比较的次数,其时间取决于桶大小及容量
基本思想:
将A[]分成bucketnum个桶,每个桶包含capacity个数,每个桶维护一个最大(小)值
RMQ(A, srart, end)查询包含两种情况:
1、start, end落在同一个桶或者相邻桶,刚直接从i到j挨个查询取最值 (数据量少)
2、start, end落在不同的桶,假设start落在第i个桶,end落在第j个桶,
则分别计算第i个桶start到桶尾的最值、i、j之间桶的最值(不包括i,j直接比较每个桶维护的最值)
以及第j个桶桶头到end的最值,最后在这三个结果中取最值
例题:http://poj.org/problem?id=3264
Source Code
/*
大小桶(将MAX分成bucketnum段,每段放capacity个数)
*/
#include<iostream>
using namespace std;
#define Max 50000
#define capacity 250 //桶容量
#define bucketnum 200 //桶的个数
//bucketmax保存桶的最大值,bucket保存桶的最小值
int score[Max+1],bucketmax[bucketnum+1],bucketmin[bucketnum+1];
int main()
{
int n,m,i,num1,num2,result1,result2;
while(scanf("%d%d",&n,&m)!=EOF)
{
//初始化,不然会影响下一次测试
memset(bucketmax,0,sizeof(bucketmax));
for(i=0;i<bucketnum;i++)
bucketmin[i]=10000000;
for(i=0;i<n;i++)
{
scanf("%d",&score[i]);
//找出一段内最大的值存入BUCKETMAX,最小的存入BUCKETMIN
if(bucketmin[i/capacity]>score[i])
bucketmin[i/capacity]=score[i];
if(bucketmax[i/capacity]<score[i])
bucketmax[i/capacity]=score[i];
}
while(m--)
{
scanf("%d %d",&num1,&num2);
//left指示左边的数落在的区间,right指示右边的数在的区间
int left=(num1-1)/capacity;
int right=(num2-1)/capacity;
result1=-1,result2=10000000;
//如果两个数在同一区间,则在两个数之间找最大最小
if(left==right)
{
for(i=num1;i<=num2;i++)
if(result1<score[i-1])
result1=score[i-1];
for(i=num1;i<=num2;i++)
if(result2>score[i-1])
result2=score[i-1];
}
else //如果不在同一区间
{
//找NUM1所在的区间,一个数一个数的找
for(i=num1;i<=(left+1)*capacity;i++)
if(result1<score[i-1])
result1=score[i-1];
for(i=num1;i<=(left+1)*capacity;i++)
if(result2>score[i-1])
result2=score[i-1];
//直接比较整段的最大值BUCKETMAX[i],最小值BUCKETMIN[i]
for(i=left+1;i<right;i++)
if(result1<bucketmax[i])
result1=bucketmax[i];
for(i=left+1;i<right;i++)
if(result2>bucketmin[i])
result2=bucketmin[i];
//找NUM2所在的区间,一个一个的找
for(i=right*capacity;i<=num2;i++)
if(result1<score[i-1])
result1=score[i-1];
for(i=right*capacity;i<=num2;i++)
if(result2>score[i-1])
result2=score[i-1];
}
printf("%d\n",result1-result2);
}
}
return 0;
}