問題
$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 以内に収まる範囲から決めた(実測は検証ログ)。
概念図
ヒント(クリックで段階的に開示)
ヒント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 解説
先手が勝つ ⇔ $v$ がすべての最大マッチングに入る(背景の定理)。勝ち手は「最大マッチングで $v$ と組む頂点へ動く」。後手がどこへ動いても、先手はその頂点のマッチング相手へ動ける。動けないなら、たどった道が増加路になり最大性に反するから。
根から交互木を BFS で広げる。偶頂点 $v$ から辺 $v$–$to$ を見て、
- $to$ が未訪問で露出していれば増加路
- $to$ が未訪問でマッチ済みなら、$to$ を奇、$match[to]$ を偶にして待ち行列へ
- $to$ も偶なら奇閉路(花)。LCA を求め、花の頂点の
baseを LCA にそろえ、奇だった頂点も偶にして待ち行列へ入れる
最大マッチングが求まったら、露出頂点をすべて根にした森で Step 2 の探索をもう 1 回だけ行う。異なる木の偶頂点どうしの辺があれば増加路になるので、最大マッチングでは起きない(コードの assert t == -1)。偶になった頂点の集合が $D(G)$。
$v\in D(G)$ なら $v$ を避ける最大マッチングがあるので後手勝ち L。それ以外は W。
入力例 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、例 2LLWLL、例 3LLLWLLL。ミニマックスと一致 - 実行時間(実測、起動込み): $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)$ の一般マッチング、勝ち手の出力、原典の確認