第三十七章 数论——博弈论
- 一、Nim游戏
- 1、题目
- 2、结论
- 3、结论验证
- 4、代码
 
- 二、台阶——Nim游戏
- 1、问题
- 2、思路
- 2、代码
 
- 三、集合——Nim游戏
- 1、问题
- 2、思路—SG()函数
- 2、代码实现(记忆化搜索)
 
一、Nim游戏
1、题目

2、结论
这里直接说结论:
 假设有
    
     
      
       
        n
       
      
      
       n
      
     
    n堆石子,对于第
    
     
      
       
        i
       
      
      
       i
      
     
    i堆石子,石子个数是
    
     
      
       
        
         a
        
        
         i
        
       
      
      
       a_i
      
     
    ai。
 那么我们可以通过以下结论判断先手是否必赢:
 
3、结论验证
当石子都是0的时候,那么一堆0抑或起来还是0,如果有一个人面对这种情况,那么必定是先手必输的状态。从这个特殊的情况出发,我们也能验证一下为什么抑或为0,先手必输。
我们可以简单验证一下结论:
 我们主要从两个角度入手,
先手必赢状态必定可以让对手为先手的时候,所面对的局面是必输状态。
证明:
 
第二件事我们要验证的就是,
我本身处于先手必输的状态,当轮到对方的时候,对方不可能也面对必输的状态。
证明:
如果我取出石子,必定会让某堆的石子数目发生变化,不变化的时候,抑或结果是0,变化之后抑或结果一定不是0。
如果我拿走了石子抑或结果还是0,那么说明我拿走前后该堆石子数目没变,说明我拿了0个,但是这是违法操作。
既然不可能是0,那么对方面对的就一定是必赢的状态。
4、代码
代码实现的话,我们只要看抑或结果是不是0就行了。
#include<iostream>
using namespace std;
int main()
{
    int res=0;
    int n=0;
    cin>>n;
    while(n--)
    {
        int a;
        scanf("%d",&a);
        res^=a;
    }
    if(res)puts("Yes");
    else puts("No");
}
二、台阶——Nim游戏
1、问题

2、思路
首先,如果台阶上没有石子了,那么这些 0 0 0抑或起来就是0,因此如果有一个先手的时候面对这个情况,那么一定是先手必输的状态,所以我们这里还是可以使用我们刚刚的结论来判断是否先手必赢。
那么对于这道题而言,我们只需要保持奇数阶上的石子抑或起来为不为0即可。
为什么呢?
假设我处于先手必赢的状态,即奇数阶上的石子抑或不为0,那么根据之前的nim结论,我们一定可以将部分石子从奇数阶拿到偶数阶,让对手处于先手必败的状态。
此时,对手处于先手必败的状态,它能否打破呢?
如果对手从偶数阶向奇数阶拿石子,那么此时抑或起来就不是0了,那么轮到我操作的时候,我依旧是先手必赢。我只需要将对手从偶数阶拿到奇数阶的石子,再从奇数阶拿到偶数阶,此时我就又能保证对手处于先手必输的状态。
如果对手从奇数阶向偶数阶拿石子,此时该奇数阶的石子个数记录为x,此时抑或起来不是0,轮到我操作,我可以再对一个奇数阶进行操作,让这个台阶的石子也变成x,此时二者抑或又成了0。这样又可以让对手成为先手必败的状态。
但是偶数阶就不一样了,假设我们的结论是保持偶数阶上的石子抑或起来为不为0即可。
 如果我处于先手必败的状态,那么必定存在这样一种情况,当第一级台阶不为0的时候,我可以把第一级台阶上的石子扔到地上,此时偶数阶不变,对手就变成了先手必败。但根据我们之前的证明,必败态是没法让自己赢的,先手必败转移给了对方这是不合理的。
所以,偶数阶只是我们缓冲的一个平台,奇数阶才决定了是否获胜。
2、代码
#include<iostream>
using namespace std;
int main()
{
    int n;
    cin>>n;
    int res=0;
    for(int i=1;i<=n;i++)
    {
        int a;
        scanf("%d",&a);
        if(i%2)res^=a;
    }
    if(res)puts("Yes");
    else puts("No");
}
三、集合——Nim游戏
1、问题

2、思路—SG()函数
我们这里再引入一个 S G ( ) SG() SG()函数的概念,而这个函数的定义需要根据一个有向无环图定义。
我们知道,我们比赛时的每一个状态都可以看作一个点,我们的操作可以看作一个有向边,经过有向边,我们可以到达下一个状态。
我们看下面这个图:
 我们把最终状态变成0,而这里还是要用到我们的nim中的结论。
如果我处于了最终的状态,意思就是我无法进行操作了,那么就说明处于一种必输的状态,所以我们把所有的终点都标记成0。
然后,我们倒退,上一个节点的 S G ( ) SG() SG()函数值等于一个最小的大于等于0值,并且这个最小的自然数值不能是它所有可能的下一个状态的 s g ( ) sg() sg()的函数值。比如,一个节点连接着 s g ( ) sg() sg()函数值为 0 , 1 , 2 0,1,2 0,1,2的点,那么当前的点就只能取 3 3 3,如果所连的点是 1 , 2 1,2 1,2,那么我可以是 0 0 0。
s g ( ) sg() sg()函数的意义同刚刚的结论一样:
如果函数值是0,说明当前是先手必输状态,如果函数值非0,说明当前的状态是先手必赢状态。
那么如我们的每一堆都可以画出这样一个图,那么思路就是,我们根据操作画出所有的情况,构成一个有向无环图,然后逆推
    
     
      
       
        S
       
       
        G
       
       
        (
       
       
        )
       
      
      
       SG()
      
     
    SG()函数。如下图:
 
那么我们对每堆石子都进行上述的操作,然后画出 n n n个图,然后我们对每个图的起点的 S G ( ) SG() SG()函数值进行抑或操作,看最终是否等于0,等于0说明先手必输,不等于0说明先手必赢。

2、代码实现(记忆化搜索)
#include <cstring>
#include <iostream>
#include <unordered_set>
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];
    unordered_set<int> St;
    for (int i = 0; i < m; i ++ )
    {
        int sum = s[i];
        if (x >= sum) St.insert(sg(x - sum));
    }
    for (int i = 0; ; i ++ )
        if (!St.count(i))
            return f[x] = i;
}
int main()
{
    cin >> m;
    for (int i = 0; i < m; i ++ ) cin >> s[i];
    cin >> n;
    memset(f, -1, sizeof f);
    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if (res) puts("Yes");
    else puts("No");
    return 0;
}



















