NIM(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;
        }
    }
}



發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章