扩展欧几里得
欧几里得算法,又叫辗转相除法,用于求两个数的最大公约数(gcd)。
int gcd(int a, int b) {
return b ? gcd(b, a%b) : a;
}
由此可以得到最小公倍数
lcm(a,b)=a/gcd(a,b)∗b
(先除防止溢出)。
扩展欧几里得,是用于解出方程
ax+by=gcd(a,b)
的一组解的。比如方程
6x+15y=3
,可以解出
(3,−1)
,当然也有其它解。事实上,若有一组解
(x0,y0)
,则有任意解
(x0+kb/gcd(a,b),y0−ka/gcd(a,b))
。
知道这个有什么用呢?可以用来求任意不定方程的解。若有
ax+by=gcd(a,b)
的一组解
(x,y)
,则方程
ax+by=c
的一组解是
(xc/gcd(a,b),yc/gcd(a,b))
。
int exgcd(int a, int b, int& d, int& x, int& y){
if(!b) {
d = a;
x = 1;
y = 0;
}else{
exgcd(b, a%b, d, y, x);
y -= x*(a/b);
}
}
扩展欧几里得可以用于求不定方程,也是一些其他算法的基础。
线性筛
lrj书上写了一种筛法,叫Eratosthenes筛。
曾经我以为那就是线性筛。直到我看到我的代码跑了3500ms而别人只跑了1400ms。
线性筛,或是欧拉筛,时间复杂度是线性的。
为什么呢?因为每个数只会被筛去一次。Eratosthenes筛每个数可能筛多次,但线性筛每次都只用最小的质数去筛,因此只会筛一次。
我们来对比一下两种代码。
这是Eratosthenes筛,它的复杂度是
O(nlog2n)
。
//vis=1表示合数
for(int i = 2; i <= n; i++) if(!vis[i])
for(int j = i*i; j <= n; j += i) vis[j] = 1;
这是线性筛。
//vis=1表示合数
for(int i = 2; i <= n; i++) {
if(!vis[i]) prime[primen++] = i;
for(int j = 0; j < primen; j++) {
if(i*prime[j] > n) break;
vis[i*prime[j]] = 1;
if(i % prime[j] == 0) break; //这句话保证只筛一次
}
}
这样就很显然了吧。
线性筛欧拉函数
欧拉函数
phi(x)
等于不超过
x
且与
欧拉函数是积性函数。一个函数是积性函数,就是说如果
x=ij
,那么
f(x)=f(i)f(j)
。利用这个性质,就可以用线性筛筛它。
在上面的代码做一点修改就可以了。
for(int i = 2; i <= n; i++) {
if(!vis[i]) { //i是质数
prime[primen++] = i;
phi[i] = i - 1; //质数的欧拉函数是比它少一
}
for(int j = 0; j < primen; j++) {
if(i*prime[j] > n) break;
vis[i*prime[j]] = 1;
if(i % prime[j] != 0)
//积性函数
phi[i*prime[j]] = phi[i] * phi[prime[j]];
else{
//若x是质数p的倍数,则phi[x]=phi[x/p]*p
phi[i*prime[j]] = phi[i] * prime[j];
break; //保证只筛一次
}
}
}
线性筛莫比乌斯函数
莫比乌斯函数有三个性质:
- 质数的莫比乌斯函数是-1。
- 完全平方数的莫比乌斯函数是0。
- 莫比乌斯函数是积性函数。若
x=ip
(p是质数),则
μ(x)=−μ(i)
。
筛筛筛。
for(int i = 2; i <= n; i++) {
if(!vis[i]) {
prime[primen++] = i;
phi[i] = i - 1;
mu[i] = -1;//质数的莫比乌斯函数是-1
}
for(int j = 0; j < primen; j++) {
if(i*prime[j] > n) break;
vis[i*prime[j]] = 1;
if(i % prime[j] != 0) {
phi[i*prime[j]] = phi[i] * phi[prime[j]];
mu[i*prime[j]] = -mu[i];
}else{
phi[i*prime[j]] = phi[i] * prime[j];
//prime[j]的倍数又乘上prime[j],肯定是完全平方数
mu[i*prime[j]] = 0;
break;
}
}
}
逆元
什么是逆元?
在一般情况下,一个数除以另一个数等于乘它的倒数。
在模意义下,一个数除以另一个数等于乘它的逆元。
当然,被除数是能被除数整除的。
定义式:
ax≡1(modn)
,即
a−
1≡x(modn)
a
与
这不就是扩展欧几里得吗。
复杂度
O(nlogp)
int inv(int a, int n) {
int d, x, y; //变量名意义同上
exgcd(a, n, d, x, y);
return d == 1 ? (x+n)%n : -1;
//d!=1则逆不存在。结果可能小于0,因此要模n。
}
当n是质数时,可以用费马小定理。简略地说(其实是我不会证),
inv(a,n)=pow(a,n−2,n)
。其中pow是幂取模,用快速幂,复杂度是
O(nlogp)
。
然而和扩欧是一样的。这里就不贴代码了。
线性求所有逆元
n是模数。取k、r,使
n=ki+r
成立。其中
r<i
。
那么显然有
k=⌊n/i⌋
,
r=nmodi
。
化简式子得
ki+r≡0
,继而有
i−
1≡−kr−
1
。
于是,
inv(i)=−⌊n/i⌋∗inv(nmodi)
。
就可以线性递推了。
编程的时候怎么处理负号?把
−⌊n/i⌋
改成
n−⌊n/i⌋
就好了。
inv[1] = 1;
for(int i = 2; i <= n; i++) inv[i] = (ll)(p-p/i) * inv[p%i] % p;
用逆元求模意义组合数
非常不好,因为我没时间了。但东西还是得写完。
以下fac[]表示阶乘,可以O(n)预处理。很容易理解,把组合数计算式中的除法改成乘以逆元就好了。逆元可以用上述三种方法求。
C(n,m)=fac[n]∗inv(fac[m]∗fac[n−m]modp,p)modp;
Lucas定理
定理内容:
其中第一部分继续递归,而第二部分直接计算。
int comb(int n, int m, int p) { //Lucas
int ans = 1;
while(n && m) {
long long nn = n%p, mm = m%p;
if(nn < mm) return 0; //防止无限递归
ans = ans * fac[nn] * inv(fac[mm]*fac[nn-mm]%p, p) % p;
n /= p; m /= p;
}
return ans;
}