2.2 卡諾圖與化簡#
概念說明#
卡諾圖(Karnaugh Map, K-map)由貝爾實驗室的 Maurice Karnaugh 於 1953 年提出,是幫助「人類」化簡布林函數的可視化工具。它把真值表排成一個二維表格,但斜排的順序經過特別設計:相鄰兩格的輸入組合只差一個位元(Gray 碼排列)。這個性質讓「可以合併的最小項」在圖上彼此相鄰,肉眼就能圈選出來。
以 4 變數(a, b, c, d)為例,橫軸用 Gray 碼 00, 01, 11, 10 排列 cd,縱軸同樣排列 ab。這樣每個格子水平或垂直移動一格,對應的輸入恰好差一個位元,於是:
- 兩個相鄰的 1 可圈成一個 2 格組,消去一個變數(A·B + A·¬B = A)
- 4 個相鄰的 1(呈 2×2 或一條 1×4)可圈成一個 4 格組,消去兩個變數
- 8 個相鄰的 1 消去 3 個變數,依此類推
規則是:圈越大越好(消去越多變數,積項越短)、圈數越少越好(產生越少的積項)、每個 1 至少要被一個圈涵蓋(必要圈 prime implicant 先圈)、邊界相接(環繞)也要算相鄰。
卡諾圖的侷限非常明顯:2 變數用 4 格、4 變數用 16 格還能畫,5 變數要兩張圖疊起來對應,6 變數以上人類已難以應付。因此在 EDA 自動化流程中,卡諾圖主要是教學與人工檢驗用途;機器化的化簡靠 2.3 節的 Quine-McCluskey,以及 Espresso 這類啟發式兩層化簡器。
演算法步驟:卡諾圖化簡#
- 畫出 2ⁿ 格的二維圖,軸以 Gray 碼標記。
- 在函數為 1 的格子填 1(可先忽略 don't care ×)。
- 圈選最大的 2 開冪個相鄰「1」群組(可用環繞邊界)。
- 每個圈產生一個積項:圈內不變的變數保留,被消去的(圈內同時出現 0 與 1 的)捨棄。
- 檢查所有 1 都被涵蓋後,把所有積項 OR 起來。
Don't care(無關項):某些輸入組合永遠不會發生(如 BCD 中的 10~15)。這些位置可標 ×,圈選時「想圈就圈、不圈也行」,用來放大圈、得到更短的積項。
Python 程式碼範例#
下面用程式實作一張 4 變數卡諾圖的座標系統,並用「枚舉所有可能的 2 開冪冪次方形組、再吸收」的啟發式,模擬人工圈選的過程:
from itertools import product
# Gray 碼順序:相鄰位元只差一個
gray = [0, 1, 3, 2] # 對應 00,01,11,10
def kmap_cells(minterms, care=True):
"""建立 4 變數卡諾圖:cell -> 1/0"""
cells = {}
for a, b, c, d in product([0,1], repeat=4):
code = (a<<3) | (b<<2) | (c<<1) | d
cells[(a,b,c,d)] = code in minterms
return cells
def adjacent_groups(cells, minterms, size):
"""枚舉所有 2^k(k=給定 size 的位元數)個相鄰格構成的組;此處
利用技巧:一個組 = 固定某些變數不變、其餘變數自由枚舉,
且組內全為 1 才合法。"""
groups = []
# 選擇固定不變的變數位置,其餘枚舉(組大小 = 2^自由數)
for frees in [(0,), (1,), (2,), (3,), # 8格組
(0,1), (0,2), (0,3), (1,2), (1,3), (2,3), # 4格組
(0,1,2), (0,1,3), (0,2,3), (1,2,3)]: # 2格組
fixed_vals = [b for i, b in enumerate(product([0,1], repeat=4 - len(frees))[0])
if i not in []] # 佔位,實際用下面方式
_ = fixed_vals
for fixed_value in product([0,1], repeat=4 - len(frees)):
members = []
for a,b,c,d in product([0,1], repeat=4):
v = (a,b,c,d)
fv = 0
ok = True
idx = 0
for i in range(4):
if i not in frees:
if v[i] != fixed_value[idx]:
ok = False; break
idx += 1
if ok:
members.append(v)
if all(cells[m] for m in members):
groups.append(frozenset(members))
# 去除相同成員的組
return set(groups)
def cover(minterms):
"""卡諾圖圈選:由大到小選組 + 吸收"""
cells = {k: k in minterms for k in product([0,1], repeat=4)}
groups6, groups4, groups2 = [], [], []
for frees_size, bucket in ((3, groups6), (2, groups4)):
pass
# 重新以「自由變數數」分組
def groups_with(free_count):
res = set()
for frees in product(range(4), repeat=free_count):
if len(set(frees)) != free_count: continue
fixed = [i for i in range(4) if i not in frees]
for fv in product([0,1], repeat=len(fixed)):
members = [v for v in product([0,1], repeat=4)
if all(v[fixed[i]] == fv[i] for i in range(len(fixed)))]
if all(cells[m] for m in members):
lits = frozenset((fixed[i], fv[i]) for i in range(len(fixed)))
res.add(frozenset(members))
return res
G2, G3, G4 = groups_with(2), groups_with(3), groups_with(4)
chosen, covered = [], set()
for pool in (G4, G3, G2): # 先用大組
for g in sorted(pool):
if not set(g) <= covered:
covered |= set(g)
chosen.append(g)
return chosen
def term_of(group):
"""由組成員回推積項"""
vals = [list(v) for v in group]
lits = []
for i in range(4):
col = {v[i] for v in vals}
if len(col) == 1:
lits.append("abcd"[i] if col.pop() else "¬" + "abcd"[i])
return "·".join(lits) if lits else "1"
minterms = {(1,0,1,0), (1,0,1,1), (1,1,1,0), (1,1,1,1)} # ab 不變 = ab·c?
print("真值為1的組合:", sorted(minterms))
for g in sorted(cover(minterms)):
print("圈選積項:", term_of(g))
輸出:
真值為1的組合: [(1,0,1,0), (1,0,1,1), (1,1,1,0), (1,1,1,1)]
圈選積項: a·c
四個最小項被一個 4 格組圈完,化簡為 a·c(b 與 d 都被消去)——這正是人工在卡諾圖上會得到的答案。
複雜度分析#
- 卡諾圖人力法只適用 n ≤ 6(格數 2ⁿ,人眼圈選可用,但自動化仍窮舉組)。
- 上面的自動圈選:組型態數 O(n·2ⁿ),乘上吸收步驟,複雜度約 O(3ⁿ),比 Q-M 快但在實務上仍稱不上線性。
- 任何「求最小兩層 SOP」的精確演算法都是 NP-hard 近親,工業工具(如 Espresso-II)採用啟發式,速度快數個數量級但結果近似最小。
練習#
- 手繪 f(a,b,c,d) = Σm(0,1,2,3,8,9,10,11) 的卡諾圖,圈出最大組並寫出最簡 SOP(提示:可圈成一個 8 格組)。
- 加入 don't care:f = Σm(1,3,7) + d(0,2,5),比較「把 d 全部視為 0」與「善用 d」兩種結果的閘數差異。
- 修改程式,支援「偏好使用 don't care 塞進組裡」的圈選,測試第 2 題的函數是否化得更短。