[可能有用科技]更多 GCD 算法

前言#

重写旧文 & 增加新内容。

欧几里得算法#

gcd(a,a)=agcd(a,b)=gcd(b,amodb)

int gcd(int a,int b) {
	return b ? gcd(b,a % b) : a;
}

非递归 :

int gcd(int a,int b) {
	int p = a % b;
	while(p)
		a = b,b = p,p = a % b;
	return b;
}

更相减损法#

可半者半之,不可半者,副置分母、子之数,以少减多,更相减损,求其等也。以等数约之。

白话 :

  1. 将被 2 整除的一直除以 2 直到因数不包含 2
  2. 用较大的数减去较小的,直到减数和差相等,得到的就是最大公因子。
  3. 同时除了 k2 的情况,要在结尾乘以 2k

gcd(a,b)=gcd(min(a,b),|ab|)

int gcd(int x,int y) {
	if(!x || !y) return x | y;
	if(!(x & 1) && !(y & 1)) return gcd(x >> 1,y >> 1) << 1;
	else if(!(y & 1)) return gcd(x,y >> 1);
	else if(!(x & 1)) return gcd(x >> 1,y);
	return gcd(abs(x - y),std::min(x,y));
}

非递归 :

int gcd(int x,int y) {
	int p = 0;
	while(!(x & 1) && !(y & 1)) {
		x >>= 1,y>>= 1;
		p++;
	}
	while(!(x & 1)) x >>= 1;
	while(!(y & 1)) y >>= 1;
	while(x && y) {
		if(x > y) std::swap(x,y);
		y = y - x;
	}
	return (1 << p) * (x | y);
}

能不能再给力一点?

尝试快速判断 a,b 能除以 2 几次,众所周知整除 2 就是二进制末位为 1 的时候二进制右移一位,那么求出 a 按位或 b 的二进制末尾连续 0 个数即可,这个过程通过跑得飞快的内置函数 __builtin_ctz() 实现.

inline int gcd(int a,int b) {
	int k = __builtin_ctz(a | b);
	a >>= __builtin_ctz(a);
	b >>= __builtin_ctz(b);
	while(a && b) {
		if(a > b) std::swap(a,b);
		b -= a;
	}
	return a << k;
}

当然可以把这个玩意和欧几里得算法结合一下 :

inline int gcd(int a,int b) {
	int k = __builtin_ctz(a | b);
	a >>= __builtin_ctz(a);
	b >>= __builtin_ctz(b);
	int p = a % b;
	while(p)
		a = b,b = p,p = a % b;
	return b << k;
}

能不能再给力一点?

值域预处理#

将值域表示为 V,质数集表示为 P

对于值域较小的情况,可以做到 O(V) 预处理,O(1) 查询。

Theorem 1

nV 可以被分解为一个三元组 {a,b,c} (abc) 满足 a,bncncP

Proof 1

归纳法 :

基础 : 对于 n=1{1,1,1}

归纳 : 假设现在对于一个数 nx[0,n) 都有解,尝试证明对于 n 有解。

首先求 n 的最小质因数 p,可知现在 np 有解,记为 {a,b,c}

根据定义 n=pabc,np=abc,那么 a,bnp3

如果 pn4,我们之前得到 :

anp3papnp3

然后

pnp3=p3np3=p2n3

并且 p2n3n

那么得到 pan,有一组合法的分解为 {pa,b,c}

然后是 p>n4 的情况。

我们知道 n=pabc,那么显然 p,a,b,c>1n>p4

于是这时候 a 显然是 1,一组可行的分解为 {p,b,c}

证毕。

可以发现分解一个数 n 成三个因数的方式就是首先求其最小质因子 p 然后求 np 的分解方案 {a,b,c}(abc),那么 {pa,b,c} 就是 n 的分解方案了。

然后线性筛的时候一个数会被其最小质因子筛到,那么显然这个分解可以直接线性预处理的。

最后我们回到如何求 gcd

对于一个 gcd(n,m),将 n 拆分为 {a,b,c}

首先算出 gcd(a,m) 然后 mmgcd(a,m)

然后对 b,c 重复这个过程即可。

具体令当前细化到求一个 gcd(p,q)

如果 pV 就求 gcd(p,qmodp) 使得要求出的两个数都在 V 范围内,这些 gcd 可以 V×V=O(V) 预处理出来。

然后如果 p>V,如果 p|q 那么结果就是 p,如果 p| q 那就是 1

最后回顾一下全过程 :

  1. 预处理 V 以内的 gcd,复杂度 O(V)
  2. 预处理 V 以内的所有数的分解方案,类似线性筛的方式,复杂度 O(V)
  3. 询问的时候需要的信息都预处理出来了,复杂度 O(1)

于是就有 <O(V),O(1)>gcd 了。

模板 : Link.

Code :

为啥我数学相关的代码都巨大常数啊……

V 是值域常量,SV 是值域根号。

struct CommonFac {
	bool vis[V + 5];
	int pri[V],pcnt;
	int fac[V + 5][3];
	int G[SV + 5][SV + 5];
	
	void init() {
		rep(i,1,SV) {
			G[i][i] = G[i][0] = G[0][i] = i;
			repl(j,1,i)
				G[i][j] = G[j][i] = G[j][i % j];
		}
		fac[1][0] = fac[1][1] = fac[1][2] = 1;
		rep(i,2,V) {
			if(!vis[i]) {
				fac[i][0] = fac[i][1] = 1;
				fac[i][2] = i;
				pri[++pcnt] = i;
			}
			for(int j = 1;j <= pcnt && i * pri[j] <= V;++j) {
				int t = i * pri[j];vis[t] = 1;
				fac[t][0] = fac[i][0] * pri[j];
				fac[t][1] = fac[i][1],fac[t][2] = fac[i][2];
				if(fac[t][0] > fac[t][1]) std::swap(fac[t][0],fac[t][1]);
				if(fac[t][1] > fac[t][2]) std::swap(fac[t][1],fac[t][2]);
				if(!i % pri[j]) break;
			}
		}
	}
	
	inline int gcd(int a,int b) {
		if(a <= SV && b <= SV) return G[a][b];
		int res = 1;
		repl(i,0,3) {
			int t;
			if(fac[a][i] > SV)
				t = (b % fac[a][i]) ? 1 : fac[a][i];
			else
				t = G[fac[a][i]][b % fac[a][i]];
			b /= t,res *= t;
		}
		return res;
	}
}F;
posted @   AstatineAi  阅读(46)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
点击右上角即可分享
微信分享提示
主题色彩