本题是一道较好的 SG 函数练习题,理解其推导过程可以帮助我们更好的掌握 SG 函数。
1.题意简述
给定一串长度为 \(n\) 的序列,第 \(i\) 位的数值为 \(a[i]\),每次操作可以选定三个下标 \(i,j,k\) 满足 \(i < j \le k\),使 \(a[i]-1,a[j]+1,a[k]+1\),两人轮流操作,无法操作者为输。求先手必胜的第一步的走法以及第一步走法的种类数。
2.解题思路
显然,本题是一道博弈论 (废话),通过分析,我们可以得到以下结论:
-
最终无法操作的局面就是只有第 \(n\) 位有数值,其余位都是 \(0\)。
-
对于 $\forall i \in [1,n] ,a[i] \ge 2 $,我们都可以将其模 \(2\) 后再处理。(因为后手可以模仿先手的操作,所以一定可以同时减少 \(2\) 的倍数)
-
面向数据编程(雾),本题数据范围为 \(N \le 21\),所以只要复杂度不是阶乘级就可以接受。我们以每一个豆子为子游戏,暴力枚举 \(i,j,k\) 求出每一位上只有一个豆子时的 SG 函数值。这是本代码的时间复杂度瓶颈,为 \(\Theta(n^3)\)。
3.代码
#include <iostream>
#include <cstdio>
#include <cctype>
#include <cstring>
#define il inline
#define ll long long
#define re register
#define gc getchar
using namespace std;
il int read() {
int x=0;bool f=0;char ch=gc();
while(!isdigit(ch)) {f|=ch=='-';ch=gc();}
while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=gc();}
return f?-x:x;
}
il int max(int a,int b) {return a>b?a:b;}
il int min(int a,int b) {return a<b?a:b;}
const int N=100;
int T,n,ans,x,y,z,sg;
int a[N],SG[N];
bool vis[N<<1];//异或可能会超出n的范围,为了防止爆数组,要开两倍空间
signed main() {
T=read();
while(T--) {
ans=x=y=z=sg=0;
memset(SG,0,sizeof(SG));
n=read();
for(re int i=1;i<=n;++i) a[i]=read();
for(re int i=n-1;i;--i) {//因为SG值由其后继状态得到,所以从后往前推
memset(vis,0,sizeof(vis));
for(re int j=i+1;j<=n;++j)
for(re int k=j;k<=n;++k)
vis[SG[j]^SG[k]]=1;//SG函数定义 :SG[i]=mex(其后继状态的SG值)
for(re int j=0;;++j) if(!vis[j]) {SG[i]=j;break;}
}
for(re int i=1;i<=n;++i) if(a[i]&1) sg^=SG[i];//只有模2余1的位草堆答案有贡献
for(re int i=1;i<=n;++i)
if(a[i]) //这一位有数才可以移动
for(re int j=i+1;j<=n;++j)
for(re int k=j;k<=n;++k)
if((sg^SG[i]^SG[j]^SG[k])==0) {
//对于总SG值不为0的博弈,第一步将总SG值变为0就是必胜策略
//移动第i位,对总SG值少了SG[i],多了SG[j],SG[k]
if(!ans) {x=i,y=j,z=k;}
++ans;
}
printf("%d %d %d\n",x-1,y-1,z-1);//题目中瓶子编号为0~n-1,所以最后ans-1
printf("%d\n",ans);
}
return 0;
}