Nim游戏及其相关拓展

        最初了解博弈相关的一些东西是在暑假集训的时候学长介绍的,当时是主讲DP时顺带谈到了一点博弈内容。最近对于一些组合游戏的问题稍微多看了一点,参考其他博文和查阅资料,发现有挺多有意思的东西,以下便是一些相关内容学习摘要和介绍。

Nim游戏模型两人取石子博弈,规则如下:有若干堆石子,每堆石子的数量都是有限的,合法的移动是“选择一堆石子并拿走若干颗(不能不拿)”,如果轮到某个人时所有的石子堆都已经被拿空了,则判负(因为他此刻没有任何合法的移动)。

       从简单的例子先来分析,如果只有两堆石子,那么应该能比较容易想到必胜策略:即当两堆石子的数量相同时,那么先手必败,或者说后手有必胜策略(当然有个前提就是这两个人都必须是足够聪明,知道游戏策略的人)。因为取石子只能取一堆中的若干,不能同时取不同堆的石子,所以后手的那个人完全可以根据先手者的取法来决定自己的取法(先手者取多少个,后者只要在另外一堆跟着取多少个就行)。例如有两堆都是只有3个石子的石子堆,用(3,3)表示初始状态,其中有一种状态过程可以这样表示:(3,3)->(2,3)->(2,2)->(0,2)->(0,0)。当然还有很多不同的状态,但最终都是后手赢。那么反之,当两堆石子不同的时候,那么先手拥有必胜策略,后手必败。结果比较容易分析,不再详细叙述。

  那么如果问题变得复杂一点,不是两堆石子,是三堆或者更多堆呢?这时我们就要分析一套能得出必胜策略的算法来解决这个问题。

  定义P-position和N-position,其中P代表Previous,N代表Next。直观的说,上一次move的人有必胜策略的局面是P-position,也就是“后手可保证必胜”或者“先手必败”,现在轮到move的人有必胜策略的局面是N-position,也就是“先手可保证必胜”。更严谨的定义是:1.无法进行任何移动的局面(也就是terminal position)是P-position;2.可以移动到P-position的局面是N-position;3.所有移动都导致N-position的局面是P-position。比如之前分析的当只有两堆石子时,当着两堆石子的石子数相等时后手有必胜策略,也即是一个P-position。之前提到的(3,3)就是一个P-position,并且,(3,3)的所有子局都是N-position。通过一点简单的数学归纳,可以严格的证明“有两堆石子时的局面是P-position当且仅当这两堆石子的数目相等”。

  根据上面这个过程,可以得到一个递归的算法——对于当前的局面,递归计算它的所有子局面的性质,如果存在某个子局面是P-position,那么向这个子局面的移动就是必胜策略。当然,可能你已经敏锐地看出有大量的重叠子问题,所以可以用DP或者记忆化搜索的方法以提高效率。但问题是,利用这个算法,对于某个Nim游戏的局面(a1,a2,...,an)来说,要想判断它的性质以及找出必胜策略,需要计算O(a1*a2*...*an)个局面的性质,不管怎样记忆化都无法降低这个时间复杂度。所以我们需要更高效的判断Nim游戏的局面的性质的方法。

  那么如何从两堆扩展到多堆的必胜策略算法。首先来回忆一下,每个正整数都有对应的一个二进制数,例如:57(10) à 111001(2) ,即:57(10)=25+24+23+20。于是,我们可以认为每一堆硬币数由2的幂数的子堆组成。这样,含有57枚硬币大堆就能看成是分别由数量为25、24、23、20的各个子堆组成。

现在考虑各大堆大小分别为N1,N2,……Nk的一般的Nim取子游戏。将每一个数Ni表示为其二进制数(数的位数相等,不等时在前面补0):
N1 = as…a1a0
N2 = bs…b1b0
……
Nk = ms…m1m0
如果每一种大小的子堆的个数都是偶数,我们就称Nim取子游戏是平衡的,而对应位相加是偶数的称为平衡位,否则称为非平衡位。因此,Nim取子游戏是平衡的,当且仅当:

as + bs + … + ms 是偶数

……

a1 + b1 + … + m1 是偶数

a0 + b0 + … + m0是偶数

于是,我们就能得出获胜策略:游戏人I能够在非平衡取子游戏中取胜,而游戏人II能够在平衡的取子游戏中取胜。

或者按之前的P-position和N-position的状态来给出结论的话也即:对于一个Nim游戏的局面(a1,a2,...,an),它是P-position当且仅当a1^a2^...^an=0,其中^表示异或(xor)运算。

  结论和异或运算有关,仔细分析可以发现这个异或运算其实就是上述描述平衡与非平衡状态的一种表示方式。下面给出这个结论的证明:根据定义,证明一种判断position的性质的方法的正确性,只需证明三个命题: 1、这个判断将所有terminal position判为P-position;2、根据这个判断被判为N-position的局面一定可以移动到某个P-position;3、根据这个判断被判为P-position的局面无法移动到某个P-position。下面给出百度百科的证明:
第一个命题显然,terminal position只有一个,就是全0,异或仍然是0。
第二个命题,对于某个局面(a1,a2,...,an),若a1^a2^...^an!=0,一定存在某个合法的移动,将ai改变成ai'后满足a1^a2^...^ai'^...^an=0。不妨设a1^a2^...^an=k,则一定存在某个ai,它的二进制表示在k的最高位上是1(否则k的最高位那个1是怎么得到的)。这时ai^k<ai一定成立。则我们可以将ai改变成ai'=ai^k,此时a1^a2^...^ai'^...^an=a1^a2^...^an^k=0。
第三个命题,对于某个局面(a1,a2,...,an),若a1^a2^...^an=0,一定不存在某个合法的移动,将ai改变成ai'后满足a1^a2^...^ai'^...^an=0。因为异或运算满足消去率,由a1^a2^...^an=a1^a2^...^ai'^...^an可以得到ai=ai'。所以将ai改变成ai'不是一个合法的移动。证毕。
根据这个定理,我们可以在O(n)的时间内判断一个Nim的局面的性质,且如果它是N-position,也可以在O(n)的时间内找到所有的必胜策略。Nim问题就这样基本上完美的解决了。

   那么对于N堆的Nim模型游戏我们已经找到了快速的解决方法。但更扩展一点,当Nim游戏规则稍微变动一下,每次最多只能取K个,或者说每次取的个数是已知并且给定的一些数呢,那么又该怎么找出必胜策略呢?

  下面是自己查阅的一些资料的汇总,粘贴其他人的博客(详见http://www.cnblogs.com/frog112111/p/3199780.html)首先是引出一个SG函数。定义:sg(x) = mex ( sg(y) |y是x的后继结点 ),其中mex(x)(x是一个自然是集合)函数是x关于自然数集合的补集中的最小值,比如x={0,1,2,4,6} 则mex(x)=3。

什么是后继结点?

所谓后继结点就是当前结点经过一个操作可以变成的状态。比如对于取石子游戏,假如每次可以取的数目是1,2,4,当前的石子数目也就是当前状态是5,那么5的后继结点就是{5-1, 5-2, 5-4}={4,3,1};如果5的三个后继结点的SG函数值分别为0,1,3,那么5的SG值就是集合{0,1,3}的补集的最小元素,也就是2。

再比如:取石子问题,有1堆n个的石子,每次只能取{1,3,4}个石子,先取完石子者胜利,那么各个数的SG值为多少?

sg[0]=0,f[]={1,3,4},

x=1时,可以取走1-f{1}个石子,剩余{0}个,mex{sg[0]}={0},故sg[1]=1;

x=2时,可以取走2-f{1}个石子,剩余{1}个,mex{sg[1]}={1},故sg[2]=0;

x=3时,可以取走3-f{1,3}个石子,剩余{2,0}个,mex{sg[2],sg[0]}={0,0},故sg[3]=1;

x=4时,可以取走4-f{1,3,4}个石子,剩余{3,1,0}个,mex{sg[3],sg[1],sg[0]}={1,1,0},故sg[4]=2;

x=5时,可以取走5-f{1,3,4}个石子,剩余{4,2,1}个,mex{sg[4],sg[2],sg[1]}={2,0,1},故sg[5]=3;

以此类推.....

   x         0  1  2  3  4  5  6  7  8....

sg[x]      0  1  0  1  2  3  2  0  1....

计算从1-n范围内的SG值。

f(存储可以走的步数,f[0]表示可以有多少种走法)

f[]需要从小到大排序

1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);

2.可选步数为任意步,SG(x) = x;

3.可选步数为一系列不连续的数,用GetSG()计算

贴一份网上找的GetSG()模板代码:

 模板一:

//f[]:可以取走的石子个数
//sg[]:0~n的SG函数值
//hash[]:mex{}
int f[N],sg[N],hash[N];     
void getSG(int n)
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=n;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1;f[j]<=i;j++)
            hash[sg[i-f[j]]]=1;
        for(j=0;j<=n;j++)    //求mes{}中未出现的最小的非负整数
        {
            if(hash[j]==0)
            {
                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;
}

对于很多组合游戏,SG函数都可以运用到,非常简单方便。

关于整个游戏的sg值之和sum,定义sum=sg1 ^ sg2 ^ sg3 ^ ……sgn.  其中^表示按位异或运算。

结论:一个游戏的初始局面是必败态当且仅当sum=0。

将此结论带到之前得出的一些结论发现仍是成立的。下面给出一些OJ上的习题练习。

hdu  1848

题意:取石子问题,一共有3堆石子,每次只能取斐波那契数个石子,先取完石子者胜利,问先手胜还是后手胜

  1. 可选步数为一系列不连续的数,用GetSG(计算)
  2. 最终结果是所有SG值异或的结果

AC代码如下:

#include<stdio.h>
#include<string.h>
#define N 1001
//f[]:可以取走的石子个数
//sg[]:0~n的SG函数值
//hash[]:mex{}
int f[N],sg[N],hash[N];     
void getSG(int n)
{
    int i,j;
    memset(sg,0,sizeof(sg));
    for(i=1;i<=n;i++)
    {
        memset(hash,0,sizeof(hash));
        for(j=1;f[j]<=i;j++)
            hash[sg[i-f[j]]]=1;
        for(j=0;j<=n;j++)    //求mes{}中未出现的最小的非负整数
        {
            if(hash[j]==0)
            {
                sg[i]=j;
                break;
            }
        }
    }
}
int main()
{
    int i,m,n,p;
    f[0]=f[1]=1;
    for(i=2;i<=16;i++)
        f[i]=f[i-1]+f[i-2];
    getSG(1000);
    while(scanf("%d%d%d",&m,&n,&p)!=EOF)
    {
        if(m==0&&n==0&&p==0)
            break;
        if((sg[m]^sg[n]^sg[p])==0)
            printf("Nacci\n");
        else
            printf("Fibo\n");
    }
    return 0;
}

hdu  1536

题意:首先输入K 表示一个集合的大小  之后输入集合 表示对于这对石子只能取这个集合中的元素的个数

之后输入 一个m 表示接下来对于这个集合要进行m次询问

之后m行 每行输入一个n 表示有n个堆  每堆有n1个石子  问这一行所表示的状态是赢还是输 如果赢输入W否则L

思路:对于n堆石子 可以分成n个游戏 之后把n个游戏合起来就好了
#include<stdio.h>
#include<string.h>
#include<algorithm>
using namespace std;
//注意 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;
}
int main()
{
    int i,m,t,num;
    while(scanf("%d",&n)&&n)
    {
        for(i=0;i<n;i++)
            scanf("%d",&s[i]);
        memset(sg,-1,sizeof(sg));
        sort(s,s+n);
        scanf("%d",&m);
        while(m--)
        {
            scanf("%d",&t);
            int ans=0;
            while(t--)
            {
                scanf("%d",&num);
                ans^=SG_dfs(num);
            }
            if(ans==0)
                printf("L");
            else
                printf("W");
        }
        printf("\n");
    }
    return 0;
}

 诸如此类的题还有HDU1846-1850,1536,2147等等,不再一一举例。

 大量的组合游戏都可以用SG函数来求解,例如另外一种模型:翻硬币的问题

问题描述:

在一条直线上排列着一行硬币,有的正面朝上、有的背面朝上。2名游戏者轮流对硬币进行翻转。翻转时,先选一枚正面朝上的硬币翻转,然后,如果愿意,可以从这枚硬币的左

边选取一枚硬币翻转。最后翻转使所有硬币反面朝上的玩家胜利。

1  2  3  4  5  6  7   8  9  10 11 12 13

T  H  T  T  H  T  T  H  T   H   H   T  H

分析:基于转化的思想,如果将位置i上的H看作一堆规模为i的石子,我们就能看到游戏与nim的相似点。不过有许多nim中的合法操作在这个游戏中是无法实现的,比如图一状态中,我们就无法从一堆规模为10的石子中取出其中的5个。不过我们将10与5同时翻转,所得到的状态。对应的nim状态SG值与从10中取5是相同的。因此我们猜想这个游戏的状态SG函数值与对应的nim相同。由于事实上虽然这个游戏无法实现nim的所有操作,但它的状态所能到达的状态SG函数值集合与对应的nim游戏状态相同。因此,它的状态SG函数值与对应nim相同。

每个硬币的SG值为它的编号。

如果对于一个局面,把正面硬币的SG值异或起来不等于0,既a1^a2^a3^…^an==x,对于an来说一定有an'=an^x<an。

如果an'==0,意思就是说,把an这个值从式子中去掉就可以了。对应游戏,就是把编号为an的正面硬币翻成背面就可以了。

如果an'!=0,意思就是说,把an这个值从式子中去掉后再在式子中加上an',an'<an。对应游戏,去掉an就是把编号为an的正面硬币翻成背面,加上an',如果编号为an'的硬币是正面,我们就把它翻成背面,是背面就翻成正面,总之,就是翻转编号为an'的硬币。

  参考:http://blog.sina.com.cn/s/blog_51cea4040100h3wl.html

 

 

 

 

 

posted @ 2014-03-12 23:39  Kevin_CongTang  阅读(790)  评论(0编辑  收藏  举报