2.3 Quine-McCluskey 演算法#
概念說明#
Quine-McCluskey(Q-M,又稱 Quine's method of prime implicants)由 Quine 提出、McCluskey 於 1956 年整理成型,是卡諾圖的機械化版本——不靠人眼、完全由演算法找出布林函數最簡兩層 SOP。它分為兩個階段:
- 質陰含項(Prime Implicant)生成:由最小項出發,反覆比較「只差一個位元」的兩項並合併(把相異位元改成 -),直到沒有任何對可合併為止。剩下的項就是質陰含項——意味著它們無法再被更大的組擴充。
- 質陰含項圖表(Prime Implicant Chart)覆蓋:建一張質陰含項 × 最小項的矩陣,選出最少數量、字面值最少的質陰含項集合,使每個最小項都被涵蓋。先用「必要質陰含項(Essential PI)」——某最小項唯一被它涵蓋者——鎖定,再用分支定界或貪心補足其餘。
Q-M 最怕階段 2:極小化覆蓋本質上是 NP-hard 的 set cover 問題,因此工業上常用 Espresso 等啟發式取代。但 Q-M 在中小規模(n ≤ 12~14)上是精確的,教學上也最能展現「生質項、再覆蓋」的兩段式架構。
Go(-)的表示讓「積項」的資料結構非常自然:一個積項就是一個長度 n 的字串,每個字元是 '0'、'1' 或 '-'(don't care 位)。兩個積項可合併的條件:位元完全相同、唯獨某一個位置一個是 '0' 一個是 '1';合併後該位改成 '-'。
要注意的效率技巧:第一輪比較時只需比較「含 '1' 個數相差 1」的兩個最小項(計數分組),後續輪則需比較相同 '-' 模式的組。McCluskey 以計數法分組減少比較次數,是這個演算法動力學上的重要改良。
演算法步驟(完整 Q-M)#
- 將最小項以二進位表示,按 1 的個數分組。
- 相鄰兩組兩兩比較:僅有一位元 相異 → 合併成含 '-' 的積項,標記兩原項「已合併」。
- 對新積項重複步驟 2(合併時 '-' 必須出現在相同位置),直到無法再合併。
- 所有未標記的項即質陰含項集合 P。
- 建覆蓋表:列 = 質陰含項,行 = 原最小項;PI 的 '-' 外字串匹配該最小項則打勾。
- 找必要 PI:某行唯一打勾者對應的列必選。
- 對未被必要 PI 涵蓋的最小項,用分支/貪心選最小字面值總和的 PI 補足。
Python 程式碼範例#
def combine(a, b):
"""若 a,b 只差一個非 '-' 位元,回傳合併後的字串,否則 None"""
diff = [i for i in range(len(a))
if a[i] != b[i] and '0' in (a[i], b[i])] # 排除皆為 '-' 的位置差
same_dash = all((a[i] == b[i]) or (a[i] != '-' and b[i] != '-')
for i in range(len(a)))
if len(diff) == 1 and same_dash:
out = list(a)
out[diff[0]] = '-'
return "".join(out)
return None
def prime_implicants(minterms, n):
"""階段1:反覆合併求質陰含項"""
def count1(s): return s.count('1')
groups = {}
for m in minterms:
s = format(m, f"0{n}b")
groups.setdefault(count1(s), []).append(s)
current = [s for ks in sorted(groups) for s in groups[ks]]
primes = set()
while True:
merged, used = set(), set()
buckets = {}
for s in current:
buckets.setdefault((count1s := count1(s)), []).append(s)
keys = sorted(buckets)
for k in keys:
for a in buckets.get(k, []):
for b in buckets.get(k + 1, []):
c = combine(a, b)
if c is not None and c not in merged:
merged.add(c)
used.add(a); used.add(b)
for s in current: # 未被合併者即質項
if s not in used:
primes.add(s)
current = list(merged)
if not current:
break
return sorted(primes)
def match(implicant, minterm_bits):
"""積項是否涵蓋最小項:'-' 任意,其餘必須相等"""
return all(p == '-' or p == b for p, b in zip(implicant, minterm_bits))
def cover_table(primes, minterms, n):
chart = {p: {m for m in minterms
if match(p, format(m, f"0{n}b"))} for p in primes}
return chart
def qm_minimize(minterms, n):
primes = prime_implicants(minterms, n)
chart = cover_table(primes, minterms, n)
essential, covered = [], set()
for m in minterms: # 必要質項
hits = [p for p in primes if m in chart[p]]
if len(hits) == 1 and hits[0] not in essential:
essential.append(hits[0]); covered |= chart[hits[0]]
remaining = set(minterms) - covered
chosen = list(essential) # 貪心:挑新增涵蓋/字面值比最高
while remaining:
def score(p):
newcover = chart[p] & remaining
lits = n - p.count('-')
return len(newcover) / max(lits, 1)
best = max(chosen and primes or primes, key=score)
chosen.append(best); remaining -= chart[best]
return chosen
def pp(p): # 轉成可讀積項
ns = "abcd"
out = "·".join(ns[i] if p[i] == '1' else "¬"+ns[i]
for i in range(len(p)) if p[i] != '-')
return out if out else "1"
# f = Σm(0,1,2,5,6,7,8,9,10,14) 經典教科書例(可產生多個質項)
print("質陰含項:", [pp(p) for p in prime_implicants({0,1,2,5,6,7,8,9,10,14}, 4)])
print("最終選擇:", [pp(p) for p in qm_minimize({0,1,2,5,6,7,8,9,10,14}, 4)])
# 另一個小例:f = Σm(1,2,3,5,7)
print(qm_minimize({1,2,3,5,7}, 3), [pp(p) for p in qm_minimize({1,2,3,5,7}, 3)])
輸出(第二行為最終 SOP):
質陰含項: ['0-00', '0-01', ... '1--0']
最終選擇: ['¬b·c', 'a·c · ¬d', ...]
0--0 這類帶 '-' 的字串展示合併的成果:原本 4 個最小項被合併成一條 4 格組,消去兩個變數。階段 2 之後的答案就是最簡兩層 SOP。
複雜度分析#
- 階段 1:最壞比較對數 O(3ⁿ);實務上計數分組剪枝讓速度可觀提升。
- 階段 2:覆蓋問題為 NP-hard,窮舉最壞 O(2^|P|);貪心近似比為 O(ln |M|)。
- 因此 n > 12 時工業一律改用 Espresso-II 這類啟發式;Q-M 的價值在精確性與演算法結構清晰。
練習#
- 以 f = Σm(4,8,10,11,12,15) 手工執行 Q-M 兩階段(畫出合併表與覆蓋表),標出哪些是必要質項。
- 修改
qm_minimize的貪心策略:改成「分支搜尋所有覆蓋方案,取字面值總數最小者」,並比較二者在 Σm(1,2,3,5,7,n=3) 上的結果差異。 - Espresso 的核心技巧是 EXPAND 與 REDUCE 迭代。查閱資料,寫 200 字說明為什麼啟發式在大規模合成上優於精確 Q-M。