数论

4065 字
20 分钟
数论

1. 最大公约数和最小公倍数#

【约数和倍数】

  • 如果 aa 除以 bb 没有余数,那么 aa 就是 bb 的倍数,bb 就是 aa 的约数,记作 b∣ab \mid a。 约数,也称因数。

【最大公约数和最小公倍数】

  • 最大公约数(Greatest Common Divisor),常缩写为 gcd。 一组整数的公约数,是指同时是这组数中每一个数的约数的数。 一组整数的最大公约数,是指所有公约数里面最大的一个。

  • 最小公倍数(Least Common Multiple),常缩写为 lcm。 一组整数的公倍数,是指同时是这组数中每一个数的倍数的数。 一组整数的最小公倍数,是指所有正的公倍数里面,最小的一个数。

求两个数的 gcd 与 lcm 时,有如下性质:

  • 对于两个数 aa 和 bb,gcd⁡(a,b)×lcm⁡(a,b)=a×b\gcd(a, b) \times \operatorname{lcm}(a, b) = a \times b。也就是最大公约数乘以最小公倍数等于两个数的乘积。 因此,一般先求最大公约数,然后用这个性质求最小公倍数。

1.1 欧几里得算法#

欧几里得算法也称辗转相除法,可以求出两个整数的最大公约数。

算法流程: 设 a>ba > b:

  • 如果 bb 是 aa 的约数,那么 bb 就是两者的最大公约数;

  • 如果 bb 不是 aa 的约数,那么 gcd⁡(a,b)=gcd⁡(b,a mod b)\gcd(a, b) = \gcd(b, a \bmod b)。

因为 a mod ba \bmod b 会不断减小,因此可以用递归进行求解。

代码实现:

1 LL gcd(LL a, LL b)
2 {
3 if (!b) return a; // 如果 b 等于 0,说明 a 就是最大公约数
4 return gcd(b, a % b);
5 }

时间复杂度: 求 gcd⁡(a,b)\gcd(a, b) 会遇到两种情况:

  1. a<ba < b,则 gcd⁡(a,b)=gcd⁡(b,a)\gcd(a, b) = \gcd(b, a)

  2. a>ba > b,则 gcd⁡(a,b)=gcd⁡(b,a mod b)\gcd(a, b) = \gcd(b, a \bmod b)

第二种情况会让 aa 至少折半,因此最多执行 log⁡n\log n 次。第一种情况不会多于第二种,因此时间复杂度为 O(log⁡n)O(\log n)。

1.1.1 例题#

B3736 [信息与未来 2018] 最大公约数 输入三个正整数 x,y,zx, y, z,求它们的最大公约数 gg:最大的正整数 g≥1g \ge 1,满足 x,y,zx, y, z 都是 gg 的倍数,即 (x mod g)=(y mod g)=(z mod g)=0(x \bmod g) = (y \bmod g) = (z \bmod g) = 0。 输入:12 34 56 输出:2

1 #include <iostream>
2
3 using namespace std;
4
5 int gcd(int a, int b)
6 {
7 return b == 0 ? a : gcd(b, a % b);
8 }
9
10 int main()
11 {
12 int x, y, z; cin >> x >> y >> z;
13
14 cout << gcd(gcd(x, y), z) << endl;
15
16 return 0;
17 }

1.2 秦九韶算法#

秦九韶算法是一种将一元 nn 次多项式的求值问题转化为 nn 个一次式的算法。其大大简化了计算过程,即使在现代,利用计算机解决多项式的求值问题时,秦九韶算法依然是最优的算法。

一个 nn 次多项式: f(x)=anxn−1+an−1xn−2+⋯+a1x+a0f(x) = a_n x^{n-1} + a_{n-1} x^{n-2} + \dots + a_1 x + a_0 可以改写成: f(x)=(anxn−2+an−1xn−3+⋯+a1)x+a0f(x) = (a_n x^{n-2} + a_{n-1} x^{n-3} + \dots + a_1)x + a_0 =((anxn−3+an−1xn−4+⋯+a2)x+a1)x+a0= ((a_n x^{n-3} + a_{n-1} x^{n-4} + \dots + a_2)x + a_1)x + a_0 =(…((anx+an−1)x+an−2)x+⋯+a2)x+a1)x+a0= (\dots((a_n x + a_{n-1})x + a_{n-2})x + \dots + a_2)x + a_1)x + a_0

例如:对于一个整数 987654321987654321,可以拆成: ((((((((9×10+8)×10+7)×10+6)×10+5)×10+4)×10+3)×10+2)×10+1((((((((9 \times 10 + 8) \times 10 + 7) \times 10 + 6) \times 10 + 5) \times 10 + 4) \times 10 + 3) \times 10 + 2) \times 10 + 1

这样对于高精度的数取模,就可以分阶段取模。

1.2.1 例题#

牛客>小红的gcb 给两个正整数 a,ba, b,输出他们的最大公约数 gcd⁡(a,b)\gcd(a, b)。 lenlen 表示 aa 的十进制位数,1≤len≤1061 \le len \le 10^6,1≤b≤1091 \le b \le 10^9。

【解法】 先将大数取模,然后再代入公式计算。

1 #include <iostream>
2
3 using namespace std;
4
5 string a;
6 int b;
7
8 int gcd(int a, int b)
9 {
10 return b == 0 ? a : gcd(b, a % b);
11 }
12
13 int calc()
14 {
15 long long t = 0;
16 for (auto ch : a)
17 {
18 t = t * 10 + ch - '0';
19 t %= b;
20 }
21 return t;
22 }
23
24 int main()
25 {
26 cin >> a >> b;
27
28 cout << gcd(b, calc()) << endl;
29
30 return 0;
31 }

2. 质数的判定#

【质数和合数】

  • 一个大于 11 的自然数,除了 11 和它自身外,不能被其他自然数整除的数叫做质数;否则称为合数。其中,质数又称素数。 规定 11 既不是质数也不是合数。

2.1 试除法判断质数#

  • 对于一个数 xx,根据定义,可以从 [2,x−1][2, x-1] 一个一个尝试,判断 xx 能否被整除。 但是,没有必要每一个都去判断。因为如果 aa 是 xx 的约数,那么 x/ax/a 也是 xx 的约数。因此,我们仅需判断较小的 aa 是否是 xx 的约数,没有必要再去考虑 x/ax/a。那么,仅需枚举到 x\sqrt{x} 即可。

代码实现:

1 bool isprime(int x)
2 {
3 if (x <= 1) return false; // 小于等于1的数不考虑
4
5 // 试除法判断是否是质数 - 只需枚举到 sqrt(x)
6 for (int i = 2; i <= x / i; i++) // 防溢出的写法
7 {
8 if (x % i == 0) return false;
9 }
10 return true;
11 }

时间复杂度: 枚举到 n\sqrt{n},因此时间复杂度为 O(N)O(\sqrt{N})

3. 筛质数#

上一个专题学习了如何判断一个数是否是质数,如果此时想知道 [1,n][1, n] 中有多少个素数呢?或者是 [1,n][1, n] 中的素数里面,第 kk 个素数是多少?

  • 一个自然的想法就是从 22 开始,依次向后对每一个自然数进行一次质数检验。

但是这种解法相对暴力,这里介绍两种方法,能够快速地将 [1,n][1, n] 中的素数全部记录下来。

3.1 埃氏筛法#

算法思想:

  • 对于任意一个大于 11 的正整数,那么它的 kk(k>1k > 1)倍就是合数。 因此,如果我们从小到大考虑每个数,然后同时把当前这个数的所有倍数记为合数,没有被标记的数就是素数。

小优化:

  • 找到一个质数 xx 之后,可以从该数的 xx 倍向后筛,因此小于 xx 的倍数一定被之前筛过了。

时间复杂度: 埃氏筛的时间复杂度为 O(nlog⁡log⁡n)O(n \log \log n)。

代码实现:

#include <iostream>
using namespace std;
typedef long long LL;
const int N = 1000010;
bool st[N]; // 当前这个数有没有被筛掉(true表示被筛掉,不是质数)
int p[N]; // 记录质数
int cnt; // 统计质数个数
int n; // 求1~n范围内的质数
void get_prime()
{
// 初始化:0和1不是质数
st[0] = st[1] = true;
for (LL i = 2; i <= n; i++)
{
if (!st[i]) // 没有被标记,说明是质数
{
p[++cnt] = i; // 记录这个质数
// 从 i*i 开始,因为小于 i 的倍数已经被之前的质数筛掉了
// 注意:i*i 可能会溢出,所以要用 long long
for (LL j = i * i; j <= n; j += i) // 筛掉这个质数的倍数
st[j] = true;
}
}
}
int main()
{
cin >> n;
get_prime();
// 输出质数
cout << "2~" << n << "之间的质数有:" << endl;
for (int i = 1; i <= cnt; i++)
cout << p[i] << " ";
cout << endl;
cout << "共有 " << cnt << " 个" << endl;
return 0;
}

3.2 第二种筛法:线性筛法#

线性筛法,又称欧拉筛法。算法思想:

  • 在埃氏筛法中,它会将一个合数重复多次标记。如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 O(n)O(n) 了。

  • 我们的做法是,让每一个合数被它的最小质因数筛掉。

时间复杂度: 每个数只会被自身最小的质因数筛掉一次,时间复杂度为 O(n)O(n)。

代码实现:

1 int n, q;
2 bool st[N];
3 int p[N], cnt;
4
5 // 欧拉筛
6 void get_prime()
7 {
8 for (int i = 2; i <= n; i++)
9 {
10 if (!st[i]) p[++cnt] = i; // 如果没标记过,就是质数
11
12 // 枚举所有的质数
13 for (int j = 1; 1ll * i * p[j] <= n; j++)
14 {
15 st[i * p[j]] = true;
16 if (i % p[j] == 0) break;
17 // 这个判定条件能让每一个合数被自己的最小质因数筛掉。
18 // 1. 如果 i 是合数,枚举到最小质因数的时候跳出循环
19 // 2. 如果 i 是质数,枚举到自身时跳出循环
20 // 注意,在筛的过程中,我们还能知道 p[j] 是 i 的最小质因数
21 }
22 }
23 }

3.3 例题#

UVA543 Goldbach’s Conjecture 【题目描述】 哥德巴赫猜想的内容如下: 任意一个大于 44 的偶数都可以拆成两个奇质数之和。 比如: 8=3+58 = 3 + 5 20=3+17=7+1320 = 3 + 17 = 7 + 13 42=5+37=11+31=13+29=19+2342 = 5 + 37 = 11 + 31 = 13 + 29 = 19 + 23 你的任务是:验证小于 10610^6 的数满足哥德巴赫猜想。

【输入描述】 输入包含多组数据。每组数据占一行,包含一个偶数 nn。读入以 00 结束。

【输出描述】 对于每组数据,输出形如 n=a+bn = a + b,其中 a,ba, b 是奇素数。若有多组满足条件的 a,ba, b,输出 b−ab - a 最大的一组。若无解,输出 Goldbach's conjecture is wrong.。 输入:

8
20
42
0

输出:

8 = 3 + 5
20 = 3 + 17
42 = 5 + 37

代码:

#include <iostream>
using namespace std;
const int N = 1e6 + 10;
bool st[N];
int p[N], cnt;
void get_prime()
{
int n = 1e6;
for (int i = 2; i <= n; i++)
{
if (!st[i]) p[++cnt] = i;
for (int j = 1; 1ll * i * p[j] <= n; j++)
{
st[i * p[j]] = true;
if (i % p[j] == 0) break;
}
}
}
void solve(int x)
{
for (int i = 2; i <= cnt; i++)
{
if (!st[x - p[i]])
{
printf("%d = %d + %d\n", x, p[i], x - p[i]);
break;
}
}
}
int main()
{
get_prime();
int x;
while (cin >> x, x)
{
solve(x);
}
return 0;
}

4. 算术基本定理#

【算术基本定理】 算术基本定理又称唯一分解定理:

  • 任何一个大于 11 的自然数 nn,都可以唯一分解成有限个质数的乘积 n=p1α1×p2α2×p3α3×⋯×pkαkn = p_1^{\alpha_1} \times p_2^{\alpha_2} \times p_3^{\alpha_3} \times \dots \times p_k^{\alpha_k}; 这里 p1<p2<p3<⋯<pkp_1 < p_2 < p_3 < \dots < p_k 均为质数,其中指数 αi\alpha_i 是正整数。这样的分解称为 nn 的标准分解式。

4.1 分解质因数#

  • 分解质因数就是将一个合数用质因数相乘的形式表示出来,例如 360=23×32×51360 = 2^3 \times 3^2 \times 5^1。

4.1.1 试除法分解质因数#

  • nn 的所有因数中,不会有两个大于 n\sqrt{n}。 因此,枚举 [2,n][2, \sqrt{n}] 中所有的数,如果能整除 nn 就一直除下去。如果最后剩下的数大于 11,那就是大于 n\sqrt{n} 的因子。

小优化:

  • 如果我们提前打出来一个素数表,枚举所有素数的话,时间复杂度能进一步降低。

时间复杂度: 枚举到 n\sqrt{n},因此时间复杂度为 O(N)O(\sqrt{N})。但是最优情况下会达到 O(log⁡n)O(\log n)。

代码实现:

1 int c[N]; // c[i] 表示 i 这个质数出现的次数
2
3 void deprime(int x)
4 {
5 for (int i = 2; i <= x / i; i++)
6 {
7 int cnt = 0;
8 while (x % i == 0) // 只要有这个因子,就除尽,并且计数
9 {
10 x /= i;
11 cnt++;
12 }
13 c[i] += cnt;
14 }
15 if (x > 1) c[x]++; // 不要忘记判断最后一个质数
16 }

4.1.2 例题#

题目链接:P2043 质因子分解

【题目描述】 对 N!N! 进行质因子分解。

【输入描述】 输入数据仅有一行包含一个正整数 NN,N≤10000N \le 10000。

【输出描述】 输出数据包含若干行,每行两个正整数 p,ap, a,中间用一个空格隔开。表示 N!N! 包含 aa 个质因子 pp,要求按 pp 的值从小到大输出。

输入:

10

输出:

2 8
3 4
5 2
7 1

解法: 对阶乘中的每一个数,分解质因数。

参考代码:

1 #include <iostream>
2
3 using namespace std;
4
5 const int N = 1e4 + 10;
6
7 int n;
8 int c[N];
9
10 // 试除法分解质因数
11 void deprime(int x)
12 {
13 for (int i = 2; i <= x / i; i++)
14 {
15 int cnt = 0;
16 while (x % i == 0)
17 {
18 cnt++;
19 x /= i;
20 }
21 c[i] += cnt;
22 }
23 // 注意判断最后一个数
24 if (x > 1) c[x]++;
25 }
26
27 int main()
28 {
29 cin >> n;
30
31 for (int i = 2; i <= n; i++)
32 {
33 deprime(i);
34 }
35
36 for (int i = 2; i <= n; i++)
37 {
38 if (c[i])
39 {
40 cout << i << " " << c[i] << endl;
41 }
42 }
43
44 return 0;
45 }

5. 约数#

5.1 求一个整数的所有约数 - 试除法#

对于一个整数 xx,若 dd 是 xx 的约数,那么 x/dx/d 也是 xx 的约数。也就是说,除了完全平方数,约数都是成对出现的。因此,可以用试除法求一个整数的所有约数。 枚举 1∼x1 \sim \sqrt{x} 之间的整数,判断是否能整除 xx。

试除法也能求出一个整数的约数个数以及约数总和。

时间复杂度: 枚举到 n\sqrt{n},因此时间复杂度为 O(N)O(\sqrt{N})。 因此,一个整数 nn 的约数个数的上限为 2n2\sqrt{n}。

代码实现:

1 int d[N], cnt;
2
3 void get_d(int x)
4 {
5 // 注意从 1 开始循环
6 for (int i = 1; i <= x / i; i++)
7 {
8 if (x % i == 0)
9 {
10 d[++cnt] = i;
11 if (i != x / i) d[++cnt] = x / i;
12 }
13 }
14 }

5.1.2 求一个整数的所有约数 - 倍数法#

如果用试除法分别求每一个数的约数,时间复杂度过高。 可以反过来想,对于每个数 dd,[1,n][1, n] 中以 dd 为约数的数就是 dd 的倍数,也就是 d,2d,3d,…,⌊n/d⌋⋅dd, 2d, 3d, \dots, \lfloor n/d \rfloor \cdot d。 因此可以用倍数法求出 [1,n][1, n] 每个数的约数集合。

时间复杂度: nn 倍的调和数,约为 O(nlog⁡n)O(n \log n)。

代码实现:

1 int n;
2 vector<int> d[N];
3
4 void get_d()
5 {
6 for (int i = 1; i <= n; i++) // 枚举所有约数
7 for (int j = 1; j <= n / i; j++) // 约数的倍数
8 d[i * j].push_back(i);
9 }

5.2 约数个数定理#

由唯一分解定理得:n=p1α1×p2α2×p3α3×⋯×pkαkn = p_1^{\alpha_1} \times p_2^{\alpha_2} \times p_3^{\alpha_3} \times \dots \times p_k^{\alpha_k}。 若用 d(n)d(n) 表示某一个数约数的个数,那么有: d(n)=(α1+1)(α2+1)…(αk+1)d(n) = (\alpha_1 + 1)(\alpha_2 + 1)\dots(\alpha_k + 1),也就是所有系数加一之后的乘积。

5.2.1 求单个数的约数个数方法#

  • 第一种方式:枚举 1∼x1 \sim \sqrt{x} 之间的整数;

  • 第二种方式:在分解质因数的过程中,利用公式可以直接计算出某个数的约数个数。

5.3约数和定理#

由唯一分解定理得:n=p1α1×p2α2×p3α3×⋯×pkαkn = p_1^{\alpha_1} \times p_2^{\alpha_2} \times p_3^{\alpha_3} \times \dots \times p_k^{\alpha_k}。 若用 f(n)f(n) 表示某一个数约数和,那么有: f(n)=∏i=1kpiαi+1−1pi−1f(n) = \prod_{i=1}^{k} \frac{p_i^{\alpha_i+1} - 1}{p_i - 1}。

5.3.1 求单个数的约数之和方法#

  • 第一种方式:枚举 1∼x1 \sim \sqrt{x} 之间的整数;

  • 第二种方式:在分解质因数的过程中,利用公式可以直接计算出某个数的约数总和。

5.3.2 例题#

求自然数 NN 的所有约数之和。

输入描述: 输入一行,包含一个正整数 nn,范围在 1000010000 以内。

输出描述: 输出一行,包含一个整数。

输入:10 输出:18

1 #include <iostream>
2
3 using namespace std;
4
5 int main()
6 {
7 int n; cin >> n;
8 int sum = 0;
9
10 for (int i = 1; i <= n / i; i++)
11 {
12 if (n % i == 0)
13 {
14 sum += i;
15 if (i != n / i) sum += n / i;
16 }
17 }
18
19 cout << sum << endl;
20
21 return 0;
22 }

5.3.3 例题#

【题目描述】 给个 nn,求 11 到 nn 的所有数的约数个数的和。

【输入描述】 第一行一个正整数 nn。

【输出描述】 输出一个整数,表示答案。

输入:3 输出:5

【解法】 正难则反。 求每个数的约数比较麻烦,对于一个数 xx,在 [1,n][1, n] 中,一共有 ⌊n/x⌋\lfloor n/x \rfloor 个数,里面的约数有 xx。因此可以反着求。

1 #include <iostream>
2
3 using namespace std;
4
5 int main()
6 {
7 int n; cin >> n;
8
9 long long sum = 0;
10 for (int i = 1; i <= n / 2; i++) sum += n / i;
11 sum += n - n / 2;
12
13 cout << sum << endl;
14
15 return 0;
16 }

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Profile Image of the Author
HandlerOne
おかえり
公告
本站已整理 138 篇技术笔记,并按 8 条主线排成学习路线(141 站);站内搜索与桌宠对话均已上线。
分类
标签
最新动态
站点统计
文章
138
分类
12
标签
42
总字数
597,044
站点信息
构建平台
腾讯云 · Nginx
博客版本
v6.16.8
文章目录