最大公约数(GCD)和最小公倍数(LCM)专题练习 ——基于罗勇军老师的《蓝桥杯算法入门C/C++》
一、1.互质数的个数 - 蓝桥云课
算法代码(只能通过30%):
#include <bits/stdc++.h> // 包含所有标准库头文件,方便编程
using namespace std; // 使用标准命名空间,避免每次调用标准库函数时写 std::
typedef long long ll; // 定义 long long 类型的别名 ll,方便使用
ll mod = 998244353; // 定义一个常量 mod,值为 998244353,用于模运算
// 计算最大公约数(GCD)的函数
ll gcd(ll a, ll b) {
return b ? gcd(b, a % b) : a; // 递归实现欧几里得算法,b 为 0 时返回 a,否则递归计算 gcd(b, a % b)
}
// 快速幂函数,计算 a 的 n 次方模 mod
ll fastPow(ll a, ll n) {
ll ans = 1; // 初始化结果为 1
a %= mod; // 对 a 取模,避免溢出
while (n) { // 当 n 不为 0 时循环
if (n & 1) { // 如果 n 的最低位是 1
ans = (ans * a) % mod; // 将 a 乘到结果中,并对 mod 取模
}
a = (a * a) % mod; // 将 a 平方,并对 mod 取模
n >>= 1; // 将 n 右移一位,相当于 n /= 2
}
return ans; // 返回最终结果
}
int main() {
ll a, b; // 定义两个长整型变量 a 和 b
cin >> a >> b; // 从标准输入读取 a 和 b 的值
ll mi = fastPow(a, b); // 计算 a 的 b 次方模 mod,结果存储在 mi 中
ll ans = 0; // 初始化计数器 ans 为 0
for (int i = 1; i < mi; i++) { // 遍历从 1 到 mi-1 的所有整数
if (gcd(i, mi) == 1) { // 如果当前整数 i 与 mi 的最大公约数为 1(即互质)
ans++; // 计数器 ans 加 1
}
}
cout << ans; // 输出最终的计数结果
return 0; // 程序正常结束
}
算法代码(通过100%):
// 本题主要考察欧拉函数和快速幂
// 欧拉函数 Euler(n): 表示不大于 n 且与 n 互质的正整数的个数,Euler(1) = 1
// 由唯一分解定理,n = p1^k1 * p2^k2 * ... * pn^km,pi 均为质数,ki 是其幂次
// 由此可推出欧拉函数的求法:Euler(n) = n / p1 * (p1 - 1) / p2 * (p2 - 1) / ... / pn * (pn - 1)
// 将欧拉函数的模板背下来即可
// 由欧拉函数的模板可知,若已知 Euler(a) = m,则 Euler(a^b) = m * (a^(b-1))
// 故先求 Euler(a),再用快速幂求 a^(b-1),二者相乘即为最终答案
#include <bits/stdc++.h> // 包含所有标准库头文件,方便编程
using namespace std; // 使用标准命名空间,避免每次调用标准库函数时写 std::
const int mod = 998244353; // 定义一个常量 mod,值为 998244353,用于模运算
typedef unsigned long long ull; // 定义 unsigned long long 类型的别名 ull,方便使用
// 快速幂算法,计算 base^power % mod
ull quick_power(ull base, ull power, ull mod) {
ull res = 1; // 初始化结果为 1
while (power) { // 当 power 不为 0 时循环
if (power & 1) // 如果 power 的最低位是 1
res = res * base % mod; // 将 base 乘到结果中,并对 mod 取模
base = base * base % mod; // 将 base 平方,并对 mod 取模
power = power >> 1; // 将 power 右移一位,相当于 power /= 2
}
return res % mod; // 返回最终结果
}
// 求 n 的欧拉函数(固定模板)
ull Euler(ull n) {
ull phi = n; // 初始化 phi 为 n
for (int i = 2; i * i <= n; i++) { // 枚举 n 的质因数
if (n % i) continue; // 如果 i 不是 n 的因数,跳过
while (n % i == 0) { // i 是质因数
n = n / i; // n 不断除以 i 直至 i 不再是 n 的质因数
}
phi = phi / i * (i - 1); // 递推欧拉函数,Euler(n) = n / pi * (pi - 1)
}
// 最后可能还剩下一个大于 sqrt(n) 的质因数,如 12 = 2 * 2 * 3,最后将剩下 3,补充上
if (n > 1) phi = phi / n * (n - 1);
return phi; // 返回欧拉函数值
}
// 由如上算法可知,n 的欧拉函数只与其质因数的组成有关,与每个质因数的个数无关
// 对于不同的数字,只要它们的质因数组成相同,计算过程中就会除以相同的 pi 乘以相同的 (pi - 1)
// 故若 m 和 n 的质因数组成相同,m 是 n 的 k 倍,则 Euler(m) 也是 Euler(n) 的 k 倍
// 而 a^b 是 a 的 a^(b-1) 倍,则由此推出 Euler(a^b) = Euler(a) * (a^(b-1)),此即为最终答案
int main() {
ull a, b; // 定义两个无符号长整型变量 a 和 b
cin >> a >> b; // 从标准输入读取 a 和 b 的值
ull Euler_a = Euler(a); // 计算 a 的欧拉函数值
// 最终答案:Euler(a) * a^(b-1) % mod
ull ans = Euler_a * quick_power(a, b - 1, mod) % mod;
cout << ans << endl; // 输出最终答案
return 0; // 程序正常结束
}
二、1.等差数列 - 蓝桥云课
算法代码:
#include <bits/stdc++.h> // 包含所有标准库头文件
using namespace std; // 使用标准命名空间
int a[100000]; // 定义一个全局数组 a,用于存储输入的整数,最大容量为 100000
int main() {
int n; // 定义一个变量 n,用于存储输入的整数个数
cin >> n; // 从标准输入读取整数个数 n
// 循环读取 n 个整数并存储到数组 a 中
for (int i = 0; i < n; i++) {
cin >> a[i]; // 读取第 i 个整数并存储到数组 a 中
}
sort(a, a + n); // 对数组 a 中的元素进行升序排序
int d = 0; // 定义一个变量 d,用于存储数组中相邻元素的差值的最大公约数(GCD)
// 计算数组中相邻元素的差值的最大公约数
for (int i = 1; i < n; i++) {
d = __gcd(d, a[i] - a[i - 1]); // 更新 d 为当前 d 和相邻元素差值的 GCD
}
// 判断 d 是否为 0
if (d == 0) {
cout << n << endl; // 如果 d 为 0,说明所有元素相同,输出 n
} else {
// 如果 d 不为 0,计算等差数列的项数并输出
printf("%d\n", (a[n - 1] - a[0]) / d + 1);
}
return 0; // 程序正常结束
}
主要思路:
三、1.核桃的数量 - 蓝桥云课
算法代码:
#include <bits/stdc++.h> // 包含所有标准库头文件
using namespace std; // 使用标准命名空间
// 定义一个函数 lcm,用于计算两个数的最小公倍数
int lcm(int a, int b) {
return a / __gcd(a, b) * b; // 使用公式 LCM(a, b) = (a * b) / GCD(a, b)
}
int main() {
int a, b, c; // 定义三个整数变量 a, b, c
cin >> a >> b >> c; // 从标准输入读取三个整数 a, b, c
int k = lcm(a, b); // 计算 a 和 b 的最小公倍数,并将结果存储在变量 k 中
cout << lcm(k, c) << endl; // 计算 k 和 c 的最小公倍数,并输出结果
return 0; // 程序正常结束
}
四、P1820 - [NewOJ Week 6] 最小公倍数 - New Online Judge
算法代码:
#include <bits/stdc++.h> // 包含所有标准库头文件
using namespace std; // 使用标准命名空间
typedef long long ll; // 定义 long long 类型为 ll,方便使用
const ll INF = 1e18; // 定义 INF 为 10^18,表示最大值
map<ll, pair<int, int>> ans; // 定义一个 map,存储 n 对应的区间 [L, R]
// 预处理函数,计算所有可能的 n 对应的 [L, R]
void init() {
// 遍历 L 从 1 到 2e6
for (ll L = 1; L <= 2000000; L++) {
ll n = L * (L + 1); // 计算 L 和 L+1 的乘积
// 遍历 R 从 L+2 开始,逐步增加
for (ll R = L + 2; ; R++) {
ll g = __gcd(n, R); // 计算 n 和 R 的最大公约数
// 检查是否溢出,如果溢出则跳出循环
if (n / g > INF / R) break;
n = n / g * R; // 更新 n 的值,防止溢出
// 如果 n 还没有被存储过,则存入 map 中
if (!ans.count(n)) {
ans[n] = make_pair(L, R);
}
}
}
}
int main()
{
init(); // 调用预处理函数
int T; scanf("%d", &T); // 读取测试数据组数 T
while (T--)
{
ll n; scanf("%lld", &n); // 读取每组测试数据的 n
// 先判断区间长度为 2 的情况: [L, L+1]
ll sqrt_n = sqrt(n + 0.5); // 计算 n 的平方根
pair<int, int> res; // 定义结果变量 res
// 检查是否存在区间 [sqrt_n, sqrt_n + 1] 满足条件
if (sqrt_n * (sqrt_n + 1) == n)
{
res = make_pair(sqrt_n, sqrt_n + 1); // 如果满足条件,存储结果
// 如果 map 中已经有更优的解,则更新 res
if (ans.count(n))
if(res.first > ans[n].first)
res = ans[n];
}
else if (ans.count(n))
{
// 如果 map 中有对应的解,则直接使用
res = ans[n];
}
else
{
// 如果没有解,则输出 -1 并继续下一组测试
puts("-1");
continue;
}
printf("%d %d\n", res.first, res.second); // 输出结果
}
return 0;
}
代码思路
1. 预处理阶段
-
目标:预先计算所有可能的 n 值及其对应的区间 [L,R],并将结果存储在
map
中。 -
实现:
-
遍历 L 从 1 到 2×10^6。
-
对于每个 L,计算 L 和 L+1 的乘积 n。
-
遍历 R 从 L+2开始,逐步增加,计算区间 [L,R] 内所有整数的乘积 n。
-
使用最大公约数(
__gcd
)防止溢出,并更新 n 的值。 -
如果 n 没有被存储过,则将其存入
map
中。
-
2. 查询阶段
-
目标:对于每个输入的 n,找到对应的区间 [L,R] 或输出
-1
表示无解。 -
实现:
-
调用预处理函数
init()
,生成所有可能的 n 和 [L,R]的映射。 -
读取测试数据组数 T。
-
对于每组测试数据:
-
读取 n。
-
检查是否存在长度为 2 的区间 [L,L+1] 满足 L×(L+1)=n。
-
如果存在,则检查
map
中是否有更优的解(即 L 更小的解)。 -
如果
map
中有对应的解,则直接使用。 -
如果无解,则输出
-1
。 -
输出结果 [L,R]。
-
-
代码思路总结
-
预处理:
-
遍历 L 从 1 到 2×10^6。
-
对于每个 L,计算区间 [L,R] 的乘积 n,并存储到
map
中。 -
使用最大公约数防止溢出,确保计算正确。
-
-
查询:
-
对于每个输入的 n,先检查是否存在长度为 2 的区间 [L,L+1] 满足条件。
-
如果存在,则检查
map
中是否有更优的解。 -
如果
map
中有解,则直接使用;否则输出-1
。
-
-
输出:
-
输出每组测试数据的结果 [L,R] 或
-1
。
-
关键点
-
预处理优化:通过限制 L的范围(1 到 2×10^6),减少计算量。
-
防止溢出:使用最大公约数(
__gcd
)和先除后乘的方式,避免乘积溢出。 -
区间长度为 2 的特殊处理:单独检查 [L,L+1]的情况,减少计算复杂度。
-
结果存储:使用
map
存储 n 和 [L,R]的映射,方便快速查询。
代码结构
-
预处理函数
init()
:-
遍历 L 和 R,计算 n 并存储到
map
中。
-
-
主函数
main()
:-
调用
init()
进行预处理。 -
读取测试数据组数 T。
-
对于每组测试数据:
-
读取 n。
-
检查是否存在长度为 2 的区间 [L,L+1]。
-
检查
map
中是否有解。 -
输出结果。
-
-