博弈论

基本知识

  1. 算法中常见的博弈论一般是 公平组合游戏
    公平组合游戏:
    (1)游戏有两个人参与,二者轮流做出决策,双方均知道游戏的完整信息;
    (2)任意一个游戏者在某一确定状态可以作出的决策集合只与当前的状态有关,而与游戏者无关;
    (3)游戏中的同一个状态不可能多次抵达,游戏以玩家无法行动为结束,且游戏一定会在有限步后以非平局结束。
    (4)大部分的棋类游戏都 不是 公平组合游戏,如国际象棋、中国象棋、围棋、五子棋等(因为双方都不能使用对方的棋子)。
  2. 必胜点 与 必败点:
    P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。
    N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。

Nim游戏

例题

题目描述

例题链接
给定n堆石子,两位玩家轮流操作,每次操作可以从任意一堆石子中拿走任意数量的石子(可以拿完,但不能不拿),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数n。
第二行包含n个数字,其中第 i 个数字表示第 i 堆石子的数量。
输出格式
如果先手方必胜,则输出“Yes”。
否则,输出“No”。
数据范围
1≤n≤10^5,
1≤每堆石子数≤10^9
输入样例:
2
2 3
输出样例 Yes

分析

  1. 分析样例:2 3 先从第二堆里拿一个(使两堆个数一样), 假设后手从A堆里拿了a个,那先手就在B堆里拿a个,先手必胜
  2. 一般规律:
    a1 ^ a2 ^ a3 ^ ...a^n = 0 先手必败(异或)
    a1 ^ a2 ^ a3 ^ ... a^n != 0 先手必胜(异或)
    异或:0 ^ 0 = 0; 1 ^ 1 = 0; 1 ^ 0 = 1; 0 ^ 1 = 1
    证明:
    (1)一个石子也没有:(终点)0 ^ 0 ^ 0… ^ 0 = 0当我们不能进行一般操作时,异或值为0
    (2)a1 ^ a2 ^ a3 … ^ an = x != 0 先手可以从某堆中拿出若干石子,使得该式异或值为0. x的二进制表示中最高的一位1在第k位, 那么a1…an中必然存在一个数ai, ai的第k位是1.则
    ai ^ x < ai. 先手拿出 ai - (ai ^ x) 个石子ai = ai - (ai - (ai ^ x)) = ai ^ x a1 ^ a2 ^ a3 … an = x
    x ^ x = 0, 所以该结论成立。
    (3)a1 ^ a2 ^ a3 …an = 0不管怎么拿,结果都不再是0.反证法, 假设从ai堆中拿出若干个后石子使各堆异或为0, ai变成ai2。 a1 ^ a2 ^ a3 ^ ai2 …an = 0, 上下两式异或应为0, an ^ an = 0, ai ^ ai2 != 0,矛盾

代码

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

台阶nim游戏

题目链接
现在,有一个n级台阶的楼梯,每级台阶上都有若干个石子,其中第i级台阶上有ai个石子(i≥1)。
两位玩家轮流操作,每次操作可以从任意一级台阶上拿若干个石子放到下一级台阶中(不能不拿)。
已经拿到地面上的石子不能再拿,最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数n。
第二行包含n个整数,其中第i个整数表示第i级台阶上的石子数ai。
输出格式
如果先手方必胜,则输出“Yes”。
否则,输出“No”。
数据范围
1≤n≤10^5,
1≤ai≤10^9

分析

所有奇数项: a1 ^ a3 ^ a2n-1 = x ! = 0 先手必胜。反之,若为0,先手必败。

证明:
(1)x != 0, 同Nim游戏一样,我们可以通过某种方式,使得剩下的石子异或起来是0,
若对手拿偶数台阶t上的石子,我们就把这拿到t-1上。这样使得奇数台阶上的石子数量不变,异或起来依旧为0。
若对手拿奇数级台阶上的石子,此时该本人拿石子,且此时奇数台阶上的石子数量异或起来不为0, 同nim游戏,我们可以通过某种操作,使得奇数台阶石子数量异或为0
(2)同理,x == 0, 先手必败
分析偶数级台阶
假设A拿完奇数台阶上的所有石子,留给B的状态是:奇数台阶上全为0, 偶数第n级台阶有m个石子, 这级台阶无疑需要偶数次操作 才可。留给A的状态又转化为,奇数台阶异或非0.

代码

#include <algorithm>
#include <iostream>
using namespace std;
int main(void){
    int n;
    int res = 0;
    scanf("%d", &n);
    for(int i = 1; i <= n; i ++){
        int x;
        scanf("%d", &x);
        if(i % 2) res ^= x;
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

SG函数

定义Mex运算

作用:S为非负整数集合, Mex(S)为求出不属于集合S的最小自然数。例如S = {1, 2, 3}, Mex(S) = 0.

定义SG

定义SG(终点) = 0
当前非终点状态X可以转化成的状态y1, y2, …yn: SG(X) = Mex(SG(y1), SG(y2), …SG(yk)).
如图,图中每个圆圈代表某种状态,红色字体为该点的Mex值。
在这里插入图片描述

分析
必败态SG() = 0
如果该点SG(X) = 0必败, SG(X) != 0必胜。
证明: 根据SG函数定义,如果SG(X) != 0,那必然该状态可通过某种方式(走一步)到0, 如果SG(X) = 0, 那么必然(走一步)到不了0
如果先手SG(X) != 0,那么一定可以使后手变成0, 后手不管怎么走,一定走到非0状态, 也就是说,先手一定可以保证自己非0, 后手一直为0, 后手必败;反之,SG(x) = 0先手必败。
我们只有一个图时,只定义成 0,1也可。但若为n个图,每个玩家可以从任意一个图走一步。任何一个图都走不了,则输掉比赛。
SG(X1) ^ SG(X2) .....^ SG(Xn) 若为0, 则必败;若非0则必胜。 证明同Nim游戏。

记忆化搜索

我们下述代码用到了记忆化搜索优化,这里简单介绍何为记忆化搜索

特点:全局最优, 记忆化搜索 = 搜索形式 + 动态规划
一定会用一个数组或其他存储结构存储之前得到的子问题的解(空间换时间)

1、适用范围:必须是分步计算,且搜索过程中的一个搜索的结果必须建立在同类型问题的基础上
2、思想:根据动态规划方程写出递归式,下函数的开头直接返回以前计算过的结果(需要一个存储结构来记录之前算过的结果)
3、核心实现:
a.要通过一个表记录已经存储下的搜索结果(二维数组或哈希表)
b.状态表示,若用数组表示,数组的下标表示每种状态
c.在每一状态搜索的开始,高效的使用数组(或哈希表)搜索这个状态是否出现过,如果已经做过,直接调用答案,回溯
d.如果没有,则按正常方法搜索 (搜索过程中不断记录搜索结果)

集合-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

代码(注意记忆化搜索的巧妙写法)

#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 ++){
        if(x >= s[i]) S.insert(SG(x - s[i]));
    }
    for(int i = 0; ; i ++){
        if(!S.count(i)) return f[x] = i;
    }
    
    
}

int main(void){
    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;
}

拆分-Nim游戏

题目链接
给定n堆石子,两位玩家轮流操作,每次操作可以取走其中的一堆石子,然后放入两堆规模更小的石子(新堆规模可以为0,且两个新堆的石子总数可以大于取走的那堆石子数),最后无法进行操作的人视为失败。
问如果两人都采用最优策略,先手是否必胜。
输入格式
第一行包含整数n。
第二行包含n个整数,其中第i个整数表示第i堆石子的数量ai。
输出格式
如果先手方必胜,则输出“Yes”。
否则,输出“No”。
数据范围
1≤n,ai≤100

代码

把每堆石子看成一个有向图游戏,每堆石子的SG值异或即可。
每一堆都可变成多个局面,每个局面都有两堆b1, b2, sg(b1, b2) = sg(b1) ^ sg(b2)

#include <iostream>
#include <algorithm>
#include <unordered_set>
#include <cstring>
using namespace std;
const int N = 110;
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 ++){
            S.insert(sg(i) ^ sg(j));
        }
    }
    for(int i = 0; ; i ++){
        if(!S.count(i)) return f[x] = i;
    }
}

int main(void){
    int n;
    cin >> n;
    int res = 0;
    memset(f, -1, sizeof(f));
    for(int i = 0; i < n; i ++){
        int x;
        cin >> x;
        res ^= sg(x);
    }
    if(res) puts("Yes");
    else puts("No");
    return 0;
}

巴什博弈

问题描述

只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。

分析

  1. 当n≤m时,这时先手的人可以一次取走所有的

  2. 当n=m+1时,这时先手无论取走多少个,后手的人都能取走剩下所有的;

  3. 当n=k∗(m+1)时,对于每m+1个石子,先手取ii个,后手一定能将剩下的(m+1−i)个都取走,因此后手必胜;

  4. n=k∗(m+1)+x(0<x<m+1)时,先手可以先取x个,之后的局势就回到了上一种情况,无论后手取多少个,先手都能取走m+1个中剩下的,因此先手必胜。
    结论:
    当n能整除m+1时先手必败,否则先手必胜。

核心代码

if(n % (m + 1) != 0)
            return first win;
else
            return second win;

斐波那契博弈(Fibonacci Nim)

问题描述

有一堆个数为n(n>=2)的石子,游戏双方轮流取石子,规则如下:
1)先手不能在第一次把所有的石子取完,至少取1颗;
2)之后每次可以取的石子数至少为1,至多为对手刚取的石子数的2倍。
约定取走最后一个石子的人为赢家,求必败态。
结论
当n为Fibonacci数的时候,必败。
F[i] = 1, 2, 3, 5, 8, 13, 21, 34, 55, 89…
任何一个正整数,都可以表现为两个不连续Fibonacci数之和。
关于证明,现只知道用数学归纳法,想了解的可问度娘。

核心代码

void Init()//斐波那契数列,先打表,范围看情况
{
    f[0]=f[1]=1;
    for(int i=2;i<=55;i++)
    {
        f[i]=f[i-1]+f[i-2];
    }
}
int n;
int flag = 0;
for(int i = 0;f[i];i++){//分别比较,判断出n是否是斐波那契数
    if(f[i] == n){
        flag = 1;
        break;
    }
}
if(!flag)
    return first win;
else
    return second win;

威佐夫博弈

问题描述

有两堆各若干个物品,两个人轮流从任意一堆中取出至少一个或者同时从两堆中取出同样多的物品,规定每次至少取一个,至多不限,最后取光者胜利。

粗略分析
当前局势若为(1, 2),不管怎么取,都是后手胜利;
寻找先手必输局势的规律:
第一种(0, 0)
第二种(1,2)

第三种(3,5)

第四种 (4 ,7)

第五种(6,10)

第六种 (8,13)

第七种 (9 , 15)
第八种 (11 ,18)
我们把这些局势称为“奇异局势”
继续分析我们会发现,每种奇异局势的第一个值(这里假设第一堆数目小于第二堆的数目)总是等于当前局势的差值乘上1.618

我们都知道0.618是黄金分割率。而威佐夫博弈正好是1.618,这就是博弈的奇妙之处!
1.618 = (sqrt(5.0) + 1) / 2

核心代码

 int z = abs(y-x);
 if((int)((sqrt(5) + 1) / 2 * z) != min(x, y))
     return first win;
 else
     return second win;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Xuhx&

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值