2. ナップサック問題と列生成法#

2.1. 背景#

ナップサック問題には次の側面がある.

  • 数理最適化問題として線形に定式化をする場合の典型的な考え方を提供してくれる.

  • 数式が素直で基本的であるため,別の問題の文脈の中で,ナップサック問題としての構造を見出しやすい.

後者について,ここでは列生成法との関連を述べる.

2.1.1. 列生成法と連続緩和問題#

列生成法そのものは線形最適化問題を直接に解くというよりは,連続緩和の文脈でよく用いられる. 整数最適化問題 \(P_{\mathbb{Z}}\) のように,応用範囲が広く,しかし問題規模が大きくなりやすい場合が典型的である. この場合,問題 \(P_{\mathbb{Z}}\) を連続緩和した問題 \(P\) を考え,列生成法を適用してなるべく少ない変数 (または制約条件式) で求解することになる.

これから説明することは問題の定式化を違った側面からアプローチすることもあって, 抽象性が高くそして多分に技巧的な面があろう. それ故に一つずつ整理して述べたい.

2.2. 設定#

列生成法の典型的な応用例として,ビンパッキング問題を採り上げよう. 問題設定が単純であるため,列生成法のアルゴリズムに集中でき,教育的でもあると考えられる.

2.2.1. ビンパッキング問題#

2.2.1.1. straight forward な定式化#

ビンパッキング問題は次のように容易に定式化できる.

集合・要素
  • アイテム \(i\) とアイテム集合 \(I\)

    (1)#\[i\in I\]
  • ビン \(b\) とビン集合 \(B\)

    (2)#\[b\in B\]
定数
  • アイテム \(i\) の容量

    (3)#\[IVOL_i\]
  • ビン \(b\) の共通する容量

    (4)#\[BVOL\]
変数
  • アイテム \(i\) をビン \(b\) に入れるかどうか.

    (5)#\[z_{i,b} \in \mathbb{B}\]
  • ビン \(b\) を使用するかどうか.

    (6)#\[use_b \in \mathbb{B}\]
制約条件
  • アイテム \(i\) は必ず何れかのビンに入れる.

    (7)#\[\sum_{b\in B} z_{i,b} = 1, \, \forall i\in I\]
  • ビンへあふれずに入れる.

    (8)#\[\sum_{i\in I} IVOL_i\cdot z_{i,b} \leq BVOL\cdot use_b, \, \forall b\in B\]
目的関数

ビンの使用数を最小化

(9)#\[\mathrm{Minimize:}~ \sum_{b\in B} use_b\]

以上の定式化は straight forward な定式化である. 幾つかのコメントをしよう.

  • アイテム一つにつきビンを一つ割り当てることを考えれば,必要なビンの総数の上限値は,アイテムの総数 \(N=|I|\) に等しい.

    (10)#\[|B| = |I| = N\]
  • 変数の総数のオーダーは \(O(|B||I|+|I|)=O(N^2)\) である.アイテムが \(10^2\) 個あれば,\(O(10^4)\) となってそれなりに大きな規模になる.

  • \(|B|=|I|\) という見積もりは最悪の場合を想定したものであり,実際のアイテム集合がビンの容量にほぼ等しいものだけで構成されていることは特殊なケースである.つまり変数が多過ぎる可能性がある.殆どが \(0\) 値をとる active でない変数と予想される.

  • 制約条件式の総数のオーダーは \(O(|I| + |B|)=O(N)\) であり,線形にスケールする.

これらのことから,ビンパッキング問題を上記の定式化で直接に求解するのは, 比較的小さな規模が現実的であり,もう少し大きな規模となると耐えられないと考えられる. 一方で無駄な変数が多そうであるから,列生成法が有用ではないかと考えられる.

2.2.1.2. パターンという観点でみた定式化#

straight forward な定式化を忘れて,違うアプローチによる定式化を試みよう. それはパターンという観点を中心に問題の離散構造を覆いつくす定式化である.

ビンパッキング問題を今一度思い出そう. ビンにアイテムを入れる方法にはいろいろな方法がある. これらをすべて列挙するとき,その中の何れか一つ以上の方法が最適解を与えていることは (実行可能である限り) 間違いない. パターンとはここで列挙するところの複数のビンにアイテムを入れる方法のことである.

例えば次を満たすビン集合 \(B\) とアイテム集合 \(I\) があったとしよう.

(11)#\[\begin{split}BVOL &= 5, \\ I &= \{1,2,3\}, \\ (IVOL_1, IVOL_2, IVOL_3) &= (1, 2, 4)\end{split}\]

このときアイテムを選び出すパターンをすべて列挙すると,次のパターンだけある.

パターン番号

パターン

合計容量

一つのビンに入るかどうか

\(0\)

\(\emptyset\)

\(0\)

yes

\(1\)

\(\{1\}\)

\(1\)

yes

\(2\)

\(\{2\}\)

\(2\)

yes

\(3\)

\(\{3\}\)

\(4\)

yes

\(4\)

\(\{1,2\}\)

\(3\)

yes

\(5\)

\(\{1,3\}\)

\(5\)

yes

\(6\)

\(\{2,3\}\)

\(6\)

no

\(7\)

\(\{1,2,3\}\)

\(7\)

no

このうち,興味あるパターンはアイテムを使用し,与えられたビンの容量に収まるパターンである. つまりパターン番号 \(1,2,3,4,5\) である. 今回はビンの容量と各アイテムの容量から,一部のパターンのみが許容されたが, それら容量の値によってはパターン番号 \(0\) を除いてすべて許容されうる. そして「パターン一つにつき,一つのビンが対応している」ということに注意しよう.

これらのことからビンパッキング問題について次の定式化を考え出すことができる. 但し straight forward な定式化で既に定義したものは,繰り返しとなるため再掲していない.

集合・要素
  • ビン一つに収まるパターンのパターン番号 \(p\) とそれらからなるパターン番号集合 \(PATTERNS\)

    (12)#\[p\in PATTERNS\]
定数
  • パターン番号 \(p\) のパターンがアイテム \(i\) を含んでいるかどうか,というパターン番号とアイテムの関連付け情報 \(ASSOC\)

    (13)#\[ASSOC_{p,i}\]
変数
  • パターン番号 \(p\) を採用するかどうか.

    (14)#\[x_p \in \mathbb{B}\]
制約条件
  • アイテム \(i\) は必ず何れか一つ以上のパターン番号 \(p\) のパターンで選ぶ.

    (15)#\[\sum_{p\in PATTERNS} ASSOC_{p,i} x_p \geq 1, \, \forall i\in I\]
目的関数

パターン数の最小化 (ビンの使用数を最小化)

(16)#\[\mathrm{Minimize:}~ \sum_{p\in PATTERNS} x_p\]

注釈

制約条件式 (15) より,ある一つのアイテムが一つしかないにもかかわらず,解によっては複数のビンに入れたパターンが選び出されうる定式化になっている. この場合でもビンの総数は正しくカウントされているため,後処理でアイテムの重複さえ除去できれば,どのビンにどのアイテムを入れるかという解を得ることができる. この後処理はビンパッキング問題よりも難しいとも考えられないし,また運用によって許容される解かもしれない. 定式化にはこの種の冗長性がしばしば伴う.

上記の定式化についてもオーダーを見積もろう. パターンの数はアイテムの総数 \(N=|I|\) に対して,パターンの総数のオーダーは \(O(2^N)\) と指数的である.

(17)#\[\sum_{l=1}^N {N\choose l} = \sum_{l=0}^N {N\choose l} - {N\choose 0} = (1+1)^N - 1 = 2^N - 1\]

つまり変数の総数のオーダーは指数的にスケールする.一方で制約条件式の総数のオーダーは \(O(|I|)=O(N)\) と線形にスケールする. 今回の定式化もまた変数の総数が多い. また straight forward の定式化と同様に,無駄なパターンが数多くあるとも考えられる. よって再び列生成法が有用ではないかと考えられるわけである 1変数の数が多い場合には疎な記述も検討すべきであるが,今回のビンパッキング問題では任意の入力データに対して,疎な構造を見出すのは難しい.

2.3. パターンと列生成法#

パターンという観点は列生成法と相性が良い. というのも,列生成法では次の価格付け問題を解く必要があったが,そこにナップサック問題としての構造を見出すことができるからである.

(18)#\[\min_{j\in J\setminus J_k} (\vec{\gamma})_j = \min_{j\in J\setminus J_k} \left((\vec{c})_j - \sum_{i\in I}A_{ji}(\vec{\omega}_k^*)_i\right)\]

パターンを用いたビンパッキング問題の定式化の場合,考える主問題の標準形 \(P(\vec{b},A,\vec{c};I,J)\) および上記の問題の諸量は次のように対応する. 但し \(PATTERNS_k(\subset PATTERNS)\) は列生成法での \(k\) 反復目のパターン番号集合である.

(19)#\[\begin{split}i \in I &\to i\in I, \\ (\vec{b})_i &\to 1, \\ j\in J &\to p\in PATTERNS, \\ J_k &\to PATTERNS_k, \\ (\vec{c})_j &\to 1, \\ A_{ji} &\to ASSOC_{p,i}\end{split}\]

よって考えるべき価格付け問題はパターン番号 \(p\) を通した \(ASSOC_{p,i}\) の自由度のみが変量とわかる. これから目的関数については最小化問題から次の最大化問題として焼き直すことができる.

(20)#\[\max_{p\in PATTERNS\setminus PATTERNS_k} \sum_{i\in I}ASSOC_{p,i}(\vec{\omega}_k^*)_i\]

ここまで整理したが,もう少し \(ASSOC_{p,i}\) について着目しよう. \(ASSOC_{p,i}\) とは「ビンに収まるパターン集合の中で,アイテム \(i\) がパターン番号 \(p\) のパターンに属しているかどうか」を表した. 今,これを変数として扱いたいので,このために次の変数を定義しよう.

変数

アイテム \(i\) を使用するかどうか.

(21)#\[y_i\in \mathbb{B}\]

これはパターンとはアイテム集合の各アイテムを選び出すかということと等価であることを用いている.

(22)#\[\mathrm{パターン番号}~p \leftrightarrow \mathrm{パターン} \leftrightarrow \{y_i\}_{i\in I}\]

例えば (11) のインスタンスの場合,パターンは \((y_1,y_2,y_3)\)\(0,1\) の並びと下表のように一対一対応する.

パターン番号

パターン

\((y_1,y_2,y_3)\)

\(0\)

\(\emptyset\)

\((0,0,0)\)

\(1\)

\(\{1\}\)

\((1,0,0)\)

\(2\)

\(\{2\}\)

\((0,1,0)\)

\(3\)

\(\{3\}\)

\((0,0,1)\)

\(4\)

\(\{1,2\}\)

\((1,1,0)\)

\(5\)

\(\{1,3\}\)

\((1,0,1)\)

\(6\)

\(\{2,3\}\)

\((0,1,1)\)

\(7\)

\(\{1,2,3\}\)

\((1,1,1)\)

これら対応の中で問題で扱うパターン番号集合 \(P\)\(P=\{1,2,3,4,5\}\) であったが, このために必要な \(\{y_i\}_{i=1}^3\)\(IVOL_1\cdot y_1 + IVOL_1\cdot y_1 + IVOL_1\cdot y_3 \leq BVOL=5\) を満たす \(\{y_i\}_{i=1}^3\) のことである.これは数理最適化問題の文脈では変数に課せられる制約条件に他ならない.

以上から,故に次のナップサック問題に価格付け問題を変形できたとわかる.

(23)#\[\begin{split}& \mathrm{Maximize:}~ \sum_{i\in I}(\vec{\omega}_k^*)_i\cdot y_i \\ & \mathrm{subject~to:}~ \\ & \sum_{i\in I} IVOL_i\cdot y_i \leq BVOL\end{split}\]

2.4. 列生成法の実装#

PySIMPLE でビンパッキング問題を列生成法によって解く実装をしよう. 列生成法が汎用的な枠組みでもあることから,共通化可能な部分についても確認しておこう.

2.4.1. 設計概要#

振り返りのために,列生成法の手続きを次にまとめておこう.

  1. \(k=1\) とする.

  2. 主問題 \(P\) のうち,適当な数の変数を選び出し,\(J_k\) を定義する.

  3. 限定主問題 \(P|_k\) を求解して,実行可能解 \(\vec{x}_k^{\star}\) を構成する.

  4. 限定双対問題 \(D|_k\) を求解して,最適解 \(\vec{\omega}_k^*\) を求める.

  5. 最適解 \(\vec{\omega}_k^*\) に対して価格付け問題に等価なナップサック問題 (23) を解いて,主問題 \(P\) の被約費用が非負かどうか決定する.非負ならば \(\vec{x}_k^{\star}\) を最適解とする.負ならば \(k\to k+1\) および次式で \(J_k\) を更新する.手続き 3 へ.

    (24)#\[\begin{split}\bar{J} &= \operatorname*{argmin}_{j\in J} (\vec{\gamma})_j, \\ J_k &\leftarrow J_k \cup \bar{J}\end{split}\]

列生成法は汎用的な枠組みであるが,そのうち共通しない部分は次の設計である.

  • パターン番号集合の初期集合 \(PATTERNS_1\) の生成

  • 価格付け問題の解き方

場合によっては他の部分も問題に応じた実装が必要になろう.

今回,パターン番号集合の初期集合 \(PATTERNS_1\) としては,一つのアイテムを一つのビンに割り当てる次の集合を考えよう.

(25)#\[PATTERNS_1 = \bigcup_{i\in I} \{i\}\]

上記を除いた部分については共通化が可能であるから, 列生成法の処理を次のスーパークラスとして実装をまとめることにしよう.

2.4.2. スーパークラスの実装例#

PySIMPLE による列生成法を司るスーパークラスの実装例 (column_generator.py) として次を挙げる.

PySIMPLE Sample Code (column_generator.py)
 1# -*- coding: utf-8 -*-
 2from abc import ABCMeta, abstractmethod
 3from pysimple import *
 4
 5class ColumnGenerator(metaclass=ABCMeta):
 6    iternum = property(lambda self: self.__iternum)
 7    patternnum = property(lambda self: self.__pn0 + self.__iternum)
 8
 9    @abstractmethod
10    def create_init_pattern(self, *, pattern, b=1, c=1):
11        """set self.pattern, self.b, self.c"""
12        self.pattern = pattern
13        self.b = b
14        self.c = c
15        self.__iternum = 0
16        self.__pn0 = len(pattern)
17
18    # type は solve_pricing_problem の変数 type と同じ
19    def solve_primal_problem_for_selection_of_pattern(self, *, type=bin, silent=True):  # 主問題
20        """生成されたパターンから b を満たす組合せを選択する"""
21        pi = Element(value=self.pattern)
22        p = Element(set=pi(0).set)
23        b = Parameter(index=pi(1), value=self.b)
24        c = Parameter(index=pi(0), value=self.c)
25        ASSOC = Parameter(index=pi, value=self.pattern)  # パターン p にアイテム i を含むか (0-1).
26        x = Variable(index=p, type=type, lb=0)  # パターン p を選択するか (type=bin),幾つ選択するか (type=int).
27
28        prb = Problem()
29        prb += Sum(ASSOC[pi]*x[pi(0)], pi(0)) >= b[pi(1)], 'cons'  # アイテム i は何れかのパターンで選ぶ.
30        prb += Sum(c[p]*x[p])
31        prb.solve(silent=silent)
32        assert prb.status == NuoptStatus.OPTIMAL
33        return x[x[p].val>0].val, prb['cons'].dual
34
35    def solve_dual_problem_for_selection_of_pattern(self, *, silent=True):  # 双対問題
36        """solve_primal_problem_for_selection_of_pattern の双対問題"""
37        pi = Element(value=self.pattern)
38        i = Element(set=pi(1).set)
39        b = Parameter(index=pi(1), value=self.b)
40        c = Parameter(index=pi(0), value=self.c)
41        ASSOC = Parameter(index=pi, value=self.pattern)
42        omega = Variable(index=pi(1), lb=0, name='ω')
43
44        prb = Problem(type=max)
45        prb += Sum(ASSOC[pi]*omega[pi(1)], pi(1)) <= c[pi(0)]
46        prb += Sum(b[i]*omega[i])
47        prb.solve(silent=silent)
48        assert prb.status == NuoptStatus.OPTIMAL
49        return omega.val
50
51    @abstractmethod
52    def solve_pricing_problem(self, dualval, *, silent=True):
53        """組合せを 1 つ生成する"""
54        pass  # return y.val, prb.objective.val
55
56    def update_pattern(self, yval):
57        """update self.pattern, self.c"""
58        self.pattern.update({(self.patternnum,i): v for (i,), v in yval.items()})
59
60    def solve(self, *, eps=0, maxiter=100):
61        # 初期パターンを生成する.
62        self.create_init_pattern()
63
64        objval = eps + 1e-6  # eps より大きい適当な値
65        while objval > eps and self.iternum < maxiter:
66            self.__iternum += 1
67
68            # 限定主問題 `P|_k` を求解して,実行可能解 `\vec{x}_k^{\star}` を構成する.
69            # 限定双対問題 `D|_k` を求解して,最適解 `\vec{\omega}_k^*` を求める.
70            _, dualval = self.solve_primal_problem_for_selection_of_pattern(type=float)  # 主問題の双対変数を使う.
71            # dualval = self.solve_dual_problem_for_selection_of_pattern()  # 双対問題を使う.
72
73            # 価格付け問題を求解して,パターン番号集合を更新する.
74            yval, objval = self.solve_pricing_problem(dualval)
75            # print('update:', dict(yval.items()))
76            self.update_pattern(yval)
77            print(f'{self.iternum}: {objval}', flush=True)
78
79        # 実行可能解を取得する.
80        yval, _ = self.solve_primal_problem_for_selection_of_pattern()
81        return yval

上記のコードの中で,初期集合の生成部分 create_init_pattern と価格付け問題の求解部分 solve_pricing_problem は, 個別の問題に応じて設計するサブクラスに具体的な実装を記述することになる. ここでは初期集合でパターンの検証用に全列挙を設定することもできるよう,幾つかの配慮がなされている.

1    @abstractmethod
2    def create_init_pattern(self, *, pattern, b=1, c=1):
3        """set self.pattern, self.b, self.c"""
4        self.pattern = pattern
5        self.b = b
6        self.c = c
7        self.__iternum = 0
8        self.__pn0 = len(pattern)

限定主問題または限定双対問題の求解はそれぞれ solve_primal_problem_for_selection_of_patternsolve_dual_problem_for_selection_of_pattern が担い,強く共通化されている部分である.

 1    def solve_primal_problem_for_selection_of_pattern(self, *, type=bin, silent=True):  # 主問題
 2        """生成されたパターンから b を満たす組合せを選択する"""
 3        pi = Element(value=self.pattern)
 4        p = Element(set=pi(0).set)
 5        b = Parameter(index=pi(1), value=self.b)
 6        c = Parameter(index=pi(0), value=self.c)
 7        ASSOC = Parameter(index=pi, value=self.pattern)  # パターン p にアイテム i を含むか (0-1).
 8        x = Variable(index=p, type=type, lb=0)  # パターン p を選択するか (type=bin),幾つ選択するか (type=int).
 9
10        prb = Problem()
11        prb += Sum(ASSOC[pi]*x[pi(0)], pi(0)) >= b[pi(1)], 'cons'  # アイテム i は何れかのパターンで選ぶ.
12        prb += Sum(c[p]*x[p])
13        prb.solve(silent=silent)
14        assert prb.status == NuoptStatus.OPTIMAL
15        return x[x[p].val>0].val, prb['cons'].dual

PySIMPLE では主問題を求解後に,対応する双対問題についても参照できるため, これらどちらかを求解するだけでよい.

列生成法で行う逐次的求解およびパターン番号集合の更新処理は solve が担う.

PySIMPLE Sample Code
 1    def solve(self, *, eps=0, maxiter=100):
 2        # 初期パターンを生成する.
 3        self.create_init_pattern()
 4
 5        objval = eps + 1e-6  # eps より大きい適当な値
 6        while objval > eps and self.iternum < maxiter:
 7            self.__iternum += 1
 8
 9            # 限定主問題 `P|_k` を求解して,実行可能解 `\vec{x}_k^{\star}` を構成する.
10            # 限定双対問題 `D|_k` を求解して,最適解 `\vec{\omega}_k^*` を求める.
11            _, dualval = self.solve_primal_problem_for_selection_of_pattern(type=float)  # 主問題の双対変数を使う.
12            # dualval = self.solve_dual_problem_for_selection_of_pattern()  # 双対問題を使う.
13
14            # 価格付け問題を求解して,パターン番号集合を更新する.
15            yval, objval = self.solve_pricing_problem(dualval)
16            # print('update:', dict(yval.items()))
17            self.update_pattern(yval)
18            print(f'{self.iternum}: {objval}', flush=True)
19
20        # 実行可能解を取得する.
21        yval, _ = self.solve_primal_problem_for_selection_of_pattern()
22        return yval

最適性条件の判定は while 文の終了条件として織り込まれている.

2.4.3. サブクラスの実装例#

ビンパッキング問題に特化したサブクラスの実装例 (bin_packing.py) が次である.

PySIMPLE Sample Code (bin_packing.py)
  1# -*- coding: utf-8 -*-
  2from itertools import combinations
  3from random import sample, randint, seed; seed(0)
  4
  5from pysimple import *
  6from column_generator import ColumnGenerator
  7
  8class BinPacking(ColumnGenerator):
  9    def __init__(self, BVOL, IVOL):
 10        self.BVOL = BVOL  # ビンの容量
 11        self.IVOL = IVOL  # アイテムの容量
 12
 13    def create_init_pattern(self):
 14        pattern = dict.fromkeys(enumerate(self.IVOL, 1), 1)
 15        super().create_init_pattern(pattern=pattern)
 16
 17    def solve_pricing_problem(self, dualval, *, silent=True):
 18        """ビンに収まるアイテムの組合せを 1 つ生成する"""
 19        i = Element(value=self.IVOL.keys())  # アイテム
 20        IVOL = Parameter(index=i, value=self.IVOL)  # アイテム i の容量
 21        DUALVAL = Parameter(index=i, value=dualval, name='ω*')
 22        y = BinaryVariable(index=i)  # アイテム i を使用するかどうか
 23
 24        prb = Problem(type=max)
 25        prb += Sum(IVOL[i]*y[i]) <= self.BVOL, 'アイテムをビンに収める'
 26        prb += Sum(DUALVAL[i]*y[i])
 27        prb.solve(silent=silent)
 28        return y[y[i].val>0].val, prb.objective.val
 29
 30    def visualize(self, zval):
 31        for (p,), val in zval.items():
 32            lentimes = [(len_, self.pattern[p,t]) for t, len_ in self.IVOL.items() if (p, t) in self.pattern]
 33            pstr = '+'.join(f'{l}{(f"*{times}","")[times==1]}' for l, times in lentimes)
 34            line = '+'+''.join(('-'*(l-1)+'+')*times for l, times in lentimes)
 35            print(f'pattern{p:2}({pstr:^20}){val:2}枚: {line}')
 36
 37def create_random_data(*, BVOL, N):  # BVOL: ビンの容量, N: アイテム種類数
 38    """テスト用のランダムデータを生成する"""
 39    # アイテム集合
 40    I = [f'item{i}' for i in range(1, N+1)]
 41    # 容量重複がない IVOL[i] を dict 形式でランダム作成
 42    IVOL = dict(zip(I, sample(range(1, BVOL//2+1), N)))
 43    return BVOL, IVOL
 44
 45def solve_at_once(BVOL, IVOL, *, silent=True):
 46    """一度に解く straight forward な定式化"""
 47    i = Element(value=IVOL.keys())  # アイテム
 48    b = Element(value=range(len(i.set)))  # ビン (数は上限のアイテム数とする)
 49    IVOL = Parameter(index=i, value=IVOL)  # アイテム i の容量
 50    BVOL = Parameter(value=BVOL) # ビンの容量
 51    z = BinaryVariable(index=(i,b))  # アイテム i をビン b に入れるかどうか
 52    use = BinaryVariable(index=b)  # ビン b を使用するかどうか
 53
 54    prb = Problem()
 55    prb += Sum(z[i,b], b) == 1 , 'いずれかのビンに入れる'
 56    prb += Sum(IVOL[i]*z[i,b], i) <= BVOL*use[b],  'アイテムをビンに収める'
 57    prb += Sum(use[b])  # ビンの総数
 58
 59    prb.solve(silent=silent)
 60    assert prb.status == NuoptStatus.OPTIMAL
 61    print(use[use[b].val>0].val)  # 使ったビン
 62    print(z[z[i,b].val>0].val)  # 選ばれたアイテムとビンの組合せ
 63
 64def solve_with_enumerate_all_patterns(BVOL, IVOL):
 65    """全列挙で解く定式化"""
 66    def enumerate_all_patterns(BVOL, IVOL):
 67        """容量 BVOL 以下になる IVOL の組合せを全列挙する"""
 68        v2i = {v: i for i, v in IVOL.items()}  # IVOL -> item
 69        patterns, p = [], 1
 70        for i in range(len(v2i)):
 71            for comb in combinations(v2i, i+1):
 72                if sum(comb) <= BVOL:
 73                    patterns.extend((p, v2i[v]) for v in comb)
 74                    p += 1
 75        return dict.fromkeys(patterns, 1)
 76
 77    bp = BinPacking(BVOL, IVOL)
 78    pattern = enumerate_all_patterns(BVOL, IVOL)
 79    ColumnGenerator.create_init_pattern(bp, pattern=pattern)
 80    zval, _ = bp.solve_primal_problem_for_selection_of_pattern()
 81    bp.visualize(zval)
 82
 83def solve_with_column_generation_method(BVOL, IVOL):
 84    """列生成法で解く定式化"""
 85    bp = BinPacking(BVOL, IVOL)
 86    zval = bp.solve(eps=1)
 87    bp.visualize(zval)
 88
 89
 90if __name__ == '__main__':
 91    BVOL, IVOL = create_random_data(BVOL=70, N=20)
 92
 93    # 1. 素直な定式化で解く場合
 94    # solve_at_once(BVOL, IVOL)
 95
 96    # 2. 全列挙で解く場合 (BVOL=70, N=20 で 6700 パターン)
 97    # solve_with_enumerate_all_patterns(BVOL, IVOL)
 98
 99    # 3. 列生成で解く場合
100    solve_with_column_generation_method(BVOL, IVOL)

設計概要で述べた「初期集合の生成」と「価格付け問題の求解」の実装は次のとおり.

1    def create_init_pattern(self):
2        pattern = dict.fromkeys(enumerate(self.IVOL, 1), 1)
3        super().create_init_pattern(pattern=pattern)

bin_packing.py では検証のために次の三つの関数が実装されている.

  • solve_at_once (straight forward な定式化による求解)

  • solve_with_enumerate_all_patterns (最初に全列挙して求解)

  • solve_with_column_generation_method (列生成法での求解)

 1def solve_at_once(BVOL, IVOL, *, silent=True):
 2    """一度に解く straight forward な定式化"""
 3    i = Element(value=IVOL.keys())  # アイテム
 4    b = Element(value=range(len(i.set)))  # ビン (数は上限のアイテム数とする)
 5    IVOL = Parameter(index=i, value=IVOL)  # アイテム i の容量
 6    BVOL = Parameter(value=BVOL) # ビンの容量
 7    z = BinaryVariable(index=(i,b))  # アイテム i をビン b に入れるかどうか
 8    use = BinaryVariable(index=b)  # ビン b を使用するかどうか
 9
10    prb = Problem()
11    prb += Sum(z[i,b], b) == 1 , 'いずれかのビンに入れる'
12    prb += Sum(IVOL[i]*z[i,b], i) <= BVOL*use[b],  'アイテムをビンに収める'
13    prb += Sum(use[b])  # ビンの総数
14
15    prb.solve(silent=silent)
16    assert prb.status == NuoptStatus.OPTIMAL
17    print(use[use[b].val>0].val)  # 使ったビン
18    print(z[z[i,b].val>0].val)  # 選ばれたアイテムとビンの組合せ

2.4.4. 求解#

BVOL=200, N=100 とアイテム数が \(100\) でそれぞれの求解方法を試すと, straight forward な定式化では数分必要となるが,列生成法での求解は数秒で終えることができた. 全列挙は言わずもがなである.

技法集

用語集

リソース

参考文献

[1]

田中俊二. 大規模組合せ最適化問題に対する数理アプローチの基礎. 計測と制御, 56(12):967–972, 2017. URL: https://doi.org/10.11499/sicejl.56.967.

[2]

宮本裕一郎. はじめての列生成法. オペレーションズ・リサーチ = Communications of the Operations Research Society of Japan : 経営の科学, 57(4):198–204, 2012. URL: https://orsj.org/wp-content/corsj/or57-4/or57_4_198.pdf.

[3]

久保幹夫, 田村明久, and 松井知己. 応用数理計画ハンドブック. 朝倉書店, 2002.

引用書式

BibTeX