二进制gcd(Binary gcd)

二进制gcd(Binary gcd)

前置知识:gcd(最大公约数),位运算

介绍

众所周知利用欧几里得算法(辗转相除法)可以做到在 log 的时间复杂度内求出两个数的最大公约数

但是有时候这还是不够快,例如这道题——Luogu P5435

如果直接辗转相除硬求 gcd 就会 T 得飞起,但是题解里那种方法 Θ() 预处理 Θ(1) 查询又好麻烦,那么有没有一种更快的直接求两个数 gcd 的方法呢?

二进制 gcd 就是这样一种算法,它利用计算机优秀的二进制处理能力来计算 gcd

原理

假设现在我们需要求 gcd(a,b) 满足 a,b>0

我们将 a,b 分为 5 种情况讨论

第一种a=b。显然 gcd(a,b)=a=b

第二种a=0b=0。根据最大公约数的定义当 b=0gcd(a,b)=gcd(a,0)=a,当 a=0gcd(a,b)=gcd(b,0)=b

第三种a,b 均为偶数。对于这种情况,显然 2 是公约数之一,直接将两个数都除以 2,再递归下去求 gcd(a2,b2) 即可,gcd(a,b)=2gcd(a2,b2)

第四种a,b 中有且仅有一个偶数。不妨设 a 是偶数,那么显然 2 不是公约数之一,直接将 a 除以 2gcd(a2,b) 即可,gcd(a,b)=gcd(a2,b)

第五种a,b 均为奇数。不妨设 a>b,那么有 gcd(a,b)=gcd(ab2,b),为什么呢?下面来证明一下

a=k1gcd(a,b), b=k2gcd(a,b)gcd(k1,k2)=1,先考虑证明 gcd(ab,b)=gcd(a,b)(即“更相减损术”)

ab=(k1k2)gcd(a,b), b=k2gcd(a,b),那么有 gcd(ab,b)=gcd(k1k2,k2)gcd(a,b),即证 gcd(k1k2,k2)=1

考虑反证法,假设 gcd(k1k2,k2)=m 其中 mN,m>1,再设k1k2=t1m, k2=t2m

就有 k1=(t1+t2)m,那么 gcd(k1,k2)=mgcd(t1+t2,t2)>1k1,k2 互质矛盾,得证 gcd(k1k2,k2)=1

综上 gcd(ab,b)=gcd(a,b)

由于 a,b 均为奇数,ab 就是偶数,那么问题就变成了和情况四一样,2 显然不是公约数之一,故 gcd(a,b)=gcd(ab2,b)

实现

直接根据上图模拟,我们可以得到以下代码(注意:~ 的运算优先级高于 |&

int gcd(int a,int b)
{
	if(a==b) return a;//a=b
	if(!a) return b;//a=0
	if(!b) return a;//b=0
	if(~a&1)//a是偶数
	{
		if(b&1)//b是奇数
			return gcd(a>>1,b);
		else//b是偶数
			return gcd(a>>1,b>>1)<<1;
	}
	if(~b&1)//a是奇数b是偶数
		return gcd(a,b>>1);
	//均为奇数
	if(a>b) return gcd((a-b)>>1,b);
	return gcd((b-a)>>1,a);
}

这样使用了大量的位运算,虽然看起来比辗转相除法递归层数更多,但是实际上大大优化了运算速度

当然我们可以把递归展开写成循环的形式,效率还能更高

首先把两个数公约数中的所有 2 提出来,也就是把二进制末尾的 0 全部提出来,然后两个数就至少有一个是奇数,把两个数末尾的 0 再分别去掉就能保证一定是两个奇数,利用更相减损术循环求解就行,具体看代码

int gcd(int a,int b)
{
	if(!a)return b;//a=0
	if(!b)return a;//b=0
	int shift;//a,b二进制下末尾0的个数
	for(shift=0;~(a|b)&1;++shift)a>>=1,b>>=1;//把两个数末尾的shift个0都去掉
	while(~a&1)a>>=1;//现在两个数至少有一个奇数,2一定不是公约数
	//现在a一定是奇数
	do
	{
		while(!(b&1))b>>=1;
		//现在两个数一定都是奇数
		if(a>b)swap(a,b);
		b-=a;//更相减损
	}while(b);
	return a<<shift;//把最开始去掉的2^shift的公约数加上
}

注意到每次还是都使用 while 来去除二进制下末尾的 0,效率低下,可以利用硬件函数 __builtin_ctz() 来继续加速

同时只有直接输入 0 时才可能 a=0 或是 b=0,所以如果输入保证不为 0 就可以去掉这个判断

再稍加卡常就得到了终极代码

inline int gcd(int a, int b)
{
	register int az=__builtin_ctz(a),bz=__builtin_ctz(b),z=az>bz?bz:az,diff;
	b>>=bz;
	while(a)
	{
		a>>=az;
		diff=b-a;
		az=__builtin_ctz(diff);
		if(a<b)b=a;
		a=diff<0?-diff:diff;
	}
	return b<<z;
}

这样我们就成功地利用 Θ(logn)gcd 切掉了 Luogu P5435 这道需要 Θ(值域) 预处理 Θ(1)gcd 的题目


博客园传送门

知乎传送门

posted @   人形魔芋  阅读(1324)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下
点击右上角即可分享
微信分享提示
目录导航