目录
#最大公约数Greatest Common Divisor,gcd
#最小公倍数Least Common Multiple,lcm
乘法逆元部分大致参考学习于知乎想做科学家的ACMer 非常好的文章乘法逆元 - 知乎 (zhihu.com)
数论基础
gcd(a,b)=gcd(a,b-a)
gcd(a,b)=gcd(a,b%a)
最大公约数Greatest Common Divisor,gcd
欧几里得算法
递归ver
LL gcd(LL a,LL b)
{ if(b==0)
return a;
else
return gcd(b,a%b);
}
LL gcd(LL a,LL b){
return b==0?a;gcd(b,a%b);
}
非递归
LL gcd(LL a,LL b){
while(b!=0){
int t = a%b;
a=b;
b=t;
}
return a;
}
时间复杂度
使用stein算法优化
step1:两数均为偶数时将其同时除以2至少一数为奇数为止,记录除掉的所有公因数2的乘积k;
step2:如果仍有一数为偶数,连续除以2直至该数为奇数为止;
step3:用更相减损法(辗转相减法),即GCD(a,b)=GCD(a-b,b),或辗转相除法求出两奇数的最大公约数d;
step4:原来两数的最大公约数即为d*k
如果 m 为偶数 n 为偶数, gcd(m, n) = gcd(m >> 1, n >> 1) << 1;
如果 m 为偶数 n 为奇数, gcd(m, n) = gcd(m >> 1, n);
如果 m 为奇数 n 为偶数, gcd(m, n) = gcd(m, n >> 1);
如果 m 为奇数 n 为奇数, gcd(m, n) = gcd(n, m - n);
#include<stdio.h>
#include<windows.h>
#pragma warning(disable:4996)
int main(){
//Stein算法
int a = 0;
int b = 0;
printf("请输入两个整数:");
scanf("%d%d", &a, &b);
int gcd = 0;
int k = 1;
while ((!(a & 1)) && (!(b & 1))){ //step1;
k <<= 1; //用k记录全部公因子2的乘积 ;
a >>= 1;
b >>= 1;
}
while (!(a & 1))a >>= 1; //step2;
while (!(b & 1))b >>= 1;
if (a<b) a ^= b, b ^= a, a ^= b; //交换,使a为较大数;
while (a != b){ //step3;
a -= b;
if (a<b) a ^= b, b ^= a, a ^= b;
}
gcd = k*a;
printf("最大公约数为:%d\n", gcd);
system("pause");
return 0;
}
最小公倍数Least Common Multiple,lcm
lcm(a,b)=a*b/gcd(a,b);
lcm(a,b)=a/gcd(a,b)*b;(防止乘法超INF)
LL gcd(LL a,LL b){
while(b!=0){
int t = a%b;
a=b;
b=t;
}
return a;
}
中国剩余定理
引子:(紫书第二章习题)
相传韩信才智过人,从不直接清点自己军队的人数,只要让士兵先后以三人一排、五人一排、七人一排地变换队形,而他每次只掠一眼队伍的排尾就知道总人数了。输入包含多组数据,每组数据包含3个非负整数a,b,c,表示每种队形排尾的人数(a<3,b<5,c<7),输出总人数的最小值(或报告无解)。已知总人数不小于10,不超过100。
Input
输入包含多组数据,每组数据包含3个非负整数a,b,c,表示每种队形排尾的人数(a<3,b<5,c<7)
Output
输出总人数的最小值(或报告无解)
Sample Input
2 1 6
2 1 3
Sample Output
41
No answer
————————————————
今天刚看这题的时候第一时间只想到了暴力枚举(太菜惹
查了下原来是有关中国剩余定理CRT的
先学习下CRT和数论再来写出提交代码
数论基础
1.若
则
2.若a%b=c
则(a*k)%b = k*c%b(k>0)
Mi^(-1)即为
//扩展欧几里得模板
int ex_gcd(int a,int b,int &x,int &y){
int d;
if(b == 0){
x = 1;
y = 0;
return a;
}
d = ex_gcd(b,a%b,y,x);
y -= a / b * x;
return d;
}
int Chinese_Remainder(int a[],int prime[],int len){
int i,d,R,y,M,m = 1,sum = 0;
//计算所有除数的积,也就是所有除数的最小公倍数m
for(i = 0; i < len; i++)
m *= prime[i];
//计算符合所有条件的数
for(i = 0; i < len; i++){
M = m / prime[i];//计算除去本身的所有除数的积M
d = ex_gcd(M,prime[i],R,y);
sum = (sum + R * M * a[i]) % m;
}
return (m + sum % m) % m;//满足所有方程的最小解
}
乘法逆元(关于取模运算)
一,模运算的性质
1. (a+b)%p=(a%p+b%p)%p
2. (a-b)%p=(a%p-b%p)%p
注意:(9-7)%8=(9%8-7%8)%8=-6 ;不是我们要的值,这时要加上p,改为:
3. (a-b)%p=((a%p-b%p)%p+p%p
4. (a*b)%p=((a%p)*(b%p))%p
5. a ^ b % p = ((a % p)^b) % p
二,除法模运算(不能拆分了就)
对于整数 a,与 a 互质的数 b 作为模数,当整数 x 满足 ax mod b ≡ 1 时,称 x 为 a 关于模 b 的逆元,代码表示就是a * x % b == 1;(存在条件是a与b互质)
因为在竞赛中经常遇到模1e9+7的运算,加减乘可以拆分,但是除法拆分中间量未必能整除,因此引入了乘法逆元的概念;
e.g.求3*6/3对7取模,最终答案为6
若拆开分别求模,则结果变成了4/3,不是我们想要的答案
因而我们引入了乘法逆元X,乘法逆元X满足3*X mod 7≡1,即X=5
我们将/3换成*5
则计算过程变成18mod7*5然后再mod7即20mod7即为6
由此总结:乘法逆元的作用是在求大数a/b mod c的值的时候 a mod c不能被b整除
因而将/换成*,b换成(b的乘法逆元) 即可方便计算
引入两种算法求乘法逆元
1.拓展欧几里得算法
代码
拓展欧几里得算法即为求 ax+by=k*gcd(a,b)(裴蜀方程)的一组可行整数解(x,y)
typedef long long LL;
LL ExGCD(LL a, LL b, LL &x, LL &y)
{
// x, y 为引用传参,故最终程序结束后,x,y会被赋值为可行解
if(b == 0)
{
// 递归终点,ax+by=GCD(a,b)的b为0,故方程变为
// ax=a,则可行解可以是 x=1, y=0
x = 1, y = 0;
return a;
}
LL d = ExGCD(b, a % b, x, y), t = x;
x = y, y = t - a / b * x;
return d; // 这里返回值是GCD(a,b)的结果,即最大公约数
}
typedef long long LL;
void ExGCD(LL a,LL b,LL& d,LL& x,LL& y)
{
if(!b){d=a; x=1; y=0; }
else {ExGCD(b,a%b,d,y,x); y=-x*(a/b);}
}
代码2及下面思考摘自紫书10.1.3
递归时x和y顺序反了没关系,上面的代码2是求出了一组解(x1,y1),那么该如何求出其他解呢?
任取一组解(x2,y2),则ax1+by1=ax2+by2,a(x1-y1)=b(y2-y1),两边同除gcd(a,b)
得a'(x1-x2)=b'(y2-y1),因此x1-x2一定是b'整数倍,设x1-x2=kb',则y2-y1=ka'
注意到上面的推到并没有用到ax+by右边是什么
因此得到结论:
a,b,c为任意整数,ax+by=c的一组整数解(x0,y0)
则它的任意整数解为(x0+kb',y0-ka') k∈Z b'=b/gcd(a,b) a'=a/gcd(a,b)
拓展gcd和求乘法逆元的关系
a、b互素,aX mod b≡1,即aX-bk=1
令X=x,-k=y 即ax+by=1
typedef long long LL;
LL ExGCD(LL a, LL b, LL &x, LL &y)
{
if(b == 0)
{
x = 1, y = 0;
return a;
}
LL d = ExGCD(b, a % b, x, y), t = x;
x = y, y = t - a / b * x;
return d;
}
int ExGcdInv(int a, int b)
{
int x, y;
ExGCD(a, b, x, y);
return x;
}
时间复杂度:大约O(logn)
(斐波那契复杂度)。
适用范围:存在逆元即可求,适用于个数不多但模数b很大的时候,最常用、安全的求逆元方式。
2.费马小定理+快速幂取模
费马小定理
若p为素数,gcd(a,p)=1
≡
或≡
快速幂 二进制
摘自oi wiki
核心:二进制加速计算(非递归ver)
long long int quik_power(int base, int power)
{
long long int result = 1;
while (power > 0) //指数大于0进行指数折半,底数变其平方的操作
{
if (power & 1) //指数为奇数,power & 1这相当于power % 2 == 1
result *= base; //分离出当前项并累乘后保存
power >>= 1; //指数折半,power >>= 1这相当于power /= 2;
base *= base; //底数变其平方
}
return result; //返回最终结果
}
快速幂应用之取模运算
long long int quik_power(int base, int power, int p)
{
long long int result = 1;
while (power > 0)
{
if (power & 1)
result = result * base % p;
//根据公式每个项都取余数后在再做累乘
base = base * base % p ;
//根据公式每个项都取余数后在再做平方操作
power >>= 1;
}
//根据公式在最后的的结果上再来一次取余数
return result % p;
}
注意:若模数p为质数 可以应用费马小定理加速快速幂 即≡mod p
计算乘法逆元
即
long long int quik_powermod(int base, int power, int p)
{
long long int result = 1;
while (power > 0)
{
if (power & 1)
result = result * base % p;
base = base * base % p ;
power >>= 1;
}
return result % p;
}
int FermatInv(int a, int b)
{
return quik_powermod(a, b - 2, b);
}
3.递归求逆元
LL Inv(LL a, LL b)
{
if(a == 1) return 1;
return (b - b / a) * Inv(b % a, b) % b;
}
原理见知乎文章,时间复杂度为O(log b),b为质数!
4.线性求逆元(之后再学)
若求1-n每个数的逆元 则单独求复杂度太高
逆元打表(递推)
逆元(费马小定理、扩展欧几里得、逆元线性打表)-CSDN博客