博弈论算法学习记录
学习资料
- 总结
- 巴什博奕
- 威佐夫博弈
- 尼姆博弈
- 博弈问题与SG值
巴什博奕(Bash Game)
博弈原题
最简单的巴什博奕题大概就是抢三十了,两个人分别报数,每人一次可以报1~4
个数,先报数到30
的人赢。
这个游戏是典型的后手必胜游戏。首先,当一个人报数到了25
,那么他必然会赢,因为不论下一个人报数多少,他都能凑足30
。既然25 -> 30
是这样,那么20 -> 25
也是这样,凡是报数到20
的人下一回合都能报数到25
。以此类推,我们发现,后手必然能够报数到5
,然后是10
,15
,20
,25
。
这样就得到了是后手必胜。
博弈原理
我们分析一下其中的原理,为什么是5
?因为 $5=4+1$ 。那么我们就得到了巴什博奕完善的表达形式:
有一堆n
个石子,两人在自己的回合可以任意取1~m
个石子,先取完者胜。那么如果 $n=k\times(m+1)$ ,后手必胜,否则先手必胜。
威佐夫博弈(Wythoff Game)
博弈原理
有两堆石子,两人可以在自己的回合取某一堆中任意数量的石子,也可以同时从两堆中任意取相同数量的石子,先取完者胜。
推导过程看不懂。但大体过程是这样的:
用(a, b)
表示两堆石子数目,第一必败态毋庸置疑是(0, 0)
;第二必败态是(1, 2)
,因为先手无论如何取,后手下一步都能取完所有石子。
然后是第三必败态(3, 5)
,因为无论先手如何取,后手都能走到下一个必胜态。以此类推可以得到:
序号 | 必败态 |
---|---|
0 | (a, b) |
1 | (0, 0) |
2 | (1, 2) |
3 | (3, 5) |
4 | (4, 7) |
5 | (6, 10) |
6 | (8, 13) |
7 | (9, 15) |
8 | (11, 18) |
观察样例,很难得出结论,但是样例数很多之后我们发现了其中的规律:
$$
a = \frac{\sqrt{5}+1}{2}\times (b-a)
$$
博弈应用
判断先\后手输赢
很好解决,直接判断a
,b
就可以了。
求先手输赢,输出第一次取法
先考虑两堆同时取,这样差值是不变的。如果只能取一堆,那么枚举差值即可。
尼姆博奕(Nimm Game)
博弈原题
有三堆石子,两人可以在自己的回合取某一堆中任意数量的石子,先取完者胜。
我们依然用(a, b, c)
表示当前状态。首先我们知道(0, 0, 0)
是必败态。第二种必败态是(0, n, n)
,这种状态,无论先手那多少,后手都可以从另一堆中拿同样数量。
然后直接给出规律吧……完全看不出来的。
必败态的充要条件是a ^ b ^ c = 0
。
博弈推广
现在有n
堆石子,其他规则不变。这样的博弈依然服从尼姆博弈的规律。
我们把满足所有值的异或值是0
的状态称为平衡状态,平衡状态为必败态。并且平衡状态只能转化为非平衡状态,非平衡状态可以转化为平衡状态。
那么我们把博弈规则改一改,改为先取完者负。
这样初看很复杂,但是实际依然存在规律。首先依然按照尼姆博弈的规律取,直到有某一堆物品数量大于1
,其他堆全部为1
的状态,将这堆取空或者取为1
,使数量为1
的堆的数目变为奇数即可。因为操作需要满足平衡状态,所以不会面对这种情况,这样就可以判断胜负了。
例题(HDU 1850)
思路
求尼姆博弈先手是否能胜,如果能,第一步有几种走法。
思路很简单,只要看当前是否是非平衡状态,下一步哪个堆可以转化为平衡状态即可。
代码
#include
using namespace std;
int main(){
int n, num[100+7];
while (scanf("%d", &n), n) {
int sum = 0, cnt = 0;
for (int i = 0; i < n; ++i) {
scanf("%d", num+i);
sum ^= num[i];
}
for (int i = 0; i < n; ++i)
if (num[i] > (sum^num[i])) ++cnt;
printf("%d\n", cnt);
}
return 0;
}
反尼姆博奕例题(HDU 1905)
思路
与正常的尼姆博弈几乎相同,只是要特判奇数个1
和偶数个1
的情况。
代码
#include
using namespace std;
int n, num[50];
void solve() {
int sum = 0, odd = 0, even = 0;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", num+i);
sum ^= num[i];
num[i] < 2 ? ++odd : ++even;
}
if ((sum && even) || (!sum && !even)) puts("John");
else puts("Brother");
}
int main(){
int t; scanf("%d", &t);
while (t--) solve();
return 0;
}