博弈论(初级)——数学知识(C++)

转载说明

Wolfycz
以上是原文链接,请大家支持原创。
——————————————————————————————————————————————————————————

博弈

博弈==,具体的例子就是下棋,双方都考虑最有利于自已的步骤,但是最终必有一方输,一方赢。

博弈的策略:参与者在行动之前所准备好的一套完整的行动方案,就是想好下完这步棋,对方会如何下,以及接下来该如何下,最终得出结果。

常见的博弈有以下:

1.博弈:合作博弈和非合作博弈
合作博弈:指参与者能够达成一种具有约束力的协议,在协议范围内选择有利于双方的策略
非合作博弈:指参与者无法达成这样一种协议
2.博弈:静态博弈和动态博弈
静态博弈:指在博弈中,参与者同时选择,或虽非同时选择,但是在逻辑时间上是同时的。(期末老师评分与同学给老师评分)
动态博弈:指在博弈中,参与者的行动有先后顺序,且后行动者能够观察到先行动者的行动。(下棋)
3.博弈:完全信息博弈与不完全信息博弈
完全信息博弈:指在博弈中,每个参与者对其他参与者的类型,策略空间及损益函数都有准确的信息。(卖家与买家)
不完全信息博弈:总有一些信息不是所有参与者都知道的
4.博弈:零和博弈与非零和博弈
零和博弈:指博弈前的损益总和与博弈后的损益总和相等
非零和博弈:指博弈后的损益大于(小于)博弈前的损益总和(正和或负和 )

必胜状态和必败状态

对下先手来说,

一个状态是必败状态当且仅当它的所有后继都是必败状态。

一个状态是必胜状态当且仅当它至少有一个后继是必败状态。

就是说,博弈者,一旦捉住了胜利的把柄,必然最后胜利。
  
博弈中常常用到的:两个数,不用中间变量实现交换。
  a b;
  a = a^b;
  b = a^b;
  a = a^b;

扫描二维码关注公众号,回复: 13112104 查看本文章

前导例子

例1:取石子游戏之一

有两个游戏者:A和B。有n颗石子。
约定:两人轮流取走石子,每次可取1、2或3颗。A先取,取走最后一颗石子的人获胜。
问题:A有没有必胜的策略?

分析:这是小学必备奥数题之一,我们可以很容易的知道,当n为0,4,8,12……时,A必定会输,因为不论A取多少,B只要和A共同取走4即可;当n不为0,4,8,12……时,A只需要将n取成4的倍数,这样就变成了B先取,B一定会输,所以A一定会赢。

经过我们的分析发现,对这个游戏而言,0,4,8,12……这些状态是对于先手的必败状态,而其他状态是对于先手的必胜状态,因此,我们现在介绍一下有关博弈的一些名词和概念

1、平等组合游戏

1、两人游戏。
2、两人轮流走步。
3、有一个状态集,而且通常是有限的。
4、有一个终止状态,到达终止状态后游戏结束。
5、游戏可以在有限的步数内结束。
6、规定好了哪些状态转移是合法的。
7、所有规定对于两人是一样的。

因此我们的例1提到的游戏即为一个平等组合游戏,但是我们生活中常见的棋类游戏,如象棋、围棋等,均不属于平等组合游戏,因为双方可以移动的棋子不同,不满足最后一个条件;而我们后续提到的游戏,以及博弈中的其他游戏,基本属于平等组合游戏

2、N状态(必胜状态),P状态(必败状态)

像例1的分析一样,0,4,8,12……等状态就是对于先手的P状态(必败状态),其他的则是对于先手的N状态(必胜状态)。

那么我们定义两个状态之间的转换:

所有的终止状态都为P状态
对于任意的N状态,存在至少一条路径可以转移到P状态
对于任意的P状态,只能转移到N状态
证明过于简单,这里不再赘述,我们只需要明白一点,每个人都会选择最策略即可。

当然这里所说的都是最后走步的人获胜的游戏,至于那些走到最后失败的游戏,我们在最后做了一个简单的讲解(Anti Nim)。

在这里插入图片描述
在这里插入图片描述

经典例题

例1:AcWing 891. Nim游戏

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

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

输入格式
第一行包含整数n。

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

输出格式
如果先手方必胜,则输出“Yes”。

否则,输出“No”。

数据范围
1≤n≤105,
1≤每堆石子数≤109
输入样例:

2
2 3

输出样例:

Yes

在这里插入图片描述

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

/*
先手必胜状态:先手操作完,可以走到某一个必败状态
先手必败状态:先手操作完,走不到任何一个必败状态
先手必败状态:a1 ^ a2 ^ a3 ^ ... ^an = 0
先手必胜状态:a1 ^ a2 ^ a3 ^ ... ^an ≠ 0
*/

int main()
{
    
    
    int n;
    scanf("%d", &n);

    int res = 0;
    while (n -- )
    {
    
    
        int x;
        scanf("%d", &x);
        res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

例2:AcWing 892. 台阶-Nim游戏

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

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

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

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

输入格式
第一行包含整数n。

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

输出格式
如果先手方必胜,则输出“Yes”。

否则,输出“No”。

数据范围
1≤n≤105,
1≤ai≤109
输入样例:

3
2 1 3

输出样例:

Yes

此时我们需要将奇数台阶看做一个经典的Nim游戏,如果先手时奇数台阶上的值的异或值为0,则先手必败,反之必胜

证明:
先手时,如果奇数台阶异或非0,根据经典Nim游戏,先手总有一种方式使奇数台阶异或为0,于是先手留了技术台阶异或为0的状态给后手
于是轮到后手:
①当后手移动偶数台阶上的石子时,先手只需将对手移动的石子继续移到下一个台阶,这样奇数台阶的石子相当于没变,于是留给后手的又是奇数台阶异或为0的状态
②当后手移动奇数台阶上的石子时,留给先手的奇数台阶异或非0,根据经典Nim游戏,先手总能找出一种方案使奇数台阶异或为0
因此无论后手如何移动,先手总能通过操作把奇数异或为0的情况留给后手,当奇数台阶全为0时,只留下偶数台阶上有石子。
(核心就是:先手总是把奇数台阶异或为0的状态留给对面,即总是将必败态交给对面)

因为偶数台阶上的石子要想移动到地面,必然需要经过偶数次移动,又因为奇数台阶全0的情况是留给后手的,因此先手总是可以将石子移动到地面,当将最后一个(堆)石子移动到地面时,后手无法操作,即后手失败。

因此如果先手时奇数台阶上的值的异或值为非0,则先手必胜,反之必败!

链接:作者:Anoxia_3

#include <iostream>
#include <algorithm>

using namespace std;

const int N = 100010;

int main()
{
    
    
    int n;
    scanf("%d", &n);

    int res = 0;
    for (int i = 1; i <= n; i ++ )
    {
    
    
        int x;
        scanf("%d", &x);
        if (i & 1) res ^= x;
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

链接:[作者:yxc](https://www.acwing.com/activity/content/code/content/53528/)

例3:AcWing 893. 集合-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

例如:若S = [2, 5]表示每次只能取2个或5个石子,h = 10表示共有10个石子,则有展开形式为:
在这里插入图片描述
mex():设集合S是一个非负整数集合,定义mex(S)为求出不属于S的最小非负整数的运算,即:mes(S)=min[x],其中x属于自然数,且x不属于S(用人话说就是不存在S集合中的数中,最小的那个数)
在这里插入图片描述
在这里插入图片描述

#include <cstring>
#include <iostream>
#include <algorithm>
#include <unordered_set>

using namespace std;

const int N = 110, M = 10010;

int n, m;
int s[N], f[M];//s存储的是可供选择的集合,f存储的是所有可能出现过的情况的sg值


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 ++ )
    {
    
    
        int sum = s[i];
        if (x >= sum) S.insert(sg(x - sum));//如果可以减去s[i],则添加到S中
    }

    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];
    cin >> n;

    memset(f, -1, sizeof f);//初始化f均为-1,方便在sg函数中查看x是否被记录过

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
    
    
        int x;
        cin >> x;
        res ^= sg(x); //观察异或值的变化,基本原理与Nim游戏相同
    }

    if (res) puts("Yes");
    else puts("No");

    return 0;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/53562/

例4:AcWing 894. 拆分-Nim游戏

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

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

输入格式
第一行包含整数n。

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

输出格式
如果先手方必胜,则输出“Yes”。

否则,输出“No”。

数据范围
1≤n,ai≤100
输入样例:

2
2 3

输出样例:

Yes

相比于集合-Nim,这里的每一堆可以变成不大于原来那堆的任意大小的两堆
即a[i]可以拆分成(b[i],b[j]),为了避免重复规定b[i]>=b[j],即:a[i]>=b[i]>=b[j]
相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。
因此需要存储的状态就是sg(b[i])^sg(b[j])(与集合-Nim的唯一区别)

#include <cstring>
#include <iostream>
#include <algorithm>
#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()
{
    
    
    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;
}

作者:yxc
链接:https://www.acwing.com/activity/content/code/content/53564/

猜你喜欢

转载自blog.csdn.net/Annabel_CM/article/details/110286728