先定义SG函数:sg(x)=mex{ sg(y) | y是x的后继 }
这里后继指在游戏合法操作下所能到达的下一个状态。
关于mex函数,简单来说就是当前最小的不属于这个集合的非负整数,这里不多介绍。
一般来说,对于一个无后继节点,也就是当前无法进行下一步操作的点,其sg函数值应当为0。
那么通过逐级向上以游戏规则递推,就可以将各个局面的sg值给求出来,进而求总体异或和来求出整体局面的情况,或者加一些操作求必胜方案数等。
下面介绍两种求sg函数的板子。
1.打表法;
#define ll long long ll f[N],vis[N],sg[N]; void SG() { for(int i=1;i<=n;++i){ for(int j=1;f[j]<=i&&j<=N;++j){//枚举所有操作 vis[sg[i-f[j]]]=i;//这里把vis标记为i而不是1, //是因为每次求mex函数都是对于当前的i统计的每一个数出现的 //情况,如果标记为1,就得每次重置vis数组,而因为i各不相同 //就起到了重置的作用; } for(int j=0;j<=n;++j){ if(vis[j]!=i){ sg[i]=j; break; } } } }
2.dfs
#define ll long long ll s[N],sg[N];//sg函数首先初始化为-1, ll vis[N];//标记是否存在过,同上 //N是操作总数 ll SG(ll x){ if(sg[x]!=-1) return sg[x]; memset(vis,0,sizeof vis); for(int i=0;i<=n&&x>=s[i];++i){ SG(x-s[i]); vis[sg[x-s[i]]]=1; } for(int j=0;;++j){ if(!vis[j]){ sg[x]=j; break; } } return sg[x]; }
以上就是两种求sg函数的方法,其核心思路都一样,就是枚举,标记,求mex函数,只是形式有差别。
牛客 游戏
对于每一堆石子(有m个),设他要取石子数为d,那么d必须是m的约数。
无法行动者输。
对于当前局面,要求求出所有必胜的方法数。
求出所有局面的sg值,再枚举所有的第一步的情况,如果第一步的sg值与其对应的所有局面的sg值异或和为0,就是合法的。这是因为我们要让第一步形成必败的局面。
首先先打表求sg值
void init()
{
sg[0]=0;
for(int i=1;i<=N;++i){
cnt++;
for(int j=1;j<=sqrt(i);++j){
if(i%j==0){//可以操作
vis[sg[i-j]]=cnt;
//if(j*j!=i)
vis[sg[i-i/j]]=cnt;
}
}
//mex函数求sg
for(int j=0;j<=N-10;j++){
if(vis[j]!=cnt){
sg[i]=j;
break;
}
}
}
}
对于每一个i,如果其存在因子y,则i=y*k,所以k也是i的因子。
这里我们并没有现成的f数组(上文的所有操作的集合) ,只能枚举来找到i的因子,为了省时,我们只枚举到sqrt(i),后面的对应的因子k就有对应的j来找到并标记
if(i%j==0){//可以操作
vis[sg[i-j]]=cnt;
//if(j*j!=i)//这个可写可不写,只是规避掉了重复标记sqrt(i)的可能
vis[sg[i-i/j]]=cnt;
}
然后枚举所有的第一步
for(int i=1;i<=n;++i){
num^=sg[mas[i]];
ll d=sqrt(mas[i]);
for(int j=1;j<=d;++j){
if(mas[i]%j==0){
if((sg[mas[i]-j]^num)==0){//使后手局面必败
ans++;
}
if(j*j!=mas[i]&&(sg[mas[i]-mas[i]/j]^num)==0) ans++;
}
}
num^=sg[mas[i]];
}
因为第一步走完之后,当前所有的局面里并不会包括第一步前的那一堆的局面,所以先用num与sg[mas[i]]进行异或操作,将其的效果抵消,最后再将其异或一次,相当又恢复初始局面。然后这里同样只枚举到sqrt(i),所以大于sqrt(i)的因子k由j推出并操作。
完整代码
#include
using namespace std;
#define ll long long
const ll N=1e5+10;
ll n;
ll sg[N];
ll vis[N];
ll cnt=0;
ll mas[N];
void init()
{
sg[0]=0;
for(int i=1;i<=N;++i){
cnt++;
for(int j=1;j<=sqrt(i);++j){
if(i%j==0){//可以操作
vis[sg[i-j]]=cnt;
//if(j*j!=i)
vis[sg[i-i/j]]=cnt;
}
}
//mex函数求sg
for(int j=0;j<=N-10;j++){
if(vis[j]!=cnt){
sg[i]=j;
break;
}
}
}
}
int main()
{
init();
cin>>n;
ll num=0;
for(int i=1;i<=n;++i){
cin>>mas[i];
num^=sg[mas[i]];
}
ll ans=0;
for(int i=1;i<=n;++i){
num^=sg[mas[i]];
ll d=sqrt(mas[i]);
for(int j=1;j<=d;++j){
if(mas[i]%j==0){
if((sg[mas[i]-j]^num)==0){//使后手局面必败
ans++;
}
if(j*j!=mas[i]&&(sg[mas[i]-mas[i]/j]^num)==0) ans++;
}
}
num^=sg[mas[i]];
}
cout<
寒冬信使
注意,操作只有两种,我们只需要对对应的操作进行讨论就可以了。
注意到字符串的长度最大也只有10,这其实是在提醒我们可以枚举字符串的所有情况,。
所以同样是sg函数,但是这里的局面我们用01串来状压表示,另w代表1,b代表0.
就得到了一个01串s。
两种操作,第一种相当于将s的高位1变成0,再反转一个低位,那么s的值必定是变小的。
第二种操做同理,把1变成0,s必定变小
所以发现无论进行什么操作,s的值都在变小,换句话说,如果从小到大枚举01串,我们枚举到一个状态的s时,通过对s进行操作得到的01串s‘一定是已经求过sg值的,这就符合我们之前讲的sg函数的板子了。就可以快乐敲代码了!
//vis是一个bitset
void init()
{
for(int i=1;i<(1<<10);++i){
vis.reset();//所有位重置为0
for(int j=1;j<=10;++j){
if((i>>(j-1))&1){//第j位为1
if(j==1) vis[sg[i^(1<<(j-1))]]=1; //代表操作2
else{
for(int k=0;k
照着题意写即可
然后每次输入时,枚举字符串的每一位得到对应的01串的数字,讨论对应的sg函数即可
完整代码
#include
using namespace std;
#define ll long long
const ll mod=1e9+7;
const ll N=15;
inline ll read()
{
int X=0; bool flag=1; char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();}
if(flag) return X;
return ~(X-1);
}
ll ksm(ll x,ll y,ll z){
ll ans=1;
while(y){
if(y&1) ans=ans*x%z;
x=x*x%z;
y>>=1;
}
return ans;
}
ll ksc(ll x,ll y,ll z){
ll ans=0;
while(y){
if(y&1) ans=(ans+x)%z;
x=(x+x)%z;
y>>=1;
}
return ans;
}
ll inv(ll x){
return ksm(x,mod-2,mod);
}
ll w(ll x){
ll cnt=0;
while(x){
x/=2;
cnt++;
}
return cnt;
}
ll C(ll a,ll b){
ll ans=1;
for(ll i=a;i>=a-b+1;i--){
ans=ans*i/(a+1-i);
}
return ans;
}
char s[N];
bitset<105> vis;
ll t,n;
ll sg[1<<10];
void init()
{
memset(sg,0,sizeof sg);
for(int i=1;i<(1<<10);++i){
vis.reset();//所有位重置为0
for(int j=1;j<=10;++j){
if((i>>(j-1))&1){//第j位为1
if(j==1) vis[sg[i^(1<<(j-1))]]=1; //代表操作2
else{
for(int k=0;k>t;
while(t--){
cin>>n;
cin>>s;
ll ans=0;
for(int i=0;i