博弈论详解(Nim游戏,台阶-Nim游戏,集合-Nim游戏,拆分-Nim游戏

3 篇文章 0 订阅
1 篇文章 0 订阅

博弈论

Nim游戏

题意:

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

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

思路:

博弈论

所有结果在开始的时候就已经注定好了

两种结果

1、后手必胜的局面(先手无论怎么操作都会输)

2、先手创造条件变成后手必胜的局面(此时先手和后手就相当于发生了变化)

因为两人都绝对聪明,所以只需要看最原始的情况:

首先,在做这道题的前提条件下,我们需要搞懂一个东西,最后后手是怎么赢的

全0的情况到了先手的人,先手的人无法再拿了

同时不难发现,这种状态下处于在后手必赢的情况下的所有小石堆的异或和为0的情况给到了先手,而后手只需要保持他一定不为0,让对手为0的情况就可以让全0被先手拿到

如何才能保持让先手拿到的异或和一定是0:

首先,后手拿到的一定不是0,设所有数的异或值为x,x的二进制表示的最高位k上的位置一定是1(因为不会有前导0),那么在原数组中一定能找到一个二进制表示中位次在k上为1的,因为x的k位就是由1和0异或而成,找到这个k之后,取出这个元素,现在我们来看看数组变成什么样了:

a1 ^ a2...ai-1 ^ ai+1 ^ ... ^ an

ai被我们取出来了,不难发现ai > (ai^x),因为ai的位次就比(ai ^x)高,然后我们从ai里选走(ai - ai^x)个(为什么这么弄?因为我后面要把它放回数组中),现在放回数组中

变成了

a1 ^ a2...ai-1 ^ ai ^ x ^ ai+1 ^ ... ^ an

化简把a1 ^ a2…ai-1^ ai ^ ai+1 ^ … ^ an 变成x

x ^ x

自然,这种情况异或为0

代码块:
#include<iostream>

using namespace std;

int main()
{
    int n;
    cin >> n;
    int res = 0;
    for(int i = 0; i < n; i ++ )
    {
        int t;
        cin >> t;
        res ^= t;
    }
    if(res) cout << "Yes" << endl;
    else cout << "No" << endl;
    return 0;
}

台阶-Nim游戏

题意:

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

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

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

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

思路:

两种结果

1、后手必胜的局面(先手无论怎么操作都会输)

2、先手创造条件变成后手必胜的局面(此时先手和后手就相当于发生了变化)

因为两人都绝对聪明,所以只需要看最原始的情况:

首先,在做这道题的前提条件下,我们需要搞懂一个东西,最后后手是怎么赢的

毫无疑问和上题一样是全为0

但是我们只能跨越一个阶梯,所以只能从奇数1到地板上,那么我们只需要将题目转换为

奇数上全为0

为什么不需要考虑偶数?

因为如果是偶数,两个聪明的人对弈一定会将偶数处于偶数的位置,比如你从四层阶梯上拿到三层,我只需要将三层的东西拿到二层,我不会允许你破坏奇数的状态,因为后手只要维持住奇数全为0给到先手就好了

剩下的操作就同上题啦~

代码块:
#include<iostream>

using namespace std;

int main()
{
    int n;
    cin >> n;
    int res = 0;
    for(int i = 1; i <= n; i ++ )
    {
        int t;
        cin >> t;
        if(i % 2 != 0) res ^= t; 
    }
    if(res) cout << "Yes" << endl;
    else cout << "No" << endl;
    return 0;
}

集合-Nim游戏

题意:

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

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

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

思路:

两种结果

1、后手必胜的局面(先手无论怎么操作都会输)

2、先手创造条件变成后手必胜的局面(此时先手和后手就相当于发生了变化)

因为两人都绝对聪明,所以只需要看最原始的情况:

首先,在做这道题的前提条件下,我们需要搞懂一个东西,最后后手是怎么赢的

毫无疑问和上题一样是全为0

通过sg函数来存储这个点所能够使用的边,如果这个点的sg函数不为0,说明它下一个一定是不为0的其他数,同样的道理,我们在规定的时候就已经把一无所有的0的sg当作0了,它前面的数就一定不是0,所以我们只需要让先手处于sg为0的点,他就永远无法变成除了0之外的其他数(因为后手也足够聪明呀)

(好问题:sg怎么存储)

因为每一个数在一个游戏中的sg是固定的,所以我们只需要把题目给的最大范围的sg到最小范围的sg求出,则一定可以包含所有的情况

求sg是一个递归过程

代码块:
#include<iostream>
#include<algorithm>
#include<cstring>
#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> S; // 定义一个集合存储情况
    for(int i = 0; i < m; i ++ )
    {
        int sum = s[i]; // 每一种取出方式
        if(x >= sum) S.insert(sg(x - sum)); // 如果数量大于这种取出方式,则递归取出了这个方式的情况,同时把这种取出方式的sg值放入集合
    }
    
    for(int i = 0; ; i ++ )
    {
        if(!S.count(i)) return f[x] = i;//s.count(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"); // 如果不为0,说明先手可以变为0从而成为后手,让原来的后手输
    else puts("No");
    
    return 0;
}

拆分-Nim游戏

题意:

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

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

思路:

理解题意:可以是任意两堆大小的新堆(只要满足堆的石子数量小于取走的即可,两堆放入的总和可以大于取走的)

思路:

两种结果

1、后手必胜的局面(先手无论怎么操作都会输)

2、先手创造条件变成后手必胜的局面(此时先手和后手就相当于发生了变化)

因为两人都绝对聪明,所以只需要看最原始的情况:

首先,在做这道题的前提条件下,我们需要搞懂一个东西,最后后手是怎么赢的

毫无疑问和上题一样是全为0

拆分拆分,即把原来一个大数拆分为两个小于它的数,那么我们只需要把它拆分后的sg算出,然后通过上一题的思路去让后手赢即可,先手无论怎么懂,拿到的都是sg为0的状态。唯一和上题有区别的sg的地方就是它的情况变的更加多了,但是也正是因为这样,题目所要求的范围就变小了。

代码块:
#include<iostream>
#include<algorithm>
#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 ++ ) // 两重循环,遍历所有可能的sg情况
    {
        for(int j = 0; j <= i; j ++ ) 
        {
            S.insert(sg(i) ^ sg(j));
        }
    }
    
    for(int i = 0; ; i ++ ) 
    {
        if(!S.count(i)) return f[x] = i;
    }
}

int main()
{
    cin >> n;
    memset(f, -1, sizeof f);
    
    int res = 0;
    while(n -- )
    {
        int x;
        cin >> x;
        res ^= sg(x);
    }
    
    if(res) puts("Yes");
    else puts("No");
    
    return 0;
}
  • 16
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值