Nim 博弈
正向博弈
P2197 【模板】Nim 游戏 - 洛谷
玩家轮流从若干堆物品中的某一堆物品取若干数量物,最终取完者为胜
结论:有 n 堆物品,数量分别为 a 1 , a 2 , a 3 . . . a n a_{1},a_{2},a_{3}...a_{n} a1,a2,a3...an
- 若: a 1 ⊕ a 2 ⊕ a 3 ⊕ . . . ⊕ a n = = 0 a_{1}⊕a_{2}⊕a_{3}⊕...⊕a_{n}==0 a1⊕a2⊕a3⊕...⊕an==0 则后手赢
- 若:
a
1
⊕
a
2
⊕
a
3
⊕
.
.
.
⊕
a
n
!
=
0
a_{1}⊕a_{2}⊕a_{3}⊕...⊕a_{n} ~!=~0
a1⊕a2⊕a3⊕...⊕an != 0 则先手赢
证明:
首先,胜者的最终状态肯定是 0 ⊕ 0 ⊕ . . . ⊕ 0 0⊕0⊕...⊕0 0⊕0⊕...⊕0
然后有两条:
总异或和为 0 的可以经过一步骤到–>总异或和不为 0 …①
总异或和不为 0 的可以经过一步骤到–>总异或和为 0 …②
左程云26:18处讲解
^和 == 0 ↔ 经过一步拿取 \xleftrightarrow{\text{~~~经过一步拿取}} 经过一步拿取 ^和 != 0(也就是说每一堆物品数的异或和是由 0
非 0 之间相互转换的)
所以肯定是先碰上异或和为 0 的状态的那个人在经过堆物品数的异或和由 0 与非 0 相互转换的操作后优先碰上胜者的胜利状态(也就是都取完的全 0 状态)
#include <iostream>
using namespace std;
int main() {
ios::sync_with_stdio(false); // 关闭同步,提高输入输出效率
cin.tie(nullptr); // 解绑 cin 与 cout
int t;
cin >> t;
while (t--) {
int n;
cin >> n;
int eor = 0;
for (int i = 0; i < n; ++i) {
int x;
cin >> x;
eor ^= x;
}
if (eor != 0) {
cout << "Yes\n";
} else {
cout << "No\n";
}
}
return 0;
}
反向博弈
玩家轮流从若干堆物品中的某一堆物品取若干数量物,最后取物品者输
从以下三种情况考虑
-
每一堆的物品数都是 1
(先手根据物品堆数是奇数还是偶数,判断自己的输赢) -
只有一堆大于 1,其他堆都是 1
(此时先手可以根据物品数等于 1 的堆数来安排自己的后续操作来保证自己赢,也就是说此时的先手是稳赢的)
例如此时情况为:
<1> 1 1 1 5,先手拿 5 个成了 1 1 1(回到了第一种情况可以判断自己的输赢,此时先手必赢)
<2> 1 1 5, 先手拿 4 个成了 1 1 1(同上,先手赢) -
若干堆大于 1 ,若干堆等于 1
(这种状态下,经过一系列的操作,肯定可以得到第二种状态的只有一堆大于 1 ,其余全是 1(其中第二种状态的这种情况的总异或和不为0)
,加上正向 Nim 博弈的①和②的结论=>异或和在非 0 和 0 之间循环转换,就可以根据此时的若干堆大于 1 ,若干堆等于 1
状态的异或和来判断谁先来到只有一堆大于 1,其他堆都是 1
的必赢状态,则谁就赢)
其中判断方法就是:
<1> 若先手此时处于若干堆大于 1 ,若干堆等于 1
的状态且堆数的异或和为 0,则经过异或和的 0 与非 0 的循环转换,每次轮到他时此时的堆的状态都是异或和为 0,则后手则每次轮到他时的堆的状态都是异或和非 0 。因为必赢状态(只有一堆大于 1,其他堆都是 1
)的异或和为非 0。故最后肯定是后手达到这种必赢状态,那么也就是后手赢
<2>反之亦然
综上,该题的思路是:
<1>如果物品堆数的初始状态是 每一堆物品都是0
,则先手根据物品堆数的奇偶性判断输赢
<2>如果物品堆数的初始状态是 只有一堆大于 1,其他堆都是 1
,这种情况是必赢状态,因为此时先手可以根据物品数为 0 的堆的个数灵活拿取,来构造可以使得自己赢的情况
<3>如果物品堆数的初始状态是 若干堆大于 1 ,若干堆等于 1
,这种情况最常见。先手可以根据此时的堆物品数的异或和以及最优解是堆物品数的异或和在 0 与非 0 之间相互转换的规律来判断是自己先达到 只有一堆大于 1,其他堆都是 1
的必赢状态还是对手先达到 只有一堆大于 1,其他堆都是 1
的必赢状态来判断自己的输赢
#include <iostream>
using namespace std;
const int MAXN = 51;
int stones[MAXN];
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int t, n;
cin >> t;
while (t--) {
cin >> n;
int eor = 0; // 异或和:用于判断当前局势对先手是否有利
int ones = 0; // 统计物品数为 1 的堆数
for (int i = 0; i < n; ++i) {
cin >> stones[i];
eor ^= stones[i];
if (stones[i] == 1) {
ones++; // 记录物品数为 1 的堆
}
}
// 情况一:每一堆物品数都是 1
// 分析:此时的胜负只与堆数的奇偶性有关
// - 如果堆数是奇数,先手最后必然面对一个堆 -> 输(Brother 赢)
// - 如果堆数是偶数,先手可对称操作 -> 赢(John 赢)
if (ones == n) {
cout << ((n % 2 == 1) ? "Brother" : "John") << '\n';
}
// 情况二 + 情况三:存在一堆或多堆物品数大于 1
else {
// 情况二:只有一堆物品数大于 1,其余全是 1
// 说明:这是先手必胜的情况,先手可以操作该堆变成情况一中的奇偶局面,使自己最终胜出
// 情况三:若干堆 >1,若干堆 =1
// 思路:经过博弈后必然转化为情况二,谁能将游戏引导到情况二中的非 0 异或状态,谁就能赢
// 判定:如果异或和 ≠ 0,说明当前是先手可控状态 -> John 赢
// 如果异或和 = 0,说明后手将控制游戏走向必胜态 -> Brother 赢
cout << ((eor != 0) ? "John" : "Brother") << '\n';
}
}
return 0;
}