基础Nim-游戏

异或(加法运算,只是不进位)

相同为0,不同为1

1.Nim游戏

给定 n 堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 n。

第二行包含 n 个数字,其中第 ii 个数字表示第 ii 堆石子的数量。

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

1≤n≤1051≤n≤105,
1≤每堆石子数≤1091≤每堆石子数≤109

输入样例:

2
2 3

输出样例:

Yes

结论: 若 a1^a2^a3^...^an=0( ^为异或 ,ai 表示第i堆石子 ),这先手必败,否则先手必胜。

为什么当a1^a2^a3^...^an=0时,先手必败呢?

1.当不能操作时,0^0^...^0^0=0;

2.如果某一个状态a1^a2^a3^...^an = x(x!=0)时,一定会存在一种情况使得从所有石堆里拿y(y=ai-(ai^x))个石子,使得 a1^a2^a3^...^an = 0;证明如下:假设x的二进制最高位1是第k位,在a1~an中一定存在一个ai的最高位1是第k位,那么显然ai^x<ai , 所以可以从ai中拿走ai-(ai^x)个石子,则ai堆石子的数量为ai-(ai-(ai^x)),那么ai的数量为ai^k,即原式为a1^a2^...^(ai^x)^...^an,又因为a1^a2^...^ai^...^an=x,所以a1^a2^...^(ai^x)^...^an=x^x=0;

3.如果某一个状态a1^a2^a3^...^an = 0时,不管后手怎么拿,a1^a2^a3^...^an 一定不等于0;

利用反证法证明:假设从i堆拿走了一些石子,使得ai变为ai′,那么原试就变为a1^a2^a3^...^ai′^...^an,两个式子进行异或,由于除了ai和ai′,其他数字都一一对应,异或为0,所以推出ai^ai′=0,与前面假设不符,所以假设错误,即证明如果某一个状态a1^a2^a3^...^an = 0时,不管后手怎么拿,a1^a2^a3^...^an 一定不等于0;

代码

#include<iostream>
using namespace std;
int main()
{
    int n;
    scanf("%d",&n);
    int res=0;
    while(n--)
    {
        int a;
        scanf("%d",&a);
        res ^= a;
    }
    if(res==0) puts("No");
    else puts("Yes");
    return 0;
}

2.台阶-Nim游戏

题目

现在,有一个 n 级台阶的楼梯,每级台阶上都有若干个石子,其中第 i 级台阶上有 ai 个石子(i≥1)。

两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。

已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 n。

第二行包含 n 个整数,其中第 i 个整数表示第 i 级台阶上的石子数 ai。

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

1≤n≤10^5,
1≤ai≤10^9

输入样例:

3
2 1 3

输出样例:

Yes

分析:

这一题只需考虑奇数台阶的石子数量,然后异或,如果异或结果为0,则先手必败,否则先手必胜。

为什么只用考虑奇数台阶呢?

因为如果对手操作偶数台阶上的数量,此时这些石子到了奇数台阶,奇数台阶总数量变了,这时我们可以再把刚才从奇数台阶拿相同的数量的石子到下一层台阶,下一层台阶是偶数台阶,所以奇数台阶数量永远不变,对手如果操作奇数台阶的话,1.如果不是从第1节台阶上拿的石子,我们可以把它放到下一个台阶上,使得奇数台阶数量不变,2.如果拿的是第1节台阶,那么可以根据第1题的Nim游戏的证明方法,我们再从其他奇数台阶拿一定数量的石子,使得异或值还为0。

那为什么不用类似方法,只考虑偶数台阶呢?

当只有第1节台阶有石子时,异或值为0,答案为先手必败,但此时先手可以把第一节所有石子放到地面,此时后手就败了,所以此种假设是错误的。

代码

#include<iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int res=0;
    for(int i=1;i<=n;i++)
    {
        int x;
        cin>>x;
        if(i%2) res^=x;
    }
    cout<<res<<endl;
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

3.集合-Nim游戏

给定 n 堆石子以及一个由 k 个不同正整数构成的数字集合 S。

现在有两位玩家轮流操作,每次操作可以从任意一堆石子中拿取石子,每次拿取的石子数量必须包含于集合 S,最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 k,表示数字集合 S 中数字的个数。

第二行包含 k 个整数,其中第 i个整数表示数字集合 S 中的第 i 个数 si。

第三行包含整数 n。

第四行包含 n 个整数,其中第 i 个整数表示第 i 堆石子的数量 hi。

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

1≤n,k≤100,
1≤si,hi≤10000

输入样例:

2
2 5
3
2 4 7

输出样例:

Yes

前提‘

(1)Mex函数

设S表示一个非负整数集合,定义mex(S)为求出不属于集合S的最小非负整数运算;

such as : S = {0 , 1,4},mex(S)=3;     S = { 1 , 2 }  mex(S)=0;

(2)SG函数

在有向图游戏中,对于每个节点x,设从x 出发共有k条有向边,分别到达节点y 1 , y 2 , … , y k ,
定义S G ( x ) SG(x)SG(x)为x xx的后继节点y 1 , y 2 , … , y k的SG函数值构成的集合执行mex运算的结果。

定义SG函数的终点为0;

假设有一堆石子,石子数量为10 ,每次可以拿走2个或5个,那么其有向图如下图所示:


 

可以把n个石子(每个石子为一个有向图),看为n堆石子(每个有向图看为每堆石子),

每个石子的SG函数值,就对应前面Nim游戏,然后把每个SG值,异或起来,看是否为0;

结论:SG(G)=SG(G1)^SG(G2)^......^SG(Gn);

代码

#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 gs(int x)//每次递归都只保存相邻两组的值,假设6连了1,2,3,4,5 那么1,2,3,4,5的状态会同时影响6的值
{
    if(f[x]!=-1) return f[x];
    unordered_set<int> S;//每次递归都会更新S中的值
    for(int i=0;i<m;i++)
    {
        int sum=s[i];
        if(x>=s[i]) S.insert(gs(x-sum));
    }
    for(int i=0;;i++)
    {
        if(!S.count(i))
        return f[x]=i;
    }
}
int main()
{
    cin>>m;
    for(int i=0;i<m;i++) scanf("%d",&s[i]);
    cin>>n;
    memset(f,-1,sizeof f);
    int res=0;
    for(int i=0;i<n;i++)
    {
        int x;
        cin>>x;
        res^=gs(x);//每次搜索完一个块,不用更新f的值,是因为,每个搜过的值,在相同的s数组中得到的f值是相同的
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

4.拆分-Nim游戏

给定 n 堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为 0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。

问如果两人都采用最优策略,先手是否必胜。

输入格式

第一行包含整数 n。

第二行包含 n 个整数,其中第 i个整数表示第 i 堆石子的数量 ai。

输出格式

如果先手方必胜,则输出 Yes

否则,输出 No

数据范围

1≤n,ai≤100

输入样例:

2
2 3

输出样例:

Yes

定理:SG(G1,G2)=SG(G1)^SG(G2)

分析:题意为可以拿出一堆石子,然后在所有石堆中加入两堆一定数量的石子(每堆石子的数量不能大于等于母堆石子的数量,但两堆石子的数量加起来可以大于母堆石子的数量) 

即 9 可以分为 0 和 0~8 ,1 和 0~8,2和0~8 ,........ ,8和0~8

然后把每个石堆相互异或,得到值,就可以判断结果

代码

#include<iostream>
#include<cstring>
#include<unordered_set>
using namespace std;
const int N = 110;
int f[N];
int gs(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++)
        {
            S.insert(gs(i)^gs(j));
        }
    }
    for(int i=0;;i++)
    {
        if(!S.count(i))
        return f[x]=i;
    }
}
int n;
int main()
{
    cin>>n;
    memset(f,-1,sizeof f);
    int res=0;
    while(n--)
    {
        int x;
        cin>>x;
        res^=gs(x);
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值