樹是一種特別的圖,它長的就像一顆樹。樹必須沒有環,而且要是連通的。以下就來討論樹的定義:
一棵樹是一個無向圖,必須滿足以下其中之一:
任意兩個頂點間存在唯一一條路徑。
沒有環且連通。
邊數比頂點數少一的簡單無環圖。
上述三點各自都是樹的充分必要條件。
接下來是一些有關樹的名詞:
根 (root):有些題目會指定一個點當根,變成有根樹 (rooted tree),根節點只有子節點沒有父節點。
父節點 (parent):對於兩個相鄰的點 $u, v$,如果 $u$ 到根節點的距離比 $v$ 到根節點的距離還要小,那麼 $u$ 就是 $v$ 的父節點。又稱親節點。
子節點 (children):如果 $u$ 是 $v$ 的父節點,那麼 $v$ 是 $u$ 的子節點。
兄弟節點 (sibling):父節點相同的節點們互為兄弟節點。
子樹 (subtree):節點 $u$ 的子樹包含 $u$、$u$ 的子節點以及他們的子樹。某個點的子樹大小就是該點形成的子樹有多少個點。
森林 (forest):一些不連通的樹構成的集合。
生成樹 (spanning tree):對於一個連通圖,我們可以從這張圖選一些邊,使得這些邊自己會形成一個包含所有點的樹,稱為此圖的生成樹。
以上圖為例, $1$ 是根節點,$2$ 是 $5$ 的父節點,$6, 7$ 是 $3$ 的子節點,$3, 4$ 互為兄弟節點,$3$ 的子樹包含 $3, 6, 7, 8$ 四個點。
讀者可以注意到我們將樹根畫在上方,而不是像日常生活中的樹根一樣在下方。這其實是某種不成文的傳統,也是大家的默契,其背後的原因我們並不是很清楚,但一個最合理的理由可能是因為我們總是從樹根開始畫出整棵樹,這樣考慮到畫圖的順序的話,由上畫到下是比較順手的畫法。
尋訪一棵樹可以用 DFS 來實現,跟普通的 DFS 沒什麼兩樣。不過樹上的每個點都只有一個父節點,所以也可以用下列實作方法:
void dfs(int s, int par) {
// process node s
for (auto child : adj[s]) {
if (child != par)
dfs(child, s);
}
}
利用變數 par 紀錄父節點的方式,在 for 迴圈內避免回頭走就行了。不須紀錄 visited 陣列。根節點沒有父節點,因此一開始呼叫這個函數的時候 par 參數要傳入非任何節點的編號(例如 $-1$)。
有時候我們需要這個節點距離樹根多遠,這時候可以慢慢往上爬到樹根,但是這樣最差可能會需要用到 $O(n)$ 時間。所以可以進行一次 DFS 預處理,之後就只要 $O(1)$ 查詢。
void dfs(int s, int par) {
for (auto child : adj[s]) {
if (child != par) {
dist[child] = dist[s] + 1;
dfs(child, s);
}
}
}
如果先將 dist[root] 初始化成 $0$,再對根節點 DFS 一次,dist 陣列就會儲存著每個節點到根的距離。這個每個節點到根的距離又被稱為「深度(depth)」。
尋訪樹的時候,我們可以在走完每一個小孩之後,把這個小孩的資訊傳回父親節點。
void dfs(int s, int par) {
siz[s] = 1;
for (auto child : adj[s]) {
if (child != par) {
dfs(child, s);
siz[s] += siz[child];
}
}
}
這個程式可以計算出以每個點為根的子樹大小,紀錄在 size[] 陣列裡。
給定一棵有根樹,節點 $1$ 為根。你能計算所有點的深度嗎?一個點的深度為它到根最少要經過幾條邊。
給定一棵有根樹,節點 $1$ 為根且每條邊都有權重。你能計算所有點的深度嗎?一個點的深度為它到根最少要經過的邊權總和。
作為一個軌道機關設計師,你製作了一連串小球下落的軌道。這個軌道有 $N-1$ 個中繼點編號 $1\sim N-1$ 以及 $N$ 個終點編號 $N\sim 2N-1$,其中編號 $1$ 的中繼點是小球進入的起點。這個機關每個中繼點都連接左右兩個出口,出口可能連接其他中繼點或終點。而終點便是囤積小球的地方,所有先前抵達的小球都會累積在這。
對於每個中繼點,所有從左側出口能到達的終點所累積的小球重量都會成為左側軌道的負重,同理右側軌道也有負重。每次小球都會選擇負重較輕的那側出口離開,如果一樣重則選擇左邊。現在請你模擬 $M$ 個小球依序落下的過程,並回答這 $M$ 個小球分別落入哪個終點。在開始之前,終點可能已累積了一些小球。