一、RMQ问题描述 和 几种解题思路
RMQ问题 (Range Minimum/Maximum Query),首先给出一个序列,然后不断询问某个区间内的最大值和最小值。显然,我们在回答所有询问之前,需要根据序列进行一定的预处理。
(1)最笨的算法显然是朴素的直接查询 ,回答每个问题是O(n)的。
(2)一种算法是采用线段树 ,即在线段树的每个节点保存该区间的最大值与最小值,O(n)的预处理时间(需要自底向上构建),可以O(logn)地回答每个问题。关于这种解法,见我线段树专题中炮兵阵地那道题即可:)
(3)另一种算法就是神奇的ST算法(Sparse Table) ,ST算法记录了所有长度为2^f的区间的最值 。以求最大值为例,设v[n][f]表示[ n,n+2^f) 这个区间内的最大值,那么在询问到[a,b)区间的最大值时答案就是max(v[a][f],v[b-2^f][f]),其中f是满足2^f<=b-a的最大的f。至于那张稀疏表,可以用递推的方法在O(nlogn)(也就是表的元素数)的时间内构建。也就是说v[n][f]=max(v[n][f-1],v[n+2^(f-1)][f-1])。
下面是我写的C++代码,有待改进:
#include <iostream>
#include <ctime>
#include <cmath>
#include <cstdlib>
using namespace std;
/*
ST(Sparse Table): find max/min number in [A,B] within O(nlogn), n=B-A
构造:v[ a~b ][ 0~ceil( log(B-A)) ]
init:
v[i][0]=d[i], i∈[A,B] , 注意:v[i][0]对应前闭后开区间[i, i+2^0)
recurse:
v[i][f]=min{ v[i][f-1], v[i-2^(f-1)][f-1] }
---------------------------------------------------------------------------------------------
prob:
find max/min in [a,b)∈ [A,B]
soln:
max{ v[a][f], v[b-2^f][f] } <== a+2^f>=b-2^f <== 2^(f+1)>=b-a, f=0↑的首个满足条件的值
*/
//查询d[A...B]范围内数组的最值
const int MAX=101;
int cnt=30; //随机交换次数
const int INF=9999;
int A=2;
int B=30;
int data[MAX];
//ST算法DS: v[][]
int v[MAX][MAX];
int minV(int i, int j){
return (i)<(j)?(i):(j);
}
//给d[]赋予随机数
void initData(){
for(int i=0;i<MAX;i++){
data[i]=i;
}
//随机交换
srand(time(0));
while(cnt-->0){
int i= A + (rand()%(B-A));
int j= A + (rand()%(B-A));
//cout<<"swap("<<i<<", "<<j<<")"<<endl;
if(i!=j){
int tmp=data[i];
data[i]=data[j];
data[j]=tmp;
}
}
}
void showData(){
for(int i=A;i<=B;i++){
cout<<data[i]<<"\t";
}
cout<<endl;
}
//构造ST算法DS: v[]
void initV(){
//init:
for(int i=0;i<MAX;i++){
for(int j=0;j<MAX;j++){
v[i][j]=INF;
}
}
for(int i=A;i<=B;i++){
v[i][0]=data[i]; //当求min时,其余的v[i][0]必须预设为很大的值
}
//DP iteration:
for(int i=1;i<=ceil( log(B-A)/log(2) );i++){
for(int j=A;j<=B;j++){
v[j][i]=minV( v[j][i-1], v[j+(int)pow((double)2,(double)(i-1))][i-1] );
/*
cout<<"v["<<j<<"]["<<i<<"]="<<v[j][i]<<"= minV(";
cout<<"v["<<j<<"]["<<i-1<<"]="<<v[j][i-1]<<", \t";
cout<<"v["<<j+(int)pow((double)2,(double)(i-1))<<"]["<<i-1<<"]="<<v[j+(int)pow((double)2,(double)(i-1))][i-1]<<")"<<endl;
*/
}
}
}
void showV(){
for(int i=0;i<=ceil( log(B-A)/log(2) );i++){
for(int j=A;j<=B;j++){
cout<<"v["<<j<<"]["<<i<<"]="<<v[j][i]<<"\t";
}
cout<<endl<<endl;
}
cout<<endl;
}
int query(int a,int b){
int f=0;
while( (int)pow((double)2,(double)(f+1)) < (b-a) ){
f++;
}
return minV( v[a][f], v[b-(int)pow((double)2,(double)f)][f] );
}
int main()
{
initData();
showData();
initV();
//showV();
int queries;
cout<<"# of queries:";
cin>>queries;
while(queries-->0){
int a,b;
cout<<"[a :";
cin>>a;
cout<<"b) :";
cin>>b;
cout<<"res : "<<query(a,b)<<endl<<endl;
}
system("pause");
return 0;
}
另外,RMQ问题与LCA(Least Common Ancestors,最近公共祖先)问题可以互相转化。LCA问题有一个经典的离线算法Tarjan算法,稍后我将会介绍。
二、ST算法
POJ 3264 Balanced Lineup ==> http://poj.org/problem?id=3264
代码参考这个大牛的: http://www.cnblogs.com/mjc467621163/archive/2011/08/24/2152133.html
//poj 3264 Balanced Lineup
#include<iostream> //ST算法,即(Sparse_Table ,稀疏表),可以在O(nlogn)的预处理以后,实现O(1)的查询效率
#include <cmath>
using namespace std;
const int max_n=50005;
int arr[max_n],n,t,l,r; //原数列arr从下标1开始
int rmq_m[max_n][16],rmq_n[max_n][16]; //16>=log2(50000)==15.609640474436811739351597147447
//rmq_m[i][j],rmq_n[i][j]分别表示原数列arr[i, i+2^j - 1]区间中的最大值和最小值,区间长度为2^j
void rmq_init() //预处理,采用DP
{
int i,j;
//1. DP-INIT: 长度为2^0==1的区间的最值就是单点上的值
for(i=1;i<=n;++i)
rmq_m[i][0]=rmq_n[i][0]=arr[i];
//2. DP-ITERATION: rmq[i][j]=max{ rmq[i][j-1], rmq[i+2^(j-1)][j-1] }
// 可见 rmq[*][j]=max{ rmq[$][j-1], rmq[#][j-1] } ,因此 j 放在循环外层,且顺序循环
for(j=1;j<=log(n+0.0)/log(2.0);++j) //j表示区间长度为2^j,区间范围[i, i+2^j - 1],小于等于n,所以 j<=log(n+0.0)/log(2.0)
{
for(i=1;i+(1<<j)-1<=n;++i) //因为rmq_m[i][j]表示arr[i, i+2^j - 1]的最大值,所以 i+2^j - 1 <= n
{
rmq_m[i][j]=max(rmq_m[i][j-1],rmq_m[i+(1<<(j-1))][j-1]);
rmq_n[i][j]=min(rmq_n[i][j-1],rmq_n[i+(1<<(j-1))][j-1]);
}
}
}
//DP的初值:rmq_m[i][0]其实就等于arr[i]; 状态转移方程:rmq_m[i][j]=max(rmq_m[i][j-1],rmq_n[i+2^(j-1)][j-1])
//采用自底向上的算法递推地给出所有符合条件的rmq_m[i][j]的值
int rmq(int a,int b) //查询
{
/*
要使得[a,b]的max =
max{ [a, a+2^m]的max, [b-2^m+1, m]的max }
需要 m 大于一定值:
a+2^m >= b-2^m+1
<==> m >= log(b-a+1)/log(2) - 1
<==> m 至少是 floor[ log(b-a+1)/log(2) ]
*/
int m=log(b-a+1.0)/log(2.0);
int x=max(rmq_m[a][m],rmq_m[b-(1<<m)+1][m]); //区间[a,b]最大值
int y=min(rmq_n[a][m],rmq_n[b-(1<<m)+1][m]);
return x-y;
}
//查询从a到b这一段的最大(小)值, 那么我们先求出一个最大的整数m, 使得m满足2^m <= (b - a + 1).
//于是我们就可以把[a, b]分成两个长度为2^m的区间: [a, a+2^m -1], [b-2^m +1, b]; (有部分重叠,b-2^m +1 <= a+2^m -1)
//而rmq_m[a][m]为[a, a+2^m-1]的最大值, rmq_m[b-2^m +1][m]为[b-2^m +1, b]的最大值
int main()
{
scanf("%d%d",&n,&t);
for(int i=1;i<=n;++i)
scanf("%d",arr+i);
rmq_init();
while(t--)
{
scanf("%d%d",&l,&r);
printf("%d\n",rmq(l,r));
}
return 0;
}
参考:
问题概述和主要思路:
http://cuitianyi.com/blog/rmq%E9%97%AE%E9%A2%98/