吴昊品游戏核心算法(新年特别篇)—— 各种博弈各种博


(一)巴什博奕(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)
2. http://acm.hdu.edu.cn/showproblem.php?pid=2147 (kiki's game)
3. http://acm.hdu.edu.cn/showproblem.php?pid=2149 (public sale)
4. http://acm.hdu.edu.cn/showproblem.php?pid=2188 (选拔志愿者)
下面介绍分析此类题目的通用方法:P/N分析:
P点: 即必败点,某玩家位于此点,只要对方无失误,则必败;
N点: 即必胜点,某玩家位于此点,只要自己无失误,则必胜。
三个定理:
定理:
     一、 所有终结点都是必败点P(上游戏中,轮到谁拿牌,还剩0张牌的时候,此人就输了,因为无牌可取);
    二、所有一步能走到必败点P的就是N点;
    三、通过一步操作只能到N点的就是P点;
以上题目均可以通过P/N分析法来解决。
这几个题目都非常的简单。下面仅对2149做简单分析:(P/N分析也可以)
       首先明确使用巴什博弈有个前提(特点)每次拿的数量是从1-m的不间断整数开始的。注意这一点很重要。无此条件巴什博弈不成立!
       其实这个题目稍微动动脑就可以把它转换为巴什博弈(特点明显)。你就假设两人拍卖的时候是从给定的最高价开始—谁先拍卖到0谁就就是胜者。需要注意的是我们对特殊情况的考虑。具体见代码:

 1 #include<cstdio>  
 2 #include<algorithm>  
 3  using  namespace std;  
 4  
 5  int main()  
 6 {  
 7     freopen( " game.in ", " r ",stdin); freopen( " game.out ", " w ",stdout);  
 8      int sum,maxnum;  
 9      while(scanf( " %d%d ",∑,&maxnum)!=EOF)  
10     {  
11          if(sum<=maxnum)  
12         {  
13              for( int i=sum;i<=maxnum;i++) printf(i==sum? " %d ": "  %d ",i);  
14             printf( " /n ");  
15         }  
16          else  if(sum%(maxnum+ 1)!= 0)  
17         {  
18              int result=sum%(maxnum+ 1);  
19             printf( " %d/n ",result);  
20         }  
21          else printf( " none/n ");  
22     }  
23      return  0;  
24 } 


(二)威佐夫博奕(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,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异局势。(三)尼姆博奕(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 =二进制012 =二进制103 =二进制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)。下面先是一道简单的尼姆博弈题目http://acm.hdu.edu.cn/showproblem.php?pid=1850只要运用上面的知识即可解决(具体细节见代码)

 1   // 运用了重要性质:a^a=0  
 2   #include<cstdio> 
 3  #include<algorithm>  
 4   using  namespace std;  
 5   #define N 100+10  
 6   int main()  
 7  {      
 8    freopen( " game.in ", " r ",stdin);         
 9    freopen( " game.out ", " w ",stdout);      
10     int heapnum,heap[N];       
11     while(scanf( " %d ",&heapnum)!=EOF && heapnum)      
12    {          
13       int sum= 0,ans= 0;         
14       for( int i= 1;i<=heapnum;i++)         
15      {           
16        scanf( " %d ",&heap[i]);  
17        sum^=heap[i];          
18      }        
19       for( int i= 1;i<=heapnum;i++)          
20      {             
21         if(heap[i]>(sum^heap[i])) 
22          ans++; // 大于号的优先级要高于异或运算!          
23       }         
24      printf( " %d/n ",ans);   
25    }    
26     return  0
27  } 

 SG函数:

  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值)
 

 1 #include<cstdio>  
 2 #include<algorithm>  
 3  using  namespace std;  
 4  #define N 1000+10  
 5  
 6  int arr[ 11],sg[N];  
 7  
 8  int pre() // 把1000以内的所有的可能一次拿的牌都算出来!   
 9  {  
10     arr[ 0]= 1;  
11      for( int i= 1;i<= 10;i++) arr[i]=arr[i- 1]* 2;  
12      return  0;  
13 }  
14  
15  int mex( int x) // 这是求解改点的sg值的算法函数(采用记忆化搜索)   
16  {  
17      if(sg[x]!=- 1return sg[x];  
18      bool vis[N];  
19     memset(vis, false, sizeof(vis));  
20      for( int i= 0;i< 10;i++)  
21     {  
22          int temp=x-arr[i];  
23          if(temp< 0break;  
24         sg[temp]=mex(temp);  
25         vis[sg[temp]]= true;  
26     }  
27      for( int i= 0;;i++)  
28     {  
29          if(!vis[i])  
30         {  
31             sg[x]=i;  break;  
32         }  
33     }  
34      return sg[x];  
35 }  
36  
37  int main()  
38 {  
39     freopen( " game.in ", " r ",stdin); freopen( " game.out ", " w ",stdout);  
40      int num;  
41     pre();  
42      while(scanf( " %d ",&num)!=EOF)  
43     {  
44         memset(sg,- 1, sizeof(sg));  
45          if(mex(num)) printf( " Kiki/n ");    
46          else printf( " Cici/n ");   
47     }  
48      return  0;  
49 }  
50         

 组合博弈(各种博弈的叠加):

 博弈-取石子游戏
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。

 1 #include<cstdio>
 2 #include<cmath>
 3 #include<algorithm>
 4  using  namespace std;
 5 typedef  long  long int64;
 6 
 7  int main()
 8 {
 9     freopen( " game.in ", " r ",stdin); freopen( " game.out ", " w ",stdout);
10      int anum,bnum;
11      double xx=sqrt( 5.0);
12      while(scanf( " %d%d ",&anum,&bnum)!=EOF)
13     {
14          if(anum== 0 && bnum== 0break;
15          bool flag= false, tag= false;
16          if(anum==( int)(abs(bnum-anum)*( 1+xx)/ 2)) printf( " 0/n ");
17          else
18         {
19              int a1,b1,a2,b2;
20              // if(!anum) printf("0 0/n");
21               for( int i= 1;i<=bnum;i++)
22             {
23                  if((anum-i)>= 0 && !flag) // 在两堆中都拿
24                  {
25                      if((anum-i)==( int)(abs(bnum-anum)*( 1+xx)/ 2))
26                     {
27                         a1=anum-i; b1=bnum-i; flag= true;
28                     }
29                 }
30                  if((anum-i>= 0) && !tag) // 在其中一堆中拿(假设a堆多,在a堆中拿)
31                  {
32                      if((anum-i)==( int)(abs(bnum-anum+i)*( 1+xx)/ 2))
33                     {
34                         a2=anum-i; b2=bnum; tag= true;
35                     }
36                 }
37                  if(!tag) // 在b堆中拿
38                  {
39                      if(((bnum-i)<anum?(bnum-i):anum)==( int)(abs((bnum-i)-anum)*( 1+xx)/ 2))
40                     {
41                         a2=anum; b2=bnum-i; tag= true;
42                      }
43                 }
44                  if(flag && tag)  break;
45             }
46              if(flag || tag)
47             {
48                 printf( " 1/n ");
49                  if(flag) printf( " %d %d/n ",a1,b1);
50                  if(tag)
51                 {
52                      if(a2>b2) printf( " %d %d/n ",b2,a2);
53                      else printf( " %d %d/n ",a2,b2);
54                 }
55             }
56         }
57     }
58      return  0;
59 }


 
 1 #include<iostream>
 2  using  namespace std;
 3  #define c 1.618033989
 4  int f( int a, int b)                                      // 判断(a,b)是否处于必败态
 5  {
 6           int k,s;
 7          k=b-a;
 8          s=k* 1.618033989;
 9           if(s==a)  return  1;
10           else  return  0;
11 }
12  int main()
13 {
14           int a,b,s,k,m,a1,b1;
15           while(cin>>a>>b)
16          {
17                     if(!a&&!b)  break;
18                     if(f(a,b))
19                    {
20                             cout<< " 0\n ";
21                              continue;
22                    }
23                    cout<< " 1\n ";
24                     if(!a)
25                    {
26                             cout<< " 0 0\n ";
27                              continue;
28                    }
29                     if(a==b)
30                    {
31                             cout<< " 0 0\n ";
32                    }
33                    k=b-a;
34                     if(k)                   // 两堆石子同时取走相同数量的石子后,使对方处于必败态
35                     {
36                             m=k/c;
37                             a1=m+k;
38                             b1=m+ 2*k;
39                              if(a>a1&&b>b1&&f(a1,b1)) cout<<a1<< '   '<<b1<<endl;
40                    }
41                    s=b/c;
42                     if(f(s,b)&&s<a) cout<<s<< '   '<<b<<endl;     // 从第一堆取石,剩s个,判断(s,b)
43                      if(a!=b)
44                    {
45                             s=a/c;                            // 从第二堆取石,剩s个,判断(s,a)
46                               if(f(s,a)&&(a-s!=b-a)) cout<<s<< '   '<<a<<endl;
47                    }
48                     for(m=a+ 1;m<b;m++)
49                    {
50                              if(f(a,m))                        // 从第二堆取石,剩m个,判断(a,m)
51                              {
52                                      cout<<a<< '   '<<m<<endl;
53                                       break;
54                             }
55                    }
56          }
57           return  1;
58 }
59 
60 

 

你可能感兴趣的:(算法)