#基础算法
1. 最大公约数和最小公倍数
【约数和倍数】
- 如果 a a a 除以 b b b 没有余数,那么 a a a 就是 b b b 的倍数, b b b 就是 a a a 的约数,记作 b ∣ a b \mid a b∣a。
约数,也称因数。
【最大公约数和最小公倍数】
-
最大公约数 (Greatest Common Divisor),常缩写为 gcd 。
一组整数的公约数,是指同时是这组数中每一个数的约数的数。
一组整数的最大公约数,是指所有公约数里面最大的一个。
-
最小公倍数 (Least Common Multiple),常缩写为 lcm 。
一组整数的公倍数,是指同时是这组数中每一个数的倍数的数。
一组整数的最小公倍数,是指所有正的公倍数里面,最小的一个数。
求两个数的 gcd 与 lcm 时,有如下性质:
- 对于两个数 a a a 和 b b b, gcd ( a , b ) × lcm ( a , b ) = a × b \gcd(a, b) \times \operatorname{lcm}(a, b) = a \times b gcd(a,b)×lcm(a,b)=a×b。也就是最大公约数乘以最小公倍数等于两个数的乘积。
因此,一般先求最大公约数,然后用这个性质求最小公倍数。
1.1 欧几里得算法
欧几里得算法也称辗转相除法,可以求出两个整数的最大公约数。
算法流程:
设 a > b a > b a>b:
-
如果 b b b 是 a a a 的约数,那么 b b b 就是两者的最大公约数;
-
如果 b b b 不是 a a a 的约数,那么 gcd ( a , b ) = gcd ( b , a m o d b ) \gcd(a, b) = \gcd(b, a \bmod b) gcd(a,b)=gcd(b,amodb)。
因为 a m o d b a \bmod b amodb 会不断减小,因此可以用递归进行求解。
代码实现:
cpp
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) gcd(a,b) 会遇到两种情况:
-
a < b a < b a<b,则 gcd ( a , b ) = gcd ( b , a ) \gcd(a, b) = \gcd(b, a) gcd(a,b)=gcd(b,a)
-
a > b a > b a>b,则 gcd ( a , b ) = gcd ( b , a m o d b ) \gcd(a, b) = \gcd(b, a \bmod b) gcd(a,b)=gcd(b,amodb)
第二种情况会让 a a a 至少折半,因此最多执行 log n \log n logn 次。第一种情况不会多于第二种,因此时间复杂度为 O ( log n ) O(\log n) O(logn)。
1.1.1 例题
输入三个正整数 x , y , z x, y, z x,y,z,求它们的最大公约数 g g g:最大的正整数 g ≥ 1 g \ge 1 g≥1,满足 x , y , z x, y, z x,y,z 都是 g g g 的倍数,即 ( x m o d g ) = ( y m o d g ) = ( z m o d g ) = 0 (x \bmod g) = (y \bmod g) = (z \bmod g) = 0 (xmodg)=(ymodg)=(zmodg)=0。
输入:12 34 56
输出:2
cpp
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 秦九韶算法
秦九韶算法是一种将一元 n n n 次多项式的求值问题转化为 n n n 个一次式的算法。其大大简化了计算过程,即使在现代,利用计算机解决多项式的求值问题时,秦九韶算法依然是最优的算法。
一个 n n n 次多项式:
f ( x ) = a n x n − 1 + a n − 1 x n − 2 + ⋯ + a 1 x + a 0 f(x) = a_n x^{n-1} + a_{n-1} x^{n-2} + \dots + a_1 x + a_0 f(x)=anxn−1+an−1xn−2+⋯+a1x+a0
可以改写成:
f ( x ) = ( a n x n − 2 + a n − 1 x n − 3 + ⋯ + a 1 ) x + a 0 f(x) = (a_n x^{n-2} + a_{n-1} x^{n-3} + \dots + a_1)x + a_0 f(x)=(anxn−2+an−1xn−3+⋯+a1)x+a0
= ( ( a n x n − 3 + a n − 1 x n − 4 + ⋯ + a 2 ) x + a 1 ) x + a 0 = ((a_n x^{n-3} + a_{n-1} x^{n-4} + \dots + a_2)x + a_1)x + a_0 =((anxn−3+an−1xn−4+⋯+a2)x+a1)x+a0
= ( ... ( ( a n x + a n − 1 ) x + a n − 2 ) x + ⋯ + a 2 ) x + a 1 ) x + a 0 = (\dots((a_n x + a_{n-1})x + a_{n-2})x + \dots + a_2)x + a_1)x + a_0 =(...((anx+an−1)x+an−2)x+⋯+a2)x+a1)x+a0
例如:对于一个整数 987654321 987654321 987654321,可以拆成:
( ( ( ( ( ( ( ( 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 ((((((((9×10+8)×10+7)×10+6)×10+5)×10+4)×10+3)×10+2)×10+1
这样对于高精度的数取模,就可以分阶段取模。
1.2.1 例题
牛客>小红的gcb
给两个正整数 a , b a, b a,b,输出他们的最大公约数 gcd ( a , b ) \gcd(a, b) gcd(a,b)。
l e n len len 表示 a a a 的十进制位数, 1 ≤ l e n ≤ 10 6 1 \le len \le 10^6 1≤len≤106, 1 ≤ b ≤ 10 9 1 \le b \le 10^9 1≤b≤109。
【解法】
先将大数取模,然后再代入公式计算。
cpp
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. 质数的判定
【质数和合数】
- 一个大于 1 1 1 的自然数,除了 1 1 1 和它自身外,不能被其他自然数整除的数叫做质数 ;否则称为合数 。其中,质数又称素数 。
规定 1 1 1 既不是质数也不是合数。
2.1 试除法判断质数
- 对于一个数 x x x,根据定义,可以从 [ 2 , x − 1 ] [2, x-1] [2,x−1] 一个一个尝试,判断 x x x 能否被整除。
但是,没有必要每一个都去判断。因为如果 a a a 是 x x x 的约数,那么 x / a x/a x/a 也是 x x x 的约数。因此,我们仅需判断较小的 a a a 是否是 x x x 的约数,没有必要再去考虑 x / a x/a x/a。那么,仅需枚举到 x \sqrt{x} x 即可。
代码实现:
cpp
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} n ,因此时间复杂度为 O ( N ) O(\sqrt{N}) O(N )
3. 筛质数
上一个专题学习了如何判断一个数是否是质数,如果此时想知道 [ 1 , n ] [1, n] [1,n] 中有多少个素数呢?或者是 [ 1 , n ] [1, n] [1,n] 中的素数里面,第 k k k 个素数是多少?
- 一个自然的想法就是从 2 2 2 开始,依次向后对每一个自然数进行一次质数检验。
但是这种解法相对暴力,这里介绍两种方法,能够快速地将 [ 1 , n ] [1, n] [1,n] 中的素数全部记录下来。
3.1 埃氏筛法
算法思想:
- 对于任意一个大于 1 1 1 的正整数,那么它的 k k k( k > 1 k > 1 k>1)倍就是合数。
因此,如果我们从小到大考虑每个数,然后同时把当前这个数的所有倍数记为合数,没有被标记的数就是素数。
小优化:
- 找到一个质数 x x x 之后,可以从该数的 x x x 倍向后筛,因此小于 x x x 的倍数一定被之前筛过了。
时间复杂度 :
埃氏筛的时间复杂度为 O ( n log log n ) O(n \log \log n) O(nloglogn)。
代码实现:
cpp
#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 ) O(n) O(n)。
代码实现:
cpp
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 例题
【题目描述】
哥德巴赫猜想的内容如下:
任意一个大于 4 4 4 的偶数都可以拆成两个奇质数之和。
比如:
8 = 3 + 5 8 = 3 + 5 8=3+5
20 = 3 + 17 = 7 + 13 20 = 3 + 17 = 7 + 13 20=3+17=7+13
42 = 5 + 37 = 11 + 31 = 13 + 29 = 19 + 23 42 = 5 + 37 = 11 + 31 = 13 + 29 = 19 + 23 42=5+37=11+31=13+29=19+23
你的任务是:验证小于 10 6 10^6 106 的数满足哥德巴赫猜想。
【输入描述】
输入包含多组数据。每组数据占一行,包含一个偶数 n n n。读入以 0 0 0 结束。
【输出描述】
对于每组数据,输出形如 n = a + b n = a + b n=a+b,其中 a , b a, b a,b 是奇素数。若有多组满足条件的 a , b a, b a,b,输出 b − a b - a b−a 最大的一组。若无解,输出 Goldbach's conjecture is wrong.。
输入:
8
20
42
0
输出:
8 = 3 + 5
20 = 3 + 17
42 = 5 + 37
代码:
cpp
#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. 算术基本定理
【算术基本定理】
算术基本定理又称唯一分解定理:
- 任何一个大于 1 1 1 的自然数 n n n,都可以唯一分解成有限个质数的乘积
n = p 1 α 1 × p 2 α 2 × p 3 α 3 × ⋯ × p k α k n = p_1^{\alpha_1} \times p_2^{\alpha_2} \times p_3^{\alpha_3} \times \dots \times p_k^{\alpha_k} n=p1α1×p2α2×p3α3×⋯×pkαk;
这里 p 1 < p 2 < p 3 < ⋯ < p k p_1 < p_2 < p_3 < \dots < p_k p1<p2<p3<⋯<pk 均为质数,其中指数 α i \alpha_i αi 是正整数。这样的分解称为 n n n 的标准分解式。
4.1 分解质因数
- 分解质因数就是将一个合数用质因数相乘的形式表示出来,例如 360 = 2 3 × 3 2 × 5 1 360 = 2^3 \times 3^2 \times 5^1 360=23×32×51。
4.1.1 试除法分解质因数
- n n n 的所有因数中,不会有两个大于 n \sqrt{n} n 。
因此,枚举 [ 2 , n ] [2, \sqrt{n}] [2,n ] 中所有的数,如果能整除 n n n 就一直除下去。如果最后剩下的数大于 1 1 1,那就是大于 n \sqrt{n} n 的因子。
小优化:
- 如果我们提前打出来一个素数表,枚举所有素数的话,时间复杂度能进一步降低。
时间复杂度 :
枚举到 n \sqrt{n} n ,因此时间复杂度为 O ( N ) O(\sqrt{N}) O(N )。但是最优情况下会达到 O ( log n ) O(\log n) O(logn)。
代码实现:
cpp
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! N! 进行质因子分解。
【输入描述】
输入数据仅有一行包含一个正整数 N N N, N ≤ 10000 N \le 10000 N≤10000。
【输出描述】
输出数据包含若干行,每行两个正整数 p , a p, a p,a,中间用一个空格隔开。表示 N ! N! N! 包含 a a a 个质因子 p p p,要求按 p p p 的值从小到大输出。
输入:
10
输出:
2 8
3 4
5 2
7 1
解法:
对阶乘中的每一个数,分解质因数。
参考代码:
cpp
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 求一个整数的所有约数 - 试除法
对于一个整数 x x x,若 d d d 是 x x x 的约数,那么 x / d x/d x/d 也是 x x x 的约数。也就是说,除了完全平方数,约数都是成对出现的。因此,可以用试除法求一个整数的所有约数。
枚举 1 ∼ x 1 \sim \sqrt{x} 1∼x 之间的整数,判断是否能整除 x x x。
试除法也能求出一个整数的约数个数以及约数总和。
时间复杂度 :
枚举到 n \sqrt{n} n ,因此时间复杂度为 O ( N ) O(\sqrt{N}) O(N )。
因此,一个整数 n n n 的约数个数的上限为 2 n 2\sqrt{n} 2n 。
代码实现:
cpp
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 求一个整数的所有约数 - 倍数法
如果用试除法分别求每一个数的约数,时间复杂度过高。
可以反过来想,对于每个数 d d d, [ 1 , n ] [1, n] [1,n] 中以 d d d 为约数的数就是 d d d 的倍数,也就是 d , 2 d , 3 d , ... , ⌊ n / d ⌋ ⋅ d d, 2d, 3d, \dots, \lfloor n/d \rfloor \cdot d d,2d,3d,...,⌊n/d⌋⋅d。
因此可以用倍数法求出 [ 1 , n ] [1, n] [1,n] 每个数的约数集合。
时间复杂度 :
n n n 倍的调和数,约为 O ( n log n ) O(n \log n) O(nlogn)。
代码实现:
cpp
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 = p 1 α 1 × p 2 α 2 × p 3 α 3 × ⋯ × p k α k n = p_1^{\alpha_1} \times p_2^{\alpha_2} \times p_3^{\alpha_3} \times \dots \times p_k^{\alpha_k} n=p1α1×p2α2×p3α3×⋯×pkαk。
若用 d ( n ) d(n) d(n) 表示某一个数约数的个数,那么有:
d ( n ) = ( α 1 + 1 ) ( α 2 + 1 ) ... ( α k + 1 ) d(n) = (\alpha_1 + 1)(\alpha_2 + 1)\dots(\alpha_k + 1) d(n)=(α1+1)(α2+1)...(αk+1),也就是所有系数加一之后的乘积。
5.2.1 求单个数的约数个数方法
-
第一种方式:枚举 1 ∼ x 1 \sim \sqrt{x} 1∼x 之间的整数;
-
第二种方式:在分解质因数的过程中,利用公式可以直接计算出某个数的约数个数。
5.3约数和定理
由唯一分解定理得: n = p 1 α 1 × p 2 α 2 × p 3 α 3 × ⋯ × p k α k n = p_1^{\alpha_1} \times p_2^{\alpha_2} \times p_3^{\alpha_3} \times \dots \times p_k^{\alpha_k} n=p1α1×p2α2×p3α3×⋯×pkαk。
若用 f ( n ) f(n) f(n) 表示某一个数约数和,那么有:
f ( n ) = ∏ i = 1 k p i α i + 1 − 1 p i − 1 f(n) = \prod_{i=1}^{k} \frac{p_i^{\alpha_i+1} - 1}{p_i - 1} f(n)=∏i=1kpi−1piαi+1−1。
5.3.1 求单个数的约数之和方法
-
第一种方式:枚举 1 ∼ x 1 \sim \sqrt{x} 1∼x 之间的整数;
-
第二种方式:在分解质因数的过程中,利用公式可以直接计算出某个数的约数总和。
5.3.2 例题
求自然数 N N N 的所有约数之和。
输入描述:
输入一行,包含一个正整数 n n n,范围在 10000 10000 10000 以内。
输出描述:
输出一行,包含一个整数。
输入:10
输出:18
CPP
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 例题
【题目描述】
给个 n n n,求 1 1 1 到 n n n 的所有数的约数个数的和。
【输入描述】
第一行一个正整数 n n n。
【输出描述】
输出一个整数,表示答案。
输入:3
输出:5
【解法】
正难则反。
求每个数的约数比较麻烦,对于一个数 x x x,在 [ 1 , n ] [1, n] [1,n] 中,一共有 ⌊ n / x ⌋ \lfloor n/x \rfloor ⌊n/x⌋ 个数,里面的约数有 x x x。因此可以反着求。
cpp
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 }