競プロにおけるNim、Grundy数とNimK
この記事は、 「競プロ!!」 競技プログラミング Advent Calendar 2017 21日目 の記事です。
昨日は DEGwer さんの「数え上げテクニック集」でした。まだ全部読んでないけど凄かったです。
こっちでも同時に競プロのアドベントカレンダーが進められています。
Nim,grundy数がわかっていると、盤面を共有して二人で対戦し、勝敗が必ず決するタイプのゲーム問題がわりとサクッと解けます。
最近では 5日前、2017/12/16 の Atcoder Regular Contest 087 E で Grundy数の問題が出ました。
この記事の本題はNimの拡張されたゲーム、NimKの話です。
導入として、Nimとかgrundy数を知らないひとにもわかるようにスライドを作成しました。
絵を描くだけのつもりだったのにスライドになってしまいました。
適当すぎてわからん!と思うひともいるかもしてないので参考リンクも貼りました。
それらのページを見てから戻ってきてくれるとうれしいです。
・参考になる記事
augusuto04さん、pekempeyさんの記事が特にわかりやすいです。書いてあるゲームを実際にやりながら考えてみてください。
(2) Nim
・他の参考記事
不偏ゲームがNIMに帰着する証明 - SSSSLIDE
グランディ数 - zukky162のブログ
grundy number - EAGLE 雑記
grundy数を習得したかった - nanikakaのAOJ航海日誌
・ 練習に良さそう
最初のリンクは超絶おすすめ、これだけ解けるようになれば優しい問題は大体解ける
Hacker Rank 5 days of game theory (15問あるけど最後のやつ以外は基本だけで解ける)
- 解説は この記事 が参考になると思います。
yukicoderのNimのタグ
yukicoderのGrundy数のタグ
競技プログラミングにおけるゲーム問題まとめ [Nim,Grundy数,後退解析,ミニマックス法] - はまやんはまやんはまやん
競技プログラミングの不偏ゲーム(Nim, grundy数にまつわる)問題集 - かっさのなにか
本題
・NimK について
NimKとは、一般に知られているNimの拡張です。
プレイヤーは二人のまま、打てる手が変化します。
プレイヤーは自分のターンでK個の山を選択し、それぞれの山に対して好きな数だけ石を取ることができる。
各山に対して石を取ることのできる数が制限される場合など、いろいろな制約がつくパターンもあるようですが、ここではどれだけでも取れるとします。
問題っぽくすると次の文章になります。
山の石を取るゲームををAlice,Bobが行う。このゲームではすべての石の中で最後の石を取った人が勝ちです。(石を取れなくなった人の負け)
各山には個の石が積まれている。プレイヤーは各自分のターンに個までの山を選択し、各山について石を1つ以上なら何個でも取ることができる。
2人が最適な石の取りかたをする時、Aliceが先攻ならば勝つことができるか?
制約: , ,
実際にシュミレーションをして、grundy数を出すことは可能ですが、KやNが大きいときは状態数がやばいので厳しさがあります。
先に必勝法の判定は何かを書いておきます。
各山にある石の数を2進数で表したとき、これを
となるようにを作成します。
次に個のについてこれを作成し、を同じxについて次のように足し合わせます。
=
このとき、が 0 でなくなるようなが存在する場合には必勝です。(値を復元するとこれがgrundy数になっている)
通常のNimでもこれが成り立っています。
Xor (排他的論理和)でgrundy数を求めるものもありますが、あれは2進数の各桁同士の和を で計算した値と一致します。どちらの計算も2進数でみると各桁の数字は1の出現回数の偶奇で決まるためです。
この前NimKについて先輩と話したところ、「本当にいつものNimと一緒で必ず勝ちの目を維持できるんですか?」といわれてウウッとなり、本当に正しいのかを考えてみて怪しいのですが少し証明をしてみました。エレガントではないし、厳密ではないかもしれないので間違っていたら教えてください。証明とかイヤ!という人はとばしてください。
というわけで、存在します。
1つの山しか選択できない Nim と同様に手番を常に勝ち→負けへと動かすことができます。
コードを見たほうがはやい!みたいな人もいるはずなのでC++で先の問題のコードを書きました。上に書いたとおりに書いただけなので実際にはbin[i][x]とかは使わずに書けます。
void Nimk(int N, int K, vector<int>& pile) { vector< vector<int> > bin(N, vector<int>(30, 0)); // 2次元配列 for (int i = 0; i < N; i++) { //pile[i]について、2進数に展開する int twopow = 1; // 2のべき乗 for (int x = 0; x < 30; x++) { if (twopow & pile[i])// 2^xにくっつく数は0か1か bin[i][x] = 1; else bin[i][x] = 0; twopow = twopow * 2; } } vector<int> digitsum(N, 0); // 桁ごとの和 for (int x = 0; x < 30; x++) {// 桁xについて for (int i = 0; i < N; i++) { digitsum[x] = digitsum[x] + bin[i][x]; } } int win = 0; // 負けを0、勝ちを1とする for (int x = 0; x < 30; x++) { if (digitsum[x] % (K + 1) != 0) { // 桁の値が0ではないときは勝ちのポジション、grundy数が0でないことが確定する win = 1; } } if (win == 1) { // 先手の勝ち cout << "Alice" << endl; } else { // 先手の負け cout << "Bob" << endl; } }
実際に解いてみましょう!
蟻本4-2節の練習問題にもある、 POJ2315 は問題文こそ分かりにくいですが、ゲームとしてみるとNimKです。
この記事を読んで理解した方なら”やるだけ”ですね!(実際にはPOJ特有のアレや問題文がアレなのでやるだけではないかも)
明日は575検出のプロ、YazatenさんのICPC引退ポエム?です。
[追記]
組合せゲームの勉強に良さそうな本 (一般的なゲーム理論ではない方)
(A) 組合せゲーム理論入門
(B) 石取りゲームの数学
競技プログラミングから外れるものも多いので、一部分だけ読めばNim,grundy数のことがわかります。(ちゃんとインターネットに演習問題の答えもあって比較的やさしい)
ちゃんと読むと、Aでは AOJ-ICPC1000点問題 が、Bでは Atcoder 1100点問題 とかが解けるようになります(もちろん多少の前提知識があれば本を読まなくても考えると解けます(という人もいる))