Day 137-Q3 — Held-Karpのビットマスク DP による厳密TSP

2026-08-29 赤色 Master / Phase 8+ ★★★★★★★★☆ 訪問済み集合×現在地の状態でO(2^N N^2)、経路復元付き

問題

$N$ 個の都市(番号 $0$ から $N-1$)があり、都市 $i$ から都市 $j$ への移動コスト $d_{i,j}$ が与えられる($d_{i,j}=-1$ の場合、その方向への直接移動はできない)。

都市 $0$ を出発し、すべての都市をちょうど1回ずつ訪れて都市 $0$ に戻ってくる巡回路(ハミルトン閉路)のうち、総移動コストが最小のものを求めよ。そのような巡回路が存在しない場合は -1 のみを出力せよ。

入力形式

N
d_{0,0} ... d_{0,N-1}
...
d_{N-1,0} ... d_{N-1,N-1}

制約

$2 \le N \le 15$
$-1 \le d_{i,j} \le 10^6$
$d_{i,i}=-1$

入出力例

入力例1

4
-1 10 15 20
10 -1 35 25
15 35 -1 30
20 25 30 -1

出力例1

80
0 2 3 1 0

入力例2

2
-1 5
-1 -1

出力例2

-1

概念図

dp[mask][i] = 集合maskを訪問済み・現在地iの最小コスト mask={0,2} i=2 未訪問j へ遷移 mask={0,1,2} i=1 dp[{0,1,2}][1] = min(dp[{0,2}][2] + cost[2][1]) 状態数 O(2^N・N)、各状態から遷移 O(N) → 全体 O(2^N・N^2) 0 1 0 mask bit=1 は「その都市を訪問済み」 全都市訪問後、都市0へ戻る辺を足した最小値が答え。parで経路復元。

ヒント(段階的開示)

ヒント1(方向性)

$N\le15$ は「全探索は間に合わないが$2^N$程度の指数時間は許容される」ことを示唆する。全順列$O(N!)$は間に合わないが、$2^{15}\times15^2$程度なら現実的。「これまでに訪れた都市の集合」を状態に持たせるとどうなるか。

ヒント2(アプローチ)

$dp[\text{mask}][i]$を「都市0から出発し、maskに含まれる都市を全て訪れ、現在都市iにいる」ときの最小コストと定義する。まだ訪れていない都市jへの遷移で更新する。答えは全都市訪問状態から都市0へ戻るコストを加えた最小値。経路復元には各状態に「直前にどこから来たか」を記録する。

ヒント3(誘導)
dp = [[INF] * N for _ in range(1 << N)]
par = [[-1] * N for _ in range(1 << N)]
dp[1][0] = 0

for mask in range(1, 1 << N):
    if not (mask & 1):
        continue
    for i in range(N):
        if not (mask >> i) & 1 or dp[mask][i] == INF:
            continue
        for j in range(N):
            if (mask >> j) & 1 or cost[i][j] < 0:
                continue
            n_mask = mask | (1 << j)
            n_cost = dp[mask][i] + cost[i][j]
            if n_cost < dp[n_mask][j]:
                dp[n_mask][j] = n_cost
                par[n_mask][j] = i

模範解答 (Python)

import sys


def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    N = int(data[idx]); idx += 1
    cost = []
    for _ in range(N):
        row = [int(data[idx + k]) for k in range(N)]
        idx += N
        cost.append(row)

    INF = float('inf')
    full = (1 << N) - 1
    dp = [[INF] * N for _ in range(1 << N)]
    par = [[-1] * N for _ in range(1 << N)]
    dp[1][0] = 0

    for mask in range(1, 1 << N):
        if not (mask & 1):
            continue
        for i in range(N):
            if not (mask >> i) & 1:
                continue
            cur = dp[mask][i]
            if cur == INF:
                continue
            for j in range(N):
                if (mask >> j) & 1:
                    continue
                c = cost[i][j]
                if c < 0:
                    continue
                n_mask = mask | (1 << j)
                n_cost = cur + c
                if n_cost < dp[n_mask][j]:
                    dp[n_mask][j] = n_cost
                    par[n_mask][j] = i

    best = INF
    best_last = -1
    for i in range(1, N):
        if dp[full][i] == INF:
            continue
        c = cost[i][0]
        if c < 0:
            continue
        total = dp[full][i] + c
        if total < best:
            best = total
            best_last = i

    if best == INF:
        print(-1)
        return

    path = []
    mask = full
    i = best_last
    while i != -1:
        path.append(i)
        pi = par[mask][i]
        mask ^= (1 << i)
        i = pi
    path.reverse()
    path.append(0)

    print(best)
    print(" ".join(map(str, path)))


main()

Step-by-Step 解説

1状態設計「集合×現在地」
同じ「訪問済み集合・現在地」に何度も到達し得るため、これをメモ化の単位にする。集合はビットマスクで表現、状態数$2^N\times N$個。
2遷移は「未訪問都市への1手」
まだ訪れていない都市への移動のみを考えることで、二重カウントや複数回訪問が構造的に発生しない。
3計算量
状態数$O(2^N N)$、各状態からの遷移$O(N)$で全体$O(2^N N^2)$。$N=15$で約$7.4\times10^6$、Pythonでも数秒以内。
4答えの取り出しと経路復元
全都市訪問状態から都市0へ戻るコストを加えた最小値が答え。parを逆にたどって訪問順を復元する。
5存在しない場合の扱い
到達不能な都市があれば dp[full][i] や cost[i][0] が無効のままとなり、best が更新されず-1を出力する。

よくあるミス

ミス原因正しい書き方
都市0を含まない状態も遷移元にしてしまう出発点固定なのに全マスクを無条件走査mask&1が0ならスキップする
d_{i,j}=-1(辺なし)を通常のコストとして加算「辺がない」を表す番兵値であることを見落とす遷移前に必ずcost[i][j]<0をチェックしてスキップ
経路復元時にmask更新順序を誤るビットを外す前に次を見てしまうpath追加→par参照→maskからビットを外す、の順を厳守
N=2などの小さいコーナーケースを確認しない制約上N≥2だが往復辺の存在確認を怠るN=2で往復両方の辺が存在するかだけの単純ケースも確認
dp[1][0]=0の初期化を忘れる出発状態を明示せず全てINFのまま探索が進まない「都市0のみ訪問済み・現在地0・コスト0」を必ず初期化

次のステップ

  • 発展: $N\le20$程度まで拡張したい場合、bitset演算やビット並列化、分枝限定法(MST下界やHeld-Karpの1-木下界、Day100-Q5参照)との組み合わせで実用的な高速化ができる。
  • 発展2: 本問は非対称TSPとして設計した。対称TSPに限定した場合の高速化(状態を半分に減らす)を検討せよ。

自己評価

自分の回答

気づき・メモ