每個點恰好經過一次:用位元把「走過哪些點」壓成一個整數,\(n!\)\(2^n\)

Hamiltonian Path:圖中一條經過每個頂點恰好一次的路徑。首尾若還有邊相連即為 Hamiltonian Cycle。判定存在性皆為 NP-Complete(Karp 21;由 Vertex Cover 歸約)。

別跟 Euler Path(每條恰好一次)搞混!Euler 路徑有漂亮的度數判準(奇度點 0 或 2 個),\(O(E)\) 可解;把「邊」換成「點」,難度立刻跳到 NP-Complete。一字之差,天堂地獄。

掃地機器人要走訪家裡每個房間恰好一次(不想重複吸同一間);郵差要送 \(n\) 個包裹,每站去一次。有沒有這樣的順路走法?——這就是 Hamiltonian Path。若還要回到充電座,就是 Cycle。

核心技術:狀態壓縮 DP

暴力的絕望與 DP 的救贖

枚舉所有拜訪順序是 \(O(n!)\)\(n=18\)\(18!\approx 6.4\times10^{15}\),宇宙熱寂前跑不完。關鍵觀察:

走到一半時,未來的可行性只取決於「已經走過哪些點」和「現在站在哪」——不在乎走過的順序\((\{1,3,5\},\ 3)\) 這個狀態不管是 \(1\to5\to3\) 還是 \(5\to1\to3\) 走出來的,後續完全一樣。合併它們,\(n!\) 條路徑坍縮成 \(2^n\cdot n\) 個狀態。

狀態設計

\(n\) 位元整數 \(S\) 表示走過的點集(第 \(i\) 位 = 點 \(i\) 走過沒):

\[dp[S][v] = \text{「存在一條恰好走過 } S \text{、結尾在 } v \text{ 的路徑」(布林)}\]

初始\(dp[\{v\}][v] =\) 真(單點路徑)。 轉移\[dp[S \cup \{u\}][u] \;\mathrel{|}= \; dp[S][v] \ \land\ (v,u)\in E \ \land\ u\notin S\] 答案:存在 \(v\) 使 \(dp[\text{全集}][v]\) 為真。時間 \(O(2^n n^2)\)、空間 \(O(2^n n)\)

三個實用細節

  • 路徑還原:記 \(parent[S][v]\) = 轉移來源點,從終點沿 \(S \setminus \{v\}\) 倒著走回去。

  • 找 Cycle:固定起點 0(環從哪開始都一樣),DP 只從 \(dp[\{0\}][0]\) 起跑;最後檢查結尾 \(v\) 與 0 有邊。

  • 位元技巧:鄰接表也存成 bitmask,「\(v\) 的未走訪鄰居」= adj[v] & ~S,配合 __builtin_ctz 逐位取出。

本程式開發時真實踩過的 bug:找 Cycle 時若 DP 仍從所有單點起跑,\(dp[\text{全集}][v]\) 為真時還原出來的路徑起點不一定是 0,直接接回 0 就錯了(立方體圖被誤判成沒有環)。教訓:要求路徑從特定點出發,就只播那一顆種子

手算範例

菱形圖:邊 \(0\!-\!1,\ 0\!-\!2,\ 1\!-\!2,\ 1\!-\!3,\ 2\!-\!3\)。求 0 出發的 Hamiltonian Path:

\(S\)(二進位) 可行的 \((S,v)\) 說明
0001 \((0001, 0)\) 起點
0011 \((0011, 1)\) \(0\to1\)
0101 \((0101, 2)\) \(0\to2\)
0111 \((0111,2), (0111,1)\) \(0\to1\to2\)\(0\to2\to1\)
1011 \((1011, 3)\) \(0\to1\to3\)
1111 \((1111,3), (1111,1)\) \(0\to1\to2\to3\)\(0\to2\to3\to1\)

\(dp[1111][3]\) 為真 \(\Rightarrow\) 存在,如 \(0\to1\to2\to3\)(用邊 \(0\!-\!1,1\!-\!2,2\!-\!3\))。又 \(3\)\(0\) 無邊,檢查其他結尾……\(dp[1111][1]\):路徑 \(0\to2\to3\to1\),而 \(1\!-\!0\) 有邊 \(\Rightarrow\) Hamiltonian Cycle 也存在\(0\to2\to3\to1\to0\)

完整 C++ 程式

包含:bitmask DP(Path 與 Cycle、含還原)、鏈+弦、立方體圖 \(Q_3\)(環 = 3 位元 Gray code)、Petersen 圖(著名的有 Path 無 Cycle)、\(n=18\) 效能展示。編譯:

g++ -std=c++17 -O2 -Wall -Wextra -o hamiltonian_path hamiltonian_path.cpp

執行結果與解讀

=== Example 1: path exists, cycle doesn't ===
Hamiltonian path : 4 -> 3 -> 2 -> 1 -> 0
Hamiltonian cycle: none

=== Example 2: cube graph Q3 ===
Hamiltonian path : 7 -> 5 -> 4 -> 6 -> 2 -> 3 -> 1 -> 0
Hamiltonian cycle: 0 -> 4 -> 5 -> 7 -> 6 -> 2 -> 3 -> 1 -> 0
(cycle = 3-bit Gray code!)

=== Example 3: Petersen graph (famous non-Hamiltonian) ===
Hamiltonian path : 7 -> 5 -> 8 -> 6 -> 9 -> 4 -> 3 -> 2 -> 1 -> 0
Hamiltonian cycle: none

=== Example 4: bitmask DP vs factorial enumeration ===
n=18: path FOUND in 50.7 ms (DP states with value=1: 534587)
compare: 18! = 6,402,373,705,728,000 permutations
         2^18 * 18^2 = 84,934,656 DP transitions
path: 16 -> 3 -> 15 -> 2 -> 12 -> 10 -> 4 -> 7 -> 13 -> 6 -> 17 -> 8 -> 14 -> 11 -> 9 -> 1 -> 5 -> 0

Example 2 的環 \(0\to4\to5\to7\to6\to2\to3\to1\to0\) 寫成二進位是 \(000,100,101,111,110,010,011,001\)——每步恰好翻一個位元,正是 3 位元 Gray code。「\(n\) 位 Gray code 存在」與「超立方體圖 \(Q_n\) 有 Hamiltonian Cycle」是同一句話。Example 3 的 Petersen 圖則是教科書級的反例:有 Path、無 Cycle(它是 hypohamiltonian 圖:自己不行,隨便刪一點就行)。

與 TSP 的關係

Hamiltonian Path 是 TSP 的「存在版」:TSP 問最短的走訪路線,這裡只問有沒有。把 \(dp\) 的布林值換成「最短距離」,轉移的 OR 換成 min,就得到下一篇的 Held–Karp 演算法——同一個狀態設計,兩道經典問題。

實務應用

  • 基因組定序:把 DNA 片段重疊圖走一遍恰好一次以重建序列(實務多轉成 Euler 路徑模型的 de Bruijn 圖,正因 Hamiltonian 太難)。

  • 電路布線與鑽孔:PCB 鑽孔機走訪每個孔位一次的路線規劃。

  • 解謎:騎士巡遊(Knight’s Tour)= 棋盤跳躍圖的 Hamiltonian Path;許多一筆畫遊戲的本體。

  • Gray code 與硬體:旋轉編碼器、K-map 化簡的排序都依賴 \(Q_n\) 的 Hamiltonian 結構。

練習題

  1. 手算 \(K_4\)(完全圖)的 \(dp\) 表,數出 Hamiltonian Cycle 的數量(提示:\((4-1)!/2 = 3\))。

  2. 把程式改成計數版本:有幾條不同的 Hamiltonian Path?(布林 OR 改加法。)

  3. 加一個優化:若圖不連通或存在度 0 的點,直接回報不存在。實測隨機稀疏圖的加速。

  4. 實作「指定起點 \(s\) 與終點 \(t\)」的 Hamiltonian Path 判定。

  5. 挑戰:\(5\times5\) 棋盤的騎士巡遊——建跳躍圖後用本程式找 Path(提示:\(n=25\) 需要 uint32_t 以上的 mask 與大量記憶體,想想怎麼分治或改用回溯 + Warnsdorff 啟發式)。

小結

方法 時間 空間 適用時機
枚舉排列 \(O(n!\,n)\) \(O(n)\) \(n\leq 12\)
bitmask DP \(O(2^n n^2)\) \(O(2^n n)\) \(n\leq 22\) 左右
回溯 + 啟發式 指數、無保證 \(O(n)\) 特殊結構圖(如騎士巡遊)
Euler 化建模 \(O(E)\) \(O(E)\) 問題能改寫成走邊

延伸閱讀:Held & Karp (1962)、Bellman (1962) 同年獨立提出此 DP;Dirac/Ore 定理(度數夠大保證 Hamiltonian);Björklund (2010) \(O(1.657^n)\) 的代數突破。