您的位置:首页 > 其它

[数论]数论的基础知识——最大公约数、最小公倍数

2008-05-07 22:00 246 查看
一、整除
一个整数a能被另一个整数d整除,记作d|a,意味着存在某个整数k,有 a=kd。
整除的性质:
(1) 如果 d|a, 则对于任意整数k有 d|ka
(2) 如果 d|a 且 d|b,则d|(a±b)
(3) 如果 b|a 且 a|b,则a=b
整除的特殊例子:
l 若2能整除a的最末位,则 2|a;若4能整除a的最末两位,则 4|a;若8能整除a的最末三位,则 8|a; ……
l 若5能整除a的最末位,则5|a;若25能整除a的最末两位,则25|a;若125能整除a的最末位,则5|a;……
l 若3能整除a的各位数字之和,则3|a;若9能整除a的个位数字之和则9|a;
l 若11能整除a的偶数位数字之和与奇数位数字之和的差,则11|a;

二、最大公约数
如果 d|a 并且d>0,则我们说d是a的约数(Divisor)。一个整数a的约数最小为1,最大为|a|。每个整数a都可以被其平凡约数1和a整除。a的平凡约数也称为a的因子(Factor)。
如果d是a的约数并且也是b的约数,则d是a与b的公约数(Common Divisor),1是任意两个整数的公约数。最大公约数(Greatest Common Divisor)指所有公约数中最大的那个,a与b的最大公约数记作gcd(a,b)。定义gcd(0,0)=0, gcd(0,N)=N。
最大公约数的一些性质
(1) gcd(a,ka) = |a|
(2) 对任意整数a与b,如果 d|a 且 d|b,则d|gcd(a,b)
(3) 对所有整数a和b以及任意非负数n,gcd(an,bn)=n gcd(a,b)
(4) 对所有正整数d,a和b,如果d|ab并且gcd(a,d)=1,则d|b
(5) 如果q和r是a除以b的商和余数,即a=b×q+r,则gcd(a,b)=gcd(b,r)

三、最小公倍数
如果d|a 则我们称a是d的倍数(Multiple)。
如果m是a的倍数并且也是b的倍数,则m是a与b的公倍数(Common Multiple)。任意两个整数的公倍数有无限多个,其中最小的那个称为最小公倍数(Least Common Multiple),a和b的最小公倍数记作lcm(a,b)。 lcm(0,0)=0, lcm(0,N)=0。
公倍数的性质:
lcm(a,b)=a×b / gcd(a,b)

四、算法分析及实现
1.最大公约数的实现
公约数算法的实现其实是直接使用的上面公约数性质中的第5条,关键是要做到深刻理解为什么该性质能用辗转相除来实现。即为什么 a=b×q+r => gcd(a,b)=gcd(b,r)
首先看一个例子
gcd(1001,767)
= gcd(767,234)
= gcd(234,65)
= gcd(65,39)
= gcd(39,26)
= gcd(26,13)
= gcd(13,0)
= 13
那么为什么可以这样呢?
首先,我们要对上面的整除性质(2)做一个推论:
如果d|(a+b),且d|a,则d|b。
证明: 利用整除的性质 若 d|a 则 a=kd ,有:
∵ d|(a+b) 则 (a+b)=kd
又 d|a, 所以 存在一个整数k1,有 a=k1d
∴ (a+b)=kd
k1d + b = kd
b = kd-k1d
又 ∵ k和k1都是整数,所以k-k1也是一个整数,因此 d|b
证毕。

那么根据以上推论
设 gcd(b,r)=m
则有 m|b 且 m|r
假设q是一整数则 m|(q×b)
那么 m|( q×b+r )
令a=q×b+r
于是,m|a
也就是说,若a能表示成 q×b+r 的形式,则gcd(a,b)=gcd(b,r)
那么为什么要辗转相除到b=0的时候a就是最小公约数呢?
因为若a能表示为 q×b+r 的形式,则有gcd(a,b)=gcd(b,r),且若b也能继续拆解,则能一直如此辗转下去,一直到gcd(0,rn)时,无法继续拆解,则说明rn已经是所能得到的最大公约数。如下面的例子:
gcd(125,25) 显然25|125,所以125 mod 25 = 0,也就是说会被拆解成25×5+0的形式,那么此时,他们的最大公约数显然就是除数了。

2.最小公倍数的实现
由于我们已经有最小公倍数的性质 lcm(a,b)=a×b / gcd(a,b),所以直接利用这个性质就可以得到最小公倍数

五、实现代码(关键代码)
// (递归版)
int gcd( int a, int b )
{
// 如果b为0则a就是a和b的最大公约数
if ( b==0 )
return a;

// 否则,继续辗转相除,递归调用自身,以获得最大公约数
return gcd( b, a%b );
}

// 当然,递归调用时由于参数的传递会导致堆栈的过度参与,因此我们还可以使用迭代形式
// 来实现这个算法以提高时间和空间效率。虽然效果是有限的,算法复杂度仍然相同,但是
// 完善本身就是我们追求的目标,所以也将这个代码列印如下
int gcd2( int a, int b)
{
// 这里必须引入一个临时变量,否则 a=b, b=a%b 的话,由于a=b在前,那么进行b=a%b运算
// 时,由于a==b,所以结果必然为0
for ( int Temp=b; b!=0; Temp=a,a=b, b=Temp%b );

return a;
}

int lcm( int a, int b )
{
if ( a*b==0 ) // 如果其中一个数是0,那么最小公倍数是0
return 0;

// 这里利用了最小公倍数和最大公约数的性质来求取最小公倍数
return a*b / gcd(a,b);
}

两个最小公倍数版本的效率比较(参考):
运行平台: Athlon 64X2 Dual 4200+ 2GB内存
编译环境: Visual Studio 2003.net,Windows32 Console Project, Debug Version(缺省配置)
测试代码:
clock_t clk=clock();
unsigned int Count=99999999;
while( (Count--)!=0 )
{
gcd(1001,767); // 此处在进行算法2测试时换成gcd2
}
clk = clock()-clk;

测试结果: gcd 31563,31500
gcd2 15906,15891

从结果可以看出,迭代版比递归版高效近一倍,估计是由于在函数调用过程还要进行额外的参数入栈,指令地址跳转(Jump)操作,然后在进入函数时,还有将参数出栈,返回值操作等等~由于算法本身耗时很少,所以这些琐碎的语言本身的内部操作所带来的成本也就显得格外的高。这在较复杂的算法中体现并不会那么明显,但若迭代版比较容易实现,那么还是尽量使用迭代版来的实在。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: