前言
RMQ问题就是所谓的“范围最小值问题”(Range Minimum Query)。
(RMQ问题)就是给出一个数组A[1],A[2],A[3]…A[n],支持查询操作Query(L,R)=min{A[L],A[L+1]…A[R]}
1.暴力(朴素)思想
这种方法看起来应该是一种非常垃圾的方法,应该没有人不会吧。原理就是对于每次查询都循环统计一个最小值,所以说每次的最坏情况的复杂度都是O(n)。这么垃圾的算法我为什么还要写呢,因为它的空间复杂度是O(1),而且代码量相当的少:
int ForceRMQ(int* A,int L,int R)//暴力枚举RMQ
{
int Min=A[L];
for(int i=L+1;i<=R;i++)
Min=min(Min,A[i]);//每一次都和当前求出的最小值进行比较
return Min;//最后就能得到区间最小值
}
接着这种暴力的思想想下去,能不能有一种方法能使查询的速度加快。假设我们用一个数组f[L][R]表示从L位到第R位的最小值,我们先预先把这个东西计算出来,然后我们调用的复杂度就可以是O(1)了。但是这是一种更加“辣鸡”的方法,因为初始化的时间复杂度为O(n^2)。
2.Sparse-Table算法
正如我们刚刚提到的,我们可以把一些最小值预先处理出来。如果处理f[L][R]显然是不行了,那我们就去看看能不能用“二进制优化”的思想来进行优化。我们做这样的一个尝试用数组d[i][j]表示从i号元素开始的包含有2^j个元素的序列的最小值。(这是一个思想的飞跃,看不懂没关系,耐心往下读。)
也就是说d[i][j]等价于上文中的f[i][i+2^j-1]。但是不同于上次的是,我们对d数组的初始化的复杂度是O(n log n)而不是O(n^2)。比如我们想要求得d[i][j]的值,我们可以先求d[i][j-1]值(也就是f[i][i+2^(j-1)-1]的值)再求d[i+2^(j-1)][j-1]的值(也就是f[i+2^(j-1)][i+2^j-1]的值)。如下图:
通过这样的方法,我们就可以建立递推表达式:d[i][j]=min(d[i][j-1],d[i+2^(j-1)][j-1])。假设总共有n个元素,那么2^j<=n。所以j=log2(n),d中的元素个数就应该为nlog2(n),又因为每个元素的计算都是在常数时间内完成的,所以总时间复杂度就为O(nlogn)。(这个代码非常好,也很少而且很高效,是个OIer就该背下来。)
int d[MaxN][MaxLgN];//Sparse-Table,MaxN表示n的最大值,MaxLgN表示logn的最大值(向上取整)
void RMQ_init(int* A,int n)//n为元素个数,A为原序列
{
for(int i=0;i<n;i++)//如果j=0,d[i][0]=A[i]因为只有一个元素
d[i][0]=A[i];
for(int j=1;(1<<j)<=n;j++)//当d>0且小于等于log2(n)时
for(int i=0;i+(1<<j)-1<=n;i++)。//1<<j=2^j(左移运算)
d[i][j]=min(d[i][j-1],d[i+(1<<(j-1))][j-1]);
}
初始化之后我们就该查询了,但是同学们会发现我们要查询的f[L][R]中的R-L+1(也就是元素个数)不一定是一个2的整数次幂。但是思考这样一个问题:
我们可以使用一个以L开始的已知区间,和一个以R结尾的已知区间用来覆盖我要查询的时间只要保证2^k*2>=R-L+1就可以覆盖L到R的全体元素。看代码:
int RMQ(int L,int R)
{
int k=0;
while((1<<(k+1))<=R-L+1)
k++;//k每次加一,一直加到覆盖整个区间
return min(d[L][k],d[R-(1<<k)+1][k]);
//整个的最小值,就可以看成是对两个覆盖区间的最小值取较小的一个
}
虽然这个查询函数里面用到了循环,但是由于k最多不会超过(logn)/2太多,而位移运算又“极快无比”,所以仅此看成O(1)。(这个“近似”实际上是非常合理的。)
3.线段树
没学过线段树的同学强烈建议看一下这个:我与线段树的故事(纯新手请进)
建立一棵线段树花费的时间复杂度为O(nlogn),每次查询的复杂度为O(logn)。这实际上并没有上文所说的Sparse-Table算法要高效,但是线段树可用于维护动态区间最小值,而不仅仅是RMQ(这个代码应该还是比较“优质”的)。但是单论我们现在的这个“RMQ”问题来说,上文中的“Sparse-Table”不仅代码少,而且效率也要比线段树高。
#include<iostream>
#include<cstdlib>
using namespace std;
#define MaxNLogN 20971521
#define MaxN 1048576
struct NODE//定义线段树结点
{
int l,r;//定义结点表示的闭区间的范围
int lch,rch;//定义结点的左子和右子
int Min;//表示区间最小值
void clr(int L,int R,int Lch,int Rch,int MIN)//懒人专用初始化函数
{
l=L;r=R;lch=Lch;rch=Rch;Min=MIN;
}
}ns[MaxNLogN];
int newnode=1;
int A[MaxN];
void build(int root,int l,int r)//构建一棵以root为根节点,表示[l,r]闭区间的树
{
if(l==r)//如果确定到一个元素
{
ns[root].clr(l,r,-1,1,A[l]);//-1表示没有字结点,最小值就是这个元素本身
return;
}
int nl=newnode++;
int nr=newnode++;//申请结点
int mid=(l+r)/2;
build(nl,l,mid);
build(nr,mid+1,r);//递归构建左右子树
ns[root].clr(l,r,nl,nr,min(ns[nl].Min,ns[nr].Min));
}
#define INF 2147483647
int query(int root,int l,int r)
{
if(ns[root].l>r || ns[root].r<l)//当前区间与所求区间没有交集
return INF;//返回无穷大不会影响结果(因为我们要取最小值)
if(l<=ns[root].l && ns[root].r<=r)//如果这个区间完全在范围之内
return ns[root].Min;//返回这个区间的最小值
int nl=ns[root].lch;
int nr=ns[root].rch;
return min(query(nl,l,r),query(nr,l,r));
//否则返回左右子树在这个区间内的最小值取较小值
}
int main()
{
int NumCount,QuestionCount;
cin>>NumCount;//输入序列长度
for(int i=1;i<=NumCount;i++)
cin>>A[i];//输入这个序列
int root=newnode++;
build(root,1,10);//建树
cin>>QuestionCount;//输入询问数量
for(int i=1;i<=QuestionCount;i++)
{
int L,R;
cin>>L>>R;//输入询问
cout<<query(root,L,R)<<endl;//输出结果
}
system("pause");
return 0;
}
这就是一棵简单的线段树,一棵没有“lazy”标记的线段树。
4.后记
其实RMQ是有标准算法的,叫做“标准RMQ算法”,实际上并不一定有我们的nlogn的Sparse-Table要快,而且代码量相当大(大概是线段树代码的两倍还要多)。“标准RMQ算法”里涉及了很多奇奇怪怪的东西,比如“笛卡尔树”什么的。而唯一的优势就是时间复杂度为O(n),可能这个复杂度在n相当大的时候会发挥出作用,但是对我们OIer实在是没有什么意义,在此不再介绍,有兴趣的同学可以自行百度。
赶稿匆忙,如有谬误,望同学们谅解。不过我这回的代码都是自己编译测试通过了的,应该不会有什么问题。
后文来自百度百科:
RMQ (Range Minimum/Maximum Query)问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在i,j里的最小(大)值,也就是说,RMQ问题是指求区间最值的问题。——360百科
主要方法及复杂度如下:
1、朴素(即搜索),O(n)-O(qn) online。
2、线段树,O(n)-O(qlogn) online。
3、ST(实质是动态规划),O(nlogn)-O(q) 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(q) online。
首先根据原数列,建立笛卡尔树,从而将问题在线性时间内规约为LCA问题。LCA问题可以在线性时间内规约为约束RMQ,也就是数列中任意两个相邻的数的差都是+1或-1的RMQ问题。约束RMQ有O(n)-O(1)的在线解法,故整个算法的时间复杂度为O(n)-O(1)。