E - 石移動ゲーム / Stone Moving Game Editorial
by
MMNMM
この問題は、不偏ゲームと Grundy 数の知識を使うことで解くことができます。
不偏ゲームと Grundy 数については、AWC0067 E の解説などを参照してください。
この問題では、プレイヤーはそれぞれの手番でただひとつの石を選び、その石に対して(他の石に依存しない)操作を行います。
これは単一の石からなるゲームの合併だと考えることができるので、単一の石からなるゲームの Grundy 数を求めることができれば、それぞれの石についての Grundy 数の総 xor を求めることでゲーム全体の Grundy 数を求めることができます。
この問題で与えられるグラフは DAG (有向非巡回グラフ)なので、次の事実をもとに頂点番号が小さいほうから Grundy 数を求めていくことができます。
不偏ゲーム \(G\) について、\(G\) から \(1\) 手進めた状態としてありえるゲームの集合を \(\lbrace G _ 1,G _ 2,\ldots\rbrace\) としたとき、次の等式が成り立つ。\[\mathcal G(G)=\operatorname{mex}\lbrace\mathcal G(G _ 1),\mathcal G(G _ 2),\ldots\rbrace\] ただし、整数の集合 \(S\) に対する \(\operatorname{mex} S\) は \(S\) に含まれない最小の非負整数を表す。
それぞれの頂点について、その頂点のみにひとつの石があり、それ以外の頂点に石が存在しないようなゲームの Grundy 数を求めたとし、頂点 \(i\) に対するその値を \(G _ i\) とします。
高橋君が除去を行う前のゲームの Grundy 数は次のようになります(ここで、\(a\oplus b\) で \(a\) と \(b\) のビットごとの排他的論理和を、\(\displaystyle\bigoplus _ {i=1} ^ Nx _ i\) で \(x _ 1\oplus x _ 2\oplus\cdots\oplus x _ N\) を表します)。\[G=\bigoplus _ {i=1} ^ NG _ i\times(A _ i\,\%\,2)\] 頂点 \(i\) に対して除去を行うと、ゲームの Grundy 数は \(G\oplus(G _ i\times(A _ i\,\%\,2))\) になります。 先手の高橋君がゲームに勝つためには、これが \(0\) でないようにする必要があります。 \(x\oplus y\ne0\iff x\ne y\) なので、\(G\) を求めたあと \(G\ne G _ i\times(A _ i\,\%\,2)\) となるような \(i\) の個数を求めることで、この問題を解くことができました。
時間計算量は \(G _ i\) を求める部分がボトルネックとなり \(O(N+M\log N)\) などになります。
実装例は以下のようになります。
#include <iostream>
#include <vector>
#include <set>
using namespace std;
int main() {
int N, M;
cin >> N >> M;
vector<int> A(N);
for (int& a : A) {
cin >> a;
a %= 2; // A の偶奇だけが結果に関係する
}
// to[i] := i から辺が伸びる頂点のリスト
vector<vector<int>> to(N);
for (int i = 0; i < M; ++i) {
int u, v;
cin >> u >> v;
--u; // 0-indexed にしておく
--v;
to[u].emplace_back(v);
}
// Grundy 数を求める
vector<int> grundy(N);
for (int i = 0; i < N; ++i) {
set<int> next_grundy; // 行先の値を列挙して
for (int next : to[i]) {
next_grundy.emplace(grundy[next]);
}
int g = 0;
while (next_grundy.contains(g)) {
++g;
} // 含まれない最小値が答え
grundy[i] = g;
}
// 除去を行わなかったときの Grundy 数を求めて
int G = 0;
for (int i = 0; i < N; ++i) {
G ^= grundy[i] * A[i];
}
// それと等しくないところを数える
int ans = 0;
for (int i = 0; i < N; ++i) {
if (G != grundy[i] * A[i]) {
++ans;
}
}
cout << ans << endl;
return 0;
}
N, M = map(int, input().split())
# A の偶奇だけが結果に関係する
A = [a % 2 for a in map(int, input().split())]
# to[i] := i から辺が伸びる頂点のリスト
to = [[] for i in range(N)]
for i in range(M):
u, v = map(int, input().split())
u -= 1 # 0-indexed にしておく
v -= 1
to[u].append(v)
# Grundy 数を求める
grundy = [0 for i in range(N)]
for i in range(N):
next_grundy = set() # 行先の値を列挙して
for next in to[i]:
next_grundy.add(grundy[next])
g = 0
while g in next_grundy:
g += 1
# 含まれない最小値が答え
grundy[i] = g
# 除去を行わなかったときの Grundy 数を求めて
G = 0
for g, a in zip(grundy, A):
G ^= g * a
# それと等しくないところを数える
ans = 0
for g, a in zip(grundy, A):
if G != g * a:
ans += 1
print(ans)
posted:
last update:
