RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。

简介

  主要方法及复杂度如下:

  1、朴素(即搜索),O(n)-O(qn) online。

  2、线段树,O(n)-O(qlogn) online。

  3、ST(实质是动态规划),O(nlogn)-O(1) online。

  ST算法(Sparse Table),以求最大值为例,设d[i,j]表示[i,i+2^j-1]这个区间内的最大值,那么在询问到[a,b]区间的最大值时答案就是max(d[a,k], d[b-2^k+1,k]),其中k是满足2^k<=b-a+1(即长度)的最大的k,即k=[ln(b-a+1)/ln(2)]。

  d的求法可以用动态规划,d[i, j]=max(d[i, j-1],d[i+2^(j-1), j-1])。

  4、RMQ标准算法:先规约成LCA(Lowest Common Ancestor),再规约成约束RMQ,O(n)-O(1) online。

  首先根据原数列,建立笛卡尔树,从而将问题在线性时间内规约为LCA问题。LCA问题可以在线性时间内规约为约束RMQ,也就是数列中任意两个相邻的数的差都是+1或-1的RMQ问题。约束RMQ有O(n)-O(1)的在线解法,故整个算法的时间复杂度为O(n)-O(1)。

ST算法

  来看一下ST算法是怎么实现的(以最大值为例):

  首先是预处理,用一个DP解决。设a是要求区间最值的数列,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。这样,DP的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j](j≥1)平均分成两段(因为j≥1时,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就是这两段的最大值中的最大值。于是我们得到了动规方程F[i,j]=max(F[i,j-1],F[i+2^(j-1),j-1])。

  接下来是得出最值,也许你想不到计算出f有什么用处,一般要想想计算max还是要O(logn),甚至O(n)。但有一个很好的办法,做到了O(1)。还是分开来。如在上例中我们要求区间[2,8]的最大值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最大值我们可以直接由f[2,2]和f[5,2]得到。扩展到一般情况,就是把区间[l,r]分成两个长度为2^n的区间(保证有f对应)。直接给出表达式:

  k:=trunc(ln(r-l+1)/ln(2));

  ans:=max(F[l,k],F[r-2^k+1,k]);

  这样就计算了从l开始,长度为2^k的区间和从r-2^k+1开始长度为2^k的区间的最大值(表达式比较烦琐,细节问题如加1减1需要仔细考虑),二者中的较大者就是整个区间[l,r]上的最大值。

标准算法

建立笛卡尔树

  数组A[0,N-1]的笛卡尔树C是这样一棵二叉树:当N=0,它是一棵空树,否则它的根节点是A中的一个最小元素A[i](并以这个最小元素的下标i标记),而左右子树分别是A[0,i-1]和A[i+1,N-1]的一棵笛卡尔树。注意如果A中有相等的元素,则A的笛卡尔树不一定唯一,但在这里我们限定所用的最小元素为在数组中最先出现者,在此限制下笛卡尔树是唯一的。

  容易看出,数组A在闭区间[l,r]上的最小值等同于笛卡尔树C中下标为l和r的两个顶点的最近公共祖先(LCA)的值。由此,RMQ问题可以转化为LCA问题。下面说明如何在O(n)时间内实现这一转化。

  我们将要将A的元素依次插入笛卡尔树C。每次插入都可能使树的形态发生变化。为了在O(N)的时间内完成整个插入过程,考虑C的右链,即根结点、根结点的右儿子、根结点的右儿子的右儿子……组成的链。注意这些元素的下标和值都是递增的。下标最大,即将要插入的元素A[i]一定是新树右链的最后一个元素。原来的右链中,值比A[i]大的元素在新树中不再属于右链,这些元素组成的链成为A[i]的左子树的右链;原来右链中的其它元素加上A[i]组成了新的右链。初看起来,寻找分界点的最佳方法是O(logN)时间的二分查找;但是对于整个过程来说,O(NlogN)的时间复杂度不是最优的。关键在于一旦一个元素比A[i]大,它就从右链中被永久地移除了。如果按照从后到前的顺序判断一个元素是否大于A[i],则每次插入的时间复杂度为O(k+1),k为本次插入中移除的右链元素个数。因为每个元素最多进出右链各一次,所以整个过程的时间复杂度为O(N)。

  用一个栈结构维护右链元素的下标,上述过程可以很容易地实现。(见下面代码部分)

转化为约束RMQ

  为了将LCA问题转化为约束RMQ,我们注意到任意树中两个结点u和v的LCA就是在一次从树根开始的深度优先搜索中,在u和v之间(包括回溯时)到达的结点中层数最小的一个。为了利用这一事实,我们建立三个数组:

  E[1,2*N-1]:在一次深度优先搜索(恰好是树的一次欧拉环游)中每一步到达的结点。

  L[1,2*N-1]:E中对应结点在树中的层数。

  H[1,N]:每个结点在E中某一次出现的下标(不妨设为第一次)。

  则对任意u和v,不妨设H[u]≤H[v](否则交换u和v),只要在L中找到[H[u],H[v]]中最小值的下标i,则E[i]就是u和v的LCA。注意到L满足约束RMQ的条件(相邻元素差的绝对值为1),这说明原来的LCA问题已经被转化为约束RMQ。转化过程显然能在O(N)时间内完成。

约束RMQ的解法

  现在仍旧用A[0,N-1]表示问题中的数列,这里有|A[i]-A[i-1]|=1(i=1,2,...,N-1)成立。

  将A分解为长度为l=[(log N)/2]的块。设A'[i]为第i块中的最小值,B[i]为该最小值的位置。A'[i]和B[i]的长度均为N/l, 所以用ST算法处理A'数组的时空复杂度均为O(N/l*log(N/l))=O(N/logN*(logN-logl))=O(N)。预处理之后,对任意多连续的块进行的查询都能在O(1)时间内实现。余下的问题是如何进行块内查询。

  注意到对任意一块中的块内查询的结果有影响的唯一因素是块内每相邻两个元素间的“升降关系”构成的序列。因为每两个元素之间的关系只有两种(“+1”、“-1”),而块的长度又只有l=[(log N)/2],所以本质不同的块最多有2^I=O(sqrt N)种。对每种块中所有可能的块内查询预处理出答案的时空复杂度是O(sqrt N*l^2)=O(N)(这里的O(N)表示不超过线性时间)。预处理出所有块的“类型”,并用二进制数存储的时间复杂度是O(N)。

  此后,每次查询可以分为两种情况:

  1、块内查询,答案已经被预处理出,只要在数组中找到它即可。

  2、块间查询,可以分解为2个块内查询,和一个A'上的RMQ,三者的时间复杂度都是O(1)。

  综上,我们给出了一个预处理时间为O(n),查询时间为O(1)的在线RMQ算法。

代码

ST算法,Pascal,程序片段

  Read(n, q);

  for i := 1 to n do

  Read(d[i, 0]);

  for j := 1 to Trunc(Ln(n) / Ln(2)) do

  for i := 1 to n - 1 shl j + 1 do

  d[i,j] := Max(d[i,j-1], d[i+1 shl (j-1),j-1]);

  for i := 1 to q do

  begin

  Read(a, b);

  k := Trunc(Ln(b - a + 1) / Ln(2));

  rmq := Max(d[a, k], d[b - 1 shl k + 1, k]);

  Writeln(rmq);

  end;

  RMQ(Range Minimum/Maximum Query)问题是求区间最值问题。你当然可以写个O(n)的(怎么写都可以),但是万一要询问最值1000000遍,估计你就要挂了。这时候你可以放心地写一个线段树(前提是不写错)O(logn)的复杂度应该不会挂。但是,这里有更牛的算法,就是ST算法,它可以做到O(nlogn)的预处理,O(1)地回答每个询问。

ST算法,Pascal,完整过程

  var p,n,m,z:longint;

  aa,data:array[0..100001]of longint;

  procedure myread;

  var i:longint;

  begin

  read(n,m);

  for i:=1 to n do read(aa[i]);

  z:=trunc(sqrt(n));data[1]:=maxlongint;p:=1;

  for i:=1 to n do

  begin

  if aa[i]<data[p] then data[p]:=aa[i];

  if i mod z=0 then begin inc(p);data[p]:=maxlongint;end;

  end;

  end;

  procedure main;

  var tt1,tt2,a,b,i,j,t1,t2,min:longint;

  begin

  for i:=1 to m do

  begin

  read(a,b);

  t1:=a div z +1;

  t2:=b div z +1;

  tt1:=a mod z;

  tt2:=b mod z;

  min:=maxlongint;

  if t1=t2 then

  begin

  for j:=a to b do

  if min>aa[j] then min:=aa[j];

  write(min,' ');

  continue;

  end;

  for j:=((t1-1)*z+tt1) to t1*z do

  if min>aa[j] then min:=aa[j];

  for j:=((t2-1)*z) to ((t2-1)*z+tt2) do

  if min>aa[j] then min:=aa[j];

  for j:=t1+1 to t2-1 do

  if min>data[j] then min:=data[j];

  write(min,' ');

  end;

  end;

  begin

  myread;main;

  end.

ST算法,C++

  #include <iostream>

  #include <cstdio>

  #include <cmath>

  #define max(a,b) (a>b?a:b)

  #define min(a,b) (a<b?a:b)

  #define MN 50005

  using namespace std;

  int mi[MN][17],mx[MN][17],w[MN];

  int n,q;

  void rmqinit()

  {

  int i,j,m;

  for(i=1;i<=n;i++){mi[i][0]=mx[i][0]=w[i];}

  m=floor(log((double)n)/log(2.0));

  for(i=1;i<=m;i++)

  {

  for(j=n;j>=1;j--)

  {

  mx[j][i]=mx[j][i-1];

  if(j+(1<<(i-1))<=n)mx[j][i]=max(mx[j][i],mx[j+(1<<(i-1))][i-1]);

  mi[j][i]=mi[j][i-1];

  if(j+(1<<(i-1)<=n))mi[j][i]=min(mi[j][i],mi[j+(1<<(i-1))][i-1]);

  }

  }

  }

  int rmqmin(int l,int r)

  {

  int m=floor(log((double)(r-l+1))/log(2.0));

  return min(mi[l][m],mi[r-(1<<m)+1][m]);

  }

  int rmqmax(int l,int r)

  {

  int m=floor(log((double)(r-l+1))/log(2.0));

  return max(mx[l][m],mx[r-(1<<m)+1][m]);

  }

  int main()

  {

  cin>>n>>q;

  for(int i=1;i<=n;i++)scanf("%d",&w[i]);//cin>>w[i];

  rmqinit();

  int l,r;

  for(int i=1;i<=q;i++)

  {

  scanf("%d%d",&l,&r);//cin>>l>>r;

  printf("%d %d\n",rmqmax(l,r),rmqmin(l,r));

  //cout<<rmqmax(l,r)<<" "<<rmqmin(l,r)<<endl;

  }

  while(cin>>n)

  return 0;

  }

建立笛卡尔树,C++

  void computeTree(int A[MAXN], int N, int T[MAXN])

  //T[i]储存每个结点的父结点(左右子树是无所谓的)

  {

  int st[MAXN], i, k, top = -1;

  //从空栈开始

  //第i步,我们将A[i]插入栈中

  for (i = 0; i < N; i++)

  {

  //找到第一个小于等于A[i]的元素

  k = top;

  while (k >= 0 && A[st[k]] > A[i])

  k--;

  //如上述,更改树的结构

  if (k != -1)

  T[i] = st[k];

  if (k < top)

  T[st[k + 1]] = i;

  //将A[i]插入栈中,并移除所有更大的元素

  st[++k] = i;

  top = k;

  }

  //栈中的第一个元素就是树根,没有父节点

  T[st[0]] = -1;

  }

<Marvolo原创,严禁转载>

RAM区间最值的更多相关文章

  1. HDU 1754 I Hate It(线段树单点替换+区间最值)

    I Hate It [题目链接]I Hate It [题目类型]线段树单点替换+区间最值 &题意: 本题目包含多组测试,请处理到文件结束. 在每个测试的第一行,有两个正整数 N 和 M ( 0 ...

  2. hdu1754 I hate it线段树模板 区间最值查询

    题目链接:这道题是线段树,树状数组最基础的问题 两种分类方式:按照更新对象和查询对象 单点更新,区间查询; 区间更新,单点查询; 按照整体维护的对象: 维护前缀和; 维护区间最值. 线段树模板代码 # ...

  3. HDU 1754区间最值 & SPLAY

    真是亲切的1754啊..第一道傻逼版的线段树做的是这个,后来学了zkw做的是这个,在后来决定打lrj线段树又打了一遍,如今再用splay和老朋友见面   从上到下依次为:加了读入优化的splay,sp ...

  4. HDU-1754I Hate It 线段树区间最值

    这道题比较基本,就是用线段树维护区间最值,可以算是模板吧-.. I Hate It Time Limit: 9000/3000 MS (Java/Others) Memory Limit: 32768 ...

  5. HDU 1754 I Hate It(线段树单点更新区间最值查询)

    I Hate It Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total ...

  6. hdu2852KiKi's K-Number(区间K值)

    http://acm.hdu.edu.cn/showproblem.php?pid=2852 区间K值写错了... #include <iostream> #include<cstd ...

  7. 【HDU】1754 I hate it ——线段树 单点更新 区间最值

    I Hate It Time Limit: 9000/3000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total S ...

  8. 【POJ】3264 Balanced Lineup ——线段树 区间最值

    Balanced Lineup Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 34140   Accepted: 16044 ...

  9. RMQ(模板 ST 区间最值,频繁的间隔时间)

    PS: 介绍:http://blog.csdn.net/liang5630/article/details/7917702 RMQ算法.是一个高速求区间最值的离线算法,预处理时间复杂度O(n*log( ...

随机推荐

  1. Java基础知识强化之集合框架笔记11:Collection集合之迭代器的原理及源码解析

    1. 迭代器为什么不定义成一个类,而是定义为一个接口 ?  答:假设迭代器定义的是一个类,这样我们就可以创建该类的对象,调用该类的方法来实现集合的遍历.但是呢? 我们想想,Java中提供了很多的集合类 ...

  2. NYOJ2括号配对问题

    括号配对是最基本的栈的问题,它是栈入门的经典题目,思路是,如果是左括号直接进栈,如果是右括号,这时就要比较栈顶的元素与他是否匹配,如果匹配则出栈,否则进栈,下面是代码的实现: #include < ...

  3. Bernese单点定位数据准备及处理

    原创作者 blog :http://yifeiyao.blog.163.com/blog/static/2058932752012669731170/1.准备所需用的数据文件,如下: 原始观测.O文件 ...

  4. PHPexcel数据按模板导出

    <?php header("Content-type: text/html; charset=gb2312"); error_reporting(E_ALL); ini_se ...

  5. Access数据库数据转换Table.Json

    使用WPF组件 xaml <Window x:Class="JsonConvert.MainWindow" xmlns="http://schemas.micros ...

  6. Swift - 27 - 使用元组让函数返回多个值

    //: Playground - noun: a place where people can play import UIKit // 定义一个数组 var userScores:[Int]? = ...

  7. zookeeper集群一次性启动

    编写shell脚本 新建文本,命名为start-zookeeper.sh #!/bin/sh echo "start zkServer…" for i in master work ...

  8. window 7 C盘整理

    发现两篇不错的文章可以参考一下: http://blog.renren.com/blog/200083873/467545630 http://www.uedbox.com/win7-c-disk-s ...

  9. Qt远程机开发时光标注意问题

    最近项目中有一个比较奇怪的问题,就是当记录了最后的m_lastPos为当前widget中间位置之后,设置了QCursor也为当前中间位置. 这个时候当开始移动的时候,发现offset出现了很怪的极大值 ...

  10. Apache Commons DbUtils Problem