数论笔记-整除
约定:
不加说明的数,默认是整数,且以其涉及的各运算定义域的交集为准。
整除
整除的定义与基本性质
定义 设 a,b∈Za,b∈Z 且 a≠0a≠0,若 bb 除以 aa 余数为 00 ,则称 aa 整除 bb ,记为 a∣ba∣b 。若 aa 不能整除 bb ,则称 a∤ba∤b 。
性质1 a∣b⟺−a∣b⟺a∣−b⟺|a|∣|b|a∣b⟺−a∣b⟺a∣−b⟺|a|∣|b| 。
性质2 a∣b 且 b∣c⇒a∣ca∣b 且 b∣c⇒a∣c 。
性质3 a∣b⇒a∣kba∣b⇒a∣kb ,其中 k∈Z 。
性质4 a∣b⟺a∣kb ,其中 gcd(a,k)=1 。
性质5 a∣b⟺ka∣kb ,其中 k∈Z∗。
性质6 a∣b 且 a∣c⇒a∣kb+mc ,其中 k,m∈Z 。
性质7 a∣b 且 b∣a⇒a=±b 。
性质8 a∣b⇒|a|≤|b| ,其中 b≠0 。
性质9 当 c=ka+b ,则 a∣b⟺a∣c ,其中 a≠0,k∈Z 。
性质10 a=kb±c⇒a,b 的公因数与 b,c 的公因数相同 。
性质11 ai∣b(i=1,2,⋯,k)⟺A∣b,A=lcm(a1,a2,⋯,ak) 。
性质12 m!∣n!(n−m)! ,其中 n,m∈N,m≤n 。
性质10的证明:
设 a,b 任意公因数为 d ,那么 a=kb±c⇒c=±(a−kb) ,因此 c 也有因数 d ,所以 b,c 有公因数 d 。
设 b,c 任意公因数为 d , 那么显然 a 也有因数 d ,所以 a,b 具有公因数 d 。
综上 a,b 的公因数与 b,c 的公因数相同。
性质11的证明:
充分性:
可由整数唯一分解定理的指数表示法证明,详见 gcd 与 lcm 的其他性质。
必要性:
易知 ai∣lcm(a1,⋯,ak) ,由性质1可得, ai∣b(i=1,2,⋯,k) 。
性质12的证明:
由组合数 (nm)=n!m!(n−m)!∈Z+ ,可得 m!∣n!(n−m)! 。
当然也可以先通过质因子相关证明整除关系,再推出组合数一定是正整数,但比较复杂。
素数
素数的定义与基本性质
定义 在正整数中,只有 1 和它本身两个正因数的数称为素数(质数),否则称为合数。
约定 1 既不是素数,也不是合数。
偶素数 2 是唯一的偶素数。
性质1 对于任意整数 n≥3 ,n,n+1 中必有一个不是素数。
性质2 素数有无穷多个。
性质3 存在任意长的连续区间,其中所有数都是合数。
素数定理
素数分布函数的定义 [1,n] 中素数的个数记作 π(n) ,称为素数分布函数。
定理1(素数定理) 素数分布存在渐进,即 π(n)∼nlnn,n→∞ 。
- 推论1(定理1的推论) 对于 n∈Z+ ,有 π(n)≈nlnn 。
定理2 int
范围内的素数间隔是小于 319 , long long
范围内的素数间隔小于 1525 。
伯特兰-切比雪夫定理
定理1(伯特兰-切比雪夫定理) 对于任意正整数 n≥4 ,存在质数 p 满足 n<p<2n−2 。
- 推论1(定理1的推论) 对于任意正整数 n≥2 ,存在质数 p 满足 n<p<2n 。
素数判定
试除法
一个数 n 若是合数,则一定在 [1,√n] 中存在一个质数整除 n 。
证明:
设 d 是 n 的一个质因子,则 nd 也能整除 n 。假设 d≤nd ,则 d≤√n 。
时间复杂度 O(√n)
空间复杂度 O(1)
bool is_prime(int n) { if (n == 2) return 1; if (n == 1) return 0; for (int i = 2;i * i <= n;i++) if (!(n % i)) return 0; return 1; }
kn+i 法
试除法的升级版,常数更小。
一个数 n 若是合数,则一定存在 [1,√n] 的质因子。因此,在试除法的基础上,枚举因子时只考虑可能成为质因子的因子。
例如 k=30 时,只有 i=1,7,11,13,17,19,23,29 时的数才有可能成为质因子,其他情况都与 30 有非 1 的公因子一定不是素数。如此,算法计算量变为原来的 415 。
k=30 时,时间复杂度 O(4√n15)
空间复杂度 O(1)
bool is_prime(int n) { if (n == 2 || n == 3 || n == 5) return 1; if (n == 1 || !(n % 2) || !(n % 3) || !(n % 5)) return 0; int a[8] = { 4,2,4,2,4,6,2,6 }, p = 0; for (int i = 7;i * i <= n;i += a[p++], p %= 8) if (!(n % i)) return 0; return 1; }
预处理法
欧拉筛 O(√n) 预处理所有素数后,试除法可以直接枚举质因子。
根据素数定理,素数占比约为 2lnn ,因此复杂度变为原来的 2lnn。
时间复杂度 O(2√nlnn)
空间复杂度 O(√n)
Miller-Rabin素性测试
Miller-Rabin素性测试通常用于判定大数(远超 264 范围)是否为素数,其是费马素性测试的改进版。
众所周知,费马小定理:
当 a 不是 p 的倍数,若 p 为素数,则 ap−1≡1(modp) 。
但其逆命题:
当 a 不是 p 的倍数,若 ap−1≡1(modp) ,则 p 为素数。
并不总是成立。例如,2341−1≡1(mod341),31105−1≡1(mod1105) ,这些数称为费马伪素数。
但事实上,费马伪素数的概率并不是很大。我们可以对一个数 p 取 k 个 a 测试,若存在 ap−1≢1(modp) ,则 p 一定是合数;否则,p 很大概率是素数。这个过程称为费马素性测试,时间复杂度为 O(klogn) 。可惜的是,到 long long
范围,费马素性测试的准确性就已经开始不高了。因此,在此基础上有了改进的算法Miller-Rabin素性测试 。
Miller-Rabin素性测试在费马素性测试的基础上增加了二次探测定理的使用。二次探测定理:
若 p 为奇素数,则 x2≡1(modp) 的解为 x≡±1(modp) 。特别地,在 [0,p−1] 的解为 x=1 或 x=p−1 。
因为只能探测奇素数,所以偶数要开始前判掉。
我们先将 p−1 拆分为 2rm (m 为奇数,r≥1 ),随后考虑对 x=am,a2m,⋯,a2r−1m 共 r 个数进行二次探测。根据费马素性测试,这个 x 序列探测到最后的结果应是 1 。我们对出现 1 的情况分类讨论:
- 如果一开始 am≡±1(modp) 成立,后续探测全是 x2≡1(modp) 就不需要判断了。
- 若不成立,则一定要在前 r−1 次探测内先得到 x2≡−1(modp) ,否则最后一定出现结果不为 1 或者出现 x2≡1(modp) 但 x≢±1(modp) 的情况,则 p 为合数。
判定大数 n 是否为素数的具体步骤:
- 特判 n=1,2 以及其他所有偶数。
- 将 n−1 拆分成 2rm ,若 am≡±1(modn) ,则后续全是 1 不需要判断,否则下一步。
- 枚举 k 个(通常为 8 到 10 个) a∈[1,n−1] 保证不是 n 的倍数,对 x=am,a2m,⋯,a2r−2m 进行共 r−1 次二次探测,判断是否在 r−1 次内探测到 1 之前探测到 −1 。
- 若 k 次测试都通过,则 n 大概率为素数;某次没通过就一定是合数。
时间复杂度 O(klogn)
空间复杂度 O(1)
template<class T> T randint(T l, T r) { static mt19937_64 eng(chrono::steady_clock::now().time_since_epoch().count()); uniform_int_distribution<T> dis(l, r); return dis(eng); } ll qpow(ll a, ll k, ll P) { ll ans = 1; while (k) { if (k & 1) ans = (__int128_t)ans * a % P; k >>= 1; a = (__int128_t)a * a % P; } return ans; } bool miller_rabin(ll n, int k = 10) {//8-10次 if (n == 2) return 1; if (n == 1 || !(n & 1)) return 0; int r = __builtin_ctzll(n - 1); ll m = n - 1 >> r; while (k--) { ll x = qpow(randint(1LL, n - 1), m, n); if (x == 1 || x == n - 1) continue;//直接满足,否则r-1次内必须有n-1 for (int i = 1;i <= r - 1 && x != 1 && x != n - 1;i++) x = (__int128_t)x * x % n;//二次探测 if (x != n - 1) return 0;//未经过n-1 } return 1; }
素数筛法
埃氏筛
素数的倍数一定是合数,合数的倍数一定被某个质因子的倍数筛掉了,因此我们只需要筛掉素数的倍数。
在 2×107 内,还是能跑到 1 秒以内的,再大就不行了。
时间复杂度 O(nloglogn)
空间复杂度 O(n)
const int N = 1e7 + 7; bool vis[N]; vector<int> prime; void get_prime(int n) { for (int i = 2;i <= n;i++) { if (vis[i]) continue; prime.push_back(i); for (int j = 2;j * i <= n;j++) vis[i * j] = 1; } }
欧拉筛(线性筛)
埃氏筛的时间复杂度已经很优秀了,但依旧会造成一个合数被筛多次的情况,我们希望每个合数都只被筛一次。因此,我们有欧拉筛,每个合数只会被其最小质因子筛掉。
证明:
假设对于 i∈[2,n] 的每个数,设其最小质因子为 p ,我们只筛到其 p 倍。
先证明任意合数 n 都能被最小质因子筛一次。
任意合数 n ,设其最小质因子为 p′ ,则 i=np′ ,那么有 p′≤p ,因此 n 一定能在 i 的 p 倍时或者之前被其最小质因子 p′ 筛掉。
再证明任意合数 n 不会被非最小质因子筛掉。
任意合数 n ,设其最小质因子为 p′ ,其他任意质因子为 p″, 有 p″>p′ ,则 i=np″ 的最小质因子 p=p′<p″ ,因此 n 根本不会被某个数的 p″ 倍筛掉。
因此,我们对每个数只筛到其最小质因子倍,就能保证筛掉的每个数只会被其最小质因子筛一次。
时间复杂度 O(n)
空间复杂度 O(n)
const int N = 1e7 + 7; bool vis[N]; vector<int> prime; void get_prime(int n) { for (int i = 2;i <= n;i++) { if (!vis[i]) prime.push_back(i); for (auto j : prime) { if (i * j > n) break; vis[i * j] = 1; if (!(i % j)) break; } } }
反素数
反素数的定义与基本性质
定义 对于正整数 n ,满足任何小于 n 的正整数的因子个数都小于 n 的因子个数,称为反素数。
性质1 [1,n] 中的数 x 是反素数,当且仅当 x 是相同因子个数的数中最小的。
显然对于任意整数 n∈[1,231] ,其质因子不会超过 10 个,且质因子的指数之和不会超过 31 。
性质2 当 x∈[1,n],n≤231 ,则 x 是反素数的必要条件是 x=2c1×3c2×5c3×7c4×11c5×13c6×17c7×19c8×23c9×29c10 ,其中 c1≥c2≥⋯≥c10≥0 。
枚举反素数
根据性质2,我们可以通过dfs枚举每个质因子的指数,进而求出可能为反素数的数。再求其因子个数,根据性质1,取相同因子个数中最小的那个数即可筛选出所有反素数。
正整数结构
唯一分解定理(算术基本定理)
定理1(唯一分解定理) 任何一个大于 1 的整数都可以被唯一分解为有限个素数的乘积,即
其中 p1<p2<⋯<ps 为质数,ci∈Z+(1≤i≤s) 。
勒让德定理
p 幂次数的定义 正整数 x 含有素数 p 的幂次数记作 vp(x) ,称为 x 的 p 幂次数。
p 进制数位和的定义 正整数 x 在 p 进制下的数位之和记作 Sp(x) ,称为 x 的 p 进制数位和。
定理1(勒让德定理) 若 p 为素数,则 vp(n!)=∞∑i=1⌊npi⌋ 。
- 推论1(定理1的推论) 若 p 为素数,则 vp(n!)=⌊n/p⌋+vp(⌊n/p⌋!) 。
- 推论2(定理1的推论) 若 p 为素数,则 vp(n!)=n−Sp(n)p−1 。
- 推论3(推论2的推论,库默尔定理) vp((nm))=Sp(m)+Sp(n−m)−Sp(n)p−1 ,即等于在 p 进制下 (n−m) 加 m 的进位次数。
定理1的证明:
n!=1×2×⋯×p×⋯×⌊np⌋p×⋯×n ,其中 p 的倍数有 p×2p×⋯×⌊np⌋p=⌊n/p⌋!p⌊n/p⌋ ,于是我们需要递归求解 vp(n!)=vp(⌊n/p⌋!)+⌊n/p⌋ ,可得 vp(n!)=∞∑i=1⌊npi⌋ 。
推论1的证明:
由定理1,易得 vp(n!)=∞∑i=1⌊npi⌋=⌊n/p⌋+∞∑i=2⌊npi⌋=⌊n/p⌋+vp(⌊n/p⌋!) 。
推论2的证明:
经过换元换序可得
n−Sp(n)p−1=1p−1(∞∑i=0(⌊npi⌋modp)pi−∞∑i=0⌊npi⌋modp)=∞∑i=1(⌊npi⌋modp)pi−1p−1=∞∑i=1i−1∑j=0(⌊npi⌋modp)pj=∞∑k=1∞∑i=k(⌊npi⌋modp)pi−k=∞∑k=1⌊npk⌋=vp(n!)于是得证。
推论3的证明:
由推论2可得
vp((nm))=vp(n!m!(n−m)!)=vp(n!)−vp(m!)−vp((n−m)!)=Sp(m)+Sp(n−m)−Sp(n)p−1其中 Sp(m)+Sp(n−m)−Sp(n) 可得 m+(n−m) 与 n 的位数之和的差,即加法被进位的位数之和,除以 p−1 即进位的个数。
分解质因数
试除法
枚举 i∈[2,√n] ,一个一个除尽质因子。当然,最后至多留一个 >√n 的质因子,需要特判。
质因子不会太多(最多几十个),所以空间当作常数。
可以提前 O(√n) 预处理素数,随后时间复杂度 O(2√nlnn) ,空间复杂度 O(√n) 。
时间复杂度 O(√n)
空间复杂度 O(1)
void get_pfactor(int n, vector<pair<int, int>> &pfactor) { for (int i = 2;i * i <= n;i++) { if (!(n % i)) { pfactor.push_back({ i,0 }); while (!(n % i)) n /= i, pfactor.back().second++; } } if (n > 1) pfactor.push_back({ n,1 }); }
Pollard-Rho算法
Pollard-Rho算法适用于快速随机找到大数的一个非 1 因子。基于这个算法,我们可以利用递归快速分解一个大数的质因子,时间复杂度大约相同。
普通试除法的时间复杂度是 O(√n) ,对于 long long
范围的大数是不可接受的。
Pollard-Rho的想法产生于生成随机数 m∈[2,n−1] ,测试 gcd(m,n)=d>1 ,来产生因子 d 的算法。但这种算法的期望复杂度是 O(√nlogn) ,比试除法还差,因此Pollard考虑利用生日悖论对随机数作差来碰撞因子。
生日悖论:
在一年有 n 天的情况下,当房间中约有 √2nln2 个人时,至少有两个人的生日相同的概率约为 50% 。
更进一步地说,我们在 [1,n] 内随机生成数字,产生第一个重复数字前期望有 √πn2 个数,约为 √n 个。
因此,假设 n 有非 1 因子 d ,我们从 [1,n−1] 期望随机抽取 √d 个数字后,就能产生两个数 i,j 满足 i−j≡0(modd) ,即 gcd(i−j,n)=d>1 。所以,产生这样一对数字的期望最差复杂度是 O(n14) ,但为了找到这些数字,需要大量互相作差并求 gcd ,因此复杂度又回到 √nlogn 。
Pollard为了避免这种情况,构造了一种伪随机序列 xn=(x2n−1+c)modn ,其中起点 x0 和常数 c 是随机在 [1,n−1] 中给定的。这样的作用是,假设 n 有非 1 因子 d ,且序列存在一组数字 xi,xj 满足 xi−xj≡0(modd) ,那么 xi+1−xj+1≡x2i−x2j≡(xi−xj)(xi+xj)≡0(modd) ,即未来所有距离为 i−j 的数的差都会产生因子 d 。
因为这组伪随机序列是模 n 的,因此一定会产生一个混循环(这也是为什么叫做 Rho→ρ ),所以在环上测一组,相当于测了环上所有组距离 i−j 的数,于是就不需要两两测试了,期望测 n14 组就够了。
此时期望复杂度是 O(n14logn) ,我们还希望把 log 去掉,因此有了倍增优化。倍增优化的原理是:
若 gcd(m,n)=d>1 ,则 gcd(km,n)≥d,k∈Z+ 。
这意味着我们可以累积计算 1,2,4,8,⋯ 次差,乘在一起求 gcd ,若某次作差产生非 1 因子,那么乘积一定会产生非 1 因子,时间复杂度为 O(n14+log(n14)logn) 。但是缺点是,我们倍增到最后可能由于单次积累量太大直接超过期望值太多反而变慢。实际上,我们不需要倍增多少次,假设我们取 dis 作为一次累积的量,那么复杂度为 O(n14+n14logndis) ,只要 dis≥logn 就能做到复杂度 O(n14) 。在 long long
范围内我们令 dis=128 就足够了,我们在倍增的基础上每隔 128 次检测一次即可,不到 128 次则在结束时检测。
还有一种优化,Floyd判环算法,用于在进入循环时及时退出不重复跑圈。我们设两个数 x,y ,每次判断 gcd(|x−y|,n)>1 ,若没有则令 x 走一步,y 走两步。因为每次 y 多走一步,如果进入环则 y 一定能追上 x ,此时退出即可。
但实际上,判环算法的优化是不如倍增算法的(时间大约多一倍),且两者不太好兼容,因此我们一般使用倍增算法,而不使用判环算法。
拥有了Pollard-Rho算法,我们就可以对大数 n 进行质因子分解,时间复杂度大约也是 O(n14) :
- 用Miller-Rabin算法判断 n 是否为素数,如果是直接返回,否则进行下一步。
- 每次用Pollard-Rho算法获得一个非 1 的因子 d ,如果为 n 就再求一次。
- 将数分解为 nd 和 d 两个数,回到第一步递归进行。
时间复杂度 O(n14)
空间复杂度 O(1)
template<class T> T randint(T l, T r) { static mt19937_64 eng(chrono::steady_clock::now().time_since_epoch().count()); uniform_int_distribution<T> dis(l, r); return dis(eng); } ll qpow(ll a, ll k, ll P) { ll ans = 1; while (k) { if (k & 1) ans = (__int128_t)ans * a % P; k >>= 1; a = (__int128_t)a * a % P; } return ans; } bool miller_rabin(ll n, int k = 10) {//8-10次 if (n == 2) return 1; if (n == 1 || !(n & 1)) return 0; int r = __builtin_ctzll(n - 1); ll m = n - 1 >> r; while (k--) { ll x = qpow(randint(1LL, n - 1), m, n); if (x == 1 || x == n - 1) continue;//直接满足,否则r-1次内必须有n-1 for (int i = 1;i <= r - 1 && x != 1 && x != n - 1;i++) x = (__int128_t)x * x % n;//二次探测 if (x != n - 1) return 0;//未经过n-1 } return 1; } ll pollard_rho(ll n) { ll s, x = randint(1LL, n - 1), c = randint(1LL, n - 1), prod = 1; for (int dis = 1;;dis <<= 1) {//路径倍增 s = x;//固定起点作差 for (int i = 1;i <= dis;i++) x = ((__int128_t)x * x % n + c) % n;//玄学预循环 for (int i = 1;i <= dis;i++) { x = ((__int128_t)x * x % n + c) % n; prod = (__int128_t)prod * abs(x - s) % n;//累积因子 if (i == dis || i % 128 == 0) {//固定最多128次一判 ll d = gcd(prod, n); if (d > 1)return d; } } } } void get_pfactor(ll n, vector<ll> &pfactor) { if (n == 1) return; if (miller_rabin(n)) { pfactor.push_back(n); return; } ll d = n; while (d >= n) d = pollard_rho(n); get_pfactor(n / d, pfactor); get_pfactor(d, pfactor); }
勒让德定理法
对于一个阶乘 n! 的分解质因数,我们可以通过勒让德定理,快速求得 n! 中某个质因子 p 的幂次,从而避免Pollard-Rho算法可能涉及的大数运算。
单次求解 p 幂次的复杂度为 O(logn) ,完整分解质因数的复杂度为 O(∑p∈[1,n]logpn)≈O(nlognlnn)≈O(n) 。
注意,需要 O(n) 预处理素数表。
时间复杂度
- 求阶乘质因子 p 的幂次 O(logn)
- 分解阶乘质因数 O(n)
空间复杂度 O(n)
const int N = 1e6 + 7; bool vis[N]; vector<int> prime; void get_prime(int n) { for (int i = 2;i <= n;i++) { if (!vis[i]) prime.push_back(i); for (auto j : prime) { if (i * j > n) break; vis[i * j] = 1; if (!(i % j)) break; } } } int legendre_fact(int n, int p) { int cnt = 0; while (n) { cnt += n / p; n /= p; } return cnt; } void get_fact_pfactor(int n, vector<pair<int, int>> &pfactor) { for (auto i : prime) { int cnt = legendre_fact(n, i); if (cnt) pfactor.push_back({ i,cnt }); } }
因数
因数的定义与基本性质
定义 若整数 a,b 满足 a∣b ,则称 a 是 b 的因数(约数,因子),b 是 a 的倍数。
约定 由于对负因数的讨论等价于正因数的讨论,所以若不特殊指明,我们假定因数都是指正因数。
设 n>1,n=s∏i=1pcii ,其中 p1<p2<⋯<ps 为质数,ci∈Z+(1≤i≤s) 。
性质1 n 的正因数有 s∏i=1(ci+1) 个。
性质2 nk,k∈Z+ 的正因数有 s∏i=1(k⋅ci+1) 个。
性质3 n 的正因数和为 s∏i=1ci∑j=0pji 。
性质4 [1,n] 内正因数集合大小约为 nlogn 。
性质5 n 的正因数个数上界是 2√n 。
但实际上这个边界很宽松, 109 内的数,正因数最多有 1344 个;1018 内的数,正因数最多有 103680 个。
性质6 n 的正因数个数期望约为 lnn 。
性质1到3可由唯一分解定理得到,性质4到6则由因数的定义得到,下面提供性质4,5证明,性质6较为复杂证明自行百度。
性质4的证明:
类似埃氏筛,枚举每个因子的倍数,共 n∑i=1ni≈nlogn 个。
性质5的证明:
注意到若 n 有因子 d 则一定有因子 nd ,因此我们枚举在 [1,√n] 的因子,其余可以对称得到,因此知道 2√n 是 n 的正因数个数上界。
正因数集合的求法
试除法
试除法适用于求单个正整数 n 的正因数集合。
根据性质5及其证明,我们枚举因子 [1,√n] 即可,正因数个数上界为 2√n 。
时间复杂度 O(√n)
空间复杂度 O(√n)
void get_factor(int n, vector<int> &factor) { for (int i = 1;i * i <= n;i++) { if (!(n % i)) { factor.push_back(i); if (i != n / i) factor.push_back(n / i); } } }
倍数法
倍数法适用于求一个区间 [1,n] 的每个数的正因数集合,但不能只求出单个数的正因数集合。
根据性质4,时间复杂度是 O(n∑i=1ni)≈O(nlogn) 。
此法常用于一些因子相关的求和,如 n∑i=1∑d∣id , n∑i=1∑d∣if(d) 等。
时间复杂度 O(nlogn)
空间复杂度 O(nlogn)
const int N = 1e6 + 7; vector<int> factor[N]; void get_factor(int n) { for (int i = 1;i <= n;i++) for (int j = i;j <= n;j += i) factor[j].push_back(i); }
最大公因数 gcd
gcd 的定义与基本性质
定义 对于不全为零的整数 a,b ,若整数 d 是满足 d∣a 且 d∣b 的最大数,则称 d 为 a,b 的最大公因数(Greatest Commom Divisor, gcd),记为 gcd(a,b)=d 。
互质的定义 对于整数 a,b ,若 gcd(a,b)=1 ,则称 a,b 互质(互素)。
性质1 gcd(a,b)=gcd(b,a)=gcd(−a,b)=gcd(|a|,|b|) 。
性质2 gcd(a,b)=gcd(a+kb,b) ,其中 k∈Z 。
性质3 gcd(a,b)=gcd(amodb,b) 。
性质4 gcd(ka,kb)=kgcd(a,b) ,其中 k∈Z∗ 。
性质5 gcd(a,k)=1⇒gcd(a,kb)=gcd(a,b) ,其中 k∈Z 。
- 推论1(性质2、5的推论) gcd(a,b)=1⟺gcd(a+b,a)=gcd(a+b,b)=1⟺gcd(a+b,ab)=1 。
- 推论2(推论1的逆否命题的推论) a+b∣ab⇒gcd(a+b,ab)≠1 。
性质6 gcd(k,ab)=1⟺gcd(k,a)=gcd(k,b)=1 ,其中 k∈Z 。
性质7 gcd(a,b,c)=gcd(gcd(a,b),c) 。
性质2的证明:
根据整除基本性质10,a,b 的公因数和 a+kb,b 的公因数相同,因此 gcd 也相同。
性质7的证明:
性质7可由 gcd 的指数表示法易得。
gcd 的求法
没有极致的效率要求的话,一般c++17及以上推荐用 std::gcd
,c++14以及更低版本推荐手写 gcd ,也可以使用 libstdc++
实现的 __gcd
。注意,除了 std::gcd
能支持负数,其他的处理负数需要取绝对值。
ACM 有数据范围且通常都是正的,不过对于如 gcd,lcm 的算法,以及 exgcd、CRT、exCRT 等涉及 gcd 的算法,从数学角度他们都是支持负数或负系数的,但是我们实现的算法由于c++语法原因,一般是不能处理负数的,对此需要谨慎处理。
辗转相除法(欧几里得算法)
利用性质3递归,直到一边为 0 ,另一边则为 gcd 。
优点是一行写完,缺点是对较大数字取模会比较慢,对于缺点可以用stein算法替代。
时间复杂度 O(log(min{a,b}))
空间复杂度 O(1)
ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; }
更相减损术(stein算法)
利用性质2、4、7迭代,直到一边减为 0 ,另一边乘上公共二次幂 k 即为 gcd 。
优点是只有加减法和位运算,比取模快。
时间复杂度 O(log(min{a,b}))
空间复杂度 O(1)
ll gcd(ll a, ll b) { if (!a || !b) return max(a, b); int i = __builtin_ctzll(a), j = __builtin_ctzll(b); a >>= i, b >>= j; while (1) { if (a < b) swap(a, b); if (!(a -= b)) break; a >>= __builtin_ctzll(a); } return b << min(i, j); }
递推法
对于求 a1,a2,⋯,an 的 gcd ,利用性质7,我们有递推式 gcd(a1,⋯,ai,ai+1)=gcd(gcd(a1,⋯,ai),ai+1) ,就能推出结果。
时间复杂度 O(nlog(min{ai}))
空间复杂度 O(1)
最小公倍数 lcm
lcm 的定义与基本性质
定义 对于整数 a,b ,若正整数 m 是满足 a∣m 且 b∣m 的最小数,则称 m 为 a,b 的最小公倍数(Least Commom Multiple, lcm),记为 lcm(a,b)=m 。
性质1 gcd(a,b)⋅lcm(a,b)=|ab| 。
性质2 lcm(a,b,c)=lcm(lcm(a,b),c) 。
性质1的证明:
性质1可由 gcd,lcm 的指数表示法易得。
性质2的证明:
性质2可由 lcm 的指数表示法易得。
lcm 的求法
一般c++17及以上推荐使用 std::lcm
,c++14及更低版本只能手写。对于负数的处理,同 gcd 的求法前言。
公式法
利用性质1直接求解。
先除后乘避免溢出。
时间复杂度 O(log(min{a,b}))
空间复杂度 O(1)
ll gcd(ll a, ll b) { return b ? gcd(b, a % b) : a; } ll lcm(ll a, ll b) { return a / gcd(a, b) * b; }
递推法
对于求 a1,a2,⋯,an 的 lcm ,利用性质2,我们有递推式 lcm(a1,⋯,ai,ai+1)=lcm(lcm(a1,⋯,ai),ai+1) ,就能推出结果。
切记 多个数的 lcm 不是 n∏i=1aigcd(a1,⋯,an) 。
时间复杂度 O(nlog(min{ai}))
空间复杂度 O(1)
gcd 与 lcm 的其他性质
gcd 和 lcm 的指数表示法
设 n,m∈Z+ ,则可以表示为
其中 p1<p2<⋯<ps 为质数,αi,βi∈N(1≤i≤s) 。
于是有
对负整数提取负号即可,无需额外讨论。
性质1 gcd(Fn,Fm)=Fgcd(n,m) ,其中 Fi 为斐波那契数列。
- 推论1(性质1的推论) 斐波那契数列相邻两项互素。
性质2 斐波那契数列相邻两项,”辗转相除”次数等于“更相减损”次数。
性质3 gcd(an−bn,am−bm)=agcd(n,m)−bgcd(n,m) ,其中 a≥b≥0 , n,m≥0 , gcd(a,b)=1 。
性质4 gcd(a,b)=1⇒gcd(an,bm)=1 ,其中 a,b,n,m≥0 。
性质5
性质6 (n+1)lcm(C0n,C1n,⋯,Cnn)=lcm(1,2,⋯,n+1) ,其中 n∈N 。
斐波那契数列(扩展知识)
斐波那契数列的定义与基本性质
历史背景 斐波那契数列(Fibonacci sequence),又称黄金分割数列,因数学家莱昂纳多·斐波那契(Leonardo Fibonacci)以兔子繁殖为例子而引入,故又称为“兔子数列”。
定义 斐波那契数列 Fn 有递推定义
列举参照
性质1 n∑i=1Fi=Fn+2−1 。
性质2 n∑i=1F2i−1=F2n 。
性质3 n∑i=1F2i=F2n+1−1 。
性质4 n∑i=1F2i=FnFn+1 。
性质5 Fn+m=Fn−1Fm−1+FnFm 。
性质6 F2n=(−1)n−1+Fn−1Fn+1 。
性质7 F2n−1=F2n−F2n−2 。
性质8 Fn=Fn−2+Fn+23 。
性质9 limn→∞Fn+1Fn=√5−12 。
性质10 Fn=(1+√52)n−(1−√52)n√5 。
本文来自博客园,作者:空白菌,转载请注明原文链接:https://www.cnblogs.com/BlankYang/p/17051071.html
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战