定义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时无法找到必胜策略是错误的:
- 先拿第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
;
}
}
}