欧几里得算法可以在 log 的时间复杂度内求出 个数的 GCD,但是这还是太慢了。
在一些题目中 ,欧几里得算法就会 TLE。
理论: gcd ( a , b ) = gcd ( b , a m o d b ) \gcd(a,b) = \gcd(b, a \bmod b) gcd(a,b)=gcd(b,amodb)
已知两个数 a a a, b b b, 求 gcd ( a , b ) \gcd(a,b) gcd(a,b)。
设 a ≥ b a \ge b a≥b,若 a = b a=b a=b,则 gcd ( a , b ) = a = b \gcd(a,b) = a = b gcd(a,b)=a=b,否则,对于 ∀ d ∣ a , d ∣ b \forall d \mid a, d \mid b ∀d∣a,d∣b,可以证明 d ∣ a − b d \mid a - b d∣a−b。
∵ d ∣ a \because d \mid a ∵d∣a ∴ a = d k 1 \therefore a = dk_1 ∴a=dk1。同理, b = d k 2 b = dk_2 b=dk2
∵ a − b = d k 1 − d k 2 = d ( k 1 − k 2 ) \because a - b = dk_1 - dk_2 = d(k_1-k_2) ∵a−b=dk1−dk2=d(k1−k2)
∴ d ∣ a − b \therefore d \mid a-b ∴d∣a−b
因此, a a a 和 b b b 的公因数都是 a − b a-b a−b 和 b b b 的。即: gcd ( a , b ) = gcd ( a − b , b ) \gcd(a,b) = \gcd(a-b, b) gcd(a,b)=gcd(a−b,b)
假设我们要求的 gcd ( a , b ) \gcd(a,b) gcd(a,b) 中,满足 a , b > 0 a, b > 0 a,b>0
则有以下 5 5 5 种情况:
第一种: a = b a = b a=b,则有 gcd ( a , b ) = a = b \gcd(a,b) = a = b gcd(a,b)=a=b
第二种: a = 0 a = 0 a=0 或 b = 0 b = 0 b=0,若 a = 0 a=0 a=0,则 gcd ( a , b ) = gcd ( 0 , b ) = b \gcd(a,b) = \gcd(0, b) = b gcd(a,b)=gcd(0,b)=b,若 b = 0 b=0 b=0,则 gcd ( a , b ) = gcd ( a , 0 ) = a \gcd(a,b) = \gcd(a,0) = a gcd(a,b)=gcd(a,0)=a
第三种: a a a 和 b b b 均是偶数。很明显,此时 a a a 和 b b b 有公因数 2 2 2,所以可以将 a , b a,b a,b 同时除以 2 2 2,再求 GCD,用公式来表达,就是 gcd ( a , b ) = gcd ( a 2 , b 2 ) \gcd(a,b) = \gcd(\frac{a}{2}, \frac{b}{2}) gcd(a,b)=gcd(2a,2b),等式右边乘以 2 2 2,是因为我们将 a , b a,b a,b 都除以了 2 2 2,而没有算入答案,所以要乘 2 2 2。
第四种: a a a 和 b b b 中有一个偶数。不妨设 a a a 就是那个偶数,显然, 2 2 2 不是 a a a 和 b b b 的公因数,所以我们直接把 a a a 除以 2 2 2 就可以了。表达式为: gcd ( a , b ) = gcd ( a 2 , b ) \gcd(a,b) = \gcd(\frac{a}{2}, b) gcd(a,b)=gcd(2a,b)
第五种: a a a 和 b b b 都是奇数,设 a > b a > b a>b, 则有 gcd ( a , b ) = gcd ( a − b 2 , b ) \gcd(a,b) = \gcd(\frac{a-b}{2}, b) gcd(a,b)=gcd(2a−b,b)。下面是证明。
设 a = gcd ( a , b ) × k 1 , b = gcd ( a , b ) × k 2 a = \gcd(a,b) \times k_1, b = \gcd(a,b) \times k_2 a=gcd(a,b)×k1,b=gcd(a,b)×k2。
∵ k 1 \because k_1 ∵k1 表示的数字和 b b b 互质, k 2 k_2 k2 表示的数字和 a a a 互质
∴ gcd ( k 1 , k 2 ) = 1 \therefore \gcd(k_1, k_2) = 1 ∴gcd(k1,k2)=1
接下来证明 gcd ( a − b , b ) = gcd ( a , b ) \gcd(a - b, b) = \gcd(a, b) gcd(a−b,b)=gcd(a,b)(这里跟前面的证明方法不一样)
∵ a = k 1 × gcd ( a , b ) , b = k 2 × gcd ( a , b ) \because a = k_1 \times \gcd(a,b), b = k_2 \times \gcd(a, b) ∵a=k1×gcd(a,b),b=k2×gcd(a,b)
∴ a − b = ( k 1 − k 2 ) × gcd ( a , b ) \therefore a - b = (k_1-k_2) \times \gcd(a, b) ∴a−b=(k1−k2)×gcd(a,b),又 ∵ b = k 2 × gcd ( a , b ) \because b = k_2 \times \gcd(a, b) ∵b=k2×gcd(a,b)
∴ gcd ( a − b , b ) = gcd ( k 1 − k 2 , k 2 ) × gcd ( a , b ) \therefore \gcd(a-b, b) = \gcd(k_1 - k_2, k_2) \times \gcd(a, b) ∴gcd(a−b,b)=gcd(k1−k2,k2)×gcd(a,b)
接下来需要证明 gcd ( k 1 − k 2 , k 2 ) = 1 \gcd(k_1 - k_2, k_2) = 1 gcd(k1−k2,k2)=1,下面采用 反证法。
设 gcd ( k 1 − k 2 , k 2 ) = m , ( m ∈ N ∗ , m > 1 ) , k 1 − k 2 = t 1 m , k 2 = t 2 m \gcd(k_1 - k_2, k_2) = m, (m \in \mathbb{N^*}, m > 1), k_1 - k_2 = t_1m, k2 = t_2m gcd(k1−k2,k2)=m,(m∈N∗,m>1),k1−k2=t1m,k2=t2m
则 k 1 = ( t 1 + t 2 ) m k_1 = (t_1 + t_2)m k1=(t1+t2)m
∴ gcd ( k 1 , k 2 ) = m × gcd ( t 1 + t 2 , t 2 ) > 1 \therefore \gcd(k_1, k_2) = m \times \gcd(t_1 + t_2, t_2) > 1 ∴gcd(k1,k2)=m×gcd(t1+t2,t2)>1
与 gcd ( k 1 , k 2 ) = 1 \gcd(k_1, k_2) = 1 gcd(k1,k2)=1 矛盾,所以 gcd ( k 1 − k 2 , k 2 ) = 1 \gcd(k_1 - k_2, k_2) = 1 gcd(k1−k2,k2)=1
∵ gcd ( k 1 − k 2 , k 2 ) = 1 \because \gcd(k_1 - k_2, k_2) = 1 ∵gcd(k1−k2,k2)=1
∴ gcd ( a , b ) = gcd ( a − b , b ) \therefore \gcd(a, b) = \gcd(a - b, b) ∴gcd(a,b)=gcd(a−b,b)
综上所述,所以 gcd ( a , b ) = gcd ( a − b , b ) \gcd(a, b) = \gcd(a-b, b) gcd(a,b)=gcd(a−b,b),证毕。
于是,第 5 5 5 种情况可以转换为 gcd ( a , b ) = gcd ( a − b , b ) \gcd(a, b) = \gcd(a - b, b) gcd(a,b)=gcd(a−b,b)
又因为 a , b a, b a,b 均为奇数,所以 a − b a - b a−b 为偶数,于是,第 5 5 5 种情况就能被转化为第 4 4 4 种情况
所以 gcd ( a − b , b ) = gcd ( a − b 2 , b ) \gcd(a-b, b) = \gcd(\frac{a-b}{2}, b) gcd(a−b,b)=gcd(2a−b,b),即 gcd ( a , b ) = gcd ( a − b 2 , b ) \gcd(a, b) = \gcd(\frac{a-b}{2}, b) gcd(a,b)=gcd(2a−b,b)
通过以上的推理,我们可以将 GCD 转化为
int gcd(int a, int b) {
if (a == b) return a; // 1
if (a == 0) return b; // 2
if (b == 0) return a; // 2
if (!(a & 1) && !(b & 1)) // 3
return gcd(a >> 1, b >> 1) << 1;
if (!(a & 1) && (b & 1)) // 4
return gcd(a >> 1, b);
if (!(b & 1)) return gcd(a, b >> 1); // 4
if (a > b) return gcd((a - b) >> 1, b); // 5
else return gcd((b - a) >> 1, a); // 5
}
可能这个代码看上去比欧几里得算法的时间复杂度高,但其实它比欧几里得算法还快上不少。
接下来,我们进行一个优化,将递归转化为递推,进一步减少时间复杂度(用到一些二进制的知识)。
我们先把两个数中的 2 2 2 全部取出,也就是把两个数的二进制末尾的 0 0 0 全部取出,保证其中至少有一个奇数,然后两个数再分别消掉末尾的 0 0 0,保证两个数都是奇数,最终使用更相减损术。
int gcd(int a, int b) {
if (!a) return b; // a = 0;
if (!b) return a; // b = 0;
int shift;
for (shift = 0; ~(a | b) & 1; shift++)
a >>= 1, b >>= 1;
/*
先 a | b,方便 2 个数一起消 0
取反再与 1,判断末尾是不是 0
*/
while (~a & 1) a >>= 1;
// 判断 a 的末尾是不是 0
do {
while (!(b & 1)) b >>= 1; // 消 b 末尾的 0
// 现在都是奇数
if (a > b) swap(a, b);
b -= a; // 更相减损术
} while (b);
return a << shift; // 公因数 2 ^ shift
}
但是,这还是不够快!每次都用 while,效率低下,于是,我在这里隆重介绍我们的硬件函数__builtin
,以这个名字开头的函数名都是运行速度极快!这里用到的是__builtin_ctz()
,它用来处理一个数二进制末尾 0 0 0 的个数。于是,我们的代码就变成了这样:
int gcd(int a, int b) {
int az = __builtin_ctz(a), bz = __builtin_ctz(b);
int z = az > bz ? bz : az, diff; // z 是后面要恢复的 2^z
b >>= bz;
while (a) {
a >>= az; // 右移
diff = b - a; // 更相减损术
az = __built_ctz(diff);
if (a < b) b = a; // 更新 b
a = diff < 0 ? -diff : diff; // 更新 a, diff < 0取相反数
}
return b << z; // 答案
}
到这里就结束啦,你学废了吗?
参考链接:这里 和 这里