目录
gcd的欧几里得算法
又称辗转相除法,是一个求解两数最大公因数的算法。算法可以用递推式 \(gcd(a,b)=gcd(b, {a}\mod{b})\) 概括,边界条件是 \(gcd(a,0)=a\)。代码如下(相信每个OIer都会):
int gcd(int a,int b){ //递归形式
return b==0?a:gcd(b,a%b);
}
int gcd(int a,int b){ //迭代形式
int t;
while(b){ t=a%b;a=b;b=t; }
return a;
}
可以大致估计出,这个算法的复杂度是 \(O(logb)\) 的,因此无需担心栈溢出问题。下面给出算法的分析证明(略长可跳过)。
首先设 \(a=r_0,b=r_1\) ,根据递推式,我们列出递推过程:
\(r_0=q_0\cdot r_1+r_2\)
\(r_1=q_1\cdot r_2+r_3\)
\(r_2=q_2\cdot r_3+r_4\)
\(......\)
\(r_{n-3}=q_{n-3}\cdot r_{n-2}+r_{n-1}\)
\(r_{n-2}=q_{n-3}\cdot r_{n-1}+r_{n}\)
\(r_{n-1}=q_{n-1}\cdot r_{n}+0\)
可以概括为 \(r_{i-2}=q_{i-2}\cdot r_{i-1}+r_{i},i=2,3,...\)
则我们要证明的问题可以归结为 \(r_n\) 是 \(r_0\) 与 \(r_1\) 的公因数,且是最大公因数。
我们从下向上分析,最底行表明 \(r_n|r_{n-1}\) 。根据递推通式,若 \(r_j|r_i\) 且 \(r_j|r_{i-1}\) ,则 \(r_j|r_{i-2}\) 。由于 \(r_n|r_n\) ,从倒数第二行开始使用这一命题,直至第一行得出 \(r_n|r_1\) 且 \(r_n|r_0\) 为止。至此证明了 \(r_n\) 是 \(r_0\) 与 \(r_1\) 的公因数,接下来证明它是最大公因数。
假设 \(p\) 是 \(r_0\) 与 \(r_1\) 的任一公因数。根据递推通式,若 \(p|r_{i-2}\) 且 \(p|r_{i-1}\) ,则 \(p|r_i\) 。从第一行开始使用这一命题,第一行得出 \(p|r_2\) ,第二行得出 \(p|r_3\) ,以此类推直到倒数第二行得出 \(p|r_n\) 为止。由于对于 \(r_0\) 与 \(r_1\) 的任一公因数 \(p\) 都存在 \(p|r_n\) ,那么 \(r_n\) 只能是最大公因数。证明完毕。
至于复杂度,我们不难得出 \(r_{i+2}=(1+q_i q_{i-1})r_i-q_i r_{i-1}\) ,于是 \(r_{i+2}<\frac{1}{2} r_i\) ,可大致推出 \(gcd\) 大约在 \(2\log_2{b}\) 步终止,复杂度大约是 \(O(logn)\)的。至于严谨的分析,根据刘汝佳的说法:由Lamé定理可以计算出Euclid的调用次数的严格上界是 \(log_φ{N}+log_φ{\sqrt{5}}≈4.785lgN+1.6723,φ=\frac{\sqrt{5}-1}{2},N=max(a,b)\) ,最坏情况是 $
gcd(F_{k+1},F_{k})$ ,需要执行 \(k-1\) 步,\(F\) 是斐波那契数。Heilbronn定理表明Euclid算法的平均执行步数是 \(\frac{12ln2}{π^2 lgn}≈0.843lgn\)。(不要问我为什么,我不会证明)
gcd的二进制算法
别名:辗转相减法,更相减损术,尼考曼彻斯法<_<
之所以叫二进制算法的原因是它的核心思想在于筛去因子2来提高算法效率。因为大整数取模很慢,相比Euclid算法,这样做能常数能小一些。(其实也小不了多少,具体用哪个看实际情况吧)
算法步骤(\(a>b\)):
- 若a,b均为偶数,gcd(a,b)=2*gcd(a/2,b/2);
- 若a为偶数,b为奇数,gcd(a,b)=gcd(a/2,b);
- 若a为奇数,b为偶数,gcd(a,b)=gcd(a,b/2);
- 若a,b均为奇数,gcd(a,b)=gcd(a-b,b);//辗转相减
注意边界条件是gcd(a,a)=a,且始终保持a>b。为避免取模运算,判断奇偶性可使用位运算 !(a&1) 代替,下面是代码(作者:刘汝佳),相当简练。
int gcd(int a,int b){
int t=1,c,d;
while(a!=b){
if(b==0) break; //务必添加,否则会死循环
if(a<b) swap(a,b);
if(!(a&1)) { a>>=1;c=1;} else c=0;
if(!(b&1)) { b>>=1;d=1;} else d=0;
if(c&&d) t<<1;
else if(!c && !d) a-=b;
}
return t*a;
}
最小公倍数lcm
根据唯一分解定理容易得到 \(gcd(a,b)\cdot lcm(a,b)=a\cdot b\)
将 \(a,b\) 分解为一系列素数的若干次幂的乘积,显然 \(gcd\) 总是取较小的幂,\(lcm\) 总是取较大的幂。
唯一需要注意的是不要写 \(lcm=a\cdot b/gcd(a,b)\) ,而要改为 \(lcm=a/gcd(a,b)\cdot b\) ,否则可能会导致乘法溢出。