「巡回セールスマン問題」を取り巻く中核キーワード群です。 検索やインデックス作成で参照する際の手がかりにしてください。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になります。
🍰 まずはやさしく
最短の回り道を探すパズルのような問題です。
一番効率の良いルートを決めるために使います。
観光地を効率よく回る計画に似ています。
この問題の結論と解決策について読みます。
最も忙しい読者のために、 まず結論だけまとめます。 詳細は以下のセクションへ:
🍰 まずはやさしく
効率的なルート選びのルールです。
移動の時間やコストを減らすために使います。
ネットショップの配達ルートなどで使われます。
どんな場面でこの考え方が役立つか読みます。
「Amazon の配送ドライバーが 50 件の届け先を効率よく回る」 — そのまま TSP。 厳密に解くと天文学的時間がかかるため、 実務は 近似手法で「十分に良い解」を高速に得ます。
このページの読み方:まず 30秒結論 と 直感 を読み、 必要に応じて 数式 や 計算例、 落とし穴 に進んでください。
🍰 まずはやさしく
選択肢が爆発的に増える不思議な問題です。
計算の限界を知るために使います。
回る場所が増えると、計算がとても大変になります。
なぜ完璧な答えを出すのが難しいか読みます。
5 都市の TSP は (5-1)!/2 = 12 通り。 全列挙可能。
でも n = 20 では 約 6 京通り。 1 秒に 10 億通り計算しても 700 年。
n = 100 では宇宙の年齢を超える。 だから「全列挙」は諦め、 「ほぼ最適」で妥協するのが現実解。
巡回セールスマン問題 (TSP) を 3 枚で押さえる: (1) 都市と巡回路の幾何、 (2) 分枝限定法 (Branch and Bound) の探索木、 (3) 都市数に対する計算量の爆発。 これで「なぜ近似/メタヒューリスティクスが必要か」が直感で分かる。
下図は 5 都市の例。 出発地に戻る閉路 (Hamilton 閉路) のうち、 総距離が最短になるものを選ぶのが TSP。 5 都市なら手で全 12 通り (=(5-1)!/2) を試せる。
辺の数字は都市間距離。 5 都市の場合は全数列挙で最適解が求まる。 都市数 N が増えると組合せ数が爆発する (図 3 参照)。
部分巡回路を「枝」、 部分解の下界を「節点」として、 現在の最良解より下界が悪い部分木を枝刈り (prune) する。 これで N! 通り全探索しなくても済む。
緑ノード=展開して探索する分枝、 赤ノード=下界が現在の最良解を超えるので刈り取られる枝。 良い下界の評価関数 (例: 最小全域木) を使うほど刈れる枝が増える。
全探索の場合、 ルートの数は (N-1)!/2 で増える。 N=10 で約 18 万、 N=15 で約 4.4 億、 N=20 で約 6.1 × 10^16。 N=20 を秒 10^9 通り評価しても 1.9 年かかる。 だから近似アルゴリズム (最近傍法、 2-opt、 遺伝アルゴリズム) が現実的選択肢になる。
この曲線が「TSP は NP-hard クラスに属する」ことを直感的に示す。 厳密最適解を求める時間は都市数とともに爆発するので、 実務では Concorde (専用ソルバ) や局所探索 + メタヒューリスティクスを併用する。
この3枚で「問題定義」→「探索戦略」→「計算量限界」の三段論法が完成する。 派生問題は 組合せ最適化・数理最適化・最適化 へ。
TSP の核を本当に掴めたかを 6 問で確認する。 すべて 1 分以内で答えられる粒度。 「自分で」答えを口に出して言える状態を目指したい。
これらの問題に詰まったセクションがあれば、 直前の「概念図」「数式」「Python 実装」セクションへ戻って再確認しよう。 TSP は厳密解の限界を理解した上で、 メタヒューリスティクスをうまく使い分けるのが現代的な向き合い方である。
最近傍法の「悪手」を 2-opt / Or-opt がほどく過程を、 1 手ずつ目で追うデモです。 組合せ最適化 のページに基本の TSP デモがあるので、 ここでは一歩踏み込んで 「アルゴリズム間の比較」 に焦点を当てます: (1) 貪欲な最近傍法がどこで遠回りの「ツケ」を払うか、 (2) 2-opt / Or-opt が交差を 1 手ずつ張り替えて改善する過程、 (3) 総当たり厳密解とのギャップ%、 (4) MST 下界との比較。 キャンバスの空白をクリック(タップ)で都市を追加、 既存の都市をクリックすると 開始都市 が変わります。
| 指標 | 総距離 | 厳密解とのギャップ | MST 下界との比 |
|---|---|---|---|
| MST 下界 (最適解はこれ未満にならない) | — | — | ×1.000 (基準) |
| 最近傍法 (貪欲構築) | — | — | — |
| 改善後 (2-opt / Or-opt) | — | — | — |
| 厳密解 (総当たり) | — | — | — |
🍰 まずはやさしく
最短ルートを数式で表したものです。
正解を正確に計算するために使います。
地図上の距離を足し算して比べます。
数学的な定義と計算式について読みます。
「TSP(巡回セールスマン問題)」は 全都市を 1 回ずつ訪問して出発点に戻る最短ルートを求める NP 困難問題。組合せ最適化の代名詞。 です。 ここでは定義式の各記号、 直感的意味、 SSDSE-B-2026 への当てはめを段階的に解きほぐします。
| n (都市数) | 厳密解探索 | 近似解 |
|---|---|---|
| 10 | 10! = 360万 (一瞬) | 瞬時 |
| 20 | 20! ≈ 2.4×10¹⁸ (数年) | 1 秒未満 |
| 50 | 不可能 | 数秒 |
| 1,000 | 不可能 | 数分 (LKH 等) |
| 85,900 | 1 度だけ達成 (2006, Concorde) | 分単位で 1% 以内 |
| 手法 | 計算量 | 近似率 | 備考 |
|---|---|---|---|
| 厳密 (DP) | $O(2^n \cdot n^2)$ | 最適 | $n \leq 20$ 限界 |
| Nearest Neighbor | $O(n^2)$ | ~25% 悪化 | 素朴貪欲 |
| 2-opt | $O(n^2)$ 反復 | ~5% 悪化 | 局所改善 |
| Christofides | $O(n^3)$ | 1.5 倍以内 | 三角不等式必要 |
| LKH | ヒューリスティック | 0.5% 以内 | 世界記録レベル |
| OR-Tools | ハイブリッド | 1-5% | 実務標準 |
| 遺伝アルゴリズム | 反復 | 5-15% | 大規模可 |
TSP は NP 困難。 「P = NP?」問題に直結する代表的組合せ最適化問題。 厳密解の多項式時間アルゴリズムは存在しない (証明されているわけではないが、 50 年以上発見されていない)。 実務では近似解で十分という割り切りが標準。
SSDSE-B-2026 で「47 都道府県庁所在地を巡回する TSP」。 入力は 47 都道府県の地理座標(仮想)と都道府県人口を訪問コストに、 出力は OR-Tools / nearest-neighbor で近似解。 47 都道府県 × 複数年の実データで具体計算を実施します。
| 業界 | 事例 | 役割 | SSDSE-B-2026 との対比 |
|---|---|---|---|
| Amazon 配送 | ラストマイル配送ルート最適化 | 毎日数百万ルート計算 | 47 都道府県巡回モデル化 |
| UPS / FedEx | ORION システムで燃料 10% 削減 | TSP + 制約 | 都道府県物流網 |
| 半導体製造 | プリント基板の穴あけ順序 | ヘッド移動最小化 | 工程最適化 |
| ゴミ収集 | 市区町村の収集ルート | TSP + 容量制約 | 47 都道府県別収集設計 |
| DNA シーケンシング | 断片の最適配置 | TSP 変種 | ゲノム解読 |
| ドローン配送 | Zipline・楽天のドローン | TSP + 時間枠 | 離島・山間部配送 |
| 手法 | 定義 | 特徴 | 用途 |
|---|---|---|---|
| TSP | Hamilton 閉路の最短 | 全都市 1 回訪問 | 配送・製造 |
| VRP | TSP + 複数車両 + 容量 | 実務の標準 | ラストマイル配送 |
| CVRP | VRP + 容量制約 | 倉庫最適化 | 在庫配送 |
| 最短経路問題 | 2 点間の最短 | Dijkstra | ナビ・経路探索 |
| 最小全域木 | 全頂点接続の最小コスト | クラスカル法 | ネット設計 |
| 巡回ロボット | TSP + 障害物 | Boustrophedon | 掃除ロボット |
| PCB 穴あけ TSP | ユークリッド TSP | ヘッド移動最小 | 電子製造 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import pandas as pd, numpy as np from sklearn.metrics.pairwise import euclidean_distances df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) # 日本語の列名で読む(2 行目を見出しにする) d = df[df['年度']==2023].reset_index(drop=True) xy = np.column_stack([d['総人口']/1e6, d['出生数']/1e3]) D = euclidean_distances(xy) # Nearest Neighbor: 北海道から出発し、未訪問のうち最も近い県へ移る tour, rest = [0], set(range(1, len(d))) while rest: nxt = min(rest, key=lambda j: D[tour[-1], j]) tour.append(nxt); rest.remove(nxt) length = sum(D[tour[i], tour[(i+1) % len(tour)]] for i in range(len(tour))) print(f"NN 経路長: {length:.2f}") print("訪問順 (先頭 5):", ' → '.join(d['都道府県'][tour[:5]])) |
💬 北海道(総人口 5.09、出生数 24.43 の点)から最寄りの県へ貪欲に移ると、静岡・広島・茨城・京都と人口 250〜360 万人級の県が続き、47 県を 1 周した経路長は 171.97 になる。この長さは「百万人」と「千人」を混ぜた座標上の距離なので単位に意味はなく、比べられるのは同じ座標で作った別の経路(2-opt 後の 169.36 など)とだけ。
1 2 3 4 5 6 7 8 9 | from ortools.constraint_solver import pywrapcp, routing_enums_pb2 m = pywrapcp.RoutingIndexManager(10, 1, 0) routing = pywrapcp.RoutingModel(m) def distance(from_i, to_i): return abs(m.IndexToNode(from_i) - m.IndexToNode(to_i)) t = routing.RegisterTransitCallback(distance) routing.SetArcCostEvaluatorOfAllVehicles(t) sol = routing.Solve() print(sol.ObjectiveValue()) |
解法のスケール感(n 都市):
| n | 全列挙 | 動的計画法 (Held-Karp) | 2-opt 近似 |
|---|---|---|---|
| 10 | 数秒 | 瞬時 | 瞬時 |
| 20 | 700 年 | 数秒 | 瞬時 |
| 100 | 不可能 | 時間超過 | 数秒で良解 |
| 10000 | 不可能 | 不可能 | 数分(Concorde 厳密も可) |
合成 4 都市で全巡回路長を列挙する。
1 2 3 4 5 6 7 8 9 | import numpy as np from itertools import permutations cities = np.array([[0,0],[1,0],[1,1],[0,1]]) def length(perm): return sum(np.linalg.norm(cities[perm[i]] - cities[perm[(i+1)%4]]) for i in range(4)) routes = list(permutations(range(1,4))) lens = [float(length([0]+list(r))) for r in routes] print(f"全巡回路長: {[round(x, 3) for x in lens]}") print(f"最短: {min(lens)}") |
💬 手計算 (Step 2) 4.0 と Python 出力が完全一致。
最小再現コード。 SSDSE-B のような実データを前提に、 4〜8 行で動く例です:
1 2 3 4 5 6 7 8 9 10 | from itertools import permutations # n=6 都市の全列挙(ブルートフォース、 デモ用) cities = [(0,0),(1,3),(4,3),(6,1),(3,0),(5,4)] def dist(a,b): return ((a[0]-b[0])**2+(a[1]-b[1])**2)**0.5 def tour_len(p): route = (0,) + p + (0,) # 都市 0 から出発して都市 0 に戻る return sum(dist(cities[route[i]], cities[route[i+1]]) for i in range(len(route)-1)) best = min(permutations(range(1,len(cities))), key=tour_len) print('最短順路:', best) print('巡回路長:', round(tour_len(best), 3)) |
💬 5 都市の並べ方 5! = 120 通りをすべて試し、都市 0 から出て 0 に戻る最短路は 0→1→2→5→3→4→0 で長さ 16.901。逆回り (4, 3, 5, 2, 1) も同じ 16.901 なので、min は先に出てきた方を返しているだけで、実質的な解は 1 本(向きを区別しないなら 60 通りの中の 1 本)。都市が 11 個になると並べ方は 10! = 362 万 8,800 通りに増えるため、この全列挙は 10 都市前後が限界で、それ以上は 2-opt などの近似解法に切り替える。
補足:ライブラリのバージョンや前処理状態によって出力は変わります。 自分の環境で動かすときは pip list でバージョンを確認し、 入力 CSV のパス・列名を実態に合わせてください。
🎯 このコードでやること:47 都道府県を「総人口・出生数」の 2 次元空間にマップし、 ユークリッド距離で TSP を構成。 Nearest Neighbor 法で初期解を求める。
📥 入力データ:df: SSDSE-B-2026 (2023 年データ)。 「総人口」「出生数」を都市座標として使う。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | import pandas as pd import numpy as np from sklearn.metrics.pairwise import euclidean_distances df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) d = df[df['年度']==2023].reset_index(drop=True) # 都市座標: 総人口 (1e6 単位) + 出生数 (1e3 単位) xy = np.column_stack([d['総人口'].values / 1e6, d['出生数'].values / 1e3]) D = euclidean_distances(xy) def nn_tour(D, start=0): n = len(D) visited = [start] remaining = set(range(n)) - {start} while remaining: nxt = min(remaining, key=lambda j: D[visited[-1]][j]) visited.append(nxt) remaining.remove(nxt) return visited def tour_length(t, D): return sum(D[t[i]][t[(i+1)%len(t)]] for i in range(len(t))) tour = nn_tour(D, start=0) print(f"NN 経路長 : {tour_length(tour, D):.2f}") print(f"\n訪問順 (先頭 10):") for i in tour[:10]: print(f" {d.iloc[i]['都道府県']}") |
📤 実行結果:
💬 結果の読み方:Nearest Neighbor で初期解 (経路長 171.97) を得た。 出発点 (北海道) から「総人口・出生数」の値が近い県を順に訪問。 北海道→静岡→広島と、 地理的な近さではなく 人口規模の近さ で並ぶ点に注意 (この座標は地理座標ではない)。 NN は素朴だが初期解として有用。
🎯 このコードでやること:NN 解を 2-opt 局所探索で改善し、 経路長をできるだけ短縮する。
📥 入力データ:前のコードの tour, D を使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | def two_opt(tour, D, max_iter=100): n = len(tour) improved = True iter_count = 0 while improved and iter_count < max_iter: improved = False iter_count += 1 for i in range(1, n - 1): for j in range(i + 1, n): new_tour = tour[:i] + tour[i:j+1][::-1] + tour[j+1:] if tour_length(new_tour, D) < tour_length(tour, D): tour = new_tour improved = True break if improved: break return tour, iter_count opt_tour, iters = two_opt(tour, D) print(f"NN 経路長 : {tour_length(tour, D):.2f}") print(f"2-opt 経路長 : {tour_length(opt_tour, D):.2f}") print(f"改善率 : {(1 - tour_length(opt_tour, D)/tour_length(tour, D)) * 100:.2f}%") print(f"2-opt 反復回数: {iters}") |
📤 実行結果:
💬 結果の読み方:2-opt で経路長を 171.97 → 169.36 と約 1.5% 短縮。 26 回の反復で収束。 この座標空間 (総人口×出生数) は点がほぼ 1 本の帯状に並ぶため NN 解が既に良く、 改善幅は小さい。 さらに 3-opt や Lin-Kernighan を使えばより最適に近づく。 NN + 2-opt は実務でも頻用される定番組合せ。
🎯 このコードでやること:都市数を 15 に絞り、 Held-Karp DP で TSP 厳密解を求める。 NN + 2-opt との差を確認。
📥 入力データ:D の先頭 15×15 部分行列を使用。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from python_tsp.exact import solve_tsp_dynamic_programming from python_tsp.heuristics import solve_tsp_local_search D15 = D[:15, :15] # 厳密解 perm_exact, dist_exact = solve_tsp_dynamic_programming(D15) # 近似解 (LS) perm_ls, dist_ls = solve_tsp_local_search(D15) print(f"Held-Karp (厳密) : {dist_exact:.4f}") print(f"Local Search (近似) : {dist_ls:.4f}") print(f"近似誤差 : {(dist_ls/dist_exact - 1) * 100:.3f}%") print(f"\n厳密解の訪問順:") for i in perm_exact[:10]: print(f" {d.iloc[i]['都道府県']}") |
📤 実行結果:
💬 結果の読み方:15 都市なら Held-Karp DP で 1 秒以下で厳密解。 ローカル探索はランダムな初期解から始めるので、実行ごとに 167.6042(厳密解と一致)か 167.6062(0.001% 長い)のどちらかになる。 47 都市での DP は約 $2^{47} \times 47^2 \approx 3 \times 10^{17}$ 操作で数年かかるため、 OR-Tools/LKH などのヒューリスティックが必要。
🎯 このコードでやること:Google OR-Tools の Guided Local Search を使って 47 都道府県 TSP を実用品質で解く。
📥 入力データ:D: 47×47 距離行列。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | from ortools.constraint_solver import pywrapcp, routing_enums_pb2 manager = pywrapcp.RoutingIndexManager(47, 1, 0) routing = pywrapcp.RoutingModel(manager) def distance_callback(from_i, to_i): f = manager.IndexToNode(from_i) t = manager.IndexToNode(to_i) return int(D[f][t] * 10000) # 整数化 transit_id = routing.RegisterTransitCallback(distance_callback) routing.SetArcCostEvaluatorOfAllVehicles(transit_id) params = pywrapcp.DefaultRoutingSearchParameters() params.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC params.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH params.time_limit.seconds = 10 solution = routing.SolveWithParameters(params) if solution: obj = solution.ObjectiveValue() / 10000 print(f"OR-Tools 経路長 : {obj:.4f}") print(f"NN + 2-opt 比較 : {tour_length(opt_tour, D):.4f}") print(f"OR-Tools 改善率 : {(1 - obj/tour_length(opt_tour, D)) * 100:.2f}%") |
📤 実行結果:
💬 結果の読み方:OR-Tools の Guided Local Search で経路長 169.05 を達成。 NN + 2-opt より 0.18% 改善。 10 秒で実行完了。 産業界では Amazon・UPS が同様のソルバーを使い毎日数百万ルートを最適化。 47 都道府県でも OR-Tools は実用的に動く。
「経路長 $\leq K$ のツアーがあるか?」は NP 完全。 TSP は NP 困難。
$G = (V, E)$ で全 $|V|$ 頂点を 1 回ずつ訪れる閉路。 TSP は重み付き Hamilton 閉路の最短。
MST + 奇数次数頂点の完全マッチング + Euler ツアー + ショートカット。 三角不等式で 1.5 倍以内。
k-opt を可変 k で行う。 経験的に LKH が最強。
2006 年 85,900 都市 (集積回路 PLA) を 136 CPU 年で厳密解。 並列計算で達成。
SSDSE-B-2026 を使った段階的ハンズオン。 初学者→中級→上級と順に深めるシナリオ構成です。
47 都道府県を「総人口・出生数」の 2D 空間にマップ。 SSDSE-B-2026 で実現。
euclidean_distances(xy) で 47×47 行列を生成。
起点 0 から最近未訪問を反復、 初期解を得る。
tour_length(tour, D) で総距離計算。
2 エッジを反転して短縮可能なら採用、 収束まで反復。 通常 5-20% 改善。
都市数を 15 まで絞れば DP で厳密解。 NN+2-opt と比較。
RoutingModel + Guided Local Search で 47 県を 10 秒で解決。
matplotlib で経路を線で結ぶ。 都道府県名をラベル表示。
ユークリッドを OSRM 実道路距離に置換。 経路長が現実的に。
2 台車両に分割、 容量制約付きで 47 県を分担配送。
実務で頻用するコード・概念・公式を 1 ページにまとめた早見表。 印刷して机に貼っておくと便利です。
| 領域 | 項目 | 説明 |
|---|---|---|
| 距離行列 | from scipy.spatial.distance import cdist | ペア距離 |
| 座標 | np.column_stack([x, y]) | 2D 座標 |
| NN | 現在地→最近未訪問都市を反復 | 貪欲法 |
| 2-opt | 経路の 2 エッジを反転 | 局所改善 |
| 3-opt | 3 エッジを並べ替え | より強力な局所改善 |
| DP | Held-Karp O(2^n n^2) | 厳密解 (n≤20) |
| Christofides | MST+完全マッチング | 1.5 倍以内保証 |
| LKH | Lin-Kernighan-Helsgaun | 世界記録レベル |
| OR-Tools | Google routing solver | 実務標準 |
| Concorde | 厳密解世界記録ソルバー | 85,900 都市まで |
| python-tsp | exact/heuristic 関数 | 簡易ライブラリ |
| Gurobi | 商用 MILP ソルバー | 高速厳密解 |
| matplotlib 描画 | plt.plot(xs, ys) | 経路可視化 |
| VRP | 複数車両版 | OR-Tools 拡張 |
| CVRP | VRP + 容量制約 | 実配送モデル |
| Pickup-Delivery | 拾い・配達制約 | Uber 配車 |
| Time Window | 時間枠制約 | 宅配の指定時刻 |
| ATSP | Asymmetric TSP | 一方通行対応 |
| Multi-depot VRP | 複数倉庫 | 大規模物流 |
| Dynamic TSP | 都市追加・削除あり | 配車サービス |
本編のコードに加え、 さらに 4 種の発展的コード例を 4 要素 (🎯/📥/📤/💬) 付きで提示。 段階的に「読む→動かす→改造する」を体験できます。
🎯 このコードでやること:NN、 2-opt、 OR-Tools の経路長と実行時間を比較。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | import time import numpy as np from sklearn.metrics.pairwise import euclidean_distances xy = np.column_stack([d['総人口']/1e6, d['出生数']/1e3]) D = euclidean_distances(xy) def nn_tour(D, start=0): n=len(D); v=[start]; rem=set(range(n))-{start} while rem: nxt=min(rem, key=lambda j: D[v[-1]][j]) v.append(nxt); rem.remove(nxt) return v def tour_len(t, D): return sum(D[t[i]][t[(i+1)%len(t)]] for i in range(len(t))) def two_opt(tour, D): improved=True while improved: improved=False for i in range(1, len(tour)-1): for j in range(i+1, len(tour)): new = tour[:i] + tour[i:j+1][::-1] + tour[j+1:] if tour_len(new, D) < tour_len(tour, D): tour = new; improved = True return tour t0 = time.time() nn = nn_tour(D) t_nn = time.time() - t0; len_nn = tour_len(nn, D) t0 = time.time() opt = two_opt(nn, D) t_opt = time.time() - t0; len_opt = tour_len(opt, D) print(f"NN : 経路長={len_nn:.2f}, 時間={t_nn:.3f}s") print(f"NN + 2-opt : 経路長={len_opt:.2f}, 時間={t_opt:.3f}s") print(f"改善率 : {(1-len_opt/len_nn)*100:.1f}%") |
📤 実行結果:
💬 結果の読み方:NN は瞬時 (1ms 未満) で、 2-opt が 0.04 秒で 1.5% 改善。 このデータでは NN 解が既に良質なため改善幅は小さいが、 「構築は速く・改善は時間をかける」という組合せ最適化の典型 trade-off (時間 vs 質) は同じ。 実務では OR-Tools/LKH でさらに改善。
🎯 このコードでやること:matplotlib で経路を線で描画。 都道府県名をラベル表示。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(10, 6)) for i in range(len(opt)-1): x1, y1 = xy[opt[i]] x2, y2 = xy[opt[i+1]] ax.plot([x1, x2], [y1, y2], 'b-', alpha=0.5) x_close, y_close = xy[opt[-1]], xy[opt[0]] ax.plot([x_close[0], y_close[0]], [x_close[1], y_close[1]], 'b-', alpha=0.5) for i, pref in enumerate(d['都道府県']): ax.annotate(pref, xy[i], fontsize=8) ax.set_xlabel('総人口 (百万人)') ax.set_ylabel('出生数 (千人)') ax.set_title(f'47 都道府県 TSP (2-opt: {len_opt:.2f})') plt.savefig('tsp_47.png', dpi=120) plt.show() |
📤 実行結果:
💬 結果の読み方:総人口・出生数空間で 47 県の TSP を可視化。 経路は北海道から千葉・埼玉・神奈川と大きい県を上って東京(1,409 万人)で折り返し、大阪・愛知・福岡・兵庫を経て規模の小さい県へ下り、鳥取(54 万人)付近で底を打ってから中規模の県を上り直し、茨城から北海道へ戻る。座標が人口規模なので、地図上の道順とは無関係な「規模の山を 1 周する」閉路になる。
🎯 このコードでやること:python-tsp の DP で厳密解を計算、 NN+2-opt と比較。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 9 10 11 12 | from python_tsp.exact import solve_tsp_dynamic_programming D15 = D[:15, :15] perm_exact, dist_exact = solve_tsp_dynamic_programming(D15) nn15 = nn_tour(D15) opt15 = two_opt(nn15, D15) len_opt15 = tour_len(opt15, D15) print(f"Held-Karp (厳密) : {dist_exact:.4f}") print(f"NN + 2-opt : {len_opt15:.4f}") print(f"近似誤差 : {(len_opt15/dist_exact - 1)*100:.3f}%") |
📤 実行結果:
💬 結果の読み方:15 都市なら NN+2-opt でも誤差 0.003% とほぼ厳密解に到達。 47 都市では局所最適にハマる可能性も。 厳密解と近似解の差を体感できる教材。
🎯 このコードでやること:Google OR-Tools で 47 県 TSP を解く。
📥 入力データ:SSDSE-B-2026 の関連カラム。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | from ortools.constraint_solver import pywrapcp, routing_enums_pb2 manager = pywrapcp.RoutingIndexManager(47, 1, 0) routing = pywrapcp.RoutingModel(manager) def dist_cb(i, j): return int(D[manager.IndexToNode(i)][manager.IndexToNode(j)] * 10000) transit_id = routing.RegisterTransitCallback(dist_cb) routing.SetArcCostEvaluatorOfAllVehicles(transit_id) params = pywrapcp.DefaultRoutingSearchParameters() params.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC params.local_search_metaheuristic = routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH params.time_limit.seconds = 5 sol = routing.SolveWithParameters(params) or_len = sol.ObjectiveValue() / 10000 print(f"OR-Tools 経路長 : {or_len:.4f}") print(f"NN + 2-opt 比較 : {len_opt:.4f}") print(f"OR-Tools 改善率 : {(1 - or_len/len_opt) * 100:.2f}%") |
📤 実行結果:
💬 結果の読み方:OR-Tools の Guided Local Search が NN+2-opt より 0.19% 短い経路を発見。 5 秒で完了。 大規模 (1000 都市超) でも OR-Tools が実用標準。
巡回セールスマン問題 を実務で扱うとき、 多くの分析者が同じところでつまずきます。 代表的な失敗パターンを先回りで押さえておくと、 後工程のトラブルを大幅に減らせます。
※ 上記は文献調査・現場経験で報告される頻度の高い注意点。 ドメインや手法のバージョンによって追加の落とし穴がある場合があります。
初学者を超えた中級者がハマる「2 周目の失敗例」を集めました。 本編の 5 件と合わせて 15 件のチェックリストとして活用してください。
(i+1) % n を忘れず。time_limit.seconds = 10 明示。本編の 10 語に加え、 さらに専門用語 20 個を整理。 文献を読むときの「分からない単語チェッカー」として使えます。
RUNS (試行回数)、 MAX_TRIALS。 デフォルトで世界記録レベル。47 都道府県 × 12 年分(2012〜2023 年度)のデータを使い、 TSP(巡回セールスマン問題) を多角的に体験する総合演習。 単発の操作ではなく、 一連の分析フローを通して理解を深めます。
🎯 このコードでやること:47 都道府県 TSP を 1 ジャブで実行
📥 入力:SSDSE-B-2026.csv の 47 都道府県データ。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | import pandas as pd import numpy as np from sklearn.metrics.pairwise import euclidean_distances df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1) d = df[df['年度']==2023].reset_index(drop=True) xy = np.column_stack([d['総人口']/1e6, d['出生数']/1e3]) D = euclidean_distances(xy) # 簡易 NN n = 47 tour = [0] remaining = set(range(1, n)) while remaining: nxt = min(remaining, key=lambda j: D[tour[-1]][j]) tour.append(nxt); remaining.remove(nxt) length = sum(D[tour[i]][tour[(i+1)%n]] for i in range(n)) print(f"NN 経路長: {length:.4f}") print(f"\n最初の 10 訪問:") for i in tour[:10]: print(f" {d.iloc[i]['都道府県']:<6s}: 人口 {d.iloc[i]['総人口']:>9,}, 出生 {d.iloc[i]['出生数']:>5,}") |
📤 実行結果:
💬 結果の読み方:Nearest Neighbor で北海道→静岡→広島→茨城…と、 「総人口×出生数」空間で規模の近い県を順に巡る (地理的な順序ではない点に注意)。 NP 困難でも 47 都市なら 0.001 秒で初期解。
TSP (巡回セールスマン問題) を中心に、 NP 困難性・厳密解法 (整数計画/分枝限定)・近似解法 (最近傍/2-opt/遺伝的)・実応用 (物流/配車) への分岐を 6 方向に整理。
TSP の中心から、 2-opt / 3-opt (局所探索)、 遺伝アルゴリズム、 Lin-Kernighan ヒューリスティック、 Concorde solver、 強化学習による TSP (Pointer Network) が放射状に配置される。 SSDSE-B-2026 の 47 都道府県庁所在地を点群と見立て、 全 47 都市を最短で巡回する経路問題は古典的 TSP の好例。
TSP は組合せ最適化の基礎で、 上流の問題定式化から下流の経路可視化まで多様な手法と連携する。
SSDSE-B-2026 から「47 県庁所在地を 1 回ずつ訪問」する場合、 OR-Tools で約 2 秒、 距離は約 8500km。 ブルートフォースだと 47! ≈ 10^58 通りで不可能。
TSP を解く方法は、 都市数 n の大きさで 4 通りに分岐する。
SSDSE-B-2026 の 47 県は n=47 なので Concorde で厳密解可能。 ただし都内 23 区を含めると n=70 になり、 OR-Tools の近似で実用十分。
(i+1) % n を忘れず。time_limit.seconds = 10 明示。巡回セールスマン問題 は「最適化」分野の中で発展してきた概念・手法です。 学術的には継続的な研究で精緻化され、 実務的にはツール・ライブラリの普及で誰でも使えるようになってきました。 用語の使い方・意味は時代と分野で少しずつ変わるため、 文脈に応じた解釈が大切です。 入門書だけでなく、 標準的な教科書(例:データサイエンス・統計学の定本)や信頼できるオンライン教材も併用すると、 ぶれない理解に近づけます。
このページの本文とデモは、 平面に置いた都市の ユークリッド距離 を前提に、 最近傍法・2-opt・厳密解を比べてきた。 ここではあえて別角度から光を当てる。 TSP が本当に食べているのは地図ではなく 距離行列 (どの都市とどの都市が何だけ離れているかの表) だけである。 では、 その数字を どう作るか で答えはどこまで動くのか — これを SSDSE-B-2026 の実測値で確かめる。
疑似座標として x = 総人口 (A1101)、 y = 15歳未満人口 (A1301) を採る (2023 年・47 都道府県、 いずれも 架空の疑似座標)。 2 列は桁が違う。
| 疑似座標軸 (実測値, 2023) | 最小 | 最大 | 標準偏差 |
|---|---|---|---|
| x = 総人口 | 537,000 | 14,086,000 | 2,797,551 |
| y = 15歳未満人口 | 65,000 | 1,513,000 | 312,020 |
標準偏差の比は 約 8.97 倍。 距離は差の二乗で効くので、 生の値のまま距離行列を作ると 二乗距離の 98.8% が x (総人口) だけで決まり、 y (15歳未満人口) はほぼ無視される (全 47C2=1081 ペアの実測平均)。 つまり「2 次元の TSP」のつもりが、 中身は 総人口で並べるだけの 1 次元問題に化けている。 各軸を z-score で標準化すると寄与は 50.0% / 50.0% に均され、 初めて 2 列が対等に効く。
この違いは巡回順に実際に現れる。 コード順の先頭 8 県 (北海道・青森・岩手・宮城・秋田・山形・福島・茨城) を最近傍法 (起点=北海道) で解くと:
アルゴリズムも都市も同じなのに、 前処理 (標準化するか否か) だけで訪問順が入れ替わる。 TSP の「最適解」は距離行列に対してのみ最適であって、 その行列が妥当かどうかは TSP の外で決まる — これが本文のアルゴリズム論とは独立した、 データ側の落とし穴である。 詳しくは 標準化 を参照。
※ 数値は data/raw/SSDSE-B-2026.csv (2023 年・47 都道府県) の実測値を Python で算出。 疑似座標および巡回順の例は学習用の 架空構成であり、 地理的な最短ルートではない。