• SG函数


    接触了几种基础的博弈论之后,应该多多少少都听过SG函数,SG函数可以解决大多数博弈问题,当然也可以通过SG函数找规律,然后计算结果。

    由于本人愚昧,一直没有体会到SG的精髓,一直半懂不懂的,然后现在终于明白了,所以记录下这个神奇的SG函数。

    SG函数:

    首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

    这一步应该是非常简单的,就是定义了新的运算为mex。

    对于任意状态 x , 定义 SG(x) = mex(F),其中F 是 x 后继状态的SG函数值的集合(就是上述mex中的数值)。最后返回值(也就是SG(X))为0为必败点,不为零必胜点。

    进一步解释一下F,就是题意中给出的可以移动的次数。举个例子来说,一堆石子,每次只能拿1,3,5,7个,那么S数组就是1,3,5,7。

    假如说是在一个游戏中有多个石子堆该怎么办了。我们只需要把对每个石子堆进行sg函数的调用,将得到的所有的值进行异或。得出来的结果为0则该情形为必败态。否则为必胜态。

    代码:

    //HDU 1847 -- Good Luck in CET-4 Everybody!
    #include <iostream>
    #include <cstring>
    #define MAXN 1010
    #define MAXM 11
    using namespace std;
    int sg[MAXN], f[MAXM];
    bool Hash[MAXN];
    
    void getSG(int m)
    {
        memset(sg, 0, sizeof(sg));
        for (int i = 1; i < MAXN; i++)//枚举石子的个数
        {
            memset(Hash, false, sizeof(Hash));
            for (int j = 0; j < m && f[j] <= i; j++)
                Hash[sg[i-f[j]]] = true;//枚举每次拿走的个数并标记 
            for (int j = 0; j < MAXN; j++)
            {
                if (!Hash[j])
                {
                    sg[i] = j; //找到这个F[](该状态可以达到的状态)中不存在的最小的数
                    break;
                }
            }
        }
    }
    
    int main()
    {
        int n, num = 1;
        for (int i = 0; i < MAXM; num <<= 1, i++)
            f[i] = num;//这里的F数组就是可以移动的步数,每次都是2的幂次
        getSG(MAXM);
        while (cin >> n)
        {
            if (sg[n])
                cout << "Kiki" << endl;
            else
                cout << "Cici" << endl;
        }
        return 0;
    }

     HDU 1848 -- Fibonacci again and again (分为三个子游戏,求原游戏sg值):

    #include <iostream>
    #include <cstring>
    #define MAXN 1010
    #define MAXM 100
    using namespace std;
    int sg[MAXN], f[MAXM];
    bool Hash[MAXN];
    
    int getFib() {
        int i;
        f[0] = 1, f[1] = 2;
        for (i = 2; f[i] <= MAXN; i++) f[i] = f[i-1] + f[i-2];
        return i;
    }
    void getSG(int m) {
        memset(sg, 0, sizeof(sg));
        for (int i = 1; i < MAXN; i++) {
            memset(Hash, false, sizeof(Hash));
            for (int j = 0; j < m && f[j] <= i; j++)
                Hash[sg[i-f[j]]] = true;
            for (int j = 0; j < MAXN; j++) {
                if (!Hash[j]) {
                    sg[i] = j;
                    break;
                }
            }
        }
    }
    
    int main() {
        int a, b, c;
        getSG(getFib());
        while (cin >> a >> b >> c && (a || b || c)) {
            if (sg[a] ^ sg[b] ^ sg[c]) cout << "Fibo" << endl;
            else cout << "Nacci" << endl;
        }
        return 0;
    }

    从以上两个题目中可以看出,f数组就是题目描述中的每次可以移动的石子数量,而getSG是相同的,具体怎么标记的可以看第一个例子中的注释。对于多堆石子,就是每一堆进行操作,然后最后将结果异或即可得出最后答案。

    接下来再看几个题目:

    模板题:HDU 1536 -- S-Nim

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #define MAXN 10010  // 最大堆数
    #define MAXM 110    // 最多有MAXM种不同个数的取石子方法
    using namespace std;
    int f[MAXM];   // f为可取石子数的集合
    int sg[MAXN];  // sg[i]表示石子数为i时的sg函数值
    bool Hash[MAXN];  // 标记一个数是否在mex{}集合中出现
    // 打表预处理sg数组
    void getSG(int m)
    {
        memset(sg, 0, sizeof(sg));
        for (int i = 1; i < MAXN; i++)
        {
            memset(Hash, false, sizeof(Hash));
            for (int j = 0; j < m && f[j] <= i; j++)
                Hash[sg[i-f[j]]] = true;  // 当前石子数为i,i-f[i]表示由i所能达到的石子数,将其sg值标记为已出现
            for (int j = 0; j < MAXN; j++)    // mex(minimal excludant)运算
            {
                if (!Hash[j])
                {
                    sg[i] = j;
                    break;
                }
            }
        }
    }
    
    int main()
    {
        int n, m;
        while (cin >> m && m)
        {
            for (int i = 0; i < m; i++) cin >> f[i];
            sort(f, f + m);
            getSG(m);
            cin >> n;
            while (n--)
            {
                int num, sum = 0;
                cin >> num;
                for (int i = 0; i < num; i++)
                {
                    int each;
                    cin >> each;
                    sum ^= sg[each];
                }
                if (sum) cout << 'W';
                else cout << 'L';
            }
            cout << endl;
        }
        return 0;
    }

    SG函数还有一个深搜版本,具体实现和循环差不多。具体如下:

    //注意 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;
    }

    一般DFS只在打表解决不了的情况下用,首选打表预处理。具体用法如下:

    还是HDU 1536 -- S-Nim

    #include <iostream>
    #include <algorithm>
    #include <cstring>
    #define MAXN 10010  // 最大堆数
    #define MAXM 110    // 最多有MAXM种不同个数的取石子方法
    using namespace std;
    int m;
    int f[MAXM];   // f为可取石子数的集合
    int sg[MAXN];  // sg[i]表示石子数为i时的sg函数值
    bool Hash[MAXN];  // 标记一个数是否在mex{}集合中出现
    // 加一个dfs预处理sg数组,注意sg数组需要初始化为-1,而上面打表解法需要初始化为0
    // 一般首选打表预处理,难以打表才用dfs
    int SG_dfs(int x)
    {
        if(sg[x]!=-1)
            return sg[x];
        bool vis[110];
        memset(vis,0,sizeof(vis));
        for(int i=0;i<m;i++)
        {
            if(x>=f[i])
            {
                SG_dfs(x-f[i]);
                vis[sg[x-f[i]]]=1;
            }
        }
        int e;
        for(int i=0;;i++)
            if(!vis[i])
            {
                e=i;
                break;
            }
        return sg[x]=e;
    }
    
    int main()
    {
        while (cin >> m && m)
        {
            for (int i = 0; i < m; i++)
                cin >> f[i];
            sort(f, f + m);
            memset(sg,-1,sizeof(sg));
            int n;
            cin >> n;
            while (n--)
            {
                int num, sum = 0;
                cin >> num;
                for (int i = 0; i < num; i++)
                {
                    int each;
                    cin >> each;
                    sum ^= SG_dfs(each);
                }
                if (sum) cout << 'W';
                else cout << 'L';
            }
            cout << endl;
        }
        return 0;
    }

    LightOJ 1315Game of Hyper Knights,此题打表不好处理,只好DFS。

    #include <iostream>
    #include <cstdio>
    #include <cstring>
    using namespace std;
    int next[6][2]={{-2,1},{1,-2},{-2,-1},{-1,-2},{-3,-1},{-1,-3}};
    #define maxn 1005
    int sg[maxn][maxn];
    int dfs(int x,int y)
    {
        int vis[105]={0}; //注意该数组是一维的 表示该点后继的sg值的情况
        if(sg[x][y]!=-1)
            return sg[x][y];
        for(int i=0;i<6;i++)
        {
            int nx=x+next[i][0];
            int ny=y+next[i][1];
            if(nx>=0&&ny>=0) //注意不能不加符号就判断 等于0也是算在内的
                vis[dfs(nx,ny)]=1; //因为可能走到的点的sg值还没有求过 所以要用递归深搜
                //之前写的非递归是因为之前的sg值都求过了 不用搜索也可以
        }
        for(int j=0;j<100;j++)
        if(!vis[j]) return sg[x][y]=j;
    }
    int main()
    {
        memset(sg,-1,sizeof(sg)); //这里定义成-1 比0 好 因为有的就是0 if的时候0还要再算一次浪费时间
        int t,cas=1;
        cin>>t;
        while(t--)
        {
            int n,x,y,ans=0;
            cin>>n;
            for(int i=0;i<n;i++)
            {
                cin>>x>>y;
                ans^=dfs(x,y);
            }
            printf("Case %d: %s
    ",cas++,ans?"Alice":"Bob");
        }
        return 0;
    }

    就先介绍到这,以后再慢慢补坑。

  • 相关阅读:
    SqlMembershipProvider的配置
    自定义HtppHandler和HttpModule
    [导入]Sql Server 2005 Express中配置用户
    [导入]Asp.net中使用客户端脚本的方法
    JumpServer 架构浅解
    你准备好开始DevOps了吗?
    为什么是戒了爱你
    今天考试了
    [心疼女友]
    这个世界看不清
  • 原文地址:https://www.cnblogs.com/aiguona/p/9126324.html
Copyright © 2020-2023  润新知