我是从西工大的全国邀请赛上了解到SG这个东西的(那一场真的是千里送人头,连SG打表都不会的菜鸡真的绝望),回来疯狂补有关SG的知识,还是一知半解的,或许是我太菜了吧直到现在才明白过来(手动滑稽),所以决定补一篇关于SG的博客。
首先谈到SG无可避免的要谈到NIM问题,简单介绍一下:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。
那么我们如何胜呢,博弈的双方都会以使自己获胜的最优策略进行游戏,我们不妨从最简单的情况开始,当一方面对只剩一堆石子时其一定会获胜,那么如果面对的是两堆石子,必胜策略是将多的一堆取到两堆相等,接着无论后手取多少先手会对另一堆取相同的个数使对手面对的一直是相等的局面,这样既可以取得胜利,如果一开始面对的个数相同,那么你就是上述策略中的后手(你凉了),到三堆的情况发现什么了吗,没错每种情况的胜负是与先手后手有关的,但明显不会有非常简单的规律让你找(笑)。
那么我们定义两种情况先手(First)和后手(Second),为了方便进行状态调整我们将也可以将先手状态理解为其处于一种必胜态(膜一下伟奇书记),后手在这一步不会胜利所以是一种必败态,那么我们可以得到其状态转移之间的性质:首先导致达到必败态的一定是一种必胜态(关键看必胜态能不能保存),其次到达必败态的前一种情况一定是必胜态的所有移动都导致必败。
证明,,,,emmmmmmm,在下太菜了,不过还是不难理解的,我们不妨还是以NIM的两堆情况为例,对(n,n)的情况,有(0,n)(1,n)……(n-1,3)(显然交换石子堆的位置不影响其性质,所以把(x,y)和(y,x)看成同一种局面)的下一步(即子状态),对(0,n)明显是一种必胜态,(1,n)的后继中(1,1)是必败态(因为(1,1)的唯一子局面(0,1)是必胜态),所以(1,n)也是必胜态。同样可以证明一直到(n-1,n)都是必胜态。所以(n,n)的所有子局面都是必胜,它就是必败。
可见每一个情况都可以由其后继情况推导出来,利用递归计算它的所有子局面的性质,所以可以用DP或者记忆化搜索的方法以提高效率,那么我们这么做怎么样呢,或许简单情况还可以但对NIM很明显即使用记忆化依旧无法避免计算计算O(a1*a2*…*an)个局面的情况,并且如果将NIM游戏的条件加强很可能无法解决,这时候我们需要更加普世性的模型(救赎终生什么的666)。
首先定义1.两名选手交替对游戏进行移动(move),每次一步,选手可以在(一般而言)有限的合法移动集合中任选一种进行移动;2.对于游戏的任何一种可能的局面,合法的移动集合只取决于这个局面本身,不取决于轮到哪名选手操作,满足这两条的都是ICG游戏,任何一个ICG都可以通过把每个局面看成一个顶点,对每个局面和它的子局面连一条有向边来抽象成一个“有向图游戏”即一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,无法移动者判负。
下面我们就要请出今日的主角“SG函数”:
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。
对于一个给定的有向无环图,定义关于图的每个顶点的Sprague-Garundy函数g如下:g(x)=mex{ g(y) | y是x的后继 }。
没搞懂吧,没搞懂就对了(笑)。
先来看一下SG函数的性质:
首先,所有的必败态所对应的顶点,也就是没有出边的顶点,其SG值为0,因为它的后继集合是空集。
对于一个g(x)=0的顶点x,它的所有后继y都满足g(y)!=0。 F(必胜态)
对于一个g(x)!=0的顶点,必定存在一个后继y满足g(y)=0。 S(必败态)
以上三个性质表明,顶点x所代表的postion是比败态当且仅当g(x)=0(跟必败态/必胜态的定义的那三句话是完全对应的)。我们通过计算有向无环图的每个顶点的SG值,就可以对每种局面找到必胜策略了。但SG函数的用途远没有这样简单。如果将有向图游戏变复杂一点,比如说,有向图上并不是只有一枚棋子,而是有n枚棋子,每次可以任选一颗进行移动,这时,怎样找到必胜策略呢?
对SG函数有
当 g(x)=k时,有对任意的0<=i<k, 都存在x的一个后继y满足 g(y)=i。
当某枚棋子的SG值是k时,我们可以把它变成0、变成1、……、变成k-1,但绝对不能保持k不变。不知道你能不能根据这个联想到Nim游戏,Nim游戏的规则就是:每次选择一堆数量为k的石子,可以把它变成0、变成1、……、变成k-1,但绝对不能保持k不变。这表明,如果将n枚棋子所在的顶点的SG值看作n堆相应数量的石子,那么这个Nim游戏的每个必胜策略都对应于原来这n枚棋子的必胜策略!
对于n个棋子,设它们对应的顶点的SG值分别为(a1,a2,…,an),再设局面(a1,a2,…,an)时的Nim游戏的一种必胜策略是把ai变成k,那么原游戏的一种必胜策略就是把第i枚棋子移动到一个SG值为k的顶点。
我们可以定义有向图游戏的和(Sum of Graph Games):设G1、G2、……、Gn是n个有向图游戏,定义游戏G是G1、G2、……、Gn的和(Sum),游戏G的移动规则是:任选一个子游戏Gi并移动上面的棋子。Sprague-Grundy Theorem就是:g(G)=g(G1)^g(G2)^…^g(Gn)。也就是说,游戏的和的SG函数值是它的所有子游戏的SG函数值的异或。
由于任何一个ICG都可以抽象成一个有向图游戏。所以“SG函数”和“游戏的和”的概念就不是局限于有向图游戏。我们给每个ICG的每个position定义SG值,也可以定义n个ICG的和。所以说当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略了!(Nim其实就是n个从一堆中拿石子的游戏求SG的变型,总SG=n个sg的异或)。
举个例子:有n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗……我们可以把它看作3个子游戏,第1个子游戏只有一堆石子,每次可以取1、2、3颗,很容易(看pic2)看出x%4==0时处于P局面,即x颗石子的局面的SG值是x%4,(即把.中的值改成原值%4)。第2个子游戏也是只有一堆石子,每次可以取奇数颗,经过简单的画图可以知道这个游戏有x颗石子时的SG值是x%2。第3个游戏有n-2堆石子,就是一个Nim游戏。对于原游戏的每个局面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,然后就可以根据这个SG值判断是否有必胜策略以及做出决策了。
SG函数使用方法:
1.把原游戏分解成多个独立的子游戏,则原游戏的SG函数值是它的所有子游戏的SG函数值的异或。
即sg(G)=sg(G1)^sg(G2)^…^sg(Gn)。
2.分别考虑没一个子游戏,计算其SG值。
SG值的计算方法:
1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
2.可选步数为任意步,SG(x) = x;
3.可选步数为一系列不连续的数,用模板计算。
模板一(打表):
//f[]:可以取走的石子个数
//sg[]:0~n的SG函数值
//hash[]:mex{}
int f[N],sg[N],hash[N];
void getSG(int n)
{
int i,j;
memset(sg,0,sizeof(sg));
for(i=1;i<=n;i++)
{
memset(hash,0,sizeof(hash));
for(j=1;f[j]<=i;j++)
hash[sg[i-f[j]]]=1;
for(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(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;
}
整体效果用打表是这样的
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++)
if(!vis[j])
{
sg[i]=j;
break;
}
}
}
int main()
{
...
memset(sg,-1,sizeof(sg));
get_sg();
if(sg[n]==0) //先手必败
else //先手必胜
//如果有多堆,则
// num=sg[n1]^sg[n2]^sg[n3]^....^sg[nx];
// if(num==0) 则先手必败
// else 先手必胜
...
}