一、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/