公平组合游戏 ICG
如果一个游戏满足:
1. 有两名玩家交替行动
2. 在游戏进程的任意时刻,都可以执行的合法行动与轮到哪名玩家无关
3. 不能行动的玩家判负
则称该游戏为一个公平组合游戏
一、巴什博弈
一堆 n 个物品,两个人从轮流中取出(1 ~ m)个;最后取光者胜(不能取的人输)
若 n = k * (m+1) + r;先手拿走 r 个,那么后手无论拿走(1 ~ m)个,先手只要拿的数目的和为 m + 1 ,那么先手必赢。
反之若 n = k * (m+1),那么先手必输。
if(n%(m+1)) return true; //先手胜
else return false;
二、威佐夫博弈
有两堆各若干个物品,两个人轮流从任意一堆中取出至少一个或者同时从两堆中取出同样多的物品,规定每次至少取一个,至多不限,最后取光这胜利
黄金分割比:首先求出差值,差值 * 黄金分割比 == 最小值 的话后手赢,否则先手赢;
double r = (sqrt(5.0) + 1) / 2.0;
int d = abs(a-b) * r; //a,b是两堆物品的数目
if(d != min(a,b)) return true; //先手赢
else return false;
显然 int d = abs(a-b) * r,存在精度丢失的问题,于是在处理威佐夫类博弈的时候,我们要注意问题的精度和给出的两堆物品的范围,如果给出的范围相当大的话,我们要采用高精度在计算这个 double r。
三、尼莫博弈
n 堆物品,两个人轮流取,每次取某堆中的不少于1个,最后取完者胜。
结论:将 n 堆物品数量全部异或后结果为 0 则先手必败,否则先手必胜。
尼莫的结论是很重要的,但是证明过程比较繁琐。
int res = 0;
for(int i=1;i<=n;i++)
res = res ^ a[i];
if(res) return true;
else return false;
四、斐波那契博弈
有一堆物品,两人轮流取物品,先手最少取一个,至多无上限,但是不能把物品取完,之后每次取的物品数不能超过上一次取的物品数的二倍且至少为1件,取走最后一件物品的人获胜。
先手胜当且仅当n不是斐波那契数(n为物品数)
五、Ferguson游戏
有两个盒子,一个装有m颗糖,一个装有n颗糖,表示为(m,n)
每次清空一个盒子,将另一个盒子里的糖转移一些过来,并保证两个盒子至少各有一颗糖
最后进行转移糖者获胜,无法转移糖者败。
if(m&1&&n&1) 先手败
else 先手胜
六、Chomp!博弈
有一个nm的棋盘,每次可以取走一个方格并拿掉它右边和上面的所有方格。拿到左下角的格子(1,1)者输,如下图是83的棋盘中拿掉(6,2)和(2,3)后的状态。
if(n==1&&m==1) 先手输
else 先手赢
变式:约数游戏
桌子上有n个数字,1~n,两个人轮流选择一个桌上的数x,然后将x与x的约数都拿走,拿去最后一个数的人胜出(无法选择数字的人失败)。
if(n!=0) 先手胜
else 先手败
七、翻棋子游戏
一个棋盘上每个格子有一个棋子,每次操作可以随便选一个朝上的棋子(x,y),代表第 i 行第 j 列的棋子,选择一个形如(x,b)或(a,y)(其中b < y,a < x)的棋子,然后把它和(x,y)一起翻转,无法操作的人输。
把坐标为(x,y)的棋子看成大小分别为 x 和 y 的两堆石子,则本题转化为了经典的Nim游戏,如果难以把棋子看作石子,可以先把Nim游戏中的一堆石子看成一个正整数,则Nim游戏中的每次操作是把其中一个正整数减小或者删除。
八、阶梯博弈
博弈在一列阶梯上进行,每个阶梯上放着自然数个点,两个人进行阶梯博弈,每一步则是将一个集体上的若干个点( >=1 )移到前面去,最后没有点可以移动的人输。
阶梯博弈也是可以转化成尼姆博弈的,把所有奇数阶梯看成N堆石子,做Nim;把石子从奇数堆移动到偶数堆可以理解为拿走石子,就相当于几个奇数堆的石子在做Nim。
九、SG函数
- 有向图游戏
给定一个有向无环图,图中有唯一的起点放有一枚棋子嘛。两名玩家交替地把这枚棋子沿着有向边移动,每次进行一次移动,无法移动者判负。该游戏被称作有向图游戏。
任何一个 ICG 游戏都可以转换为有向图游戏。具体方法是,把每个局面看成图中的每一个节点,并且从每个局面想沿着合法行动能够到达的下一个局面连有向边。 - Mex 运算
设 S 为一个非负整数集合。定义 mes(S) 为求出不属于集合 S 的最小非负整数的运算,即:mes(S) = min{x|x∈N&&x∉S}
mes({0,1,2,4}) = 3、mes({ }) = 0…… - SG函数
在有向图游戏中,对于每个节点 x ,设从 x 出发共有 k 条有向边,分别到达节点 y1,y2……yk,定义 SG(x) 为 x 的后继节点 y1,y2……yk 的 SG 函数值构成的集合再执行 mex 运算的结果,即:
SG(x) = mex( { SG(y1) , SG(y2) , …… , SG(yk) } )
特别地,整个有向图游戏 G 的 SG 函数值被定义为有向图游戏起点 s 的 SG 函数值,即 SG(G) = SG(s)。 - 有向图游戏的和
设 G1,G2,… ,Gm 是 m 个有向图游戏。定义有向图游戏 G ,他的行动规则是任选某个有向图游戏 Gi,并在 Gi 上行动一步。G 被称为有向图游戏 G1,G2,… ,Gm 的和。
有向图游戏的和的 SG 函数值等于它包含的每个子游戏 SG 函数的异或和,即:
SG(G) = SG(G1) ^ SG(G2) ^ … ^ SG(Gm) - 定理
有向图游戏的某个局面必胜,当且仅当该局面对应节点的 SG 函数值大于0;
有向图游戏的某个局面必输,当且仅当该局面对应节点的 SG 函数值等于0;
一般步骤:
1.找寻必败态(即SG值为0)
2.找出当前所有状态的前驱节点
3.根据定义计算节点SG值
重复上述步骤,直到整棵树建立完成
SG函数模板(打表)
int sg[maxn]; //sg[n] n表示每堆数量
int s[k]; //每次能取的值,下标从0开始,0 ~ k-1,必须有序,可以sort(s,s+k);
bool vis[maxn];
const int k ; //k表示集合s的大小
void get_sg()
{
int i,j;
for(i = 0;i < maxn;i++){
memset(vis,0,sizeof(vis));
j = 0;
while(j<k&&s[j]<=i){
vis[sg[i-s[j]]] = 1; //标记后继集合
j++;
}
for(j = 0;j<maxn;j++){ //mex运算
if(!vis[j]){
sg[i] = j;
break;
}
}
}
}
int main(void)
{
……
memset(sg,-1,sizeof(sg));
get_sg();
if(sg[n]==0) //先手必输
else //先手必胜
//如果多堆
num = sg[n1] ^ sg[n2] ^ …… ^ sg[nx];
if(num==0) //先手必败
else //先手必输
……
return 0;
}
例题:HDU 1848 Fibonacci again and again
Problem Description
任何一个大学生对菲波那契数列(Fibonacci numbers)应该都不会陌生,它是这样定义的:
F(1)=1;
F(2)=2;
F(n)=F(n-1)+F(n-2)(n>=3);
所以,1,2,3,5,8,13……就是菲波那契数列。
在HDOJ上有不少相关的题目,比如1005 Fibonacci again就是曾经的浙江省赛题。
今天,又一个关于Fibonacci的题目出现了,它是一个小游戏,定义如下:
1、 这是一个二人游戏;
2、 一共有3堆石子,数量分别是m, n, p个;
3、 两人轮流走;
4、 每走一步可以选择任意一堆石子,然后取走f个;
5、 f只能是菲波那契数列中的元素(即每次只能取1,2,3,5,8…等数量);
6、 最先取光所有石子的人为胜者;
假设双方都使用最优策略,请判断先手的人会赢还是后手的人会赢。
Input
输入数据包含多个测试用例,每个测试用例占一行,包含3个整数 m,n,p(1<=m,n,p<=1000)。
m=n=p=0则表示输入结束。
Output
如果先手的人能赢,请输出“Fibo”,否则请输出“Nacci”,每个实例的输出占一行。
Sample Input
1 1 1
1 4 1
0 0 0
Sample Output
Fibo
Nacci
模板题:把 s[k] 数组 换成 斐波那契数就行
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<string>
#include<cmath>
#include<set>
#include<map>
#include<stack>
#include<queue>
#include<ctype.h>
#include<vector>
#include<algorithm>
#include<sstream>
#define PI acos(-1.0)
using namespace std;
typedef long long ll;
const int maxn = 1010;
const int k = 20;
int sg[maxn];
int s[k];
bool vis[maxn];
void get_sg()
{
int i,j;
s[0] = s[1] = 1;
for(i = 2;i<17;i++)
s[i] = s[i-1] + s[i-2];
//cout<<s[16]<<endl;
for(i = 0;i<maxn;i++){
memset(vis,false,sizeof(vis));
j = 0;
while(j<k&&s[j]<=i){
vis[sg[i-s[j]]] = true;
j++;
}
for(j = 0;j<maxn;j++){
if(!vis[j]){
sg[i] = j;
break;
}
}
}
}
int main(void)
{
int m,n,p;
memset(sg,-1,sizeof(sg));
get_sg();
while(~scanf("%d%d%d",&m,&n,&p)&&(m+n+p)){
int num = sg[m] ^ sg[n] ^ sg[p];
if(num==0) printf("Nacci\n");
else printf("Fibo\n");
}
}