博弈论---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\n",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\n");
        else
            printf("No\n");
    }
    return 0;
} 

 

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

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

posted @ 2020-03-14 00:05  Sakura_Momoko  阅读(272)  评论(0编辑  收藏  举报