这几天开始学习博弈,发现这一块是个难啃的骨头。以下是我从网上收集的资料汇总:
我国民间有个古老的游戏:就是有物品若干堆,(物品可以是火柴,围棋都可以)。两个人轮流从堆中取若干件,规定取光物体者为胜。这个就是我们今天要研究的组合游戏。
组合游戏定义:
1、有且仅有两个玩家 2、游戏双方轮流操作 3、游戏操作状态是个有限的集合(比如:取石子游戏,石子是有限的,棋盘中的棋盘大小的有限的) 4、游戏必须在有限次内结束 5、当一方无法操作时,游戏结束。
现在我们来研究如何取胜:
(一)巴什博奕(Bash Game):有一堆n个物品,两人轮流从堆中取物品,每次取 x 个 ( 1 ≤ x ≤ m)。最后取光者为胜。
如果 n = m + 1, 一次至多取 m 个,所以无论先取者,取了多少个,一定还剩余 x 个( 1 ≤ x ≤ m)。所以,后取者必胜。因此我们发现了取胜的秘诀:如果我们把 n 表示为
n = (m + 1) * r + s 。(0 ≤ s < m , r ≥ 0)。先取者 拿走 s 个, 后取者 拿走 k 个 (1 ≤ k ≤ m),那么先取者 再 拿走 m + 1 - k 个。结果还剩下 ( m + 1 ) * ( r - 1 ) 个。我们只要始终给对手留下 m + 1 的倍数,那么 先取者 肯定必胜。 现在 我们可以知道,如果 s = 0,那么后取者必胜。 否则 先取者 必胜。
看完这个可以用这个练练手:hdu 1846 Brave Game
(二)威佐夫博奕(Wythoff Game):有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
这种情况下是颇为复杂的。我们用(a[k],b[k])(a[k] ≤ b[k] ,k=0,1,2,...,n)( a[k] 其中 k 为下标 )表示两堆物品的数量并称其为局势,如果甲面对(0,0),那么甲已经输了,这种局势我们称为奇异局势。前几个奇异局势是:(0,0)、(1,2)、(3,5)、(4,7)、(6,10)、(8,13)、(9,15)、(11,18)、(12,20)。
可以看出,a[0] = b[0] = 0,a[k]是未在前面出现过的最小自然数,而 b[k] = a[k] + k。
奇异局势的性质:
1、任何自然数都包含在一个且仅有一个奇异局势中。
由于ak是未在前面出现过的最小自然数,所以有a[k] > a[k-1] ,而 b[k] = a[k] + k > a[k-1] + k > a[k-1] + k - 1 = b[k-1] > a[k-1] 。所以性质1成立。
2。任意操作都可将奇异局势变为非奇异局势。
事实上,若只改变奇异局势(a[k],b[k])的某一个分量,那么另一个分量不可能在其他奇异局势中,所以必然是非奇异局势。如果使(a[k],b[k])的两个分量同时减少,则由于其差不变,且不可能是其他奇异局势的差,因此也是非奇异局势。
3。采用适当的方法,可以将非奇异局势变为奇异局势。
假设面对的局势是(a , b),若 b = a,则同时从两堆中取走 a 个物体,就变为了奇异局势(0,0);如果 a = a[k] ,b > b[k] ,那么,取走b - b[k]个物体,即变为奇异局势;如果 a = a[k] , b < b[k] 则同时从两堆中拿走a - a[b-a] 个物体(如果不懂为什么减去a - a[b-a],详见文章最后的注释1)变为奇异局势( a[b-a], a[b-a] + b - a);如果a > a[k] ,b= a[k] + k 则从第一堆中拿走多余的数量a - a[k] 即可;如果a < a[k] ,b= a[k] + k,分两种情况,第一种,a=a[j] (j < k)从第二堆里面拿走 b - b[j] 即可;第二种,a=b[j] (j < k)从第二堆里面拿走 b - a[j] 即可。
由上述性质可知,如果双方都采取正确操作,那么面对非奇异局势,先取者必胜。
那么我们要如何判断一个局势是否为奇异局势?公式如下:
a[k] = [k(1+√5)/2](a[k]这个方括号为下标运算符,[k(1+√5)/2]这个方括号为取整运算符),b[k] = a[k] + k 。奇妙的是其中出现了黄金分割数(1+√5)/2 = 1.618...因此,由a[k],b[k]组成的矩形近似为黄金矩形,由于2/(1+√5)=(√5-1)/2,可以先求出 j = [a(√5-1)/2],若 a = [ j(1+√5)/2],那么a = a[j],b[j] = a[j] + j,若不等于,那么a = a[j]+1,b = a[j] + j + 1,若都不是,那么就不是奇异局势。然后再按照上述法则进行,一定会遇到奇异局势。
看完这个可以做一做poj 1067 取石子游戏,练练手
#include <stdio.h>
#include <stdlib.h>
#include <math.h>
int main(){
int a,b,k,a_k;
while(scanf("%d%d",&a,&b)!=EOF){
k = abs(a-b);
a = a < b? a : b;
a_k = floor(k*(1.0 + sqrt(5.0))/2);
printf("%d\n",a!=a_k);
//输出为0,说明该点为必败点,1为必胜点
}
return 0;
}
【文字描述上面代码】
k = | a - b |, a[k] = [ k(1+√5)/2], 如果 a,b中的最小值, min{a,b} 与 a[k]相等,那么 他就是奇异局势。
(三)尼姆博奕(Nimm Game):有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
这种情况最有意思,它与二进制有密切关系,我们用(a,b,c)表示某种局势,首先(0,0,0)显然是奇异局势,无论谁面对奇异局势,都必然失败。第二种奇异局势是(0,n,n),只要与对手拿走一样多的物品,最后都将导致(0,0,0)。仔细分析一下,(1,2,3)也是奇异局势,无论对手如何拿,接下来都可以变为(0,n,n)的情形。
计算机算法里面有一种叫做按位模2加,也叫做异或的运算,我们用符号(+)表示这种运算,先看(1,2,3)的按位模2加的结果:
1 =二进制01
2 =二进制10
3 =二进制11 (+)
———————
0 =二进制00 (注意不进位)
对于奇异局势(0,n,n)也一样,结果也是0。
任何奇异局势(a,b,c)都有a(+)b(+)c =0。
注意到异或运算的交换律和结合律,及a(+)a=0,:
a(+)b(+)(a(+)b)=(a(+)a)(+)(b(+)b)=0(+)0=0。
所以从一个非奇异局势向一个奇异局势转换的方式可以是:
1)使 a = c(+)b
2)使 b = a(+)c
3)使 c = a(+)b
那么对于(57 8 9 10)呢?
1.从5中取不了,因为7^8^9^10=12
2.从7中取不了,因为5^8^9^10=14
3.可以从8中取7个(5,7,1,9,10)
4.可以从9中去除9个(5,7, 8, 0,10)
5.可以从10中去除7个(5,7,8,9,3)
如何计算取法?
方法一:二重循环
for(i=0;i<n;i++) { int m=0; for(j=0;j<n;j++) { if(j==i) continue; m^a[j]; } if(a[i]>m) num=a[i]-m; }
方法2:再异或(当对其中所有元素已经异或一次得到值m后,再对其中某一个值a[i]进行异或,得到的值是去除a[i]后剩下所有元素的异或值m1)
for(i=0;i<n;i++)//假设已经求出了所有值的异或结果 { int k=m^a[i]; if(k<a[i]) { printf("%d",a[i]-k);//a[i]-k代表要拿走的值 } }
同样给一个练手题:HDU 2176
(四)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时。
SG函数适应题型:一堆石子,可取的石子数不连续;
【实例】取石子问题
有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 的函数值。
代码实现如下:
//SG[]:0~n的SG函数值 //S[]:为x后继状态的集合 int f[N]={1,3,4},SG[MAXN],S[MAXN]; void getSG(int n){ int i,j; memset(SG,0,sizeof(SG)); //因为SG[0]始终等于0,所以i从1开始 for(i = 1; i <= n; i++){ //每一次都要将上一状态 的 后继集合 重置 memset(S,0,sizeof(S)); for(j = 0; f[j] <= i && j <= N; j++) S[SG[i-f[j]]] = 1; //将后继状态的SG函数值进行标记 for(j = 0;; j++) if(!S[j]){ //查询当前后继状态SG值中最小的非零值 SG[i] = j; break; } } }
现在我们来一个实战演练(题目链接):
只要按照上面的思路,解决这个就是分分钟的问题。
代码如下:
#include <stdio.h> #include <string.h> #define MAXN 1000 + 10 #define N 20 int f[N],SG[MAXN],S[MAXN]; void getSG(int n){ int i,j; memset(SG,0,sizeof(SG)); for(i = 1; i <= n; i++){ memset(S,0,sizeof(S)); for(j = 0; f[j] <= i && j <= N; j++) S[SG[i-f[j]]] = 1; for(j = 0;;j++) if(!S[j]){ SG[i] = j; break; } } } int main(){ int n,m,k; f[0] = f[1] = 1; for(int i = 2; i <= 16; i++) f[i] = f[i-1] + f[i-2]; getSG(1000); while(scanf("%d%d%d",&m,&n,&k),m||n||k){ if(SG[n]^SG[m]^SG[k]) printf("Fibo\n"); else printf("Nacci\n"); } return 0; }
大家是不是还没有过瘾,那我就在给大家附上一些组合博弈的题目:
POJ 2234 Matches Game
HOJ 4388 Stone Game II
POJ 2975 Nim
HOJ 1367 A Stone Game
POJ 2505 A multiplication game
ZJU 3057 beans game
POJ 1067 取石子游戏
POJ 2484 A Funny Game
POJ 2425 A Chess Game
POJ 2960 S-Nim
POJ 1704 Georgia and Bob
POJ 1740 A New Stone Game
POJ 2068 Nim
POJ 3480 John
POJ 2348 Euclid's Game
HOJ 2645 WNim
POJ 3710 Christmas Game
POJ 3533 Light Switching Game
下面是杭电OJ部分博弈题解
HDU 1517 A Multiplication Game
#include<iostream> using namespace std; int main() { double n; while(cin>>n) { while(n>18) n/=18; if(n<=9) cout<<"Stan wins."<<endl; else cout<<"Ollie wins."<<endl; } return 0; }
①、如果输入是2~9,因为Stan是先手,所以Stan必胜。
②、如果输入是10~18(9*2),因为Ollie是后手,不管第一次Stan乘的是多少,Stan肯定在2~9之间,如果Stan乘以2,那么Ollie就乘以9,那么Ollie乘以大于1的数都能超过10~18中的任何一个数,Ollie必胜。
③、如果输入的是19~162(9*2*9),那么这个范围Stan必胜。
④、如果输入是163~324(2*9*2*9),这个是Ollie的必胜范围。
…………
可以发现必胜态是对称的。
如果“我方”首先给出了一个在N不断除18后的得到不足18的数M,“我方”就可以胜利,然而双方都很聪明,所以这样胜负就决定与N了,如果N不断除18后的得到不足18的数M,如果1<M<=9则先手胜利,即Stan wins.如果9<M<=18则后手胜利。
巴什博弈n%(m+1)==0先手必败。
#include <iostream> #include<cstdio> #include<cstring> #include<cstdlib> #include<cmath> using namespace std; int main() { int n,a,b; scanf("%d",&n); while(n--) { scanf("%d%d",&a,&b); if(a%(b+1)==0) printf("second\n"); else printf("first\n"); } return 0; }
hdu1847 good luck in cet--4 everybody
只要留下两类都是2的指数幂就是必输状态然后找规律发现这两类的和为3的倍数。即
有下面的结论。 若总数为 3 的倍数,后手保证所拿数量与先手之和为3的倍数则必赢!!
若总数非3的倍数,先手拿除于3的余数,以后先手所拿数量与后手所拿之和为 3 的倍数,则先手必赢!!
#include <iostream> #include<cstdio> #include<cmath> using namespace std; int main() { int n; while(scanf("%d",&n)!=EOF) { if(n%3==0) printf("Cici\n"); else printf("Kiki\n"); } return 0; }
HDU 1848 Fibonacci again and again
#include <iostream> #include<cstdio> #include<cmath> #include<cstring> using namespace std; #define N 1005 int f[N]; int sg[N]; void fun() { int i; f[0]=1; f[1]=1; f[2]=2; for(i=3;;i++) { f[i]=f[i-1]+f[i-2]; if(f[i]>1000) break; } } int dfs(int v) { int i; if(sg[v]!=-1) return sg[v]; bool visit[N]={0}; for(i=1;i<16;i++) { if(v>=f[i]) { int temp=dfs(v-f[i]); visit[temp]=1; } } for(i=0;visit[i];i++); return sg[v]=i; } int main() { fun(); int m,n,p; while(scanf("%d%d%d",&m,&n,&p),m||n||p) { memset(sg,-1,sizeof(sg)); int ans; ans=dfs(m)^dfs(n)^dfs(p); if(ans) printf("Fibo\n"); else printf("Nacci\n"); } return 0; }
HDU1849 Rabbit and Grass
裸的NIM博弈直接异或即可。
#include <iostream> #include<cstdio> using namespace std; int main() { int n,p; while(scanf("%d",&n)!=EOF) { if(n==0)break; int ans=0; for(int i=0;i<n;i++) { scanf("%d",&p); ans^=p; } if(ans==0) printf("Grass Win!\n"); else printf("Rabbit Win!\n"); } return 0; }
HDU 1850 Being a Good Boy in Spring Festival
简单NIM博弈如果异或不为0找出异或可以使其成为0的值 if(num[j]>(ans^num[j]))这
是关键。
#include <iostream> #include<cstdio> #include<cmath> #include<cstring> using namespace std; int num[105]; int main() { int t; while(scanf("%d",&t)&&t) { int ans=0; for(int i=0;i<t;i++) { scanf("%d",&num[i]); ans^=num[i]; } int cnt=0; for(int j=0;j<t;j++) { if(num[j]>(ans^num[j])) { cnt++; } } printf("%d\n",cnt); } return 0; }
HDU 1851 A Simple Game
简单NIM博弈
#include <iostream> #include<cstdio> #include<cmath> #include<cstdlib> #include<cstring> using namespace std; int main() { int t,n,m,l; scanf("%d",&t); while(t--) { scanf("%d",&n); int ans=0; while(n--) { scanf("%d%d",&m,&l); ans^=(m%(l+1)); } if(ans==0) printf("Yes\n"); else printf("No\n"); } return 0; }
HDU1907 John
NIM博弈全是1的时候特判数1的个数奇数输偶数赢了。
#include <iostream> #include<cstdio> #include<cmath> #include<cstring> #include<cstdlib> using namespace std; int main() { int t,num; scanf("%d",&t); while(t--) { int ans=0,flag=0; int p[50]; scanf("%d",&num); for(int i=0; i<num; i++) { scanf("%d",&p[i]); if(p[i]!=1) flag=1; ans^=p[i]; } if(flag) { if(ans==0) printf("Brother\n"); else printf("John\n"); }else { if(num%2!=0) printf("Brother\n"); else printf("John\n"); } } return 0; }
判断P和N的状态画出图标即可。利用SG函数的基本性质。P后面的都是NN后面的
一定存在一个P状态。 观察图表规律知道n和m都是奇数时候必败。
#include <iostream> #include<cstdio> #include<cstring> #include<cmath> #include<cstring> using namespace std; int main() { int n,m; while(scanf("%d%d",&n,&m)!=EOF) { if(n==0&&m==0)break; if(n&1&&m&1) printf("What a pity!\n"); else printf("Wonderful!\n"); } return 0; }
简单的巴什博弈。
#include <iostream> #include<cstdio> #include<cmath> #include<cstdlib> #include<cstring> using namespace std; int main() { int m,n; while(scanf("%d%d",&m,&n)!=EOF) { if(m%(n+1)==0) { printf("none\n"); continue; } else if(n>m) { for(int i=m;i<n;i++) printf("%d ",i); printf("%d\n",n); } else printf("%d\n",m%(n+1)); //这里是开始的时候如果先出m%(n+1)个那么剩下的就是 //必败态了。 } return 0; }