题目描述如下:甲乙丙丁四人游戏,甲有二整数 X 和 Y,四人已知 2 <= X <= Y <= N,但乙丙丁未知 X 和 Y 确值。现在,甲告诉乙 X + Y 的值,甲告诉丙 X * Y 的值,下面四人对话:
乙:我不知道 X 和 Y 的确值,但我知道丙不知道 X 和 Y 的确值;
丙:我原不知道 X 和 Y 的确值,但我现在知道 X 和 Y 的确值了;
乙:我现在也知道 X 和 Y 的确值了;
丁:我现在也知道 X 和 Y 的确值了。
要求给定 N,判断这些话是否能被同时满足,输出 YES 或 NO。T 组数据,1 <= T <= 200,2 <= N <= 3000。
据说此题为 HIT2018春校赛I题,真实性不论,本题还是比较有意思的。可以注意到,四人中只有丁和解题者是处于同一信息状态下的。换句话说,若丁最后确实知道了 X 和 Y 的确值,那么我们也可以知道 X 和 Y 的确值。设初始解集 P = { ( X , Y ) ∣ 2 < = X < = Y < = N , X ∈ N , Y ∈ N } P = \{(X, Y) | 2 <= X <= Y <= N, X \in \mathbb{N}, Y \in \mathbb{N}\} P={(X,Y)∣2<=X<=Y<=N,X∈N,Y∈N},下面就根据对话逐步缩小解集 P,直至仅剩余一个元素,即为解。若剩余多个元素或解集为空,即条件不能被同时满足。
对于第一句前半乙的话和第二句前半丙的话,我们可以初步缩小解集。将解集 P 中数对 (X, Y) 按 X * Y 的值划为不超过 N ∗ N N * N N∗N 个数簇,X * Y 的值相同的划为同一数簇,不同的划为不同数簇。若某一数簇内只有一个数对,则该数对必不为解,从解集中除去。这样处理后的解集 P 中所有数对可以使得第二句前半丙的话为真。对于第一句前半乙的话则可以简化处理,6 <= X + Y <= N + N - 2 即可。
接下来处理第一句后半乙的话,对于任一 S ∈ [ 6 , N + N − 2 ] S \in [6, N + N - 2] S∈[6,N+N−2],将解集 P 中所有 X + Y = S 的数对 (X, Y) 取出,构成子集 Q S Q_S QS。换句话说,取集合 Q S ∈ P Q_S \in P QS∈P 使得 ∀ ( X , Y ) ∈ Q S , X + Y = S \forall (X, Y) \in Q_S, X + Y = S ∀(X,Y)∈QS,X+Y=S 并且 ∀ ( X , Y ) ∈ P − Q S , X + Y ≠ S \forall (X, Y) \in P - Q_S, X + Y ≠ S ∀(X,Y)∈P−QS,X+Y=S。对于任一 Q S Q_S QS,若 ∃ ( X , Y ) ∈ Q S \exist (X, Y) \in Q_S ∃(X,Y)∈QS 使得 (X, Y) 所在数簇仅有一个数对,则集合 Q S Q_S QS 内所有数对必不为解,从解集中除去。这样处理后的解集 P 中所有数对可以使得第一句后半乙的话为真。
对于第二句后半丙的话,根据当前解集 P 重新构建数簇,若某一数簇内有超过一个数对,则该数簇内所有数对必不为解,从解集中除去。对于第三句乙的话,根据当前解集 P 重新构建 Q S Q_S QS,若某一 Q S Q_S QS 内有超过一个数对,则该 Q S Q_S QS 内所有数对必不为解,从解集中除去。
最后,若解集 P 中仅剩余一个元素,则丁和我们均知道了 X 和 Y 的确值,否则这些话不能被同时满足。这样,我们得到了一个朴素的算法。
代码如下:
#include<stdio.h>
#define MAX_N2 (9000005)
#define MAX_N22 (5000000)
struct pr
{
int x,y;
}p[MAX_N22];
int cnt[MAX_N2];
bool fgr(int n)
{
bool flg;
int m=0;
for(int i=4;i<=n*n;i++)
cnt[i]=0;
for(int i=2;i<=n;i++)
for(int j=i;j<=n;j++)
cnt[i*j]++;
for(int i=6;i<=n+n-2;i++)
{
flg=true;
for(int j=2;flg&&j+j<=i;j++)
if(cnt[j*(i-j)]<=1)
flg=false;
for(int j=2;flg&&j+j<=i;j++)
p[m].x=j,p[m++].y=i-j;
}
if(m==0)
return false;
for(int i=4;i<=n*n;i++)
cnt[i]=0;
for(int i=0;i<m;i++)
cnt[p[i].x*p[i].y]++;
for(int i=0;i<m;i++)
if(cnt[p[i].x*p[i].y]>1)
p[i].x=p[--m].x,p[i--].y=p[m].y;
if(m==0)
return false;
for(int i=4;i<=n+n;i++)
cnt[i]=0;
for(int i=0;i<m;i++)
cnt[p[i].x+p[i].y]++;
for(int i=0;i<m;i++)
if(cnt[p[i].x+p[i].y]>1)
p[i].x=p[--m].x,p[i--].y=p[m].y;
return m==1;
}
int main()
{
int n,t;
scanf("%d",&t);
while(t--)
scanf("%d",&n),
puts(fgr(n)?"YES":"NO");
return 0;
}
很明显,上面的代码并不能在时间限制内跑完,需要优化。最容易想到的是打表,实施后发现跑完 2 到 3000 所有数据花费时间不超过一分钟。不过这使得代码略长,于是考虑一个问题:使得所有话成立的 N 是否构成了一个整数连续的区间?当 N 很小的时候,作为解的数对不在初始解集里;当 N 充分大的时候,原本成立的解变得不成立或不唯一了。但是,也可能随 N 增大而产生新解。也就是说,从理论上不能证明使得解成立的 N 取值为一个整数区间。但事实上,观察打出的表可以得出,使得数对成立的 N 构成了一个区间,该区间为 [62, 865],当 N 取值为该区间内整数时,存在唯一数对解 (4, 13)。
优化代码如下:
#include<stdio.h>
int main()
{
int n,t;
scanf("%d",&t);
while(t--)
scanf("%d",&n),
puts(n>61&&n<866?"YES":"NO");
return 0;
}