Skip to content

4. 基础数学初识

4.1 质数

概念

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

4.1.1 试除法判定质数


思想

  • \(N<2\)不是质数。
  • \(i=2\)开始枚举,直到\(\sqrt{n}\),若\(i\)能被\(N\)整除,说明不是质数;否则为质数。

模板

C++ 模板

cpp
bool is_prime(int n) {
    if (n < 2) return 0;  // 若小于2直接返回false
    for (int i = 2; i <= n / i; i++) {  // 优化为sqrt(n)
        if (n % i == 0) return 0;
    }
    return 1;
}

例题 866. 试除法判定质数

原题链接

描述

给定 n 个正整数 ai,判定每个数是否是质数。

输入格式 第一行包含整数 n。

接下来 n 行,每行包含一个正整数 ai。

输出格式 共 n 行,其中第 i 行输出第 i 个正整数 ai 是否为质数,是则输出 Yes,否则输出 No。

数据范围 1≤n≤100, 1≤ai≤2^31−1

输入样例:

2
2
6

输出样例:

Yes
No

代码

cpp
#include <bits/stdc++.h>
using namespace std;

bool is_prime(int n) {
    if (n < 2) return 0;
    for (int i = 2; i <= n / i; i++) {
        if (n % i == 0) return 0;
    }
    return 1;
}

int main() {
    int t;
    cin >> t;
    while (t--) {
        int x;
        cin >> x;
        if (is_prime(x)) cout << "Yes" << endl;
        else cout << "No" << endl;
    }
    return 0;
}

4.1.2 分解质因数


概念

  • 每个合数都可以写成几个质数相乘的形式,其中每个质数都是这个合数的因数。
    • 把一个合数用质因数乘积的形式表示出来,叫做分解质因数。
    • \(30=2\times3\times5\),分解质因数只针对合数

思想

  • 算术基本定理:任何一个大于 \(1\) 的自然数 \(N\),如果 \(N\) 不为质数,
    • 那么 \(N\) 可以唯一分解成有限个质数的乘积 \(N=p_1^{a_1}\times p_2^{a_2}\dots\times p_k^{a_k}\),且最多只有一个大于 \(\sqrt{N}\) 的质因子。
    • 这里 \(p_1&lt;p_2&lt;p_3\dots&lt;p_k\) 均为质数,其中指数 \(a_1, a_2, \dots, a_k\) 是正整数。

模板

cpp
#include <bits/stdc++.h>
using namespace std;

map<int, int> primes; // 存储质因子底数和其指数的映射

void get_div(int n) {
    primes.clear(); // 清空数据
    for (int i = 2; i <= n / i; i++) { // 从2开始枚举质因子
        if (n % i == 0) { // 当其为质因子时
            while (n % i == 0) {
                primes[i]++; // 指数增加
                n /= i;
            }
        }
    }
    if (n > 1) primes[n]++; // 剩余的数大于1则为最后的质因子
}

例题 867. 分解质因数

原题链接

描述

给定 n 个正整数 ai,将每个数分解质因数,并按照质因数从小到大的顺序输出每个质因数的底数和指数。

输入格式 第一行包含整数 n。

接下来 n 行,每行包含一个正整数 ai。

输出格式 对于每个正整数 ai,按照从小到大的顺序输出其分解质因数后,每个质因数的底数和指数,每个底数和指数占一行。

每个正整数的质因数全部输出完毕后,输出一个空行。

数据范围 1≤n≤100, 2≤ai≤2×10^9

输入样例:

2
6
8

输出样例:

2 1
3 1

2 3

代码

cpp
#include <bits/stdc++.h>
using namespace std;

map<int, int> primes;

void get_div(int n) {
    primes.clear();
    for (int i = 2; i <= n / i; i++) {
        if (n % i == 0) {
            while (n % i == 0) {
                primes[i]++;
                n /= i;
            }
        }
    }
    if (n > 1) primes[n]++;
}

int main() {
    int n;
    cin >> n;
    while (n--) {
        int a;
        cin >> a;
        get_div(a);
        for (auto &p : primes) {
            cout << p.first << " " << p.second << endl;
        }
        cout << endl;
    }
    return 0;
}

4.1.3 筛质数(线性筛)


模板

cpp
#include <bits/stdc++.h>
using namespace std;

const int N = 1e6 + 3;

int cnt;                // 记录质数个数
int primes[N];          // 存储当前筛选出的质数
bool vis[N];            // 标记是否被筛掉

void get_primes(int n) {
    for (int i = 2; i <= n; i++) {          // 外层从2~n迭代
        if (!vis[i]) primes[cnt++] = i;    // 没被筛掉说明是质数,记录到primes[]中
        for (int j = 0; primes[j] <= n / i; j++) { // 将1~n范围内质数primes[j]的i倍的合数筛掉
            vis[primes[j] * i] = 1;        // 用最小质因子primes[j]筛掉合数
            if (i % primes[j] == 0) break;
        }
    }
}

// 例题 868. 筛质数
int main() {
    int n;
    cin >> n;
    get_primes(n);
    cout << cnt << endl;
    return 0;
}

4.2 约数


概念

  • 约数,又称因数。整数\(a\)除以整数\(b(b≠0)\) 除得的商正好是整数而没有余数,我们就说\(a\)能被\(b\)整除,或\(b\)能整除\(a\)\(a\)称为\(b\)倍数\(b\)称为\(a\)约数

4.2.1 试除法求约数


思想

  • \(i=1\)开始枚举到\(\sqrt{N}\)
    • \(i\)\(\frac{N}{i}\)即为\(N\)的约数

模板

cpp
const int N=1e6+3;
int res[N];  // 存储约数
int cnt;     // 记录数量
void get_div(int n){
    cnt=0;  // 初始化
    for(int i=1;i<=n/i;i++){  // 从1开始枚举
        if(n%i==0){
            res[cnt++]=i;  // 将i作为约数
            if(i!=n/i) res[cnt++]=n/i;  // 将n/i作为约数
        }
    }
    sort(res, res+cnt);  // 将约数从小到大排序
}

例题 869. 试除法求约数

原题链接

描述

给定 n 个正整数 ai,对于每个整数 ai,请你按照从小到大的顺序输出它的所有约数。

输入格式 第一行包含整数 n。

接下来 n 行,每行包含一个整数 ai。

输出格式

输出共 n 行,其中第 i 行输出第 i 个整数 a_i 的所有约数。

数据范围

1 ≤ n ≤ 100, 2 ≤ a_i ≤ 2 × 10^9

输入样例:

2
6
8

输出样例:

1 2 3 6 
1 2 4 8

代码

cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

// 试除法求n的所有约数,存入res数组,并返回约数个数
vector<int> get_divisors(int n) {
    vector<int> res;
    // i*i <= n 避免开方和溢出,效率等价于 i <= n/i
    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() {
    int n;
    cin >> n;
    while (n--) {
        int x;
        cin >> x;
        vector<int> divs = get_divisors(x);
        for (int d : divs) {
            cout << d << " ";
        }
        cout << endl;
    }
    return 0;
}

4.2.2 约数个数


思想

  • 算术基本定理:任何一个大于 1 的自然数 N,如果 N 不为质数,那么 N 可以唯一分解成有限个质数的乘积:N = p₁^{a₁} × p₂^{a₂} × ... × p_k^{a_k}。且最多只有一个大于 √N 的质因子。
  • 这里 p₁ < p₂ < ... < p_k 均为质数,其中指数 a_i 是正整数。
  • 设 d 为 N 的任意一个约数,则 d 可以表示为:d = p₁^{b₁} × p₂^{b₂} × ... × p_k^{b_k},其中 0 ≤ b_i ≤ a_i。
  • 由算术基本定理可知,对于 d 中的每个 p_i^{b_i} 项,指数 b_i 取值的不同组合,会得到不同的约数 d(因为每个数的质因数分解是唯一的)。
  • 因此,求 N 的约数个数,等价于求指数 b_i 的选法总数。
    • 对于 p₁,指数 b₁ 可以从 0 选到 a₁,共 (a₁+1) 种选法。
    • 对于 p₂,指数 b₂ 可以从 0 选到 a₂,共 (a₂+1) 种选法。
    • ...
    • 对于 p_k,指数 b_k 可以从 0 选到 a_k,共 (a_k+1) 种选法。
  • 根据乘法原理可知:N 的约数个数为 (a₁+1) × (a₂+1) × ... × (a_k+1)。

模板例题 870. 约数个数

原题链接

描述

给定 n 个正整数 a_i,请你输出这些数的乘积的约数个数,答案对 1e9+7 取模。

输入格式 第一行包含整数 n。 接下来 n 行,每行包含一个整数 a_i。

输出格式
输出一个整数,表示所给正整数的乘积的约数个数,答案需对 10^9+7 取模。

数据范围
1 ≤ n ≤ 100, 1 ≤ a_i ≤ 2×10^9

输入样例:

3
2
6
8

输出样例:

12

代码

cpp
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;

const LL mod = 1e9 + 7;
LL cnt = 1;
map<int, int> primes;  // 存储质因子底数和其指数的映射

void get_div(int n) {
    for (int i = 2; i <= n / i; i++) {  // 枚举可能的质因子
        if (n % i == 0) {  // 当其为质因子时
            while (n % i == 0) {
                primes[i]++;  // 指数增加
                n /= i;
            }
        }
    }
    if (n > 1) primes[n]++;  // 剩余的数大于1则为最后的质因子
}

int main() {
    int n;
    cin >> n;
    while (n--) {
        int x;
        cin >> x;
        get_div(x);
    }
    for (auto &p : primes) cnt = cnt * (p.second + 1) % mod;  // 核心:N的约数个数为 (a1+1)*(a2+1)*(a3+1)*…*(ai+1)
    cout << cnt << endl;
    return 0;
}

4.2.3 约数之和


思想

  • 算术基本定理:任何一个大于 \(1\) 的自然数 \(N\),如果 \(N\) 不为质数。
  • 那么 \(N\) 可以唯一分解成有限个质数的乘积 \(N=p_1^{a_1}\times p_2^{a_2}\dots\times p_k^{a_k}\),且最多只有一个大于 \(\sqrt{N}\) 的质因子。
  • 这里 \(p_1 &lt; p_2 &lt; p_3 \dots &lt; p_k\) 均为质数,其中指数 \(a_i\) 是正整数。
\[\begin{cases} p_1\text{的约数之和} = p_1^{0}+p_1^{1}+\dots+p_1^{a_1} \\ p_2\text{的约数之和} = p_2^{0}+p_2^{1}+\dots+p_2^{a_2} \\ p_3\text{的约数之和} = p_3^{0}+p_3^{1}+\dots+p_3^{a_3} \\ \vdots \\ p_k\text{的约数之和} = p_k^{0}+p_k^{1}+\dots+p_k^{a_k} \\ \end{cases} \]
  • 根据乘法原理可知:\(N\) 的约数之和 \(=(p_1^{0}+p_1^{1}+\dots+p_1^{a_1})\times(p_2^{0}+p_2^{1}+\dots+p_2^{a_2})\times\dots\times(p_k^{0}+p_k^{1}+\dots+p_k^{a_k})\)

模板例题 871. 约数之和

原题链接

描述
给定 n 个正整数 a_i,请你输出这些数的乘积的约数之和,答案对 10^9+7 取模。

输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个整数 a_i。

输出格式
输出一个整数,表示所给正整数的乘积的约数之和,答案需对 10^9+7 取模。

数据范围
1 ≤ n ≤ 100,1 ≤ a_i ≤ 2×10^9

输入样例:

text
3
2
6
8

输出样例:

text
252

代码

cpp
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;
const LL mod = 1e9 + 7;
LL res = 1;

map<int, int> primes;  // 存储质因子底数及其指数

void get_div(int n) {
    for (int i = 2; i <= n / i; i++) {  // 从2开始枚举质因子
        if (n % i == 0) {              // 当其为质因子时
            while (n % i == 0) {
                primes[i]++;           // 指数增加
                n /= i;
            }
        }
    }
    if (n > 1) primes[n]++;            // 剩余的数大于1则为最后的质因子
}

int main() {
    int n;
    cin >> n;
    while (n--) {
        int x;
        cin >> x;
        get_div(x);
    }

    for (auto &p : primes) {
        LL t = 1;
        int a = p.first, b = p.second;
        while (b--) {
            t = (t * a + 1) % mod;  // 核心:从 p^0 加到 p^k 的和
        }
        res = res * t % mod;
    }

    cout << res << endl;
    return 0;
}

4.2.4 最大公约数和最小公倍数


概念

  • 最大公约数指两个或多个整数共有约数(因数)中最大的数。
  • 最小公倍数指两个或多个整数的公倍数里最小的数。

思想

  • 辗转相除法求最大公约数。

> 例如: 假如需要求 100 和 18 两个正整数的最大公约数,用欧几里得算法,是这样进行的:

100 / 18 = 5 (余 10)
18 / 10 = 1 (余 8)
10 / 8 = 1 (余 2)
8 / 2 = 4 (余 0)
至此,最大公约数为 2。
以除数和余数反复做除法运算,当余数为 0 时,取当前算式除数为最大公约数,所以就得出了 100 和 18 的最大公约数 2。

  • \(N\)\(M\) 的最小公倍数 \(\operatorname{lcm}(N, M)\),则先求 \(N\)\(M\) 的最大公约数 \(\gcd(N, M)\),然后 \(\frac{N \times M}{\gcd(N, M)}\) 则为最小公倍数。

模板

cpp
// 最大公约数
int gcd(int a, int b){
    return b ? gcd(b, a % b) : a;
}

// 最小公倍数
int lcm(int a, int b){
    return a / gcd(a, b) * b;
}

4.3 欧拉函数


概念

  • \(1 \sim N\)中与\(N\)互质的数的个数被称为欧拉函数,记为 \(\phi(N)\)。特别地,\(\phi(1)=1\)
    • 欧拉函数是一个积性函数,若\(m\)\(n\)互质,则有\(\phi(m \times n)=\phi(m) \times \phi(n)\)

4.3.1 公式法求欧拉函数


思想

  • 算术基本定理:任何一个大于\(1\)的自然数\(N\),如果\(N\)不为质数,那么\(N\)可以唯一分解成有限个质数的乘积\(N=p_1^{a_1}\times p_2^{a_2}\times\cdots\times p_k^{a_k}\),且最多只有一个大于\(\sqrt{N}\)的质因子。
  • 这里\(p_1&lt;p_2&lt;p_3&lt;\cdots &lt; p_k\)均为质数,其中指数\(a_i\)是正整数。
  • \(\phi(N)=\phi(p_1^{a_1})\times\phi(p_2^{a_2})\times\cdots\times\phi(p_k^{a_k})\)
  • 对于任意一项\(\phi(p_i^{a_i})\),与\(p_i^{a_i}\)不互质的数有\(p_i,\ 2\times p_i,\ 3\times p_i,\ \dots,\ p_i^{a_i-1}\times p_i\),共\(p_i^{a_i-1}\)项。
  • \(\phi(p_i^{a_i})=p_i^{a_i}-p_i^{a_i-1}\)
\[\begin{aligned} \phi(N) &= \phi(p_1^{a_1})\times\phi(p_2^{a_2})\times\cdots\times\phi(p_k^{a_k}) \\ &= (p_1^{a_1}-p_1^{a_1-1})\times(p_2^{a_2}-p_2^{a_2-1})\times\cdots\times(p_k^{a_k}-p_k^{a_k-1}) \\ &= p_1^{a_1}\times p_2^{a_2}\times\cdots\times p_k^{a_k} \times (1-\frac{1}{p_1})\times(1-\frac{1}{p_2})\times\cdots\times(1-\frac{1}{p_k}) \\ &= N \times \prod_{i=1}^{k}{(1-\frac{1}{p_i})} \end{aligned} \]

模板

cpp
typedef long long LL;

LL phi(LL n){
    LL res = n;
    for (int i = 2; i <= n / i; i++){
        if (n % i == 0){
            res = res / i * (i - 1);  // (1-1/i) 转换为 (i-1)/i
            while (n % i == 0) n /= i;
        }
    }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

例题 873. 欧拉函数

原题链接

描述

给定 n 个正整数 ai,请你求出每个数的欧拉函数。

欧拉函数的定义:1 ~ N 中与 N 互质的数的个数被称为欧拉函数,记为 ϕ(N)。
若在算术基本定理中,N = p₁^a₁ · p₂^a₂ · … · pₘ^aₘ,则:
ϕ(N) = N × (p₁ - 1)/p₁ × (p₂ - 1)/p₂ × … × (pₘ - 1)/pₘ

输入格式
第一行包含整数 n。
接下来 n 行,每行包含一个正整数 ai。

输出格式
输出共 n 行,每行输出一个正整数 ai 的欧拉函数。

数据范围
1 ≤ n ≤ 100, 1 ≤ ai ≤ 2×10⁹

输入样例:

3
3
6
8

输出样例:

2
2
4

代码
C++ 解法

cpp
#include <bits/stdc++.h>
using namespace std;

typedef long long LL;

LL phi(LL n) {
    LL res = n;
    for (int i = 2; i <= n / i; i++) {
        if (n % i == 0) {
            res = res / i * (i - 1);
            while (n % i == 0) n /= i;
        }
    }
    if (n > 1) res = res / n * (n - 1);
    return res;
}

int main() {
    int n;
    cin >> n;
    while (n--) {
        int x;
        cin >> x;
        cout << phi(x) << endl;
    }
    return 0;
}

4.3.2 筛法求欧拉函数


思想

  • 利用线性筛,在筛选 1~N 中的质数时,将 1~N 的欧拉函数 ϕ(i) 求出。
  • 对于质数 p,其 ϕ(p) = p - 1。
  • 对于合数,其欧拉函数值通过已筛出的质因子推导而来。

以下是修复了错别字、格式问题并调整了逻辑表述后的文本:

\[\phi(n)=n\times\prod_{p|n}(1-\frac{1}{p}) \]
  • 在线性筛法模板中,利用最小质因子筛掉合数的过程如下:
    • \(i\%primes[j]=0\) 时,说明 \(primes[j]\)\(i\) 的一个质因子,且是 \(primes[j]\times i\) 的最小质因子。因此 \(\phi(i)\) 的计算中已包含 \((1-\frac{1}{primes[j]})\) 项,故 \(\phi(primes[j]\times i)=primes[j]\times\phi(i)\)
    • \(i\%primes[j]\ne 0\) 时,说明 \(primes[j]\)\(primes[j]\times i\) 的最小质因子,且与 \(i\) 互质。因此 \(\phi(i)\) 中不包含 \((1-\frac{1}{primes[j]})\) 项,故有:
    \[\begin{aligned} \phi(primes[j]\times i) &= \phi(primes[j])\times\phi(i) \\ &= (primes[j]-1)\times\phi(i) \end{aligned} \]

模板

cpp
int primes[N]; // 存储当前筛选出的质数
bool vis[N];   // 标记是否被筛掉
int phi[N];    // 记录欧拉函数的值
int cnt;       // 记录质数个数

void get_phi(int n) {
    phi[1] = 1; // 特别地,phi[1] = 1
    for (int i = 2; i <= n; i++) {
        if (!vis[i]) {
            primes[cnt++] = i; // 没有被筛掉说明是质数,记录到primes中
            phi[i] = i - 1;    // 质数的欧拉函数
        }
        for (int j = 0; primes[j] <= n / i; j++) { // 将1~n范围内质数primes[j]的i倍的合数筛掉
            vis[primes[j] * i] = 1;
            if (i % primes[j] == 0) { // 用最小质因子primes[j]筛掉合数
                phi[primes[j] * i] = primes[j] * phi[i]; // 包含(1 - 1/primes[j])的情况
                break;
            } else {
                phi[primes[j] * i] = (primes[j] - 1) * phi[i]; // 不包含(1 - 1/primes[j])的情况
            }
        }
    }
}

例题 874. 筛法求欧拉函数

原题链接

描述

给定一个正整数 n,求 1∼n 中每个数的欧拉函数之和。

输入格式 共一行,包含一个整数 n。

输出格式 共一行,包含一个整数,表示 1∼n 中每个数的欧拉函数之和。

数据范围 1 ≤ n ≤ 10^6

输入样例:

6

输出样例:

12

代码

cpp
#include <bits/stdc++.h>
using namespace std;

const int N=1e6+3;
typedef long long LL;
int primes[N];
bool vis[N];
int phi[N];
int cnt;

void get_phi(int n){
    phi[1]=1;
    for(int i=2;i<=n;i++){
        if(!vis[i]){
            primes[cnt++]=i;
            phi[i]=i-1;
        }
        for(int j=0;primes[j]<=n/i;j++){
            vis[primes[j]*i]=1;
            if(i%primes[j]==0){
                phi[primes[j]*i]=primes[j]*phi[i];
                break;
            }
            else phi[primes[j]*i]=(primes[j]-1)*phi[i];
        }
    }
}

int main(){
    int n;
    cin>>n;
    get_phi(n);
    LL res=0;
    for(int i=1;i<=n;i++){
        res+=phi[i];
    }
    cout<<res<<endl;
    return 0;
}

4.4 快速幂


概念

  • 快速求出\(a^k\mod p\)的结果

思想

  • 预处理出\(a^{2^0},a^{2^1},a^{2^2}\dots a^{2^{\log_2 k}}\)的结果
    • 则使得\(k=2^{p_1}+2^{p_2}+\dots+2^{p_i}\)
    • 即:\(a^k=a^{2^{p_1}}\times a^{2^{p_2}}\times\dots\times a^{2^{p_i}}\)
  • 对于\(a^{2^0}\times a^{2^0}=a^{2^{1}},a^{2^{1}}\times a^{2^{1}}=a^{2^{2}}\),即
\[a^{2^{p_i}}=a^{2^{p_{i-1}}}\times a^{2^{p_{i-1}}} \]
  • 综上所述,在操作时记录\(a^{2^{p_i}}\)的值,和累乘的结果
  • \(k\)化为二进制表示,按位&gt;>操作,若当前位是\(1\),则对当前累乘的结果\(\times a^{2^{p_i}} \mod p\)
  • 每次对\(p\)取模
\[k $$进行按位`>>`操作后,更新\]

a^{p_{i+1}}=a^{p_i}\times a^{p_i}\mod p

\[ * 当二进制下的$k$无法再`>>`时,累乘结果即为答案 **模板** ```cpp typedef long long LL; LL qmi(LL a, LL k, LL p) { // 计算 a^k % p 的结果 LL res = 1; // 记录累乘结果 while(k) { if(k & 1) res = res * a % p; // k&1得到当前位,若为1则累乘a^{p_i} a = a * a % p; // 更新a^{p_i} k >>= 1; // 右移1位 } return res; } ``` --- #### 4.4.1 快速幂求逆元 --- **概念** * 同余:设$m$为正整数,$a$和$b$是整数,若$m|a-b$,则称$a$模$m$同余于$b$,或$a$与$b$模$m$同余,记作$a\equiv b(\mod m)$ * 若$ab\equiv 1(\mod m)$,则称$b$为$a$的模$m$逆,记作$a^{-1}(\mod m)$或$a^{-1}$ **注意** * $a$的模$m$逆存在 $\Leftrightarrow$ $a$与$m$互质 * 当$m$为质数时,用费马小定理求 * 当$m$不为质数时,用扩展欧几里得算法求 **思想** * 利用快速幂实现$m$为质数时用费马小定理求逆元 * 费马小定理:设$p$为素数,且$a$与$p$互质,则$a^{p-1}\equiv 1(\mod p)$ * \]

\begin{aligned} a^{p-1}\equiv 1(\mod p) \rightarrow &a \times a^{p-2}\equiv 1(\mod p)\ &a \times b\equiv 1(\mod p)\ &\text{即:} b=a^{p-2} \end

\[ --- **模板例题 876. 快速幂求逆元** [原题链接](https://www.acwing.com/problem/content/878/) **描述** 给定 n 组 ai,pi,其中 pi 是质数,求 ai 模 pi 的乘法逆元,若逆元不存在则输出 impossible。 注意:请返回在 0∼p−1 之间的逆元。 > 乘法逆元的定义 若整数 b,m 互质,并且对于任意的整数 a,如果满足 b|a,则存在一个整数 x,使得 a/b≡a×x(\mod m),则称 x 为 b 的模 m 乘法逆元,记为 b^{-1}(\mod m)。 b 存在乘法逆元的充要条件是 b 与模数 m 互质。当模数 m 为质数时,b^{m-2} 即为 b 的乘法逆元。 **输入格式** 第一行包含整数 n。 接下来 n 行,每行包含一个数组 ai,pi,数据保证 pi 是质数。 **输出格式** 输出共 n 行,每组数据输出一个结果,每个结果占一行。 若 ai 模 pi 的乘法逆元存在,则输出一个整数,表示逆元,否则输出 impossible。 **数据范围** 1≤n≤105, 1≤ai,pi≤2∗109 **输入样例:** ``` 3 4 3 8 5 6 3 ``` **输出样例:** ``` 1 2 impossible ``` **代码** ```cpp #include <bits/stdc++.h> using namespace std; typedef long long LL; LL qmi(LL a, LL k, LL p) { LL res = 1; while (k) { if (k & 1) res = res * a % p; a = a * a % p; k >>= 1; } return res; } int main() { int n; cin >> n; while (n--) { int a, p; cin >> a >> p; if (a % p == 0) cout << "impossible" << endl; // a与p不互质则说明无逆元 else cout << qmi(a, p - 2, p) << endl; } return 0; } ``` --- ### 4.5 扩展欧几里得算法 --- **思想** * 欧几里得算法:`gcd(a, b) = gcd(b, a % b)`,特别的 `gcd(a, 0) = a` * 裴蜀定理:对于任意正整数 `a, b`,一定存在非零的 `x, y`,使得 `ax + by = gcd(a, b)` \]

\begin{cases} b=0 \text{时:} & \begin{cases} \gcd(a,b)=a \ ax+by=\gcd(a,b) \end{cases} \Rightarrow \begin{cases} x=1 \ y=0 \end{cases} \ \ b \neq 0 \text{时:} & \begin{aligned} & \text{①设 } ax+by=\gcd(a,b)=d \ & \text{因为由欧几里得算法可知:} \gcd(a,b)=\gcd(b,a\bmod b)=d \ & \text{所以由裴蜀定理得:} b x' + (a\bmod b) y' = d \ & \text{又因为 } ax+by=d \ & \text{所以联立} \begin{cases} ax+by=d \ b x' + (a\bmod b) y' = d \ a\bmod b = a - \lfloor \frac{a}{b} \rfloor b \end{cases} \Rightarrow \begin{cases} x = y' \ y = x' - \lfloor \frac{a}{b} \rfloor y' \end{cases} \ & \text{②设 } a' = b, b' = a\bmod b \ & \text{所以 } \gcd(b,a\bmod b)=\gcd(a',b')=d \ & \text{因为 } \gcd(a',b')=\gcd(b',a'\bmod b')=d \ & \text{所以 } b' x'' + (a'\bmod b') y'' = d \ & \text{又因为 } b x' + (a\bmod b) y' = d \ & \text{所以联立} \begin{cases} b x' + (a\bmod b) y' = d \ b' x'' + (a'\bmod b') y'' = d \ a'\bmod b' = a' - \lfloor \frac{a'}{b'} \rfloor b' \end{cases} \Rightarrow \begin{cases} x' = y'' \ y' = x'' - \lfloor \frac{a'}{b'} \rfloor y'' \end{cases} \ & \text{③设 } a'' = b', b'' = a'\bmod b' \ & \dots \ & \dots \ & \text{直到 } b=0 \text{ 时,联立解得} \begin{cases} x^i = 1 \ y^i = 0 \end{cases} \ & \text{然后逐步返回每一次联立所得的结果} \begin{cases} x^{i-1} = y^i \ y^{i-1} = x^i - \lfloor \frac{a^i}{b^i} \rfloor y^i \end{cases} \text{最后返回得到 } x \text{ 和 } y \text{ 的值} \end{aligned} \end

\[ **注意** * 当方程符合$ax+by=gcd(a,b)$的形式时,才可以用扩展欧几里得算法求解$(x_0,y_0)$ * **推论:** 可以进一步求解任意方程$ax+by=n$,得到一个整数解 \]

\begin{aligned} &\textbf{判断与求解步骤:}\ &(1)\text{判断方程 } ax + by = n \text{ 是否有整数解,有解的条件为:} \gcd(a,b) \mid n\ &(2)\text{用扩展欧几里得算法求 } ax + by = \gcd(a,b) \text{,得到一个特解 } (x_0, y_0)\ &(3)\text{在 } ax_0 + by_0 = \gcd(a,b) \text{ 两边同时乘以 } \frac{n}{\gcd(a,b)} \Rightarrow \frac{ax_0 n}{\gcd(a,b)} + \frac{by_0 n}{\gcd(a,b)} = n\ &(4)\text{对照 } ax + by = n \text{ 可知该方程的一个特解为 } (x', y'),\text{ 其中 } \begin{cases} x' = \dfrac{x_0 n}{\gcd(a,b)} \ y' = \dfrac{y_0 n}{\gcd(a,b)} \end{cases} \end

\[ --- **模板** ```cpp void exgcd(int a, int b, int &x, int &y) { if (!b) { // 若 b = 0 x = 1; y = 0; return; } else { // b != 0 exgcd(b, a % b, x, y); // 递归调用 int t = x; // 递归返回后执行 x = y; y = t - a / b * y; } } ``` --- **例题 877. 扩展欧几里得算法** [原题链接](https://www.acwing.com/problem/content/877/) **描述** 给定 n 对正整数 ai, bi,对于每对数,求出一组 xi, yi,使其满足 ai×xi + bi×yi = gcd(ai, bi)。 **输入格式** 第一行包含整数 n。 接下来 n 行,每行包含两个整数 ai, bi。 **输出格式** 输出共 n 行,对于每组 ai, bi,求出一组满足条件的 xi, yi,每组结果占一行。 本题答案不唯一,输出任意满足条件的 xi, yi 均可。 **数据范围** 1 ≤ n ≤ 10^5, 1 ≤ ai, bi ≤ 2×10^9 **输入样例:** ``` 2 4 6 8 18 ``` **输出样例:** ``` -1 1 -2 1 ``` **代码** ```cpp #include <bits/stdc++.h> using namespace std; void exgcd(int a, int b, int &x, int &y) { if (!b) { x = 1; y = 0; return; } else { exgcd(b, a % b, x, y); int t = x; x = y; y = t - a / b * y; } } int main() { int n; cin >> n; while (n--) { int a, b, x, y; cin >> a >> b; exgcd(a, b, x, y); cout << x << " " << y << endl; } return 0; } ``` --- #### 4.5.1 解一元线性同余方程 --- **概念** * $ax \equiv b \pmod{m}$,即 $\frac{ax}{m}$ 与 $\frac{b}{m}$ 的余数相同,且 $a, b, m$ 为整数,求 $x$ 的值 * 该方程即为一元线性同余方程 **思想** * 对 $ax \equiv b \pmod{m}$ 做等价变形:$ax + my = b$ * \]

\begin{aligned} &\because & ax &\equiv b \pmod{m} \ &\therefore & ax \bmod m &= k (b \bmod m), \quad (k \in \mathbb{Z}) \ &\therefore & ax - \lfloor \frac{ax}{m} \rfloor m &= k \left( b - \lfloor \frac{b}{m} \rfloor m \right) \ &\therefore & ax - k b &= \left( \lfloor \frac{ax}{m} \rfloor - k \lfloor \frac{b}{m} \rfloor \right) m \ &\because & \lfloor \frac{ax}{m} \rfloor, & \lfloor \frac{b}{m} \rfloor, k \in \mathbb{Z} \ &\therefore & \left( \lfloor \frac{ax}{m} \rfloor - k \lfloor \frac{b}{m} \rfloor \right) &\in \mathbb{Z} \ && \text{设 } \left( \lfloor \frac{ax}{m} \rfloor - k \lfloor \frac{b}{m} \rfloor \right) = y, \quad (y \in \mathbb{Z}) \ &\therefore & ax - k b &= m y \Rightarrow ax - m y = b \ \text{又} &\because & y &\text{可以为负数} \ &\therefore & ax &\equiv b \pmod{m} \leftrightarrow ax + m y = \end

\[ **模板** ```cpp #include <bits/stdc++.h> using namespace std; typedef long long LL; void exgcd(LL a, LL b, LL &x, LL &y){ if(!b){ // 若b=0时 x = 1, y = 0; return; } else{ exgcd(b, a % b, x, y); LL t = x; x = y; y = t - a / b * y; } } LL gcd(LL a, LL b){ return b ? gcd(b, a % b) : a; } int main(){ int n; cin >> n; while(n--){ LL a, b, m, x, y; cin >> a >> b >> m; LL d = gcd(a, m); exgcd(a, m, x, y); if(b % d) cout << "impossible" << endl; else cout << (b / d) * x % m << endl; } return 0; } ``` --- ### 4.6 中国剩余定理 --- **概念** * 若存在整数 \( m_1, m_2, m_3, \dots, m_n \) 两两互质,则对于任意的整数 \( a_1, a_2, a_3, \dots, a_n \),一元线性同余方程组 \((S)\) 有通解。 * \]

(S): \begin{cases} x \equiv a_1 \pmod{m_1}\ x \equiv a_2 \pmod{m_2}\ x \equiv a_3 \pmod{m_3}\ \dots\ x \equiv a_n \pmod{m_n} \end

\[ **思想** * 对于 \((S)\) 中的两个式子进行合并。 * \]

\begin{cases} x \equiv a_1 \pmod{m_1}\ x \equiv a_2 \pmod{m_2} \end{cases} \Rightarrow \begin{cases} x = k_1 m_1 + a_1\ x = k_2 m_2 + a_2 \end

\[ * 将等价转换后的两式进行联立,化简得 \( k_1 m_1 + k_2 (-m_2) = a_2 - a_1 \) …… ① * 由扩展欧几里得算法可以得到一组解 \((k_1', k_2')\),使得:\( k_1' m_1 + k_2' (-m_2) = \gcd(m_1, -m_2) \)。 * 若 \(\gcd(m_1, -m_2)\) 不能整除 \(a_2 - a_1\),则无解,否则有通解。 * 设 \( d = \gcd(m_1, -m_2) \),\( y = \frac{a_2 - a_1}{d} \)。 * 由扩展欧几里得算法的推论可知: \]

\begin{cases} k_1 = k_1' y\ k_2 = k_2' y \end

\[* 引入通解: \]

\begin{cases} k_1 = k_1 + k \frac{m_2}{d}\ k_2 = k_2 + k \frac{m_1}{d} \end{cases} \quad (k \in \mathbb{Z})

\[* 将通解代入 ① 得:\( \left( k_1 + k \frac{m_2}{d} \right) m_1 + \left( k_2 + k \frac{m_1}{d} \right) (-m_2) = a_2 - a_1 \)。 * 化简得 $k_1m_1 + k_2(-m_2) = a_2 - a_1$ 和 $①$ 相同,故成立 * 重复上述步骤,不断合并剩余的式子,即可得到最终的通解 --- **例题 204. 表达整数的奇怪方式** [原题链接](https://www.acwing.com/problem/content/206/) **描述** 给定 $2n$ 个整数 $a_1, a_2, \dots, a_n$ 和 $m_1, m_2, \dots, m_n$,求一个最小的非负整数 $x$,满足 $\forall i \in [1, n], x \equiv m_i \pmod{a_i}$。 **输入格式** 第 1 行包含整数 $n$。 第 2 行到第 $n+1$ 行:每行包含两个整数 $a_i$ 和 $m_i$,数之间用空格隔开。 **输出格式** 输出最小非负整数 $x$,如果 $x$ 不存在,则输出 $-1$。如果存在 $x$,则数据保证 $x$ 一定在 64 位整数范围内。 **数据范围** $1 \leq a_i \leq 2^{31}-1,\ 0 \leq m_i &lt; a_i,\ 1 \leq n \leq 25$ **输入样例:** ``` 2 8 7 11 9 ``` **输出样例:** ``` 31 ``` **代码** ```cpp #include <bits/stdc++.h> using namespace std; typedef long long LL; void exgcd(LL a, LL b, LL &x, LL &y) { if (!b) { x = 1, y = 0; return; } else { exgcd(b, a % b, x, y); LL t = x; x = y; y = t - a / b * y; } } LL gcd(LL a, LL b) { return b ? gcd(b, a % b) : a; } int main() { int n; cin >> n; LL x = 0, m1, a1; cin >> m1 >> a1; for (int i = 0; i < n - 1; i++) { LL m2, a2; cin >> m2 >> a2; LL k1, k2; LL d = gcd(m1, m2); exgcd(m1, m2, k1, k2); if ((a2 - a1) % d) { x = -1; break; } k1 *= (a2 - a1) / d; k1 = (k1 % (m2 / d) + m2 / d) % (m2 / d); x = k1 * m1 + a1; LL m = abs(m1 / d * m2); a1 = k1 * m1 + a1; m1 = m; } if (x != -1) x = (a1 % m1 + m1) % m1; cout << x << endl; return 0; } ``` --- ### 4.7 高斯消元 --- **概念** * 利用初等行(列)变换,对一组线性方程组进行消元,把增广矩阵化为阶梯型矩阵 \]

\begin{aligned} \text{已知某线性方程组:}\ &\begin{cases} a_{11}x_1 + a_{12}x_2 + \dots + a_{1n}x_n = b_1 \ a_{21}x_1 + a_{22}x_2 + \dots + a_{2n}x_n = b_2 \ \vdots \ a_{n1}x_1 + a_{n2}x_2 + \dots + a_{nn}x_n = b_n \end{cases} \ \text{增广矩阵为:}\ &\begin{pmatrix} a_{11} & a_{12} & \dots & a_{1n} & b_1 \ a_{21} & a_{22} & \dots & a_{2n} & b_2 \ \vdots & \vdots & \vdots & \vdots & \vdots \ a_{n1} & a_{n2} & \dots & a_{nn} & b_n \end{pmatrix} \ \text{运用初等行变换:}\ &\begin{pmatrix} a_{11} & a_{12} & \dots & a_{1n} & b_1 \ & a_{22} & \dots & a_{2n} & b_2 \ & & \ddots & \vdots & \vdots \ & & & a_{nn} & b_n \end{pmatrix} \ \end

\[ **解的情况** \begin{cases} \text{无解:若在最后化成的上三角矩阵中,主对角线中某个元素为 } 0,\\ \text{但其所在行的最后一列元素不为 } 0 \text{ 时,此时矩阵无解。} \\ \text{无穷解:若在最后化成的上三角矩阵中,存在主对角线中某个元素为 } 0,\\ \text{且其所在行的最后一列元素也为 } 0 \text{ 时,此时矩阵有无穷多解。} \\ \text{唯一解:若在最后化成的上三角矩阵中,主对角线元素全不为 } 0,\\ \text{此时矩阵有唯一解。} \end{cases} **初等行(列)变换** * 某一行乘上一个非零数,矩阵不变。 * 某一行乘上一个常数加到另一行上,矩阵不变。 * 交换矩阵中某两行的元素,矩阵不变。 **思想** * 对增广矩阵的每一列 \(c_i\) 进行枚举,找到当前列中绝对值最大的元素所在的行 \(r_i\)。 * 将 \(r_i\) 行与最上方未确定阶梯型的行进行交换。 * 用初等行变换将 \(r_i\) 行变为原来的 \(k\) 倍,且使得变换后 \(r_i\) 行的第一个主元变成 \(1\)。 * 继续用初等行变换,将 \(r_i\) 行下方的所有的行的 \(c_i\) 列的值变为 \(0\)。 * 重复上述步骤,直到最终得到阶梯型矩阵,判断解的情况。 * 若有解,则从最后一行向上回代,得出方程组的解。 **模板** ```cpp const int N = 110; const double eps = 1e-8; int n; double a[N][N]; int gauss() { int c, r; for (c = 0, r = 0; c < n; c++) { int t = r; for (int i = r; i < n; i++) { // 筛选出所在列元素最大的行 if (fabs(a[i][c]) > fabs(a[t][c])) t = i; } if (fabs(a[t][c]) < eps) continue; for (int j = c; j <= n; j++) swap(a[t][j], a[r][j]); for (int i = n; i >= c; i--) a[r][i] /= a[r][c]; for (int i = r + 1; i < n; i++) { if (fabs(a[i][c]) > eps) { for (int j = n; j >= c; j--) a[i][j] -= a[r][j] * a[i][c]; } } r++; } if (r < n) { for (int i = r; i < n; i++) { if (fabs(a[i][n]) > eps) return 0; // 无解 } return 2; // 无穷多解 } for (int i = n - 1; i >= 0; i--) { for (int j = i + 1; j < n; j++) a[i][n] -= a[i][j] * a[j][n]; } return 1; // 唯一解 } ``` **主要修复内容:** | 问题 | 修复 | |------|------| | 代码被拆分到多个语言标签的代码块中(`java` 和 `cpp` 交替) | 合并为一个完整的 `cpp` 代码块 | | 缩进不一致、代码块之间存在空行 | 统一缩进,整理格式 | | 末尾 `main` 函数缺少 `return 0;` 和 `}` | 补全结尾 | | 运算符前后缺少空格(如 `c=0,r=0`) | 统一添加空格,提高可读性 | | 注释中逗号不规范(`为0,这时` → `为0,这时`) | 统一使用中文标点 | --- #### 4.7.1 高斯消元解线性方程组 --- **模板例题 883. 高斯消元解线性方程组** [原题链接](https://www.acwing.com/problem/content/885/) **描述** 输入一个包含 n 个方程 n 个未知数的线性方程组。方程组中的系数为实数。求解这个方程组。 下图为一个包含 m 个方程 n 个未知数的线性方程组示例: \]

\begin{cases} a_{11}x_1 + a_{12}x_2 + \dots + a_{1n}x_n = b_1 \ a_{21}x_1 + a_{22}x_2 + \dots + a_{2n}x_n = b_2 \ \vdots \ a_{m1}x_1 + a_{m2}x_2 + \dots + a_{mn}x_n = b_m \ \end

\[ **输入格式** 第一行包含整数 n。 接下来 n 行,每行包含 n+1 个实数,表示一个方程的 n 个系数以及等号右侧的常数。 **输出格式** 如果给定线性方程组存在唯一解,则输出共 n 行,其中第 i 行输出第 i 个未知数的解,结果保留两位小数。 如果给定线性方程组存在无数解,则输出 `Infinite group solutions`。 如果给定线性方程组无解,则输出 `No solution`。 **数据范围** 1≤n≤100,所有输入系数以及常数均保留两位小数,绝对值均不超过 100。 **输入样例:** ``` 3 1.00 2.00 -1.00 -6.00 2.00 1.00 -3.00 -9.00 -1.00 -1.00 2.00 7.00 ``` **输出样例:** ``` 1.00 -2.00 3.00 ``` **代码** ```cpp #include <bits/stdc++.h> using namespace std; const int N = 110; const double eps = 1e-8; int n; double a[N][N]; int gauss() { int c, r; for (c = 0, r = 0; c < n; c++) { int t = r; for (int i = r; i < n; i++) { // 筛选出所在列元素最大的行 if (fabs(a[i][c]) > fabs(a[t][c])) t = i; } if (fabs(a[t][c]) < eps) continue; // 正对角线中有元素为0,这时有无穷解和无解 if (t != r) for (int i = c; i <= n; i++) swap(a[t][i], a[r][i]); // 若t改变,则交换两行 for (int i = n; i >= c; i--) a[r][i] /= a[r][c]; // 将所在行所在列元素变为1 for (int i = r + 1; i < n; i++) { // 将所在行所在列的下方的行的所在列元素变为0 if (fabs(a[i][c]) > eps) { for (int j = n; j >= c; j--) a[i][j] -= a[r][j] * a[i][c]; } } r++; } if (r < n) { // row < n,即对角线中元素为0的行未被算上 for (int i = r; i < n; i++) { if (fabs(a[i][n]) > eps) return 2; } return 1; } for (int i = n - 1; i >= 0; i--) { // 将非主元位置的A系数矩阵的其他x消去 for (int j = i + 1; j < n; j++) a[i][n] -= a[j][n] * a[i][j]; } return 0; } int main() { cin >> n; for (int i = 0; i < n; i++) { for (int j = 0; j <= n; j++) { cin >> a[i][j]; } } int t = gauss(); if(t==2){ cout<<"No solution"<<endl; } else if(t==1) cout<<"Infinite group solutions"<<endl; else{ for(int i=0;i<n;i++){ if(fabs(a[i][n])<eps) a[i][n]=0; printf("%.2lf\n",a[i][n]); } } return 0; } ``` --- #### 4.7.2 高斯消元解异或线性方程组 --- **思想** * 将解线性方程组的计算化为异或运算 **模板例题 884. 高斯消元解异或线性方程组** [原题链接](https://www.acwing.com/problem/content/886/) **描述** 输入一个包含 n 个方程 n 个未知数的异或线性方程组。 方程组中的系数和常数为 0 或 1,每个未知数的取值也为 0 或 1。 求解这个方程组。 异或线性方程组示例如下: M[1][1]x[1] ^ M[1][2]x[2] ^ … ^ M[1][n]x[n] = B[1] M[2][1]x[1] ^ M[2][2]x[2] ^ … ^ M[2][n]x[n] = B[2] … M[n][1]x[1] ^ M[n][2]x[2] ^ … ^ M[n][n]x[n] = B[n] 其中 ^ 表示异或(XOR),M[i][j] 表示第 i 个式子中 x[j] 的系数,B[i] 是第 i 个方程右端的常数,取值均为 0 或 1。 **输入格式** 第一行包含整数 n。 接下来 n 行,每行包含 n+1 个整数 0 或 1,表示一个方程的 n 个系数以及等号右侧的常数。 **输出格式** 如果给定线性方程组存在唯一解,则输出共 n 行,其中第 i 行输出第 i 个未知数的解。 如果给定线性方程组存在多组解,则输出 `Multiple sets of solutions`。 如果给定线性方程组无解,则输出 `No solution`。 **数据范围** 1≤n≤100 **输入样例:** ``` 3 1 1 0 1 0 1 1 0 1 0 0 1 ``` **输出样例:** ``` 1 0 0 ``` **代码** ```cpp #include <bits/stdc++.h> using namespace std; const int N = 105; int n; int a[N][N]; int guass() { int r = 0; for (int c = 0; c < n; c++) { int t = r; for (int i = r + 1; i < n; i++) { if (a[i][c]) { t = i; break; } } if (!a[t][c]) continue; for (int j = 0; j <= n; j++) swap(a[r][j], a[t][j]); for (int i = r + 1; i < n; i++) { if (a[i][c]) { for (int j = n; j >= c; j--) a[i][j] ^= a[r][j]; } } r++; } if (r < n) { for (int i = r; i < n; i++) { if (a[i][n]) return 2; } return 1; } for (int i = n - 1; i >= 0; i--) { for (int j = i + 1; j < n; j++) { a[i][n] ^= a[i][j] * a[j][n]; } } return 0; } int main() { cin >> n; for (int i = 0; i < n; i++) { for (int j = 0; j < n + 1; j++) { cin >> a[i][j]; } } int t = guass(); if (t == 0) { for (int i = 0; i < n; i++) cout << a[i][n] << endl; } else if (t == 1) { cout << "Multiple sets of solutions" << endl; } else { cout << "No solution" << endl; } return 0; } ``` --- ### 4.8 组合数 --- **概念** * 从 $n$ 个不同元素中每次取出 $m$ 个不同元素,不管其顺序合成一组,称为从 $n$ 个元素中不重复地选取 $m$ 个元素的一个组合。 * 所有这样的组合的种数称为组合数 **公式** * $C_n^m = \frac{n!}{m!(n-m)!},\quad C_n^0 = C_n^n = 1$ * $C_n^m = C_n^{n-m}$ \]

C_n^m = C_{