LCA-Tarjan,RMQ,倍增演算法超詳細原理講解+python實踐(Lowest Common Ancestor of a Binary Tree)
最近公共祖先演算法:
通常解決這類問題有兩種方法:線上演算法和離線演算法
線上演算法:每次讀入一個查詢,處理這個查詢,給出答案
離線演算法:一次性讀入所有查詢,統一進行處理,給出所有答案
我們接下來介紹一種離線演算法:Tarjan,兩種線上演算法:RMQ,倍增演算法
Tarjan的時間複雜度是 O(n+q)
RMQ是一種先進行 O(nlogn) 預處理,然後O(1)線上查詢的演算法。
倍增演算法是一種時間複雜度 O((n+q)logn)的演算法
----------------------------------------------------------------------------------------------------------------
本文采用的例子是leetcode的236題,當然本題不是非要採用這三種方法,其實有程式碼更簡潔的方法,可以在討論區看到,但是本文重在說明這三種方法,其意義在於如線上演算法,我們在進行了看似時間複雜度很高的預處理後,以後查詢就很快了,即一次預處理換來了以後都很快的查詢。
本文采用的結構是三個大模組即Tarjan,RMQ,倍增演算法(以%%%%%%%號為分界線)。每個模組下先結合具體的例子講原理,然後給出演算法的虛擬碼,接著給出使用該演算法解決本題的具體實踐程式碼(python實現),最後給出在具體實踐中程式碼方面需要注意的問題
廢話不多說啦!先看一下本題:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
Tarjan:
它的過程是:
一:首先從根節點u開始進行遍歷
二:遍歷u的所有子節點v
三:如果v有孩子節點,則返回二直接往下遍歷
四:檢視與當前節點v有訪問關係的節點e
五:如果e有已訪問過的標記,則可以確認v和e的最近公共祖先為e被合併到的父親節點m。 否則什麼都不做
六:將v的祖先合併為u,將v設定為已訪問
七:回溯
其實上面整個就是一個深度遍歷
我們以上面題目為例說明Tarjan原理:
假設我們要找LCA(6,7)和LCA(4,0)
我們首先定義兩個陣列f和vis
f[i]表示i的祖先,初始化為自身,vis[i]表示節點i是否被訪問過的標記,初始化為False ,對應到本題就是:
f[3] = 3 , f[5] = 5 , f[6] = 6 , f[2] = 2 , f[7] = 7 , f[4] = 4 , f[1] = 1 , f[0] = 0 , f[8] = 8
vis[3] = False, vis[5] = False, vis[6] = False, vis[2] = False, vis[7] = False, vis[4] = False, vis[1] = False, vis[0] = False, vis[8] = False
--------------------------------------------------------------------------------------------------------------------------------------------------------
過程:
首先遍歷根節點3
發現其孩子有5和1
接著遍歷5,發現其還有孩子6和2接著遍歷6,發現其是葉子節點即沒有子節點
現在檢視和6有訪問關係的元素,發現是7(注意這裡可以是多個,比如我們要找的是LCA(6,7),LCA(6,,0)等等),此時vis[7] = False即7沒有被訪問過,所以什麼都不做
接下來將其祖先設為5 標誌為已訪問所以此時:
f[3] = 3 , f[5] = 5 , f[6] = 5 , f[2] = 2 , f[7] = 7 , f[4] = 4 , f[1] = 1 , f[0] = 0 , f[8] = 8
vis[3] = False, vis[5] = False, vis[6] = True, vis[2] = False, vis[7] = False, vis[4] = False, vis[1] = False, vis[0] = False, vis[8] = False
最後直接回溯
##############回溯到元素2,發現2還有孩子節點7和4,那麼遍歷7,發現其是葉子節點,那麼就檢視和7有訪問關係的元素,發現是6,此時vis[6] = True,是已訪問狀態,所以它們的最近公共祖先就是find(6) = 5
接下來將其祖先設為2,標誌為已訪問,即:
f[3] = 3 , f[5] = 5 , f[6] = 5 , f[2] = 2 , f[7] = 2 , f[4] = 4 , f[1] = 1 , f[0] = 0 , f[8] = 8
vis[3] = False, vis[5] = False, vis[6] = True, vis[2] = False, vis[7] = True, vis[4] = False, vis[1] = False, vis[0] = False, vis[8] = False
最後回溯
注意:find是這樣實現的:
find(n)
{
if n!=f[n]
{
return find(n);
}
else
return n;
}
對應到這裡就是
接著回溯到4,發現其是葉子節點,那麼就檢視和4有訪問關係的元素,發現是0,此時vis[0] = False,所以什麼都不做
合併其祖先是2,標記為已訪問,即:
f[3] = 3 , f[5] = 5 , f[6] = 5 , f[2] = 2 , f[7] = 2 , f[4] = 2 , f[1] = 1 , f[0] = 0 , f[8] = 8
vis[3] = False, vis[5] = False, vis[6] = True, vis[2] = False, vis[7] = True, vis[4] = True, vis[1] = False, vis[0] = False, vis[8] = False
最後回溯:
回溯到2,發現其沒有與之有訪問關係的元素,這直接合並祖先,標記訪問
f[3] = 3 , f[5] = 5 , f[6] = 5 , f[2] = 5 , f[7] = 2 , f[4] = 2 , f[1] = 1 , f[0] = 0 , f[8] = 8
vis[3] = False, vis[5] = False, vis[6] = True, vis[2] = True, vis[7] = True, vis[4] = True, vis[1] = False, vis[0] = False, vis[8] = False
回溯到5,發現其沒有與之有訪問關係的元素,這直接合並祖先,標記訪問
f[3] = 3 , f[5] = 3 , f[6] = 5 , f[2] = 5 , f[7] = 2 , f[4] = 2 , f[1] = 1 , f[0] = 0 , f[8] = 8
vis[3] = False, vis[5] = True, vis[6] = True, vis[2] = True, vis[7] = True, vis[4] = True, vis[1] = False, vis[0] = False, vis[8] = False
回溯到1,發現其有孩子節點0和8,接著遍歷0
發現0是葉子節點,檢視與其有訪問關係的元素,發現是4,而此時vis[4] = True,所以find(4)=3
接下來將其祖先設為1,標記為已訪問
f[3] = 3 , f[5] = 3 , f[6] = 5 , f[2] = 5 , f[7] = 2 , f[4] = 2 , f[1] = 1 , f[0] = 1 , f[8] = 8
vis[3] = False, vis[5] = True, vis[6] = True, vis[2] = True, vis[7] = True, vis[4] = True, vis[1] = False, vis[0] = True, vis[8] = False
最後回溯到8
發現8是葉子節點,發現其沒有與之有訪問關係的元素,這直接合並祖先,標記訪問
f[3] = 3 , f[5] = 3 , f[6] = 5 , f[2] = 5 , f[7] = 2 , f[4] = 2 , f[1] = 1 , f[0] = 1 , f[8] = 1
vis[3] = False, vis[5] = True, vis[6] = True, vis[2] = True, vis[7] = True, vis[4] = True, vis[1] = False, vis[0] = True, vis[8] = True,
最後回溯
回溯到3即根節點結束
--------------------------------------------------------------------------------------------------------------------------------------------------------------
注意:在實際過程中假如我們是找一個LCA,比如我們的任務是LCA(6,7),那麼其實我們可以在上面過程中##############處結束,即找到了LCA是5後就完成了任務,直接返回就好了,不變再遍歷其他元素啦
Tarjan虛擬碼:
f[i] = i //儲存i的父節點,初始化為自身即i
vis[i] = 0 //節點i的訪問標誌,初始化為0
Tarjan(u) //根節點u
{
for each(u,v) //遍歷與U相鄰的所有節點v
{
Tarjan(v); //遞迴v
join(u,v); //把v合併到u上,即將v的父節點設為u
vis[v]=1; //訪問標記
}
for each(u,e) //遍歷與u有詢問關係的節點v
{
if(vis[e])
{
ans=find(e);
}
}
}
find(n)
{
if n!=f[n]
{
return find(n);
}
else
return n;
}
最後實踐一下最開始給出的題目吧,這裡使用的是python
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
"""
self.f = {}
self.vis = {}
#下面的dfs遍歷主要是為了初始化 self.f和self.vis
self.dfs(root)
return self.Tarjan(root,p.val,q.val)
def Tarjan(self,root,p,q):
self.vis[root.val] = True
if root.left!=None:
c=self.Tarjan(root.left,p,q)
if c!=None:
return c
self.f[root.left.val] = root.val
if root.right!=None:
c=self.Tarjan(root.right,p,q)
if c!=None:
return c
self.f[root.right.val] = root.val
if root.val==p and self.vis[q] :
return self.find(q)
if root.val==q and self.vis[p] :
return self.find(p)
def dfs(self,root):
if root!=None:
self.f[root.val] = root.val
self.vis[root.val] = False
self.dfs(root.left)
self.dfs(root.right)
def find(self,x):
if x!=self.f[x]:
return self.find(self.f[x])
else:
return TreeNode(x)
注意這裡開始運行了一個dfs,它的目的只是為了初始化 self.f = {},self.vis = {}
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
RMQ:
我們還是以題目給出的例子來說明RMQ的原理:
注意:下面我們說“位置”的時候是從1開始的,因為陣列的第一個元素是從0開始,這裡有點區別,事先說明一下,以免誤會。
首先我們使用深度遍歷得到尤拉序列ves:
ves = [3, 5, 6, 5, 2, 7, 2, 4, 2, 5, 3, 1, 0, 1, 8, 1, 3]
其在樹上對應的深度分別為:
R = [1, 2, 3, 2, 3, 4, 3, 4, 3, 2, 1, 2, 3, 2, 3, 2, 1]
我們要找兩個節點的最近祖先LCA,其實就是找深度最淺的點,怎麼說呢?我們來幾個例子:
比如我們要找5和8的LCA,對應到ves中就是區間[3, 5, 6, 5, 2, 7, 2, 4, 2, 5, 3, 1, 0, 1, 8, 1, 3]
注意:我們在找區間的時候是從元素第一次出現的地方開始,所以上面的區間是對的,而不是[5, 3, 1, 0, 1, 8]
而這一段區間元素對應的深度是:[1, 2, 3, 2, 3, 4, 3, 4, 3, 2, 1, 2, 3, 2, 3, 2, 1]
這一段區間深度最小的就是1啦,那它對應的位置就是11,對應的元素就是3
所以LCA(5,8) = 3
再比如我們要找6和4的LCA,對應到ves中就是區間[3, 5, 6, 5, 2, 7, 2, 4, 2, 5, 3, 1, 0, 1, 8, 1, 3]
而這一段區間元素對應的深度是: [1, 2, 3, 2, 3, 4, 3, 4, 3, 2, 1, 2, 3, 2, 3, 2, 1]
這一段區間深度最小的就是2啦,那它對應的位置就是4,對應的元素就是5
所以LCA(6,4) = 5
原理呢就是這麼簡單,概括說就是先找到區間,然後找這一區間深度最淺的元素,該元素就是我們要找的LCA
ves和R的獲得比較容易,就想上面說的可以直接使用DFS遍歷得到
下面我們再來說一下怎麼找某一段區間最小的問題,通常這裡使用ST表解決
-----------------------------------------------------------------------------------------------------------------------------------------------------------------
ST表:
ST表是用來解決給定區間求最值問題的,這裡就以求最小值為例來說明原理吧:
假如給定區間為:[3,10,4,8,2,11,7]
dp[i,j]表示以i為起點,區間長度為的區間最小值
比如:dp[1,0]對應的區間就是[3,10,4,8,2,11,7] 所以dp[1,0]=3
dp[1,1]對應的區間就是[3,10,4,8,2,11,7] 所以dp[1,1]=3
dp[1,2]對應的區間就是[3,10,4,8,2,11,7] 所以dp[1,2]=3
dp[2,2]對應的區間就是[3,10,4,8,2,11,7] 所以dp[2,2]=2
在求解dp[i,j]時,使用動態規劃,其過程是先對長度為的區間分成兩等份,每份長度均為。之後在分別求解這兩個區間的最小值dp[i,j-1]和dp[i+,j-1]。,最後結合這兩個區間的最值,求出整個區間的最值。特殊情況,當j = 0時,即區間長度等於1,就是是說區間只有一個元素即本身,此時dp[i,0]就應該等於自身。
假如我們現在需要求dp[3,2]時,對應的區間是[3,10,4,8,2,11,7] 那麼我們可以分成兩段即[3,10,4,8,2,11,7]和[3,10,4,8,2,11,7]
即求dp[3,1]和dp[5,1] 所以dp[3,2] = min (dp[3,1] , dp[5,1])
於是動態方程就是:
初始化為:
在進行遞推的時候,是對每一元素,先求所有元素區間長度為1的區間最值,接著求所有元素區間長度為2的區間最值,之後再求區間長度為4的區間最值….,最後求解區間長度為的區間最值,其中N表示元素個數。
即:先初始化dp[1,0],dp[2,0],dp[3,0],,,,dp[N,1]
接著求 dp[1,1] dp[2,1],dp[3,1],, dp[N,1],再求.dp[1,2],dp[2,2],dp[3,2],,,dp[N,2],… 。
---------------------------------------------------------------------------------------------------------------------------------------------------------
注意現在我們希望得到的是某一段區間最小值對應的位置即:
所以關於ST的虛擬碼可以寫成:
#dp裡面儲存的是某一段區間裡面擁有最小深度元素的位置
void ST(int len){
int k = int(log(len,2));#以2為低的log函式
#初始化
for (int i=1;i<=len;i++){
dp[i,0] = i;
}
#區間長度分別是1,2,4,8,,,,,,,,,,
for (int j=1;j<=k;j++){
#遍歷每一個元素
for(int i=1;i+pow(2,j)-1<=len;i++){
int a = dp[i,j-1];
int b = dp[i+pow((j-1),2),j-1];
#這裡是用樹的深度來選擇最小值
if (R[a]<R[b]) dp[i,j] = a;
else dp[i,j] = b;
}
}
}
現在我們有了dp就好辦啦:
所以RMQ的虛擬碼就是:
int RMQ(int m,int n){
int k = int(log((n-m+1,2)));
int a = dp[m,k];
int b = dp[n-pow(k,2)+1,k];
if a<b return a;
else return b;
}
我們要找兩個節點的LCA時,這裡的m,n就是這兩個節點的位置說白了就是其在陣列ves中對應的下標比如我們要找LCA(5,8)
那麼m=2,n=15
好啦最後就是我們LCA啦:
int LCA(int u ,int v) //返回點u和點v的LCA
{
int x = first[u] , y = first[v];
if(x > y) swap(x,y);
int res = RMQ(x,y);
return ver[res];
}
綜上所述我們大概需要四個功能的函式:
DFS,ST,RMQ,LCA
最後總體寫一下RMQ全部程式碼的模板:
void dfs(int u ,int depth){
tot++;
ves[tot] = u;
R[tot] = depth;
if (u not in first){
first[u] = tot;
}
for(k=u.next){
dfs(k,depth+1);
ves[tot] = u;
R[tot] = dep;
}
}
int RMQ(int x ,int y)
{
int K = (int)(log((double)(y-x+1)) / log(2.0));
int a = dp[x][K] , b = dp[y-_pow[K]+1][K];
if(R[a] < R[b]) return a;
else return b;
}
int LCA(int u ,int v)
{
int x = first[u] , y = first[v];
if(x > y) swap(x,y);
int res = RMQ(x,y);
return ver[res];
}
void ST(int len){
int k = int(log(len,2));
for (int i=1;i<=len;i++){
dp[i,0] = i;
}
for (int j=1;j<=k;j++){
for(int i=1;i+pow(2,j)-1<=len;i++){
int a = dp[i,j-1];
int b = dp[i+pow((j-1),2),j-1];
if (R[a]<R[b]) dp[i,j] = a;
else dp[i,j] = b;
}
}
}
int main(){
#res,R,dp的陣列申請
res[];
R[];
dp[];
dfs(root ,1)
ST(len(R))
#我們要找的是p,q的共同祖先
LCA(p,q)
}
最後還是實踐一下開始給出的題目,筆者這裡使用的是python,程式碼如下:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
import math
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
"""
#記錄每一個元素對應的深度
self.R = []
#記錄遍歷過的元素對應
self.ves = []
#記錄每一個區段的最值,它的結構是這樣{1: [1, 1, 1, 1, 1], 2: [2, 2, 2, 2, 11],.........}
#2: [2, 2, 2, 2, 11]比如代表的意義就是從第二個位置開始,長度為1的區間中(本身)深度最淺元素的位置是2,長度為2的區間中深度最淺元素的位置是2
#長度為4的區間中(本身)深度最淺元素的位置是2,長度為8的區間中(本身)深度最淺元素的位置是2,長度為16的區間中(本身)深度最淺元素的位置是11
self.dp = {}
#記錄每一個元素在尤拉序中出現的第一個位置
self.first = {}
self.dfs(root,1)
self.ST(len(self.R))
m = self.LCA(p.val,q.val)
return TreeNode(m)
def LCA(self,f,g):
if self.first[f]<self.first[g]:
c = self.RMQ(self.first[f],self.first[g])
else:
c = self.RMQ(self.first[g],self.first[f])
return self.ves[c-1]
def RMQ(self,m,n):
K = int(math.log(n-m+1,2))
a = self.dp[m][K]
b = self.dp[n-2**K+1][K]
if self.R[a-1]<self.R[b-1]:
return a
else:
return b
def dfs(self,root,depth):
self.R.append(depth)
self.ves.append(root.val)
if root.val not in self.first.keys():
self.first[root.val] = len(self.ves)
if root.left!=None:
self.dfs(root.left,depth+1)
self.R.append(depth)
self.ves.append(root.val)
if root.right!=None:
self.dfs(root.right,depth+1)
self.R.append(depth)
self.ves.append(root.val)
def ST(self,lenth):
K = int(math.log(lenth,2))
for i in range(lenth):
self.dp[i+1] = [i+1]
for j in range(1,K+1):
i = 1
while i+2**j-1<=lenth:
a = self.dp[i][j-1]
b = self.dp[i+2**(j-1)][j-1]
if self.R[a-1]<= self.R[b-1]:
self.dp[i].append(a)
else:
self.dp[i].append(b)
i+=1
直觀看一下各個變數:
self.ves:
[3, 5, 6, 5, 2, 7, 2, 4, 2, 5, 3, 1, 0, 1, 8, 1, 3]
self.R:
[1, 2, 3, 2, 3, 4, 3, 4, 3, 2, 1, 2, 3, 2, 3, 2, 1]
self.dp:
{1: [1, 1, 1, 1, 1], 2: [2, 2, 2, 2, 11], 3: [3, 4, 4, 4], 4: [4, 4, 4, 11], 5: [5, 5, 5, 11], 6: [6, 7, 7, 11], 7: [7, 7, 10, 11], 8: [8, 9, 11, 11], 9: [9, 10, 11, 11], 10: [10, 11, 11, 11], 11: [11, 11, 11], 12: [12, 12, 12], 13: [13, 14, 14], 14: [14, 14, 17], 15: [15, 16], 16: [16, 17], 17: [17]}
注意這裡採用字典的形式代表dp[i,j],如3: [3, 4, 4, 4]代表的含義就是從位置3開始(注意我們這裡所說的位置不是從0開始的,即第一個元素的位置就是1,而不是0,不要和陣列的下標混了,因為陣列下標是從0開始的)長度分別為1,2,4,8的區間內對應的深度最淺的元素的位置是3,4,4,4結合self.ves:可以知道3和4的位置是6和5
self.first:
{0: 13, 1: 12, 2: 5, 3: 1, 4: 8, 5: 2, 6: 3, 7: 6, 8: 15}
它的含義就是元素第一次出現的位置,比如0: 13就是說0這個元素在ves中第一次出現的位置是13
[3, 5, 6, 5, 2, 7, 2, 4, 2, 5, 3, 1, 0, 1, 8, 1, 3]
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
倍增演算法:
為了更加形象說明其工作原理,我們還是用上面題目給出的例子來講解:
假如我們求LCA(1,8),LCA(7,8)過程如下:
先將深度大的那個節點上移,使兩者處於同一深度,具體就是我們要找LCA(7,8)轉化為LCA(2,8),要找LCA(1,8)轉化為LCA(1,1)
當轉化為兩者處於同一深度時,先判斷新的兩個元素是否相同,如果相同則其就是結果返回即可,例如LCA(1,1)返回1,如果不相同,那麼兩者一同上移,知道轉化後的兩個新元素相同返回,例如LCA(2,8)繼續轉化為LCA(5,1),LCA(5,1)繼續轉化為LCA(3,3),此時相同返回3即可。
原理呢就是這麼簡單,但是每次上移都是一步的話,這樣太慢啦,而這裡的倍增演算法,關鍵之處就是體現在上移過程中不是一步一步移,而是以2的倍數進行上移
現在定義兩個陣列,f[i,j]和depth[i]
f[i,j]代表的意思是從節點i開始上移所到達的節點,例如f[7,0]=2,f[0,1]=3
所以可以得到遞推式:
depth[i]代表的是節點i在樹中的深度,例如depth[6]=3 , depth[4]=4
寫一下其模板:
void dfs(int prev,int rt){
depth[rt]=depth[prev]+1;
fa[rt][0]=prev;
for (int i=1;i<100;i++)
fa[rt][i]=fa[fa[rt][i-1]][i-1];
for (int i=0;i<son[rt].size();i++)
dfs(rt,son[rt][i]);
注意這裡的100是指最多可以上跳
有了這些資訊,我們來講一下怎麼使用倍增的思想來跳
首先解決怎麼將兩者移位同一深度:
假如LCA(m,n),假設n的深度大於m
設兩者的高度差是H,我們的i從100開始即從邁最大的步數開始(這裡所說的i其實就是一個for迴圈),看H是否大於等於,如果大於等於的話我們就將深度深的那個節點上移即n=f[n,i],如果不大於的話就什麼也不做,接著我們減少i為99,用新的n和m重複上面的過程,直到兩者處於同一深度,好像有點複雜,來舉個例子吧
對於這個樹結構來說,上面的100在這裡只需要3即最大上移8
假設現在求LCA(D,P)
已知f[P,0] = M,f[p,1] = I,f[P,2] = B
depth[P]=6
f[D,0]=B,f[D,1]=A
depth[D]=3
f[I,0]=E , f[I,1]=A
對應到上面的話
那麼這裡高度差為H=6-3=3,我們首先從P開始邁最大的步i=2,即邁,此時H<,那麼我們什麼也不做,於是i--,此時i=1,於是我們這次邁,此時H>,於是上移到I(f[p,1] = I),此時i--,即i=0,於是我們這次邁步,H>,於是上移到E(f[I,0]=E)
i--,此時i=-1小於0結束,可以看到由P移動到E,移動到同一深度啦,於是問題就轉化為求LCA(D,E)
注意:其實一旦H大於等於,以後i不斷的減小,這一條件都是滿足的,所以從此時的i開始,以後都要上移,只不過上移的步伐都是上次的一半,總結來說就是先找出可以邁的最大步伐,邁出去,更新當前元素,然後從當前元素開始,向上邁,邁的步伐是上次的一半,然後不斷重複上面的過程,直到深度相同
接下來我們解決怎麼利用倍增思想將兩者怎麼同時上移,找到共同祖先:
假設我們找LCA(P,O),此時我們利用上面的過程已將問題轉化為求LCA(M,O)
已知 f[M,0]=I , f[M,1]=E , f[M,2]=A
f[O,0]=K, f[O,1]=F, f[O,2]=A
f[E,0]=B, f[E,1]=A
f[F,0]=C, f[F,1]=A
f[B][0]=A
f[C][0]=A
我們還是從i最大開始,即從邁最大的步伐開始,兩者同時邁即同時上移,上移後,如果兩者元素不同就更新,否則不更新。
對應到這裡就是我們將從i=2開始,兩者同時上移,發現上移後元素相同都是A(f[M,2]=A=f[O,2]),那麼什麼不做,i--,此時i=1,即同時上移,發現元素不相同即E和F(f[M,1]=E, f[O,1]=F),更新,此時兩個元素各自更新為E和F即
i--,此時i=0,將E和F同時上移發現元素不相同即B和C(f[E,0]=B,f[F,0]=C),此時兩個元素各自更新為B和C即
i--,此時i=-1,i<0結束。
返回f[B][0]或f[C][0]即是結果:A
其實就是說先找出可以邁的最大步伐,邁出去,分別更新兩者當前元素,然後從當前元素開始,向上邁,邁的步伐是上次的一半,然後不斷重複上面的過程,直到元素相同
說了這麼多還是給出程式碼模板吧,這樣邏輯看起來會更加清晰:(這裡還是假設i最大為100)
int LCA(int x,int y){
if (depth[x]<depth[y])
swap(x,y);
for (int i=100;i>=0;i--)
if (depth[x]-(1<<i)>=depth[y])
x=fa[x][i];
if (x==y)
return x;
for (int i=100;i>=0;i--)
if (fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
所以總體來說就是先用dfs遍歷得到f[i,j]和depth[i]
接著使用LCA函式得到結果
虛擬碼總結一下就是:
void dfs(int prev,int rt){
depth[rt]=depth[prev]+1;
fa[rt][0]=prev;
for (int i=1;i<100;i++)
fa[rt][i]=fa[fa[rt][i-1]][i-1];
for (int i=0;i<son[rt].size();i++)
dfs(rt,son[rt][i]);
int LCA(int x,int y){
if (depth[x]<depth[y])
swap(x,y);
for (int i=100;i>=0;i--)
if (depth[x]-(1<<i)>=depth[y])
x=fa[x][i];
if (x==y)
return x;
for (int i=100;i>=0;i--)
if (fa[x][i]!=fa[y][i])
x=fa[x][i],y=fa[y][i];
return fa[x][0];
}
int main(){
#假如我們要求LCA(p,q)
#depth,fa陣列的申請
depth[m]
fa[m][n]
#depth[root]的初始化
depth[root] = 1
if root.left!=None{
dfs(root,root.left);
}
if root.right!=None{
dfs(root,root.right);
}
return LCA(p,q)
}
最後還是實踐一下開始給出的題目,筆者這裡使用的是python,程式碼如下:
# Definition for a binary tree node.
# class TreeNode:
# def __init__(self, x):
# self.val = x
# self.left = None
# self.right = None
class Solution:
import math
def lowestCommonAncestor(self, root, p, q):
"""
:type root: TreeNode
:type p: TreeNode
:type q: TreeNode
:rtype: TreeNode
"""
self.depth = {}
self.f = {}
self.maxstep = 100
self.depth[root.val] = 1
if root.left!=None:
self.dfs(root,root.left)
if root.right!=None:
self.dfs(root,root.right)
c = self.LCA(root.val,p.val,q.val)
return TreeNode(c)
def LCA(self,root,m,n):
#因為 self.f中沒有根節點,所以這裡判斷一下,如果其中一個是根節點,那麼其LCA必是根節點,直接返回即可
if m==root or n==root:
return root
if self.depth[n]<self.depth[m]:
temp = m
m = n
n = temp
#目的就是將m和n的深度調為一樣
for i in range(len(self.f[n])):
if self.depth[n]-self.depth[m]>=2**(len(self.f[n])-i-1):
n = self.f[n][len(self.f[n])-i-1]
if n==m:
return n
#兩者一同向上跳,注意這裡的length的重要性
length = len(self.f[n])
for i in range(length):
if self.f[n][length-i-1]!=self.f[m][length-i-1]:
n = self.f[n][length-i-1]
m = self.f[m][length-i-1]
return self.f[m][0]
def dfs(self,prev,rt):
self.f[rt.val] = [prev.val]
self.depth[rt.val] = self.depth[prev.val]+1
for i in range(1,self.maxstep):
if self.f[rt.val][i-1] in self.f.keys() and len(self.f[self.f[rt.val][i-1]])>=i :
self.f[rt.val].append(self.f[self.f[rt.val][i-1]][i-1])
else:
break
if rt.left!=None:
self.dfs(rt,rt.left)
if rt.right!=None:
self.dfs(rt,rt.right)
對應著
來直觀看一下各個變數:
self.f:
{0: [1, 3], 1: [3], 2: [5, 3], 4: [2, 5], 5: [3], 6: [5, 3], 7: [2, 5], 8: [1, 3]}
self.depth:
{0: 3, 1: 2, 2: 3, 3: 1, 4: 4, 5: 2, 6: 3, 7: 4, 8: 3}
注意:
一:f中並沒有根節點,對應到上面的例子中即f字典中沒有關鍵3
二:因為python的字典不需要事先申請大小,所以我們在深度遍歷f時,事先判斷一下是否能繼續繼續上跳,能的話就append
不能的話就break
其實就是看是否存在並且所對應的列表長度得大於j,否則怎麼可能會有對吧,所以這裡準確來說有兩個條件
舉個例子
我們在求self.f[5][1]=self.f[self.f[5][0]][0]=self.f[3][0],而正如上面給出的self.f中並沒有關鍵字3所以就break好啦,結果就是self.f[5]中的列表就只是一個元素即self.f[5][0],而沒有self.f[5][1] self.f[5][2] self.f[5][3],,,,,,,正如上面
{0: [1, 3], 1: [3], 2: [5, 3], 4: [2, 5], 5: [3], 6: [5, 3], 7: [2, 5], 8: [1, 3]}
再比如我們求self.f[7][2]=self.f[self.f[7][1]][1]=self.f[5][1],此時滿足第一條件即5是f的關鍵字,但len(self.f[5])=1,只有一個元素即5: [3],
只有self.
f[5][0],而我們要求self.
f[5][1]顯然不存在,所以self.f[7][2]不存在,直接break,最後self.f[7]只有self.f[7][0] , self.f[7][1] 而self.f[7][2] , self.f[7][3] , ,,,,,,都沒有
對應的程式碼就是:
if self.f[rt.val][i-1] in self.f.keys() and len(self.f[self.f[rt.val][i-1]])>=i :
self.f[rt.val].append(self.f[self.f[rt.val][i-1]][i-1])
else:
break
三:這裡說一下length的重要性,筆者之前是這樣寫的:
#兩者一同向上跳,注意這裡的length的重要性
for i in range(len(self.f[n])):
if self.f[n][len(self.f[n])-i-1]!=self.f[m][len(self.f[n])-i-1]:
n = self.f[n][len(self.f[n])-i-1]
m = self.f[m][len(self.f[n])-i-1]
return self.f[m][0]
結果一直不通過,顯示錯誤,我們來看一下原因:
對比:
#兩者一同向上跳,注意這裡的length的重要性
length = len(self.f[n])
for i in range(length):
if self.f[n][length-i-1]!=self.f[m][length-i-1]:
n = self.f[n][length-i-1]
m = self.f[m][length-i-1]
return self.f[m][0]
可以看到這裡的length在每一次迴圈下其實是個定值,而錯誤的那個len(self.f[n])其實是個變數,隨著n的更新變化而變化
我們拿過來一開始分析的那段話:其實就是說先找出可以邁的最大步伐,邁出去,分別更新兩者當前元素,然後從當前元素開始,向上邁,邁的步伐是上次的一半,然後不斷重複上面的過程,直到元素相同
這裡的“邁的步伐是上次的一半”就是說self.f[n][length-i-1]中length-i-1體現的正是如此,而錯誤的中self.f[n][len(self.f[n])-i-1]的不僅i在變化,len(self.f[n])也在不斷變化,也就是說不能保證“邁的步伐是上次的一半”。
這可能不是什麼大錯誤,大家也不會犯這種錯誤,可是這個錯誤花了筆者好長時間才找到,心痛,,,,,,,,,
四:同時因為self.f中沒有根節點的資訊,即沒有self.f[3],所以當我要求LCA(3,8)等的時候在用f的時候就會出錯,所以針對這一特殊情況我們直接判斷一下要求的的兩個節點中是否有根節點,如果有的話直接返回根節點就可以來(因為這種情況的LCA就是根節點),對應的程式碼部分就是:
#因為 self.f中沒有根節點,所以這裡判斷一下,如果其中一個是根節點,那麼其LCA必是根節點,直接返回即可
if m==root or n==root:
return root