SG函数

简述

想要学好博弈论,不只是要了解巴什博奕、威佐夫博奕等等典型的博弈问题,这些都只是基础,而SG函数,是我们想要走向博弈大师必须掌握的知识,接下来的分析,可能枯燥无味,但是真正看懂之后,会受益匪浅。

分析

现在我们来研究一个看上去似乎更为一般的游戏:给定一个有向无环图和一个起始顶点上的一枚棋子,两名选手交替的将这枚棋子沿有向边进行移动,无法移动者判 负。事实上,这个游戏可以认为是所有Impartial Combinatorial Games(公平组合游戏,以下简称ICG)的抽象模型。也就是说,任何一个ICG都可以通过把每个局面看成一个顶点,对每个局面和它的子局面连一条有向边来抽象成这个“有向图游戏”。下 面我们就在有向无环图的顶点上定义Sprague-Garundy函数。

首先定义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函数的性质。首先,所有的terminal position所对应的顶点,也就是没有出边的顶点,其SG值为0,因为它的后继集合是空集。然后对于一个g(x)=0的顶点x,它的所有后继y都满足 g(y)!=0。对于一个g(x)!=0的顶点,必定存在一个后继y满足g(y)=0。

以上这三句话表明,顶点x所代表的postion是P-position当且仅当g(x)=0(跟P-positioin/N-position的定义的 那三句话是完全对应的)。我们通过计算有向无环图的每个顶点的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的顶点。这听上去有点过于神奇——怎么绕了一圈又回到Nim游戏上了。

其实我们还是只要证明这种多棋子的有向图游戏的局面是P-position当且仅当所有棋子所在的位置的SG函数的异或为0。这个证明与上节的 Bouton's Theorem几乎是完全相同的,只需要适当的改几个名词就行了。

刚才,我为了使问题看上去更容易一些,认为n枚棋子是在一个有向图上移动。但如果不是在一个有向图上,而是每个棋子在一个有向图上,每次可以任选一个棋子 (也就是任选一个有向图)进行移动,这样也不会给结论带来任何变化。

所以我们可以定义有向图游戏的和(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堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗……我们可 以把它看作3个子游戏,第1个子游戏只有一堆石子,每次可以取1、2、3颗,很容易看出x颗石子的局面的SG值是x%4。第2个子游戏也是只有一堆石子, 每次可以取奇数颗,经过简单的画图可以知道这个游戏有x颗石子时的SG值是x%2。第3个游戏有n-2堆石子,就是一个Nim游戏。对于原游戏的每个局 面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,然后就可以根据这个SG值判断是否有必胜策略以及做出决策了。其实看作3个子游戏还是保守了 些,干脆看作n个子游戏,其中第1、2个子游戏如上所述,第3个及以后的子游戏都是“1堆石子,每次取几颗都可以”,称为“任取石子游戏”,这个超简单的 游戏有x颗石子的SG值显然就是x。其实,n堆石子的Nim游戏本身不就是n个“任取石子游戏”的和吗?

所以,对于我们来说,SG函数与“游戏的和”的概念不是让我们去组合、制造稀奇古怪的游戏,而是把遇到的看上去有些复杂的游戏试图分成若干个子游戏,对于 每个比原游戏简化很多的子游戏找出它的SG函数,然后全部异或起来就得到了原游戏的SG函数,就可以解决原游戏了。

模版

  • 计算从1-n范围内的SG值
/*
Array(存储可以走的步数,Array[0]表示可以有多少种走法)
Array[]需要从小到大排序
1.可选步数为1-m的连续整数,直接取模即可,SG(x)=x%(m+1);
2.可选步数为任意步,SG(x) = x;
3.可选步数为一系列不连续的数,用GetSG(计算)
*/
int SG[MAX],hash[MAX];
void init(int Array[],int n)
{
    int i,j;
    memset(SG,0,sizeof(SG));
    for(i=0; i<=n; i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1; j<=Array[0]; j++)
        {
            if(i<Array[j])
                break;
            hash[SG[i-Array[j]]]=1;
        }
        for(j=0; j<=n; j++)
        {
            if(hash[j]==0)
            {
                SG[i]=j;
                break;
            }
        }
    }
}

有些时候预处理出来所有的SG值会超时,而且有些SG值是用不到的,这时候我们会用到下面的模版,获得单独的一个SG值。

  • 获得一个单独的SG值
int s[101],sg[10001],k; //k为可走步数,s数组存储可走步数(0~k-1)
int getsg(int m)
{
    int hash[101]={0};
    int i;
    for(i=0; i<k; i++)
    {
        if(m-s[i]<0)
            break;
        if(sg[m-s[i]]==-1)
            sg[m-s[i]]=getsg(m-s[i]);
        hash[sg[m-s[i]]]=1;
    }
    for(i=0;; i++)
        if(hash[i]==0)
            return i;
}

例题

例题1.hdu 1848

题意:

  • 这是一个二人游戏;
  • 一共有3堆石子,数量分别是m, n, p个;
  • 两人轮流走;
  • 每走一步可以选择任意一堆石子,然后取走f个;
  • f只能是菲波那契数列中的元素(即每次只能取1,2,3,5,8…等数量);
  • 最先取光所有石子的人为胜者;
  • 假设双方都使用最优策略,请判断先手的人会赢还是后手的人会赢。

要求:Time Limit: 1000 MS , Memory Limit: 32768 K
思路:乍一瞅,像不像Nim博弈?不过单纯的Nim博弈无法解决它,我们要用到这一节学到的SG函数,套用上面的模版,相信大家能看明白代码~

#include <iostream>
using namespace std;
#define MAX 1005
#include<cstdio>
#include<cstring>

int SG[MAX], hash[MAX];
void init(int Array[], int n)
{
    int i, j;
    memset(SG, 0, sizeof(SG));
    for(i = 0; i <= n; i++)
    {
        memset(hash, 0, sizeof(hash));
        for(j = 1; j <= Array[0]; j++)
        {
            if(i < Array[j])
                break;
            hash[SG[i - Array[j]]] = 1;
        }
        for(j = 0; j <= n; j++)
        {
            if(hash[j] == 0)
            {
                SG[i] = j;
                break;
            }
        }
    }
}

int main()
{
    int m,n,p;
    int fibo[30];
    fibo[0]=19;
    fibo[1]=1;
    fibo[2]=1;
    for(int i=3;i<=19;i++)
        fibo[i]=fibo[i-1]+fibo[i-2];
    init(fibo,1000);
    //puts("lalal");
    while(~scanf("%d%d%d",&m,&n,&p))
    {
        if(m==0&&n==0&&p==0)
            return 0;
        if((SG[m]^SG[n]^SG[p])==0)
            printf("Nacci\n");
        else
            printf("Fibo\n");
    }
    return 0;
}

例题2.hdu 1846

题意:本游戏是一个二人游戏,有一堆石子一共有n个,两人轮流进行,每走一步可以取走1…m个石子,最先取光石子的一方为胜,如果游戏的双方使用的都是最优策略,请输出哪个人能赢。
要求:Time Limit: 1000 MS , Memory Limit: 32768 K
思路:大家看见这道题的题面一定觉得特别熟悉,没错,这道题就是巴什博奕中的例题1,在这里我们用SG函数的方法来解决这道题。在第一个模版中的注释中的第一条中我们可以看到,可选步数为~的连续整数,直接取模即可,%,所以这道巴什博奕的题我们能够直接得到SG值,从而解决它!

#include<cstdio>
#include<cstring>

int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n,m;
        scanf("%d%d",&n,&m);
        int SG[1500];
        memset(SG,0,sizeof(SG));
        for(int i=1;i<=n;i++)
            SG[i]=i%(m+1);
        if(SG[n])
            printf("first\n");
        else
            printf("second\n");
    }
    return 0;
}

例题3.hdu 1536

题意:

  • 输入K表示一个集合的大小,之后输入集合表示对于这对石子只能去这个集合中的元素的个数
  • 输入一个m表示接下来对于这个集合要进行m次询问
  • 接下来m行 每行输入一个n,表示有n个堆,每堆有ni个石子,问这一行所表示的状态是赢还是输 如果赢输出W否则L

要求:Time Limit: 1000 MS , Memory Limit: 32768 K
思路:如果做明白了前面两个例题,大家很容易就能想到,无非是多调用几次SG函数的模版啊,年少无知的我当时也是这么想的,但是。。。超时了~所以我们要动用第二个模版啦,采用类似于“记忆化搜索”的方式,用到哪个SG值,现求就好了,求过的就存好,下一次用到直接调用,这样能省去不少时间。
超时代码:

#include<cstdio>
#include<cstring>

int SG[10005],hash[10005];
void init(int Array[],int n)
{
    int i,j;
    memset(SG,0,sizeof(SG));
    for(i=0; i<=n; i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1; j<=Array[0]; j++)
        {
            if(i<Array[j])
                break;
            hash[SG[i-Array[j]]]=1;
        }
        for(j=0; j<=n; j++)
        {
            if(hash[j]==0)
            {
                SG[i]=j;
                break;
            }
        }
    }
}

int main()
{
    int snum;
    while(scanf("%d",&snum))
    {
        if(snum==0)
            return 0;
        int s[10005];
        memset(s,0,sizeof(s));
        s[0]=snum;
        for(int i=1; i<=snum; i++)
            scanf("%d",&s[i]);
        //init(s,10005);
        int n;
        scanf("%d",&n);
        memset(SG,0,sizeof(SG));
        for(int i=1; i<=n; i++)
        {
            int nn;
            scanf("%d",&nn);
            int sum=0;
            while(nn--)
            {
                int nnn;
                scanf("%d",&nnn);
                if(SG[nnn]==0)
                    init(s,nnn);
                sum^=SG[nnn];
            }
            if(sum)
                printf("W");
            else
                printf("L");
        }
        printf("\n");
    }
    return 0;
}

正确代码:

#include<cstdio>
#include<cstring>
#include<algorithm>

using namespace std;
int sg[10005],hash[10005];
int s[100005];
int k;
int getsg(int m)
{
    int hash[101]= {0};
    int i;
    for(i=0; i<k; i++)
    {
        if(m-s[i]<0)
            break;
        if(sg[m-s[i]]==-1)
            sg[m-s[i]]=getsg(m-s[i]);
        hash[sg[m-s[i]]]=1;
    }
    for(i=0;; i++)
        if(hash[i]==0)
            return i;
}

int main()
{
    int snum;
    while(scanf("%d",&snum))
    {
        k=snum;
        if(snum==0)
            return 0;
        memset(s,0,sizeof(s));
        for(int i=0; i<snum; i++)
            scanf("%d",&s[i]);
        sort(s,s+k);
        //init(s,10005);
        int n;
        scanf("%d",&n);
        memset(sg,-1,sizeof(sg));
        for(int i=1; i<=n; i++)
        {
            int nn;
            scanf("%d",&nn);
            int sum=0;
            while(nn--)
            {
                int nnn;
                scanf("%d",&nnn);
                if(sg[nnn]==-1)
                    sg[nnn]=getsg(nnn);
                sum^=sg[nnn];
            }
            if(sum)
                printf("W");
            else
                printf("L");
        }
        printf("\n");
    }
    return 0;
}

results matching ""

    No results matching ""