整除理论
整除的基本知识
有
分别以
设
在 C++ 中,用 a%b==0
来表示
整除有几个常用性质:
- 若
且 ,那么 。
回忆一下直尺上的刻度,每 个 小刻度形成一个 中刻度,两个 中刻度会形成一个 大刻度。这蕴涵了 个 小刻度形成 大刻度。也就是说,已知 和 ,可以推出 。 - 若
且 ,那么对于任意的整数 ,有 。
对 的倍数任意加减得到的仍然是 的倍数。想象一下一杯饮料 元,那么 元纸币恰好能买两杯, 元纸币恰好能买五杯,那么无论多少张 元和 元纸币,都能买到整数杯饮料。甚至,找回来的钱也可以是 元纸币或 元纸币,对应减法。值得注意的是, 是任何正整数的倍数。也就是说, 也是对的。 - 设整数
,那么 等价于 。
显然将两数放大若干倍,它们之间的整除性不变。
有
实质上就是找
不超过
同理,可以尝试应用这种优化过的枚举法寻找
vector<int> vec; // vec用来存储n的所有约数(无序) void find_divisors(int n) { // 返回值为约数个数 for (int k = 1; k * k <= n; k++) { // 考虑到k*k有可能溢出,有时写成k<=n/k if (n % k == 0) { vec.push_back(k); if (k != n / k) vec.push_back(n / k); // 约数成对出现,但要注意k=n/k时可能出现重复 } } }
习题:P1403 [AHOI2005] 约数研究
表示 的约数个数,现在给出 ,要求求出 到 的总和。
解题思路
如果循环计算每个数的约数个数,则时间复杂度为
站在每个约数的视角来考虑问题,
#include <cstdio> int main() { int n; scanf("%d", &n); int ans = 0; for (int i = 1; i <= n; i++) { ans += n / i; } printf("%d\n", ans); return 0; }
例题:P2926 [USACO08DEC] Patting Heads S
给定
和 个正整数,求每个数是另外多少个数的倍数。 ,数字 不超过 。例如给出 个数,分别是 时,答案分别是 。
分析:如果直接枚举每对数,则时间复杂度为
参考代码
#include <cstdio> const int N = 1e5 + 5; int a[N], ans[N]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) scanf("%d", &a[i]); for (int i = 1; i <= n; i++) { for (int j = 1; j <= n; j++) { if (i == j) continue; if (a[i] % a[j] == 0) ans[i]++; } } for (int i = 1; i <= n; i++) printf("%d\n", ans[i]); return 0; }
如果从枚举约数的角度思考,会很自然地得到这样一个算法:对于每个数
参考代码
#include <cstdio> const int N = 1e5 + 5; const int A = 1e6 + 5; int a[N], ans[N], cnt[A]; int main() { int n; scanf("%d", &n); for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); cnt[a[i]]++; } for (int i = 1; i <= n; i++) { int ans = 0; // 枚举a[i]的所有约数 for (int j = 1; j * j <= a[i]; j++) { if (a[i] % j == 0) { int d1 = j, d2 = a[i] / j; ans += cnt[d1]; if (d2 != d1) ans += cnt[d2]; } } printf("%d\n", ans - 1); // 减去自己的贡献 } return 0; }
本题还可以从枚举倍数的角度思考:对于一个数
#include <cstdio> #include <algorithm> using std::max; const int N = 1e5 + 5; const int A = 1e6 + 5; int a[N], ans[A], cnt[A]; int main() { int n; scanf("%d", &n); int maxa = 0; for (int i = 1; i <= n; i++) { scanf("%d", &a[i]); cnt[a[i]]++; maxa = max(maxa, a[i]); } for (int i = 1; i <= maxa; i++) { for (int j = i; j <= maxa; j += i) { ans[j] += cnt[i]; // i 这个数会对 j 产生 cnt[i] 的贡献 } } for (int i = 1; i <= n; i++) printf("%d\n", ans[a[i]] - 1); // 要减掉 a[i] 对自己的贡献 return 0; }
乍一看有上限到
调和级数复杂度的证明涉及到微积分,目前可以先当成结论记忆。感兴趣可以阅读:调和级数的复杂度。
质数与合数
当寻找
设正整数
若
计算题:从
答案
从
例题:P3383 【模板】线性筛素数
给出若干(不超过
)个正整数 ,依次输出第 小的素数,素数的范围是 以内。
判断一个整数是否为质数有一个时间复杂度为
bool is_prime(int x) { // 判断x是否为质数 for (int i = 2; i * i <= x; i++) { // 枚举不超过sqrt(x)的i if (x % i == 0) return false; } return true; }
这里之所以枚举到
这同时也给出了一个寻找一定范围内所有质数的算法。例如,为了求出不超过
只需要用不超过
可以看出,没有删去的数是
vector<int> p; void eratosthenes_sieve(int n) { // 寻找不超过n的所有质数,数组flag用来做标记,flag[i]==true表示i被标记,是合数 flag[1] = true; for (int i = 2; i * i <= n; i++) { if (!flag[i]) { // 如果i未被之前的数所标记说明i是质数 for (int j = 2 * i; j <= n; j += i) { // 实际上也可以直接从i*i开始筛,因为2到i-1的倍数之前已经筛过了 flag[j] = true; // 将i除了自己以外的倍数标记为合数 } } } for (int i = 2; i <= n; i++) if (!flag[i]) p.push_back(i); }
埃氏筛的时间复杂度是
参考代码
#include <cstdio> #include <vector> using std::vector; const int N = 1e8 + 5; vector<int> p; bool flag[N]; void eratosthenes_sieve(int n) { for (int i = 2; i * i <= n; i++) { if (!flag[i]) { for (int j = 2 * i; j <= n; j += i) { flag[j] = true; } } } for (int i = 2; i <= n; i++) if (!flag[i]) p.push_back(i); } int main() { int n, q; scanf("%d%d", &n, &q); eratosthenes_sieve(n); for (int i = 1; i <= q; i++) { int k; scanf("%d", &k); printf("%d\n", p[k - 1]); } return 0; }
既然质数的(大于
for (int i = 2; i <= n; i++) { if (!flag[i]) { // 如果i没有被筛去 p.push_back(i); // 那么i就是一个质数 } for (int j : p) { // 枚举当前的所有质数 if (i > n / j) break; flag[i * j] = true; // i的j倍是合数 } }
在这个形式中,只有当
在这个基础上,再添加一行代码,就可以得到欧拉筛,一种时间复杂度为
for (int i = 2; i <= n; i++) { if (!flag[i]) { p.push_back(i); } for (int j : p) { if (i > n / j) break; flag[i * j] = true; if (i % j == 0) break; // 欧拉筛的核心 } }
为什么增加这一句判断,就可以提前结束循环呢?设
首先证明充分性:合数 p
数组中。同时,如果 break
了,就代表着有一个比
然后证明必要性:合数 break
了。这其实也很简单:因为 break
,而不会让
也就是说,对于每一个合数,它只会被 flag[i*j]=true
这条语句标记为合数一次,也就是说这条语句实际上只会被执行不超过
参考代码
#include <cstdio> #include <vector> using std::vector; const int N = 1e8 + 5; vector<int> p; bool flag[N]; void euler_sieve(int n) { for (int i = 2; i <= n; i++) { if (!flag[i]) { p.push_back(i); } for (int j : p) { if (i > n / j) break; flag[i * j] = true; if (i % j == 0) break; } } } int main() { int n, q; scanf("%d%d", &n, &q); euler_sieve(n); for (int i = 1; i <= q; i++) { int k; scanf("%d", &k); printf("%d\n", p[k - 1]); } return 0; }
完善程序题:
(好运的日期)一个日期可以用
#include <bits/stdc++.h> using namespace std; const int MAXW = ①; const int days[13] = {0, 31, 0, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; int prime[MAXW], cnt; bool not_prime[MAXW]; void linear_prime(int n) { --n; not_prime[0] = not_prime[1] = true; for (int i = 2; i <= n; i++) { if (not_prime[i] == false) prime[++cnt] = i; for (int j = 1; ②; j++) { not_prime[i * prime[j]] = 1; if (i % prime[j] == 0) ③; } } } bool check(int n) { return ④; } int main() { linear_prime(MAXW); int x, y, z, w; cin >> x >> y >> z; if (y == ⑤) w = check(x) ? 29 : 28; else w = days[y]; if (not_prime[x * y * (w - z + 1)]) cout << "unlucky" << endl; else cout << "lucky" << endl; return 0; }
①处可以填?
A. 753005
/ B. 10000000000
/ C. 725041
/ D. 2024
②处可以填?
A. j <= cnt
/ B. i * prime[j] <= n
C. (j <= cnt) && (i * prime[j] <= n)
/ D. (i <= cnt) && (prime[i] * prime[j] <= n)
③处可以填?
A. not_prime[i] = true
/ B. return
/ C. continue
/ D. break
④处可以填?
A. n % 4 == 0
/ B. (n % 400 == 0 || (n % 4 == 0 && n % 100 != 0))
C. (n % 4 == 0 && n % 100 != 0)
/ D. (n % 100 == 0 || (n % 4 == 0 && n % 100 != 0))
⑤处可以填?
A. 1
/ B. 7
/ C. 8
/ D. 2
答案
答案:ACDBD
例题:P1835 素数密度
询问
之间的质数个数。
分析:乍一看这题的数据范围非常大,只有
事实上,再联系到
因为只需要利用
参考代码
#include <cstdio> #include <algorithm> using ll = long long; using std::max; const int N = 1e6 + 5; const int BOUND = 50000; bool isprime[N]; // isprime[x]表示x是否为素数 bool ans[N]; // ans[x]表示l+x是否为素数 void init() { for (int i = 2; i <= BOUND; i++) isprime[i] = true; for (int i = 2; i * i <= BOUND; i++) { if (isprime[i]) { for (int j = i * i; j <= BOUND; j += i) { isprime[j] = false; } } } } int main() { init(); int l, r; scanf("%d%d", &l, &r); if (l == 1) l = 2; // 1不是素数,靠筛法筛不到1 for (int i = 0; i <= r - l; i++) ans[i] = true; for (int i = 2; i <= BOUND; i++) { if (isprime[i]) { for (ll j = 1ll * i * max((l - 1) / i + 1, 2); j <= r; j += i) { // j的初始值是为了找到[l,r]范围内开始筛的第一个数字 ans[j - l] = false; } } } int cnt = 0; for (int i = 0; i <= r - l; i++) if (ans[i]) cnt++; printf("%d\n", cnt); return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?