目录
什么是RMQ
举个例子
给你一个长度为n (n≤50000)的序列,求区间[l,r]上的最大值,怎么求?So easy,\(O(n)\)扫一遍记录最大就可以了。
可是如果有Q (Q≤50000)次询问怎么办?这肯定过不了!所以我们就要用RMQ了。既然是一个面对多次查询的算法,那么为了提高查询的效率,都是要经过较长时间的预处理,RMQ也不例外,它拥有一个时间复杂度为\(O(nlogn)\)的预处理,然后可以在\(O(1)\)的时间内查询。下面我们来举一个简单的实例:
算法原理与代码实现
假设有一个数组\(a[9]=\{0,1,9,2,6,0,8,1,7\}\)
设数组dp[i][j]表示从第i位开始向后数\(2^j\)个数中的最小值。如dp[3][2]即表示从第三位(数字2)开始数4个数中最小的一个,即2,6,0,8中的最小值,这里是0,所以dp[3][2]=0。
既然我用了dp数组来表示,那么他是有递推方程的。可以这样来想,当前\(i\)点向右数\(2^j\)个中的最大值,可以表示成【\(i\)点向右数\(2^{j-1}-1\)个中的最大值和从\(i+2^{j-1}\)向右数\(2^{j-1}\)个中的最大值】中的最大值(有点绕,仔细看)
那么我们可以轻而易举的写出dp转移方程
$$dp[i][j]=min(dp[i][j-1],dp[i+(1<<(j-1))][j-1])$$
然后就能轻而易举的写出代码:
(由于我没有找到这么裸的模板题,所以自己出一道)
题目描述:
已知一个长为n的队列,给出m次提问\(l,r\),求区间[l,r]或[r,l]上的最值。 (n≤50000,m≤100000)
输入格式:
第一行两个整数n,m,第二行n个整数表示序列,接下来m行每行两个整数表示查询区间[l,r](或[r,l])
输出格式:
m行,第i行对应问题i的答案
建议你们自己写一下跑一下这两个数据,然后实在不会再看我的程序(我都讲得这么清楚明白怎么会不会呢?)
#include<cmath>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int M=50010;
int n,m;
int a[M];
int dp[M][20];
int Read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9')
ch=getchar();
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x;
}
void RMQ(){
for(int i=1;i<=n;i++)
dp[i][0]=a[i];
for(int j=1;(1<<j)<=n;j++)
for(int i=1;i<=n-(1<<j)+1;i++)
dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
}
int main(){
// freopen("testdata1.in","r",stdin);
// freopen("testdata1.out","w",stdout);
n=Read(),m=Read();
for(int i=1;i<=n;i++)
a[i]=Read();
RMQ();
for(int i=1;i<=m;i++){
int l=Read(),r=Read(),k;
if(l==r){
printf("%d\n",a[l]);
continue;
}
if(l>r) swap(l,r);
for(int i=1;i<=20;i++)
if((1<<i)>=r-l+1){
k=i-1;
break;
}
int rr=r+1-(1<<k);
printf("%d\n",max(dp[l][k],dp[rr][k]));
}
return 0;
}
是不是很简单呢?
拓展:求LCA
我们在最近公共祖先LCA一文中讲到,倍增求LCA是很常见的好写的算法,其时间复杂度是\(O((n+m)logn)\),这里我们介绍的RMQ算法,则是可以以\(O(nlogn)\)的复杂度做预处理,\(O(1)\)的复杂度查询,看起来比倍增更快哦!
首先要介绍一个东西:DFS序


现在你有一个长得非常像cxk的树,这棵树的DFS序就是从根结点出发,DFS遍历是依次经过的节点的序列。比如这棵坤坤树的DFS序就是\(1,2,3,4,5,8,6,9,10,7\)或者\(1,6,7,9,10,2,4,5,8,3\)或者……,这些不同的DFS序都是对的,只不过取决于你先加入了哪条边等,但既然你用DFS序,那么他的顺序不同对答案是没有影响的!
很显然的我们可以想到DFS有一个性质,就是进入一个子树后不会再进入,因此一个子树中深度最浅的节点必定是这个子树的树根。又,我们能想到关于LCA的一个性质,就是\(LCA(a,b)\)一定是同时囊括a,b两点的一棵子树的根节点。【这里一定要想明白啊!!这里我说的“子树”是整个树的子树,意味着这里的“子树”也可以是整个树,表示节点a,b的LCA就是整个树的树根。】
所以知道了上面的两个性质,我们来思考一下LCA该如何用RMQ来求。
首先我们要来一个DFS序的加强版欧拉序(介绍我在链接里,我懒得再写一遍了),然后对欧拉序做RMQ的预处理,处理出从每一个点开始i向右数\(2^j\)个中的点的深度最小值。这就是预处理部分。
查询操作直接查询欧拉序上s[x]到s[y]区间上的深度最小的点,就是他们的LCA了!是不是很简单很神奇???
还是上一个我WA了100次的题: https://www.luogu.org/problem/SP14932
上代码:
#include<cmath>
#include<cstdio>
#include<string>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int M=2080;
bool vis[M<<1];
int n,m,q,cnt,now,rt;
int s[M<<1],deep[M<<1],el[M<<4],head[M<<1];
int dp[M<<1][20];
struct edge{
int to,nxt;
edge(){
to=nxt=0;
}
}e[M<<1];
int Read(){
int x=0;
char ch=getchar();
while(ch<'0'||ch>'9')
ch=getchar();
while(ch>='0'&&ch<='9'){
x=(x<<1)+(x<<3)+(ch^48);
ch=getchar();
}
return x;
}
inline void Init(){
now=cnt=0;
memset(vis,false,sizeof(vis));
memset(deep,0,sizeof(deep));
memset(head,0,sizeof(head));
memset(el,0,sizeof(el));
memset(dp,0,sizeof(dp));
memset(e,0,sizeof(e));
}
inline void Add(int x,int y){
cnt++;
e[cnt].to=y;
e[cnt].nxt=head[x];
head[x]=cnt;
}
void DFS(int x){
el[++now]=x;
s[x]=now;
for(int i=head[x];i;i=e[i].nxt){
int v=e[i].to;
deep[v]=deep[x]+1;
DFS(v);
el[++now]=x;
}
}
void RMQ(){
for(int i=1;i<=(n<<1);i++)
dp[i][0]=el[i];
for(int j=1;(1<<j)<=(n<<1);j++)
for(int i=1;i<=(n<<1)+1-(1<<j);i++)
dp[i][j]=deep[dp[i][j-1]]<=deep[dp[i+(1<<j-1)][j-1]]?dp[i][j-1]:dp[i+(1<<j-1)][j-1];
}
int main(){
int T=Read();
for(int sign=1;sign<=T;sign++){
Init();
n=Read();
for(int i=1;i<=n;i++){
m=Read();
for(int j=1;j<=m;j++){
int tmp=Read();
vis[tmp]=true;
Add(i,tmp);
}
}
for(int i=1;i<=n;i++)
if(!vis[i]){
rt=i;
break;
}
deep[rt]=1;
DFS(rt);
RMQ();
q=Read();
printf("Case %d:\n",sign);
for(int i=1;i<=q;i++){
int u=Read(),v=Read();
if(u==v){
printf("%d\n",u);
continue;
}
if(s[u]>s[v]) swap(u,v);
int k=log2(s[v]-s[u]+1);
printf("%d\n",deep[dp[s[u]][k]]<=deep[dp[s[v]-(1<<k)+1][k]]?dp[s[u]][k]:dp[s[v]-(1<<k)+1][k]);
}
}
return 0;
}
这个理论上比倍增复杂度低,但是好像跑出来我倍增20ms,这个30ms,不知道为什么。这个在写的时候还是看自己吧,觉得哪个好写就哪个!
