【W的AC企划 - 第一期】博弈论

往期浏览

第一期 - 博弈论

第二期 - 前缀和

第三期 - 二分与三分算法

第四期 - 莫队算法

第五期 - 线段树(暂时未公开)

第六期 - 位运算

第七期 - 树上分治

第八期 - Tarjan缩点

第九期 - 网络流

第十期 - 字符串哈希






讲解

博弈论主要分为两个大块,其一是经典模板博弈论,这一部分的内容较多且非常杂,但是由于过于典,考察的并不是很多;其二是思维为主的原创博弈,除了需要靠思维能力之外,一般我们有“SG函数(优雅打表)”和“博弈推导(类似于数学归纳法)”两个通用的辅助解题方法。

来点彩蛋 截图

现在大家是不是完全明白了 总之还是要多做题






传统博弈

巴什博奕

问题模板

\(N\) 个石子,两名玩家轮流行动,按以下规则取石子:。

规定:每人每次可以取走 \(X(1 \le X \le M)\) 个石子,拿到最后一颗石子的一方获胜。

双方均采用最优策略,询问谁会获胜。

最核心思路:分类讨论

巴什博弈是一种减法博弈,减法博弈的共同特征为玩家轮流从某一总数(对应本题 \(N\) 件物品)中减去某个数值(对应本题拿取物品),所减去的数值限定在某个集合中(对应本题 \(1 \le X \le M\) ),先将数值减为 \(0\) 者(对应本题拿到最后一颗石子的一方)获胜。

结论与证明

  • \(N=K*(M+1)\) (其中 \(K \in \mathbb{N}^+\) ),后手必胜(后手可以控制每一回合结束时双方恰好取走 \(M+1\) 个,重复 \(K\) 轮后即胜利);
  • \(N=K*(M+1)+R\) (其中 \(K \in \mathbb{N}^+,0 < R < M + 1\) ),先手必胜(先手先取走 \(R\) 个,之后控制每一回合结束时双方恰好取走 \(M+1\) 个,重复 \(K\) 轮后即胜利)。

变体

  1. 报数巴什博弈

    这一变体本质上与传统的巴什博弈完全等价,题目背景为:

    两名玩家轮流报数。

    规定:第一个报数的人可以报 \(X(1 \le X \le M)\) ,后报数的人需要比前者所报数大 \(Y(1 \le Y \le M)\) ,率先报到 \(N\) 的人获胜。

    双方均采用最优策略,询问谁会获胜。

    直接用传统巴什博弈的解法做题即可。

  2. 反转获胜条件的巴什博弈

    这一变体本质上就是传统的巴什博弈反一下结论。不会吧真的有出题人出这种水题吗 题目背景为:

    \(N\) 个石子,两名玩家轮流行动取石子。

    规定:每人每次可以取走 \(X(1 \le X \le M)\) 个石子,将石子取完的一方输掉比赛。

    双方均采用最优策略,询问谁会获胜。

    将传统巴什博弈的结论反一下即可:\(N=K*(M+1)\) ,先手必胜。






扩展巴什博弈

问题模板

\(N\) 颗石子,两名玩家轮流行动,按以下规则取石子:。

规定:每人每次可以取走 \(X(a \le X \le b)\) 个石子,如果最后剩余物品的数量小于 \(a\) 个,则不能再取,拿到最后一颗石子的一方获胜。

双方均采用最优策略,询问谁会获胜。

最核心思路:分类讨论

结论

参考传统巴什博弈的结论,我们可以得到:

  • \(N = K*(a+b)\) 时,后手必胜;
  • \(N = K*(a+b)+R_1\) (其中 \(K \in \mathbb{N}^+,0 < R_1 < a\) ) 时,后手必胜(这些数量不够再取一次,先手无法逆转局面);
  • \(N = K*(a+b)+R_2\) (其中 \(K \in \mathbb{N}^+,a \le R_2 \le b\) ) 时,先手必胜;
  • \(N = K*(a+b)+R_3\) (其中 \(K \in \mathbb{N}^+,b < R_3 < a + b\) ) 时,先手必胜(这些数量不够再取一次,后手无法逆转局面);

变体

  1. 需要取完全部的石头

    \(N\) 颗石子,两名玩家轮流行动,按以下规则取石子:。

    规定:每人每次可以取走 \(X(a \le X \le b)\) 个石子,如果最后剩余物品的数量小于 \(a\) 个,则可以一次性取完,拿到最后一颗石子的一方获胜。

    双方均采用最优策略,询问谁会获胜。

    对传统扩展巴什博弈的结论做一下修改即可得到结论:

    • \(N = K*(a+b)\) 时,后手必胜;
    • \(N = K*(a+b)+R_1\) (其中 \(K \in \mathbb{N}^+,0 < R_1 < a\) ) 时,先手必胜(这些数量刚好够再取一次);
    • \(N = K*(a+b)+R_2\) (其中 \(K \in \mathbb{N}^+,a \le R_2 \le b\) ) 时,先手必胜;
    • \(N = K*(a+b)+R_3\) (其中 \(K \in \mathbb{N}^+,b < R_3 < a + b\) ) 时,后手必胜(这些数量刚好够再取一次);

参考博文1 - CSDN - ACM 数论篇——博弈论(注:这篇文章里面错误的地方挺多的)。






解题工具:\(\tt{}PN\)

最核心思路:必胜态( \(\tt{}N\) )和必败态 ( \(\tt{}P\) )的相互转化

  • 任何可以转化为必败态的状态都是必胜态;
  • 任何只能转化为必胜态的状态都是必败态。

例题

题意:给出一个 \(N*M\) 的棋盘,右上角有个石子。两名玩家轮流行动,每次可以将石子向下、左、左下任一方向移动一格。当一名玩家无法再移动石子时他就输了。求解输赢情况。

思路:本题有一个很显然的必败态——左下角。以此为突破口进行逆推即可得解。

截图 截图 截图 截图 截图

至此,我们可以找到规律:横纵坐标同时为奇数时必败,反之必胜。参考博文1参考博文2






\(\tt{}NIM\) 游戏

问题模板

\(N\) 堆石子,给出每一堆的石子数量,两名玩家轮流行动,按以下规则取石子:

规定:每人每次任选一堆,取走正整数颗石子,拿到最后一颗石子的一方获胜(注:几个特点是不能跨堆不能不拿)。

双方均采用最优策略,询问谁会获胜。

最核心思路:逆向推导 + 镜像局面

个人感觉诀窍在于能否建立一个镜像局面,然后自己位居“被动”局面,使得无论对手作何操作,你只需要照着他的操作去做即可,最先耗死的一定是对手。

一般的解法是从结局状态出发,通过必胜态( \(\tt{}N、W\) )和必败态 ( \(\tt{}P、L\) )的相互转化得到猜想,然后再证明猜想成立。

结论

记初始时各堆石子的数量 \((A_1,A_2, … ,A_n)\) ,定义尼姆和 \(Sum_N = A_1 \bigoplus A_2 \bigoplus … \bigoplus A_n\)

\(\pmb{ Sum_N = 0 }\) 时先手必败,反之先手必胜。

由最终态(每堆石子数量均为 \(0\) ,此时 \(Sum_N = 0\) ,必败)逆向推导得到。

证明

点击查看证明

为了证明,首先引入一个定义:

  • 尼姆和:\(Sum_N=A_1 \bigoplus A_2 \bigoplus … \bigoplus A_n\)

再引入两条定理:

  • 若当下 \(Sum_N = 0\)无论怎么操作必定会有 \(Sum_N' \neq 0\)
  • 若当下 \(Sum_N \neq 0\)存在一种取法使得 \(Sum_N' = 0\)

使用反证法证明第一条定理:

假设 \(Sum_N' = 0\) 成立。

约定现在从第 \(i\) 堆石子中取走若干颗,定义第 \(i\) 堆石子当前数量为 \(A_i'\)

\(Sum_N'\) 与原式左右异或,得 \(Sum_N \bigoplus Sum_N'\) 为:

\[\\(A_1 \bigoplus A_1) \bigoplus (A_2 \bigoplus A_2) \bigoplus … \bigoplus (A_i \bigoplus \pmb A_i') \bigoplus (A_n \bigoplus A_n)=0\bigoplus 0 \]

\[0 \bigoplus 0 \bigoplus … \bigoplus (A_i \bigoplus \pmb A_i') \bigoplus 0 = 0 \]

我们发现,当且仅当 \(A_i=A_i'\) 时上式成立,而这与题意矛盾,故假设不成立,即无论怎么操作,必定会得到 \(Sum_N' \neq 0\)

下不严格证明第二条定理(与变体1一致):

约定 \(Sum_N=x \ne 0\) ,现在从第 \(i\) 堆石子中取走若干颗石头,使得第 \(i\) 堆石头剩下 \(A_i \bigoplus x\) 颗(需要满足 \(A_i \bigoplus x < A_i\) )。

那么此时 \(Sum_N'\) 成为:

\[A_1 \bigoplus A_2 \bigoplus … \bigoplus \pmb{(A_i \bigoplus x)} \bigoplus … \bigoplus A_n = Sum_N \bigoplus x = x \bigoplus x = 0 \]

即为所求。而上面这种取法需要满足的条件只有一个,即存在一个 \(A_i\) 使得 \(A_i \bigoplus x < A_i\) ,我们由 \(x\) 的定义可知,这样的 \(A_i\) 一定存在。所以,一定存在取法使得 \(Sum_N' = 0\)

当取完全部石子时, \(0 \bigoplus 0 \bigoplus … \bigoplus 0 = 0\)

由于题目规定每次至少取一个石子,那么场上的石子数量最终肯定会被耗尽。所以,只要先手操作前,场上 \(Sum_N=0\) (必输),那么按照上面的定理,无论TA怎么操作一定会将一个 \(Sum_N' \ne 0\) (必胜)的局面给对手,而后手一定存在一种取法使得 \(Sum_N''=0\) (必输)的局面给先手,这样循环下去,先手永远翻不了盘(拿到的都是必输局面),后手只要等待石头取光,就能达成胜利。

变体

  1. 具体取法

    这一变体实际上考查的是尼姆博弈的证明,题目背景为:

    给出一局先手必胜的尼姆博弈,问:先手应该如何取石子才能保证仍是先手必胜。

    结论如下:

    先计算出尼姆和,再对每一堆石子计算 \(A_i \bigoplus Sum_N\) ,记为 \(X_i\)

    若得到的值 \(X_i<A_i\)\(X_i\) 即为一个可行解,即剩下 \(\pmb X_i\) 颗石头,取走 \(\pmb {A_i - X_i}\) 颗石头(这里取小于号是因为至少要取走 \(1\) 颗石子)。

    这一题的推导过程与尼姆博弈的一样,都是需要根据最终态(先手第一回合取完后要构造必败态,即 \(Sum_N = 0\) )逆向推导,如下:

    证明(非严格)

    剩下 \(X_i\) 颗石头,那么新的尼姆和即为

    \[Sum_N' = A_1 \bigoplus A_2 \bigoplus … \bigoplus A_{i - 1} \bigoplus \pmb{X_i} \bigoplus A_{i + 1} \bigoplus … \bigoplus A_n \]

    等价于

    \[Sum_N' = A_1 \bigoplus A_2 \bigoplus … \bigoplus A_{i - 1} \bigoplus \pmb{A_i \bigoplus Sum_N} \bigoplus A_{i + 1} \bigoplus … \bigoplus A_n \]

    \(Sum_N\) 代回,得到 \(Sum_N'=0\)

  2. 带限定的 \(\tt{} NIM\) 游戏

    题目背景为:

    给定一个集合 \(T=\{t_1,t_2,… | t_i \in \mathbb{N}^+\}\),有 \(N\) 堆石子,给出每一堆的石子数量,,两名玩家轮流行动,按以下规则取石子:

    规定:每人每次任选一堆,取走 \(X \in T\) 颗石子,拿到最后一颗石子的一方获胜。

    双方均采用最优策略,询问谁会获胜。

    解题方法请参见本文《SG函数》。






\(\tt{} Moore’s\ Nim\) 游戏(\(\tt{}Nim - K\) 游戏)

问题模板

\(N\) 堆石子,给出每一堆的石子数量,两名玩家轮流行动,按以下规则取石子:

规定:每人每次任选不超过 \(K\) 堆,对每堆都取走不同的正整数颗石子,拿到最后一颗石子的一方获胜。

双方均采用最优策略,询问谁会获胜。

结论

把每一堆石子的石子数用二进制表示,定义 \(One_i\) 为二进制第 \(i\) 位上 \(1\) 的个数。

以下局面先手必胜:

对于每一位, \(\pmb{One_1,One_2,… ,One_N}\) 均不为 \(\pmb{K+1}\) 的倍数。

证明

点击查看证明 - 数学归纳法
  1. 必败态:石子全部为 \(0\) ,没有石子可取。

  2. 任何一个必败态,经过一次操作后必定转变为必胜态。显然的,一次操作至多只能改变 \(K\) 堆石子的数量,即使得某一位 \(One_i\) 变为 \(One_i' \in [One_i - K, One_i + K]\) ,此时, \(One_i'\) 必然不可能是 \(k+1\) 的整数倍,故必定转变为必胜态。

  3. 任何一个必胜态,总有一种方式使得经过一次操作后转变为必败态。这里使用数学归纳法的思想会比较好理解(我查询了几乎大半个中文互联网的资料,可能是我理解力太弱了,看了很久别人的博客都没明白这里是怎么证明的,终于,在我自己理解之后,我发现用数规解释比较直观,所以在这里我就用不严格的数规证明一下)。

    我们从高到低考虑每一个二进制位,假设\(N=i\) 时命题成立,即我们挑选了 \(j(j \le K)\) 堆石子取石子,使得第 \(1\) 位到第 \(i\) 位上 \('1'\) 的数量全部为 \(K+1\) 的倍数。

    现在证明对于 \(N=i+1\) 时命题也成立。由于已经挑选了 \(j\) 堆石子,所以这 \(j\) 堆石子的第 \(i+1\) 位是不固定的,假设此时还有 \(t\) 堆石子的这一位是 \('1'\) ,分类讨论:

    • \(t+j \le K\) ,那么只需要将这一位全部的 \('1'\) 置为 \('0'\) 即可;
    • \(t+j > K\) ,那么我们只需要将 \(j\) 堆已选石子中的 \(K+1-t\) 堆的这一位置为 \('0'\)

    综上,可证命题恒成立。

点击查看证明 - 基于传统 $\tt{} Nim$ 游戏

考虑传统 \(\tt{}Nim\) 游戏的结论:对石头的数量取异或。异或本质上是二进制异或,而本题即考虑为 \(K+1\) 进制异或。

参考博文1(注:仅借鉴了结论,这篇文章里面的证明应该是有很多漏洞的)。






\(\tt{}Anti-Nim\) 游戏(反 \(\tt{}Nim\) 游戏)

问题模板

\(N\) 堆石子,给出每一堆的石子数量,两名玩家轮流行动,按以下规则取石子:

规定:每人每次任选一堆,取走正整数颗石子,拿到最后一颗石子的一方出局

双方均采用最优策略,询问谁会获胜。

最核心思路:分类讨论

结论

以下局面先手必胜:

  • 所有堆的石头数量均不超过 \(1\) ,且 \(\pmb {Sum_N=0}\) (也可看作“且有偶数堆”);
  • 至少有一堆的石头数量大于 \(1\) ,且 \(\pmb{Sum_N \neq 0}\)

证明

点击查看证明
  • (1)所有堆的石子数量均为 \(1\) ,与奇偶有关:

    • (1.1)共奇数堆石子,先手必败;
    • (1.2)共偶数堆石子,先手必胜;
  • (2)仅有一堆的石子数量大于 \(1\)

    • (2.1)共奇数堆石子,先手将大于 \(1\) 的那堆石子取到 \(1\) 颗,此时转化为(1.1),先手必胜;
    • (2.2)共偶数堆石子,先手将大于 \(1\) 的那堆石子取完,此时转化为(1.1),先手必胜;
  • (3)有两堆及以上的石子数量大于 \(1\)

我们利用传统 \(\tt{}Nim\) 博弈的结论对于上述分类讨论的第(3)点进行更进一步的分类讨论。

若当下 \(Sum_N=0\) ,无论怎么操作必定会有 \(Sum_N'\neq 0\)

若当下 \(Sum_N\neq 0\) ,存在一种取法使得 \(Sum_N'=0\)

(3.1)当 \(Sum_N=0\) 时进行操作,只可能得到两种结果:

  • 依然有两堆及以上的石子数量大于 \(1\) ,此时 \(Num_N\neq 0\)

  • 仅有一堆的石子数量大于 \(1\) ,此时转化为(2),先手必胜。

(3.2)当 \(Sum_N\neq 0\) 时进行操作,存在以下的结果:

  • 依然有两堆及以上的石子数量大于 \(1\) ,且 \(Num_N = 0\) ,转化为(3.1);

  • 依然有两堆及以上的石子数量大于 \(1\) ,且 \(Num_N \neq 0\) ,转化为(3.2);

  • 仅有一堆的石子数量大于 \(1\) ,此时转化为(2),先手必胜。

任何一个必败态,经过一次操作后必定转变为必胜态。我们发现,对于必败态(3.1)这一条件成立。

任何一个必胜态,总有一种方式使得经过一次操作后转变为必败态。我们发现,对于必胜态(3.2),一定存在一种方式转变为(3.1),这一条件也成立。

综上,可证命题恒成立






阶梯 - \(\tt{}NIM\) 博弈

问题模板

\(N\) 级台阶,每一级台阶上均有一定数量的石子,给出每一级石子的数量,两名玩家轮流行动,按以下规则操作石子:

规定:每人每次任选一级台阶,拿走正整数颗石子放到下一级台阶中,已经拿到地面上的石子不能再拿,拿到最后一颗石子的一方获胜。

双方均采用最优策略,询问谁会获胜。

——来自AcWing892 - 台阶-Nim游戏

最核心思路:镜像局面

结论

以下局面先手必胜:

对奇数台阶做传统 \(\pmb{\tt{}Nim}\) 博弈,当 \(\pmb{Sum_N=0}\) 时先手必败,反之先手必胜。

证明

点击查看证明

首先分析这一问题与传统的 \(\tt{}NIM\) 博弈的区别,在于传统 \(\tt{}NIM\) 是直接将石子拿走,故不会对其他堆的石子数量造成影响,而这一问题会改变其他堆的石子数量。

然而,真的是这样吗?其实不然,我们有方法把这个影响归零。

假设对手移动了偶数台阶上的 \(K\) 个石子,那么你只需要将这 \(K\) 个石子再移动一次,那么这 \(K\) 个石子又会回到偶数台阶上,这样,奇数台阶上的石子数量不会发生改变(相当于无效回合)。以此往复,可以保证你将所有偶数台阶上的石子亲手移动到地面上,所以,移动偶数台阶上的石子是没有意义的,答案只与奇数台阶上的石子数量挂钩。

再次分析这一问题与传统 \(\tt{}NIM\) 博弈的区别,我们发现,由于偶数台阶可以被忽视,移动奇数台阶上的石子(到偶数台阶上)就相当于直接拿走。至此,本题转化为奇数台阶的 \(\tt{}NIM\) 博弈。

变体

POJ1704 - Georgia and Bob

题意:

长条的盒子划分为 \(N\) 个格子,一些格子里有石子。两名玩家轮流行动,每次可以任选一个石子,将其向左移动,但不允许反超其他棋子,且一个格子只能放置一个石子。没有石子可以移动时就算输。

思路 - 1(我自己解题用的是这种):

石子间的空格就相当于是引例中台阶上的石子数量,两题等价。

思路 - 2:

网络上更多人的思路是这一种。

将石子从后往前,两两捆绑为一组。一旦对手移动某一组的前一个石子,那么你只需要将后一个石子移动相同的格子,这一组石子之间的距离便不会被改变(相当于无效回合)。所以,组所在的位置是没有意义的,答案只与组内石子的距离挂钩。

一旦对手移动某一组的后一个石子,那么就相当于在做传统的 \(\tt{}NIM\) 博弈。故我们只需要考虑每一组两个石子之间的距离即可。

HDU4315 - Climbing the Hill

题意:

一座山划分为 \(N\) 个高度,一些高度上有人。两名玩家轮流行动,每次可以任选一个人,将其向上移动,但不允许反超其他人,且除了山顶外,同一高度只有一个人。将其中一个人定义为国王,将国王移动到山顶就算赢。

思路:

我们先不考虑国王这一条件,可以发现,虽然山顶可以有多个人,但这只影响了组所在的位置,这一条件是为了“国王”而存在的,所以这一问题与上一题并没有区别。

现在考虑国王这一条件,有情况需要特判:

  • 当国王在第一个时,先手必胜;
  • 当国王在第二个,且总人数为奇数时,显然,第一个人不能被率先移到山顶,所以要进行特判【?这里其实不懂】。
  • 其余情况等同于无国王情况。

参考博文1参考博文2参考博文3参考博文4






解题工具:\(\tt SG\) 游戏(有向图游戏)

任何博弈问题都可以转化为有向图游戏。简单来说,即是将一局博弈的每一种局面全部罗列下来,然后暴力求解,得到规律;形象来说,我们把一个局面抽象成一个点,在起点有一颗棋子,两个人选取最优策略轮流对这颗棋子进行移动,最后不能移动棋子的人失败,这样可以得到一张有向图。我们使用以下几条规则来定义暴力求解的过程:

  • 使用数字来表示输赢情况,\(0\) 代表局面必败,非 \(0\) 代表存在必胜可能,我们称这个数字为这个局面的SG值;
  • 找到最终态,根据题意人为定义最终态的输赢情况;
  • 对于非最终态的某个节点,其SG值为所有子节点的SG值取 \(\tt{}mex\)
  • 单个游戏的输赢态即对应根节点的SG值是否为 \(0\) ,为 \(0\) 代表先手必败,非 \(0\) 代表先手必胜;
  • 多个游戏的总SG值为单个游戏SG值的异或和。

其中,在学习时我提出的一个问题是,为什么要使用 \(\tt{}mex\) 函数代替 \(0,1\) ,在重构本文时我有了进一步的理解,即这是求解为了多个游戏的总SG值服务,如果只是单个游戏的话,直接使用 \(0,1\) 也可以。

下面使用两个具体例题对这一知识点的使用做讲解。

例题

  1. 经典 \(\tt {}NIM\) 游戏

    我们使用SG函数与有向图游戏验证结论的正确性,题目背景为:

    有三堆石子,数量分别为 \(3,2,5\) ,两名玩家轮流行动,按以下规则取石子:

    规定:每人每次任选一堆,取走正整数颗石子,拿到最后一颗石子的一方获胜。

    双方均采用最优策略,询问谁会获胜。

    建立有向图如下:

    随后,对于所有的最终态,我们人为的标定其SG值:

    最后,填充所有节点的SG值:

    我们发现,这道题的总SG值即为 \(3 \bigoplus 6 \bigoplus 2\) ,即上方尼姆博弈的结论。

  2. 带限定的 \(\tt{} NIM\) 游戏

    题目背景为:

    有两堆石子,数量分别为 \(10,5\) ,两名玩家轮流行动,按以下规则取石子:

    规定:每人每次任选一堆,取走 \(2\) 颗或 \(5\) 颗石头,拿到最后一颗石子的一方获胜。

    双方均采用最优策略,询问谁会获胜。

    建立有向图如下:

    3773cfe000ea3f1536ed049a29d3a26.jpg

    我们发现,总SG值即为 \(1 \bigoplus 2=3\) ,本题先手必胜。

代码模板

int n, m, a[N], num[N];
int sg(int x) {
    if (num[x] != -1) return num[x];
    
    unordered_set<int> S;
    for (int i = 1; i <= m; ++ i) 
        if(x >= a[i]) 
            S.insert(sg(x - a[i]));
    
    for (int i = 0; ; ++ i)
        if (S.count(i) == 0)
            return num[x] = i;
}
void Solve() {
    cin >> m;
    for (int i = 1; i <= m; ++ i) cin >> a[i];
    cin >> n;
    
    int ans = 0; memset(num, -1, sizeof num);
    for (int i = 1; i <= n; ++ i) {
        int x; cin >> x;
        ans ^= sg(x);
    }
    
    if (ans == 0) no;
    else yes;
}






\(\tt Anti-SG\) 游戏(反 \(\tt SG\) 游戏)

\(\tt SG\) 游戏中最先不能行动的一方获胜。

结论

以下局面先手必胜:

  • 单局游戏的SG值均不超过 \(\pmb 1\) ,且总SG值为 \(\pmb 0\)

  • 至少有一局单局游戏的SG值大于 \(\pmb 1\) ,且总SG值不为 \(\pmb 0\)

在本质上,这与 \(\tt Anti-Nim\) 游戏的结论一致。






\(\tt{}Lasker’s-Nim\) 游戏(\(\tt Multi-SG\) 游戏)

问题模板

\(N\) 堆石子,给出每一堆的石子数量,两名玩家轮流行动,每人每次任选以下规定的一种操作石子:

  • 任选一堆,取走正整数颗石子;
  • 任选数量大于 \(2\) 的一堆,分成两堆非空石子。

拿到最后一颗石子的一方获胜。双方均采用最优策略,询问谁会获胜。

最核心思路:SG函数

结论

本题使用SG函数求解,SG值定义为:

\[\pmb{ SG(x) = \begin{cases} x-1 & \text{ , } x\mod 4= 0\\ x & \text{ , } x \mod 4 = 1\\ x & \text{ , } x \mod 4 = 2\\ x+1 & \text{ , } x \mod 4 = 3 \end{cases}}\]

证明

点击查看证明

根据SG函数(一局游戏的SG值等于其所有子游戏SG值的异或和)的定义我们不难得到本题结论






\(\tt{}Every-SG\) 游戏

问题模板

给出一个有向无环图,其中 \(K\) 个顶点上放置了石子,两名玩家轮流行动,按以下规则操作石子:

移动图上所有还能够移动的石子;

无法移动石子的一方出局。双方均采用最优策略,询问谁会获胜。

最核心思路:SG函数

结论

定义 \(step\) 为某一局游戏至多需要经过的回合数。

以下局面先手必胜:

\(\pmb{step}\)为奇数

证明

点击查看证明






斐波那契博弈

问题模板

有一堆石子,数量为 \(N\) ,两名玩家轮流行动,按以下规则取石子:

先手第1次可以取任意多颗,但不能全部取完,此后每人取的石子数不能超过上个人的两倍,拿到最后一颗石子的一方获胜。

双方均采用最优策略,询问谁会获胜。

结论

当且仅当 \(N\) 为斐波那契数时先手必败。

代码模板

int fib[100] = {1, 2};
map<int, bool> mp;
void Force() {
  for (int i = 2; i <= 86; ++ i) fib[i] = fib[i - 1] + fib[i - 2];
    for (int i = 0; i <= 86; ++ i) mp[fib[i]] = 1;
}
void Solve() {
    int n; cin >> n;
    if (mp[n] == 1) cout << "lose\n";
    else cout << "win\n";
}






威佐夫博弈

问题模板

有两堆石子,给出每一堆的石子数量,两名玩家轮流行动,每人每次任选以下规定的一种操作石子:

  • 任选一堆,取走正整数颗石子;
  • 从两队中同时取走正整数颗石子。

拿到最后一颗石子的一方获胜。双方均采用最优策略,询问谁会获胜。

结论

以下局面先手必败:

\(\pmb{ (1, 2), (3, 5), (4, 7), (6, 10), …}\) 具体而言,每一对的第一个数为此前没出现过的最小整数,第二个数为第一个数加上 \(\pmb{1,2,3,4,…}\)

更一般地,对于第 \(\pmb k\) 对数,第一个数为 \(\pmb {First_k= \left \lfloor \frac{k*(1+\sqrt 5)}{2} \right \rfloor}\) ,第二个数为 \(\pmb{Second_k=First_k+k}\)

其中,在两堆石子的数量均大于 \(10^9\) 次时,由于需要使用高精度计算,我们需要人为定义 \(\frac{1+\sqrt 5}{2}\) 的取值为 \(lorry = 1.618033988749894848204586834\)

代码模板

const double lorry = (sqrt(5.0) + 1.0) / 2.0;
//const double lorry = 1.618033988749894848204586834;
void Solve() {
    int n, m; cin >> n >> m;
    if (n < m) swap(n, m);
    double x = n - m;
    if ((int)(lorry * x) == m) cout << "lose\n";
    else cout << "win\n";
}






树上删边游戏

问题模板

给出一棵 \(N\) 个节点的有根树,两名玩家轮流行动,按以下规则操作:

选择任意一棵子树并删除(删去任意一条边,不与根相连的部分会同步被删去);

删掉最后一棵子树的一方获胜。双方均采用最优策略,询问谁会获胜。

最核心思路:SG函数

结论

相较于传统SG值的定义,本题的SG函数值定义为:

  • 叶子节点的SG值为 \(\pmb 0\)
  • 非叶子节点的SG值为其所有孩子节点SG值 \(\pmb + 1\) 的异或和。

证明

点击查看证明






无向图删边游戏

问题模板

给出一张 \(N\) 个节点的无向联通图,有一个点作为图的根,两名玩家轮流行动,按以下规则操作:

选择任意一条边删除,不与根相连的部分会同步被删去;

删掉最后一条边的一方获胜。双方均采用最优策略,询问谁会获胜。

最核心思路:转化为“树上删边游戏”+SG函数

结论

这个模型有一个著名的结论——Fusion Principle定理。描述如下:

我们根据下方的结论对无向图进行等价转换:

  • 对于奇环,我们将其缩成一个新点+一条新边;
  • 对于偶环,我们将其缩成一个新点;
  • 所有连接到原来环上的边全部与新点相连。

此时,本模型转化为“树上删边游戏”。

这个原理的证明非常复杂,只需记结论即可。

代码模板






题单与部分题解

该部分内容较多,我在 Vjudge 上建立了详细的题单,这里不再重复罗列。

P3150 pb的游戏(1):奇偶博弈

题意:一个数字 \(K\) 。两名玩家轮流行动,一名玩家将给出的数分割成两个非零自然数,之后由另一名玩家选择留下两个数中的其中一个;另一名玩家继续将自己留下的数字分割成两个非零自然数。当一名玩家无法对数继续分割的时他就输了。

思路:如果能保证每次自己操作时数字都为偶数,并且能做局给对手(当剩余的数字为 \(1\) 时就输了,故奇数必输),则必赢。根据镜像局面,相似的可以推出两条定理:

  • 若当下 \(K\) 为偶数,一定存在操作使得其变成奇数(给下一个人挖坑);
  • 若当下 \(K\) 为奇数,再一次操作后必定会分得一个偶数。

P4702 取石子:奇偶博弈

题意:有 \(N\) 堆石子,给出每一堆的石子数量,保证从小到大排列,两名玩家轮流行动,按以下规则取石子——每人每次任选一堆满足 \(a_i > a_{i-1}\) 的堆(\(a_0\) 视作 \(0\) ),取走 \(1\) 颗石子,拿到最后一颗石子的一方获胜。双方均采用最优策略,询问谁会获胜。

思路:我们发现,所有的石子最终一定会被全部取完,所以只需要判断总石子数的奇偶性即可。

P4136 谁能赢呢?:奇偶博弈

题意:有一个 \(N*N\) 的棋盘,左上角有个石子。两名玩家轮流行动,按以下规则移动石子——每人每次可以将石子向上下左右任一方向移动一格,但目标格此前不能被走过,无法再移动石子的玩家出局。双方均采用最优策略,询问谁会获胜。

思路:由于本题采用最优策略,所以棋盘的每一个格子都会被走过(这个非常难想,我参考了大量题解还是一知半解,画了很多图才隐隐归纳出规律,但是无法用语言描述,这里还是先留一个坑吧),所以只需要判断总格子数的奇偶性即可。

HDU1849 - Rabbit and Grass:NIM游戏略微变形

板子题。双方轮流将任意棋子向左移动任意格,即选择任意堆取走任意多石子。复杂度 \(\mathcal O(M)\)

HDU1730 - Northcott Game:NIM游戏变形

需要稍微进行变形。归纳结论,有以下几条显然:

  • 双方肯定是想办法压缩对方生存空间(例如图2样例,假设其为初始局面,那么黑子一定会选择向右移动若干格,而非向左);
  • 当黑白子相邻时,黑子必败(镜像局面,黑子走一步,白子依旧走到黑子相邻位置);

而当黑白子不相邻时,黑子作为先手可以向白子方向移动任意格,套用到 \(\tt NIM\) 游戏的情况——每一行黑白子间距为”每一堆石子的数量“,黑子移动若干格为“从石堆中取走若干颗石子”。

复杂度 \(\mathcal O(N)\)

HDU2999 - Stone Game, Why are you always there?:SG函数

板子题,题目非常难读懂,WA了好几发;且卡了 vector 的去重函数,T了一发;中途在测试的时候还不小心把 get 数组写到 sg 函数外面去了,导致一直跑不对正确答案,查了好久……

附一下题目意思与解释(在这里参考了这一篇博客后才懂得题目是什么意思,翻阅全网其他的博客感觉都写得有所欠缺……):

\(N\) 个石子排成一排,给出一个正整数集合 \(S\) ,两名玩家轮流行动,按以下规则顺序操作:

  • 任选 \(X(X\in S)\) 个连续石子并取走;
  • 取走石子后,剩余的石子分为独立的两段(比如将第 \(2\) 个石子取走,\(1\)\(3\) 还是视作不相邻的);

双方均采用最优策略,询问谁会获胜。

HDU3980 - Paint Chain:Multi-SG函数

\(\tt Multi-SG\) 简单题,但是伪装的很不简单(对于我来说),刚拿到题想当然的以为是巴什博弈,结果发现样例过不了,随后发现需要取连续段,想到 \(SG\) 函数,然后又想当然的写了个基于搜索和特判的Wida式 \(SG\) 函数(因为我一直是用“搜索”或者“记忆化搜索”来理解 \(SG\) 的,所以我习惯于写搜索了),结果发现复杂度是错的,然后我又开始找规律(之前打多校的时候遇到过好多道博弈论,我都是用搜索+找规律来解的,但是解的非常慢,我一直以为是我题目做的太少),由于开了编译器计时器,一个小时过后我发现规律依旧没办法总结,于是百度。

第一篇题解就是用 \(\tt Multi-SG\) 解的,我一看到这个思路就意识到自己的理解出现偏差了,用新思路很快便得解了。

复盘整个思路,出现问题的原因大致有两个,其一是我习惯于原来自己的那套解 \(SG\) 函数的方式了,但是其实是不够好的(因为确实能解一定的题,但是非常花时间),而且这道题的搜索写错了;其二是因为我在笔算举例时算错了 \(N=8,M=2\) 这个样例的解,应当是后手胜,但是我一直考虑的是先手胜,导致花费了非常多的时间。

个人反思,为什么做了这么久

这道题我原来用的搜索思路是,在第一个人涂完颜色之后,第二个人开始涂的格子一定是与第一个人涂的最后一个格子相差 \([0, M-1]\) ,从局部最优分析来看这确实是可以推导出来的,但是正如我总结的,这并不是全局最优—— \(N=8,M=2\) 这个样例,第二个人开始涂的格子与第一个人涂的最后一个格子相差 \(M\) ,这样才是全局最优。

在今年的多校中也有一道类似的题目,我也是遵从上方这个思路去暴力求解的(那个时候还没有开启第二轮博弈论学习,所以没有妄图和这道题一样写搜索),捣鼓了很久都是假做法,但是那个时候队友及时的更换思路补上了这个漏洞,使得最终得解,在此之前我一直以为只是那时我脑子一时秀逗,想漏了情况,而从今天这道题的错误来看,是因为我一直以来对于 \(SG\) 函数的理解出现了偏差,前人总结的规律是非常有普适性的(这道题和多校那一道均可以通过 \(\tt Multi-SG\) 快速求解,根本不需要搜索或者暴力)。

小有明悟,特此记录一下。

HDU1404 - Digital Deletions:SG函数

给定一个包含 \(0-9\) 的字符串 \(S\) ,两名玩家轮流行动,从以下规则中任选一条操作:

  • 选中任意一个数字,将其变小成一个正整数;
  • 选中任意一个 \(0\) ,删掉其与其之后的全部内容;

删掉最后一位的一方获胜。双方均采用最优策略,询问先手必胜与否。

睿频环节

这是一道数据结构+模拟+博弈论的好题,之所以涉及到一个数据结构,在于本题需要进行字符串、数值类型的转换,如果是知道很多内置函数与数据结构但是并不懂得原理与细节的假高手(比如我),就有可能因为方便、简短但是运行速度较慢的高级数据结构(相较于传统模拟算法、数组)而出现各种各样的奇怪错误。

分析

首先应当注意到的是这一道题是单独游戏,不应当使用SG定理划分子游戏这一结论可以帮助我们节约大量计算时间(然而一开始我并没有意识到这一点)。

随后归纳结论,有以下几条显然:

  • 当第一个数字为 \(0\) 时必胜;
  • 当只有一个数字且为 \(1\) 时必败。

思路1:暴力DFS+SG函数记录(T)

这个思路在施行前并没有详细分析过时间复杂度(搜索的时间复杂度不太容易分析),大体上是遵循:“读入一个数字,使用标准SG函数模板枚举每一种子状态计算SG值”。

预期时间复杂度:\(\mathcal O(Q*N)\) ,对于每一个询问,枚举至多 \(N\) 种子状态,妥妥超时。

点击查看代码
unordered_map<string, int> mp;
int dfs(string s) {
    if (mp.count(s) != 0) return mp[s];
    if (s.sz == 0) return 0;
    
    unordered_set<int> S;
    int len = s.sz - 1;
    FOR (i, 0, len) {
        FOR (j, 1, s[i] - '0') {
            string t = s.substr(0, i) + (char)(s[i] - j) + s.substr(i + 1);
            S.insert(dfs(t));
            // _(s, "(", t, ")", dfs(t));
        }
        if (s[i] == '0') {
            string t = s.substr(0, i);
            S.insert(dfs(t));
            // _(s, "(", t, ")", dfs(t));
        }
    }
    
    for (int i = 0; ; ++ i)
        if (S.count(i) == 0)
            return mp[s] = i;
}

void Force() {
    // FOR (t, 0, 9)
    // FOR (j, 0, 9)
    // FOR (i, 0, 9) cout << t << j << i << endl;
    string s; 
    while (cin >> s) {
        if (s[0] == '0') Yes; //剪枝
        else if (dfs(s) == 0) No;
        else Yes;
    }
}

思路2:逆向暴力递推打表+SG函数记录

我们发现,从一个必败态的序列+任意一次操作,可以得到一个必胜态的序列,由此,我们可以构建一个 \(10^6\) 的表,通过类似于欧拉筛的方式实现。预期时间复杂度:\(\mathcal O(N+Q)\)

这里需要注意的是,如果使用 map 进行存储,会导致TLE,如果使用 unordered_map 进行存储,会MLE(由于我是从思路1转换过来的,以string为key值进行存储,所以一开始没有使用常规数组)。

除此之外,如果直接对于字符串进行操作(众所周知,字符串有着无与伦比方便的内置函数),使用多个内置函数会很悲伤的导致TLE,还是建议使用数值进行计算(代码长度+++)。

点击查看代码 | 全数据本地耗时560ms
int mp[N];
int pre[] = {1, 10, 100, 1000, 10000, 100000};
void dfs(int n) {
    mp[n] = 1;
    int len = upper_bound(pre, pre + 6, n) - pre;
    if (len >= 6) return;
    
    FOR (i, 0, 9) dfs(n * 10 + i);
}
void extra(int n) { //拓展:必败态+一次操作=必胜态
    int len = upper_bound(pre, pre + 6, n) - pre;
    FOR (i, 1, len) {
        int val = n / pre[len - i] % 10;
        FOR (j, val + 1, 9) {
            int ans = n + (j - val) * pre[len - i];
            mp[ans] = 1;
        }
    }
    if (len == 6) return;
    dfs(n * 10);
}
void init() {
    FOR (i, 1, 999999) if (mp[i] == 0) extra(i);
}
void Solve() {
    init();
    string s;
    while (cin >> s) {
        int ans = stoi(s, 0, 10);
        if (s[0] == '0') Yes; //剪枝
        else if (mp[ans] == 0) No;
        else Yes;
    }
}

HDU1536 - S-Nim:SG函数

板子题。由数据范围知直接记忆化搜索,不用找规律,复杂度 \(\mathcal O(m*l*k)\)

需要注意的地方:

  • 本题卡 unordered_set ,请使用数组代替;
  • 清空 \(SG\) 值数组应当是在一组样例之前,而不是每一次询问之前。

HDU1848 - Fibonacci again and again:SG函数

板子题。由数据范围知直接记忆化搜索,不用找规律,记忆化复杂度 \(\mathcal O(1000)\) ,非记忆化可能超时,没试过。

HDU1524 - A Chess Game:SG函数

板子题,结合了基础图论知识。由数据范围知直接记忆化搜索,不用找规律,题目比较粪,范围没标清,理论复杂度 \(\mathcal O(m*(n+\sum_{i=1}^{n} X_i))\)

需要注意的地方:

  • 多组样例,图清空;
  • 本题节点是从 \(0\) 开始计算的,图清空时需要留意。

HDU1729 - Stone Game:SG函数

\(N\) 个盒子,给出每一个盒子的容积与盒子中已经有的石子数量,两名玩家轮流行动,按以下规则操作:

  • 任选一个盒子 \(i\) ,记容积 \(S_i\) 、盒子中已经有的石子数量 \(C_i\) ,放入 \(X\) 颗石子,要求满足 \(1 \le X \le C_i^2\) ,且 \(C_i+X \le S_i\)
  • 不能再放石子的一方输;

双方均采用最优策略,询问先手是否必胜。

睿频环节

样例组数较多。这是一道博弈论+找规律(大量贪心优化)的好题,之所以不涉及到传统的记忆化搜索,是因为理论复杂度为 \(\mathcal O(N*S_i) \approx 5 * 10^7\) ,在杭电这个复杂度不太能够被接受(这个确实离谱,我一开始也想着能不能靠剪枝卡过去,但是显然不能,光荣T一发……),所以需要贪心找规律优化。

分析

归纳结论,只有以下一条显然:

  • 如果某一方能在一回合内填满盒子,那么必胜;

思路

尝试使用数学方式描述这条结论:

记某一轮某一方选择了一个容积 \(s\) 、已有 \(c\) 颗石子的盒子,定义其 \(SG\) 值为 \(sg(s,c)\) 。假设其想要放入 \(q(1 \le q \le c)\) 颗石子,使得 \(q\) 满足 \(\left\{\begin{matrix} q+q^2<s\\ s \le (q+1)+(q+1)^2\end{matrix}\right.\) ,那么当实际放入的石子数 \(X\) 满足 \(q < X\) ,那么必胜。此时的 \(SG\) 值可以 \(\mathcal O(1)\) 计算:由于满足条件的放入方法有放入 \((X+1,X+2,…,s)\) 颗石子,故 \(sg(s,c)=s-X\)

尝试推广结论,可以发现当 \(q=X\) 时必败(因为对手在下一回合一定能放满盒子);当 \(X<q\) 时直接递归查找 \(sg(X,c)\) 【这里的原因我不是特别能说明白,可能是结论吧,先空着】。

至此,我们发现唯一的难处还是在于 \(q\) 的求解,如果直接暴力查找,则复杂度并没有变化,考虑 \(q\) 的推导过程,我们发现可以直接计算 \(\sqrt s\) ,随后小范围进行暴力查找,这样一来,每次查找 \(s\) 变为 \(\sqrt s\) ,至多查找 \(5\)\(s \approx 1\) ,至此预期时间复杂度:\(\mathcal O(N*5)\)

点击查看代码
int num[N], dic[N];
int sg(int s, int c) {
    int q = sqrt(s);
    while (q + q * q >= s) -- q;
    if (c == q) return 0;
    else if (c > q) return s - c;
    else return sg(q, c);
}
int main() {
    ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
    int n, cnt = 0;
    while (cin >> n && n) {
        fill(num, num + n + 1, 0);
        int ans = 0;
        for (int i = 1; i <= n; ++i) {
            int s, c; cin >> s >> c;
            ans ^= sg(s, c);
        }
        cout << "Case " << ++ cnt << ":\n";
        if (ans == 0) cout << "No\n";
        else cout << "Yes\n";
    }
    return 0;
}

HDU2873 - Bomb Game:SG函数

给出一张图,图上有一定数量的炸弹(用 # 标记),两名玩家轮流行动,按以下规则操作:

  • 选择任意一个炸弹,并将其引爆;
  • 被引爆的炸弹会在其所在的行左侧、列上方各产生一个新的炸弹,如果其位于边界,则只生成一个新炸弹;
  • 当两个炸弹位于同一个格子时,立即爆炸;
  • 当炸弹位于左上角格子时,立即爆炸;
  • 引爆最后一颗炸弹的一方获胜;

双方均采用最优策略,询问先手是否必胜。

睿频环节

刚拿到手时比较懵,虽然一眼 \(SG\) 函数,但是如何处理炸弹问题比较头疼,就导致第一眼感觉很难。而实际上这些花里胡哨的内容均与答案无关。

分析

归纳结论,有很多显然,但是只有两条是有用的结论:

  • 如果只在左上角上有且仅有一个炸弹,那么必输;

  • 如果只在第一行(第一列)上有且仅有一个炸弹,那么必胜;

  • 如果只在第一行(第一列)上有且仅有两个炸弹,那么必胜(无用结论);

  • 如果有且仅有三个炸弹且其中两个位于同一行、其中两个位于同一列,那么必胜(无用结论);

思路

当炸弹位于 \((1,i)\)\((i,1)\) 时,显然其 \(SG\) 值为 \(i-1\) ;而对于其他位置的炸弹,先假设他们相互独立,当两个炸弹位于同一个格子时不会立即爆炸,其能够分裂出两个子状态,那么当前状态的 \(SG\) 值即为两个子状态 \(SG\) 值的异或和;再来考虑不相互独立的情况,(这里我并没有搞懂,先留个坑,以后回来补;我翻阅全文应该这篇博客写的是最好的)。

我们发现,这样的思考方式可以使得”立即爆炸“这个条件无效

点击查看代码
const int N = 55;
const int M = N * N;

int num[N][N];
int sg(int x, int y) {
	if (num[x][y] != -1) return num[x][y];
	
	int get[M] = {};
	for (int i = 1; i < y; ++ i) {
		for (int j = 1; j < x; ++ j) {
			get[ sg(x, i) ^ sg(j, y)] = 1;
		}
	}
	
	for (int i = 0; ; ++ i) {
		if (get[i] == 0) return num[x][y] = i;
	}
}
int main() {
	ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
	
	int n, m;
	memset(num, -1, sizeof(num));
	for (int i = 1; i < N; ++ i) {
		num[1][i] = num[i][1] = i - 1;
	}
	while (cin >> n >> m && n && m) {
		int ans = 0;
		for (int i = 1; i <= n; ++ i) {
			for (int j = 1; j <= m; ++ j) {
				char x; cin >> x;
				if (x == '#') ans ^= sg(i, j);
				cout << sg(i, j);
			}
			cout << endl;
		}
		
		if (ans == 0) cout << "Jack\n";
		else cout << "John\n";
	}
	
	return 0;
}
posted @ 2022-08-09 22:43  hh2048  阅读(467)  评论(0编辑  收藏  举报