天天看點

[洛谷P3865] ST表ST表——RMQ問題

洛谷P3865

ST表——RMQ問題

RMQ問題:

RMQ問題就是指求區間最值的問題。

這裡的解決方法采用ST表

思想:

首先先回顧一下線段樹的想法,将一個數列不斷二分,并記錄目前管轄區間内的最值。

[洛谷P3865] ST表ST表——RMQ問題

但線段樹查詢操作并不是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)的效果。

[洛谷P3865] ST表ST表——RMQ問題

什麼是倍增的思想? 顧名思義,就是不斷翻倍1–>2,2–>4,4–>8

ST表便是讓我們能知道每個點之後的 1個長度的最值,2的長度的最值,4個長度的最值,n^2的長度的最值

完美的情況來說,剛好可以把一個大區間分成兩個不重合的小區間

[洛谷P3865] ST表ST表——RMQ問題

記: 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)

[洛谷P3865] ST表ST表——RMQ問題

如上圖,可以把(1,6)分為(1,4[2^2])和(3[6-2^2 +1],6)

是以我們建立一個表,存所有倍增劃分出來的區間最值,這裡便是離線預處理

回答就直接以玄學操作查表并回答

大概的操作便是這樣

實作

建立ST表

注意到: 以上講的都是正着倍增的操作,即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)的區間

[洛谷P3865] ST表ST表——RMQ問題

便可以Dp的思想,将ST表求出來

代碼如下:

void Init()
{
    for (int i=; i<=n; i++) F[i][]=Num[i];    //剛開始f[n][0],即長度為1的序列中最小的就是它本身。
    //從2開始用Dp求 
    for (int i=; i<=n; i++)
    { 
        F[i][]=Work(F[i][],F[i-][]);    //這裡特殊處理個[i][1],因為2^(1-1)=1,而2<<(1-1)=2, 
        for (int j=; 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-],F[i- (<<(j-))][j-]);  //Dp操作前面已解釋
    }
}
           

這裡出現了一個Log表,作用已在代碼中解釋了,那麼如何求出這個Log表呢[以下均用log代替log2

void InitLog()  //初始化Log數組 
{
    Log[]=,Log[]=;
    for (int i=; i<=n; i++) Log[i]=Log[i/]+;
}
           

其實就這麼簡單幾句,因為 logx=logx−log2+1=log(x2)+1

查詢ST表

我們已經将所有區間都分成了2^k,根據前面的思路,怎麼剛好把這個(l,r)區間拆成(l,l+2^k-1)與(r-2^k,r)這樣的兩個區間呢?

正确的操作:

[洛谷P3865] ST表ST表——RMQ問題

這裡必定有且僅有一個k使分成的兩個區間為兩個可能重合的小區間

如果k小1或大1必定會成為下面這兩種區間【這裡自己想一下吧很好了解orz……

窒息的操作:

[洛谷P3865] ST表ST表——RMQ問題

嗦了這麼多,k到底怎麼找呢?……

既要滿足

l<=r−2k<=l+2k−1<=r

【可看上圖了解……

會易得 k=Log[r-l+1]

e.g. (2,7),k=Log[6]=2; 驗證: (2,5)(4,8)剛好滿足

因為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+];   //當這裡查詢的時候,我們将這個區間化為兩個存在于我們ST表的區間,而這裡即看(l,l+2^k)∪(l+2^k+1,r)=(l,r),發現k求解=log2(r-l+1) 
    return Work(F[l+(<<k)-][k],F[r][k]);
}
           

最後Ac代碼:

#include<bits/stdc++.h>
#define N 100005
#define Work max
int Log[N],Num[N],F[N][],n;
using namespace std;
inline int Read()
{
    int f=,num=;
    char t=getchar();
    while (t<'0' || t>'9') if (t=='-') f=-,t=getchar(); else t=getchar();
    while (t>='0' && t<='9') num=num*+t-'0',t=getchar();
    return f*num;
}
void InitLog()  //初始化Log數組 
{
    Log[]=,Log[]=;
    for (int i=; i<=n; i++) Log[i]=Log[i/]+;
}
void Init()
{
    InitLog();
    for (int i=; i<=n; i++) F[i][]=Num[i];    //剛開始f[n][0],即長度為1的序列中最小的就是它本身。
    //2開始,log2=1;log3=1;log4=2;log5=2;log6=2;log7=2;log8=3; 
    for (int i=; i<=n; i++)
    { 
        F[i][]=Work(F[i][],F[i-][]);//這裡特殊處理個1,因為2^(1-1)=1,而2<<(1-1)=2, 
        for (int j=; j<=Log[i]; j++) 
        F[i][j]=Work(F[i][j-],F[i- (<<(j-))][j-]);
    }
}
int Find(int l, int r)
{
    //if (l==r) return Num[l]; //這裡l==r的時候直接特判【實際上是因為會求解出來k=0造成1<<k=1,即2^k=0出錯…… //後面發現完全不用自己當時阿庫娅了……//雖然也不知道為啥但是debug看出來的确不用orz……
    int k=Log[r-l+];   //當這裡查詢的時候,我們将這個區間化為兩個存在于我們ST表的區間,而這裡即看(l,l+2^k)∪(l+2^k+1,r)=(l,r),發現k求解=log2(r-l+1) 
    return Work(F[l+(<<k)-][k],F[r][k]);
}
void Test() //輸出測試資料而已懶得删_(:зゝ∠)_……
{
    for (int i=; i<=n; i++)
    {
        cout<<"N is: "<<i<<endl; 
        for (int j=; j<=Log[i]; j++) cout<<F[i][j]<<" ";
        cout<<endl;
    }
}
int main()
{
    int q,l,r;
    n=Read(),q=Read();
    for (int i=; i<=n; i++) Num[i]=Read();
    Init();
    //Test();
    while (q--)
    {
        l=Read(),r=Read();
        printf("%d\n",Find(l,r));
    }
}
           

最後祝您,身體健康題題Ac,再見……