ナップサック問題#

JijZeptSolverとJijModelingを用いて、ナップサック問題を解く方法を紹介します。 この問題はLucas, 2014, "Ising formulations of many NP problems" 5.2. Knapsack with Integer Weightsなどでも取り上げられています。

ナップサック問題とは#

ナップサック問題は、以下のような場合において最適解を見つける問題です。 この問題はNP困難な整数計画問題として知られています。 ナップサック問題の具体例として、以下のストーリーを考えてみましょう。

ある探検家が洞窟を冒険していると、思いがけず宝物を見つけました。

Treasure A

Treasure B

Treasure C

Treasure D

Treasure E

Treasure F

Price

$5000

$7000

$2000

$1000

$4000

$3000

weight

800g

1000g

600g

400g

500g

300g

しかし、探検家が持っているのは容量2キロの小さなナップサックだけでした。当然、探検家はこのナップサックに出来るだけ価値の高い宝物を詰め込みたいと考えます。探検家は、どの宝物をナップサックに詰め込めば良いでしょうか?

このような状況下で最適解を求めるのがナップサック問題です。ナップサック問題はNP困難な整数計画問題として有名なものの1つです。

定式化#

上の例を一般化して数理モデルを定式化してみましょう。ナップサックに入れるアイテム(宝物)の集合を \(\{ 0, 1, \dots, i, \dots, N-1 \}\) とし、各アイテム \(i\) の価値を \(v_i\)、重量を \(w_i\) と書くことにします。

\[ v = \{v_0, v_1, \dots, v_i, \dots, v_{N-1}\} \]
\[ w = \{w_0, w_1, \dots, w_i, \dots, w_{N-1}\} \]

さらに、 アイテム \(i\) を詰め込むかどうかを表現する決定変数として \(x_i\) を導入します。 \(x_i\) はアイテム \(i\) をナップサックに詰め込む場合に1を取り、そうでない場合には0になるバイナリ変数です。また、ナップサックの容量を \(W\) で表すことにします。
ナップサックに入れるアイテムの価値の合計を最大化することを目指します。 そのために、ナップサックに入れるアイテムの価値の合計を数式で表現しましょう。 そしてナップサックの容量制限を数式で表現する必要があります。 最終的に、この問題の数理モデルは以下のようになります。

\[ \max \quad \sum_{i=0}^{N-1} v_i x_i \tag{1} \]
\[ \mathrm{s.t.} \quad \sum_{i=0}^{N-1} w_i x_i \leq W \tag{2} \]
\[ x_i \in \{0, 1\} \quad (\forall i \in \{0, 1, \dots, N-1\}) \tag{3} \]

JijModelingによる定式化#

では、JijModelingを使って上記の数理モデルを実装してみましょう。まず、ナップサック問題は目的関数を最大化する問題であるため、 sense=jm.ProblemSense.MAXIMIZE を設定します。

import jijmodeling as jm

problem = jm.Problem("Knapsack", sense=jm.ProblemSense.MAXIMIZE)

次に、数理モデルに現れる変数およびパラメーターを定義します。

v = problem.Float("v", ndim=1)
N = problem.DependentVar("N", v.len_at(0))
w = problem.Float("w", shape=(N,))
W = problem.Float("W")
x = problem.BinaryVar("x", shape=(N,))

v はアイテム \(i\) の価値 \(v_i\) のリスト、 w はアイテム \(i\) の重量 \(w_i\) のリストを定義しており、 N はアイテムの個数、 W はナップサックの容量を定義しています。また、\(x_i\) に相当するバイナリ変数として x を定義しています。

目的関数の実装#

目的関数として式(1)を実装すると以下のようになります。

problem += jm.sum(v * x)

制約条件の実装#

制約条件として式(2)を実装すると以下のようになります。

problem += problem.Constraint("weight", jm.sum(w * x) <= W)

これで数理モデルの実装は完了です。正しく数理モデルが実装されているかをLaTeX表示を通して確認してみましょう。

problem
\[\begin{split}\begin{array}{rl} \text{Problem}\colon &\text{Knapsack}\\\displaystyle \max &\displaystyle \sum _{\vec{\imath }}{{{\left(v\cdot x\right)}}_{\vec{\imath }}}\\&\\\text{s.t.}&\\&\begin{aligned} \text{weight}&\quad \displaystyle \sum _{\vec{\imath }}{{{\left(w\cdot x\right)}}_{\vec{\imath }}}\leq W\end{aligned} \\&\\\text{where}&\\&\text{Decision Variables:}\\&\qquad \begin{alignedat}{2}x&\in \mathop{\mathrm{Array}}\left[N;\left\{0, 1\right\}\right]&\quad &1\text{-dim binary variable}\\\end{alignedat}\\&\\&\text{Placeholders:}\\&\qquad \begin{alignedat}{2}v&\in \mathop{\mathrm{Array}}\left[(-);\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\W&\in \mathbb{R}&\quad &\text{A scalar placeholder in }\mathbb{R}\\w&\in \mathop{\mathrm{Array}}\left[N;\mathbb{R}\right]&\quad &1\text{-dimensional array of placeholders with elements in }\mathbb{R}\\\end{alignedat}\\&\\&\text{Dependent Variables:}\\&\qquad \begin{alignedat}{2}N&=\mathop{\mathtt{len\_{}at}}\left(v,0\right)&\quad &\in \mathbb{N}\\\end{alignedat}\end{array} \end{split}\]

インスタンスデータの準備#

次にインスタンスデータを準備します。ここでは100個のアイテムをランダムに生成し、ナップサックの容量を100とするものとします。

import numpy as np

inst_v = np.random.randint(5,30,100)
inst_w = inst_v + np.random.randint(-2,20,100)
inst_W = 100
instance_data = {"v": inst_v, "w": inst_w, "W": inst_W}

inst_v , inst_w, inst_Wはそれぞれアイテムの価値のリスト、アイテムの重量のリスト、ナップサックの容量を表します。

ナップサック問題を解く#

jijzept_solverを用いて、この問題を解きましょう。

import jijzept_solver

instance = problem.eval(instance_data)
solution = jijzept_solver.solve(instance, solve_limit_sec=1.0)

解の可視化#

得られた解を用いて、詰め込んだアイテムの価値と重量を表示してみましょう。

df = solution.decision_variables_df
obj = solution.objective
indices = np.ravel(df[df["value"]==1]["subscripts"].to_list())
sum_w = np.sum(inst_w[indices])

print("Values of chosen items: ", inst_v[indices])
print("Weights of chosen items: ", inst_w[indices])
print("Total value from objective: ", obj)
print("Total weight: ", sum_w)
Values of chosen items:  [26  6 15 10  7 29 21]
Weights of chosen items:  [24  4 13  8  5 27 19]
Total value from objective:  114.0
Total weight:  100

参考資料#

[1] Lucas, 2014, "Ising formulations of many NP problems"