专题训练之博弈
推荐几篇文章: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 }
SG值相关:
1.(HDOJ3980)http://acm.hdu.edu.cn/showproblem.php?pid=3980
题意:有一串长度为n的珠子组成的圆环,每次必须将连续的m个珠子涂色(等效为取出柱子),当没有连续的m个珠子可以涂色时则判负
分析:首先对n<m&&n==m这两种情况进行特判。对于n>m的情况第一次取出m颗柱子,将圆环变成直线,同时先手变后手,后手变先手。对于剩下的n-m个珠子来说,从小到大求出每个位置对应的sg值,具体求法是考虑其每个后继点(若将一串变成两串,则需要将两串对应长度的SG值进行异或操作)的SG值,进而得到该位置的SG值
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int maxn=1005; 6 int sg[maxn]; 7 bool vis[maxn]; 8 9 int main() 10 { 11 int T,i,j,k,h,x,y,z,n,m; 12 scanf("%d",&T); 13 for ( h=1;h<=T;h++ ) { 14 scanf("%d%d",&n,&m); 15 printf("Case #%d: ",h); 16 if ( n<m ) printf("abcdxyzk\n"); 17 else if ( n==m ) printf("aekdycoin\n"); 18 else { 19 n-=m; 20 memset(sg,0,sizeof(sg)); 21 for ( i=m;i<=n;i++ ) { 22 memset(vis,false,sizeof(vis)); 23 vis[sg[i-m]]=true; 24 for ( j=1;j<(i-m)/2+1;j++ ) { 25 x=j; 26 y=i-m-j; 27 z=sg[x]^sg[y]; 28 vis[z]=true; 29 } 30 for ( j=0;j<=i;j++ ) { 31 if ( !vis[j] ) { 32 sg[i]=j; 33 break; 34 } 35 } 36 } 37 if ( sg[n]==0 ) printf("aekdycoin\n"); 38 else printf("abcdxyzk\n"); 39 } 40 } 41 return 0; 42 }
2.(HDOJ1944)http://acm.hdu.edu.cn/showproblem.php?pid=1944
题意:变形版的NIm,有n堆石子,每堆有num[i]个石子,但是每次取的石子数是有限制的,只有m种取法(f[1],f[2]……f[m]个),求先手是否会获胜
分析:计算对于一堆i个的石子其对应的SG值是多少,采用打表法
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<vector> 5 using namespace std; 6 const int maxn=10010; 7 const int maxm=110; 8 const int N=10000; 9 int sg[maxn],f[maxm],num[maxm]; 10 bool vis[maxn]; 11 vector<int>G[maxm]; 12 13 int main() 14 { 15 int n,m,i,j,k,x,y,z,q,maxx,ans; 16 while ( scanf("%d",&m)!=EOF && m ) { 17 for ( i=1;i<=m;i++ ) scanf("%d",&f[i]); 18 scanf("%d",&q); 19 maxx=N; 20 for ( i=1;i<=q;i++ ) G[i].clear(); 21 for ( i=1;i<=q;i++ ) { 22 scanf("%d",&num[i]); 23 for ( j=0;j<num[i];j++ ) { 24 scanf("%d",&x); 25 maxx=max(maxx,x); 26 G[i].push_back(x); 27 } 28 } 29 memset(sg,0,sizeof(sg)); 30 for ( i=1;i<=maxx;i++ ) { 31 memset(vis,false,sizeof(vis)); 32 for ( j=1;j<=m;j++ ) { 33 if ( f[j]<=i ) vis[sg[i-f[j]]]=true; 34 } 35 for ( j=0;j<=i;j++ ) { 36 if ( !vis[j] ) { 37 sg[i]=j; 38 break; 39 } 40 } 41 } 42 for ( i=1;i<=q;i++ ) { 43 ans=0; 44 for ( j=0;j<num[i];j++ ) { 45 x=G[i][j]; 46 ans^=sg[x]; 47 } 48 if ( ans==0 ) printf("L"); 49 else printf("W"); 50 } 51 printf("\n"); 52 } 53 return 0; 54 }
3.(HDOJ1404)http://acm.hdu.edu.cn/showproblem.php?pid=1404(暴力打表求SG)
题意:给定一段最大长度为6的字符串,先有两种操作,第一种是将某个位置的值变成比他小的数,第二种是若某一位置为0则将该位置及其后面的字符都删去,谁删去最后一个字符谁赢
分析:因为最大长度为6,所以可以考虑采用暴力的方式算出[1,1e6]范围内的sg值。对于0来说一定是个必胜点,而1一定是个必败点。故有sg[0]=1,sg[1]=0。那么对于那些能转化到1的数字来说都是必胜点(此题的SG值只考虑0/1),因此可以考虑从头到尾进行遍历,当一个位置是必败点时,则从该位置进行转化,转化得到的点都是必胜点。转化有两种方式,其中之一是分别扩大每一位,第二种方式是往后添加位数(先添0,再添其他),如对于1来说,10xxxx都是可以通过转化得到的。
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 #include<cmath> 5 using namespace std; 6 const int maxn=1e6+10; 7 const int N=1e6; 8 bool sg[maxn]; 9 10 int num(int m) 11 { 12 if ( m<10 ) return 1; 13 else if ( m<100 ) return 2; 14 else if ( m<1000 ) return 3; 15 else if ( m<10000 ) return 4; 16 else if ( m<100000 ) return 5; 17 else return 6; 18 } 19 20 void dfs(int m) 21 { 22 int i,j,k,x,y,cnt,n,q; 23 cnt=num(m); 24 for ( i=1;i<=cnt;i++ ) { 25 x=pow(10,i-1); 26 q=m/x%10; 27 n=m; 28 for ( j=q;j<9;j++ ) { 29 n+=x; 30 sg[n]=true; 31 } 32 } 33 y=m; 34 k=1; 35 while ( cnt<6 ) { 36 y*=10; 37 for ( i=0;i<k;i++ ) sg[y+i]=true; 38 k*=10; 39 cnt++; 40 } 41 } 42 43 int main() 44 { 45 int i,j,k,n,x; 46 char s[10]; 47 memset(sg,false,sizeof(sg)); 48 sg[0]=true; 49 for ( i=1;i<N;i++ ) { 50 if ( !sg[i] ) dfs(i); 51 } 52 while ( scanf("%s",s)!=EOF ) { 53 n=0; 54 if ( s[0]=='0' ) { 55 printf("Yes\n"); 56 continue; 57 } 58 for ( i=0;s[i];i++ ) { 59 x=s[i]-'0'; 60 n=n*10+x; 61 } 62 if ( sg[n] ) printf("Yes\n"); 63 else printf("No\n"); 64 } 65 return 0; 66 }
Every-SG博弈:
1.(HDOJ3595)http://acm.hdu.edu.cn/showproblem.php?pid=3595
Every-SG博弈:有N个游戏同时进行,每个游戏有两堆石子,每次从个数多的堆中取走数量小的数量的整数倍的石子。取最后一次的获胜。并且N个游戏同时进行,除非游戏结束,否则必须操作。
同时进行,必须操作这就是Every-SG的特点。
题意:两个人玩游戏,每次只能在石子数多的那一堆取出石子数少的那一堆的kk倍的石子,同时有nn个这样的游戏同时进行,谁不能取谁输。
分析:关于求解Every-SG博弈的解法及先手必胜的条件:
1 #include<cstdio> 2 #include<cstring> 3 #include<algorithm> 4 using namespace std; 5 const int maxn=1010; 6 const int inf=1e9; 7 int step[maxn][maxn],sg[maxn][maxn]; 8 9 int getstep(int a,int b) 10 { 11 if ( sg[a][b]!=-1 ) return sg[a][b]; 12 if ( a>b ) swap(a,b); 13 int M,mi; 14 M=-1,mi=inf; 15 for ( int i=a;i<=b;i+=a ) { 16 int k=getstep(a,b-i); 17 if ( k==0 ) { 18 M=max(M,step[a][b-i]); 19 sg[a][b]=sg[b][a]=1; 20 } 21 else mi=min(mi,step[a][b-i]); 22 } 23 if ( sg[a][b]==1 ) step[a][b]=step[b][a]=M+1; 24 else { 25 step[a][b]=step[b][a]=mi+1; 26 sg[a][b]=sg[b][a]=0; 27 } 28 return sg[a][b]; 29 } 30 31 int main() 32 { 33 int n,i,j,k,m,x,y,z,a,b,ans; 34 while ( scanf("%d",&n)!=EOF ) { 35 memset(sg,-1,sizeof(sg)); 36 ans=-1; 37 for ( i=0;i<=1000;i++ ) { 38 sg[i][0]=sg[0][i]=0; 39 step[i][0]=step[0][i]=0; 40 } 41 for ( i=0;i<n;i++ ) { 42 scanf("%d%d",&a,&b); 43 getstep(a,b); 44 ans=max(ans,step[a][b]); 45 } 46 if ( ans%2 ) printf("MM\n"); 47 else printf("GG\n"); 48 } 49 return 0; 50 }