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]得到。
演算法偽**:
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),具體可閱讀這篇文章:《資料結構之線段樹》。
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表示詢問 的次數。更多關於並查集的資料,可閱讀這篇文章:《資料結構之並查集》。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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演算法》
RMQ問題(區間最值查詢)
有一類問題被稱作區間最值問題,描述的是,給定 n 個元素,需要查詢下標位於 p q 之間的最大 小值。首先確定,針對每一次查詢,肯定是不能動態求最值的,因為每次都要計算,可能造成比較多的時間耗費。有 一種比較好的解決辦法是,先得到所有結果,在查詢時直接取出結果。這樣,就需要一種資料結構,能夠覆蓋所有...
RMQ 區間最值 模板
rmq 的全稱為range max min query。構造dp陣列的時間為o nlogn 但是查詢時間為o 1 所以當資料量小於logn時,用樸素遍歷找最值就好,但是請求次數大於logn時,就要用這個模板。利用動態規劃的思想。int order maxn 使每乙個2 order i i 2 ord...
RMQ求區間最值
rmq演算法,是乙個快速求區間最值的離線演算法,預處理時間複雜度o n log n 查詢o 1 所以是乙個很快速的演算法,當然這個問題用線段樹同樣能夠解決。問題 給出n個數ai,讓你快速查詢某個區間的的最值。一 首先是預處理,用動態規劃 dp 解決。設a i 是要求區間最值的數列,f i,j 表示從...