Day 167-Q1 — 2026-10-01 | 一般グラフの Green Hackenbush(融合原理で 2 辺連結成分を縮約し、橋木の Grundy 数に落とす)

2026-10-01 赤色 Master (Phase 8+) ★★★★★★★★★ ゲーム理論(焦点帯 M1)。木の Hackenbush は Colon Principle で解けるが、閉路や多重辺・自己ループがあると使えない。2 辺連結成分は 1 点に融合でき、成分内の辺は「辺数の偶奇」だけが残る。偶奇を捨てる誤りを数で確かめる 10/1 のバックフィル分(出題日を 2026-10-01 として生成)

問題

無向グラフ(多重辺と自己ループを許す)が与えられる。頂点 1 は「地面」である。2 人が交互に 辺を 1 本選んで取り除く。辺を取り除いた後、地面(頂点 1)につながらなくなった辺は全て自動的に消える。取る辺が無くなった方が負け。

最初のグラフは、全ての辺が地面につながっている(頂点 1 から辺をたどって全ての辺に届く)。先手と後手が最善を尽くすとき、勝つのはどちらか。さらに、先手が最初に取ると必勝になる辺の本数を求めよ。

入力形式

N M
a_1 b_1
...
a_M b_M

制約

#条件
1$1\le N\le500$, $1\le M\le1000$
2$1\le a_i,b_i\le N$($a_i=b_i$ の自己ループ、同じ頂点対の多重辺を含む)
3全ての辺は頂点 1 から辺をたどって届く

出力: 1 行目に先手が勝つなら First、負けなら Second。2 行目に先手の必勝の初手(取る辺)の本数。

入力例1

3 3
1 2
2 3
3 1

出力例1

First
1

(地面につながる三角形。奇数長の閉路は辺 1 本分 $*1$ に等しい。必勝の初手は、地面に接しない辺 2-3 を取って、地面から出る 2 本の辺($*1\oplus*1=0$)を残す 1 通りだけ。辺 1-2 を取ると残りは長さ 2 の棒 $*2$ で、後手が勝つ)

入力例2

4 4
1 2
2 3
3 1
3 4

出力例2

Second
0

入力例3

3 3
1 2
1 2
1 3

出力例3

First
3

(頂点 1-2 の多重辺は 2 本で偶数、$*1\oplus*1=0$ 。橋 1-3 が $*1$。全体 $*1$ で先手の勝ち。必勝手は橋 1-3 を取る 1 通りと、多重辺の 1 本を取る 2 通りの計 3)

背景と出典

Green Hackenbush(辺が全て緑で、どちらのプレイヤーも取れる)は、Berlekamp・Conway・Guy の *Winning Ways* の主題の 1 つとして知られる。木では Colon Principle(根の近くの枝を入れ替えても、同じ Grundy 数の枝なら全体の値が変わらない)で $g(v)=\bigoplus_{c}(g(c)+1)$ になる。閉路を含む場合は Fusion Principle(閉路上の頂点を融合しても値が変わらない)が使える、と記憶している。本セッションでは Winning Ways の原典を確認していない。以下の定式化は、この記憶から組み立てた式を、全状態探索で確かめたものである。

融合した結果を次のように読む。2 辺連結成分(橋を使わずに互いに行き来できる頂点の塊)を 1 点に縮めると、成分内の辺は全て自己ループになる。自己ループ 1 本は「取っても他が切れない 1 本の辺」で、値は $*1$。ループが $k$ 本なら $*1$ の $k$ 個の XOR で、$k$ の偶奇だけが残る。したがって縮約した木(橋木)の各頂点で $$g(v)=\bigl(k_v\bmod 2\bigr)\ \oplus\ \bigoplus_{c\in\text{橋の子}}\bigl(g(c)+1\bigr)$$ で、全体の値は地面を含む成分の $g$。先手の必勝は $g\ne0$。必勝の初手は、辺を 1 本消した後(地面に届かなくなった辺も消した後)の値が 0 になる辺の数。

この問題で確かめたいのは次の 3 点。(1) この式が全状態探索の勝敗と必勝手の数に一致するか。(2) 成分内の辺数の偶奇を捨てる(または「1 本でもあれば 1」とする)とどれだけ外れるか。(3) 初手の数え上げを素朴に $M$ 回の再計算で行って、制約の大きさで間に合うか。

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

ヒント1(方向性)

三角形は $*1$、四角形は 0。閉路の長さが偶数か奇数かだけが効く。まず「地面につながる 1 本の閉路」の値を小さい例で全探索して確かめ、閉路を 1 点に縮めるとその閉路の辺はループになり、ループ 1 本が $*1$ と見ると、偶奇が出る理由が分かる。

ヒント2(アプローチ)

2 辺連結成分に分けるには、橋(取ると連結でなくなる辺)を見つければよい。橋だけを残すと木になる。この木で、頂点(= 成分)の値を子から親へ $g(v)=(\text{ループ数の偶奇})\oplus\bigoplus(g(c)+1)$ と畳む。

ヒント3(誘導)

橋の判定は lowlink。多重辺があるので、親を頂点ではなく辺の番号で区別する(親と同じ頂点への別の辺は後退辺)。自己ループは後退辺として扱われ、成分内の辺として数える。再帰は深さ $N$ で危ないので、再帰なしで書く。必勝手の数は、辺を 1 本取り除いたグラフを作り、頂点 1 から届かない辺を捨ててから値を求め直し、0 になる本数を数える。

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
def grundy(n, edges):
    # edges: list of (u,v), vertex 0 = ground. 接地成分に届かない辺は消える
    adj = [[] for _ in range(n)]
    for i,(u,v) in enumerate(edges):
        adj[u].append((v,i))
        if u != v: adj[v].append((u,i))
    # lowlink(再帰なし)。辺番号で親辺を判定(多重辺・自己ループに強い)
    order = [-1]*n; low = [0]*n; used_e = [False]*len(edges)
    bridge = [False]*len(edges)
    t = 0; order[0] = 0; low[0] = 0; t = 1
    stack = [(0, -1, 0)]
    parent_of = {}
    seq = [0]
    while stack:
        v, pe, idx = stack.pop()
        if idx < len(adj[v]):
            stack.append((v, pe, idx+1))
            w, ei = adj[v][idx]
            if ei == pe: continue
            if order[w] == -1:
                order[w] = low[w] = t; t += 1
                parent_of[w] = (v, ei)
                seq.append(w)
                stack.append((w, ei, 0))
            else:
                if order[w] < low[v]: low[v] = order[w]
        else:
            if v in parent_of:
                p, ei = parent_of[v]
                if low[v] < low[p]: low[p] = low[v]
                if low[v] > order[p]: bridge[ei] = True
    # 2 辺連結成分: 橋を使わずに到達できる頂点の塊(到達済み頂点 seq のみ)
    comp = [-1]*n; nc = 0
    for s in seq:
        if comp[s] != -1: continue
        comp[s] = nc; st = [s]
        while st:
            x = st.pop()
            for y, ei in adj[x]:
                if not bridge[ei] and comp[y] == -1:
                    comp[y] = nc; st.append(y)
        nc += 1
    par = [0]*nc          # 成分内の辺数の偶奇(自己ループ・多重辺を含む)
    kids = [[] for _ in range(nc)]
    for i,(u,v) in enumerate(edges):
        if comp[u] == -1: continue     # 接地成分の外
        if bridge[i]:
            # 橋: 根側が親。order が小さい側が親
            a, b = (u, v) if order[u] < order[v] else (v, u)
            kids[comp[a]].append(comp[b])
        else:
            par[comp[u]] ^= 1
    g = [0]*nc
    # comp 番号は発見順 → 子の番号は親より大きい。逆順に畳む
    for c in range(nc-1, -1, -1):
        x = par[c]
        for d in kids[c]: x ^= g[d] + 1
        g[c] = x
    return g[comp[0]]
def solve(n, edges):
    g = grundy(n, edges)
    cnt = 0
    for i in range(len(edges)):
        if grundy(n, edges[:i] + edges[i+1:]) == 0: cnt += 1
    return ("First" if g else "Second"), cnt
def main():
    data = sys.stdin.read().split()
    n, m = int(data[0]), int(data[1])
    edges = [(int(data[2+2*i])-1, int(data[3+2*i])-1) for i in range(m)]
    w, c = solve(n, edges)
    print(w); print(c)
if __name__ == "__main__": main()

計算量: 値を 1 回求めるのは $O(N+M)$。必勝手の数え上げは $M$ 回の再計算で $O(M(N+M))$。実測(Python 3.14.7、M1 Mac)で $N=500,M=1000$ が 1.13 s。

Step-by-Step 解説

1値の定義(木ならこうなる)

木では子の値 $g(c)$ に、親との辺 1 本を足した $g(c)+1$ を XOR する(Colon Principle)。1 本の辺だけなら $g=1$、長さ $k$ の棒は $*k$。

2閉路は融合する

三角形は 3 頂点を 1 点に融合すると 3 本の自己ループ。ループ 1 本は $*1$ なので $*1\oplus*1\oplus*1=*1$。四角形は 4 本で 0。成分が一般の 2 辺連結グラフでも、中身の形に関係なく、辺の本数の偶奇だけが残る。

3橋木を作って畳む

lowlink で橋を決め、橋を使わずに行き来できる頂点を同じ成分に入れる。成分内の辺(自己ループ・多重辺・非橋)の本数を数え、橋の根側の成分を親として値を畳む。成分の番号は発見順で、子の番号は親より大きいので、番号の逆順に処理すればよい。

4必勝の初手を数える

辺 $e$ を取り除くと、頂点 1 につながらない部分が消える。lowlink の探索は頂点 1 から始めるので、探索で届いた頂点の辺だけを数えれば、消えた辺は自然に無視される。取り除いた後の値が 0 になる $e$ の本数が答え。

5偶奇を捨てると外れる

成分内の辺の偶奇を無視する(pass)と、検算の 520 件のうち 325 件で勝敗か必勝手の数が外れる。「成分内に辺があれば 1」(=1)としても 305/520 件で外れる(ともに実測)。

迷った他案と採らなかった理由

案計算量採否理由
2 辺連結成分の縮約(融合)+橋木の Grundy(本解)値は $O(N+M)$採用520 件の全状態探索と、勝敗・必勝手の数が一致
辺集合の全状態探索(mex を直接取る)$O(2^M\cdot M)$検算用のみ$M\le9$ でしか動かない
木のまま扱う(DFS 木の非木辺を無視)$O(N+M)$不採用閉路を持つ入力で誤る。非木辺を捨てると融合の偶奇が失われる
閉路を見つけたら「辺数が偶数なら捨てる」だけの場合分け$O(N+M)$不採用閉路同士が頂点を共有する(サボテン以外の一般グラフ)と場合分けが閉じない。縮約なら一般の 2 辺連結成分にそのまま使える

前提が変わったら:

  • 必勝手の数を $M=2\times10^5$ で求める場合は、$M$ 回の再計算では間に合わない。橋木で「この辺を取ると親側・子側の値がどう変わるか」を根から伝播する方法(木の Hackenbush で必勝手を列挙した方法と同じ形)に置き換える必要がある。実装していない
  • 辺に色(青・赤)が付く(Partizan な Hackenbush)と、Grundy 数ではなく Conway の超現実数が要る。本解はそのままは使えない
  • 地面が複数ある場合は、地面を 1 点に融合して同じ式に落とせるはず。確かめていない

よくあるミス

ミス原因正しい書き方
成分内の辺の偶奇を落とす閉路を「消えるもの」と考えるpar[comp] ^= 1(325/520 件で誤答)
親辺を頂点で判定する多重辺の 2 本目を親辺とみなして後退辺を見落とす辺の番号 ei == pe で判定
自己ループを辺として数えない隣接リストに入れない成分内の辺として par に足す
取り除いた後に地面と切れた辺も数える消える辺を残して再計算頂点 1 からの探索で届いた頂点の辺だけ数える

次のステップ

  • 発展1: 必勝手の数を $M=2\times10^5$ で $O(N+M)$ で求める(橋木での「必要な Grundy 値」の伝播。Day 158-Q2 の木の場合を一般グラフに拡げる)
  • 発展2: 青と赤の辺があるとき(Blue-Red Hackenbush)の値を、木の場合に超現実数で求める
  • 次回予告: 焦点帯 M1 から 3 問以上を含む通常のローテーション

検証ログ(本セッションで実行した数値)

  • 全状態探索との照合(実測): 頂点 2〜6、辺 1〜9 本のランダムな多重グラフ(自己ループ・多重辺を含む。地面に届かない辺は除く)520 件で、勝敗と必勝手の数が全件一致。全状態探索は「辺を 1 本取り、地面に届かない辺を消す」を直接実装し、mex は使わず勝敗だけを再帰で求めた
  • 誤った式(実測): 成分内の辺の偶奇を捨てると 325/520 件、「1 本でもあれば 1」とすると 305/520 件で外れる
  • 入力例(実測): 三角形(First, 1)、三角形+ぶら下がり(Second, 0)、橋 1-2 の先に三角形があり別の橋 1-5 がある例(First, 1)、多重辺 2 本+自己ループ 2 本(Second, 0)、入力例 3(First, 3)、多重辺 3 本(First, 3)が全状態探索と一致
  • 実行時間(実測): $N=500,M=1000$ のランダムな連結グラフで 1.13 s(必勝手の数え上げ込み)
  • 扱っていない範囲: Fusion Principle の証明と原典の確認、$M$ が大きい場合の必勝手の数え上げ、Blue-Red / Red-Blue-Green の場合、地面が複数の場合

自己評価

自分の回答

気づき・メモ