SG函数
\(SG\) 函数
本文借鉴了自为风月马前卒的博客
基本定理:
\(ICG\) 游戏:
- 游戏两人轮流,并且决策最佳
- 无法决策时游戏结束,并且在有限步内结束。
- 同一个状态不能多次表达,且没有平局出现。
- 游戏者在任意状态做出的决策和自己无关,只与当前状态有关。
满足以上条件就为 \(ICG\) 游戏,属于组合游戏。
\(nim\) 游戏就是一种 \(ICG\) 游戏。
必胜和必败:
- 无法移动的状态为必败 \(P\)
- 可以移动到必败( \(P\) )的局面为必胜( \(N\) )
- 所有移动都会进入 \(N\) 的局面为 \(P\).
\(DAG\) 中的博弈
定义:给定一张有向无环图,起始点有一个棋子,轮流挪动,不能移动的人算输。
事实上,所有的 \(ICG\) 游戏都可以抽象成为这种游戏。
即把一个状态向另一个状态连边,判断状态合法性
正式介绍:
对于给定的有向无环图,定义每个点的 \(SG\) 函数为:
\(mex\) 就是最小的不属集合的非负整数。
性质:
- 所有汇点的 \(SG\) 函数为 \(0\),因为所有汇点后继状态都是空集。
- \(SG(x)=0\) 该节点为必败点。
此性质决定了该节点后继节点 \(SG\) 值不为 \(0\),满足必败点定义 - \(SG(x)!=0\) 该节点为必胜点。
此性质决定了该节点的后继节点中一定有一个节点 \(SG(y)=0\) ,满足必胜点定义。
这个问题实际上就是巴什博弈。
推导:
如果这个棋盘上有 \(n\) 个棋子的时候呢?
我们当 \(SG(x)=k\) 表明后继状态有 \(SG(y)=[1,k-1]\)
也就是说,我们从 \(k\) 可以转移到 \([1,k-1]\) 的任何一个状态,当前共有 \(n\) 个棋子。
是不是像 \(nim\) 游戏?
因此,在 \(nim\) 游戏中,\(SG\) 函数异或值不为零,就先手必胜。
推广:
我们设想:游戏的和的 \(SG\) 值是他们 \(SG\) 值的异或和。
我们可以将所有的 \(ICG\) 问题对应到 \(DAG\) 上,然后通过 \(SG\) 函数之间的转移解决所有问题。
所以说当我们面对由 \(n\) 个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,就可以把这些 \(SG\) 值全部看成 \(Nim\) 的石子堆,
然后依照找 \(Nim\) 的必胜策略的方法来找这个游戏的必胜策略了!( \(Nim\) 其实就是 \(n\) 个从一堆中拿石子的游戏求 \(SG\) 的变型,总 \(SG = n\) 个 \(sg\) 的异或.
解题模型:
把原游戏分解成独立的子游戏,原游戏的 \(SG\) 函数值是它所有子游戏的 \(SG\) 函数值异或和。
考虑分别设计每一个子游戏:
计算方法:
- 可选步数为 \([1,m]\) 的连续整数,直接取模即可 \(SG(x)=x\%(m+1)\)
- 可选择步数为任意步 \(SG(x)=x\)
- 可选步数为一系列不连续的数,用模板计算。
模板
- 打表
//f[]:可以取走的石子个数
//sg[]:0~n的SG函数值
//hash[]:mex{}
int f[N],sg[N],hash[N];
void getSG(int n){
memset(sg,0,sizeof(sg));
for(int i=1;i<=n;i++){
memset(hash,0,sizeof(hash));
for(int j=1;f[j]<=i;j++)
hash[sg[i-f[j]]]=1;
for(int j=0;j<=n;j++) //求mes{}中未出现的最小的非负整数
if(hash[j]==0){ 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(int i=0;i<n;i++)
if(x>=s[i]){
SG_dfs(x-s[i]);
vis[sg[x-s[i]]]=1;
}
int e;
for(int i=0;;i++)
if(!vis[i]){ e=i; break; }
return sg[x]=e;
}
- 计算:
如果 \(SG(x)=0\) ,就先手必败。
例题:
每组石子是一个相对独立的游戏,对于每组两堆石子的个数暴力计算SG函数并打表.
发现后继状态 \(SG\) 值集合 \(S\) 用二进制表示的结果为 \((x_1−1)|(x_2−1)\)。
因此,通过 \(SG\) 函数的性质,即可算出这道题。
#include<bits/stdc++.h>
using namespace std;
int T,n;
int main(){
cin>>T;
while(T--){
cin>>n; n>>=1;
int sg=0;
for(int i=0,x,y;i<n;i++){
scanf("%d%d",&x,&y);
int s=(x-1)|(y-1),mex=0;
for(;s&1;s>>=1) mex++;
sg^=mex;
}
puts(sg?"YES":"NO");
}
system("pause");
return 0;
}