sg函数与博弈论

时间:2023-01-07 09:12:26

这个标题是不是看起来很厉害呢...

我们首先来看一个最简单的游戏。比如我现在有一堆石子,有p个,每次可以取走若干个(不能不取),不能取的人就输了。

现在假设有两个人要玩这个游戏,一个人先手,一个人后手,假设两个人都是足够聪明的AI,那么谁会赢?

显然p≠0时先手赢,他只要全部取完就行了...

我们先不管这个游戏有多傻逼,我们看一看这个游戏所隐含的模型。

比如我们把当前游戏局面抽象成一个点,把这个点往每下一步可以到达的新状态连一个边,这样就形成了一个有向无环图。(如果有环这个游戏就不会结束了)

现在我们来定义sg函数。sg(T),T表示一个状态,当T=0先手必败,其他情况后手必败。

你说这玩意儿是啥?定义怎么这么不清楚

没关系我们先来看一个新函数bool_sg(T),当T=0时先手必败,T=1时后手必败。

那么我们发现bool_sg(结束态)=0,其他情况下只有当一个状态的后继状态后手都必败时才为先手必败(显然吧)。

sg函数与它类似,不过相对更为复杂。sg(结束态)=0。

其他情况下sg(T)=mex{sg(G)} (G为T的后继状态,mex表示不在集合中的最小非负整数)

可以发现bool_sg(T)=(bool)sg(T) (也很显然吧)。

我们上面说的这个游戏中,假设我们用p这个数来表示p个石子的状态。那么可以发现sg(p)=p (p>=0)。

所以我们也可以发现先手在p>0时必胜(mdzz)。

虽然上面这个游戏的解只要目测就可以得出,但在比较复杂一点的游戏中它就能派上用场了。

比如每次要取一个斐波那契数?我们只要求sg的时候开个数组模拟mex的过程就可以得到一个平方做法啦!

例1 nim游戏

现在有n堆石子,每一次你可以从任意一堆中任意个石子,但不能不选。如果你不能选你就输了。

你是先手,现在你想知道你有没有必胜策略。

如果是一堆石子,这个sg函数就是之前说过的sg(p)=p。

现在有多堆石子了,该怎么办呢?

我们定义一些游戏的和为你可以轮流从几个游戏中任意选择一个进行操作,那么有一个性质:

一些游戏的和中状态的sg函数等于这些子游戏状态sg函数的异或值。

至于为什么是这样?其实我也不知道2333 这当然是可以证明的,想要证明可以百度啊

例2 hdu1848

一共三堆石子,分别m、n、p个。两人轮流取石子,每步可以从任意一堆中取出一个斐波那契数列中的元素,但不能不取,不能取的人就输了。假设双方都使用最优策略,请判断先手的人会赢还是后手的人会赢。

多组数据,1<=m,n,p<=1000

如果只有一堆石子,可以用一个bool数组暴力求出sg函数。那么有三堆石子只要异或在一起就行了。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#define SZ 666666
int fib[SZ+5],sg[SZ+5];
bool ok[SZ+5];
#define P 2333
int main()
{
fib[0]=fib[1]=1;
int S=20;
for(int i=2;i<=S;i++) fib[i]=fib[i-1]+fib[i-2];
sg[0]=0;
for(int i=1;i<=2000;i++)
{
for(int j=0;j<P;j++) ok[j]=0;
for(int j=0;j<=S;j++)
{
if(fib[j]<=i) ok[sg[i-fib[j]]]=1;
}
for(int j=0;;j++)
{
if(!ok[j]) {sg[i]=j; break;}
}
}
int n,m,p;
while(scanf("%d%d%d",&n,&m,&p),n||m||p)
{
int sgg=sg[n]^sg[m]^sg[p];
if(sgg) puts("Fibo"); else puts("Nacci");
}
}

例3 hdu1850

现在还是有n堆石子,第i堆有i个石子,不能不选。如果你不能选你就输了。

你是先手,现在你只想知道你为了必胜,第一步有几种走法。

首先这还是一个nim游戏的模型,不过改成了求方案数。

如果不能必胜可以简单地输出0,否则我们求出整个游戏起始态的sg函数,假设叫s好了。

那我们可以知道s=1^2^3^4^....^n对吧。

那我们考虑第一次假设选第i堆,我们发现我们目标是这次选完让先手必败(因为这里的先手就是后手)即选完让sg函数为0。

那我们可以发现不考虑这一堆剩下的sg函数就是(s^i),那为了让选完sg函数为0,那么这堆剩下的就要是(s^i)。

于是我们发现只要判一下(s^i)<=i是否成立就行了,如果成立我们就可以一开始在i堆中选(i-(s^i))个。这也是一般博弈问题中求第一步走法的一般思路。

sg函数的基本东西讲完了,接下来是一些(相对)比较复杂(鬼畜)的应用。

例4 hdu1517

有一个数p,开始p=1,两人轮流操作,每一次可以使p乘上2~9的一个整数,最先使得p>=n的人赢。

给出n,求最优策略下先手是必胜还是必败。

1<n<4294967295

可以发现当<4294967295的只有2、3、5、7这些质因子的数直观上感觉不是很多,暴力跑一下发现只有6k+个。

然后我们可以用2、3、5、7的质因数个数来表示一个数,然后暴力sg一波~

需要注意的是这里是p>=n的人赢...不过没关系就当做是输吧,输出的时候再反过来。

//By zzq
#include <iostream>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
#include <string.h>
#include <math.h>
#include <set>
#include <map>
using namespace std;
typedef long long ll;
ll n;
map<ll,int> sgs;
int sg(ll cur)
{
if(cur>=n) return 0;
int& mp=sgs[cur];
if(mp) return mp-1;
bool ff[6700];
memset(ff,0,sizeof(ff));
for(int i=2;i<=9;i++) ff[sg(cur*i)]=1;
for(int p=0;;p++)
{
if(!ff[p]) {mp=p+1; return p;}
}
}
int main()
{
while(cin>>n)
{
sgs.clear();
int ans=sg(1);
if(ans) puts("Stan wins."); else puts("Ollie wins.");
}
}

例5 bzoj1188

还是n堆石子,这回要求每次选出3个数i,j,k,1<=i<j<=k<=n,i堆中至少要有一个石子,那么在i堆中取出一个石子,在j、k堆中各放入一个,当j=k时就在j堆中放入两个,无法选出的人就输了。

求是否有必胜策略及必胜策略下第一步字典序最小的走法(i,j,k)。

1<n<=21,1<=每堆石子数<=10000。

我们可以发现一些性质,题目只和每堆石子的奇偶性有关,所以我们可以把每堆石子数视为mod 2意义下的。

然后我们还可以发现每堆石子是完全独立的,就是说我们可以用每一堆的异或在一起得到总的sg值。

发现这两个性质以后就比较可做了。

#include <iostream>
#include <stdio.h>
#include <stdlib.h>
using namespace std;
#define SZ 666666
int T,n,a[SZ],sg[SZ];
bool gg[SZ];
int main()
{
scanf("%d",&T);
while(T--)
{
scanf("%d",&n);
int shit=1;
while(shit<=n) shit*=2;
shit*=2;
for(int i=1;i<=n;i++) scanf("%d",a+i), a[i]&=1;
sg[n]=0;
for(int i=n-1;i>=1;i--)
{
for(int j=0;j<shit;j++) gg[j]=0;
for(int j=i+1;j<=n;j++)
{
for(int k=j;k<=n;k++) gg[sg[j]^sg[k]]=1;
}
for(int j=0;;j++)
{
if(!gg[j]) {sg[i]=j; break;}
}
}
int ans=0,gs=0;
for(int i=1;i<=n;i++) if(a[i]) ans^=sg[i];
for(int i=1;i<=n;i++)
{
for(int j=i+1;j<=n;j++)
{
for(int k=j;k<=n;k++)
{
if(ans^sg[i]^sg[j]^sg[k]) continue;
++gs; if(gs==1) printf("%d %d %d\n",i-1,j-1,k-1);
}
}
}
if(!gs) puts("-1 -1 -1");
printf("%d\n",gs);
}
}

这方面题做得也不多,今天就写到这里吧。