Day 090-Q2 — 双調巡回セールスマン問題(Bitonic TSP)

2026-07-13 赤色 Master / Phase 8+ ★★★★★★★★★ 幾何・O(N²) DP

問題

平面上に $N$ 個の点($x$ 座標はすべて相異なる)が与えられる。双調巡回路とは、最も $x$ が小さい点から出発し $x$ 単調増加で最も $x$ が大きい点へ至り(上側経路)、そこから $x$ 単調減少で出発点に戻る(下側経路)、という条件ですべての点をちょうど1回訪れる閉路のこと。総移動距離が最小の双調巡回路の長さを求めよ。

制約

パラメータ範囲備考
$N$$2 \le N \le 1000$点の数
$x_i, y_i$$-10^4 \le x_i,y_i \le 10^4$整数座標
出力誤差$\le 10^{-6}$絶対誤差または相対誤差

入出力例

入力例1

3
0 0
1 2
2 0

出力例1

6.4721359550

入力例2

5
1 1
2 3
3 1
4 3
5 1

出力例2

10.4721359550

例2: ジグザグ配置。単純な往復($4\sqrt5+4\approx12.94$)より上下経路をうまく振り分けた方が短くなる。

概念図: ジグザグ点を上側・下側経路に振り分ける

$x$ 座標順に追加、末端の組 $(i,j)$ を dp で管理 P0 P1 P2 P3 P4 上側経路: P0→P1→P2→P3→P4($x$単調増加) 下側経路: P4→P0(直接戻る、$x$単調減少)

ヒント

ヒント1(方向性)

一般のTSPはNP困難だが、「双調」制約(2本の$x$単調経路に分解できる)により $O(N^2)$ のDPで厳密に解ける。まず点を $x$ 座標でソートする。

ヒント2(アプローチ)

点を左から右へ1つずつ追加していくと考えると、ある時点で片方の経路の末端は必ず「直近に追加した点」になる。もう片方の末端を $i$ とすれば、状態は $(i,j)$ の組だけで表現できる。

ヒント3(ほぼ答え)
dp[0][1] = dist(0, 1)
for j in range(2, n):
    for i in range(j):
        if i < j - 1:
            dp[i][j] = dp[i][j-1] + dist(j-1, j)
        else:  # i == j-1
            dp[j-1][j] = min(dp[k][j-1] + dist(k, j) for k in range(j-1))
answer = min(dp[i][n-1] + dist(i, n-1) for i in range(n-1))

模範解答

import sys
import math

def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    pts = []
    for _ in range(n):
        x = int(data[idx]); idx += 1
        y = int(data[idx]); idx += 1
        pts.append((x, y))
    pts.sort()

    def dist(a, b):
        return math.hypot(pts[a][0] - pts[b][0], pts[a][1] - pts[b][1])

    if n == 2:
        print(f"{dist(0, 1) * 2:.10f}")
        return

    INF = float('inf')
    dp = [[INF] * n for _ in range(n)]
    dp[0][1] = dist(0, 1)

    for j in range(2, n):
        d_j1_j = dist(j - 1, j)
        for i in range(j - 1):
            dp[i][j] = dp[i][j - 1] + d_j1_j
        best = INF
        for k in range(j - 1):
            v = dp[k][j - 1] + dist(k, j)
            if v < best:
                best = v
        dp[j - 1][j] = best

    ans = min(dp[i][n - 1] + dist(i, n - 1) for i in range(n - 1))
    print(f"{ans:.10f}")

main()

計算量: 状態数 $O(N^2)$、遷移は各 $j$ で合計 $O(N)$ の走査(i==j-1 の行のみ)なので全体 $O(N^2)$。

Step-by-Step 解説

Step 1: $x$ 座標でソートして追加順序を固定

点を $x$ 昇順に処理すれば、新しく追加する点は常にその時点で最も右にある点になる。

Step 2: 状態を $(i,j)$ に圧縮できる理由

片方の末端が常に「直近に追加した点」に固定されるため、独立な自由度は実質 $i$ 側の1つだけになり、状態数が $O(N^2)$ に抑えられる。

Step 3: 2種類の遷移

条件遷移の意味
$i < j-1$点 $j$ は「$j-1$側の経路」の続きとして追加、$i$側は不変
$i = j-1$点 $j$ は「$k$で終わっていた経路」の続きとして追加され $j-1$ が新たな確定側末端

Step 4: 最終的な閉路の形成

配置し終えたら2本の経路の末端はどちらも点 $n-1$ に到達している。最後にその末端同士を直接結んで閉路を閉じる。

よくあるミス

ミス原因正しい書き方
「$j$は常に直近追加点」という不変条件を使わず遷移を誤る状態圧縮の理由を理解していない$i
最後に $\min_i dp[i][n-1]$ だけを答えにする閉路を閉じる辺を加え忘れる+ dist(i, n-1) を必ず加える
距離を整数二乗和のまま比較しようとする合計距離は無理数を含むmath.hypotで浮動小数点計算し誤差許容に委ねる

次のステップ

  • 発展問題: 一般の平面TSPを小さい $N$($\le15$程度)で bitDP により厳密に解く
  • 発展問題: 双調巡回路の実際の訪問順序を遷移元の記録から復元する

自己評価

理解度: / /

自分の回答:

気づき・メモ: