Tree
程度★ 難度★★
庭中有奇樹,綠葉發華滋。攀條折其榮,將以遺所思。《古詩十九首》
Tree
「樹」。樹是一種很特別的圖。樹的定義是:任兩點之間都相通,並且沒有環的圖。
樹的定義對初學者來說或許太過抽象。換個說法吧:一棵樹可想做是由一個點開始,藉由許多條邊不斷地延伸拓展到其他點,而且點和邊都不會重複地被拓展到。
Node
「節點」。進行延伸拓展的點、被延伸拓展到的點,稱作「節點」,也就是說樹上的點都是「節點」。
【註:為了方便,以下仍稱呼「點」。】
Branch
「枝」。延伸拓展所用到的邊稱作「枝」,也就是說樹上的邊都是「枝」。一個點藉由邊往外延伸拓展,稱做「分枝(Branching)」。
【註:為了方便,以下仍稱呼「邊」。】
Root
「根」。方才提到,一棵樹可想做是由一個點開始分枝──這個點便是「根」。一棵樹上的每一個點都可以做為根。
Leaf
「葉」。在一棵樹上選定根後,由根開始不斷分枝,途中所有無法繼續分枝的點皆是「葉」。
另一種說法是:除了根以外,只連著一條邊的點就是葉。但這種說法有個例外:如果樹上總共只有一個點,那麼此點既是根、也是葉。
Level
「層」。在一棵樹上選定根後,按照拓展的順序(也就是按照每個點離根的距離),可以將樹上的點分層次,使得樹上每一個點都擁有一個層數。如果改變根,那麼分層的結果就會不同。
還有另一種比較少見的分層方式,是設定所有葉在同一層,並由葉開始計算層數。
Parent & Child
「父親」與「小孩」。在一棵樹上選定根後,以邊相連的任兩點,靠近樹根者相對地稱作「父親」,靠近樹葉者相對地稱作「小孩」。
一個點的父親,是指與其相鄰的點當中,較此點靠近樹根者,為其父親。父親只會有一個,特例是:樹根沒有父親。
一個點的小孩,是指與其相鄰的點當中,較此點靠近樹葉者,為其小孩。小孩可以是任意多個,特例是:樹葉沒有小孩。
Ancestor & Descendant
「祖先」與「子孫」。在一棵樹上選定根後,一個點的父親、父親的父親、……皆是此點的「祖先」。一個點的小孩、小孩的小孩、……皆是此點的「子孫」。
Directed Tree
在一棵樹上選定樹根後,可以把邊的方向設定成分枝的方向、遠離樹根的方向;也可以把邊的方向設定成朝向樹根的方向,但是這種情況比較少。
Weight
一棵樹可以有權重。當邊擁有權重時,一棵樹的權重等於樹上所有邊的權重總和。
Forest
「森林」。很多棵樹稱作一叢「森林」。
Tree資料結構
程度★ 難度★★
使用圖的資料結構
可以直接使用圖的資料結構來儲存一棵樹,adjacency matrix或adjacency lists都可以。
有根樹:單向(往樹根方向)
開一條int陣列,每個格子對應圖上的各個點,格子裡儲存著該點的父親。
查詢路徑時,只能往根的方向行走,無法往葉的方向行走。儘管不太便利,但是足以應付大部分情況了。【待補文字】
有根樹、無根樹:雙向
【待補文字】
UVa 10729
Tree相關性質
程度★ 難度★★★
樹的特性
1. 樹沒有環。 2. 任意兩點之間只有唯一一條路徑。 3. 樹上所有點之間都相連通。 4. 在樹上任意添加一條邊,就會產生環。 5. 在樹上任意刪除一條邊,一顆樹就裂成兩棵樹。 6. 邊數等於點數減一。
UVa 615 599
height (rooted tree)
運用Divide and Conquer,移除一棵樹的樹根,形成許多子樹,並分頭處理子樹。
寫成程式碼之後,D&C的運作順序,剛好就是DFS的遍歷順序。因此,有人把這樣的程式碼,直接稱為DFS。這用詞並非精準,然而其過程恰是遍歷一張圖,讀取資訊算出答案,故稱之為DFS倒也無妨。
diameter
樹上相離最遠的兩個點的路徑長度,便是樹的直徑。稍微修改一下計算高度的程式碼,就可以順便計算直徑。
一棵樹的各種直徑一定會相交在同一點(同一群點)。
1. 反證法。 現在有兩條分開的直徑, 可是一棵樹上各點都得連通, 所以這兩條分開的直徑,中間一定有某處互相連接, 一旦連接起來,勢必變成更長的直徑,矛盾。 故所有直徑必相交。 2. 反證法。 現在已有兩條直徑相交在某一點, 如果另外一條直徑與這兩條直徑相交在另一點, 勢必變成更長的直徑,矛盾。 故所有直徑必相交在同一點(同一群點)。
UVa 10308 11695
balanced height
想辦法選定一個樹根,讓樹的高度最小。演算法請自行參考程式碼,時間複雜度為兩次DFS的時間。
UVa 10459 10939
parent-child relationship (rooted tree)
建立DFS tree或者BFS tree,就可以輕鬆判斷一點是不是另一點的父親。
ancestor-descendant relationship (rooted tree)
利用DFS的遍歷順序,就可以輕鬆判斷一點是不是另一點的祖先。
distance between two nodes
樹上任兩點之間只有一條路徑。由其中一點開始進行DFS或BFS,直到遇見另一點,就得到距離了。程式碼就不提供了。
時間複雜度為一次Graph Traversal的時間。
distance from one node to any nodes
由該點開始進行BFS或DFS即可。時間複雜度為一次Graph Traversal的時間。
distance between all arbitrary two nodes
算出所有兩點之間的距離。可以對圖上各點使用Graph Traversal求得,時間複雜度為O(V^2)。
下面要介紹稍微複雜一點的方法。先隨便選定一個樹根,然後利用Least Common Ancestor將路徑分割成兩條,分頭計算兩條路徑的長度。
這裡提供一個實作方式,時間複雜度為O(V^2)。
1. 樹上隨便挑一點作為樹根。 2. 求所有兩點之間的Least Common Ancestor。 3. 求樹根到圖上各點之距離d(‧)。 4. 樹上x點與y點的距離為 (d(x)-d(z)) + (d(y)-d(z)), 其中z點是x點與y點的Least Common Ancestor。
再提供另一個實作方式,時間複雜度也是O(V^2)。
1. 樹上隨便挑一點作為樹根。
2. 求所有兩點之間的Least Common Ancestor。
3. 以top-down recursive DP計算樹上x點與y點的距離:
d(x,y) =
{ 0 , when x = y
{ w(x, px), d(px, y) , when y is the ancestor of x
{ w(y, py), d(x, py) , when x is the ancestor of y
{ d(x,z) + d(y,z) , otherwise (z is the lca of x and y)
d(x,y)為x點與y點之間的距離
w(x,y)為邊xy的權重。如果邊xy不存在,w(x,y)為無限大。
px為x的父親,py為y的父親。
Least Common Ancestor: Tarjan's Algorithm
程度★★ 難度★★
Least Common Ancestor
一棵有根樹,樹上兩點共同祖先當中,離根最遠、深度最深的那一個祖先,常簡稱為LCA,亦有人稱lowest common ancestor、nearest common ancestor。
求LCA的演算法相當多元,此處先介紹其中一種:求出樹上所有點對的LCA,運用DFS的特性以及Disjoint-set Forest的概念。
這裡是一個簡單的實作。
時間複雜度切成三部份討論:
一、DFS:很明顯的是O(V^2)或O(V+E),端看樹的資料結構是哪一種。
二、Disjoint-set Forest:直接考慮p[]被設值的次數,總共是O(V^2)次。
三、設定lca[][]的時間共為O(V^2)。
故時間複雜度總共是O(V^2)。
Least Common Ancestor: Divide and Conquer
程度★★ 難度★★★
UVa 10938
Least Common Ancestor: ±1 Range Minimum Query
程度★★ 難度★★★
Modeling
用DFS遍歷,記下到訪次序(作為索引值)以及深度(作為元素值),則可將LCA問題轉換成±1RMQ問題。
建立Cartesian Tree,RMQ問題就可以變成LCA問題。