Semorphe

樹上走訪

樹是沒有環的圖,所以走它只要記住「從哪來」。 · ⏱ 約 35 分鐘

你會學到三件事

  1. 樹是一種特別的圖:n 個點、n−1 條邊、沒有環
  2. DFS 走樹只要記 parent,不用 visited 陣列
  3. 深度是一路傳下去的

開始之前

第 11 課的 BFS 需要一個 dist 陣列擋住走過的點。

樹上不用,因為沒有環:你只可能從來的那條邊走回去。

🟢 先把編輯器打開,跟著下面一起做:跟著做

一、樹的三個等價說法

說法
n 個點、n−1 條邊、連通
任兩點之間恰好一條路徑 所以「最短路」就是「那條路」
連通且沒有環

⚠️ 三個是等價的,證明其中一個就有另外兩個。 題目說「保證是一棵樹」時,這三件事你都可以用。

二、DFS 只要記 parent

樹上不會繞回去,所以記住「從哪裡來」就夠了:

1void dfs(int u, int parent) {2    vector<int> nb = g[u];3    for (int v : nb) {4        if (v == parent) continue;      // 🔴 唯一要擋的5        depth[v] = depth[u] + 1;6        dfs(v, u);                      // 我是 v 的 parent7    }8}
變數g變數u陣列的第 [] 格變數nb變數v變數parent=跳至下一次變數v變數depth變數u陣列的第 [] 格1+變數v變數u呼叫函式dfs()設定 陣列depth的第 [] 格 ←如果則對每個intv在中建立int(整數)列表nb大小每格填複製自定義函式 回傳型別void(無回傳)dfs(參數int(整數)u,int(整數)parent)使用命名空間std引入函式庫vector(動態列表)123456

一般的圖要 visited 陣列,而樹上只要擋住「回頭那一步」。

因為沒有環,除了來的那條邊,其他邊都通向沒去過的地方。

⚠️ 呼叫時根的 parent 給 -1(一個不存在的點):

1dfs(0, -1);
0-1呼叫函式dfs()1

三、深度是傳下去的

深度不用另外算——往下走一層就加一:

1depth[v] = depth[u] + 1;
變數v變數depth變數u陣列的第 [] 格1+設定 陣列depth的第 [] 格 ←1

⚠️ 這一行要在 dfs(v, u) 之前:先設好,再往下走。

放在後面的話,子節點在計算時拿到的是舊值(多半是 0)。 症狀是所有深度都變成 0 或 1,而程式不會報錯。

    0        depth = 0
    |
    1        depth = 1
    |
    2        depth = 2

四、同一個形狀,換一個地方做事

樹上的 DFS 有兩個時機:

1void dfs(int u, int parent) {2    // ① 【進入】u 的時候——由上往下傳的東西(深度、從根到這裡的和)3    for (int v : g[u]) {4        if (v == parent) continue;5        dfs(v, u);6        // ② 【子樹處理完】——由下往上收的東西(子樹大小、子樹最大值)7    }8}
變數g變數u陣列的第 [] 格變數v變數parent=跳至下一次變數v變數u備註:② 【子樹處理完】——由下往上收的東西(子樹大小、子樹最大值)呼叫函式dfs()如果則對每個intv在中備註:① 【進入】u 的時候——由上往下傳的東西(深度、從根到這裡的和)定義函式 回傳型別void(無回傳)dfs(參數int(整數)u,int(整數)parent)123456
放在哪 算得出什麼
① 進入時 深度、到根的距離、祖先
② 回來時 子樹大小、子樹的和、樹的直徑
1size[u] = 1;2for (int v : g[u]) {3    if (v == parent) continue;4    dfs(v, u);5    size[u] += size[v];      // ← 在②,因為要等 v 算完6}
變數u1變數g變數u陣列的第 [] 格變數v變數parent=跳至下一次變數v變數u變數size變數u陣列的第 [] 格變數size變數v陣列的第 [] 格把加上(+=)呼叫函式dfs()如果則對每個intv在中設定 陣列size的第 [] 格 ←12345

「往下傳」和「往上收」是樹上演算法的兩個方向, 而它們只差在你把那行程式寫在遞迴呼叫的前面還是後面。

五、⚠️ 遞迴深度

一條鏈狀的樹(每個點只有一個小孩),n = 10⁵ 的話遞迴會疊 10⁵ 層, 可能 stack overflow(第 5 課)。

競賽裡多半沒事,而真的爆了就要改成用 stack 的迴圈版本。

完成的樣子

這一課的參考做法——程式碼每一行的號碼,就印在對應的那塊積木上。

1#include <iostream>2#include <vector>3using namespace std;4vector<vector<int>> g;5vector<int> depth(3, 0);6 7void dfs(int u, int parent) {8    vector<int> nb = g[u];9    for (int v : nb) {10        if (v == parent) continue;11        depth[v] = depth[u] + 1;12        dfs(v, u);13    }14}15 16int main() {17    vector<int> a;18    a.push_back(1);19    vector<int> b;20    b.push_back(0);21    b.push_back(2);22    vector<int> c;23    c.push_back(1);24    g.push_back(a);25    g.push_back(b);26    g.push_back(c);27    dfs(0, -1);28    cout << depth[2] << endl;29    return 0;30}
30變數g變數u陣列的第 [] 格變數nb變數v變數parent=跳至下一次變數v變數depth變數u陣列的第 [] 格1+變數v變數u呼叫函式dfs()設定 陣列depth的第 [] 格 ←如果則對每個intv在中建立int(整數)列表nb大小每格填複製自1變數a0變數b2變數b1變數c變數a變數g變數b變數g變數c變數g0-1變數depth2陣列的第 [] 格換行0回傳印出呼叫函式dfs()在尾端加入在尾端加入在尾端加入在尾端加入建立int(整數)列表c大小每格填複製自在尾端加入在尾端加入建立int(整數)列表b大小每格填複製自在尾端加入建立int(整數)列表a大小每格填複製自定義函式 回傳型別int(整數)main定義函式 回傳型別void(無回傳)dfs(參數int(整數)u,int(整數)parent)建立int(整數)列表depth大小每格填複製自建立vector<int>列表g大小每格填複製自使用命名空間std引入函式庫vector(動態列表)引入函式庫iostream(輸入輸出)123457816917101811191220212223242526272829

換你了

加一個 size 陣列,算出每個節點的子樹大小。

⚠️ 記得它要放在遞迴呼叫的後面,那是「往上收」。

到編輯器做這一題:練習:算出每個節點的子樹大小

做一個

一棵樹,邊是 0-1、0-2、1-3、3-4、2-5。以 0 為根,印出最深的節點和它的深度。

🔴 深度是一路傳下去的:dfs(v, u, d + 1)。 ⚠️ 而這一題不用 visited 陣列——記住 parent 就夠了,因為樹沒有環。

到編輯器做這一題:做一個:最深的那個節點

這一課你做了什麼

如果卡住了

你看到 多半是因為
遞迴停不下來 if (v == parent) continue; 漏了
深度全是 0 或 1 那一行寫在 dfs(v, u) 後面了
子樹大小不對 累加寫在遞迴呼叫前面了
有些點沒走到 無向邊只加了一邊(第 11 課那件事)
stack overflow 樹太深。改成迴圈 + stack
在編輯器打開這一課 →