基础数学算法
目录
- 质数
- 试除法判定质数
- 分解质因数
- 线性筛法求质数
- 约数
- 试除法求约数
- 约数个数
- 约数之和
- 最大公约数(gcd)算法
- Euler函数
- 容斥原理证明欧拉函数
- 线性筛法求欧拉函数
- 快速幂, 龟速乘
- 快速幂算法
- 快速幂求逆元
- 龟速乘算法
- 扩展欧几里得算法
质数
试除法判定质数

根据定义 [ 1 , n ] [1, n] [1,n]中与 n n n互质的数的个数只有 1 1 1和他本身, 也就是约数个数为 0 0 0
, 因为约数是成对出现的, 只需要枚举较小的约数, 也就是 d ≤ n d d \le \frac{n}{d} d≤dn, 推理得到 d 2 ≤ n d ^ 2 \le n d2≤n, 算法时间复杂度 O ( n ) O(\sqrt n) O(n)
#include <bits/stdc++.h>using namespace std;int n;bool is_prime(int val) {if (val < 2) return false;for (int i = 2; i <= val / i; ++i) {if (val % i == 0) return false;}return true;
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n;while (n--) {int val;cin >> val;cout << (is_prime(val) ? "Yes" : "No") << '\n';}return 0;
}
分解质因数

以为一个数的质因数只能从 2 2 2开始, 从小到大枚举所有的因子, 尝试 n n n的所有约数
算法时间复杂度 O ( n ) O(n) O(n)
引理: n n n中最多只包含一个大于 n \sqrt n n的质因子
证明: 假设存在两个大于 n \sqrt n n的质因子 p p p和 q q q, 因为 p p p和 q q q都是 n n n的质因子, 并且 p p p和 q q q互质, 因此 p × q p \times q p×q也是 n n n的质因子, 但是因为 p × q > n p \times q > n p×q>n, 因此矛盾
大于 ( n ) \sqrt(n) (n)的质因子指数只能是 1 1 1, 因为如果指数$ > 1 , 那么分解质因数的结果 , 那么分解质因数的结果 ,那么分解质因数的结果 > n, 矛盾
因此 n n n分解质因数后至多存在一个大于 n \sqrt n n的质因子, 并且指数是 1 1 1
优化后算法时间复杂度 O ( n ) O(\sqrt n) O(n)
#include <bits/stdc++.h>using namespace std;const int N = 110;int n;void solve() {int val;cin >> val;for (int i = 2; i <= val / i; ++i) {if (val % i == 0) {int cnt = 0;while (val % i == 0) {val /= i;cnt++;}if (cnt > 0) cout << i << ' ' << cnt << '\n';}}if (val > 1) cout << val << ' ' << 1 << '\n';cout << '\n';
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n;while (n--) solve();return 0;
}
线性筛法求质数

算法时间复杂度 O ( n ) O(n) O(n)
#include <bits/stdc++.h>using namespace std;const int N = 1e6 + 10;int n;
bool st[N];
int primes[N], cnt = 0;void get_primes() {for (int i = 2; i <= n; ++i) {if (!st[i]) primes[cnt++] = i;for (int j = 0; i * primes[j] <= n; ++j) {st[i * primes[j]] = true;if (i % primes[j] == 0) break;}}
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n;get_primes();cout << cnt << '\n';return 0;
}
关于线性筛法的证明
因为每个合数只会被它的最小质因子筛掉, 因此算法是正确的并且是线性的
为什么只会被最小质因子筛掉, 分情况进行讨论
- 情况 1 1 1, 因为第二维循环的质因子是从小到大枚举的, 当枚举到 i m o d p j = 0 i \mod p_j = 0 imodpj=0的时候, 说明 p j p_j pj是 i i i的最小质因子, 因此 p j p_j pj也是 i × p j i \times p_j i×pj的最小质因子, 因此将 i × p j i \times p_j i×pj筛掉
- 情况 2 2 2, 因为第二维循环的质因子是从小到大枚举的, 当没有枚举到 i m o d p j = 0 i \mod p_j = 0 imodpj=0的时候, p j p_j pj一定是 p j × i p_j \times i pj×i的最小质因子, 也需要筛掉
综上, 只要是从小到大枚举质因子, 就需要筛掉, 直到当前的 p j p_j pj已经是 i i i的最小质因子了, 退出循环, 保证算法的线性时间复杂度
约数
试除法求约数

因为约数是成对出现的, 因此只需要枚举 d ≤ n d \le \sqrt n d≤n的情况, 并且大于 n \sqrt n n的约数只能是质数并且只能有一个, 计算约数后还需要去重排序, 算法时间复杂度 O ( n + n log n ) O(\sqrt n + n\log n) O(n+nlogn)
#include <bits/stdc++.h>using namespace std;const int N = 110;int n;void solve() {int val;vector<int> nums;cin >> val;for (int i = 1; i <= val / i; ++i) {if (val % i == 0) {nums.push_back(i);if (val / i != i) nums.push_back(val / i);}}sort(nums.begin(), nums.end());for (int d : nums) cout << d << ' ';cout << '\n';
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n;while (n--) solve();return 0;
}
约数个数

根据乘法原理, 约数个数 c n t = ( α 0 + 1 ) × ( α 1 + 1 ) × . . . × ( α k + 1 ) cnt = (\alpha _0 + 1) \times (\alpha _1 + 1) \times ... \times (\alpha _k + 1) cnt=(α0+1)×(α1+1)×...×(αk+1), 试除法分解质因数过程中求解指数的大小, 算法时间复杂度 O ( n ) O(\sqrt n) O(n)
#include <bits/stdc++.h>using namespace std;typedef long long LL;
const int MOD = 1e9 + 7;int n;
unordered_map<int, int> mp;void solve() {int val;cin >> val;for (int i = 2; i <= val / i; ++i) {while (val % i == 0) {val /= i;mp[i]++;}}if (val > 1) mp[val]++;
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n;while (n--) solve();LL res = 1;for (auto [x, y] : mp) res = res * (y + 1) % MOD;cout << res << '\n';return 0;
}
约数之和

根据算术基本定理, 约数之和
s u m = ( p 1 0 + p 1 1 + . . . + p 1 α 1 ) ( p 2 0 + p 2 1 + . . . + p 2 α 2 ) . . . ( p k 0 + p k 1 + . . . + p k α k ) sum = (p _1 ^ 0 + p_1 ^ 1 + ... + p_1 ^ {\alpha_{1}})(p _2 ^ 0 + p_2 ^ 1 + ... + p_2 ^ {\alpha_{2}}) ... (p _k ^ 0 + p_k ^ 1 + ... + p_k ^ {\alpha_{k}}) sum=(p10+p11+...+p1α1)(p20+p21+...+p2α2)...(pk0+pk1+...+pkαk)
依旧是分解质因数, 计算结果要取模, 算法时间复杂度 O ( n ) O(\sqrt n) O(n)
注意在计算 t m p tmp tmp过程中要取模, 秦九韶算法求每一项, 内部的算法时间复杂度 O ( n ) O(n) O(n)
总的算法时间复杂度 O ( n n ) O(n \sqrt n) O(nn)
#include <bits/stdc++.h>using namespace std;typedef long long LL;
const int MOD = 1e9 + 7;int n;
unordered_map<int, int> mp;void solve() {int val;cin >> val;for (int i = 2; i <= val / i; ++i) {while (val % i == 0) {val /= i;mp[i]++;}}if (val > 1) mp[val]++;
}int main() {ios::sync_with_stdio(false);cin.tie(0);cin >> n;while (n--) solve();LL res = 1;for (auto [x, y] : mp) {if (x != 0) {LL tmp = 1;// 秦九韶算法求p ^ 0 + p ^ 1 + ... + p ^ nwhile (y--) tmp = (tmp * x + 1) % MOD;res = res * tmp % MOD;}}cout << res << '\n';return 0;
}
最大公约数(gcd)算法

求最大公约数有两种算法, 欧几里得算法和更相减损法, 这里只讨论欧几里得算法
算法核心 gcd ( a , b ) = gcd ( b , a m o d b ) \gcd(a, b) = \gcd(b, a \mod b) gcd(a,b)=gcd(b,amodb)
由 d ∣ a ,可设 a = d ⋅ m ( m ∈ Z ) 由 d ∣ b ,可设 b = d ⋅ n ( n ∈ Z ) 那么: a x + b y = ( d ⋅ m ) x + ( d ⋅ n ) y = d ( m x + n y ) 因为 m , n , x , y ∈ Z ,所以 m x + n y ∈ Z 因此 d ∣ ( a x + b y ) \begin{aligned} &\text{由 } d \mid a \text{,可设 } a = d \cdot m \quad (m \in \mathbb{Z}) \\ &\text{由 } d \mid b \text{,可设 } b = d \cdot n \quad (n \in \mathbb{Z}) \\ &\text{那么:} \\ &ax + by = (d \cdot m)x + (d \cdot n)y = d(mx + ny) \\ &\text{因为 } m, n, x, y \in \mathbb{Z} \text{,所以 } mx + ny \in \mathbb{Z} \\ &\text{因此 } d \mid (ax + by) \end{aligned} 由 d∣a,可设 a=d⋅m(m∈Z)由 d∣b,可设 b=d⋅n(n∈Z)那么:ax+by=(d⋅m)x+(d⋅n)y=d(mx+ny)因为 m,n,x,y∈Z,所以 mx+ny∈Z因此 d∣(ax+by)
算法时间复杂度 O ( log k ) O(\log k) O(logk)
#include <bits/stdc++.h>using namespace std;int gcd(int a, int b) {return b ? gcd(b, a % b) : a;
}int main() {ios::sync_with_stdio(false);cin.tie(0);int n;cin >> n;while (n--) {int a, b;cin >> a >> b;cout << gcd(a, b) << '\n';}return 0;
}
Euler函数
容斥原理证明欧拉函数

将 n n n分解质因数, 算法时间复杂度 O ( n ) O(\sqrt n) O(n), 一个数的质因子最大数量是 log n \log n logn, 因此求一个数字的欧拉函数算法时间复杂度 O ( log m × m ) O(\log m \times \sqrt m) O(logm×m), 一共有 n n n个数字, 算法时间复杂度 O ( O ( n × log m × m ) ) O(O(n \times \log m \times \sqrt m)) O(O(n×logm×m))
注意为了避免超出 i n t int int范围, 先做除法再做乘法
#include <bits/stdc++.h>using namespace std;int n;int solve() {int val;cin >> val;double res = val;for (int i = 2; i <= val / i; ++i) {if (val % i == 0) {res = res / i * (i - 1);while (val % i == 0) val /= i;}}if (val > 1) res = res / val * (val - 1);return res;
}int main() {ios::sync_with_stdio(false);cin.tie(0), cout.tie(0);cin >> n;while (n--) {int res = solve();cout << res << '\n';}return 0;
}
线性筛法求欧拉函数

在线性筛质数基础上, 计算欧拉函数, 算法时间复杂度 O ( n ) O(n) O(n)
下面是证明过程
如果当前数字是质数 i i i, 那么 ϕ ( i ) = p − 1 \phi(i) = p - 1 ϕ(i)=p−1
如果当前数字 p p p是 i i i的最小质因子, ϕ ( i ) = i × k × p − 1 p \phi (i) = i \times k \times \frac{p - 1}{p} ϕ(i)=i×k×pp−1, 因此 ϕ ( i × j ) = p × ϕ ( i ) \phi (i \times j) = p \times \phi (i) ϕ(i×j)=p×ϕ(i)
如果当前数字 p p p比 i i i的最小质因子小, 那么 p p p是 i × p i \times p i×p的最小质因子, 那么 ϕ ( i × j ) = ϕ ( i ) × p × p − 1 p \phi (i \times j) = \phi (i) \times p \times \frac{p - 1}{p} ϕ(i×j)=ϕ(i)×p×pp−1
#include <bits/stdc++.h>using namespace std;typedef long long LL;
const int N = 1e6 + 10;int n;
bool st[N];
int primes[N], phi[N], cnt;void init() {phi[1] = 1;for (int i = 2; i <= n; ++i) {if (!st[i]) {phi[i] = i - 1;primes[cnt++] = i;}for (int j = 0; primes[j] <= n / i; ++j) {st[i * primes[j]] = true;if (i % primes[j] == 0) {phi[i * primes[j]] = primes[j] * phi[i];break;}else phi[i * primes[j]] = phi[i] * (primes[j] - 1);}}
}int main() {ios::sync_with_stdio(false);cin.tie(0), cout.tie(0);cin >> n;init();LL res = 0;for (int i = 1; i <= n; ++i) res += (LL) phi[i];cout << res << '\n';return 0;
}
快速幂, 龟速乘
快速幂算法

快速幂算法核心是反复平方法, 假设计算 a b m o d M a ^ b \mod M abmodM, 可以预处理 a 2 0 , a 2 1 , a 2 2 , . . . a ^ {2 ^ 0}, a ^ {2 ^ 1}, a ^ {2 ^ 2}, ... a20,a21,a22,... , 然后将 b b b展开为二进制数, 有 1 1 1的地方说明要参与运算, 算法时间复杂度 O ( log b ) O(\log b) O(logb)
#include <bits/stdc++.h>using namespace std;typedef long long LL;int pow(int a, int b, int p) {a = a % p;int res = 1;while (b) {if (b & 1) res = (LL) res * a % p;a = (LL) a * a % p;b >>= 1;}return res % p;
}int main() {ios::sync_with_stdio(false);cin.tie(0), cout.tie(0);int n;cin >> n;while (n--) {int a, b, p;cin >> a >> b >> p;cout << pow(a, b, p) << '\n';}return 0;
}
快速幂求逆元

乘法逆元的意思就是 a b ≡ a × x ( m o d n ) \frac {a}{b} \equiv a \times x (\mod n) ba≡a×x(modn), x x x就是乘法逆元, 也就是在 m o d n \mod n modn的情况下, 可以将除法转化为乘法
欧拉定理: a ϕ ( n ) ≡ 1 ( m o d n ) a ^ {\phi (n)} \equiv 1 (\mod n) aϕ(n)≡1(modn), 其中 a a a与 n n n互质
欧拉定理的证明: 对于数字 n n n的质因子如下 a 1 , a 2 , a 3 , . . . , a ϕ ( n ) a_1, a_2, a_3, ..., a_{\phi(n)} a1,a2,a3,...,aϕ(n), 一共 ϕ ( n ) \phi(n) ϕ(n)个, 那么因为 a a a与 n n n会互质, 因此 a ⋅ a 1 , a ⋅ a 2 , . . . , a ⋅ a ϕ ( n ) a\cdot a_1, a \cdot a_2, ... , a\cdot a_{\phi(n)} a⋅a1,a⋅a2,...,a⋅aϕ(n)也与 n n n互质, 但是因为 1 1 1到 n n n中与 n n n互质的数的是确定的, 两段其实是一个集合, 也就是有 a 1 × a 2 × . . . × a ϕ ( n ) = a ⋅ a 1 × a ⋅ a 2 × . . . × a ⋅ a ϕ ( n ) a_1 \times a_2 \times... \times a_{\phi(n)} = a\cdot a_1 \times a \cdot a_2\times ... \times a\cdot a_{\phi(n)} a1×a2×...×aϕ(n)=a⋅a1×a⋅a2×...×a⋅aϕ(n)
也就是等于 a ϕ ( n ) × k = k a ^ {\phi(n)}\times k = k aϕ(n)×k=k, k = a 1 × a 2 × . . . × a ϕ ( n ) k = a_1 \times a_2 \times... \times a_{\phi(n)} k=a1×a2×...×aϕ(n), 因为每一个质因子都与 n n n互质, 乘起来也与 n n n互质, 因此有 a ϕ ( n ) ≡ 1 ( m o d n ) a ^ {\phi (n)} \equiv 1 (\mod n) aϕ(n)≡1(modn), 证毕
费马小定理: a p − 1 ≡ 1 ( m o d p ) a ^ {p - 1} \equiv 1 (\mod p) ap−1≡1(modp)
根据费马小定理, 计算乘法逆元的过程可以表示为 a ⋅ a p − 2 ≡ 1 ( m o d n ) a\cdot a ^ {p - 2} \equiv 1 (\mod n) a⋅ap−2≡1(modn), a p − 2 a ^ {p - 2} ap−2就是 a a a在 m o d p \mod p modp下的乘法逆元, 其中 a a a与 n n n互质
利用快速幂计算 a p − 2 a ^ {p - 2} ap−2, 时间复杂度 O ( log b ) O(\log b) O(logb)
#include <bits/stdc++.h>using namespace std;typedef long long LL;LL pow(int a, int b, int p) {LL res = 1 % p;a %= p;b %= p;while (b) {if (b & 1) res = (LL) res * a % p;b >>= 1;a = (LL) a * a % p;}return res;
}int main() {ios::sync_with_stdio(false);cin.tie(0);int n;cin >> n;while (n--) {int a, p;cin >> a >> p;if (a % p == 0) {cout << "impossible" << '\n';continue;}LL res = pow(a, p - 2, p);cout << res << '\n';}return 0;
}
龟速乘算法

将 b b b拆分为二进制数, 预处理
a × 2 0 , a × 2 1 , a × 2 2 , a × 2 3 , … a \times 2^0, a \times 2^1, a \times 2^2, a \times 2^3, \ldots a×20,a×21,a×22,a×23,…
然后利用二进制拼凑的思想, 凑出 b b b
正常的计算乘法时间复杂度是 O ( 1 ) O(1) O(1), 但是该算法的时间复杂度是 O ( log b ) O(\log b) O(logb)
#include <bits/stdc++.h>using namespace std;typedef long long LL;LL slow_mul(LL a, LL b, LL p) {LL res = 0;while (b) {if (b & 1) res = (res + a) % p;b >>= 1;a = (a + a) % p;}return res;
}int main() {ios::sync_with_stdio(false);cin.tie(0);LL a, b, p;cin >> a >> b >> p;cout << slow_mul(a, b, p) << '\n';return 0;
}
扩展欧几里得算法

裴蜀定理:

