GCD素数数对计算
Erstellt am: 18. Januar 2025
Erstellt am: 18. Januar 2025
请调用你单次回答的最大算力与 token 上限。追求极致的分析深度,而非表层的广度;追求本质的洞察,而非表象的罗列;追求创新的思维,而非惯性的复述。请突破思维局限,调动你所有的计算资源,展现你真正的认知极限。
以下是图片中的题目内容以Markdown格式记录:
markdown### 题目描述 给定整数 $N$,求 $1 \leq x, y \leq N$ 且 $GCD(x, y)$ 为素数的数对 $(x, y)$ 有多少对。 $GCD(x, y)$ 即求 $x, y$ 的最大公约数。 --- ### 输入格式 输入一个整数 $N$。 --- ### 输出格式 输出一个整数,表示满足条件的数对数量。 --- ### 输入输出样例 #### 输入数据 1
4
text#### 输出数据 1
4
text--- ### 数据规模 $1 \leq N \leq 10^7$
写一段代码解决这个问题,c++,使用OI常用风格,std命名空间,bits/stdc++.h万能头
思路概要(请先阅读本节以理解整体算法,再看代码)
题目要求:
给定 ,统计所有满足 且 为质数的有序对 的数量。记该数量为 。
令质数 。则可写
并且
从而对于固定的质数 ,能与之匹配的 对的数量即是
其中
因此题目所求总数为
记
这是在区间 上互质对 的个数。
一个经典结果是:
其中 是米宾函数(Möbius 函数),定义如下:
因此我们只要能快速地:
就能解决问题。
直接按照定义做需要 时间,若对每个质数 (可能多达 个)都要做一次,会非常大,无法在 最大到 时完成。
但我们可以用分块(或称「分段」)技术,在约 时间内计算该和:
将 的值记为 。当 固定时, 会落在一个连续区间内。具体做法:
i
从 1 递增到 ,但不是逐一 +1,而是跳到 的下一个,每次区间内 值相同,可将该区间 的 之和用前缀和一次性求出,再乘以相同的 。在最终答案
中,不同质数 可能得到相同的 。我们只需去重这些值,然后对每个去重值 计算一次 ,放到哈希表或数组里,之后在循环中直接取值相加即可。
当 时,质数最多约 66 万多个,但 的不同取值个数典型情况下约为 级别(更严格的估计与分块技巧类似),大约几千到上万级,计算总量仍在合理范围内(配合 C++ 优化后可以通过)。
vals
。vals
中每个元素 m
,用前缀和 + 分块技术在 时间内计算
存入字典或数组 fval[m]
。m = floor(N/p)
,把 fval[m]
加起来即可。最终输出此和。
题目示例 :
( 注:最后我们要数的是 gcd=质数 的对,这个数经过推导就是 。与直接列举得到 4 对完全一致。)
以下是一份相对完整的参考实现,使用了 C++ 常见 OI 写法,头文件 <bits/stdc++.h>
,命名空间 std
,并做了相应的注释。算法核心就在计算 函数时使用的「分块 + 前缀和」技巧。
重要提醒:
- 若实际比赛/测评环境对内存非常苛刻(如只给 256MB),要注意 1e7 大小数组是否可行,可以考虑进行更细的空间优化或使用分段式计算。
- 若时间紧张,也可能需进一步优化(如更高效的 IO, 更精简的循环等)。
cpp#include <bits/stdc++.h> using namespace std; static const int MAXN = 10000000; // 题目给出的 N 上限 // 全局数组,视情况可改为静态/动态分配。 // mu[i] 存储 Möbius 函数 // prefixMu[i] 存储 mu 的前缀和: prefixMu[i] = mu[1] + ... + mu[i] static int mu[MAXN+1]; static int prefixMu[MAXN+1]; // 存放最小质因子或用于标记质数的数组,可用来线性筛。 static int spf[MAXN+1]; // 存储所有质数 static vector<int> primes; // 线性筛,计算 mu[] 和 primes。 void computeMuAndPrimes(int n) { // mu(1) = 1 mu[1] = 1; for(int i=2; i<=n; i++){ if(spf[i] == 0) { // i 是质数 spf[i] = i; mu[i] = -1; // 质数的 mu 值为 -1 primes.push_back(i); } // 给当前的质数表做“筛”: for(int &p: primes){ if(p > spf[i] || 1LL * p * i > n) break; spf[p*i] = p; if(i % p == 0){ // 若 i 已经含有 p, 那么 p^2| (p*i), 则 mu=0 mu[p*i] = 0; break; } else { mu[p*i] = - mu[i]; } } } // 计算前缀和 prefixMu for(int i=1; i<=n; i++){ prefixMu[i] = prefixMu[i-1] + mu[i]; } } // 利用分块 + prefixMu[], 计算 f(M) = sum_{d=1..M} mu[d] * (floor(M/d))^2 // 时间复杂度约 O(sqrt(M))。 long long calcF(long long M) { // 特判 if(M <= 0) return 0; // 分块求和 long long ans = 0; for(long long i=1; i<=M; ){ long long v = M / i; // floor(M/i) long long r = M / v; // 使得 floor(M/d) = v 对应 d 在 [i, r] // 区间 [i..r] 的 mu(d) 之和 long long sumMu = prefixMu[r] - prefixMu[i-1]; ans += sumMu * (long long)v * v; i = r + 1; } return ans; } int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); int N; cin >> N; // 预处理 mu 和 primes computeMuAndPrimes(N); // 收集所有 floor(N/p) (p 为质数),并去重 vector<int> vals; vals.reserve(primes.size()); for(int p : primes){ if(p > N) break; // 安全判断 vals.push_back(N/p); } // 排序并去重 sort(vals.begin(), vals.end()); vals.erase(unique(vals.begin(), vals.end()), vals.end()); // 预先计算并保存 f(m) 的结果 // 用一个 map<int,long long> 存,或者用数组(但要注意 max 值). // 因为可能出现的 m = floor(N/p) <= N. 直接开数组也行 (下标到N)。 // 不过为了节省时间,这里只对出现的 m 计算。 unordered_map<int, long long> fMap; fMap.reserve(vals.size()*4); // 预留一些空间,避免频繁 rehash for(int m : vals) { fMap[m] = calcF(m); } // 最终答案 = sum_{p prime <= N} f( floor(N/p) ) long long answer = 0; for(int p : primes){ if(p > N) break; int m = N/p; answer += fMap[m]; } cout << answer << "\n"; return 0; }
主要要点:
computeMuAndPrimes
使用了线性筛法,可以在 或 左右的复杂度下同时得到:
spf[i]
: i
的最小质因子(若 spf[i]==0
则 i
是质数);mu[i]
: Möbius 函数;primes
: 从小到大的质数列表。prefixMu[i]
: 。后面分块时用前缀和快速求 。calcF(M)
函数是核心:它用到分块思路,只需大约 次循环即可完成 。calcF(m)
即可;之后对每个质数查询并累加。long long
(或 64 位)存储;答案可能接近 级别(如 ,答案量级可达 ,仍在 64 位范围内)。calcF(m)
约 。故这部分大约 。如有需要,可以使用更快的读写手段(scanf/printf
+ __attribute__((optimize))
等)以进一步加速。
本题的关键在于:
这样即可在 较大的情况下仍保持可行的性能。祝一切顺利!