K. Lonely Numbers - 题解
比赛与标签
比赛: Bubble Cup 13 - Finals [Online Mirror, unrated, Div. 1] 标签: binary search, math, number theory, two pointers 难度: *1600
题目喵述~
这道题呀,定义了一种奇妙的 "朋友" 关系。在数字的世界里,如果两个不同的数 a 和 b 满足一个条件,它们就是朋友啦。这个条件是:gcd(a, b)(它俩的最大公约数)、a/gcd(a, b) 和 b/gcd(a, b) 这三个数可以组成一个三角形。
一个数如果在 1 到 n 这个群体里一个朋友都找不到,那它就是个 "孤独" 的数字。我们的任务就是,对于给定的 n,找出在 1, 2, ..., n 这个群体里,到底有多少个这样的孤独数字呢?
输入输出格式喵~
- 输入: 先是一个整数
t,表示有t组测试。接下来有t个数,每个数是n。 - 输出: 对每个
n,输出1到n中孤独数字的数量。
举个栗子🌰:当 n=5 时,孤独的数字是 1, 3, 5,所以答案是 3。
猫娘的思路分析喵~
嘿嘿,这个问题看起来有点绕,但只要我们像小猫咪一样,一步步拆解,就能发现其中的奥秘啦!
第一步:翻译 "朋友" 条件
首先,三个数 x, y, z 能组成三角形,需要满足三角形两边之和大于第三边,也就是:
x + y > zy + z > xz + x > y
现在,我们把题目里的三个数代进去。令 g = gcd(a, b),a' = a/g,b' = b/g。根据最大公约数的性质,我们知道 a' 和 b' 是互质的(gcd(a', b') = 1)。 那么,a 和 b 是朋友的条件就是 g, a', b' 能组成三角形。
我们来分析一下这三个不等式:
g + a' > b'g + b' > a'a' + b' > g
因为 a 和 b 是不同的数,所以 a' != b'。不妨假设 a' < b'。因为它们是互质的整数,所以 b' - a' >= 1。
g + b' > a'这个不等式总是成立的,因为g >= 1并且b' > a'。a' + b' > g这个不等式也常常成立。a'和b'是a和b除以g后的部分,通常情况下,它们的和会比g大得多。
所以,最关键的约束是 g + a' > b',也就是 g > b' - a'。因为 a', b' 都是整数,这等价于 g > |a' - b'|。
一句话总结:a 和 b 是朋友,当且仅当 gcd(a, b) > |a/gcd(a, b) - b/gcd(a, b)|。
第二步:哪些数字会是孤独的?
一个数 x 是孤独的,意味着对于集合 {1, 2, ..., n} 中任何一个不等于 x 的 y,它们都不是朋友。
我们来分情况讨论一下数字 x 的性质吧!
情况一:x = 11 会是孤独的吗?我们来给它找个朋友 y > 1 试试。
g = gcd(1, y) = 1x' = 1/g = 1y' = y/g = y- 朋友条件是
g > |x' - y'|,也就是1 > |1 - y|。 - 因为
y >= 2,所以|1 - y| = y - 1 >= 1。 1 > y - 1这个不等式永远不会成立! 所以,1在任何群体里都交不到朋友,它永远是孤独的。我们的计数器先加一!
情况二:x 是一个素数 p 我们来给素数 p 找朋友 y。
- 如果
y是p的倍数,比如y = k*p(k > 1且y <= n)。g = gcd(p, kp) = px' = p/g = 1y' = kp/g = k- 朋友条件是
p > |1 - k| = k - 1。 - 我们还需要满足三角形的另一个条件
x' + y' > g,也就是1 + k > p。 - 结合起来,我们需要
p-1 < k < p+1。满足这个条件的整数k只有k=p! - 这意味着,如果
y = p*p = p^2存在于集合{1, ..., n}中,那么p和p^2就是朋友! - 所以,如果一个素数
p满足p^2 <= n,它就不是孤独的。
- 如果
y和p互质g = gcd(p, y) = 1x' = p/g = py' = y/g = y- 朋友条件是
1 > |p - y|。这要求p和y是相邻的整数,但它们又要互质,这不可能发生(除非一个是1,但我们讨论的是p>=2)。
素数小结:一个素数 p 是孤独的,当且仅当它的唯一潜在朋友 p^2 不在 1 到 n 的范围内,也就是 p^2 > n,即 p > sqrt(n)。
情况三:x 是一个合数 c 合数会孤独吗?猫娘的直觉是不会!我们来证明一下,任何一个合数 c 都能找到朋友。
- 令
p是c的最小质因数。那么c可以写成c = p * k,其中k >= p。 - 我们来为
c构造一个朋友y。一个好的想法是找一个和c"结构相似" 的数。 - 试试
y = c - p = p * (k-1)。- 因为
c是合数,k >= p >= 2,所以k-1 >= 1,y = p(k-1) >= p >= 2。所以y是一个合法的、小于c的数。 g = gcd(c, y) = gcd(pk, p(k-1)) = p * gcd(k, k-1) = p * 1 = p。c' = c/g = k。y' = y/g = k-1。- 朋友条件是
g > |c' - y'|,也就是p > |k - (k-1)| = 1。 - 因为
p是质数,所以p >= 2,p > 1恒成立!
- 因为
- 我们还需要检查另外两个三角不等式:
g + y' > c'=>p + (k-1) > k=>p > 1,成立。c' + y' > g=>k + (k-1) > p=>2k-1 > p。因为k >= p,所以2k-1 >= 2p-1。而2p-1 > p(即p>1) 对所有质数成立。所以这个也成立!
太棒啦!我们证明了,对于任何合数 c,c-p (其中p是c的最小质因子) 都是它的朋友!所以,所有合数都不是孤独的!
最终结论!
梳理一下我们的发现:
- 数字
1永远是孤独的。 - 素数
p,如果p > sqrt(n),它就是孤独的。 - 素数
p,如果p <= sqrt(n),它不是孤独的(朋友是p^2)。 - 所有合数都不是孤独的。
所以,孤独数字的总数 = 1 (来自数字1) + (满足 sqrt(n) < p <= n 的素数 p 的数量)。
这个问题瞬间就转化成了一个数素数的问题!我们可以用 (n以内素数的总数) - (sqrt(n)以内素数的总数) 来计算后半部分。
为了快速回答多组查询,我们可以预处理出 1 到 10^6 范围内的所有素数,并计算一个素数前缀和数组 prime_counts[i],表示 1 到 i 之间素数的数量。这样每次查询 n,答案就是 1 + prime_counts[n] - prime_counts[floor(sqrt(n))],可以在 O(1) 时间内搞定!
AC代码奉上喵~
#include <iostream>
#include <vector>
#include <cmath>
// 题目 n 的最大值是 10^6,我们开一个稍大一点的数组,喵~
const int MAX_N = 1000001;
// is_prime[i] 用来标记 i 是不是素数
std::vector<bool> is_prime(MAX_N, true);
// prime_counts[i] 用来存 1 到 i 之间素数的总个数(前缀和)
std::vector<int> prime_counts(MAX_N, 0);
// 使用埃氏筛法 (Sieve of Eratosthenes) 预处理出所有素数
void sieve() {
is_prime[0] = is_prime[1] = false; // 0 和 1 都不是素数哦
for (int p = 2; p * p < MAX_N; ++p) {
// 如果 p 是素数
if (is_prime[p]) {
// 那么 p 的所有倍数都不是素数,把它们筛掉
for (int i = p * p; i < MAX_N; i += p)
is_prime[i] = false;
}
}
}
// 预处理素数的前缀和
void precompute_prime_counts() {
prime_counts[0] = 0;
for (int i = 1; i < MAX_N; ++i) {
// 当前的前缀和等于前一个数的前缀和...
// ...如果当前数是素数,就再加 1
prime_counts[i] = prime_counts[i - 1] + (is_prime[i] ? 1 : 0);
}
}
int main() {
// 加速输入输出,让程序跑得飞快!
std::ios_base::sync_with_stdio(false);
std::cin.tie(NULL);
// 在处理查询前,先把筛法和前缀和都跑一遍
sieve();
precompute_prime_counts();
int t;
std::cin >> t;
while (t--) {
int n;
std::cin >> n;
// 根据我们的推导,孤独数字的数量是:
// 1 (代表数字1)
// + (n以内的素数数量)
// - (sqrt(n)以内的素数数量)
// 这就等于 1 + (pi(n) - pi(floor(sqrt(n)))) 啦
// 计算 floor(sqrt(n))
int s = static_cast<int>(sqrt(n));
// 利用前缀和数组 O(1) 计算答案
int ans = 1 + prime_counts[n] - prime_counts[s];
std::cout << ans << "\n";
}
return 0;
}时空复杂度分析的说~
时间复杂度:
O(MAX_N * log(log(MAX_N)) + T)的说。- 埃氏筛法的时间复杂度是
O(MAX_N * log(log(MAX_N)))。 - 预处理前缀和数组是
O(MAX_N)。 - 这两部分是一次性的预处理。之后对于
T次查询,每次查询都只需要O(1)的时间。 - 所以总时间就是预处理的时间加上所有查询的时间。
- 埃氏筛法的时间复杂度是
空间复杂度:
O(MAX_N)的说。- 我们需要两个大小为
MAX_N的数组is_prime和prime_counts来存储预处理的结果。
- 我们需要两个大小为
猫娘的小课堂~
这道题真是太有意思啦!它教会了我们几件重要的事情:
- 化繁为简: 不要被题目复杂的定义吓到!"朋友" 关系看起来很复杂,但通过数学分析,我们把它简化成了一个非常清晰的不等式
g > |a' - b'|。 - 分类讨论是好朋友: 当你对一个问题没有头绪时,尝试把研究对象(这里的数字
x)根据它的性质(是1,是素数,还是合数)进行分类,往往能让思路豁然开朗! - 构造的力量: 在证明所有合数都不是孤独的时候,我们通过构造一个特定的朋友
y = c - p来完成了证明。这种构造性的证明方法在数论和组合数学里非常有用哦。 - 预处理大法好: 对于有多组查询,并且查询范围固定的问题,一定要想到预处理!通过一次性的计算,把之后每次查询的复杂度降到最低,是竞赛中超级实用的技巧。
总之,这道题完美地结合了数论分析和算法技巧。希望通过本猫娘的讲解,你也感受到了解决它的乐趣!下次遇到类似的题目,也要像这样勇敢地去分析和拆解哦!加油喵~ 💖