筆記:RMQ(區間最值)之ST算法
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 8和6 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算法