07.4 圖論初步#
本節目標#
- 建立圖(graph)的直覺,學會用 adjacency matrix 表示圖。
- 用 SymPy 矩陣操作圖,理解「矩陣乘法 = 路徑計數」的意義。
- 用程式驗證 Euler path 的存在條件。
- 認識組合計數在圖上的應用(生成樹數目的直覺),並實驗:建立六都市連通圖並數路徑。
一、圖的直覺與 adjacency matrix#
圖由頂點(vertex)與邊(edge)組成,用來描述「哪些東西之間有連結」:城市與公路、人與友誼、網頁與超連結。
adjacency matrix \(A\) : \(n\) 個頂點的圖用一個 \(n \times n\) 的 0-1 矩陣表示,若頂點 \(i\) 與 \(j\) 有邊相連,則 \(A_{ij} = 1\) 。無向圖的 \(A\) 是對稱矩陣。
例如三個頂點 \(1-2\) 、 \(2-3\) 相連的圖:
from sympy import Matrix, eye, zeros
A = Matrix([[0, 1, 0],
[1, 0, 1],
[0, 1, 0]])
print(A.is_symmetric()) # True
print(A[0, 2]) # 0:頂點 1 與 3 不相連
二、矩陣乘法的意義:路徑計數#
圖論最美的結果之一:
原因: \((A^2)_{ij} = \sum_{t} A_{it} A_{tj}\) ,正好枚舉「先走一步到 \(t\) 、再走一步到 \(j\) 」的所有中間點 \(t\) 。依數學歸納法可推廣到任意 \(m\) 。
用例子驗證:上面的三頂點圖, \(A^2\) 的 \((1,3)\) 元素應為 1(路徑 \(1 \to 2 \to 3\) ):
A2 = A**2
print(A2)
# Matrix([[1, 0, 1],
# [0, 2, 0],
# [1, 0, 1]])
print(A2[0, 2]) # 1:1 到 3 走兩步恰有一條路
A3 = A**3
print(A3[0, 2]) # 0:1 到 3 走三步沒有路(會走回頭)
print(A3[0, 0]) # 2:1 走三步回到自己的路有兩條(1-2-1-2?不,是 1-2-3-2 與 1-2-1-2 需重畫圖檢查)
自環數 \((A^2)_{ii}\) 等於頂點 \(i\) 的度數(degree)——與它相連的邊數。這也是一個矩陣語言翻譯圖論概念的例子:
print([A2[i, i] for i in range(3)]) # [1, 2, 1],頂點 2 的度數是 2
三、Euler path 條件的驗證實驗#
Euler path:走過圖中每一條邊恰好一次的路徑。Euler circuit(一筆畫回到起點)則要求起點也是終點。
Euler 的經典定理:
- 圖連通且恰有 0 或 2 個奇度數頂點時,存在 Euler path;
- 0 個奇度數頂點時存在 Euler circuit(可以一筆畫回原點)。
七橋問題就是因為四個頂點度數都是奇數而無解。寫一個檢查器驗證:
def degrees(A):
"""無向圖:度數 = A^2 對角線"""
return [A**2[i, i] for i in range(A.shape[0])]
def has_euler_path(A):
"""檢查連通性(用所有頂點兩兩有路徑;此例以 A 的冪判斷)與奇度數頂點個數"""
n = A.shape[0]
# 連通性:所有非對角元素在 A + A^2 + ... + A^(n-1) 中不為 0
reach = zeros(n)
P = eye(n)
for _ in range(1, n):
P = P * A
reach = reach + P
connected = all(reach[i, j] + reach[j, i] != 0
for i in range(n) for j in range(i + 1, n))
odd = sum(1 for d in degrees(A) if d % 2 == 1)
return connected and odd in (0, 2)
# 測試 1:三頂點路徑圖 1-2-3,度數 [1,2,1],奇度數 2 個 -> 有 Euler path
print(has_euler_path(A)) # True
# 測試 2:三角形,度數 [2,2,2],奇度數 0 個 -> 有 Euler circuit
T = Matrix([[0, 1, 1],
[1, 0, 1],
[1, 1, 0]])
print(has_euler_path(T)) # True
# 測試 3:星形圖(中心連三葉),度數 [1,1,1,3],奇度數 4 個 -> 無 Euler path
S = Matrix([[0, 1, 0, 0],
[1, 0, 1, 1],
[0, 1, 0, 0],
[0, 1, 0, 0]])
print(has_euler_path(S)) # False
用 Euler path 的定義人工驗證測試 1: \(1 \to 2 \to 3\) 走完兩條邊各一次,確實是 Euler path。
四、組合計數在圖上的應用:數生成樹的直覺#
生成樹(spanning tree):保留原圖所有頂點、邊數恰為 \(n-1\) 且連通的子圖。一個連通圖可能有非常多棵生成樹——這是組合計數問題。
經典結果(Matrix-Tree 定理的直覺版):**完全圖 \(K_n\) **(任兩點都有邊)的生成樹數目是
用小圖驗證: \(K_3\) 有 \(3^1 = 3\) 棵生成樹(三條邊去掉任何一條):
# K3:3 條邊,生成樹需 2 條邊且連通 -> 3 種
print(3**(3 - 2)) # 3
# K4:理論值 4^2 = 16,可以用暴力枚舉驗證
K4 = Matrix([[0, 1, 1, 1],
[1, 0, 1, 1],
[1, 1, 0, 1],
[1, 1, 1, 0]])
edges = [(i, j) for i in range(4) for j in range(i + 1, 4)]
from itertools import combinations
def connected(n, chosen_edges):
reach = eye(n)
P = eye(n)
for _ in range(n - 1):
B = zeros(n)
for i, j in chosen_edges:
B[i, j] = B[j, i] = 1
P = P * B
reach = reach + P
return all(reach[i, j] > 0 for i in range(n) for j in range(n) if i != j)
count = sum(1 for c in combinations(edges, 3) if connected(4, c))
print(count) # 16
print(4**(4 - 2)) # 16,Cayley 公式吻合
另一個組合計數視角:完全圖的邊數是 \(\binom{n}{2}\) ——「任兩點連一條邊」就是從 \(n\) 點中選 2 點:
from sympy import binomial
print(binomial(5, 2)) # 10 條邊
五、動手實驗:六都市連通圖並數路徑#
建立六都市的公路連通圖(無向),並用矩陣冪數路徑:
cities = ['台北', '桃園', '台中', '嘉義', '台南', '高雄']
# 邊:台北-桃園, 台北-台中, 桃園-台中, 台中-嘉義, 台中-台南, 嘉義-台南, 台南-高雄
edges = [(0, 1), (0, 2), (1, 2), (2, 3), (2, 4), (3, 4), (4, 5)]
C = zeros(6)
for i, j in edges:
C[i, j] = C[j, i] = 1
print(C)
# 數路徑:台北到高雄走 2 步、3 步、4 步各有幾條
P2 = C**2
P3 = C**3
P4 = C**4
print('台北->高雄 2 步路徑數:', P2[0, 5]) # 0
print('台北->高雄 3 步路徑數:', P3[0, 5])
print('台北->高雄 4 步路徑數:', P4[0, 5])
列出台北到高雄 4 步的所有路徑(人工對照矩陣結果):
def all_paths(cur, target, visited, path):
if cur == target and len(path) - 1 == 4:
print(' -> '.join(cities[v] for v in path))
return
if len(path) - 1 > 4:
return
for nb in range(6):
if C[cur, nb] == 1 and nb not in path or (nb == target and C[cur, nb] == 1):
all_paths(nb, target, visited, path + [nb])
all_paths(0, 5, set(), [0])
實驗 2:用 Euler 條件檢查這個都市圖:
print([C**2[i, i] for i in range(6)])
# 度數:台北 2、桃園 2、台中 4、嘉義 2、台南 3、高雄 1
# 奇度數頂點有 2 個(台南、高雄)-> 存在 Euler path,起訖點正是台南與高雄!
實驗 3:把圖加密一點——若新闢「台北-台南」高速道路,路徑數怎麼變?
C2 = C.copy()
C2[0, 4] = C2[4, 0] = 1
print('新增道路後台北->高雄 3 步路徑數:', (C2**3)[0, 5])
觀察重點: 1. adjacency matrix 把圖論問題翻譯成線性代數;矩陣乘冪自動「枚舉」所有路徑。 2. 度數藏在 \(A^2\) 的對角線,Euler 條件只需數對角線上的奇數。 3. 新增一條邊會以組合的方式增加大量路徑——這正是「連通性」的價值。
小結與練習#
本節我們學會了: - 用 adjacency matrix 表示圖,並理解 \((A^m)_{ij}\) 是 \(i\) 到 \(j\) 的 \(m\) 步路徑數。 - Euler path 條件:連通且奇度數頂點恰 0 或 2 個。 - 生成樹計數的直覺與 Cayley 公式 \(n^{n-2}\) (完全圖)。 - 用 SymPy 矩陣對六都市圖做路徑計數。
練習: 1. 畫出 \(K_4\) 的 adjacency matrix,驗證 \((A^3)_{ii} = 6\) 對每個頂點成立(每點有 6 條 3 步迴路)。 2. 構造一個連通但沒有 Euler path 的 4 頂點圖,用本節的 has_euler_path 驗證。 3. 對六都市圖計算 \(C + C^2 + C^3\) ,檢查是否所有頂點對都可達(連通性驗證)。 4. 用 Matrix-Tree 定理的完整版(刪去度數矩陣與 adjacency 的差的任一餘子式的行列式)計算六都市圖的生成樹數目,並與暴力枚舉比較。