sg函数小结

sg函数小结

sg函数是处理博弈问题的重要工具。

我们知道sg(x)=mex{sg(j)|x能到达状态j}

sg(x)=0时代表后手赢,否则先手赢。

对于一个问题,如果某些子问题是相互独立的,我们就可以用sg定理,总问题的sg等于各个子问题的异或和。

看几道题:

hdu1848 Fibonacci again and again

任何一个大学生对菲波那契数列(Fibonacci numbers)应该都不会陌生,它是这样定义的:
F(1)=1;
F(2)=2;
F(n)=F(n-1)+F(n-2)(n>=3);
所以,1,2,3,5,8,13……就是菲波那契数列。
在HDOJ上有不少相关的题目,比如1005 Fibonacci again就是曾经的浙江省赛题。
今天,又一个关于Fibonacci的题目出现了,它是一个小游戏,定义如下:
1、 这是一个二人游戏;
2、 一共有3堆石子,数量分别是m, n, p个;
3、 两人轮流走;
4、 每走一步可以选择任意一堆石子,然后取走f个;
5、 f只能是菲波那契数列中的元素(即每次只能取1,2,3,5,8…等数量);
6、 最先取光所有石子的人为胜者;
假设双方都使用最优策略,请判断先手的人会赢还是后手的人会赢。

Input

输入数据包含多个测试用例,每个测试用例占一行,包含3个整数m,n,p(1<=m,n,p<=1000)。
m=n=p=0则表示输入结束。

Output

如果先手的人能赢,请输出“Fibo”,否则请输出“Nacci”,每个实例的输出占一行。

一道暴力算sg函数的题

#include<bits/stdc++.h>
using namespace std;
#define REP(i,st,ed) for(register int i=st,i##end=ed;i<=i##end;++i)
#define DREP(i,st,ed) for(register int i=st,i##end=ed;i>=i##end;--i)
typedef long long ll;
inline int read(){
	int x;
	char c;
	int f=1;
	while((c=getchar())!='-' && (c<'0' || c>'9'));
	if(c=='-') c=getchar(),f=-1;
	x=c^'0';
	while((c=getchar())>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0');
	return x*f;
}
inline ll readll(){
	ll x;
	char c;
	ll f=1;
	while((c=getchar())!='-' && (c<'0' || c>'9'));
	if(c=='-') c=getchar(),f=-1;
	x=c^'0';
	while((c=getchar())>='0' && c<='9') x=(x<<1ll)+(x<<3ll)+(c^'0');
	return x*f;
}
const int maxn=1000+10;
int f[maxn],fib[maxn],tmp;
bool p[maxn];
int main(){
#ifndef ONLINE_JUDGE
	freopen("sg.in","r",stdin);
	freopen("sg.out","w",stdout);
#endif
	int n,m,P;
	fib[1]=1,fib[2]=2;
	for(tmp=3;;++tmp){
		fib[tmp]=fib[tmp-1]+fib[tmp-2];
		if(fib[tmp]>1000) break;
	}
	--tmp;
	REP(i,1,1000){
		memset(p,0,sizeof(p));
		REP(j,1,tmp){
			if(i<fib[j]) break;
			p[f[i-fib[j]]]=1;
		}
		REP(j,0,i)
			if(!p[j]){
				f[i]=j;
				break;
			}
	}
	while(scanf("%d%d%d",&n,&m,&P)!=EOF && (n || m || P)){
		if(f[n]^f[m]^f[P]) printf("Fibo\n");
		else printf("Nacci\n");
	}
	return 0;
}

bzoj1228: [SDOI2009]E&D

Description

小E 与小W 进行一项名为“E&D”游戏。游戏的规则如下:桌子上有2n 堆石子,编号为1..2n。其中,为了方便起见,我们将第2k-1 堆与第2k 堆(1 ≤ k ≤ n)视为同一组。第i堆的石子个数用一个正整数Si表示。一次分割操作指的是,从桌子上任取一堆石子,将其移走。然后分割它同一组的另一堆石子,从中取出若干个石子放在被移走的位置,组成新的一堆。操作完成后,所有堆的石子数必须保证大于0。显然,被分割的一堆的石子数至少要为2。两个人轮流进行分割操作。如果轮到某人进行操作时,所有堆的石子数均为1,则此时没有石子可以操作,判此人输掉比赛。小E 进行第一次分割。他想知道,是否存在某种策略使得他一定能战胜小W。因此,他求助于小F,也就是你,请你告诉他是否存在必胜策略。例如,假设初始时桌子上有4 堆石子,数量分别为1,2,3,1。小E可以选择移走第1堆,然后将第2堆分割(只能分出1 个石子)。接下来,小W 只能选择移走第4 堆,然后将第3 堆分割为1 和2。最后轮到小E,他只能移走后两堆中数量为1 的一堆,将另一堆分割为1 和1。这样,轮到小W 时,所有堆的数量均为1,则他输掉了比赛。故小E 存在必胜策略。

Input

的第一行是一个正整数T(T ≤ 20),表示测试数据数量。接下来有T组数据。对于每组数据,第一行是一个正整数N,表示桌子上共有N堆石子。其中,输入数据保证N是偶数。第二行有N个正整数S1..SN,分别表示每一堆的石子数。

Output

包含T 行。对于每组数据,如果小E 必胜,则输出一行”YES”,否则输出”NO”。

数据规模和约定

对于20%的数据,\(N = 2\)

对于另外20%的数据,\(,N ≤ 4,S_i ≤ 50\)

对于100%的数据,\(,N ≤ 2×10^4,S_i ≤ 2×10^9\)

很显然每组之间是独立的,所以我们可以考虑每一组的sg函数。

设两堆石子分别是x,y

打个表,发现规律:

当x和y均为奇数时sg(x,y)=0;

否则sg(x,y)=sg((x+1)/2,(y+1)/2)+1

#include<bits/stdc++.h>
using namespace std;
#define REP(i,st,ed) for(register int i=st,i##end=ed;i<=i##end;++i)
#define DREP(i,st,ed) for(register int i=st,i##end=ed;i>=i##end;--i)
typedef long long ll;
inline int read(){
    int x;
    char c;
    int f=1;
    while((c=getchar())!='-' && (c<'0' || c>'9'));
    if(c=='-') c=getchar(),f=-1;
    x=c^'0';
    while((c=getchar())>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0');
    return x*f;
}
inline ll readll(){
    ll x;
    char c;
    ll f=1;
    while((c=getchar())!='-' && (c<'0' || c>'9'));
    if(c=='-') c=getchar(),f=-1;
    x=c^'0';
    while((c=getchar())>='0' && c<='9') x=(x<<1ll)+(x<<3ll)+(c^'0');
    return x*f;
}
int getsg(int x,int y){
    if((x&1) && (y&1)) return 0;
    return getsg((x+1)>>1,(y+1)>>1)+1;
}
int main(){
#ifndef ONLINE_JUDGE
    freopen("sg.in","r",stdin);
    freopen("sg.out","w",stdout);
#endif
    int T=read();
    while(T--){
        int n=read();
        int sum=0;
        REP(i,1,n>>1){
            int x=read(),y=read();
            sum^=getsg(x,y);
        }
        if(sum) printf("YES\n");
        else printf("NO\n");
    }
    return 0;
}

ARC091 F - Strange Nim

题意:有n堆石子,每堆石子有\(a_i\)个,有一个数\(k_i\)现在两个人博弈,每个人可以拿掉一堆石子里\(1\)~\(\lfloor \frac {x} {k_i}\rfloor\)数量的石子,x为当前这一堆石子的数量,谁不能拿就输了,求谁赢。

数据范围:

\(1≤N≤200\)

\(1≤A_i,K_i≤10^9\)

又是找规律。。k=2的情况是石子游戏。规律也有些相似

通过打表发现

当x%k=0时\(sg(x)=x/k\)

\(sg(x)=sg(x-\lfloor \frac {x} {k} \rfloor + 1)\)

所以我们可以递归求sg函数,但是这样会T

我们\(\lfloor \frac {x} {k} \rfloor + 1\)相同的放在一次一起处理就可以了

#include<bits/stdc++.h>
using namespace std;
#define REP(i,st,ed) for(register int i=st,i##end=ed;i<=i##end;++i)
#define DREP(i,st,ed) for(register int i=st,i##end=ed;i>=i##end;--i)
typedef long long ll;
inline int read(){
	int x;
	char c;
	int f=1;
	while((c=getchar())!='-' && (c<'0' || c>'9'));
	if(c=='-') c=getchar(),f=-1;
	x=c^'0';
	while((c=getchar())>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0');
	return x*f;
}
inline ll readll(){
	ll x;
	char c;
	ll f=1;
	while((c=getchar())!='-' && (c<'0' || c>'9'));
	if(c=='-') c=getchar(),f=-1;
	x=c^'0';
	while((c=getchar())>='0' && c<='9') x=(x<<1ll)+(x<<3ll)+(c^'0');
	return x*f;
}
const int maxn=1e5+10;
int main(){
	int n=read(),ans=0;
	REP(i,1,n){
		int x=read(),y=read();
		while(x%y!=0){
			int r=(x/y)*y;
			x-=(x/y+1)*max((x-r)/(x/y+1),1);
			if(x<y) break;
		}
		ans^=x/y;
	}
	if(ans) printf("Takahashi\n");
	else printf("Aoki\n");
	return 0;
}

1188: [HNOI2007]分裂游戏

Description

聪聪和睿睿最近迷上了一款叫做分裂的游戏。该游戏的规则试:共有n个瓶子,标号为0,1,2.....n-1,第i个瓶子中

装有p[i]颗巧克力豆,两个人轮流取豆子,每一轮每人选择3个瓶子。标号为i,j,k,并要保证i<j,j<=k且第i个瓶子

中至少要有1颗巧克力豆,随后这个人从第i个瓶子中拿走一颗豆子并在j,k中各放入一粒豆子(j可能等于k)。如

果轮到某人而他无法按规则取豆子,那么他将输掉比赛。胜利者可以拿走所有的巧克力豆!两人最后决定由聪聪先

取豆子,为了能够得到最终的巧克力豆,聪聪自然希望赢得比赛。他思考了一下,发现在有的情况下,先拿的人一

定有办法取胜,但是他不知道对于其他情况是否有必胜策略,更不知道第一步该如何取。他决定偷偷请教聪明的你

,希望你能告诉他,在给定每个瓶子中的最初豆子数后是否能让自己得到所有巧克力豆,他还希望你告诉他第一步

该如何取,并且为了必胜,第一步有多少种取法?

假定 \(1 < n < = 21,p[i] < = 10000\)

Input

输入文件第一行是一个整数t表示测试数据的组数,

接下来为t组测试数据(t<=10)。

每组测试数据的第一行是瓶子的个数n,

接下来的一行有n个由空格隔开的非负整数,表示每个瓶子中的豆子数。

Output

对于每组测试数据,输出包括两行,

第一行为用一个空格两两隔开的三个整数,表示要想赢得游戏,

第一步应该选取的3个瓶子的编号i,j,k,

如果有多组符合要求的解,那么输出字典序最小的一组。

如果无论如何都无法赢得游戏,那么输出用一个空格两两隔开的三个-1。

第二行表示要想确保赢得比赛,第一步有多少种不同的取法。

由于每一堆会影响后面的堆,所以不能把每一堆看成独立。

我们发现实际上问题只和每一堆的奇偶性有关。

所以我们可以把第i堆的一粒石子是独立的

这样我们就可以算sg函数了。

需要注意的是因为n不同所以预处理要反过来记录

求方案数直接枚举第一次的操作去算就行了。

#include<bits/stdc++.h>
using namespace std;
#define REP(i,st,ed) for(register int i=st,i##end=ed;i<=i##end;++i)
#define DREP(i,st,ed) for(register int i=st,i##end=ed;i>=i##end;--i)
typedef long long ll;
inline int read(){
    int x;
    char c;
    int f=1;
    while((c=getchar())!='-' && (c<'0' || c>'9'));
    if(c=='-') c=getchar(),f=-1;
    x=c^'0';
    while((c=getchar())>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0');
    return x*f;
}
inline ll readll(){
    ll x;
    char c;
    ll f=1;
    while((c=getchar())!='-' && (c<'0' || c>'9'));
    if(c=='-') c=getchar(),f=-1;
    x=c^'0';
    while((c=getchar())>='0' && c<='9') x=(x<<1ll)+(x<<3ll)+(c^'0');
    return x*f;
}
const int maxn=30;
int f[maxn],a[maxn];
bool p[10000+10];
void init(int n){
    REP(i,1,n){
        memset(p,0,sizeof(p));
        REP(j,1,i-1)
            REP(k,1,j)
                p[f[j]^f[k]]=1;
        REP(j,0,n*n) if(!p[j]){
            f[i]=j;
            break;
        }
    }
}
int main(){
#ifndef ONLINE_JUDGE
    freopen("sg.in","r",stdin);
    freopen("sg.out","w",stdout);
#endif
    init(21);
    int T=read();
    while(T--){
        int sum=0,n=read(),ans=0;
        REP(i,1,n) a[i]=read();
        REP(i,1,n) if(a[i]&1) sum^=f[n-i+1];
        REP(i,1,n) if(a[i])
            REP(j,i+1,n)
                REP(k,j,n)
                    if((sum^f[n-i+1]^f[n-j+1]^f[n-k+1])==0){
                        if(!ans) printf("%d %d %d\n",i-1,j-1,k-1);
                        ++ans;
                    }
        if(!ans) printf("-1 -1 -1\n");
        printf("%d\n",ans);
    }
    return 0;
}

4035: [HAOI2015]数组游戏

Description

有一个长度为N的数组,甲乙两人在上面进行这样一个游戏:首先,数组上有一些格子是白的,有一些是黑的。然

后两人轮流进行操作。每次操作选择一个白色的格子,假设它的下标为x。接着,选择一个大小在1~n/x之间的整数

k,然后将下标为x、2x、...、kx的格子都进行颜色翻转。不能操作的人输。现在甲(先手)有一些询问。每次他

会给你一个数组的初始状态,你要求出对于这种初始状态他是否有必胜策略。

Input

接下来2*K行,每两行表示一次询问。在这两行中,第一行一个正整数W,表示数组中有多少个格子是白色的,第二

行则有W个1~N之间的正整数,表示白色格子的对应下标。

Output

对于每个询问,若先手必胜输出"Yes",否则输出"No"。答案之间用换行隔开

数据范围

$,N<=1000000000 , K,W<=100 $, 不会有格子在同一次询问中多次出现。

考虑可以把问题转化为既可以选白点,又可以选黑点。因为如果选黑点,一定不能一次胜利,而反倒对对方有利,且对方可以选一样的将状态重置。

这样\(sg(x)=mex\{sg(2*x),sg(2*x)\) ^ \(sg(3*x) \cdots\}\)

这样时间复杂度还是不能过。

我们可以发现一个性质:

当 $ \lfloor \frac { n } { i } \rfloor $ = $ \lfloor \frac { n } { j } \rfloor $ 时 $ sg( i ) = sg( j ) $,可以用归纳法证明

之后我们就可以分块,相同的块放在一起。

记录答案时,大于\(\sqrt{n}\)的直接记录在$ \lfloor \frac {n} {i} \rfloor $上就可以了

#include<bits/stdc++.h>
using namespace std;
#define REP(i,st,ed) for(register int i=st,i##end=ed;i<=i##end;++i)
#define DREP(i,st,ed) for(register int i=st,i##end=ed;i>=i##end;--i)
typedef long long ll;
inline int read(){
    int x;
    char c;
    int f=1;
    while((c=getchar())!='-' && (c<'0' || c>'9'));
    if(c=='-') c=getchar(),f=-1;
    x=c^'0';
    while((c=getchar())>='0' && c<='9') x=(x<<1)+(x<<3)+(c^'0');
    return x*f;
}
inline ll readll(){
    ll x;
    char c;
    ll f=1;
    while((c=getchar())!='-' && (c<'0' || c>'9'));
    if(c=='-') c=getchar(),f=-1;
    x=c^'0';
    while((c=getchar())>='0' && c<='9') x=(x<<1ll)+(x<<3ll)+(c^'0');
    return x*f;
}
const int maxn=1e5+10;
int lim;
int n,f[maxn],g[maxn];
bool p[maxn];
int a[maxn];
inline int sg(int x){
    if(x>lim) return f[n/x];
    return g[x];
}
void init(){
    for(int i=1,r;i<=n;i=r+1){
        r=n/(n/i);int tmp=0,res=0;
        for(int j=2,r1;j<=r;j=r1+1){
            r1=r/(r/j);
            int t=sg(r/j);tmp^=t;
            p[tmp]=1;
            a[++res]=tmp;
            if((r1-j)&1) tmp^=t;
        }
        int t=1;
        while(p[t]) ++t;
        if(i>lim) f[n/i]=t;
        else g[i]=t;
        REP(i,1,res) p[a[i]]=0;
    }
}
int main(){
#ifndef ONLINE_JUDGE
    freopen("sg.in","r",stdin);
    freopen("sg.out","w",stdout);
#endif
    n=read();
    lim=(int)sqrt(n);
    int T=read();
    init();
//  REP(i,1,n) cout<<i<<' '<<sg(n/i)<<endl;
    while(T--){
        int ans=0,m=read();
        REP(i,1,m){
            int x=read();x=n/x;
            ans^=sg(x);
        }
        if(ans) printf("Yes\n");
        else printf("No\n");
    }
    return 0;
}
posted @ 2018-03-12 12:53  zhou888  阅读(385)  评论(2编辑  收藏  举报