問題
$N$ 個のアイテムと $D$ 次元の容量制約がある多次元ナップサック問題。アイテム $i$ は価値 $v_i$、各次元 $d$ の重さ $w_{i,d}$ を持つ。以下を求めよ:
- LP 緩和解($x_i \in [0,1]$)の最適値
- LP 解を用いたランダム化丸めを $T$ 回繰り返し、実行可能かつ最大の整数解
制約
$1 \le N \le 50$
$1 \le D \le 5$
$1 \le T \le 100$
$1 \le v_i, w_{i,d}, C_d \le 500$
時間制限: 3sec / メモリ: 256MB
入出力例
入力例 1
4 2 100
10 6 12 8
2 3
1 2
4 1
3 2
6 5
出力例 1
22
最適解: アイテム1+3を選択 → 価値10+12=22、制約: 2+4=6≤6, 3+1=4≤5
概念図: LP 緩和からランダム化丸めへのフロー
ヒント(段階的開示)
ヒント1: LP 緩和の意味
整数制約 $x_i \in \{0,1\}$ を $x_i \in [0,1]$ に緩和すると線形計画問題になり多項式時間で解ける。LP 最適値は整数最適値の上界。
ヒント2: ランダム化丸めのアイデア
LP 解 $x_i^* \in [0,1]$ を各 $x_i$ が 1 になる確率として使う。独立にサンプリングして実行可能解を T 回生成し最大値を採用。制約を破る場合はその解を棄却。
ヒント3: scipy を使った実装
from scipy.optimize import linprog
import numpy as np
c = -v # 最小化 → 最大化
A_ub = W.T # shape (D, N)
b_ub = C
bounds = [(0, 1)] * N
result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
x_star = result.x # LP 解 x_i* in [0,1]
# ランダム化丸め
x = [1 if random.random() < x_star[i] else 0 for i in range(N)]
if np.all(W.T @ np.array(x) <= C + 1e-9):
val = int(v @ np.array(x))
模範解答 (Python)
import sys
import random
import numpy as np
from scipy.optimize import linprog
input = sys.stdin.readline
def solve():
N, D, T = map(int, input().split())
v = list(map(int, input().split()))
W = []
for _ in range(N):
W.append(list(map(int, input().split())))
C = list(map(int, input().split()))
v = np.array(v, dtype=float)
W = np.array(W, dtype=float) # shape (N, D)
C = np.array(C, dtype=float)
# LP 緩和: maximize v@x s.t. W.T @ x <= C, 0<=x<=1
c = -v
A_ub = W.T # shape (D, N)
b_ub = C
bounds = [(0, 1)] * N
result = linprog(c, A_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs')
if result.success:
x_star = result.x
else:
x_star = np.ones(N) * 0.5 # フォールバック
# ランダム化丸め T 回
best = 0
for _ in range(T):
x = np.array([1 if random.random() < x_star[i] else 0 for i in range(N)])
usage = W.T @ x
if np.all(usage <= C + 1e-9):
val = int(v @ x)
if val > best:
best = val
# 貪欲(保険)
greedy_x = np.zeros(N, dtype=int)
order = sorted(range(N), key=lambda i: -v[i])
usage = np.zeros(D)
for i in order:
if np.all(usage + W[i] <= C + 1e-9):
greedy_x[i] = 1
usage += W[i]
g_val = int(v @ greedy_x)
best = max(best, g_val)
print(best)
solve()
Step-by-Step 解説
1LP 緩和の定式化
整数制約 $x_i \in \{0,1\}$ を $x_i \in [0,1]$ に緩和。これは多項式時間で解ける。LP 最適値 ≥ 整数最適値(緩和なので)。
整数制約 $x_i \in \{0,1\}$ を $x_i \in [0,1]$ に緩和。これは多項式時間で解ける。LP 最適値 ≥ 整数最適値(緩和なので)。
2scipy.optimize.linprog の使い方
linprog は最小化問題。maximize v@x = minimize -v@x。制約 A_ub @ x <= b_ub に対応。W の形状は (N, D) なので A_ub = W.T (D, N)。
3ランダム化丸めのアイデア
LP 解 $x_i^* \in [0,1]$ を各 $x_i$ が 1 になる確率として使う。独立にサンプリングして実行可能解を T 回生成し最大値を採用。
LP 解 $x_i^* \in [0,1]$ を各 $x_i$ が 1 になる確率として使う。独立にサンプリングして実行可能解を T 回生成し最大値を採用。
4実行可能性確保
ランダム丸めで制約を破る場合があるため実行可能性をチェック。破っている場合はその解を棄却(貪欲の保険あり)。
ランダム丸めで制約を破る場合があるため実行可能性をチェック。破っている場合はその解を棄却(貪欲の保険あり)。
5近似保証
理論的に $D=1$ の場合はランダム化丸めで近似比が保証される。多次元では保証は弱いが実用的。貪欲を保険として組み合わせる。
理論的に $D=1$ の場合はランダム化丸めで近似比が保証される。多次元では保証は弱いが実用的。貪欲を保険として組み合わせる。
計算量
LP 解法: $O(N^{2.5} D)$(内点法)
ランダム化丸め 1 回: $O(ND)$
全体: $O(N^{2.5} D + T \cdot ND)$
$N=50, D=5, T=100$ では十分高速
ランダム化丸め 1 回: $O(ND)$
全体: $O(N^{2.5} D + T \cdot ND)$
$N=50, D=5, T=100$ では十分高速
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| A_ub の形状ミス | W.T を忘れて W のまま使う | A_ub = W.T(shape D×N) |
| 最大化を最小化に変換し忘れ | linprog は最小化 | c = -v |
| 浮動小数点誤差で実行可能性チェック失敗 | <= C のみ | <= C + 1e-9 |
| ランダム化丸めで全解が棄却される | T が少ない or LP 解が極端 | 貪欲解を保険として追加 |
次のステップ
- 発展: 分枝限定法(Branch and Bound)で多次元ナップサックの厳密解
- 応用: 半整数丸め(x_i > 0.5 なら 1、else 0)との比較
- 類題: セットカバー問題の LP 緩和 + ランダム化丸め(O(log n) 近似)