博弈论

目录

 

前言:

巴什博奕

威佐夫博奕(Wythoff Game)

尼姆博奕(Nimm Game)

 


前言:

初次系统学习博弈题,这篇文章我将从常见的博弈入手,浅谈一下自己的感触。

 

巴什博奕

描述:只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个,最后取光者得胜。

我们假设n=(m+1)*k+r,可以得出结论当r=0的时候,后者胜,反之前者胜。下面给出证明:

当r=0时:

n=(m+1)*k,这个时候我们假设第一个人取x个物品(x的范围是[1,m]),因为两个人是绝顶聪明的,所以第二个人可以取(m+1-x)个物品,这样在两个人进行一轮取物品的操作之后,还剩下n=(m+1)*(k-1)个物品,以此类推,最后一定是第二个人赢。

当r≠0时:

n=(m+1)*k+r,这个时候第一个人取r个物品,这样后面的问题就变成了r=0时候的情况,此时一定是第一个人赢。

 

总结起来就是:若n%(m+1)==0,第二个人胜,反之第一个人胜。

   int n,m;
   int k=n%(m+1);
   if(k==0)
       cout<<"B Win"<<endl;
   else 
       cout<<"A Win"<<endl; 

           巴什博奕延伸:如果说最后一个取物品的人输了该怎么办?

这时候假设 n-1=(m+1)*k+r

当r=0时:

第一个人取x个,第二个人取(m+1-x)个,那么按照这种方法,最后多出来的那一个物品会被第一个人取出,也就是仍然是第二个人胜。

当r≠0时:

第一个人拿走r个物品,这个时候问题就变成了r=0时候的情况,最后得出的结论是第一个人胜。

 

 

例题:

HDU-1846 Brave Game 

模板题

HDU-2188 悼念512汶川大地震遇难同胞——选拔志愿者

模板题,不过要特判m>n的情况。

HDU-2149 public sale 

 这道题可以作为理解当r≠0的时候第一个人取r件物品的情况。

#include<cstring>
#include<cstdio>
#include<algorithm>
#include<iostream>
#include<vector>
#include<map>
#include<string>
using namespace std;
int T,n,m;
int main()
{
    while(~scanf("%d%d",&n,&m))
    {
        if(m>=n)
        {
            for(int i=n;i<=m;++i)
            {
                printf("%d",i);
                if(i!=m)
                    printf(" ");
            }
        }
        else
        {
            if(n%(m+1)==0) printf("none");
            else
            {
                printf("%d",n%(m+1));
            }
        }
        printf("\n");
    }
    return 0;
}

 

威佐夫博奕(Wythoff Game)

有两堆石子,石子数目分别为n和m,现在两个人轮流从两堆石子中拿石子,每次拿时可以从一堆石子中拿走若干个,也可以从两中拿走相同数量的石子,拿走最后一个石子的人赢。

威佐夫博弈也是一个比较经典的神奇博弈,在进行判断的时候提到了奇异局势,对于这种局势,先手必输

下面我列举几种奇异局势:

(0,0),(1,2),(3,5),(4,7),(6,10)........

通过观察我们发现,对于每一个奇异局势的第一个数字,都是前面没有出现过的最小的正整数

比如(3,5)中的3,前面两组是(0,1,2),那么最小的正整数是3。

对于这几种情况都是先手会输,如果想看证明的话,推荐这篇博客:

https://blog.csdn.net/qq_34374664/article/details/52814983

下面就是玄学内容了,目前不能推导,后补:

对于每组数据的第一个数,它的值就等于这组数据的差值*1.618(我算了算,这一步要取整,比如对于第四组数据,3*1.618=4.854,(int)4.854=4)。

并且1.618刚好等于 ( sqrt(5.0) +1 ) / 2

下面通过例题给出模板:

例题:
HDU-1527 取石子游戏

模板题

#include<cmath>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
int n,m;
int main()
{
    while(cin>>n>>m)
    {
        int x;
        if(n>m)
        {
            x=n;
            n=m;
            m=x;
        }
        x=m-n;
        if( (int)((sqrt(5.0)+1)/2*x)==n )
            printf("0\n");
        else
            printf("1\n");
    }
    return 0;
}

HDU-2177 取(2堆)石子游戏

 这道题相较于前面的模板题就有一点点难度了,不过万变不离其宗,接下来咱们就来谈谈这道题。

1、前面我们提到奇异局势的时候有提到对于每组数据的第一个数,它的值就等于这组数据的差值*1.618,换种思路,就是说随着数据的展开,它们之间的距离是逐渐变大的,并且不同的距离对应不同的奇异局势!好的,现在知道了这一点,那么接下来在跟随我去看看其他的思考点。

2、对于数据中的奇异数据,一定满足的是先手是必输的,对于一组数据,我们想要让先手胜利的话,只需满足第一个人在使用正确的方法取出部分石子后的数据满足奇异局势就行了,也就是说在上面的模板的基础之上,我们要对输出1(必输)的情况进行再讨论,使得输出的数据中的每一组都是奇异局势。

3、我们知道,我们取石子的时候是按照从第一个里面取出一部分或者在两堆石子里面同时取出相同的数量的石子来运算的,那么我们就应该分两种情况进行讨论:

①:n和m之间的距离不变,我们通过这个距离求出在当前距离下的奇异数据的第一个值,并判断这个值是否小于n,若这个值小于n的话就输出,如果说这个值大于等于n的话,就continue(因为我们只能把石子取出,不能放入)。

②:n和m之间的距离变小,对于这种情况只能是m往左移动,原因在①中已经提到,我们通过枚举的方法找到符合条件的值,因为枚举的点有可能在n的左边,也有可能在n的右边,所以我们要分情况进行讨论。

#include<cmath>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
int n,m;
int main()
{
    while(cin>>n>>m,n+m)
    {
        int x=m-n;
        double sq= ((sqrt(5.0)+1)/2);
        if( (int)(sq*x)==n )
            printf("0\n");
        else
            {
                printf("1\n");
                int x1=(int)x*sq;//计算在(m-n)距离下对应的奇异数据的第一个值
                if(x1<n)
                    printf("%d %d\n",x1,x1+(m-n));
                for(int i=0;i<=m;++i)
                {
                    //从右往左枚举
                    int x2=m-i;
                    if(x2>n&&(int)((x2-n)*sq)==n)
                    {
                        printf("%d %d\n",n,x2);
                    }
                    else if(x2<n&&(int)((n-x2)*sq)==x2)
                    {
                        printf("%d %d\n",x2,n);
                    }
                }
            }
    }
    return 0;
}

 

尼姆博奕(Nimm Game)

有3堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取1个,多者不限,最后取光者得胜。

 

分析

1、首先自己想一下,就会发现只要最后剩两堆物品一样多(不为零),第三堆为零,那面对这种局势的一方就必败

那我们用(a,b,c)表示某种局势,首先(0,0,0)显然是必败态,无论谁面对(0,0,0) ,都必然失败;第二种必败态是(0,n,n),自己在某一堆拿走k(k ≤ n)个物品,不论k为多少,对方只要在另一堆拿走k个物品,最后自己都将面临(0,0,0)的局势,必败。仔细分析一下,(1,2,3)也是必败态,无论自己如何拿,接下来对手都可以把局势变为(0,n,n)的情形

那这种奇异局势有什么特点呢?

也不知谁这么牛逼,竟然能把这种局势和二进制联系在一起

这里说一种运算符号,异或'^',a^b=a'b+ab'(a'为非a)

我们用符号XOR表示这种运算,这种运算和一般加法不同的一点是1 XOR 1 = 0。先看(1,2,3)的按位模2加的结果:

1 = 二进制01

2 = 二进制10

3 = 二进制11  XOR

———————

0 = 二进制00 (注意不进位)

 

对于奇异局势(0,n,n)也一样,结果也是0

任何奇异局势(a,b,c)都有a XOR b XOR c = 0

 

如果我们面对的是一个非必败态(a,b,c),要如何变为必败态呢?

假设 a < b < c,我们只要将 c 变为a XOR b,即可。因为有如下的运算结果:

a XOR b XOR (a XOR b)=(a XOR a) XOR (b XOR b) = 0 XOR 0 = 0

要将c 变为a XOR b,只要对 c进行 c-(a XOR b)这样的运算即可

 

2、尼姆博弈模型可以推广到:有n堆若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

这个游戏中的变量是堆数k和各堆的物品数N1,N2,……,Nk。

对应的组合问题是,确定先手获胜还是后手获胜以及两个游戏人应该如何取物品才能保证自己获胜

 

3、为了进一步理解Nim取物品游戏,我们看看特殊情况。

如果游戏开始时只有一堆物品,先手则通过取走所有的物品而获胜。现在设有2堆物品,且物品数量分别为N1和N2。游戏者取得胜利并不在于N1和N2的值具体是多少,而是取决于它们是否相等。

也就说两堆的策略我们有了,现在我们如何从两堆的取子策略扩展到任意堆数中呢?

 

首先回忆一下,每个正整数都有对应的一个二进制数,例如:57(10) = 111001(2) ,即:57(10)=25+24+23+20。于是,我们可以认为每一堆物品数由2的幂数的子堆组成。这样,含有57枚物品大堆就能看成是分别由数量为25、24、23、20的各个子堆组成

 

现在考虑各大堆大小分别为N1,N2,……Nk的一般的Nim博弈。将每一个数Ni表示为其二进制数(数的位数相等,不等时在前面补0):

N1 = as…a1a0

N2 = bs…b1b0

……

Nk = ms…m1m0

如果每一种大小的子堆的个数都是偶数,我们就称Nim博弈是平衡的,而对应位相加是偶数的称为平衡位,否则称为非平衡位。因此,Nim博弈是平衡的,当且仅当:

as +bs + … + ms 是偶数,即as XOR bs XOR … XOR ms  = 0

……

a1 +b1 + … + m1 是偶数,即a1 XOR b1 XOR … XOR m1 = 0

a0 +b0 + … + m0是偶数,即a0 XOR b0 XOR … XOR m0 = 0

  

于是,我们就能得出尼姆博弈中先手获胜策略:

Bouton定理:先手能够在非平衡尼姆博弈中取胜,而后手能够在平衡的尼姆博弈中取胜。即状态(x1, x2, x3, …, xn)为P状态当且仅当x1 xor x2 xor x3 xor … xor xn =0。这样的操作也称为Nim和(Nim Sum)

我们以一个两堆物品的尼姆博弈作为试验。设游戏开始时游戏处于非平衡状态。这样,先手就能通过一种取子方式使得他取子后留给后手的是一个平衡状态下的游戏,接着无论后手如何取子,再留给先手的一定是一个非平衡状态游戏,如此反复进行,当后手在最后一次平衡状态下取子后,先手便能一次性取走所有的物品而获胜。而如果游戏开始时游戏牌平衡状态,那根据上述方式取子,最终后手能获

 

下面应用此获胜策略来考虑4堆的Nim博弈。其中各堆的大小分别为7,9,12,15枚硬币。用二进制表示各数分别为:0111,1001,1100和1111

于是可得到如下一表:

 由Nim博弈的平衡条件可知,此游戏是一个非平衡状态的Nim博弈,因此,先手在按获胜策略一定能够取得最终的胜利。具体做法有多种,先手可以从大小为12的堆中取走11枚硬币,使得游戏达到平衡(如下表)

之后,无论后手如何取子,先手在取子后仍使得游戏达到平衡

同样的道理,先手也可以选择大小为9的堆并取走5枚硬币而剩下4枚,或者,先手从大小为15的堆中取走13枚而留下2枚

归根结底, Nim博弈的关键在于游戏开始时游戏处于何种状态(平衡或非平衡)和先手是否能够按照取子游戏的获胜策略来进行游戏

当堆数大于2时,我们看出Bouton定理依旧适用,下面用数学归纳法证明

 

证明:如果每堆都为0,显然是P状态(必败)。下面验证P状态和N状态的后两个递推关系:

一、每个N状态都可以一步到达P状态。

证明是构造性的。检查Nim和X的二进制表示中最左边一个1,则随便挑一个该位为1的物品堆Y,根据Nim和进行调整(0变1,1变0)即可。例如Nim和为100101011,而其中有一堆为101110001。为了让Nim和变为0,只需要让操作的物品数取操作前的物品数和Nim的异或即可

显然操作后物品数变小,因此和合法的。设操作前其他堆的Nim和为Z,则有Y xor Z = X。操作后的Nim和为X xor Y xor Z = X xor X = 0,是一个P状态

二、每个P状态(必胜态)都不可以一步到达P状态

由于只能改变一堆的物品,不管修改它的哪一位,Nim的对应位一定不为0,不可能是P状态。

这样就证明了Bouton定理

实际解决

Nim博弈中如果规定最后取光者输,情况是怎样的?

初看起来问题要复杂很多(因为不能主动拿了,而要“躲着”拿,以免拿到最后一个物品),但对于Nim游戏来说,几乎是一样的:

首先按照普通规则一样的策略进行,直到恰好有一个物品数大于1的堆x。在这样的情况下,只需要把堆x中的物品拿得只剩1个物品或者拿完,让对手面临奇数堆物品,这奇数堆物品每堆恰好1个物品。这样的状态显然是必败的。由于你每次操作后需要保证Nim和为0,因此不可能在你操作后首次出现“恰好有一个物品数大于1的堆”。新游戏得到了完美解决

(上面这一小部分转自: https://www.cnblogs.com/jiangjun/archive/2012/11/01/2749937.html

 

例题:

HDU-2176 取(m堆)石子游戏

这种从m堆里去石子的题目,可以拆分成一个一个的单独的Nim博奕,先说结论,只要满足每一堆的异或为0,就是先手必输,后手必胜,异或不为0,则是先手获胜。
对于某个局面(a1,a2,…,an),若a1^ a2 ^ …^ an不为0,一定存在某个合法的移动,将ai改变成ai’后满足a1^ a2^ …^ ai’^ …^ an=0(a1^a2^....^an=0就代表必输,如果说一定存在某个合法的移动使得必输的话,这个地方就是必胜!)。不妨设a1^ a2^ …^ an=k,则一定存在某个ai,它的二进制表示在k的最高位上是1。因为只有这样,才能使k的最高位为1。而且我们知道ai^ k < ai(因为只有ai和k的二进制最高位是1,一异或肯定比ai小,就像1000>0111),所以此时我们ai^ k去替换ai,此时原式就变成了a1^ a2^ … ^ ai^ … ^ an^ k = k ^ k = 0。

这个操作说明了我们可以通过一次操作使任何一个异或不为0的序列变成异或为0的序列,而后手的操作一定会打破这种情况,如此往复会得到全0的结果,也就是先手会获得胜利。 类似的,如果开始情况下异或为0, 则先手会打破这种情况,后手就会获得胜利。 至于题目中要求的操作,我们可以通过什么提到的方式来实现,即求出所有比ai小的ai^ k,也就是替换后的结果。

 

#include<cmath>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxn=2e5+100;
int n,a[maxn];
int main()
{
    while(cin>>n,n)
    {
        int sum=0;
        for(int i=1;i<=n;++i)
        {
            scanf("%d",&a[i]);
            sum^=a[i];
        }
        if(sum==0)
        {
            printf("N0\n");
        }
        else
        {
            printf("Yes\n");
            for(int i=1;i<=n;++i)
            {
                if((sum^a[i])<a[i])
                {
                    printf("%d %d\n",a[i],sum^a[i]);
                }
            }
        }
    }
    return 0;
}

HDU-1850 Being a Good Boy in Spring Festival

跟前面的例题几乎相同的一道题

#include<cmath>
#include<cstdio>
#include<algorithm>
#include<iostream>
using namespace std;
const int maxn=2e5+100;
int n,a[maxn];
int main()
{
    while(cin>>n,n)
    {
        int sum=0;
        for(int i=1;i<=n;++i)
        {
            scanf("%d",&a[i]);
            sum^=a[i];
        }
        if(sum==0)
        {
            printf("0\n");
        }
        else
        {
            int cont=0;
            for(int i=1;i<=n;++i)
            {
                if((sum^a[i])<a[i])
                {
                    cont++;
                }
            }
            printf("%d\n",cont);
        }
    }
    return 0;
}

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值