Day 163-Q4 — 2026-09-28 — 無向グラフの最短閉路(内周 girth。全頂点から BFS し、木の親へ戻らない辺で閉じる長さの最小。深さで打ち切る)

2026-09-28 赤色 Master (Phase 8+) ★★★★★★★★★ グラフ(焦点帯外 M2)。1 回の BFS で見つかる「閉路らしきもの」は始点が閉路上に無いと長く見積もられる。最短閉路上の頂点から始めた BFS だけが正確な値を返す ことを使い、全始点の最小を取る

問題

$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) の候補 = dist(u) + dist(v) + 1 1 d=0 2 d=1 3 d=2 4 d=3 5 d=3 始点 1(閉路の外) 辺 4-5 の候補 = 3 + 3 + 1 = 7 1 d=2 2 d=1 3 d=0 4 d=1 5 d=1 始点 3(最短閉路の上) 辺 4-5 の候補 = 1 + 1 + 1 = 3 候補 7 は 1→3 の共通部分を往復した「閉じた歩道」。打ち切り付きだと 7 > N=5 で「閉路なし」とまで誤る 最短閉路の上の頂点から始めた BFS は長さちょうど 3 を返す → 全始点の最小を取る

ヒント(クリックで段階的に開示)

ヒント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 解説

1候補はいつも「本物の閉路」以上

始点 $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$ を下回らない。

2最短閉路上から始めれば $g$ ちょうどが出る

長さ $g$ の最短閉路 $C$ 上の頂点 $r$ から BFS する。$C$ 上で $r$ から最も遠い辺($g$ が奇数なら向かい側の 1 辺、偶数なら向かい側の頂点に入る 2 辺のうち木に入らない方)を $(u,v)$ とすると、$C$ が最短なので $C$ 上の距離がそのまま BFS の距離になり、候補は $g$ ちょうど。Step 1 と合わせて、全始点の最小は $g$。

31 つの始点だけでは足りない

入力例 1 のように、始点が閉路から離れていると候補は長く見積もられる($7$)。模範解答の打ち切りと組み合わせると $7>N=5$ なので -1(閉路なし)とまで誤る。頂点 1 だけから BFS する版は検算 700 件中 176 件で誤った(実測)。

4DFS の後退辺では駄目な理由

DFS 木では、後退辺 $(u,v)$ の作る閉路の長さは「深さの差+1」だが、最短閉路が 後退辺 2 本と木の辺 からなるとき、どの後退辺単独の閉路より短くなりうる。入力例 2 がそれで、DFS 版は 4 と答える。検算 700 件中 31 件で誤った(実測)。

5深さで打ち切る

深さ $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 の近似法の実装

自己評価

自分の回答

気づき・メモ