博弈入门----SG
SG解题模型:
1.把原游戏分解成多个独立的子游戏,则原游戏的SG函数值是它的所有子游戏的SG函数值的异或。
即sg(G)=sg(G1)^sg(G2)^...^sg(Gn)。
2.分别考虑没一个子游戏,计算其SG值。
SG值的计算方法:(重点) 1.可选步数为1~m的连续整数,直接取模即可,SG(x) = x % (m+1);
2.可选步数为任意步,SG(x) = x;
3.可选步数为一系列不连续的数,用模板计算。
分割博弈
题意:长度为n的环,每次操作取m个连续的块染色,aek先手,轮流操作,不能继续操作的人输。
分析:长度为n的环,我们先让aek先取一段m,长度变成了n-m的链(这样可以使得往后每一次决策后状态相同).
局面由一个n变为两个长度为 x 和 n-x-m 子游戏了。由 SG 定理,SG(n)=SG(x)^SG(n-x-m)。再由 SG 函数的定义式 SG[u]=mex(seg[v]).然后进行记忆化搜索。SG[n-m]=0的时候就是先手必败的局面。
#include<bits/stdc++.h> using namespace std; const int maxn=1e3+10; int n,m,sg[maxn]; int mex( int n ) { if( sg[n]!=-1 ) return sg[n]; if( n<m ) return sg[n]=0; int vis[maxn]; memset(vis,0,sizeof(vis)); for( int i=0;i+m<=n;i++ ) { if( sg[i]==-1 ) sg[i]=mex(i); if( sg[n-i-m]==-1 ) sg[n-i-m]=mex(n-i-m); vis[sg[i]^sg[n-i-m]]=1; } for( int i=0;i<maxn;i++ ) { if( vis[i]==0 ) { sg[n]=i; break; } } return sg[n]; } int main() { int T,cas=0; cin>>T; while( T-- ) { cin>>n>>m; memset(sg,-1,sizeof(sg)); printf("Case #%d: ",++cas); if( m>n ) puts("abcdxyzk"); else if( m==n ) puts("aekdycoin"); else { n-=m; puts( mex(n) ? "abcdxyzk" : "aekdycoin" ); } } return 0; }
POJ2311
题意:一个n * m的方格纸,每次可以横着或者竖着切一刀。切出1*1格子的人算赢,问先手是否存在必胜战略。
分析:每次切割一定是至少从长度2开始切割,(2,3),(3,2),(2,2)局面都是先手必败的局面。那么我们根据决策进行sg打表。
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstdlib> #include<cstring> using namespace std; int sg[205][205]; int get_sg( int n,int m ) { if( sg[n][m]!=-1 ) return sg[n][m]; bool vis[1010]={false}; for( int i=2;i<=n/2;i++ ) { vis[get_sg(i,m)^get_sg(n-i,m)]=true; } for( int j=2;j<=m/2;j++ ) { vis[get_sg(n,j)^get_sg(n,m-j)]=true; } int i=0; while( vis[i] ) i++; return sg[n][m]=i; } int main() { int n,m; memset(sg,-1,sizeof(sg)); sg[2][2]=sg[2][3]=sg[3][2]=0; get_sg(200,200); while( cin>>n>>m ) { if( sg[n][m] ) puts("WIN"); else puts("LOSE"); } }
取走-分割游戏博弈
题意:Alice和Bob轮流取N堆石子,每堆S[i]个,Alice先,每一次可以从任意一堆中拿走任意个石子,也可以将一堆石子分为两个小堆。先拿完者获胜。
分析:SG定理: .找到所有子局面的sg值然后异或运算得到总局面的sg答案。
我们要预处理出所有子局面的sg值。
- 对于当前一堆石子数量为i,考虑可以拿任意堆,枚举1-i堆的sg值标记。
- 将一堆分成两堆非空石子,标记sg[j],sg[i-j].
- 然后根据SG定理中的mex()求得当前i状态的sg值。
但是由于打表复杂度为n^2,那么我们只能从打表中找到规律。
通过观察可以发现,每i%4==0的sg值为i-1,i%4==3的sg值为i+1,其他i的sg值为i.
那么我们就可以O(1)判断一个状态的sg值。
#include<bits/stdc++.h> using namespace std; const int maxn=2e5+10; int sg[maxn]; void init() { sg[0]=0; sg[1]=1; for( int i=1;i<=1000;i++ ) { bool vis[1010]={false}; for( int j=0;j<=i;j++ ) { vis[sg[j]]=true; if( j==0 || j==i ) continue; vis[sg[j]^sg[i-j]]=true; } int j=0; while( vis[j] ) j++; sg[i]=j; } /* for( int i=1;i<=1000;i++ ) { printf("%d ",sg[i]); if( i%4==0 ) puts(""); } */ } int get_sg( int x ) { if( x%4==0 ) return x-1; if( x%4==3 ) return x+1; return x; } int main() { init(); int T; scanf("%d",&T); while( T-- ) { int n,ans=0; scanf("%d",&n); for( int i=0;i<n;i++ ) { int x;scanf("%d",&x); ans^=get_sg(x); } if( ans==0 ) printf("Bob\n"); else printf("Alice\n"); } }
练习题
HDU1536
题意:给定n堆石子和一个取数的集合。每次移走的数量必须是集合里面的数字。最后取完的人赢。求必胜策略。
分析:SG打表模板
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstdlib> #include<cstring> using namespace std; const int maxn=1e4+10; int sg[maxn],a[maxn]; int n; bool vis[maxn]; int init_sg( int *s,int t ) { memset(sg,0,sizeof(sg)); for( int i=1;i<maxn;i++ ) { memset(vis,0,sizeof(vis)); for( int j=0;j<t;j++ ) { if( i-s[j]>=0 ) vis[sg[i-s[j]]]=true; } int j=0; while( vis[j] ) j++; sg[i]=j; } } int main() { int n; while( cin>>n && n ) { for( int i=0;i<n;i++ ) cin>>a[i]; sort(a,a+n); init_sg(a,n); int q; cin>>q; while( q-- ) { int m,ans=0; cin>>m; for( int i=0;i<m;i++ ) { int x;cin>>x; ans^=sg[x]; } if( ans ) printf("W"); else printf("L"); } puts(""); } }
HDU1847
题目大意:一堆石子,每次可以取2^k(0<k<32),最后取完的赢。问必胜策略。(n<=1000)
分析:和上题一样,有个取数集合。直接sg打表。(数据范围比较小
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstdlib> #include<cstring> using namespace std; const int maxn=1e3+10; int sg[maxn],a[maxn]; int n; bool vis[maxn]; int init_sg( int *s,int t ) { memset(sg,0,sizeof(sg)); for( int i=1;i<maxn;i++ ) { memset(vis,0,sizeof(vis)); for( int j=0;j<t;j++ ) { if( i-s[j]>=0 ) vis[sg[i-s[j]]]=true; } int j=0; while( vis[j] ) j++; sg[i]=j; } } int main() { a[0]=1; for( int i=1;i<12;i++ ) a[i]=a[i-1]*2; init_sg(a,12); int n; while( cin>>n ) { if( sg[n] ) puts("Kiki"); else puts("Cici"); } }
打表观察sg函数,发现n是3的倍数有先手必败。
#include <iostream> using namespace std; int main() { int t; cin>>t; while(t--) { string s; int sum=0; cin>>s; for(auto i: s) { sum+=i-'0'; } if(sum%3) cout<<"Alan"<<endl; else cout<<"Frame"<<endl; } return 0; }
HDU1848
题目大意:三堆石子,每次取石子只能去斐波那契数列数列的数字,最后取完的赢。问必胜策略。(n<=1000)
分析:sg打表。
#include<iostream> #include<cstdio> #include<algorithm> #include<cmath> #include<cstdlib> #include<cstring> using namespace std; const int maxn=4e3+10; int sg[maxn],a[maxn]; int n; bool vis[maxn]; int init_sg( int *s,int t ) { memset(sg,0,sizeof(sg)); for( int i=1;i<maxn;i++ ) { memset(vis,0,sizeof(vis)); for( int j=0;j<t;j++ ) { if( i-s[j]>=0 ) vis[sg[i-s[j]]]=true; } int j=0; while( vis[j] ) j++; sg[i]=j; } } int main() { a[0]=a[1]=1; for( int i=1;i<20;i++ ) a[i]=a[i-1]+a[i-2]; init_sg(a,20); int n,m,q; while( cin>>n>>m>>q && ( n || m || q ) ) { if( sg[n]^sg[m]^sg[q] ) puts("Fibo"); else puts("Nacci"); } }