跳轉到

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 這類啟發式兩層化簡器。

演算法步驟:卡諾圖化簡#

  1. 畫出 2ⁿ 格的二維圖,軸以 Gray 碼標記。
  2. 在函數為 1 的格子填 1(可先忽略 don't care ×)。
  3. 圈選最大的 2 開冪個相鄰「1」群組(可用環繞邊界)。
  4. 每個圈產生一個積項:圈內不變的變數保留,被消去的(圈內同時出現 0 與 1 的)捨棄。
  5. 檢查所有 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)採用啟發式,速度快數個數量級但結果近似最小。

練習#

  1. 手繪 f(a,b,c,d) = Σm(0,1,2,3,8,9,10,11) 的卡諾圖,圈出最大組並寫出最簡 SOP(提示:可圈成一個 8 格組)。
  2. 加入 don't care:f = Σm(1,3,7) + d(0,2,5),比較「把 d 全部視為 0」與「善用 d」兩種結果的閘數差異。
  3. 修改程式,支援「偏好使用 don't care 塞進組裡」的圈選,測試第 2 題的函數是否化得更短。