res得出的答案是在先手的情况下,先拿几个石头,在后面的回合只模仿对手拿的数量。
Nim游戏:
cpp
#include <iostream>
#include <cstdio>
using namespace std;
/*
先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ ... ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ ... ^an ≠ 0
*/
int main()
{
int n;
scanf("%d", &n);
int res = 0;
for(int i = 0; i < n; i++)
{
int x;
scanf("%d", &x);
res ^= x;//每堆石头都异或一下答案,有几堆石头就更新几次答案
}
if(res == 0) printf("No\n");
else printf("Yes\n");
}
台阶-Nim游戏:
注意:因为最后要把石子都放到地面,地面是第0层(偶数层)
cpp
#include <iostream>
using namespace std;
int main()
{
int res = 0;
int n;
scanf("%d",&n);
for(int i = 1 ; i <= n ; i++)
{
int x;
scanf("%d",&x);
if(i % 2) res ^= x;//只要奇数的台阶的值异或值为0就必胜
}
if(res) printf("Yes\n");
else printf("No\n");
return 0;
}
集合-Nim游戏:
1.Mex运算:
设S表示一个非负整数集合.定义mex(S)为求出不属于集合S的最小非负整数运算,即:
mes(S)=min{x};
例如:S={0,1,2,4},那么mes(S)=3;
2.SG函数
在有向图游戏中,对于每个节点x,设从x出发共有k条有向边,分别到达节点y1,y2,····yk,定义SG(x)的后记节点y1,y2,····
yk的SG函数值构成的集合在执行mex运算的结果,即:
SG(x)=mex({SG(y1),SG(y2)····SG(yk)})
特别地,整个有向图游戏G的SG函数值被定义为有向图游戏起点s的SG函数值,即 SG(G)=SG(s).
3.有向图游戏的和
设G1,G2,····,Gm是m个有向图游戏.定义有向图游戏G,他的行动规则是任选某个有向图游戏Gi,并在Gi上行动一步.G被称为有向图游戏G1,G2,·····,Gm的和.
有向图游戏的和的SG函数值等于它包含的各个子游戏SG函数的异或和,即:
SG(G)=SG(G1)xorSG(G2)xor···xor SG(Gm)
如:某个石堆:每次只拿2或者5个,一开始有10个,把所有可能画出来,计算SG(10)的结果,是1则必胜,是0则必败
题目要求是只能有两种操作:2和5;一共有三个石堆:2 4 7
cpp
#include<iostream>
#include<cstring>
#include<algorithm>
#include<set>
using namespace std;
const int N=110,M=10010;
int n,m;
int f[M],s[N];//s存储的是可供选择的集合的个数,f存储的是所有可能出现过的情况的sg值
int sg(int x)
{
//因为取石子数目的集合是已经确定了的,所以每个数的sg值也都是确定的,如果存储过了,直接返回即可
if(f[x]!=-1) return f[x];
set<int> S;
//set代表的是有序集合(注:因为在函数内部定义,所以下一次递归中的S不与本次相同)
for(int i=0;i<m;i++)
{
int sum=s[i];
if(x>=sum) S.insert(sg(x-sum));
//先延伸到终点的sg值后,再从后往前排查出所有数的sg值
}
for(int i=0;;i++)
//循环完之后可以进行选出最小的没有出现的自然数的操作
if(!S.count(i))
return f[x]=i;
}
int main()
{
scanf("%d",&m);
for(int i=0;i<m;i++)
scanf("%d",&s[i]);
scanf("%d",&n);
memset(f,-1,sizeof(f));//初始化f均为-1,方便在sg函数中查看x是否被记录过
int res=0;
for(int i=0;i<n;i++)
{
int x;
scanf("%d",&x);
res^=sg(x);//观察异或值的变化,基本原理与Nim游戏相同
}
if(res) printf("Yes\n");
else printf("No\n");
return 0;
}
拆分-Nim游戏:
注意:该题的意思是:取走一个堆,再造两个更小的堆。比如取走50的堆,新造49和48的堆。
cpp
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
unordered_set<int> S;
int sg(int x)
{
if(f[x] != -1) return f[x];//因为取石子数目的集合是已经确定了的,所以每个数的sg值也都是确定的,如果存储过了,直接返回即可
for(int i = 0 ; i < x ; i++)
for(int j = 0 ; j <= i ; j++)//规定j不大于i,避免重复
S.insert(sg(i) ^ sg(j));//先延伸到终点的sg值后,再从后往前排查出所有数的sg值
//相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和
for(int i = 0 ; ; i++)
if(!S.count(i))//循环完之后可以进行选出最小的没有出现的自然数的操作
return f[x] = i;
}
int main()
{
memset(f , -1 , sizeof f);//初始化f均为-1,方便在sg函数中查看x是否被记录过
scanf("%d",&n);
int res = 0;
while(n--)
{
int x;
scanf("%d",&x);
res ^= sg(x);//观察异或值的变化,基本原理与Nim游戏相同
}
if(res) printf("Yes\n");
else printf("No\n");
return 0;
}