Day 121-Q1 — Gomory-Hu Tree(全点対最小カット木)

2026-08-13 赤色 Master / Phase 8+ ★★★★★★★★★ グラフ理論・最大流・全点対最小カット

問題

N頂点M辺の無向グラフがあり、各辺 $(u, v)$ には容量 $c$ が定められている。Q個のクエリが与えられるので、各クエリ $(u, v)$ に対して「頂点 $u$ と頂点 $v$ を切り離すのに必要な最小カット容量(= u-v間の最大流量)」を出力せよ。

入力形式

N M
u_1 v_1 c_1
...
u_M v_M c_M
Q
s_1 t_1
...
s_Q t_Q

制約

$2 \le N \le 200$
$N-1 \le M \le 2000$
グラフは連結
$1 \le c \le 10^9$
$1 \le Q \le 200$
$s \ne t$

入出力例

入力例1

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

出力例1

5
7

入力例2

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

出力例2

6
5
5
5
5
7

概念図: サイクルグラフとGomory-Hu Tree

元のグラフ (サイクル) Gomory-Hu Tree 1 2 3 4 4 2 5 3 1 2 3 4 6 5 7 mincut(2,4) = パス2-1-3-4上の最小重み = min(6,5,7) = 5

ヒント(段階的開示)

ヒント1: 方向性
Q個のクエリごとに毎回最大流を計算すると $O(Q \times \text{MaxFlow})$ かかり、$Q$ が大きいと間に合わない。しかし全点対 $\binom{N}{2}$ 通りの最小カット値には強い構造がある——それらはたった $N-1$ 本の辺を持つ木で完全に表現できる。この木を1回だけ作ってしまえば、以降のクエリは木上のパス最小値を求めるだけでよい。
ヒント2: アプローチ
Gusfieldの構築法は次のように動く。頂点1を根とし全頂点の親を仮に頂点1とする。頂点 $i=2,\ldots,N$ の順に、「$i$ と現在の親 $p_i$ の間の最大流」を計算する。このとき最大流に対応する最小カットの分割 $(S,T)$($i\in S$, $p_i\in T$)が得られるので、木の辺 $(i,p_i)$ の重みをこの最大流量とし、まだ処理していない頂点 $j>i$ で親が $p_i$ かつ $S$ 側にいるものがあれば親を $i$ に付け替える。
ヒント3: 誘導(コード骨格)
parent = [0] * (N + 1)   # parent[i] = 木上の親頂点
weight = [0] * (N + 1)   # weight[i] = 辺(i, parent[i])の重み

for i in range(2, N + 1):
    flow, s_side = max_flow_with_mincut(i, parent[i])
    weight[i] = flow
    for j in range(i + 1, N + 1):
        if parent[j] == parent[i] and j in s_side:
            parent[j] = i

# クエリ処理: sからtまで木を辿りながら最小重みを求める

模範解答 (Python)

import sys
from collections import deque


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

    edges_uvc = []
    for _ in range(M):
        u = int(input_data[idx]); idx += 1
        v = int(input_data[idx]); idx += 1
        c = int(input_data[idx]); idx += 1
        edges_uvc.append((u, v, c))

    class Dinic:
        def __init__(self, n):
            self.n = n
            self.graph = [[] for _ in range(n)]

        def add_edge(self, u, v, cap):
            self.graph[u].append([v, cap, len(self.graph[v])])
            self.graph[v].append([u, cap, len(self.graph[u]) - 1])

        def bfs(self, s):
            self.level = [-1] * self.n
            self.level[s] = 0
            q = deque([s])
            while q:
                u = q.popleft()
                for e in self.graph[u]:
                    v, cap, _ = e
                    if cap > 0 and self.level[v] < 0:
                        self.level[v] = self.level[u] + 1
                        q.append(v)

        def dfs(self, u, t, f):
            if u == t:
                return f
            while self.it[u] < len(self.graph[u]):
                e = self.graph[u][self.it[u]]
                v, cap, rev = e
                if cap > 0 and self.level[v] == self.level[u] + 1:
                    d = self.dfs(v, t, min(f, cap))
                    if d > 0:
                        e[1] -= d
                        self.graph[v][rev][1] += d
                        return d
                self.it[u] += 1
            return 0

        def max_flow(self, s, t):
            flow = 0
            while True:
                self.bfs(s)
                if self.level[t] < 0:
                    return flow
                self.it = [0] * self.n
                while True:
                    f = self.dfs(s, t, float('inf'))
                    if f == 0:
                        break
                    flow += f

        def min_cut_side(self, s):
            visited = [False] * self.n
            visited[s] = True
            q = deque([s])
            while q:
                u = q.popleft()
                for e in self.graph[u]:
                    v, cap, _ = e
                    if cap > 0 and not visited[v]:
                        visited[v] = True
                        q.append(v)
            return visited

    def build_dinic():
        d = Dinic(N + 1)
        for u, v, c in edges_uvc:
            d.add_edge(u, v, c)
            d.add_edge(v, u, c)
        return d

    parent = [1] * (N + 1)
    weight = [0] * (N + 1)

    for i in range(2, N + 1):
        d = build_dinic()
        flow = d.max_flow(i, parent[i])
        weight[i] = flow
        s_side = d.min_cut_side(i)

        for j in range(i + 1, N + 1):
            if parent[j] == parent[i] and s_side[j]:
                parent[j] = i

    tree = [[] for _ in range(N + 1)]
    for i in range(2, N + 1):
        tree[i].append((parent[i], weight[i]))
        tree[parent[i]].append((i, weight[i]))

    def query(s, t):
        visited = [False] * (N + 1)
        visited[s] = True
        q = deque([(s, float('inf'))])
        while q:
            u, best = q.popleft()
            if u == t:
                return best
            for v, w in tree[u]:
                if not visited[v]:
                    visited[v] = True
                    q.append((v, min(best, w)))
        return -1

    Q = int(input_data[idx]); idx += 1
    out = []
    for _ in range(Q):
        s = int(input_data[idx]); idx += 1
        t = int(input_data[idx]); idx += 1
        out.append(str(query(s, t)))
    print('\n'.join(out))


solve()
計算量: 木の構築に $N-1$ 回のDinic法最大流を実行する。1回のDinicは $O(V^2E)$ 程度(一般グラフの目安)で、$N\le200,\ M\le2000$ の制約下では十分高速。クエリは木を毎回BFSで辿るので $O(N)$、$Q\le200$ 件でも余裕がある。手計算で検証したサイクルグラフ(1-2:4,2-3:2,3-4:5,4-1:3)の全 $\binom{4}{2}=6$ 通りの最小カットを列挙し、構築した木のパス最小値と完全一致することを確認済み。

Step-by-Step 解説

1全点対最小カットに潜む木構造
$N$頂点のグラフには $\binom{N}{2}$ 通りの頂点対があるが、最小カットの値は劣モジュラ性という性質を満たしており、全点対の最小カット値はちょうど $N-1$ 本の重み付き辺からなる木のパス最小値として表現できる。
2Gusfieldの簡略構築法
オリジナルのGomory-Hu構築法は頂点縮約を伴うが、Gusfieldはグラフを変形せず元のまま $N-1$ 回の最大流を計算するだけで同じ木が作れることを示した。
3親の初期化と逐次更新
頂点1を根として全頂点の親を1に初期化し、頂点2からNまで順に「iと親$p_i$の最大流」を計算して木の辺重みとする。
4最小カット側の集合を使った親の付け替え
残余グラフでiから到達可能な頂点集合$S$(最小カットのi側)を求め、未処理の頂点$j>i$で親が$p_i$かつ$S$側にいるものは親を$i$に付け替える。
5クエリ処理
木が完成すれば、2頂点間の最小カットは木上のパスに現れる辺の最小重みに等しい。$N\le200$ ならBFSで十分高速。

よくあるミス

ミス原因正しい書き方
クエリのたびに毎回最大流を計算してしまう前処理の再利用という利点を理解していない木は最初に1回だけ構築し、クエリは木上のパス最小値で答える
無向辺を片方向の弧としてしか張らない有向グラフの最大流をそのまま流用してしまう無向辺(u,v,c)はu→vとv→uの両方に容量cの弧を張る
Dinicを毎回使い回して残余状態が残ったまま次に進むグラフオブジェクトの再構築コストを気にしすぎるN-1回それぞれで新しいDinicインスタンスを構築する
親の付け替え条件を重みの大小で判定してしまうカット構造ではなく数値比較で分岐を書いてしまう残余グラフでの到達可能性(S側判定)で分岐する

次のステップ

  • 発展: クエリが非常に多い($Q\le10^5$)場合に対応するため、LCA前計算でパス最小値クエリを $O(\log N)$ にする。
  • 次回予告: Push-Relabel法(Preflow-Push + Gap Heuristic)

自己評価

自分の回答

気づき・メモ