跳轉到

5.4 旅行銷售員與漢米爾頓迴路#

概念說明#

漢米爾頓迴路(Hamiltonian Cycle):給定圖 G,問是否存在一個經過每個頂點恰好一次、最後回到起點的封閉迴路。名字來自數學家漢米爾頓 1857 年發明的「環遊世界」棋盤遊戲。這個問題與尤拉迴路(經過每條邊恰好一次)形成鮮明對比:尤拉迴路有 O(n + m) 的簡單演算法(檢查連通性與每個頂點的度數是否為偶數),而漢米爾頓迴路卻是 NP-complete——兩個問題只差在「點」與「邊」一字,難度卻天差地遠,這是 NP 理論最迷人的對比之一。

旅行銷售員問題(TSP, Traveling Salesman Problem):給定 n 個城市與兩兩之間的距離,求經過每個城市恰好一次、總距離最短的路線。TSP 的判定版本(「是否存在總長 ≤ B 的路線?」)是 NP-complete;最佳化版本是 NP-hard。TSP 是所有 NP-hard 問題中被研究得最透徹的:從 1950 年代至今,Concorde 求解器已能精確解出數萬城市的實例,而這些進展反過來推動了整數規劃、切割平面法等最佳化技術的發展。

兩者的關係:把漢米爾頓迴路化約到 TSP 非常直接——令相鄰頂點距離為 1、不相鄰為 2(或以鄰接矩陣 0/∞ 編碼),則「存在長度為 n 的迴路」⟺「存在漢米爾頓迴路」。這說明 TSP 不比漢米爾頓迴路容易。

演算法步驟#

TSP 的暴力法與動態規劃:

  1. 暴力法:固定起點 0,列舉其餘 n-1 個城市的所有排列((n-1)! 種),計算每條路線總長,取最小值。
  2. Held-Karp 動態規劃:用位元遮罩 S 表示「已拜訪城市集合」。
  3. 定義 dp[S][j] = 從起點 0 出發、拜訪集合 S 中的城市、目前位於 j 的最短路徑長,其中 j ∈ S。
  4. 轉移:dp[S ∪ {k}][k] = min(dp[S][j] + d(j, k)),對所有 j ∈ S。
  5. 答案為 min_j(dp[全部城市][j] + d(j, 0)),時間 O(n²·2ⁿ),遠優於 O(n!)。

Python 程式碼範例#

from itertools import permutations

def tsp_brute_force(dist):
    """暴力法:O(n!)"""
    n = len(dist)
    best, best_path = float('inf'), None
    for perm in permutations(range(1, n)):
        path = (0,) + perm + (0,)
        length = sum(dist[path[i]][path[i + 1]]
                     for i in range(n))
        if length < best:
            best, best_path = length, path
    return best, best_path

def tsp_held_karp(dist):
    """Held-Karp 動態規劃:O(n^2 * 2^n)"""
    n = len(dist)
    FULL = (1 << n) - 1
    dp = [[float('inf')] * n for _ in range(1 << n)]
    parent = [[-1] * n for _ in range(1 << n)]
    dp[1][0] = 0                      # 起點 0 已拜訪
    for S in range(1 << n):
        for j in range(n):
            if not (S >> j) & 1 or dp[S][j] == float('inf'):
                continue
            for k in range(n):
                if (S >> k) & 1:
                    continue
                ns = S | (1 << k)
                cand = dp[S][j] + dist[j][k]
                if cand < dp[ns][k]:
                    dp[ns][k] = cand
                    parent[ns][k] = j
    # 回到起點並回溯路徑
    best, last = min((dp[FULL][j] + dist[j][0], j)
                     for j in range(1, n))
    path, j = [], last
    for _ in range(n - 1):
        path.append(j)
        j = parent[FULL][j]
        # 注意:回溯需配合遮罩縮小,此處示範略
    return best

import math
pts = [(0, 0), (2, 3), (5, 1), (6, 4), (1, 5)]
d = [[math.dist(a, b) for b in pts] for a in pts]
print(tsp_brute_force(d))
print(tsp_held_karp(d))

複雜度分析#

  • 暴力法:O(n!) 時間,n = 12 已需數秒,n = 20 完全不可行。
  • Held-Karp:O(n²·2ⁿ) 時間、O(n·2ⁿ) 空間,n = 20 約 4×10⁸ 次基本運算,尚可接受;n = 40 則超出任何電腦的能力。
  • 空間最佳化:按 popcount 分層處理遮罩,可減少同時需要的記憶體。
  • TSP 若滿足三角不等式,有 Christofides 1.5-近似演算法(見 8.1 節);一般情況下則沒有常數倍近似(除非 P = NP)。

練習#

  1. 寫程式完成 Held-Karp 的路徑回溯(提示:回溯時同步從 FULL 遮罩中移除目前頂點的位元),驗證與暴力法的答案一致。
  2. 對 n = 8~12 的隨機歐氏實例,實測暴力法與 Held-Karp 的執行時間比例,說明成長趨勢。
  3. 證明:若無向圖每個頂點的度數都 ≥ n/2,則必存在漢米爾頓迴路(Dirac 定理),並對 n = 8 寫程式隨機驗證。