Nim游戏及其相关拓展

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

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游戏的局面的性质的方法。spa

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

如今考虑各大堆大小分别为N 1,N 2,……N k的通常的Nim取子游戏。将每个数N i表示为其二进制数(数的位数相等,不等时在前面补0):
N 1 = a s…a 1a 0
N 2 = b s…b 1b 0
……
N k = m s…m 1m 0
若是每一种大小的子堆的个数都是偶数,咱们就称Nim取子游戏是平衡的,而对应位相加是偶数的称为平衡位,不然称为非平衡位。所以,Nim取子游戏是平衡的,当且仅当:

as + bs + … + ms 是偶数htm

……

a1 + b1 + … + m1 是偶数blog

a0 + b0 + … + m0是偶数排序

因而,咱们就能得出获胜策略:游戏人I可以在非平衡取子游戏中取胜,而游戏人II可以在平衡的取子游戏中取胜。

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

  结论和异或运算有关,仔细分析能够发现这个异或运算其实就是上述描述平衡与非平衡状态的一种表示方式。下面给出这个结论的证实:根据定义,证实一种判断position的性质的方法的正确性,只需证实三个命题: 一、这个判断将全部terminal position判为P-position;二、根据这个判断被判为N-position的局面必定能够移动到某个P-position;三、根据这个判断被判为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

相关文章
相关标签/搜索