【BZOJ】【Nim游戏-SG函数练习题】1457棋盘游戏、3576[HNOI2014]江南乐、3105[CQOI2013]新Nim游戏

前言

三道SG函数的应用题。

都是中文题面,不需要我补充其它信息了吧

棋盘游戏

如果一开始存在一个棋子在两坐标轴上或 y = x y=x y=x 线上,那么先手一定可以一步将它走到终点。

否则,容易发现如果某位玩家某一步将棋子走到了两坐标轴上或 y = x y=x y=x 线上,那么另一位玩家下一步一定可以将这步棋子走到终点,不妨把两坐标轴上或 y = x y=x y=x 线上叫做必败区。非必败区的棋子无法一步走到终点,必然要经过必败区,所以当某位玩家没有别的路可走,下一步必须走进必败区的时候,他就输了。

所以规则可以等价地改为棋子只能在非必败区内移动,不能移动者输。这就是比较标准的Nim游戏了,暴力求出每个位置棋子的SG函数值异或起来即可。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define uns unsigned
#define IF (it->first)
#define IS (it->second)
#define END putchar('\n')
using namespace std;
const int MAXN=16400;
const ll INF=1e18;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[30],lpt;
inline void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt)putchar(ptf[lpt--]^48);
	putchar(c);
}
inline ll lowbit(ll x){return x&-x;}

int n,ans;
bool po[MAXN];
int sg[105][105];
signed main()
{
	for(int i=1;i<100;i++)
		for(int j=1;j<100;j++){
			if(i==j)continue;
			for(int k=1;k<=i;k++){
				if(i-k==0||i-k==j)continue;
				po[sg[i-k][j]]=1;
			}
			for(int k=1;k<=j;k++){
				if(j-k==0||j-k==i)continue;
				po[sg[i][j-k]]=1;
			}
			for(int k=1;k<=min(i,j);k++){
				if(i-k==0||j-k==0)continue;
				po[sg[i-k][j-k]]=1;
			}
			while(po[sg[i][j]])sg[i][j]++;
			for(int k=1;k<=i;k++){
				if(i-k==0||i-k==j)continue;
				po[sg[i-k][j]]=0;
			}
			for(int k=1;k<=j;k++){
				if(j-k==0||j-k==i)continue;
				po[sg[i][j-k]]=0;
			}
			for(int k=1;k<=min(i,j);k++){
				if(i-k==0||j-k==0)continue;
				po[sg[i-k][j-k]]=0;
			}
		}
	for(int T=read();T--;){
		n=read(),ans=0;
		for(int i=1;i<=n;i++){
			int x=read(),y=read();
			if(x==0||y==0||x==y)ans=-1;
			else ans^=sg[x][y];
		}
		printf(ans?"^o^\n":"T_T\n");
	}
	return 0;
}

[HNOI2014]江南乐

对于操作若干堆石子、不能移动者输的公平游戏,都可以对每堆石子求出SG函数值然后异或。这道题显然也可以这么做。

按照这题的定义,暴力求SG函数仿佛是 O ( n 2 ) O(n^2) O(n2) 的。然而,在求 m e x mex mex 的过程中,我们求了很多次重复信息,例如将数量为 x x x 的石子分为 m m m 堆,其中有 x   m o d   m x\bmod m xmodm 堆数量为 ⌊ x m ⌋ + 1 \lfloor\frac{x}{m}\rfloor+1 mx+1 的石子,剩余都是数量为 ⌊ x m ⌋ \lfloor\frac{x}{m}\rfloor mx 的石子。这时我们求的SG函数值只与 ⌊ x m ⌋ \lfloor\frac{x}{m}\rfloor mx 的值以及 m m m x   m o d   m x\bmod m xmodm 的奇偶性有关,故在所有 ⌊ x m ⌋ \lfloor\frac{x}{m}\rfloor mx 相同的 m m m 中只需要计算最大的两个即可。

用整除分块可以 O ( n n ) O(n\sqrt{n}) O(nn ) 解决,只是常数有亿点大(20532ms)。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define uns unsigned
#define IF (it->first)
#define IS (it->second)
#define END putchar('\n')
using namespace std;
const int MAXN=140005;
const ll INF=1e18;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[30],lpt;
inline void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt)putchar(ptf[lpt--]^48);
	putchar(c);
}

int n,T,F,a[MAXN],ans;
bool vis[MAXN],po[MAXN];
int sg[MAXN];
inline void getsg(int x){
	sg[x]=0;
	for(int i=2,j,la;i<=x;i=la+1){
		j=x/i,la=x/j;
		int k=x%j;
		for(int o=la;o>=i&&o>la-2;o--){
			int p=k+(la-o)*j,q=0;
			if(p>=o)break;
			if(p&1)q^=sg[j+1];
			if((o-p)&1)q^=sg[j];
			po[q]=1;
		}
	}
	while(po[sg[x]])sg[x]++;
	for(int i=2,j,la;i<=x;i=la+1){
		j=x/i,la=x/j;
		int k=x%j;
		for(int o=la;o>=i&&o>la-2;o--){
			int p=k+(la-o)*j,q=0;
			if(p>=o)break;
			if(p&1)q^=sg[j+1];
			if((o-p)&1)q^=sg[j];
			po[q]=0;
		}
	}
}
signed main()
{
	T=read(),F=read();
	for(int i=F;i<=100000;i++)getsg(i);
	while(T--){
		n=read(),ans=0;
		for(int i=1;i<=n;i++)a[i]=read(),ans^=sg[a[i]];
		print(ans>0,T?' ':'\n');
	}
	return 0;
}

[CQOI2013]新Nim游戏

首先想到,一回合过后就是最经典的Nim游戏,所以只要确保对手在第一局不可能搞出一个石子数异或和为0的局面即可。

首先明白一件事:先手必胜。
这很好证明,先手只要拿到只剩下一堆石子即可。

然后发现,如果第一局先手操作过后,石子集合中存在一个非空子集的异或和为0,那么一定输。所以我们要求的就是如何花费删去最少数量的石子,使得不存在任何非空子集异或和为0。

先猜后证:结论应该是如果遇到一个极小的异或和为0的非空子集,至少要拿走一个,那就拿最小的那个。

感性证明:拿走较大的不会更优,因为在拿走任何一个的情况下,原子集里的所有数都仍可以被凑出来,不会影响其它异或和为0的极小非空子集内的决策。

然后只要把数降序排序,用线性基就可以了。

#include<bits/stdc++.h>//JZM yyds!!
#define ll long long
#define uns unsigned
#define IF (it->first)
#define IS (it->second)
#define END putchar('\n')
using namespace std;
const int MAXN=105;
const ll INF=1e18;
inline ll read(){
	ll x=0;bool f=1;char s=getchar();
	while((s<'0'||s>'9')&&s>0){if(s=='-')f^=1;s=getchar();}
	while(s>='0'&&s<='9')x=(x<<1)+(x<<3)+(s^48),s=getchar();
	return f?x:-x;
}
int ptf[30],lpt;
inline void print(ll x,char c='\n'){
	if(x<0)putchar('-'),x=-x;
	ptf[lpt=1]=x%10;
	while(x>9)x/=10,ptf[++lpt]=x%10;
	while(lpt)putchar(ptf[lpt--]^48);
	putchar(c);
}
int n;
ll a[MAXN],tb[35],ans;
signed main()
{
	n=read();
	for(int i=1;i<=n;i++)a[i]=read();
	sort(a+1,a+1+n,[](ll x,ll y){return x>y;});
	for(int i=1;i<=n;i++){
		bool ok=0;
		int x=a[i];
		for(int j=30;j>=0&&x>0;j--)if((x>>j)&1){
			if(tb[j]>0)x^=tb[j];
			else tb[j]=x,x=0,ok=1;
		}
		if(!ok)ans+=a[i];
	}
	print(ans);
	return 0;
}
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值