圖論實戰題解
範圍 — 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
# 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
# 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
# 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
// 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,不是二著色)