Day 091-Q2 — 最小費用流(MCMF・ポテンシャル法 Dijkstra)

2026-07-14 赤色 Master / Phase 8+ ★★★★★★★★★ 最小費用流・Successive Shortest Path

問題

$N$ 頂点 $M$ 辺の有向グラフ(各辺に容量 $\mathrm{cap}$・単位コスト $\mathrm{cost}\ge0$)で、頂点 $1$ から $N$ への最大流量と、それを達成する最小総コストを求める。

制約

パラメータ範囲備考
$N$$2 \le N \le 200$頂点数
$M$$1 \le M \le 2000$辺数
$\mathrm{cap}_i$$1 \le \mathrm{cap}_i \le 10^6$容量
$\mathrm{cost}_i$$0 \le \mathrm{cost}_i \le 10^6$非負コスト

入出力例

入力例1

4 5
1 2 2 1
1 3 1 2
2 3 1 1
2 4 1 3
3 4 2 1

出力例1

3 10

最安路から順に $1\to2\to3\to4$(3), $1\to3\to4$(3), $1\to2\to4$(4) で合計10。

概念図

残余グラフ上で最安の増加路に繰り返し流す s=1 2 3 t=N cap2,c1 cap1,c2 cap1,c1 cap1,c3 cap2,c1 補正コスト = cost + h[u] - h[v] ≥ 0 → 負辺を消して Dijkstra を適用

ヒント

ヒント1(方向性)

同じ最大流の中で総コスト最小を得るには、最短(最安)の増加路に沿って繰り返し流す(Successive Shortest Path)。

ヒント2(アプローチ)

逆辺のコストは $-\mathrm{cost}$ で負辺が生じるため素の Dijkstra は使えない。

ヒント3(ほぼ答え)
# ポテンシャル法(Johnson 変換)
reduced = cost + h[u] - h[v]   # >= 0 が保証
# 1回の最短路後に h[v] += dist[v]
total_cost += f * h[t]         # h[t] が実距離

模範解答

import sys
import heapq

def main():
    data = sys.stdin.buffer.read().split()
    idx = 0
    n = int(data[idx]); idx += 1
    m = int(data[idx]); idx += 1

    # graph[u] = list of [to, cap, cost, rev_index]
    graph = [[] for _ in range(n + 1)]
    def add_edge(u, v, cap, cost):
        graph[u].append([v, cap, cost, len(graph[v])])
        graph[v].append([u, 0, -cost, len(graph[u]) - 1])

    for _ in range(m):
        u = int(data[idx]); v = int(data[idx+1])
        cap = int(data[idx+2]); cost = int(data[idx+3]); idx += 4
        add_edge(u, v, cap, cost)

    s, t = 1, n
    INF = float('inf')
    total_flow = 0
    total_cost = 0
    h = [0] * (n + 1)   # ポテンシャル(初期コスト非負なので0でよい)

    while True:
        dist = [INF] * (n + 1)
        dist[s] = 0
        prevv = [-1] * (n + 1)
        preve = [-1] * (n + 1)
        pq = [(0, s)]
        while pq:
            d, u = heapq.heappop(pq)
            if d > dist[u]:
                continue
            for i, e in enumerate(graph[u]):
                v, cap, cost, _ = e
                if cap > 0:
                    nd = d + cost + h[u] - h[v]   # 補正済みコスト >= 0
                    if nd < dist[v]:
                        dist[v] = nd
                        prevv[v] = u
                        preve[v] = i
                        heapq.heappush(pq, (nd, v))
        if dist[t] == INF:
            break   # これ以上流せない → 最大流に到達
        for v in range(1, n + 1):
            if dist[v] < INF:
                h[v] += dist[v]
        # 増加路のボトルネック容量を求める
        f = INF
        v = t
        while v != s:
            f = min(f, graph[prevv[v]][preve[v]][1])
            v = prevv[v]
        # 流す(順辺を減らし逆辺を増やす)
        v = t
        while v != s:
            e = graph[prevv[v]][preve[v]]
            e[1] -= f
            graph[v][e[3]][1] += f
            v = prevv[v]
        total_flow += f
        total_cost += f * h[t]   # h[t] は s->t の実際の最短コスト

    print(total_flow, total_cost)

main()

Step-by-Step 解説

Step 1: 残余グラフの表現

順辺 [v,cap,cost,rev] と逆辺 [u,0,-cost,rev] を張る。rev で逆辺容量を更新する。

Step 2: ポテンシャルで負辺を消す

補正コスト $\mathrm{cost}+h[u]-h[v]$ は三角不等式から非負が保証され Dijkstra が使える。

Step 3: 最短路に沿って流す

$s\to t$ 最短路の最小残余容量 $f$ を流し、順辺を $f$ 減らし逆辺を $f$ 増やす。

Step 4: ポテンシャル更新と総コスト加算

各頂点で $h[v] \mathrel{+}= \mathrm{dist}[v]$。実距離は $h[t]$ に一致し total_cost += f*h[t]。到達不能になれば最大流に到達し終了。

よくあるミス

ミス原因正しい書き方
逆辺コストを $+\mathrm{cost}$ にする残余の意味を誤解逆辺は $-\mathrm{cost}$・容量0
ポテンシャル未使用で誤答逆辺の負コストを無視補正コストで Dijkstra
total_cost += f*dist[t]dist は補正後の値実距離は更新後の h[t]
到達不能でも流し続ける終了条件漏れdist[t]==INF で break

次のステップ

  • 発展: 「ちょうど $F$ 単位流す最小費用」に変更
  • 発展: 負辺を含む場合の初回 Bellman-Ford 初期化
  • 次回予告: Aho-Corasick + オートマトンDP

自己評価

理解度: / /

自分の回答:

気づき・メモ: