浅谈RMQ算法

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

ST函数:
有的同学说,很简单,两个for循环遍历就行了,这是最简单的方法,要是给你的数据范围N=500000 1s 你咋办 O(n * n)。TLE…

接下来就给大家介绍一种RMQ方法。
预处理O(n*logn) 查询O(1)

一个数组arr: 1 6 2 8 9 3 7
我们用一个二维数组dp[ i ][ j ]表示从i开始连续的2的j次方个数中的最值
dp[2][2]表示从第二个数6开始连续的4个数(2的平方)中的最大值(假设求最大值)
6 2 8 9 这四个数中,也就是dp[2][2]=9 是不是很容易理解。

普遍情况:
dp[i][j]是从 i 开始连续的2的 j 次方中的最值,我们把这个区间分成相等的两部分
前一部分为 i ~ 2的(j-1)次方 -1 表示为dp[i][j-1]
后一部分为 i + 2的(j-1)次方 ~ 2的 j 次方-1 表示为dp[ i+( 1<<(j-1) ) ][j-1]

转移方程为 dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1])

void st_prework()
{
 	for(int i=1;i<=n;i++) 
 	{
  		dp[i][0]=num[i];
	 }
 	int N=log(n)/log(2)+1;
 	for(int j=1;j<N;j++)
 	{
  		for(int i=1;i+(1<<j)-1<=n;i++)
  		{
   			dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
  		}
 	}
}

很多同学就直接把这个板子给背过了,其实这样没用,你要理解每个for循环变量在递推式的意义,问你一个问题,为什么是外循环是j,内循环是i ,相信很多刚入门的同学答不上来吧,大佬略过。

解释:我们在递推的时候,
先比较自己,
在比较相邻的两个数,遍历一边数组,
在比较相邻的四个数,遍历一遍数组,
八个…
十六个…

所以2的指数幂先不动,i从1遍历到n-(1<<j) +1 比较相邻的2的指数幂的大小。

再看O(1)的查找

ll st_query(ll l,ll r)
{
 	ll len=log(r-l+1)/log(2);
 	return max(dp[l][len],dp[r-(1<<len)+1][len]);
}

我们先计算[l.r] 中的2的指数幂是多少,看看我们最大能遍历到哪。log是向下取整
看两个区间就行了,dp[l][len] 表示从l连续的2的len次方区间中的最大值,
dp[r-(1<<len)+1][len] 表示剩下的一段区间,两个区间在预处理时的最大值已经求出来,所以二者取个最大的就行了。所以查询效率为O(1)。

给两个例题:
洛谷 P3865 (模板题)

简单模板题:不解释

#include<iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn=1e6+10;
int a[maxn],dp[maxn][100];
int n,m;
int read()
{
    int num = 0;
    char c = getchar();
    while(c > '9' || c < '0')c = getchar();
    while(c >= '0'&& c<= '9')
    {
        num=num*10+c-'0';
        c = getchar();
    }
    return num;
}
void write(int x)
{
	if(x<0)
	{
		putchar('-');
		x=-x;
	}
	if(x>9)	
		write(x/10);
	putchar(x%10+'0');
}
void st_prework()
{
	for(int i=1;i<=n;i++)
	{
		dp[i][0]=a[i];
	}
	ll t=log(n)/log(2)+1;
	for(int j = 1 ; j < t ; 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 st_query(int l,int  r)
{
	int k=log(r-l+1)/log(2);
	return max(dp[l][k],dp[r-(1<<k)+1][k]);	
}
int main()
{
	ios::sync_with_stdio(false);
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		a[i]=read();
	st_prework();
	for(int i=1;i<=m;i++)
	{
		int l,r;
		l=read(),r=read();
		int k=st_query(l,r);
		printf("%d\n",k);
	
	}
	return 0;
}

第二题
Frequent values

这题我们求的区间大值不是区间的每个数的最大值,而是出现次数(本题中用num数组)的最大值。
用num表示这个元素出现的次数
用rt表示这个元素最后出现的位置,等下解释。
-1 -1 1 1 1 1 3 10 10 10
num 1 2 1 2 3 4 1 1 2 3
rt 2 2 6 6 6 6 7 10 10 10

分3种情况
1.
如果这个区间正好所有元素相同即a[l]==a[r]
ans=r-l+1
2.
如果取得区间把一段连续的数在左端点切断 比如例子中取【5,10】 第5个元素是1,第4个元素也是1,这时候就需要计算一下,是被切断的数频率大,还是这些元素后面的数出现的频率大。
这是rt数组就用到了,让t取一个rt【l】和r的最小值,t代表被切断的数的最后一个的位置,只需要计算max(t-l+1,st_query(t+1,r) )右端点的数被切断没关系,因为num存的数是从小到大存的,切断了就是这个数的最多出现的次数。
3.
区间没有切断,区间也不是所有数都相同,所以求st_query(l,r)就行了。

#include<iostream>
#include<cstring>
#include<cmath>
#include<cstdio>
using namespace std;
typedef long long ll;
const int maxn=1e6+10;
ll a[maxn],rt[maxn],num[maxn],dp[maxn][20];
ll n,k;
void st_prework()
{
	for(int i=1;i<=n;i++) 
	{
		dp[i][0]=num[i];
	}
	int N=log(n)/log(2)+1;
	for(int j=1;j<N;j++)
	{
		for(int i=1;i+(1<<j)-1<=n;i++)
		{
			dp[i][j]=max(dp[i][j-1],dp[i+(1<<(j-1))][j-1]);
		}
	}
}
ll st_query(ll l,ll r)
{
	ll len=log(r-l+1)/log(2);
	return max(dp[l][len],dp[r-(1<<len)+1][len]);
}
int main()
{
	while(~scanf("%lld",&n))
	{
		if(n==0)	break;
		scanf("%lld",&k);
		memset(a,0,sizeof(a));
		memset(rt,0,sizeof(rt));
		for(int i=1;i<=n;i++)
		{
			scanf("%lld",&a[i]);
			num[i]=1;
		}
		a[0]=9999999;
		for(int i=1;i<=n;i++)
		{
			if(a[i-1]==a[i])
			num[i]=num[i-1]+1;
		}

		for(int i=n;i>0;i--)
		{
			if(a[i]==a[i+1])
				rt[i]=rt[i+1];
			else
				rt[i]=i;
		}
		
		st_prework();
		for(int i=1;i<=k;i++)
		{
			ll l,r,ans;
			scanf("%lld%lld",&l,&r);
			if(a[l]==a[r])
				ans=r-l+1;
			else
			{
				if(a[l-1]==a[l])   //数被截断
				{
					ll t=min(rt[l],r);
					ans=max(t-l+1,st_query(t+1,r));
				}
				else
					ans=st_query(l,r);
			}
			printf("%lld\n",ans);
		}
	}
	return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

aaHua_

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值