NIM游戏
我的回合!
概念
先手必胜状态 我的回合下,我可以走到让局面进入必败状态 先手必败状态 我的回合下,走不到任何一个必败状态
[!原型题] 两个人,聪明绝顶,每次操作都是最优的操作 对多堆石子操作,任取其中一堆的任意数量,判断先手是否必胜
石子数量 a 满足 先手必败 否则先手必胜
证明: 假设上述式子等于x x的二进制最高位为第k位,则一定存在某个a,a的第k位为1(只有至少一个k位为1才有可能x最高位是k)
在这种情况下,异或的结果a1一直到an为x,x异或x为0,必败
若为0 则拿走任意数量石子后,假设还是0 拿走前后做异或,结果还得是0,而每个数都成对,则异或后每对相消,除了有更改的那一堆石子消不去,则结果不为0
得证 ——证明中用了递归定义,可能不够严谨
int ans = 0; //0开始才可以计算全部异或结果
int x;
for(int i = 0; i < n; i ++)
{
cin >> x;
ans ^= x;
}
SG函数
定义SG函数是 其中,y是从当前状态能走到的状态,x是当前状态,mex函数是集合中不存在的最小非负整数,同时定义终点为
发现,当SG(X) = 0 时,意味着x能走到的状态里没有一个状态是0,而SG(x) != 0时,则一定有一个状态SG(y)为0,而终点SG为0 则SG(x) = 0,先手必败,因为只能走到一个SG(x) != 0 的状态,后手必可以走到一个SG(x) = 0的状态,反复循环,则走到终点的必定是后手,先手败,反之则先手胜
多堆同时操作 多个SG图,可以用Nim游戏的证明逻辑证明
必败,否则必胜 因为可以找到一个,则必可以走到,使得最后异或结果为0 结果为0时,可以反证 任意操作之后还是0不成立
#include <iostream>
#include <algorithm>
#include <unordered_set>
#include <cstring>
using namespace std;
const int N = 110, M = 100010;
int s[N], f[M];
int n, k;
//记忆化搜索
int sg(int x)
{
if(f[x] != -1) return f[x]; // 找过不再找,语句避免对同样数量的石堆多次查找,memset
unordered_set<int> S; // 起点下一步的状态集合
for(int i = 0; i < k; i ++)
{
if(x >= s[i]) S.insert(sg(x - s[i]));
}
for(int i = 0; ; i ++) //找非负最小数
{
if(!S.count(i)) return f[x] = i; //状态里没有,则说明找到最小的数了
}
}
台阶Nim游戏
基本概念与Nim游戏一样
[!题意] 从任意台阶取任意石子放在下一级上,如果没得操作就输
证明:先手时奇数台阶异或为0,则先手必败,否则必胜
- 全是0时为败,此时奇数异或为0
- 若先手不为0(奇数),则按照Nim游戏,可以将0的局面创造给对手
- 后手操作奇数,先手可以操作奇数台阶,找到一种方案将奇数台阶的异或值变为0,一直把0的局面给对手
- 后手操作偶数,先手操作其操作的石子再下放到另一个偶数台阶或地面,奇数异或值还是0
- 由于总是对手遇到异或为0的局面,意味着后手一定输
为什么看奇数? 因为偶数台阶放到地面总是需要偶数次移动,这样需要偶数次参与,而奇数全是0的局面是留给后手的,那么意味着后手总是没有办法参与到把石头放在地上这一步,则其必输
因此做题只需要判断奇数台阶的情况就可以了
int main()
{
int n;
cin >> n;
int res = 0;
// 注意这里是代表台阶,要从1开始
for(int i = 1; i <= n; i ++)
{
int x;
cin >> x;
if(i % 2) res ^= x; //奇数台阶就异或
}
if(res) puts("Yes");
else puts("No");
return 0;
}
参考AcWing 892. 台阶-Nim游戏 - AcWing
拆分Nim游戏
SG函数的应用
[!题意] 有多堆石子,每次可以拆其中一堆石子为两堆,拆出来的总数可以大于原来的石子数,也可以少于,但是拆出来的单独一堆不可以大于等于原来的石堆
依旧是定义终点SG为0 由 要计算开局的SG异或值就可以断定每次的输赢
有性质 ——SG定理 然后应用记忆化搜索来计算SG的值
int sg(int x)
{
if(f[x] != -1) return f[x]; //找过不找
unordered_set<int> S;
for(int i = 0; i < x; i ++)
for(int j = 0; j <= i; j ++) //避免重复(i,j)(j,i)
S.insert(sg(i) ^ sg(j)); //当前状态的下个状态
for(int i = 0; ; i ++)
{
if(!S.count(i)) return f[x] = i;
}
}