第三十七章 数论——博弈论

第三十七章 数论——博弈论

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

一、Nim游戏

1、题目

第三十七章 数论——博弈论_第1张图片

2、结论

这里直接说结论
假设有 n n n堆石子,对于第 i i i堆石子,石子个数是 a i a_i ai
那么我们可以通过以下结论判断先手是否必赢:
第三十七章 数论——博弈论_第2张图片

3、结论验证

当石子都是0的时候,那么一堆0抑或起来还是0,如果有一个人面对这种情况,那么必定是先手必输的状态。从这个特殊的情况出发,我们也能验证一下为什么抑或为0,先手必输。

我们可以简单验证一下结论:
我们主要从两个角度入手,

先手必赢状态必定可以让对手为先手的时候,所面对的局面是必输状态。

证明:
第三十七章 数论——博弈论_第3张图片


第二件事我们要验证的就是,

我本身处于先手必输的状态,当轮到对方的时候,对方不可能也面对必输的状态。

证明:

如果我取出石子,必定会让某堆的石子数目发生变化,不变化的时候,抑或结果是0,变化之后抑或结果一定不是0。

如果我拿走了石子抑或结果还是0,那么说明我拿走前后该堆石子数目没变,说明我拿了0个,但是这是违法操作。

既然不可能是0,那么对方面对的就一定是必赢的状态。

4、代码

代码实现的话,我们只要看抑或结果是不是0就行了。

#include
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、问题

第三十七章 数论——博弈论_第4张图片

2、思路

首先,如果台阶上没有石子了,那么这些 0 0 0抑或起来就是0,因此如果有一个先手的时候面对这个情况,那么一定是先手必输的状态,所以我们这里还是可以使用我们刚刚的结论来判断是否先手必赢。

那么对于这道题而言,我们只需要保持奇数阶上的石子抑或起来为不为0即可

为什么呢?

假设我处于先手必赢的状态,即奇数阶上的石子抑或不为0,那么根据之前的nim结论,我们一定可以将部分石子从奇数阶拿到偶数阶,让对手处于先手必败的状态。

此时,对手处于先手必败的状态,它能否打破呢?

如果对手从偶数阶向奇数阶拿石子,那么此时抑或起来就不是0了,那么轮到我操作的时候,我依旧是先手必赢。我只需要将对手从偶数阶拿到奇数阶的石子,再从奇数阶拿到偶数阶,此时我就又能保证对手处于先手必输的状态。

如果对手从奇数阶向偶数阶拿石子,此时该奇数阶的石子个数记录为x,此时抑或起来不是0,轮到我操作,我可以再对一个奇数阶进行操作,让这个台阶的石子也变成x,此时二者抑或又成了0。这样又可以让对手成为先手必败的状态。

但是偶数阶就不一样了,假设我们的结论是保持偶数阶上的石子抑或起来为不为0即可。
如果我处于先手必败的状态,那么必定存在这样一种情况,当第一级台阶不为0的时候,我可以把第一级台阶上的石子扔到地上,此时偶数阶不变,对手就变成了先手必败。但根据我们之前的证明,必败态是没法让自己赢的,先手必败转移给了对方这是不合理的。

所以,偶数阶只是我们缓冲的一个平台,奇数阶才决定了是否获胜。

2、代码

#include
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、问题

第三十七章 数论——博弈论_第5张图片

2、思路—SG()函数

我们这里再引入一个 S G ( ) SG() SG()函数的概念,而这个函数的定义需要根据一个有向无环图定义。

我们知道,我们比赛时的每一个状态都可以看作一个点,我们的操作可以看作一个有向边,经过有向边,我们可以到达下一个状态。

我们看下面这个图:第三十七章 数论——博弈论_第6张图片
我们把最终状态变成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()函数。如下图:
第三十七章 数论——博弈论_第7张图片

那么我们对每堆石子都进行上述的操作,然后画出 n n n个图,然后我们对每个图的起点的 S G ( ) SG() SG()函数值进行抑或操作,看最终是否等于0,等于0说明先手必输,不等于0说明先手必赢。

第三十七章 数论——博弈论_第8张图片

2、代码实现(记忆化搜索)

#include 
#include 
#include 
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;
}

你可能感兴趣的:(算法合集(c++实现),游戏,算法,c++)