6.3 從3-SAT到圖著色的實例轉換#
概念說明#
把 3-SAT 化約到 3-著色是化約技巧的教科書範例:它示範了如何用「圖的結構」表達「邏輯的約束」。構造需要三種零件:
- 真值三角(truth gadget):三個特殊頂點 T(真)、F(假)、R(紅),兩兩相連成三角形。任何合法 3-著色必須給它們三種不同顏色,我們把其中一種顏色解讀為「真」、一種為「假」。
- 變數零件(variable gadget):每個變數 xᵢ 對應兩個頂點 xᵢ 與 ¬xᵢ,兩者相連,且各自與 R 相連。於是 xᵢ 與 ¬xᵢ 必須一個取「真色」、一個取「假色」——圖的著色被迫模擬了變數的賦值。
- 子句零件(clause gadget):每個子句 C = (a ∨ b ∨ c) 對應一個小三角形的六個頂點(每個文字兩個頂點相連成對),其中輸入頂點與對應的文字頂點相連。構造經過精心設計,使得:子句可滿足 ⟺ 子句零件可著色。若三個文字全為假色,著色會沿著零件內部傳播衝突,導致無解。
整張圖有 3 + 2n + 6m 個頂點,邊數為多項式級,構造時間 O(n + m)。其正確性證明是雙向的:滿足指派 → 可著色(照指派上色、零件內部有固定配色方案);可著色 → 滿足指派(由真值三角的顏色讀出指派,子句零件的可著色性保證每個子句有真文字)。這種「每個子句一個小機關」的設計模式,在 3-SAT 化約到頂點覆蓋、集團等問題時反覆出現。
演算法步驟#
- 建立頂點 T、F、R,加邊 (T,F)、(F,R)、(T,R)。
- 對每個變數 i:建頂點 xi、!xi,加邊 (xi, !xi)、(xi, R)、(!xi, R)。
- 對每個子句 j = (a, b, c):建立輸入頂點 a₁、b₁、c₁ 與輸出頂點 a₂、b₂、c₂,組成內部結構,並把 a₁ 連到對應的文字頂點。
- 得到圖 G,共 3 + 2n + 6m 個頂點。
- 對 G 跑 3-著色回溯(第 5.3 節的程式)。
- 若有解:讀出每個 xi 頂點的顏色,等於 T 色者賦值 True。
Python 程式碼範例#
def sat_to_3color(n, clauses):
"""3-SAT -> 3-著色,回傳 (頂點數, 邊集, 文字->頂點映射)"""
V, E = ['T', 'F', 'R'], []
E += [('T', 'F'), ('F', 'R'), ('T', 'R')]
lit_vertex = {}
def vname(lit):
key = lit
if key not in lit_vertex:
lit_vertex[key] = f"v{lit}"
V.append(f"v{lit}")
return lit_vertex[key]
for i in range(1, n + 1):
for lit in (i, -i):
v = vname(lit)
E += [(v, 'R')]
E.append((vname(i), vname(-i)))
for j, clause in enumerate(clauses):
# 簡化子句零件:三個輸入頂點互連成一個三角形,
# 每個輸入頂點再連到對應文字頂點與 F(迫使至少一個非假色)
ins = []
for lit in clause:
name = f"c{j}_{lit}"
V.append(name)
ins.append(name)
E.append((name, vname(lit)))
E += [(ins[0], ins[1]), (ins[1], ins[2]), (ins[0], ins[2])]
for name in ins:
E.append((name, 'F'))
return len(V), E, lit_vertex
def color_3(nV, E):
"""簡單回溯 3-著色(頂點以字串命名)"""
adj = {v: set() for v in set(
[str(i) for i in range(nV)] + [u for u, _ in E])}
for u, w in E:
adj[u].add(w); adj[w].add(u)
names = list(adj)
color = {}
def solve(k):
if k == len(names):
return True
v = names[k]
used = {color[u] for u in adj[v] if u in color}
for c in range(3):
if c not in used:
color[v] = c
if solve(k + 1):
return True
del color[v]
return False
return color if solve(0) else None
n = 2
clauses = [[1, 2, -1], [-1, -2, 2]]
nV, E, lv = sat_to_3color(n, clauses)
c = color_3(nV, E)
print("可著色:" , c is not None) # True ⟺ 公式可滿足
複雜度分析#
- 化約構造:頂點數 3 + 2n + O(m·|clause|),邊數同階,時間 O(n + m)——這是化約「多項式時間」要求的最直接展示。
- 著色求解:回溯法最壞 O(3^(3+2n+6m)),但化約後的圖有豐富結構(真值三角先定色可大幅剪枝:先固定 T、F、R 的顏色,再讓變數頂點傳播)。
- 正確性核心:子句零件確保「三個文字皆假 → 無法著色」。讀者可用程式列舉小實例驗證此雙向對應。
- 教學意義:本化約把「邏輯約束」編碼進「鄰接關係」,是理解 NP-complete 問題互相轉換的關鍵範本。
練習#
- 寫一個函式,從 3-著色的結果還原變數指派,並用 SAT 驗證器確認其滿足原公式。
- 完整實作本節的子句零件(含每個文字成對的六頂點版本),並用窮舉驗證:三文字全假時該零件確實不可著色。
- 把化約改成「3-SAT → 4-著色」:放寬顏色數後構造可以簡化到什麼程度?討論為什麼 k 越大化約越容易、但問題(k ≥ 3)難度不變。