NIM游戏讲解(超级全面,SG函数代码模板,三种题型)

Nim 游戏

n堆物品,每堆有 m 个,两个玩家轮流取走任意一堆的任意个物品,但不能不取。

取走最后一个物品的人获胜。

我们先来看第一道题目

题目一:

给定n堆石子,两个玩家轮流操作,每次可以从任意一堆石子中拿走任意数量的石子,可以拿完但不能不拿。无法操作者视为失败。问先手是否存在必胜策略。

输入格式:
第一行包含整数n。第二行包含n个数字,其中第i个数字表示第i堆石子的数量。

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

Nim游戏模型,有个结论,设每堆石子个数分别是a_1,a_2,....,a_n

NIM和为:NIM = a_1 \oplus a_2 \oplus .... \oplus a_n

当且仅当 Nim 和为 0 时,该状态为必败状态;否则该状态为必胜状态。

为什么呢?

为了详细介绍NIM游戏以及这个结论,我先介绍一下博弈图和有向图游戏:

博弈图:

如果将每个状态视为一个节点,再从每个状态向它的后继状态连边,我们就可以得到一个博弈状态图。

有向图游戏:

有向图游戏是一个经典的博弈游戏——实际上,大部分的公平组合游戏都可以转换为有向图游戏。

在一个有向无环图中,只有一个起点,上面有一个棋子,两个玩家轮流沿着有向边推动棋子,不能走的玩家判负。

我们能发现,如果子节点都是必胜状态,那么当前一定必败。 如果子节点有一个必败状态,其他节点不用管,当前节点就是必胜状态,因为我可以让拿走若干石子,达到这个状态。

简单总结如下

1:没有后继状态的状态是必败态
2:一个状态是必胜态当且仅当存在至少一个必败态为它的后继状态
3:一个状态是必败状态当且仅当它的所有后继状态均为必胜状态
显然,我们定义必胜状态值大于0,必败状态值为0,那么我们可以得到有向图里面的mex函数就是这样定义的,子节点中没出现的最小非负整数值;

 有了mex函数,那么我们就能定义sg函数了,按照我的理解,sg函数主要是处理后继节点的状态转移。先直接看定义:

 我们再给两个结论(注,这些结论我都不会证明,想看证明的读者请查阅其他资料)

一个局面的SG函数可以用起点的SG函数代替。

故而对于第一题,我们本来的结论应该是NIM = SG(a_1) \oplus SG(a_2) \oplus ... \oplus SG(a_n)

但是对于第一道题,因为我们对于每次拿的石子数不限制,所以我们能得出SG(a1) = a1,故而有了

NIM = a_1 \oplus a_2 \oplus .... \oplus a_n

说到这里,我先给出SG函数的代码模板:

int n, k;
int s[N], f[M];     // s存取石子的方法,f存取所有取法对应的sg值

// dfs求取sg结果
int sg(int x) {
    if (f[x] != -1) return f[x];
    
    unordered_set<int> S;               // 哈希表存所有可以到的局面
    for (int i = 0; i < k; ++i) 
        if (x >= s[i]) 
            S.insert(sg(x - s[i]));     
            
    for (int i = 0 ; ; ++i) 
        if (!S.count(i))
            return f[x] = i;
}

代码中的第一个if是用来做记忆化搜索的。

第一重循环是dfs的遍历,这里面的s[i]是每次允许拿的石子个数,对于第一题,我们可以直接初始化为s[i]  = i-1。其他s[i]初始化的情况我稍后会介绍。

第二重循环是计算mex函数。

下面我们来介绍第二种题型,加上对石子个数的限定,也就是限制了状态转移,后面我们会知道,实际上与第一种题目一模一样,甚至代码都是一样的,只需要更改s[i]。

题目二:

注意看,除了增加了对石子的限制,其他都没变,也就是说,我们只需要改变s[i]即可。

其实做到这里我们发现,关键改变点就是状态转换的边数

再来一题: 

题目三:

这里的转换,不是减少石子的个数,而是把一个石子变成两堆规模更小的石子,也就是状态转移有O(n^2)个,代码如下。 

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;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值