整除理论

整除的基本知识

12 个苹果,恰好平分给 x 个人(每个人分到的苹果完整且数量相同),x 能取到哪些值?

分别以 112 假设 x,发现只有 x=1,2,3,4,6,126 个数字满足,这里用到的就是整除的概念。整数之间的整除性,体现为两个整数相除没有余数,此时两数具有倍数和约数的关系。

a,b 是两个整数,且 a0,如果存在另一个整数 q,使得 b=aq,那么就说 b 可被 a 整除,记作 a|b,且称 ba倍数ab约数

在 C++ 中,用 a%b==0 来表示 b 能够整除 a

整除有几个常用性质:

  1. a|bb|c,那么 a|c
    回忆一下直尺上的刻度,每 51mm 小刻度形成一个 5mm 中刻度,两个 5mm 中刻度会形成一个 1cm 大刻度。这蕴涵了 101mm 小刻度形成 1cm 大刻度。也就是说,已知 1|55|10,可以推出 1|10
  2. a|ba|c,那么对于任意的整数 x,y,有 a|bx+cy
    a 的倍数任意加减得到的仍然是 a 的倍数。想象一下一杯饮料 10 元,那么 20 元纸币恰好能买两杯,50 元纸币恰好能买五杯,那么无论多少张 20 元和 50 元纸币,都能买到整数杯饮料。甚至,找回来的钱也可以是 20 元纸币或 50 元纸币,对应减法。值得注意的是,0 是任何正整数的倍数。也就是说,a|(bb) 也是对的。
  3. 设整数 m0,那么 a|b 等价于 ma|mb
    显然将两数放大若干倍,它们之间的整除性不变。

120 个苹果,恰好平分给 x 个人(每个人分到的苹果完整且数量相同),x 能取到哪些值?

实质上就是找 120 的所有约数,但是对于 120,逐个枚举的计算量很大,但是注意到 120 的约数是成对出现的,比如 1×120=1202×60=120,其中 1,120,2,60 全都是 120 的约数。也就是说,一旦知道较小的因数 k 就可以求出那个较大的因数 120k,因为限定了大小,所以 k120kk 是整数,可以得出 k10<120<11,可以发现枚举量急剧减小。

不超过 10120 的因数有 1,2,3,4,5,6,8,10,那么对应的另一个约数就是 120,60,40,30,24,20,15,12。至此,很快就找出了 120 的全部约数。

同理,可以尝试应用这种优化过的枚举法寻找 n 的所有约数:如果 kn 的约数,那么 n/k 也一定是 n 的约数,只要限定 knk,即 kn,就可以在 O(n) 的时间复杂度内找到 n 的所有约数。

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] 约数研究

f(n) 表示 n 的约数个数,现在给出 n (n106),要求求出 f(1)f(n) 的总和。

解题思路

如果循环计算每个数的约数个数,则时间复杂度为 O(nn),会超时。

站在每个约数的视角来考虑问题,1 这个数会做 1,2,3,,n 这些数的约数,2 这个数会做 2,4,6, 这些数的约数,……。可以归纳为 i 这个数会做 ni 这么多个数的约数,因此原问题等价于计算 i=1nni

#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

给定 nn 个正整数,求每个数是另外多少个数的倍数。n105,数字 ai 不超过 106。例如给出 5 个数,分别是 2,1,2,3,4 时,答案分别是 2,0,2,1,3

分析:如果直接枚举每对数,则时间复杂度为 O(n2),显然不足以通过本题(能够获得 47 分)。

参考代码 O(n2)
#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;
}

如果从枚举约数的角度思考,会很自然地得到这样一个算法:对于每个数 ai,枚举它的所有约数,再看这些约数在 n 个数中出现了几次(可以在输入时预处理),对它们求和即为答案。这个算法的时间复杂度是 O(nA),这里 A 指的是 max{ai},比一开始的 O(n2) 更优,能够通过本题。

参考代码 O(nA)
#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;
}

本题还可以从枚举倍数的角度思考:对于一个数 i,如果它在原数组中出现了 ci 次,那么 i 这个数会对它的每一个倍数产生 ci 的贡献。

#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;
}

乍一看有上限到 106 的双重循环,但是实际上有这样一个数学性质:i=1nninlnn,因此上面这个算法的时间复杂度为 O(AlogA),由于换底公式,对数的底数不同可以忽略,不过不同于其他很多算法比如二分,在二分这样的算法中,对数的来源是折半,因此会涉及 log2,而本题这种情况下涉及的对数是自然对数 ln,这个复杂度俗称为调和级数复杂度。

调和级数复杂度的证明涉及到微积分,目前可以先当成结论记忆。感兴趣可以阅读:调和级数的复杂度

质数与合数

当寻找 2,3,5,7,11,13 这些数的约数时,可以发现它们除了 1 和自身以外没有其他的约数,这类数被称作质数。

设正整数 p0,1,如果它除了 1p 以外没有其他的约数,那么就称 p质数。若正整数 a01a 不是质数,则称 a合数

a 为合数,则 a 能被表示成 a=pq,其中 p,q>1。易证 p,q 中一定有一个不超过 a,因为如果 p,q 都超过 a,则会有 pq>a,矛盾。更严格地,若 a 为合数,则一定存在质数 p|a,且 pa

计算题:从 287 个整数中随机取 2 个不同的数,这两个数互质的概率为?

答案

8 个数中随机取 2 个不同的数有 C72=21 种情况,其中互质的情况包括:23,5,734,5,7,845,756,7,86778,因此概率为 3+4+2+3+1+121=23

例题:P3383 【模板】线性筛素数

给出若干(不超过 106)个正整数 k,依次输出第 k 小的素数,素数的范围是 108 以内。

判断一个整数是否为质数有一个时间复杂度为 O(n) 的算法:

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;
}

这里之所以枚举到 x,就是因为如果 x 是合数,则一定存在质数 p|x,且 px

这同时也给出了一个寻找一定范围内所有质数的算法。例如,为了求出不超过 100 的所有质数,只要把 1 和不超过 100 的正合数都删去。

只需要用不超过 10 的全部质数(2,3,5,7)找出,然后删去它们在 100 以内的所有倍数(2 的倍数,3 的倍数,5 的倍数,7 的倍数),就删去了所有 100 以内的合数,剩下的就是 100 以内的质数。

image

可以看出,没有删去的数是 2,3,5,7,,97,共 25 个质数。从这 25 个质数出发,还可以找出 100×100=10000 以内的所有质数。这个方法由古希腊数学家埃拉托斯特尼提出,称为埃拉托斯特尼筛,简称埃氏筛。

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);
}

埃氏筛的时间复杂度是 O(nlnlnn)

参考代码
#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;
}

既然质数的(大于 1 的)整数倍是合数,那么(大于 1 的)整数的质数倍数也一定是合数。就像 3412 是合数,那么 43 倍也一定是合数。于是可以得到埃氏筛的另外一种形式:

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倍是合数
}
}

在这个形式中,只有当 ji 时,i×j 才会被筛去。为什么这是正确的?考虑 j>i 的情况,设 i 的最小质因子是 pi,则 i×j 会在 i=j×ipi, j=pi 的时候被筛去。

在这个基础上,再添加一行代码,就可以得到欧拉筛,一种时间复杂度为 O(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; // 欧拉筛的核心
}
}

为什么增加这一句判断,就可以提前结束循环呢?设 n 的最小质因子为 pn,如 p9=3,p12=2。那么在线性筛中,一个合数 n 会且仅会在 i=npn 的时候被筛去。

首先证明充分性:合数 n 会在 i=npn 的时候被筛去。显然 npn 是大于等于 pn 的,否则 npn 的最小质因子一定比 pn 小,且它也是 n 的一个质因子,那么 n 的最小质因子就不是 pn 了。也就是说,当 i=npn 时,pn 一定已经被保存在 p 数组中。同时,如果 j 还没访问到 pn 就已经 break 了,就代表着有一个比 pn 更小的某个 j,它可以整除 i,也就是 i=npn 的一个质因子,那么这个比 pn 更小的质数 j 也一定是 n 的一个质因子,这也与 pnn 的最小质因子矛盾了。

然后证明必要性:合数 n 仅会在 i=npn 的时候被筛去。假设 p 是一个大于 pnn 的质因子,那么需要证明的是:当 i=np 的时候,在 j 枚举到 p 之前,第二层循环就已经 break 了。这其实也很简单:因为 ppn 互质,且同时是 n 的质因子,所以 pn 也一定是 np 的一个质因子。那么当 i=np 时,当 j 枚举到 pn 时,j 就整除 i 了,此时第二层循环就会 break,而不会让 j 再往后枚举到 p

也就是说,对于每一个合数,它只会被 flag[i*j]=true 这条语句标记为合数一次,也就是说这条语句实际上只会被执行不超过 n 次,那么整个算法的时间复杂度显然就是 O(n) 了。

参考代码
#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;
}

完善程序题:

好运的日期)一个日期可以用 xyz 日来表示。我们称一个日期是好运的,当且仅当 xy(wz+1) 为质数,其中 wxy 月的总天数。输入 x,y,z,判断其对应的日期是否好运。保证 x 是不超过 2024 的正整数,y 是不超过 12 的正整数,x,y,z 可以构成一个合法的日期。试补全线性筛法算法,空间限制 512MiB。

#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

答案

image

答案:ACDBD

例题:P1835 素数密度

询问 [L,R] (LR<231, RL106) 之间的质数个数。

分析:乍一看这题的数据范围非常大,只有 RL 的范围比较正常。虽然 L,R 可以很大,但理论上只要利用不超过 214748364750000 的质数就可以筛除题目范围内的所有合数。

事实上,再联系到 RL106 和筛法的流程,一个区间筛的算法大致成型:预先求出 50000 以内的所有质数(实际上不到 6000 个),再利用每个质数将 [L,R] 范围内该质数的倍数(不包括自己)筛除。最后统计一下没被筛除的就是质数了。

image

因为只需要利用 [L,R] 的数,所以代码中可以将这段区间平移到 [0,RL],从而节省空间。

参考代码
#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;
}
posted @   RonChen  阅读(116)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现
· 【杂谈】分布式事务——高大上的无用知识?
点击右上角即可分享
微信分享提示