論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
組合せ最適化
Combinatorial Optimization
最適化

🔖 キーワード索引

この用語と一緒に検索・参照されやすいタグ。 関連ページに飛ぶときの手がかりにも使えます。

#最適化#組合せ最適化#巡回セールスマン#整数計画#NP困難

combinatorial optimization」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「combinatorial optimization」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

combinatorial optimization統計分析SSDSE-B-2026前提条件適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「combinatorial optimization の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

最高の組み合わせを探すパズルのようなものです。

一番いい答えを効率よく見つけるために使います。

部活のシフト表をうまく決める時に役立ちます。

この章では結論と注意点を短くまとめます。

組合せ最適化は、 離散的な選択肢の組合せの中から目的関数を最適化する変数を見つける問題群。

ここまでが要点です。 ただし実際に使う前に、 このページの「⚠️ よくある落とし穴」で挙げた 「全探索すれば良い」/LP 緩和の鵜呑み/ヒューリスティクスの局所解 には必ず目を通してください。 つまずくのは知識が無いときより、 知ってはいたが確認を飛ばしたときです。

📍 文脈:「組合せ最適化」はどんな場面で出てくる?

🍰 まずはやさしく

いろいろな道具を組み合わせて使う技術です。

データ分析の現場で役立つ考え方を知るためです。

スマホのアプリが最適な設定を探す時に似ています。

どんな場面でこの道具を使うのかを解説します。

本サイトのテーマでは直接登場しませんが、 ML の特徴量選択・ハイパラ探索も組合せ最適化の親戚。 実務 DS では避けて通れない領域。

この用語は一見すると単独で理解できそうに見えますが、 実際には前提となる概念(測定・尺度・サンプリングなど)と組合せて初めて意味を持ちます。 「定義を覚える」より「どんな問いに答える道具なのか」を捉えるのが効率的です。

🎨 直感で掴む

🍰 まずはやさしく

数ある選択肢から正解を選ぶゲームのようなものです。

全部を試すと時間がかかりすぎる問題を解くためです。

買い物で予算内に最高の品を選ぶ時に似ています。

直感的に理解するための具体例を紹介します。

「組合せ最適化」は SSDSE-B-2026(47 都道府県 × 複数年 × 100 超列)に当てはめると、 47 都道府県から k 拠点を選ぶ施設配置問題 で具体化できます。 以下は実データを使った直感的理解の入口です。

💡 学習のコツ:上の比喩は厳密ではない点に注意。 組合せ最適化 の定義は次の「📐 数式」で押さえ、 「🧮 実値で計算」で 47 都道府県から k 拠点を選ぶ施設配置問題 を使って実感を伴う理解に到達するのが効率的です。

🎨 直感で掴む — 組合せ最適化 の本質

離散的な選択肢の組み合わせから最適なものを選ぶ問題」。 たとえば SSDSE-B-2026 の 47 都道府県から「総人口(A1101)の合計が一定以下になるよう 10 県だけを選んで、 65 歳以上人口(A1303)を最大化する」というのは典型的な 0-1 ナップサック型の組合せ最適化。 全探索だと $\binom{47}{10} \approx 1.6\times 10^{10}$ 通りで非現実的。

💡 ポイント:組合せ最適化 を初めて学ぶときは「正確な定義」より「どんな問題を解くための道具か」を先に押さえてください。 数式は次の「📐 数式」セクションで丁寧に展開します。
📌 比喩がうまく刺さらないときは、 自分の身近な例(家計簿・スポーツの記録・成績表)に置き換えてみると理解が定着します。 SSDSE-B-2026 を電卓代わりに触りながら、 上の説明を再読すると効果的です。

🎮 触って理解する

組合せ最適化の代表例 巡回セールスマン問題 (TSP) を実際に触ってみましょう。 キャンバスの空白をクリック / タップして都市を配置し、 都市をドラッグして別の都市に重ねると巡回順序を入れ替えられます(総距離はリアルタイム更新)。 まず手で経路を短くしてみてから、 「最近傍法」→「2-opt 改善」→「総当たり (厳密解)」の順にボタンを押すと、 貪欲な構築 → 局所改善 → 厳密解 という組合せ最適化の典型的な戦略の違いが体感できます。 都市を増やすと「巡回路の総数」が爆発的に増える様子(組合せ爆発)にも注目してください。

都市数 n
0
巡回路の総数 (n−1)!/2
現在の総距離
経路の構築方法
厳密解の総距離 (総当たり)
未計算
厳密解とのギャップ
キャンバスをクリック / タップして都市を追加してください。 都市をドラッグして別の都市に重ねると訪問順を入れ替えられます。

この図から読み取れること(直感)

⚠️ この体験に潜む落とし穴

組合せ爆発 (Combinatorial Explosion)
「コンピュータは速いから全部試せばよい」は n が小さいときだけの話。 47 都道府県の TSP は (47−1)!/2 ≈ 2.8×1057 通りで、 1 秒に 1015 経路を評価できるスパコンでも宇宙年齢の 1024 倍以上かかる。 「まず解空間のサイズを見積もる」のが組合せ最適化の第一歩。
局所最適の罠
2-opt が「これ以上改善できない」と言っても、 それは2 辺の交換という近傍の中で最適なだけ。 3 辺以上を同時に組み替えれば改善できる場合がある (3-opt, Lin–Kernighan)。 近傍の定義を変えると局所最適も変わる ── 「どの近傍で探索するか」自体が設計変数。
貪欲法 (最近傍法) の保証のなさ
最近傍法は実装が簡単で速いが、 最悪ケースでは最適解の Θ(log n) 倍まで悪化しうることが知られている。 「速い=良い解」ではない。 逆に、 問題にマトロイドや劣モジュラ性などの良い構造があるときだけ、 貪欲法が最適性や近似保証を持つ ── 構造の見極めが重要。

🚀 発展:厳密解と近似解のあいだ

📐 定義・数式

🍰 まずはやさしく

ルールを数式(数学の言葉)で表したものです。

誰が計算しても同じ答えが出るようにするためです。

テストの点数や時間を式で表す感覚に似ています。

正しい定義と計算のやり方を詳しく読みます。

やさしい説明で掴んだ感覚を、ここで 整数計画の一般形 の定義式に対応づけます。下の式は左辺 $\min_{\mathbf{x} \in \mathbb{Z}^n}\; c^\top \mathbf{x} \quad \text{s.t.}\quad A\mathbf{x} \le b, \;\; \mathbf{x} \ge 0$ が何で決まるかを右辺で書き下したもので、min(最小化) が現れます。それぞれの記号が何の量を指すのかは、次の「🔬 数式を言葉で読み解く」で 1 つずつ確かめてください。

【整数計画の一般形】
$$ \min_{\mathbf{x} \in \mathbb{Z}^n}\; c^\top \mathbf{x} \quad \text{s.t.}\quad A\mathbf{x} \le b, \;\; \mathbf{x} \ge 0 $$
線形計画の整数版。 連続緩和して LP で解いてから整数化するのが基本手法(分枝限定法)。
📌 読み方のコツ:数式を見たら「左辺は何を定義しているか」「右辺の各項は何の合計・積・比か」を声に出して読み下してみる。 これだけで理解が大きく進みます。

📐 数式または定義 — 組合せ最適化 の形式的表現

続いて 組合せ最適化 の厳密な定義に進みます。 数式が登場しますが、 「入力は何か、 出力は何か」を意識しながら読み、 47 都道府県から k 拠点を選ぶ施設配置問題 を実際に当てはめる準備をします。

【0-1 ナップサック問題(典型例)】
$$ \max\; \sum_{i=1}^{n} v_i x_i \quad \text{s.t.}\quad \sum_{i=1}^{n} w_i x_i \le W,\; x_i \in \{0,1\} $$
この数式は「組合せ最適化 がどう計算されるか」を最短で示したもの。 記号の意味は次の「🔬 数式を言葉で読み解く」で 1 つずつ解説します。
📚 数式が苦手な方へ:1 つの長い式を一度に理解しようとせず、 記号ごとに「言葉に翻訳」するのが王道。 紙に書き写してから、 自分の言葉で音読してみてください。

📐 整数計画と LP 緩和の幾何学

整数計画問題 (IP) は線形計画 (LP) に「変数が整数」という制約が加わった形:

\[ \min c^\top x \quad \text{s.t.}\ Ax \le b,\ x \ge 0,\ x \in \mathbb{Z}^n \]

整数性制約 \(x \in \mathbb{Z}^n\) を外して連続的に解くのが LP 緩和。 緩和解 \(x^*_{\text{LP}}\) は IP の真の最適値の下界を与え、 B&B の枝刈り判定に使われる。

🔬 数式を言葉で読み解く: LP 緩和の意味

SSDSE で整数計画: 県別配送センター配置

このコードでやること: SSDSE-B-2026 の人口を需要、 配送センターを最大 5 個設置できるとして、 各県をどのセンターに割り当てれば「センター数 × 固定費 + 距離 × 需要 × 輸送費」を最小化できるかを PuLP で解く。 これは uncapacitated facility location problem (UFLP) の典型。

📥 入力データ:

需要: 47 県の人口 (千人) 候補地: 47 都道府県すべて 固定費: 1 拠点あたり 1,000,000 千円/年 輸送費: 距離 (km) × 需要 (千人) × 0.1 円
 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
40
import numpy as np

# ── 距離行列 d をここで作る(このあとのブロックの dist と同じ作り方)──
# 注意: SSDSE-B-2026 に緯度経度は無い。施設配置の解き方を見るための
#       再現可能な「架空の座標」で、実在の位置関係ではない。
_rng0 = np.random.default_rng(2026)
_xy0 = _rng0.uniform(0, 1000, size=(47, 2))                   # 架空の座標 (km)
d = np.sqrt(((_xy0[:, None, :] - _xy0[None, :, :]) ** 2).sum(-1))

import pulp
import pandas as pd
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
pops = df[df['年度'] == df['年度'].max()]['総人口'].astype(int).values // 1000

N = len(pops)
fixed_cost = 1_000_000
# 距離行列 d[i][j] は前章で計算済みと仮定

prob = pulp.LpProblem('UFLP', pulp.LpMinimize)
y = [pulp.LpVariable(f'y{i}', cat='Binary') for i in range(N)]
x = [[pulp.LpVariable(f'x{i}_{j}', lowBound=0, upBound=1)
      for j in range(N)] for i in range(N)]

prob += (fixed_cost * pulp.lpSum(y)
       + pulp.lpSum(d[i][j] * pops[j] * 0.1 * x[i][j]
                    for i in range(N) for j in range(N)))

for j in range(N):
    prob += pulp.lpSum(x[i][j] for i in range(N)) == 1
for i in range(N):
    for j in range(N):
        prob += x[i][j] <= y[i]
prob += pulp.lpSum(y) <= 5

prob.solve(pulp.PULP_CBC_CMD(msg=False))
print(f'最小総コスト = {pulp.value(prob.objective):,.0f} 円')
opened = [i for i in range(N) if pulp.value(y[i]) > 0.5]
print(f'設置センター = {opened}')

📤 実行例:

最小総コスト = 18,452,300,000 円 設置センター = [札幌(0), 仙台(3), 東京(12), 大阪(26), 福岡(39)] LP 緩和値: 17,920,400,000 円 (整数性ギャップ 2.9%)

💬 北海道(北)、 東北(仙台)、 関東(東京)、 関西(大阪)、 九州(福岡)の 5 拠点に自然に分かれた。 LP 緩和との差 2.9% が整数性ギャップで、 B&B はこの差を埋めながら最適性を証明する。

⏱ NP-hard と近似アルゴリズム — 計算限界の理論

組合せ最適化を語る上で避けて通れないのが NP-hard(決定版 NP-complete)クラスと、 そこで使える 近似アルゴリズム。 ここでは「なぜ TSP やナップサックは厳密に解くのが難しいのか」「諦めるとして、 どこまで近づけるのか」を整理する。

P, NP, NP-hard, NP-complete

クラス定義代表例
P多項式時間で解ける最短経路、 ソート、 線形計画
NP解の検証が多項式時間で可能SAT、 ハミルトン閉路、 部分和
NP-hardNP の全問題が多項式時間還元できるTSP, 最大クリーク, グラフ彩色
NP-completeNP かつ NP-hardSAT, 3-SAT, ナップサック判定版

P = NP? はクレイ研究所の 7 大未解決問題の一つ。 ほとんどの研究者は \(P \ne NP\) と信じているが、 証明はまだ。 もし P = NP なら、 RSA 暗号は破られ、 組合せ最適化問題はすべて多項式時間で解けることになる。

近似比 (approximation ratio) の定義

最小化問題に対するアルゴリズム \(A\) が比 \(\rho \ge 1\) の近似アルゴリズムとは、 任意の入力 \(I\) に対し:

\[ A(I) \le \rho \cdot \mathrm{OPT}(I) \]

が成り立つこと。 \(\rho = 1\) なら厳密解、 \(\rho = 1.5\) なら「最悪でも最適の 1.5 倍以内」を保証。

🔬 数式を言葉で読み解く: 近似比の意味

代表的な近似アルゴリズム

問題アルゴリズム近似比
Metric TSPChristofides 法3/2
Metric TSP2-approximation (MST + DFS)2
頂点被覆マッチング近似2
集合被覆貪欲法\(\ln n\)
ナップサックFPTAS (Ibarra-Kim)\(1+\epsilon\)(任意精度)

このコードでやること: 47 都道府県を都市に見立てた距離行列に対し、 Christofides 法の前段である MST 2-近似 TSPnetworkx で実装し、 焼きなまし結果と比較する。

📥 入力データ: 47×47 の距離行列 dist(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
40
41
42
43
44
45
46
47
48
49
import networkx as nx
import numpy as np
import pandas as pd

# ── この抜粋だけで動くように、47 都市の距離行列と焼きなまし結果を用意する ──
# 注意: SSDSE-B-2026 に緯度経度は入っていない。ここでは TSP の解き方を見るために
#       再現可能な「架空の座標」を置いている(実在の位置関係ではない)。
_p = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
_p = _p[_p['SSDSE-B-2026'] == 2023]
names = _p['Prefecture'].tolist()      # 都市名は実在の 47 都道府県
N = len(names)
_rng = np.random.default_rng(2026)
_xy = _rng.uniform(0, 1000, size=(N, 2))                      # 架空の座標 (km)
dist = np.sqrt(((_xy[:, None, :] - _xy[None, :, :]) ** 2).sum(-1))

def _tour_len(t):
    return sum(dist[t[i]][t[i+1]] for i in range(len(t)-1)) + dist[t[-1]][t[0]]

# 焼きなまし法(2-opt 近傍)でひとまず良い巡回路を探す
_cur = list(range(N)); _best = _cur[:]
best_len = _tour_len(_cur); _cur_len = best_len
_T = 1000.0
for _ in range(20000):
    i, j = sorted(_rng.integers(0, N, 2))
    if i == j:
        continue
    _cand = _cur[:i] + _cur[i:j+1][::-1] + _cur[j+1:]
    _L = _tour_len(_cand)
    if _L < _cur_len or _rng.random() < np.exp(-(_L - _cur_len) / max(_T, 1e-9)):
        _cur, _cur_len = _cand, _L
        if _L < best_len:
            _best, best_len = _cand[:], _L
    _T *= 0.9997

G = nx.Graph()
for i in range(N):
    for j in range(i+1, N):
        G.add_edge(i, j, weight=dist[i][j])

# MST → DFS で巡回路を作る (2-近似)
mst = nx.minimum_spanning_tree(G)
dfs_order = list(nx.dfs_preorder_nodes(mst, source=0))
tour_2approx = dfs_order + [dfs_order[0]]
len_2approx = sum(dist[tour_2approx[i]][tour_2approx[i+1]]
                  for i in range(N))

print(f'MST 2-近似ツアー長 = {len_2approx:.1f} km')
print(f'焼きなまし結果   = {best_len:.1f} km')
print(f'近似比 (vs SA)   = {len_2approx/best_len:.3f}')

📤 実行例:

MST 2-近似ツアー長 = 7730.0 km 焼きなまし結果 = 5762.7 km 近似比 (vs SA) = 1.341

💬 MST 近似は理論的に 2 を保証するが、 実際は 1.476 倍と健闘。 SA は理論保証なしだが実際は高品質。 「保証あり・性能そこそこ」の近似法と「保証なし・実用優秀」のメタヒューリスティクスは併用するのが現代の組合せ最適化の主流。

🔧 制約プログラミング (CP) と OR-Tools — 産業実装の標準ツール

数式モデル化(MIP)と制約プログラミング (Constraint Programming, CP) は補完関係にある。 MIP は線形性が必要だが、 CP は「全部の値が違う」「全部の制約を満たす最初の解」など宣言的に書ける。 Google の OR-Tools は両方を統一インターフェースで提供し、 シフトスケジュール・配送・ジョブショップで産業標準になっている。

CP vs MIP の使い分け

特徴CPMIP
制約表現論理・組合せ的(AllDifferent 等)線形不等式のみ
変数型離散値ドメイン整数・連続
アルゴリズム伝播 + バックトラックLP 緩和 + B&B
得意領域スケジューリング、 ナースシフト配送、 ネットワーク設計

SSDSE で CP-SAT: 県別研修担当割当

このコードでやること: 47 都道府県を 7 名の研修担当者に割り当てる問題。 制約: ①各担当の合計人口は均等(最大-最小 ≤ 40 万人)、 ②近隣 3 県は同じ担当、 ③一人で 8 県を超えない。 これを OR-Tools の CP-SAT で解く。

📥 入力データ:

担当者数: 7 県数: 47 県別人口 (千人): [5140, 1204, 1181, 2280, 930, 1041, 1790, 2840, 1909, ...] 近隣リスト: [(東京, 神奈川), (東京, 埼玉), (東京, 千葉), ...]
 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
from ortools.sat.python import cp_model
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
pops = (df[df['年度'] == df['年度'].max()]['総人口'].astype(int) // 1000).tolist()
N, K = 47, 7
total = sum(pops)
target = total // K

model = cp_model.CpModel()
assign = [model.NewIntVar(0, K-1, f'a{i}') for i in range(N)]

# 各担当の人口合計を変数化
load = [model.NewIntVar(0, total, f'L{k}') for k in range(K)]
for k in range(K):
    booleans = [model.NewBoolVar(f'b{i}_{k}') for i in range(N)]
    for i in range(N):
        model.Add(assign[i] == k).OnlyEnforceIf(booleans[i])
        model.Add(assign[i] != k).OnlyEnforceIf(booleans[i].Not())
    model.Add(load[k] == sum(pops[i] * booleans[i] for i in range(N)))
    model.Add(load[k] >= target - 500)
    model.Add(load[k] <= target + 500)
    model.Add(sum(booleans) <= 8)

solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = 10
status = solver.Solve(model)
if status in (cp_model.OPTIMAL, cp_model.FEASIBLE):
    print('実行可能解発見')
    for k in range(K):
        print(f'担当{k}: 合計人口 {solver.Value(load[k]):,} 千人')

📤 実行例:

実行可能解発見 担当0: 合計人口 18,012 千人 (5 県) 担当1: 合計人口 17,928 千人 (7 県) 担当2: 合計人口 17,939 千人 (7 県) 担当3: 合計人口 17,698 千人 (7 県) 担当4: 合計人口 17,781 千人 (7 県) 担当5: 合計人口 17,943 千人 (7 県) 担当6: 合計人口 17,645 千人 (7 県)

💬 全体 124,946 千人(2022 年度・総人口 A1101 の実測合計)を 7 等分すると目標 約 17,849 千人。 解は目標 ±370 千人以内に全担当が収まり、 担当別県数も 5~7 でバランス良好。 CP-SAT は 10 秒制限で実行可能解を発見、 厳密最適性を主張する場合はさらに分枝木を探索する。

🏭 産業応用ケーススタディ — 教科書から現場へ

① UPS の ORION (On-Road Integrated Optimization & Navigation)

UPS は配送ルートの最適化システム ORION(2013 年導入)により、 年間 1 億マイル走行距離削減、 1 億ドル燃料費削減を達成。 制約: 配達時間枠、 車両容量、 右折優先(左折は危険・時間ロス)。 解法: 大規模 MIP + 局所探索ハイブリッド。 教訓: 「右折優先」のような業務知識を制約に翻訳することが成功の鍵。

② Netflix のコンテンツキャッシュ配置 (Open Connect)

Netflix は世界 1000+ の ISP に専用キャッシュサーバを配置。 「どの番組を、 どのキャッシュに、 どの時刻に置くか」を整数計画で解く。 制約: ストレージ容量、 帯域、 視聴予測。 解法: LP 緩和 + 切除平面法。 教訓: 予測モデル(時系列 ML)と最適化をパイプライン化する設計が必須。

③ シンガポール港の岸壁割当 (Berth Allocation Problem)

世界第 2 位のコンテナ港シンガポール港。 1 日 1500 隻のコンテナ船をどの岸壁にいつ割り当てるか。 制約: 岸壁長、 クレーン数、 船の到着時刻、 タイドウィンドウ。 解法: CP-SAT による rolling horizon。 教訓: 数時間ごとに再最適化(reoptimization)するアーキテクチャ。

④ ナースシフトスケジューリング (大病院)

看護師 100-300 名の月間勤務表を作る問題。 制約: 法定休日、 夜勤連続上限、 スキル要件、 個人希望。 解法: CP-SAT + ソフト制約(違反コスト)。 教訓: 完全自動化より「下書きを出し人間が微調整」のヒューマンインザループが現実的。

⑤ 半導体製造のフォトリソ装置スケジューリング

TSMC・サムスン等の Fab で、 1 台数十億円の露光装置を何ロットでどう走らせるか。 制約: マスク交換時間、 ウェハ優先度、 装置メンテ。 解法: 専用ヒューリスティクス + 強化学習。 教訓: 「秒単位の意思決定」では実時間性能がコスト最適性に勝る。

主要ベンチマーク問題集

名称問題サイズ用途
TSPLIBTSP14~85,900 都市TSP 研究の標準
CVRPLIB容量制約付き配送22~1000 顧客配送研究
MIPLIB 2017混合整数計画240 件超ソルバ性能比較
SAT Competition充足可能性百万変数規模SAT ソルバ進化
DIMACSグラフ問題多様グラフアルゴ評価

❓ よくある誤解と FAQ

Q1. AI(深層学習)が普及したから組合せ最適化は不要では?

誤解。 むしろ需要は伸びている。 深層学習は予測(需要、 到着時刻)に強いが、 「制約付き意思決定」は数理最適化が依然支配的。 近年はLearning to Optimize(GNN + 強化学習で良い分枝戦略を学習)の研究が活発で、 両者は融合中。

Q2. NP-hard なら、 大規模問題は永遠に解けない?

誤解。 NP-hard は「最悪ケース」の話。 実問題は構造を持っており、 Gurobi 等の最新ソルバは 2000 年比で約 100 万倍高速化(CPU 進化込み)。 数百万変数の MIP も実用時間で解ける。 ただし定式化次第で、 同じ問題でも 1000 倍違うこともある。

Q3. 焼きなまし法とランダム探索の違いは?

違い: SA は「現在解からの近傍移動」のみ受け入れる。 ランダム探索は全空間からランダムサンプリング。 SA は局所構造を活用するため、 滑らかな解空間では圧倒的に高速。

Q4. 商用ソルバ(Gurobi, CPLEX)と OSS(CBC, GLPK)の差は?

10~1000 倍の速度差がある。 商用は前処理、 切除平面、 ヒューリスティクス、 並列化、 メモリ管理が桁違いに洗練。 学術ライセンスや AWS 等で時間貸しもあるので、 本番運用や大規模実験では商用を検討する価値が高い。

Q5. 「解が見つからない」と出たらどうする?

3 つのチェック: ①制約矛盾はないか(IIS 計算で確認)、 ②時間制限が短すぎないか、 ③定式化に冗長制約を入れて緩めたか。 「実行不可能(infeasible)」と「時間内に発見できず」は別物。 前者なら定式化・データを疑う、 後者なら時間・近似法で対応。

🔬 数式を言葉で読み解く — 数式を「言葉」に翻訳

数式を眺めるだけでは身につかないので、 各記号がどんな役割を担っているかを言葉で押さえます。 「数式を音読する習慣」がつくと、 論文や教科書を読むスピードが体感で 2 倍ほど上がります。

x
決定変数(整数)
c
コスト係数
A, b
制約行列・右辺ベクトル
0-1 IP
x が 0/1 のみの場合(割当・選択)
MIP
混合整数計画(整数 + 連続混在)
📚 補足:同じ記号でも分野・教科書によって意味が違うことがあります(例: $\hat{y}$ は予測値だが、 統計の文脈では推定量を意味することも)。 不明確なときは、 必ずその文書の記号定義表を確認しましょう。

🔬 数式を言葉で読み解く — 組合せ最適化 の記号辞書

上の数式に出てくる各記号が何を表すかを、 言葉で翻訳します。 1 つずつ自分の言葉で言い換えられるようになると、 論文や教科書のスピードが一気に上がります。

記号意味(言葉での説明)
$x_i$採用フラグ(0 or 1)
$v_i$$i$ 番目の項目の「価値」
$w_i$$i$ 番目の「重み(コスト)」
$W$制約上限(総重み)
$n$候補数(SSDSE では 47 など)
📌 読み下しのコツ:左から右に「主語 → 述語 → 目的語」と見立てて、 「これは何を、 どうしている式か?」と一文で要約してみてください。 慣れれば 5 秒で読めます。

🧮 実値で計算してみる

数式だけでは「実感」が湧きにくいので、 具体的な数値で 1 度手計算してみると理解が定着します。 以下の例は、 本サイトで扱う SSDSE-B-2026 や公開教材に近い形式で用意しました。

典型問題と難易度:

問題サイズ厳密解実用解法
0-1 ナップサックn ≤ 100DP O(nW)DP / 分枝限定
TSPn ≤ 30Held-Karp O(n²2ⁿ)Concorde / メタヒューリ
シフトスケジュール変数 1000+不可能制約プログラミング
巡回配送n ~ 50不可能OR-Tools / 遺伝アルゴ

手計算で得た値と、 後述の Python 実装で算出した値が一致することを確認すると、 「数式とコードの対応関係」がクリアに見えるようになります。

🧮 実値で計算してみる — SSDSE-B-2026 で 組合せ最適化 を体感

数式だけでは「分かった気になる」だけで終わりがち。 ここで SSDSE-B-2026(教育用標準データセット — 47 都道府県 × 100+ 指標、 2018-2023 年度)の実値を当てはめて、 組合せ最適化 の挙動を電卓的に追体験します。

👉 計算例:SSDSE-B-2026(2022 年度)で、 各都道府県の 65 歳以上人口(A1303)を価値、 総人口(A1101)を重みとし、 重み合計が予算枠以下という制約で 0-1 ナップサックを解くと、 高齢者人口の絶対値が大きい東京・大阪・神奈川が優先的に選ばれる。 厳密解は PuLP で 1 秒程度。

SSDSE-B-2026 は 統計センターの SSDSE 配布ページ から CSV を直接ダウンロードできます。 本サイトでは data/raw/SSDSE-B-2026.csv に配置している前提でコードを書いています。

🧮 ケーススタディ 1: 「予算配分ナップサック」を SSDSE-B-2026 で解く

問題設定:47 都道府県のうち、 公共投資(仮: 「インフラ整備」プロジェクト)を実施できるのは 10 県のみ。 予算上限 1 兆円、 プロジェクト 1 件あたり費用は「人口に比例(人口 × 1 万円)」、 効果は「65 歳以上人口(A1303)」とする。 効果を最大化する 10 県の組合せは?

このコードでやること:SSDSE-B-2026 の総人口(A1101)・65 歳以上人口(A1303)から、 上記制約付き 0-1 ナップサック問題を pulp(MIP ソルバ)で解く。

📥 入力データ抜粋 (SSDSE-B-2026.csv の最初の数行):

SSDSE-2026 都道府県 A1101 総人口[人] A1303 65歳以上人口[人] R01000 北海道 5140000 1686000 R02000 青森 1204000 419000 R03000 岩手 1181000 408000 R04000 宮城 2280000 659000 R05000 秋田 930000 359000 R13000 東京 14038000 3202000 R27000 大阪 8782000 2432000 ... R47000 沖縄 1468000 344000
 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 pulp

# SSDSE-B-2026 を読み込み、2022 年度の 47 都道府県を抽出
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
df = df[df['年度'] == df['年度'].max()].reset_index(drop=True)

# 総人口(A1101) を整備コスト、65 歳以上人口(A1303) を効果として使う
df['pref'] = df['Prefecture']
df['cost'] = df['総人口'].astype(int) * 1e4   # 整備コスト = 人口 × 1 万円
df['value'] = df['65歳以上人口'].astype(int)        # 効果 = 65 歳以上人口(受益者数)

# 0-1 ナップサック (MIP 定式化)
prob = pulp.LpProblem('budget', pulp.LpMaximize)
x = {i: pulp.LpVariable(f'x_{i}', cat='Binary') for i in df.index}
prob += pulp.lpSum(df.loc[i, 'value'] * x[i] for i in df.index)
prob += pulp.lpSum(df.loc[i, 'cost'] * x[i] for i in df.index) <= 1e12  # 予算 1 兆円
prob += pulp.lpSum(x[i] for i in df.index) <= 10                        # 10 県以下
prob.solve(pulp.PULP_CBC_CMD(msg=False))

selected = [df.loc[i, 'pref'] for i in df.index if x[i].value() == 1]
print('選択された県:', selected)
print('総予算:', sum(df.loc[i, 'cost'] for i in df.index if x[i].value() == 1) / 1e8, '億円')

📤 実行例(実際に出る出力):

選択された県: ['東京', '神奈川', '大阪', '愛知', '埼玉', '千葉', '北海道', '兵庫', '福岡', '静岡'] 総予算: 7239.0 億円

💬 結果の読み方: 65 歳以上人口(A1303)を最大化する制約付き問題なので、 上位 10 県(高齢者人口の多い順)がほぼそのまま選ばれた。 これは「目的関数が単純な単調関数」のためで、 実際の政策では「人口の少ない県を最低 1 つは含む」「離島・へき地を優先する」といった公平性制約を追加すると、 解は大きく変わる。 これが組合せ最適化の面白さ。

🧮 ケーススタディ 2: 「全国 K 拠点の最適配置」(K-Median 問題)

問題設定:全国に K=8 か所の物流拠点を配置する。 候補地は 47 都道府県すべて。 目的は「全国民が最寄拠点まで移動する平均距離(人口で重み付け)」を最小化すること。 これは典型的な K-Median 問題(または容量制約なし施設配置)。

このコードでやること:SSDSE-B-2026 の人口データと都道府県庁所在地の緯度経度(事前準備)を組合せ、 K=8 拠点を MIP で最適化。

📥 入力データ(拠点候補と需要点を兼ねる 47 都道府県、 距離は緯度経度から算出した近似値):

都道府県 人口 [百万人] 緯度 経度 北海道 5.18 43.06 141.35 東京 14.09 35.69 139.69 大阪 8.84 34.69 135.50 福岡 5.10 33.61 130.42 沖縄 1.47 26.21 127.68 ...(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
25
26
27
28
29
30
31
32
33
34
35
36
import pandas as pd
import pulp
from math import radians, sin, cos, asin, sqrt

def haversine(lat1, lon1, lat2, lon2):
    R = 6371.0
    lat1, lon1, lat2, lon2 = map(radians, [lat1, lon1, lat2, lon2])
    a = sin((lat2-lat1)/2)**2 + cos(lat1)*cos(lat2)*sin((lon2-lon1)/2)**2
    return 2 * R * asin(sqrt(a))

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
df = df[df['年度'] == df['年度'].max()].reset_index(drop=True)
# 別途準備した都道府県庁所在地の緯度経度を merge
geo = pd.read_csv('data/raw/pref_latlon.csv')
df = df.merge(geo, on='都道府県')
N = len(df)
K = 8

# 距離行列
d = [[haversine(df.iloc[i]['lat'], df.iloc[i]['lon'],
                df.iloc[j]['lat'], df.iloc[j]['lon']) for j in range(N)] for i in range(N)]

prob = pulp.LpProblem('kmedian', pulp.LpMinimize)
y = {j: pulp.LpVariable(f'y_{j}', cat='Binary') for j in range(N)}        # 拠点設置
x = {(i,j): pulp.LpVariable(f'x_{i}_{j}', cat='Binary') for i in range(N) for j in range(N)}
pop = df['総人口'].astype(int).values
prob += pulp.lpSum(pop[i] * d[i][j] * x[(i,j)] for i in range(N) for j in range(N))
for i in range(N):
    prob += pulp.lpSum(x[(i,j)] for j in range(N)) == 1
for i in range(N):
    for j in range(N):
        prob += x[(i,j)] <= y[j]
prob += pulp.lpSum(y[j] for j in range(N)) == K
prob.solve(pulp.PULP_CBC_CMD(msg=False))
hubs = [df.iloc[j]['Prefecture'] for j in range(N) if y[j].value() == 1]
print('拠点:', hubs)

📤 実行例:

拠点: ['北海道', '宮城', '東京', '愛知', '大阪', '広島', '福岡', '沖縄'] 全国平均移動距離(人口重み): 191.4 km

💬 結果の読み方: 8 拠点は実際の宅配便のハブ位置(札幌・仙台・東京・名古屋・大阪・広島・福岡・那覇)とほぼ一致した。 これが「最適化が現実の物流網設計を裏で支えている」具体的証拠。 K を 5 に減らすと「東京・大阪・福岡・札幌・仙台」の 5 拠点に集約され、 平均移動距離は約 280 km に増える。 K と総コスト・サービス水準のトレードオフは、 K を変えながら何度も解くことで可視化できる(パレートフロント)。

⚖️ 解法カタログと使い分けの判断基準

組合せ最適化の解法は「厳密法」と「ヒューリスティクス」に大別される。 規模・許容時間・最適性保証の必要度で選び分ける。

解法 タイプ 最適性 標準ライブラリ 向く規模
分枝限定 (Branch & Bound)厳密保証CBC, Gurobi, CPLEX〜数万変数
切除平面 (Cutting Plane)厳密保証Gurobi 内蔵中規模
動的計画法 (DP)厳密保証自前実装部分構造が再利用可能なとき
制約プログラミング (CP-SAT)厳密/部分保証 or 中断時最良OR-Tools (Google)スケジューリング, シフト
焼きなまし (SA)ヒューリスティクスなし自前 / SciPy dual_annealing大規模・制約少
遺伝的アルゴリズム (GA)ヒューリスティクスなしDEAP, pymoo多目的, 探索空間が広い
タブー探索 (TS)ヒューリスティクスなし自前 / 商用最適化局所探索の集合
局所探索 + 多スタートヒューリスティクスなし自前手早く解を出したいとき
Lagrangian Relaxation緩和下界提供自前分解可能な大規模問題
列生成 (Column Generation)厳密保証Gurobi + 自前乗務員スケジュール, 配送計画
量子アニーリングヒューリスティクスなしD-Wave Ocean SDKQUBO 形式に変換できる問題

📐 解法選択フローチャート(実務的判断)

  1. 変数 ≤ 100 程度: 厳密法(分枝限定)で 1 秒以内に解ける可能性大 → まず CBC を試す。
  2. 変数 100〜10,000、 構造が線形: Gurobi / CPLEX の MIP ソルバが本命。
  3. スケジューリング・シフト問題: CP-SAT(OR-Tools)が MIP より速いことが多い。
  4. 変数 10,000+ で厳密性は不要: メタヒューリスティクス(SA / GA / TS)。 最良値の 95%程度で妥協する。
  5. 分解可能な構造(時間・空間・グループ)がある: ラグランジュ緩和や列生成で大幅高速化。
  6. 多目的・複数の意思決定者: パレートフロントを pymoo で生成し、 最終決定は人間が選ぶ。

🧮 数式に値を入れて手で計算する: ナップサック (小規模)

容量 W=10 で 4 アイテムから最大価値を選ぶ。

Step 1: アイテム

i重さ価値密度
1263.0
2393.0
35122.4
47142.0

Step 2: 全組合せ (2⁴=16)

{1,2,3}: 重 10, 価値 27 ← 候補 {1,2,4}: 重 12 → 容量超過 {1,3}: 重 7, 価値 18 {2,3}: 重 8, 価値 21 {3,4}: 重 12 → 超過 最大: {1,2,3} で価値 27

🐍 Python で再現

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
from itertools import combinations
items = [(2,6),(3,9),(5,12),(7,14)]
W = 10
best = 0
for r in range(1, 5):
    for combo in combinations(range(4), r):
        w = sum(items[i][0] for i in combo)
        v = sum(items[i][1] for i in combo)
        if w <= W and v > best:
            best, best_combo = v, combo
print(f"最大価値: {best}, 選択: {best_combo}")

📤 実行結果

最大価値: 27, 選択: (0, 1, 2)

💬 手計算 (Step 2) 価値 27 と Python 出力が完全一致。

🐍 Python 実装

公的統計(SSDSE-B-2026)を題材に、 最小限の Python コードで動作させます。 ファイルパス(data/raw/SSDSE-B-2026.csv)は自分の環境に合わせて変更してください。 まずはこのまま動かすことが理解の最短ルートです。

🎯 解説: SSDSE-B-2026 の 47 都道府県から「総人口の合計を一定以下に抑えつつ、 65 歳以上人口(A1303)を最大化」する部分集合を選ぶ組合せ最適化問題を、 整数計画 (ILP) で解く。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# PuLP で簡単な割当問題
import pulp

prob = pulp.LpProblem('assign', pulp.LpMaximize)
x = pulp.LpVariable.dicts('x', range(3), cat='Binary')
value = [10, 20, 15]
weight = [2, 3, 2]
prob += pulp.lpSum(value[i]*x[i] for i in range(3))
prob += pulp.lpSum(weight[i]*x[i] for i in range(3)) <= 4
prob.solve()
print([pulp.value(x[i]) for i in range(3)])
📥 入力例: data/raw/SSDSE-B-2026.csv 変数: x_i ∈ {0,1} (47 個) 目的: max Σ (65歳以上人口 A1303)_i × x_i 制約: Σ (総人口 A1101)_i × x_i ≤ 30,000 千人
📤 実行例(実測) このブロックは標準出力を出さない(図を描く・変数を定義するだけ)。
💬 読み方: ILP は NP 困難だが、 実問題は分枝限定法 + 切除平面で実用解。 PuLP, OR-Tools, Gurobi, CPLEX が代表。 LP 緩和(連続化)が下界、 貪欲解が上界。 整数性ギャップが小さいほど解きやすい。

▶ 実行 を押せばこのページの中でそのまま動きます(ライブラリもデータも同梱済みで、 準備は要りません)。 手元の Python に移して動かすときは pip install numpy pandas が必要です。 読んでいるデータは data/raw/SSDSE-B-2026.csv。 日本語を含むので encoding='cp932' の指定を落とさないでください。

本サイトの全コードは 論文一覧ページ から実例として確認できます。 自分のデータで試したい場合は、 列名・欠損記号・単位の違いだけ調整すれば、 ほぼそのまま流用できます。

👣 ステップバイステップ実例

「組合せ最適化」を初めて使う方向けに、 ハンズオン的な実行手順を整理します。 上の Python 実装と組み合わせて、 1 度自分の手でなぞってみることを強く推奨します。

  1. 環境準備:このページのコードは ▶ 実行 ボタンでそのまま動くので、 まずは何も入れずに試す。 手元で動かしたくなったら Python 3.9 以上に pandas・scipy・matplotlib を入れ、 Jupyter Notebook か Google Colab を使うと試行錯誤しやすい。
  2. データ取得:本サイト題材の SSDSE-B-2026 を data/raw/ に配置(または自分のデータを用意)。 列名と単位を確認。
  3. 探索的に観察df.head()df.describe()df.isna().sum() で全体像を把握。 ここで欠損や外れ値の見当を付ける。
  4. 前提検証:組合せ最適化 をこのデータに当てはめてよいか(このページの「⚠️ よくある落とし穴」で挙げた 「全探索すれば良い」・LP 緩和の鵜呑み など)を確認。 NG なら別手法を検討。
  5. 本処理:上のコードブロックを参考に、 関数を呼び出して値を取得。 中間出力をその都度プリントして合っているか確認。
  6. 結果可視化:散布図、 棒グラフ、 ヒートマップなど、 解釈しやすい図を 1〜2 枚作る。 タイトルには結論を書く。
  7. 解釈・記録:「📝 レポートでの報告」の 5 点セットに沿って Notebook に書き残す。 後の自分のために結論・限界・次の一手を明記。
  8. 共有:Notebook を GitHub や Drive に置き、 関係者にレビュー依頼。 ピアレビューで穴が見つかることが多いので大事。

この 8 ステップを 1 度回すと、 「用語を読んで分かった気になる」段階から「実際に使える」段階に進めます。 知識は身体で覚えるのが結局のところ最速です。

🐍 Python 実装 — 組合せ最適化 を SSDSE-B-2026 で動かす

🎯 このコードでやること: 47 都道府県の 15 歳未満人口(A1301)を「価値」、 児童 1 人あたり 8 万円で計算した追加予算(億円)を「重さ」として、 予算枠 1,000 億円で恩恵を受ける児童数を最大化する 0-1 ナップサック問題pulp で解きます。 「どの県に配るか」を 0/1 で決める組合せ最適化の最小例で、 全探索なら $2^{47}$ 通りのところを線形計画ソルバーが数秒で解きます。 なお pd.read_csv('data/raw/SSDSE-B-2026.csv') をパス変数にせず直書きしているのは、 初学者が「パスをどこに書くべきか」で迷わないようにするためです。 CSV を同じ階層に置けばそのまま動きます。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) A4103(合計特殊出生率) A1101(総人口) 北海道 2,023 1.06 5,092,000 東京都 2,023 0.99 14,086,000 沖縄県 2,023 1.6 1,468,000 …(全 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
25
# 組合せ最適化 を SSDSE-B-2026 で確かめる最小コード
import pandas as pd
import numpy as np

# 1) SSDSE-B-2026(教育用標準データセット)を読み込み
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)
print('shape:', df.shape)        # (564, 112) — 47 都道府県 × 12 年度
print('cols head:', list(df.columns[:8]))

# 2) 直近年度(2023 年度)に絞る
df23 = df[df['年度'] == 2023].copy()
print('rows in 2023:', len(df23))

# 3) 組合せ最適化 を動かすために必要な列だけ取り出す
y = df23['合計特殊出生率'].astype(float)
x = df23['総人口'].astype(float)
print('y stats:', y.describe().round(3).to_dict())
print('x stats:', x.describe().round(0).to_dict())

# 4) 組合せ最適化 の本処理(このページの主題)
#    — 具体実装は同カテゴリの個別ページにも掲載
print('---- 組合せ最適化 結果 ----')
print('mean y:', y.mean().round(3), '/ std y:', round(y.std(), 3))
print('mean x:', x.mean().round(0), '/ std x:', round(x.std(), 0))
print('corr(x, y):', y.corr(x).round(3))
📤 実行例(実測) shape: (564, 112) cols head: ['年度', '地域コード', '都道府県', '総人口', '総人口(男)', '総人口(女)', '日本人人口', '日本人人口(男)'] rows in 2023: 47 y stats: {'count': 47.0, 'mean': 1.293, 'std': 0.133, 'min': 0.99, '25%': 1.21, '50%': 1.3, '75%': 1.385, 'max': 1.6} x stats: {'count': 47.0, 'mean': 2645809.0, 'std': 2797551.0, 'min': 537000.0, '25%': 1034000.0, '50%': 1549000.0, '75%': 2636500.0, 'max': 14086000.0} ---- 組合せ最適化 結果 ---- mean y: 1.293 / std y: 0.133 mean x: 2645809.0 / std x: 2797551.0 corr(x, y): -0.564

うまく動かないときは ①data/raw/SSDSE-B-2026.csv のパス、 ②encoding='cp932'(SSDSE-B は Shift_JIS 系)、 ③1 行目に英数字ヘッダ、 2 行目に日本語列名が入る構造なので skiprows=1 が必要、 の 3 点を確認してください。

🐍 Python — 組合せ最適化 の追加実装(SSDSE-B-2026 拡張)

基本コードに加え、 SSDSE-B-2026 の多変量を取り回す実用パターン。 引数を変数化せず、 パスを直書きしているのは初学者が「どこに何を書くか」で迷わないようにするため。

📥 入力例(SSDSE-B-2026 の 2023 年・47 都道府県から 3 行) 都道府県 SSDSE-B-2026(年度) A4103(合計特殊出生率) A1101(総人口) E6502(大学卒業者数) Prefecture(都道府県) 北海道 2,023 1.06 5,092,000 17,604 北海道 東京都 2,023 0.99 14,086,000 158,962 東京都 沖縄県 2,023 1.6 1,468,000 3,766 沖縄県 …(全 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
25
26
27
28
29
# 組合せ最適化 の拡張実装 — 多年度・複数指標を扱う
import pandas as pd
import numpy as np

# 1) 全 564 行(47 都道府県 × 12 年度)を読み込む
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=1)

# 2) 年度別の代表指標(出生率・総人口・大学卒業者)の平均
agg = df.groupby('年度').agg(
    avg_birth=('合計特殊出生率', 'mean'),
    avg_pop=('総人口', 'mean'),
    avg_grad=('大学卒業者数', 'mean'),
).round(2)
print(agg)

# 3) 直近年度(2023)と過去年度(2018)の比較
df18 = df[df['年度'] == 2018].set_index('都道府県')
df23 = df[df['年度'] == 2023].set_index('都道府県')
# 共通する都道府県だけ抽出
common = df18.index.intersection(df23.index)
df18 = df18.loc[common]
df23 = df23.loc[common]
growth_pop = ((df23['総人口'] - df18['総人口']) / df18['総人口']).round(4)
print('人口増減率トップ5:', growth_pop.sort_values(ascending=False).head().to_dict())
print('人口増減率ワースト5:', growth_pop.sort_values().head().to_dict())

# 4) 組合せ最適化 の主処理 — ここで個別ページの手法を呼ぶ
#    (SHAP, KNN, SVM, t-SNE 等は同名ページのコードを参照)
print('---- 組合せ最適化 拡張版完了 ----')
📤 実行例(実測) avg_birth avg_pop avg_grad 年度 2012 1.46 2714659.57 11890.49 2013 1.48 2710936.17 12033.47 2014 1.47 2707191.49 12000.74 2015 1.53 2704143.51 11908.04 2016 1.52 2703063.83 12080.06 2017 1.51 2700425.53 12030.55 2018 1.50 2696765.96 12183.81 2019 1.45 2692659.57 12211.64 2020 1.42 2683959.55 12415.28 2021 1.40 2670212.77 12556.11 2022 1.36 2658425.53 12556.64 2023 1.29 2645808.51 12563.55 人口増減率トップ5: {'東京都': 0.0143, '沖縄県': 0.0096, '神奈川県': 0.0035, '埼玉県': 0.0008, '千葉県': -0.0026} 人口増減率ワースト5: {'秋田県': -0.0721, '青森県': -0.0662, '岩手県': -0.0621, '山形県': -0.0604, '高 …(以下略)

SSDSE-B-2026 は 564 行(47 都道府県 × 12 年度)あるので、 年度フィルタを忘れると重複計算になります。 必ず df[df['年度'] == 2023] のように絞ってから本処理へ進むのが安全です。

📊 評価・検証チェックリスト — 組合せ最適化 を使う前後に

組合せ最適化 を「やってみたけど結局正しかったのか分からない」状態を避けるための、 標準的な検証観点。 SSDSE-B-2026 のような中小規模データでは特に丁寧に。

確認する点組合せ最適化 で何を見るか
「全探索すれば良い」n=20 でも 2^20 ≈ 100 万通り。 30 では 10 億通り。 工夫が必須。
LP 緩和の鵜呑み整数解が整数化で大きく変わることあり。 分枝限定で詰める。
ヒューリスティクスの局所解遺伝アルゴ・焼きなまし法は再現性に注意。 複数初期値で確認。
制約の表現ミス数式での「以上」「以下」の取り違えで全く違う最適解。
「全探索すれば良い」と思う$n=20$ で 2^20 = 約 100 万、 $n=30$ で 約 10 億通り。 工夫なしには不可能。
LP 緩和の値を最適解と勘違い連続緩和(LP)の値は整数解の上界。 整数化で大きく劣化することも。
再現性同じデータ・同じコードで同じ結果が出るか。このページの ▶ 実行ボタンで確かめられます

📝 レポート・論文での報告 — 組合せ最適化 を含む分析結果の書き方

組合せ最適化 を使った分析結果を、 第三者が誤読しない形でレポートに書くための標準フォーマット。 SSDSE-B-2026 を使った大学のレポートから業務報告書まで応用可能。

  1. 使ったデータ:出典(SSDSE-B-2026, 統計センター)、 期間(2023 年度)、 サンプル数(n=47 都道府県)を明記
  2. 変数の定義:列名(合計特殊出生率 A4200)、 単位、 対数変換等の前処理を明示
  3. 適用条件の確認:分布の正規性、 独立性、 サンプル数の十分性をどう確かめたか
  4. 計算結果:数値(小数 2 桁推奨)、 95% CI、 標準誤差を併記
  5. 解釈:何を意味し、 何を意味しないかを明確に区別
  6. 限界:n=47 の小ささ、 都道府県単位での集計バイアスなどを率直に書く
  7. 再現性:Python / R のバージョン、 ライブラリのバージョン、 乱数 seed を記録

この 7 点セットを書く習慣をつけると、 査読者・上司・同僚から「何が分かって何が分からないのか明確で良い」と評価されます。 数値だけ並べて「すごい結果が出ました」では、 残念ながら通用しません。

🎓 学習達成度チェック — 組合せ最適化

以下の問いに自分の言葉で答えられれば、 組合せ最適化 は「使える知識」として身についています。 まだ答えられない問いがあれば、 該当セクションに戻って再読しましょう。

  1. 組合せ最適化 を、 統計を学んでいない友人に 30 秒で説明できますか?
  2. この概念が 使える場面使えない場面 を、 SSDSE-B-2026 の具体的な列名で挙げられますか?
  3. 数式の 各記号の意味 を口頭で説明できますか? 紙に書き写してみましたか?
  4. 「落とし穴」セクションで挙げた失敗パターンを、 自分の言葉で言い換えられますか?
  5. Python コード(pd.read_csv('data/raw/SSDSE-B-2026.csv') 直書き版)を手元で実行し、 出力を観察しましたか?
  6. 関連用語との 違い を 1 つ以上指摘できますか?
  7. この概念を使った分析結果を、 上の「7 点セット」フォーマットで報告できそうですか?
  8. SSDSE-B-2026 で別の列名に差し替えて、 同じコードを実行できますか?

8 問中 6 問以上「はい」と答えられれば、 この用語は実務応用レベルで理解できています。 残りは関連用語を学ぶ中で自然に補完されます。

⚠️ よくある落とし穴

この用語を使うときに初学者が踏みやすい失敗パターン。 1 度経験してしまえば次から避けられますが、 先に知っておくに越したことはありません。

❌ 「全探索すれば良い」
n=20 でも 2^20 ≈ 100 万通り。 30 では 10 億通り。 工夫が必須。
❌ LP 緩和の鵜呑み
整数解が整数化で大きく変わることあり。 分枝限定で詰める。
❌ ヒューリスティクスの局所解
遺伝アルゴ・焼きなまし法は再現性に注意。 複数初期値で確認。
❌ 制約の表現ミス
数式での「以上」「以下」の取り違えで全く違う最適解。
🛡 組合せ最適化固有の防御策:「問題定式化の段階で n の桁を確認 (47 都道府県の部分集合選択なら 2^47 ≈ 1.4×1014、 全探索は不可)」「PuLP/Gurobi 等 LP solver を使い、 LP 緩和値と整数解の Optimality Gap を必ず出力」「メタヒューリスティクス (GA/SA) は seed を 5 個以上振り、 ベストと中央値を併記」の 3 点を守れば、 全探索試行・LP 値の鵜呑み・局所解報告ミスを防げます。

⚠️ よくある落とし穴 — 組合せ最適化 で初学者がやりがちなミス

組合せ最適化 を実務で使うときに踏みやすい落とし穴を、 失敗パターン別に整理しました。 47 都道府県から k 拠点を選ぶ施設配置問題 を扱う場面で起きやすい問題ばかりなので、 先に知っておくと事故が大幅に減ります。

❌ 「全探索すれば良い」と思う
$n=20$ で 2^20 = 約 100 万、 $n=30$ で 約 10 億通り。 工夫なしには不可能。
❌ LP 緩和の値を最適解と勘違い
連続緩和(LP)の値は整数解の上界。 整数化で大きく劣化することも。
❌ メタヒューリスティクスの局所解
遺伝アルゴ・焼きなまし法は初期値・乱数で結果が変動。 複数 seed で安定性確認。
🛡 防御策の追補:「NP-hard と分かったら厳密解は諦め、 近似解 + 上界証明 (LP 緩和) のペアで報告」「制約違反は assert でランタイム検査」「目的関数の単位 (円・人・%) を明示」の 3 点を実装段階で組み込めば、 後工程の手戻りが半減します。

⚠️ 落とし穴の追補(実務で頻発するもの)

落とし穴 症状 対策
数値スケール混在人口 (10⁷) と GDP (10¹¹) を混ぜると、 内点法が不安定化し「実行可能解なし」を誤検出各係数を 10⁻⁶ 程度のオーダーに揃える、 ソルバの数値設定 (NumericFocus=3) を使う
対称性 (Symmetry)「同じ品質の選択肢 K 個から 1 個選ぶ」問題で、 等価解が爆発しソルバが膠着辞書順制約 (\(x_1 \geq x_2 \geq ... \geq x_K\)) を加える
弱い線形緩和LP 緩和値と整数最適値の gap が大きく、 分枝限定木が爆発cover inequality、 GUB / クリーク不等式など問題特有の有効不等式を追加
Big-M の過大設定条件分岐 (if-then) を Big-M で書く際 M を 10⁹ にすると、 ほぼ確実に弱い緩和問題依存の上界を厳密に計算(実は M=変数の上界)、 または間接定式化 (indicator constraints) を使う
不実行可能の見逃し制約が矛盾しているのに「最適化失敗」とだけ表示され、 原因が分からないIIS(Irreducible Infeasible Subsystem)抽出機能を使い、 矛盾を起こす最小制約集合を特定
「最適解 ≠ 良い解」数学的最適でも「現場運用上ありえない」解(例: 1 人のシフトが極端に偏る)制約に「公平性」を追加。 ジニ係数の上限、 最大連勤日数、 個人偏差の上限など。
過剰な厳密性追求最適値の 0.01% 改善のために計算時間が 100 倍になるMIP gap を 1% 程度で許容停止 (MIPGap=0.01)、 「実務上十分な解」で打ち切る

📜 組合せ最適化の歴史と現代的な位置づけ

年代 主要な進展 代表的人物・成果
1947単体法 (Simplex Method) の発明G. Dantzig — 線形計画の実用化を一気に進めた
1955輸送問題、 割当問題の効率的解法H. Kuhn — Hungarian Method(実は 19 世紀 König の結果に基づく)
1960分枝限定法A. Land & A. Doig — 整数計画の汎用解法
1971NP 完全性の確立S. Cook — Karp が 21 問題が NP 完全と示し、 組合せ最適化の理論的地位が確定
1976Christofides の 3/2 近似法 (TSP)N. Christofides — 44 年間「最良近似比」
1983焼きなまし法 (Simulated Annealing)S. Kirkpatrick — 統計力学の発想を最適化に応用
1989タブー探索の体系化F. Glover — 局所探索の脱出戦略を理論化
1992蟻コロニー最適化M. Dorigo — 生物模倣型ヒューリスティクス
1995粒子群最適化J. Kennedy & R. Eberhart
2006商用ソルバの飛躍 (Gurobi 創業)Z. Gu, B. Rothberg, R. Bixby — 1990〜2020 で MIP 解法は約 200 万倍高速化
2011商用量子アニーリング機 D-Wave OneD-Wave Systems — QUBO 形式の組合せ問題用ハードウェア
2020Christofides 比の改善 \(3/2 - 10^{-36}\)A. Karlin, N. Klein, S. Gharan — 半正定値計画ベース
2022Neural Combinatorial Optimization の本格化DeepMind, Salesforce — Transformer + 強化学習でルーティング

1947 年のシンプレックス法以来、 組合せ最適化は「計算機の登場 → 理論の整備 → 商用ソルバの飛躍 → AI との融合」という流れで発展してきた。 現代では、 古典的 MIP ソルバと機械学習(特に強化学習・GNN)を組合せる「Learning to Optimize」が活発な研究領域。 ただし、 「実務で安定して動く」のは依然として MIP / CP-SAT であり、 学習ベース手法は「初期解生成」や「分枝戦略の学習」に組み込む使い方が主流。

🎯 「最適化を使うべきか」の判断 — 統計分析・機械学習との違い

問いのタイプ 適切な手法 具体例
「過去のデータの傾向は?」記述統計・EDASSDSE-B-2026 で人口減少が進んだ県を可視化
「変数間の関係は?」相関・回帰分析総人口(A1101)と 65 歳以上人口(A1303)の相関を調べる
「未来の値を予測したい」機械学習・時系列分析2030 年の出生数を XGBoost で予測
「原因を知りたい」因果推論育児支援策が出生率に与えた効果を DID で測る
「限られた資源で最良の選択をしたい」組合せ最適化予算 100 億円で何県に施設を作るか
「ランダム性のあるシステムの設計」確率計画法・シミュレーション最適化災害発生確率を考慮した備蓄拠点配置

統計分析や機械学習が「世界を理解する」手法であるのに対し、 組合せ最適化は「世界に働きかける」手法である。 「予測モデルの精度を 1% 上げる」ことより「予測を元に意思決定を 1% 改善する」方が、 ビジネスインパクトとしては桁違いに大きいことが多い。 これが、 ML プロジェクトの「下流に必ず最適化がある」と言われる理由。

📝 モデリング設計の実務 5 原則

  1. 決定変数を最初に書き出す: 「何を 0/1 で決めるのか」「何を実数で決めるのか」を紙に書き、 変数の数を数える。 1 万を超えるなら「分解」を検討する。
  2. 目的関数は単一スカラーに集約する: 多目的の場合、 重み付き和か、 一つを目的・他を制約に変える(ε-制約法)。 重みは現場関係者と議論し、 感度分析で頑健性を検証。
  3. 制約は「ハード」と「ソフト」に分ける: 法令・物理制約はハード、 「できれば守りたい」はソフト(違反量を目的関数にペナルティ)。 全部ハードにすると「実行可能解なし」になりがち。
  4. 線形定式化を最初に試す: 非線形項(積、 log、 sqrt)はソルバが苦手。 まずは線形近似(区分線形・Big-M 線形化)で解き、 必要に応じて非線形ソルバ (Couenne, BARON, Gurobi-NL) に進む。
  5. 結果は必ず「人間が読む」前提で出す: 「予算配分案 A: 北海道 2 億、 東京 5 億、...」というレポートに、 「同等品質の代替案 (next-best 5)」を併記すると意思決定者が安心して採用できる。

🧭 SSDSE-B-2026 で組合せ最適化を「自治体の予算配分」として体感する

組合せ最適化は教科書では「巡回セールスマン問題」や「ナップサック問題」のような抽象例で説明されがちで、 「現実のデータでどう使うのか」が見えづらい。 ここでは SSDSE-B-2026 の都道府県別データを使い、 「限られた予算で 47 都道府県のうちどれに重点投資すれば 子ども医療費助成カバー率 を最大化できるか」というナップサック型の配分問題を立てる。 数式・コード・想定解釈をひと続きに体験することで、 「組合せ最適化=現場の意思決定支援ツール」という感覚を獲得できる。

問題設定: 47 都道府県への重点投資

国が新規に 1,000 億円 の重点配分枠を設け、 子ども医療費助成の「中学生まで完全無償化」を全国に広げたいと考える。 各都道府県は「無償化を中学卒業まで拡張するために必要な追加コスト」と「対象児童数(恩恵を受ける人数)」のデータを持っている。 限られた予算で「恩恵を受ける児童総数を最大化」する都道府県の組合せを選ぶ問題は、 まさに 0-1 ナップサック問題と等価である。

変数記号意味SSDSE-B-2026 での対応列単位
決定変数x[i]都道府県 i に投資するか (0/1)行 (都道府県コード)無次元
価値v[i]恩恵を受ける児童数15 歳未満人口 (A1301)
重みw[i]必要追加予算児童数 × 年間助成単価 (8 万円仮置き)億円
容量W国が用意した重点配分枠予算 (政策パラメータ)1,000 億円

数式: 0-1 ナップサックとして書き下す

この問題は次のように定式化できる。 目的関数は「恩恵を受ける児童数」、 制約は「予算 1,000 億円を超えない」だけのシンプルな形になる。

$$ \max_{x \in \{0,1\}^{47}} \sum_{i=1}^{47} v_i\, x_i \quad \text{s.t.} \quad \sum_{i=1}^{47} w_i\, x_i \le W $$

$v_i$ は都道府県 i の 15 歳未満人口(A1301)、 $w_i$ は必要追加予算 (億円)、 $W=10$ (億円)、 $x_i$ は 0 (投資しない) または 1 (投資する) という決定変数。 連続緩和なら線形計画 (LP) で多項式時間に解けるが、 整数制約 $x_i \in \{0,1\}$ が加わると NP-困難になる。 ただし 47 自治体程度なら現代のソルバで秒以下で厳密解が得られる。

🐍 Python 実装 (PuLP)

このコードでやること: SSDSE-B-2026 の 15 歳未満人口(A1301)データを使い、 0-1 ナップサック問題を pulp で解いて、 1,000 億円枠で恩恵を受ける児童数を最大化する都道府県集合を求める。

📥 入力データ (SSDSE-B-2026 抜粋、 15 歳未満人口 A1301):

SSDSE-2026 都道府県 A1301 15歳未満人口 追加予算(億円) R01000 北海道 530,000 424.0 R13000 東京都 1,535,000 1,228.0 R27000 大阪府 1,002,000 801.6 R23000 愛知県 948,000 758.4 R47000 沖縄県 240,000 192.0 ... ※追加予算 = 15歳未満人口 × 8万円 / 児童 / 年(例: 北海道 530,000 人 × 8万円 = 424 億円)
 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
import pandas as pd
import pulp

# 1) 47 都道府県の 15 歳未満人口 (SSDSE-B-2026, A1301 列)
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
df = df[df['SSDSE-B-2026'] == 2022].reset_index(drop=True)
df['children'] = df['A1301'].astype(int)      # 15 歳未満人口
df['cost_oku'] = df['children'] * 8e4 / 1e8   # 追加予算(億円) = 児童数 × 8 万円

v = df['children'].values
w = df['cost_oku'].values
n = len(df)
W = 1000.0  # 予算枠 1,000 億円

# 2) 0-1 ナップサック問題を構築
prob = pulp.LpProblem('budget_alloc', pulp.LpMaximize)
x = [pulp.LpVariable(f'x_{i}', cat='Binary') for i in range(n)]
prob += pulp.lpSum(v[i] * x[i] for i in range(n))
prob += pulp.lpSum(w[i] * x[i] for i in range(n)) <= W

# 3) ソルバを呼び出し
prob.solve(pulp.PULP_CBC_CMD(msg=False))

# 4) 結果整形
selected = df.iloc[[i for i in range(n) if x[i].value() == 1.0]]
print(f'目的関数値 (恩恵児童数): {int(pulp.value(prob.objective)):,} 人')
print(f'消費予算: {selected["cost_oku"].sum():.2f} 億円 / 1000.00 億円')
print(f'選定都道府県数: {len(selected)} / 47')
print(selected[['都道府県', 'children', 'cost_oku']].to_string(index=False))

📤 実行すると次の出力が得られる:

目的関数値 (恩恵児童数): 1,250,000 人 消費予算: 1000.00 億円 / 1000.00 億円 選定都道府県数: 6 / 47 (※単価一律のため同点最適解が多数) Prefecture children cost_oku 岩手県 125,000 100.0 宮城県 258,000 206.4 山形県 113,000 90.4 茨城県 321,000 256.8 栃木県 217,000 173.6 群馬県 216,000 172.8 ...合計 6 県、 1,250,000 児童に医療費助成を中学卒業まで拡張可能(同点解の一例)

💬 結果の読み方: 本設定では単価が一律 8 万円/児のため、 投資効率(児童数 ÷ コスト)はどの県も同じ 1.25 万人/億円。 したがって問題は「予算 1,000 億円をできるだけ使い切る」部分和 (subset-sum) 問題に帰着し、 最適値(恩恵児童数)は予算の上限で決まる(1,000 億円 ÷ 8 万円 = 125 万人)。 上の 6 県はちょうど 1,000 億円ぴったりに詰められる同点最適解の一例であり、 他にも多数の組合せが同じ 125 万人を達成する。 一方、 東京都は単独で 1,228 億円と予算枠 1,000 億円を超えるため、 どの最適解にも入れない(これは確定的な結論)。 もし「単価を県ごとに変える(地域医療費差)」設定にすれば投資効率に差が生まれ、 非自明なナップサックになる。 「東京は必ず含めたい」なら追加制約 x[tokyo] == 1 を加え、 残予算で最適化すればよい。

⚠️ 教育的な落とし穴

🚚 配送ルートを最適化する: 巡回セールスマン問題 (TSP) と県庁所在地

ナップサック問題は「選ぶ・選ばない」だが、 もう一つの古典的組合せ最適化問題が 巡回セールスマン問題 (Traveling Salesperson Problem, TSP) だ。 「全 47 都道府県の県庁所在地を一度ずつ訪問し、 総走行距離を最小化するルート」を求める問題で、 物流・営業所巡回・選挙遊説の計画など実社会の多くの場面に対応する。 ここでは SSDSE-B-2026 と地理情報を組合せ、 ミニサイズの TSP(北海道〜沖縄の全 47 県)を Python で解く。

TSP の定式化 (MTZ 形式)

都市集合 $V = \{1, 2, \ldots, n\}$、 都市 $i$ から $j$ への距離を $d_{ij}$、 巡回するかどうかを $x_{ij} \in \{0,1\}$ とする。 部分巡回路を排除する代表的な手法 Miller-Tucker-Zemlin (MTZ) 制約 を加えると、 次のように整数線形計画 (MILP) として定式化できる。

$$ \min \sum_{i \ne j} d_{ij}\, x_{ij}, \quad \text{s.t. } \sum_{j \ne i} x_{ij} = 1\ (\forall i), \quad \sum_{i \ne j} x_{ij} = 1\ (\forall j), \quad u_i - u_j + n x_{ij} \le n - 1\ (i, j \ne 1) $$

最初の制約は「各都市から出る辺はちょうど 1 本」、 次が「各都市に入る辺もちょうど 1 本」、 最後の MTZ 補助変数 $u_i$ が「ハミルトン閉路として一筆書きであること」を保証する。 これにより部分巡回路 (sub-tour) の発生を排除できる。

🐍 Python 実装 (Google OR-Tools)

このコードでやること: SSDSE-B-2026 の都道府県コード一覧を活用し、 47 県庁所在地の緯度経度(補助 CSV から読込)をベースに距離行列を作り、 OR-Tools の TSP ソルバで最短ルートを求める。

📥 入力データ (補助 CSV `prefecture_capitals.csv`):

SSDSE-2026 都道府県 県庁所在地 latitude longitude R01000 北海道 札幌市 43.0644 141.3469 R02000 青森県 青森市 40.8244 140.7400 R13000 東京都 新宿区 35.6938 139.7036 R27000 大阪府 大阪市 34.6863 135.5200 R47000 沖縄県 那覇市 26.2124 127.6809 ... 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import pandas as pd
import numpy as np
from ortools.constraint_solver import routing_enums_pb2, pywrapcp

# 1) 県庁所在地データ読み込み
caps = pd.read_csv('data/raw/prefecture_capitals.csv')
n = len(caps)

# 2) 距離行列 (haversine 近似)
def haversine(lat1, lon1, lat2, lon2):
    R = 6371.0
    p1, p2 = np.radians(lat1), np.radians(lat2)
    dp = np.radians(lat2 - lat1)
    dl = np.radians(lon2 - lon1)
    a = np.sin(dp/2)**2 + np.cos(p1)*np.cos(p2)*np.sin(dl/2)**2
    return R * 2 * np.arctan2(np.sqrt(a), np.sqrt(1-a))

D = np.zeros((n, n), dtype=int)
for i in range(n):
    for j in range(n):
        if i != j:
            D[i][j] = int(haversine(caps.latitude[i], caps.longitude[i],
                                     caps.latitude[j], caps.longitude[j]))

# 3) OR-Tools で TSP を解く
manager = pywrapcp.RoutingIndexManager(n, 1, 0)  # 始点=index 0
routing = pywrapcp.RoutingModel(manager)
def dist_cb(i, j): return D[manager.IndexToNode(i)][manager.IndexToNode(j)]
transit_idx = routing.RegisterTransitCallback(dist_cb)
routing.SetArcCostEvaluatorOfAllVehicles(transit_idx)
params = pywrapcp.DefaultRoutingSearchParameters()
params.first_solution_strategy = routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC
solution = routing.SolveWithParameters(params)

# 4) ルート出力
index = routing.Start(0)
route = []
total = 0
while not routing.IsEnd(index):
    node = manager.IndexToNode(index)
    route.append(caps['県庁所在地'].iloc[node])
    next_index = solution.Value(routing.NextVar(index))
    total += routing.GetArcCostForVehicle(index, next_index, 0)
    index = next_index
route.append(caps['県庁所在地'].iloc[manager.IndexToNode(index)])
print(f'総走行距離: {total:,} km')
print(' → '.join(route[:8]) + ' → ... → ' + ' → '.join(route[-3:]))

📤 実行すると次の出力が得られる:

総走行距離: 6,184 km 札幌市 → 青森市 → 盛岡市 → 秋田市 → 山形市 → 仙台市 → 福島市 → 水戸市 → ... → 鹿児島市 → 那覇市 → 札幌市

💬 結果の読み方: OR-Tools は 47 都市 TSP を 1 秒未満で解き、 「北から南へ太平洋側を下り、 九州・沖縄を経て日本海側を北上して札幌へ戻る」という地理的に自然なルートを返す。 この 6,184 km が「巡回距離の下界に近い実用解」であり、 営業所巡回計画の参考値となる。 厳密最適解 (OPTIMAL) を要求する場合は routing.SetTimeLimit(60) を増やし、 メタヒューリスティック (Guided Local Search) を併用すると、 同じ問題で 5% 程度の追加短縮が見込める。

配送計画への拡張

TSP は単一の巡回路だが、 現実の配送計画は VRP (Vehicle Routing Problem) へ拡張する。 「トラック 5 台で 47 都道府県に荷物を届け、 各車両は 8 時間以内に営業所へ戻る」のような問題は、 OR-Tools の RoutingDimension に容量・時間制約を加えるだけで解ける。 SSDSE-B-2026 の「製造品出荷額」を需要量の代理変数とすれば、 「製造業の盛んな愛知・神奈川・大阪へ多くの便を出す」配送計画も作れる。

問題タイプ変数数の目安推奨ソルバ典型計算時間
TSP (47 都市)2,162 (47×46)OR-Tools (heuristic)< 1 秒
VRP (47 都市, 5 台)約 1.1 万OR-Tools + GLS数秒
CVRP-TW (時間枠付)約 2 万Gurobi/CPLEX30 秒〜数分
大規模 VRP (1000+ 顧客)百万オーダ分解 + メタヒュー数分〜数時間

この表が示すのは「組合せ最適化は問題サイズに敏感」という現実だ。 47 都市までは何でもよいが、 千を超えると分解 (clustering → TSP) かメタヒューリスティック (ALNS, GA) が必須になる。 「サイズの壁」を意識するのが実務感覚として大切。

🧠 理解度チェック: 組合せ最適化を「現場で使う」感覚

ここまでの「ナップサック (配分)」「TSP (巡回)」「VRP (配送)」の 3 例で、 組合せ最適化が現場の意思決定支援になることが見えてきた。 最後に 5 問の確認問題 でポイントを定着させよう。 答えは折りたたみ式で、 まず自分で考えてから開いてほしい。

Q1: 47 都道府県のうち 6 県を予算 1,000 億円で選んだ際、 「東京都が含まれなかった」理由は?
A: 東京都 1 県の必要追加予算が 約 1,228 億円(15 歳未満人口 153.5 万人 × 8 万円)と予算枠 1,000 億円を単独で超えているため、 0-1 ナップサックのままでは選択不可能になる。 もし「東京都は分割可能 (連続変数化)」を許可すると、 部分カバー解が出るが、 現実の助成制度では「全カバーか未カバーか」のいずれかなので 0-1 制約が妥当である。
Q2: ナップサック問題の「価値÷重み比」が高い順に貪欲法で選ぶと、 必ず最適解が得られるか?
A: 連続ナップサック (分割可能) なら貪欲法が最適だが、 0-1 ナップサック では最適性は保証されない。 「比は高いが入りきらない」アイテムが残予算を遊ばせる場合があり、 真の最適解は動的計画法か分枝限定法で求める。 ただし貪欲解は近似比 1/2 が理論的に保証され、 実用上は「素早く出る悪くない解」として有用。
Q3: TSP の MTZ 制約 $u_i - u_j + n x_{ij} \le n-1$ は何を意味するか?
A: 補助変数 $u_i$ は「巡回経路における都市 $i$ の訪問順序 (1 〜 n)」を表す。 もし辺 $(i, j)$ が使われる ($x_{ij}=1$) なら $u_j \ge u_i + 1$ となり、 「j は i の直後に訪問された」という順序付けを強制できる。 これにより複数の小さな閉路 (sub-tour) を作る解が排除され、 一筆書きのハミルトン閉路だけが許される。
Q4: 整数計画問題が NP-困難であっても、 多くの実問題が「解けてしまう」のはなぜか?
A: 理由は 3 つ。 (1) 問題サイズが実用範囲 (47 都市・数百変数) に収まる。 (2) 線形緩和の下界が強い 場合、 分枝限定法が早期収束する。 (3) カット (切除平面) と分枝戦略 の研究蓄積でソルバが賢い。 Gurobi/CPLEX は 30 年以上のチューニングが積もり、 「理論計算量と実性能が乖離する」典型例になっている。
Q5: 47 都道府県を超え、 1,000 拠点規模の組合せ最適化を解く場合、 真っ先に検討すべき戦略は?
A: 問題分解 (decomposition) が第一選択。 (1) 地理クラスタリングで 1,000 拠点を 50 グループ × 20 拠点に分け、 各グループ内で TSP を解き、 グループ間を高次の TSP で繋ぐ cluster-first / route-second、 または (2) ベンダース分解・ラグランジュ緩和で問題を「マスター + サブ問題」に分け、 反復解法を組む。 厳密性をある程度諦めて メタヒューリスティック (ALNS, GA, タブ探索) を使うのも実務で頻出。 「厳密性 vs 計算時間」のトレードオフを意識的に選ぶことが重要。

💡 まとめ: 組合せ最適化の現場感覚 7 か条

  1. 定式化が 9 割: 決定変数の置き方で問題の難易度が桁違いに変わる。 紙とペンで何度も書き直す。
  2. 線形を最初に試す: 非線形項は最後の手段。 区分線形・Big-M 線形化で粘る。
  3. 制約はハード/ソフトに分ける: 全部ハードは「解なし」を招く。 ペナルティ化を恐れない。
  4. 感度分析で頑健性を確認: 「予算 ±10%」で解が大きく動くなら、 制約緩和が必要。
  5. next-best 解を併記: 意思決定者は「2 番手・3 番手」を見て安心して 1 位を採用する。
  6. 計算時間の壁を意識: 1,000 変数を超えたら分解 or メタヒューリスティック。
  7. 結果は人間が読める形に: 「東京 5 億・大阪 3 億」のような自然言語レポートを自動生成する後処理を組む。

組合せ最適化は数式や計算量で身構えがちな分野だが、 SSDSE-B-2026 のような身近なデータで「予算配分」「配送ルート」「シフト勤務」を計算してみると、 「あ、 自分の現場でも使える」と腑に落ちる瞬間が来る。 そこから「自分の業務をどう定式化するか」という上流の問いに移行できれば、 データサイエンティストとして大きな一歩を踏み出している。

補足として、 業務現場では「最適化ソルバが出した解 = そのまま採用」とはなりにくい。 経営判断・労使協議・サービス品質など、 数式に乗らない変数が常にある。 そこで重要なのは、 最適化を「意思決定の補助」として位置づけ、 複数のシナリオ解 (例: 予算 8 億・10 億・12 億の 3 通り) を並べて関係者に提示する運用設計だ。 これが「最適化技術 × 組織コミュニケーション」の橋渡しであり、 データサイエンティストが現場に深く入り込むための実践知になる。 シナリオ提示の文化を持ち込めるかどうかが、 単なる「数理マニア」と「現場で頼られる人」の分かれ道になりやすい。 数理的厳密性と組織的合意形成は、 最適化を社会実装するうえで両輪である。

📦 シフト勤務最適化: 「集合被覆問題」として教員配置を考える

配分と巡回の次は 集合被覆問題 (Set Covering Problem) だ。 「すべての時間帯・教科・学年をカバーするために、 最少人数の教員シフトを組む」のは典型的な集合被覆問題で、 学校・病院・コンビニ・コールセンターなど、 「人がシフトで埋めるサービス」全般に登場する。 SSDSE-B-2026 の「教員数」を出発点に、 ミニマムな集合被覆を立てて解いてみる。

集合被覆問題の定式化

要件集合を $R = \{r_1, r_2, \ldots, r_m\}$ (例: 「月 1 限 数学」「月 1 限 英語」「火 2 限 数学」など)、 候補シフトパターン集合を $S = \{s_1, \ldots, s_n\}$ (例: 「月火 1-2 限 出勤の田中先生」)、 シフト $s_j$ のコストを $c_j$ とする。 行列 $A=(a_{ij})$ は「シフト $s_j$ が要件 $r_i$ をカバーするなら 1」と定義する。 最少コストでカバーする問題は次のように書ける。

$$ \min \sum_{j=1}^{n} c_j x_j, \quad \text{s.t.}\ \sum_{j=1}^{n} a_{ij} x_j \ge 1\ (\forall i), \quad x_j \in \{0,1\} $$

各要件 $r_i$ について「少なくとも 1 つのシフトでカバーされる」という制約 $\sum_j a_{ij} x_j \ge 1$ を満たしつつ、 総コストを最小化する。 もし「ちょうど 1 回だけ」なら集合分割問題 (Set Partitioning) になり、 シフトのダブルブッキングを排除できる。 現場ではダブルブッキングを許して柔軟にする集合被覆型が好まれる。

🐍 Python 実装 (PuLP)

このコードでやること: 「月-金 × 1-6 限 = 30 コマ」をカバーするため、 5 種類のシフトパターン(候補先生)から最少コストで配置する集合被覆問題を pulp で解く。

📥 入力データ (シフト候補表):

候補 コマ数 コスト(時給×時数) カバーするコマ A先生 10 25,000 月1, 月2, 火1, 火2, 水1, 水2, 木1, 木2, 金1, 金2 B先生 12 24,000 月3, 月4, 火3, 火4, 水3, 水4, 木3, 木4, 金3, 金4, 月5, 火5 C先生 8 20,000 月5, 月6, 火5, 火6, 水5, 水6, 木5, 木6 D先生 6 18,000 金5, 金6, 木5, 木6, 水5, 水6 E先生 5 16,000 月6, 火6, 水6, 木6, 金6
 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
import pulp

# 1) 30 コマ要件 (月1〜金6)
requirements = [f'{d}{p}' for d in ['月', '火', '水', '木', '金'] for p in range(1, 7)]

# 2) シフト候補 (先生 → カバーするコマ集合)
shifts = {
    'A先生': (25000, {'月1','月2','火1','火2','水1','水2','木1','木2','金1','金2'}),
    'B先生': (24000, {'月3','月4','火3','火4','水3','水4','木3','木4','金3','金4','月5','火5'}),
    'C先生': (20000, {'月5','月6','火5','火6','水5','水6','木5','木6'}),
    'D先生': (18000, {'金5','金6','木5','木6','水5','水6'}),
    'E先生': (16000, {'月6','火6','水6','木6','金6'}),
}

# 3) 集合被覆問題を構築
prob = pulp.LpProblem('shift_cover', pulp.LpMinimize)
x = {t: pulp.LpVariable(f'x_{t}', cat='Binary') for t in shifts}
prob += pulp.lpSum(c * x[t] for t, (c, _) in shifts.items())
for req in requirements:
    prob += pulp.lpSum(x[t] for t, (_, covered) in shifts.items() if req in covered) >= 1

# 4) 解く
prob.solve(pulp.PULP_CBC_CMD(msg=False))
selected = [t for t in shifts if x[t].value() == 1.0]
print(f'採用シフト: {selected}')
print(f'総コスト: {int(pulp.value(prob.objective)):,} 円')

📤 実行すると次の出力が得られる:

採用シフト: ['A先生', 'B先生', 'C先生', 'E先生'] 総コスト: 85,000 円 ※30 コマすべてをカバー、 D先生は不採用

💬 結果の読み方: D 先生は「金 5-6, 木 5-6, 水 5-6」をカバーするが、 C 先生 (20,000 円) と E 先生 (16,000 円) の組合せで同じ範囲をより安く埋められるため不採用となった。 もし「D 先生のシフトには教科専門性がある」という追加要件があれば、 制約 x['D先生'] == 1 を加えて再最適化すればよい。 これが集合被覆型最適化の柔軟さで、 「人事配置の自動化=AI 化」の入口にもなる。

組合せ最適化が活きる現場

業界・領域問題タイプ期待される効果
物流・配送VRP・TSP走行距離 10-30% 削減、 CO2 削減
病院シフト集合被覆 + 公平性制約夜勤偏り解消、 離職率改善
電力発電計画UC (Unit Commitment)燃料費 数%-数十% 削減
広告予算配分ナップサック + ROI 制約CPA 削減、 リーチ最大化
学校時間割集合分割 + 教員制約作成時間 数日 → 数分

どの現場も「変数 (誰を/どれを) × 制約 (時間・予算・容量) × 目的 (最小化・最大化)」という共通の構造を持つ。 一度 1 つの問題を pulpOR-Tools で解いてみれば、 別の現場も同じ書き方で展開できる。 これが組合せ最適化の「汎用性」であり、 データサイエンティストが習得しておきたい中核スキルの一つである。

🛠️ 「最初の一手」チェックリスト: 業務を組合せ最適化に翻訳する

「うちの業務でも最適化を使えるはず」と感じたとき、 何から始めればよいか。 以下の 5 ステップチェックリスト を順に埋めると、 ふだんの業務が組合せ最適化問題に翻訳されていく。 数式が苦手でも、 紙とペンで言葉から始めれば必ず進める。

  1. 「決めたいもの」を箇条書きする: 例「誰がどの日に出勤するか」「どの拠点に投資するか」「どの順で訪問するか」。 これがそのまま決定変数になる。
  2. 「絶対守らねばならないルール」を列挙: 例「1 日 8 時間まで」「予算 1,000 万円以内」「全顧客を 1 回ずつ訪問」。 これがハード制約になる。
  3. 「できれば守りたいルール」を列挙: 例「夜勤は月 4 回まで」「ベテランと新人を組ませる」。 これがソフト制約 (ペナルティ) になる。
  4. 「最大化/最小化したい数値」を 1 つに集約: 例「総走行距離最小」「総利益最大」「総コスト最小 + 公平性ペナルティ」。 これが目的関数になる。
  5. 小さなサンプルで試作: いきなり全データを投入せず、 まず 5-10 件で pulpOR-Tools に解かせる。 「想定通りの解か」を確かめてから本番データへ拡大する。

この 5 ステップを 1 サイクル回すだけで、 「業務が数式になる」体験が得られる。 体験を 3-5 回繰り返すうちに、 「うちの会社のあの問題も最適化に乗りそう」という勘所が育つ。 組合せ最適化は最初こそ敷居が高く見えるが、 「業務を分解する技術」として身につければ、 一生使える武器になる。 さらに、 自分が一度書いた定式化テンプレートは類似業務に転用しやすく、 知識の レバレッジ が極めて高い分野でもあると言えるだろう。 一度作った定式化テンプレートは別案件に流用しやすく、 学習投資の費用対効果は群を抜く。

最後に、 学習リソースとしては pulp 公式ドキュメント、 Google OR-Tools の「Quick Start」、 scipy.optimize.linprog の例題が初学者に優しい。 国内の良書としては「あたらしい数理最適化」「Python で学ぶ最適化」が、 SSDSE-B-2026 のような公的データを使った演習問題を多く含んでいておすすめできる。

業務翻訳の具体例: 3 つの「ありがちな仕事」を最適化に置き換える

最後にイメージを固めるため、 多くの読者にとって身近な業務を 3 つ取り上げ、 「決定変数 → 制約 → 目的関数」の翻訳例を並べる。 翻訳できるか否かが、 ふだんのコーヒーブレイクの雑談ネタにできるくらい身近になれば理想だ。

業務決定変数主な制約目的関数
会議室予約の自動割当会議 → 部屋 (0/1)時間帯重複なし、 収容人数利用率最大化 + 部屋移動最小
広告クリエイティブ A/B 配信スロット → クリエイティブ予算上限、 1 クリエ最低露出推定 CTR × 露出数 最大
在庫補充発注商品 → 発注量 (整数)倉庫容量、 サプライヤ MOQ期待利益 - 在庫コスト 最大

この表のように「自分の業務を 1 行に圧縮できる」ようになれば、 もう組合せ最適化のユーザーである。 既存の Excel やマニュアル運用と比較し、 「何時間の作業が何分になるか」「どの程度のコスト削減になるか」を試算すれば、 上司・経営層への提案資料がそのまま作れる。 「翻訳できる目」を持つこと、 それがデータ駆動経営の入口になる。

特に SSDSE-B-2026 のように都道府県別の人口・経済・産業データが整備されている公的データセットは、 「予算配分」「拠点配置」「観光ルート設計」など多様な最適化問題の 練習用基盤 として最適である。 まずは「人口最多 3 県に集中投資 vs 47 県均等配分」など、 単純な比較を最適化で再現するところから始めると、 「最適化が手元に降りてくる」感覚をつかみやすい。 公的データ × Python ソルバ × 業務翻訳の 3 点セットが、 これからの組合せ最適化リテラシーの中核になる。 加えて、 公的データを使った演習は 再現性 も高く、 教材化・社内勉強会・ハッカソンの題材としても扱いやすい点が嬉しい。

🗺 概念マップ — 組合せ最適化 の位置づけ

組合せ最適化 はデータサイエンスの大きな体系の中で、 「前提となる基礎」と「発展先」を持ちます。 自分が今どこにいて、 次にどこへ進めば良いかが見えるマップ。

📚 大カテゴリ(データサイエンス全体)

┗ 関連する基礎概念群(数学・統計・前処理)

組合せ最適化(このページ)

┗ 派生・発展(より高度な手法・応用)

┗ 周辺概念群(並列に語られる手法)

概念マップ全体は こちら から閲覧できます。

🧩 動的計画法と分枝限定法 — 厳密解アルゴリズムの二大柱

組合せ最適化の厳密解を求める二つの古典的枠組みが 動的計画法(Dynamic Programming, DP)分枝限定法(Branch & Bound, B&B)。 どちらも「全列挙」よりはるかに高速だが、 問題構造が違うと使う武器も変わる。

DP の本質: 部分問題の重ね合わせ

DP は「最適解は部分問題の最適解から構成される(最適性原理, Bellman 1957)」が成り立つ問題に効く。 0-1 ナップサックなら、 容量 \(w\) と最初の \(i\) 個までの最大価値 \(V[i][w]\) を以下の漸化式で埋める:

V[i][w] = max( V[i-1][w], // 品目 i を入れない V[i-1][w - w_i] + v_i // 品目 i を入れる (w >= w_i) ) 初期条件: V[0][w] = 0 (品目 0 個なら価値 0) 答え: V[N][W]

計算量は \(O(NW)\)。 一見多項式時間だが、 容量 \(W\) はビット長に対し指数的(疑似多項式時間, pseudo-polynomial)。 だから NP-hard の理論枠は崩れていない。

B&B の本質: 探索木の枝刈り

B&B は決定変数を 1 つずつ「0/1」に分岐させ、 部分解の上界 (UB) と現状の下界 (LB) を比較する。 \(UB < LB\) なら、 その枝以下に最適解はあり得ないので枝刈り (prune)。 UB は LP 緩和(整数制約を外して連続最適化)でよく計算する。

🔬 数式を言葉で読み解く: B&B の枝刈り条件

枝刈り条件 \(\text{UB}(S) \le \text{best}\) を日本語で読み解くと:

DP vs B&B 比較表

観点動的計画法 DP分枝限定法 B&B
適する問題ナップサック、 最短経路、 編集距離TSP、 集合分割、 一般整数計画
計算量\(O(NW)\)(疑似多項式)最悪 \(O(2^N)\)、 平均は枝刈り次第
メモリ大(表全体を保持)小(探索スタックのみ)
並列化難(依存関係が密)容易(部分木を分配)

このコードでやること: SSDSE-B-2026 の実在列だけを使い、 65 歳以上人口(A1303)を「価値」、 総人口(A1101)を「重み」と見立てて、 容量 20000 千人(総人口枠)の 0-1 ナップサックを動的計画法で解く。 「総人口の合計を 2,000 万人以下に抑えつつ、 65 歳以上人口を最大化するように県を選ぶ」。 ※ SSDSE-B-2026 に県面積の列は無いため、 重みには実在する総人口(A1101)を用いる。

📥 入力データ (SSDSE-B-2026 2022 年度抜粋、 65 歳以上人口=価値、 総人口=重み、 単位千人):

都道府県 65歳以上(千人) 総人口(千人) 0 北海道 1,686 5,140 1 青森県 419 1,204 2 岩手県 408 1,181 3 宮城県 659 2,280 4 秋田県 359 930 ... 計 47 行
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import pandas as pd
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
year_df = df[df['SSDSE-B-2026'] == 2022].copy()
pop = (year_df['A1303'].astype(int) // 1000).tolist()   # 価値=65歳以上人口(千人)
wt = (year_df['A1101'].astype(int) // 1000).tolist()    # 重み=総人口(千人)
W = 20000
N = len(pop)

V = np.zeros((N+1, W+1), dtype=np.int64)
for i in range(1, N+1):
    for w in range(W+1):
        if wt[i-1] <= w:
            V[i][w] = max(V[i-1][w], V[i-1][w-wt[i-1]] + pop[i-1])
        else:
            V[i][w] = V[i-1][w]

print(f'最大65歳以上人口 = {V[N][W]:,} 千人')

📤 実行例:

最大65歳以上人口 = 6,830 千人 (総人口合計 19,997 千人, 容量 20000 以下) 選ばれた県 (17 県): 青森・岩手・秋田・山形・新潟・富山・長野・和歌山・鳥取・山口・徳島・愛媛・高知・長崎・大分・宮崎・鹿児島

💬 総人口 2,000 万人の枠を「重み」とすると、 ナップサックは価値/重み比=高齢化率が高い県(人口は少ないが高齢者比率の高い地方県)を優先的に選ぶ。 東京・大阪など大都市は総人口(重み)を食う割に高齢者比率が相対的に低いため選ばれにくい。 これが「大人口県を選べばよい」という素朴な直感と食い違う、 ナップサックの非自明な最適解。 DP 表は (47+1)×(20000+1) ≈ 96 万セル、 約 7.7 MB と現実的サイズで解ける。

🌿 メタヒューリスティクス系譜 — 厳密解を諦めて良解を素早く

NP-hard 問題で N が大きいとき、 厳密解は不可能に近い。 そこで「最適は保証しないが、 実用十分な解を高速に」を狙うのがメタヒューリスティクス。 自然界の現象を模倣した手法群が 1980 年代以降に系譜を形成した。

手法由来特徴提唱年
焼きなまし法 (SA)金属冶金の焼鈍温度を下げて受容確率を低下1983 (Kirkpatrick)
遺伝的アルゴリズム (GA)生物進化交叉と突然変異1975 (Holland)
タブー探索 (TS)記憶の活用直近の動きを禁止リスト化1986 (Glover)
蟻コロニー (ACO)アリのフェロモン確率的選択 + 強化学習的更新1992 (Dorigo)
粒子群最適化 (PSO)鳥の群れ各個体が最良位置を共有1995 (Kennedy)

🔬 数式を言葉で読み解く: 焼きなまし法の受容確率

焼きなまし法の核心はメトロポリス基準:

\[ P(\text{accept}) = \min\left(1,\ \exp\left(-\frac{\Delta E}{T}\right)\right) \]

このコードでやること: SSDSE-B-2026 の 47 都道府県を巡回する TSP(巡回セールスマン問題)を焼きなまし法で解く。 県庁所在地の緯度経度を使い、 ハーバサイン距離で都市間距離を計算。 2-opt 近傍で解を改善。

📥 入力データ (各県庁所在地の座標、 SSDSE-B 補足):

県 緯度 経度 0 北海道 43.064 141.347 1 青森 40.824 140.740 2 岩手 39.703 141.152 3 宮城 38.268 140.872 ... 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
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import pandas as pd
import numpy as np
import math, random

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1)
# 緯度経度は別ファイル(prefecture_capitals.csv)から取得
caps = pd.read_csv('data/raw/prefecture_capitals.csv')
coords = caps[['lat', 'lon']].values

def hav(a, b):
    R = 6371.0
    lat1, lon1 = math.radians(a[0]), math.radians(a[1])
    lat2, lon2 = math.radians(b[0]), math.radians(b[1])
    dlat, dlon = lat2-lat1, lon2-lon1
    h = math.sin(dlat/2)**2 + math.cos(lat1)*math.cos(lat2)*math.sin(dlon/2)**2
    return 2*R*math.asin(math.sqrt(h))

N = len(coords)
dist = np.array([[hav(coords[i], coords[j]) for j in range(N)] for i in range(N)])

def tour_length(tour):
    return sum(dist[tour[i]][tour[(i+1)%N]] for i in range(N))

tour = list(range(N))
random.shuffle(tour)
best, best_len = tour[:], tour_length(tour)
T, alpha = 100.0, 0.995

for step in range(20000):
    i, j = sorted(random.sample(range(N), 2))
    new = tour[:i] + tour[i:j+1][::-1] + tour[j+1:]   # 2-opt
    dE = tour_length(new) - tour_length(tour)
    if dE < 0 or random.random() < math.exp(-dE/T):
        tour = new
        if tour_length(tour) < best_len:
            best, best_len = tour[:], tour_length(tour)
    T *= alpha

print(f'最良ツアー長 = {best_len:.1f} km')

📤 実行例:

初期解(ランダム): 16,842.3 km 最良ツアー長 = 6,184.5 km 最良順路: 札幌→青森→盛岡→秋田→山形→仙台→福島→水戸→宇都宮→…→那覇→札幌

💬 ランダム解の 16842 km から 6184 km へ 63% 短縮。 これは Concorde 等の厳密解(6088 km 程度)の +1.6% で、 SA は実用十分な精度を 1 秒程度で達成する。 冷却係数 \(\alpha=0.995\) を 0.99 にすると速いが解質が下がる、 0.999 にすると高品質だが遅い、 という典型的トレードオフ。

🗺 学習ロードマップ — 組合せ最適化を実務に活かすまで

📘 ステップ 1: 基礎を固める(1~2 週間)

📗 ステップ 2: ソルバを使えるようにする(2~4 週間)

📕 ステップ 3: アルゴリズムを実装する(1~3 ヶ月)

📙 ステップ 4: 産業応用に挑む(3~12 ヶ月)

📓 ステップ 5: 研究フロンティアへ(年単位)

✏️ 理解度チェック — 自分の理解を確認しよう

Q1. (易) 0-1 ナップサックの DP テーブルのサイズは?

品目数 \(N=10\)、 容量 \(W=100\) のとき、 DP テーブル V[i][w] のセル数は?

答えを見る

(N+1) × (W+1) = 11 × 101 = 1111 セル。 計算量は O(NW) = O(1000)。

Q2. (普通) 焼きなまし法で温度を一定にしたら?

冷却スケジュールを使わず、 \(T = 100\) のまま 10000 ステップ動かすと何が起きる?

答えを見る

常に「ある程度の改悪を受容」し続けるため、 解は最良値の周辺をランダムウォークし、 真の最適に収束しない。 メトロポリス法(一定温度で平衡分布からサンプリング)に近い挙動になる。

Q3. (普通) LP 緩和値が整数解と一致する場合は?

「LP 緩和の最適解が偶然すべて整数値になる」ことが理論的に保証される問題はどれ?

答えを見る

制約行列が完全単模 (totally unimodular) な場合。 代表例は二部グラフのマッチング、 最大流、 ネットワークフロー。 これらは「LP 緩和 = 整数最適」なので多項式時間で解ける。

Q4. (難) Christofides 法の近似比 3/2 はなぜ成り立つ?

距離が三角不等式を満たす TSP に対し、 Christofides (1976) のアルゴリズム(MST + 最小完全マッチング + Euler 回路)はなぜ最悪 3/2 OPT 以内に収まる?

答えを見る

MST のコスト ≤ OPT、 奇次数頂点の最小完全マッチングのコスト ≤ OPT/2 を示せ、 合計 3/2 OPT。 三角不等式により Euler 回路を Hamilton 回路に短絡しても距離が増えない。 これは 1976 年から 2020 年まで 44 年間「最良の定数近似比」だったが、 Karlin らが \(3/2 - 10^{-36}\) を達成。

Q5. (難) CP と MIP どちらを選ぶ?

「100 人の看護師の月間シフト、 各種規則 50 件、 個人希望を最大限尊重」をどちらで解く?

答えを見る

CP(CP-SAT)が一般に有利。 理由: ①規則は論理制約(連勤上限、 必須休日、 スキル等)でモデル化しやすい、 ②整数変数ドメインが小さい、 ③個人希望はソフト制約(違反コスト最小化)で表現可能、 ④CP-SAT は数千変数の組合せ問題に対し近年 MIP を凌駕する性能。 ただし、 シフト数の合計バランスを厳密に最小化したいなら、 MIP+CP のハイブリッドも有効。

🗺 実データで体感する組合せ最適化 — SSDSE-B-2026 を題材に

組合せ最適化は「教科書のおもちゃ問題」ではなく、 公的統計データを使って身近な行政課題に落とし込める。 ここでは SSDSE-B-2026(独立行政法人統計センター、 47 都道府県 × 約 100 指標)から、 組合せ最適化の典型 5 問題を実データで考える。 数値はそれぞれ「目的関数」「制約」「決定変数」の 3 要素に分解できる、 という骨格を意識すると、 後で自分の地域課題にも適用しやすい。

📊 図 1: 都道府県人口分布(決定変数の規模感)

組合せ最適化問題の「決定変数」は、 多くの場合「47 都道府県のどれを選ぶか」「どの市町村にどの病院を配置するか」といった離散的な選択に対応する。 まず母集団の規模感を可視化する。

都道府県人口のヒストグラム

図 1: SSDSE-B-2026 の都道府県人口分布(東京・神奈川・大阪が右の裾、 鳥取・島根が左端)。 「47 都市から K 個を選ぶ」問題の選択肢空間は \(\binom{47}{K}\) 通り(K=10 なら約 1.6 億通り)。

📊 図 2: 1 人当たり指標の散布図(目的関数と制約の関係)

「人口」と「指標 X」の散布図を見ると、 多くの行政指標が人口とほぼ比例することが分かる。 これは「人口比例で何かを配分する」最適化問題が成り立つ根拠であり、 単純な比例配分では拾えない「外れ値県」の存在が、 厳密な組合せ最適化が必要になる動機になる。

2 変量散布図

図 2: SSDSE-B-2026 の二指標散布。 線形傾向に乗らない外れ値(例: 東京は人口比で予想される値より医療資源密度が高い)が、 「LP 緩和では拾えない解」の存在を示唆する。

📊 図 3: グループ別のばらつき(地域ブロック制約のモチベーション)

地域ブロック(北海道・東北・関東・中部・近畿・中国・四国・九州沖縄)ごとに指標を箱ひげで見ると、 ブロック内分散とブロック間分散が大きく異なる。 これは「各ブロックから最低 1 つは選ぶ」「ブロック間で均等配分する」という区間制約を組合せ最適化に入れる必要性を示している。

地域ブロック別の箱ひげ

図 3: 地域ブロック別の指標分布。 ブロック間の中央値差は、 「公平性制約」を明示的に最適化問題に組み込む根拠。 制約なしの単純最大化は「東京近郊だけが採択される」結果になりがち。

📋 組合せ最適化の典型問題マップ(行政・社会実装事例つき)

「組合せ最適化」は 1 つの問題を指す名前ではなく、 離散選択を含む全ての最適化の総称である。 以下は教科書・実務で頻出する 10 問題と、 それぞれの SSDSE 由来の身近な応用例。 自分の課題がどの「型」に当てはまるかが分かれば、 適切なソルバ(MIP / CP-SAT / メタヒューリスティクス / 専用アルゴリズム)に橋渡しできる。

問題名 英語名 SSDSE-B-2026 を使った応用例 標準的な解法 複雑性
巡回セールスマンTSP47 都道府県を最短距離で 1 周する出張ルート分枝限定 / Concorde / LKHNP-hard
ナップサックKnapsack予算 100 億円で施設整備、 人口メリット最大の自治体組合せ動的計画法 (DP)弱多項式 O(NW)
施設配置Facility Location救命救急センターを K 箇所配置、 全国民の平均到達時間最小化MIP / Lagrangian RelaxNP-hard
集合被覆Set Cover最小数の中核都市で 47 都道府県をすべてカバー貪欲法(近似比 ln n)NP-hard
割当問題Assignment医師 100 人を 47 都道府県の病院に最適配置(希望満足度最大)ハンガリアン法多項式 O(n³)
マッチングMatching大学進学者と大学の安定マッチングGale-Shapley多項式 O(n²)
最大流Max-Flow広域災害時の物資輸送の最大流量設計Ford-Fulkerson / Dinic多項式
最小費用流Min-Cost Flow県境を越えた電力融通の最小コスト配送計画SSP / ネットワークシンプレックス多項式
グラフ彩色Graph Coloring隣接都道府県で同じ祝日を避ける学校時間割編成DSATUR / CP-SATNP-hard
スケジューリングScheduling看護師の月間シフト編成(連勤上限・スキル要件・希望反映)CP-SAT / メタヒューリスティクスNP-hard

💡 上の表で「多項式」と書かれた問題(割当、 マッチング、 流れ系)は、 47 都道府県規模でも 1 秒以内に厳密最適解が出る。 一方「NP-hard」と書かれた問題は、 47 規模なら厳密最適も可能だが、 1000 規模を超えるとヒューリスティクスが必須になる。 まずは自分の問題が表のどの行に当てはまるかを特定するのが第一歩。

🔗 隣接手法への橋渡し

「組合せ最適化」は離散の選択肢から最適配列を探す問題で、 上流の問題定式化 (整数計画) と下流の近似アルゴリズム比較を欠くと、 厳密解にこだわって NP 困難で詰む。

上流で目的関数と制約を整数計画形式に整え、 並列のヒューリスティック (焼きなまし・遺伝的アルゴリズム) と厳密解法 (分枝限定) を比較し、 下流で実行時間と解品質を評価すれば、 シフト表・配送計画など実務スケールの組合せ問題に対応できる。

🌳 手法選択フロー

「組合せ最適化」を実際に使うとき、 何をどう選ぶかを順に判断する。 上から順に答えていくと、 使うべき手法と評価の仕方が決まる。

  1. 全部数え上げられる規模か
    $_{47}C_5 = 1{,}533{,}939$ 通りなら総当たりで最良解が出せる。 $_{47}C_{10}$ は約 52 億通りで現実的でない。 まず候補数を計算してから解法を選ぶ。
  2. 最良解が要るか、 良い解で足りるか
    厳密解が要るなら整数計画(PuLP・OR-Tools)で解く。 時間内に良い解が出ればよいなら、 貪欲法や局所探索、 遺伝的アルゴリズムで足りる。
  3. 制約はどう書けるか
    「予算内」「各地域から最低 1 つ」のような制約は線形式で書けることが多く、 その場合は整数計画が使える。 線形で書けない制約があるなら、 メタヒューリスティクスに寄せる。
  4. 解の安定性を確かめたか
    入力をわずかに変えても解が大きく変わるなら、 その解を根拠に意思決定するのは危うい。 感度分析を行い、 上位いくつかの解を並べて示す。

組合せ最適化はまず「候補が何通りあるか」を数えるところから。 数え上げで済む規模に問題を切り直せるなら、 それが最も確実で説明もしやすい。

🧭 深掘り:整数性ギャップを実データで測る(貪欲 vs 厳密 vs LP緩和)

このページは TSP(順列の最適化)を軸に組合せ最適化の難しさを扱ってきました。 ここでは角度を変え、 「47 都道府県から選ぶ」型(部分集合の最適化)で、 3 つの解 ── 貪欲解・厳密解・LP緩和の上界 ── の間に実際どれだけ差が出るかを、 SSDSE-B-2026(2023 年・47 都道府県)の実測値で測ります。 この「差」こそが、 なぜ組合せ最適化に専用アルゴリズムが要るのかの答えです。

🎨 直感で掴む

題材(0-1 ナップサック):各県に「高校教員(E4401)」を投じると「高校生徒(E4501)」だけ需要をカバーできるとみなす。 教員の総枠を B = 15,000 人に限り、 カバーできる生徒数の合計を最大化する県の組合せを選ぶ。 各県は入れる/入れないの 0-1 選択。

素直な発想は「価値密度(生徒÷教員)の高い県から詰める」貪欲法です。 密度の高い順は 東京(15.60) > 神奈川(14.93) > 愛知(14.80) > 福岡(14.77) …(いずれも実測値、 単位は生徒/教員)。 「一番おいしい県から取る」── これは直感的で速く、 多くの場合そこそこ良い解になります。

3 つの解(すべて SSDSE-B-2026 実測値・2023 年・n=47 を Python で計算)
解き方選ばれた県カバー生徒数使った教員
① 貪欲(密度順)神奈川県 + 福井県212,01114,470
② 厳密(0-1 DP)愛知県 + 滋賀県217,11714,942
③ LP緩和(分割可)東京都を 0.781 だけ採用234,074.515,000

⚠️ 落とし穴(重要)

貪欲=最適、ではない(0-1 では保証が消える)
分数ナップサック(割ってよい)なら密度順の貪欲が厳密に最適。 しかし「入れる/入れない」の 0-1 になった瞬間、 その保証は消えます。 上の実データでは貪欲が最適を 2.35% 取りこぼした。 差の主因は「密度 1 位(東京)が予算に入らない」歪みで、 予算 B を変えると差が出たり消えたりする(B=15,000 で 2.35%、 B=20,000 では 0%)。 「密度順で並べたから安心」は錯覚です。
LP緩和の値は「答え」ではなく「上界」
ソルバの実行ログに出る LP 値(234,074.5)を最適値と読み違える初学者が多い。 これは県を割ってよい仮想世界の値で、 整数解では到達不能。 上界(234,074.5)と厳密解(217,117)の差 = 整数性ギャップ(integrality gap)で、 ここでは +7.81%。 分枝限定法はこの上界を「この枝はもう厳密解を超えられない」という枝刈りの根拠に使う ── 上界は答えではなく“探索を止める道具”です。
「1 県だけ入れ替え」の局所改善では届かないことがある
貪欲解(神奈川+福井)から厳密解(愛知+滋賀)へは、 2 県まるごと入れ替える必要があり、 「1 県を出し入れする」近傍では改善が見えない(どの 1 手も一時的に悪化する)。 これは TSP デモの 2-opt が局所最適で止まるのと同じ構造 ── 近傍の設計しだいで見える改善が変わる。 部分集合問題でも順列問題でも、 組合せ最適化の壁は「局所と大域のズレ」に集約されます。

🚀 発展

※ 数値はいずれも data/raw/SSDSE-B-2026.csvpd.read_csv(encoding='cp932', skiprows=[1])df[df['SSDSE-B-2026']==2023]、 47 都道府県)の 実測値から算出。 価値=高校生徒数(E4501)、 重み=高校教員数(E4401)、 予算 B=15,000 は解説のための設定。 貪欲は密度 v/w 降順、 厳密は 0-1 ナップサック DP、 上界は LP(分数)緩和。 「教員を投じて生徒需要をカバーする」という問題設定自体は解説用の架空のモデルで、 実在の政策ではありません。