跳转至

欧几里得算法(辗转相除法)

一、公约数与最大公约数

1. 定义与记法

如果一个整数 \(x\) 既是整数 \(A\) 的约数,也是整数 \(B\) 的约数,那么 \(x\) 就是 \(A\) 和 \(B\) 的公约数(也叫公因数)。

例如:5 是 20 和 15 的公约数,7 是 21 和 35 的公约数。

公约数中最大的那个,称为最大公约数(Greatest Common Divisor),记作 \(\gcd(A, B)\)。

例如:24、36、60 的公约数有 2、3、4、6、12,其中最大的是 12,所以 \(\gcd(24, 36, 60) = 12\)。

2. 基本规定

\[ \gcd(a, 0) = \gcd(a, a) = a \]

理解: 任何数都是 0 的约数(因为 \(a \times 0 = 0\)),所以 \(\gcd(a, 0) = a\)。这个规定是辗转相除法递归终止的基础。


二、求 GCD 的方法

1. 暴力枚举法

思路: \(\gcd(a, b)\) 一定在 \([1, \min(a, b)]\) 之间,枚举这个区间内所有数,找最大的公约数。

从小到大枚举:

int gcd = 1;
for(int i = 1; i <= min(a, b); i++){
    if(a % i == 0 && b % i == 0) gcd = i;
}
cout << gcd << endl;

从大到小枚举(优化): 从 \(\min(a, b)\) 往下枚举,找到的第一个公约数就是最大的,直接 break。

int gcd = 1;
for(int i = min(a, b); i >= 1; i--){
    if(a % i == 0 && b % i == 0){
        gcd = i;
        break;
    }
}
cout << gcd << endl;

时间复杂度: \(O(\min(a, b))\)。当 \(a\)、\(b\) 很大时(如 \(10^9\)),会超时。

2. 更相减损法

更相减损术是中国古代《九章算术》记载的求最大公约数的算法,通过反复相减实现化简。

核心结论: 整数 \(a\) 和 \(b\)(设 \(a \geq b\))的最大公约数,等于 \(a - b\) 和 \(b\) 的最大公约数,即:

\[ \gcd(a, b) = \gcd(a - b, b) \]

证明: 设 \(c = \gcd(a, b)\),则 \(a = p \times c\),\(b = q \times c\)(\(p\)、\(q\) 互质)。

那么 \(a - b = p \times c - q \times c = (p - q) \times c\),也是 \(c\) 的倍数,所以 \(c\) 是 \(a - b\) 和 \(b\) 的公约数。

再证 \(c\) 是最大的:假设 \(a - b\) 和 \(b\) 有更大的公约数 \(d = k \times c > c\),则 \(d\) 也能整除 \(a = (a-b) + b\),说明 \(a\) 和 \(b\) 有公约数 \(d > c\),与 \(c = \gcd(a, b)\) 矛盾。所以 \(c = \gcd(a-b, b)\)。

举例: 求 \(\gcd(28, 60)\)

步骤 较大值 较小值 操作
初始 60 28 —
第 1 次 32 28 \(60 - 28 = 32\)
第 2 次 28 4 \(32 - 28 = 4\)
第 3 次 24 4 \(28 - 4 = 24\)
第 4 次 20 4 \(24 - 4 = 20\)
第 5 次 16 4 \(20 - 4 = 16\)
第 6 次 12 4 \(16 - 4 = 12\)
第 7 次 8 4 \(12 - 4 = 8\)
第 8 次 4 4 \(8 - 4 = 4\),两数相等
第 9 次 4 0 \(4 - 4 = 0\),最小值为0

一个数为0时,另一个数就是最大公约数,即 \(\gcd(28, 60) = 4\)。

代码实现:

int gcd(int a, int b){
    //边界条件:若移除,则当a==0 且 b>0时,死循环
    if(a == 0 || b == 0) return a + b;
    //更相减损
    while(a != b){
        if(a > b) a -= b;
        else      b -= a;
    }
    return a;
}

问题: 当 \(a\) 远大于 \(b\) 时(如 \(a = 10^{10}\),\(b = 1\)),需要减 \(10^{10} - 1\) 次,时间复杂度退化为 \(O(\max(a, b))\)。

3. 辗转相除法(欧几里得算法)

3.1 从更相减损法到辗转相除法

更相减损法中,\(\gcd(a, b) = \gcd(a - b, b)\)。当 \(a \gg b\) 时,要从 \(a\) 中连续减很多次 \(b\) 直到不够减,剩下的就是余数 \(a \bmod b\)。与其一次次减,不如直接用除法取余数:

\[ \gcd(a, b) = \gcd(b, a \bmod b) \]

这就是辗转相除法,又称欧几里得算法。

3.2 正确性证明

要证: \(\gcd(a, b) = \gcd(b, a \bmod b)\),其中设 \(a = q \times b + r\)(\(r = a \bmod b\),\(0 \leq r < b\))。

证明:

设 \(d = \gcd(a, b)\),则 \(d \mid a\) 且 \(d \mid b\)。

因为 \(r = a - q \times b\),而 \(d \mid a\) 且 \(d \mid b\),所以 \(d \mid r\)。因此 \(d\) 是 \(b\) 和 \(r\) 的公约数。

反过来,设 \(d' = \gcd(b, r)\),则 \(d' \mid b\) 且 \(d' \mid r\)。因为 \(a = q \times b + r\),所以 \(d' \mid a\)。因此 \(d'\) 是 \(a\) 和 \(b\) 的公约数。

两边互为公约数,所以 \(d = d'\),即 \(\gcd(a, b) = \gcd(b, a \bmod b)\)。\(\blacksquare\)

3.3 时间复杂度证明

结论: \(O(\log(\max(a, b)))\)

关键: 每两次递归,较大的数至少减半。

设 \(a \ge b>0\),序列 \(r_0=a,\ r_1=b\),且 \(r_{i+1}=r_{i-1}\bmod r_i\),那么有:

\((r_0, r_1) \to (r_1, r_2) \to (r_2, r_3) \to \cdots\)

于是,对于递归序列为 \(r_0, r_1, r_2, \dots\),有:

  • 若 \(r_k \leq r_{k-1} / 2\),根据 \(r_{k+1}=r_{k-1}\bmod r_k\),则 \(r_{k+1} < r_k \leq r_{k-1} / 2\),较大值减半。
  • 若 \(r_k > r_{k-1} / 2\),则 \(r_{k+1} = r_{k-1} \bmod r_k = r_{k-1} - r_k < r_{k-1} / 2\),下一步的较大值 \(r_k\) 的余数 \(r_{k+1} < r_{k-1}/2\),也减半。

所以每两步,数至少减半,总步数不超过 \(2 \log_2(\max(a, b))\)。

3.4 举例验证

求 \(\gcd(28, 60)\):

步骤 \(a\) \(b\) \(a \bmod b\) 操作
初始 28 60 — —
第 1 次 60 28 \(28 \bmod 60 = 28\) \(\gcd(28, 60) = \gcd(60, 28)\)
第 2 次 28 \(60 \bmod 28 = 4\) — \(\gcd(60, 28) = \gcd(28, 4)\)
第 3 次 4 \(28 \bmod 4 = 0\) — \(\gcd(28, 4) = \gcd(4, 0)\)
终止 4 0 — \(b = 0\),返回 \(a = 4\)

\(\gcd(28, 60) = 4\) ✓(与更相减损法结果一致,但只用了 3 步)

3.5 代码实现

递归写法:

int gcd(int a, int b){
    if(b == 0) return a;
    return gcd(b, a % b);
}

逐行解释:

代码 作用
if(b == 0) return a 递归终止:\(b=0\) 时,\(a\) 就是 GCD
return gcd(b, a % b) 辗转相除:把 \(b\) 放前面,\(a \bmod b\) 放后面

迭代写法:

int gcd(int a, int b){
    while(b != 0){
        int t = a % b;
        a = b;
        b = t;
    }
    return a;
}

递归 vs 迭代: 两者逻辑完全相同。递归写法更简洁(一行核心代码),迭代写法无函数调用开销。信奥中两种都常见,递归更易记。

4. Stein 算法(更相减损法优化)

针对更相减损法在 \(a\) 远大于 \(b\) 时的低效问题,Stein 算法利用"偶数可以快速除以 2"的性质进行优化。

三条规则:

  1. 若 \(a\) 和 \(b\) 都是偶数,则 \(\gcd(a, b) = 2 \times \gcd(a/2, b/2)\)
  2. 若只有一个偶数,把偶数的因子 2 丢弃:\(\gcd(a, b) = \gcd(a/2, b)\) 或 \(\gcd(a, b/2)\)
  3. 若 \(a\) 和 \(b\) 都是奇数,则 \(\gcd(a, b) = \gcd(|a-b|, \min(a, b))\)

为什么快: 两个奇数相减得到偶数,下一步就可以除以 2。所以每 1~2 次操作,较大值至少减半,时间复杂度 \(O(\log(\max(a, b)))\)。

代码实现:

int gcd_stein(int a, int b){
    a = abs(a);
    b = abs(b);
    if(a == 0 || b == 0) return a + b;

    // 步骤1:提取公因子 2
    int d = 0;// 记录公因子 2 的个数
    while(!(a & 1) && !(b &1)) d++, a >>= 1, b >>= 1;

    // 步骤2:消除各自单独的因子 2
    while(!(a & 1)) a >>= 1;
    while(!(b & 1)) b >>= 1;

    // 步骤3:更相减损,每次减后消因子 2
    while(a != b){
        if(a > b) a -= b;
        else      b -= a;
        while(!(a & 1)) a >>= 1;
        while(!(b & 1)) b >>= 1;
    }
    return a << d;
}

位运算说明: a & 1 等价于 a % 2(判断奇偶),a >>= 1 等价于 a /= 2(除以 2),a << d 等价于 a * 2^d。大整数场景下位运算比取模快得多,这是 Stein 算法存在的意义。

适用场景: 大整数运算中,取模(%)的时间开销远大于加减法和位移。Stein 算法用加减法和除以 2(位运算右移)替代取模,适合大整数 GCD。

5. 四种方法对比

方法 核心思路 时间复杂度 适用场景
暴力枚举法 枚举 \([1, \min(a,b)]\) \(O(\min(a,b))\) 理解概念,不用于比赛
更相减损法 大减小,直到相等 \(O(\max(a,b))\) 最坏 历史了解,实际不用
辗转相除法 \(\gcd(a,b) = \gcd(b, a \bmod b)\) \(O(\log(\max(a,b)))\) 比赛首选
Stein 算法 更相减损 + 消因子 2 \(O(\log(\max(a,b)))\) 大整数(取模昂贵时)

信奥结论: 比赛中一律用辗转相除法(欧几里得算法),代码短、速度快、不易出错。


三、公倍数与最小公倍数

1. 定义与记法

如果一个整数 \(x\) 既是 \(A\) 的倍数,也是 \(B\) 的倍数,那么 \(x\) 是 \(A\) 和 \(B\) 的公倍数。

公倍数中最小的那个,称为最小公倍数(Least Common Multiple),记作 \(\text{lcm}(A, B)\)。

例如:2、3、6 的公倍数有 6、12、24…,最小的是 6,所以 \(\text{lcm}(2, 3, 6) = 6\)。

2. 求 LCM 的方法

2.1 暴力枚举法

思路: \(\text{lcm}(a, b)\) 不超过 \(a \times b\),在 \([1, a \times b]\) 内从小到大枚举,找第一个同时被 \(a\) 和 \(b\) 整除的数。

int lcm(int a, int b){
    for(int i = 1; i <= a * b; i++){
        if(i % a == 0 && i % b == 0) return i;
    }
}

时间复杂度: \(O(a \times b)\),当 \(a\)、\(b\) 较大时严重超时。

2.2 用 GCD 计算 LCM

这是最实用的方法,先求 GCD,再用公式算 LCM。

公式:

\[ \text{lcm}(a, b) = \frac{a \times b}{\gcd(a, b)} \]

代码实现:

int gcd(int a, int b){
    if(b == 0) return a;
    return gcd(b, a % b);
}

int lcm(int a, int b){
    return a / gcd(a, b) * b;  // 先除后乘,避免溢出
}

关键技巧: 写成 a / gcd(a, b) * b,先除后乘。如果写成 a * b / gcd(a, b),a * b 可能溢出(如 \(a = b = 10^9\) 时,\(a \times b = 10^{18}\) 超过 int 范围)。

公式的推导见第四节,它来自唯一分解定理。


四、唯一分解定理与 GCD、LCM 的关系

1. 从唯一分解定理看 GCD(取小指数)

由唯一分解定理,设 \(a\) 和 \(b\) 分解质因数为(缺失的质因子指数视为 0):

\[ a = p_1^{a_1} \times p_2^{a_2} \times \dots \times p_n^{a_n} \]
\[ b = p_1^{b_1} \times p_2^{b_2} \times \dots \times p_n^{b_n} \]

则:

\[ \gcd(a, b) = p_1^{\min(a_1, b_1)} \times p_2^{\min(a_2, b_2)} \times \dots \times p_n^{\min(a_n, b_n)} \]

理解: GCD 取每个质因数在两数中指数的较小值。只出现在一个数中的质因子,另一个数指数为 0,\(\min\) 取 0,所以不参与 GCD。

这就是质因数分解法求 GCD 的理论基础。

核心口诀:GCD 取公共质因数的小指数。

举例: \(a = 360 = 2^3 \times 3^2 \times 5^1\),\(b = 120 = 2^3 \times 3^1 \times 5^1\)

质因子 \(a\) 的指数 \(b\) 的指数 GCD 取小
2 3 3 3
3 2 1 1
5 1 1 1
\[ \gcd(360, 120) = 2^3 \times 3^1 \times 5^1 = 120 \]

代码实现:

int gcd(int a, int b){
    // 对 a 分解质因数
    int pa[100] = {}, cnta[100] = {}, lena = 0;
    for(int i = 2; i <= a / i; i++){
        if(a % i == 0){
            pa[++lena] = i;
            while(a % i == 0) cnta[lena]++, a /= i;
        }
    }
    if(a > 1) pa[++lena] = a, cnta[lena] = 1;

    // 对 b 分解质因数
    int pb[100] = {}, cntb[100] = {}, lenb = 0;
    for(int i = 2; i <= b / i; i++){
        if(b % i == 0){
            pb[++lenb] = i;
            while(b % i == 0) cntb[lenb]++, b /= i;
        }
    }
    if(b > 1) pb[++lenb] = b, cntb[lenb] = 1;

    // 双指针合并,取公共质因数的小指数
    int d = 1, i = 1, j = 1;
    while(i <= lena && j <= lenb){
        if(pa[i] < pb[j]) i++;              // a 独有的质因子,跳过
        else if(pa[i] > pb[j]) j++;         // b 独有的质因子,跳过
        else {                               // 公共质因子,取小指数
            int mincnt = min(cnta[i], cntb[j]);
            for(int k = 1; k <= mincnt; k++) d *= pa[i];
            i++, j++;
        }
    }
    return d;
}

注意: GCD 只取公共质因数,非公共的(只出现在一个数中的)不参与。这与 LCM 不同,下面对比。

2. 从唯一分解定理看 LCM(取大指数)

\[ \text{lcm}(a, b) = p_1^{\max(a_1, b_1)} \times p_2^{\max(a_2, b_2)} \times \dots \times p_n^{\max(a_n, b_n)} \]

理解: LCM 取每个质因数在两数中指数的较大值。只出现在一个数中的质因子也参与,取那个非零的指数。

这就是质因数分解法求 LCM 的理论基础。

核心口诀:LCM 取所有质因数的大指数。

与 GCD 的区别: GCD 只取公共质因数的小指数;LCM 取所有质因数的大指数(包括只出现在一个数中的)。

举例: \(a = 360 = 2^3 \times 3^2 \times 5^1\),\(b = 120 = 2^3 \times 3^1 \times 5^1\)

质因子 \(a\) 的指数 \(b\) 的指数 LCM 取大
2 3 3 3
3 2 1 2
5 1 1 1
\[ \text{lcm}(360, 120) = 2^3 \times 3^2 \times 5^1 = 360 \]

代码实现:

int lcm(int a, int b){
    // 对 a 分解质因数
    int pa[100] = {}, cnta[100] = {}, lena = 0;
    for(int i = 2; i <= a / i; i++){
        if(a % i == 0){
            pa[++lena] = i;
            while(a % i == 0) cnta[lena]++, a /= i;
        }
    }
    if(a > 1) pa[++lena] = a, cnta[lena] = 1;

    // 对 b 分解质因数
    int pb[100] = {}, cntb[100] = {}, lenb = 0;
    for(int i = 2; i <= b / i; i++){
        if(b % i == 0){
            pb[++lenb] = i;
            while(b % i == 0) cntb[lenb]++, b /= i;
        }
    }
    if(b > 1) pb[++lenb] = b, cntb[lenb] = 1;

    // 双指针合并,取所有质因数的大指数
    int d = 1, i = 1, j = 1;
    while(i <= lena || j <= lenb){
        if(j > lenb || (i <= lena && pa[i] < pb[j])){
            // a 独有的质因子,取 a 的指数
            for(int k = 1; k <= cnta[i]; k++) d *= pa[i];
            i++;
        } else if(i > lena || (j <= lenb && pa[i] > pb[j])){
            // b 独有的质因子,取 b 的指数
            for(int k = 1; k <= cntb[j]; k++) d *= pb[j];
            j++;
        } else {
            // 公共质因子,取大指数
            int maxcnt = max(cnta[i], cntb[j]);
            for(int k = 1; k <= maxcnt; k++) d *= pa[i];
            i++, j++;
        }
    }
    return d;
}

易错点: LCM 的质因数分解法必须处理所有质因数,包括只出现在 \(a\) 或只出现在 \(b\) 中的。只处理公共质因数会漏掉非公共部分,导致结果偏小。

3. 核心公式:GCD × LCM = A × B

观察: 对每个质因子 \(p_i\),\(\min(a_i, b_i)\) 和 \(\max(a_i, b_i)\) 分别取到了 \(a_i\) 和 \(b_i\) 各一次,所以:

\[ \min(a_i, b_i) + \max(a_i, b_i) = a_i + b_i \]

因此:

\[ \gcd(a, b) \times \text{lcm}(a, b) = \prod_{i=1}^{n} p_i^{\min(a_i, b_i) + \max(a_i, b_i)} = \prod_{i=1}^{n} p_i^{a_i + b_i} = a \times b \]

即:

\[ \boxed{\gcd(a, b) \times \text{lcm}(a, b) = a \times b} \]

变形得:

\[ \text{lcm}(a, b) = \frac{a \times b}{\gcd(a, b)} \]

这就是第三节中"用 GCD 计算 LCM"公式的由来。

4. 举例验证

例: \(a = 12 = 2^2 \times 3^1\),\(b = 18 = 2^1 \times 3^2\)

质因子 \(a\) 的指数 \(b\) 的指数 GCD 取 \(\min\) LCM 取 \(\max\)
2 2 1 1 2
3 1 2 1 2
\[ \gcd(12, 18) = 2^1 \times 3^1 = 6 \]
\[ \text{lcm}(12, 18) = 2^2 \times 3^2 = 36 \]

验证公式:\(\gcd \times \text{lcm} = 6 \times 36 = 216\),\(a \times b = 12 \times 18 = 216\) ✓


五、常见错误与注意事项

编号 错误 后果 改正
E1 LCM 写成 a * b / gcd(a,b) \(a \times b\) 溢出 写成 a / gcd(a,b) * b,先除后乘
E2 LCM 质因数分解法只处理公共质因数 漏掉非公共质因子,结果偏小 处理所有质因数,非公共的也要取大指数
E3 辗转相除法递归终止条件写错 死递归或返回错误 终止条件是 b == 0,返回 a
E4 忘记 gcd(a, 0) = a 的规定 边界情况处理错误 记住 0 的约数是所有数,\(\gcd(a, 0) = a\)
E5 更相减损法用于 \(a\) 远大于 \(b\) 超时(如 \(a=10^{10}, b=1\)) 用辗转相除法或 Stein 算法
E6 GCD 和 LCM 的取指数搞反 GCD 取了大指数,LCM 取了小指数 GCD 取 \(\min\),LCM 取 \(\max\)
E7 递归写法参数顺序写反 计算错误 gcd(b, a % b),余数放后面

口诀防混淆: GCD 是"公小"(公共质因数取小指数),LCM 是"全大"(所有质因数取大指数)。


六、方法、套路与策略小结

1. 求 GCD 的套路

  • 比赛首选: 辗转相除法(欧几里得算法),一行递归搞定。
  • 记忆公式: \(\gcd(a, b) = \gcd(b, a \bmod b)\),终止于 \(b = 0\)。
  • 大整数场景: 取模昂贵时用 Stein 算法(加减 + 除以 2)。
  • 需要分解结果: 用质因数分解法(见第四节),顺便得到每个质因数的指数。

2. 求 LCM 的套路

  • 比赛首选: 先求 GCD,再套公式 \(\text{lcm} = a / \gcd(a, b) \times b\)。
  • 防溢出: 先除后乘,a / gcd(a, b) * b。
  • 口诀: LCM 取所有质因数的大指数(全大),GCD 取公共质因数的小指数(公小)。

3. 唯一分解定理与 GCD/LCM 的关系

  • 桥梁: 唯一分解定理是 GCD 和 LCM 的理论基础。
  • 核心公式: \(\gcd(a, b) \times \text{lcm}(a, b) = a \times b\),知道 GCD 就能算 LCM,反之亦然。
  • 指数关系: \(\min + \max = a_i + b_i\),所以 GCD 和 LCM 的指数互补。

4. 信奥策略意识

  • 复杂度意识: 辗转相除法 \(O(\log(\max(a, b)))\),即使 \(a, b = 10^{18}\) 也只需约 60 步,极快。
  • 溢出意识: 求 LCM 时先除后乘;如果结果可能超过 int,用 long long。
  • 代码简洁意识: 递归一行 return b ? gcd(b, a % b) : a; 是最简写法。
  • 验证习惯: 算完 GCD 后用唯一分解定理验证;算完 LCM 后用 \(\gcd \times \text{lcm} = a \times b\) 验证。

七、训练题单

1. 基础题目

2. 拓展题目