浅谈公平组合游戏

· · 算法·理论

本文同步发表在博客园。

简单来说就是 Nim 游戏和 SG 函数,但是浅谈,只有一些比较基础的知识喔 qwq。可以用作入门,但并不建议使用本文进行完整学习。

贴一个并非上集,以及一个整体难度更高的优质专栏。大家都可以去看一下,尤其后者,额感觉前者写的太烂了

upd on 2026/6/2:

同样的,也欢迎各位在评论区捉虫、提建议或推荐例题,我会视情况采纳的!

公平组合游戏的定义

公平组合游戏,又称公平组合博弈,是一种满足如下条件的博弈游戏:

  1. 不论当前游戏处于什么状态,所有玩家能做的行动完全一致,仅取决于状态情况,与身份无关
  2. 博弈游戏中,同一个状态不可能多次到达,博弈以玩家不能移动为结束,且游戏一定会在有限步后以非平局状态结束

Nim 游戏

Nim 游戏的规则很简单,如下:

共有 n 堆石子,第 i 堆有 a_i 枚石子,两名玩家轮流取任意一堆中的任意多枚石子,但不能不取,取到最后一枚石子的玩家获胜。

显然,Nim 游戏是一个常规的公平组合游戏。

博弈图的表示和状态的定义

在 Nim 游戏中,局面的变化情况可以用博弈图来描述。

将每个可能的状态看作图中一个节点,如果从某个状态 A 能在经过一次操作后转移到另一个状态 B,那么就让 A 所对应的图中节点向 B 所对应的图中节点连一条有向边,最终即可得到一张有向无环图。这,就是博弈图。

事实上,对于任意公平组合游戏,都可以建出一张有向无环图作为博弈图。

在公平组合游戏中的博弈图里,可以对每个状态标记为必胜状态必败状态。具体的,可以按照以下引理:

  1. 无法移动,即在博弈图中没有出边的状态是必败状态
  2. 对于一个点,若其是必胜状态,当且仅当其后继状态(博弈图中出边)中存在至少一个必败状态
  3. 对于一个点,若其是必败状态,当且仅当其后继状态(博弈图中出边)全部都是必胜状态

证明是显然的。

这样就能在 O(|V| + |E|) 的时间内用拓扑排序的方式得到每个点具体是必胜状态还是必败状态了。

Nim 和

还是继续考虑 Nim 游戏。上一部分简要介绍了「博弈图」这个东西,虽然其已经可以在 O(|V| + |E|) 的时间内得出每个状态的必胜或必败情况了,但当 Nim 游戏的 n 过大或 a_i 过大时,图就建不出来了(存不下),更别谈什么遍历求解了。所以,我们需要更厉害的武器。

有一个结论,就是说,一个 Nim 游戏的状态是否为先手必胜状态,只与当前局面的石子数目的「Nim 和」有关。

对,这里引入了一个新词:Nim 和。对于一个有 n 堆石子、第 i 堆石子的个数为 a_i 的 Nim 游戏状态,它的 Nim 和就是 a_1 \oplus a_2 \oplus \dots \oplus a_n。其实就是所有石子数量的异或和

知道 Nim 和这个概念以后,Nim 游戏的必胜必败情况也就能很快的算出来了,因为有一个特别厉害的定理:

Nim 游戏中,对于一个有 n 堆石子、第 i 堆石子的个数为 a_i 的 Nim 游戏状态,当且仅当它的 Nim 和 a_1 \oplus a_2 \oplus \dots \oplus a_n = 0 时该状态为必败状态,否则均为必胜状态

这样就可以在 O(n) 的时间复杂度下判断一个 Nim 游戏的状态是必胜还是必败了。但是光知道结论是没用的啊,我们需要知道原理。那么,上证明!

考虑对所有可能出现的状态用归纳法。具体的:

  1. 对于状态 a_i = 0 (1 \le i \le n),其没有后继状态,是必败状态且 Nim 和为 0,命题成立。

  2. 如果 k = a_1 \oplus a_2 \oplus \dots \oplus a_n \not= 0,需证明该状态为必胜状态,等价于需要构造一个合法的移动方案使得后继状态为必败状态;由归纳假设,只需证明后继状态满足 a^{\prime}_1 \oplus a^{\prime}_2 \oplus \dots \oplus a^{\prime}_n = 0。利用异或的性质,这等价于说,需要存在一堆石子 a_i 使 a_i > a_i \oplus k。设 k 的二进制表示中最高位的 1 是第 pos 位,那么由异或的性质,一定存在一个 a_i 使其第 pos 为也为 1,那么就一定有 a_i > a_i \oplus k,因为 a_i \oplus k 的第 pos 位为 0,更高位与 a_i 相同。

  3. 如果 a_1 \oplus a_2 \oplus \dots \oplus a_n = 0,需证明该状态为必败状态,由归纳假设,只需证明后继状态满足 a^{\prime}_1 \oplus a^{\prime}_2 \oplus \dots \oplus a^{\prime}_n \not= 0。这是必然的,因为任何移动都会使恰好一个 a^{\prime}_i \not= a_i,就会使 Nim 和变为 a^{\prime}_i \oplus a_i \not= 0

综上,得证。

SG 理论

SG 理论指出,任何一个公平组合游戏都等价于一个单堆 Nim 游戏

游戏的记法

前面提到过,任意一个公平组合游戏都可以绘制博弈图。由于博弈图中,每个状态的性质只由它的后继状态决定,所以,可以将博弈图中的状态用它的后继状态的集合来表示。

一个游戏则可以用它的初始状态表示。

公平游戏的表示通常相当复杂,但单堆 Nim 游戏的表示相对来说简单得多。当石子数为 n 时,我们可以将其表示为

*0=\{\},*n = \{ *m : m<n , m \in \mathbb{N} \} = \{ *0,*1,\dots,*(n-1) \}

其中 *n 表示石子数量为 n 时的单堆 Nim 游戏初始状态

之后会用到记号 T \in S,表示状态 T 是状态 S后继状态

游戏的和与等价关系

游戏的等价关系依赖于游戏的和的定义,所以我们先看游戏的和的定义:

游戏 AB,或者叫游戏组合,记作 A+B,是指游戏 A+B = \{ a+B:a \in A \} \cup \{ A+b : b \in B \}

游戏的和,可以理解为由两个同时进行且互不干扰的子游戏组成的游戏,玩家在每一步只能选择恰好一个子游戏移动一步,且游戏在两个子游戏都无法移动时结束。游戏的和的概念可以推广到任意多个游戏的情形,且也满足结合律和交换律。

不难发现,对于一个单堆 Nim 游戏,除了没有石子的情况,都是先手必胜状态,但是这些不同的单堆 Nim 游戏在和其他的单堆 Nim 游戏组合起来时,得到的游戏并不相同。比如 *n 只有在和另一个 *n 组合时才能得到一个先手必败游戏,否则和其他任意 *n^{\prime} \not= *n 组合都会得到先手必胜游戏。

这可以带来一个启示,启发我们通过考察与其他游戏的和来研究某个游戏的性质。于是就引出了游戏的等价这个概念:

如果对于所有游戏 B,游戏 A_1 + B 和游戏 A_2 + B 都同时处于必胜或必败状态,那么称游戏 A_1A_2等价的,记作 A_1 \approx A_2

当然,这一切都只是在为下一部分的内容做铺垫。

SG 函数

前面对 Nim 游戏的分析说明不同的单堆 Nim 游戏互不等价,但是所有公平组合游戏都等价于某个单堆 Nim 游戏。因此,可以为每个公平组合游戏分配一个整数,这个就叫做 SG 函数。

为了证明这些结论,首先需要用到关于游戏等价的两个引理

  1. 第一条引理:对于游戏 A 和任意必败游戏 P,都有 A \approx A+P

    • 证明:
      按照定义,只需证明对于任意游戏 G 都有 A + G \approx A+P+G 成立。
      如果游戏 A+G 有必胜策略,那么游戏 A+P+G 也一定有必胜策略。因为,如果对手在游戏 P 中进行了移动,就也进行移动将其恢复至必败状态;否则按照游戏 A+G 中的必胜策略移动。这样一定能取得最后的胜利。
      如果游戏 A+G 是必败的,那么游戏 A+P+G 也同样必败。因为无论这一回合先手在子游戏 A+G 还是 A+P+G 中进行了移动,后手都可以在同一子游戏中移动将其恢复回必败状态,最终先手一定无法获胜。
  2. 第二条引理:游戏 A 和游戏 A^{\prime} 等价,当且仅当 A+A^{\prime} 是必败游戏。反过来也成立。

    • 证明:
      如果游戏 AA^{\prime} 等价,那么 A + A^{\prime}A+A 同时必败或必胜。而游戏 A+A 是必败游戏,这是因为对于先手的任何操作,后手都可以在另一个子游戏中采取相同的行动,最后一定是先手不能移动、后手获胜。
      反过来,如果 A + A^{\prime} 是必败游戏,那么由第一条引理可知 A \approx A + (A + A^{\prime}) \approx (A+A) + A^{\prime} \approx A^{\prime}

利用这些引理,最终能得到以下定理(即 SG 定理):

对于任何(有限的)公平游戏 G,都存在 n \in \mathbb{N} 使 G \approx *n 成立。

知道定理是没用的,我们需要证明它。

可以用数学归纳法来证明。设游戏 G = \{G_1,G_2,\dots,G_k\},根据归纳假设可知,存在 n_1,n_2,\dots,n_k 使 G_i \approx *n_i。那么,考察游戏 G^{\prime} = \{*n_1,*n_2,\dots,*n_k\},将要证明的是,G^{\prime} \approx *m 其中 m = \text{mex}\{n_1,n_2,\dots,n_k\}

首先要说明 G \approx G^{\prime}:根据前面第二条引理,只需证明 G + G^{\prime} 为必败游戏。不妨设 G \not= *0,如果先手选择 G_i,那么后手就可以选择 n_i,反之亦然。总之,操作后,游戏变为 G_i + *n_i,而根据第二条引理及 G_i \approx *n_i 知这是必败游戏,这就证明了 G \approx G^{\prime}

接着还需说明 G^{\prime} \approx *m:根据前面第二条引理,同样只需证明 G^{\prime} + *m 是必败游戏。不妨设 G^{\prime} \not= *0,如果先手选择了 *n_i \in *m,那么根据 m 的定义,后手就可以选择 *n_i \in G^{\prime},将游戏局面变为 *n_i + *n_i,先手必败,反之亦然;还有一种,如果先手选择了 *n_i \in G^{\prime}n_i > m,那么后手可以选择 *m \in *n_i,局面变为 *m_i + *m_i,还是先手必败。这就证明了 G^{\prime} \approx *m

由等价关系的传递性,G \approx m,这就完成了归纳,证明了任何一个游戏都等价于一个单堆 Nim 游戏。

这就说明了,可以为每一个公平游戏 G 分配一个自然数 n 使 G \approx *n且这个自然数是唯一的。因此,有如下定义:

一个公平游戏的对应的 Nim 数就是使 G \approx *n唯一自然数 n

这个将公平游戏映射到 Nim 数的函数被称为 Sprague–Grundy 函数,简称为 SG 函数,记作 \text{SG}(\cdot)。由于每个公平游戏的状态都是另一个公平游戏,所以对于公平游戏的每一个状态都可以计算相应的 Nim 数,也称为相应的 SG 函数值

根据定理的证明过程,可以得到 SG 函数的递归计算方式 \text{SG}(x) = \text{mex}\{ \text{SG}( x^{\prime} ) : x^{\prime} \in x \}。也就是说,一个状态的 SG 函数值等于它的所有后继状态的 SG 值的 \text{mex}

利用 SG 函数值(即 Nim 数)可判断一个状态是否为先手必胜:公平游戏 G 中的状态 x先手必胜状态,当且仅当 \text{SG}(x) \not= 0

最后还有一个定理,是用于计算游戏的和的 SG 函数值的:

对于公平游戏 G_1,G_2,\dots,G_n,有 \text{SG}(G_1 + G_2 + \dots G_n) = \text{SG}(G_1) \oplus \text{SG}(G_2) \oplus \dots \oplus \text{SG}(G_n)

来证明一下。

因为 *a_1 + *a_2 + \dots + *a_n 就是石子数量为 (a_1,a_2,\dots,a_n) 的 Nim 游戏,所以根据 Nim 游戏的结论可知,游戏 *a_1 + *a_2 + \dots *a_n + *(a_1 \oplus a_2 \oplus \dots \oplus a_n) 是先手必败的。根据前面提到过的第二条引理,*a_1 + *a_2 + \dots *a_n \approx *(a_1 \oplus a_2 \oplus \dots \oplus a_n),所以有 \text{SG}(*a_1 + *a_2 + \dots *a_n) = a_1 \oplus a_2 \oplus \dots \oplus a_n。设 a_i = \text{SG}(G_i),就有 G_i \approx *a_i,有 (G_1 + G_2 + \dots + G_n) + (*a_1 + *a_2 + \dots a_n) = \sum_{i=1}^{n} (G_i + *a_i) 是必败游戏,所以就有 \text{SG}(G_1 + G_2 + \dots G_n) = \text{SG}(G_1) \oplus \text{SG}(G_2) \oplus \dots \oplus \text{SG}(G_n),得证。

利用这个定理,在计算游戏的和的 SG 函数值的时候,能极大程度上减少计算量、降低思维难度。

部分常见公平游戏

Bachet 游戏

与单堆 Nim 游戏类似,但 Bachet 游戏限制了每次取的石子的个数。

Bachet 游戏规则:有一堆共 n 枚石子,两名玩家轮流取石子,每次至少取 1 枚、至多取 k 枚,取走最后一枚石子的玩家获胜,不能移动的玩家判负。

对此,有结论:先手必败当且仅当 n \equiv 0 \pmod {k+1}

对于这个结论,有两种不同的证明,第一种证明是从构造方案角度入手,第二种证明是从 SG 函数值入手。你可以选择自己喜欢的证明方式以学习、运用。

  1. 「构造方案」式证明:

    • n \equiv 0 \pmod {k+1} 时,每一轮中若先手取 1 \le x \le k 枚石子,则后手取 k+1-x 枚(可以证明在 1 \sim k 范围内),最终一定是先手无法移动,后手获胜。
    • 反之,若 n \not\equiv 0 \pmod {k+1},那么先手可以先取走 n \bmod (k+1) 枚石子,剩下的部分则满足 n^{\prime} \equiv 0 \pmod {k+1},可以理解为先手变为了后手,后手成了先手,由于前面所述的后手必胜,因此事实上先手必胜。
  2. 「SG 函数值」式证明:

    • 计算 f(n) 表示剩下 n 枚棋子时对应局面的 SG 函数值。
    • 对于 n \le k,可以归纳证明 f(n) = n,这与单堆 Nim 游戏完全相同,因为取走石子数量的限制没有发挥作用。对于 n > k,可以证明 f(n) = n \bmod (k+1),所以有 f(x) = \text{mex}\{ f(n-k),f(n-k+1),\dots,f(n-1) \},这遍历了模 k+1 的所有余数 0 \sim k 除了 n \bmod (k+1),因此就有 f(n) = n \bmod (k+1)

Moore's Nim-k 游戏

相较于 Nim 游戏,Moore's Nim-k 游戏允许一次从 k 个石子堆中取石子。

Moore's Nim-k 游戏规则:共 n 堆石子,第 i 堆有 a_i 枚石子,两名玩家轮流从至少 1 堆、至多 k 堆中取走任意多枚石子,但不能不取,取走最后一枚石子的玩家获胜,不能移动的玩家判负。

对此,有结论:将每堆石子的个数 a_i 表示为二进制数,并对于每个数位统计有多少堆石子的 a_i 的二进制下这一位为 1,并计算这个数量对 (k+1) 的余数,如果对于每个数位这个余数都是 0 那么先手必败,否则先手必胜。

接下来考虑证明。

我们仿照 Nim 游戏的证明方式。设 d 为余数不为 0 的最高二进制位,且对应余数 k^{\prime} \le k,那么必胜策略为,在石子数量 a_i 的二进制表示下第 d 位为 1 的石子堆中选择恰好 k 堆,并选择移走的石子数量使对手局面中每个数位的余数都是 0。实际上,只需要选定 k^{\prime} 堆石子,每堆都取走 2^d 枚石子,就能使结果中第 d 位的余数变为 0;对于更低的数位的余数,将这些余数随便指派给某个堆即可。

阶梯 Nim 游戏

阶梯 Nim 游戏相较于普通 Nim 游戏更为复杂,它允许将石子在相邻堆之间移动。

阶梯 Nim 游戏规则:共 n 堆石子,第 i 堆有 a_i 枚石子,两名玩家轮流操作,每轮操作可以从第一堆中取走任意枚石子,或将第 i(i>1) 堆的任意枚石子移至第 i-1 堆,取走最后一枚石子的玩家获胜,不能移动的玩家判负。

对此,有结论:先手必败当且仅当 a_1 \oplus a_3 \oplus a_5 \oplus \dots = 0,即所有奇数下标的 a_i 的异或和为 0

怎么证明?当任意一名玩家将偶数位置堆中石子移动至奇数位置堆时,另一名玩家都可以将这些石子移到下一个偶数位置堆或直接取走,因此这种移动操作不会影响奇数位置堆的情况,那么它们就是互相独立的普通 Nim 游戏,根据 Nim 游戏的结论,当且仅当 a_1 \oplus a_3 \oplus a_5 \oplus \dots = 0 时先手必败,否则先手必胜。于是就证明了。

Fibonacci Nim 游戏

Fibonacci Nim 游戏与 Bachet 游戏十分类似,都只有一堆石子且限制了每次取走的数量,唯一不同的是 Fibonacci Nim 游戏中每次限制的数量是动态变化的

Fibonacci Nim 游戏规则:有一堆共 n 枚石子的堆,两名玩家轮流取石子,第一个行动的玩家不限制个数(但不能不取完),随后每次取走的石子不能超过上次(对手回合)取走的石子个数的两倍,也不能不取。取走最后一枚石子的玩家获胜,不能移动的玩家判负。

对此,有结论:先手必败当且仅当最初的石子个数 n 为 Fibonacci 数。

然后我们需要证明这个结论。

q 为当前局面可移走石子数量的限制,第一回合中 q = n-1,而之后的回合中 q 是上次(对手回合)取走的石子个数的两倍。考察剩余石子数量 n 的 Fibonacci 分解(即 Fibonacci 编码),也就是将 n 分解为一系列不相邻的 Fibonacci 数之和。需要证明的是,当前状态是必胜状态,当且仅当 q 不小于 n 的分解中最小的 Fibonacci 数。

必胜策略是:如果可以,移走所有剩余石子;否则,移走分解中最小的 Fibonacci 数。由于分解中,次小的 Fibonacci 数一定大于最小的 Fibonacci 数的两倍,所以只要处于必胜状态的当前回合无法取完所有石子,对手在下一回合也取不走次小的 Fibonacci 数(即下一回合最小的 Fibonacci 数),对手一定处于必败状态。

反过来,如果当前处于必败状态,那么设当前取走的个数为 k,它一定小于当前分解中最小的 Fibonacci 数 F。假设下一回合最小的 Fibonacci 数是 F^{\prime},它也一定是 F-k 对应的分解中最小的 Fibonacci 数。设 F^{\prime} = F^{\prime\prime} + F^{\prime\prime\prime}F^{\prime\prime} > F^{\prime\prime\prime},也就是 F^{\prime\prime\prime},F^{\prime\prime},F^{\prime} 是 Fibonacci 数列中的连续三项。如果 k < F^{\prime\prime\prime},那么利用 Fibonacci 分解(Fibonacci 编码)计算 k + (F-k) 时不用进位,自然得不到 F;所以一定有 k \ge F^{\prime\prime\prime}。这说明下一回合的限额 2k > F^{\prime\prime} + F^{\prime\prime\prime} = F^{\prime},是必胜状态。

例题选讲

SG 函数、Nim 游戏这部分还是有很多经典例题的。

以下例题不保证按真实难度排序(并且博弈论这方面的内容,对难度的评价也有很多主观因素),顺序仅供参考,如果你要做里面的题的话不一定非得按照讲解顺序来做喔 qwq。

题外话:如果你把这里提到的例题全做了的话你会收获一个绿三个蓝两个紫三个黑,以及 RMJ 的一绿一蓝。

P7589 黑白棋

首先可以发现,两列棋子之间的距离是完全没用的东西,因为在移动的时候并不会出现这一列的棋子移到另一列上去,所以题目读入给的所谓 Y 轴截距 y_i 也都是没用的。

任意两个不同的列中的棋子的移动是互不干扰的,也就是说是 n独立的子游戏。我们只需要根据每个游戏单独考虑,最后计算游戏的和即可。

对于一个单独的游戏,如果只有前进操作,那么其实一个子游戏就是一个|b_i - w_i|-1 枚石子的单堆 Nim 游戏,这个是可以直接得到 SG 函数值最后把所有的异或起来的。

嗯,但是现在出现了后退操作,怎么办呢?其实本质没变——如果对手执行后退操作,那么另一方前进同样步数,石子的数量还是没有变,也不影响游戏的局面。后退操作的数量是有限的,所以本质还是一个单堆 Nim 游戏。

两个人都足够聪明。假设参与游戏的是 A 和 B 两个人,它们都已经提前知道了在看作 Nim 游戏时谁会赢,假设此时 A 会赢。那么按照最优策略,A 全程都不会也不需要执行后退操作,因为后退操作做的事情是增多石子数量,此时要是数字选择不当,B 可能会借机赢得游戏;但 B 可能会抱着侥幸心理执行后退操作,而 A 为了保住自己的胜利机会也会选择前进同样步数保证局面不变。所以到了最后,就是一个普通的 Nim 游戏罢了,题目给定的什么 kd 也都是没用的东西。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
int T,n,k,d,sum;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    T=read();
    while(T--){
        n=read(),k=read(),d=read();
        sum=0;
        for(int i=1;i<=n;i++){
            int o=read(),x=read(),y=read();
            sum^=abs(x-y)-1;
        }
        if(sum)cout<<"Yes\n";
        else cout<<"No\n";
    }
    return 0;
}

::::

AT_arc168_b Arbitrary Nim

和 Nim 游戏的规则类似,但是存在每次取石子的个数限制,那这不就是前面提到过的「Bachet 游戏」嘛!好像不太一样,这里有 n 堆石子?没事我们按照 SG 函数算游戏的和的规则来看,只要把每个部分的都 \bmod (k+1) 后异或起来即可,和 Nim 游戏的判断是十分类似的。

我们先考虑 -1 的情况,也就是存在无穷多个能让先手获胜的 k。显然,如果当前异或和已经不为 0 了,那么只要 k \ge \max a\bmod (k+1) 情况下的值也不会为 0(就是原先的异或和啊),那就能取到无穷多个让先手获胜的 k 了,那么此时输出 -1

那就只剩异或和为 0 的情况了。我们发现,如果存在 i \not= ja_i = a_j 的二元组 (i,j),那么这么一组 a_ia_j 可以直接从数组中移去,因为它们不论模上什么之后都是相同的,异或起来也就抵消了,对最终异或和没有贡献。

那么把这些二元组一个个消掉后,会剩下一个数值互不相同的数组。注意!如果这个数组已经空了,那么这种情况下无论你模多少都没有胜利的希望了,此时不存在让先手获胜的 k,输出 0

最后一种情况,最大的 k 能取到多少啊?这些数的异或和是 0,假设其中最大值为 p,剩下的值异或起来是 q,显然 p \oplus q = 0 也就是 p=q,我们取 k+1=pk = p-1,此时其他值都没变,q 没变,只有 p \gets 0,那么这个时候的全局异或值是 p \oplus q = 0 \oplus q = q \not= 0,先手必胜。

这一定是能取到的最大 k 吗?一定,因为 k 如果再大哪怕只大 1,任何数在 \bmod (k+1) 就得不到变化了,那整个的异或值也就会是 0 了,先手也不会再赢了。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 3e5+5;
int n,a[N],sum;
set<int> st;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    n=read();
    for(int i=1;i<=n;i++){
        a[i]=read(),sum^=a[i];
        if(st.count(a[i]))st.erase(a[i]);
        else st.insert(a[i]);
    }
    if(sum)cout<<"-1\n";
    else if(st.empty())cout<<"0\n";
    else cout<<*st.rbegin()-1<<"\n";
    return 0;
}

::::

P4279 小约翰的游戏

我们可以发现,这个游戏和 Nim 游戏极其相似,但是在仔细阅读后,会发现,正常 Nim 游戏的规则是「取到最后一枚石子的玩家胜利」,而在这道题中是「取到最后一枚石子的玩家失败」。唔,那这就不能直接用正常 Nim 游戏的结论做了,因为这是反常 Nim 游戏

既然正常 Nim 游戏有一个万用的结论,反常 Nim 游戏也一定有,对吧?嗯,直觉挺准。不过反常 Nim 的游戏稍微比正常 Nim 游戏的结论要复杂一些。

当局面为 n 堆石子且第 i 堆有 a_i 枚石子,那么先手必败当且仅当:

  1. 存在至少一个 a_i > 1a_1 \oplus a_2 \oplus \dots \oplus a_n = 0,或
  2. 对于所有 i 都有 a_i \le 0a_i = 1i 有奇数个。

如何证明?

先考虑后者情况,显然不论轮到先手还是后手都只有一个操作方案,就是将一堆(只有 1 枚石子的)石子堆移空。那么先手取到最后一枚石子当且仅当最初有奇数堆石子非空。

接着考虑前者情况,这需要分类讨论:

  1. 只存在恰好一个 i 使 a_i > 1 其他 j \not= i 都是 a_j \le 1,那么此时先手可以在第一部操作使 a_i \le 1 且可以控制局面非空石子堆的奇偶性(选择将 a_i 变为 0 还是 1),此时先手必胜;
  2. 存在至少两个 i 使 a_i > 1,那么无论当前轮如何操作,下一轮都一定也存在 j 满足 a_j > 1,这就可以用数学归纳法了。类似 Nim 游戏的证明方式即可证明。(这里真的不想再写一遍了呜,只要你认真看了 Nim 函数部分的证明,这部分是很好理解的。)

于是就可以证明反常 Nim 游戏的结论了!再直接利用这个结论做本题即可。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
int T,n,cnt,sum;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    T=read();
    while(T--){
        n=read();
        cnt=0,sum=0;
        for(int i=1;i<=n;i++){
            int x=read();
            cnt+=(x<=1),sum^=x;
        }
        if(cnt==n)cout<<(sum?"Brother\n":"John\n");
        else cout<<(sum?"John\n":"Brother\n");
    }
    return 0;
}

::::

P2148 E&D

备注:为了不影响阅读体验感,将本题讲解中打的所有表都放进了折叠框里,要看的时候自己点开。

显然可以发现的是,不同组的石子的游戏过程是互不影响的,所以整个游戏其实是 n 个完全独立的子游戏的和罢了。也就是说,我们只需要对于每个子游戏计算出它们分别的 SG 函数值求 Nim 和就可以得出整个游戏的 SG 函数值了。

关键在于这个 SG 函数值怎么算呐,这并没有什么能快速得到的结论,于是我们可以先考虑开始颓废摆烂不做题了打个表看看有没有规律。设 \text{SG}(i,j) 表示一组石子第一堆有 i 枚第二堆有 j 枚的情况下的 SG 函数值。

::::info[戳这里看表]

 0 1 0 2 0 1 0 3 0 1 0 2 0 1 0 4 
 1 1 2 2 1 1 3 3 1 1 2 2 1 1 4 4
 0 2 0 2 0 3 0 3 0 2 0 2 0 4 0 4
 2 2 2 2 3 3 3 3 2 2 2 2 4 4 4 4
 0 1 0 3 0 1 0 3 0 1 0 4 0 1 0 4
 1 1 3 3 1 1 3 3 1 1 4 4 1 1 4 4
 0 3 0 3 0 3 0 3 0 4 0 4 0 4 0 4
 3 3 3 3 3 3 3 3 4 4 4 4 4 4 4 4
 0 1 0 2 0 1 0 4 0 1 0 2 0 1 0 4
 1 1 2 2 1 1 4 4 1 1 2 2 1 1 4 4
 0 2 0 2 0 4 0 4 0 2 0 2 0 4 0 4
 2 2 2 2 4 4 4 4 2 2 2 2 4 4 4 4
 0 1 0 4 0 1 0 4 0 1 0 4 0 1 0 4
 1 1 4 4 1 1 4 4 1 1 4 4 1 1 4 4
 0 4 0 4 0 4 0 4 0 4 0 4 0 4 0 4
 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4 4

::::

感觉这个表一看就很有规律,首先可以发现,它由若干个 2 \times 2 的小矩阵拼接而成,且每个这样的小矩阵的左上角都是 0、其余部分是一样的数字。试着把这个表“压缩”一下,将每个小矩阵用一个数字也就是除左上角外的那个数字来表示,可以得到另一张表。

::::info[戳这里看表]

 1 2 1 3 1 2 1 4 
 2 2 3 3 2 2 4 4 
 1 3 1 3 1 4 1 4 
 3 3 3 3 4 4 4 4 
 1 2 1 4 1 2 1 4 
 2 2 4 4 2 2 4 4 
 1 4 1 4 1 4 1 4 
 4 4 4 4 4 4 4 4

::::

这个表……怎么感觉那么熟悉呢?左上角是 1、剩下部分是同样的数字,唔,这和前面那张更大的表很相似啊,仔细比对一下,欸,这张小一点的表不就是前面那张大表的左上角,每个数都 +1 后的结果嘛!

此时到了这里就已经得出结论了,如果你还不太信的话,还可以再次压缩一下新表验证思想。

\text{SG}(i,j) = \begin{cases} 0, & 2 \mid i \ \text{and} \ 2 \mid j \\ \text{SG}(\lfloor \frac{i}{2}\rfloor , \lfloor \frac{j}{2}\rfloor) + 1 , & \text{otherwise}. \end{cases}

这就是我们最后得到的结论了,此时利用这个结论就可以做这道题了。

是不是感觉有点不安……嗯哼,因为我们这个所谓结论是打表得出的,谁给你的勇气说它是结论啊,所以我们还是要证明一下!(如果你是打表、猜结论选手,可以跳过这一部分证明。)

我们设 S_k 表示将 k 枚石子能得到的局面的 SG 函数值集合,则 \text{SG}(i,j) = \text{mex}( S_i \cup S_j),故 S_k 有递推式 S_k = \{ \text{mex}(S_i \cup S_j) :i+j=k,i,j \in \mathbb{N}^*\}

那么此时需要证明的就是,d \in S_k 当且仅当 k-1 的二进制表示下第 d 位(最低位是第 0 位,第 d 位表示 2^d 那位)是 1

利用数学归纳法,S_1 = \varnothing 显然成立。假设命题对 < k 的正整数都成立,那么 d \in S_k 当且仅当存在 i,j \in \mathbb{N}^* 使 i+j=ki-1j-1 两个数的第 d^{\prime} < d 至少有一个位 1 且第 d 位都是 0(求 \text{mex} 的规则)。显然存在这样一种拆分,当且仅当只考虑第 0 \sim d 的部分即模 2^{d+1} 时,(k-1) = (i-1) + (j-1) + 1 的取值范围为 [2^d,2^{d+1}-1) 之间,等价于说 k-1 的第 d 位是 1。故,归纳步骤成立,原命题成立。

这样也就完成了证明。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
int T,n,res;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
int f(int x,int y){return ((x&1||y&1)?f(x/2,y/2)+1:0);}
int sg(int x,int y){return f(x-1,y-1);}
int main(){
    T=read();
    while(T--){
        n=read();res=0;
        for(int i=1;i<=n/2;i++){
            int x=read(),y=read();
            res^=sg(x,y);
        }if(res)cout<<"YES\n";else cout<<"NO\n";
    }
    return 0;
}

::::

P5675 取石子游戏

我们发现这个游戏的规则和 Nim 游戏的规则是一样的(换句话说,这个游戏就是 Nim 游戏),但是这次,先手第一轮操作具体在哪堆石子里选是可以被指定的。

那么先手必败有且仅有两种情况:

  1. 全局石子数的异或和为 0,此时无论第一轮操作在哪取都不可能赢。
  2. 全局石子数的异或和不为 0,但第一轮指定石子堆无论怎么取也不能让异或和为 0,此时到了后手那里全局异或和还是不为 0,于是后手站在先手的角度就是必胜的,等价于先手必败。

第一种情况是好处理的,重点在于第二种情况。有一个通过并不非常惊人的注意力得出的结论:只有当第一轮指定的石子数严格小于其他石子数的异或和时才能满足第二种情况的条件。

数据范围很小,可以暴力一点,枚举第一轮指定先手取石子堆 i,然后每次 DP 处理有多少种选择方案使其他石子数的异或和为 x。那么这一轮的贡献就是 \sum_{x=a_i}^{255} dp_x——欸?不是说要严格小于吗,怎么还把 a_i 包含进来了?因为正好等于 a_i 的情况就是全局异或和为 0 的情况,也就是第一种情况,也是需要考虑的。

于是就做完了,DP 是简单的,真不会可以看下代码。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 305;
const int MOD = 1e9+7;
int n,a[N],dp[N][N],Ans;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    n=read();
    for(int i=1;i<=n;i++)a[i]=read();
    for(int k=1;k<=n;k++){
        dp[0][0]=1;
        for(int i=1;i<=n;i++)
            for(int x=0;x<256;x++)
                if(i==k)dp[i][x]=dp[i-1][x];
                else dp[i][x]=(dp[i-1][x]+dp[i-1][x^a[i]])%MOD;
        for(int x=a[k];x<256;x++)
            Ans=(Ans+dp[n][x])%MOD;
    }cout<<Ans<<"\n";
    return 0;
}

::::

AT_arc219_d Grid Game

没绷住之 Bachet 套上阶梯 Nim 再塞进一个网格图里,什么神秘大嵌套啊。

先不考虑网格图,如果是 Bachet 套上阶梯 Nim 该怎么做呢?很简单的,我们把 Bachet 里的 \bmod (k+1) 做在阶梯 Nim 里的每个 a_i 上,再照常让奇数位的 a_i 异或起来判断就行。证明也是简单的,在阶梯 Nim 的基础上,如果对方移动了 x 枚石子且原堆含有 > k 枚石子时,我方可以在同样堆做同样动作移动 k+1 - x 枚石子,这样就等价于消除了对方操作

那么,现在有了一个网格图,怎么办呢?也是简单的,想到黑白染色,就能发现每步操作都是在两个不同色格子之间互相移动,最后移动到 (1,1) 的值等价于被彻底移走。令 (1,1) 位置为黑格,则这等价于黑格为偶数位置、白格为奇数位置的带个数限制(即套上 Bachet 游戏)的阶梯 Nim 游戏,直接做就行了。更具体的,黑格是 (i+j) 为偶数的格子 (i,j) 而白格是 (i+j) 为奇数的格子 (i,j)

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
int T,n,k,sum;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    T=read();
    while(T--){
        n=read(),k=read(),sum=0;
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++){
                int x=read()%(k+1);
                if((i+j)%2==1)sum^=x;
            }
        if(sum)cout<<"Alice\n";
        else cout<<"Bob\n";
    }
    return 0;
}

::::

P3480 KAM-Pebbles

取石子的数量限制取决于相邻两堆石子的石子数之差,于是可以考虑从差的方面入手,令 d_i = a_i - a_{i-1}(注意这里可以认为 a_0 = 0,也就是最左边有一个没有石子的石子堆,这样能保证不会把其他石子堆的数量取爆)并在其基础上继续考虑。

肯定需要知道操作转化到 d 上的效果是什么,假设在第 i 堆石子中取走了 x 枚石子即 a_i \gets a_i - x,那么有 d^{\prime}_i = a_i - x - a_{i-1} = d_i - xd^{\prime}_{i+1} = a_{i+1} - a_i + x = d_{i+1} + x等价于将 d_i 里的 x 移动到了 d_{i+1}

是不是很熟悉?对啊,这不就是之前讲过的阶梯 Nim 游戏吗?嗯,只不过这里唯一的一个差别是,前面讲过的阶梯 Nim 游戏是由 i 移到 i-1,这里是由 i 移到 i+1,所以计算的时候要倒着求,也就是判断是否 d_n \oplus d_{n-2} \oplus d_{n-4} \oplus \dots = 0

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1005;
int T,n,a[N],d[N],sum;
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    T=read();
    while(T--){
        n=read();sum=0;
        for(int i=1;i<=n;i++)a[i]=read();
        for(int i=1;i<=n;i++)d[i]=a[i]-a[i-1];
        for(int i=n;i>=1;i-=2)sum^=d[i];
        if(sum)cout<<"TAK\n";
        else cout<<"NIE\n";
    }
    return 0;
}

::::

P3185 分裂游戏

首先有一个,嗯,技巧,就是说在这种规模比较大的游戏中,直接去考虑是不好做的,我们可以讲其拆成若干个子游戏分别去考虑。但在这题中,可以发现以瓶子为单位拆子游戏是不好做的,可以考虑以巧克力豆为单位拆子游戏

终止状态是什么?游戏会结束,当且仅当所有巧克力豆都在最后一个瓶子里,此时不可能再有其他任何移动方案,相反,只要不全在最后一个瓶子里,总能选到合适的 i,j,k 以进行移动。为了方便,我们可以将巧克力豆的编号倒着处理,也就是从 n-10,这样就有 \text{SG}(0) = 0

可以发现,每次在 i 取出一个巧克力豆并在 j,k 加入巧克力豆,相当于在 j,k 处增加了一个子游戏,因此有 \text{SG}(i) = \text{mex}\{ \text{SG}(j) \oplus \text{SG}(k) : j \in [0,i),k \in [0,j]\}。根据这个公式,我们就可以在 O(n^3) 的时间内预处理得到每个 \text{SG}(i) 了,在 n \le 21 情况下绰绰有余。

预处理结束,接下来考虑具体求解,令 sum = \bigoplus_{i \in [0,n) \ \text{and} \ 2 \nmid p_i} \text{SG}(i)当且仅当 sum = 0 时先手必败

知道了胜负态还不够,题目还要求我们算出第一步的方案个数和字典序最小的方案,于是暴力枚举 i,j,k,当 sum \oplus \text{SG}(i) \oplus \text{SG}(j) \oplus \text{SG}(k) = 0 时,对于下一步的后手来说是必败的,等价于先手必胜,此时增加方案数,并在按字典序枚举的同时第一次到达记录方案最后输出字典序最小的方案即可。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1e4+5;
int T,n,sg[N],sum,cnt,a,b,c;
bool vis[N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
void init_SG(){
    sg[0]=0;
    for(int i=1;i<=100;i++){
        memset(vis,0,sizeof(vis));
        for(int j=0;j<i;j++)
            for(int k=0;k<=j;k++)vis[sg[j]^sg[k]]=1;
        for(int j=0;j<=500;j++)
            if(!vis[j]){sg[i]=j;break;}
    }
}
int main(){
    init_SG();
    T=read();
    while(T--){
        n=read();sum=0;
        for(int i=n-1;i>=0;i--){
            int x=read();
            if(x&1)sum^=sg[i];
        }
        if(!sum){
            cout<<"-1 -1 -1\n0\n";
            continue;
        }
        cnt=0;
        for(int i=n-1;i>=0;i--)
            for(int j=i-1;j>=0;j--)
                for(int k=j;k>=0;k--){
                    int now=sum^sg[i]^sg[j]^sg[k];
                    if(now)continue;
                    if(!cnt)a=n-1-i,b=n-1-j,c=n-1-k;
                    ++cnt;
                }
        cout<<a<<" "<<b<<" "<<c<<"\n"<<cnt<<"\n";
    }
    return 0;
}

::::

P6487 HRPA

为了方便描述,以下描述中提到的 f 数组均为 Fibonacci 数组。

看到这个题目是不是感觉挺熟悉的?对啊,这不就是前面提到过的 Fibonacci Nim 游戏吗?嗯,确实,基本上是一样的,但是这题有个不同点,就是它没有限制第一步不能把所有石子取完,所以先手一定是必胜的(第一轮就全取完不就结束了嘛)。呵呵,题目可不会这么简单,它要求你算的是先手第一步最小取多少能必胜。

根据前面 Fibonacci Nim 游戏的结论,当且仅当 n = f_i 时先手第一步才不得不把所有石子取完(不然就输了哇)。

剩下的就是第一次不必全部取完的情况了。回忆之前 Fibonacci Nim 游戏结论的证明,我们用到了一个知识,就是 Fibonacci 拆分,即将 n 拆成若干个不相邻的 Fibonacci 数之和。这个拆分是必然存在的(它是一条定理,证明在此不细说,感兴趣的可以搜齐肯多夫定理以了解)。

那么,设 n = f_{i_1} + f_{i_2} + \dots + f_{i_k},不妨设 i 单调递增且相邻两个之差不为 1。由于不存在相邻的 Fibonacci 数,一定有 f_{i_j} \times 2 < f_{i_{j+1}}。那么先手第一次取 f_{i_1} 个,后手在下一轮无法取完 f_{i_2},先手再取完 f_{i_2} 剩余部分,以此类推,最终先手一定获胜,所以答案是 f_{i_1}

最后代码就只需要算出这个拆分即可,时间复杂度 O(\log^2{n})

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
LL n;
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    n=read();
    while(n>0){
        if(n<=2){cout<<n<<"\n";break;}
        LL x=1,y=2,z=3;
        while(z<n)x=y,y=z,z=x+y;
        if(z==n){cout<<n<<"\n";break;}
        n-=y;
    }
    return 0;
}

::::

P6791 取石子

上一题的数位 DP 版本。

先不考虑第一次取的个数的限制,那么这就是一个反常 Fibonacci Nim 游戏,因为其规则是取到最后一枚石子的玩家输。这非常该死啊,导致我们用不了结论了,但是由于这个游戏的性质,可以让 n \gets n-1,这样规则又可以变为取到最后一枚石子的玩家赢了。可以证明这个变换是等价的。

由 Fibonacci Nim 游戏的结论,即先手必败当且仅当 n 为 Fibonacci 数,再根据上一题的推导,先手如果要赢,第一次最小要取 Fibonacci 分解下的 f_{i_1},因此在这道题中 f_{i_1} > k 时先手必败。

那么这个问题就转化成:{\text{lowbit}_F}(n) 表示 n 在 Fibonacci 分解下的最小的 Fibonacci 项,则统计

\sum_{i=1}^{n} [k \ge {\text{lowbit}_F}(n)]

即可,最后容斥变成 > k 的数量。

发现这个分解就和二进制很像啊,考虑数位 DP 去做,感觉在这里记搜比递推好写啊,主要记录一个上界标记(当前是否顶格)以及上一位是否为 1(Fibonacci 分解中要求没有两个相邻的 Fibonacci 数所以需要特殊考虑这一点)就能做了。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
LL T,n,k,f[105],dp[105];
bool vis[105];
LL read(){
    LL su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}
LL DFS_dp(int u,bool up,bool lst){
    if(u<k)return 1;
    if(!lst&&!up&&dp[u]!=-1)return dp[u];
    LL res=DFS_dp(u-1,up&(!vis[u]),0);
    if(!lst&&(!up||vis[u]))
        res+=DFS_dp(u-1,up,1);
    if(!lst&&!up)dp[u]=res;
    return res;
}
int main(){
    T=read();
    f[1]=1,f[2]=1;
    for(int i=3;i<=90;i++)
        f[i]=f[i-1]+f[i-2];
    while(T--){
        k=read(),n=read()-1;
        LL tmp=n,pp=k;
        memset(vis,0,sizeof(vis));
        memset(dp,-1,sizeof(dp));
        for(int i=90;i>=1;i--)if(f[i]>pp)k=i;
        for(int i=90;i>=1;i--)
            if(tmp>=f[i])tmp-=f[i],vis[i]=1;
        cout<<n-DFS_dp(90,1,0)+1<<"\n";
    }
    return 0;
}

::::

P2599 取石子游戏

神仙题啊。会讲的比较详细,这部分讲解内容也会比较长,希望你能耐心看完哦。(如果你并不想了解其本质,只是想水黑的话,可以选择直接跳过推导部分看最后的转移结论和代码,虽然我并不建议这么做。)

首先有一个很厉害的状态定义,令 \text{L}(i,j) 表示在区间 [i,j]左侧加上一堆数量为 \text{L}(i,j) 的石子后该局面为必败状态,即 (\text{L}(i,j),a_i,a_{i+1},\dots,a_j) 为必败状态。\text{R}(i,j) 的定义同理。

由于 \text{L}\text{R} 在定义、推导、转移等各个方面都是类似的,为了避免文章过于冗长,这边仅以 \text{L} 举例,\text{R} 的内容同理,看懂了其中一个另外一个就很简单了。

对于 \text{L},有两个重要的性质:存在性唯一性。将给出这两个性质的证明。

  1. - 用反证法。 假设不存在满足定义的 $\text{L}(i,j)$,则对于任意非负整数 $x$,$(x,a_i,a_{i+1},\dots,a_j)$ 都是必胜局面。 由于该局面为必胜局面,则一定能**一步**转移到某个**必败局面**。 若拿的是左边一堆,则不可能变为必败局面,因为其局面状态与当前局面是同一类,根据假设应均为必胜状态。 于是只能拿右边一堆,设当前局面能一步到达的某个必败局面为 $(x,a_i,a_{i+1},\dots,a_{j-1},y)$,要求 $0 \le y < a_j$。 由于 $x$ 有**无限个**,而 $y$ 只有 $a_j$ 个,由抽屉原理,一定存在 $x_1 > x_2$ 满足 $(x_1,a_i,a_{i+1},\dots,a_{j-1},y)$ 和 $(x_2,a_i,a_{i+1},\dots,a_{j-1},y)$ **都是必败状态**,而这两个状态之间实际上**一步可达**,矛盾!故原命题成立。
  2. - 还是用反证法。 假设 $\text{L}(i,j)$ 不唯一,则必存在 $x_1 > x_2$ 使 $(x_1,a_i,a_{i+1},\dots,a_j)$ 和 $(x_2,a_i,a_{i+1},\dots,a_j)$ **都是必败状态**,而这两个状态之间实际上**一步可达**,矛盾!故原命题成立。

这样,我们就证明了 \text{L}(i,j) 的存在性和唯一性,也就能得到一个看似没啥用实则还是有用的结论:对于 x \not= \text{L}(i,j),局面 (x,a_i,a_{i+1},\dots,a_j) 都是必胜状态。

嗯,说了这么多,终于要开始状态转移了!

DP 一定要有边界情况,这里的边界情况就是 L(i,i) = a_i,因为后手永远可以模仿先手的操作以保证两个子游戏状态一模一样从而导致先手必败。

然后就是具体的转移了。\text{L}(i,j) 的答案由 \text{L}(i,j-1)\text{R}(i,j-1) 转移而来。

为了方便,我们令 L = \text{L}(i,j-1) , R = \text{R}(i,j-1) , x = a_j

转移时需要分类讨论:

  1. - 这个是最简单的一种情况——由 $\text{R}(i,j-1)$ 的定义,$(a_i,a_{i+1},\dots,a_j)$ 自身就是必败状态,因此右侧不需要添加任何石子堆。
  2. - 要做的就是证明 $(x,a_i,a_{i+1},\dots,a_{j-1},x)$ 为必败局面。 由于最左边一堆和最右边一堆的石子数相同,后手可先与先手在相对堆上做相同操作,直到某个时刻后手得到形如 $(y,a_i,a_{i+1},\dots,a_{j-1})$ 或 $(a_i,a_{i+1},\dots,a_{j-1},y)$ 的局面,此时因 $0 < y \le x < \min(L,R)$,结合 $\text{L}(i,j-1)$ 及 $\text{R}(i,j-1)$ 的定义知此时是必胜局面,即先手必败,得证。
  3. - 如果先手先拿左边一堆,设拿了以后还剩 $z$ 枚石子: 若 $z > L$,则后手可以将最右边拿成 $z-1$ 枚石子,就能回到第三种情况本身,递归证明即可。 若 $z = L$,则后手将最右边拿完,根据 $\text{L}(i,j-1)$ 的定义知此局面先手必败。 若 $0 < z < L$,则后手将最右边拿成 $z$ 枚石子,由第二种情况知此时先手必败。 若 $z=0$,此时最右边石子数 $k$ 满足 $L \le k < R$,结合 $\text{R}(i,j-1)$ 知此局面必胜,即对于先手来说必败。 如果先手先拿右边一堆,设拿了以后还剩 $z$ 枚石子: 若 $z \ge L$,则后手可以将最左边拿成 $z+1$ 枚石子,回到其本身,递归证明。 若 $0 < z < L$,则后手将最左边拿成 $z$ 枚石子,由第二种情况知此时先手必败。 若 $z = 0$,则后手将最左边拿成 $L$ 枚石子,由 $\text{L}(i,j-1)$ 的定义知此局面先手必败。
  4. - 证明与上一种(第三种情况)是类似的,可以先尝试自己证一下。 如果先手先拿左边一堆,设拿了以后还剩 $z$ 枚石子: 若 $z \ge R$,则后手可以将最右边拿成 $z+1$ 枚石子,就能回到第四种情况本身,递归证明即可。 若 $0 < z < R$,则后手将最右边拿成 $z$ 枚石子,由第二种情况知此时先手必败。 若 $z=0$,则后手将最右边拿成 $R$ 枚石子,由 $\text{R}(i,j-1)$ 的定义知先手必败。 如果先手先拿右边一堆,设拿了以后还剩 $z$ 枚石子: 若 $z > R$,则后手可以将最左边拿成 $z-1$ 枚石子,回到其本身,递归证明。 若 $z = R$,则后手把最左边拿完,根据 $\text{R}(i,j-1)$ 的定义知此局面先手必败。 若 $0 < z < R$,则后手将最左边拿成 $z$ 枚石子,由第二种情况知此时先手必败。 若 $z = 0$,此时最左边石子数 $k$ 满足 $0 < k < L$,结合 $\text{L}(i,j-1)$ 的定义知此局面必胜,即对于先手来说必败。
  5. - 设先手将左右两堆中其中一堆拿成了 $z$ 枚石子。 若 $z > \max(L,R)$,回到第五种情况本身,递归证明。 若 $0 < z < \min(L,R)$,后手把另一堆也拿成 $z$ 回到第二种情况,先手必败。 若 $z=0$ 将另一堆拿成 $L$ 或 $R$ 个即可。 剩下的就是 $L \le z \le R$ 或 $R \le z \le L$,第三种情况可以解决最左边 $L < z \le R$ 及最右边 $L \le z < R$ 的状态,第四种情况可以解决最左边 $R \le z < L$ 及最右边 $R < z \le L$ 的状态,因此只剩最左边 $z = L$ 和最右边 $z = R$,而这两种情况下只需把另一堆拿完即可由 $\text{L}(i,j-1)$ 或 $\text{R}(i,j-1)$ 的定义证明。

综上,我们得到了整个 \text{L}(i,j) 在所有情况下的转移式:

\text{L}(i,j) = \begin{cases} 0, & x=R \\ x+1, & L \le x < R \\ x-1, & R < x \le L \\ x , & \text{otherwise}. \end{cases}

同理能求 \text{R},具体推导过程不放了(码字好累的 TAT,而且基本是一样的内容啊),这里放出最后的转移式(注意这里的 L = \text{L}(i+1,j) , R = \text{R}(i+1,j) , x = a_i):

\text{R}(i,j) = \begin{cases} 0, & x=L \\ x+1, & R \le x < L \\ x-1, & L < x \le R \\ x , & \text{otherwise}. \end{cases}

回到本题最后要求输出的内容,只有当 \text{L}(2,n) = a_1 时先手无必胜策略,否则都是先手必胜哦!

总时间复杂度是 O(n^2) 的,求 \text{L}\text{R} 的过程是区间 DP。

::::success[sample code]

提交记录在这里。

#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
#define _i128 __int128
using namespace std;
const int N = 1005;
int T,n,a[N],L[N][N],R[N][N];
int read(){
    int su=0,pp=1;char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
    return su*pp;
}

int main(){
    T=read();
    while(T--){
        n=read();
        for(int i=1;i<=n;i++)
            a[i]=read(),L[i][i]=a[i],R[i][i]=a[i];
        for(int len=2;len<=n;len++)
            for(int i=1;i+len-1<=n;i++){
                int j=i+len-1,x=a[j];
                int l=L[i][j-1],r=R[i][j-1];
                if(x==r)L[i][j]=0;
                else if(l<=x&&x<r)L[i][j]=x+1;
                else if(r<x&&x<=l)L[i][j]=x-1;
                else L[i][j]=x;
                l=L[i+1][j],r=R[i+1][j],x=a[i];
                if(x==l)R[i][j]=0;
                else if(r<=x&&x<l)R[i][j]=x+1;
                else if(l<x&&x<=r)R[i][j]=x-1;
                else R[i][j]=x;
            }
        cout<<(L[2][n]!=a[1])<<"\n";
    }
    return 0;
}

::::

简单总结

  1. 明确子游戏拆分:很多题目直接给出的博弈游戏是十分复杂、混乱的,要找到合适的单位拆分成若干个形式类似且互相独立的子游戏,最后求它们的和即可得到最终答案。

  2. 寻找规律:通过状态分析或打表找到 SG 函数固定的递推式以通过预处理或其他方式能快速求得每种状态下的 SG 函数值,或经过一些变形后转变为常见公平组合游戏(如 Bachet 游戏、阶梯 Nim 游戏等)以套用常规公式求解。

  3. 进行证明:很多情况下,SG 函数的公式或结论是由打表后的合理猜测或感性理解得到,此时在时间充足的情况下,最好还是要尽力给出尽可能理性的证明,防止被假解蒙蔽双眼。

  4. 确保边界:不要漏掉 0,1,\inf 这类容易被忽略的边界情况,分类讨论的话需要考虑完全所有情况,如果漏了或重了很可能导致整个过程逻辑崩塌,进而影响整体思路。

嗯,最后感谢你能看到这里!码字也不容易,整篇文章有超过三万个字符(虽然可能有一些滥竽充数的代码喵 qwq),还希望你能留个赞支持一下,真是太谢谢你啦!>^<