問題
$N$ 頂点 $M$ 辺の連結な単純無向グラフが与えられる。辺をいくつか追加して、どの 1 辺を取り除いても連結のまま(2 辺連結、橋が無い)にしたい。追加する辺の最小の本数を求めよ。追加する辺は、すでにある辺と同じ頂点対を結ぶもの(多重辺)でもよいが、自己ループは不可。
入力形式
N M
u_1 v_1
...
u_M v_M
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le2\times10^5$ |
| 2 | $N-1\le M\le3\times10^5$ |
| 3 | $1\le u_i<v_i\le N$、同じ辺は 2 度現れない、グラフは連結 |
| 4 | 入力はすべて整数 |
入出力例
入力例1
5 4
1 2
2 3
3 4
4 5出力例1
1(パスの両端 1 と 5 を結べば全体が 1 つの閉路になる)
入力例2
7 6
1 2
1 3
1 4
1 5
1 6
1 7出力例2
3(星形。葉が 6 つなので 3 本。葉を 2 つずつ組にして結ぶ)
入力例3
4 4
1 2
2 3
1 3
3 4出力例3
1(三角形 1-2-3 に頂点 4 がぶら下がっている。橋は辺 3-4 の 1 本。4 と、三角形の中の頂点(1 でも 2 でも 3 でもよい)を結べばよいので 1 本)
背景と出典
橋を除いて残る連結成分(2 辺連結成分)を 1 点に縮約すると木ができる(橋木、bridge tree)。この木の葉の数を $L$ とすると、2 辺連結にするのに必要な追加辺の最小数は $\lceil L/2\rceil$ になる(木が 1 点なら 0)。この結果は私の記憶では K. P. Eswaran と R. E. Tarjan の 1976 年の論文(Augmentation problems, SIAM J. Computing)に遡る。本セッションでは論文を開いておらず、帰属は未確認。正しさの根拠は、辺の追加の総当たり(下の検証ログ)。
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
橋でない辺だけでつながる部分は、これ以上いじる必要がない。そこを 1 点にまとめた木を考える。その木の「葉」にあたる成分は、今は 1 本の橋でしか外とつながっていない。
ヒント2(アプローチ)
葉の成分が 1 つ残る限り、その成分の橋は消えない。1 本の追加辺は、木の上の 2 点を結ぶので、そのパス上の橋をすべて消せる。端点に葉を選べば、一度に 2 つの葉を片づけられる。だから必要な本数は $\lceil L/2\rceil$。
ヒント3(誘導)
橋の見つけ方は lowlink(DFS の訪問順 order と、そこから辿れる最小の訪問順 low)。子 $u$ から親 $p$ への辺について low[u] > order[p] なら橋。$N=2\times10^5$ なのでパス状のグラフで再帰が深くなる。再帰を使わず、スタックに「次に見る隣接の位置」を持たせる。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
def main():
data = sys.stdin.buffer.read().split()
n, m = int(data[0]), int(data[1])
g = [[] for _ in range(n)] # (隣接頂点, 辺番号)
for i in range(m):
u, v = int(data[2 + 2 * i]) - 1, int(data[3 + 2 * i]) - 1
g[u].append((v, i))
g[v].append((u, i))
# 橋を探す(再帰なしの lowlink)
order = [-1] * n
low = [0] * n
is_bridge = [False] * m
t = 0
stack = [(0, -1, 0)] # (頂点, 親へ来た辺番号, 次に見る隣接の位置)
order[0] = low[0] = 0
t = 1
while stack:
u, pe, idx = stack.pop()
if idx < len(g[u]):
stack.append((u, pe, idx + 1))
v, e = g[u][idx]
if e == pe: # 来た辺そのものは戻らない(辺番号で判定。多重辺があっても正しい)
continue
if order[v] == -1:
order[v] = low[v] = t
t += 1
stack.append((v, e, 0))
elif order[v] < low[u]:
low[u] = order[v]
elif pe != -1:
# u の探索が終わった。親 p へ low を伝え、橋かどうか決める
p = None
for v, e in g[u]:
if e == pe:
p = v
break
if low[u] < low[p]:
low[p] = low[u]
if low[u] > order[p]:
is_bridge[pe] = True
# 橋を除いた連結成分(2 辺連結成分)に番号を振る
comp = [-1] * n
c = 0
for s in range(n):
if comp[s] != -1:
continue
comp[s] = c
st = [s]
while st:
u = st.pop()
for v, e in g[u]:
if not is_bridge[e] and comp[v] == -1:
comp[v] = c
st.append(v)
c += 1
# 橋木での次数。葉(次数 1)の数 L に対して答えは ceil(L/2)
deg = [0] * c
for u in range(n):
for v, e in g[u]:
if is_bridge[e]:
deg[comp[u]] += 1
leaves = sum(1 for d in deg if d == 1)
print((leaves + 1) // 2)
main()
計算量: $O(N+M)$。実測(Python 3.14.7、M1 Mac)で $N=2\times10^5$ のパス 0.29 s、ランダムな木 0.52 s、星形 0.29 s、ランダムな木に辺を 10 万本足した($M\approx3\times10^5$)グラフ 0.89 s。
Step-by-Step 解説
DFS で各頂点の訪問順 order と、「子孫の後退辺で到達できる最小の訪問順」low を求める。親 $p$ から子 $u$ への辺は、low[u] > order[p] のとき橋($u$ の部分木から $p$ より上へ戻る道が無い)。
無向グラフの DFS では、来た辺を逆に辿って親へ戻る動きを後退辺と数えてはいけない。頂点番号で「親には戻らない」と判定すると、同じ頂点対を結ぶ多重辺があるとき、2 本目の辺を見落として橋と誤判定する。本問の入力は単純グラフだが、辺番号で判定する書き方は多重辺でも正しく、橋木を作る一般の場面でそのまま使える。
橋を除いて連結成分に番号を振り、各成分が接する橋の本数(橋木での次数)を数える。次数 1 の成分が葉。連結グラフなので橋木も連結な木。橋が無ければ成分が 1 個で、次数 0 の頂点が 1 個だけ。葉は 0 個になり、答えは 0。
下界: 葉の成分ごとに、その成分に接する橋は 1 本。この橋を消すには、追加辺の少なくとも 1 本の端点がその葉の成分の中になければならない。1 本の追加辺は端点が 2 つなので、葉を $L$ 個片づけるには $\lceil L/2\rceil$ 本以上要る。上界(その本数で足りること)の証明は、木の葉を「うまく組にして」結ぶ構成が要る。本ファイルでは証明せず、総当たりで確かめた(下の検証ログ)。
答えは橋の数に比例しない(パスは橋が $N-1$ 本でも答えは 1)。「元のグラフの次数 1 の頂点数」で数えると、葉の成分が閉路や三角形のときに外れる(入力例 3 は次数 1 の頂点が頂点 4 だけで $\lceil1/2\rceil=1$ とたまたま合うが、三角形の側にも 1 つ葉があるので $L=2$)。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 辺の追加を総当たりして橋が無くなるか確かめる | 指数 | 検算用のみ | $N\le7$ なら $k\le3$ 本まで全探索できる。検算に使った |
| 橋の本数を答える | $O(N+M)$ | 不採用 | パスの橋は $N-1$ 本だが答えは 1。400 件中 238 件誤答 |
| 葉の数 $L$ から $L-1$ | $O(N+M)$ | 不採用 | $L=2,3$ では合うが $L\ge4$ で外れる。400 件中 23 件誤答 |
| $\lfloor L/2\rfloor$ | $O(N+M)$ | 不採用 | $L$ が奇数のとき 1 本足りない。400 件中 72 件誤答 |
| 元のグラフの次数 1 の頂点数を使う | $O(N+M)$ | 不採用 | 葉の成分が閉路を含むと外れる。400 件中 5 件誤答(小さい入力では当たりやすく危険) |
| 再帰の DFS で lowlink | $O(N+M)$ | 不採用 | パス状の入力で再帰の深さが $N$。Python では再帰上限の引き上げか別実装が要る |
| 再帰なしの lowlink+橋木の葉(本解) | $O(N+M)$ | 採用 | 総当たりと 400 件全件一致 |
前提が変わったら:
- 頂点連結度(関節点)で 2 連結にしたいなら、橋木ではなくブロック・カット木を作り、葉ブロックを数える。式は同じ形($\lceil L/2\rceil$)だが、葉ブロックが 2 点以上のときに端点の選び方に注意が要る
- 辺に追加の費用や重みがある(どの頂点対を結べるかに制限がある)と、最小本数の式は使えず NP 困難になる問題群に入る(既知と記憶するが、本セッションで確認していない)
- 非連結なら、先に連結にする辺が要る。孤立点や木の本数まで含めた式が必要で、本問の式は使えない
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 橋の本数を答える | 「橋を 1 本ずつ消す」と思う | 橋木の葉の数 $L$ を数え $\lceil L/2\rceil$(238/400 誤答) |
| $\lfloor L/2\rfloor$ にする | 切り上げを忘れる | (L + 1) // 2(72/400 誤答) |
| 頂点番号で「親に戻らない」を判定する | 多重辺があると、親への 2 本目の辺を無視する | 辺番号 e == pe で判定する |
| 再帰で lowlink を書く | 木・パスで深さ $N$ | 明示的なスタックに「次に見る位置」を持たせる |
| $N=1$ で落ちる | 橋木が 1 点で、次数 0 の頂点しかない | 葉は次数 1 の成分だけを数える(葉 0 個 → 答え 0) |
次のステップ
- 発展1: 追加する辺の具体的な端点の組を出力する(橋木の葉を DFS 順に並べ、$i$ 番目と $i+\lceil L/2\rceil$ 番目を結ぶ、という構成を調べる)
- 発展2: 頂点連結度版(2 頂点連結、ブロック・カット木)で同じ問題を解く
- 次回予告: 焦点帯 M3(数論)
検証ログ(本セッションで実行した数値)
- 総当たりとの一致(実測): $N\le7$ のランダムな連結グラフ(ランダムな木に辺を 0〜3 本足したもの)400 件で、辺を $k=0,1,2,\dots$ 本(多重辺可)追加して橋が無くなる最小の $k$ を全探索し、プログラム全体の出力と全件一致。答えの分布は 0: 54、1: 254、2: 89、3: 3。**答えが 3 の件は 3 件しかなく、$L\ge5$ の大きい場合の裏付けは薄い**
- 誤った方法(実測): 橋の本数 238/400、$L-1$ 23/400、$\lfloor L/2\rfloor$ 72/400、元の次数 1 の頂点数から $\lceil\cdot/2\rceil$ 5/400 で誤答
- 入力例(実測): 例 1 は 1、例 2 は 3、例 3 は 1。$N=2$ のパス 1 本は 1(多重辺が必要)、$N=1$ は 0
- 実行時間(実測): $N=2\times10^5$ のパス 0.29 s、ランダムな木 0.52 s、星形 0.29 s、$M=3\times10^5$ 規模 0.89 s
- 扱っていない範囲: 上界($\lceil L/2\rceil$ 本で必ず足りること)の証明、$L\ge5$ の総当たり、追加辺の復元、Eswaran–Tarjan の原典確認