博弈论小结之尼姆博弈

博弈论小结

一、重头戏之尼姆博弈

  概述:有n堆若干个石子,两个人轮流从某一堆取任意多的石子,规定每次至少取一个,多者不限,最后取光者得胜。

  分析一下可知,如果n-2堆石子个数为0,剩下的两堆石子个数相同,那么面临这种局势的人必败。就比如说我有三堆石头个数分别为5 5 0,那么不论先手怎么操作,后手只需要在另一堆中取走同样多的石子就可以一直保证自己不败。5 5 0 --> 3 5 0 --> 3 3 0 --> 0 3 0 -->0 0 0 后手必胜。

  现在我们引入L . Bouton在1902年给出的定理:状态(x1,x2,x3...xn)为必败状态当且仅当x1^x2^x3……^xn=0。

  57(10) = 111001(2) ,即:57(10)=2^5+2^4+2^3+2^0。于是,我们可以认为每一堆物品数由2的幂数的子堆组成。这样,含有57枚物品大堆就能看成是分别由数量为32、16、8、1的各个子堆组成。 

  现在考虑各大堆大小分别为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定理先手能够在非平衡尼姆博弈中取胜,而后手能够在平衡的尼姆博弈中取胜。
上模板题:

Matches Game

 POJ - 2234 
Here is a simple game. In this game, there are several piles of matches and two players. The two player play in turn. In each turn, one can choose a pile and take away arbitrary number of matches from the pile (Of course the number of matches, which is taken away, cannot be zero and cannot be larger than the number of matches in the chosen pile). If after a player’s turn, there is no match left, the player is the winner. Suppose that the two players are all very clear. Your job is to tell whether the player who plays first can win the game or not.
Input
The input consists of several lines, and in each line there is a test case. At the beginning of a line, there is an integer M (1 <= M <=20), which is the number of piles. Then comes M positive integers, which are not larger than 10000000. These M integers represent the number of matches in each pile.
Output
For each test case, output "Yes" in a single line, if the player who play first will win, otherwise output "No".
Sample Input
2 45 45
3 3 6 9
 
Sample Output
No
Yes
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 using namespace std;
 5 int main()
 6 {
 7     int n;
 8     while(~scanf("%d",&n))
 9     {
10         int sum=0;
11 
12         for(int i=0;i<n;i++)
13         {
14             int x;
15             scanf("%d",&x);
16             sum^=x;
17         }//printf("%d\n",sum);
18         if(sum!=0)
19         {
20             printf("Yes\n");
21         }
22         else
23         {
24             printf("No\n");
25         }
26     }
27 }
深入一下,讲一下SG函数和SG定理;推荐极好的博客  戳我戳我
首先介绍一下PN状态:
必胜点和必败点的概念:
       P点:必败点,换而言之,就是谁处于此位置,则在双方操作正确的情况下必败。

   N点:必胜点,处于此情况下,双方操作均正确的情况下必胜。

必胜点和必败点的性质:

      1、所有终结点是必败点 P。(我们以此为基本前提进行推理,换句话说,我们以此为假设)

        2、从任何必胜点N 操作,至少有一种方式可以进入必败点 P。

        3、无论如何操作,必败点P 都只能进入必胜点 N。

大学英语四级考试就要来临了,你是不是在紧张的复习?也许紧张得连短学期的ACM都没工夫练习了,反正我知道的Kiki和Cici都是如此。当然,作为在考场浸润了十几载的当代大学生,Kiki和Cici更懂得考前的放松,所谓“张弛有道”就是这个意思。这不,Kiki和Cici在每天晚上休息之前都要玩一会儿扑克牌以放松神经。 
“升级”?“双扣”?“红五”?还是“斗地主”? 
当然都不是!那多俗啊~ 
作为计算机学院的学生,Kiki和Cici打牌的时候可没忘记专业,她们打牌的规则是这样的: 
1、  总共n张牌; 
2、  双方轮流抓牌; 
3、  每人每次抓牌的个数只能是2的幂次(即:1,2,4,8,16…) 
4、  抓完牌,胜负结果也出来了:最后抓完牌的人为胜者; 
假设Kiki和Cici都是足够聪明(其实不用假设,哪有不聪明的学生~),并且每次都是Kiki先抓牌,请问谁能赢呢? 
当然,打牌无论谁赢都问题不大,重要的是马上到来的CET-4能有好的状态。 

Good luck in CET-4 everybody! 

Input输入数据包含多个测试用例,每个测试用例占一行,包含一个整数n(1<=n<=1000)。Output如果Kiki能赢的话,请输出“Kiki”,否则请输出“Cici”,每个实例的输出占一行。 
Sample Input
1
3
Sample Output
Kiki
Cici
   n 1 2 3 4 5 6 ...
状态 P P N P P N.... 对于6来说,先手可以拿走1、2、4三种,给后手剩下5、4、2,然而后手面对5、4、2都必胜的,所以先手必败。
所以这是有规律的,找出规律,就变得简单了。
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 using namespace std;
 5 int main()
 6 {
 7    int n;
 8    while(~scanf("%d",&n))
 9    {
10        if(n%3==0)
11         puts("Cici");
12        else
13         puts("Kiki");
14    }
15    return 0;
16 }
再来一个稍微复杂一点的: hdu 2147 kiki's game
Recently kiki has nothing to do. While she is bored, an idea appears in his mind, she just playes the checkerboard game.The size of the chesserboard is n*m.First of all, a coin is placed in the top right corner(1,m). Each time one people can move the coin into the left, the underneath or the left-underneath blank space.The person who can't make a move will lose the game. kiki plays it with ZZ.The game always starts with kiki. If both play perfectly, who will win the game? 
InputInput contains multiple test cases. Each line contains two integer n, m (0<n,m<=2000). The input is terminated when n=0 and m=0. 

OutputIf kiki wins the game printf "Wonderful!", else "What a pity!". 
Sample Input
5 3
5 4
6 6
0 0
Sample Output
What a pity!
Wonderful!
Wonderful!
画出NP关系图如图:
P N P N P N P
N N N N N N N
P N P N P N P
N N N N N N N
P N P N P N P
N N N N N N N
P N P N P N P

 

 

P N P N P N P
N N N N N N N
P N P N P N P
N N N N N N N
P N P N P N P
N N N N N N N
P N P N P N P

这个图从终止点开始往回画,最后一个点一定是必败态,故为P,而它的前驱(图中加红的一定为必胜态),之后看最上面一行和最右面一行,图中蓝色状态的下一步只能往上走,而上面一个格为必胜态,所有蓝色状态一定为必败态,以此类推得出最上面一行和最右面一行。而对于图中紫色的状态,因为它的下一步可以是(N,N,P),只要其中有必败态,那么它一定为必胜态。只有当该状态的下一步状态全为必胜态时,它才会变成必败态。如图中橙色状态。
 1 #include <cstdio>
 2 #include <cstdlib>
 3 #include <cstring>
 4 #include <algorithm> 
 5 using namespace std; 
 6 int main(){
 7     int n,m;
 8     while(scanf("%d %d",&n,&m)!=EOF){
 9         if(n==0 && m==0)break;
10         if(n%2==0 || m%2==0){
11             printf("Wonderful!\n");
12         }else{
13             printf("What a pity!\n");
14         }
15     }
16     return 0;
17 }

  现在我们就来介绍今天的主角吧。组合游戏的和通常是很复杂的,但是有一种新工具,可以使组合问题变得简单————SG函数和SG定理。

Sprague-Grundy定理(SG定理):

        游戏和的SG函数等于各个游戏SG函数的Nim和。这样就可以将每一个子游戏分而治之,从而简化了问题。而Bouton定理就是Sprague-Grundy定理在Nim游戏中的直接应用,因为单堆的Nim游戏 SG函数满足 SG(x) = x。

SG函数:

        首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、mex{2,3,5}=0、mex{}=0。

        对于任意状态 x ,定义 SG(x) = mex(S),其中 S 是 x 后继状态的SG函数值的集合。如 x 有三个后继状态分别为 SG(a),SG(b),SG(c),那么SG(x) = mex{SG(a),SG(b),SG(c)}。 这样集合S 的终态必然是空集,所以SG函数的终态为 SG(x) = 0,当且仅当 x 为必败点P时。

 

【实例】取石子问题

 

有1堆n个的石子,每次只能取{ 1, 3, 4 }个石子,先取完石子者胜利,那么各个数的SG值为多少?

 

SG[0]=0,f[]={1,3,4},

x=1 时,可以取走1 - f{1}个石子,剩余{0}个,所以 SG[1] = mex{ SG[0] }= mex{0} = 1;

x=2 时,可以取走2 - f{1}个石子,剩余{1}个,所以 SG[2] = mex{ SG[1] }= mex{1} = 0;

x=3 时,可以取走3 - f{1,3}个石子,剩余{2,0}个,所以 SG[3] = mex{SG[2],SG[0]} = mex{0,0} =1;

x=4 时,可以取走4-  f{1,3,4}个石子,剩余{3,1,0}个,所以 SG[4] = mex{SG[3],SG[1],SG[0]} = mex{1,1,0} = 2;

x=5 时,可以取走5 - f{1,3,4}个石子,剩余{4,2,1}个,所以SG[5] = mex{SG[4],SG[2],SG[1]} =mex{2,0,1} = 3;

以此类推.....

   x     0  1  2  3  4  5  6  7  8....

SG[x]    0  1  0  1  2  3  2  0  1....

由上述实例我们就可以得到SG函数值求解步骤,那么计算1~n的SG函数值步骤如下:

1、使用数组f 将可改变当前状态的方式记录下来。

2、然后我们使用另一个数组将当前状态x 的后继状态标记。

3、最后模拟mex运算,也就是我们在标记值中 搜索 未被标记值的最小值,将其赋值给SG(x)。

4、我们不断的重复 2 - 3 的步骤,就完成了 计算1~n 的函数值。

代码实现如下:

 1 //f[N]:可改变当前状态的方式,N为方式的种类,f[N]要在getSG之前先预处理
 2 //SG[]:0~n的SG函数值
 3 //S[]:为x后继状态的集合
 4 int f[N],SG[MAXN],S[MAXN];
 5 void  getSG(int n){
 6     int i,j;
 7     memset(SG,0,sizeof(SG));
 8     //因为SG[0]始终等于0,所以i从1开始
 9     for(i = 1; i <= n; i++){
10         //每一次都要将上一状态 的 后继集合 重置
11         memset(S,0,sizeof(S));
12         for(j = 0; f[j] <= i && j <= N; j++)
13             S[SG[i-f[j]]] = 1;  //将后继状态的SG函数值进行标记
14         for(j = 0;; j++) if(!S[j]){   //查询当前后继状态SG值中最小的非零值
15             SG[i] = j;
16             break;
17         }
18     }
19 }
那么上面的hdu 1847 Good Luck in CET-4 Everybody!这个题,就可以用SG函数做:
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cmath>
 4 #include <cstring>
 5 #include <algorithm>
 6 using namespace std;
 7 const int maxn=1e4;
 8 int n,m,p;
 9 int f[50],sg[maxn],s[maxn];
10 void init()
11 {
12     for(int i=0;i<=15;i++)
13     {
14         f[i]=pow(2,i);
15     }
16 return;
17 }
18 void getsg(int n){
19     int i,j;
20     memset(sg,0,sizeof(sg));
21     for(i=1;i<=n;i++){
22         memset(s,0,sizeof(s));
23         for(j=0;f[j]<=i&&j<=maxn;j++)
24             s[sg[i-f[j]]]=1;
25         for(j=0;;j++) if(!s[j]){
26             sg[i]=j;
27             break;
28         }
29     }
30 }
31 int main()
32 {
33     init();
34     getsg(1010);
35     while(~scanf("%d",&n))
36     {
37         if(sg[n])
38             puts("Kiki");
39         else
40             puts("Cici");
41     }
42 }

再来一道!Fibonacci again and again HDU - 1848 

 

任何一个大学生对菲波那契数列(Fibonacci numbers)应该都不会陌生,它是这样定义的: 
F(1)=1; 
F(2)=2; 
F(n)=F(n-1)+F(n-2)(n>=3); 
所以,1,2,3,5,8,13……就是菲波那契数列。 
在HDOJ上有不少相关的题目,比如1005 Fibonacci again就是曾经的浙江省赛题。 
今天,又一个关于Fibonacci的题目出现了,它是一个小游戏,定义如下: 
1、  这是一个二人游戏; 
2、  一共有3堆石子,数量分别是m, n, p个; 
3、  两人轮流走; 
4、  每走一步可以选择任意一堆石子,然后取走f个; 
5、  f只能是菲波那契数列中的元素(即每次只能取1,2,3,5,8…等数量); 
6、  最先取光所有石子的人为胜者; 

假设双方都使用最优策略,请判断先手的人会赢还是后手的人会赢。 

Input输入数据包含多个测试用例,每个测试用例占一行,包含3个整数m,n,p(1<=m,n,p<=1000)。 
m=n=p=0则表示输入结束。 
Output如果先手的人能赢,请输出“Fibo”,否则请输出“Nacci”,每个实例的输出占一行。 
Sample Input

1 1 1
1 4 1
0 0 0

Sample Output

Fibo
Nacci
 1 #include <iostream>
 2 #include <cstdio>
 3 #include <cstring>
 4 #include <algorithm>
 5 using namespace std;
 6 const int maxn=1e4;
 7 int n,m,p;
 8 int f[maxn],sg[maxn],s[maxn];
 9 void init()
10 {
11     f[0]=1;
12     f[1]=2;
13     for(int i=2;i<30;i++)
14         f[i]=f[i-1]+f[i-2];
15 return;
16 }
17 void getsg(int n){
18     int i,j;
19     memset(sg,0,sizeof(sg));
20     for(i=1;i<=n;i++){
21         memset(s,0,sizeof(s));
22         for(j=0;f[j]<=i&&j<=maxn;j++)
23             s[sg[i-f[j]]]=1;
24         for(j=0;;j++) if(!s[j]){
25             sg[i]=j;
26             break;
27         }
28     }
29 }
30 int main()
31 {
32     init();
33     getsg(1000);
34     while(~scanf("%d%d%d",&n,&m,&p)&&n+m+p)
35     {
36         if(sg[n]^sg[m]^sg[p])
37             puts("Fibo");
38         else
39             puts("Nacci");
40     }
41 }

未完待续。。。

 

 

posted @ 2018-10-06 21:29  *starry*  阅读(1517)  评论(0编辑  收藏  举报