Nim 游戏
先手必胜状态:可以走到某一个对方必败状态
先手必败状态:无论怎么走,下一个状态都是对方必胜
先给出结论:
当
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
≠
0
时
,
先
手
必
胜
a_n\neq0时,先手必胜
an=0时,先手必胜
当
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
=
0
时
,
先
手
必
败
a_n=0时,先手必败
an=0时,先手必败
证明:
当
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
=
x
≠
0
时
a_n=x\neq0时
an=x=0时,一定存在一个数,拿掉这个数剩下的数异或起来为0
x的二进制位的最高位1在第k位,那么
a
1
−
a
n
中
,
必
有
一
个
数
a
i
的
第
k
位
是
1
。
反
证
法
,
假
设
第
k
位
全
是
0
,
那
么
x
的
第
k
位
也
是
0
a_1-a_n中,必有一个数a_i的第k位是1。反证法,假设第k位全是0,那么x的第k位也是0
a1−an中,必有一个数ai的第k位是1。反证法,假设第k位全是0,那么x的第k位也是0
所以必定存在
a
i
a_i
ai ^
x
x
x
<
a
i
<a_i
<ai,所以在
a
i
a_i
ai 中拿掉
a
i
−
a
i
a_i-a_i
ai−ai ^
x
x
x,那么
a
i
a_i
ai变成了
a
i
a_i
ai ^
x
x
x
所以
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
i
a_i
ai ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
=
x
a_n=x
an=x^
x
=
0
x=0
x=0变成了
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
i
a_i
ai ^ x
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
=
x
a_n=x
an=x^
x
=
0
x=0
x=0
拿完之和剩余所有的数异或起来变成了
0
0
0
当
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
i
a_i
ai ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
=
0
a_n=0
an=0时,不管怎么拿,剩下的数异或起来一定不为0$
假设把
a
i
a_i
ai 拿了之后,
a
i
a_i
ai 变成了
a
i
t
a_i^t
ait
反证法:假设
a
1
a_1
a1^
a
2
a_2
a2 ^
a
3
a_3
a3 ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
i
t
a_i^t
ait ^
⋅
⋅
⋅
···
⋅⋅⋅ ^
a
n
=
0
a_n=0
an=0,把这个式子与没拿之前的异或起来,
就有
a
i
a_i
ai ^
a
i
t
=
0
a_i^t=0
ait=0,说明
a
i
=
a
i
t
a_i=a_i^t
ai=ait,那么矛盾,所以拿完之和异或起来一定不为
0
0
0
因为石子是有限的,所以在经过有限次拿之后,最后所有石子一定会拿完,所以要保证赢必须保证拿完之后的所有数的异或值是 0 0 0,让别人无石子可拿。
最最经典的Nim游戏
模板题链接:AcWing 891. Nim游戏
代码如下:
#include <iostream>
using namespace std;
int n,x;
int main()
{
int res=0;
scanf("%d",&n);
while(n--)
{
scanf("%d",&x);
res^=x;
}
if(res)
printf("Yes");
else
printf("No");
return 0;
}
经典Nim游戏的变形:AcWing 892. 台阶-Nim游戏
这道题也是一样的,其实只用看奇数阶的台阶的异或值就行了
SG函数与mex
mex运算
m e x ( m i n i m a l e x c l u d a n t ) mex(minimal excludant) mex(minimalexcludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如 m e x mex mex{ 0 , 1 , 2 , 4 0,1,2,4 0,1,2,4} = 3 =3 =3、 m e x mex mex{ 1 , 3 , 5 1,3,5 1,3,5} = 0 =0 =0、 m e x mex mex{} = 0 =0 =0
SG函数
S G ( x ) = m e x SG(x)=mex SG(x)=mex{ y 1 , y 2 , y 3 , ⋅ ⋅ ⋅ , y n y_1,y_2,y_3,···,y_n y1,y2,y3,⋅⋅⋅,yn},其中 y 1 , y 2 , y 3 , ⋅ ⋅ ⋅ , y n \,y_1,y_2,y_3,···,y_n\, y1,y2,y3,⋅⋅⋅,yn是 x \,x\, x能到达的子局面
S G ( 终 点 ) = 0 SG(终点)=0 SG(终点)=0
在SG图中,任何一个非零状态一定能到 0 \,0 0,任何一个 0 \,0 0状态一定不能到达非零状态
比如,对于一个有
10
10
10 个石子的石头堆,每次只能拿
2
2
2 个或
5
5
5 个,那么这个
S
G
SG
SG 图如下,其中红色的为
S
G
SG
SG 的值
模板题:AcWing 893. 集合-Nim游戏
注意点:①:要用集合来存
②:
f
[
x
]
!
=
−
1
f[x]!=-1
f[x]!=−1 的时候说明
x
x
x 这个点已经存在
s
g
sg
sg 值了,记忆化,可以直接返回
③:最后一步返回
f
[
x
]
=
i
f[x]=i
f[x]=i 的时候,包含处理了
0
0
0这种情况了
#include<bits/stdc++.h>
using namespace std;
const int N = 110, M = 1e4 + 10;
int f[M], se[N];//f用来存储每个数的sg值,se用来存储集合里面的数
int k, s, n, h;
int sg(int x)
{
if(f[x] != -1)
return f[x];
set<int> S;
for(int i = 1; i <= k; ++i)
{
if(x >= se[i])
S.insert(sg(x - se[i]));
}
for(int i = 0; i < M; ++i)
if(!S.count(i))
return f[x] = i;
}
int main()
{
memset(f, -1, sizeof(f));
scanf("%d", &k);
for(int i = 1; i <= k; ++i)
scanf("%d", &se[i]);
scanf("%d", &n);
int res = 0;
for(int i = 1; i <= n; ++i)
{
scanf("%d", &h);
res ^= sg(h);
}
if(res)
printf("Yes");
else
printf("No");
return 0;
}
SG函数变形的一道题:AcWing 894. 拆分-Nim游戏
这道题就是把一个数拆成了两个数,本质上还是SG函数
核心就是:当前异或值不为0的,一定可以经过操作变成0
当前异或值为0的,经过操作后的异或值一定不是0
经过有限次操作之后,所有的数一定都可以变成0,即游戏结束