Day 174-Q1 — 2026-10-10 | 一般グラフの無向頂点ジオグラフィ(花を縮める最大マッチング+Gallai–Edmonds の D 集合)

2026-10-10 赤色 Master (Phase 8+) ★★★★★★★★★ ゲーム理論×マッチング(焦点帯 M1)。Day173-Q3 は二部グラフに限って Hopcroft–Karp+交互路 BFS で解いた。今回は奇閉路を含む一般グラフに広げ、Edmonds の花の縮約で同じ判定をする

問題

$N$ 頂点 $M$ 辺の単純無向グラフがある。次のゲームを考える。

  • 最初に頂点 $v$ にコマを置き、$v$ を「訪問済み」にする
  • 先手から交互に、コマを今いる頂点に隣接する未訪問の頂点へ動かし、その頂点を訪問済みにする
  • 動かせなくなった人の負け

各 $v=1,\dots,N$ について、両者が最善を尽くしたとき先手が勝つなら W、後手が勝つなら L を並べた長さ $N$ の文字列を出力せよ。

入力形式

N M
u_1 v_1
...
u_M v_M

制約

#条件
1$1\le N\le2000$、$0\le M\le10^4$
2$1\le u_i,v_i\le N$、$u_i\ne v_i$、多重辺なし
3グラフは連結とは限らない

入出力例

入力例1

3 2
1 2
2 3

出力例1

LWL

(端から始めると先手は真ん中へ動くしかなく、後手が反対の端へ動いて先手が詰む。真ん中から始めれば先手は端へ動いて勝つ)

入力例2

5 5
1 3
2 4
2 5
3 5
4 5

出力例2

LLWLL

(三角形 2-4-5 に、5-3-1 の道がぶら下がる形。花を縮めない二部グラフ用の交互路 BFS は、頂点 3 まで L と誤る。本セッションのランダム探索で見つけた最小の反例)

入力例3

7 8
1 2
2 3
3 1
3 4
4 5
5 6
6 7
7 5

出力例3

LLLWLLL

(2 つの三角形を頂点 4 でつないだ形。3 例ともミニマックスの全探索と一致)

背景と出典

  • 定理(無向頂点ジオグラフィ): 頂点 $v$ から始めて先手が勝つ ⇔ $v$ はすべての最大マッチングに含まれる。先手は「最大マッチング $M$ で $v$ と組む相手」へ動き続ければよく、後手が $M$ の外へ出ると増加路ができて最大性に反する。逆に $v$ を避ける最大マッチングがあれば、後手が同じ戦略を取る。Fraenkel–Scheinerman–Ullman(1993)の結果と記憶している(原典は本セッションで未確認。代わりに 1300 グラフ・7130 頂点でミニマックスと照合した)
  • Gallai–Edmonds 分解: 「ある最大マッチングで露出する頂点」の集合を $D(G)$ と書く。最大マッチングを 1 つ求めたあと、露出頂点すべてを根にして交互木を広げ、偶(根から偶数距離)になる頂点がちょうど $D(G)$ になる。奇閉路(花)を縮めると、花の中の頂点はすべて偶になる
  • Day173-Q3 の二部グラフ版では、花が無いので「露出頂点から交互路で届く同じ側の頂点」を BFS するだけでよかった。一般グラフで同じ BFS をすると、花を通って偶になる頂点を見落とす(入力例 2)

本問は Day173-Q3 の発展として自作。$N\le2000$ は Python の $O(N^3)$ 型の花アルゴリズムが 1 s 以内に収まる範囲から決めた(実測は検証ログ)。

概念図

入力例 2: 最大マッチング {3-5, 2-4}(太線)。露出頂点 1 から交互木を広げる 花 {2,4,5}(偶–偶の辺 4-5 で縮める) 1 L 3 W 5 L 2 L 4 L 偶(= D(G)、後手勝ち L): 1, 5, 2, 4 奇のまま: 3 → すべての最大マッチングに入る → 先手勝ち W 花を縮めない BFS: 4 の隣 5 を「奇」とみなし、5 の相手 3 まで偶にしてしまう

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

ヒント1(方向性)

小さい例で「先手が勝つ頂点」を並べ、最大マッチングとの関係を見る。道 1-2-3 の最大マッチングは {1-2} か {2-3}。どちらにも入る頂点は 2 だけ。

ヒント2(アプローチ)

「$v$ がすべての最大マッチングに入る」⇔「$v$ がどの最大マッチングでも露出しない」⇔ $v\notin D(G)$。$D(G)$ は最大マッチング 1 つから交互路の探索で求まる。頂点ごとにマッチングを解き直す必要はない。

ヒント3(誘導)
  • 最大マッチングは Edmonds の花アルゴリズム(base[]・p[]・match[] を持ち、偶–偶の辺を見つけたら LCA までの花を縮める)
  • 最後に露出頂点すべてを同時に根にして同じ探索を 1 回走らせる。最大マッチングなので増加路は見つからない。探索が終わったとき「偶」として待ち行列に入った頂点が $D(G)$。W はそれ以外

模範解答 (Python)

模範解答を表示する(まず自力で解いてから)
import sys
from collections import deque


def main():
    data = sys.stdin.buffer.read().split()
    n, m = int(data[0]), int(data[1])
    adj = [[] for _ in range(n)]
    for i in range(m):
        u = int(data[2 + 2 * i]) - 1
        v = int(data[3 + 2 * i]) - 1
        adj[u].append(v)
        adj[v].append(u)

    match = [-1] * n
    base = list(range(n))
    p = [-1] * n
    used = [False] * n

    def lca(a, b):
        seen = [False] * n
        while True:
            a = base[a]
            seen[a] = True
            if match[a] == -1:
                break
            a = p[match[a]]
        while True:
            b = base[b]
            if seen[b]:
                return b
            b = p[match[b]]

    def mark_path(v, b, child, blossom):
        while base[v] != b:
            blossom[base[v]] = blossom[base[match[v]]] = True
            p[v] = child
            child = match[v]
            v = p[match[v]]

    def search(roots):
        # roots から交互木(森)を BFS で広げる。増加路が見つかればその端点を返す
        for i in range(n):
            used[i] = False
            p[i] = -1
            base[i] = i
        q = deque()
        for r in roots:
            used[r] = True
            q.append(r)
        while q:
            v = q.popleft()
            for to in adj[v]:
                if base[v] == base[to] or match[v] == to:
                    continue
                if (to in roots_set and used[to]) or (match[to] != -1 and p[match[to]] != -1):
                    # 偶–偶の辺: 花を縮める
                    cb = lca(v, to)
                    blossom = [False] * n
                    mark_path(v, cb, to, blossom)
                    mark_path(to, cb, v, blossom)
                    for i in range(n):
                        if blossom[base[i]]:
                            base[i] = cb
                            if not used[i]:
                                used[i] = True
                                q.append(i)
                elif p[to] == -1:
                    p[to] = v
                    if match[to] == -1:
                        return to
                    nxt = match[to]
                    used[nxt] = True
                    q.append(nxt)
        return -1

    # 貪欲な初期マッチング
    for v in range(n):
        if match[v] == -1:
            for to in adj[v]:
                if match[to] == -1:
                    match[v] = to
                    match[to] = v
                    break
    for r in range(n):
        if match[r] == -1:
            roots_set = {r}
            t = search([r])
            while t != -1:
                pv = p[t]
                ppv = match[pv]
                match[t] = pv
                match[pv] = t
                t = ppv
    # Gallai–Edmonds: 露出頂点すべてを根にした最後の探索で「偶」になる頂点が D
    roots = [v for v in range(n) if match[v] == -1]
    roots_set = set(roots)
    t = search(roots)
    assert t == -1
    print(''.join('L' if used[v] else 'W' for v in range(n)))


main()

計算量: 増加 1 回の探索が花の縮約込みで $O(N^2+M)$(縮約のたびに全頂点の base を見直す実装)、増加は高々 $N/2$ 回で全体 $O(N^3)$。貪欲な初期マッチングで増加の回数を減らしている。$N=2000$、$M\le10^4$ で 0.22〜0.69 s(Python 3.14.7、M1 MacBook Pro 8GB、負荷平均 2〜3 での実測、起動込み)。

Step-by-Step 解説

1ゲームを最大マッチングに言い換える

先手が勝つ ⇔ $v$ がすべての最大マッチングに入る(背景の定理)。勝ち手は「最大マッチングで $v$ と組む頂点へ動く」。後手がどこへ動いても、先手はその頂点のマッチング相手へ動ける。動けないなら、たどった道が増加路になり最大性に反するから。

2最大マッチングを 1 つ求める(花の縮約)

根から交互木を BFS で広げる。偶頂点 $v$ から辺 $v$–$to$ を見て、

  • $to$ が未訪問で露出していれば増加路
  • $to$ が未訪問でマッチ済みなら、$to$ を奇、$match[to]$ を偶にして待ち行列へ
  • $to$ も偶なら奇閉路(花)。LCA を求め、花の頂点の base を LCA にそろえ、奇だった頂点も偶にして待ち行列へ入れる
3露出頂点すべてを根にした探索で $D(G)$

最大マッチングが求まったら、露出頂点をすべて根にした森で Step 2 の探索をもう 1 回だけ行う。異なる木の偶頂点どうしの辺があれば増加路になるので、最大マッチングでは起きない(コードの assert t == -1)。偶になった頂点の集合が $D(G)$。

4出力

$v\in D(G)$ なら $v$ を避ける最大マッチングがあるので後手勝ち L。それ以外は W。

5二部グラフ版がなぜ一般グラフで誤るか

入力例 2 で最大マッチング {3-5, 2-4}(露出は 1)を取る。二部グラフ用の BFS は「偶頂点 $u$ の隣 $w$ の相手 $match[w]$ を偶にする」だけで、$w$ が奇か偶かを区別しない。1→3→5 で 5 が偶、5→2→4 と 5→4→2 で 4 と 2 が偶になる。ここで偶頂点 4 の隣 5(実は偶)を奇とみなして進むと、5 の相手 3 まで偶にしてしまう。正しい探索では辺 4-5 は偶–偶の辺なので花 {2, 4, 5} を縮め、3 は奇のまま残る。実際 3 はすべての最大マッチングで 1 か 5 と組むので W。この取り違えは 1000 グラフ中 10 グラフ・13 頂点で起きた(検証ログ)。

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

案計算量採否理由
ミニマックスの全探索$O(2^N N)$不採用検証用。$N\le10$ まで
頂点ごとに $G-v$ の最大マッチングを解き直し、$\nu(G-v)<\nu(G)$ を判定$O(N\cdot N^3)$不採用定理の言い換えとしては正しいが、1 回 0.2 s 程度なので $N=2000$ で約 400 s(見積もり)
Tutte 行列の階数を頂点ごとに比べる(乱択)$O(N^4)$(素朴)不採用逆行列を使う高速化はあるが、Python で $N=2000$ の行列演算は重い。決定的な答えが欲しい本問では花アルゴリズムの方が単純
二部グラフ用の交互路 BFS(Day173-Q3)$O(N+M)$不採用(誤り)花を縮めないと偶頂点を見落とす。1000 グラフ中 10 グラフ・13 頂点で誤答
見つけた 1 つの最大マッチングに入るかで判定$O(N^3)$不採用(誤り)別の最大マッチングで露出しうる。1000 グラフ中 360 グラフ・1375 頂点で誤答
花アルゴリズム+露出頂点すべてを根にした探索(本解)$O(N^3)$採用0.22〜0.69 s

前提が変わったら:

  • $N=10^5$ 級なら、$O(E\sqrt V)$ の Micali–Vazirani か、Gabow の実装が要る。本解の素朴な花アルゴリズムは $N=10^4$, $M=8000$ で 2.93 s、$N=2\times10^4$ で 6.74 s(実測)で、ここが限界
  • 二部グラフと分かっているなら、Day173-Q3 の Hopcroft–Karp+BFS の方が速い($10^5+10^5$ 頂点で 2.0 s)
  • 有向グラフ(有向頂点ジオグラフィ)なら PSPACE 完全として知られ、この方法は使えない(記憶。原典未確認)

よくあるミス

ミス原因正しい書き方
1 つの最大マッチングで露出していなければ W別の最大マッチングで露出する頂点を見落とす1000 グラフ中 360 グラフで誤答(実測)。露出頂点から交互木を広げて $D(G)$ を求める
花を縮めずに交互路 BFS二部グラフ版をそのまま使う10 グラフ・13 頂点で誤答(実測)。偶–偶の辺で花を縮める
最後の探索を露出頂点 1 つずつ別々に行い、毎回 used を初期化1 回ずつだと $O(N)$ 回の探索になる露出頂点すべてを同時に根にして 1 回だけ探索する
花の縮約で、もともと奇だった頂点を待ち行列に入れ忘れる花の中の頂点はすべて偶になるif not used[i]: used[i] = True; q.append(i)
孤立頂点を W にする動けない先手は負け孤立頂点は露出するので $D(G)$ に入り L

次のステップ

  • 発展1: 先手の勝ち手(どの隣へ動くか)を出力する。最大マッチングの相手で十分か、$D(G)$ の構造から説明する
  • 発展2: 辺ジオグラフィ(訪問済みになるのが辺)。無向でも PSPACE 完全と記憶しており、本問との差を調べる(未確認)
  • 発展3: $N=10^5$ 向けに Micali–Vazirani か Gabow の $O(E\sqrt V)$ 実装と比べる

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

  • 正解との照合(実測): 頂点数 1〜10・辺の確率ランダムの 1300 グラフ(計 7130 頂点)で、(現在地, 訪問済み集合) のミニマックスと出力が完全一致。不一致 0
  • 誤った方法(実測、1000 グラフ・5451 頂点): 「見つけた 1 つの最大マッチングに入るか」は 360 グラフ・1375 頂点で誤答。「花を縮めない交互路 BFS」は 10 グラフ・13 頂点で誤答。最小の反例は入力例 2(5 頂点 5 辺)
  • 入力例(実測): 例 1 LWL、例 2 LLWLL、例 3 LLLWLLL。ミニマックスと一致
  • 実行時間(実測、起動込み): $N=2000$ で、ランダム $M=10^4$: 0.24 s、ランダム $M=2500$: 0.24 s、三角形を鎖状につないだ奇閉路だらけのグラフ($N=2001$, $M=2000$〜$3000$): 0.44〜0.69 s。貪欲な初期マッチングを外すと 0.26〜0.77 s
  • 限界(実測): $N=10^4$, $M=8000$ で 2.93 s、$N=2\times10^4$, $M=1.6\times10^4$ で 6.74 s。このため制約を $N\le2000$ にした
  • 扱っていない範囲: $O(E\sqrt V)$ の一般マッチング、勝ち手の出力、原典の確認

自己評価

自分の回答

気づき・メモ