8.1 拓撲的驅動:龐加萊(1895)與同調的誕生#
學習目標#
- 了解龐加萊在 1895 年《位置分析》(Analysis Situs)中為研究多面體與流形而發明同調的歷史動機。
- 掌握鏈複形(chain complex)、邊界算子 \(\partial\) 與同調群 \(H_n = \ker \partial_n / \operatorname{im} \partial_{n+1}\) 的嚴格定義,並會證明 \(\partial^2 = 0\) 。
- 會對具體的單純複形(圓、球面、環面、射影平面)計算整係數同調群。
- 理解同調群與有限交換群結構定理(5.6 節)的深刻關聯:同調群的不變因子正是矩陣的 Smith 標準形對角元。
- 會用 SymPy(含
sympy.matrices.normalforms.smith_normal_form)實作整數鏈複形並計算同調群。
歷史背景#
為什麼要發明同調?#
19 世紀中葉,數學家面對一個樸素但頑固的問題:如何在不依賴度量(長度、角度)的情況下,區分不同的幾何圖形?
- 黎曼(Riemann,1851、1857) 在研究阿貝爾積分與代數函數時,發現曲面上的「連通階數」(连通性 / Geschlecht,即虧格)決定了積分的行為。這是「拓撲不變量控制分析」的第一個訊號。
- 貝蒂(Enrico Betti,1871) 受黎曼啟發,把「連通階數」推廣到高維,定義了後世所稱的貝蒂數(Betti numbers)。但貝蒂的定義是直觀的、缺乏嚴格代數機制的。
- 龐加萊(Henri Poincaré,1895) 為了研究天體力學中軌道的定性性質,以及多面體、流形的分類問題,發表了 108 頁的巨著《位置分析》(Analysis Situs),並在 1899–1904 年間寫了五篇補篇。他引進了單純複形、鏈、邊界算子與同調的計算機制——同調從此從直觀變成可計算的代數。
- 諾特(Emmy Noether,1925) 在海德堡與哥廷根的討論班上指出:同調不該被想成「數」或「形式和」,而應該被想成阿貝爾群,邊界算子是群同態。這個觀點把同調變成了純代數對象,也直接催生了同調代數(見 8.2 節)。
timeline
title 同調誕生的時間線
1851年 : 黎曼:曲面的連通階數(虧格)
1871年 : 貝蒂:高維連通數(貝蒂數)
1895年 : 龐加萊《位置分析》:單純同調、對偶定理
1899-1904年 : 五篇補篇:撓(torsion)、基本群
1925年 : 諾特:同調 = 阿貝爾群,邊界 = 同態
1945年 : Eilenberg–Mac Lane:範疇論與函子(見 8.2 節)這是「技術與理論互相驅動」的又一個範例:龐加萊想計算多面體的洞,於是被迫發明鏈複形;而鏈複形一旦變成純代數(諾特),立刻被抽象成同調代數,反過來成為 20 世紀代數、拓撲、幾何的通用語言。
鏈複形與同調群#
單純複形#
定義 8.1.1(單純複形) 一個(抽象)單純複形 \(K\) 是一組有限集合(稱為單形,simplex),滿足:若 \(\sigma \in K\) 且 \(\tau \subseteq \sigma\) ,則 \(\tau \in K\) 。 \(q\) 維單形是有 \(q+1\) 個頂點的集合。記 \(K\) 的 \(q\) 維單形全體為 \(K_q\) 。
例如:三角形(2 維單形)的邊(1 維)與頂點(0 維)自動屬於複形。
鏈群與邊界算子#
定義 8.1.2(鏈群) 對交換群係數 \(G\) (通常取 \(G = \mathbb{Z}\) ), \(q\) 維鏈群是自由阿貝爾群
元素稱為 \(q\) 維鏈,形式上是 \(q\) 維單形的整係數線性組合。
定義 8.1.3(邊界算子) 邊界算子 \(\partial_q : C_q \to C_{q-1}\) 在 \(q\) 維單形 \(\sigma = [v_0, v_1, \ldots, v_q]\) 上定義為交錯和:
其中 \(\widehat{v_i}\) 表示刪去該頂點,再線性擴張到整條鏈。
定理 8.1.4(關鍵恆等式) \(\partial_{q-1} \circ \partial_q = 0\) ,即 \(\partial^2 = 0\) 。
證明:對 \(2\) 維情形直接驗證(一般情形同理,每個頂點被刪兩次、符號相反):
這個看似簡單的恆等式是整個同調代數的引擎:「邊界的邊界是空的」。它保證了下面的定義有意義。
同調群#
定義 8.1.5(鏈複形與同調) 一個鏈複形是一列阿貝爾群與同態
因為 \(\operatorname{im} \partial_{q+1} \subseteq \ker \partial_q\) ,可定義 \(q\) 維同調群:
- \(\ker \partial_q\) 的元素稱為閉鏈(cycles)——沒有邊界的鏈,幾何上是「洞」的候選。
- \(\operatorname{im} \partial_{q+1}\) 的元素稱為邊界鏈(boundaries)——本身是更高維區域的邊界,不算洞。
- 同調群衡量「是閉鏈但不是邊界」的鏈,也就是真正的洞。
注意 \(H_q\) 是兩個阿貝爾群的商群,由有限交換群結構定理(5.6 節),它必同構於
其中 \(\beta_q\) 稱為 \(q\) 維貝蒂數, \(d_i\) 稱為撓係數(torsion coefficients,龐加萊在 1899 年的第二補篇中發現,例如射影平面)。
歐拉–龐加萊公式#
定理 8.1.6(Euler–Poincaré,1895) 對有限單純複形 \(K\) ,
歐拉 1752 年的多面體公式 \(V - E + F = 2\) 是其在球面上的特例;龐加萊證明了它對任意複形成立,且右邊只依賴同調——拓撲不變量第一次以代數方式現身。
基本例子#
用標準 \(\Delta\) -複形結構(允許單形被同一頂點的排列黏合)計算:
| 空間 | \(H_0\) | \(H_1\) | \(H_2\) | 解讀 |
|---|---|---|---|---|
| 一點 | \(\mathbb{Z}\) | \(0\) | \(0\) | 無洞 |
| 圓 \(S^1\) | \(\mathbb{Z}\) | \(\mathbb{Z}\) | \(0\) | 一個一維洞 |
| 球面 \(S^2\) | \(\mathbb{Z}\) | \(0\) | \(\mathbb{Z}\) | 一個二維洞 |
| 環面 \(T^2\) | \(\mathbb{Z}\) | \(\mathbb{Z}^2\) | \(\mathbb{Z}\) | 兩個一維洞+一個二維洞 |
| 克萊因瓶 | \(\mathbb{Z}\) | \(\mathbb{Z} \oplus \mathbb{Z}/2\) | \(0\) | 有撓!不可定向 |
| 射影平面 \(\mathbb{R}P^2\) | \(\mathbb{Z}\) | \(\mathbb{Z}/2\) | \(0\) | 純撓,貝蒂數看不見 |
最後兩列說明:只算貝蒂數不夠,必須計算整個同調群(含撓),而撓的偵測正是 Smith 標準形(5.6 節)的工作。環面的兩個一維洞即「經圈」與「緯圈」,龐加萊當年正是為了研究環面上的積分與軌道而行此事。
graph LR
C3["C_2 = Z"] -->|"∂_2"| C2["C_1 = Z^2"] -->|"∂_1"| C1["C_0 = Z"]
style C3 fill:#eef,stroke:#88f
style C2 fill:#eef,stroke:#88f
style C1 fill:#eef,stroke:#88f上圖是環面的鏈複形片段。同調的計算本質上就是:對整數矩陣 \(\partial\) 做Smith 標準形(整數版的對角化),讀出核與像的商群結構。
SymPy 實作:整數鏈複形的同調#
核心工具:含基底變換追蹤的 Smith 標準形#
SymPy 內建 sympy.matrices.normalforms.smith_normal_form 可以算不變因子,但不回傳基底變換矩陣,而計算核 \(\ker \partial\) 需要變換矩陣。因此我們先手寫一個帶追蹤的版本(U A V = D, \(U, V\) 為么模矩陣),之後各節都會用到:
import sympy as sp
def snf_tracked(A):
"""整數矩陣 A 的 Smith 標準形,含么模變換追蹤。
回傳 (D, U, V):D = U * A * V,U、V 皆么模(行列式 ±1)。"""
if isinstance(A, sp.MatrixBase):
m, n = A.shape # 保留形狀(0 列的矩陣也要記得欄數)
A = A.tolist()
else:
m = len(A)
n = len(A[0]) if m > 0 else 0
a = [[int(x) for x in row] for row in A]
A0 = sp.Matrix(m, n, lambda i, j: a[i][j]) # 原始矩陣(保留形狀)
U = [[1 if i == j else 0 for j in range(m)] for i in range(m)]
V = [[1 if i == j else 0 for j in range(n)] for i in range(n)]
def rowop(E): # a <- E a, U <- E U
nonlocal a, U
a = [[sum(E[i][k]*a[k][j] for k in range(m)) for j in range(n)] for i in range(m)]
U = [[sum(E[i][k]*U[k][j] for k in range(m)) for j in range(m)] for i in range(m)]
def colop(F): # a <- a F, V <- V F
nonlocal a, V
a = [[sum(a[i][k]*F[k][j] for k in range(n)) for j in range(n)] for i in range(m)]
V = [[sum(V[i][k]*F[k][j] for k in range(n)) for j in range(n)] for i in range(n)]
def swapmat(n_, i, j):
E = [[1 if (r == c and r != i and r != j) else 0 for c in range(n_)] for r in range(n_)]
E[i][j] = E[j][i] = 1
return E
def rowswap(i, j):
rowop(swapmat(m, i, j))
def colswap(i, j):
colop(swapmat(n, i, j))
def rowcomb(i, j, q): # row_i <- row_i + q*row_j
E = sp.eye(m).tolist(); E[i][j] = q
rowop(E)
def colcomb(i, j, q): # col_i <- col_i + q*col_j
F = sp.eye(n).tolist(); F[j][i] = q
colop(F)
for k in range(min(m, n)):
while True:
piv = pr = pc = None
for i in range(k, m): # 找絕對值最小的非零元當軸心
for j in range(k, n):
if a[i][j] != 0 and (piv is None or abs(a[i][j]) < abs(piv)):
piv, pr, pc = a[i][j], i, j
if piv is None:
break
if pr != k: rowswap(k, pr)
if pc != k: colswap(k, pc)
piv = a[k][k]; ok = True
for j in range(k+1, n): # 消去第 k 列其餘元素
if a[k][j] % piv != 0: ok = False
colcomb(j, k, -(a[k][j] // piv))
for i in range(k+1, m): # 消去第 k 行其餘元素
if a[i][k] % piv != 0: ok = False
rowcomb(i, k, -(a[i][k] // piv))
if ok: # 軸心整除整列整行,收工
break
if a[k][k] < 0: # 對角元取非負
for j in range(n): a[k][j] = -a[k][j]
for j in range(n): V[j][k] = -V[j][k]
D = sp.Matrix(m, n, lambda i, j: a[i][j])
Um, Vm = sp.Matrix(U), sp.Matrix(V)
assert Um.det() in (1, -1) and Vm.det() in (1, -1)
assert (Um * A0 * Vm - D).is_zero_matrix
return D, Um, Vm
由鏈複形算同調#
給 \(\partial_{q+1} : C_{q+1} \to C_q\) 與 \(\partial_q : C_q \to C_{q-1}\) (皆整數矩陣,且 \(\partial_q \partial_{q+1} = 0\) ),計算 \(H_q = \ker \partial_q / \operatorname{im} \partial_{q+1}\) 的步驟:
- 用 Smith 標準形的變換矩陣 \(V\) 求出 \(\ker \partial_q\) 的整數基底(行向量)。
- 把 \(\operatorname{im} \partial_{q+1}\) 的每個生成元用該基底表出(解線性方程,係數必為整數,因為 \(\operatorname{im} \partial_{q+1} \subseteq \ker \partial_q\) )。
- 對表出矩陣再做一次 Smith 標準形,讀出商群 \(\mathbb{Z}^d / \operatorname{im}\) 的自由秩與不變因子。
import sympy as sp
def kernel_basis(B):
"""B: Z^k -> Z^m 的整數核基底(回傳 k x d 矩陣,每一欄是一個基底向量)。"""
B = sp.Matrix(B)
m, k = B.shape
D, U, V = snf_tracked(B)
# D y = 0 的整數解由「零對角元所在的座標」生成;x = V y
free_cols = [j for j in range(k) if j >= min(m, k) or D[j, j] == 0]
return sp.Matrix([[V[i, j] for j in free_cols] for i in range(k)])
def coker_structure(A):
"""Z^m / im A 的結構:回傳 (自由秩, [不變因子])。"""
A = sp.Matrix(A)
m, n = A.shape
D, U, V = snf_tracked(A)
invars = [int(D[i, i]) for i in range(min(m, n)) if D[i, i] > 1]
rank = sum(1 for i in range(min(m, n)) if D[i, i] != 0)
return m - rank, invars
def homology(d_higher, d_lower):
"""H = ker d_lower / im d_higher(兩者的餘域都是 C_q = Z^m)。
回傳 (自由秩 beta_q, [撓不變因子])。"""
d_lower = sp.Matrix(d_lower)
d_higher = sp.Matrix(d_higher)
K = kernel_basis(d_lower) # m x d,核基底(欄為基底向量)
d = K.shape[1]
if d == 0:
return 0, []
C = sp.zeros(d, d_higher.shape[1]) # 像(d_higher) 在核基底下的座標
for j in range(d_higher.shape[1]):
sol, _ = K.gauss_jordan_solve(d_higher[:, j])
assert all(x.is_Integer for x in sol) # 座標必為整數
C[:, j] = sol
return coker_structure(C)
例一:環面 \(T^2\)#
呼叫 homology(d_higher, d_lower) 時,兩個矩陣必須有相同的餘域 \(C_q = \mathbb{Z}^m\) (因為 \(H_q = \ker \partial_q / \operatorname{im} \partial_{q+1}\) 的分子分母都是 \(C_q\) 的子群),每個維度一次算一個。
環面採用 Hatcher 的標準 \(\Delta\) -複形:一個頂點 \(v\) 、三條閉邊 \(a, b, c\) 、兩個三角形 \(U, L\) ,其中 \(\partial U = a + b - c\) 、 \(\partial L = c - a - b\) ,所以 \(\partial_1 = 0\) (每條邊兩端都是 \(v\) )。
import sympy as sp
# ---- 前置:snf_tracked / kernel_basis / coker_structure(見上文定義) ----
# 環面 T^2:C_2 = Z^2, C_1 = Z^3, C_0 = Z^1
d2 = sp.Matrix([[ 1, -1], # a 分量
[ 1, -1], # b 分量
[-1, 1]]) # c 分量
d1 = sp.zeros(1, 3) # ∂_1 : Z^3 -> Z^1
assert (d1 * d2).is_zero_matrix # 驗證 ∂² = 0
print("H_2 =", homology(sp.zeros(3, 0), d2)) # H_2 = ker d2 / 0
print("H_1 =", homology(d2, d1)) # H_1 = ker 0 / im d2
print("H_0 =", homology(d1, sp.zeros(0, 1))) # H_0 = ker 0 / im d1
輸出:
H_2 = (1, [])
H_1 = (2, [])
H_0 = (1, [])
即 \(H_2(T^2) = \mathbb{Z}\) 、 \(H_1(T^2) = \mathbb{Z}^2\) (經圈與緯圈兩個洞)、 \(H_0(T^2) = \mathbb{Z}\) (連通)。
與 8.1 節的表完全一致:環面有兩個獨立的一維洞與一個二維洞,且無撓。
例二:射影平面 \(\mathbb{R}P^2\) ——撓的現身#
\(\mathbb{R}P^2\) 用最簡的 CW 結構:一個 \(0\) 維胞腔、一個 \(1\) 維胞腔 \(a\) 、一個 \(2\) 維胞腔沿映射 \(a \mapsto 2a\) (度數 \(2\) )貼上。於是 \(C_2 = \mathbb{Z}\) 、 \(C_1 = \mathbb{Z}\) 、 \(C_0 = \mathbb{Z}\) ,邊界矩陣 \(\partial_2 = [2]\) 、 \(\partial_1 = [0]\) :
import sympy as sp
# ---- 前置:snf_tracked / kernel_basis / coker_structure(見上文定義) ----
# RP^2:C_2 = Z, C_1 = Z, C_0 = Z
d2 = sp.Matrix([[2]]) # ∂_2 : Z -> Z,度數 2 的貼映射
d1 = sp.Matrix([[0]]) # ∂_1 : Z -> Z
assert (d1 * d2).is_zero_matrix
print("H_2 =", homology(sp.zeros(1, 0), d2))
print("H_1 =", homology(d2, d1))
輸出:
H_2 = (0, [])
H_1 = (0, [2])
即 \(H_2(\mathbb{R}P^2) = 0\) 、 \(H_1(\mathbb{R}P^2) = \mathbb{Z}/2\) 。
撓不變因子 \(2\) 被找出來了!若只看貝蒂數 \(\beta_1 = 0\) ,會誤以為射影平面沒有一維洞——這正是龐加萊 1899 年在第二補篇中引進撓的理由。同樣的 CW 手法可算克萊因瓶: \(\partial_2 = [0, 2]^T : \mathbb{Z} \to \mathbb{Z}^2\) 、 \(\partial_1 = 0\) ,讀者可驗證得到 \(H_1 = \mathbb{Z} \oplus \mathbb{Z}/2\) (見習題 3)。
對照:SymPy 內建的 smith_normal_form#
當我們只需要不變因子(例如判斷餘核結構)而不需要基底變換時,可直接用 SymPy 內建工具,記得指定整數域 ZZ:
import sympy as sp
from sympy.matrices.normalforms import smith_normal_form
from sympy import ZZ
M = sp.Matrix([[2, 4, 4],
[6, 6, 12]]) # 例:某個 ∂_1 的表現矩陣
D = smith_normal_form(M, domain=ZZ)
print(D)
# 解讀:Z^2 / im M = Z/2 ⊕ Z/6 (無自由部分)
Matrix([[2, 0, 0], [0, 6, 0]])
這驗證了 coker_structure 的輸出:不變因子 \(2, 6\) ,滿足整除鏈 \(2 \mid 6\) 。
SymPy 要點回顧#
sympy.matrices.normalforms.smith_normal_form(M, domain=ZZ):計算整數矩陣的不變因子(Smith 標準形對角元),但不回傳么模變換矩陣;計算核需要變換矩陣,故本節手寫snf_tracked。- 么模(unimodular)變換:行列式為 \(\pm 1\) 的整數矩陣,其逆仍是整數矩陣,對應鏈群的基底變換,不影響同調。
- 同調計算三部曲:
kernel_basis(整數核)→ 座標轉換(gauss_jordan_solve)→coker_structure(商群結構)。 - 核心定理 \(\partial^2 = 0\) 使 \(\operatorname{im} \subseteq \ker\) ,商群才有定義——程式中的
assert (d1 * d2).is_zero_matrix就是在驗證這件事。 - 有限交換群的結構定理(5.6 節)保證輸出 \((\beta_q, [d_1, \ldots, d_k])\) 唯一( \(d_1 \mid \cdots \mid d_k\) )。
習題#
- (∂² = 0) 寫出 \(\partial^2\) 在 \(3\) 維單形 \([v_0, v_1, v_2, v_3]\) 上的完整展開,驗證每一項兩兩抵消。(提示:每對 \((i, j)\) , \(i < j\) ,貢獻 \([v_i, v_j]\) 兩次、符號相反。)
- (圓的同調) 取 \(S^1\) 的一個頂點、一條邊的複形: \(C_1 = \mathbb{Z}\) 、 \(C_0 = \mathbb{Z}\) 、 \(\partial_1 = [0]\) 。用本節程式碼計算 \(H_0\) 與 \(H_1\) ,確認得到 \(\mathbb{Z}\) 與 \(\mathbb{Z}\) 。
- (克萊因瓶) 用邊界矩陣 \(\partial_2 = [0, 2]^T\) (一個 2 維胞腔)、 \(\partial_1 = 0\) (兩條邊都閉合)計算克萊因瓶的 \(H_1\) ,確認得到 \(\mathbb{Z} \oplus \mathbb{Z}/2\) 。
- (歐拉示性數) 用環面的邊界矩陣驗證: \(\chi = \#\{K_0\} - \#\{K_1\} + \#\{K_2\} = 2 - 3 + 2 = 1\) ?這與 \(\beta_0 - \beta_1 + \beta_2 = 1 - 2 + 1 = 0\) 矛盾嗎?(提示: \(\Delta\) -複形的 \(0\) 維胞腔數為 \(2\) 而非 \(1\) ;檢查你的計數,並用
coker_structure找出 \(H_0 = \mathbb{Z}\) 時 \(\chi = 0\) 的正確胞腔數。) - (模 \(2\) 係數的陷阱) 把 \(\mathbb{R}P^2\) 的 \(\partial_2\) 對 \(2\) 取模(係數變成 \(\mathbb{Z}/2\) ),計算「同調」,觀察撓 \(\mathbb{Z}/2\) 如何消失。這說明為什麼整係數計算不可取代。
- (挑戰:莫比烏斯帶) 建構莫比烏斯帶的單純複形(可用 \(5\) 個頂點、 \(10\) 個三角形),用程式計算 \(H_0, H_1, H_2\) ,並與「可帶邊曲面」的預期 \(H_1 = \mathbb{Z}\) 比對。
下一節 8.2 正合序列與蛇引理將把「核、像、商」的計算語言公理化,展示同調代數如何從拓撲的計算工具蛻變為獨立的代數理論。