演算法之LCA與RMQ問題
1、 概述
LCA(Least Common Ancestors),即最近公用祖先,是指這樣一個問題:在有根樹中,找出某兩個結點u和v最近的公用祖先(另一種說法,離樹根最遠的公用祖先)。 RMQ(Range Minimum/Maximum Query),即區間最值查詢,是指這樣一個問題:對於長度為n的數列A,回答若干詢問RMQ(A,i,j)(i,j<=n),返回數列A中下標在i,j之間的最小/大值。這兩個問題是在實際應用中經常遇到的問題,本文介紹了當前解決這兩種問題的比較高效的演算法。
2、 RMQ演算法
對於該問題,最容易想到的解決方案是遍曆,複雜度是O(n)。但當資料量非常大且查詢很頻繁時,該演算法也許會存在問題。
本節介紹了一種比較高效的線上演算法(ST演算法)解決這個問題。所謂線上演算法,是指使用者每輸入一個查詢便馬上處理一個查詢。該演算法一般用較長的時間做預先處理,待資訊充足以後便可以用較少的時間回答每個查詢。ST(Sparse Table)演算法是一個非常有名的線上處理RMQ問題的演算法,它可以在O(nlogn)時間內進行預先處理,然後在O(1)時間內回答每個查詢。
首先是預先處理,用動態規劃(DP)解決。設A[i]是要求區間最值的數列,F[i, j]表示從第i個數起連續2^j個數中的最大值。例如數列3 2 4 5 6 8 1 2 9 7,F[1,0]表示第1個數起,長度為2^0=1的最大值,其實就是3這個數。 F[1,2]=5,F[1,3]=8,F[2,0]=2,F[2,1]=4……從這裡可以看出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))。用上例說明,當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])。
然後是查詢。取k=[log2(j-i+1)],則有:RMQ(A, i, j)=min{F[i,k],F[j-2^k+1,k]}。 舉例說明,要求區間[2,8]的最大值,就要把它分成[2,5]和[5,8]兩個區間,因為這兩個區間的最大值我們可以直接由f[2,2]和f[5,2]得到。
演算法虛擬碼:
//初始化 INIT_RMQ //max[i][j]中存的是重j開始的2^i個資料中的最大值,最小值類似,num中存有數組的值 for i : 1 to n max[0][i] = num[i] for i : 1 to log(n)/log(2) for j : 1 to (n+1-2^i) max[i][j] = MAX(max[i-1][j], max[i-1][j+2^(i-1)] //查詢 RMQ(i, j) k = log(j-i+1) / log(2) return MAX(max[k][i], max[k][j-2^k+1])
當然,該問題也可以用線段樹(也叫區間樹)解決,演算法複雜度為:O(N)~O(logN),具體可閱讀這篇文章:《資料結構之線段樹》。
3、 LCA演算法
對於該問題,最容易想到的演算法是分別從節點u和v回溯到根節點,擷取u和v到根節點的路徑P1,P2,其中P1和P2可以看成兩條單鏈表,這就轉換成常見的一道面試題:【判斷兩個單鏈表是否相交,如果相交,給出相交的第一個點。】。該演算法總的複雜度是O(n)(其中n是樹節點個數)。
本節介紹了兩種比較高效的演算法解決這個問題,其中一個是線上演算法(DFS+ST),另一個是離線演算法(Tarjan演算法)。
線上演算法DFS+ST描述(思想是:將樹看成一個無向圖,u和v的公用祖先一定在u與v之間的最短路徑上):
(1)DFS:從樹T的根開始,進行深度優先遍曆(將樹T看成一個無向圖),並記錄下每次到達的頂點。第一個的結點是root(T),每經過一條邊都記錄它的端點。由於每條邊恰好經過2次,因此一共記錄了2n-1個結點,用E[1, ... , 2n-1]來表示。
(2)計算R:用R[i]表示E數組中第一個值為i的元素下標,即如果R[u] < R[v]時,DFS訪問的順序是E[R[u], R[u]+1, …, R[v]]。雖然其中包含u的後代,但深度最小的還是u與v的公用祖先。
(3)RMQ:當R[u] ≥ R[v]時,LCA[T, u, v] = RMQ(L, R[v], R[u]);否則LCA[T, u, v] = RMQ(L, R[u], R[v]),計算RMQ。
由於RMQ中使用的ST演算法是線上演算法,所以這個演算法也是線上演算法。
【舉例說明】
T=<V,E>,其中V={A,B,C,D,E,F,G},E={AB,AC,BD,BE,EF,EH},且A為樹根。則圖T的DFS結果為:A->B->D->B->E->F->E->G->E->B->A->C->A,要求D和G的最近公用祖先, 則LCA[T, D, G] = RMQ(L, R[D], R[G])= RMQ(L, 3, 8),L中第4到7個元素的深度分別為:1,2,3,3,則深度最小的是B。
離線演算法(Tarjan演算法)描述:
所謂離線演算法,是指首先讀入所有的詢問(求一次LCA叫做一次詢問),然後重新組織查詢處理順序以便得到更高效的處理方法。Tarjan演算法是一個常見的用於解決LCA問題的離線演算法,它結合了深度優先遍曆和並查集,整個演算法為線性處理時間。
Tarjan演算法是基於並查集的,利用並查集優越的時空複雜度,可以實現LCA問題的O(n+Q)演算法,這裡Q表示詢問 的次數。更多關於並查集的資料,可閱讀這篇文章:《資料結構之並查集》。
同上一個演算法一樣,Tarjan演算法也要用到深度優先搜尋,演算法大體流程如下:對於新搜尋到的一個結點,首先建立由這個結點構成的集合,再對當前結點的每一個子樹進行搜尋,每搜尋完一棵子樹,則可確定子樹內的LCA詢問都已解決。其他的LCA詢問的結果必然在這個子樹之外,這時把子樹所形成的集合與當前結點的集合合并,並將當前結點設為這個集合的祖先。之後繼續搜尋下一棵子樹,直到當前結點的所有子樹搜尋完。這時把當前結點也設為已被檢查過的,同時可以處理有關當前結點的LCA詢問,如果有一個從當前結點到結點v的詢問,且v已被檢查過,則由於進行的是深度優先搜尋,當前結點與v的最近公用祖先一定還沒有被檢查,而這個最近公用祖先的包涵v的子樹一定已經搜尋過了,那麼這個最近公用祖先一定是v所在集合的祖先。
演算法虛擬碼:
LCA(u) { Make-Set(u) ancestor[Find-Set(u)]=u 對於u的每一個孩子v { LCA(v) Union(u,v) ancestor[Find-Set(u)]=u } checked[u]=true 對於每個(u,v)屬於P // (u,v)是被詢問的點對 { if checked[v]=true then { 回答u和v的最近公用祖先為ancestor[Find-Set(v)] } } }
【舉例說明】
根據實現演算法可以看出,只有當某一棵子樹全部遍曆處理完成後,才將該子樹的根節點標記為黑色(初始化是白色),假設程式按上面的樹形結構進行遍曆,首先從節點1開始,然後遞迴處理根為2的子樹,當子樹2處理完畢後,節點2, 5, 6均為黑色;接著要回溯處理3子樹,首先被染黑的是節點7(因為節點7作為葉子不用深搜,直接處理),接著節點7就會查看所有詢問(7, x)的節點對,假如存在(7, 5),因為節點5已經被染黑,所以就可以斷定(7, 5)的最近公用祖先就是find(5).ancestor,即節點1(因為2子樹處理完畢後,子樹2和節點1進行了union,find(5)返回了合并後的樹的根1,此時樹根的ancestor的值就是1)。有人會問如果沒有(7, 5),而是有(5, 7)詢問對怎麼處理呢? 我們可以在程式初始化的時候做個技巧,將詢問對(a, b)和(b, a)全部儲存,這樣就能保證完整性。
4、 總結
LCA和RMQ問題是兩個非常基本的問題,很多複雜的問題都可以轉化這兩個問題解決,這兩個問題在ACM編程競賽中遇到的尤其多。這兩個問題的解決方案中用到很多非常基本的資料結構和演算法,包括並查集,深度優先遍曆,動態規劃等。
5、 參考資料
(1) 判斷兩個鏈表是否相交
(2) 博文《LCA問題(含RMQ的ST演算法)》
(3) 博文《Range Minimum Query and Lowest Common Ancestor》
(4) 博文《LCA問題(最近公用祖先問題)+ RMQ問題》
(5) 博文《最近公用祖先(LCA)的Tarjan演算法》
(6) 博文《LCA 最近公用祖先的Tarjan演算法》
轉載自董的部落格
本文連結地址: http://dongxicheng.org/structure/lca-rmq/