(接上一篇尼姆博奕,大家觉得哪个好就看哪个,其实差不太多。)
在书籍《编程之美》中,总共讲述了三个关于取石子博弈的问题。书中对这三个博弈问题的本身都有详细的解答。然而,看懂这些解答本身并不是一件难事。我们学知识呢,应该学会举一反三,这样才算是真正掌握了知识,同时也才能真正体会到解决问题的乐趣。本文写作的目的就是从这三个问题出发,然后对一系列与之相关的拓展问题,并给予相应的分析与论证。
与其他类型的问题不同的是,博弈论的问题只要稍做修改,其解答过程就很可能和原问题完全不一样。绝不是问题修改一点,答案只需更改一点的。然而,就最本质的分析思路而言却是想通的。话不多说,接下来就对这三个问题展开拓展性讨论吧。
尼姆博弈
原题如下:有N堆石头,记第i堆石头数量为。玩家A和B由A开始依次轮流取石头,每次玩家从N堆石头中选择一堆石头,取这一堆石头的任意数目(至少一个)。最后取光石头者获胜。
答案:这题是经典的博弈题。结论非常有趣,将游戏的局势两类,一类满足将每堆石头的数量取异或和等于0:;另一类满足将每堆石头的数量取异或和结果不等于0:。
其中,当且仅当局势时,S为奇异局势。
拓展一:取子过程和原问题相同,不同的是最后取光石头的人输了。
对拓展一分析如下:
首先,我们发现没有石头的状态为非奇异局势。而只有1块石头的状态为奇异局势,因为当玩家遇到这一状态时,他只能取那唯一的一块石头,从而成为最后取光石头的人而输掉游戏。
由上分析,不难发现“局势时,S为奇异局势”这一命题就不成立了。因为只有1块石头,对应的异或值非0,但却是奇异局势,而没有石头时,异或值为0,却非奇异局势。
我们将局势再按照是否存在某一堆石头多于一个这一条件将局势再分为以下两种情况:1、任何一堆石头的石头数量均不大于1:;2、存在某一堆石头数量多于1个:。
接下来,我们将分别对这两种情况进行分析。
①局势时:
这种情况下,比赛的结果就完全取决于石头的堆数N。当玩家面对N为奇数时,之后的每回合面对的石头堆数均为奇数。总是要面对只有1块石头的局面而输。同理,玩家面对N为偶数时,他永远不会遇到只有1块石头的局面。
因此,如果局势时,N为奇数为奇异局势。
②局势时:
这种情况比较复杂。我们先将局势再细分为如下两种局势集合:1、只有一堆石头的数量大于1:;2、多于一堆石头的数量大于1:。
当时,可以得出以下性质。
性质一:如果局势,则S为非奇异局势。
证:将N分为如下两种情况进行讨论。
情况1:N为奇数,此时玩家取第i堆石头,将这堆石头取到只剩1个。此时,符合情况①,且剩下的石头堆数为奇数。可以将状态转为奇异状态。
情况2:N为偶数,此时玩家取第i堆石头,将这堆石头全取完。剩下的石头堆数为N-1,而N-1为奇数。此时,也符合情况①,且剩下的石头堆数为奇数。可以将状态转为奇异状态。
综上所述,题设中的所有状态都能够通过某种方法使对手面临奇异状态。因此,题设中的状态为非奇异状态。
接下来,当初始博弈局势时,玩家在博弈过程中必然出现局势。
证:显然,当游戏结束时只有0堆石头满足。
记第t轮取石头时,局面中满足的堆数为。根据游戏规则,每次玩家只能选取一堆石头取,因此每次取石头至多减少一堆数目大于1的石头堆,即或。假设,游戏过程中不存在只有一堆石头满足的情况。即。由于为递减序列,因此必存在t,有。与或相矛盾。
性质二:对于新的游戏规则,当且仅当局势时,S为奇异局势。
证:首先,当游戏结束时,只剩下一堆石头数量大于1,即结束局势。不难证明,即。由性质一可知,该状态为非奇异状态。
对于其他状态,由最初的尼姆博弈的论证过程可知,当时,存在一种方法使得对手面临的局面,而当时,则无论采用怎样的取法,对手都将面临的局面。
那么,假如当前玩家A所面临的局势,那么他无论怎么取,对手都将面临局势。且对手(玩家B)都有办法让该玩家永远面临局势。因此,玩家A不可能面临局势。因为,假如玩家A面临的局势,则说明他面临的局势满足,与“对手永远让他面临局势”相矛盾。而随着游戏的进行,满足的石头堆的数目只会不断减少,最终必然出现局势,而玩家A永远不可能遇到这一局势。因此,最终将由玩家B面临该局势而赢得游戏。因此局势为奇异局势。
反过来,如果当前玩家面临局势时,他就可以采取上述情况中对手的策略从而让自己获胜。因此局势为非奇异局势。
性质二得证。
综上所述,如果游戏的初始局势,那么就局势S中还剩多少堆石头,当且仅当堆数为奇数时是奇异局势;如果初始局势,则当且仅当时为奇异局势。
拓展二:假如玩家可以选择的堆数为1—k堆,那情况又该如何呢?
分析这一问题时,我们先来回顾一下只能取1堆时(原问题)的分析过程。原问题判断博弈局势是否是奇异局势的最主要的判断标准就是每一堆石头数量的异或和是否等于0。
为了将这一结果进行拓展,我们不妨换个角度来看待异或和这一问题。异或和的实质就是对每堆石头数量对应的二进制数的每一位独立相加,然后再对其模2。从这一角度上看,只能取1堆时是模2,那有没有可能可以取到K堆时只要判断模k+1,就可以判断局势是否是奇异局势了呢?
带着这一猜想,我们进行了进一步探究。
首先,在没有石头的时候与原问题一样是奇异局势。此时,模多少都是0。
为了更好地进行表达,我们先将石头的数量表示成二进制:设第i堆石头的数量为,则其对应的二进制表示为。根据上述理论,可将异或和表示成为。
记表示对的二进制表示的每位相加然后再模k的运算,是异或和运算的拓展,表示为:。传统的异或和相当于。
和之前的推论过程一样,我们把游戏局势分为两类,一类满足将每堆石头数量的二进制表示每位相加模k+1等于0:;另一类满足将每堆石头数量的二进制表示每位相加模k+1的结果不等于0:。
接下来我们将论证以下性质成立:
性质三:当局势时,存在一种方法使局势变为;而当局势时,取完石头后局势都将变成。
证:首先,当局势时,记集合表示将要取的石头堆的集合。将该集合的二进制位每位相加然后模k+1可得:。由于要取的石头堆不大于k,因此有。不难推断,假如要保持取完后满足局势,则需要保持取完之后的值与原来完全一样。
假设取之前,而取完之后有,且。而对求累加和,可以表示为:。这说明取石头前后中的石头是总量是没有变化的。与每次取石头至少取一个这个前提矛盾。
因此,当局势时,无论采用何种策略,局势都将变成。
而当局势时,则可以通过以下步骤使局势。
令表示当前被选择的石头堆数的集合,而则表示已被选择的石头的堆数。同时,任何被选取的石头堆维护性质①:,其中表示已经任意均已被处理完成,即。为正无穷。
初始状态下。显然满足性质①。
令,每次循环都寻找最大的j满足。
然后,我们来考察值三种情况:
情况①::由于满足性质①,此时,只要从中选择个,即可令变为0。然后令。该操作并不影响,而且更新后,依旧满足性质①。
情况②::同样由于性质①,可将所有中的,则更新后的。此时,不难论证在未被选择的集合中,至少有堆石头满足。这几堆取若干个石头,使之满足,进一步变成了0,且满足性质①。最后将新选择的堆石头的编号加入,令。
情况③::这一情况同情况一相类似。与之唯一的不同是从中选择令变为k+1。
可见,以上三种情况,都是能够从高到低将结果所有变为0。因此,按照上述取法,必然可以获得一个新的局势。
由此,性质三得证。
从性质三不难进一步推断:当前局势S为奇异局势,当且仅当当。其论证过程和原题几乎一样,本文将不再赘述。
为了更好地说明局势时,可以通过上文的步骤使得。本文用一个简单的例子加以说明:
假设当前局势S={12,24,6,8,8},k=3。
将这些石头数量转成二进制如下:
12=<01100>
24=<11000>
6 =<00110>
8 =<01000>
8 =<01000>
进行运算得:R=<10210>
一开始为空,R=<10210>,R中最大非0位为第4位。符合上述情况②,取第2堆石头24个石头,取9个将其置为<01111>。S={12,15,6,8,8},={2}。
R=<00320>,R中最大非0位为第2位。取合上述情况②,从中每堆再取4个。取第1堆石头12个石头,取1个将其置为<01011>;取第3堆石头6个石头,取3个将其置为<00011>。S={11,11,3,8,8},={1,2,3}。
R=<00033>,R中最大非0位为第1位。取合上述情况①,从中每堆再取2个。S={9,9,1,8,8}。
R=<00003>,R中最大非0位为第0位。取合上述情况①,从中每堆再取1个。S={8,8,0,8,8}。
最终R=<00000>。即取第1,2,3堆,各取16,4,6个。
实现算法如下:
<span style="font-size:14px;">#define N 1000
#define BIT 30
//二进制相加然后模K+1
void getMod(int an[],int n,int k,int bn[]){
for (int i=0;i<n;i++)
{
if (an[i]<0)
{
bn[0]=-1;
cout<<"fuck an[i]<0"<<endl;
return ;
}
int j=0;
int t=an[i];
while (t)
{
if (t%2)
{
bn[j]++;
}
t/=2;
j++;
}
}
for (int j=0;j<=BIT;j++)
{
bn[j]=bn[j]%(k+1);
}
}
//判断是否奇异
bool isQiyi(int an[],int n,int k){
int bn[32];
memset(bn,0,sizeof bn);
getMod(an,n,k,bn);
for (int j=0;j<=BIT;j++)
{
if (bn[j])
{
return false;
}
}
return true;
}
//判断是否奇异
bool isQiyi(int bn[]){
for (int j=0;j<=BIT;j++)
{
cout<<bn[j]<<" ";
}
cout<<endl;
for (int j=0;j<32;j++)
{
if (bn[j])
{
return false;
}
}
return true;
}
//判断是否已选
bool hasSel(int sel[],int n,int t){
for (int i=0;i<n;i++)
{
if (sel[i]==t)
{
return true;
}
}
return false;
}
//获得取石子的结果
void getStone(int an[],int n,int k){
int rn[N];
int sel[N];
memset(rn,0,sizeof rn);
memset(sel,0,sizeof sel);
int bn[32];
memset(bn,0,sizeof bn);
getMod(an,n,k,bn);
int done=0;
while (!isQiyi(bn))
{
int t=-1;
for (int i=31;i>=0;i--)
{
if (bn[i])
{
t=i;
break;
}
}
if (bn[t]<=done)
{
for (int i=0;i<bn[t];i++)
{
rn[sel[i]]+=(1<<t);
}
bn[t]=0;
}else if (bn[t]<k+1)
{
for (int i=0;i<done;i++)
{
rn[sel[i]]+=(1<<t);
bn[t]--;
}
for (int i=0;i<bn[t];i++)
{
int theSel=-1;
for (int j=0;j<n;j++)
{
if (!hasSel(sel,done,j)&&(an[j]&(1<<t)))
{
theSel=j;
break;
}
}
rn[theSel]+=(an[theSel]&((1<<(1+t))-1))-((1<<(t))-1);
for (int j=0;j<t;j++)
{
if (!(an[theSel]&(1<<j)))
{
bn[j]++;
}
}
sel[done]=theSel;
done++;
}
bn[t]=0;
}else{
bn[t]%=(k+1);
for (int i=0;i<bn[t];i++)
{
rn[sel[i]]+=(1<<t);
}
bn[t]=0;
}
for (int i=0;i<done;i++)
{
printf("Select:%d\tgetNum:%d\n",sel[i],rn[sel[i]]);
}
cout<<endl;
}
for (int i=0;i<done;i++)
{
printf("Select:%d\tgetNum:%d\n",sel[i],rn[sel[i]]);
}
for (int i=0;i<n;i++)
{
rn[i]=an[i]-rn[i];
cout<<rn[i]<<" ";
}
cout<<endl;
printf("Res:%d",isQiyi(rn,n,k));
}</span>