用 Stein 二进制 GCD 把大数取模换成移位与减法,在 10^10000 量级上高效求最大公约数。
OJ: roj
题目 ID: 1630
难度:普及+/提高-
标签:
创建: 2026-09-30 23:06
更新: 2026-10-07 13:50
形式化题目
给定两个正整数
输入两行,每行一个十进制大整数;输出一行,即
暴力解法
思路
最容易想到的是标准的欧几里得算法:
对普通大小的整数它非常快。但本题 math.gcd 帮你封装好了,但除法本身在这类量级上代价依然很高。瓶颈清晰可见——取模 = 大数除法。
代码
复杂度
单次取模
瓶颈
欧几里得算法的核心运算"取模"无法回避大数除法。要消除它,就必须找一个只用移位、比较、减法的 gcd 算法。
正解
思路
Stein 二进制 GCD(又称"移位减法 GCD")正是为这个瓶颈设计的。它基于三条 gcd 性质:
——公因子 2 可以直接提出; ( 为奇数时)——只有一个数是偶数时,因子 2 不贡献 gcd,直接去掉; ( 均为奇数时)——两个奇数之差是偶数。
在二进制下,"除以 2"就是右移一位,代价远低于一次除法。算法流程如下:
flowchart TD
A[输入 a, b] --> B["k = min(v2(a), v2(b))<br/>一次性提出公因子 2"]
B --> C["a 去掉全部尾零<br/>此后 a 一直是奇数"]
C --> D{b = 0 ?}
D -- 是 --> E["输出 a << k"]
D -- 否 --> F["b 去掉全部尾零"]
F --> G{a > b ?}
G -- 是 --> H["交换 a, b"]
G -- 否 --> I["b ← b - a"]
H --> I
I --> D
- 第一步统计
(2 的幂次)用位技巧 (x & -x).bit_length() - 1一次完成; - 循环中只给
去尾零、 保持奇数,这样每轮只做一次比较和一次减法; - 关键在第 3 条性质:两个奇数相减必得偶数,所以
减完之后下一轮至少还能再右移一位——这就是规模快速下降、总轮数维持在 量级的原因。
用一个具体的二进制例子走一遍(求
| 轮次 | 操作 | a(二进制) | b(二进制) | 说明 |
|---|---|---|---|---|
| 初始 | 提出 |
11 |
1001 |
|
| 1 | b 去尾零;b − a | 11 |
110 |
1001 − 11 = 110(偶数) |
| 2 | b 去尾零 | 11 |
11 |
相等了 |
| 3 | b − a = 0 | 11 |
0 |
循环结束 |
| 输出 | a 左移 |
— | — |
观察点:每次"减法 → 尾零 → 右移"的组合都在把
还有一个 Python 特有的坑:Python 3.11 起,int 与字符串互转默认有 4300 位的上限,ValueError。必须先执行 sys.set_int_max_str_digits(0) 解除限制,再读入大数。
代码
Python 版:
python
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 23:10
# update_at: 2026-09-30 23:10
import sys
sys.set_int_max_str_digits(0) # 解除 $10^{10000}$ 级大数与字符串互转的默认 4300 位上限
def trailing_zeros(x: int) -> int:
"""取 x 二进制尾部 0 的个数,即 x 含因子 2 的幂次。"""
return (x & -x).bit_length() - 1
def binary_gcd(a: int, b: int) -> int:
"""Stein 二进制 GCD:只用移位、减法和比较,避免对 $10^{10000}$ 级大数做除法取模。"""
if a == 0: # 较小数为 0 时 gcd 就是另一个数
return b
if b == 0:
return a
shift = min(trailing_zeros(a), trailing_zeros(b)) # 公共因子 2 的个数
a >>= trailing_zeros(a) # 之后 a 一直是奇数
while b:
b >>= trailing_zeros(b) # 去掉 b 的尾零:奇偶差异不贡献 gcd
if a > b:
a, b = b, a
b -= a # 奇 - 奇 = 偶,下一轮还能整除 2,规模减半
return a << shift # 补回公共因子 2
def solve() -> None:
data = iter(sys.stdin.buffer.read().split()) # 仅按位置顺序消费两个大整数字串
a = int(next(data)) # 第一个 $10^{10000}$ 级大数
b = int(next(data))
print(binary_gcd(a, b))
if __name__ == "__main__":
solve()C++ 版(同一算法):
cpp
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 23:22
* update_at: 2026-10-05 23:22
*/
#include <cstdio>
#include <iostream>
#include <string>
using namespace std;
typedef long long ll;
// 手写高精度:以 10^9 为一位,低位在前。
// 本题 A,B 最多 10^10000,约 1112 个 10^9 进制位,开 1200 足够。
const int BASE = 1000000000;
const int MAXL = 1200;
struct Big {
int len; // 有效位数(以 10^9 为一位,至少为 1)
int a[MAXL]; // a[0] 是最低 9 位,依次向高位存
};
Big ga; // 第一个大数
Big gb; // 第二个大数
// 去掉高位多余的 0,零统一表示为 len = 1 且 a[0] = 0
void big_trim(Big &x) {
while (x.len > 1 && x.a[x.len - 1] == 0) {
x.len--;
}
}
// 读入一个十进制大整数串,从低位开始每 9 个数字打包成一位
void big_read(Big &x) {
string s;
cin >> s;
for (int i = 0; i < MAXL; i++) {
x.a[i] = 0;
}
x.len = 0;
int n = s.size();
for (int end = n; end > 0; end -= 9) {
int start = end - 9;
if (start < 0) {
start = 0;
}
int value = 0;
for (int i = start; i < end; i++) {
value = value * 10 + (s[i] - '0');
}
x.a[x.len] = value;
x.len++;
}
big_trim(x);
}
bool big_is_zero(const Big &x) {
return x.len == 1 && x.a[0] == 0;
}
// 十进制大数看最低位即可判断奇偶
bool big_is_even(const Big &x) {
return x.a[0] % 2 == 0;
}
// x /= 2,从高位向低位处理,余数借给下一位
void big_div2(Big &x) {
int carry = 0;
for (int i = x.len - 1; i >= 0; i--) {
int cur = x.a[i] + carry * BASE;
x.a[i] = cur / 2;
carry = cur % 2;
}
big_trim(x);
}
// x *= 2,从低位向高位进位
void big_mul2(Big &x) {
int carry = 0;
for (int i = 0; i < x.len; i++) {
int cur = x.a[i] * 2 + carry;
x.a[i] = cur % BASE;
carry = cur / BASE;
}
if (carry > 0) {
x.a[x.len] = carry;
x.len++;
}
}
// 返回 -1 / 0 / 1 表示 x < y / x == y / x > y
int big_cmp(const Big &x, const Big &y) {
if (x.len != y.len) {
return x.len < y.len ? -1 : 1;
}
for (int i = x.len - 1; i >= 0; i--) {
if (x.a[i] != y.a[i]) {
return x.a[i] < y.a[i] ? -1 : 1;
}
}
return 0;
}
// x -= y,调用前保证 x >= y
void big_sub(Big &x, const Big &y) {
int borrow = 0;
for (int i = 0; i < x.len; i++) {
int yv = (i < y.len) ? y.a[i] : 0;
int cur = x.a[i] - yv - borrow;
if (cur < 0) {
cur += BASE;
borrow = 1;
} else {
borrow = 0;
}
x.a[i] = cur;
}
big_trim(x);
}
void big_swap(Big &x, Big &y) {
Big tmp = x;
x = y;
y = tmp;
}
// 高位直接输出,其余每位补足 9 位前导零
void big_print(const Big &x) {
printf("%d", x.a[x.len - 1]);
for (int i = x.len - 2; i >= 0; i--) {
printf("%09d", x.a[i]);
}
printf("\n");
}
// Stein 二进制 GCD:只用去尾零(除 2)、比较和减法,避免高精度大数除法取模。
void solve() {
big_read(ga);
big_read(gb);
if (big_is_zero(ga)) {
big_print(gb);
return;
}
if (big_is_zero(gb)) {
big_print(ga);
return;
}
int shift = 0; // 两数公共因子 2 的个数,最后再补回
while (big_is_even(ga) && big_is_even(gb)) {
big_div2(ga);
big_div2(gb);
shift++;
}
while (big_is_even(ga)) {
big_div2(ga);
}
while (big_is_even(gb)) {
big_div2(gb);
}
// 此后 ga 恒为奇数;每轮把 gb 去尾零到奇数,再用大减小
while (!big_is_zero(gb)) {
while (big_is_even(gb)) {
big_div2(gb);
}
if (big_cmp(ga, gb) > 0) {
big_swap(ga, gb);
}
big_sub(gb, ga);
}
while (shift > 0) {
big_mul2(ga);
shift--;
}
big_print(ga);
}
int main() {
solve();
return 0;
}复杂度
- 时间:每轮的尾零统计、比较、减法都是
( 为当前位数),总轮数 量级,合计 。对 个二进制位( )大约是百万级位操作,实测最大测例仅 0.05 s。 - 空间:
,存放两个大整数。
总结
- 本题唯一难点是
量级的大数 gcd:欧几里得算法的取模意味着大数除法,成为瓶颈。 - Stein 二进制 GCD 用"提出公因子 2 + 去尾零 + 奇数相减"三条性质,把除法完全替换成移位、比较、减法,复杂度
。 - Python 实现要点:
(x & -x).bit_length() - 1求尾零个数;sys.set_int_max_str_digits(0)解除大数读入上限。