Divide by Zero 2017 and Codeforces Round #399 (Div. 1 + Div. 2, combined) E

题意:

Sam和Jon在玩取石子游戏,现在一共有n堆石子,第i堆石子初始时有si个石头。每次,轮到的那个玩家可以任选一堆石子并从中任意取走一定数量的石头,先不能取的人算输

当然,游戏有特殊的约束,对于第i堆石子,如果之前在这堆石子取走过k块石头,那么再也不能从这堆石子里一次性取走k块石头。通俗地说,假如某堆石子有16块,某次取走了8块,那么下一次如果要取这堆石子,可以从1~7任选一个数字但就是不能选择8。

现在,Jon先手这局游戏,Sam想知道在两个人都操作正确的情况下他是否有必胜策略

n <= 1E6    si <= 60

solution:

其实就是用sg定理瞎做一下就行了,不过现场碰到题的时候苟蒻我并不会sg定理啊。。。(E过的人这么多活该没得涨分- -)

那既然是刚学的知识,这里就直接当是sg定理的学习笔记好了

定义F(x)为游戏进行到状态x的时候所有能够到达的后继状态的集合,sg(x)为状态x的sg函数

有sg(x) = mex{sg(y)|y ∈ F(x)},而mex{}是定义在自然数集上的函数,值为集合中第一个未出现过的自然数

显然,如果状态T为终止态(已经无法对游戏进行任何操作),则应有sg(T) = 0

先考虑一种简单的情形,当前的局面只有一堆,含有k个石头的,从未进行过任何操作的石子

那么从这个局面开始,每个状态可以用一个唯一的二元组(res,o)表示,其中,res代表剩下的石头数,o代表已经取出过的石头数的集合

注意到si <= 60,所以完全可以通过记忆化搜搜确定出sg((1,0)) ~ sg((60,0))。

先打张表存好,然后记SG(i) = sg((i,0))

先不加证明地给出sg定理的结论:记sum = SG(s1) xor SG(s2) xor SG(s3) ... xor SG(sn),若sum不为0,先手必胜,否则先手必败

考虑整场游戏的终止态为T,显然此时每堆石子的SG值都为0,因为无法继续操作了,那么此时的sum值也应该为0。

可以证明:只要当前sum值不为0,总有一种方案,使得操作之后sum值为0

记当前sum值二进制数码中最高位为k,那么在所有石子中,任选一堆SG值的二进制数码第k位为1的石子,假设它的SG值为x,记y = sum xor x

显然,有x > y,因为,如果x的最高位也为k,那么y的最高位肯定不是k了,此时y < x成立。如果x的最高位大于k,那么y的第k位为0,大于k的位置值与x相同,所以y < x也是成立的

既然y < x,那么x的所有后继状态中,必定至少有一个状态的SG值为y,这个由mex的性质就知道了,如果没有y这个值,那怎么可能SG值为x?

只要让这堆石子转移到SG值为y的那个状态,就有sum' = sum xor x xor y = 0,证明完毕

再有:只要当前sum值为0,那么无论当前玩家如何操作,下一个状态的sum值必不为0

因为sum为0的状态当且仅当所有石子目前状态中的SG值为0,而只要是SG值为0的状态能转移一定是转移到SG值不为0的状态,因此总有sum' = sum xor x(x > 0),证明完毕

假如先手拿到的状态为sum > 0,那么他总有一种方案能使对手拿到的状态sum = 0,而他的对手无论采取怎样的决策都只能让下一个状态的sum > 0。在本场游戏中,先碰到终止态T的人就宣告游戏失败了,因此,只要先手的人足够聪明,他要取胜只需时刻维护交到对手的状态有sum = 0即可,而构造方案已经证明了

至于sum = 0时先手必败,反过来就是了



那就先贴个打表代码吧。。。开个map一下就搜好了

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;

typedef long long LL;
const int N = 66;

struct data{
	int x; LL k; data(){}
	data(int x,LL k): x(x),k(k){}
	bool operator < (const data &B) const
	{
		if (x < B.x) return 1;
		if (x > B.x) return 0;
		return k < B.k;
	}
};

int n,tot,sg[N];

map <data,int> s;

int Dfs(int x,LL k)
{
	if (s.count(data(x,k))) return s[data(x,k)];
	bool bo[N]; memset(bo,0,sizeof(bo));
	for (LL i = 1; i <= x; i++)
	{
		if (k & (1LL << i)) continue;
		bo[Dfs(x - i,k | (1LL << i))] = 1;
	}
	for (int i = 0; i < N; i++)
		if (!bo[i]) return s[data(x,k)] = i;
}

int main()
{
	#ifdef DMC
		freopen("DMC.txt","w",stdout);
	#endif
	
	for (int i = 1; i <= 60; i++)
	{
		sg[i] = Dfs(i,0); s.clear();
		printf("%d,",sg[i]); if (i % 10 == 0) puts("");
	}
	return 0;
}

然后解决问题的代码其实很好写的

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;

typedef long long LL;
const int sg[60] = {1,1,2,2,2,3,3,3,3,4,
					4,4,4,4,5,5,5,5,5,5,
					6,6,6,6,6,6,6,7,7,7,
					7,7,7,7,7,8,8,8,8,8,
					8,8,8,8,9,9,9,9,9,9,
					9,9,9,9,10,10,10,10,10,10};

int n,tot;

int main()
{
	#ifdef DMC
		freopen("DMC.txt","r",stdin);
	#endif
	
	cin >> n;
	while (n--)
	{
		int x; scanf("%d",&x);
		tot ^= sg[x - 1];
	}
	puts(tot ? "NO" : "YES");
	return 0;
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值