组合博弈基础
本文仅作为个人学习使用,图中引用了HDACM课程的代码和图片,在此感谢.
题目描述
- 一堆棋子两个人抓,一次可以抓1,2,3个,但是不能抓0个,谁抓完最后的棋子谁获胜.
名词解释
组合游戏:
- 两个人玩
- 能在有限次内结束
- 游戏的操作状态是一个有限的棋盘
- 不能进行下去时,游戏结束
终结点:
-
游戏按规则进行不下去的点.
-
一次能拿2,3,4个,那么0个和1个都是终结点.
必胜点:
-
N点
-
现在这个人拿,一定至少有某种方法到达必败点.
必败点:
-
P点
-
现在这个人拿,按规则无论怎么拿都会进入到必胜点.
举个栗子
- 还剩4颗棋子,游戏规定一次可以拿1个或者2个或者3个.
- 4 就是必败点
- 1 2 3 就是必胜点
- 0 是终结点 那么也就一定是必败点
算法实现
-
所有终结点标记为必败点
-
能直接到达必败点的标记为必胜点
-
无论怎么走都只能到必胜点的标记为必败点
-
如果步骤三找不到新的必败点了,算法结束
否则,返回到步骤二.
例题演示
一. 玩游戏的小男孩
-
只能往左,下和左下走,如图,如果无路可走则判负.
-
**思路一:**组合博弈思想,找N,P点. 从最终状态开始找,最左下角是个 P点.横竖往前倒腾,发现规律.
-
**思路二:**想象成两堆牌,要么在一堆中抓一张,要么两堆各自抓一张.(转化了问题而已)其实就是往下走行减少一,往左走列减少一,往左下走都减少一.
Nim游戏
-
题目描述:
三堆牌,每人每次可以任意选择一堆,抓任意张牌,但是不能是0张,抓完最后一张牌的人获得胜利.
-
终结点: 0,0,0
-
可以用三元组画出状态转移图
Nim和
-
每堆牌的数量,用二进制表示,进行异或得到的结果称为Nim和
-
重要结论1:
Nim和为0的点一定为必败点.
-
可以理解的证明:
0,0,0点 Nim和为零
在Nim和为零的位置,减少任意一堆牌的数量都会使得Nim和不再为零
在Nim和不为零的位置,一定可以通过让Nim和跟某堆牌异或的方式使得Nim和变为0.(有三种解决方案)
以上三条与组合博弈的基本理论,必胜点和必败点之间的转换相对应.
SG函数
-
SG函数的值是不等于它的后继状态的最小的自然数值
-
举例:
注意4的值是0,因为他的后继节点sg值分别是1,2,3.不等于1,2,3的最小的自然数是 0.
-
惊人的发现必败点的SG值为0!!!
组合游戏的并
-
题目描述:
-
SG函数的一个重要应用:
可以先求出每堆当前状态的SG值,再将三堆的值异或,得到的结果如果是0就是必败点,如果是1就是必胜点.
代码求SG函数
1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
2.可选步数为任意步,SG(x) = x;
3.可选步数为一系列不连续的数,用模板计算。
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 110;
const int M = 10010;
int a[N],f[M];
//a中保存合法的取法
//f中保存各个点的SG值
int k;
int sg(int p)
{
bool g[N] = {0};
for(int i = 0; i < k; i++)
{
int t = p - a[i];
if(t < 0) break;
if(f[t] == -1) f[t] = sg(t);
g[f[t]] = 1;
}
for(int i = 0;; i++)
{
if(!g[i]) return i;
}
}
int main()
{
while(scanf("%d",&k) == 1)
{
if(k==0) break;
for(int i = 0;i < k; i++)
{
scanf("%d",&a[i]);
}
sort(a,a+k);
memset(f,-1,sizeof(f));
int c;
cin >> c;
while(c--)
{
int t;
cin >> t;
int s = 0;
while(t--)
{
int p;
cin >> p;
if(f[p]==-1) f[p] = sg(p);
s = s^f[p];
}
if(s == 0) cout << "L";
else cout << "W";
}
cout << endl;
}
}