洛谷P3865
RMQ问题就是指求区间最值的问题。
这里的解决方法采用ST表
首先先回顾一下线段树的想法,将一个数列不断二分,并记录当前管辖区间内的最值。
但线段树查询操作并不是O(1),因此面对大量查询操作会t
因为线段树会把大区间划分成很多个不会重合的小区间,所以会耗时
而如果我们能将这个大区间直接划分成两个区间,直接用max求得,不是就会快很多了吗。
e.g. f[1,5]记录1-5之间的最值,f[4,8]记录5-8最值
因此求[1,8]最值就直接max(f[1,5],f[4,8]);
而我们采用倍增的思想的话,可以直接将一个大区间化分成两个可能重合的两个小区间
求f[n,m],可以直接求两个区间的max,达到了O(1)的效果。
什么是倍增的思想? 顾名思义,就是不断翻倍1–>2,2–>4,4–>8
ST表便是让我们能知道每个点之后的 1个长度的最值,2的长度的最值,4个长度的最值,n^2的长度的最值
完美的情况来说,刚好可以把一个大区间分成两个不重合的小区间
记: f数组存区间最值
蓝色为(1,n)的f数组
而绿色是为: 若求 下一层区间最值 所需的另一个区间
比如求(1,8),你已经求得(1,4)的最值,只需要知道个(5,8)的最值,再max两个区间便可求得
会发现在 求f(5) 长度为4的区间最值时,会把f(5,8)求出来
求f(9) 长度为8的时候,会把f(9,16)求出来
即一个区间(n,m)恰好可以分为(n,2^k) (2^k +1,m) (k∈Z) 的两个区间
为什么会这样呢?【白学打死x……】
因为倍增既是二分的反操作,因此可以恰好把一个区间分成两区间
当然不完美的情况,即有重合的情况【即无法将一个区间(n,m)恰好分成(n,2^k) (2^k +1,m)的情况
我们仍可以把这个区间,分成一个(n,k1)(k1< m, k1为n+2^k) 和(k2,m)(k2> n, k2为m-2^k +1)
如上图,可以把(1,6)分为(1,4[2^2])和(3[6-2^2 +1],6)
因此我们建立一个表,存所有倍增划分出来的区间最值,这里便是离线预处理
回答就直接以玄学操作查表并回答
大概的操作便是这样
注意到: 以上讲的都是正着倍增的操作,即1找(1,1)(1,2)(1,4)(1,8)…(1,2^k)的最值
然后2找(2,2)(2,3)(2,4)(2,6)…
但个人写的是倒着倍增的,即1只找(1,1)的最值
2为(2,2)(2,1);3为(3,3)(3,2)(3,1);4为(4,4)(4,3),(4,2),(4,0)的最值
以下都是按倒着倍增来讲的
我们定义个ST表,ST[N][log2N]
ST[i][j]表示第i个数开始, 前面长度为2^j的最值
e.g.ST[13][3]便是(6[13-2^3 +1],13)的最值。【这里+1因为是长度为8,所以6到13长度才为8
至于为什么第二位定位log2N,因为每次倍增的话2^n到N,所以只需定义到log2N
因此易知,所有ST[i][0]便是其本身(ST[i][0]为长度为1范围的最值所以就是本身啦
之后开始正式求ST表了
我们会发现,ST[i][j]=max(ST[i][j-1],ST[i- 2^(j-1)][j-1]) 【这里看不懂先别急后面解释【因为我也很讨厌什么说一大堆无关的然后「我们易得:kjaf89&*^834ho18ikG90)93」就把结论说出来了明明完全看不懂呀_(:зゝ∠)_……
e.g.ST[13][3]求(6,13)的最值,便可从(6,9)(10,13)这两个区间中求出,便刚好为ST[13][2](10[9- 2^2 +1],13)和ST[9 (13- 2^2) ][2](6,9)当中求得
因为ST[i][j]表示i开始前面2^j长度的最值
2^j=2^(j-1)+2^(j-1),即长度为2^j的区间恰好可以分为两个2^(j-1)的区间
代码如下:
void Init()
{
for (int i=1; i<=n; i++) F[i][0]=Num[i]; //刚开始f[n][0],即长度为1的序列中最小的就是它本身。
//从2开始用Dp求
for (int i=2; i<=n; i++)
{
F[i][1]=Work(F[i][0],F[i-1][0]); //这里特殊处理个[i][1],因为2^(1-1)=1,而2<<(1-1)=2,
for (int j=2; j<=Log[i]; j++)
/*
这里自己提前打了个Log表,从来存 这个数最大能存到前面 几个2^k长度 e.g.log2=1;log3=1;log4=2;log5=2;log6=2;log7=2;log8=3;log9=3
3只能存到(2,3),4只能存到(1,4)[长度为4<---2^log[4] ], 9只能存到(2,9)[长度为8<---2^log[9]
*/
F[i][j]=Work(F[i][j-1],F[i- (1<<(j-1))][j-1]); //Dp操作前面已解释
}
}
这里出现了一个Log表,作用已在代码中解释了,那么如何求出这个Log表呢[以下均用log代替log2
void InitLog() //初始化Log数组
{
Log[1]=0,Log[2]=1;
for (int i=3; i<=n; i++) Log[i]=Log[i/2]+1;
}
其实就这么简单几句,因为 logx=logx−log2+1=log(x2)+1
我们已经将所有区间都分成了2^k,根据前面的思路,怎么刚好把这个(l,r)区间拆成(l,l+2^k-1)与(r-2^k,r)这样的两个区间呢?
正确的操作:
这里必定有且仅有一个k使分成的两个区间为两个可能重合的小区间
如果k小1或大1必定会成为下面这两种区间【这里自己想一下吧很好理解orz……
窒息的操作:
嗦了这么多,k到底怎么找呢?……
既要满足
因为r-l+1 代表是这个区间的长度,∴区间一半长度<=2^k<=区间长度,
然后balabala带到公式里会发现这样才刚好成立【其实是你懒得证吧x……【提示了这么多自己应该会证了吧233……
求出k过后,再看上图,应该不难理解直接return max(F[l+2^k-1][k],F[r][k])了吧……
【l+2^k -1的-1不懂的话自己啃啃吧qwq懒癌又犯了……
代码如下:
int Find(int l, int r)
{
int k=Log[r-l+1]; //当这里查询的时候,我们将这个区间化为两个存在于我们ST表的区间,而这里即看(l,l+2^k)∪(l+2^k+1,r)=(l,r),发现k求解=log2(r-l+1)
return Work(F[l+(1<1][k],F[r][k]);
}
#include
#define N 100005
#define Work max
int Log[N],Num[N],F[N][30],n;
using namespace std;
inline int Read()
{
int f=1,num=0;
char t=getchar();
while (t<'0' || t>'9') if (t=='-') f=-1,t=getchar(); else t=getchar();
while (t>='0' && t<='9') num=num*10+t-'0',t=getchar();
return f*num;
}
void InitLog() //初始化Log数组
{
Log[1]=0,Log[2]=1;
for (int i=3; i<=n; i++) Log[i]=Log[i/2]+1;
}
void Init()
{
InitLog();
for (int i=1; i<=n; i++) F[i][0]=Num[i]; //刚开始f[n][0],即长度为1的序列中最小的就是它本身。
//2开始,log2=1;log3=1;log4=2;log5=2;log6=2;log7=2;log8=3;
for (int i=2; i<=n; i++)
{
F[i][1]=Work(F[i][0],F[i-1][0]);//这里特殊处理个1,因为2^(1-1)=1,而2<<(1-1)=2,
for (int j=2; j<=Log[i]; j++)
F[i][j]=Work(F[i][j-1],F[i- (1<<(j-1))][j-1]);
}
}
int Find(int l, int r)
{
//if (l==r) return Num[l]; //这里l==r的时候直接特判【实际上是因为会求解出来k=0造成1<
int k=Log[r-l+1]; //当这里查询的时候,我们将这个区间化为两个存在于我们ST表的区间,而这里即看(l,l+2^k)∪(l+2^k+1,r)=(l,r),发现k求解=log2(r-l+1)
return Work(F[l+(1<1 ][k],F[r][k]);
}
void Test() //输出测试数据而已懒得删_(:зゝ∠)_……
{
for (int i=1; i<=n; i++)
{
cout<<"N is: "<for (int j=0; j<=Log[i]; j++) cout<" ";
cout<int main()
{
int q,l,r;
n=Read(),q=Read();
for (int i=1; i<=n; i++) Num[i]=Read();
Init();
//Test();
while (q--)
{
l=Read(),r=Read();
printf("%d\n",Find(l,r));
}
}
最后祝您,身体健康题题Ac,再见……