Day 173-Q4 — 2026-10-09 | 費用スケーリングの最小費用 b-フロー(ε-最適性と push/relabel)

2026-10-09 赤色 Master (Phase 8+) ★★★★★★★★★ フロー(焦点帯 M2)。Day172-Q4 は容量をスケーリングし、増加路(Dijkstra)で流した。今回は費用をスケーリングし、増加路を使わずに「余りのある頂点から許容辺へ押し出す」push/relabel で流す。最短路を 1 回も計算しない

問題

$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/α ずつ縮め、各段で ε-最適なフローを作り直す(費用は N 倍済み) ε = max|w|·N / αε / α…ε = 1 の段で終了(= 元の費用で 1/N 未満 → 最適) 各段: ① 被約費用 w + p_u − p_v < 0 の残余辺を全部飽和(0-最適、収支は崩れる) ② 余りのある頂点 u から許容辺(被約費用 < 0)へ押し出す(push) ③ 許容辺が無ければ p_u ← max(p_v − w) − ε(relabel)。最小の被約費用がちょうど −ε に 最短路は 1 回も計算しない。負の費用の辺も特別扱い不要

ヒント(クリックで段階的に開示)

ヒント1(方向性)

増加路を探さない。「頂点に溜まった余り(excess)」を、被約費用が負の辺(許容辺)へ押し出す。押し出せる辺が無ければ、その頂点のポテンシャルを下げて(再ラベル)許容辺を作る。最大流の push/relabel と同じ形。

ヒント2(アプローチ)

費用を $N$ 倍し、$\varepsilon=\max|w|\cdot N$ から始める。各段:

  1. $\varepsilon\leftarrow\max(1,\lfloor\varepsilon/\alpha\rfloor)$
  2. 被約費用が負の残余辺を全部飽和させる(これで 0-最適、ただし収支は崩れる)
  3. 余りのある頂点を 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 解説

1ε-最適なら「ほぼ最適」

すべての残余辺で $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 ケースで誤答(実測)。

2段の始めに負の辺を全部飽和

前の段で $\alpha\varepsilon$-最適だったフローを、$\varepsilon$-最適にしたい。まず被約費用が負の残余辺をすべて飽和させる。飽和した辺は残余グラフから消え、逆辺の被約費用は正になる。これで 0-最適(=どの $\varepsilon$ でも最適性条件を満たす)になるが、各頂点の収支が崩れて余り(excess)と不足が生じる。

3押し出し(push)

余りのある頂点 $u$ から、許容辺(残余容量 $>0$、被約費用 $<0$)へ $\min(\text{余り},\text{容量})$ を流す。被約費用が $-\varepsilon$ 以上 $0$ 未満の辺に流すので、逆辺の被約費用は $0$ より大きく $\varepsilon$ 以下になり、ε-最適性は崩れない。

4再ラベル(relabel)

$u$ から許容辺が無いとき、$p_u$ を $\max(p_v-w)-\varepsilon$ に下げる。これで最小の被約費用がちょうど $-\varepsilon$ になり、許容辺が 1 本以上でき、他の辺の被約費用は $-\varepsilon$ 以上のまま。実行可能が保証されているので、余りのある頂点には必ず残余辺がある(余りは供給か、流れ込んだものを逆辺で戻せる)。

5α の選び方

$\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、例 3 14(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 との同一入力での比較、原典の確認

自己評価

自分の回答

気づき・メモ