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

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值