圖論實戰題解

圖論優先度 3/5 — 值得會 — 多半是必備模式的變形值得會 更新於 Nov 12, 2026
章節優先度優先度 5/5 — 必備 — 幾乎每一輪面試都會出現必備優先度 4/5 — 高價值 — 這裡有缺口就會掉關高價值優先度 3/5 — 值得會 — 多半是必備模式的變形值得會優先度 2/5 — 冷門 — 讀過一次即可,除非目標公司已知會問冷門只標在真正需要的章節上 —— 沒標的是背景/參考資料。

範圍 — graph.md 的題目集:每題一張 problem card,依照它歸約到的 graph.md 模板分組,本身不放任何模板或理論。 另見:graph.md — 圖的表示法、走訪、連通性與環偵測,以及下面每張卡歸約到的模板;graph_advanced.md — Tarjan、Euler、最大流與二分圖的進階題;dfs_examples.md 和 bfs_examples.md — 這份指過去的網格題與樹題;union_find_examples.md — DSU 那份自己的題目集。

LeetCode 題目清單

總覽

不屬於網格、樹或依賴順序的經典圖論題,每題一張 problem card,放在它歸約到的 graph.md 模板底下。graph.md 放模板;這份放題目。 已經在別處完整解過的題目,在 Index 裡只留一列指標,不再放第二份。

索引

LC 題目 歸約到 難度
323 Number of Connected Components in an Undirected Graph graph § Template 1 — 走訪計算連通分量 Medium
133 Clone Graph graph § Template 2 — DFS 可達性 Medium
947 Most Stones Removed with Same Row or Column graph § Template 3 — union-find Medium
886 Possible Bipartition graph § Template 6 — 二著色 Medium

在別份解過 — 一題一個家,所以這些只是指標:

LC 題目 解在
200 Number of Islands dfs.md § Template 2
695 Max Area of Island dfs_examples.md
742 Closest Leaf in a Binary Tree bfs_examples.md
1319 Number of Operations to Make Network Connected union_find_examples.md
207 Course Schedule graph.md § Template 4
802 Find Eventual Safe States graph.md § Template 5
399 Evaluate Division dfs.md § Template 10
329 Longest Increasing Path in a Matrix topology_sorting.md § Template 9
815 Bus Routes bfs_advanced.md

連通分量範例

Number of Connected Components in an Undirected Graph — LC 323

關鍵 — 從還沒走訪過的節點發射的每一次走訪,恰好涵蓋一個連通分量,所以答案就是發射的次數。 圖是以邊列表給的,所以要先把鄰接表的兩個方向都建好。

歸約到:graph § Template 1 — 同一個「發射並計數」的迴圈,只是輸入從鄰接矩陣換成邊列表。

python
# python
# LC 323 - Number of Connected Components in an Undirected Graph
# IDEA: build an undirected adjacency list; each DFS launched from an unvisited node is one component
# time = O(V + E), space = O(V + E)
from collections import defaultdict

def countComponents(n, edges):
    graph = defaultdict(list)
    for u, v in edges:
        graph[u].append(v)
        graph[v].append(u)          # undirected: both directions
    visited = set()
    count = 0
    for start in range(n):
        if start in visited:
            continue
        count += 1                  # an unvisited node opens a new component
        visited.add(start)
        stack = [start]
        while stack:
            node = stack.pop()
            for nxt in graph[node]:
                if nxt not in visited:
                    visited.add(nxt)
                    stack.append(nxt)
    return count

# countComponents(5, [[0,1],[1,2],[3,4]]) -> 2

陷阱

  • 孤立節點沒有邊,所以永遠不會出現在 graph 裡:要對 range(n) 迴圈,不要對鄰接表的 key 迴圈, 否則 n = 3, edges = [] 會回傳 0 而不是 3。
  • union-find 的版本(每次成功合併就把計數器減一)一樣好,解在 union_find_examples.md。

DFS 可達性範例

Clone Graph — LC 133 優先度 4/5 — 高價值 — 這裡有缺口就會掉關

關鍵 — {original: copy} 這個 map 就是 visited 集合。在遞迴進鄰居之前先登記節點的複本; 這樣繞回它的環會直接找到已經存在的複本,而不是無限遞迴。

歸約到:graph § Template 2 — 從單一節點出發的 DFS 可達性,只是「走訪」的意思變成「建立複本」。

python
# python
# LC 133 - Clone Graph
# IDEA: DFS; copies = {original: copy} doubles as visited; wire each copy's neighbours from the recursion
# time = O(V + E), space = O(V)
class Node:
    def __init__(self, val=0, neighbors=None):
        self.val = val
        self.neighbors = neighbors if neighbors is not None else []

def cloneGraph(node):
    copies = {}                         # original -> its copy
    def dfs(cur):
        if cur in copies:
            return copies[cur]
        copy = Node(cur.val)
        copies[cur] = copy              # register BEFORE recursing, or a cycle recurses forever
        for nxt in cur.neighbors:
            copy.neighbors.append(dfs(nxt))
        return copy
    return dfs(node) if node else None

陷阱

  • 空圖:node 是 None,答案也是 None,不是一個新節點。
  • map 的 key 用節點物件,不要用 node.val:LC 133 的值剛好唯一,但拿掉這個保證的追問會讓以值為 key 的 map 無聲地出錯。

Union-Find 範例

Most Stones Removed with Same Row or Column — LC 947

關鍵 — 一個有 k 顆石頭的連通分量總能縮到只剩一顆,所以答案是 n − 分量數。邊沒有直接給: 兩顆石頭同列或同行就相連。把列和行本身當成 DSU 節點,每顆石頭就是一條邊 第 r 列 — 第 c 行。 這樣就不必比較全部 O(n²) 對。

歸約到:graph § Template 3 — union-find,只是節點是屬性值而不是石頭本身。

python
# python
# LC 947 - Most Stones Removed with Same Row or Column
# IDEA: union ("row", r) with ("col", c) for each stone; answer = stones - components
# time = O(n log n) with path halving alone, space = O(n)
def removeStones(stones):
    parent = {}
    def find(x):
        parent.setdefault(x, x)
        while parent[x] != x:
            parent[x] = parent[parent[x]]      # path halving
            x = parent[x]
        return x
    def union(a, b):
        ra, rb = find(a), find(b)
        if ra != rb:
            parent[ra] = rb
    for r, c in stones:
        union(("row", r), ("col", c))          # the stone IS the edge row r -- column c
    roots = {find(("row", r)) for r, c in stones}
    return len(stones) - len(roots)

# [[0,0],[0,1],[1,0],[1,2],[2,1],[2,2]] -> 5   (one component of 6 stones)
# [[0,0],[0,2],[1,1],[2,0],[2,2]]       -> 3   (components of 4 and 1 stones)
java
// java
// LC 947 - Most Stones Removed with Same Row or Column
// IDEA: union the stone's row with the stone's column (~col keeps the two id spaces apart)
// time = O(n log n) with path halving alone, space = O(n)
import java.util.*;

class Solution {
    private Map<Integer, Integer> parent = new HashMap<>();

    private int find(int x) {
        parent.putIfAbsent(x, x);
        while (parent.get(x) != x) {
            parent.put(x, parent.get(parent.get(x)));   // path halving
            x = parent.get(x);
        }
        return x;
    }

    private void union(int a, int b) {
        int ra = find(a), rb = find(b);
        if (ra != rb) {
            parent.put(ra, rb);
        }
    }

    public int removeStones(int[][] stones) {
        parent = new HashMap<>();
        for (int[] s : stones) {
            union(s[0], ~s[1]);          // row id = r, column id = ~c (negative, never a row)
        }
        Set<Integer> roots = new HashSet<>();
        for (int[] s : stones) {
            roots.add(find(s[0]));
        }
        return stones.length - roots.size();
    }
}

陷阱

  • 列和行都是小整數,所以 union(r, c) 會把第 1 列和第 1 行併在一起。要把兩個命名空間分開: 加標籤的 tuple、~c,或 c + 10001。
  • 數根,不要數成功合併的次數。不論大小,一個分量只留下一顆。
  • 兩兩比較的版本(同列或同行的石頭兩兩合併)正確但是 O(n²);那是 union_find_examples.md 裡的版本。

相似題 — LC 721(共享屬性是 email)· LC 990(共享屬性是變數)

二分圖檢查範例

Possible Bipartition — LC 886

關鍵 — 「分成兩組,讓互相討厭的人不在同一組」恰好就是「討厭關係圖是不是二分圖?」。 先用配對建圖,再對每個分量二著色:一條邊兩端同色就是奇數環,不可能分組。

歸約到:graph § Template 6 — 和 LC 785 一樣的 BFS 二著色,只是先從邊列表建圖,而且人是從 1 開始編號。程式碼就是 Template 6 的,跑在討厭關係圖上;奇偶併查集的寫法在 graph_advanced § Template 3。

陷阱

  • 討厭關係圖通常不連通:要從每一個還沒上色的人發射,否則第二個分量裡的衝突永遠看不到。
  • 人編號是 1..n,所以顏色陣列要開 n + 1。

相似題 — LC 785(圖以鄰接表給出)· LC 1042(四種顏色:用 greedy,不是二著色)