数理最適化セミナーのご案内

2.5 ナップサック問題

 ナップサック問題は,ナップサックの中にいくつかの品物を詰め込み入れた品物の総価値を最大にするという問題です.ただし,ナップサックと品物にはそれぞれ容量やサイズが与えられていて,入れた品物のサイズの総和がナップサックの容量を超えてはいけないという条件があります.この問題は,組合せ最適化問題の代表的な例の一つとしてよく知られていて,プロジェクトの選択や物資の購入などの問題に応用されています.以下は,整数ナップサック問題と呼ばれるものです.なお,0-1ナックサック問題につきましては,本節の最後で紹介します.

例題

 容量65のナップサックに次の表にある品物を詰め込むことにします.この時,詰め込んだ品物の総価値を最大にするためには何をいくつ詰め込むと良いでしょうか.ただし,同じ品物を何個詰め込んでも良いものとします.

品物 1個あたりの価値 1個あたりのサイズ
缶コーヒー 120 10
水入りペットボトル 130 12
バナナ 80 7
りんご 100 9
おにぎり 250 21
パン 185 16

 まず,変数は各品物を詰め込む個数です.よって,この変数は整数値しか取らないということになります.ただし,「-1個詰め込む」というようなありえない答えを排除する必要があります.このため,各変数は0以上の値しか取らないということを制約条件として明示しておく必要があります.なお,「0個詰め込む」は「その品物を詰め込まない」と解釈します.

 次に,最大化することになる目的関数は詰め込んだものの総価値です.これは,各品物について「1個あたりの価値」と「その品物を詰め込んだ個数」の積を求め,その総和を取ることで表現できます.

 制約条件は,先ほど述べた変数に関するものの他に,詰め込んだ品物のサイズの総和がナップサックの容量を超えないというものがあります.目的関数の時と同様に考えると,各品物に関する「1個あたりのサイズ」と「その品物を詰め込んだ個数」の積の総和をとると詰め込んだ品物のサイズの総和が得られます.よって,この総和がナップサックの容量である65を超えないということを式で表せばよいことになります.

 以上のことから,この例題は次のように定式化することが出来ました.

整数変数
$coffee$ 缶コーヒーの個数
$water$ 水入りペットボトルの個数
$banana$ バナナの個数
$apple$ りんごの個数
$rice\_ball$ おにぎりの個数
$bread$ パンの個数
 
目的関数(最大化)
$120coffee + 130water + 80banana + 100apple + 250rice\_ball + 185bread$ 総価値を最大化する
 
制約条件
$10coffee + 12water + 7banana + 9apple + 21rice\_ball + 16bread \le 65$ 容量に関する制約
$coffee \ge 0$ 缶コーヒーは0個以上詰め込む
$water \ge 0$ 水入りペットボトルは0個以上詰め込む
$banana \ge 0$ バナナは0個以上詰め込む
$apple \ge 0$ りんごは0個以上詰め込む
$rice\_ball \ge 0$ おにぎりは0個以上詰め込む
$bread \ge 0$ パンは0個以上詰め込む

 定式化した結果をC++SIMPLEで記述すると次のようになります.

// 整数変数を宣言する
IntegerVariable coffee(name = "缶コーヒーの個数");
IntegerVariable water(name = "水入りペットボトルの個数");
IntegerVariable banana(name = "バナナの個数");
IntegerVariable apple(name = "りんごの個数");
IntegerVariable rice_ball(name = "おにぎりの個数");
IntegerVariable bread(name = "パンの個数");

// 総価値を最大化する
Objective total_value(name = "総価値", type = maximize);
total_value = 120 * coffee + 130 * water + 80 * banana + 100 * apple +
              250 * rice_ball + 185 * bread;

// 容量に関する制約
10 * coffee + 12 * water + 7 * banana + 9 * apple + 21 * rice_ball + 16 * bread <= 65;

// 各品物は 0 個以上詰め込む
coffee >= 0;
water >= 0;
banana >= 0;
apple >= 0;
rice_ball >= 0;
bread >= 0;

// 求解し結果を出力する
solve();
coffee.val.print();
water.val.print();
banana.val.print();
apple.val.print();
rice_ball.val.print();
bread.val.print();
total_value.val.print();

 このモデルをNuorium Optimizerで実行すると,最後に

缶コーヒーの個数=3
水入りペットボトルの個数=0
バナナの個数=2
りんごの個数=0
おにぎりの個数=1
パンの個数=0
総価値=770

という表示がされます.そして,この表示から「缶コーヒーを3個,バナナを2個,そしておにぎりを1個詰め込むと良い」というこの例題の答えを確認できます.

 

 ところで,このモデルについて品物の種類などを変更したい場合C++SIMPLEでの記述を修正する箇所が多く大変な手間がかかってしまいます.この対策として,ナップサックの容量,品物の価値および品物のサイズを別に用意したdatファイルから与えることにします.このようにすることで,C++SIMPLEでの記述が汎用的なものになり,品物の種類が変わったとしてもdatファイルの変更のみで対応できるようになります.そのために,ここでは「品物の集合」という概念を導入します.すると定式化は次のように書き直すことができます.

集合
$Object=\{缶コーヒー,水入りペットボトル,バナナ,りんご,おにぎり,パン\}$ 品物の集合
 
整数変数
$quantity_i, i \in Object$ 品物iを詰め込む個数
 
定数
$capacity$ ナップサックの容量
$value_i, i \in Object$ 品物iの1個あたりの価値
$weight_i, i \in Object$ 品物iの1個あたりのサイズ
 
目的関数(最大化)
$\displaystyle \sum_{i \in Object} value_i \cdot quantity_i$ 総価値を最大化する
 
制約条件
$\displaystyle \sum_{i \in Object} weight_i \cdot quantity_i \le capacity$ 容量に関する制約
$quantity_i \ge 0, \forall i \in Object$ 各品物は0個以上詰め込む

 この定式化をC++SIMPLEで記述すると,以下のような簡潔なものになります.なお,品物の集合の具体的な要素についてはNuorium Optimizerではdatファイルから自動的に認識します.

// 集合と添字
Set Object;
Element i(set = Object);

// パラメータ
Parameter capacity(name = "ナップサックの容量");
Parameter value(name = "品物の価値", index = i);
Parameter size(name = "品物のサイズ", index = i);

// 変数
IntegerVariable quantity(name = "詰め込む個数", index = i);

// 目的関数
Objective total_value(name = "総価値", type = maximize);
total_value = sum(value[i] * quantity[i], i);

// 容量に関する制約
sum(size[i] * quantity[i], i) <= capacity;

// 各品物は 0 個以上詰め込む
quantity[i] >= 0;

// 求解
solve();

// 結果出力
quantity.val.print();
total_value.val.print();

 なお,今回の例題についてのdatファイルは次のようになります.

ナップサックの容量 = 65;

品物の価値 =
[缶コーヒー] 120
[水入りペットボトル] 130
[バナナ] 80
[りんご] 100
[おにぎり] 250
[パン] 185
;

品物のサイズ =
[缶コーヒー] 10
[水入りペットボトル] 12
[バナナ] 7
[りんご] 9
[おにぎり] 21
[パン] 16
;

 最後に,この例題では「缶コーヒーが3個」というように容量が許す限り同じ品物を何個でも詰め込むことができました.それでは,各品物についてナップサックに詰め込むことができるのは1個だけということにするとどのようになるでしょうか.なお,この制限を加えると0-1ナップサック問題という典型的な0-1整数計画問題になります.C++SIMPLEでは,0-1変数であるという宣言を簡単に行うことができます.具体的には,先ほど汎用化させたC++SIMPLEモデル中で変数を宣言している部分を

IntegerVariable quantity(name="詰め込む個数",index=i, type=binary);

とするだけです.さらに,この宣言から各変数がとりうる値は0か1しかないということが明らかなため,各品物は0個以上詰め込むという制約「quantity[i]>=0;」を記述する必要がなくなります.以上の点についてモデルファイルを書き換えた上で,Nuorium Optimizerで実行させると,

詰め込む個数["おにぎり"] = 1
詰め込む個数["りんご"] = 1
詰め込む個数["バナナ"] = 1
詰め込む個数["パン"] = 1
詰め込む個数["缶コーヒー"] = 0
詰め込む個数["水入りペットボトル"] = 1
総価値 = 745

という結果が得られ,缶コーヒー以外の品物を詰め込むと良いという事がわかります.


 

 

上に戻る