問題
$N$個の供給地と$M$個の需要地がある。供給地$i$の供給量は$s_i$、需要地$j$の需要量は$d_j$であり、供給地$i$から需要地$j$へ荷物を1単位輸送するコストは$c_{ij}$である。$\sum_i s_i = \sum_j d_j$が保証されている(均衡輸送問題)。すべての需要をちょうど満たすように輸送量$x_{ij}\ge 0$を決めるとき、総輸送コスト$\sum_{i,j} c_{ij}x_{ij}$の最小値を求めよ。
入力形式
N M
s_1 s_2 ... s_N
d_1 d_2 ... d_M
c_11 c_12 ... c_1M
c_21 c_22 ... c_2M
...
c_N1 c_N2 ... c_NM
制約
$1 \le N, M \le 50$
$1 \le s_i, d_j \le 10^6$
$\sum_i s_i = \sum_j d_j$
$0 \le c_{ij} \le 10^6$
入出力例
入力例1
3 4
7 9 18
5 8 7 14
19 30 50 10
70 30 40 60
40 8 70 20
出力例1
743
概念図
ヒント(段階的開示)
ヒント1(方向性)
これは輸送問題という特殊構造を持つ最小費用流であり、一般のMCMFでも解けるが、二部グラフに特化した輸送単体法(Transportation Simplex=ネットワーク単体法の特化版)を使うと、単体法の各ステップが「表の上のセルの出入り」として直感的に実行できる。
ヒント2(アプローチ)
①北西隅法で初期基底可能解(N+M-1個の基底セル)を作る。②基底セルだけから双対変数u_i,v_jを求める(基底で c_ij=u_i+v_j)。③非基底セルの被約費用 c_ij-(u_i+v_j) が負なら、そのセルを基底に入れ、既存基底との唯一の閉路に沿って輸送量を交互に増減させる(stepping stone)。すべて非負になったら最適。
ヒント3(誘導)
def find_loop(cells_set, start):
cells = set(cells_set); cells.add(start)
changed = True
while changed:
changed = False
for c in list(cells):
row_cnt = sum(1 for cc in cells if cc[0] == c[0])
col_cnt = sum(1 for cc in cells if cc[1] == c[1])
if row_cnt < 2 or col_cnt < 2:
cells.discard(c); changed = True
# cells に残ったものが唯一の閉路模範解答 (Python)
import sys
def solve_transportation(supply, demand, cost):
m, n = len(supply), len(demand)
x = [[0] * n for _ in range(m)]
basis = set()
s, d = supply[:], demand[:]
i = j = 0
while i < m and j < n:
amt = min(s[i], d[j])
x[i][j] = amt
basis.add((i, j))
s[i] -= amt
d[j] -= amt
if s[i] == 0 and d[j] == 0:
if i < m - 1:
i += 1
elif j < n - 1:
j += 1
else:
break
elif s[i] == 0:
i += 1
else:
j += 1
def get_uv():
u = [None] * m
v = [None] * n
u[0] = 0
changed = True
while changed:
changed = False
for (bi, bj) in basis:
if u[bi] is not None and v[bj] is None:
v[bj] = cost[bi][bj] - u[bi]
changed = True
elif v[bj] is not None and u[bi] is None:
u[bi] = cost[bi][bj] - v[bj]
changed = True
return u, v
def find_loop(start):
cells = set(basis)
cells.add(start)
changed = True
while changed:
changed = False
for c in list(cells):
row_cnt = sum(1 for cc in cells if cc[0] == c[0])
col_cnt = sum(1 for cc in cells if cc[1] == c[1])
if row_cnt < 2 or col_cnt < 2:
cells.discard(c)
changed = True
order = [start]
used = {start}
cur = start
horizontal = True
while True:
if horizontal:
nxts = [c for c in cells if c[0] == cur[0] and c not in used]
else:
nxts = [c for c in cells if c[1] == cur[1] and c not in used]
if not nxts:
break
cur = nxts[0]
order.append(cur)
used.add(cur)
horizontal = not horizontal
return order
for _ in range(2000):
u, v = get_uv()
enter, best = None, 0
for a in range(m):
for b in range(n):
if (a, b) not in basis:
rc = cost[a][b] - (u[a] + v[b])
if rc < best:
best, enter = rc, (a, b)
if enter is None:
break # 最適
loop = find_loop(enter)
minus_cells = loop[1::2]
theta = min(x[a][b] for (a, b) in minus_cells)
leaving = None
for idx, (a, b) in enumerate(loop):
if idx % 2 == 0:
x[a][b] += theta
else:
x[a][b] -= theta
if x[a][b] == 0 and leaving is None:
leaving = (a, b)
basis.add(enter)
basis.discard(leaving)
return sum(cost[a][b] * x[a][b] for a in range(m) for b in range(n))
def solve():
data = sys.stdin.read().split()
idx = 0
n = int(data[idx]); idx += 1
m = int(data[idx]); idx += 1
supply = [int(data[idx + k]) for k in range(n)]; idx += n
demand = [int(data[idx + k]) for k in range(m)]; idx += m
cost = []
for _ in range(n):
cost.append([int(data[idx + k]) for k in range(m)])
idx += m
print(solve_transportation(supply, demand, cost))
solve()
Step-by-Step 解説
1北西隅法で初期基底可能解
表の左上から順に供給・需要の小さい方を割り当て、消費し尽くしたら次の行/列へ。N+M-1個の基底セル(木構造)が得られる。
表の左上から順に供給・需要の小さい方を割り当て、消費し尽くしたら次の行/列へ。N+M-1個の基底セル(木構造)が得られる。
2双対変数u,vを求める
基底セルではc_ij=u_i+v_j。基底が木なのでu_1=0と固定すれば残りは一意に伝播で決まる。
基底セルではc_ij=u_i+v_j。基底が木なのでu_1=0と固定すれば残りは一意に伝播で決まる。
3MODI法で最適性判定
非基底セルの被約費用c_ij-(u_i+v_j)が負なら改善余地あり。すべて0以上なら最適(線形計画の相補性定理)。
非基底セルの被約費用c_ij-(u_i+v_j)が負なら改善余地あり。すべて0以上なら最適(線形計画の相補性定理)。
4閉路に沿ったピボット
被約費用が最小のセルを選び、既存基底との唯一の閉路上で+θ,-θを交互に加える。θ=(-θ側セルの現在値の最小)。最初に0になったセルを基底から外す。
被約費用が最小のセルを選び、既存基底との唯一の閉路上で+θ,-θを交互に加える。θ=(-θ側セルの現在値の最小)。最初に0になったセルを基底から外す。
5計算量
基底サイズはN+M-1で固定。ピボット回数は最悪O(NM)程度、各ステップO(NM)で全体O(N²M²)程度。N,M≤50なら十分高速。
基底サイズはN+M-1で固定。ピボット回数は最悪O(NM)程度、各ステップO(NM)で全体O(N²M²)程度。N,M≤50なら十分高速。
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 初期基底セル数がN+M-1にならない | 北西隅法の終端で供給・需要が同時に0になる分岐漏れ | 同時に尽きたら最後の行/列でなければ片方だけ進める特別処理を入れる |
| 双対変数の基準点を毎回変える | u[0]=0を固定せず不定形の連立方程式を解こうとする | 根を1つ固定すれば基底の木構造上でBFS的に一意に伝播する |
| 閉路上の符号を逆にする | entering cellから交互に+,-を割り当てる順序ミス | 0番目(entering)を+θ、奇数番目を-θと固定してからθを決める |
| 不均衡輸送問題を想定してしまう | ダミー供給地/需要地の追加を忘れる | 本問は均衡が保証されるが、実務では差分を吸収するコスト0のダミー行/列を追加する |
次のステップ
- 発展: 容量制約付き輸送問題や一般グラフ上の最小費用流に拡張したネットワーク単体法(スパニングツリー基底+ポテンシャル更新+木を跨ぐ辺のピボット)
- 次回予告: 次回のテーマは今後のセッションで選定