論文一覧に戻る 📚 用語集トップ 🗺 概念マップ
📚 用語解説
📚 用語解説
ナップサック問題
Knapsack Problem
最適化

🔖 キーワード索引

ナップサック組合せ最適化動的計画法NP-hard整数計画制約

このページで扱う言葉を、意味と行き先つきで並べます。 知らない語があればここから辿ってください。

語 一言でいうと このページのどこ/関連ページ
ナップサック問題容量の上限がある入れ物に、価値の合計が最大になるよう品物を選ぶ問題。📐 定義/数式
0-1 ナップサック各品物を「入れる/入れない」の 2 択で選ぶ、最も基本的な形。📐 定義/数式
容量制約選んだ品物の重さの合計が超えてはいけない上限。🧮 実値で計算してみる
動的計画法(DP)小さい容量の答えを積み上げて、大きい容量の答えを作る解き方。🐍 Python 実装
擬多項式時間容量 W に比例する計算量。W が桁違いに大きいと現実には終わらない。⚠️ 落とし穴
NP 困難入力が増えると現実的な時間で厳密解を出せなくなる難しさの分類。⚠️ 落とし穴
貪欲法価値÷重さが高い順に詰める近似解法。分数版では最適だが 0-1 版では最適とは限らない。🌐 関連手法・派生
分数ナップサック品物を分割して詰められる版。貪欲法で厳密に解ける。🌐 関連手法・派生
整数計画(MILP)0-1 の決定変数を含む最適化。ソルバーに任せる実務的な解き方。数理最適化
組合せ最適化「どれを選ぶか」を決める最適化問題の総称。ナップサックはその代表例。組合せ最適化
メタヒューリスティクス厳密解を諦めて良い解を速く探す方法。大規模なときの選択肢。メタヒューリスティクス
緩和問題整数の条件を外して解きやすくした問題。上界を知るのに使う。🔗 隣接手法への橋渡し

💡 30秒で分かる結論

🍰 まずはやさしく

限られた袋に物を詰めるパズルです。

価値の合計を最大にするために使います。

リュックに荷物を入れる時に似ています。

この章では問題の結論を学びます。

ナップサック問題 ── 容量制約付き選択問題の代表例

📍 文脈 ── どこで出会うか

🍰 まずはやさしく

組み合わせの中から正解を探す方法です。

効率よく物を選ぶために使います。

予算内で広告枠を選ぶ時に役立ちます。

この章では出会う場面について読みます。

組合せ最適化の入門として最も有名。 実務でも「予算内で広告枠を選ぶ」「容量内で配送荷物を選ぶ」など、 形を変えて頻出します。

🎨 直感で掴む

🍰 まずはやさしく

袋の容量と価値を考えるゲームです。

一番いい組み合わせを見つけるために使います。

限られた時間で勉強する物に似ています。

この章では直感的な考え方を読みます。

典型例:

🎨 直感を深掘り

「限られた容量のリュック」に「価値の合計が最大」になるよう品物を詰める問題。日常では「限られた予算で広告を出す」「限られた時間で論文を読む」「限られた人月でプロジェクトを選ぶ」など、構造的に同じ問題が無数に存在します。リュックの「重さ」を「コスト」、価値を「期待リターン」と読み替えれば、ビジネスの資源配分はほとんどがナップサック型と言えます。

📐 定義/数式

🍰 まずはやさしく

ルールを数式で表したものです。

正確に計算するために使います。

スマホのメモリ割り当てに似ています。

この章では数式での定義を読みます。

【0/1 ナップサック】
最大化: $\sum_{i=1}^{n} v_i x_i$
制約: $\sum_{i=1}^{n} w_i x_i \le W$, $x_i \in \{0, 1\}$
【動的計画法の漸化式】
$$ dp[i][w] = \max(dp[i-1][w], \; dp[i-1][w-w_i] + v_i) $$

📐 数式の読み解き ── ナップサック問題 の核心式

$$ \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\} $$

ナップサック問題の標準的な整数計画定式化。 $v_i$ は品物 i の価値、 $w_i$ は重さ、 $W$ は容量。

❓ 計算量の疑問 ── 47 県から選ぶだけなら全探索でよいのでは?

Q3. ナップサック問題 の計算量・スケーラビリティは?

ナップサック問題は NP 困難で、 全探索は品物 $n$ 個に対して $O(2^n)$。 $n=47$(47 都道府県から選ぶ)なら $2^{47} \approx 1.4 \times 10^{14}$ 通りで、 1 秒に 10 億通り調べても 39 時間かかります。 一方、 動的計画法なら容量 $W$ に対して $O(nW)$ で解けます。 $n=47$・$W=10^6$ なら 4,700 万回の更新で済み、 数秒で終わります。 ただし $O(nW)$ は擬多項式時間で、 $W$ の「桁数」に対しては指数です(入力長 $\log W$ に対して $2^{\log W}$)。 容量を「人」単位から「千人」単位に丸めるだけで表が 1000 分の 1 になる、 というのが実務上の効き所です。

🔬 数式を言葉で読み解く

$x_i$
品物 $i$ を入れるか(0 or 1)
$v_i$
品物 $i$ の価値
$w_i$
品物 $i$ の重さ
$W$
リュック容量
dp[i][w]
「i 番目までで、 容量 w 以下」での最大価値

🔬 深堀り — ナップサック問題 の発展的論点

ナップサック問題は NP 困難な組合せ最適化問題の代表例。 0-1 ナップサック、 分数ナップサック、 多次元ナップサック、 制約付きナップサックなど多くの変種が存在します。 動的計画法では O(nW) の擬似多項式時間で解けますが、 W が大きい場合は分枝限定法やメタヒューリスティクス(GA, PSO 等)を併用します。 47 都道府県への補助金配分のような実問題はナップサック類似の構造を持ち、 価値関数の定義・制約の追加(公平性等)に応じて拡張定式化が必要になります。 整数計画ソルバー(PuLP, OR-Tools, Gurobi 等)の利用も実用的選択肢です。

🧮 実値で計算してみる

上の例で動的計画法のテーブル(一部抜粋):

        w=0 w=2 w=3 w=4 w=5 w=7 w=10
       --- --- --- --- --- --- ---
i=0     0   0   0   0   0   0   0
i=A     0   0   0   5   5   5   5
i=B     0   0   4   5   5   9   9
i=C     0   0   4   5   6   9  13
i=D     0   3   4   5   8   9  13

右下 dp[D][10] = 13 が最大価値。 B+C+D の組合せ。

🧮 SSDSE-B 実値で計算してみる ── ナップサック問題

47都道府県の中から、人口(A1101)と一般病院数(I510120)といった重要指標を考慮して『限られた予算で K 都道府県の重点支援』を選ぶシミュレーション。各県の人口(=価値)と何らかの介入コスト(=重さ)の組合せで、合計人口を最大化する問題と見なせる。

都道府県 人口(価値、2023 年度) 介入コスト(重さ、仮の値)
北海道509.2万人10億円
青森県118.4万人3億円
岩手県116.3万人3億円
宮城県226.4万人5億円
秋田県91.4万人2億円
山形県102.6万人3億円
福島県176.7万人4億円

※ 人口は SSDSE-B-2026 の 2023 年度の総人口 A1101(万人、 小数第 1 位まで)。 介入コスト(億円)は説明のために置いた仮の値で、 データには含まれていません。

🧮 数式に値を入れて手で計算する: ナップサック DP

容量 W=5、 アイテム 3 個で動的計画法を計算する。

Step 1: アイテム

i重さ価値
123
234
345

Step 2: DP 表 (一部)

dp[i][w] = max(dp[i-1][w], dp[i-1][w-wi] + vi) dp[3][5]: - 入れない: dp[2][5] - 入れる: dp[2][1] + 5 = 0 + 5 = 5 - dp[2][5] = max(dp[1][5], dp[1][2]+4) = max(3, 7) = 7 最大価値 = 7 (item1 + item2)

🐍 Python で再現

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
items = [(2, 3), (3, 4), (4, 5)]
W = 5
n = len(items)
dp = [[0]*(W+1) for _ in range(n+1)]
for i in range(1, n+1):
    wi, vi = items[i-1]
    for w in range(W+1):
        dp[i][w] = dp[i-1][w]
        if w >= wi:
            dp[i][w] = max(dp[i][w], dp[i-1][w-wi] + vi)
print(f"最大価値: {dp[n][W]}")

📤 実行結果

最大価値: 7

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

🎮 触って理解する

品物カードをタップ(クリック)してナップサックに入れたり出したりしてみましょう。容量ゲージと合計価値がリアルタイムに更新され、容量を超えると警告が出ます。「貪欲法」「動的計画法(厳密解)」ボタンで自動解をナップサックに読み込み、自分の選択・貪欲・DP の 3 つの価値を並べて比較できます。既定の「例題セット」は本文『🎨 直感で掴む』と同じ 4 品(最適値 13)で、実は貪欲法が最適を逃す例になっています。

容量ゲージ(重さ合計 / 容量 W)
容量 W を調整(ドラッグ / スワイプ対応、4〜15)

📊 3 つの解の比較(あなた / 貪欲 / DP最適)

🧮 DP テーブルをステップ表示(行=品物、列=容量、セル=最大価値)

漸化式 dp[i][w] = max(dp[i−1][w], dp[i−1][w−wi] + vi) が 1 セルずつ埋まる様子を観察できます。橙=いま計算中・青=入れない場合の参照元・緑=入れる場合の参照元。

🧭 直感 ── この演習で何を体感しているのか

ナップサック問題の本質は「限られた容量の中で価値の合計を最大化する」こと。品物を 1 つ入れるたびに容量という共有資源が減り、残りの選択肢が変わります。だからこそ「単品で一番お得なもの」を順に取るだけでは全体最適に届かないことがある ── これが上の手動プレイで体感できる核心です。組合せは品物 n 個で 2n 通りに爆発しますが、DP テーブルは「i 番目までの品物で容量 w 以下」という部分問題の答えを再利用することで、全列挙せずに厳密解へ到達します。

⚠️ よくある落とし穴(この演習で見えるもの)

🚀 発展 ── 計算量と実用解法へ

🐍 Python 実装

まず 4 品物・容量 10 の小さな例で、0/1 ナップサックの動的計画法(DP)を動かす。続けて SSDSE-B-2026 の 47 都道府県を品物にした例で、DP と貪欲法の答えがどう違うかを比べる。

🎯 解説: 容量 W=10 のリュックに、 重み [4,3,5,2]・価値 [5,4,6,3] の 4 品物から、 容量を超えない範囲で価値合計が最大になる組合せを 0/1 ナップサックの動的計画法で選ぶ。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# 動的計画法
def knapsack(weights, values, W):
    n = len(weights)
    dp = [[0]*(W+1) for _ in range(n+1)]
    for i in range(1, n+1):
        for w in range(W+1):
            dp[i][w] = dp[i-1][w]
            if weights[i-1] <= w:
                dp[i][w] = max(dp[i][w],
                               dp[i-1][w - weights[i-1]] + values[i-1])
    return dp[n][W]

print(knapsack([4,3,5,2], [5,4,6,3], W=10))   # 13
📥 入力: weights = [4, 3, 5, 2](各品物の重さ) values = [5, 4, 6, 3](各品物の価値) 容量 W = 10
📤 実行結果: 13 → 品物 B(重さ3,価値4) + C(重さ5,価値6) + D(重さ2,価値3) を選択 → 合計 重さ 10(=容量ちょうど)・価値 13 が最大
💬 読み方: 0/1 ナップサックの DP は O(nW)、 W = 容量。 各品目を入れる/入れないの 2 択。 W が大きいと擬似多項式時間で実用に。 連続版(分数許可)なら貪欲法で価値/重量降順に詰めれば O(n log n) で最適解。

🐍 SSDSE-B を使った Python 実装

公的データ SSDSE-B(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
import pandas as pd
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932')   # 英字コードの列名を使う
df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True)   # 2023 年度の 47 都道府県
# 各都道府県の人口(A1101)を価値、人口(万人)の平方根を仮想コストとして
# 限られた『予算 100』で都道府県を選び合計人口を最大化
values = df['A1101'].astype(float).values
weights = np.round(np.sqrt(values / 10_000)).astype(int)   # 鳥取県 7 〜 東京都 38
W = 100

n = len(values)
dp = np.zeros((n+1, W+1), dtype=float)
for i in range(1, n+1):
    for w in range(W+1):
        dp[i][w] = dp[i-1][w]
        if weights[i-1] <= w:
            dp[i][w] = max(dp[i][w], dp[i-1][w-weights[i-1]] + values[i-1])
print('最大合計人口:', dp[n][W])

# 選ばれた都道府県を後ろからたどる
w, chosen = W, []
for i in range(n, 0, -1):
    if dp[i][w] != dp[i-1][w]:
        chosen.append(df['Prefecture'][i-1])
        w -= weights[i-1]
print('選ばれた都道府県:', chosen[::-1], '使ったコスト:', W - w)
📤 実行例(実測) 最大合計人口: 32078000.0 選ばれた都道府県: ['東京都', '神奈川県', '大阪府'] 使ったコスト: 98

💬 コストを人口(万人)の平方根にすると、鳥取県 7 〜 東京都 38 の範囲に収まり、予算 100 の中で東京都・神奈川県・大阪府(コスト 38+30+30=98)を選んだとき合計 3,207.8 万人で最大になる。人口 p の価値に対してコストは √p なので、1 コストあたりの人口は √p に比例し、大きな県ほど割安になる。そのため貪欲に大きい順で詰めた答えと一致しやすく、DP の強みが出るのはコストが価値とこれほど揃っていない場合である。

🐍 出生数を価値、人口を重さにした DP と貪欲法の比較

🎯 このコードでやること:2023 年度の 47 県を品物とし、価値 = 出生数(A4101)、重さ = 総人口(A1101)を 10 万人単位に丸めた整数、容量 = 200(人口 2,000 万人分)の 0/1 ナップサックを DP で解く。比べる相手として「出生数 ÷ 重さ」の大きい順に詰める貪欲法の答えも出す。

📥 入力例 SSDSE-B-2026 の 2023 年度 47 行(使う列: Prefecture・A1101 総人口・A4101 出生数) 都道府県 A1101(総人口) 重さ(10万人) A4101(出生数) 北海道 5,092,000 51 24,430 東京都 14,086,000 141 86,348 沖縄県 1,468,000 15 12,549 …(全 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
import numpy as np
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932')
latest = df[df['SSDSE-B-2026'] == 2023].copy()     # 品目は「県」なので 1 県 1 行に絞る
num = latest.select_dtypes(include='number')

# 価値 = 出生数 A4101(人)、重さ = 総人口 A1101 を 10 万人単位に丸めた整数、容量 = 200(2,000 万人分)
# 「人口 2,000 万人分の枠で、出生数の合計が最大になる県の組」を選ぶ 0/1 ナップサック
names = latest['Prefecture'].values
value = num['A4101'].astype(int).values
weight = np.round(num['A1101'].values / 100_000).astype(int)
W = 200

n = len(value)
dp = np.zeros((n + 1, W + 1), dtype=int)
for i in range(1, n + 1):
    for w in range(W + 1):
        dp[i][w] = dp[i - 1][w]
        if weight[i - 1] <= w:
            dp[i][w] = max(dp[i][w], dp[i - 1][w - weight[i - 1]] + value[i - 1])

# 選んだ県を後ろからたどる
w, chosen = W, []
for i in range(n, 0, -1):
    if dp[i][w] != dp[i - 1][w]:
        chosen.append(i - 1); w -= weight[i - 1]

# 比べる相手: 出生数 / 重さ の大きい順に詰める貪欲法
used, g_val, g_set = 0, 0, []
for i in np.argsort(-value / weight):
    if used + weight[i] <= W:
        used += weight[i]; g_val += value[i]; g_set.append(i)

print(f'DP の最大出生数 : {dp[n][W]:,} 人({len(chosen)} 県, 重さ {weight[chosen].sum()})')
print(f'貪欲法の出生数   : {g_val:,} 人({len(g_set)} 県, 重さ {used})')
print('DP だけが選んだ県 :', [names[i] for i in sorted(set(chosen) - set(g_set))])
print('貪欲だけが選んだ県:', [names[i] for i in sorted(set(g_set) - set(chosen))])
📤 実行例(実測) DP の最大出生数 : 133,606 人(8 県, 重さ 200) 貪欲法の出生数 : 132,188 人(13 県, 重さ 200) DP だけが選んだ県 : ['愛知県'] 貪欲だけが選んだ県: ['福井県', '島根県', '岡山県', '広島県', '徳島県', '宮崎県']

💬 人口 2,000 万人分の枠で DP が選んだのは愛知・滋賀・鳥取・福岡・佐賀・熊本・鹿児島・沖縄の 8 県で、出生数 133,606 人。人口 10 万人あたりの出生数が大きい順に詰める貪欲法は 13 県で 132,188 人にとどまり、1,418 人(約 1.1%)取り逃す。愛知県は比が 645 と中位だが重さ 75 と大きいので貪欲法では後回しになり、DP は愛知県 1 県を入れる代わりに福井・島根・岡山・広島・徳島・宮崎の 6 県を外す方が得だと見つけている。ただし重さは 10 万人単位に丸めた値なので、選ばれた 8 県の実際の人口の合計は 20,045,000 人で、2,000 万人の枠を 4.5 万人超えている(⚠️ の「重さを丸める単位」の節)。

📊 比較表 — ナップサック問題 と類似手法の使い分け

ナップサック問題 は単独で完結する手法ではなく、 周辺手法と比較して使い分ける必要があります。 以下に主要な類似・代替手法との比較を表でまとめます。

観点ナップサック問題 (0/1, DP)分数ナップサック (貪欲)巡回セールスマン (TSP)
目的容量 W 以下で価値合計最大化分割可能な財での価値最大化全都市を回る最短経路
適用条件アイテム整数選択、 重み・価値正アイテム分割可、 連続変数完全グラフ、 三角不等式可
計算量O(nW) (擬多項式)O(n log n) (貪欲)O(n^2 2^n) (Held-Karp)
最適性DP で厳密最適価値密度ソート貪欲で最適NP-hard、 近似 (Christofides 等)
問題規模n=100, W=10^4 程度まで現実的n=10^6 でも瞬時n=30 で限界、 大規模はメタヒューリスティクス
Python 実装numpy DP 配列 / PuLP MILPsorted + greedy ループnetworkx / OR-Tools / pulp
適用例予算配分、 投資選択、 倉庫積込原材料調達、 ETF 構成配送経路、 工場 NC 加工順序

分割可能なら貪欲で最適 (分数ナップサック)。 0/1 制約があれば DP か MILP ソルバ。 経路問題に拡張するなら TSP や VRP の枠組みに移行する。

⚠️ よくある落とし穴

❌ 1. 容量が大きいとDPメモリ爆発
O(nW) は擬多項式。 W が連続値なら近似アルゴリズム
❌ 2. 貪欲法を最適だと信じる
0/1ナップサックでは貪欲は最適解を保証しない
❌ 3. 複数制約への拡張を見落とす
多次元ナップサックは NP困難で計算量爆発
❌ 4. 実数価値の精度問題
価値が小数なら整数化スケーリングを工夫
❌ 5. 分枝限定の枝刈り設計
深い探索で時間切れ。 上界推定が肝

⚠️ 追加の落とし穴 ── 実務で踏み抜く罠

❌ 1. 実数価値での精度誤差
scaling して整数化するときに桁を間違えると最適解からずれる。 ε近似 (FPTAS) の精度パラメータ ε と問題サイズの兼ね合いを確認。
❌ 2. DP の容量 W がメモリ爆発
W=10^7 だと 1e7 × n のテーブルは GB 級。 1次元 DP(rolling)に書き換え、 必要ならビット圧縮を検討。
❌ 3. 複数制約(多次元)を見落とす
「重さ」「体積」「人月」3 つの容量を同時に守る問題は通常の DP では指数的。 整数計画ソルバ(CBC/Gurobi)に切り替える。
❌ 4. 最適解と最適『集合』の混同
dp[n][W] は値しか保持しない。 復元には選択ビットを記録するか、 逆向きに DP テーブルを辿る処理が必須。
❌ 5. 0/1 と整数版の混同
個数あり版(bounded knapsack)は二進法分割で 0/1 に帰着できるが、 ナイーブに書くと O(nWk) で遅い。

⚠️ 重さを丸める単位で答えが変わる — 10 万人単位では枠を 4.5 万人超える

DP は重さと容量が整数であることを前提にするので、実数や大きな値は「単位」を決めて丸める。上の 🐍 は総人口を 10 万人単位に丸めて容量 200(= 2,000 万人)とした。丸めの単位を 100 万人・10 万人・1 万人・千人と細かくしていくと、選ばれる県と、選んだ県の本当の人口の合計がどう変わるかを確かめる。

🎯 このコードでやること:総人口を 4 通りの単位で整数に丸め、それぞれ 0/1 ナップサックを DP で解いて、出生数の最大値・県数・選んだ県の実際の人口(丸める前)を並べる。

📥 入力例 SSDSE-B-2026 の 2023 年度 47 行(使う列: Prefecture・A1101 総人口・A4101 出生数) 本当の容量: 人口 20,000,000 人 都道府県 A1101(総人口) 100万人単位 10万人単位 1万人単位 愛知県 7,477,000 7 75 748 佐賀県 795,000 1 8 80 福井県 744,000 1 7 74 …(全 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
import numpy as np
import pandas as pd

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', skiprows=[1], encoding='cp932')
d = df[df['SSDSE-B-2026'] == 2023]
names = d['Prefecture'].values
value = d['A4101'].astype(int).values          # 価値 = 出生数
pop = d['A1101'].astype(int).values            # 重さの元 = 総人口(人)
CAP = 20_000_000                               # 本当の容量 = 人口 2,000 万人

def knap(weight, W):                           # 1 次元 DP(容量を後ろから更新)と選んだ品物の復元
    n = len(value)
    dp = np.zeros(W + 1, dtype=np.int64)
    keep = np.zeros((n, W + 1), dtype=bool)
    for i in range(n):
        w = weight[i]
        if w > W:
            continue
        cand = dp[:W + 1 - w] + value[i]
        better = cand > dp[w:]
        keep[i, w:] = better
        dp[w:] = np.where(better, cand, dp[w:])
    c, chosen = W, []
    for i in range(n - 1, -1, -1):
        if keep[i, c]:
            chosen.append(i); c -= weight[i]
    return dp[W], chosen

for unit in [1_000_000, 100_000, 10_000, 1_000]:
    weight = np.round(pop / unit).astype(int)
    W = CAP // unit
    best, chosen = knap(weight, W)
    true_pop = pop[chosen].sum()
    ok = '以内' if true_pop <= CAP else '超過'
    print(f'単位 {unit:>9,} 人: W={W:>6,}  DP 表 {len(value)}×{W + 1:,}  出生数 {best:,}  '
          f'{len(chosen)} 県  実際の人口 {true_pop:,}({ok})')

for unit in [100_000, 10_000]:
    weight = np.round(pop / unit).astype(int)
    _, chosen = knap(weight, CAP // unit)
    print(f'単位 {unit:,} 人で選ばれた県:', sorted(names[chosen]))
📤 実行例(実測) 単位 1,000,000 人: W= 20 DP 表 47×21 出生数 146,139 10 県 実際の人口 22,758,000(超過) 単位 100,000 人: W= 200 DP 表 47×201 出生数 133,606 8 県 実際の人口 20,045,000(超過) 単位 10,000 人: W= 2,000 DP 表 47×2,001 出生数 133,025 8 県 実際の人口 19,994,000(以内) 単位 1,000 人: W=20,000 DP 表 47×20,001 出生数 133,025 8 県 実際の人口 19,994,000(以内) 単位 100,000 人で選ばれた県: ['佐賀県', '愛知県', '沖縄県', '滋賀県', '熊本県', '福岡県', '鳥取県', '鹿児島県'] 単位 10,000 人で選ばれた県: ['愛知県', '沖縄県', '滋賀県', '熊本県', '福井県', '福岡県', '鳥取県', '鹿児島県']

💬 100 万人単位では県ごとの丸め誤差が最大 47.7 万人(愛知県 7,477,000 → 7 百万)になり、10 県で出生数 146,139 人という答えが出るが、実際の人口は 22,758,000 人で枠を 276 万人も超えている。10 万人単位(上の 🐍 と同じ)でも 8 県・133,606 人の組は実際には 20,045,000 人で 4.5 万人超過する。1 万人単位にすると佐賀県が福井県に入れ替わって 133,025 人・19,994,000 人となり枠内に収まり、千人単位にしても答えは変わらない。DP 表は 47 × 201 から 47 × 20,001 に大きくなるが、この規模なら計算は一瞬で終わる。

丸めは「重さを小さく見積もる」方向にも働くので、粗い単位で解いた最適解は本当の制約を破ることがある。丸めた後の解を元の単位で必ず検算し、超えていたら単位を細かくするか、重さを切り上げて(np.ceil)丸めて制約を守る側に倒す。FPTAS はこの丸めを意図的に使う近似で、単位を粗くするほど速いが、価値の誤差の上限を ε で保証する形にしてある。

🧠 理解度チェック — このページの実測値で

Q1. 47 県から支援先を選ぶ組を全部調べるといくつあるか。1 秒に 10 億通り調べると何時間かかるか。

各県を入れる・入れないの 2 通りなので 2⁴⁷ ≈ 1.41 × 10¹⁴ 通り。10⁹ 通り/秒なら約 1.41 × 10⁵ 秒 ≈ 39 時間。DP なら重さを 1 万人単位にしても 47 × 2,001 ≈ 9.4 万個のセルを埋めるだけで済む。

Q2. 上の 🐍 で、DP は出生数 133,606 人、貪欲法は 132,188 人だった。貪欲法が取り逃した割合はいくらか。なぜ取り逃したか。

(133,606 − 132,188) / 133,606 ≈ 1.06%。貪欲法は「出生数 ÷ 重さ」の大きい順に詰めるので、比が中位で重さ 75 の愛知県を後回しにし、残りの容量を小さい県 6 つで埋めてしまう。DP は愛知県 1 県と小さい県 6 県の入れ替えまで含めて比べるので、合計で上回る組を見つける。

Q3. 10 万人単位の DP の答え(8 県・133,606 人)を「人口 2,000 万人以内で出生数が最大の組」と報告してよいか。

よくない。8 県の実際の人口は 20,045,000 人で枠を超えている。1 万人単位で解き直すと、佐賀県の代わりに福井県を入れた 8 県・133,025 人(19,994,000 人)が枠内の最適になる。丸めた重さで解いたら、元の単位で制約を検算してから報告する。

🗺 ナップサック問題 の概念マップ

『ナップサック問題』は『最適化』カテゴリに属する重要概念で、 以下の関連概念群と密接につながっています。

最適化
  ├── 連続最適化(線形計画 LP)
  │   └── 分数ナップサック … 貪欲法(価値/重さの大きい順)で最適
  └── 組合せ最適化(NP 困難な問題群)
      ├── ナップサック問題  ← このページ
      │   ├── 解法: 動的計画法 O(nW)/分枝限定法/整数計画ソルバ(PuLP)
      │   ├── 近似: 貪欲法(0/1 では最適を保証しない)/FPTAS
      │   └── 変種: 個数制限つき・多次元(重さ+体積)
      └── 巡回セールスマン問題(TSP)… 選ぶ問題ではなく並べる問題

完全な概念マップは 🗺 概念マップ で確認できます。

📜 歴史と発展

1897 年に Mathews が初めて研究した古典的問題で、 Bellman の動的計画法(1957)で標準解法が確立。 NP困難であることは Karp の 21 問題(1972)に含まれる。 近年は量子コンピュータ(QAOA)への応用や、 機械学習でのバッチサイズ最適化への転用が活発。

原典は Mathews (1897) で、 コンピュータどころか計算理論より半世紀早い問題です。 Bellman (1957) の動的計画法が標準解法を与え、 Karp (1972) の「21 の NP 完全問題」に含まれたことで難しさが理論的に確定しました。 「解ける(多項式時間の DP がある)」と「NP 困難」が両立しているのが混乱しやすい点で、 鍵は DP の $O(nW)$ が擬多項式——入力の「値」には多項式でも「桁数」には指数——であることです。

ナップサック問題 動的計画法 分枝限定法 FPTAS 整数計画法 貪欲法 組合せ最適化 (前提)

🔗 隣接手法への橋渡し

ナップサック問題は組合せ最適化の典型例として、 問題モデリング (前段) と解法選択 (DP/貪欲/分枝限定) を統合的に設計してこそ実用解が得られる。

上流の品目価値・重みのデータ整備が問題定義そのものを決め、 並列の貪欲法/分枝限定法/メタヒューリスティクス (GA) と比較して問題規模に応じた解法を選び、 下流の感度分析で容量制約変更時の解の頑健性を確認する流れで実務適用が固まる。

🌳 手法選択フロー

ナップサック問題 を実際の課題に当てはめるとき、 用語固有の判断軸に沿って次の 3 段階で適切な選択を行う。

  1. 品目数 N ≤ 100 で容量も整数か? Yes → 動的計画法 (O(NW))、 No → 次へ
  2. 厳密最適解が必須か? Yes → 分枝限定法 / 整数計画ソルバ (CPLEX/Gurobi)、 No → 次へ
  3. 近似解で十分か? Yes → 貪欲法 (価値/重み比) / 遺伝的アルゴリズム、 No → 問題定式化を見直し

このフローは組合せ最適化問題の典型的選択軸。 0-1 ナップサックは NP 困難だが、 整数容量なら DP で多項式時間 (O(NW)) で解ける点が実務でよく利用される。

🔎 解説を深める ── ナップサック問題を「詰め方」と「計算量」から捉え直す

組合せ最適化の一般論(→ 姉妹ページ)とは重ならないよう、ここでは 0-1 ナップサック固有の 2 つの落とし穴——「比の良い順に詰める貪欲が最適とは限らない」ことと、「O(NW) は見かけほど速くない(擬多項式)」こと——を、SSDSE-B-2026 の実測値で確かめる。

🧭 直感 ── 分数なら貪欲=最適、しかし 0-1 では割れない

品物を 切り分けてよい「分数ナップサック」では、価値/重み比の大きい順に詰めるだけで厳密に最適になる(連続緩和。これは古典的な貪欲最適の一例)。ところが 0-1 ナップサックでは品物を割れない。すると「比は最高だが大きすぎて入らない品物」や「詰め終わった後に残る中途半端な空き容量」が生じ、貪欲が取りこぼす。0-1 が NP 困難で、分数版が多項式時間で解けるのは、まさにこの「割れない」制約が本質だからである。

⚠️ 落とし穴(重要)── 実データで貪欲が負け、W が大きいと DP も詰む

SSDSE-B-2026・2023 年・47 都道府県で、総人口(A1101)を「重み=人口予算の消費」、15 歳未満人口(A1301)を「価値=支援したい子ども数」とみなし、人口予算 W の枠内で子ども数を最大化する 0-1 ナップサックを実際に解いた(人口・子ども数を万人単位に四捨五入し、厳密 DP と「比 v/w が高い順」の貪欲を比較。値はすべて実測値で、合成データは使っていない)。予算 W = 2050 万人での結果(貪欲と最適の差が最大になる容量):

解法子ども数の合計(価値)使った人口(重み)選んだ県数
🎯 動的計画法(厳密最適)263 万人2050 万人(枠ぴったり)8 県
⚡ 貪欲法(比 v/w 順)254 万人1999 万人(51 万人ぶん枠が余る)12 県

貪欲は比が最も高い 沖縄県(子ども/人口 ≈ 0.163、47 都道府県で最高)から順に小さい県を詰めるため、最後に 51 万人ぶんの枠を余らせ、DP 最適より 約 9 万人の子どもを取りこぼす(263 − 254 = 9 万人)。「比が良い順」という直感がそのまま損失になる典型例だ。なお同じデータでも W = 3000 万人だと差はわずか 1 万人(DP 376 対 貪欲 375)——ratio がほぼ均質なデータでは貪欲が最適に極めて近く見えるが、決して一致はしない。この「ほぼ合うから貪欲でよい」という油断こそ最大の罠である。

もう一つの罠が計算量。DP の O(NW) は多項式に見えるが、W は容量の“値”であって桁数(入力サイズ)ではない。上の例で人口を「万人」に丸めたから W=2050 で済んだが、1 人単位(W=2050 万)にした瞬間、DP 表は約 2050 万列に膨れ上がる。入力の数値が大きいだけで爆発するこの性質を 擬多項式(pseudo-polynomial)と呼び、0-1 ナップサックが NP 困難でありながら DP で解けることの正体でもある。単位・スケーリングの選び方が実行可能性を左右する。

🚀 発展 ── 上界・厳密解・近似のグラデーション

実務では次の順で手を替える。(1) 連続緩和で上界: 分数ナップサックを解いた値は 0-1 最適の上界になり、分枝限定法の枝刈りに使える。(2) 価値でDP: 容量 W が巨大でも価値の総和 V が小さいなら、O(N²·Vmax) の「最小重みで価値 v を達成する DP」に切り替えると速い(W と V のどちらが小さいかで DP の軸を選ぶ)。(3) FPTAS: 価値を丸めて誤差 ε 以内を保証する完全多項式時間近似スキームが存在する(0-1 ナップサックは近似しやすい NP 困難問題の代表)。(4) 多制約化: 重み軸が 2 本以上(人口かつ面積など)になると「多次元ナップサック」となり DP が一気に苦しくなり、整数計画ソルバ(分枝限定)が現実的になる。

🔗 関連ページ

📝 補足: 上の数値は data/raw/SSDSE-B-2026.csv を pd.read_csv(encoding='cp932', skiprows=[1]) で読み、df[df['SSDSE-B-2026']==2023] の 47 都道府県について A1101(総人口)・A1301(15 歳未満人口)を万人単位に四捨五入し、厳密 DP と貪欲を Python で実行して得た実測結果。架空・合成データは含まない。