AOJ 2376 Disconnected Game
解法
石取りゲームだからいい感じにわかるんでは?と思ったけどよくわかんなかったのでメモ化再帰で解きます.
勝ちが決まるタイミングはは,連結成分の数が2であり,かつ自分の手番で辺を追加すると,追加できる辺の数が 0 になるときですね.(それはそう)
それまでは,異なる連結成分を連結させるか,連結成分内の頂点同士に辺を張るかのどちらかです.
残りの辺の数は愚直にもたずに偶奇で判定できますね.異なる連結成分同士を結ぶ辺以外で使える辺の数を $unused$ とします.
異なる連結成分同士を結んだ時,$unused$ も変化しますが,偶奇だけわかればいいので,連結成分のサイズの偶奇の情報を持つだけでいいです.
今,サイズが奇数,偶数の連結成分の数を $(odd, even)$ とします.
$(odd, even, unused)$ でメモ化再帰します.
自分の手番でできるパターンは以下の4通りあります.
- 大きさが偶数の連結成分同士を結ぶ.結果として,偶数の連結成分が1つ減り,新たに使える辺が奇数本増える.
- 大きさが奇数の連結成分同士を結ぶ.結果として,奇数の連結成分が2つ減り,偶数の連結成分が1つ増える.また,使える辺の数が偶数本増える.
- 大きさが偶数と奇数の連結成分を結ぶ.結果として,偶数の連結成分が1つ減る.また,使える辺の数が奇数本増える.
- 連結成分同士を結ばないように辺を張る.
4つ目の場合はメモ化再帰でループが発生してしまいますが,実は $unused$ を偶数から奇数に移す場合は無視できます.
なぜなら,こういう操作をされた場合,次の手番の人が $unused$ を奇数から偶数に移せば,先ほどと同じ状態になるため,いずれは連結成分同士を結ぶ状況に陥るからです.(つまり偶数から奇数に移す操作が最善手であることはない.そういう状況はすでに負け確定.)
これでループが解消できたので,安心してかけます.
ソースコード
#include <bits/stdc++.h> using namespace std; int dfs(int v, vector<string> const& g, vector<bool>& visited) { visited[v] = true; int res = 1; for(int i = 0; i < (int)g.size(); ++i) { if(g[v][i] == 'Y' && !visited[i]) { res += dfs(i, g, visited); } } return res; } int rec(int odd, int even, int unused, vector<vector<vector<int>>>& dp) { int& res = dp[odd][even][unused]; if(res != -1) { return res; } if(odd + even == 2) { return res = unused; } if(unused == 1 && !rec(odd, even, !unused, dp)) { return res = 1; } if(odd > 0 && even > 0 && !rec(odd, even - 1, !unused, dp)) { return res = 1; } if(odd >= 2 && !rec(odd - 2, even + 1, unused, dp)) { return res = 1; } if(even >= 2 && !rec(odd, even - 1, !unused, dp)) { return res = 1; } return res = 0; } int main() { int N; cin >> N; vector<string> g(N); for(int i = 0; i < N; ++i) { cin >> g[i]; } vector<bool> visited(N); int odd = 0, even = 0; int unused = 0; for(int i = 0; i < N; ++i) { if(!visited[i]) { int C = dfs(i, g, visited); unused += C * (C - 1) / 2; if(C & 1) { ++odd; } else { ++even; } } for(int j = i + 1; j < N; ++j) { unused -= (g[i][j] == 'Y'); } } unused %= 2; vector<vector<vector<int>>> dp(N + 1, vector<vector<int>>(N + 1, vector<int>(2, -1))); if(rec(odd, even, unused, dp)) { cout << "Taro" << endl; } else { cout << "Hanako" << endl; } }
感想
4つ目のパターンを書き忘れた状態で間違えて提出してしまったんですが,なぜか通ってしまい,ジャッジのケースが弱いのか,実は完全に無視して良いのかよくわからなくなった….
もしあるなら O(1) 解をもっと考えればそのうちわかるかも(考えませんけどね).