NIM(1)一排石头的游戏

《编程之美》第1.11 NIM(1)一排石头的游戏:

原问题:一排石头,每次只能取其中一个,或者连续的两个(中间石头即使被取走,也算不连续),问有没有必胜的策略。

解:如果石头的个数是奇数,那么就从中间取走一个,如果是偶数,就从中间取走两个,保证左右两边石头数相等,这样,如果下一个人从左边取,只需要和他取的数目一样,从相反方向取石头即可。这样当另一个人把一边的取完,则剩下的一边必然只剩下一个或者两个,这时候,第一个取石头的人必胜。

扩展问题1、如果最后取走石头的人输,有没有必胜的策略。

解:没有想到有效的解法,只能枚举,可以参考:http://arieshout.me/2012/04/nim-problem.html。下面就是摘自这篇博客:

《编程之美》一书中1.11章节介绍了NIM游戏的取胜问题。N块石头排成一行,每块石头有各自固定的位置。两个玩家依次取石头,每个玩家每次可以取其中任意一块石头,或相邻的两块石头,石头在游戏过程中不能移位(即编号不会改变),最后能将剩下的石头一次取光的玩家获胜。在这样的规则下,先取的玩家可以在第一步取走最中间的一个(总数为奇数时)或者两个(总数为偶数时)石头,然后后续过程中总取与对手取走的石头对称位置的相同数目的石头。因而先取者有必胜策略。

文末的扩展问题部分提出一个问题:若规定最后取光石头的人输,又该如何应对呢?

网上似乎也没有这个问题的明确解法,有的给出过证明石头总数3N+1时无法找到必胜策略其余的则可以,但是可以看出证明过程中有明显的漏洞,而且证明的这个3N+1的命题本来就是错误的……

定义S为石头摆放的一个格局,格局标识石头目前的连续区段的状态以及每个连续区段的石头数目。初始状态下,N块石头连成一体,可以表示为{N},即N个连续的石头。取走第二块石头之后格局变成{1, N-2},即两段数目分别为1和N-2的连续的石头。

这样,问题可以描述为:对于初始格局S_0={N},甲需要找到制胜的策略。甲取完石头将格局变为S_1后,无论乙怎么取(记乙取完后的格局为S_2),甲总能在当前格局S_2中找到制胜的策略。问题转化成S_2上的小一个规模的问题。需要注意,S_2实际应为从S_1中任意取一次石头后可能形成的众多格局中的一个,只要其中任意一个S_2能让甲无法找到制胜策略,那么甲这次从S_0中取石头的方法就是失败的行不通的。依照这种思路,可以使用递归思路检查甲是否能够找到制胜的策略。

当格局中的石头数目C较小时,可以直接检测是否存在制胜途径,这些条件可以作为递归过程中的边界条件,如:

  • C=1时,甲必输
  • C=2时,甲随意取走其中一个,即赢
  • C=3时,如果有连续的两个,甲取走即赢;否则甲必输。这一条件可以使用递归思路转化为C=1或C=2

每一次递归都需要枚举所有可能的情况O(N^2),每一次枚举都需要递归地检查N-2规模上的可能情况,这样下来递归算法的复杂度为O(N^N)。文末列出了未使用缓存删减分支的方法的C#的实现,使用这个方法可以在短时间内跑出N<=15的结果,但是N=16等了十几分钟没出来。

递归过程中会出现大量的重复计算,一种思路是将当前格局的计算结果缓存起来,这样后续的计算中碰到相同的格局时只需要查表。而且,注意到格局{A, B, C}的查找结果和格局{B, A, C}, {C, A, B}等应该是一样的,这样可以在计算和缓存前对格局进行一致性转换,比如将格局中连续区段按区段中包含的石头数目的升序进行排列,这样也可以减少大量重复的分支计算。但是即使这样,当N较大时,可能出现的格局总数增长也将很快(粗看也在O(N^N)的水平),这意味着结果缓存空间的需求的增长也将很快,而且如何有效的索引缓存空间也是一个问题。在取得一定的时间效率增长时,空间可能又会成为问题。在石头总数N<=32的规模下,可以使用一个整型变量表示当前的格局(某位为1代表当前位置有石头,否则为空),这样可以在32位机器上使用一个大数组缓存结果,从而将可计算规模扩展到32左右。

通过对拿石头的步骤进行记录,找到了石头数N=7时的必胜策略,因而上文所述的网络上所说的3N+1时无法找到必胜策略是错误的:

  1. 先拿第2个石头
  2. 乙拿走一块或者两块石头后,想办法在剩余的石头中制造{1,1,1}或{2,2}或者{4}的格局,可能的步骤为(<>标识我方拿石头的方法,[]标识对方拿石头的方法,只记录前三步,因为后续即为简单的必败格局了):
    • <2>, [1], <3>
    • <2>, [3], <1>
    • <2>, [4], <5,6>
    • <2>, [5], <1>
    • <2>, [6], <3,4>
    • <2>, [7], <1>
    • <2>, [3,4], <6>
    • <2>, [4,5], <6>
    • <2>, [5,6], <3>
    • <2>, [6,7], <4>

N<16时,必胜策略存在的情况为:1×, 2√, 3√, 4×, 5√, 6√, 7√, 8√, 9×, 10√, 11√, 12×, 13√, 14√, 15√。

using System;
using System.Collections.Generic;
using System.Linq;

namespace Beauty.of.Programming
{
    sealed class Move
    {
        private readonly string _repr;

        public Move(int stone1, bool myturn = true)
            : this(stone1, null, myturn)
        {
        }

        public Move(int stone1, int? stone2, bool myturn = true)
        {
            string format1 = myturn ? "<{0}>" : "[{0}]";
            string format2 = myturn ? "<{0},{1}>" : "[{0},{1}]";
            string format = stone2.HasValue ? format2 : format1;
            _repr = string.Format(format, stone1, stone2);
        }

        public override string ToString()
        {
            return _repr;
        }
    }

    sealed class Nim
    {
        static void Main(string[] args)
        {
            for (int i = 1; i <= 16; ++i)
                Nim.FindNimApproach(i);
            //Nim.FindNimApproach(7);
        }

        public static bool FindNimApproach(int n)
        {
            return new Nim(n).FindNimApproach();
        }

        private readonly int[] _stones;
        private readonly List<Move> _moves;
        private int _stonesRemain;

        public Nim(int n)
        {
            _stonesRemain = n;
            _stones = new int[n];
            for (int i = 0; i < _stones.Length; i++)
                _stones[i] = 1;
            _moves = new List<Move>();
        }

        public bool FindNimApproach()
        {
            bool ret = FindNimHelper();
            Console.WriteLine(_stones.Length + " ==> " + (ret ? "Found" : "Failed"));
            return ret;
        }

        private void DumpSuccessfulMoves()
        {
            //var msg = string.Join(", ", _moves.Reverse());
            //Console.WriteLine(msg);
        }

        private IEnumerable<int> EnumerateStones(bool myturn = true)
        {
            for (int i = 0; i < _stones.Length; i++)
            {
                if (_stones[i] != 0)
                {
                    _stones[i] = 0;
                    _stonesRemain--;
                    _moves.Add(new Move(i, myturn));
                    try
                    {
                        yield return i;
                    }
                    finally
                    {
                        _moves.RemoveAt(_moves.Count - 1);
                        _stonesRemain++;
                        _stones[i] = 1;
                    }
                }
            }
        }

        private IEnumerable<int> EnumerateContinuousStones(bool myturn = true)
        {
            for (int i = 1; i < _stones.Length; i++)
            {
                if (_stones[i] != 0 && _stones[i - 1] != 0)
                {
                    _stones[i] = _stones[i - 1] = 0;
                    _stonesRemain -= 2;
                    _moves.Add(new Move(i - 1, i, myturn));
                    try
                    {
                        yield return i;
                    }
                    finally
                    {
                        _moves.RemoveAt(_moves.Count - 1);
                        _stonesRemain += 2;
                        _stones[i] = _stones[i - 1] = 1;
                    }
                }
            }
        }

        private bool HasContinousStones()
        {
            for (int i = 1; i < _stones.Length; i++)
                if (_stones[i] > 0 && _stones[i - 1] > 0)
                    return true;
            return false;
        }

        private bool FindNimHelper()
        {
            if (_stonesRemain == 1)
                return false;
            if (_stonesRemain == 2)
                return true;
            if (_stonesRemain == 3)
            {
                return HasContinousStones();
            }

            foreach (var mytake in EnumerateStones())
            {
                bool fail = EnumerateStones(false).Any(other => !FindNimHelper());
                if (fail)
                    continue;
                fail = EnumerateContinuousStones(false).Any(other2 => !FindNimHelper());
                if (!fail)
                {
                    DumpSuccessfulMoves();
                    return true;
                }
            }

            foreach (var mytake2 in EnumerateContinuousStones())
            {
                bool fail = EnumerateStones(false).Any(other => !FindNimHelper());
                if (fail)
                    continue;
                fail = EnumerateContinuousStones(false).Any(other2 => !FindNimHelper());
                if (!fail)
                {
                    DumpSuccessfulMoves();
                    return true;
                }
            }

            return false;
        }
    }
}

扩展 问题2、若两个人轮流取一堆石头,每人每次最少取1块石头,最多取K块石头,最后取光石头的人赢得此游戏。

解:参考:http://blog.csdn.net/swingboard/article/details/6552533,下面是原博主的分析:

1)极端情况:K>=N

   玩家A可以直接取掉所有石头,获得胜利。

2) 一般情况:K<N

   这里面有个规律,就是,无论一个玩家拿了多少个石头,另一个玩家都能够选择相应的石头数量,使得两个玩家一起拿K+1个石头。

   从上面这个思路,我们考虑N与K+1之间的数值关系:

   N = (K+1) * c + d,其中c>=1,d>=0

   当d!=0时,玩家A先取d-1个石头,这样还剩(K+1)*c+1个石头。然后轮到玩家B取石头,不管玩家B取了多少个石头,玩家A都再取相应

的石头,使得玩家A和玩家B一起取(K+1)个石头,这样,还剩(K+1)*(c-1)+1个石头,如此往复,最后,肯定会剩下1个石头等着A来取。

  举例说明:

  剩余石头数目         取石头

  (K+1) * c + d          A取d-1个

  (K+1) * c + 1          B取X个,A取(K+1-X)个

  (K+1) * (c-1) + 1    ...

   ...                         ...

   1                          A取得最后一个石头

   0

   当d=0时,无论A取多少个石头,B取相应的石头,使得A和B一起取(K+1)个石头,这样最后取到石头的肯定是玩家B。

  举例说明:

  剩余石头数目        取石头

  (K+1) * c               A取X个,B取(K+1-X)个

  (K+1) * (c-1)         ...

   ...                         ...

   K+1                      A取X个,B取(K+1-X)个

   0

   即N%(K+1)=0时,玩家B有必胜策略,N%(K+1)!=0时,玩家A有必胜策略。

   可以看出来,如果K很大的话,而N的数值是随机的话,先手的优势是很明显的。


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值