Day 146-Q1 — Hao-Orlinアルゴリズムの基礎(大域最小カット・固定始点からのmax-flow帰着)

2026-09-07 赤色 Master / Phase 8+ ★★★★★★★★★ 固定始点からのN-1回max-flowの最小値が大域最小カットに一致することを構成的に実装

問題

$N$ 頂点 $M$ 辺の無向重み付きグラフが与えられる。大域最小カット(頂点集合を2つの非空部分 $S,V\setminus S$ に分割したときの、両者間を結ぶ辺重み総和の最小値)を求めよ。

以下の定理を利用する。

頂点 $0$ を固定する。$t=1,\dots,N-1$ について $\text{maxflow}(0,t)$ を計算すると、大域最小カット $=\min_t \text{maxflow}(0,t)$ が成り立つ。

直感: 大域最小カット $(S,V\setminus S)$ において頂点0はどちらか一方に属する。頂点0の反対側に $t$ を選べば $\text{maxflow}(0,t)\le$ 大域最小カット。逆に各 $\text{maxflow}(0,t)$ 自体が何らかの切断なので、大域最小カット $\le\min_t\text{maxflow}(0,t)$。両者を合わせて等号成立。

無向辺 $\{u,v\}$(重み $w$)は $u\to v$,$v\to u$ の2本の有向辺(各容量 $w$)として扱う。

入力形式

N M
u_1 v_1 w_1
:
u_M v_M w_M

制約

$2 \le N \le 10$
$N-1 \le M \le N(N-1)/2$
$0 \le u_i,v_i
$1 \le w_i \le 100$
グラフは連結(多重辺・自己ループなし)

入出力例

入力例1

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

出力例1

5

頂点0単独のカット(次数和 2+3=5)が最小。maxflow(0,1)=maxflow(0,2)=maxflow(0,3)=5となり最小値5が大域最小カットと一致する。

概念図:固定始点からのN-1回max-flow

固定頂点0からの3回のmax-flow計算 0 1 2 3 2 3 2 3 4 maxflow(0,1)=5 maxflow(0,2)=5 maxflow(0,3)=5 min(5,5,5)=5 が大域最小カット(頂点0単独のカット)と一致

ヒント(段階的開示)

ヒント1(方向性)

大域最小カットは全分割 $2^{N-1}-1$ 通りを試せば求まるが指数的。s-t最小カット(=s-t最大流)は多項式時間で解けるので、これをうまく使い回して大域最小カットに帰着したい。

ヒント2(アプローチ)

任意の大域最小カット $(S,V\setminus S)$ で頂点0は必ずどちらかに属する。頂点0の反対側にある頂点 $t$ を選べば $\text{maxflow}(0,t)$ はこのカットを候補として含む。全ての $t\ne0$ を試せば必ず一度は大域最小カットの反対側の頂点を $t$ として捉えられる。

ヒント3(誘導)
def dinic_maxflow(N, capacity, s, t):
    INF = float('inf')
    total_flow = 0
    while True:
        level = [-1] * N
        level[s] = 0
        queue = [s]
        for u in queue:
            for v in range(N):
                if capacity[u][v] > 0 and level[v] == -1:
                    level[v] = level[u] + 1
                    queue.append(v)
        if level[t] == -1:
            return total_flow
        # DFSでブロッキングフロー(省略)
        ...

各 $t$ ごとに容量行列を複製してから呼び出すこと。$N\le10$ならDinic法を9回呼んでも余裕で間に合う。

模範解答 (Python)

import sys
from copy import deepcopy

def dinic_maxflow(N, capacity, s, t):
    INF = float('inf')
    total_flow = 0
    while True:
        level = [-1] * N
        level[s] = 0
        queue = [s]
        head = 0
        while head < len(queue):
            u = queue[head]; head += 1
            for v in range(N):
                if capacity[u][v] > 0 and level[v] == -1:
                    level[v] = level[u] + 1
                    queue.append(v)
        if level[t] == -1:
            return total_flow

        it = [0] * N

        def dfs(u, f):
            if u == t:
                return f
            while it[u] < N:
                v = it[u]
                if capacity[u][v] > 0 and level[v] == level[u] + 1:
                    d = dfs(v, min(f, capacity[u][v]))
                    if d > 0:
                        capacity[u][v] -= d
                        capacity[v][u] += d
                        return d
                it[u] += 1
            return 0

        while True:
            f = dfs(s, INF)
            if f == 0:
                break
            total_flow += f
    return total_flow


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

    base_capacity = [[0] * N for _ in range(N)]
    for _ in range(M):
        u = int(data[idx]); idx += 1
        v = int(data[idx]); idx += 1
        w = int(data[idx]); idx += 1
        base_capacity[u][v] += w
        base_capacity[v][u] += w

    best = float('inf')
    for t in range(1, N):
        cap_copy = deepcopy(base_capacity)
        f = dinic_maxflow(N, cap_copy, 0, t)
        best = min(best, f)

    print(best)

main()

Step-by-Step 解説

1無向グラフの容量行列化
無向辺$\{u,v\}$(重み$w$)を、$u\to v$と$v\to u$の両方向に容量$w$を加算する形で隣接行列に落とし込む。
2固定始点0からのN-1回のmax-flow
頂点0を始点に固定し$t=1,\dots,N-1$についてDinic法で最大流を求める。各$t$ごとに容量行列をコピーし直すことが重要。
3最小値の取得と正当性
$N-1$個の$\text{maxflow}(0,t)$の最小値が大域最小カットの値と一致する。
4Hao-Orlinの高速化アイデア(発展)
本来のHao-Orlinは前回のpreflowを引き継ぎ活性頂点の選択規則を工夫することで全体O(NM log(N²/M))を達成する。Stoer-Wagner法(頂点縮約方式)とも異なる第三のアプローチ。

よくあるミス

ミス原因正しい書き方
容量行列を使い回して答えが小さくなりすぎる前回のDinic計算で容量を消費したまま次を計算`deepcopy`で毎回容量行列を複製する
無向辺を片方向にしか容量を張らない有向グラフの感覚で実装$u\to v$と$v\to u$の両方に同じ重みの容量を加算
$t=0$自身を含めてループしてしまうループ範囲を`range(N)`にする`range(1, N)`で始点自身を除く
多重辺で片方の辺の容量を上書きする`capacity[u][v]=w`と代入`capacity[u][v]+=w`で加算
Dinic法のlevel配列を使い回すループ外で1回だけ初期化毎フェーズでlevel配列を作り直す

次のステップ

  • 発展: preflowを再利用するHao-Orlinの高速化版を実装し、Dinic法をゼロから呼ぶ版とaugment回数を比較せよ。
  • 次回予告: Nemhauser-Trotter定理(頂点被覆問題のLP緩和・最大流帰着による半整数解の構成)

自己評価

自分の回答

気づき・メモ