【洛谷3179】[HAOI2015] 数组游戏(SG函数+除法分块)
大致题意: 一个\(1\sim n\)的棋盘,每组询问给出棋盘上白色格子的下标。对弈双方轮流操作,每次可以选择一个白色格子(设其下标为\(x\)),将下标为\(x,2x,...,kx\)的格子颜色翻转,其中\(1\le k\le\lfloor\frac nx\rfloor\)。问先手是否有必胜策略。
\(SG\)函数
考虑我们求出每个格子的\(SG\)函数,则一个棋盘的\(SG\)值就是所有白色格子\(SG\)函数的异或和。
众所周知一个状态的\(SG\)函数等于其后继状态\(SG\)值的\(mex\)。
考虑一个节点\(x\)的后继状态,就会发现:
\[SG(x)=mex\{0,SG(2x),SG(2x)\oplus SG(3x),SG(2x)\oplus SG(3x)\oplus SG(4x)...\}
\]
由于\(n\le10^9\),直接暴力求出所有\(SG(x)\)是不现实的,需要考虑优化。
除法分块
根据此题中\(SG\)函数的转移式,容易发现,若\(\lfloor\frac nx\rfloor=\lfloor\frac ny\rfloor\),则\(SG(x)=SG(y)\)。
也就是说,可以通过除法分块,将\(SG\)函数分成\(O(\sqrt n)\)段。
而要求出某一段的\(SG\)函数,设这段开头为\(p_i\),就需要在\(\lfloor\frac n{p_i}\rfloor\)范围内再次除法分块,利用每一段的\(SG\)值与前缀异或和更新答案。
注意除法分块过程中维护前缀异或和时,要根据这一段的奇偶性判断是否给前缀异或和异或上这一段的\(SG\)值。
具体实现详见代码,还是非常简单的。
代码
#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define SN 32000
#define SG(x) ((x)<=sn?S[0][x]:S[1][n/(x)])//把SG函数分两个数组存储
using namespace std;
int n,m,p[2*SN+5],S[2][SN+5],vis[2*SN+5];
int main()
{
RI l,r;for(scanf("%d",&n),l=1;l<=n;l=r+1) p[++m]=l,r=n/(n/l);//除法分块,求出每一段
RI i,t,s,sn=sqrt(n);for(i=m;i;--i)//倒枚每一段求出SG函数
{
for(t=n/p[i],s=0,l=2;l<=t;l=r+1) r=t/(t/l),//再次除法分块
vis[s^SG(l*p[i])]=i,(r-l+1)&1&&(s^=SG(l*p[i]));//vis记录每种值否出现过,如果这段长度为奇数则更新前缀异或和
SG(p[i])=1;W(vis[SG(p[i])]==i) ++SG(p[i]);//求mex,由于后继状态必然存在0,因此SG初始化为1
}
RI Qt,x;scanf("%d",&Qt);W(Qt--)//处理询问
{scanf("%d",&t),s=0;W(t--) scanf("%d",&x),s^=SG(x);puts(s?"Yes":"No");}//把SG值异或起来判断是否为0
return 0;
}
待到再迷茫时回头望,所有脚印会发出光芒