最大公约数(Greatest Common Divisor,GCD)是机试中高频基础考点,核心是掌握欧几里得算法(辗转相除法),并能灵活应用到 "最简分数、分数化简、组合计数" 等变形题中。
一、核心知识点
1. 欧几里得算法(辗转相除法)
核心原理 :两个数的最大公约数 = 较小数 和 较大数 % 较小数 的最大公约数,直到其中一个数为 0,另一个数就是 GCD。递归实现(最简版):
cpp
// 求a和b的最大公约数(a,b为正整数)
int gcd(int a, int b) {
if (b == 0) return a; // 终止条件:b=0时,a是GCD
return gcd(b, a % b); // 递归:gcd(a,b) = gcd(b, a%b)
}
迭代实现(避免递归栈溢出):
cpp
int gcd(int a, int b) {
while (b != 0) {
int temp = b;
b = a % b;
a = temp;
}
return a;
}
2. 核心性质(机试常用)
- 若
gcd(a, b) = 1,则 a 和 b互质(无法约分,是最简分数的核心条件); - 分数化简:
x/y = (x/gcd(x,y)) / (y/gcd(x,y))(如 12/30 = (12/6)/(30/6) = 2/5); - 多个数的 GCD:
gcd(a,b,c) = gcd(gcd(a,b), c)(递推计算)。
二、经典例题 1:最简真分数计数
题目描述
输入 n 个正整数(n≤600,数≤1000),任取两个数作为分子 / 分母组成最简真分数(分子 < 分母且互质),求组合数。
- 输入样例:7 → 3 5 7 9 11 13 15 → 输出 17;
- 核心条件:① 分子 <分母(枚举时 i<j,避免重复);② gcd (分子,分母)=1(互质)。
完整 C 语言代码(注释版)
cpp
#include <stdio.h>
// 欧几里得算法求最大公约数
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
int main() {
int n;
int nums[605]; // 存储输入的n个数(n≤600,留冗余)
// 循环输入测试用例,直到文件结束
while (scanf("%d", &n) != EOF) {
// 输入n个正整数
for (int i = 0; i < n; i++) {
scanf("%d", &nums[i]);
}
int count = 0; // 统计最简真分数的组合数
// 枚举所有i<j的组合(保证分子<分母,避免重复)
for (int i = 0; i < n; i++) {
for (int j = i + 1; j < n; j++) {
// 互质则计数+1
if (gcd(nums[i], nums[j]) == 1) {
count++;
}
}
}
// 输出结果
printf("%d\n", count);
}
return 0;
}
代码逻辑验证(输入样例)
输入数组:[3,5,7,9,11,13,15]
- 枚举 i=0(3):j=1 (5, gcd=1)、j=2 (7, gcd=1)、j=3 (9, gcd=3)、j=4 (11, gcd=1)、j=5 (13, gcd=1)、j=6 (15, gcd=3) → 计数 + 4;
- 枚举 i=1(5):j=2 (7,1)、j=3 (9,1)、j=4 (11,1)、j=5 (13,1)、j=6 (15,5) → 计数 + 4(累计 8);
- 后续枚举累计最终为 17,和样例输出一致。
三、经典例题 2:分数化简(机试常见变形)
题目描述
输入分数 x/y(x、y 为正整数),将其化简为最简分数(分子分母互质)。
- 示例:输入 12 30 → 输出 2/5;输入 18 24 → 输出 3/4。
完整 C 语言代码
cpp
#include <stdio.h>
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
int main() {
int x, y;
// 输入分子x和分母y
while (scanf("%d%d", &x, &y) != EOF) {
int g = gcd(x, y); // 求最大公约数
// 化简:分子分母分别除以GCD
int simplified_x = x / g;
int simplified_y = y / g;
printf("%d/%d\n", simplified_x, simplified_y);
}
return 0;
}
边界处理
- 若 y=0:需提示输入错误(分母不能为 0);
- 若 x=0:化简结果为 0/1(0 和任何数的 GCD 是该数本身)。
补充容错版代码:
cpp
#include <stdio.h>
int gcd(int a, int b) {
if (b == 0) return a;
return gcd(b, a % b);
}
int main() {
int x, y;
while (scanf("%d%d", &x, &y) != EOF) {
if (y == 0) {
printf("Input error! Denominator cannot be 0.\n");
continue;
}
if (x == 0) {
printf("0/1\n");
continue;
}
int g = gcd(x, y);
printf("%d/%d\n", x/g, y/g);
}
return 0;
}
四、GCD 其他常见变形考法
1. 最小公倍数(LCM)
- 公式:
lcm(a,b) = a*b / gcd(a,b)(先乘后除可能溢出,建议a/gcd(a,b)*b); - 应用:如 "求两个数的最小公倍数""多个数的最小公倍数"。
代码实现:
cpp
int lcm(int a, int b) {
int g = gcd(a, b);
return a / g * b; // 先除后乘避免溢出(如a=1e9, b=1e9时,a*b会溢出)
}
2. 多个数的 GCD
- 递推计算:先算前两个数的 GCD,再和第三个数算 GCD,依此类推。
代码实现:
cpp
// 求数组nums中n个数的最大公约数
int gcd_n(int nums[], int n) {
int res = nums[0];
for (int i = 1; i < n; i++) {
res = gcd(res, nums[i]);
if (res == 1) break; // 优化:GCD=1时无需继续计算
}
return res;
}
3. 统计区间内与 n 互质的数
- 问题:求 1~m 中与 n 互质的数的个数;
- 解法:遍历 1~m,判断 gcd (i,n)==1,计数即可。
总结
- GCD 核心:欧几里得算法 (递归 / 迭代均可),记住
gcd(a,b) = gcd(b,a%b),终止条件b=0; - 核心应用:
- 最简真分数:枚举 i<j 的组合,判断
gcd(nums[i],nums[j])==1; - 分数化简:分子分母分别除以 GCD;
- 最小公倍数:
lcm(a,b) = a/gcd(a,b)*b;
- 最简真分数:枚举 i<j 的组合,判断
- 优化技巧:
- 枚举组合时仅需 i<j(避免重复计算,如 3/5 和 5/3 算一个);
- 计算 LCM 时先除后乘,避免数值溢出;
- 多个数的 GCD 计算中,若中途结果为 1 可直接终止(1 和任何数的 GCD 都是 1)。
掌握这些核心知识点和变形,机试中所有 GCD 相关题目都能解决。
3.4 斐波那契数列
核心知识点
- 定义:F(1)=1,F(2)=1,F(n)=F(n-1)+F(n-2)(n≥3);
- 关键注意点 :
- 数列增长极快:F (45)≈1.1e9(超出 int 范围),F (90)≈2.88e18(超出 long long 范围);
- 若题目要求取模(如模 1e9+7),可边算边取模,甚至用矩阵快速幂计算超大项(如第 1e7 项);
- 特殊变形:数列
a(1)=1,a(n+1)=1+1/a(n)的通项为a(n)=F(n+1)/F(n)。
实用代码(带取模,避免溢出)
cpp
#include <stdio.h>
// 计算斐波那契第n项,mod为模数(如1e9+7)
long long fib(int n, int mod) {
if (n <= 2) return 1 % mod;
long long a = 1, b = 1, res;
for (int i = 3; i <= n; i++) {
res = (a + b) % mod; // 边算边取模,避免溢出
a = b;
b = res;
}
return res;
}
int main() {
int n;
scanf("%d", &n);
printf("F(%d) = %lld\n", n, fib(n, 1000000007)); // 示例模数
return 0;
}
机试常见变形
- 走台阶问题:一次走 1/2 步,走 n 级台阶的方法数 = 斐波那契第 n+1 项;
- 兔子繁殖问题:经典斐波那契应用,和数列定义完全一致;
- 分数通项问题:如
a(n)=1+1/a(n-1)可转化为斐波那契比值。
3.5 素数判定(单个素数判断)
核心知识点
- 素数定义:大于 1 的自然数,除了 1 和自身无其他约数;
- 优化判定逻辑 :无需遍历到 x-1,只需遍历到
sqrt(x)(若 x 有大于 sqrt (x) 的约数,必然有一个小于 sqrt (x) 的约数); - 边界处理:1 不是素数,2 是最小的素数,偶数(除 2 外)一定不是素数。
经典例题:找大于 n 的第一个素数
题目描述
输入整数 n(最大 10000),若 n 是素数则输出 n;否则输出大于 n 的第一个素数(如输入 14→输出 17)。
完整代码(优化版)
cpp
#include <stdio.h>
#include <math.h>
// 判定x是否为素数(核心函数)
int isPrime(int x) {
if (x <= 1) return 0; // 1及以下不是素数
if (x == 2) return 1; // 2是素数
if (x % 2 == 0) return 0;// 偶数(除2外)不是素数
// 只遍历奇数,从3到sqrt(x),步长2(优化效率)
for (int j = 3; j <= sqrt(x); j += 2) {
if (x % j == 0) return 0;
}
return 1;
}
int main() {
int n;
scanf("%d", &n);
// 从n开始遍历,找到第一个素数
for (int i = n; ; i++) {
if (isPrime(i)) {
printf("%d\n", i);
break;
}
}
return 0;
}
代码优化点
- 单独处理偶数:减少一半遍历次数;
- 提取
isPrime函数:代码复用性更高,逻辑更清晰; - 遍历步长设为 2:仅判断奇数,效率提升。
3.6 素数筛选(区间素数统计)
核心知识点
- 问题背景 :若需多次查询区间内素数个数,逐个判定效率低(O (n√n)),需用线性筛(欧拉筛) 预处理(O (n));
- 线性筛原理 :
- 用数组标记非素数,遍历每个数,若未被标记则为素数;
- 用当前素数乘以遍历数,标记乘积为非素数;
- 若遍历数能被当前素数整除,停止标记(避免重复标记,保证线性复杂度)。
经典例题:统计区间 [a,b] 内的素数个数
题目描述
多组输入 a、b(2≤a,b≤1000),输出区间内素数个数(如输入 2 4→输出 2,输入 4 6→输出 1)。
完整 C 语言代码
cpp
#include <stdio.h>
#include <string.h>
#define MAXN 1000005 // 筛到1e6,满足绝大多数机试需求
// prime数组:prime[0]存素数个数,prime[1..prime[0]]存素数,其余位置标记非素数(1=非素数,0=素数)
int prime[MAXN];
// 线性筛预处理素数
void getPrime() {
memset(prime, 0, sizeof(prime)); // 初始化全为0(默认是素数)
for (int i = 2; i < MAXN; i++) {
if (!prime[i]) { // 未被标记,是素数
prime[++prime[0]] = i;
}
// 用当前素数标记乘积为非素数
for (int j = 1; j <= prime[0] && (long long)prime[j] * i < MAXN; j++) {
prime[prime[j] * i] = 1; // 标记为非素数
if (i % prime[j] == 0) { // 避免重复标记,终止循环
break;
}
}
}
}
int main() {
getPrime(); // 预处理所有素数(只需执行一次)
int a, b;
while (scanf("%d%d", &a, &b) != EOF) {
// 保证a ≤ b
if (a > b) {
int temp = a;
a = b;
b = temp;
}
int count = 0;
// 遍历预处理的素数,统计区间内的数量
for (int i = 1; i <= prime[0]; i++) {
if (prime[i] >= a && prime[i] <= b) {
count++;
}
if (prime[i] > b) { // 超出区间,提前终止
break;
}
}
printf("%d\n", count);
}
return 0;
}
代码关键说明
- 纯 C 实现:替换
bits/stdc++.h和swap,用memset初始化,符合机试 C 语言环境; - 类型安全:
(long long)prime[j] * i避免乘法溢出; - 提前终止:遍历素数时超出 b 则 break,提升效率。
核心对比:素数判定 vs 素数筛选
| 方法 | 时间复杂度 | 适用场景 |
|---|---|---|
| 单个判定 | O(√n) | 仅判断 1-2 个数是否为素数 |
| 线性筛 | O(n) | 多次查询区间素数、大范围素数 |
总结
斐波那契数列
- 核心:边算边取模避免溢出,超大项用矩阵快速幂;
- 变形:走台阶、兔子繁殖、分数通项均需转化为斐波那契模型。
素数相关
- 单个素数判定:遍历到√x,提前处理偶数,效率更高;
- 区间素数统计:先线性筛预处理,再查询(机试高频考法);
- 线性筛关键:
i % prime[j] == 0时 break,保证线性复杂度。
掌握这些代码和思路,机试中所有斐波那契、素数相关题目都能解决,尤其是素数筛选是机试 "区间统计类" 题目的核心工具。