問題
$N$ 頂点 $M$ 辺の有向グラフがある。辺 $i$ は頂点 $u_i$ から $v_i$ へ、容量 $c_i$・1 単位あたりの費用 $w_i$(負もありうる)を持つ。各頂点 $v$ には収支 $b_v$ が決まっていて、$b_v>0$ なら供給、$b_v<0$ なら需要である($\sum b_v=0$)。
各辺の流量 $0\le f_i\le c_i$(整数)を、すべての頂点で「出る量 − 入る量 $=b_v$」となるように決める。費用 $\sum f_i w_i$ の最小値を求めよ。条件を満たす流し方が存在することは保証される。
入力形式
N M
b_1 b_2 ... b_N
u_1 v_1 c_1 w_1
...
u_M v_M c_M w_M
制約
| # | 条件 |
|---|---|
| 1 | $2\le N\le 300$、$1\le M\le 3000$ |
| 2 | $1\le u_i,v_i\le N$、$u_i\ne v_i$(多重辺はありうる) |
| 3 | $1\le c_i\le 10^6$、$|w_i|\le 10^6$ |
| 4 | $\sum b_v=0$。実行可能な流し方が存在する |
入出力例
入力例1
3 3
2 0 -2
1 2 2 1
2 3 2 1
1 3 1 3出力例1
4(1→2→3 に 2 流す。直行の辺は費用 3 で、迂回の 2 より高い)
入力例2
2 2
0 0
1 2 3 -2
2 1 2 1出力例2
-2(供給も需要もないが、費用 $-2+1=-1$ の閉路に 2 回す)
入力例3
4 6
3 0 -1 -2
1 2 2 4
1 3 2 6
2 3 1 -3
2 4 2 2
3 4 3 1
4 1 1 -5出力例3
14(3 例とも逐次最短路(SSP)と一致)
背景と出典
費用スケーリング法は Goldberg–Tarjan(1990 年前後、*Finding minimum-cost circulations by successive approximation*)による、と私は記憶している(原典は本セッションでは確認していない)。LEMON などのライブラリにある CostScaling はこの系統だ、と記憶している(同じく未確認)。
中心の概念は ε-最適性: 頂点ポテンシャル $p$ があって、すべての残余辺 $(u,v)$ で被約費用 $c_p(u,v)=w(u,v)+p_u-p_v\ge-\varepsilon$。$\varepsilon=0$ なら負の閉路が無く最適。費用をあらかじめ $N$ 倍しておけば、どの閉路も $N$ 本以下の辺なので、$\varepsilon=1$(元の費用で $1/N$)で最適になる。
各段で $\varepsilon$ を $1/\alpha$ に縮め、「負の被約費用の辺を全部飽和→余りを押し出す」で $\varepsilon$-最適なフローを作り直す(refine)。本問は自作。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
増加路を探さない。「頂点に溜まった余り(excess)」を、被約費用が負の辺(許容辺)へ押し出す。押し出せる辺が無ければ、その頂点のポテンシャルを下げて(再ラベル)許容辺を作る。最大流の push/relabel と同じ形。
ヒント2(アプローチ)
費用を $N$ 倍し、$\varepsilon=\max|w|\cdot N$ から始める。各段:
- $\varepsilon\leftarrow\max(1,\lfloor\varepsilon/\alpha\rfloor)$
- 被約費用が負の残余辺を全部飽和させる(これで 0-最適、ただし収支は崩れる)
- 余りのある頂点を FIFO で処理し、許容辺へ押し出す。無ければ再ラベル
$\varepsilon=1$ の段を終えたら終了。
ヒント3(誘導)
- 許容辺: 残余容量 $>0$ かつ $w+p_u-p_v<0$
- 再ラベル: $p_u\leftarrow\max_{(u,v)\text{ 残余}}(p_v-w(u,v))-\varepsilon$(最小の被約費用がちょうど $-\varepsilon$ になる)
- 現在辺ポインタ
cur[u]を持ち、見終わったら再ラベルして先頭に戻す - 答えは元の($N$ 倍していない)費用で $\sum(c_i-\text{残余}_i)w_i$
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque
def min_cost_b_flow(n, edges, b, alpha=8):
# edges: (u, v, 容量, 費用)。b[v] = 供給(正)/ 需要(負)。実行可能が前提
m = len(edges)
to = [0] * (2 * m)
cap = [0] * (2 * m)
cost = [0] * (2 * m)
frm = [0] * (2 * m)
g = [[] for _ in range(n)]
for i, (u, v, c, w) in enumerate(edges):
to[2 * i], cap[2 * i], cost[2 * i], frm[2 * i] = v, c, w * n, u # 費用を n 倍
to[2 * i + 1], cap[2 * i + 1], cost[2 * i + 1], frm[2 * i + 1] = u, 0, -w * n, v
g[u].append(2 * i)
g[v].append(2 * i + 1)
excess = list(b)
p = [0] * n
eps = max([abs(c) for c in cost] + [1])
while True:
eps = max(1, eps // alpha) # ε を 1/α 倍ずつ小さくし、最後に ε=1 の段を必ず回す
# (1) 被約費用が負の残余辺をすべて飽和させる → 0-最適の前フロー
for e in range(2 * m):
if cap[e] > 0 and cost[e] + p[frm[e]] - p[to[e]] < 0:
f = cap[e]
cap[e] = 0
cap[e ^ 1] += f
excess[frm[e]] -= f
excess[to[e]] += f
# (2) 余りのある頂点から、許容辺(被約費用 < 0)で押し出す。無ければ再ラベル
cur = [0] * n
q = deque(v for v in range(n) if excess[v] > 0)
inq = [excess[v] > 0 for v in range(n)]
while q:
u = q.popleft()
inq[u] = False
gu = g[u]
while excess[u] > 0:
if cur[u] == len(gu): # 再ラベル: 最も小さい被約費用を -eps にする
best = None
for e in gu:
if cap[e] > 0:
t = p[to[e]] - cost[e]
if best is None or t > best:
best = t
p[u] = best - eps
cur[u] = 0
continue
e = gu[cur[u]]
v = to[e]
if cap[e] > 0 and cost[e] + p[u] - p[v] < 0:
f = excess[u] if excess[u] < cap[e] else cap[e]
cap[e] -= f
cap[e ^ 1] += f
excess[u] -= f
excess[v] += f
if excess[v] > 0 and not inq[v]:
inq[v] = True
q.append(v)
if cap[e] == 0:
cur[u] += 1
else:
cur[u] += 1
if eps == 1:
break
return sum((c - cap[2 * i]) * w for i, (u, v, c, w) in enumerate(edges))
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
b = [int(x) for x in data[2:2 + n]]
edges = []
p = 2 + n
for _ in range(m):
u, v, c, w = int(data[p]) - 1, int(data[p + 1]) - 1, int(data[p + 2]), int(data[p + 3])
p += 4
edges.append((u, v, c, w))
print(min_cost_b_flow(n, edges, b))
if __name__ == "__main__":
main()
計算量: 段の数 $O(\log_\alpha(NW))$、1 段あたり汎用 push/relabel の上界で $O(N^2M)$(FIFO 版の上界は $O(N^3)$ と記憶。本セッションでは導出していない)。$N=300,M=3000$ で 0.27〜0.47 s($\alpha=8$。Python 3.14.7、M1 MacBook Pro、負荷平均 15〜23 の混んだ時間帯での実測)。
Step-by-Step 解説
すべての残余辺で $w+p_u-p_v\ge-\varepsilon$ なら、どの残余閉路も費用 $\ge-\varepsilon\times(\text{辺数})\ge-\varepsilon N$。費用を $N$ 倍してあるので、元の費用では $\ge-\varepsilon$。$\varepsilon=1$ なら元の費用で $>-1$、整数なので $\ge0$。負の閉路が無い=最適。
$N$ 倍を忘れると、$\varepsilon=1$ で止めても負の閉路が残りうる。700 ケース中 15 ケースで誤答(実測)。
前の段で $\alpha\varepsilon$-最適だったフローを、$\varepsilon$-最適にしたい。まず被約費用が負の残余辺をすべて飽和させる。飽和した辺は残余グラフから消え、逆辺の被約費用は正になる。これで 0-最適(=どの $\varepsilon$ でも最適性条件を満たす)になるが、各頂点の収支が崩れて余り(excess)と不足が生じる。
余りのある頂点 $u$ から、許容辺(残余容量 $>0$、被約費用 $<0$)へ $\min(\text{余り},\text{容量})$ を流す。被約費用が $-\varepsilon$ 以上 $0$ 未満の辺に流すので、逆辺の被約費用は $0$ より大きく $\varepsilon$ 以下になり、ε-最適性は崩れない。
$u$ から許容辺が無いとき、$p_u$ を $\max(p_v-w)-\varepsilon$ に下げる。これで最小の被約費用がちょうど $-\varepsilon$ になり、許容辺が 1 本以上でき、他の辺の被約費用は $-\varepsilon$ 以上のまま。実行可能が保証されているので、余りのある頂点には必ず残余辺がある(余りは供給か、流れ込んだものを逆辺で戻せる)。
$\alpha$ が大きいと段数は減るが、1 段の仕事が増える。$N=300,M=3000$ で $\alpha=2,8,16$ が 1.09 / 0.47 / 0.60 s、$N=1000,M=5000$ で 3.28 / 2.06 / 4.11 s(実測)。本解は $\alpha=8$。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 逐次最短路(負の辺を飽和+Bellman–Ford) | 増加回数×$O(NM)$ | 検算用のみ | 700 ケースの照合に使用。$N=300,M=3000$ で 1.51〜2.69 s、$N=1000,M=5000$ で 18.77 s(実測。ただし Dijkstra+ポテンシャルではなく SPFA で書いたので、公平な比較ではない) |
| 容量スケーリング(Day172-Q4) | $O(M\log U\cdot\text{Dijkstra})$ | 比較せず | Day172-Q4 の実測は $N=300,M=3000$ で 1.01〜1.07 s。同じ入力で並べて測っていないので、本解の 0.27〜0.47 s との差は参考程度 |
| 再ラベルを $p_u\leftarrow p_u-\varepsilon$ に固定 | 同じ上界 | 不採用 | 書くのは簡単だが、1 回で許容辺ができる保証がなく、再ラベルの回数が増える(私の判断。計測はしていない) |
| ネットワーク単体法 | 理論上は指数 | 不採用 | 実用上は速いと記憶しているが、ピボット規則と木の更新の実装が長い |
| 費用スケーリング+FIFO push/relabel(本解) | 上記 | 採用 | 最短路を計算せず、負の費用も特別扱い不要 |
前提が変わったら:
- 実行可能かどうか分からないなら、本解は余りを押し出せずに再ラベルを繰り返して止まらない。先に最大流(Dinic)で実行可能性を確かめるか、超頂点への費用の大きい辺を足して「流せなかった量」を罰金にする
- $N$ が $10^4$ 以上なら、Python では 1 段の push/relabel が重い。C++ の LEMON
CostScalingか、global update(ポテンシャルを Dijkstra 風に一括更新するヒューリスティック)を入れる(本セッションでは未実装) - 費用が実数なら、$N$ 倍して $\varepsilon=1$ で止める議論が使えない。$\varepsilon<1/N$ で止めても最適の保証は「整数費用」に頼っている
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 費用を $N$ 倍しない | $\varepsilon=1$ が元の費用で $1/N$ 未満になっていない | 700 ケース中 15 ケースで誤答(実測) |
| 段の始めに負の辺を飽和しない | 前の段の $\alpha\varepsilon$-最適のままでは、許容辺で押し出しても ε-最適にならない | 段ごとに必ず飽和させる |
| 再ラベルで $\min$ と $\max$ を取り違える | 被約費用 $w+p_u-p_v$ を下げたいので $p_u$ を下げる | $p_u\leftarrow\max_v(p_v-w)-\varepsilon$ |
| 最後の $\varepsilon=1$ の段を飛ばす | eps //= alpha で 0 になり、ループを抜ける | eps = max(1, eps // alpha) にして、eps == 1 の段を終えてから抜ける |
| 答えを $N$ 倍した費用で出す | 内部の費用と元の費用の混同 | 元の $w_i$ で $\sum f_iw_i$ を計算する |
次のステップ
- 発展1: global update と price refinement(どちらも LEMON にあると記憶)を入れて、$N=10^4$ で測る
- 発展2: 実行可能性の判定を組み込む(Dinic で先に確かめる)
- 発展3: Goldberg–Tarjan の原典で、FIFO 版の計算量の上界を確認する(本セッションでは未確認)
検証ログ(本セッションで実行した数値)
- 逐次最短路(負の辺を先に飽和+SPFA)との照合(実測): $N\le8,M\le16$、容量の上限 $\{1,3,10,1000\}$、費用の上限 $\{1,5,100,10^6\}$ のランダム 700 ケースで不一致 0。$\alpha=2$ でも 0
- 全列挙との照合(実測): $N\le4,M\le4$、容量 2 以下の 400 ケースで、各辺の流量を全通り試した最小値と不一致 0
- 誤った方法(実測): 費用を $N$ 倍しない版は 700 ケース中 15 ケースで誤答
- 入力例(実測): 例 1
4、例 2-2、例 314(SSP と一致) - 実行時間(実測、負荷平均 15〜23): $N=300,M=3000$(容量 $10^6$)で $\alpha=2/8/16$ が 1.09/0.47/0.60 s、容量 10 で 0.47/0.27/0.40 s。$N=1000,M=5000$ で 3.28/2.06/4.11 s。SPFA の SSP はそれぞれ 2.69 s・1.51 s・18.77 s(答えは全一致)
- 扱っていない範囲: 実行不能の判定、global update などの実用的な高速化、Dijkstra 版 SSP や Day172-Q4 との同一入力での比較、原典の確認