問題
$N$ 頂点 $M$ 辺の有向グラフが与えられる(多重辺・自己ループがあってよい)。有向辺を最小で何本足せば、グラフ全体が強連結になるかを求めよ。すでに強連結なら 0。
入力形式
N M
u_1 v_1
...
u_M v_M
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le 2\times10^5$ |
| 2 | $0\le M\le 4\times10^5$ |
| 3 | $1\le u_i,v_i\le N$ |
| 4 | 入力はすべて整数 |
入出力例
入力例1
4 3
1 2
2 3
3 4出力例1
1($4\to1$ を足す)
入力例2
5 0出力例2
5(5 頂点が全てばらばら。$1\to2\to3\to4\to5\to1$ の 5 本が必要で、それより少ない本数では足りない)
入力例3
3 3
1 2
2 3
3 1出力例3
0背景と出典
この問題は Eswaran–Tarjan の "Augmentation problems"(SIAM J. Computing, 1976 と記憶している)の有向版の結果で、答えが「縮約した DAG の始点(入次数 0)の数と終点(出次数 0)の数の最大(SCC が 1 つなら 0)」になる、と私は記憶している。本セッションでは論文を開いておらず未確認。
本セッションで確かめたのは、辺集合を全て試す全探索との一致(400 件)だけ。下界(少なくともこれだけ要る)は Step 3 に本文で示した。上界(この本数で必ず足りる)の構成は本文では示していない。
ヒント(クリックで段階的に開示)
ヒント1(方向性)
強連結成分(SCC)に縮約すると DAG ができる。DAG のままでは、入る辺の無い成分と、出る辺の無い成分が必ずある。強連結にするには、その両方を解消しないといけない。
ヒント2(アプローチ)
入る辺の無い成分(始点)の数を $s$、出る辺の無い成分(終点)の数を $t$ とする。足す辺 1 本で、始点を 1 つ解消でき、終点を 1 つ解消できる。どちらか多い方の数は最低限要る。
ヒント3(誘導)
SCC が 1 つなら 0。そうでなければ $\max(s,t)$。孤立した成分(入るも出るもない)は始点でも終点でもあるので、両方に数える。
---
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
adj = [[] for _ in range(n)]
es = []
for i in range(m):
u = int(data[2 + 2 * i]) - 1; v = int(data[3 + 2 * i]) - 1
adj[u].append(v); es.append((u, v))
# 反復版 Tarjan で強連結成分に分ける
idx = [-1] * n; low = [0] * n; onst = [False] * n; comp = [-1] * n
st = []; cnt = 0; nc = 0
for s in range(n):
if idx[s] != -1: continue
stack = [(s, 0)]; idx[s] = low[s] = cnt; cnt += 1; st.append(s); onst[s] = True
while stack:
v, i = stack[-1]
if i < len(adj[v]):
stack[-1] = (v, i + 1); w = adj[v][i]
if idx[w] == -1:
idx[w] = low[w] = cnt; cnt += 1; st.append(w); onst[w] = True
stack.append((w, 0))
elif onst[w] and idx[w] < low[v]:
low[v] = idx[w]
else:
stack.pop()
if stack:
u = stack[-1][0]
if low[v] < low[u]: low[u] = low[v]
if low[v] == idx[v]:
while True:
w = st.pop(); onst[w] = False; comp[w] = nc
if w == v: break
nc += 1
if nc == 1:
print(0); return
indeg = [0] * nc; outdeg = [0] * nc
for u, v in es:
if comp[u] != comp[v]:
outdeg[comp[u]] += 1; indeg[comp[v]] += 1
src = sum(1 for c in range(nc) if indeg[c] == 0) # 入る辺が無い成分
snk = sum(1 for c in range(nc) if outdeg[c] == 0) # 出る辺が無い成分
print(max(src, snk))
main()
計算量: $O(N+M)$。実測(Python 3.14.7、M1 Mac)で、$N=2\times10^5$ のパス(辺をシャッフル)が 0.31 s、$N=2\times10^5,M=4\times10^5$ のランダムなグラフが 0.55 s(答え 27206)。
---
Step-by-Step 解説
Tarjan 法を反復で書く。stack に (頂点, 次に見る辺の添字) を積む。$N=2\times10^5$ のパスで再帰すると深さが足りず落ちる。low の更新は、訪問済みでスタック上にある頂点への辺(onst[w])で idx[w] を使い、子から戻ったときに low[v] を親へ伝える。
辺 $(u,v)$ で comp[u] != comp[v] のものだけ、outdeg[comp[u]]、indeg[comp[v]] を増やす。同じ成分内の辺と自己ループは無視する。
始点には入る辺が必要で、足す辺 1 本は 1 つの成分にしか入れない。よって少なくとも $s$ 本。同様に終点は出る辺が必要で、少なくとも $t$ 本。したがって $\max(s,t)$ 本は要る。
$\max(s,t)$ 本で足りることの構成は Eswaran–Tarjan にあると記憶している(未確認)。本セッションでは構成を書かず、$N\le6$ のランダム 400 件で全探索(足す辺の全組合せ)と一致することだけを確かめた。
SCC が 1 つなら始点も終点も 1 つに見えるが、答えは 0($\max(1,1)=1$ ではない)。このため nc == 1 を先に出力する。
---
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 始点数・終点数の最大(本解) | $O(N+M)$ | 採用 | 全探索と 400 件全件一致 |
| 足す辺の全組合せを試す | 指数 | 検算用のみ | $N\le6$ でしか動かない |
| 始点数だけ・終点数だけ | $O(N+M)$ | 不採用 | 400 件中それぞれ 29 件・34 件で誤答 |
| 始点数 + 終点数 | $O(N+M)$ | 不採用 | 400 件中 302 件で誤答(2 倍近い) |
| $\lceil(s+t)/2\rceil$ | $O(N+M)$ | 不採用 | 400 件中 6 件で誤答 |
前提が変わったら:
- 無向グラフで 2 辺連結にする問題は別(橋木の葉の数 $L$ に対し $\lceil L/2\rceil$。Day 164 Q4 の扱い)。有向と無向で答えの形が違うことに注意
- 辺に重みがある(辺ごとの追加コスト)なら、最小コストの強連結化は NP 困難とされる、と私は記憶している(未確認)。本問は辺の本数だけ
- 加える辺の向きが決まっているなら、本問の最大の公式は使えない
---
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 始点数と終点数の和 | 両方解消するのに別々の辺が要ると思う | 1 本で両方解消できる。max(302/400 誤答) |
| 始点数だけ(または終点数だけ) | 片方で全体が決まると思う | max(src, snk)(29・34/400 誤答) |
(src + snk + 1) // 2 | 辺の 1 本が 2 つ解消するから半分と思う | max(6/400 誤答) |
SCC が 1 つのときに max(1, 1) = 1 を出す | 1 成分は始点でも終点でもある | nc == 1 で 0 を先に出す |
| 再帰 DFS で SCC | $N=2\times10^5$ のパスで深さが超える | 反復版 Tarjan |
次のステップ
発展1: 加える辺を具体的に出力する(Eswaran–Tarjan の構成。始点と終点を DFS で対応づける)
- 発展2: 無向グラフで 2 辺連結にする最小の辺数(橋木の葉 $L$ に対し $\lceil L/2\rceil$)と、本問を比較する
- 次回予告: 焦点帯外(M3 以降)の問題
検証ログ(本セッションで実行した数値)
- 全探索との照合(実測): $N\le6$、$M\le7$ のランダムなグラフ 400 件(自己ループは除く)で、足す辺の全組合せを試す全探索と全件一致。答えの分布は 0 が 98、1 が 60、2 が 100、3 が 66、4 が 37、5 が 25、6 が 14 件
- 誤った方法(実測): 始点数だけ 29/400、終点数だけ 34/400、$\lceil(s+t)/2\rceil$ 6/400、$s+t$ 302/400 で誤答
- 入力例(実測): 例 1 は 1、例 2 は 5、例 3 は 0(出力例どおり)
- 実行時間(実測): $N=2\times10^5$ のパス 0.31 s、$N=2\times10^5,M=4\times10^5$ のランダム 0.55 s
- 扱っていない範囲: 上界($\max(s,t)$ 本で足りること)の証明、追加する辺の復元、重み付き、Eswaran–Tarjan の原典確認