第六章 数学(一)

一、数论

1 质数

定义: 在所有大于1的自然数,如果只包含1和本身这两个约数,就被称为质数,或者叫做素数。

1.1 判定质数——试除法

ACWing 866

时间复杂度: 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 分解质因数——试除法

ACWing 867

因数与约数:

  • 数域不同。约数只能是自然数(即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 阶乘分解

ACwing 197

算法思路

  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 log210620,所以 n l o g 2 n ≈ 50000 \frac{n}{log_2^n} \approx 50000 log2nn50000,因此质数大约 50000 50000 50000个。
  2. 对每个质数 p p p的次数。首先分别求出 [ 1 , n ] [1, n] [1,n] p 、 p 2 、 p 3 . . . p、p^2、p^3... pp2p3...的倍数(直到 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 筛质数

ACWing 868

方法一:朴素筛法

时间复杂度: O ( n l o g n ) O(nlogn) O(nlogn)

算法思想:

2 ~ nn - 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)

优化思路: 将其每个合数用它的某一个质因子筛掉即可。

核心:每个数只会被它的最小质因子筛掉

对于埃氏筛法存在一个问题:同一个合数可能会被两个质数筛。任何一个合数,一定会被筛掉,因为任何一个合数一定存在一个最小质因子,每个数都是用最小质因子来筛除,所以每个数只会被筛一次,所以整个算法是线性的。

代码中的几点说明:

  1. 代码第7行循环:表示从小到大枚举所有的质数;
  2. 代码第8行:使用最小质因子primes[j]来筛除primes[j] * i,解释见下面第3点;
  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 哥德巴赫猜想

ACwing 1292

哥德巴赫猜想:任意一个大于 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 夏洛克和他的女朋友

ACwing 1293

算法思路

如果从图论角度来思考,可以将所有的质数放在一边,所有的合数放在一边,然后建立从质数指向合数的边,这个边表示该质数为该合数的质因数,所以这里就形成了一个二分图。

二分图可以对所有质数和所有合数进行染色,就可以得到本题解,并且可以知道最终解一定属于 [ 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 23,没有合数,答案为 1 1 1
n = 3 n=3 n=3时,价值为 2 、 3 、 4 2、3、4 234,有合数,答案为 2 2 2

因此当 n ≥ 3 n \ge 3 n3时就会存在合数,答案为 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 质数距离

ACwing 196

因为题目中 1 ≤ L 、 R ≤ 2 31 − 1 ≈ 2 × 1 0 9 1 \le L、R \le 2^{31} - 1 \approx 2 \times 10^9 1LR23112×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} ddn,则有 d ≤ n d \le \sqrt{n} dn

因此可以先将 [ 1 , 2 31 − 1 ] [1, \sqrt{2^{31}-1}] [1,2311 ]之间的质因子筛出来,因为 2 31 − 1 ≈ 2 × 1 0 9 2^{31} - 1 \approx 2 \times 10^9 23112×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 pxp<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]之间的所有数都为质数。

  1. [ 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的所有倍数。
  2. 那么如何在 [ 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+p1×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+p1)

关于 ⌈ L p ⌉ = ⌊ L + p − 1 p ⌋ \left \lceil \frac{L}{p} \right \rceil = \left \lfloor \frac{L + p - 1}{p} \right \rfloor pL=pL+p1的证明 (c++ 的运算符"\"是向下取整):

  • 如果 L L L不是 p p p的倍数, L   m o d   p ≥ 1 L \space mod \space p \ge 1 L mod p1,再加上 p − 1   m o d   p = p − 1 p - 1 \space mod \space p = p - 1 p1 mod p=p1后一定大于 1 1 1,最后向下取整的结果就相比于原来多 1 1 1
  • 如果 L L L p p p的倍数,那么 L + p − 1 L+p-1 L+p1在整除 p p p的算式中,即 L + p − 1 p \frac{L+p-1}{p} pL+p1等于没有加 p p p,比如 L = 4 、 P = 2 L= 4、P=2 L=4P=2,那么 ⌊ 4 + 2 − 1 2 ⌋ = 2 \left \lfloor \frac{4 + 2 - 1}{2} \right \rfloor = 2 24+21=2

时间复杂度

由题 m a x { R − L } = 1 0 6 max\{R - L\} = 10^6 max{RL}=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+...+n 1)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 )

ACWing 869

#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 约数个数

ACWing 870

由算数基本定理:任何一个数 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α1P2α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β1P2β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+...+NNNlog2N

常识: 0 ≤ N ≤ 2 × 1 0 9 0 \le N \le 2 \times 10^9 0N2×109范围内,即 i n t int int 范围内,约数个数最多的个数有 1600 1600 1600个。(计算见 2.2.3 反素数)

算法思路: 先求出乘积 a 1 ⋅ a 2 . . . ⋅ a n a_1·a_2...·a_n a1a2...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} a1a2...an=P1α1P2α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 轻拍牛头

ACwing 1291

因为求约数个数(除法)的时间复杂度为 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+...+NNNlog2N,因此这个题目的时间复杂度为 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 樱花

ACwing 1294

题目求满足 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] xyN+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!1xn!+yn!=xyxn!=(xn!)yy=xn!xn!=xn!(xn!+n!)n!=xn!(xn!)n!+xn!(n!)2y=n!+xn!(n!)2该题问题就转换为了 x ∈ N + x \in N_+ xN+有多少种取值,使得 y ∈ N + y \in N_+ yN+。因为 n ! ∈ N + n! \in N_+ n!N+,故只需要使 ( n ! ) 2 x − n ! ∈ N + \frac{(n!)^2}{x - n!} \in N_+ xn!(n!)2N+即可,即 x ∈ N + x \in N_+ xN+有多少种取值,使得 x − n ! x - n! xn! ( n ! ) 2 (n!)^2 (n!)2 的约数

更进一步,因为 1 y = 1 n ! − 1 x \frac{1}{y} = \frac{1}{n!} - \frac{1}{x} y1=n!1x1,当 x ≤ n ! x \le n! xn!的时候, 1 y ≤ 0 \frac{1}{y} \le 0 y10,不满足条件。所以一定满足 x − n ! > 0 x - n! > 0 xn!>0,故问题 x ∈ N + x \in N_+ xN+有多少种取值,使得 x − n ! x - n! xn! ( 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 反素数

ACwing 198

[ 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,x1]中所有数的 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]一定不是反素数。

对于这个题,有三点性质:

  1. 因为 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 23571113171923
  2. 每个质因子的次数最大为 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的质因数的次方一定更小;
  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 ... c1c2c3... 。假设 3 4 3^4 34 5 5 5^5 55,它们次数不递减,交换二者次数之后,有 3 5 、 5 3 3^5、5^3 3553,那么 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、... 235...的次数,并且次数依次递减,求出满足上面三点性质的、 [ 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的趣味题

ACwing 200

有2000组测试数据,每一组中有四个数 a 、 b 、 c 、 d ∈ [ 1 , 2 × 1 0 9 ] a、b、c、d \in [1, 2 \times 10^9] abcd[1,2×109],求解有多少个 x ∈ N + x \in N_+ xN+满足 ( 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)ab

优化:枚举 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 约数之和

ACWing 871

任何一个数 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α1P2α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 最大公约数 (欧几里得算法、辗转相除法)

ACwing 872

性质:若 d d d能整除 a a a b b b,记为 d ∣ a 、 d ∣ b d|a、d|b dadb。那么 d d d也能整除 a + b 、 a x + b y a + b、ax + by a+bax+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=abab,令 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=acb,则有 ( a , b ) = ( b , a − c ⋅ b ) (a, b) = (b, a - c · b) (a,b)=(b,acb)从左往右:设 d d d a 、 b a、b ab的最大公约数,由性质可知,左边的最大公约数也为右边的最大公约数;
从右往左:设 d d d b 、 a − c ⋅ b b、a - c · b bacb的最大公约数,由性质可知 d d d也为 c ⋅ b c·b cb的最大公约数。所以 d d d也为 ( a − c ⋅ b ) + c ⋅ b = a (a - c · b) + c · b = a (acb)+cb=a的最大公约数,即为 a 、 b a、b ab的最大公约数。

时间复杂度: 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;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值