問題
個の茶碗が横一列に並んでいる。各茶碗には整数が書かれており、中には個の豆が入っている。これらを用いて次のゲームを行う。
- プレイヤーは自分のターンに茶碗以外の茶碗に入っている豆を1つ選んで取り出す。
- 茶碗から豆を取り出したとき、茶碗,茶碗, , 茶碗のいずれかの茶碗に豆を入れなければならない。
- 交互にターンを繰り返し、自分のターンに選べる豆がなくなったプレイヤーの負けとなる。
両プレイヤーが最適な戦略をとったとき、先手と後手のどちらが勝つか判定せよ。
解法
Grundy数を考える。個目の茶碗に入っている豆を遷移させられるのはからまでなので、この区間のGrundy数に含まれない最小の整数が番目についてのGrundy数となる。
これを単純に数えると以上かかるが、セグメント木を利用して高速化することでで求めることができる。
具体的な方法としては、まず番目のGrundy数を求める状況を考えることとする。セグメント木には各Grundy数に対して、そのGrundy数が番目までで最後にどこで出てきたかを記録しておく。
このセグメント木を利用して、「番目のGrundy数としてが可能かどうか」という二分探索をしていく。セグメント木にの範囲でクエリを投げ、以下のGrundy数として最小の出現位置を得る。これがより大きかったらつまり以下がからの範囲に全て詰まっているということなのでは番目のGrundy数にはなれない。逆に小さかったら以下のどれかが番目のGrundy数として選べる。この判定をもとに二分探索を行うことでこの問題が解ける。
反省
2時間41分(2WA)でAC。眠気と格闘しながら1時間20分ほど考えたが、そもそもGrundy数という発想がなく全然ダメな考察をしていた。
そこから解説スライドを見てまず100点分の部分点解法を得る。Grundy数、言われるとなるほどなんだがこれを思いつける気はしない。
さらに満点を得るためにはセグメント木を実装しなければならず、また他人の提出をいろいろ見てみたところ二分探索部分がセグメント木の中に埋め込まれている? ような感じでよくわからなかった。多少効率は悪いのかもしれないが、セグメント木を用いた値の取得で、それを使った二分探索でさらにかかる実装とした。これでも十分間に合う(222msec)。
セグメント木として確保する範囲が大きすぎて一度REを出してしまった。理解が浅いのでこういうミスが出るという面もあるのだと思う。
Grundy数とこのセグメント木による高速化は相性が良さそうに思えるが、典型と言えるんだろうか。
コード
#include"bits/stdc++.h" using namespace std; using ll = int64_t; class SegmentTree { public: SegmentTree(ll n, ll value) { n_ = (ll)pow(2, ceil(log2(n))); nodes_.resize(2 * n_ - 1, value); } void update(ll x, ll v) { nodes_[x + n_ - 1] = v; for (ll i = (x + n_ - 2) / 2; i > 0; i = (i - 1) / 2) { nodes_[i] = min(nodes_[2 * i + 1], nodes_[2 * i + 2]); } } ll getMin(ll a, ll b, ll k = 0, ll l = 0, ll r = -1) { if (r < 0) { r = n_; } if (r <= a || b <= l) { return INT_MAX; } if (a <= l && r <= b) { return nodes_[k]; } ll left = getMin(a, b, 2 * k + 1, l, (l + r) / 2); ll right = getMin(a, b, 2 * k + 2, (l + r) / 2, r); return min(left, right); } private: //2のべき乗 ll n_; vector<ll> nodes_; }; int main() { ll N; cin >> N; vector<ll> C(N), A(N); C[0] = A[0] = 0; ll sum = 0; for (ll i = 1; i < N; i++) { cin >> C[i] >> A[i]; sum += A[i]; } constexpr ll MAX = 1e5 + 1; SegmentTree st(MAX, -1); vector<ll> grundy(N); grundy[0] = 0; st.update(0, 0); for (ll i = 1; i < N; i++) { //取り得るgrundy数について2分探索をする ll ng = -1, ok = MAX; while (ng + 1 != ok) { ll mid = (ng + ok) / 2; //0からmidまでのgrundy数について,今まで出てきた位置の最小値を求める ll min_index = st.getMin(0, mid + 1); (min_index >= i - C[i] ? ng = mid : ok = mid); } grundy[i] = ok; st.update(grundy[i], i); } ll xor_value = 0; for (ll i = 1; i < N; i++) { if (A[i] % 2) { xor_value ^= grundy[i]; } } cout << (xor_value == 0 ? "Second" : "First") << endl; }