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)。但當資料量非常大且查詢很頻繁時,該演算法也許會存在問題。
首先是預處理,用動態規劃(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。 舉例說明,要求區間[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是樹節點個數)。
(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。
【舉例說明】
t=,其中v=,e=,且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表示詢問 的次數。更多關於並查集的資料,可閱讀這篇文章:《資料結構之並查集》。
lca(u)
checked[u]=true
對於每個(u,v)屬於p // (u,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演算法》
———————————————————————————————-
更多關於資料結構和演算法的介紹,請檢視:資料結構與演算法彙總
———————————————————————————————-
,作者介紹:
本部落格的文章集合:
dfs, lca, rmq, st, tarjan演算法, 區間最值查詢, 最近公共祖先, 深度優先遍歷, 稀疏表
LCA與RMQ演算法
初始化 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 ...
RMQ問題與LCA問題
一 區間最小 最大查詢 range minimum maximum query rmq 問題 toj 2762 描述 已知長度為l 的數列a 詢問區間 l,r 中的最值。若詢問的次數較少,可以用線性的複雜度來查詢,但如果詢問的次數過多且l 過大,那麼複雜度就會很高。所以需要更快速的查詢方法。st 演...
LCA和RMQ問題雜談
首先 mathrm 問題指的是求解樹上兩點的最近公共祖先,mathrm 問題指的是求解數列區間最值。mathrm 問題轉 mathrm 問題應該是人盡皆知了,我們可以先跑出樹的 mathrm 序,使用每次進入或回到節點都記錄一次的那種 mathrm 序,那麼只需記錄每個節點第一次出現位置就可以查詢了...