10.1.1 欧几里德算法和唯一分解定理
除法表达式。给出一个这样的除法表达式:\(X_1/X_2/X_3 / …/ X_k\),其中\(X_i\)是正整数。除法表达式应当按照从左到右的顺序求和,例如,表达式\(1/2/1/2\)的值为\(1/4\)。但可以在表达式中嵌入括号以改变计算顺序,例如,表达式\(\frac{1/2}{1/2}\)的值为1。
输入\(X_1, X_2, …, X_k\),判断是否可以通过添加括号,使表达式的值为整数。K≤10000,Xi≤109。
【分析】
表达式的值一定可以写成A/B的形式:A是其中一些Xi的乘积,而B是其他数的乘积。不难发现,X2必须放在分母位置,那其他数呢?
幸运的是,其他数都可以在分子位置:
接下来的问题就变成了:判断E是否为整数。
第1种方法是利用前面介绍的高精度运算:k次乘法加一次除法。显然,这个方法是正确的,但却比较麻烦。
第2种方法是利用唯一分解定理,把X2写成若干素数相乘的形式:
然后依次判断每个是否是\(X_1,X_3,X_4…X_k\)的约数。这次不用高精度乘法了,只需把所有Xi中pi的指数加起来。如果结果比ai小,说明还会有pi约不掉,因此E不是整数。这种方法在第5章中已经用过,这里不再赘述。
第3种方法是直接约分:每次约掉Xi和X2的最大公约数gcd(Xi, X2),则当且仅当约分结束后X2=1时E为整数,程序如下:
int judge(int* X) {
????? X[2] /= gcd(X[2], X[1]);
????? for(int i = 3; i <= k; i++) X[2] /= gcd(X[i], X[2]);
????? return X[2] == 1;
}
整个算法的时间效率取决于这里的gcd算法。尽管依次试除也能得到正确的结果,但还有一个简单、高效,而且相当优美的算法——辗转相除法。它也许是最广为人知的数论算法。
辗转相除法的关键在于如下恒等式:gcd(a,b) = gcd(b, a mod b)。它和边界条件gcd(a, 0)=a一起构成了下面的程序:
int gcd(int a, int b) {???
??? return b == 0 ? a : gcd(b, a%b);
}
这个算法称为欧几里德算法(Euclid algorithm)。既然是递归,那么免不了问一句:会栈溢出吗?答案是不会。可以证明,gcd函数的递归层数不超过\(4.785lgN + 1.6723\),其中\(N=max{a,b}\)。值得一提的是,让gcd递归层数最多的是\(gcd(Fn,Fn-1)\),其中Fn是后文要介绍的Fibonacci数。
利用gcd还可以求出两个整数a和b的最小公倍数lcm(a,b)。这个结论很容易由唯一分解定理得到。
由此不难验证\(gcd(a,b)*lcm(a,b)=a*b\)。不过即使有了公式也不要大意。如果把lcm写成a * b/gcd(a,b),可能会因此丢掉不少分数——ab可能会溢出!正确的写法是先除后乘,即a/gcd(a,b) b。这样一来,只要题面上保证最终结果在int范围之内,这个函数就不会出错。但前一份代码却不是这样:即使最终答案在int范围之内,也有可能中间过程越界。注意这样的细节,毕竟算法竞赛不是数学竞赛。