博弈论(Nim游戏的扩展)

公平组合游戏ICG

若一个游戏满足:

1.由两名玩家交替行动;

2.在游戏进程的任意时刻,可以执行的合法行动与轮到哪名玩家无关;

3.不能行动的玩家判负;

则称该游戏为一个公平组合游戏。

NIM博弈属于公平组合游戏,但城建的棋类游戏,比如围棋,就不是公平组合游戏

必胜状态和必败状态

必胜状态,先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态 。即先手可以走到某一个必败状态。

必败状态,先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态 。即先手走不到任何一个必败状态。

  • **如果当前局面所有石堆的异或和(Nim和)为0,那么无论如何操作,当前玩家都处于必败态;**如果当前局面是必败态,则无论如何操作,另一方总有必胜的策略。

  • **如果当前局面所有石堆的异或和(Nim和)不为0,那么当前玩家可以通过正确的策略使对手进入必败态,因此当前玩家处于必胜态。**如果当前局面是必胜态,玩家可以通过正确操作将局面转化为必败态,从而保证胜利。

异或运算三个性质:

①任何数和 0做异或运算,结果仍然是原来的数

②任何数和其自身做异或运算,结果是 0

③异或运算满足交换律和结合律

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int n; cin >> n;
    int res = 0;
    for(int i = 0; i < n; i++){
        int x; cin >> x;
        res^=x;
    }
    if(res) cout << "Yes\n";
    else cout <<"No\n";
    return 0;
}

台阶-Nim游戏

与经典Nim的区别

  • 在经典的Nim游戏中,石子堆是独立的,而在台阶-Nim中,台阶之间可能存在关联。例如,某个台阶上的石子数量可能会影响其他台阶的策略

结论:

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

证明:

先手时,如果奇数台阶异或非0,根据经典Nim游戏,先手总有一种方式使奇数台阶异或为0,于是先手留了奇数台阶异或为0的状态给后手,此时先手必赢。

于是轮到后手:

①当后手移动偶数台阶上的石子到奇数台阶上时,先手只需将对手移动的石子继续移到下一个台阶,这样奇数台阶的石子相当于没变,奇数异或和仍是非0, 于是留给后手的又是奇数台阶异或为0的状态

②当后手移动奇数台阶上的石子到偶数台阶上时,留给先手的奇数台阶异或非0,根据经典Nim游戏,先手总能找出一种方案使奇数台阶异或为0

因此无论后手如何移动,先手总能通过操作把奇数异或为0的情况留给后手,当奇数台阶全为0时,只留下偶数台阶上有石子。

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

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;

int main() {
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    int n; cin >> n;
    int res = 0;
    for(int i = 1; i <= n; i++){
        int x; cin >> x;
        if(i & 1)
        res^=x;
    }
    if(res) cout << "Yes\n";
    else cout <<"No\n";
    return 0;
}

集合-Nim游戏:

1.Mex运算:
设S表示一个非负整数集合.定义mex(S)为求出不属于集合S的最小非负整数运算
例如:S={0,1,2,4},那么mes(S)=3;

2.SG函数

  • 对于一个游戏状态 x,SG函数值 表示从该状态出发,所有可能的后续状态的SG值所能达到的"最小非负整数"(Mex)。
  • 具体来说,若从状态 x通过合法操作可以到达的所有状态的SG值分别为 那么 就是一个不在这些SG值集合中的最小非负整数。

SG函数的性质:类似于经典的Nim游戏

  • 必败态 (Losing Position):对于某个状态 x,如果 那么当前处于该状态的玩家将无法避免失败(即在对方采取最优策略的情况下无论如何都会输)。
  • 必胜态 (Winning Position):对于某个状态 x,如果 ,那么当前处于该状态的玩家有策略可以使对手进入必败态。也就是先手一定会胜利。

递归计算:

例如s = [2,5],x = 10时递归得到

初始状态:
  • f[x] 是用来存储每个x的SG值的数组,初始时f[x]的所有值都被设置为-1(表示尚未计算过)。
  • x = 10 是我们要计算的状态,s = [2, 5] 是可供选择的石子数。
计算 sg(10) 的过程:
  1. 调用 sg(10)

    • 首先检查 f[10],因为f[10] = -1,说明还没有计算过,所以继续计算。
    • 初始化集合 S
  2. 遍历 s[i]

    • s[0] = 2
      • 因为 10 >= 2,所以我们递归调用 sg(10 - 2) = sg(8)
    • s[1] = 5
      • 因为 10 >= 5,所以我们递归调用 sg(10 - 5) = sg(5)
  3. 递归调用 sg(8)

    • 首先检查 f[8],因为f[8] = -1,所以继续计算。
    • 初始化集合 S
    • 遍历 s[i]
      • s[0] = 2
        • 因为 8 >= 2,所以我们递归调用 sg(8 - 2) = sg(6)
      • s[1] = 5
        • 因为 8 >= 5,所以我们递归调用 sg(8 - 5) = sg(3)
  4. 递归调用 sg(6)

    • 首先检查 f[6],因为f[6] = -1,所以继续计算。
    • 初始化集合 S
    • 遍历 s[i]
      • s[0] = 2
        • 因为 6 >= 2,所以我们递归调用 sg(6 - 2) = sg(4)
      • s[1] = 5
        • 因为 6 >= 5,所以我们递归调用 sg(6 - 5) = sg(1)
  5. 递归调用 sg(4)

    • 首先检查 f[4],因为f[4] = -1,所以继续计算。
    • 初始化集合 S
    • 遍历 s[i]
      • s[0] = 2
        • 因为 4 >= 2,所以我们递归调用 sg(4 - 2) = sg(2)
      • s[1] = 5
        • 因为 4 < 5,所以跳过。
  6. 递归调用 sg(2)

    • 首先检查 f[2],因为f[2] = -1,所以继续计算。
    • 初始化集合 S
    • 遍历 s[i]
      • s[0] = 2
        • 因为 2 >= 2,所以我们递归调用 sg(2 - 2) = sg(0)
      • s[1] = 5
        • 因为 2 < 5,所以跳过。
  7. 递归调用 sg(0)

    • sg(0)是一个基本情况,因为没有石子可以取走,故f[0] = 0,返回 0
  8. 接下来如上继续.....

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
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]; // 如果x的SG值已经计算过,直接返回
    set<int> S;
    //S是一个有序集合,用来存储当前状态的所有可能的下一步的SG值
    
    // 循环每一次可以选择拿走的石头数
    for(int i = 0; i < m; i++){
        // 递归计算x减去s[i]后的状态的SG值,并插入到集合S中
        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() {
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin >> m;
    for(int i = 0; i < m; i++) cin >> s[i];
    cin >> n;
    memset(f,-1,sizeof f);
    f[0] = 0; //当x为0的时候sg函数返回0
    // 类似于经典的Nim游戏
    int res = 0;
    while(n--){
        int x; cin >> x;
        res ^= sg(x);
    }
    if(res) cout << "Yes\n";
    else cout <<"No\n";
    return 0;
}

拆分-Nim游戏

相比于集合-Nim,这里的每一堆可以变成小于原来那堆的任意大小的两堆

即a[i]可以拆分成(b[i],b[j]),为了避免重复规定b[i]>=b[j] 即:a[i]>b[i]>=b[j]

相当于一个局面拆分成了两个局面,由SG函数理论,多个独立局面的SG值,等于这些局面SG值的异或和。SG(i^j) = SG(i) ^SG(j)

因此需要存储的状态就是sg(b[i])^sg(b[j]),与集合-Nim的唯一区别

cpp 复制代码
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
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]; // 如果x的SG值已经计算过,直接返回
    set<int> S;//这里可以改为unordered_set<int> S
    //S是一个有序集合,用来存储当前状态的所有可能的下一步的SG值

    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() {
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin >> n;
    memset(f,-1,sizeof f);
    f[0] = 0;
    // 类似于经典的Nim游戏
    int res = 0;
    while(n--){
        int x; cin >> x;
        res ^= sg(x);
    }
     if(res) puts("Yes");
    else puts("No");
    return 0;
}
相关推荐
网易独家音乐人Mike Zhou3 小时前
【卡尔曼滤波】数据预测Prediction观测器的理论推导及应用 C语言、Python实现(Kalman Filter)
c语言·python·单片机·物联网·算法·嵌入式·iot
‘’林花谢了春红‘’4 小时前
C++ list (链表)容器
c++·链表·list
机器视觉知识推荐、就业指导6 小时前
C++设计模式:建造者模式(Builder) 房屋建造案例
c++
Swift社区6 小时前
LeetCode - #139 单词拆分
算法·leetcode·职场和发展
Kent_J_Truman7 小时前
greater<>() 、less<>()及运算符 < 重载在排序和堆中的使用
算法
IT 青年7 小时前
数据结构 (1)基本概念和术语
数据结构·算法
Yang.997 小时前
基于Windows系统用C++做一个点名工具
c++·windows·sql·visual studio code·sqlite3
熬夜学编程的小王7 小时前
【初阶数据结构篇】双向链表的实现(赋源码)
数据结构·c++·链表·双向链表
zz40_8 小时前
C++自己写类 和 运算符重载函数
c++