原文链接:http://www.cnblogs.com/frog112111/p/3199073.html
一、介绍概念:
P点 即必败点,某玩家位于此点,只要对方无失误,则必败;
N点 即必胜点,某玩家位于此点,只要自己无失误,则必胜。
定理:
(1) 所有终结点都是必败点P(上游戏中,轮到谁拿牌,还剩0张牌的时候,此人就输了,因为无牌可取);
(2)所有一步能走到必败点P的就是N点;
(3)通过一步操作只能到N点的就是P点;
首先来玩个游戏,引用杭电课件上的:
(1) 玩家:2人;
(2) 道具:23张扑克牌;
(3) 规则:
游戏双方轮流取牌;
每人每次仅限于取1张、2张或3张牌;
扑克牌取光,则游戏结束;
最后取牌的一方为胜者
自己画下图(N-P图)看看。
x : 0 1 2 3 4 5 6 7 8 9 10。。。
pos:P N N N P N N N P N N 。。。
所以若玩家甲位于N点。只要每次把P点让给对方,则甲必胜;
反之,若玩家甲位于P点,他每次只能走到N点,而只要乙每次把P点让给甲,甲必败;
hdu 2147 2188 2149 1847
二、巴什博奕(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)的倍数,就能最后获胜。
hdu 1846
AC代码如下:
1 #include<stdio.h> 2 int main() 3 { 4 int t,n,m; 5 scanf("%d",&t); 6 while(t--) 7 { 8 scanf("%d %d",&n,&m); 9 if(n%(m+1)==0) 10 printf("second "); 11 else 12 printf("first "); 13 } 14 return 0; 15 }
三、威佐夫博奕(Wythoff Game):
有两堆各若干个物品,两个人轮流从某一堆或同时从两堆中取同样多的物品,规定每次至少取一个,多者不限,最后取光者得胜。、
(此处先省略。。。。)
四、尼姆博奕(Nimm Game):
有三堆各若干个物品,两个人轮流从某一堆取任意多的物品,规定每次至少取一个,多者不限,最后取光者得胜。
1.有两个玩家;
2. 有三堆扑克牌(比如:可以分别是 5,7,9张);
3. 游戏双方轮流操作;
4. 玩家的每次操作是选择其中某一堆牌,然后从中取走任意张;
5.最后一次取牌的一方为获胜方;
对于一个Nim游戏的局面(a1,a2,...,an),它是P-position当且仅当a1^a2^...^an=0,其中^表示位异或(xor)运算。
参考博文:http://www.cnitblog.com/weiweibbs/articles/42734.html
http://www.cnblogs.com/kuangbin/archive/2011/08/28/2156426.html
证明:http://acm.hdu.edu.cn/forum/read.php?fid=9&tid=10617
任何奇异局势(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。
例:(14,21,39),14(+)21=27,39-27=12,所以从39中拿走12个物体即可达到奇异局势(14,21,27)。
我们来实际进行一盘比赛看看:
甲:(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)奇异局势
甲胜。
练习题:hdu 1849 1850
hdu1849解题报告:
Nim游戏模型:有三堆石子,分别含有a、b、c个石子。两人轮流从某一堆中取任意多的石子,规定每次至少取一个,多者不限。最后取光者得胜。
定理1:Nim游戏的一个状态(a、b、c)是P状态,当且仅当a^b^c =0。
对应此题就是:共有m个棋子就是m堆石子,把每个位置的标号等价于该堆石子的数目,取走最后一颗石子的人获胜,就是最后一个回到0位置的人获胜。即Nim博弈问题。
AC代码如下:
1 #include<stdio.h> 2 int main() 3 { 4 int n,i,Nim[1010]; 5 while(scanf("%d",&n)&&n!=0) 6 { 7 int sg=0; 8 for(i=0;i<n;i++) 9 { 10 scanf("%d",&Nim[i]); 11 sg^=Nim[i]; 12 } 13 if(sg==0) 14 printf("Grass Win! "); 15 else 16 printf("Rabbit Win! "); 17 } 18 return 0; 19 }
hdu 1850解题报告
题意:有n堆石子,两个人轮流从n堆石子中取走至少1个石子问先手能否获胜,若能,输出先手可取的方案数
解法:
- 本题中每一堆都可以选任意个,所以每一堆的SG值都是所剩余的个数。
- 最后结果是所有堆的SG值异或的结果。令sg = 所有堆的SG值异或的结果
- 如果sg == 0,则是必败点。
- 如果sg != 0,使取后结果为0的策略是必胜策略
- 具体怎么取呢? if(sg^a[i]<a[i]) ans++ 即可,为什么呢?
- 有n堆物品,最多有n个情况,因为一次只能拿一堆的物品,所以在这一堆拿多少受限于其他n-1堆,所以就可以枚举所有的奇异局势,看目前这一堆需要多少个物品才能满足奇异局势,(上面这些是针对开始时候是非奇异局势来说的),然后求出来这个值再与之比较
- 每一堆的数值与sg相异或,所得的结果就是这一堆取走物品后剩余的数量,即(a,b,c)-->(a,b,a^b), 因为sg=a^b^c,所以sg^c=a^b^(c^c)=a^b^0=a^b,故sg^c就是非奇异局势时 c 堆上取走一定的石子后剩余的数量,明显这要比原始的数量少才可行,所以要满足条件:sg^a[i]<a[i],只要满足这个条件就是一种可行方案了
AC代码如下:
1 #include<stdio.h> 2 int main() 3 { 4 int n,i,a[105]; 5 while(scanf("%d",&n)&&n) 6 { 7 int sg=0; 8 for(i=0;i<n;i++) 9 { 10 scanf("%d",&a[i]); 11 sg^=a[i]; 12 } 13 int ans=0; 14 for(i=0;i<n;i++) 15 if((sg^a[i])<a[i]) 16 ans++; 17 printf("%d ",ans); 18 } 19 return 0; 20 }
五、SG函数
参考博文:http://www.cnitblog.com/weiweibbs/articles/42735.html
SG即Sprague-Garundy函数,把博弈游戏抽象成有向无环图
(1) 有向无环图
(2) 玩家1先移动,起点是x0
(3) 两个玩家轮流移动
(4) 对于顶点x, 玩家能够移动到的顶点集记为F(x).
(5) 不能移动的玩家会输掉游戏
首先定义mex(minimal excludant)运算,这是施加于一个集合的运算,表示最小的不属于这个集合的非负整数。例如mex{0,1,2,4}=3、 mex{2,3,5}=0、mex{}=0。
定义: 一个图的Sprague-Grundy函数(X,F)是定义在X上的非负函数g(x),并且满足:
g(x) = mex{g(y) : y∈F(x)}
看到这里先好好理解一下sg值是怎么求的;
如果在取子游戏中每次只能取{1,2,3},那么各个数的SG值是多少?
x 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14. . .
g(x) 0 1 2 3 0 1 2 3 0 1 2 3 0 1 2. . .
看看这个和上面那个图的规律:
P-点: 即令 g(x) = 0 的 x 点!
N-点: 即令 g(x) > 0 的 x 点!
最后看下组合博弈,就是把简单的游戏组合起来,比如3堆的可以看成3个一堆的游戏。
定理:
假设游戏 Gi的SG函数是gi, i=1,…,n, 则
G = G1 + … + Gn 的 SG函数是
g(x1,…,xn) = g1(x1)⊕…⊕gn(xn).
其中那个符合就是异或^
看看是不是和Nim游戏的结论差不多?
SG打表模板 :
1 //f[]:可以取走的石子个数 2 //sg[]:0~n的SG函数值 3 //hash[]:mex{} 4 int f[N],sg[N],hash[N]; 5 void getSG(int n) 6 { 7 int i,j; 8 memset(sg,0,sizeof(sg)); 9 for(i=1;i<=n;i++) 10 { 11 memset(hash,0,sizeof(hash)); 12 for(j=1;f[j]<=i;j++) 13 hash[sg[i-f[j]]]=1; 14 for(j=0;j<=n;j++) //求mes{}中未出现的最小的非负整数 15 { 16 if(hash[j]==0) 17 { 18 sg[i]=j; 19 break; 20 } 21 } 22 } 23 }
dfs模板:
1 //注意 S数组要按从小到大排序 SG函数要初始化为-1 对于每个集合只需初始化1遍 2 //n是集合s的大小 S[i]是定义的特殊取法规则的数组 3 int s[110],sg[10010],n; 4 int SG_dfs(int x) 5 { 6 int i; 7 if(sg[x]!=-1) 8 return sg[x]; 9 bool vis[110]; 10 memset(vis,0,sizeof(vis)); 11 for(i=0;i<n;i++) 12 { 13 if(x>=s[i]) 14 { 15 SG_dfs(x-s[i]); 16 vis[sg[x-s[i]]]=1; 17 } 18 } 19 int e; 20 for(i=0;;i++) 21 if(!vis[i]) 22 { 23 e=i; 24 break; 25 } 26 return sg[x]=e; 27 }
有关SG 函数的模板请参考: