Day 170-Q4 — 2026-10-05 | 負の閉路がある最小費用 b-フロー(負のコストの辺を先に容量いっぱい流し、逆辺だけ残すと全辺が非負になる)

2026-10-05 赤色 Master (Phase 8+) ★★★★★★★★★ グラフ・フロー(焦点帯外 M2)。Day037-Q5 は「負の辺はあるが負の閉路は無い」前提で、Bellman–Ford のポテンシャルから Dijkstra を回した。負の閉路があるとポテンシャルが存在せず、その方法は誤る。負の辺を先に飽和させる変形で、閉路があってもなくても同じコードで解く

問題

頂点 $1..N$、有向辺 $M$ 本のネットワークがある。辺 $k$ は $u_k\to v_k$、容量 $c_k$、1 単位あたりのコスト $w_k$(負もあり得る。負のコストは「流すと利益が出る」こと)。各頂点 $v$ に供給量 $b_v$ がある(正なら湧き出し、負なら吸い込み、$\sum b_v=0$)。

各辺の流量 $0\le f_k\le c_k$(整数)を決めて、すべての頂点で

$$(\text{出ていく流量})-(\text{入ってくる流量})=b_v$$

を満たすとき、$\sum_k w_kf_k$ の最小値を求めよ。条件を満たす流し方が無ければ infeasible と出力せよ。

入力形式

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\le300$、$1\le M\le3000$
2$\vert b_v\vert \le10^{12}$、$\sum b_v=0$
3$1\le u_k,v_k\le N$、$u_k\ne v_k$(多重辺はあり得る)
4$0\le c_k\le10^9$、$\vert w_k\vert \le10^6$
5入力はすべて整数

入出力例

入力例1

3 3
2 0 -2
1 2 2 1
2 3 3 1
3 2 1 -5

出力例1

0

(1→2 に 2、2→3 に 3、3→2 に 1。2→3→2 の閉路はコスト $1-5=-4$ の負の閉路で、1 単位回すと得。$2+3-5=0$)

入力例2

2 2
0 0
1 2 5 -3
2 1 4 1

出力例2

-8

(供給は 0 でも、負の閉路 1→2→1 に 4 単位回して $-12+4=-8$)

入力例3

2 1
3 -3
1 2 2 1

出力例3

infeasible

背景と出典

最小費用流の逐次最短路法(SSP)は、残余グラフに負の閉路が無いこと(=各頂点のポテンシャルで全辺の被約コストを非負にできること)を前提にする。Edmonds–Karp(1972)がポテンシャル付き Dijkstra の形を示した、と私は理解している(原典は本セッションでは未確認)。

負の閉路があってもよい一般の最小費用流(最小費用循環)の解き方は大きく 2 つ。

  1. 負閉路消去法(Klein 1967): まず実行可能な流れを作り、残余グラフの負の閉路を見つけては流し切る。正しいが遅い
  2. 負の辺の先行飽和: コストが負の辺 $u\to v$ には最初から容量いっぱい $c$ を流したことにし、$b_u$ から $c$ を引き、$b_v$ に $c$ を足す。残余グラフにはその辺の逆辺 $v\to u$(コスト $-w>0$)だけが残るので、全辺のコストが非負になり、負の閉路も消える。あとは超頂点を足した普通の SSP

本問は 2 を主解、1 を検算に使う。2 の名前は AtCoder Library の解説や日本語の競プロ記事で「負辺を飽和させる」と呼ばれているのを見た記憶がある(特定の出典は未確認)。

概念図

左: 入力例 1(b = 2, 0, −2)。2→3→2 はコスト 1−5 = −4 の負の閉路 1b=2 2 3b=−2 cap2, w1 cap3, w1 cap1, w−5 最適: 1→2 に 2、2→3 に 3、3→2 に 1。コスト 2+3−5 = 0 Bellman–Ford+Dijkstra は閉路に流さず 4 を出す 右: 負の辺 3→2 を先に飽和させた後の残余グラフ 1b=2 2b=0+1=1 3b=−2−1=−3 cap2, w1 cap3, w1 逆辺 2→3 cap1, w+5 定数 1×(−5) = −5 を先に計上。全辺が非負 → Dijkstra でよい SSP の結果 5(2→3 に 3 流す: 2+3、逆辺は使わない) 答え −5 + 5 = 0

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

ヒント1(方向性)

「負のコストの辺は、流せるだけ流したほうが得」が出発点。もし最適解でその辺に容量より少なく流していたら、それは「流したものを一部押し戻した」と見なせる。押し戻すことを辺の逆向きの流れとして表すと、コストの符号が反転する。

ヒント2(アプローチ)

コスト $w<0$ の辺 $u\to v$(容量 $c$)を次のように置き換える。

変更内容
定数答えに $c\cdot w$ を足しておく
供給$b_u\leftarrow b_u-c$、$b_v\leftarrow b_v+c$
辺$v\to u$、容量 $c$、コスト $-w$($>0$)を追加。元の辺は消す

変換後のどの流れも元の問題の流れと 1 対 1 に対応し(新しい辺の流量 $g$ ↔ 元の辺の流量 $c-g$)、コストは定数 $cw$ だけずれる。

ヒント3(誘導)

超頂点 $S$ から $b_v>0$ の頂点へ容量 $b_v$、$b_v<0$ の頂点から $T$ へ容量 $-b_v$ の辺を張る。全辺が非負コストなので、ポテンシャル 0 から Dijkstra の SSP を回せる。$S$ から出る容量を流し切れなければ infeasible。

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
from heapq import heappush, heappop


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]]
    pos = 2 + n
    S, T = n, n + 1
    V = n + 2
    # 残余グラフ: 辺 k の逆辺は k^1
    to, cap, cost, head, nxt = [], [], [], [-1] * V, []

    def add_edge(u, v, c, w):
        to.append(v); cap.append(c); cost.append(w); nxt.append(head[u]); head[u] = len(to) - 1
        to.append(u); cap.append(0); cost.append(-w); nxt.append(head[v]); head[v] = len(to) - 1

    excess = b[:]
    base = 0
    for _ in range(m):
        u, v, c, w = (int(x) for x in data[pos:pos + 4]); pos += 4
        u -= 1; v -= 1
        if w >= 0:
            add_edge(u, v, c, w)
        else:
            # 負の辺は先に容量いっぱい流しておく。残余グラフには v→u(コスト -w > 0)だけが残る
            base += c * w
            excess[u] -= c
            excess[v] += c
            add_edge(v, u, c, -w)
    need = 0
    for v in range(n):
        if excess[v] > 0:
            add_edge(S, v, excess[v], 0)
            need += excess[v]
        elif excess[v] < 0:
            add_edge(v, T, -excess[v], 0)

    # 全辺のコストが非負なので、ポテンシャル 0 から Dijkstra で逐次最短路
    INF = float('inf')
    h = [0] * V
    flow = 0
    total = 0
    while flow < need:
        dist = [INF] * V
        pe = [-1] * V
        dist[S] = 0
        pq = [(0, S)]
        while pq:
            d, u = heappop(pq)
            if d > dist[u]:
                continue
            hu = h[u]
            k = head[u]
            while k != -1:
                if cap[k] > 0:
                    v = to[k]
                    nd = d + cost[k] + hu - h[v]
                    if nd < dist[v]:
                        dist[v] = nd
                        pe[v] = k
                        heappush(pq, (nd, v))
                k = nxt[k]
        if dist[T] == INF:
            print("infeasible")
            return
        for v in range(V):
            if dist[v] < INF:
                h[v] += dist[v]
        f = need - flow
        v = T
        while v != S:
            k = pe[v]
            if cap[k] < f:
                f = cap[k]
            v = to[k ^ 1]
        v = T
        while v != S:
            k = pe[v]
            cap[k] -= f
            cap[k ^ 1] += f
            v = to[k ^ 1]
        flow += f
        total += f * (h[T] - h[S])
    print(base + total)


main()

計算量: 1 回の増加が Dijkstra の $O((N+M)\log N)$。増加の回数は理論上は流量に比例し得るが、各回でどこかの辺が飽和するので実際は辺数程度。$N=300,M=3000$(負の辺 5 割)で 増加 830 回・1.04 s(Python 3.14.7、M1 Mac、実測)。答えの絶対値は $3000\cdot10^9\cdot10^6=3\times10^{18}$ 以下で 64 ビットに収まる(導出)。

Step-by-Step 解説

1先行飽和が元の問題と同値な理由

元の辺の流量 $f\in[0,c]$ と、新しい逆辺の流量 $g=c-f\in[0,c]$ は 1 対 1。保存則は $u$ で「出る $f$」が「出る $c$ − 入る $g$」に、$v$ で「入る $f$」が「入る $c$ − 出る $g$」に変わるだけなので、$b$ を $\mp c$ ずらせば一致する。コストは $wf=wc+(-w)g$。

2なぜ負の閉路が消えるか

変換後の辺はすべてコスト非負。超頂点の辺もコスト 0。だから初期の残余グラフに負の閉路は無く、ポテンシャル 0 が正しい。SSP は「最短路で増やす限り負の閉路が生まれない」性質を保つので、最後まで Dijkstra が使える。

3Bellman–Ford+Dijkstra(Day037-Q5 型)が誤る理由

負の閉路があると、Bellman–Ford の距離は収束せず、得られた値をポテンシャルにしても被約コストが負の辺が残る。Dijkstra は確定済みの頂点を更新できないので、最短でない路で増やし、負の閉路にも流さない。入力例 2 で 0(正解 −8)、入力例 1 で 4(正解 0)を出す(実測)。

4供給が 0 でも流す

入力例 2 のように $b$ がすべて 0 でも、負の閉路に流すと得をする。「$S$ から流す量が 0 だから何もしない」は誤り。先行飽和すると $b$ が 0 でなくなり、自動的に正しく流れる。

迷った他案と採らなかった理由

案計算量採否理由
変数ごとの全列挙指数検算用のみ$N\le4,M\le6$、容量 $\le2$ の 300 件の正解
負閉路消去法(最大流→Bellman–Ford で閉路を消す)閉路の数 × $O(NM)$検算用のみ正しいが遅い。中規模 200 件と $N=60,M=400$ の 5 件で本解と一致
Bellman–Ford のポテンシャル+Dijkstra$O(NM)+$SSP不採用負の閉路で誤る(小 19/300・中 87/200 誤答)
先行飽和+Dijkstra の SSP(本解)増加回数 × $O(M\log N)$採用全照合で一致、$N=300,M=3000$ で 1.04 s
容量スケーリング$O(M\log U\cdot M\log N)$不採用増加回数が流量に比例する入力で強いが、本セッションで実装・検証していない
ネットワーク単体法実用上最速級不採用実装が長い。$N,M$ が $10^5$ なら LEMON などの実装を使う側に回る

前提が変わったら:

  • 負の閉路が無いと保証されるなら、Bellman–Ford(または DAG なら DP)でポテンシャルを作る Day037-Q5 の方法でもよい。先行飽和でも同じく正しい
  • $N=1000,M=5000$ で 4.4〜5.1 s、$N=2000,M=10^4$ で 17.8 s(実測)。この規模なら容量スケーリングかネットワーク単体法に切り替える
  • 流量の上限を与えて「流量 $F$ の最小費用」なら、$S$ と $T$ をつないで $b$ を作るだけで同じコードになる

よくあるミス

ミス原因正しい書き方
Bellman–Ford のポテンシャルで Dijkstra負の閉路が無いと思い込む負の辺を先行飽和(87/200 誤答)
先行飽和で $b$ の符号を逆にする「出る−入る=$b$」の向き$b_u-=c$、$b_v+=c$
定数 $c\cdot w$ を足し忘れる変換後の流れのコストだけ出すbase += c * w
供給が全部 0 なら即 0 を出力循環の得を見落とす先行飽和すれば自動的に扱える(入力例 2)
容量 0 の負の辺で落ちる0 除算などを心配して特別扱い容量 0 なら何も変わらない(そのまま)

次のステップ

  • 発展1: 最適な流れを出力し(元の辺の流量 $=c-g$)、各頂点のポテンシャルが双対の最適解(被約コストが非負・相補性)になっていることを確かめる
  • 発展2: 容量スケーリングを実装し、$N=1000,M=5000$ で本解と比べる
  • 発展3: 「負の閉路があるかどうか」だけを $O(NM)$ で判定し、閉路を 1 つ復元する

検証ログ(本セッションで実行した数値)

  • 全列挙との照合(実測): $N\le4$、$M\le6$、容量 $\le2$、コスト $|w|\le5$(半分を負に)のランダム 300 件で本解・負閉路消去法とも不一致 0。うち 134 件が infeasible
  • 負閉路消去法との照合(実測): $N\le30$、$M\le80$、容量 $\le10^6$ のランダム 200 件で不一致 0(84 件が infeasible)。$N=60,M=400$、容量 $\le10^9$ の 5 件も一致(例: −5782574504741494)
  • 誤った方法(実測): Bellman–Ford+Dijkstra は小 300 件中 19 件、中 200 件中 87 件誤答。入力例 1 で 4、例 2 で 0 を出す
  • 実行時間(実測): $N=200,M=2000$ で 0.40〜0.63 s(増加 454〜670 回)、$N=300,M=3000$ で 1.04 s(830 回、$|w|\le10^6$)と 1.14 s(838 回、$|w|\le10^9$)、$N=1000,M=5000$ で 4.44・5.06 s、$N=2000,M=10^4$ で 17.83 s(これが $N\le300$ にした理由)
  • 入力例(実測): 例 1 0、例 2 -8、例 3 infeasible(全列挙でも同じ)
  • 扱っていない範囲: 容量スケーリング・ネットワーク単体法、双対解の出力、Klein・Edmonds–Karp の原典

自己評価

自分の回答

気づき・メモ