Gilmore-Gomory列生成アルゴリズムの覚書

引用 Gemini

 

カッティングストック問題に対する列生成アルゴリズム、特に Gilmore-Gomory アルゴリズムの疑似コードでの説明ですね。文章だけでは分かりにくいとのこと、承知いたしました。できるだけシンプルで分かりやすい疑似コードと説明を心がけます。

Gilmore-Gomory アルゴリズムの概要

Gilmore-Gomory アルゴリズムは、大規模なカッティングストック問題に対して、全ての可能なカットパターンを事前に列挙するのではなく、必要に応じて動的に生成していくことで効率的に最適解(またはそれに近い解)を見つける手法です。

基本的な流れ:

  1. 初期化: いくつかの基本的なカットパターン(例えば、各アイテムを1つずつカットするパターン)を用意し、線形計画問題を解きます。
  2. 価格付け問題 (Pricing Problem): 現在の線形計画問題の双対変数(shadow price)を使って、新しい、より利益の高い(無駄の少ない)カットパターンが存在するかどうかを判定します。これは通常、ナップサック問題として定式化されます。
  3. 列の追加: 価格付け問題で見つかった新しいカットパターンを線形計画問題に追加(列を追加)し、再度線形計画問題を解きます。
  4. 収束判定: 新しい利益の高いカットパターンが見つからなくなるまで、ステップ2と3を繰り返します。
  5. 整数解の探索 (必要に応じて): 線形計画問題の解は一般的に実数値であるため、最終的に整数解を得るために分枝限定法などの手法を適用することがあります。

Gilmore-Gomory アルゴリズムの疑似コード

// 入力:
//   - 需要ベクトル D = [d1, d2, ..., dn] (各アイテムの必要数)
//   - 材料の幅 W

// 変数:
//   - P: 現在までに生成されたカットパターンの集合 (各パターンは各アイテムのカット数を示すベクトル)
//   - c: 各カットパターンのコスト (通常は 1。1枚の材料を使うため)
//   - x: 各カットパターンの使用頻度を示す変数ベクトル

// 初期化:
P = { [1, 0, ..., 0], [0, 1, ..., 0], ..., [0, 0, ..., 1] } // 各アイテムを1つずつカットする初期パターン
c = [1, 1, ..., 1] // 初期パターンのコストは全て 1
x = 線形計画問題を解いた結果の初期パターンの使用頻度ベクトル

while (true) {
    // 1. 現在の線形計画問題を解く
    LP_解 = solve_linear_program(目的関数: sum(c_i * x_i),
                                制約条件: sum(P_ij * x_i) >= D_j  for all j,
                                          x_i >= 0)

    // 線形計画問題の最適解と双対変数 π = [π1, π2, ..., πn] を取得
    最適解コスト = LP_解.目的関数値
    π = LP_解.双対変数

    // 2. 価格付け問題 (ナップサック問題) を解く
    //    新しいカットパターンで、材料の幅を超えない範囲で、双対変数の価値の合計を最大化する
    新しいパターン = solve_knapsack_problem(アイテムの価値: π = [π1, π2, ..., πn],
                                          アイテムの重さ: 各アイテムの幅 w = [w1, w2, ..., wn],
                                          ナップサック容量: W)

    // 新しいパターンの価値を計算
    新しいパターンの価値 = sum(π_j * 新しいパターンのアイテム j のカット数)

    // 3. 収束判定
    //    新しいパターンの価値が 1 (材料のコスト) より大きい場合、改善の余地あり
    if (新しいパターンの価値 > 1 + ε) { // ε は小さな許容誤差
        // 新しいパターンを P に追加
        P.add(新しいパターン)
        // 線形計画問題を再解
    } else {
        // 新しい利益のあるパターンが見つからなかったため、収束
        break
    }
}

// 最終的な線形計画問題の解 x が得られた
// 必要に応じて、x を整数解に変換する (例: 丸め、分枝限定法)

return 最終的な解
 

疑似コードの説明

  1. 初期化:

    • D: 各アイテムの必要な数(需要)を表すベクトルです。
    • W: 材料の全体の幅です。
    • P: 最初は、最も単純なカットパターンとして、各アイテムを1つだけカットするパターンを格納します。例えば、3種類のアイテムがある場合、初期の P[[1, 0, 0], [0, 1, 0], [0, 0, 1]] のようになります。
    • c: 各カットパターンのコストです。通常、1枚の材料を使うので、初期値は全て 1 です。
    • x: 各カットパターンの使用頻度を示す変数です。初期状態では、線形計画問題を解いてこれらの値を決定します。
  2. ループ:

    • 線形計画問題を解く (solve_linear_program): 現在までに生成されたカットパターン P を用いて、総コスト(使用する材料の枚数)を最小化しつつ、需要 D を満たすように各パターンの使用頻度 x を決定します。このとき、各パターンの使用頻度 x は実数値でも構いません。線形計画ソルバーは、最適解のコストと、制約条件に対応する双対変数 π(各アイテムの潜在的な価値やシャドウプライス)を返します。
    • 価格付け問題 (solve_knapsack_problem):
      • 双対変数 π を各アイテムの「価値」、各アイテムの幅 w を「重さ」、材料の幅 W をナップサックの「容量」と見なしたナップサック問題を解きます。
      • このナップサック問題の目的は、材料の幅を超えない範囲で、双対変数の価値の合計を最大にするような新しいカットパターンを見つけることです。
      • 新しいパターン は、各アイテムをいくつカットするかを示すベクトルとして得られます。
    • 収束判定:
      • 新しいパターンの価値 を計算します。これは、新しいパターンに含まれる各アイテムの双対変数の合計です。
      • もしこの価値が材料のコストである 1 よりも十分に大きい場合(1 + ε は小さな許容誤差)、それは現在の線形計画問題にとって「利益のある」新しいパターンであることを意味します。
      • 新しいパターンが見つかった場合は、P に追加され、再び線形計画問題を解くためにループが繰り返されます。
      • もし新しい利益のあるパターンが見つからなかった場合、現在の線形計画問題の解は最適(またはほぼ最適)であると判断され、ループを終了します。
  3. 最終的な解:

    • ループ終了時の線形計画問題の解 x は、各カットパターンの最適な使用頻度を示しています。これらの値は一般的に実数値であるため、必要に応じて、丸め処理や分枝限定法などの整数計画法の手法を適用して、現実的な整数解を得ます。

なぜ文章だけでは理解しにくいのか?

列生成アルゴリズムは、線形計画法と組み合わせ最適化(価格付け問題としてのナップサック問題)という異なる最適化問題を反復的に解くという構造を持つため、その流れを文章だけで追うのは難しい場合があります。疑似コードで具体的なステップを示すことで、アルゴリズムの動きがより明確になるかと思います。