引言
前几天看了《小灰的算法之旅》的最大公约数部分,看完只晓得说“妙啊!”,然后就没然后了
今天汇编老师突然在群里发题目,让我们用x86汇编写个最大公约数,我突然一楞
意识到问题严重性的我打算好好深入了解一下,吓得我找了波李永乐老师
(此文代码演示使用C++)
引入——图解法
假设有一个长为104,宽为40的长方形,我们要找一个长度为a(a为整数)的正方形,将这个长方形铺满,那么a=?
(其实本质就是在求104和40的最大公约数)
按宽(即小的一边)来切:40、40、24、16、8、8,刚好剩下2个边长相等的正方形就是了
熟悉吗xdm?这不就是大名鼎鼎的《九章算术》中的更相减损术吗?
更相减损术
还是上面那道题,演算一下好吧:
104 - 40 = 64
64 - 40 = 24
40 - 24 = 16
24 - 16 = 8
16 - 8 = 8
8 - 8 = 0
剩了两个8,得出结果8,easy的,我们看看代码
#include <iostream>
using namespace std;
int gcd(int a,int b){
if(a == b){
return a;
}else{
int gmax = a > b ? a : b;
int gmin = a < b ? a : b;
return gcd(gmin,gmax-gmin);
}
}
int main(){
int a=104,b=40;
cout << gcd(a,b) << endl;
return 0;
}
假如说我要减少一下运行次数呢,比如我不想减两次40,万一我是4020和40,那我搁儿那减100下?
这时候就不得不亮求最大公约数的升级版算法了——辗转相除法
辗转相除法
也叫欧几里得算法,听着就贼吊对吧,其实就是把“-”升级成“%”,得到了余数为0的那个除数就是了
还是那题,演算一遍:
104 % 40 = 24
40 % 24 = 16
24 % 16 = 8
16 % 8 = 0
不多bb,直接看代码
#include <iostream>
using namespace std;
int gcd(int a,int b){
int gmax = a > b ? a : b;
int gmin = a < b ? a : b;
if(a % b == 0){
return gmin;
}
return gcd(gmin,gmax%gmin);
}
int main(){
int a=104,b=40;
cout << gcd(a,b) << endl;
return 0;
}
讲道理,其实差不多,微改一下而已
虽然我们可能减少了运行的次数,但是取模的性能是比较抱歉的,所以反而更相减损在计算机里面用会更厉害一点,只是我们人类手算,用辗转相除会友好一点而已
所以在写代码时候要二选一,还是建议更相减损
但这还没完,如果你要用更相减损的话,你有没有发现这其中存在一个问题?
不够优雅?算死!
对于更相减损来说,如果说我是求1000000000和1的最大公约数呢?
减到你xxx,算s你吧xd
发现它的不稳定了没?
那咋办?还能优化吗???可以!
我们的思路就是:
- 避免大整数取模
- 尽可能减少运算次数
所以,我们将会在更相减损的基础上增加移位运算,就是不要楞头减
更相减损 + 位运算 = 优雅解出gcd
取模看起来是快,但是实则性能不佳,什么佳?位运算。
方法论:(假设求a和b的gcd)
(1)a、b均为偶:
gcd(a,b) = 2 * gcd(a/2,b/2)
(2)a为偶,b为奇:
gcd(a,b) = gcd(a/2,b)
(3)a为奇,b为偶:
gcd(a,b) = gcd(a,b/2)
(4)a、b均为奇:(如果b>a-b)
gcd(a,b) = gcd(b,a-b)
这时a-b肯定是偶数,就可以继续位运算了
----------------------------------------
以上所有的“/2”或者“*2”操作,均改为“>>1”或者“<<1”位运算模式,上面这么写方便表达而已
判断奇偶就直接和1相与就好,别忘了位运算都加上括号(位运算优先级淦低)
来演算一遍嘛
gcd(104,40) = 2 * gcd(52,20)
= 4 * gcd(26,10)
= 8 * gcd(13,5)
= 8 * gcd(8,5)
= 8 * gcd(5,4)
= 8 * gcd(5,2)
=...
= 8
get了吗?代码写起来也很快的
#include <iostream>
using namespace std;
int gcd(int a,int b){
if(a == b){
return a;
}
if((a&1)==0 && (b&1)==0){
return gcd((a >> 1),(b >> 1)) << 1;
}
else if((a&1)==1 && (b&1)==0){
return gcd(a,(b >> 1));
}
else if((a&1)==0 && (b&1)==1){
return gcd((a >> 1),b);
}
else{
int gmax = a > b ? a : b;
int gmin = a < b ? a : b;
return gcd(gmax-gmin,gmin);
}
}
int main(){
int a=104,b=40;
cout << gcd(a,b) << endl;
return 0;
}
xdm,把公屏打在优雅上!!!!
比较时间复杂度,看清谁是真的菜
- 暴力枚举法——O(min(a,b))
- 辗转相除法——O(log(max(a,b))),其实还行,输在性能上
- 更相减损术——O(max(a,b)),不稳定的时候是比暴力还菜
- 更相减损 + 位运算 —— O(log(max(a,b)))
懂自懂!
深入数论,探究本质
别跑啊,研究一下本质啊!
什么?图解法不就是吗???
不不不,要用数学方法证明比较好!
证明
(如果 有a | b ,说明a是b的一个约数)
假设:d | a ,且 d | b ,即d为a、b的公约数,证明辗转相除法的依据
∵d | a ,且 d | b
∴余数r = a - k * b(k为整数) = x1 * d - k * x2 * d = (x1 - k * x2) * d ,且(x1 - k * x2)为整数
∴d | r
∴gcd(a,b) = gcd(b,r),故而得证
最小公倍数
很简单,前面算出来最大公约数了,最小公倍数就是两个数的乘积除以最大公约数。比如:
- 假设求10,8的最小公倍数。
gcd(10,8) = 2
lcm(10,8) * 2 = 10 * 8
lcm(10,8) = 40
你自己验算一波,看看是不是