「SNOI2020」取石子 题解

% Fuyuki

转换一下,取到最后一个石子的人输,那么等同于取到倒数第二个石子的赢,那么为了方便一开始就把石子数减一并且变成求取到最后一个石子的人赢。

定义 \(dp_{i,j}\) 为剩下 \(i\) 个石子,可以取的范围为 \(1 \sim j\)

\[dp_{i,j} = (\operatorname{not} dp_{i-1,2}) \operatorname{or} (\operatorname{not} dp_{i-2,4}) \operatorname{or} \cdots \operatorname{or} (\operatorname{not} dp_{\max(0,i-j),2\times (i-\max(0,i-j))}) \]

显然,对于所有 \(j \geq 1\),都有 \(dp_{1,j}=1\)

有结论:对于 \(dp_i(i \neq 1)\),一定存在一个分界点 \(p_i\),使得对于任意 \(j \geq p_i,1 \leq k \leq p_i\),都有 \(dp_{i,j}=1,dp_{i,k}=0\)。因为当取值包含了 \(p_i\) 之后,至少存在 \(p_i\) 是合法的;同时还可以推得更深的结论,也就是对于所有 \(j \geq p_i\),都会有贡献。

那么我们现在想求这个 \(p_i\),怎么求呢?


前置知识:齐肯多夫定理:任何正整数可以表示为若干个不连续的斐波那契数之和。下面会用到。

写个程序打个表:

#include<bits/stdc++.h>
using namespace std;
bool dp[1005][1005];
int main(){
	for(int i=2;i<=40;++i)
	{
		for(int j=1;j<=90;++j)
		{
			for(int k=1;k<=min(i,j);++k)
			{
				dp[i][j]|=!dp[i-k][k*2];
			}
		}
	}
	for(int i=2;i<=40;++i)
	{
		for(int j=1;j<=90;++j)
		{
			if(dp[i][j]!=dp[i][j-1])
			{
				printf("%d %4d\n",i-1,j);
				break;
			}
		}
	}
	return 0;
}

其中 \(i-1\) 就是有 \(i\) 个石子,\(j\) 为取值范围,\(dp\) 定义如上。

可以列一个表出来(因为懒得制表了所以打代码块应该没问题吧):

num 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
par 1 2 3 1 5 1 2 8 1 2  3  1  13 1  2

肉眼可见 par(就是 \(p\) 啦)一行的所有数都是斐波那契数(而且这个题目看起来也很像 Fib Nim 所以应该不难联想到)。

考虑将 par 对应成斐波那契数(以下 \(f_i\) 表示第 \(i\) 个斐波那契数,其中 \(f_0=f_1=1\))序号:

num 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
fib 1 2 3 1 4 1 2 5 1 2  3  1  6  1  2
par 1 2 3 1 5 1 2 8 1 2  3  1  13 1  2

再引入一个东西,叫做斐波那契进制,分解方法如下:

  • 从高到低位枚举斐波那契数 \(f_i\),如果当前的 \(n > f_i\),那么就将 \(n\) 减去 \(f_i\) 并且将此位置设成 \(1\)
  • 否则置 \(0\)

这样就会将一个数 \(n\) 划分为不会重复的,一个只有 \(0\) 或者 \(1\) 的字符串,这个字符串就是 \(n\) 在斐波那契进制下的表示。可以用齐肯多夫定理证明可行。

因为 \(f_{i+2}>f_i \times 2\),所以一次分解时间复杂度约为 \(O(\log n)\)

对着这个东西看,我也不知道为什么可以看出 num 所对应的 par 就是此 num 在斐波那契进制下的表示的最低位的 \(1\) 所表示的值。(其实也可以稍微看出点眉目,因为 \(f_i = f_{i-1} + f_{i-2}\),这样两个相邻的 \(1\) 就会变成一个 \(1\)

根据我们可能看出的东西,同斐波那契进制表示的性质(就是上面括号里面的东西),也可以得到 \(p\) 数列的一个小结论:

  • 对于数列 \(p\) 的前 \(f_i\) 项,由数列的前 \(f_{i-1}\) 项与前 \(f_{i-2}\) 项接在一起,并且将 \(f_i\) 项更改成 \(f_i\) 即可。

然而我们会构造并没有什么用。


我们知道了 \(p_i\),考虑将答案表示出来。

根据 \(p_i\) 的定义,答案显然为:

\[\sum_{i=1}^{N}[k \geq p_i] \]

既然知道了构造方法,那么定义 \(sum(i,j)\) 为,数列 \(p\) 的前 \(f_i\) 项出现了 \(sum(i,j)\)\(f_j\)

根据我们的构造方法,我们可以将其用 \(O(\log^2 n)\) 的时间预处理出来所有的 \(sum(i,j)\)

回答询问时,我们先找到最大的 \(c\),使得 \(k \geq p_c\) 并且 \(k<p_{c+1}\)。然后去回答这个问题。

因为根据构造方案,\([1,f_i]\) 一段显然与 \([f_{x}+1,f_x+f_i]\) 一段相等,所以贡献相同,可以将 \(N\) 用斐波那契进制拆开,然后去分段计算。

然后发现这个过程还可以对 \(sum(i,j)\) 滚一次前缀和,可以少一只 \(\log\)

时间复杂度 \(O(T \log n + \log n^2)\)

#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
LL sum[105][105],f[105];
int N;
int main(){
	f[0]=f[1]=1;
	f[2]=2;
	for(N=3;f[N-1]<=1e18;++N)	f[N]=f[N-1]+f[N-2];
	--N;
//	for(int i=1;i<=N;++i)	printf("%lld\n",f[i]);
	for(int i=1;i<=N;++i)
	{
		sum[i][i]=1;
		for(int j=1;j<=i;++j)	sum[i+1][j]=sum[i-1][j]+sum[i][j]-int(j==i-1);
	}
	for(int i=1;i<=N;++i)	for(int j=1;j<=N;++j)	sum[i][j]+=sum[i][j-1];
	int T;
	scanf("%d",&T);
	while(T-->0)
	{
		LL k,n,ans=0;
		scanf("%lld %lld",&k,&n);
		LL cor=1;
		while(f[cor+1]<=k)	++cor;
		--n;
		for(int i=N;i>=1;--i)	if(n>=f[i])	n-=f[i],ans+=sum[i][cor],--i;
		printf("%lld\n",ans);
	}
	return 0;
}
posted @ 2020-09-27 20:39  SyadouHayami  阅读(222)  评论(0编辑  收藏  举报

My Castle Town.