問題
頂点 $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$ へ届く集合)と強連結成分を足して、全最小カットの「共通部分」と「和」を辺ごとに答える
- 本問の問題文・制約・入力例は自作。判定の条件は下で導き、全探索で確かめた
概念図
ヒント(クリックで段階的に開示)
ヒント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 つ以上あった。分類の内訳は
04876・1184・2478 本 - 誤った方法(実測、同じ 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 の辺、無向グラフ