[学习笔记] 质数与唯一分解定理 - 数论
素性测试
素性测试就是判断某个数是否为质数。
费马素性测试
费马小定理 内容:若 \(p\) 为质数,\(a\)为任意整数,有 \(a^{p-1}\equiv 1(mod\ p)\)
那么可以多次随机取一个基数 \(a\in (1,p)\) 若 \(p\) 满足上式,那么它为质数的可能性就越大。称 \(p\) 是基于 \(a\) 的伪素数。
但对于Carmichael数(561、1105、1729…),不管取什么 \(a\) 的值总能够通过测试。但Carmicheal数的分布极为稀疏,所以还是个不错的选择。但若追求更高的准确性,就要依靠下面的方法。
Millar Rabin 素性测试
结合了费马小定理和二次探测原理,详情见OI WIKI。
inline ll qpow(ll a, ll n, ll mod){
ll ans = 1;
while(n){
if(n & 1) ans = (__int128)a * ans % mod;
a = (__int128)a * a % mod;
n >>= 1;
} return ans;
}
inline bool MillarRabin(ll n){
if(n < 3 || n%2 == 0) return n == 2;
ll u = n-1, t = 0;
while(!(u%2)) u >>= 1, ++t;
for(int i=1; i<=8; ++i){
ll a = rand()%(n-2) + 2, v = qpow(a, u, n), s;
if(v == 1) continue;
for(s=0; s<t; ++s){
if(v == n-1) break;
v = qpow(v, 2, n);
}
if(s == t) return false;
} return true;
}
素数筛
素数筛一般用来找到1~n内所有的质数。
埃氏筛
欧拉筛
欧拉筛是一种线性筛,每个数只能被他的最小质因子筛去,所以每个数只被筛了一遍,所以复杂度\(O(n)\)。
唯一分解定理(算数基本定理)
任何一个大于1的整数n都可以分解成若干个素因数的连乘积。\(n=p_1^{k1}p_2^{k2}p_3^{k3}p_4^{k4}...\)
这条定理看似简单实则非常厉害,可以吧许多复杂的数论问题转化到质因子层面上。
约数个数定理
任意一个大于1的整数 \(n\) 的因数和都可以表示为 \(\prod {ki+1}\)。证明很简单,对所有质因子的系数排列组合+乘法原理即可。
约数和定理
枚举正整数 \(n\) 的全部因子,每个素因子 \(p\) 都有 \(p^0\),\(p^1\),\(p^2\)……,\(p^k\)。总共 \(k+1\) 种情况。利用乘法原理,一个正整数 \(n\) 的约数和即为 \(\prod \sum_{j=0}^{k}p_i^j =(p_1^0+p_1^1+…+p_1^{k_1})(p_2^0+p_2^1+…+p_2^{k_2})…\)
约数积定理
同样枚举每个因子,每个素因子都有 \(p_i^0 \times p_i^1 \times p_i^2 \times … \times p_i^{k_i}=p_i^{\frac{k_i(k_i+1)}{2}}\) 种情况。根据乘法原理,约数积即为 \(\begin{split} prod=(p_1p_2p_3…p_i)^{\frac{k_1k_2k_3…k_i(k_1+1)(k_2+1)…(k_i+1)}{2^i}} \end{split}\)
分解质因数
试除法
就是从1到 \(\sqrt n\) 的质数全部试一遍,能整除的就是它的质因子。对于小数据或者是已知质因子数量较小时还是比较实用的。
Pollard_Rho
该算法是一种启发式的算法,适用于超大数的质因数分解。详情见OI WIKI
inline ll qpow(ll a, ll n, ll mod){
ll ans = 1;
while(n){
if(n & 1) ans = (__int128)ans * a % mod;
a = (__int128)a * a % mod;
n >>= 1;
} return ans;
}
inline bool millarrabin(ll n){
if(n < 3 || n % 2 == 0) return n == 2;
ll u = n - 1, t = 0;
while(u % 2 == 0) u /= 2, ++t;
for(int i=0; i<8; ++i){
ll a = rand() % (n-2) + 2, v = qpow(a, u, n);
if(v == 1) continue;
ll s;
for(s = 0; s < t; ++s){
if(v == n - 1) break;
v = qpow(v, 2, n);
}
if(s == t) return false;
}
return true;
}
inline ll pollardrho(ll n){
ll s = 0, t = 0;
ll c = rand() % (n-1) + 1;
ll step = 0, goal = 1;
ll val = 1;
for(goal = 1;; goal<<=1, s = t, val = 1){
for(step=1; step <= goal; ++step){
t = (qpow(t, 2, n) + c) % n;
val = (__int128)val * llabs(t - s) % n;
if((step % 127) == 0){
ll d = __gcd(val, n);
if(d > 1) return d;
}
}
ll d = __gcd(val, n);
if(d > 1) return d;
}
}
inline void findfac(ll n){
if(millarrabin(n)){
factor[cnt++] = n;
return;
}
ll p = n;
while(p >= n) p = pollardrho(p);
while(!(n%p)) n /= p;
findfac(p), findfac(n);
}
例题
阶乘分解
不难发现:\(n!\) 的质因数即为从1到n所有的质数,可以使用欧拉筛,但问题在于如何求质因数的系数。通过找规律(找了我一个多小时👀️)可以发现,某个质因子 \(p\) 的系数 )可以发现,某个质因子 \(p\) 的系数 \(k=\sum_{i=1}^{\left \lfloor log_p{n} \right \rfloor } \left \lfloor \frac{n}{p^i} \right \rfloor\),最后利用循环求解即可。
#include<bits/stdc++.h>
using namespace std;
int n, prime[80000], cnt;
bitset<1000010> vis;
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n;
for(int i=2; i<=n; ++i){
if(!vis[i]){
prime[++cnt] = i;
int t = 1, cn = 0;
for(int k=1; k<=log(n)/log(i); ++k){
t *= i;
cn += n/t;
}
cout<<i<<' '<<cn<<'\n';
}
for(int j=1; j<=cnt; ++j){
if(prime[j]*i > n) break;
vis[prime[j]*i] = 1;
if(i % prime[j] == 0) break;
}
} return 0;
}
[Violet 5]樱花
这道题主要是推狮子,先来把分母去掉:
接着因式分解:
由上式易知,解的数量即为 \((n!)^2\) 的因子数。
经过"阶乘分解"那道题之后我们可以轻松求出 \(n!\) 的质因子和指数,平方操作就是把指数乘2。最后统计因子个数即为所求。
#include<bits/stdc++.h>
using namespace std;
const int m = 1e9 + 7;
int n, pr[80000], cnt, ans = 1;
bitset<1000010> vis;
int main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>n;
for(int i=2; i<=n; ++i){
if(!vis[i]){
pr[cnt++] = i;
int cn = 0, lg = log(n)/log(i), t = 1;
for(int j=1; j<=lg; ++j) cn += n/(t*=i);
ans = (long long)ans * ((cn<<1) | 1) % m;
}
for(int j=0; j<cnt; ++j){
if(pr[j]*i > n) break;
vis[pr[j]*i] = 1;
if(i % pr[j] == 0) break;
}
} return cout<<ans, 0;
}
Sumdiv
由约数和定理可知,约数和为 \(Sumdiv=\prod \sum_{j=0}^{k}p_i^j\)。但直接一项一项求的话复杂度太高,要不起。所以考虑等比数列求和。
等比数列求和
等比数列即为每相邻两项之比都相等的数列,并且他们的比叫做公比。下面推导求和公式。
首先在左右两边同乘初始项:
然后提出来 \(a^{n+1}\) 并加上 \(a^0\) 项得:
移项并化系数为一得:
注意,在 \(a=1\) 时有 \(S=(n+1)r\)
回到题目,狮子就可以写成 $Sumdiv=\prod \frac{p^{k+1}-1}{p-1}\ mod\ 9907 $。
计算时,\(p^{k+1}\) 用快速幂求出,\((p-1)^{-1}\) 用逆求解。可用费马小定理,也可用扩展欧几里得。
#include<bits/stdc++.h>
using namespace std;
#define ll long long
ll a, b, m = 9901, ans = 1;
map<ll ,bool> vis;
inline ll qpow(ll a, ll n, ll mod){ //快速幂
ll ans = 1;
while(n){
if(n & 1) ans = (__int128)ans * a % mod;
a = (__int128)a * a % mod;
n >>= 1;
}return ans;
}
inline bool mr(ll k){ // Millar Rabin 素性测试
if(k < 3 || k%2 == 0) return k == 2;
ll u = k-1, t = 0;
while(!(u%2)) u >>= 1, ++t;
for(int i=1; i<=8; ++i){
ll a = rand()%(k-2)+2, v = qpow(a, u, k), s;
if(v == 1) continue;
for(s=0; s<t; ++s){
if(v == k-1) break;
v = qpow(v, 2, k);
}
if(s == t) return false;
} return true;
}
inline ll pr(ll k){ // pollard_rho 分解质因数
ll i = 0, t = 1, c = rand()%(k-1)+1, x = rand()%k;
ll y = x;
while(++i){
x = (qpow(x, 2, k) + c) % k;
ll d = __gcd(llabs(x-y), k);
if(d != 1 && d != k) return d;
if(y == x) return k;
if(i == t) y = x, t <<= 1;
}
}
inline void findfac(ll n){
if(n < 2) return;
if(mr(n)){
if(vis[n]) return;
vis[n] = 1;
ll s = a, cn = 0;
ll inv = qpow(n-1, m-2, m); //费马小定理求逆
while(!(s%n)) s /= n, ++cn;
ans *= (__int128)(qpow(n, cn*b+1, m)-1) * inv % m;
return;
}
ll p = n;
while(p >= n) p = pr(p);
while(!(n%p)) n /= p;
findfac(n), findfac(p);
}
signed main(){
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin>>a>>b;
findfac(a);
return cout<<(ans%m + m)%m, 0; //重新取模变为最小正整数
}