博弈论总结


博弈论是二人或多人在平等的对局中各自利用对方的策略变换自己的对抗策略,达到取胜目标的理论。博弈论是研究互动决策的理论。博弈可以分析自己与对手的利弊关系,从而确立自己在博弈中的优势,因此有不少博弈理论,可以帮助对弈者分析局势,从而采取相应策略,最终达到取胜的目的。
博弈论分类:(摘自百度百科)

(一)巴什博奕(Bash Game):只有一堆n个物品,两个人轮流从这堆物品中取物,规定每次至少取一个,最多取m个。最后取光者得胜。

显然,如果n=m+1,那么由于一次最多只能取m个,所以,无论先取者拿走多少个,后取者都能够一次拿走剩余的物品,后者取胜。因此我们发现了如何取胜的法则:如果n=(m+1)*r+s,(r为任意自然数,s≤m),那么先取者要拿走s个物品,如果后取者拿走k(≤m)个,那么先取者再拿走m+1-k个,结果剩下(m+1)(r-1)个,以后保持这样的取法,那么先取者肯定获胜。总之,要保持给对手留下(m+1)的倍数,就能最后获胜。
这个游戏还可以有一种变相的玩法:两个人轮流报数,每次至少报一个,最多报十个,谁能报到100者胜。巴什博弈博弈论里面最简单的一种形式。以下题目利用巴什博弈可以轻松解决:

1. http://acm.hdu.edu.cn/showproblem.php?pid=1846 (brave game)
[c]
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
int t,m,n;
scanf("%d",&t);
while(t–)
{
scanf("%d %d",&m,&n);
if(m<=n || m%(n+1)!=0)
printf("firstn");
else
printf("secondn");
}
return 0;
}
[/c]
2. http://acm.hdu.edu.cn/showproblem.php?pid=2147 (kiki’s game)
[c]#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
int m,n;
while(scanf("%d %d",&m,&n),m||n)
{
m%=2;
n%=2;
if(m&&n)
printf("What a pity!n");
else
printf("Wonderful!n");
}
return 0;
}
[/c]
3. http://acm.hdu.edu.cn/showproblem.php?pid=2149 (public sale)
[c]
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
int m,n;
while(scanf("%d %d",&m,&n)!=EOF)
{
if(m<=n)
{
for(int i=m;i<=n;i++)
printf(i==m?"%d":" %d",i);
printf("n");
}
else if(m%(n+1)!=0)
printf("%dn",m%(n+1));
else
printf("nonen");
}
return 0;
}
[/c]
4. http://acm.hdu.edu.cn/showproblem.php?pid=2188 (选拔志愿者)
[c]
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
int t,m,n;
scanf("%d",&t);
while(t–)
{
scanf("%d %d",&m,&n);
if(m<=n || m%(n+1)!=0)
printf("Grassn");
else
printf("Rabbitn");
}
return 0;
}[/c]

下面介绍分析此类题目的通用方法:P/N分析:

P点: 即必败点,某玩家位于此点,只要对方无失误,则必败;

N点: 即必胜点,某玩家位于此点,只要自己无失误,则必胜。

三个定理:

一、 所有终结点都是必败点P(上游戏中,轮到谁拿牌,还剩0张牌的时候,此人就输了,因为无牌可取);

二、所有一步能走到必败点P的就是N点;

三、通过一步操作只能到N点的就是P点;

以上题目均可以通过P/N分析法来解决。

这几个题目都非常的简单。

对2147分析如图所示:

对2149做简单分析:(P/N分析也可以)

首先明确使用巴什博弈有个前提(特点)每次拿的数量是从1-m的不间断整数开始的。注意这一点很重要。无此条件巴什博弈不成立!

其实这个题目稍微动动脑就可以把它转换为巴什博弈(特点明显)。你就假设两人拍卖的时候是从给定的最高价开始—谁先拍卖到0谁就就是胜者。需要注意的是我们对特殊情况的考虑。
 

(二)威佐夫博奕(Wythoff Game):有两堆各若干个物品,两个人轮流从某一堆或同
时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。

这种情况下是颇为复杂的。我们用(ak,bk)(ak ≤ bk ,k=0,1,2,…,n)表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。

可以看出,a0=b0=0,ak是未在前面出现过的最小自然数,而 bk= ak + k,奇异局势有
如下三条性质:

1:任何自然数都包含在一个且仅有一个奇异局势中。
由于ak是未在前面出现过的最小自然数,所以有ak > ak-1 ,而 bk= ak + k > ak-1 + k-1 = bk-1 > ak-1 。所以性质1。成立。
2:任意操作都可将奇异局势变为非奇异局势。
事实上,若只改变奇异局势(ak,bk)的某一个分量,那么另一个分量不可能在其他奇异局势中,所以必然是非奇异局势。如果使(ak,bk)的两个分量同时减少,则由于其差不变,且不可能是其他奇异局势的差,因此也是非奇异局势。
3:采用适当的方法,可以将非奇异局势变为奇异局势。

假设面对的局势是(a,b),若 b = a,则同时从两堆中取走 a 个物体,就变为了奇异局势(0,0);如果a = ak ,b > bk,那么,取走b  – bk个物体,即变为奇异局势;如果 a = ak ,  b < bk ,则同时从两堆中拿走 ak – ab – ak个物体,变为奇异局势( ab – ak , ab – ak+ b – ak);如果a > ak ,b= ak + k,则从第一堆中拿走多余的数量a – ak 即可;如果a < ak ,b= ak + k,分两种情况,第一种,a=aj (j < k),从第二堆里面拿走 b – bj 即可;第二种,a=bj (j < k),从第二堆里面拿走 b – aj 即可。

从如上性质可知,两个人如果都采用正确操作,那么面对非奇异局势,先拿者必胜;反之,则后拿者取胜。

那么任给一个局势(a,b),怎样判断它是不是奇异局势呢?我们有如下公式:

ak =[k(1+√5)/2],bk= ak + k  (k=0,1,2,…,n 方括号表示取整函数)

奇妙的是其中出现了黄金分割数(1+√5)/2 = 1。618…,因此,由ak,bk组成的矩形近似为黄金矩形,由于2/(1+√5)=(√5-1)/2,可以先求出j=[a(√5-1)/2],若a=[(1+√5)/2],那么a = aj,bj = aj + j,若不等于,那么a = aj+1,bj+1 = aj+1+ j + 1,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异局势。

下面给个例子:http://acm.hdu.edu.cn/showproblem.php?pid=1527

有两堆石子,数量任意,可以不同。游戏开始由两个人轮流取石子。游戏规定,每次有两种不同的取法,一是可以在任意的一堆中取走任意多的石子;二是可以在两堆中同时取走相同数量的石子。最后把石子全部取完者为胜者。现在给出初始的两堆石子的数目,如果轮到你先取,假设双方都采取最好的策略,问最后你是胜者还是败者。

这道题就用到了上面的公式
[c]
#include<iostream>
#include<cstdio>
#include<cmath>
using namespace std;
int main(void)
{
int num1,num2,temp,k;
while(scanf("%d%d",&num1,&num2)==2)
{
if(num1>num2)
{
num1=num1^num2;
num2=num1^num2;
num1=num1^num2;
}
k=num2-num1;
temp=(int)(k*(1+sqrt(5))/2);
if(num1==temp)
cout<<"0"<<endl;
else
cout<<"1"<<endl;
}
return 0;
}
[/c]

 

(三)尼姆博奕(Nimm Game):有三堆各若干个物品,两个人轮流从某一堆取任意多的
物品,规定每次至少取一个,多者不限,最后取光者得胜。

这种情况最有意思,它与二进制有密切关系,我们用(a,b,c)表示某种局势,首先(0,0,0)显然是奇异局势,无论谁面对奇异局势,都必然失败。第二种奇异局势是(0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情形。

计算机算法里面有一种叫做按位模2加,也叫做异或的运算,我们用符号(+)表示这种运算。这种运算和一般加法不同的一点是1+1=0。先看(1,2,3)的按位模2加的结果:

1 =二进制01
2 =二进制10
3 =二进制11 (+)
———————
0 =二进制00 (注意不进位)

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

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

如果我们面对的是一个非奇异局势(a,b,c),要如何变为奇异局势呢?假设 a < b< c,我们只要将 c 变为 a(+)b,即可,因为有如下的运算结果: a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0。要将c 变为a(+)b,只要从 c中减去 c-(a(+)b)即可。

例1:(14,21,39),14(+)21=27,39-27=12,所以从39中拿走12个物体即可达到奇异局势(14,21,27)。

例2:(55,81,121),55(+)81=102,121-102=19,所以从121中拿走19个物品就形成了奇异局势(55,81,102)。

例3:(29,45,58),29(+)45=48,58-48=10,从58中拿走10个,变为(29,45,48)。

例4:我们来实际进行一盘比赛看看:
甲:(7,8,9)->(1,8,9)奇异局势
乙:(1,8,9)->(1,8,4)
甲:(1,8,4)->(1,5,4)奇异局势
乙:(1,5,4)->(1,4,4)
甲:(1,4,4)->(0,4,4)奇异局势
乙:(0,4,4)->(0,4,2)
甲:(0.4,2)->(0,2,2)奇异局势
乙:(0,2,2)->(0,2,1)
甲:(0,2,1)->(0,1,1)奇异局势
乙:(0,1,1)->(0,1,0)
甲:(0,1,0)->(0,0,0)奇异局势
甲胜。

下面先是一道简单的尼姆博弈题目

http://acm.hdu.edu.cn/showproblem.php?pid=1850

只要运用上面的知识即可解决(具体细节见代码)

[c]
//运用了重要性质:a^a=0
#include <iostream>
#include <cstdio>
#define N 105
using namespace std;

int main()
{
//freopen("data.txt","r",stdin);
int heapnum,heap[N];
while(scanf("%d",&heapnum),heapnum)
{
int sum=0,ans=0;
for(int i=1;i<=heapnum;i++)
{
scanf("%d",&heap[i]);
sum^=heap[i];
}
for(int i=1;i<=heapnum;i++)
if(heap[i]>(sum^heap[i]))
ans++;
printf("%dn",ans);
}
return 0;
}
[/c]

然而这并不是博弈的重点,LCY老师讲到博弈之王道乃是SG值(具体见 http://acm.hdu.edu.cn/forum/read.php?tid=6875)。

SG值:一个点的SG值就是一个不等于它的后继点的SG的且大于等于零的最小整数。

我的理解:在步骤允许的情况下,与前面一个必败点的差(也就是说这个差是规定的、能走的、其中一个步数)!
后继点:也就是按照题目要求的走法(比如取石子可以取的数量,方法)能够走一步达到的那个点。(sg值的理解很抽象。我的队友mo、xi说多画画就可以了)
现在我们拿http://acm.hdu.edu.cn/showproblem.php?pid=1847这道题分析一下。

我们枚举下牌数为2-10的sg值:(SG(x)=mex{SG(x-S[i])}。)

num: 2     3     4     5     6     7     8     9     10

sg值: 2     0     1      2     0     1     2     0      1

具体的代码如下:(当然这题用P/N分析要简单得多,这里仅理解sg值)

这个Runtime Error(STACK_OVERFLOW)了。。。。。。
[c]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define N 2000
int arr[11],sg[N];
int pre()
{
arr[0]=1;
for(int i=1;i<=10;i++)
arr[i]=arr[i-1]*2;
return 0;
}
int mex(int x)
{
int i;
if(sg[x]!=-1)
return sg[x];
bool vis[N];
memset(vis,false,sizeof(vis));
for(i=0;i<10;i++)
{
int temp=x-arr[i];
if(temp<0) break;
sg[temp]=mex(temp);
vis[sg[temp]]=true;
}
for(i=0;;i++)
{
if(!vis[i])
{
sg[x]=i; break;
}
}
return sg[x];
}
int main()
{
int num;
pre();
while(scanf("%d",&num)!=EOF)
{
memset(sg,-1,sizeof(sg));
if(mex(num))
printf("Kikin");
else printf("Cicin");
}
return 0;
}
[/c]

AC代码(这个题有规律的,呵呵)
首先我们可以想到在面对3的时候是必败局,谁面对3时无论拿多少都会败 ! <---这是关键>,那么就要尽量造成这样的局势给对方,因为任何不是3的倍数的数加1或2都可以变成3的倍数,同理减去1或2也可以变成3的倍数,也就是说假设目前的个数不是3的倍数,那我肯定能把它拿成3的倍数,比如现在是11个,那我拿走2个就变成9,这样就造成对方为3的倍数局势,那么对方拿m个我都可以通过拿1或者2使总共一轮拿的数目成为3的倍数,这样就会有两种情况:
1.刚好拿完.
2.剩下的还有3的倍数个,那继续;
所以这样拿下去必胜

[c]
#include <iostream>
#include <cstdio>
using namespace std;

int main()
{
int a;
while(scanf("%d",&a)!=EOF)
{
if(a%3==0)
printf("Cicin");
else
printf("Kikin");
}
return 0;
}
[/c]
(四)最后我们来看组合博弈:(博弈的精华)

博弈-取石子游戏

http://acm.hdu.edu.cn/forum/read.php?fid=20&tid=5748
http://hi.baidu.com/netnode/blog/item/30932c2edc7384514fc226ea.html

组合博弈无疑是对sg值的熟练操作例如:有n堆石子,每次可以从第1堆石子里取1颗、2颗或3颗,可以从第2堆石子里取奇数颗,可以从第3堆及以后石子里取任意颗…… 我们可以把它看作3个子游戏,第1个子游戏只有一堆石子,每次可以取1、2、3颗,很容易看出x颗石子的局面的SG值是x%4。第2个子游戏也是只有一堆 石子,每次可以取奇数颗,经过简单的画图可以知道这个游戏有x颗石子时的SG值是x%2。第3个游戏有n-2堆石子,就是一个Nim游戏。对于原游戏的每 个局面,把三个子游戏的SG值异或一下就得到了整个游戏的SG值,然后就可以根据这个SG值判断是否有必胜策略以及做出决策了。其实看作3个子游戏还是保 守了些,干脆看作n个子游戏,其中第1、2个子游戏如上所述,第3个及以后的子游戏都是“1堆石子,每次取几颗都可以”,称为“任取石子游戏”,这个超简单的游戏有x颗石子的SG值显然就是x。

发表评论