当前位置: 首页 > news >正文

【算法笔记】ACM数论基础模板

目录

  • 几个定理
    • 唯一分解定理
    • 鸽巢原理(抽屉原理)
    • 容斥原理
    • 裴蜀定理
  • 最大公约数、最小公倍数
    • 最大公约数
    • 最小公倍数
  • 质数
    • 试除法判断质数
    • 分解质因数
  • 筛质数
    • 朴素筛法(埃氏筛法)
    • 线性筛法(欧拉筛法)
  • 约数
  • 欧拉函数
    • 定义求一个数的欧拉函数
    • 筛法求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=1npiN

约数定理
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=1k(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}) (A1A2A3...An)=0<inAi0<i<jn(AiAj)+0<i<j<kn(AiAjAj)+...+(1)n+1(A1A2A3...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) ax+by=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的最大公约数} ab的最小公倍数=ab的最大公约数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 1N 中与 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=p1a1p2a2pmam,则:

φ ( 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×p1p11×p2p21××pmpm1

定义求一个数的欧拉函数

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×xibi(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} b1
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×b1(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], xmi(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=Cnm1+Cn1m1

阶乘式

C n m = n ! m ! × ( n − m ) ! C_n^m = \frac{n!}{m! \times (n - m)!} Cnm=m!×(nm)!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}. CnkCn/pk/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 1n105,1ba2000

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 1n105,1ba105

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} 1n201ba1018

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;
}

相关文章:

  • 反射(Reflection)详解
  • 快速上手Linux nfs网络文件系统
  • 网站遭受扫描攻击,大量爬虫应对策略
  • C语言while循环的用法(非常详细,附带实例)
  • 绘制时间对应的数据曲线
  • C/C++ 程序执行的主要过程
  • [万字]qqbot开发记录,部署真寻bot+自编插件
  • JavaScript进阶(九)
  • mysql8常用sql语句
  • VLLM推理大模型显存不够后,导致程序引擎崩溃的调优方案尝试
  • Mac M系列 安装 jadx-gui
  • java架构设计
  • git cherry-pick和git stash命令详解
  • 反转链表II
  • leetcode0295. 数据流的中位数-hard
  • 关于 javax.validation.constraints的详细说明
  • HuggingFace的SafeSensor格式模型能转成Ollama格式的吗?
  • 26、测试咒语:魔法校验—— Jest+Testing Lib
  • SimpleAdmin云服务器发布
  • 浅聊一下数据库的索引优化
  • 山西临汾哪吒主题景区回应雕塑被指抄袭:造型由第三方公司设计
  • 沈阳卫健委通报“健康证”办理乱象:涉事医院已被立案查处
  • 青海规范旅游包车行为:不得引导外省籍旅游包车违规驻地运营
  • 国务院新闻办公室发布《新时代的中国国家安全》白皮书
  • 十大券商看后市|A股中枢有望逐步震荡抬升,把握结构性行情
  • 专访|家人眼中的周碧初:用色彩写诗,实践油画“民族化”