【算法笔记】ACM数论基础模板
目录
- 几个定理
- 唯一分解定理
- 鸽巢原理(抽屉原理)
- 容斥原理
- 裴蜀定理
- 最大公约数、最小公倍数
- 最大公约数
- 最小公倍数
- 质数
- 试除法判断质数
- 分解质因数
- 筛质数
- 朴素筛法(埃氏筛法)
- 线性筛法(欧拉筛法)
- 约数
- 试除法求约数
- 求约数个数
- 一个数求约数个数
- 求1~n所有数的约数个数
- 约数之和
- 一个数求约数之和
- 求1~n所有数的“约数之和”
- 欧拉函数
- 定义求一个数的欧拉函数
- 筛法求1~n每个数的欧拉函数
- 快速幂
- 快速幂求a的b次方
- 不带取模
- 带取模
- 拓展欧几里得算法
- 原理
- 拓展欧几里得算法求解线性同余方程
- 逆元
- 快速幂求逆元
- 拓展欧几里得算法求逆元
- 预处理1~n的逆元
- 中国剩余定理
- 中国剩余定理求解线性同余方程组(所有a彼此互质)
- 中国剩余定理求解线性同余方程组(所有a不一定互质)
- 组合数
- 几个性质
- 组合数的两种表示
- 递推式
- 阶乘式
- 卢卡斯定理
- 递推求组合数
- 预处理阶乘求组合数
- 卢卡斯定理求组合数
- 卢卡斯定理 + 预处理阶乘求组合数
本文中默认:
using LL = long long;
几个定理
唯一分解定理
对于一个大于1的正整数 n n n, n n n可以分解质因数为:
n = Π i = 1 k p i a i = p 1 a 1 × p 2 a 2 × . . . × p k a k n=\Pi_{i=1}^k p_i^{a_i}=p_1^{a_1} \times p_2^{a_2} \times ... \times p_k^{a_k} n=Πi=1kpiai=p1a1×p2a2×...×pkak
n n n的质因子 p p p的个数:
∑ i = 1 n N p i ∑^n_{i=1} \frac{N}{p^i} i=1∑npiN
约数定理:
d ( n ) = ∏ i = 1 k ( a i + 1 ) = ( a 1 + 1 ) ( a 2 + 1 ) . . . ( a k + 1 ) d(n)=∏^k_{i=1}(a_i+1)=(a_1+1)(a_2+1)...(a_k+1) d(n)=i=1∏k(ai+1)=(a1+1)(a2+1)...(ak+1)
鸽巢原理(抽屉原理)
桌上有十个苹果,要把这十个苹果放到九个抽屉里,无论怎样放,我们会发现至少会有一个抽屉里面放不少于两个苹果。这一现象就是我们所说的“抽屉原理”。 抽屉原理的一般含义为:“如果每个抽屉代表一个集合,每一个苹果就可以代表一个元素,假如有n+1个元素放到n个集合中去,其中必定有一个集合里至少有两个元素。” 抽屉原理有时也被称为鸽巢原理。
容斥原理
对于 n n n个集合 A 1 , A 2 , A 3 , . . . A n : A_1,A_2, A_3,...A_n: A1,A2,A3,...An:
( A 1 ∪ A 2 ∪ A 3 ∪ . . . ∪ A n ) = ∑ 0 < i ≤ n A i − ∑ 0 < i < j ≤ n ( A i ∩ A j ) + ∑ 0 < i < j < k ≤ n ( A i ∩ A j ∩ A j ) + . . . + ( − 1 ) n + 1 ( A 1 ∩ A 2 ∩ A 3 ∩ . . . ∩ A n ) (A_{1} \cup A_{2} \cup A_{3} \cup ... \cup A_{n})=\sum_{0<i\le n} A_{i}-\sum_{0<i<j\le n} (A_{i} \cap A_{j})+\sum_{0<i<j<k\le n}(A_{i}\cap A_{j}\cap A_{j})+...+(-1)^{n+1}(A_{1} \cap A_{2}\cap A_{3}\cap ... \cap A_{n}) (A1∪A2∪A3∪...∪An)=0<i≤n∑Ai−0<i<j≤n∑(Ai∩Aj)+0<i<j<k≤n∑(Ai∩Aj∩Aj)+...+(−1)n+1(A1∩A2∩A3∩...∩An)(加减加减加减…)
裴蜀定理
如果 a a a 和 b b b 是两个整数,则存在整数 x , y x ,y x,y 使得:
a ⋅ x + b ⋅ y = gcd ( a , b ) a \cdot x + b \cdot y = \gcd(a, b) a⋅x+b⋅y=gcd(a,b)
最大公约数、最小公倍数
最大公约数
欧几里得算法(辗转相除法)
int gcd(int a, int b){return b ? gcd(b, a % b) : a;
}
或者直接
__gcd(a, b);
最小公倍数
a 和 b 的最小公倍数 = a × b a 和 b 的最大公约数 a和b的最小公倍数=\frac{a \times b}{a和b的最大公约数} a和b的最小公倍数=a和b的最大公约数a×b
防止越界,先除再乘。
int lcm(int a, int b){return a / gcd(a,b) * b ;
}
质数
试除法判断质数
见"C语言入门讲义"
bool is_prime(int x){if(x < 2) return false;for(int i = 2; i <= x / i; i++){if(x % i == 0) return false;}return true;
}
分解质因数
分解出 n n n的 k k k个质因数: n = i 1 s 1 × i 2 s 2 × . . . × i k s k n = i_1^{s_1} \times i_2^{s_2} \times ... \times i_k^{s_k} n=i1s1×i2s2×...×iksk
void divide(int x){for(int i = 2; i <= x / i; i++){if(x % i == 0){int s = 0;while(x % i == 0){x /= i;s++;}cout << i << ' ' << s << endl; // i:底数 s:指数}}if(x != 1) cout << x << ' ' << 1 << endl;cout << endl;
}
筛质数
将 1 1 1~ n n n中所有的质数筛到 p r i m e s primes primes数组中
朴素筛法(埃氏筛法)
vector<int> primes;
bool not_prime[N]; // not_prime[i] = true 表示 i 不是质数void get_primes(int n){for(int i = 2; i <= n; i++){if(not_prime[i]) continue;primes.push_back(i);for(int j = i + i; j <= n; j += i) not_prime[j] = true;}
}
线性筛法(欧拉筛法)
vector<int> primes;
bool not_prime[N]; // not_prime[i] = true 表示 i 不是质数void get_primes(int n){for(int i = 2; i <= n; i++){if(!not_prime[i]) primes.push_back(i);for(int p : primes){if(i * p > n) break;not_prime[i * p] = true;if(i % p == 0) break;}}
}
约数
试除法求约数
vector<int> get_divisors(int x){vector<int> res;for(int i = 1; i <= x / i; i ++)if(x % i == 0){res.push_back(i);if(i != x / i) res.push_back(x / i);}sort(res.begin(), res.end());return res; // 返回一个vector,里面是n的所有约数(因子)
}
求约数个数
一个数求约数个数
int get_divisors(int x){unordered_map<int, int> primes; // 存x的质因数和其对应的次数for(int i = 2; i <= x / i;i++){while(x % i == 0){x /= i;primes[i]++;}}if(x > 1) primes[x]++; // 如果最后剩下一个大于 1 的数,说明它也是一个质因数LL res = 1;for(auto p : primes) res = res * (p.second + 1); // 约数定理return res;
}
求1~n所有数的约数个数
O ( n l o g n ) O(nlogn) O(nlogn)筛法
int cnt[N]; // cnt[i] : i 的约数个数for(int i = 1; i <= n; i++){for(int j = i; j <= n; j += i){cnt[j]++;}
}
O ( n ) O(n) O(n)筛法
bool not_prime[N];
int d[N], num[N]; // d[i] 表示 i 的约数个数,num[i] 表示 i 的最后一个质因子的幂(从 i 分解质因数得到的最后一个质因子 j,i = a * j^num)
vector<int> primes; // 筛质数void get_divisors(int n){d[1] = 1;num[1] = 0;for(int i = 2; i <= n; i++){if(!not_prime[i]){primes.push_back(i);num[i] = 1;d[i] = 2;}for(int p : primes){if(i * p > n) break;not_prime[i * p] = true;if(i % p == 0){num[i * p] = num[i] + 1;d[i * p] = d[i] / (num[i] + 1) * (num[i * p] + 1);break;} else{num[i * p] = 1;d[i * p] = d[i] * 2;}}}
}
约数之和
一个数求约数之和
LL get_divisor_sum(int x){unordered_map<int, int> primes; // 存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 t = 1, sum = 1;for(int i = 0; i < p.second; i++){t *= p.first;sum += t;}res *= sum;}return res; // res表示x的所有约数的和
}
求1~n所有数的“约数之和”
O ( n l o g n ) O(nlogn) O(nlogn)筛法
LL sum[N]; // sum[i]表示i的约数和for(int i = 1; i <= n; i++){for(int j = i; j <= n; j += i){sum[j] += i;}
}
O ( n ) O(n) O(n)筛法
bool not_prime[N];
int num[N]; // num[i] 表示 i 的最后一个质因子的幂次(i = a * p^num)
vector<int> primes;
LL sd[N], sp[N]; // sd[i] 表示 i 的所有约数之和, sp[i] 表示 i 最后一个质因子的贡献项(1 + p + p^2 + ... + p^k)void get_divisors(int n){sd[1] = 1;num[1] = 0;sp[1] = 1;for(int i = 2; i <= n; i++){if(!not_prime[i]){primes.push_back(i);num[i] = 1;sp[i] = 1 + i;sd[i] = 1 + i;}for(int p : primes){if(i * p > n) break;not_prime[i * p] = true;if(i % p == 0){num[i * p] = num[i] + 1;sp[i * p] = sp[i] * p + 1;sd[i * p] = sd[i] / sp[i] * sp[i * p];break;} else{num[i * p] = 1;sp[i * p] = 1 + p;sd[i * p] = sd[i] * sp[i * p];}}}
}
欧拉函数
1 ∼ N 1 \sim N 1∼N 中与 N N N 互质的数的个数被称为欧拉函数,记为 φ ( N ) \varphi(N) φ(N)。
若在算术基本定理中, N = p 1 a 1 p 2 a 2 … p m a m N = p_1^{a_1} p_2^{a_2} \dots p_m^{a_m} N=p1a1p2a2…pmam,则:
φ ( N ) = N × p 1 − 1 p 1 × p 2 − 1 p 2 × ⋯ × p m − 1 p m \varphi(N) = N \times \frac{p_1 - 1}{p_1} \times \frac{p_2 - 1}{p_2} \times \dots \times \frac{p_m - 1}{p_m} φ(N)=N×p1p1−1×p2p2−1×⋯×pmpm−1
定义求一个数的欧拉函数
int phi(int x){int res = x;for(int i = 2; i <= x / i; i ++){if(x % i == 0){res = res / i * (i - 1);while(x % i == 0) x /= i;}}if(x > 1) res = res / x * (x - 1);return res; // res即为phi(x)的值
}
筛法求1~n每个数的欧拉函数
vector<int> primes;
bool not_prime[N];
int euler[N]; // eular[i]表示phi(i)的值void get_eulers(int n){euler[1] = 1;for(int i = 2; i <= n; i++){if(!not_prime[i]){primes.push_back(i);euler[i] = i - 1;}for(int p : primes){LL t = 1LL * i * p;if(t > n) break;not_prime[t] = true;if(i % p == 0){euler[t] = euler[i] * p;break;} else{euler[t] = euler[i] * (p - 1);}}}
}
快速幂
快速幂求a的b次方
不带取模
LL qmi(int a, int b){ LL res = 1;while(b){if(b & 1) res = res * a;a = (LL)a * a;b >>= 1;}return res; // pow(a, b)
}
带取模
LL qmi(int a, int b, int p){LL res = 1;while(b){if(b & 1) res = res * a % p;a = (LL)a * a % p;b >>= 1;}return res; // pow(a, b) % p
}
拓展欧几里得算法
原理
给定 n n n 对正整数 a i , b i a_i, b_i ai,bi,对于每对数,求出一组 x i , y i x_i, y_i xi,yi,使其满足 a i × x i + b i × y i = gcd ( a i , b i ) a_i \times x_i + b_i \times y_i = \gcd(a_i, b_i) ai×xi+bi×yi=gcd(ai,bi)。
int exgcd(int a, int b, int &x, int &y){ // 一定要加 &if(!b){x = 1, y = 0;return a;}int d = exgcd(b, a % b, y, x);y -= a / b * x;return d;
}int main(){int n;cin >> n;while(n --){int a, b;cin >> a >> b;int x, y;exgcd(a, b, x, y);cout << x << ' ' << y << endl; }return 0;
}
拓展欧几里得算法求解线性同余方程
给定 n n n 组数据 a i , b i , m i a_i, b_i, m_i ai,bi,mi,对于每组数求出一个 x i x_i xi,使其满足 a i × x i ≡ b i ( m o d m i ) a_i \times x_i \equiv b_i \pmod{m_i} ai×xi≡bi(modmi),如果无解则输出 impossible
。
int exgcd(int a, int b, int &x, int &y){ // 一定要加 &if(!b){x = 1, y = 0;return a;}int d = exgcd(b, a % b, y, x);y -= a / b * x;return d;
}int main(){int n;cin >> n;while(n--){int a, b, m;cin >> a >> b >> m;int x, y;int d = exgcd(a, m, x, y);if(b % d) cout << "impossible" << endl;else cout << (LL)b / d * x % m << endl;}return 0;
}
逆元
b b b的乘法逆元,记作: b − 1 b^{-1} b−1。
a b ( m o d p ) = a × b − 1 ( m o d p ) \frac{a}{b}\pmod p = a \times b^{-1} \pmod p ba(modp)=a×b−1(modp)
(将题中的运算除法变成运算乘法)
快速幂求逆元
适用条件:模数 p p p一定要是质数,并且 n n n和 p p p互质,即 g c d ( n , p ) = 1 gcd(n, p) = 1 gcd(n,p)=1
LL qmi(int a, int b, int p){LL res = 1;while(b){if(b & 1) res = res * a % p;a = (LL)a * a % p;b >>= 1;}return res;
}LL inv(int n, int p){ // n 在模 p下的逆元return qmi(n, p - 2, p);
}
拓展欧几里得算法求逆元
适用条件:模数 p p p可以是任何数,只要满足 n n n和 p p p互质,即 g c d ( n , p ) = 1 gcd(n, p) = 1 gcd(n,p)=1即可。
LL exgcd(LL a, LL b, LL &x, LL &y){if(!b){x = 1, y = 0;return a;}LL d = exgcd(b, a % b, y, x);y -= a / b * x;return d;
}LL inv(LL n, int p){ // n 在模 p下的逆元LL x, y;LL d = exgcd(n, p, x, y);if(d == 1) return (x % p + p) % p;else return -1;
}
预处理1~n的逆元
LL inv[N]; // inv[i] : i 在模 p 下的逆元inv[1] = 1;
for(int i = 2; i <= n; i++){inv[i] = (p - (p / i) % p) * inv[p % i] % p;
}
中国剩余定理
中国剩余定理求解线性同余方程组(所有a彼此互质)
给定 2 n 2n 2n 个整数 a 1 , a 2 , … , a n a_1, a_2, \ldots, a_n a1,a2,…,an 和 m 1 , m 2 , … , m n m_1, m_2, \ldots, m_n m1,m2,…,mn,求一个最小的非负整数 x x x,满足
∀ i ∈ [ 1 , n ] , x ≡ m i ( m o d a i ) \forall i \in [1, n],\ x \equiv m_i \pmod{a_i} ∀i∈[1,n], x≡mi(modai)。
LL exgcd(LL a, LL b, LL &x, LL &y){if(!b){x = 1;y = 0;return a;}LL d = exgcd(b, a % b, y, x);y -= (a / b) * x;return d;
}LL inv(LL n, LL p){LL x, y;LL d = exgcd(n, p, x, y);if(d != 1) return -1;return (x % p + p) % p;
}LL CRT(LL a[], LL m[], int n){LL mm = 1;for(int i = 0; i < n; i++){mm *= m[i];}LL res = 0;for(int i = 0; i < n; i++){LL t = mm / m[i];LL inv_t = inv(t, m[i]);res = (res + t * inv_t % mm * (a[i] % mm)) % mm;}if(res < 0) res += mm;return res;
}
中国剩余定理求解线性同余方程组(所有a不一定互质)
LL exgcd(LL a, LL b, LL &x, LL &y){if(!b){x = 1;y = 0;return a;}LL d = exgcd(b, a % b, y, x);y -= (a / b) * x;return d;
}LL exCRT(LL a[], LL m[], int n){LL M = m[0], A = a[0];for(int i = 1; i < n; i++){LL x, y;LL d = exgcd(M, m[i], x, y);if((a[i] - A) % d != 0) return -1;LL t = m[i] / d;x = x * ((a[i] - A) / d);x = (x % t + t) % t;A += x * M;M = M / d * m[i];A %= M;}A = (A % M + M) % M;return A;
}
组合数
几个性质
组合数的两种表示
( n k ) ⇔ C n k \dbinom{n}{k} \Leftrightarrow C_n^k (kn)⇔Cnk
递推式
C n m = C n m − 1 + C n − 1 m − 1 C_n^m = C_n^{m - 1} + C_{n - 1}^{m - 1} Cnm=Cnm−1+Cn−1m−1
阶乘式
C n m = n ! m ! × ( n − m ) ! C_n^m = \frac{n!}{m! \times (n - m)!} Cnm=m!×(n−m)!n!
卢卡斯定理
对于素数 p p p,有
C n k ≡ C ⌊ n / p ⌋ ⌊ k / p ⌋ × C n m o d p k m o d p ( m o d p ) . \mathrm{C}_n^k \equiv \mathrm{C}_{\lfloor n/p \rfloor}^{\lfloor k/p \rfloor} \times \mathrm{C}_{n \bmod p}^{k \bmod p} \pmod{p}. Cnk≡C⌊n/p⌋⌊k/p⌋×Cnmodpkmodp(modp).
其中,当 n < k n < k n<k 时,组合数 C n k \mathrm{C}_n^k Cnk 规定为 0 0 0。
递推求组合数
适用条件: C a b C_a^b Cab中的 a a a和 b b b都比较小,二维数组 C [ a ] [ b ] C[a][b] C[a][b]要能存的下
例如: 1 ≤ n ≤ 1 0 5 , 1 ≤ b ≤ a ≤ 2000 1\le n \le 10^5,1 \le b \le a \le 2000 1≤n≤105,1≤b≤a≤2000
const int N = 2020;
const int mod = 1e9 + 7;int C[N][N]; // C[a][b] 即为C_a^bvoid init() {for(int i = 0; i < N; i++){for(int j = 0; j <= i; j++){if(j == 0) C[i][j] = 1;else C[i][j] = (C[i - 1][j] + C[i - 1][j - 1]) % mod;}}
}
预处理阶乘求组合数
用快速幂预处理阶乘和逆元求组合数。
适用条件: a a a和 b b b的范围适中,二维数组存不下但可以开一维。
比如: 1 ≤ n ≤ 1 0 5 , 1 ≤ b ≤ a ≤ 1 0 5 1 \le n \le 10^5, 1 \le b \le a \le 10^5 1≤n≤105,1≤b≤a≤105
LL qmi(int a, int b, int p){LL res = 1;while(b){if(b & 1) res = res * a % p;a = (LL)a * a % p;b >>= 1;}return res;
}LL inv(LL n, int p){return qmi(n, p - 2, p);
}LL fact[N], infact[N]; // 预处理阶乘和阶乘的逆元LL Cnk(int n, int k){ // Cnk即为C_n^kif(k < 0 || k > n) return 0;return (LL)fact[n] * infact[k] % mod * infact[n - k] % mod;
}void init(){fact[0] = 1;for(int i = 1; i < N; i++){fact[i] = fact[i - 1] * i % mod;}infact[N - 1] = inv(fact[N - 1]);for(int i = N - 1; i > 0; i--){infact[i - 1] = infact[i] * i % mod;}
}
卢卡斯定理求组合数
适用情况: a a a和 b b b特别大,用数组存不下,无法预处理,且查询较少,每次直接用定理求组合数不会超时。
比如: 1 ≤ n ≤ 20 , 1 ≤ b ≤ a ≤ 1 0 18 1 \le n \le 20,1 \le b \le a \le 10^{18} 1≤n≤20,1≤b≤a≤1018
LL qmi(LL a, LL b, int p){LL res = 1;while(b){if(b & 1) res = res * a % p;a = (LL)a * a % p;b >>= 1;}return res;
}int Cnk(int a, int b, int p){ // 较小数(0~p-1)的组合数int res = 1;for(int i = 1, j = a; i <= b; i++, j--){res = (LL)res * j % p;res = (LL)res * qmi(i, p - 2, p) % p;}return res;
}// c[a][b] = c[a % p][b % p] * c[a / p][b / p]
int lucas(LL a, LL b, int p){ // lucas(a, b, p) 即为C_a^b % pif(a < p && b < p) return C(a, b, p);return (LL)Cnk(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}
卢卡斯定理 + 预处理阶乘求组合数
适用情况: a a a和 b b b特别大,用数组存不下,无法预处理,且查询多,每次都求一遍组合数会超时。
LL qmi(LL a, LL b, int p){LL res = 1;while(b){if(b & 1) res = res * a % p;a = (LL)a * a % p;b >>= 1;}return res;
}LL fact[N], infact[N];void init(){fact[0] = 1;for(int i = 1; i < p; i++){fact[i] = fact[i - 1] * i % mod;}infact[p - 1] = inv(fact[p - 1]);for(int i = p - 1; i > 0; i--){infact[i - 1] = infact[i] * i % mod;}
}LL Cnk(int n, int k){ // 较小数(0~p-1)的组合数if(k < 0 || k > n) return 0;return (LL)fact[n] * infact[k] % mod * infact[n - k] % mod;
}// c[a][b] = c[a % p][b % p] * c[a / p][b / p]
int lucas(LL a, LL b, int p){ // lucas(a, b, p) 即为C_a^b % pif(a < p && b < p) return C(a, b, p);return (LL)Cnk(a % p, b % p, p) * lucas(a / p, b / p, p) % p;
}