一、经典Nim游戏
题目:给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
先手必胜态:可以走到某一个必败状态
先手必败态:无法走到一个必胜状态
定理:如果初始所有值的异或结果不为0,则先手必胜,反之必败
证明:
①败的最终状态000^0…=0
②当a[1] ^ a[2] ^ … ^ a[n] = x != 0 时
记x的二进制表示中最高位的1是第k位,则肯定存在一个a[i]的第k位为1(因为要使两个数异或结果为1,必然要有一个1)
可知a[i]^x < a[i](因为x的最高位的1在k,第k都为1,因此异或后第k为0,比k位低的异或结果不管是怎么样的,最终结果肯定比a[i])
因此可以把a[i]变成a[i]^x ,即从a[i]中拿走a[i]-a[i]^x个
这样结果会变成a[1] ^ a[2] ^ … ^ a[i] ^ x ^ … ^ a[n] = 0
③当a[1] ^ a[2] ^ … ^ a[n]= 0 (式1)时
当从任意一堆中拿走任何数量的石子后,异或结果肯定不等于0
反证法证明:
若从任意一堆a[i]中拿走任何数量的石子后(a[i]变成a[t]),异或结果等于0,则a[1] ^ a[2] ^ … ^ a[t] ^ … a[n] = 0(式2)
则 式1^式2 = 0 -> a[t] ^ a[i] = 0 -> a[t] = a[i] , 这与a[i] > a[t]矛盾,因此假设不成立
因此若先手的状态为②,那么总存在一种取法,使得结果变成③,当③达到①的状态时,后手的人无法操作,先手胜利
反之同理。
所以证得:如果初始所有值的异或结果不为0,则先手必胜,反之必败
#include <iostream>
using namespace std;
const int N = 100010;
int n;
int main()
{
int res = 0;
cin >> n;
while(n--)
{
int x;
cin >> x;
res ^= x;
}
if(res) puts("Yes");
else puts("No");
return 0;
}
二、台阶-Nim游戏
题目:现在,有一个n级台阶的楼梯,每级台阶上都有若干个石子,其中第i级台阶上有ai个石子(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
此时我们需要将奇数台阶看做一个经典的Nim游戏
定理:如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜
(PS:下面记xor为奇数台阶异或值)
证明:
先手时,如果xor非0,根据经典Nim游戏,先手总有一种方式使xor=0,于是先手留了xor=0的状态给后手
于是轮到后手:
①当后手移动偶数台阶上的石子时,先手只需将对手移动的石子继续移到下一个台阶,这样奇数台阶的石子相当于没变,于是留给后手的又是xor=0的状态
②当后手移动奇数台阶上的石子时,留给先手的xor != 0,根据经典Nim游戏,先手总能找出一种方案使xor=0
因此无论后手如何移动,先手总能通过操作把xor=0的情况留给后手,当奇数台阶全为0时,只留下偶数台阶上有石子。
(核心就是:先手总是把xor=0的状态留给对面,即总是将必败态交给对面)
因为偶数台阶上的石子要想移动到地面,必然需要经过偶数次移动,又因为奇数台阶全0的情况是留给后手的,因此先手总是可以将石子移动到地面,
当将最后一个(堆)石子移动到地面时,后手无法操作,即后手失败。
因此如果先手时奇数台阶上的值的异或值为非0,则先手必胜,反之必败!
#include <iostream>
using namespace std;
int main()
{
int res = 0;
int n;
cin >> n;
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;
}
三、集合-Nim游戏
题目:给定n堆石子以及一个由k个不同正整数构成的数字集合S。
现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合S,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
将每一个h[i]的所有方案看做是一张有向图,例
S={2,5} , h = 10,则有如下展开形式:
先给出两个函数:
mex():设集合S是一个非负整数集合,定义mex(S)为求出不属于S的最小非负整数的运算,即:mes(S)=min{x},x属于自然数,且x不属于S**(用人话说就是不存在S集合中的数中,最小的那个数)**
SG():在有向图中,对于每个节点x,设从x出发共有k条有向边,分别达到节点y1,y2……yk,定义SG(x)为x的后继节点的SG值构成的集合执行mex()运算后的值
即:SG(x)=mex({SG(y1),SG(y2)…SG(yk)});(用人话说就是比后继节点的SG都小的值)
特别的整个图G的SG值被定义为起点s的SG值,即SG(G)=SG(s)
上图标红的值就是每一个节点的SG值
性质:1.SG(k)有k个后继节点,且分别是0~k-1。
2.非0可以走向0
3.0只能走向非0
定理:
对于一个图G,如果SG(G)!=0,则先手必胜,反之必败
证明:
若SG(G)=!0,
1.根据性质2,先手必可以走向0,
2.因此留给后手的是0,根据性质2,后手只能走向非0
3.以此类推,后手始终无法走向0,当先手永远处于0,当先手到达终点的0时,先手获胜
(由此我们可以知道,有些事是命中注定的~~~)
反之同理,必败
定理:
对于n个图,如果SG(G1) ^ SG(G2) ^ … SG(Gn) != 0 ,则先手必胜,反之必败
证明(类似与Nim游戏):
①当SG(Gi) = 0 时 , xor = 0 , 显然先手必败
(PS:结束状态必是状态①,但状态①不一定是结束状态)
②当xor = x != 0 时,因为肯定存在一个SG(xi)^x < SG(xi),而根据SG()的性质1可知,SG(k)可以走到0~k-1的任何一个状态,
因此,必定可以从SG(xi) -> SG(xi)^x , 于是使得xor=0
③当xor = 0时,当移动任何一个节点时,对应的SG值必然减小,可以证明:xor!=0
下证:xor!=0
假设:xor=0,则说明移动的那个节点的值并没有变化,即从SG(k)变成了k,但是这与SG函数的性质1相矛盾,因此不成立
证得:若先手面对的状态是xor != 0,则先手方总能使xor=0,即使后手面对的永远是必败态直到结束状态①,因此先手必胜!
反之,必败!
#include <iostream>
#include <unordered_set>
#include <cstring>
using namespace std;
const int N = 110 , M = 10010;
int n , m;
int s[N] , f[M];
int sg(int x)
{
if(f[x] != -1) return f[x];//记忆化搜索,如果f[x]被计算过就不用计算了
unordered_set<int> S;//用哈希表来存储能到达的所有状态,便于mex搜索
for(int i = 0 ; i < m ; i++)
if(x >= s[i]) S.insert(sg(x - s[i]));//如果大于s[i],则存入状态
for(int i = 0 ; ; i++)//找出不在集合中,最小的那个数,即mex()
if(!S.count(i)) return f[x] = i;
}
int main()
{
cin >> m;
for(int i = 0 ; i < m ; i++) cin >> s[i];
memset(f , -1 , sizeof f);
cin >> n;
int res = 0;
while(n--)
{
int x;
cin >> x;
res ^= sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}
四、拆分-Nim
题目:给定n堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
相比于集合-Nim,这里的每一堆可以变成不大于原来那堆的任意大小的两堆
即a[i]可以拆分成(b[i],b[j]),为了避免重复规定b[i]>=b[j],即:a[i] >= b[i] >= b[i] **
相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。
因此需要存储的状态就是sg(b[i])^sg(b[j])**(与集合-Nim的唯一区别)
#include <iostream>
#include <cstring>
#include <unordered_set>
using namespace std;
const int N = 110;
int n;
int f[N];
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++)//规定j不大于i,避免重复
S.insert(sg(i) ^ sg(j));//相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,
//等于这些局面SG值的异或和
for(int i = 0 ; ; i++)
if(!S.count(i))
return f[x] = i;
}
int main()
{
memset(f , -1 , sizeof f);
cin >> n;
int res = 0;
while(n--)
{
int x;
cin >> x;
res ^= sg(x);
}
if(res) puts("Yes");
else puts("No");
return 0;
}