最大公约数与最小公倍数
设 \(a_1,a_2\) 是两个整数,如果 \(d|a_1, \ d|a_2\),那么 \(d\) 就称为 \(a_1\) 和 \(a_2\) 的公约数,其中最大的称为 \(a_1\) 和 \(a_2\) 的最大公约数,记作 \((a_1,a_2)\)。一般地,可以类似地定义 \(k\) 个整数 \(a_1,a_2,\cdots,a_k\) 的公约数和最大公约数,后者记作 \((a_1,\cdots,a_k)\)。
设 \(a_1,a_2\) 是两个整数,如果 \(a_1|l, \ a_2|l\),那么 \(l\) 就称为 \(a_1\) 和 \(a_2\) 的公倍数,其中最小的称为 \(a_1\) 和 \(a_2\) 的最小公倍数,记作 \([a_1,a_2]\)。一般地,可以类似地定义 \(k\) 个整数 \(a_1,a_2,\cdots,a_k\) 的公倍数和最小公倍数,后者记作 \([a_1,\cdots,a_k]\)。
下面是 \(4\) 个有关最大公约数和最小公倍数的常见性质与结论。
- 对任意整数 \(m\),有 \(m(a_1,\cdots,a_k) = (ma_1, \cdots, ma_k)\),即整数同时成倍放大,最大公约数也放大相同倍数。例如:\(9 = (9,18,27) = (3 \times 3, 3 \times 6, 3 \times 9) = 3 \times (3,6,9) = 3 \times 3 = 9\)。
该性质同样适用于最小公倍数的情况。 - 对任意整数 \(x\),\((a_1,a_2) = (a_1, a_2 + a_1 x)\),即一个整数加上另一整数的任意倍数,它们的最大公约数不变。例如:\((16, 10) = (16 - 10, 10) = (6, 10) = (6, 10 - 6) = (6, 4) = (6 - 4, 4) = (2, 4) = (2, 4 - 2 \times 2) = (2,0) = 2\)。
这里要注意的是,\((a,0) = a\)。
该性质不适用于最小公倍数的情况。 - \((a_1, a_2, a_3, \cdots, a_k) = ((a_1, a_2), a_3, \cdots, a_k)\),以及一个显然的推论 \((a_1, a_2, a_3, \cdots, a_{k+r}) = ((a_1, \cdots, a_k), (a_{k+1}, \cdots, a_{k+r}))\)。
这是计算多元最大公约数的主要手段。
例如求 \((12, 18, 21)\),先求出 \((12, 18) = 6\);再把 \(6\) 代入原式,求出 \((6, 21) = 3\)。连起来写就是 \((12, 18, 21) = ((12, 18), 21) = (6, 21) = 3\)。
更深刻一点,这个性质说明了最大公约数运算具有某种“结合律”。
该性质同样适用于最小公倍数的情况。 - \([a_1,a_2](a_1,a_2)=a_1a_2\),即最大公约数乘以最小公倍数等于原来两个数的乘积。
例如 \((16,10) \times [16,10] = 2 \times 80 = 160 = 16 \times 10\)。
性质 \(2\) 可以给出一个高效的求两数最大公约数的算法:每次让较大的数对较小数取模(相当于较大数减了若干倍较小数,最高效地利用了性质 \(2\),而且运用模运算时不必区分两数的大小),可以缩小问题规模而保持最大公约数不变,然后重复(递归)这个步骤。递归边界是某数变成了 \(0\),而此时另一个数即为所求答案。
int gcd(int x, int y) {
if (y == 0) return x; // 递归边界
else return gcd(y, x % y);
}
也可以利用三目运算符
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
这种利用两数相除(取模)求最大公约数的方法叫作辗转相除法或 Eculid 算法,最坏情况下的时间复杂度是 \(O(\log \max(x,y))\)。值得注意的是,相近规模下能让辗转相除执行次数最多(最坏情况)的数是相邻两个斐波那契数,而对于大多数情况,辗转相除法的计算效率都非常高。
利用性质 \(4\),用两数之积除以它们的最大公约数,代码如下:
int lcm(int x, int y) {
return x / gcd(x, y) * y; // 要注意乘除的先后顺序
}
因为 \(x\) 一定是 \(gcd(x,y)\) 的倍数,所以这样先除后乘没有问题,而且可以避免可能的溢出事件。
习题:P2660 zzc 种田
有一块 \(x \times y \ (x,y \le 10^{16})\) 的矩形田地,zzc 每次只能种一个正方形,所花的体力值是正方形的周长,种过的田不可以再种,zzc 想花最少的体力值去种完这块田地,问其最小体力值。
解题思路
这个种田的过程实际上就是辗转相除法的运作过程,在辗转相除法的过程中计算每一次种田消耗的体力值即可。
#include <cstdio>
using ll = long long;
ll ans = 0;
void gcd(ll x, ll y) {
if (y == 0) return;
ans += x / y * y * 4;
gcd(y, x % y);
}
int main()
{
ll x, y; scanf("%lld%lld", &x, &y);
gcd(x, y);
printf("%lld\n", ans);
return 0;
}
习题:P4057 [Code+#1] 晨跑
\(3\) 个同学分别每 \(a,b,c \ (\le 100000)\) 天晨跑一次,假设他们在第 \(0\) 天同时晨跑,求下一次他们同时晨跑是第几天。
解题思路
求 \([a,b,c]\) 即可。
#include <cstdio>
using ll = long long;
ll gcd(ll x, ll y) {
return y == 0 ? x : gcd(y, x % y);
}
ll lcm(ll x, ll y) {
return x / gcd(x, y) * y;
}
int main()
{
int a, b, c; scanf("%d%d%d", &a, &b, &c);
printf("%lld\n", lcm(lcm(a, b), c));
return 0;
}
习题:P2651 添加括号III
给形如 \(a_1/a_2/a_3/\cdots/a_n\) 的表达式添加括号,判断是否能使其值为整数。(\(n \le 10000\))
解题思路
可以发现这个式子中,\(a_1\) 必然是分子,\(a_2\) 必然是分母,而 \(a_3, \cdots, a_n\) 通过不同的加括号方式既可以放在分子上又可以放在分母上,为了让整个数更有可能变成整数,显然是把它们放分子上更好。
所以只需要确认 \(a_1 a_3 \cdots a_n / a_2\) 是否为整数即可,这一点并不需要计算具体的值,只需要不断计算分子中每一项与分母的最大公约数并进行约分。
最终分母如果能约分到 \(1\),就说明可以变成整数。
#include <cstdio>
const int N = 10005;
int a[N];
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
void solve() {
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
int g = gcd(a[1], a[2]);
a[2] /= g;
for (int i = 3; i <= n; i++) {
g = gcd(a[i], a[2]);
a[2] /= g;
}
printf("%s\n", a[2] == 1 ? "Yes" : "No");
}
int main()
{
int t; scanf("%d", &t);
for (int i = 1; i <= t; i++) {
solve();
}
return 0;
}
习题:P1572 计算分数
计算若干分数之和,结果用分数表示(如果结果为整数则用整数表示)。例如,输入是
2/1+1/3-1/4
时,输出是25/12
。
解题思路
模拟分数加减的通分和约分过程,\(\dfrac{a}{b} + \dfrac{c}{d} = \dfrac{[b,d] / b \times a + [b,d] / d \times c}{[b,d]}\)。
注意当分子/分母为负数时,求最大公约数要用绝对值去求。
#include <cstdio>
#include <cstring>
#include <cmath>
using std::abs;
const int N = 105;
char s[N];
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
int lcm(int x, int y) {
return x / gcd(x, y) * y;
}
int main()
{
scanf("%s", s + 1);
int len = strlen(s + 1);
int numerator = 0, denominator = 0, sign = 1;
int ans_num = 0, ans_den = 1;
int i = 1;
if (s[i] == '-') {
sign = -1; i++;
}
while (i <= len) {
int j = i, idx = -1;
while (j <= len && s[j] != '+' && s[j] != '-') {
if (s[j] >= '0' && s[j] <= '9') {
if (idx == -1) numerator = numerator * 10 + s[j] - '0';
else denominator = denominator * 10 + s[j] - '0';
}
if (s[j] == '/') idx = j;
j++;
}
if (denominator == 0) denominator = 1;
int g = gcd(numerator, denominator);
numerator /= g; denominator /= g;
int tmp_den = lcm(ans_den, denominator);
int tmp_num = tmp_den / ans_den * ans_num + sign * tmp_den / denominator * numerator;
g = gcd(abs(tmp_num), tmp_den); // 注意此时分子可能是负数
ans_num = tmp_num / g; ans_den = tmp_den / g;
if (j <= len) {
if (s[j] == '+') sign = 1;
else sign = -1;
}
i = j + 1; numerator = 0; denominator = 0;
}
if (ans_den == 1) printf("%d\n", ans_num); // 如果最终结果是整数不用输出分数形式
else printf("%d/%d\n", ans_num, ans_den);
return 0;
}
习题:P9750 [CSP-J 2023] 一元二次方程
参考代码
#include <cstdio>
#include <cmath>
const int N = 1505;
int sqr[N], len;
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
int sign(int x) {
if (x == 0) return 0;
return x > 0 ? 1 : -1;
}
void solve() {
int a, b, c; scanf("%d%d%d", &a, &b, &c);
int delta = b * b - 4 * a * c;
if (delta < 0) {
printf("NO\n"); return;
}
int t2_num = a > 0 ? 1 : -1; // a>0则较大的根是加根号delta那个,否则减根号delta
if (delta == 0) t2_num = 0;
else {
// 利用平方数化简根式
for (int i = len; i >= 2; i--) {
if (delta % sqr[i] == 0) {
t2_num *= i; delta /= sqr[i];
}
}
}
int t1_num = -b;
int t1_den = 2 * a, t2_den = 2 * a;
if (delta == 1) { // 如果根式为完全平方数,则根式部分不用输出了,并入-b部分
t1_num += t2_num; t2_num = delta = 0;
t2_den = 1;
}
if (t1_num == 0 && delta == 0) {
printf("0\n"); return;
}
int sign1 = sign(t1_num) * sign(t1_den);
int sign2 = sign(t2_num) * sign(t2_den);
t1_num = abs(t1_num); t2_num = abs(t2_num); // -b部分的分子、分母
t1_den = abs(t1_den); t2_den = abs(t2_den); // 根式部分的分子分母
int g = gcd(t1_num, t1_den); t1_num /= g; t1_den /= g;
g = gcd(t2_num, t2_den); t2_num /= g; t2_den /= g;
if (t1_num != 0) {
if (sign1 < 0) printf("-");
printf("%d", t1_num);
if (t1_den != 1) printf("/%d", t1_den);
}
if (t1_num != 0 && delta != 0 && sign2 > 0) printf("+");
else if (delta != 0 && sign2 < 0) printf("-");
if (delta > 1) {
if (t2_num > 1 || (t2_num == 1 && delta == 0)) printf("%d*", t2_num);
if (delta > 1) printf("sqrt(%d)", delta);
if (t2_den != 1) printf("/%d", t2_den);
}
printf("\n");
}
int main()
{
int t, m; scanf("%d%d", &t, &m);
int bound = m * m * 2;
for (int i = 1; i * i <= bound; i++) {
sqr[i] = i * i; len = i; // 预处理平方数
}
for (int i = 1; i <= t; i++) {
solve();
}
return 0;
}
例题:删数
给定 \(n\) 个整数。对于其中的每个数 \(a_i\),求出删去它以后剩下的所有数的最大公约数,\(n \le 10^6\)。例如,\(n = 5, a = [12, 36, 24, 18, 48]\),则结果为 \([6, 6, 6, 12, 6]\)。
分析:到目前为止并没有直接的定理或者工具来维护删掉某个数以后的最大公约数,但是性质 \(3\) 说明,可以相对容易地把两堆最大公约数“拼”起来。对于删去 \(a_i\) 后的数组,显然剩下的数一定是 \(a_1\) 到 \(a_{i-1}\) 和 \(a_{i+1}\) 到 \(a_n\),这分别是一段前缀和一段后缀。这意味着,如果用 \(left_i\) 表示 \(a_1\) 到 \(a_i\) 的最大公约数,\(right_i\) 表示 \(a_i\) 到 \(a_n\) 的最大公约数,那么删除 \(a_i\) 以后的答案就是 \((left_{i-1}, right_{i+1})\),可以快速求出。
而 \(left\) 和 \(right\) 也可以利用性质 \(3\) 递推求出。
所以这道题的核心可以概括为:
- \(left_i = (a_1, \cdots, a_i) = ((a_1, \cdots, a_{i-1}), a_i) = (left_{i-1}, a_i)\)
- \(right_i = (a_i, \cdots, a_n) = (a_i, (a_{i+1}, \cdots, a_n)) = (a_i, right_{i+1})\)
- \(ans_i = (a_1, \cdots, a_{i-1}, a_{i+1}, \cdots, a_n) = ((a_1, \cdots, a_{i-1}), (a_{i+1}, \cdots, a_n)) = (left_{i-1}, right_{i+1})\)
例题:P1029 [NOIP2001 普及组] 最大公约数和最小公倍数问题
给定某两个未知正整数 \((P,Q)\) 的最大公约数 \(x_0 \ (x_0 \le 10^5)\) 和最小公倍数 \(y_0 \ (y_0 \le 10^5)\),求满足条件的所有可能的 \((P,Q)\) 的个数。
分析:根据题意可以得到 \(PQ=x_0y_0\),既然 \(y_0\) 是 \(P,Q\) 的最小公倍数,那么 \(P,Q\) 必然是 \(y_0\) 的约数,因此可以枚举 \(y_0\) 的约数,假定为 \(P\),从而求出 \(Q=\dfrac{x_0y_0}{P}\),再检验这对 \(P\) 和 \(Q\) 是否满足条件。这样整个算法的时间复杂度就是枚举约数的时间复杂度,为 \(O(\sqrt{y})\)。
参考代码
#include <cstdio>
using ll = long long;
int x0, y0;
ll gcd(ll x, ll y) {
return y == 0 ? x : gcd(y, x % y);
}
int calc(ll p) {
ll q = y0 / p * x0; // 注意x0*y0可能会爆int
if (gcd(p, q) == x0) return 1;
return 0;
}
int main()
{
scanf("%d%d", &x0, &y0);
int ans = 0;
for (int i = 1; i * i <= y0; i++) {
if (y0 % i == 0) {
ans += calc(i);
if (y0 / i != i) ans += calc(y0 / i); // 注意判断重复解
}
}
printf("%d\n", ans);
return 0;
}
例题:P1072 [NOIP2009 提高组] Hankson 的趣味题
已知正整数 \(1 \le a_0,a_1,b_0,b_1 \le 2 \times 10^9\),设某未知正整数 \(x\) 满足 \((x,a_0)=a_1, \ [x,b_0]=b_1\),求所有满足条件的正整数 \(x\) 的个数。\(2000\) 组数据。
分析:直接枚举 \(b_1\) 的约数再检验即可。
参考代码
#include <cstdio>
using ll = long long;
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
ll lcm(int x, int y) {
return 1ll * x / gcd(x, y) * y;
}
void solve() {
int a0, a1, b0, b1;
scanf("%d%d%d%d", &a0, &a1, &b0, &b1);
int ans = 0;
for (int i = 1; i * i <= b1; i++) {
if (b1 % i == 0) {
if (gcd(i, a0) == a1 && lcm(i, b0) == b1) {
ans++;
}
if (b1 / i != i) { // 注意重复判断
if (gcd(b1 / i, a0) == a1 && lcm(b1 / i, b0) == b1) {
ans++;
}
}
}
}
printf("%d\n", ans);
}
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) {
solve();
}
return 0;
}
习题:P1414 又是毕业季II
给定 \(n \ (n \le 10^4)\) 个不超过 \(10^6\) 的整数,对于 \(1\) 到 \(n\) 之间的每个 \(k\),求出这 \(n\) 个整数选出 \(k\) 个数的最大公约数的最大值(选 \(k\) 个数,最大化其最大公约数)。
解题思路
暴力搜索所有的选择方案,时间复杂度 \(O(2^n \log inf)\),能够获得 \(n \le 5\) 的部分分。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 10005;
int a[N], ans[N];
int gcd(int x, int y) {
return y == 0 ? x : gcd(y, x % y);
}
void dfs(int cur, int chosen, int n, int g) {
if (cur == n + 1) {
if (chosen > 0) ans[chosen] = max(ans[chosen], g);
return;
}
dfs(cur + 1, chosen + 1, n, g == 0 ? a[cur] : gcd(g, a[cur]));
dfs(cur + 1, chosen, n, g);
}
int main()
{
int n; scanf("%d", &n);
for (int i = 1; i <= n; i++) scanf("%d", &a[i]);
dfs(1, 0, n, 0);
for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
return 0;
}
分析另外 \(30 \%\) 的数据,注意到这部分数据 \(inf\) 非常小。因此答案的范围就是 \(1\) 到 \(inf\),所以可以从大到小枚举答案,如果有不小于 \(k\) 个数能被该答案整除,则 \(n\) 选 \(k\) 的答案就是这个值,时间复杂度 \(O(n^2 inf)\)。能够获得 \(48\) 分。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 10005;
int a[N];
int main()
{
int n; scanf("%d", &n);
int inf = 0;
for (int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
inf = max(inf, a[i]);
}
for (int i = 1; i <= n; i++) {
for (int j = inf; j >= 1; j--) {
int cnt = 0;
for (int k = 1; k <= n; k++) {
if (a[k] % j == 0) cnt++;
}
if (cnt >= i) {
printf("%d\n", j); break;
}
}
}
return 0;
}
上面的部分分提醒我们可以从值域入手,注意到值域范围不大(\(10^6\)),可以把这 \(n\) 个数所有的约数都求出来,如果某个约数出现过 \(k\) 次及以上,那么这个约数就可以作为选出 \(k\) 个人及以上的情况下的公约数,题目相当于要找次数达标的最大公约数。
可以在求出每个约数的出现次数之后,记录每个次数下最大的数。
如果选 \(k\) 个数的最大公约数是 \(m\),那么选 \(k-1\) 个数的最大公约数一定大于等于 \(m\),因此可以基于上面的结果逆向递推。时间复杂度 \(O(n \sqrt{inf} + inf)\)。
参考代码
#include <cstdio>
#include <algorithm>
using std::max;
const int N = 1e4 + 5;
const int INF = 1e6 + 5;
int cnt[INF], ans[N];
int main()
{
int n; scanf("%d", &n);
int maxx = 0;
for (int i = 1; i <= n; i++) {
int x; scanf("%d", &x); maxx = max(maxx, x);
for (int j = 1; j * j <= x; j++) {
if (x % j == 0) {
cnt[j]++;
if (x / j != j) cnt[x / j]++;
}
}
}
for (int i = 1; i <= maxx; i++) {
ans[cnt[i]] = i;
}
for (int i = n; i >= 1; i--) {
ans[i] = max(ans[i], ans[i + 1]);
}
for (int i = 1; i <= n; i++) printf("%d\n", ans[i]);
return 0;
}