SG函数解析

先定义SG函数:sg(x)=mex{ sg(y) | y是x的后继 }

这里后继指在游戏合法操作下所能到达的下一个状态。

关于mex函数,简单来说就是当前最小的不属于这个集合的非负整数,这里不多介绍。

一般来说,对于一个无后继节点,也就是当前无法进行下一步操作的点,其sg函数值应当为0。

那么通过逐级向上以游戏规则递推,就可以将各个局面的sg值给求出来,进而求总体异或和来求出整体局面的情况,或者加一些操作求必胜方案数等。

下面介绍两种求sg函数的板子。

1.打表法;

f数组要从小到大排序!!!

#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

s数组同样要从小到大排序!!!

#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函数,只是形式有差别。

上例题

牛客 游戏

SG函数解析_第1张图片

大意 

对于每一堆石子(有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<

寒冬信使

SG函数解析_第2张图片

SG函数解析_第3张图片

注意,操作只有两种,我们只需要对对应的操作进行讨论就可以了。

注意到字符串的长度最大也只有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

你可能感兴趣的:(博弈,算法)