博弈论SG函数-算法介绍及例题

一、 基本概念:

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");
    }
}
发布了34 篇原创文章 · 获赞 26 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/sinat_40471574/article/details/100051563