作业介绍
一、 埃氏筛 (Sieve of Eratosthenes)
这是最基础、最符合直觉的筛法。
- 算法流程:
- 假设所有数都是质数。
- 从 2 开始从小到大遍历,如果遇到一个质数 ,就把它的所有倍数()全都标记为合数。
-
适用场景:
-
求 的质数(但速度不如线性筛)。
-
核心特长:由于它是枚举质数的倍数,所以非常适合在筛的过程中,顺便求出每个数的所有质因子,或者处理某些非积性函数。
-
时间复杂度:
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000005;
bool is_prime[N];
int primes[N], cnt;
void eratosthenes(int n) {
// 1. 初始化全为质数
memset(is_prime, 1, sizeof(is_prime));
is_prime[0] = is_prime[1] = 0;
// 2. 从 2 开始遍历
for (int i = 2; i <= n; i++) {
if (is_prime[i]) {
primes[cnt++] = i; // 记录质数
// 3. 将 i 的倍数标记为合数,从 i*i 开始优化(因为更小的倍数已被更小的质数筛去)
for (int j = i * i; j <= n; j += i) {
is_prime[j] = 0;
}
}
}
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
eratosthenes(100);
return 0;
}
二、 线性筛 / 欧拉筛 (Euler's Sieve)
竞赛绝对主力,也是你必须肌肉记忆的模板。
- 算法流程:
- 假设所有数都是质数。
- 遍历 :如果是质数,加入质数表。
- 遍历已知质数表,将
当前数 * 质数标记为合数。 - 灵魂一步:如果当前数能被该质数整除,立刻
break。保证每个合数只被它的最小质因数筛掉一次。
-
适用场景:
-
快速求 的所有质数。
-
核心特长:结合
break的性质,极其适合线性求各种积性函数(如欧拉函数 、莫比乌斯函数 、约数个数等)。 -
时间复杂度:绝对严格的
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 1000005;
bool is_prime[N];
int primes[N], cnt;
void linear_sieve(int n) {
memset(is_prime, 1, sizeof(is_prime));
is_prime[0] = is_prime[1] = 0;
for (int i = 2; i <= n; i++) {
// 发现质数
if (is_prime[i]) primes[cnt++] = i;
// 遍历已知质数
for (int j = 0; j < cnt && i * primes[j] <= n; j++) {
is_prime[i * primes[j]] = 0; // 筛掉合数
// 遇到最小质因数,立刻停止,防止重复筛选
if (i % primes[j] == 0) break;
}
}
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
linear_sieve(100);
return 0;
}
根据数论基础:如果一个数 是合数,那么它必定有一个质因数 。 因为 ,所以我们只需要: 第一步(预处理):用线性筛求出 范围内的所有质数。 第二步(区间筛):对于每组输入的 和 ,遍历我们预处理出的质数 。算出在 区间内 的倍数,把它们全部标记为合数。为了不爆内存,我们将下标平移,用 is_p[i - L] 来代表数字 i 是否为合数。 第三步(统计):遍历平移后的标记数组,记录相邻两个质数的差值,不断更新最大值和最小值即可。
樱花
一、 数学等式推导
我们的目标是求不定方程 的正整数解 的数目。为了方便处理,我们需要将分式化为整式。
-
去分母:
等式两边同时乘以 ,得到:
-
移项与因式分解:
将所有项移到等号一侧,得到:
为了把等式左侧凑成可以因式分解的形式,我们在等式两边同时加上 :
$$x \cdot y - x \cdot n! - y \cdot n! + (n!)^2 = (n!)^2$$此时,等号左边可以提取公因式,完美地分解为:
-
分析解的对应关系:
由于题目要求 和 均为正整数,并且 ,显然必然有 (因为 ),这就意味着推导出了边界条件: 且 。
我们可以令 ,。既然 ,那么 和 必定都是正整数。
此时原方程变为了:
这说明,只要我们能找到 的任意一个正约数 ,就可以唯一确定一个正约数 ,进而唯一对应确定一组符合题意的正整数解 。
满足条件的正整数解 的对数,完全等于 的正约数个数。
二、 约数个数求法推导
根据算术基本定理(唯一分解定理),如果我们将一个整数分解为质因数的乘积形式 ,那么它的正约数个数为:
假设 质因数分解后,质因子 的指数为 ,那么 中质因子 的指数就是 。因此 的正约数总个数为:
如何快速求 中各个质因子的指数?
利用勒让德公式 (Legendre's Formula),阶乘 中包含质因子 的个数 可以通过不断将 除以 的幂次方并向下取整求和来计算:
$$c_p = \sum_{j=1}^{\infty} \left\lfloor \frac{n}{p^j} \right\rfloor$$反素数
#include<bits/stdc++.h>
using namespace std;
#define int long long
// n: 题目给定的上限
// mxd: 搜索过程中记录的最大约数个数
// ans: 对应的最大反素数数值
// p数组: 前10个质数。因为 2*3*5*7*11*13*17*19*23*29 > 2*10^9,所以最多只需要用到前10个质数!
int n, mxd, ans, p[] = {0, 2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
/*
* dfs参数释义:
* u : 当前正在枚举第 u 个质数
* val : 当前累乘得到的数值
* d : 当前数值的约数总个数(根据约数个数定理累乘得到)
* lim : 当前质数能够枚举的指数上限
*/
void dfs(int u, int val, int d, int lim) {
// 【核心数论性质 1】:更新最优解
// 1. 如果约数个数 d 更大,毫无疑问更新答案。
// 2. 如果约数个数 d 一样大,为什么取更小的 val?
// 因为反素数定义是:比它小的所有数的约数个数都严格小于它。
// 如果存在 val1 < val2 且约数个数相同,那么 val2 绝对不可能是反素数!
// 所以我们要找的,其实是“在产生最大约数个数时,数值最小的那个数”。
if (d > mxd || (d == mxd && val < ans)) {
mxd = d;
ans = val;
}
// 【剪枝】:用到第11个质数必然超出 2e9,直接回溯
if (u > 10) return;
int t = val;
// 【核心数论性质 2】:反素数的质因子指数必须单调不增!
// 假设存在 2^3 * 3^4,它的约数个数和 2^4 * 3^3 一样多,但前者数值更大,必定不是反素数。
// 所以 p[u] 的指数 i,绝对不能超过 p[u-1] 的指数 lim。
for (int i = 1; i <= lim; i++) {
if (t * p[u] > n) break; // 数值超过上限 n,直接剪枝
t *= p[u];
// 走向下一层:下个质数是 u+1,数值变大为 t,约数个数乘以 (i+1),下一个指数的上限限制为当前的 i
dfs(u + 1, t, d * (i + 1), i);
}
}
signed main() {
ios::sync_with_stdio(0); cin.tie(0);
cin >> n;
// 初始调用:从第1个质数(2)开始,初始值为1,约数个数为1。
// 指数上限给 31,因为 2^31 > 2*10^9,这是能达到的绝对极限。
dfs(1, 1, 1, 31);
cout << ans << '\n';
return 0;
}
题目
- 状态
- 正在进行…
- 题目
- 1
- 开始时间
- 2026-6-2 0:00
- 截止时间
- 2027-6-2 23:59
- 可延期
- 24 小时