1. 程式人生 > >筆記:RMQ(區間最值)之ST算法

筆記:RMQ(區間最值)之ST算法

運算 不變 想要 parse 計算機語言 c++ 是我 動態規劃 容易

RMQ(區間最值)之ST算法

RMQ即Range Minimum/Maximun Query 中文意思:查詢一個區間的最小值/最大值

比如有這樣一個數組:A{3 2 4 5 6 8 1 2 9 7},然後問你若幹問題:

數組A下標2~7區間最小的值是多少? 最小值是(1)

數組A下標3~6區間最小的值是多少? 最小值是(4)

數組A下標1~10區間最小的值是多少? 最小值是(1)

......

專業術語:對於長度為n的數列A,回答若幹詢問RMQ(A,i,j)(i,j<=n),返回數列A中下標在i,j之間的最小/大值。

這個問題其實可以用線段樹輕松實現O(N)預處理,O(logN)查詢,已經是很不錯的復雜度了。

但是:有的時候查詢操作很多,所以我們需要一個O(1)的查詢算法。

那就是Sparse Table 即ST算法

ST算法是比較高效的在線算法。所謂在線算法,是指用戶每輸入一個查詢便馬上處理一個查詢。該算法一般用較長的時間做預處理,待信息充足以後便可以用較少的時間回答每個查詢。ST算法是一個非常有名的在線處理RMQ問題的算法,它可以在O(nlogn)時間內進行預處理,然後在O(1)時間內回答每個查詢。

(一)首先是預處理,用動態規劃(DP)解決。

A[i]是要求區間最值的數列,F[i, j]表示從i開始的連續2^j個數中的最大值。(DP的狀態)

例如:A數列為:3 2 4 5 6 8 1 2 9 7 12 3 21

下標為:1 2 3 4 5 6 7 8 9 10 11 12 13

F[1,0]表示第1個數起,長度為2^0=1的最大值,其實就是A數列中3這個數。同理:

F[1, 1] = max(3,2) = 3,

F[1,2]=max(3,2,4,5) = 5,

F[3,2]=max(4,5,6,8) = 8, F[3,2]表示從第三個數4開始連續(2^2)4個數中的最大值8

F[1,3] = max(3,2,4,5,6,8,1,2) = 8;

並且我們可以容易的看出F[i,0]就等於A[i]。(DP的初始值)

假如i=3,那麽F[i, 0]=max(4,4)=4, A[3]=4, 所以DP的初值就是F[i,0]=A[i]

這樣,DP的狀態、初值都已經有了,剩下的就是狀態轉移方程。

我們把F[i,j]平均分成兩段(因為F[i,j]一定是偶數個數字),

前半段為 i 到i + 2 ^ (j - 1) - 1 (請動手計算一下)

後半段為i + 2 ^ (j - 1)i + 2 ^ j - 1 需要註意的是2 ^ (j - 1) 2 ^ j - 1 的區別

前後段的長度都為2 ^ (j - 1)。

用上例說明,當i=1,j=3時就是3,2,4,5 和 6,8,1,2這兩段。

F[i,j]就是這兩段各自最大值中的最大值。

於是我們得到了狀態轉移方程:

f[i,j]=max(f[i,j-1],f[i+2^(j-1),j-1]);

方程前半段為f[i,j-1] 表示 i 起連續2^(j-1)個數的最大值。

方程後半段f[i+2^(j-1),j-1] 表示i + 2 ^ (j - 1)起連續2^(j-1)個數據的最大值。

然後取前後兩段各自最大值中的最大值。

總結:求從i起連續2^j個數的最大值,就是把2^j 分成前後兩個2^(j-1),分別取其最大值,再通過比較獲得此狀態最大值。

我們先來學習個小知識點“<<”位移符號的使用

C或C++中:

<<可作為左移運算符 (向左移一位,右邊自動補0)

比如:i<<4,是按位左移4位。

比如8<<4:

二進制狀態下:

0000 0000 0000 1000 = 十進制8

0000 0000 1000 0000 = 十進制128 128=8^4

十進制狀態下:

1<<1 等價於 1*2^1

1<<2 等價於 1*2^2

3<<3 等價於 3*2^3

5<<4 等價於 5*2^4

聽說位移"<<" 速度比乘法快。

註意+-運算符優先於<<符號

比如:5-1<<1等同(5-1)*2^1 ,而不是5-1*2^1

所以如果要表示 5-1*2^1,要寫成5-(1<<1)

DP預處理的代碼如下:請選擇一種適合自己的方法作為模板。

//第一種寫法:請理解好循環條件的意義

int n;  //n是數組元素個數
int a[100004]; //數列
int f[100004][30]; //f[i][j]
//f[i][j]表示以i為起點,區間長度為2^j的一段區間的最小(大)值
void RMQ_ST (  )   ////預處理ST表,數組中共n個元素  O(nlogn)  
{  
for (int i=1;i<=n;i++)   f[i][0]=a[i];//dp初始值
    for(int j=1;j<=20;j++)  //為什麽是20? 2^j最大可以是多大?
        for ( int i = 1; i <= n; i++ )  //思考為什麽外循環j套i,而不是外循環i套j
            if(i+(1<<j)-1<=n)  //“<<”符號請看前面知識點 (1<<j)註意括號
            {  
              int s=i+(1<<(j-1)); //後半段的起點  等價 i+2^(j-1)
              f [i][j]=max ( f [i][j-1], f [s][j-1] );  //區間最大值,最小值用min函數
            }  
}

//第二種寫法:請理解好循環條件的意義

void RMQ_ST2 ( )   //預處理ST表,數組中共n個元素  O(nlogn)
{
for (int i=1;i<=n;i++)   f[i][0]=a[i];  //dp初始值
for (int j=1;(1<<j)<=n; j++)         //註意(1<<j)加上括號
   for (int i=1;i+(1<<j)-1<=n;i++)  //
      f [i][j]=max( f [i] [j-1], f [i+(1<<(j-1))] [j-1] );//求最小值是函數min
 }

這裏我們需要註意的是循環的順序,我們發現外層是j,內層所i,這是為什麽呢?

可以是i在外,j在內嗎?

答案是不可以。因為我們需要理解這個狀態轉移方程的意義。

狀態轉移方程的含義是:我們先得到F[i,0](即1個元素)的值,然後通過2個1個元素的最值,獲得所有長度為F[i,1](即2個元素的最值),然後再通過2個2個元素的最值,獲得所有長度為F[i,2](即4個元素的最值),以此類推更新所有長度的最值。

而如果是i在外,j在內的話,我們更新的順序就是F [1,0],F [1,1],F [1,2],F [1,3],表示更新從1開始的1個元素,2個元素,4個元素,8個元素(a[0],a[1],....a[7])的最值,這跟我們的思維和計算方法完全不同,所以這樣的方法肯定是錯誤的。

為了避免這樣的錯誤,一定要好好理解這個狀態轉移方程所代表的含義。

(二)接著解決怎麽樣查詢。

先看小知識:

冪:指乘方運算的結果。nm指將n自乘m次(針對m為正整數的場合)。把nm看作乘方的結果,叫做“n的m次冪”或“n的m次方”。

其中,n稱為“底數”,m稱為“指數”(寫成上標)。當不能用上標時,通常寫成n^m。

對數:如果 技術分享圖片,即a的x次方等於N(a>0,且a≠1),那麽x叫做以a為底N的對數,記作技術分享圖片。其中,a叫做對數的底數N叫做真數

特別註意:如果在c或c++語言裏,註意加上頭文件cmath或math.h。

對數形式應該寫成x=loga (N)其中a是底數(N)是真數,真數N一定要用()

比如x=log2 (8),那麽x=3。註意:log和2之間不要有空格。

還可以寫成另外一種形式:x=log(N) / log(a),真數和底數都要加括號,而且真數在前面,不建議用這個形式。

查詢:

假設要查詢從(i, j)這一段的最, 這個區間的長度我們可以計算得出是j - i + 1

那麽我們先求出一個最大的k, 使得k滿足2^k <= (j - i + 1).

我們可以取k=log2( j - i + 1),舉例說明:

要求區間[1,5]的最大值,k =(int)(log2(5 - 1 + 1))= 2。(計算機語言寫法),

(int)是用來向下取整。(想想為什麽要向下取整)

我們就可以把[i, j]分成兩個(部分重疊的)長度為2^k的區間: [i, i+2^k-1], [j-2^k+1, j];

比如查詢數列A:5,6,7,8,9,我們可以查詢(1, 4) 和(2, 5)這兩個區間。

就是查詢5 6 7 86 7 8 9 ,原數列左邊界i和右邊界j是不變的, 只是中間的三個數6 7 8重疊。

又比如數列B: 4 5 6 7 8 9 經計算數列B長度為 j-i+1=6,那麽:

k=int(log2(j-i+1))=2 (int)是用來向下取整。(想想為什麽要向下取整)

[i, i+2^k-1]區間計算後為[1,4] 包含元素為 4 5 6 7共4個。

[j-2^k+1, j]區間計算後為[3,6] 包含元素為 6 7 8 9 也是4個。

而我們之前dp預處理時已經求出了

f(i, k)區間[i, i+2^k-1]的最, 比如上面的f(1, 4)我們在預處理時已經求出來了。

f(j-2^k+1, k)區間[j-2^k+1, j]的最比如上面的f(2, 5)我們在預處理時也已經求出來了。

我們只要返回其中更的那個, 就是我們想要的答案, 這個算法的時間復雜度是O(1)的.

則有:RMQ(A, i, j)=max{ f [i , k], f [ j - 2 ^ k + 1, k]}。(偽代碼)

又比如:要求區間[2,8]的最大值,k =(int)( log2(8 - 2 + 1))= 2

即求max (f [2, 2],f [8 - 2 ^ 2 + 1, 2]) = max (f [2, 2],f[5, 2]);

f [2, 2]和f[5, 2]在前面已經預處理過了

請計算一下區間[1 ,11]的最大值,像上面一樣手寫過程。

查詢代碼如下:

int rmq_ask (int l, int r) //查詢區間(l, r)
{
int k=(int) (log2(r-l+1));  
//l->r區間長度---為2^k(向下取整) 千萬記住對數一定要整體括起來
    return max (f[l][k], f[r-(1<<k)+1][k]); //根據需要取min或者max
}

ST算法解RMQ模板(洛谷1816 忠誠)

https://www.luogu.org/problemnew/show/P1816

P3865 【模板】ST表

https://www.luogu.org/problemnew/show/P3865

P3379 【模板】最近公共祖先(LCA)請用RMQ

https://www.luogu.org/problemnew/show/P3379

參考文章:

http://blog.csdn.net/qq_35776409/article/details/62890728

http://blog.csdn.net/u012860063/article/details/40752197

http://blog.csdn.net/qq1169091731/article/details/51981497

https://wenku.baidu.com/view/6a7d691aa8114431b90dd877.html

筆記:RMQ(區間最值)之ST算法