Day 106-Q4 — Simplex法(線形計画法・単体法)

2026-07-29 赤色 Master / Phase 8+ ★★★★★★★★★ Fraction厳密演算 + Bland's rule

問題

$N$変数$x_1,\dots,x_N$について次の線形計画問題を解け。

$\text{maximize}\ \sum_j c_j x_j \quad \text{s.t.}\ \sum_j A_{ij}x_j \le b_i\ (i=1,\dots,M),\ x_j\ge0$

最適値は必ず有理数になる。答えを既約分数$p/q$($q>0$、$\gcd(|p|,q)=1$)としてp qの形式で出力せよ。

入力形式

N M
c_1 ... c_N
A_11 ... A_1N b_1
...
A_M1 ... A_MN b_M

制約

$1 \le N,M \le 8$
$0 \le c_j,A_{ij} \le 20$
$1 \le b_i \le 50$
各変数に$A_{ij}>0$の制約が1つ以上(有界性を保証)

入出力例

入力例1

2 3
3 5
1 0 4
0 2 12
3 2 18

出力例1

36 1

$x_1\le4,\ x_2\le6,\ 3x_1+2x_2\le18$の下で$3x_1+5x_2$を最大化する古典例題。最適解$(2,6)$で値36。

概念図: 実行可能領域とSimplex法の頂点巡回

入力例1: max 3x+5y, x≤4, 2y≤12, 3x+2y≤18 x y (0,0) z=0 (4,0) z=12 (4,3) z=27 (2,6) z=36 ★最適 (0,6) z=30 Simplex法は頂点を1つずつ辿り目的関数を改善 スラック変数基底(0,0)から出発し、隣接する頂点へ移動を繰り返す

ヒント(段階的開示)

ヒント1: 方向性
N,M≤8と小さいので頂点を全列挙して比較する方法もあるが非効率。実行可能領域の頂点を「隣接する頂点へ目的関数値が改善する方向にだけ移動する」ことを繰り返すSimplex法(単体法)が古典的解法。
ヒント2: アプローチ
各制約にスラック変数$s_i\ge0$を導入し等式$\sum_jA_{ij}x_j+s_i=b_i$に変形する。$b_i\ge0$が保証されるので「全$x_j=0$, $s_i=b_i$」が最初から実行可能(2段階法のPhase 1が不要)。目的行に負の係数を持つ変数を選んでピボット(Gauss-Jordan消去)を繰り返す。ただしピボットの選び方が悪いと無限ループ(cycling)に陥りうるため、「入場・退出変数は常にインデックス最小のものを選ぶ」Bland's ruleを使う。浮動小数点誤差を避けるためfractions.Fractionで厳密演算する。
ヒント3: 誘導(コード骨格)
while True:
    enter = 目的行で最初に負になっている列(Bland's rule)
    if enter が見つからない: break  # 最適
    leave = enter列が正の行の中でRHS/係数が最小の行
            # 同点なら基底変数インデックス最小を優先
    (leave行, enter列)を軸にGauss-Jordan消去
    basis[leave] = enter
答え = tableau[目的行][-1]

模範解答 (Python)

import sys
from fractions import Fraction as F

def solve():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    m = int(data[idx]); idx += 1
    c = []
    for _ in range(n):
        c.append(int(data[idx])); idx += 1
    A = []
    b = []
    for _ in range(m):
        row = []
        for _ in range(n):
            row.append(int(data[idx])); idx += 1
        A.append(row)
        b.append(int(data[idx])); idx += 1

    ncols = n + m + 1
    tab = [[F(0) for _ in range(ncols)] for _ in range(m + 1)]
    basis = [0] * m
    for i in range(m):
        for j in range(n):
            tab[i][j] = F(A[i][j])
        tab[i][n + i] = F(1)
        tab[i][-1] = F(b[i])
        basis[i] = n + i
    for j in range(n):
        tab[m][j] = F(-c[j])

    while True:
        enter = -1
        for j in range(n + m):
            if tab[m][j] < 0:
                enter = j
                break
        if enter == -1:
            break

        leave = -1
        best_ratio = None
        for i in range(m):
            if tab[i][enter] > 0:
                ratio = tab[i][-1] / tab[i][enter]
                if best_ratio is None or ratio < best_ratio or (ratio == best_ratio and basis[i] < basis[leave]):
                    best_ratio = ratio
                    leave = i

        piv = tab[leave][enter]
        tab[leave] = [x / piv for x in tab[leave]]
        for i in range(m + 1):
            if i != leave and tab[i][enter] != 0:
                factor = tab[i][enter]
                tab[i] = [tab[i][k] - factor * tab[leave][k] for k in range(ncols)]
        basis[leave] = enter

    ans = tab[m][-1]
    print(f"{ans.numerator} {ans.denominator}")


solve()
計算量: ピボット回数は実用上$O(N+M)$〜数十回程度(最悪指数だがBland's ruleで必ず有限回で停止)。scipy.optimize.linprogとの一致をランダム500ケースで確認済み。

Step-by-Step 解説

1標準形への変換とタブロー初期化
スラック変数$s_i$で等式に変形。$b_i\ge0$なのでスラック基底の初期解がそのまま実行可能。
2入場変数の選択(Bland's rule その1)
目的行を左から走査し最初の負係数の列を選ぶ。cyclingしないことが理論的に保証される。
3退出変数の選択(比の最小テスト)
正係数を持つ行の中でRHS÷係数が最小の行を選び、実行可能性を保つ。同点は基底変数インデックス最小を優先。
4ピボット操作
軸要素で正規化し他の全行から定数倍を引く。目的行に負係数がなくなるまで繰り返せば最適解。

よくあるミス

ミス原因正しい書き方
浮動小数点で実装する誤差の蓄積で境界条件の比較が不安定になるfractions.Fractionで厳密な有理数演算を行う
入場変数を「最も負が大きい列」で選ぶ特定の退化した入力でcyclingしうるBland's rule(常に最小インデックス)を使う
比の最小テストで同点処理をしない複数行が同じ最小比を持つ退化ケースでcyclingしうる同点なら基底変数インデックス最小の行を選ぶ
$b_i\ge0$を仮定せず一般LPに適用$b_i<0$では初期スラック解が実行可能とは限らない本問は$b_i\ge1$保証だが、一般には2段階法のPhase 1が必要

次のステップ

  • 発展: $b_i<0$を許す一般形に拡張し、2段階Simplex法(人工変数によるPhase 1)を実装する
  • 発展: 双対問題を同時に構築し、強双対性を数値的に確認する
  • 次回予告: König's Edge Coloring(二部グラフの辺彩色・オイラー閉路分割によるΔ色構築)

自己評価

自分の回答

気づき・メモ