[AcWing] 893. 集合-Nim游戏(C++实现)博弈论SG函数模板题

1. 题目

在这里插入图片描述

2. 读题(需要重点注意的东西)

思路:
首先要知道几个定义

公平组合游戏(ICG)

公平组合游戏(ICG)
(1)由两名玩家交替行动
(2)在游戏进行的任意时刻,可以执行的合法行动与轮到哪位玩家无关
(3)轮流走,当一个玩家不能走时游戏结束
(4)游戏不能区分玩家的身份,例如黑白棋就是不行的
特征
给定初始局势,指定先手玩家,如果双方都采取最优策略,那么获胜者已经确定了,也就是说ICG问题存在必胜策略

必胜状态和必败状态

必胜状态和必败状态
必胜状态:先手进行某一个操作,留给后手是一个必败状态时,对于先手来说是一个必胜状态。即先手可以走到某一个必败状态。
必败状态:先手无论如何操作,留给后手都是一个必胜状态时,对于先手来说是一个必败状态。即先手走不到任何一个必败状态。
结论
假设n堆石子,石子数目分别是a1,a2,…,an,如果a1⊕a2⊕…⊕an≠0,先手必胜;否则先手必
败。

SG函数

SG函数
给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,无法移动者判负。事实上,这个游戏可以认为是所有公平组合游戏的抽象模型。

mex运算

mex运算
表示最小的不属于这个集合的非负整数。
对于一个给定的有向无环图,定义图的每个顶点的SG函数如下:SG(x)=mex{ SG(y) | y是x的后继 }。
首先将终点全部置0,再逐一找出前驱节点的SG值
在这里插入图片描述

有向图游戏的和

有向图游戏的和
设G1、G2、……、Gn是n个有向图游戏,定义游戏G是G1、G2、……、Gn的和,游戏G的移动规则是:任选一个子游戏Gi并移动上面的棋子。则SG =SG(G1) ⊕ SG(G2) ⊕ … ⊕ SG(Gn)。也就是说,游戏的和的SG函数值是它的所有子游戏的SG函数值的异或。

SG值的意义

SG值的意义
当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏求它的SG值,就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略。
结论:
先手必胜:SG =SG(G1) ⊕ SG(G2) ⊕ … ⊕ SG(Gn) ≠ 0
先手必败:SG =SG(G1) ⊕ SG(G2)⊕ … ⊕ SG(Gn) = 0

本题思路:
本题的主要思路就是代结论,求出每个节点的SG值,代入上述结论,
如果SG =SG(G1) ⊕ SG(G2) ⊕ … ⊕ SG(Gn) ≠ 0 ,先手必胜;
如果SG =SG(G1) ⊕ SG(G2) ⊕ … ⊕ SG(Gn) = 0 ,先手必败。


读题
集合S中有k个数,表示每次只能取这k个数中的某一个数的石头。
如 k = 2,S = {2,5} ,表示S中有两个数,取石头时每次要么取2,要么取5。

n堆石子,如 n = 3,每堆分别有 2 4 7个石子。将每堆石子的取法看成一张有向图,此处取7,则取法的有向图如下:

在这里插入图片描述
所以 SG(G7) = 0 (初始状态的SG值),同理求出第一堆和第二堆的SG值,SG(G2) ,SG(G4)
然后把每堆石子的SG值异或起来
则最终结果SG = SG(G2) ⊕ SG(G4) ⊕ SG(G7)

3. 解法

---------------------------------------------------解法---------------------------------------------------

#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];

// 求出每堆石子的SG值
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)); // 将新的状态加进来
    }
    
    // 找出集合当中不存在的最小值并返回
    for (int i = 0; ; i ++ )
        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);

    int res = 0;
    for (int i = 0; i < n; i ++ )
    {
        // 输入每堆石子的个数
        int x;
        cin >> x;
        // 求出每堆石子的SG值将其异或
        res ^= sg(x);
    }
    // 得到结果
    if (res) puts("Yes");
    else puts("No");

    return 0;
}

可能存在的问题
求每堆石子的SG值的代码如何分析?

// 求出每堆石子的SG值
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)); // 将新的状态加进来
    }
    
    // 找出集合当中不存在的最小值并返回
    for (int i = 0; ; i ++ )
        if (!S.count(i))
            return f[x] = i;
}

4. 可能有帮助的前置习题

5. 所用到的数据结构与算法思想

  • 博弈论
  • SG函数
  • 记忆化搜索
  • 递归

6. 总结

博弈论SG函数模板题,理解思想并背下代码。

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Cloudeeeee

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

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

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

打赏作者

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

抵扣说明:

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

余额充值