Day 165-Q4 — 2026-09-29 | 強連結にするために足す最小の辺数(縮約 DAG の始点数と終点数の最大)

2026-09-29 赤色 Master (Phase 8+) ★★★★★★★★★ 強連結成分分解(焦点帯 M2)。有向グラフに最小何本の辺を足せば全体が強連結になるかを、SCC を縮約した DAG の「入次数 0 の成分数」と「出次数 0 の成分数」の最大で出す。落とし穴は和・平均・片方だけ

問題

$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 解説

1SCC を求める(再帰なし)

Tarjan 法を反復で書く。stack に (頂点, 次に見る辺の添字) を積む。$N=2\times10^5$ のパスで再帰すると深さが足りず落ちる。low の更新は、訪問済みでスタック上にある頂点への辺(onst[w])で idx[w] を使い、子から戻ったときに low[v] を親へ伝える。

2縮約した DAG の次数を数える

辺 $(u,v)$ で comp[u] != comp[v] のものだけ、outdeg[comp[u]]、indeg[comp[v]] を増やす。同じ成分内の辺と自己ループは無視する。

3下界 $\max(s,t)$

始点には入る辺が必要で、足す辺 1 本は 1 つの成分にしか入れない。よって少なくとも $s$ 本。同様に終点は出る辺が必要で、少なくとも $t$ 本。したがって $\max(s,t)$ 本は要る。

4上界(必要十分)について

$\max(s,t)$ 本で足りることの構成は Eswaran–Tarjan にあると記憶している(未確認)。本セッションでは構成を書かず、$N\le6$ のランダム 400 件で全探索(足す辺の全組合せ)と一致することだけを確かめた。

5場合分け

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 の原典確認

自己評価

自分の回答

気づき・メモ