• 博弈论---SG和NIM


    感觉博弈论ACM还蛮经常考,现在我只记得NIM结论似乎不太行

    博弈论就是要静下心一口气先把概念看完才懂啊(所以建议找个时间一口其总结完)

    参考了https://www.cnblogs.com/Knuth/archive/2009/09/05/1561007.html


    NIM简介

    Nim游戏属于“Impartial Combinatorial Games”(以下简称ICG):

    满足以下条件的游戏是ICG(可能不太严谨):

    1、有两名选手;

    2、两名选手交替对游戏进行移动(move),每次一步,选手可以在(一般而言)有限的合法移动集合中任选一种进行移动;

    3、对于游戏的任何一种可能的局面,合法的移动集合只取决于这个局面本身,不取决于轮到哪名选手操作、以前的任何操作、骰子的点数或者其它什么因素;

    4、如果轮到某名选手移动,且这个局面的合法的移动集合为空(也就是说此时无法进行移动),则这名选手负。根据这个定义,很多日常的游戏并非ICG。例如象棋就不满足条件3,因为红方只能移动红子,黑方只能移动黑子,合法的移动集合取决于轮到哪名选手操作。

    就像我们玩的回合制游戏有木有

    任何一个ICG游戏都 有不同的局面,而每一个局面都可以看成一个顶点,该局面到它的子局面 由有向边链接(类比一下下棋)

    可以抽象为:给定一个有向无环图和一个起始顶点,两名选手交替的将这顶点沿有向边进行移动到它的子集,无法移动者判负。

    显然:失败的条件就是该顶点没有子集(无法达到下一个局面)

    现在定义:先手必胜局面(局面)和先手必败局面(顶点):

    1.无法进行任何移动的局面(也就是terminal position 终局)是特殊的P-position(先手必败);2.可以移动到P-position的局面是N-position (先手必胜居局面);3.所有移动都导致N-position的局面是P-position 。

    然后为了方便表示现在就要引进一个mex(minimal excludant)运算:表示mex{....}=最小的不属于这个集合的非负整数

    比如:mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

    现在再定义 SG函数: 对于一个给定的有向无环图,关于图的每个顶点的Sprague-Garundy(sg)函数如下:sg(x)=mex{sg(y) | y是x的后继(子集) }。

    然后这个sg函数有个性质:所有的terminal position所对应的顶点(也就是没有出边的顶点|也是先手必败局面),其sg值为0,因为它的后继集合是空集(mex{}=0)

    然后对于一个sg(x)=0的顶点x,它的所有后继y都满足g(y)!=0。对于一个sg(x)!=0的顶点,必定存在一个后继y满足sg(y)=0。(即能到达先手必败局面|当前局面就是先手必胜局面)

    注意:

    sg=0一定是先手必败局面,但是不一定没有子集,而 terminal-position (终局)没有子集且 sg=0


    sg函数的现实意义:

    如果sg(x)=k,则可以从x到sg(i)=[0,k-1]的一个顶点y(局面):就是一堆有n石子,可以被拿到只剩[0,n-1]个。(是不是很像NIM游戏?)

    但是如果是多堆石子呢,因为任何一个ICG都可以抽象成一个有向图游戏。所以“SG函数”和“游戏的和”的概念就不是局限于有向图游戏。所以说当我们面对由n个游戏组合成的一个游戏时,只需对于每个游戏找出求它的每个局面的SG值的方法,就可以把这些SG值全部看成Nim的石子堆,然后依照找Nim的必胜策略的方法来找这个游戏的必胜策略了!

    发现还是只用NIM的结论

    所以我们可以定义有向图游戏的和(Sum of Graph Games):设G1、G2、……、Gn是n个有向图游戏,定义游戏G是G1、G2、……、Gn的和(Sum),游戏G的移动规则是:任选一个子游戏Gi并移动上面的棋子。Sprague-Grundy Theorem就是:sg(G)=sg(G1)^sg(G2)^…^sg(Gn)。也就是说,游戏的和的SG函数值是它的所有子游戏的SG函数值的异或。

    :每一个子游戏最终都只有一个sg值 如 sg(n)而这个sg值要从sg(0),开始逆推,所以不要误以为是每个节点i的sg(i)值取xor

    一般的求sg(x)函数的套路:

    1. 如果是可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
    2. 如果可选步数为任意步,SG(x) = x;
    3. 如果可选步数为一系列不连续的数,用模板计算(dp|dfs)。

    例:有T堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗……假设第一堆有n个石子,第二堆有m个,其余的有a[1]~a[T-2]个石子,我们可以把它看作3个子游戏,第1个子游戏只有一堆石子,每次可以取1、2、3颗,可以得出sg1[n]值是n%4。第2个子游戏也是只有一堆石子,每次可以取奇数颗,经过简单的画图可以知道这个游戏的sg2[m]值是x%2。第3个游戏有T-2堆石子,就是一个Nim游戏即sg3=sg[a[i]]^sg[a[2]]...。对于原游戏的每个局面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,SG=sg1[n]^sg2[m]^sg3 然后就可以根据这个 if SG ==0 ==>必败,否则必胜

    下面贴一下可以取不连续的数的模板:

    例1:

    这题似乎交不了??!!还是贴一下代码吧

    https://www.luogu.com.cn/problem/UVA1559

    题目大意:若输入n=0代表结束,否则输入n(n<10),s(<2^13),n代表有n*2个人,s代表一共有s个石子 ,然后有n*2个数a1,a2....代表第i个人最多能拿的石子数,你拥有奇数的队员(1,3,5...),每次按顺序来取,不断循环,最后谁的组员把石堆拿走最后一个石子谁输(sg[1]=0,sg[0]=1),若这组数据你能能赢输出1,否则输出0。

    因为只有一堆石子,直接判断正负状态就行了,用记忆化搜索|dp都可以

    #include<cstdio>
    #include<algorithm>
    #include<vector>
    #include<cstring>
    #include<iostream>
    using namespace std;
    const int N=20,M=(2<<13)+1431;
    int n,S,x,sg[N*2][M],a[2*N];//这里的sg实际上是表示输赢0|1 
    int dfs(int x,int sum)
    {
        if(sum==1)
        {
            sg[x][1]=0;
            return 0;
        }
        if(sum<=0)
        {
            sg[x][sum]=1;
            return sg[x][sum];
        }
        if(sg[x][sum]!=-1)
            return sg[x][sum];
        
        sg[x][sum]=0;    //预先假定现在是先手必输局面记 
        for(int i=1;i<=a[x];i++)
        {
            if(dfs((x+1)%2*n,sum-i)==0)//如果下一步必输 则现在是先手必赢局面与假设不成立 
            {
                sg[x][sum]=1; 
                     //因为要把所有状态全部枚举完,所以不能中途return 
            //中途return 会输出 你输入的最末尾的数字 不知为什么 
            }                     
        }
        return sg[x][sum];
    }
    int main()
    {
        while(1)
        {
            memset(sg,-1,sizeof(sg));
            scanf("%d",&n);
            if(n==0)
                break;
            scanf("%d",&S);
            for(int i=0;i<n*2;i++)
            {
                scanf("%d",&a[i]);
            }
            printf("%d
    ",dfs(0,S));
        }
        return 0;
    } 

    例2:

    https://www.luogu.com.cn/problem/P2197

    就是个nim模板

    #include<cstdio>
    #include<algorithm>
    #include<vector>
    #include<cstring>
    #include<iostream>
    using namespace std;
    const int N=10006;
    int n,ans=0,sg[N],x,T,use[N];
    void get_sg(int n)//得到每一个点的sg值 
    {
        memset(sg,0,sizeof(sg));//数组下标不能是负号 
        for(int i=0;i<=n;i++)
        {
            memset(use,0,sizeof(use)); 
            for(int j=1;j<=i;j++)//这里是全部拿走 否则应该是枚举f[...]中的值 
                use[sg[i-j]]=1;    //枚举i子集,并加入集合 
            for(int j=0;j<=n;j++)//找出i这一点的sg值 
            {
                if(use[j]==0)
                {
                    sg[i]=j;     
                    break;
                }
            }
        }
    }
    int main()
    {
        scanf("%d",&T);
        get_sg(N-5);
        while(T--)
        {
            scanf("%d",&n);
            ans=0;
            for(int i=1;i<=n;i++)
            {
                scanf("%d",&x);
                ans=ans^sg[x];
            }
            if(ans>0) //xor的结果不一定是1除非是上题那种直接判断当前sg是P|N局面才是0|1
                printf("Yes
    ");
            else
                printf("No
    ");
        }
        return 0;
    } 

    如果上上题那种直接判断当前sg是P|N局面最后发现结果并不对,意思是sg值和输赢0|1各自的xor并不同 也就是说直接判断输赢只能用于一个游戏中

    n个游戏就应该求出每一个游戏的sg值再xor判断

  • 相关阅读:
    eclipse adt 项目依赖,使用git上的项目
    Fragment用app包还是v4包解析
    nohttp的使用
    安卓学习笔记2
    HashMap和HashSet的区别
    Fragment 和 FragmentActivity的使用
    Android Studio 使用教程
    打开eclipse报错:发现了以元素 'd:skin' 开头的无效内容。此处不应含有子元素。
    java 四种内部类和内部接口
    安卓学习笔记1
  • 原文地址:https://www.cnblogs.com/cherrypill/p/12490073.html
Copyright © 2020-2023  润新知