第 3 题:江南乐(game),运行时限 3s,内存上限 512M
【问题描述】
小 A 是一个名副其实的狂热的回合制游戏玩家。在获得了许多回合制游戏的世界级奖项
之后,小 A 有一天突然想起了他小时候在江南玩过的一个回合制游戏。
游戏的规则是这样的,首先给定一个数 F,然后游戏系统会产生 T 组游戏。每一组游戏包
含 N 堆石子,小 A 和他的对手轮流操作。每次操作时,操作者先选定一个不小于 2 的正整数
M(M 是操作者自行选定的,而且每次操作时可不一样),然后将任意一堆数量不小于 F 的石
子分成 M 堆,并且满足这 M 堆石子中石子数最多的一堆 至多 比石子数最少的一堆多 1(即
分的尽量平均,事实上按照这样的分石子方法,选定 M 和一堆石子后,它分出来的状态是固
定的)。当一个玩家不能操作的时候,也就是当每一堆石子的数量都严格小于 F 时,他就输掉
了。(补充:先手从 N 堆石子中选择一堆数量不小于 F 的石子分成 M 堆后,此时共有 N+M-1
堆石子,接下来小 A 从这 N+M-1 堆石子中选择一堆数量不小于 F 的石子,依此类推。)
小 A 从小就是个有风度的男生,他邀请他的对手作为先手。小 A 现在想要知道,面对给
定的一组游戏,而且他的对手也和他一样聪明绝顶的话,究竟谁能够获得胜利?
【输入格式】
输入文件为 game.in。
输入第一行包含两个正整数 T 和 F,分别表示游戏组数与给定的数。
接下来 T 行,每行第一个数 N 表示该组游戏初始状态下有多少堆石子。之后 N 个正整数,
表示这 N 堆石子分别有多少个。
【输出格式】
输出文件为 game.out。
输出一行,包含 T 个用空格隔开的 0 或 1 的数,其中 0 代表此时小 A(后手)会胜利,而
1 代表小 A 的对手(先手)会胜利。
【样例输入】
4 3
1 1
1 2
1 3
1 5
0 0 1 1
【数据范围】
对于 10%的数据,T=1,N=1,F≤1,每堆石子数量≤10;
对于 20%的数据,T≤100,N≤2,F≤1,每堆石子数量≤10;
对于 30%的数据,T≤100,N≤100,F≤1,每堆石子数量≤10;
对于 40%的数据,T≤100,N≤100,F≤5,每堆石子数量≤15;
对于 70%的数据,T≤100,N≤100,F≤1000,每堆石子数量≤1000;
对于 100%的数据,T≤100,N≤100,F≤100000,每堆石子数量≤100000。
以上所有数均为正整数。
【本人题解】
对于10分的数据:明显当石子数大于1时,先手胜。因为只有一堆,先手只要把石子全拆成只有1个的若干堆就赢了,但如果本来就只有1个石子,就是后手赢,复杂度O(1)。
对于70分的数据:直接暴力求在当前的 f 值下每个石堆对的的sg值即可。每堆石子有O(n)中分裂方式,每个分裂方式的sg求值是O(1)的,有O(n)个石子,复杂度O(n ^ 2)。
对于100分的数据:我们考虑两个性质
1、(n / i)(商向下取整),至多有 2 * sqrt(n) 个不同的结果。
2、如果 n / i == n / (i + 2) ,那么通过将石子分裂成 i 份得到的 i 堆石子的sg值异或和与将石子分裂成 i + 2 份石子所得到的结果相同。
如果上述两个性质是对的,我们就可以只枚举 n / i 来求解,因为有很多分裂方式在本质上是相同的,我们完全可以不考虑。
现在我们来证明这些性质:
证明性质1:在1 - sqrt(n) 之间,因为只有 sqrt(n) 个数,所以 (n / i) 在1 - sqrt(n) 之间当然最多只有sqrt(n) 个结果。
在 sqrt(n) - n 之间,因为 n / n = 1 , n / sqrt(n) = sqrt(n),n 除以该范围内的数的结果必然在1 - sqrt(n) 之间 。所以 (n / i) 在 sqrt(n) - n 之间也只会出现sqrt(n) 个不同的结果。
综上,在1 - n之间,(n / i) 至多有 2 * sqrt(n) 个不同的结果,性质 1 是正确的。
证明性质2:因为把 n 个石子按照题目要求分成 i 份,会产生 n % i 个石子数为 n / i + 1 的石头堆,i - n % i 个石子数为 n / i 的石头堆。(这个挺显然的)
而把 n 个石子按照题目要求分成 i + 1 份,就相当于在分成了 i 份的基础上,从 n / i 个石子数为 n / i + 1 的石头堆中,各拿出1个石子,组成一个新的石子数为n / i 的石头堆。
而把 n 个石子按题目要求分成 i + 2 份的话,就相当于做了两次组新石子堆的工作,其石子堆的奇偶性不变,因为你操作了两次,所以各种石子堆的改变量就必然是2的倍数,既然这样,石子堆的sg值异或和就不会变。
所以性质 2 是正确的。
好的,现在我们保证了这个算法的正确性,来说说具体做法吧。
对于每个石子数n,我们枚举它的 n / i 值,然后取分裂成 n / i 份和 n / (i +1) 份的两种情况就可以了。
实现过程中,我们还可以进行记忆化搜索来提高程序的效率。
每个石子数的sg求值是 O(sqrt(n)) 的,有 O(n) 个石子,所以时间复杂度是 O(n * sqrt(n)) 的。
【本人代码】
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
#define N 100000 + 5
int T, F, id, mex[N], sg[N];
bool done[N];
int Get_sg(int n)
{
if (n < F)
return 0;
if (done[n])
return sg[n];
done[n] = 1;
for (int i = 2; i <= n; i = n / (n / i) + 1)
for (int j = i; j <= i + 1 && j <= n; j ++)
Get_sg(n / j), Get_sg(n / j + 1);
id ++;
for (int i = 2; i <= n; i = n / (n / i) + 1)
for (int j = i; j <= i + 1 && j <= n; j ++)
{
int tmp = 0;
if ((n % j) % 2 == 1) tmp ^= sg[n / j + 1];
if ((j - n % j) % 2 == 1) tmp ^= sg[n / j];
mex[tmp] = id;
}
for (sg[n] = 0; mex[sg[n]] == id; sg[n] ++) ;
return sg[n];
}
void begin()
{
freopen("game.in", "r", stdin);
freopen("game.out", "w", stdout);
scanf("%d%d", &T, &F);
}
void work()
{
int t, xsum = 0;
scanf("%d", &t);
while (t --)
{
int x;
scanf("%d", &x);
xsum ^= Get_sg(x);
}
printf("%d ", xsum == 0 ? 0 : 1);
}
void end()
{
fclose(stdin);
fclose(stdout);
}
int main()
{
begin();
while (T --)
work();
end();
return 0;
}