什么是尼姆博弈?(最优策略)
尼姆博弈是一个两人博弈,2名玩家轮流从n堆物品中拿取一定数量(最小值1最大值全部)的物品,每次拿取时先选择某一堆,再从中拿取任意数量个物品,至少拿1个,至多将这一堆物品全部拿走,不能不拿。拿到最后一个物品的玩家获胜。
这种题一般不要模拟不要模拟不要模拟(根本模拟不了),所以我们需要在写代码前用数学进行分析和优化。
优化:最优策略是什么
我们采用逆向思维来想n堆沙子的最后的几种情况。
1、当只剩下一堆沙子时: 你的最佳选择是将所有石子全部拿走,你赢。
2、当剩下两堆沙子时:假设现在有两堆石子且数量不相同,那么你的最佳选择是取走多的那堆石子中多出来的那几个,使得两堆石子数量相同,这样,不管另一个怎么取,你都可以在另一堆中和他取相同的个数,这样的局面你就是必胜。比如有两堆石子,第一堆有3个,第二堆有5个,这时候你要拿走第二堆的三个,然后两堆就都变成了3个,这时你的对手无论怎么操作,你都可以“学”他,比如他在第一堆拿走两个,你就在第二堆拿走两个,这样你就是稳赢的。同理,两堆沙子数量相同时先手必败。
3、当剩下三堆沙子时 ,我们用(a,b,c)表示3堆沙子的局势,首 先(0,0,0)显然是奇异局势(这里不用管为什么叫奇异局势,往后面看即可)。无论谁面对奇异局势,都必然失败。第二种奇异局势是 (0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情型。
……………………………………………………………………………………………………
博弈双方都采取最佳策略,那么必然会存在以下的情况
一个状态是必败状态当且仅当它的所有后继都是必胜状态
((先手人的)必胜状态都不可能一步再走到(后手人的)必胜状态)
一个状态是必胜状态当且仅当它至少有一个后继是必败状态
((先手人的)必输状态都可以一步到达(先手人的)必胜状态)
(反证法举例可以证明 以上两个命题的条件或结果一旦更改为假命题)
那么双方的决策就都是给对方留下必败态,那么先手人是否赢(是否给对方留下必败态)只取决于输入堆的分布。
这里我们正式引入尼姆博弈结论(想看怎么来的可以去搜以下数学分析-尼姆博弈)
Bouton定理: 先手能够在非平衡尼姆博弈中取胜,而后手能够在平衡的尼姆博弈中取胜。即状态(x1, x2, x3, …, xn)为必胜状态当且仅当x1 xor x2 xor x3 xor … xor xn =0。这样的操作也称为Nim和(Nim Sum)
而异或运算符号为^;
所以我们只需要堆每堆的沙子数异或即可。
例题1:
例题简要题干:两个玩家A、B,A先拿沙子,每次拿一堆沙子中的若干沙子(最小为1最大为全部),最后一个拿沙子的人赢。
输入 T (1≤T≤10^4)表示数据组数,n堆沙子(10^5)和每堆沙子的个数(10^4)
输出 获胜玩家
代码如下。
#include<iostream>
using namespace std;
int main()
{
int T;
cin >> T;
int n, a;
while (T--)
{
cin >> n;
int ans = 0;
while(n--)
{
scanf("%d", &a);
ans ^= a;
if (ans) printf("A\n");
else printf("B\n");
}
}
return 0;
}
例题二(看似尼姆实则贪心)
例题简要题干:两个玩家A和B,A先拿沙子。但是游戏按如下步骤进行:
- 奇数轮由A指定一堆非空的沙子堆开始。然后 B从这堆黑灰中拿走至少一(至多全部)单位数量黑灰。
- 偶数轮由B指定一堆非空的黑灰堆开始。然后A从这堆黑灰中拿走至少一(至多全部)单位数量黑灰。
输入 T (1≤T≤10^4)表示数据组数,n堆沙子(10^5)和每堆沙子的个数(10^4)
输出 获胜玩家
那么双方的决策会受到对方的制约,不符合尼姆博弈的规律,所以本题进行贪心。
再说一次,不要模拟不要模拟不要模拟(根本模拟不了),所以我们需要在写代码前用数学进行分析和优化。
优化一:对输入的数据(堆数)进行优化
对于输入的数据,我们只需要开辟大小为2的int数组,分别储存沙子个数为1的沙堆数和沙子个数大于1的沙堆数。沙子个数大于1的沙堆数可以归并的证明如下。
一堆沙子,其被取走的可能性如下:
1.A取走
2.A取走一部分,B全部取走
3.A取走一部分,B取走一部分,A全部取走(等效为A全部取走)(在最优化策略中是a全部取走,这情况不会出现)
4.……………………(等效为第二步)
所以我们只需要开辟大小为2的int类型num数组,分别计数沙子为1的堆数(num[1})和沙子不为1的堆数num[2],进行判断。
优化二:输入的沙子数为1的堆可以两两相消
因为沙子数为1的沙堆只有一次操作的空间,而最贪心的策略就是模仿对方的下法,所以对方一但指定一个沙子数为1的堆让我全拿,我就同样指定一个一个沙子数为1的堆让对方全拿,即最后沙子数为1的堆只剩1或0。
在这个条件下演化num[2]的个数得到如下结果
所以贪心代码如下
#include<iostream>
using namespace std;
int main()
{
int T;
cin >> T;
while (T--) {
int n,a,sum[3]={0,0,0};
scanf_s("%d", &n);
while (n--)
{
scanf_s("%d", &a);
if (a == 1)
{
sum[1]++;
}
else {
sum[2]++;
}
}
if (sum[2] == 0)
{
if (sum[1] % 2 == 1)
cout << "A" << endl;
else
cout << "B" << endl;
}
else
{
if(sum[1]%2==1)
cout << "B" << endl;
else
cout << "A" << endl;
}
}
return 0;
}