問題
$N$ 頂点 $M$ 辺の連結な単純無向グラフがある。警官 1 人と泥棒 1 人が次の順に動く。
- 警官が好きな頂点に立つ
- 泥棒が、警官の位置を見てから好きな頂点に立つ
- 以後、警官・泥棒・警官・泥棒…の順に交互に 1 手ずつ指す
- 警官の 1 手: 隣の頂点へ移るか、その場に留まる
- 泥棒の 1 手: 長さ 2 以下の道に沿って移るか、その場に留まる。ただし警官のいる頂点を通ること・そこで止まることはできない
- 警官と泥棒が同じ頂点にいた時点で捕獲(警官の勝ち)
両者は互いの位置を常に知っている。警官は捕獲までの警官の手数を最小に、泥棒は最大にしたい。
警官が必ず捕まえられるなら、最適に指したときの警官の手数 $T$ と、そのときの警官の最初の頂点(複数あれば番号最小)を出力せよ。泥棒が逃げ切れるなら -1 を出力せよ。
入力形式
N M
u_1 v_1
...
u_M v_M
出力形式
捕まえられるなら T c、逃げ切られるなら -1
制約
| # | 制約 |
|---|---|
| 1 | $1\le N\le500$、$N-1\le M\le5000$ |
| 2 | グラフは連結、自己ループ・多重辺なし |
入出力例
入力例1
3 2
1 2
2 3出力例1
1 2(中央に立てば、泥棒は警官を通れないので片側の端に閉じ込められ、1 手で捕まる)
入力例2
6 9
1 2
1 3
1 5
1 6
2 3
2 4
2 6
3 4
3 5出力例2
-1(三角形 1-2-3 の各辺に頂点 4・5・6 を 1 つずつ足した 3-sun。弦グラフなので解体可能で、速さ 1 の泥棒なら頂点 1 から 2 手で捕まる(Day176-Q1 の答えは 2 1)。速い泥棒は、警官が中央の三角形に来るたびに外側の頂点から別の外側の頂点へ一気に移り、逃げ続けられる)
入力例3
8 9
1 6
1 8
2 6
3 5
3 6
3 8
4 5
6 8
7 8出力例3
3 3(速さ 1 なら 2 3。捕まえられることは変わらないが、1 手多くかかる。3 例とも値反復による全探索と一致)
背景と出典
- 速い泥棒(速さ $s$ の泥棒、1 手で長さ $s$ 以下の道を警官を通らずに動く)の警官と泥棒は、Fomin–Golovach–Kratochvíl–Nisse–Suchan “Pursuing a fast robber on a graph”(Theoretical Computer Science, 2010 と記憶)で調べられ、警官 1 人で勝てるグラフの特徴付けは Chalopin–Chepoi–Nisse–Vaxès “Cop and robber games when the robber can hide and ride”(SIAM J. Discrete Math., 2011 と記憶)にある、と私は記憶している(どちらも本セッションでは未確認)
- 速さ 1 の結果(警官 1 人で勝てる ⇔ 解体可能、Nowakowski–Winkler 1983/Quilliot 1978)は Day176-Q1 で扱い、本問ではそれが崩れることを全探索で数えた
- 本問の問題文・制約・入力例は自作。入力例 2 の 3-sun は、6 頂点以下の連結グラフを全部調べて見つけた「解体可能なのに速い泥棒が逃げ切る」最小の例(辺数も最小)
概念図
ヒント(クリックで段階的に開示)
ヒント1(方向性)
局面は(警官の位置, 泥棒の位置, 手番)の $2N^2$ 通りで、Day176-Q1 と同じく終局($c=r$)から逆向きに確定させればよい。違いは泥棒の 1 手の行き先だけで、それは警官の位置 $c$ に依存する。
ヒント2(アプローチ)
- 泥棒の行き先 $R_2(c,r)$: $r$ 自身、$r$ の隣($c$ を除く)、$r$ の隣 $u\ne c$ のさらに隣($c$ を除く)
- $c$ が $r$ の隣でなければ $R_2(c,r)$ は「$r$ から距離 2 以内」から $c$ を除いたもの。$c$ が $r$ の隣のときだけ、$c$ を経由する行き先を除く必要がある
- 確定の規則は速さ 1 と同じ: 警官手番は行き先の 1 つが確定すれば(その値 +1)、泥棒手番は行き先が全部確定したら(その最大)
ヒント3(誘導)
- 「警官手番で確定した $r$ の集合」を $c$ ごとの整数のビット
dcop[c]で持つ。泥棒手番 $(c,r)$ はR2[c][r] & ~dcop[c] == 0で確定 - 段 $v$ で確定した泥棒手番の集合
newrob[c]から、警官手番の新しい確定は $\bigl(\bigcup_{c\in N[c']}\texttt{newrob}[c]\bigr)\setminus\texttt{dcop}[c']$ - 泥棒手番を調べ直すのは、その段で
dcop[c]が増えた $c$ だけでよい
模範解答 (Python)
模範解答を表示する(まず自力で解いてから)
import sys
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)
nb = [sum(1 << w for w in adj[v]) for v in range(n)] # 開近傍
cl = [nb[v] | (1 << v) for v in range(n)] # 閉近傍 N[v]
full = (1 << n) - 1
# R2[c][r]: 警官が c にいるとき、泥棒が r から 1 手で行ける頂点(長さ 2 以下、c を通らない、c で止まらない)
R2 = [[0] * n for _ in range(n)]
for r in range(n):
ns = adj[r]
d = len(ns)
pre = [0] * (d + 1)
for i in range(d):
pre[i + 1] = pre[i] | nb[ns[i]]
suf = [0] * (d + 1)
for i in range(d - 1, -1, -1):
suf[i] = suf[i + 1] | nb[ns[i]]
ball2 = cl[r] | pre[d] # 距離 2 以内
for c in range(n):
if c != r:
R2[c][r] = ball2 & ~(1 << c)
for i, c in enumerate(ns): # c が r の隣: c を経由する行き先を除く
R2[c][r] = (cl[r] | pre[i] | suf[i + 1]) & ~(1 << c)
INF = -1
cop = [[INF] * n for _ in range(n)] # 警官の手番での残り手数
rob = [[INF] * n for _ in range(n)] # 泥棒の手番での残り手数
dcop = [0] * n # dcop[c]: 警官手番で値が確定した r の集合
drob = [0] * n
for c in range(n):
cop[c][c] = rob[c][c] = 0
dcop[c] = drob[c] = 1 << c
newrob = [1 << c for c in range(n)] # 直前の段で確定した泥棒手番
v = 0
while True:
# 警官手番 (c', r): 隣 c ∈ N[c'] の泥棒手番 (c, r) が段 v で確定 → 値 v+1
newcop = [0] * n
anyc = False
for c2 in range(n):
acc = 0
for c in adj[c2]:
acc |= newrob[c]
acc |= newrob[c2]
acc &= ~dcop[c2]
if acc:
anyc = True
newcop[c2] = acc
dcop[c2] |= acc
row = cop[c2]
while acc:
low = acc & -acc
row[low.bit_length() - 1] = v + 1
acc ^= low
if not anyc:
break
v += 1
# 泥棒手番 (c, r): 行き先 R2[c][r] がすべて確定(値 ≤ v)→ 値 v(最大を取る)
newrob = [0] * n
for c in range(n):
if not newcop[c]:
continue
dc = dcop[c]
und = full & ~drob[c]
row = R2[c]
got = 0
while und:
low = und & -und
r = low.bit_length() - 1
und ^= low
if row[r] & ~dc == 0:
got |= low
rob[c][r] = v
if got:
newrob[c] = got
drob[c] |= got
best, arg = -1, -1
for c in range(n):
if dcop[c] == full:
t = max(cop[c])
if best < 0 or t < best:
best, arg = t, c
print(-1 if best < 0 else f"{best} {arg + 1}")
main()
計算量: $R_2$ の表が $N^2$ 個の $N$ ビット整数で $O(N^3/w)$ ビット演算。段の数を $T$ として、各段で警官側 $O((N+M)\cdot N/w)$、泥棒側は増えた $c$ について未確定の $r$ ごとに 1 回の論理積。$N=500,M\le5000$ の 7 グラフで 0.08〜0.59 s・43〜47 MB(Python 3.14.8、Apple M5 Pro、負荷平均 4〜13 での実測、起動込み)。
Step-by-Step 解説
1泥棒の行き先を $(c,r)$ ごとに作る
$c$ が $r$ の隣でなければ、長さ 2 の道 $r\to u\to w$ の中継 $u$ が $c$ になることはないので、行き先は「$r$ から距離 2 以内」から $c$ を除くだけ。$c$ が $r$ の隣のときは、中継 $u=c$ を除いた $\bigcup_{u\in N(r),u\ne c}N(u)$ が要る。$r$ の隣の並びで前からの和 pre と後ろからの和 suf を持てば、$c$ を 1 つ抜いた和が pre[i] | suf[i+1] で $O(1)$ 回の演算になる。
2警官を「通り抜ける」規則は速さ 2 では結果を変えない
泥棒が $r\to c\to w$ と警官を通り抜けられるとしても、行き先 $w$ は警官の隣なので次の警官の手で捕まる。だから最適な泥棒はこの手を使わない。600 グラフで「通り抜け可」の版と比べ、答えの違いは 0 件だった(実測)。速さ 3 以上では通り抜けた先が警官から距離 2 になりうるので、この規則が効く。
3段ごとの確定をビットでまとめる
- 段 $v$ で確定した泥棒手番 $(c,r)$ を
newrob[c]のビットで持つ - 警官手番 $(c',r)$ は、警官の行き先 $c\in N[c']$ のどれかで泥棒手番が段 $v$ に確定したら値 $v+1$。最初に確定した行き先が最小なので、未確定のビットだけを取れば済む
- 泥棒手番 $(c,r)$ は、行き先 $R_2(c,r)$ の警官手番が全部確定したら値 $v$(最後に確定した値=最大)。行き先は同じ $c$ の警官手番だけなので、
dcop[c]が増えた $c$ だけ調べ直す
4答え
警官の最初の頂点 $c$ について、泥棒はどの $r$ にも立てるので $\max_r\text{cop}[c][r]$。どれかの $r$ が未確定(dcop[c] が全ビットでない)なら、その $c$ からは捕まえられない。全ての $c$ がそうなら -1。
5解体可能性との比較
速さ 1 では「警官が勝つ ⇔ 解体可能」だったので、同じ判定で済ませたくなる。ラベル付きの連結グラフを全部数えると、5 頂点まではどちらも一致し、6 頂点で 15369 個の解体可能グラフのうち 660 個、7 頂点で 770778 個のうち 142380 個で速い泥棒が逃げ切った(実測)。最小の例が入力例 2 の 3-sun。
迷った他案と採らなかった理由
| 案 | 計算量 | 採否 | 理由 |
|---|---|---|---|
| 値反復(全状態を収束まで更新し続ける) | 反復 × $O(N^2\cdot\text{行き先})$ | 検算用のみ | 捕獲が遅い道 200 頂点で 6.75 s(本解 0.025 s)、逃げ切られる 40 頂点のグラフで 14.23 s(本解 0.0005 s)。逃げ切りは上限まで回さないと判定できない |
| Day176-Q1 の速さ 1 の解をそのまま使う | 同じ | 不採用(誤り) | 600 グラフ中 28 で誤答(実測)。入力例 2・3 |
解体可能性で -1 を判定 | $O(N^3)$ | 不採用(誤り) | 600 グラフ中 23 で勝敗を誤る(実測)。7 頂点のラベル付きグラフで 142380 個 |
| 状態ごとにカウンタを持つ後退解析(Day176-Q1 の形) | $O(\sum_{c,r}|R_2(c,r)|)$ | 不採用 | 正しいが、$|R_2|$ が $N$ 近くまで増える密なグラフでは $N^3$ 回の Python の命令になる。ビットでまとめれば機械語 1 語で 64 状態を扱える |
| 段ごとにビットで確定(本解) | 上記 | 採用 | $N=500$ で 0.59 s 以下 |
前提が変わったら
- $N$ が 1000 以上なら、$R_2$ の表($N^2$ 個の $N$ ビット整数)と段ごとの走査が重くなる。木 1000 頂点で 2.26 s(実測)だったので本問は $N\le500$ とした
- 泥棒の速さが 3 以上なら、行き先は「$c$ を除いたグラフでの距離 $s$ 以内」になり、$c$ ごとに BFS が要る($O(N\cdot N(N+M))$)。Step 2 の「通り抜けは無意味」も成り立たなくなる
- 警官が 2 人以上なら状態が $N^3$ になり、本解の形では $N\le60$ 程度が限度と見込む(測っていない)
よくあるミス
| ミス | 原因 | 正しい書き方 |
|---|---|---|
| 泥棒の行き先を「距離 2 以内」だけで作る | 警官を通れない規則を忘れる | $c$ が $r$ の隣のときは $c$ を経由する行き先を除く(ただし速さ 2 では Step 2 の理由で答えは変わらない) |
| 行き先に警官の頂点 $c$ を入れたまま「全部確定」を待つ | $c$ で止まる手は自滅なので、入れても値は変わらないと思い込む | cop[c][c]=0 は確定済みなので結果は変わらないが、$R_2$ から除いておく方が意図が明確 |
| 泥棒手番の値を「最初に確定した値」にする | 警官側の規則をそのまま使う | 泥棒は最大を選ぶので、全部確定したとき(最後の値) |
| 解体可能性で答える | 速さ 1 の定理の流用 | 3-sun で崩れる(入力例 2) |
| 警官の「留まる」手を忘れる | 隣だけを見る | 行き先は閉近傍 $N[c']$ |
次のステップ
- 発展1: 速さ 3 の泥棒で「警官を通り抜けられる/られない」の違いが出る最小のグラフを探す
- 発展2: 速い泥棒に警官 1 人で勝てる 7 頂点のグラフ(ラベル付きで 628398 個)に共通する構造を、Chalopin ほかの特徴付けと照らし合わせる
- 発展3: 警官 2 人・速さ 1 の後退解析を書き、Petersen グラフ(警官 3 人が必要と記憶)で確かめる
検証ログ(本セッションで実行した数値)
- 値反復との照合(実測): 1〜9 頂点のランダムな連結グラフ 600 個で、捕獲時間と最初の頂点が全件一致。値反復側の行き先は、道を 2 歩たどる別実装で作った。警官が勝つのは 462 個、捕獲時間の最大は 3
- 誤った方法(実測、同じ 600 個): 速さ 1 の解 28、解体可能性 23 で誤答。警官の通り抜けを許す版は 0
- ラベル付き連結グラフの全数(実測): 頂点数 4/5/6/7 で、連結 38/728/26704/1866256、解体可能 35/556/15369/770778、速い泥棒に警官が勝つ 35/556/14709/628398。7 頂点以下の捕獲時間の最大は 3
- 時間(実測、起動込み): $N=500$ の木 3 個 0.39〜0.59 s、全頂点とつながる頂点+乱択辺($M=5000$)0.15 s、毛虫 0.22 s、道 0.19 s、乱択 $M=5000$ 0.08 s。メモリ 43〜47 MB。$N=1000$ の木は 2.26 s
- 扱っていない範囲: 原典(Fomin ほか、Chalopin ほか)の確認、速さ 3 以上、警官 2 人以上