acwing——数学知识(四)Nim游戏

一、经典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,则有如下展开形式:
11.png

先给出两个函数:


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;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值