目录
一、数论
1 质数
定义: 在所有大于1
的自然数,如果只包含1
和本身这两个约数,就被称为质数,或者叫做素数。
1.1 判定质数——试除法
时间复杂度: O ( n ) O(n) O(n)
方法一:定义法
bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i < n; i++)
if (n % i == 0) return false;
return true;
}
方法二:对定义法进行优化
时间复杂度: O ( n ) O(\sqrt{n}) O(n)
对于整数n
,可以发现n
的约数都是成对出现的,比如n = 12
,<2, 6>
与<3, 4>
都是12
的约数,且成对出现,所以每次枚举仅枚举每对约数中较小的一个,即判断条件修改为i <= n / i
即可。
bool is_prime(int n) {
if (n < 2) return false;
for (int i = 2; i <= n / i; i++)
if (n % i == 0) return false;
return true;
}
注意两个细节:
- 条件写成
i <= sqrt(n)
不好,因为函数sqrt
计算很慢; - 条件写成
i * i <= n
不好,因为这里的i * i
当(i+1)*(i+1)
时会存在溢出风险,这是i
会变成一个负数影响结果。
1.2 分解质因数——试除法
因数与约数:
- 数域不同。约数只能是自然数(即0,1,2,3…);而因数可以是任何数,当然也可以是小数;
- 关系不同。约数是对两个自然数的整除关系而言,只要两个数是自然数,就能确定它们之间是否存在约数关系,如:40÷5=8,40能被5整除,5就是40的约数,12÷10=1.2,12不能被10整除,10不是12的约数。因数是两个或两个以上的数对它们的乘积关系而言的。如:8×2=16,8和2都是积16的因数,离开乘积算式就没有因数了;
- 大小关系不同。当数a是数b的约数时,a不能大于b,当a是b的因数时,a可以大于b,也可以小于b。例如,5可以是10的约数,也可以是10的因数;但20只能是10的约数。
合数:
合数是指在大于1的整数中除了能被1和本身整除外,还能被其他数(0除外)整除的数。与之相对的是质数,而1既不属于质数也不属于合数。最小的合数是4。
分解质因数:
每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数,把一个合数用质因数相乘的形式表示出来,叫做分解质因数。如30=2×3×5 。分解质因数只针对合数。
方法一:定义法(会TLE)
时间复杂度: O ( n ) O(n) O(n)
void divide(int n) {
for (int i = 2; i <= n; i++) {
if (n % i == 0) {
int s = 0;
while (n % i == 0) {
n /= i;
s++;
}
printf("%d %d\n", i, s);
}
}
puts("");
}
注:这个代码中有个细节
每一次for
循环的时候,我们都将2 ~ n
所有的数(包括质数和非质数)都遍历了,这会有问题么?没有,注意代码中的while
循环,因为任意一个合数可以表示成几个质数的乘积。证明一下循环里面的 i
一定是一个质数:假如 i
是一个合数,那么它一定可以分解成多个质因子相乘的形式,这多个质因子同时也是 n
的质因子且比 i
要小,而比 i
小的数在之前的循环过程中一定是被条件除完了的,所以 i
不可能是合数,只可能是质数。
方法二:对定义法进行优化
时间复杂度: O ( n ) O(\sqrt{n}) O(n)
重要性质:n 中最多只包含一个大于 n \sqrt{n} n 的质因子。证明通过反证法:如果有两个大于 n \sqrt{n} n的因子,那么相乘会大于n,矛盾。
故如果遍历完后n > 1
,则n就是n中那个大于sqrt(n)
的质因子
void divide(int n) {
for (int i = 2; i <= n / i; i++) {
if (n % i == 0) {
int s = 0;
while (n % i == 0) {
n /= i;
s++;
}
printf("%d %d\n", i, s);
}
}
if (n > 1) // 如果n大于1,则n就是n中那个大于sqrt(n)的质因子
printf("%d %d\n", n, 1);
puts("");
}
1.2.1 阶乘分解
算法思路:
- 先筛除 [ 1 , 1 0 6 ] [1,10^6] [1,106]中的所有质数,由质数定理可得 [ 1 , n ] [1,n] [1,n]之间的质数个数为 n l n n \frac{n}{ln^n} lnnn,做一下放缩 n l n n > n l o g 2 n \frac{n}{ln^n} > \frac{n}{log_2^n} lnnn>log2nn。当 n = 1 0 6 n = 10^6 n=106的时候, l o g 2 1 0 6 ≈ 20 log_2^{10^6} \approx 20 log2106≈20,所以 n l o g 2 n ≈ 50000 \frac{n}{log_2^n} \approx 50000 log2nn≈50000,因此质数大约 50000 50000 50000个。
- 对每个质数 p p p的次数。首先分别求出 [ 1 , n ] [1, n] [1,n]中 p 、 p 2 、 p 3 . . . p、p^2、p^3... p、p2、p3...的倍数(直到 p u > n p^u > n pu>n),有 ⌊ n p ⌋ + ⌊ n p 2 ⌋ + ⌊ n p 3 ⌋ . . . \left \lfloor \frac{n}{p} \right \rfloor + \left \lfloor \frac{n}{p^2} \right \rfloor + \left \lfloor \frac{n}{p^3} \right \rfloor... ⌊pn⌋+⌊p2n⌋+⌊p3n⌋...个,每一个 p p p的倍数都至少有一个 p p p的质因子,所以 p p p的次数为 ⌊ n p ⌋ + ⌊ n p 2 ⌋ + ⌊ n p 3 ⌋ . . . \left \lfloor \frac{n}{p} \right \rfloor + \left \lfloor \frac{n}{p^2} \right \rfloor + \left \lfloor \frac{n}{p^3} \right \rfloor... ⌊pn⌋+⌊p2n⌋+⌊p3n⌋...,总共约为 l o g p n log_p^n logpn个。
时间复杂度:
总共时间复杂度为 l o g 1 1 0 6 + l o g 2 1 0 6 + l o g 3 1 0 6 + . . . ≤ 50000 × l o g 2 1 0 6 log_1^{10^6} + log_2^{10^6} + log_3^{10^6} +... \le 50000 \times log_2^{10^6} log1106+log2106+log3106+...≤50000×log2106,时间复杂度为 O ( 50000 × l o g 2 1 0 6 ) O(50000 \times log_2^{10^6}) O(50000×log2106)。如果按照公式则为 O ( n l o g 2 n × l o g 2 n ) = O ( n ) O(\frac{n}{log_2^n} \times log_2^n) = O(n) O(log2nn×log2n)=O(n)。
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n; cin >> n;
init(n);
for (int i = 0; i < cnt; i++) {
int p = primes[i], s = 0; // s记录次数
for (int j = n; j; j /= p) s += j / p;
printf("%d %d\n", p, s);
}
return 0;
}
1.3 筛质数
方法一:朴素筛法
时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)
算法思想:
将2 ~ n
这n - 1
个数存入数组中,将2 ~ n
这几个数的所有倍数都删掉,剩下的数都是质数。比如对于一个数p
而言,如果最后p
没有被删掉,那么2 ~ p-1
都不是p
的因数,所以p
就是一个质数。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int n;
int primes[N], cnt; // primes 存储质数, cnt 统计质数个数
bool st[N]; // 判断给位置上的数是否被筛过
void get_prime(int n) {
for (int i = 2; i <= n; i ++ ) {
if (!st[i]) primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i) st[j] = true;
}
printf("%d\n", cnt);
}
int main() {
scanf("%d", &n);
get_prime(n);
return 0;
}
方法二:埃氏筛法
时间复杂度: O ( n l o g l o g n ) O(nlog^{log^n}) O(nloglogn)
优化思路:
执行for (int j = i + i; j <= n; j += i) st[j] = true;
的时候,并不需要将每个数的倍数都删掉,而只需要将质数的倍数都删掉。因为对于一个数p
,如果对于2 ~ p-1
中的所有质数都不是p
的因数,那么p
就是一个质因数。
质数定理:1 ~ n 个数中含有 n l n n \frac{n}{ln^n} lnnn 个质数。
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int n;
int primes[N], cnt;
bool st[N];
void get_prime(int n) {
for (int i = 2; i <= n; i ++ ) {
if (!st[i]) {
primes[cnt ++ ] = i;
for (int j = i + i; j <= n; j += i) st[j] = true;
}
}
printf("%d\n", cnt);
}
int main() {
scanf("%d", &n);
get_prime(n);
return 0;
}
方法三:线性筛法
时间复杂度: O ( n ) O(n) O(n)
优化思路: 将其每个合数用它的某一个质因子筛掉即可。
核心:每个数只会被它的最小质因子筛掉
对于埃氏筛法存在一个问题:同一个合数可能会被两个质数筛。任何一个合数,一定会被筛掉,因为任何一个合数一定存在一个最小质因子,每个数都是用最小质因子来筛除,所以每个数只会被筛一次,所以整个算法是线性的。
代码中的几点说明:
- 代码第7行循环:表示从小到大枚举所有的质数;
- 代码第8行:使用最小质因子
primes[j]
来筛除primes[j] * i
,解释见下面第3点; - 代码第9行:因为是从小到大枚举的质数
- 当第一次出现
i % primes[j] == 0
的时候,primes[j]
一定是i
的最小质因子,primes[j]
也一定是primes[j] * i
的最小质因子; - 当
i % primes[j] != 0
的时候,因为是从小到大枚举的质因子并且没有枚举到i
的最小质因子,说明primes[j]
一定小于i
的所有质因子,所以primes[j]
也一定是primes[j] * i
的最小质因子; - 所以综上两点,不管什么情况下,代码第8行一定是使用最小质因子
primes[j]
来筛除primes[j] * i
的。
- 当第一次出现
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1e6 + 10;
int n;
int primes[N], cnt;
bool st[N];
void get_prime(int n) {
for (int i = 2; i <= n; i ++ ) {
if (!st[i]) primes[cnt ++ ] = i;
for (int j = 0; primes[j] <= n / i; j ++ ) {
st[primes[j] * i] = true;
if i % primes[j] == 0) break;
}
}
printf("%d\n", cnt);
}
int main() {
scanf("%d", &n);
get_prime(n);
return 0;
}
1.3.1 哥德巴赫猜想
哥德巴赫猜想:任意一个大于 4 4 4 的偶数都可以拆成两个奇素数之和。
算法思路:
暴力做法:代码如下。
时间复杂度:因为最多
n
n
n个数,每个数使用试除法判断其是否为质数,时间复杂度为
O
(
n
)
O(\sqrt{n})
O(n),所以总共时间复杂度为
O
(
n
n
)
O(n \sqrt{n})
O(nn)。又
n
n
n最坏为
10
e
6
10e^6
10e6,所以时间复杂度最坏为
1
0
6
1
0
6
=
1
0
9
10^6 \sqrt{10^6} = 10^9
106106=109,总共
T
T
T组数据,最终时间复杂度为
O
(
T
1
0
9
)
O(T10^9)
O(T109),时间复杂度太糟糕了。
for (a = 3; ; a ++ ) {
b = n - a;
if (a、b均是质数) 输出并break
}
优化做法:因为 m a x ( n ) = 1 0 6 max(n) = 10^6 max(n)=106,所以可以先将 1 1 1到 1 0 6 10^6 106之间的所有质数预处理出来存入数组 b o o l s t [ ] bool \space st[\space] bool st[ ]中,后面做法如代码。
时间复杂度:
- 由质数定理, 1 1 1到 1 0 6 10^6 106之间的所有质数个数有 n l n n \frac{n}{ln^n} lnnn 个,所以预处理时间复杂度为 ( n l n n ) (\frac{n}{ln^n}) (lnnn);
- 判断的时间复杂度为 O ( 1 ) O(1) O(1)。
若有 T T T组数据,时间复杂度为 O ( 1 0 6 + T 1 0 6 l n 1 0 6 ) ≈ 1 0 6 + 50000 T O(10^6 + T \frac{10^6}{ln^{10^6}})\approx 10^6 + 50000T O(106+Tln106106)≈106+50000T。
for (a = 所有奇质数; ; a ++ ) {
b = n - a;
if (st[b]) 输出并break
}
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
init(N - 1);
int n;
while (cin >> n, n) {
for (int i = 1;; i++) {
int a = primes[i];
int b = n - a;
if (!st[b]) {
printf("%d = %d + %d\n", n, a, b);
break;
}
}
}
return 0;
}
1.3.2 夏洛克和他的女朋友
算法思路:
如果从图论角度来思考,可以将所有的质数放在一边,所有的合数放在一边,然后建立从质数指向合数的边,这个边表示该质数为该合数的质因数,所以这里就形成了一个二分图。
二分图可以对所有质数和所有合数进行染色,就可以得到本题解,并且可以知道最终解一定属于 [ 1 , 2 ] [1,2] [1,2],当二分图里面没有边的时候,就是 1 1 1,有一条边就是 2 2 2。
进一步,对于这个题,因为一个合数一定会有质因子,因此只要存在合数就一定存在边,答案就是 2 2 2,否则答案为 1 1 1。
当
n
=
1
n=1
n=1时,价值为
2
2
2,没有合数,答案为
1
1
1;
当
n
=
2
n=2
n=2时,价值为
2
、
3
2、3
2、3,没有合数,答案为
1
1
1;
当
n
=
3
n=3
n=3时,价值为
2
、
3
、
4
2、3、4
2、3、4,有合数,答案为
2
2
2;
因此当
n
≥
3
n \ge 3
n≥3时就会存在合数,答案为
2
2
2,染色方案为所有质数一种颜色,所有合数一种颜色;
当
n
=
1
n = 1
n=1或者
2
2
2的时候,答案为
1
1
1,染色方案为所有数为一种颜色。
时间复杂度: O ( n ) O(n) O(n)
#include <cstdio>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 100010;
int primes[N], cnt;
bool st[N];
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int n; cin >> n;
init(n + 1);
if (n <= 2) puts("1");
else puts("2");
for (int i = 2; i <= n + 1; i++)
if (!st[i]) printf("1 ");
else printf("2 ");
return 0;
}
1.3.3 质数距离
因为题目中 1 ≤ L 、 R ≤ 2 31 − 1 ≈ 2 × 1 0 9 1 \le L、R \le 2^{31} - 1 \approx 2 \times 10^9 1≤L、R≤231−1≈2×109,如果直接使用线性筛质数,将其存储进一个数组,时间复杂度和空间复杂度都太糟糕了,并且普通的线性筛法只能在 [ 1 , n ] [1,n] [1,n]之间筛质数,不能在任意一个区间 [ L , R ] [L,R] [L,R]之间筛质数。
算法思路:
首先明确一个性质:如果一个数 n n n为合数,必然存在因子 d d d和 n d \frac{n}{d} dn,不妨设 d ≤ n d d \le \frac{n}{d} d≤dn,则有 d ≤ n d \le \sqrt{n} d≤n。
因此可以先将 [ 1 , 2 31 − 1 ] [1, \sqrt{2^{31}-1}] [1,231−1]之间的质因子筛出来,因为 2 31 − 1 ≈ 2 × 1 0 9 2^{31} - 1 \approx 2 \times 10^9 231−1≈2×109, 5000 0 2 = 2.5 × 1 0 9 50000^2 = 2.5 \times 10^9 500002=2.5×109,所以可以先将 [ 1 , 50000 ] [1, 50000] [1,50000]之间的质因数先筛出来。
对于 [ L , R ] [L,R] [L,R]之间的任意一个数 x x x,如果 x x x为合数,那么它就必然存在一个 [ 1 , 50000 ] [1, 50000] [1,50000]之间的质因子 p p p,满足 p ∣ x 、 p < x p|x、p < x p∣x、p<x(注意:如果一个数为质数,它的质因子可以是自己,如果一个数为合数,它的质因子一定小于自己)。反之如果 x x x为质数,则必然不存在一个 [ 1 , 50000 ] [1, 50000] [1,50000]之间的质因子。
然后对于 [ 1 , 50000 ] [1,50000] [1,50000]中的每一个质数 p p p,将 [ L , R ] [L,R] [L,R]之间 p p p的所有倍数筛掉(至少两倍,保证为合数),这样在区间 [ L , R ] [L,R] [L,R]之间的所有数都为质数。
- 在 [ L , R ] [L,R] [L,R]之间找到一个 p p p的倍数的方法:首先在 [ L , R ] [L,R] [L,R]之间找到一个大于等于 L L L的最小的 p p p的倍数 p 0 p_0 p0,然后可以从 p 0 p_0 p0开始,每次 + p +p +p,从而找出 [ L , R ] [L,R] [L,R]之间 p p p的所有倍数。
- 那么如何在 [ L , R ] [L,R] [L,R]之间找到一个大于等于 L L L的 p p p的最小的倍数 p 0 p_0 p0呢?可以知道大于等于 L L L的 p p p的最小的倍数应该为 ⌈ L p ⌉ × p = ⌊ L + p − 1 p ⌋ × p \left \lceil \frac{L}{p} \right \rceil \times p = \left \lfloor \frac{L + p - 1}{p} \right \rfloor \times p ⌈pL⌉×p=⌊pL+p−1⌋×p。并且这里的 p p p至少要从 2 p 2p 2p开始筛,所以 p 0 = m a x ( 2 p , ⌊ L + p − 1 p ⌋ ) p_0 = max(2p, \left \lfloor \frac{L + p - 1}{p} \right \rfloor) p0=max(2p,⌊pL+p−1⌋)。
关于 ⌈ L p ⌉ = ⌊ L + p − 1 p ⌋ \left \lceil \frac{L}{p} \right \rceil = \left \lfloor \frac{L + p - 1}{p} \right \rfloor ⌈pL⌉=⌊pL+p−1⌋的证明 (c++ 的运算符"\"是向下取整):
- 如果 L L L不是 p p p的倍数, L m o d p ≥ 1 L \space mod \space p \ge 1 L mod p≥1,再加上 p − 1 m o d p = p − 1 p - 1 \space mod \space p = p - 1 p−1 mod p=p−1后一定大于 1 1 1,最后向下取整的结果就相比于原来多 1 1 1;
- 如果 L L L是 p p p的倍数,那么 L + p − 1 L+p-1 L+p−1在整除 p p p的算式中,即 L + p − 1 p \frac{L+p-1}{p} pL+p−1等于没有加 p p p,比如 L = 4 、 P = 2 L= 4、P=2 L=4、P=2,那么 ⌊ 4 + 2 − 1 2 ⌋ = 2 \left \lfloor \frac{4 + 2 - 1}{2} \right \rfloor = 2 ⌊24+2−1⌋=2。
时间复杂度:
由题 m a x { R − L } = 1 0 6 max\{R - L\} = 10^6 max{R−L}=106,对于 [ 1 , 50000 ] [1,50000] [1,50000]中的每一个质数 p p p, [ L , R ] [L,R] [L,R]之间 p p p的倍数最多有 1 0 6 p \frac{10^6}{p} p106个。因为每一个质数都需要计算一遍,所以计算 [ 1 , 50000 ] [1,50000] [1,50000]中所有质数倍数的时间复杂度为 1 0 6 2 + 1 0 6 3 + 1 0 6 5 + . . . = 1 0 6 ( 1 2 + 1 3 + 1 5 + . . . + 1 n ) = O ( 1 0 6 l o g 1 2 l o g 1 0 6 ) = O ( 1 0 6 ( l o g 1 2 + l o g l o g 1 0 6 ) ) = O ( 1 0 6 ( l o g C + l o g l o g 1 0 6 ) ) = O ( 1 0 6 l o g l o g 1 0 6 ) ≈ O ( 1 0 6 l o g 17 ) ≈ O ( 4 × 1 0 6 ) \begin{aligned} &\frac{10^6}{2} + \frac{10^6}{3} + \frac{10^6}{5} + ...\\ =&10^6(\frac{1}{2} + \frac{1}{3} + \frac{1}{5} + ... + \frac{1}{\sqrt{n}})\\ =&O(10^6log^{\frac{1}{2}log^{10^6}})\\ =&O(10^6 (log^{\frac{1}{2}} + log^{log^{10^6}}))\\ =&O(10^6 (log^{C} + log^{log^{10^6}}))\\ =& O(10^6 log^{log^{10^6}})\\ \approx &O(10^6 log^{17})\\ \approx &O( 4\times 10^6 )\\ \end{aligned} =====≈≈2106+3106+5106+...106(21+31+51+...+n1)O(106log21log106)O(106(log21+loglog106))O(106(logC+loglog106))O(106loglog106)O(106log17)O(4×106)所以整个时间复杂度约为 O ( n ) O(n) O(n)。
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
typedef long long LL;
const int N = 1000010;
int primes[N], cnt;
bool st[N];
void init(int n) {
memset(st, 0, sizeof st);
cnt = 0;
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[i * primes[j]] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
int l, r;
while (cin >> l >> r) {
init(50000);
memset(st, 0, sizeof st);
for (int i = 0; i < cnt; i++) {
LL p = primes[i];
for (LL j = max(p * 2, (l + p - 1) / p * p); j <= r; j += p) // j至少从2p开始筛
st[j - l] = true;
}
cnt = 0;
for (int i = 0; i <= r - l; i++) // 将[L,R]之间所有质数存入primes
if (!st[i] && i + l >= 2)
primes[cnt++] = i + l;
if (cnt < 2) puts("There are no adjacent primes.");
else {
int minp = 0, maxp = 0;
for (int i = 0; i + 1 < cnt; i++) {
int d = primes[i + 1] - primes[i];
if (d < primes[minp + 1] - primes[minp]) minp = i;
if (d > primes[maxp + 1] - primes[maxp]) maxp = i;
}
printf("%d,%d are closest, %d,%d are most distant.\n",
primes[minp], primes[minp + 1], primes[maxp], primes[maxp + 1]);
}
}
return 0;
}
2 约数
2.1 求约数——试除法
时间复杂度: O ( n ) O(\sqrt{n}) O(n)
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
int n, m;
vector<int> get_divisors(int n) {
vector<int> res;
for (int i = 1; i <= n / i; i ++ ) {
if (n % i == 0) {
res.push_back(i);
if (i != n / i)
res.push_back(n / i);
}
}
sort(res.begin(), res.end());
return res;
}
int main() {
scanf("%d", &n);
while (n -- ) {
scanf("%d", &m);
vector<int> res = get_divisors(m);
for (int i = 0; i < res.size(); i ++ )
printf("%d ", res[i]);
puts("");
}
return 0;
}
2.2 约数个数
由算数基本定理:任何一个数
N
N
N可以表示成
N
=
P
1
α
1
⋅
P
2
α
2
⋅
⋅
⋅
P
k
α
k
N = P_1^{\alpha_1}·P_2^{\alpha_2}···P_k^{\alpha_k}
N=P1α1⋅P2α2⋅⋅⋅Pkαk那么约数个数为
(
α
1
+
1
)
⋅
(
α
1
+
1
)
⋅
⋅
⋅
(
α
k
+
1
)
(\alpha_1 + 1)·(\alpha_1+1)···(\alpha_k+1)
(α1+1)⋅(α1+1)⋅⋅⋅(αk+1)
证明:设 N N N的任意一个约数为 d d d,那么 d d d可以表示为
d = P 1 β 1 ⋅ P 2 β 2 ⋅ ⋅ ⋅ P k β k ( 0 ≤ β i ≤ α i ) d =P_1^{\beta_1}·P_2^{\beta_2}···P_k^{\beta_k} \space\space(0 \le \beta_i \le \alpha_i) d=P1β1⋅P2β2⋅⋅⋅Pkβk (0≤βi≤αi)只要任意一个 β i \beta_i βi不同,约数 d d d就不同。因为 β 1 ∈ [ 0 , α 1 ] , β 2 ∈ [ 0 , α 2 ] . . . β k ∈ [ 0 , α k ] \beta_1 \in [0, \alpha_1],\beta_2 \in [0, \alpha_2]...\beta_k \in [0, \alpha_k] β1∈[0,α1],β2∈[0,α2]...βk∈[0,αk]。由乘法原理,约数的个数为 ( α 1 + 1 ) ⋅ ( α 1 + 1 ) ⋅ ⋅ ⋅ ( α k + 1 ) (\alpha_1 + 1)·(\alpha_1+1)\space···\space(\alpha_k+1) (α1+1)⋅(α1+1) ⋅⋅⋅ (αk+1)。
同时要注意对于 [ 1 , N ] [1,N] [1,N]中,约数个数为 N 1 + N 2 + . . . + N N ≈ N l o g 2 N \frac{N}{1} + \frac{N}{2} + ... +\frac{N}{N} \approx Nlog_2^N 1N+2N+...+NN≈Nlog2N。
常识: 0 ≤ N ≤ 2 × 1 0 9 0 \le N \le 2 \times 10^9 0≤N≤2×109范围内,即 i n t int int 范围内,约数个数最多的个数有 1600 1600 1600个。(计算见 2.2.3 反素数)
算法思路: 先求出乘积 a 1 ⋅ a 2 . . . ⋅ a n a_1·a_2...·a_n a1⋅a2...⋅an的因式分解,即 a 1 ⋅ a 2 . . . ⋅ a n = P 1 α 1 ⋅ P 2 α 2 ⋅ ⋅ ⋅ P k α k a_1·a_2...·a_n = P_1^{\alpha_1}·P_2^{\alpha_2}···P_k^{\alpha_k} a1⋅a2...⋅an=P1α1⋅P2α2⋅⋅⋅Pkαk然后结果为 ( α 1 + 1 ) ⋅ ( α 1 + 1 ) ⋅ ⋅ ⋅ ( α k + 1 ) (\alpha_1 + 1)·(\alpha_1+1)···(\alpha_k+1) (α1+1)⋅(α1+1)⋅⋅⋅(αk+1)
对乘积进行质因数分解等价于对每个数进行质因式分解。
时间复杂度: O ( n n ) O(n\sqrt{n}) O(nn)
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int n;
unordered_map<int, int> primes; // 每一个质因数的底数 : 指数
int main() {
scanf("%d", &n);
while (n -- ) {
int x;
scanf("%d", &x);
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0) {
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (auto p: primes)
res = res * (p.second + 1) % mod;
printf("%lld\n", res);
return 0;
}
2.2.1 轻拍牛头
因为求约数个数(除法)的时间复杂度为 O ( n n ) O(n\sqrt{n}) O(nn),太糟糕了,所以可以考虑求每一个数的倍数(乘法)。
算法思路:
从 1 1 1到 1 0 6 10^6 106枚举每个数 i i i,判断 [ 1 , 1 0 6 ] [1, 10^6] [1,106]中有多少个数为 i i i的倍数,倍数记为 j j j,这时候因为 i i i为 j j j的约数, j j j为 i i i的倍数,所以应该同时将 i i i所出现的次数添加进 j j j对应的数组中。
时间复杂度:
由开头注解可知道: [ 1 , N ] [1,N] [1,N]中,约数个数为 N 1 + N 2 + . . . + N N ≈ N l o g 2 N \frac{N}{1} + \frac{N}{2} + ... +\frac{N}{N} \approx Nlog_2^N 1N+2N+...+NN≈Nlog2N,因此这个题目的时间复杂度为 O ( 1 0 6 l o g 2 1 0 6 ) O(10^6log_2^{10^6}) O(106log2106)。
#include <cstdio>
using namespace std;
const int N = 1000010;
int n;
int a[N], cnt[N], s[N]; // a存储元素 cnt元素出现次数 s元素约数个数
int main() {
scanf("%d", &n);
for (int i = 0; i < n; i++) {
scanf("%d", a + i);
cnt[a[i]]++;
}
for (int i = 1; i < N; i++)
for (int j = i; j < N; j += i)
s[j] += cnt[i];
for (int i = 0; i < n; i++) printf("%d\n", s[a[i]] - 1); // 最后要减去自己
return 0;
}
2.2.2 樱花
题目求满足 1 x + 1 y = 1 n ! \frac{1}{x} + \frac{1}{y} = \frac{1}{n!} x1+y1=n!1,其中 x 、 y ∈ N + , n ∈ [ 1 , 1 0 6 ] x、y \in N_+,n \in [1,10^6] x、y∈N+,n∈[1,106],这样的一对 ( x , y ) (x,y) (x,y)有多少对。
算法思路:
对公式变形有: 1 x + 1 y = 1 n ! ⇒ x + y x y = 1 n ! ⇒ x ⋅ n ! + y ⋅ n ! = x y ⇒ x ⋅ n ! = ( x − n ! ) y ⇒ y = x ⋅ n ! x − n ! = ( x − n ! + n ! ) n ! x − n ! = ( x − n ! ) n ! x − n ! + ( n ! ) 2 x − n ! ⇒ y = n ! + ( n ! ) 2 x − n ! \begin{aligned} &\frac{1}{x} + \frac{1}{y} = \frac{1}{n!}\\ \Rightarrow& \frac{x + y}{xy} = \frac{1}{n!}\\ \Rightarrow& x \cdot n! + y \cdot n! = xy\\ \Rightarrow& x \cdot n! = (x - n!)y\\ \Rightarrow& y = \frac{x \cdot n!}{x - n!} = \frac{(x - n! + n!)n!}{x - n!} = \frac{(x - n!)n!}{x - n!} + \frac{(n!)^2}{x - n!} \\ \Rightarrow& y= n! + \frac{(n!)^2}{x - n!}\\ \end{aligned} ⇒⇒⇒⇒⇒x1+y1=n!1xyx+y=n!1x⋅n!+y⋅n!=xyx⋅n!=(x−n!)yy=x−n!x⋅n!=x−n!(x−n!+n!)n!=x−n!(x−n!)n!+x−n!(n!)2y=n!+x−n!(n!)2该题问题就转换为了求 x ∈ N + x \in N_+ x∈N+有多少种取值,使得 y ∈ N + y \in N_+ y∈N+。因为 n ! ∈ N + n! \in N_+ n!∈N+,故只需要使 ( n ! ) 2 x − n ! ∈ N + \frac{(n!)^2}{x - n!} \in N_+ x−n!(n!)2∈N+即可,即 x ∈ N + x \in N_+ x∈N+有多少种取值,使得 x − n ! x - n! x−n! 为 ( n ! ) 2 (n!)^2 (n!)2 的约数。
更进一步,因为 1 y = 1 n ! − 1 x \frac{1}{y} = \frac{1}{n!} - \frac{1}{x} y1=n!1−x1,当 x ≤ n ! x \le n! x≤n!的时候, 1 y ≤ 0 \frac{1}{y} \le 0 y1≤0,不满足条件。所以一定满足 x − n ! > 0 x - n! > 0 x−n!>0,故问题 “ x ∈ N + x \in N_+ x∈N+有多少种取值,使得 x − n ! x - n! x−n! 为 ( n ! ) 2 (n!)^2 (n!)2 的约数” 进一步转换为了 x x x取值个数等价于 ( n ! ) 2 (n!)^2 (n!)2的约数个数,即求 ( n ! ) 2 (n!)^2 (n!)2的约数个数。
由题 1.2.1 阶乘分解 可以求解出每一个阶乘的表示,进而可以表示出 ( n ! ) 2 (n!)^2 (n!)2的表示,即 n ! = p 1 c 1 + p 2 c 2 + . . . p k c k ⇒ ( n ! ) 2 = p 1 2 c 1 + p 2 2 c 2 + . . . p k 2 c k \begin{aligned} &n! = p_1^{c_1} + p_2^{c_2} + ... p_k^{c_k}\\ \Rightarrow & (n!)^2 = p_1^{2c_1} + p_2^{2c_2} + ... p_k^{2c_k}\\ \end{aligned} ⇒n!=p1c1+p2c2+...pkck(n!)2=p12c1+p22c2+...pk2ck因此约数个数为 ( 2 c 1 + 1 ) ( 2 c 2 + 1 ) . . . ( 2 c k + 1 ) (2c_1 + 1 )(2c_2 + 1)...(2c_k + 1) (2c1+1)(2c2+1)...(2ck+1)
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 1e6 + 10, mod = 1e9 + 7;
int n, primes[N], cnt;
bool st[N];
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int main() {
scanf("%d", &n);
init(n);
int res = 1;
for (int i = 0; i < cnt; i++) {
int p = primes[i], s = 0; // s为质数p的次数
for (int j = n; j; j /= p) s += j / p;
res = (LL) res * (2 * s + 1) % mod;
}
printf("%d\n", res);
return 0;
}
2.2.3 反素数
[ 1 , N ] [1,N] [1,N]中最大的反素数 等价于 [ 1 , N ] [1,N] [1,N]中约数个数最多的最小的数
解释:假设区间 [ 1 , N ] [1,N] [1,N]中约数个数最多的最小的数为 x x x,那么在 i ∈ [ 1 , x − 1 ] i \in [1,x-1] i∈[1,x−1]中所有数的 g ( i ) < g ( x ) g(i) < g(x) g(i)<g(x),因此 x x x一定是一个反素数;因为 x x x在整个区间中约数个数最多,所以对于 i ∈ [ x + 1 , N i \in [x + 1, N i∈[x+1,N, g ( x ) > g ( i ) g(x) > g(i) g(x)>g(i),因此 i ∈ [ x + 1 , N ] i \in [x + 1, N] i∈[x+1,N]一定不是反素数。
对于这个题,有三点性质:
- 因为 N ∈ [ 1 , 2 × 1 0 9 ] N \in [1, 2 \times 10^9] N∈[1,2×109],对于 N N N不同的质因子数目最多只有 9 9 9个。因为 2 × 3 × 5 × 7 × 11 × 13 × 17 × 19 × 23 × 29 = 6469693230 > 2 × 1 0 9 2 \times 3 \times 5 \times 7 \times 11 \times 13 \times 17 \times 19 \times 23 \times 29 = 6469693230 > 2 \times 10^9 2×3×5×7×11×13×17×19×23×29=6469693230>2×109因此不同的质因子数目最多只有 9 9 9个,即 2 、 3 、 5 、 7 、 11 、 13 、 17 、 19 、 23 2、3、5、7、11、13、17、19、23 2、3、5、7、11、13、17、19、23;
- 每个质因子的次数最大为 30 30 30。因为质因子 2 2 2的 31 31 31次方 2 31 > 2 × 1 0 9 2^{31} > 2 \times 10^9 231>2×109,所以质数 2 2 2的次方数目最多为 30 30 30,则对于质因子 3 3 3的次数一定小于 30 30 30,对于大于 3 3 3的质因数的次方一定更小;
- 所有质因子的次数一定是递减的。当 N = 2 c 1 × 3 c 2 × 5 c 3 × 7 c 4 × . . . N = 2^{c_1} \times 3^{c_2} \times 5^{c_3} \times 7^{c_4} \times ... N=2c1×3c2×5c3×7c4×...,则一定有 c 1 ≥ c 2 ≥ c 3 ≥ . . . c_1 \ge c_2 \ge c_3 \ge ... c1≥c2≥c3≥... 。假设 3 4 3^4 34和 5 5 5^5 55,它们次数不递减,交换二者次数之后,有 3 5 、 5 3 3^5、5^3 35、53,那么 3 4 × 5 5 3 5 × 5 4 = 5 3 < 1 \frac{3^4 \times 5^5}{3^5 \times 5^4} = \frac{5}{3} < 1 35×5434×55=35<1,因此质因数次数递减的时候所求出的 N N N更小, 满足” [ 1 , N ] [1,N] [1,N]中约数个数最多的最小的数“的特点。
因此这个题能搜索的情况很少,可以使用爆搜。依次枚举质因数 2 、 3 、 5 、 . . . 2、3、5、... 2、3、5、...的次数,并且次数依次递减,求出满足上面三点性质的、 [ 1 , N ] [1,N] [1,N]中约数个数最多的最小的数。
#include <cstdio>
using namespace std;
typedef long long LL;
int primes[] = {2, 3, 5, 7, 11, 13, 17, 19, 23};
int maxd, number; // 约数个数maxd 数值number
int n;
// 第u个质数 上一次质数的次数last 当前乘积p 约数个数s
void dfs(int u, int last, int p, int s) {
if (s > maxd || s == maxd && p < number) maxd = s, number = p;
if (u == 9) return;
for (int i = 1; i <= last; i++) { // 枚举次数
if ((LL) p * primes[u] > n) break;
p *= primes[u];
dfs(u + 1, i, p, s * (i + 1));
}
}
int main() {
scanf("%d", &n);
dfs(0, 30, 1, 1);
printf("%d\n", number);
return 0;
}
2.2.4 Hankson的趣味题
有2000组测试数据,每一组中有四个数 a 、 b 、 c 、 d ∈ [ 1 , 2 × 1 0 9 ] a、b、c、d \in [1, 2 \times 10^9] a、b、c、d∈[1,2×109],求解有多少个 x ∈ N + x \in N_+ x∈N+满足 ( a , x ) = b 、 [ c , x ] = d (a, x) = b、[c, x] = d (a,x)=b、[c,x]=d ?
暴力解法:枚举 d d d的所有约数 x x x,然后判断是否满足 ( a , x ) = b (a, x) = b (a,x)=b。 前一步时间复杂度会达到 n \sqrt{n} n!
最大公倍数与最大公约数关系: [ a , b ] = a ⋅ b ( a , b ) [a,b] = \frac{a \cdot b}{(a, b)} [a,b]=(a,b)a⋅b
优化:枚举 d d d的所有约数 x x x的时候,先对 d d d分解质因数,然后在求约数的时候并不是从 1 1 1开始枚举,而仅枚举所有质数,然后再通过 d f s dfs dfs爆搜出所有约数。
#include <cstdio>
using namespace std;
typedef long long LL;
const int N = 50010; // 筛质数最大50000
int primes[N], cnt;
bool st[N];
struct Factor { // 存储d的每个质因子
int p, s; // p质因子 s质因子出现次数
} factor[10]; // int范围内的数据不同的质因子最多有9位
int fcnt; // 质因子个数
// int范围内的数据不同的约数最多为1600个
int dividor[1601], dcnt; // dividor所有约数 dcnt约数个数
void init(int n) {
for (int i = 2; i <= n; i++) {
if (!st[i]) primes[cnt++] = i;
for (int j = 0; primes[j] * i <= n; j++) {
st[primes[j] * i] = true;
if (i % primes[j] == 0) break;
}
}
}
int gcd(int a, int b) { return b ? gcd(b, a % b) : a; }
// 第u个质因子 约数的值为p
void dfs(int u, int p) {
if (u == fcnt) {
dividor[dcnt++] = p;
return;
}
for (int i = 0; i <= factor[u].s; i++) {
dfs(u + 1, p);
p *= factor[u].p;
}
}
int main() {
init(N);
int n; scanf("%d", &n);
while (n--) {
int a, b, c, d; scanf("%d%d%d%d", &a, &b, &c, &d);
fcnt = 0;
int t = d;
for (int i = 0; primes[i] <= t / primes[i]; i++) { // 分解质因式
int p = primes[i];
if (t % p == 0) {
int s = 0;
while (t % p == 0) t /= p, s++;
factor[fcnt++] = {p, s};
}
}
if (t > 1) factor[fcnt++] = {t, 1};
dcnt = 0;
dfs(0, 1); // 求每个约数
int res = 0;
for (int i = 0; i < dcnt; i++) { // 判断所有约数
int x = dividor[i];
if (gcd(a, x) == b && (LL) c * x / gcd(c, x) == d) res++;
}
printf("%d\n", res);
}
return 0;
}
2.3 约数之和
任何一个数
N
N
N可以表示成
N
=
P
1
α
1
⋅
P
2
α
2
⋅
⋅
⋅
P
k
α
k
N = P_1^{\alpha_1}·P_2^{\alpha_2}···P_k^{\alpha_k}
N=P1α1⋅P2α2⋅⋅⋅Pkαk那么约数之和为
(
P
1
0
+
p
1
1
+
.
.
.
+
P
1
α
1
)
(
P
2
0
+
p
2
1
+
.
.
.
+
P
2
α
1
)
⋅
⋅
⋅
(
P
k
0
+
p
k
1
+
.
.
.
+
P
k
α
1
)
(P_1^0 + p_1^1 + ... +P_1^{\alpha_1})(P_2^0 + p_2^1 + ... +P_2^{\alpha_1})···(P_k^0 + p_k^1 + ... +P_k^{\alpha_1})
(P10+p11+...+P1α1)(P20+p21+...+P2α1)⋅⋅⋅(Pk0+pk1+...+Pkα1)
证明:将式子 ( P 1 0 + p 1 1 + . . . + P 1 α 1 ) ( P 2 0 + p 2 1 + . . . + P 2 α 1 ) ⋅ ⋅ ⋅ ( P k 0 + p k 1 + . . . + P k α 1 ) (P_1^0 + p_1^1 + ... +P_1^{\alpha_1})(P_2^0 + p_2^1 + ... +P_2^{\alpha_1})···(P_k^0 + p_k^1 + ... +P_k^{\alpha_1}) (P10+p11+...+P1α1)(P20+p21+...+P2α1)⋅⋅⋅(Pk0+pk1+...+Pkα1)展开,可见其为约数之和。
时间复杂度: O ( n n ) O(n\sqrt{n}) O(nn)
#include <iostream>
#include <algorithm>
#include <unordered_map>
using namespace std;
typedef long long LL;
const int N = 110, mod = 1e9 + 7;
int n;
unordered_map<int, int> primes;
int main() {
scanf("%d", &n);
while (n -- ) {
int x;
scanf("%d", &x);
for (int i = 2; i <= x / i; i ++ )
while (x % i == 0) {
x /= i;
primes[i] ++ ;
}
if (x > 1) primes[x] ++ ;
}
LL res = 1;
for (auto p: primes) {
LL a = p.first, b = p.second;
LL t = 1;
while (b -- ) t = (t * a + 1) % mod;
res = res * t % mod;
}
printf("%lld\n", res);
return 0;
}
2.4 最大公约数 (欧几里得算法、辗转相除法)
性质:若 d d d能整除 a a a和 b b b,记为 d ∣ a 、 d ∣ b d|a、d|b d∣a、d∣b。那么 d d d也能整除 a + b 、 a x + b y a + b、ax + by a+b、ax+by( a a a的倍数和 b b b的倍数),记为 d ∣ ( a + b ) 、 d ∣ ( a x + b y ) d|(a+b)、d|(ax+by) d∣(a+b)、d∣(ax+by)。
a a a和 b b b的最大公约数 ( a , b ) = ( b , a m o d b ) (a, b) = (b, a\space mod \space b) (a,b)=(b,a mod b)
证明: a m o d b = a − ⌊ a b ⌋ ⋅ b a \space mod \space b = a - \left \lfloor \frac{a}{b} \right \rfloor · b a mod b=a−⌊ba⌋⋅b,令 c = ⌊ a b ⌋ c = \left \lfloor \frac{a}{b} \right \rfloor c=⌊ba⌋,那么有 a m o d b = a − c ⋅ b a \space mod \space b = a - c · b a mod b=a−c⋅b,则有 ( a , b ) = ( b , a − c ⋅ b ) (a, b) = (b, a - c · b) (a,b)=(b,a−c⋅b)从左往右:设 d d d为 a 、 b a、b a、b的最大公约数,由性质可知,左边的最大公约数也为右边的最大公约数;
从右往左:设 d d d为 b 、 a − c ⋅ b b、a - c · b b、a−c⋅b的最大公约数,由性质可知 d d d也为 c ⋅ b c·b c⋅b的最大公约数。所以 d d d也为 ( a − c ⋅ b ) + c ⋅ b = a (a - c · b) + c · b = a (a−c⋅b)+c⋅b=a的最大公约数,即为 a 、 b a、b a、b的最大公约数。
时间复杂度: O ( l o g n ) O(log^n) O(logn)
#include <iostream>
#include <algorithm>
using namespace std;
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
int main() {
int n;
scanf("%d", &n);
while (n -- ) {
int a, b;
scanf("%d%d", &a, &b);
printf("%d\n", gcd(a, b));
}
return 0;
}