Day 178-Q5 — 2026-10-14 | 最小カットに「必ず入る辺」「入りうる辺」を見分ける(最大流 1 回の残余グラフで、s から届く集合・t へ届く集合・強連結成分の 3 つを見るだけ)

2026-10-14 赤色 Master (Phase 8+) ★★★★★★★★★ フロー・最小カットの構造(焦点帯外 M2)。最小カットは一般に 1 つではない。すべての最小カットは、最大流の残余グラフで「辺の向きに閉じた集合」として 1 対 1 に対応する。だから「どの最小カットにも入る辺」は最小と最大の 2 つのカットを見れば決まり、「ある最小カットに入る辺」は残余グラフで $u$ から $v$ へ行けないことで決まる。辺ごとに最大流を解き直す方法($M$ 回)を 1 回に縮める

問題

頂点 $1..N$、有向辺 $M$ 本のネットワークがある。辺 $i$ は $u_i\to v_i$、容量 $c_i$。始点は $s=1$、終点は $t=N$。

$s\in S$、$t\notin S$ を満たす頂点集合 $S$ を カット と呼び、その容量を「$u\in S$、$v\notin S$ である辺 $u\to v$ の容量の和」とする。容量が最小のカットを 最小カット と呼ぶ。辺 $u\to v$ が $u\in S$、$v\notin S$ のとき「辺はカット $S$ に入る」という。

最小カットの容量を 1 行目に出力せよ。2 行目には各辺 $i$ について次の数を空白区切りで出力せよ。

  • 2: どの最小カットにも入る
  • 1: ある最小カットには入るが、入らない最小カットもある
  • 0: どの最小カットにも入らない

入力形式

N M
u_1 v_1 c_1
...
u_M v_M c_M

制約

#制約
1$2\le N\le10^4$、$1\le M\le10^5$
2$1\le u_i,v_i\le N$、$u_i\ne v_i$(多重辺はありうる)
3$1\le c_i\le10^9$

入出力例

入力例1

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

出力例1

6
1 1 1 2 0

(最小カットは $\{1\},\{1,2\},\{1,2,3\}$ の 3 つで容量はどれも 6。道の 3 本はそれぞれ 1 つずつにしか入らないので 1、$1\to4$ は 3 つすべてに入るので 2。逆向きの $3\to2$ はどれにも入らない)

入力例2

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

出力例2

5
0 0 2 0 0

(最小カットは $4\to5$ だけの 1 つ。$1\to2$ と $2\to4$ は流量が容量いっぱい(飽和)なのにどの最小カットにも入らない。「飽和した辺 = 入りうる辺」とすると 1 1 2 0 0 で誤る。2 例とも全カットの列挙と一致)

背景と出典

  • 「すべての最小カットは残余グラフの閉集合に対応する」ことは Picard–Queyranne(1980 と記憶、“On the structure of all minimum cuts in a network and applications”、原典は本セッションで未確認)による、と記憶している
  • Day168-Q4(最大重み閉包)では残余グラフで $s$ から届く集合を「最小の最適解」として使った。本問はその反対側($t$ へ届く集合)と強連結成分を足して、全最小カットの「共通部分」と「和」を辺ごとに答える
  • 本問の問題文・制約・入力例は自作。判定の条件は下で導き、全探索で確かめた

概念図

入力例 2:辺のラベルは 流量/容量。最小カットは {4→5} の 1 つだけ 1/1 → 0 1/1 → 0 5/5 → 2 4/5 → 0 4/5 → 0 1=s 2 3 4 5=t 唯一の最小カット(容量 5) 青=A(s から残余グラフで届く)= {1,2,3,4}。緑=B(t へ届く)= {5}。4→5 は A から B へ → 2(必ず入る) 1→2 と 2→4 は飽和しているが、残余路 2→1→3→4 があり 2 と 4 は同じ強連結成分 → 0(どれにも入らない) 「飽和 = 入りうる」とすると 1 1 2 0 0 で誤る。600 グラフ中 133 で誤り(実測)

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

ヒント1(方向性)

最大流 $f$ を 1 つ求め、残余グラフ $G_f$ を作る。カット $S$ が最小である ⇔ $S$ から出る辺はすべて飽和し、$S$ に入る辺はすべて流量 0 ⇔ $G_f$ で $S$ から外へ出る残余辺が無い($S$ は $G_f$ の辺の向きに閉じている)。

ヒント2(アプローチ)
  • 閉じた集合のうち最小のものは $A=$「$s$ から $G_f$ で届く頂点」、最大のものは $V\setminus B$、$B=$「$G_f$ で $t$ へ届く頂点」
  • どの最小カット $S$ も $A\subseteq S\subseteq V\setminus B$ を満たす
  • 「必ず入る」は $u\in A$ かつ $v\in B$ のとき
ヒント3(誘導)

「入りうる」は、$u$ を含み $v$ を含まない閉集合があること。$u$ を含む最小の閉集合は $A\cup\mathrm{Reach}(u)$ なので、条件は $v\notin\mathrm{Reach}(u)$ かつ $t\notin\mathrm{Reach}(u)$ かつ $v\notin A$。辺が飽和していれば(流量 $>0$ なので)逆向きの残余辺 $v\to u$ があり、条件は「$G_f$ で $u$ と $v$ が別の強連結成分」と同じになる。

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    s, t = 0, n - 1
    # 辺 i は 2i(順向き)と 2i+1(逆向き)。to/cap を平らな配列で持つ
    head = [[] for _ in range(n)]
    to = [0] * (2 * m)
    cap = [0] * (2 * m)
    eu = [0] * m
    ev = [0] * m
    for i in range(m):
        u = int(data[2 + 3 * i]) - 1
        v = int(data[3 + 3 * i]) - 1
        c = int(data[4 + 3 * i])
        eu[i], ev[i] = u, v
        to[2 * i], cap[2 * i] = v, c
        to[2 * i + 1], cap[2 * i + 1] = u, 0
        head[u].append(2 * i)
        head[v].append(2 * i + 1)
    # ---- Dinic
    flow = 0
    while True:
        level = [-1] * n
        level[s] = 0
        dq = deque([s])
        while dq:
            x = dq.popleft()
            for e in head[x]:
                if cap[e] and level[to[e]] < 0:
                    level[to[e]] = level[x] + 1
                    dq.append(to[e])
        if level[t] < 0:
            break
        it = [0] * n
        while True:                        # 反復 DFS で増加路を 1 本ずつ
            path = []
            x = s
            while x != t:
                adv = False
                hx = head[x]
                while it[x] < len(hx):
                    e = hx[it[x]]
                    y = to[e]
                    if cap[e] and level[y] == level[x] + 1:
                        path.append(e)
                        x = y
                        adv = True
                        break
                    it[x] += 1
                if not adv:
                    if x == s:
                        break
                    level[x] = -1          # 行き止まり。以後この頂点は使わない
                    e = path.pop()
                    x = to[e ^ 1]
                    it[x] += 1
            if x != t:
                break
            f = min(cap[e] for e in path)
            for e in path:
                cap[e] -= f
                cap[e ^ 1] += f
            flow += f
    # ---- 残余グラフ: s から届く集合 A、t へ届く集合 B
    A = [False] * n
    A[s] = True
    dq = deque([s])
    while dq:
        x = dq.popleft()
        for e in head[x]:
            if cap[e] and not A[to[e]]:
                A[to[e]] = True
                dq.append(to[e])
    B = [False] * n
    B[t] = True
    dq = deque([t])
    while dq:
        x = dq.popleft()
        for e in head[x]:                  # 残余辺 y→x(= 辺 e^1 の残り容量)を逆にたどる
            y = to[e]
            if cap[e ^ 1] and not B[y]:
                B[y] = True
                dq.append(y)
    # ---- 残余グラフの強連結成分(反復 Tarjan)
    comp = [-1] * n
    idx = [-1] * n
    low = [0] * n
    onst = [False] * n
    st = []
    counter = 0
    nc = 0
    for r in range(n):
        if idx[r] >= 0:
            continue
        work = [(r, 0)]
        idx[r] = low[r] = counter
        counter += 1
        st.append(r)
        onst[r] = True
        while work:
            x, i = work[-1]
            hx = head[x]
            if i < len(hx):
                work[-1] = (x, i + 1)
                e = hx[i]
                if cap[e]:
                    y = to[e]
                    if idx[y] < 0:
                        idx[y] = low[y] = counter
                        counter += 1
                        st.append(y)
                        onst[y] = True
                        work.append((y, 0))
                    elif onst[y] and idx[y] < low[x]:
                        low[x] = idx[y]
                continue
            work.pop()
            if work:
                px = work[-1][0]
                if low[x] < low[px]:
                    low[px] = low[x]
            if low[x] == idx[x]:
                while True:
                    y = st.pop()
                    onst[y] = False
                    comp[y] = nc
                    if y == x:
                        break
                nc += 1
    out = []
    for i in range(m):
        u, v = eu[i], ev[i]
        if cap[2 * i]:                     # 飽和していない辺はどの最小カットにも入らない
            out.append(0)
        elif A[u] and B[v]:
            out.append(2)                  # 最小の S と最大の S の両方をまたぐ → 全部に入る
        elif comp[u] != comp[v]:
            out.append(1)                  # 残余で u から v へ行けない → u を含み v を含まない閉集合がある
        else:
            out.append(0)
    print(flow)
    print(" ".join(map(str, out)))


main()

計算量: 最大流(Dinic、一般に $O(N^2M)$、実際はずっと速い)+残余グラフの BFS 2 回と強連結成分分解 $O(N+M)$。$N=10^4,M=10^5$ の乱数で 0.20〜0.26 s、$100\times100$ の格子($M\approx3\times10^4$、最小カットが中ほどにある)で最大 0.65 s(Python 3.14.8、Apple M5 Pro、負荷平均 1〜3 での実測、起動込み)。

Step-by-Step 解説

1最小カット ⇔ 残余グラフで閉じた集合

最大流 $f$ の値は、どのカット $S$ についても「$S$ から出る流量 − $S$ に入る流量」。これが容量と等しい(最小カット)⇔ 出る辺は全部飽和・入る辺は全部 0 ⇔ $S$ から外へ向かう残余辺(順向きの残り容量・逆向きの流量)が無い。どの最大流を取っても同じ集合の族になる。

2最小と最大

閉じた集合は和と共通部分で閉じている。$s$ を含む最小の閉集合は $A=\mathrm{Reach}_{G_f}(s)$、$t$ を含まない最大の閉集合は $V\setminus B$($B$ は $t$ へ届く頂点)。$A\cap B=\emptyset$($s$ から $t$ へ残余路が無い=最大流)。

3必ず入る辺

辺 $u\to v$ がすべての最小カット $S$ に入る ⇔ $u$ がすべての $S$ に入り、$v$ がどの $S$ にも入らない ⇔ $u\in A$(最小の $S$ に入る)かつ $v\notin V\setminus B$、すなわち $v\in B$。この辺は自動的に飽和している。

4入りうる辺

$u$ を含み $v,t$ を含まない閉集合があるか。$u$ を含む最小の閉集合は $C=A\cup\mathrm{Reach}(u)$ で、$C$ が条件を満たすかを見ればよい。飽和した辺($c\ge1$ なので流量 $>0$)では

  • 逆向きの残余辺 $v\to u$ がある。だから $v\notin\mathrm{Reach}(u)$ ⇔ $u,v$ が別の強連結成分
  • 流量を道と閉路に分けると、この辺を通る道 $s\leadsto u\to v\leadsto t$ の逆向きの辺で $u\leadsto s$ と $t\leadsto v$ がある。すると $t\in\mathrm{Reach}(u)$ なら $v\in\mathrm{Reach}(u)$、$s\in\mathrm{Reach}(u)$ なので $A\subseteq\mathrm{Reach}(u)$。この辺を流れる量が閉路の一部なら、閉路の逆向きの辺で $u\leadsto v$ があり、成分が同じになる

結局、飽和していて成分が違えば 1(2 の条件に当たらなければ)、それ以外は 0。

5「飽和 = 入りうる」ではない

入力例 2 の $1\to2\to4$ は飽和しているが、迂回路 $1\to3\to4$ に残り容量があり、残余グラフで $2\to1\to3\to4$ と $2$ から $4$ へ行ける。だから $2$ を含み $4$ を含まない閉集合は無い。

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

案計算量採否理由
全カットの列挙$O(2^N M)$検算用のみ$N\le9$ 用
辺ごとに容量を 1 減らして最大流を解き直す(減れば「必ず入る」)最大流 $M+1$ 回不採用$20\times40$ 格子($M=2340$)で 4.66 s(実測、本解は 0.01 s 台)。しかも「入りうる」は判定できない
飽和した辺を「入りうる」とする$O(M)$不採用(誤り)600 グラフ中 133 で誤る(実測)。入力例 2
$A$ の境界だけを見る(最小の最小カット 1 つ)$O(N+M)$不採用(誤り)「入りうる」「必ず入る」のどちらに使っても 600 中 65 で誤る(実測)
$B$ の境界だけで「必ず入る」を決める同上不採用(誤り)600 中 65 で誤る(実測)
残余グラフの $A$・$B$・強連結成分(本解)最大流 1 回+$O(N+M)$採用最大 0.65 s

前提が変わったら

  • 最小カットの個数を数えたいなら、閉集合の数え上げ(DAG のイデアルの数)になり、一般には #P 困難と記憶している(未確認)。本問の 3 分類までは多項式で済む
  • 無向グラフなら各辺を両向き 2 本にして同じ判定ができるが、「入る」の定義を「両端が別の側」に変える必要がある
  • 容量 0 の辺を許すと、飽和していても流量 0 なので逆向きの残余辺が無く、Step 4 の「成分が違う ⇔ 行けない」が崩れる。$\mathrm{Reach}(u)$ を直接見る判定が要る

よくあるミス

ミス原因正しい書き方
飽和した辺はすべて最小カットに入りうるとする最小カットの辺が飽和する、の逆を使う残余グラフで $u$ と $v$ が別の成分であることも要る
$t$ へ届く集合 $B$ を順向きの残余辺でたどる向きの取り違え$t$ から逆向きにたどる(残余辺 $y\to x$ は辺 $e^{\wedge}1$ の残り容量)
最小カットを 1 つ($A$)求めて終わる最小カットが一意だと思う入力例 1 は 3 つある
強連結成分を元のグラフで取る残余グラフと混同残り容量が正の辺だけで取る
再帰 DFS で Tarjan$N=10^4$ の深さで再帰の上限反復版

次のステップ

  • 発展1: 「必ず入る辺」の容量を 1 ずつ増やしたとき最大流が増えるかを、本問の分類から答える(2 の辺だけが候補)
  • 発展2: 最小カットの数を、残余グラフの強連結成分を縮めた DAG のイデアルとして $N\le20$ で数え、全列挙と比べる
  • 発展3: 無向グラフ版にして、Day115 の Stoer–Wagner(大域最小カット)と組み合わせる

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

  • 正しさ(実測): 頂点 2〜9、辺 1〜25、容量の上限 1/2/3/10 の乱数グラフ 600 個で、全カット($s\in S,t\notin S$ の $2^{N-2}$ 通り)の列挙と最小値・3 分類が全件一致。600 個中 326 個は最小カットが 2 つ以上あった。分類の内訳は 0 4876・1 184・2 478 本
  • 誤った方法(実測、同じ 600 個): 飽和 = 入りうる 133、$A$ の境界で「入りうる」65、$A$ の境界で「必ず入る」65、$B$ の境界で「必ず入る」65 個で誤る
  • 辺ごとに最大流を解き直す方法(実測): $20\times40$ 格子($N=802,M=2340$)で 4.66 s、容量を減らすと流量が減る辺 23 本は本解の 2 と完全に一致
  • 時間(実測、起動込み、負荷平均 1〜3): 乱数 $N=10^4,M=10^5$ で 0.20〜0.26 s(57〜65 MB)、格子 $100\times100$($M=29900$)で容量上限 1/3/10/100/$10^9$ に対し 0.03/0.21/0.40/0.65/0.64 s
  • 扱っていない範囲: 原典の確認、最小カットの個数、容量 0 の辺、無向グラフ

自己評価

自分の回答

気づき・メモ