問題
$N$ 頂点 $M$ 辺の単純無向グラフが与えられる(連結とは限らない)。グラフに含まれる閉路(同じ頂点を 2 度通らず始点に戻る、長さ 3 以上の路)のうち最も短いものの長さ(辺の本数)を出力せよ。閉路が無ければ -1 を出力せよ。
入力形式
N M
u_1 v_1
...
u_M v_M
制約
| # | 条件 |
|---|---|
| 1 | $1\le N\le 2000$ |
| 2 | $0\le M\le 5000$ |
| 3 | $1\le u_i<v_i\le N$、同じ辺は 2 度現れない |
| 4 | 入力はすべて整数 |
入出力例
入力例1
5 5
1 2
2 3
3 4
4 5
5 3出力例1
3(三角形 3-4-5。頂点 1 からだけ BFS すると、4 と 5 がどちらも深さ 3 で、閉路は長さ $3+3+1=7$ に見える)
入力例2
5 6
1 2
2 3
1 4
4 5
3 5
1 5出力例2
3(三角形 1-4-5。この順で辺を持つと、DFS で「後退辺の深さの差+1」を取る方法は 4 と答える)
入力例3
4 0出力例3
-1背景と出典
全頂点から BFS して最短閉路を求める $O(NM)$ の方法は A. Itai, M. Rodeh, "Finding a Minimum Circuit in a Graph", *SIAM Journal on Computing* 7(4), 1978 で扱われている(同論文には偶数長なら誤差 1 以内で速く近似する方法もある)。本セッションでは論文を開いておらず、正しさの根拠は下の検証ログ(「各辺を除いた最短距離+1」の総当たりとの照合 700 件)。
最短閉路(girth)は、ネットワークの冗長経路の短さ(1 本切れたときの迂回の長さ)や、LDPC 符号のタナーグラフで短い閉路が復号性能を落とす話など、グラフの「局所的な木らしさ」を測る量として使われる。
概念図:始点と BFS の候補
ヒント(クリックで段階的に開示)
ヒント1(方向性)
BFS 木を作ると、木に含まれない辺 $(u,v)$ は必ず閉路を作る。始点を $r$ とすると、その長さは高々 $\mathrm{dist}(r,u)+\mathrm{dist}(r,v)+1$。
ヒント2(アプローチ)
上の値は「$r\to u\to v\to r$ と戻る閉じた歩道」の長さで、$r$ から $u,v$ への道が途中まで重なると実際の閉路より長い。でも $r$ が最短閉路の上にあれば、その閉路の長さちょうどが見つかる。だから全頂点を始点にして最小を取る。
ヒント3(誘導)
始点ごとに BFS し、頂点 $u$ を取り出したら隣接 $v$ について
- 未訪問なら木の辺($\mathrm{par}[v]=u$)
- 訪問済みで $v\ne\mathrm{par}[u]$ なら候補 $\mathrm{dist}[u]+\mathrm{dist}[v]+1$
さらに、取り出した頂点の深さ $d$ で $2d+1\ge$ いまの最小なら、この始点ではもう改善しないので打ち切る。
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque
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)
g[v].append(u)
best = n + 1 # 単純グラフの閉路は長さ n 以下
for r in range(n):
dist = [-1] * n
par = [-1] * n
dist[r] = 0
q = deque([r])
while q:
u = q.popleft()
du = dist[u]
if 2 * du + 1 >= best: # ここから見つかる候補は 2*du 以上。改善しないので打ち切る
break
for v in g[u]:
if dist[v] == -1:
dist[v] = du + 1
par[v] = u
q.append(v)
elif par[u] != v: # 木の親へ戻る辺以外 = 閉路を閉じる辺
c = du + dist[v] + 1
if c < best:
best = c
print(best if best <= n else -1)
main()
計算量: $O(N(N+M))$。打ち切りで実際はもっと小さいことが多い。実測(Python 3.14.7、M1 Mac)で長さ 2000 の閉路 1 本(打ち切りが効かない)0.77 s、木 $N=2000$ 0.61 s、ランダム $N=2000$・$M=5000$ 0.06 s(打ち切りなしは 1.44 s)。
Step-by-Step 解説
始点 $r$ の BFS 木で、木の親子でない辺 $(u,v)$ があるとする。$r\to u$ と $r\to v$ の木の道が最後に分かれる頂点を $w$ とすると、$w\to u\to v\to w$ は長さ $\mathrm{dist}(u)+\mathrm{dist}(v)+1-2\,\mathrm{dist}(w)$ の本物の閉路。よって候補 $\mathrm{dist}(u)+\mathrm{dist}(v)+1$ は 何かの閉路の長さ以上。全候補の最小は最短閉路の長さ $g$ を下回らない。
長さ $g$ の最短閉路 $C$ 上の頂点 $r$ から BFS する。$C$ 上で $r$ から最も遠い辺($g$ が奇数なら向かい側の 1 辺、偶数なら向かい側の頂点に入る 2 辺のうち木に入らない方)を $(u,v)$ とすると、$C$ が最短なので $C$ 上の距離がそのまま BFS の距離になり、候補は $g$ ちょうど。Step 1 と合わせて、全始点の最小は $g$。
入力例 1 のように、始点が閉路から離れていると候補は長く見積もられる($7$)。模範解答の打ち切りと組み合わせると $7>N=5$ なので -1(閉路なし)とまで誤る。頂点 1 だけから BFS する版は検算 700 件中 176 件で誤った(実測)。
DFS 木では、後退辺 $(u,v)$ の作る閉路の長さは「深さの差+1」だが、最短閉路が 後退辺 2 本と木の辺 からなるとき、どの後退辺単独の閉路より短くなりうる。入力例 2 がそれで、DFS 版は 4 と答える。検算 700 件中 31 件で誤った(実測)。
深さ $d$ の頂点 $u$ を取り出したとき、ここから出る候補は $\mathrm{dist}(v)\ge d-1$ より $2d$ 以上。$2d+1\ge\mathrm{best}$ なら、$2d$ の候補($\mathrm{dist}(v)=d-1$)は $v$ を取り出したときにすでに数えられている(そのとき $v$ の深さ $d-1$ で打ち切り条件は成り立っていない)ので、ここで止めても取りこぼさない。短い閉路があるグラフでは各 BFS がすぐ止まる(ランダム $N=2000$, $M=5000$ で 1.44 s → 0.06 s)。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 各辺 $(a,b)$ を除いて $a\to b$ の最短距離+1 | $O(M(N+M))$ | 検算用のみ | 正しいが $M=5000$ で $2.5\times10^7$ 以上、本解より遅い。検算に使った |
| 頂点 1 だけから BFS | $O(N+M)$ | 不採用 | 始点が閉路上に無いと長く見積もる(700 件中 176 件誤答) |
| DFS の後退辺の深さの差+1 | $O(N+M)$ | 不採用 | 700 件中 31 件誤答(入力例 2) |
| 打ち切りなしの全始点 BFS | $O(N(N+M))$ | 不採用 | 正しいが、短い閉路があるときも毎回全体を見る(1.44 s vs 0.06 s) |
| 行列積で長さ 3 の閉路(三角形)判定 | $O(N^\omega)$ | 不採用 | 三角形の有無しか分からない。girth が 4 以上のときに続かない |
| 全始点 BFS+深さの打ち切り(本解) | $O(N(N+M))$ | 採用 | 総当たりと 700 件全件一致 |
前提が変わったら:
- 重み付き なら BFS を Dijkstra に替え、「各辺 $(a,b,w)$ を除いた $a\to b$ の最短距離+$w$」の最小が安全(BFS の打ち切りの議論は重みがあると使えない)
- 有向グラフ なら、各頂点 $r$ から BFS して「$r$ に戻る辺 $u\to r$」の $\mathrm{dist}(u)+1$ の最小。無向の「親へ戻る辺を除く」処理は要らない
- $N=10^5$ 規模で近似でよい なら、Itai–Rodeh の近似(誤差 1)や、girth が小さいことを使って深さ制限 BFS を回す方法を検討する
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 始点を 1 つにする | 「BFS なら最短」の思い込み | 全始点の最小(700 件中 176 件誤答) |
| DFS の後退辺で測る | 閉路が後退辺 2 本からなる場合を見落とす | BFS で候補を作る(700 件中 31 件誤答) |
| 親へ戻る辺を閉路と数える | 無向グラフで $u\to v\to u$ を長さ 2 の閉路と見る | par[u] != v を確かめる(単純グラフなので多重辺は無い) |
| 打ち切り条件を $2d\ge\mathrm{best}$ にする | 候補の下限 $2d$ だけ見る | $2d$ の候補は深さ $d-1$ 側で見つかる。$2d+1\ge\mathrm{best}$ で止めてよい(Step 5) |
次のステップ
- 発展1: 最短閉路を 1 つ復元して頂点列を出力する($\mathrm{par}$ を辿って $w$ で合流させる。Step 1 の $w$ を求める)
- 発展2: 「頂点 $s$ を通る最短閉路」を各 $s$ について求める。全始点 BFS の候補が「$s$ を通る」とは限らないことに注意して、BFS 木の根の子ごとに色を付ける方法を調べる
- 次回予告: 焦点帯 M1(DP・ゲーム)に戻る
検証ログ(本セッションで実行した数値)
- 総当たりとの一致(実測): 「各辺を除いた最短距離+1 の最小」と、(a) $N\le12$、辺 $N+3$ 本以下のランダム 400 件(答え -1: 242・3: 131・4: 19・5: 5・6: 2・8: 1)、(b) ランダムな木に辺を 1〜3 本足した $N\le30$ の 300 件(答え -1: 22・3: 122・4: 72・5: 39・6: 26・7: 9・8: 7・9: 2・10: 1)で、プログラム全体の出力が全件一致
- 誤った方法(実測): 頂点 1 だけから BFS は (a) 66 件・(b) 110 件、DFS 後退辺は (a) 10 件・(b) 21 件で誤答。打ち切りなし版は全件一致
- 入力例(実測): 入力例 1 で頂点 1 だけの版は
-1、入力例 2 で DFS 版は4 - 実行時間(実測): 長さ 2000 の閉路 0.77 s(打ち切りなし 0.55 s)、木 $N=2000$ 0.61 s、パスに一定間隔で弦を張った girth 41/101/301 のグラフで 0.18/0.49/0.58 s、ランダム $N=2000$・$M=5000$ で 0.06 s(打ち切りなし 1.44 s)
- 扱っていない範囲: 最悪ケースを理論的に作った計測(girth が大きく辺も多いグラフは Moore 限界で辺数が制限されるので、上の計測が最悪に近いと見ているが未証明)、Itai–Rodeh の近似法の実装