博弈论题目总结(二)——SG组合游戏及变形

时间:2022-07-30 08:30:08

SG函数

为了更一般化博弈问题,我们引入SG函数

SG函数有如下性质:

1.如果某个状态SG函数值为0,则它后继的每个状态SG函数值都不为0

2.如果某个状态SG函数值不为0,则它至少存在一个后继的状态SG函数值为0

如果某个局面SG函数值为0,则该局面先手必败

放到有向图中,该有向图的核就是SG值为0的点构成的集合

游戏的和

游戏的和的SG函数值=所有子游戏SG函数值的异或和Xor

如果所有子游戏都进行完毕,那么Xor=0,必败

如果某个状态的SG函数值为0,那么后手一定可以做出一种动作,保持Xor=0,那么先手必败。

反之某个状态的SG函数值不为0,先手可以让Xor=0,变成后手,重复上述动作,那么先手必胜

这样就能轻松合并多个独立的组合游戏啦

mex函数

$sg[$当前局面$]=mex(sg[$后继局面$])$

$mex$函数表示第一次还没出现的数

某种后继局面可能是很多个子游戏,那么该后继局面的$sg$函数就是这些子游戏的和,即子游戏$sg$函数的异或和

SG组合游戏

NIM游戏

有n堆石子,两个人玩游戏,每次轮流在一堆里取走任意个,取走最后一堆的最后一个石子的人赢,问谁赢

一堆石子相当于一个子游戏,显然该子游戏的SG函数值为该堆中石子数

再根据游戏的和的思想,把子游戏合并就能求出谁赢了

NIMk游戏

同样是NIM游戏,现在变成了每次在k堆中取任意个

NIM游戏采取策略的根本是,保证当SG函数值为0时,不论先手如何操作,后手一定能做出一种动作,保持Xor=0

把每堆石子都转化成k+1进制数,再进行不进位的加法即可

SG组合游戏

POJ 2960 S-Nim (SG函数递推)

裸题,考察对SG函数的理解。利用游戏的和以及mex函数。暴力递推出石子SG函数即可

 #include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 10010
#define ll long long
#define ull unsigned long long
using namespace std; const int inf=0x3f3f3f3f;
int gint()
{
int ret=,fh=;char c=getchar();
while(c<''||c>''){if(c=='-')fh=-;c=getchar();}
while(c>=''&&c<=''){ret=ret*+c-'';c=getchar();}
return ret*fh;
} int n,m,o,T,A,B,de;
int s[N1],a[N1],sg[N1],use[N1]; int main()
{
while(scanf("%d",&m)) { if(m==) break;
int i,j,ans=,flag;
memset(sg,,sizeof(sg));
for(i=;i<=m;i++) s[i]=gint();
sg[]=;
for(i=;i<=;i++)
{
for(j=;j<=m;j++) if(i>=s[j]) use[sg[i-s[j]]]=;
for(j=;j<=m;j++) if(!use[j]){ sg[i]=j; break; }
for(j=;j<=m;j++) if(i>=s[j]) use[sg[i-s[j]]]=;
} o=gint();
while(o--) { n=gint();
for(i=;i<=n;i++) a[i]=gint();
for(i=,ans=;i<=n;i++) ans^=sg[a[i]];
if(ans) putchar('W'); else putchar('L'); }
puts(""); }
return ;
}

BZOJ 2940 条纹 (SG函数递推)

稍微复杂了一点,但也没什么好说的,暴力递推SG函数就行了

 #include <cstdio>
#include <cstring>
#include <algorithm>
#define il inline
#define N1 1010
using namespace std;
const int maxn=; template <typename _T> void read(_T &ret)
{
ret=; _T fh=; char c=getchar();
while(c<''||c>''){ if(c=='-') fh=-; c=getchar(); }
while(c>=''&&c<=''){ ret=ret*+c-''; c=getchar(); }
ret=ret*fh;
} int T,n,A,B,C;
int use[N1],sg[N1]; int main()
{
scanf("%d%d%d",&A,&B,&C);
if(A>B) swap(A,B); if(A>C) swap(A,C); if(B>C) swap(B,C);
int i,j,k;
for(i=A;i<=maxn;i++)
{
for(j=;j+A<=i;j++) use[sg[j]^sg[i-j-A]]=;
for(j=;j+B<=i;j++) use[sg[j]^sg[i-j-B]]=;
for(j=;j+C<=i;j++) use[sg[j]^sg[i-j-C]]=;
for(j=;j<=maxn*maxn;j++) if(!use[j]){ sg[i]=j; break; }
for(j=;j+A<=i;j++) use[sg[j]^sg[i-j-A]]=;
for(j=;j+B<=i;j++) use[sg[j]^sg[i-j-B]]=;
for(j=;j+C<=i;j++) use[sg[j]^sg[i-j-C]]=;
}
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
if(sg[n]) puts(""); else puts("");
//printf("%d\n",f[n]);
}
return ;
} /* */

一个不错的模型转化

POJ 1704 Georgia and Bob (阶梯博弈)

题目大意:略

把相邻两个数的距离的差值-1看成一堆石子,第一堆石子数是第一个数到1的距离

那么把第i个数向左移x格相当于把x个石子从第i堆放到第i+1堆里,而挪第n堆的石子则是把石子移出游戏

游戏结束状态就是所有的石子都移出了游戏

发现从右往左数的偶数堆(第2,4,6..堆)的石子是无意义的,因为先手挪,后手就跟着挪,先手只会输

我们只考虑从右往左数的奇数堆,先手把石子从奇数堆移动到了偶数堆,相当于把这些石子移除游戏,也就是删除了奇数堆中的这些石子!

问题转化成了NIM游戏!我们只对从右往左数的奇数堆讨论即可

 #include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 10010
#define ll long long
#define ull unsigned long long
using namespace std; const int inf=0x3f3f3f3f;
int gint()
{
int ret=,fh=;char c=getchar();
while(c<''||c>''){if(c=='-')fh=-;c=getchar();}
while(c>=''&&c<=''){ret=ret*+c-'';c=getchar();}
return ret*fh;
} int n,m,T,A,B,de;
int a[N1],sg[N1]; int main()
{
scanf("%d",&T);
while(T--){ int i,j,ans=;
scanf("%d",&n);
for(i=;i<=n;i++) a[i]=gint();
sort(a+,a+n+);
for(i=;i<=n;i++) sg[n-i+]=a[i]-a[i-]-; sg[n]=a[]-;
for(i=;i<=n;i+=) ans^=sg[i];
if(!ans) puts("Bob will win");
else puts("Georgia will win"); }
return ;
}

很多问题里状态很大,我们不能预处理出SG函数值,只能打表找规律

博弈问题的精髓是打表!

HDU 3032 Nim or not Nim (SG函数打表)

题目大意:NIM游戏,每次操作还可以把一堆石子分成两堆,问谁赢

先预处理出单独一堆石子时的sg函数值。

那么,该游戏的sg函数值=每堆石子的sg函数值的异或和

暴力枚举后继状态,打个表找规律就行了

 #include <cmath>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 1000050
#define ll long long
#define dd double
using namespace std;
const dd eps=1e-; template <typename _T> void read(_T &ret)
{
ret=; _T fh=; char c=getchar();
while(c<''||c>''){ if(c=='-') fh=-; c=getchar(); }
while(c>=''&&c<=''){ ret=ret*+c-''; c=getchar(); }
ret=ret*fh;
} int T,n;
int a[N1],sg[N1],use[N1];
const int maxn=; int main()
{
scanf("%d",&T);
while(T--){ scanf("%d",&n);
int i,j,sum=;
for(i=;i<=n;i++)
{
read(a[i]);
if(a[i]%==) sum^=a[i]+;
else if(a[i]%==) sum^=a[i]-;
else sum^=a[i];
}
if(!sum) puts("Bob"); else puts("Alice"); }
return ;
} /*
scanf("%d",&n);
int i,j;
//for(i=1;i<=n;i++) scanf("%d",&a[i]);
sg[0]=0; sg[1]=1;
for(i=2;i<=n;i++)
{
use[sg[0]]=1;
for(j=1;j<i;j++) use[sg[j]]=1, use[sg[j]^sg[i-j]]=1;
for(j=0;j<=i*2;j++) if(!use[j]){ sg[i]=j; break; }
use[sg[0]]=0;
for(j=1;j<i;j++) use[sg[j]]=0, use[sg[j]^sg[i-j]]=0;
}
//for(i=0;i<=n;i++) printf("%d:%d\n",i,sg[i]);
for(i=1;i<=n;i++) printf("%d\n",sg[i]);
*/

HDU 4644 Triangulation (SG函数打表)

题目大意:圆上有$a_{i}$个点,两个人玩游戏,轮流在这些点之间连边,边和边不能交叉,现在有n个圆,问谁赢

和上面的题一模一样的套路,每次连线都会产生两个子游戏,先写暴力打表,然后找规律

规律比较丧病

 #include <cstdio>
#include <cstring>
#include <algorithm>
#define il inline
#define N1 1000010
using namespace std; template <typename _T> void read(_T &ret)
{
ret=; _T fh=; char c=getchar();
while(c<''||c>''){ if(c=='-') fh=-; c=getchar(); }
while(c>=''&&c<=''){ ret=ret*+c-''; c=getchar(); }
ret=ret*fh;
} int T,n;
int a[N1];
int sg1[]={,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,};
int sg2[]={,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,,}; int main()
{
scanf("%d",&T);
while(T--) { scanf("%d",&n);
int i,j,sum=;
for(i=;i<=n;i++)
{
read(a[i]);
if(a[i]<=) sum^=sg1[a[i]-];
else sum^=sg2[(a[i]-)%];
}
if(sum) puts("Carol"); else puts("Dave"); }
return ;
} /* */

SG组合游戏变形

气氛变得怪异起来

一些SG组合游戏的终止状态比较特殊,我们可以通过转化解决

POJ 3537 Crosses and Crosses

题目大意:给出一个1*n的网格,一开始全都是白格子,两个人轮流把一个白格子涂黑,谁先涂出来连续3个黑格子谁就赢了

游戏的结束状态不容易直接搞啊

我们剖析游戏本身的性质

先手把一个格子涂黑后,它左右一共连续5个格子(边界另外讨论)一定不能被后手涂

相当于每次涂黑一个格子,删掉连续的不超过5个格子,两侧剩下的格子构成了一个或两个子游戏

依次求出长度为1~n的连续格子的游戏的SG函数即可

 #include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 2010
#define M1 200010
#define ll long long
#define dd double
using namespace std;
const dd eps=1e-; int n,tl;
int sg[N1],use[N1],que[N1]; int main()
{
scanf("%d",&n);
if(n==){ puts(""); return ; }
if(n==){ puts(""); return ; }
if(n==){ puts(""); return ; }
if(n==){ puts(""); return ; }
sg[]=; sg[]=sg[]=sg[]=; sg[]=;
int i,j;
for(i=;i<=n;i++)
{
que[++tl]=sg[i-]; que[++tl]=sg[i-];
for(j=;j+<=i;j++) que[++tl]=sg[j]^sg[i-j-];
for(j=;j<=tl;j++) use[que[j]]=;
for(j=;j<=i;j++) if(!use[j]){ sg[i]=j; break; }
while(tl) use[que[tl--]]=;
}
if(sg[n]>) puts("");
else puts("");
return ;
}

POJ 2311 Cutting Game

题目大意:给出一张n*m的纸,每次可以把它剪成两半,先剪出来1*1小纸片的人赢

虽然1*1是必败局面,但并不容易直接推出其他格子的SG函数

显然x>1时,1*x的局面先手必胜。而2*2局面必败,进而可以推出2*3,3*3也都是必败局面

利用这两点就可以轻松推出整张纸的SG函数了

 #include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 205
#define M1 200010
#define ll long long
#define dd double
using namespace std;
const dd eps=1e-; int T,n,m,de;
int sg[N1][N1],use[N1*]; int main()
{
int i,j,k,x,y,ans,flag;
for(i=;i<=;i++) sg[][i]=;
for(i=;i<=;i++) sg[i][]=;
for(i=;i<=;i++) for(j=;j<=;j++) //if(i+j>4)
{
for(k=;k<j-;k++) use[sg[i][k]^sg[i][j-k]]=; //if(i+k>=4&&i+j-k+1>=4)
for(k=;k<i-;k++) use[sg[k][j]^sg[i-k][j]]=; //if(j+k>=4&&j+i-k+1>=4) for(k=;k<=;k++) if(!use[k]){ sg[i][j]=k; break; } for(k=;k<j-;k++) use[sg[i][k]^sg[i][j-k]]=; //if(i+k>=4&&i+j-k+1>=4)
for(k=;k<i-;k++) use[sg[k][j]^sg[i-k][j]]=; //if(j+k>=4&&j+i-k+1>=4)
}
while(scanf("%d%d",&n,&m)!=EOF)
{
if(sg[n][m]) puts("WIN");
else puts("LOSE");
}
return ;
}

BZOJ 1457 棋盘游戏

题目大意:给出一个坐标系,上面有很多个皇后,皇后只能向左/向下/向左下走,两个人轮流每次选择一个皇后移动,谁先把皇后移动到(0,0)谁赢

如果直接把(0,0)当做游戏终止局面的话,求解的问题就是谁先把所有皇后都移动到(0,0)谁赢了

显然如果存在x=y||x=0||y=0的皇后,先手必胜

所以两个人都极力避免自已移动出来上述三种情况的皇后

而皇后只能向左下移动,最终皇后一定集中在(1,2)和(2,1)

我们把SG函数为0的位置设为(1,2)和(2,1)即可

 #include <queue>
#include <cmath>
#include <vector>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 105
#define M1 200010
#define ll long long
#define dd double
using namespace std;
const dd eps=1e-; int T,n;
int sg[N1][N1],use[N1]; int main()
{
int i,j,k,x,y,ans,flag;
for(i=;i<=;i++) for(j=;j<=;j++) if(i!=j)
{
for(k=;k<j;k++) if(k!=i) use[sg[i][k]]=;
for(k=;k<i;k++) if(k!=j) use[sg[k][j]]=;
for(k=;k<min(i,j);k++) use[sg[i-k][j-k]]=; for(k=;k<;k++) if(!use[k]){ sg[i][j]=k; break; } for(k=;k<j;k++) if(k!=i) use[sg[i][k]]=;
for(k=;k<i;k++) if(k!=j) use[sg[k][j]]=;
for(k=;k<min(i,j);k++) use[sg[i-k][j-k]]=;
}
scanf("%d",&T);
while(T--)
{
scanf("%d",&n); ans=,flag=;
for(i=;i<=n;i++)
{
scanf("%d%d",&x,&y);
if(x==y||!x||!y) flag=;
ans^=sg[x][y];
}
if(ans||flag) puts("^o^");
else puts("T_T");
}
return ;
}

还有一些更加奇怪的变形..

POJ 3480 John (anti-SG组合游戏)

题目大意:$n$堆石子的$NIM$游戏,改成取走最后一个石子的人输,问谁赢

先求出该游戏的$sg$函数

<1>$sg$函数为0,且每堆石子数量都是1,显然先手必胜

<2>$sg$函数为1,且每堆石子数量都是1,显然先手必败

那单堆石子数量>1的情况呢?

<3>$sg$函数为0,且存在一堆石子数量>1,先手必败

(1)先手取走了一个石子的石子堆,把$sg$函数变成1

显然后手一定能把$sg$函数还原成0

最后一定会还剩下至少2个"石子数量>1的堆",且此时$sg$函数值为0

不论先手怎么取,sg函数都会变得>1

$NIM$游戏具有对称性!

即对于一个$sg$函数值为0的局面而言,不论先手如何操作,后手都能把$sg$函数调成0

而这种情况下,一定存在石子数>1的堆,所以后手肯定能保持$sg$函数值为1!

最后会剩下一个石子被先手取走,后手赢

(2)先手取走了石子数量>1的石子堆中的任意数量个,把$sg$函数变成>1

此时一定还剩下"石子数量>1的堆",后手也能把$sg$函数调成1

先手如果取单个石子的堆,后手也跟着取,保持$sg$函数值为1即可

<4>$sg$函数为>1,且存在一堆石子数量>1,先手必胜

先手把局面调成<3>就能让对手必败了

 #include <cstdio>
#include <cstring>
#include <algorithm>
#define N1 5050
using namespace std; int n,m,T; template <typename _T> void read(_T &ret)
{
ret=; _T fh=; char c=getchar();
while(c<''||c>''){ if(c=='-') fh=-; c=getchar(); }
while(c>=''&&c<=''){ ret=ret*+c-''; c=getchar(); }
ret=ret*fh;
} int a[N1]; int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
int i,sum=,ma=;
for(i=;i<=n;i++) read(a[i]), sum^=a[i], ma=max(ma,a[i]);
if(ma==){
if(sum) puts("Brother");
else puts("John");
}else{
if(sum) puts("John");
else puts("Brother");
}
}
return ;
}

HDU 3595 GG and MM (every-SG组合游戏)

题目大意:两个人玩游戏,每个子游戏有两堆石子,设石子较少那一堆数量为$x$,那么当前操作的人要在较多的那一堆中取走$kx$个,$k$是正整数且$kx$不能超过石子数量。两个人必须轮流对每一个能操作的子游戏进行操作,结束最后一个游戏的人获胜。

我们希望必胜的游戏一直保持下去,必败的局面早点结束

先处理出每种子游戏的$sg$函数

定义$step$函数表示先手令该游戏结束的最优步数,必胜局面取最大步数,必败局面取最小步数,可得

$step[u]=$

$0\;(sg[u]=0)$

$min(step[v])+1\;(sg[u]=0,sg[v]>0)$

$max(step[u])+1\;(sg[u]>0,sg[v]=0)$

那么先手必胜当且仅当单一游戏中最大的$step$为奇数

这里的$sg$函数的用途是分析局面何时结束,$sg$函数本身并不能决定胜负

 #include <cstdio>
#include <cstring>
#include <algorithm>
#define il inline
#define N1 1010
using namespace std;
const int maxn=;
const int inf=0x3f3f3f3f; template <typename _T> void read(_T &ret)
{
ret=; _T fh=; char c=getchar();
while(c<''||c>''){ if(c=='-') fh=-; c=getchar(); }
while(c>=''&&c<=''){ ret=ret*+c-''; c=getchar(); }
ret=ret*fh;
} int T,n,A,B,C,de;
int use[N1*N1],sg[N1][N1],step[N1][N1]; int main()
{ int i,j,k,w,ans,a,b;
for(i=;i<=maxn;i++)
for(j=;j<=i;j++)
{
if(i==&&j==)
de=;
for(k=j;k<=i;k+=j) use[sg[max(i-k,j)][min(i-k,j)]]=;
for(k=;k<=maxn*maxn;k++) if(!use[k]){ sg[i][j]=k; break; }
if(!sg[i][j]) step[i][j]=inf;
for(k=j;k<=i;k+=j)
{
a=max(i-k,j), b=min(i-k,j);
if(!sg[i][j]&&sg[a][b]) {
step[i][j]=min(step[i][j],step[a][b]);
}else if(!sg[a][b]){
step[i][j]=max(step[i][j],step[a][b]);
}
use[sg[a][b]]=;
}
step[i][j]++;
} while(scanf("%d",&n)!=EOF) { ans=;
for(i=;i<=n;i++)
{
scanf("%d%d",&A,&B);
if(A<B) swap(A,B);
ans=max(ans,step[A][B]);
}
if(ans&) puts("MM"); else puts("GG"); }
return ;
} /* */

BZOJ 1393 Knight (every-SG组合游戏+打表)

打表没商量,思路和上一题差不多

这道题也启示我们一个技巧,辅助函数(本题中的$step$函数)和$sg$函数之间可能存在某种神♂秘的关系,如果表本身的规律不够明显,尝试分类讨论打表

例如此题中,$sg$函数值为0的状态step函数很有规律。而$sg$值不为0的状态的$step$函数,可以根据$sg$函数值为0的后继状态推出来

 #include <cstdio>
#include <cstring>
#include <algorithm>
#define il inline
#define N1 200010
using namespace std;
const int maxn=;
const int inf=0x3f3f3f3f; template <typename _T> void read(_T &ret)
{
ret=; _T fh=; char c=getchar();
while(c<''||c>''){ if(c=='-') fh=-; c=getchar(); }
while(c>=''&&c<=''){ ret=ret*+c-''; c=getchar(); }
ret=ret*fh;
} int m,n,de; inline int check(int x,int y)
{
if(x==n||y==n)
{
if(x==n) swap(x,y);
if(n%==){ if(x==n) return ; return ; }
if(n%==){ if(x==n-) return ; return ; }
if(n%==){ if(<=x%&&x%<=) return ; return ; }
if(n%==){ return ; }
}
if( <=x% && x%<= && <=y% && y%<= )
return ;
return ;
}
inline int query(int x,int y)
{
if(x==n||y==n)
{
if(x==n) swap(x,y);
if(n%==)
{
if(x<=) return n/-;
if(x==n) return n/-+((x-)/)*+;
return n/-+(x/)*;
}
if(n%==)
{
if(x==n) return n-;
if(x==n-) return n-;
return n/+(x-)/*;
}
if(n%==)
{
return n/-+(x-)/;
}
if(n%==)
{
return n/+x/*;
}
}
if(!check(x,y)) return (x/+y/)*;
int ans=;
if(<=x- && y+<=n && !check(x-,y+)) ans=max(ans,query(x-,y+)+);
if(<=x- && y->= && !check(x-,y-)) ans=max(ans,query(x-,y-)+);
if(<=x- && <=y- && !check(x-,y-)) ans=max(ans,query(x-,y-)+);
if(x+<=n && <=y- && !check(x+,y-)) ans=max(ans,query(x+,y-)+);
return ans;
}
int xx[N1],yy[N1],step[N1];
void solve()
{
int i,j,x,y,ma,tmp;
for(j=,ma=;j<=m;j++)
{
scanf("%d%d",&xx[j],&yy[j]);
ma=max(ma,query(xx[j],yy[j]));
}
if(ma&){ puts("YES");
for(j=;j<=m;j++)
{
x=xx[j]; y=yy[j]; tmp=query(x,y);
if(<=x- && y+<=n) if(tmp==query(x-,y+)+){ printf("%d %d\n",x-,y+); continue; }
if(<=x- && y->=) if(tmp==query(x-,y-)+){ printf("%d %d\n",x-,y-); continue; }
if(<=x- && <=y-) if(tmp==query(x-,y-)+){ printf("%d %d\n",x-,y-); continue; }
if(x+<=n && <=y-) if(tmp==query(x+,y-)+){ printf("%d %d\n",x+,y-); continue; }
} }else puts("NO");
} int main()
{
int i,j,k,w,ans,x,y;
scanf("%d%d",&m,&n);
//if(n<=100) S1::solve(); else S2::solve();
solve();
return ;
}