程式設計之法 面試和演算法心得 最近公共祖先LCA問題

2021-09-16 19:55:05 字數 4049 閱讀 3502

小結:

暴力

tarjan演算法

是乙個找強連通分量的演算法。dfs+並查集,每次將兩個節點對的最近公共祖先的查詢儲存起來,然後dfs更新一次。

複雜度:o(n + q), q為查詢個數

rmq(沒看)

但前面暴力求a[i,j]最小值的位子可以考慮一下: 1. 普通的o(n^3) 2. 動態規劃將為o(n^2)

線段樹

用m[i]儲存節點i區間的最小值的位置,其中左節點為2*i, 右節點為2*i+1

複雜度, 構建o(n),查詢o(logn)

求有根樹的任意兩個節點的最近公共祖先

直觀的做法,可能是針對是否為二叉查詢樹分情況討論,這也是一般人最先想到的思路。除此之外,還有所謂的tarjan演算法、倍增演算法、以及轉換為rmq問題(求某段區間的極值)

1.1 是二叉查詢樹

演算法:從root開始:

**://modified by july 2014

public int query(node t, node u, node v)

while (true) else if (t.value > right) else

}

}

1.2 不是二叉查詢樹

我們可以從任何乙個節點出發,得到乙個到達根節點的單向鍊錶。因此這個問題轉換成兩個單向鍊錶的第乙個公共節點。

此外,如果給出根節點,lca問題可以用遞迴很快解決。關於樹的問題一般都可以轉換成遞迴。

參考**如下:

node* getelca(node* root, node* node1, node* node2)

然不論是針對普通的二叉樹,還是針對二叉查詢樹,上面的解法有乙個很大的弊端就是:如需n 次查詢,則總體複雜度會擴大n 倍,故這種暴力解法僅適合一次查詢,不適合多次查詢

2.1 tarjan演算法

tarjan演算法是乙個在圖中尋找強連通分量的演算法。演算法的基本思想為:任選一節點開始進行深度優先搜尋dfs(若深度優先搜尋結束後仍有未訪問的節點,則再從中任選一點再次進行)。搜尋過程中已訪問的節點不再訪問。搜尋樹的若干子樹構成了圖的強連通分量。

對於lca問題:對於新搜尋到的乙個節點u,先建立u構成的集合,在對u的每棵子樹進行搜尋,每搜尋完一棵子樹,這時候子樹中所有的節點的最近公共祖先就是u了

舉例,如下圖(不同顏色的節點相當於不同的集合):

假設遍歷完10的孩子,要處理關於10的請求了,取根節點到當前正在遍歷的節點的路徑為關鍵路徑,即1-3-8-10,集合的祖先便是關鍵路徑上距離集合最近的點。

比如:1,2,5,6為乙個集合,祖先為1,集合中點和10的lca為1

3,7為乙個集合,祖先為3,集合中點和10的lca為3

8,9,11為乙個集合,祖先為8,集合中點和10的lca為8

10,12為乙個集合,祖先為10,集合中點和10的lca為10

得出的結論便是:lca(u,v)便是根至u的路徑上到節點v最近的點。

2.2 tarjan演算法流程

procedure dfs

(u):

begin

設定u號節點的祖先為u

若u的左子樹不為空,

dfs(u -> 左子樹)

; 若u的右子樹不為空,dfs

(u -> 右子樹)

; 訪問每一條與u相關的詢問u,v

若v已經被訪問過,則輸出v的當前祖先t

(t 即u, v的lca)

標記u為已經訪問,將所有u的孩子包括u本身的祖先改為u的父親

普通的dfs 不能直接解決lca問題,故tarjan演算法的原理是dfs + 並查集它每次把兩個結點對的最近公共祖先的查詢儲存起來,然後dfs 更新一次。如此,利用並查集優越的時空複雜度,此演算法的時間複雜度可以縮小至o(n+q),其中,n為資料規模,q為詢問個數。

參考轉換為rmq問題,用sparse table(簡稱st)演算法解決。

rmq問題

rmq問題:

全稱為range minimum query,顧名思義,則是區間最值查詢,它被用來在陣列中查詢兩個指定索引中最小值的位置。即rmq相當於給定陣列a[0, n-1],找出給定的兩個索引如 i、j 間的最小值的位置

假設我們定義:

假設乙個演算法預處理時間為 f(n),查詢時間為g(n),那麼這個演算法複雜度的標記為。我們將用rmqa(i, j) 來表示陣列a 中索引i 和 j 之間最小值的位置。 u和v的離樹t根結點最遠的公共祖先用lca t(u, v)表示。

舉例:如下圖所示,rmqa(2,7 )則表示求陣列a中從a[2]~a[7]這段區間中的最小值:

很顯然,從上圖中,我們可以看出最小值是a[3] = 1,所以也就不難得出最小值的索引值rmqa(2,7) = 3。

rmq問題解決

1. trivial algorithm for rmq

我們對對每一對索引(i, j),將陣列中索引i 和 j 之間最小值的位置 rmqa(i, j) 儲存在m[0, n-1][0, n-1]表中。

先引出3種計算方法:

普通的計算將得到乙個的演算法。通過乙個簡單的動態規劃,我們可以將複雜度降為

//modified by july 2014

void process1(int m[maxn][maxn], int a[maxn], int n)

乙個比較有趣的點子是把向量分割成sqrt(n)大小的段。我們將在m[0,sqrt(n)-1]為每乙個段儲存最小值的位置。如此,m可以很容易的在o(n)時間內預處理。

3. 乙個更好的方法預處理rmq 是對2^k 的長度的子陣列進行動態規劃。我們將使用陣列m[0, n-1][0, logn]進行儲存,其中m[ i ][ j ] 是以i 開始,長度為 2^j 的子陣列的最小值的索引。這就引出了咱們接下來要介紹的sparse table (st) algorithm。

2. sparse table(st) algorithm

…(待看)

線段樹是乙個類似堆的資料結構,可以在基於區間陣列上用對數時間進行更新和查詢操作。我們用下面遞迴方式來定義線段樹的[i, j]區間:

呼叫函式時使用node=1, b = 0, e = n -1

void initialize(int node, int b, int e, int m[maxind], int a[maxn], int n)

}

int

query

(int node,

int b,

int e,

int m[maxind]

,int a[maxn]

,int i,

int j)

可以很容易的看出任何查詢都可以在o(log n)內完成。注意當我們碰到完整的in/out區間時我們停止了,因此數中的路徑最多**一次。用線段樹我們獲得了的演算法

線段樹非常強大,不僅僅是因為它能夠用在rmq上,還因為它是乙個非常靈活的資料結構,它能夠解決動態版本的rmq問題和大量的區間搜尋問題。

除此之外,還有倍增法、重鏈剖分演算法和後序遍歷也可以解決該問題。其中,倍增思路相當於層序遍歷,逐層或幾層跳躍查,查詢時間複雜度為o(log n),空間複雜度為nlogn,對於每個節點先儲存向上1層2層4層的節點,每個點有depth資訊。

程式設計之法 面試和演算法心得 筆記

一 字串翻轉 三步反轉法,先將兩部分分別反轉,然後再整體反轉。abcdef defabc 1 cbadef 旋轉前一部分 2 cbafed 旋轉後一部分 3 defabc 整個旋轉 那麼將 i am a student.studnet.a am i 也類似。二 字串的包含 a abcxyzlmnop...

程式設計之法 面試和演算法心得(奇偶調序)

輸入乙個整數陣列,調整陣列中數字的順序,使得所有奇數字於陣列的前半部分,所有偶數字於陣列的後半部分。要求時間複雜度為o n 最容易想到的辦法是從頭掃瞄這個陣列,每碰到乙個偶數,拿出這個數字,並把位於這個數字後面的所有數字往前挪動一位。挪完之後在陣列的末尾有乙個空位,然後把該偶數放入這個空位。由於每碰...

程式設計之法 面試和演算法心得(最大連續子陣列和)

輸入乙個整形陣列,陣列裡有正數也有負數。陣列中連續的乙個或多個整數組成乙個子陣列,每個子陣列都有乙個和。求所有子陣列的和的最大值,要求時間複雜度為o n 例如輸入的陣列為1,2,3,10,4,7,2,5,和最大的子陣列為3,10,4,7,2,因此輸出為該子陣列的和18。求乙個陣列的最大子陣列和,我想...