一、 基本概念:
1. Impartial Combinatorial Games(ICG) 公平组合游戏:
1. 两名选手
2. 交替进行某种游戏规定的操作,每操作一次,选手可以在有限的操作(操作必须合法)集合中任选一种。
3. 对于游戏的任何一种可能的局面,合法操作集合只取决于这个局面本身,不取决于其它因素(跟选手,以前的所有操作无关)
4. 如果当前选手无法进行合法的操作,即合法集合为空,则为负。
2. P-position 和 N-position 的定义与性质:
P-position:后手必胜
N-position:先手必胜
定义:
1.无法进行任何移动的局面是P-position。
2.可以移动到P-position的局面是N-position。
3.所有移动都导致N-position的局面是P-position。性质:
1. 所有的终结点都是P-position。
2. 从任何N-position操作,至少有一种方式进入P-position。
3. 无论如何操作, 从P-position都只能进入N-position。
3. mex 运算 (minimal excludant 最小排除运算):
最小的不属于这个集合的非负整数。
如 mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
4. SG函数:
设x为任一对局状态,S是该状态的后续对局集合
则SG(x) = mex(S)
如 x 有三个后继状态分别为 SG(a),SG(b),SG(c)
那么SG(x) =mex{SG(a)SG(b),SG(c)}。(某一状态的SG就是对后续对局状态进行最小排除的值)集合S的终态必然是空集,所以SG函数的终态为 SG(x)=0,为P-position
SG函数实例部分可参考:https://blog.csdn.net/bestsort/article/details/88197959
可以看到SG函数的值有递推性质。
5. 尼姆(Nim)博弈:
有任意堆物品,每堆物品的个数是任意的,双方轮流从中取物品,每一次只能从一堆物品中取部分或全部物品,最少取一件,取到最后一件物品的人获胜(即无物品可取的人失败)。
6. 反尼姆(anti-Nim)博弈:
在尼姆博奕中取完最后一颗石子的人为赢家,而取到最后一颗石子为输家的就是反尼姆博奕。
二、结论及算法:
1. SG函数:
(打表)
//f[N]:可改变当前状态的方式,N为方式的种类,f[N]要在getSG之前先预处理 //SG[]:0~n的SG函数值 //S[]:为x后继状态的集合 int f[N],SG[MAXN],S[MAXN]; void getSG(int n){ int i,j; memset(SG,0,sizeof(SG)); //因为SG[0]始终等于0,所以i从1开始 for(i = 1; i <= n; i++){ //每一次都要将上一状态 的 后继集合 重置 memset(S,0,sizeof(S)); for(j = 0; f[j] <= i && j <= N; j++) S[SG[i-f[j]]] = 1; //将后继状态的SG函数值进行标记 for(j = 0;; j++) if(!S[j]){ //查询当前后继状态SG值中最小的非零值 SG[i] = j; break; } } }
(DFS)
//注意 S数组要按从小到大排序 SG函数要初始化为-1 对于每个集合只需初始化1遍 //n是集合s的大小 S[i]是定义的特殊取法规则的数组 int s[110],sg[10010],n; int SG_dfs(int x) { int i; if(sg[x]!=-1) return sg[x]; bool vis[110]; memset(vis,0,sizeof(vis)); for(i=0;i<n;i++) { if(x>=s[i]) { SG_dfs(x-s[i]); vis[sg[x-s[i]]]=1; } } int e; for(i=0;;i++) if(!vis[i]) { e=i; break; } return sg[x]=e; }
2. Nim博弈:
对于一个Nim游戏的局面(a1,a2,...,an),它是P-position当且仅当a1^a2^...^an=0,其中^表示位异或(xor)运算。
3. anti-Nim博弈:
在反尼姆博奕中判断必胜局面的条件有两点,满足任意一点先手都能取胜,即必胜局面(N-position)。
1. 各堆石子数目异或结果不等于0,且存在有石子数目大于1的石子堆。
2. 各堆石子数目异或结果等于0,且所有石子堆数目全部为1。
4. ICG的解题模型:
把一个复杂游戏分解成多个独立的子游戏,分别考虑每一个子游戏,计算其SG值,则原游戏的SG值就是所有子游戏的SG函数值的异或,即sg(G)=sg(G1)^sg(G2)^...^sg(Gn)。
SG值的计算方法:
1. 可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
2. 可选步数为任意步,SG(x) = x;
3. 可选步数为一系列不连续的数,用上述模板计算。
原理证明部分可参照:https://blog.csdn.net/strangedbly/article/details/51137432
三、例题:
1. S-Nim:http://acm.hdu.edu.cn/showproblem.php?pid=1536
模板题,对于每组数据更新一下f[i]即可。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int N=100+5;
const int M=1e4+5;
int numOfF, f[N];
int m, l;
int re;
int sg[M];
bool s[M]; //注意此处要为bool类型,int类型会超时
void getSG(int numF,int n)
{
memset(sg,0,sizeof(sg));
for(int i=1;i<=n;i++)
{
memset(s,0,sizeof(s));
for(int j=1;f[j]<=i && j<=numF;j++)
{
s[sg[i-f[j]]]=1;
}
for(int j=0;;j++)
{
if(s[j]==0)
{
sg[i]=j;
break;
}
}
}
}
int main()
{
while(scanf("%d",&numOfF)!=-1)
{
if(numOfF==0) break;
for(int i=1;i<=numOfF;i++) scanf("%d",&f[i]);
sort(f+1, f+1+numOfF);
getSG(numOfF, M);
scanf("%d",&m);
for(int i=1;i<=m;i++)
{
scanf("%d",&l);
int x;
re=0;
for(int i=1;i<=l;i++)
{
scanf("%d",&x);
re^=sg[x];
}
if(re==0) printf("L"); //P-position
else printf("W");
}
printf("\n");
}
}
2. Northcott Game:http://acm.hdu.edu.cn/showproblem.php?pid=1730
把两个棋子之间的距离当作一堆石子来看待,可以看出当两个棋子之间距离为0时是P-position。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int main()
{
int n,m;
int x,y;
int p,re;
while(scanf("%d%d",&n,&m)!=-1)
{
re=0;
for(int i=1;i<=n;i++)
{
scanf("%d%d",&x,&y);
p=abs(x-y)-1;
re^=p;
}
if(re==0) printf("BAD LUCK!\n"); //P-position
else printf("I WIN!\n");
}
}
3. Fibonacci again and again:http://acm.hdu.edu.cn/showproblem.php?pid=1848
计算或者直接给出f[ ]的值,套用模板即可。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
const int N=1e3+5;
int a[5];
int f[16]={1,1,2,3,5,8,13,21,34,55,89,144,233,377,610,987};
bool s[N];
int sg[N];
void getSG(int numF,int n)
{
memset(sg,0,sizeof(sg));
for(int i=1;i<=n;i++)
{
memset(s,0,sizeof(s));
for(int j=1;f[j]<=i && j<=numF;j++)
s[sg[i-f[j]]]=1;
for(int j=0;j<=n;j++)
{
if(s[j]==0)
{
sg[i]=j;
break;
}
}
}
}
int main()
{
getSG(15, N);
int re;
while(scanf("%d%d%d",&a[0],&a[1],&a[2])!=-1)
{
re=0;
if(a[0]+a[1]+a[2]==0) break;
re^=sg[a[0]]^sg[a[1]]^sg[a[2]];
if(re==0) printf("Nacci\n"); //P-position
else printf("Fibo\n");
}
}
4. Rabbit and Grass:http://acm.hdu.edu.cn/showproblem.php?pid=1849
把每一个棋子看成一堆石子即可。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int main()
{
int m;
while(scanf("%d",&m)!=-1)
{
if(m==0) break;
int x;
int re=0;
while(m--)
{
scanf("%d",&x);
re^=x;
}
if(re==0) printf("Grass Win!\n"); //P-positon
else printf("Rabbit Win!\n");
}
}
5. John:http://acm.hdu.edu.cn/showproblem.php?pid=1907
反尼姆博弈模板题。
#include <iostream>
#include <stdio.h>
using namespace std;
int main()
{
int t;
int n;
cin>>t;
while(t--)
{
scanf("%d",&n);
int flag=0;
int re=0;
while(n--)
{
int x;
scanf("%d",&x);
if(x>1) flag=1;
re^=x;
}
if(re==0 && flag==0 || re!=0 && flag==1)
printf("John\n"); //N-position
else
printf("Brother\n");
}
}
6. Be the Winner:http://acm.hdu.edu.cn/showproblem.php?pid=2509
反尼姆博弈。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int main()
{
int n;
while(scanf("%d",&n)!=-1)
{
int x;
int re=0;
int flag=0;
while(n--)
{
scanf("%d",&x);
if(x>1) flag=1;
re^=x;
}
if(re==0 && flag==0 || re!=0 && flag==1)
{
printf("Yes\n");
}
else printf("No\n");
}
}
7. Digital Deletions:http://acm.hdu.edu.cn/showproblem.php?pid=1404
根据上述P-position和N-position的性质暴力出所有的N-position即可。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
#include <math.h>
using namespace std;
const int N=1e6+5;
char s[10];
bool sg[N];
int getLen(int x)
{
for(int i=6;i>=1;i--)
{
int p=pow(10,i-1);
if(x/p) return i;
}
}
void getSG(int x)
{
int len=getLen(x);
for(int i=1;i<=len;i++)
{
int p=pow(10,i-1);
int w=x/p%10;
int s=x;
for(int j=w;j<9;j++) //该位0-9都是N-position
{
s+=p;
if(s<1e6) sg[s]=1;
}
}
int s=x,p=1;
while(len<6) //位数小于6直接抹0达到 N-position
{
s*=10;
for(int i=0;i<p;i++)
{
if(s+i<1e6) sg[s+i]=1;
}
p*=10;
len++;
}
}
int main()
{
memset(sg,0,sizeof(sg));
sg[0]=1; //N-position
for(int i=1;i<1e6;i++)
{
if(sg[i]==0) getSG(i); //P-position
}
while(gets(s))
{
if(s[0]=='0') printf("Yes\n");
else
{
int num=atoi(s);
if(sg[num]!=0) printf("Yes\n"); //N-position
else printf("No\n");
}
}
}
8. Bomb Game:http://acm.hdu.edu.cn/showproblem.php?pid=2873
二维SG,炸弹爆炸后的sg可看做新产生的两个炸弹的sg的异或。
其次由棋盘左上角sg[0][0]=0,知边框处sg[i][0]=sg[0][i]=i 。
#include <iostream>
#include <stdio.h>
#include <string.h>
#include <algorithm>
using namespace std;
int sg[55][55];
bool s[3000];
int getSG(int x,int y)
{
memset(s,0,sizeof(s));
for(int i=0;i<x;i++)
{
for(int j=0;j<y;j++)
{
s[sg[x][j]^sg[i][y]]=1;
}
}
for(int i=0;;i++)
{
if(s[i]==0)
{
return i;
}
}
}
void SG()
{
for(int i=0;i<55;i++)
{
sg[i][0]=sg[0][i]=i;
}
for(int i=1;i<55;i++)
{
for(int j=1;j<55;j++)
{
sg[i][j]=getSG(i,j);
}
}
}
int main()
{
SG();
int n,m;
char in[55][55];
while(scanf("%d%d",&n,&m)!=-1)
{
if(n==0 && m==0) break;
int re=0;
for(int i=0;i<n;i++)
{
scanf("%s",in[i]);
for(int j=0;j<m;j++)
if(in[i][j]=='#')
re^=sg[i][j];
}
if(re!=0) printf("John\n"); //N-position
else printf("Jack\n");
}
}