推荐几篇文章:https://blog.csdn.net/strangedbly/article/details/51137432(SG函数详细介绍)
https://blog.csdn.net/shahdza/article/details/7832997 博弈题集
SG函数解题模型:
1.把原游戏分解成多个独立的子游戏,则原游戏的SG函数值是它的所有子游戏的SG函数值的异或。
即sg(G)=sg(G1)^sg(G2)^...^sg(Gn)。
2.分别考虑没一个子游戏,计算其SG值。
SG值的计算方法:(重点)
A.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
B.可选步数为任意步,SG(x) = x;
C.可选步数为一系列不连续的数,用模板计算。
模板:假设起点为n,总共有m种移动步数可以选择
1.打表(首选打表预处理)
思路:对于一个位置,找到其所有的后继点的SG值有哪些,该位置的SG值即为不等于其后继点的SG值的最小非负整数
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int maxn=1005; 6 const int maxm=105; 7 int f[maxm],sg[maxn]; //f表示移动步数,sg表示每个位置的sg值 8 bool vis[maxn]; //vis表示对于某个位置来说,其后继节点的sg值有哪些 9 int n,m; 10 11 void get_sg() 12 { 13 int i,j; 14 memset(sg,0,sizeof(sg)); 15 for ( i=1;i<=n;i++ ) { 16 memset(vis,false,sizeof(vis)); 17 for ( j=1;f[j]<=i&&j<=m;j++ ) vis[sg[i-f[j]]]=true; //对后继节点所处的位置标记为true 18 for ( j=0;j<=i;j++ ) { //该位置的sg值 19 if ( !vis[j] ) { 20 sg[i]=j; 21 break; 22 } 23 } 24 } 25 }
2.dfs
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int maxn=1005; 6 const int maxm=105; 7 int f[maxn],sg[maxn],n,m; //sg除了位置0初始化为0外,其余点初始化为-1 8 bool vis[maxn]; 9 10 int dfs(int x) 11 { 12 if ( sg[x]!=-1 ) return sg[x]; 13 memset(vis,false,sizeof(vis)); 14 for ( int i=1;i<=m;i++ ) { 15 if ( x>f[i] ) { 16 int y=dfs(x-f[i]); 17 vis[y]=true; 18 } 19 } 20 for ( int i=0;i<=n;i++ ) { 21 if ( !vis[i] ) return sg[x]=i; 22 } 23 }
打表找规律求SG值:这类题目可以通过暴力求得小范围的SG值,再通过找规律得到SG值取值的规律
1.(HDOJ2897)http://acm.hdu.edu.cn/showproblem.php?pid=2897
分析:bash博弈变形,一般的bash博弈是从[1,m]个数中取一个,判断先手是否获胜只用看n%(1+m)是否为0。此题是从[p,q],当i<=p时都为必败点,当p<i<=q时都为必胜点。而原先必定是以(p+q)为周期的。所以只需对n%(p+q),若结果为0或者结果大于p则为输出WIN,否则输出LOST
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 6 int main() 7 { 8 int n,q,p; 9 while ( scanf("%d%d%d",&n,&p,&q)!=EOF ) { 10 n%=(p+q); 11 if ( n>p || n==0 ) printf("WIN\n"); 12 else printf("LOST\n"); 13 } 14 return 0; 15 }
2.(HDOJ3032)http://acm.hdu.edu.cn/showproblem.php?pid=3032
题意:有n堆,每堆有s[i]个,每次可以从一堆中取走[1,a[i]]个或者将这堆分成任意数量的两堆,Alice先,求最后谁获胜
分析:暴力打表得sg+找规律。得到sg( 4k+1 ) = 4k+1; sg( 4k+2 ) = 4k+2; sg( 4k+3 ) = 4k+4; sg( 4k+4 ) = 4k+3;最后再将这n个sg值异或和即可。
规律的正确性证明:http://acm.hdu.edu.cn/discuss/problem/post/reply.php?postid=29685&messageid=1&deep=0
变形:可以分成任意数量的三堆
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 typedef long long ll; 6 7 int main() 8 { 9 ll T,n,i,j,k,x,y,z,ans; 10 scanf("%lld",&T); 11 while ( T-- ) { 12 scanf("%lld",&n); 13 ans=0; 14 for ( i=1;i<=n;i++ ) { 15 scanf("%lld",&x); 16 if ( x%4==3 ) x++; 17 else if ( x%4==0 ) x--; 18 ans^=x; 19 } 20 if ( ans!=0 ) printf("Alice\n"); 21 else printf("Bob\n"); 22 } 23 return 0; 24 }
3.(HDOJ3537)http://acm.hdu.edu.cn/showproblem.php?pid=3537(翻硬币模型)
翻硬币游戏
一般的翻硬币游戏的规则是这样的:
N 枚硬币排成一排,有的正面朝上,有的反面朝上。我们从左开始对硬币按1 到N 编号。
第一,游戏者根据某些约束翻硬币,但他所翻动的硬币中,最右边那个硬币的必须是从正面翻到反面。例如,只能翻3个硬币的情况,那么第三个硬币必须是从正面翻到反面。如果局面是正正反,那就不能翻硬币了,因为第三个是反的。
第二,谁不能翻谁输。有这样的结论:局面的SG 值为局面中每个正面朝上的棋子单一存在时的SG 值的异或和。即一个有k个硬币朝上,朝上硬币位置分布在的翻硬币游戏中,SG值是等于k个独立的开始时只有一个硬币朝上的翻硬币游戏的SG值异或和。比如THHTTH这个游戏中,2号、3号、6号位是朝上的,它等价于TH、TTH、TTTTTH三个游戏和,即sg[THHTTH]=sg[TH]^sg[TTH]^sg[TTTTTH].我们的重点就可以放在单个硬币朝上时的SG值的求法。
推荐博客:http://www.cnblogs.com/kuangbin/p/3218060.html(翻硬币模型的8种变形)
总结:对于每种模型,先将其变成求单个位置上的sg值,如:T,FT,FFT,FFFT(F表示反面,T表示正面),再根据游戏规则求出该位置的后驱有几种情况,并且求出每种情况的SG值是多少,进而得到该位置的SG值。然后根据前几组数据找规律
注意:对于本题而言,对于某个位置i来说,当二进制表示下1的个数为1时sg[i]=2*i,否则为sg[i]=2*i+1。特别注意本题的输入数据可能包含同一个位置的硬币,所以需要用set记录已经使用过的位置
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<set> 5 using namespace std; 6 set<int>st; 7 8 bool judge(int x) 9 { 10 int cnt=0; 11 for ( int i=0;(1<<i)<=x;i++ ) { 12 int y=1<<i; 13 if ( y&x ) cnt++; 14 } 15 if ( cnt%2==1 ) return true; 16 else return false; 17 } 18 19 int main() 20 { 21 int n,i,j,k,x,y,z,ans; 22 while ( scanf("%d",&n)!=EOF ) { 23 ans=0; 24 st.clear(); 25 for ( i=0;i<n;i++ ) { 26 scanf("%d",&x); 27 if ( st.find(x)!=st.end() ) continue; 28 st.insert(x); 29 if ( judge(x) ) x=2*x; 30 else x=2*x+1; 31 ans^=x; 32 } 33 if ( ans ) printf("No\n"); 34 else printf("Yes\n"); 35 } 36 return 0; 37 }
反nim博弈:
1.(HDOJ2509)http://acm.hdu.edu.cn/showproblem.php?pid=2509
题意:有n堆,每堆有a[i]个,现在每次可以从第i堆中取[1,a[i]]个,谁取到最后一个谁输(nim博弈为谁取到最后一个谁赢)
分析:首先给出结论:先手胜当且仅当 ①所有堆石子数都为1且游戏的SG值为0(即有偶数个孤单堆-每堆只有1个石子数);②存在某堆石子数大于1且游戏的SG值不为0.
给出简略证明:对于①来说当每堆石子数都为1时最后的结果一定是固定的,所以易得当有偶数堆1时最后先手一定会输,反之先手一定会赢
对于②来说。先假设只有一堆(记作第X堆)的石子数个数大于1(此时游戏的SG值一定不为0),对于剩下的堆数则满足①,若剩下石子数为1的堆数为奇数,为确保先手获胜,那么先手直接取完第X堆中的所有石子。若剩下石子数为1的堆数为偶数,那么我们可以将第X堆的石子取到只剩1个,那么对于该游戏来说石子数为1的堆数变成了奇数。
当石子数大于1的堆数超过一堆时,此时若游戏的SG值不为0时,先手只需要将游戏的SG值变成0,剩下的事就大体同Nim博弈了,只是Nim博弈最后取走最后一堆的所以石子,而此题最后一堆时只需要留一颗石子即可
1 #include<cstdio> 2 using namespace std; 3 4 int main() 5 { 6 int n,ans,i,x; 7 bool flag; 8 while ( scanf("%d",&n)!=EOF ) { 9 ans=0; 10 flag=false; 11 for ( i=1;i<=n;i++ ) { 12 scanf("%d",&x); 13 ans^=x; 14 if ( x>1 ) flag=true; 15 } 16 if ( flag ) { 17 if ( ans==0 ) printf("No\n"); 18 else printf("Yes\n"); 19 } 20 else { 21 if ( n%2 ) printf("No\n"); 22 else printf("Yes\n"); 23 } 24 } 25 return 0; 26 }
2.(HDOJ1907)http://acm.hdu.edu.cn/showproblem.php?pid=1907
同上
1 #include<cstdio> 2 using namespace std; 3 4 int main() 5 { 6 int n,ans,i,x,T; 7 bool flag; 8 scanf("%d",&T); 9 while ( T-- ) { 10 scanf("%d",&n); 11 ans=0; 12 flag=false; 13 for ( i=1;i<=n;i++ ) { 14 scanf("%d",&x); 15 ans^=x; 16 if ( x>1 ) flag=true; 17 } 18 if ( flag ) { 19 if ( ans==0 ) printf("Brother\n"); 20 else printf("John\n"); 21 } 22 else { 23 if ( n%2 ) printf("Brother\n"); 24 else printf("John\n"); 25 } 26 } 27 return 0; 28 }