一、 概述php
LCA(Least Common Ancestors),即最近公共祖先,是指這樣一個問題:在有根樹中,找出某兩個結點u和v最近的公共祖先(另外一種說法,離樹根最遠的公共祖先)。 RMQ(Range Minimum/Maximum Query),即區間最值查詢,是指這樣一個問題:對於長度爲n的數列A,回答若干詢問RMQ(A,i,j)(i,j<=n),返回數列A中下標在i,j之間的最小/大值。這兩個問題是在實際應用中常常遇到的問題,本文介紹了當前解決這兩種問題的比較高效的算法。html
二、 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]獲得。數據結構
算法僞代碼:數據結構和算法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
|
//初始化
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),具體可閱讀這篇文章:《數據結構之線段樹》。wordpress
三、 LCA算法ui
對於該問題,最容易想到的算法是分別從節點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,EG},且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所在集合的祖先。
算法僞代碼:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
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)所有存儲,這樣就能保證完整性。
四、 總結
LCA和RMQ問題是兩個很是基本的問題,不少複雜的問題均可以轉化這兩個問題解決,這兩個問題在ACM編程競賽中遇到的尤爲多。這兩個問題的解決方法中用到不少很是基本的數據結構和算法,包括並查集,深度優先遍歷,動態規劃等。
五、 參考資料
(1) 判斷兩個鏈表是否相交
(3) 博文《Range Minimum Query and Lowest Common Ancestor》
(4) 博文《LCA問題(最近公共祖先問題)+ RMQ問題》
———————————————————————————————-
更多關於數據結構和算法的介紹,請查看:數據結構與算法彙總
———————————————————————————————-
原創文章,轉載請註明: 轉載自董的博客