問題
$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: 方向性
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$なのでスラック基底の初期解がそのまま実行可能。
スラック変数$s_i$で等式に変形。$b_i\ge0$なのでスラック基底の初期解がそのまま実行可能。
2入場変数の選択(Bland's rule その1)
目的行を左から走査し最初の負係数の列を選ぶ。cyclingしないことが理論的に保証される。
目的行を左から走査し最初の負係数の列を選ぶ。cyclingしないことが理論的に保証される。
3退出変数の選択(比の最小テスト)
正係数を持つ行の中でRHS÷係数が最小の行を選び、実行可能性を保つ。同点は基底変数インデックス最小を優先。
正係数を持つ行の中で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(二部グラフの辺彩色・オイラー閉路分割によるΔ色構築)