Что такое НОД?
НОД — это наибольший общий делитель.
То есть самое большое число, на которое делятся два числа без остатка.
такое что a делится на d
и b делится на d
В программировании НОД часто называют:
gcd
Пример НОД
Пусть:
a = 12
b = 18
Делители 12:
1 2 3 4 6 12
Делители 18:
1 2 3 6 9 18
Общие делители:
1 2 3 6
Самый большой общий делитель:
НОД(12, 18) = 6
Что такое НОК?
НОК — это наименьшее общее кратное.
То есть самое маленькое положительное число, которое делится и на a, и на b.
такое что x делится на a
и x делится на b
В программировании НОК часто называют:
lcm
Пример НОК
Пусть:
a = 12
b = 18
Кратные 12:
12 24 36 48 60 ...
Кратные 18:
18 36 54 72 ...
Первое общее кратное:
НОК(12, 18) = 36
Связь НОД и НОК
Для двух положительных чисел:
gcd(a, b) * lcm(a, b) = a * b
Поэтому:
lcm(a, b) = a / gcd(a, b) * b
Наивный способ найти НОД
Можно перебрать все числа от 1 до min(a, b) и найти максимальное, которое делит оба числа.
int gcd_slow(int a, int b) {
int ans = 1;
for (int d = 1; d <= min(a, b); d++) {
if (a % d == 0 && b % d == 0) {
ans = d;
}
}
return ans;
}
Алгоритм Евклида
Быстрый способ найти НОД — алгоритм Евклида.
Главная идея:
gcd(a, b) = gcd(b, a % b)
Пока b не станет 0, заменяем:
b становится a % b
Когда b == 0, ответ — a.
Пример алгоритма Евклида
Найдем:
gcd(48, 18)
gcd(48, 18) = gcd(18, 12)
18 % 12 = 6
gcd(18, 12) = gcd(12, 6)
12 % 6 = 0
gcd(12, 6) = gcd(6, 0)
Ответ:
6
Рекурсивный gcd
int gcd(int a, int b) {
if (b == 0) {
return a;
}
return gcd(b, a % b);
}
Это самый короткий и популярный вариант.
Итеративный gcd
int gcd(int a, int b) {
while (b != 0) {
int r = a % b;
a = b;
b = r;
}
return a;
}
Этот вариант не использует рекурсию.
gcd для long long
long long gcd(long long a, long long b) {
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return a;
}
Если числа до 10¹⁸, используй long long.
НОК через НОД
Когда НОД уже умеем считать, НОК считается просто:
lcm(a, b) = a / gcd(a, b) * b
Код:
long long lcm(long long a, long long b) {
return a / gcd(a, b) * b;
}
Полная реализация gcd и lcm
#include <bits/stdc++.h>
using namespace std;
long long gcd(long long a, long long b) {
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return a;
}
long long lcm(long long a, long long b) {
return a / gcd(a, b) * b;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long a, b;
cin >> a >> b;
cout << gcd(a, b) << '\n';
cout << lcm(a, b) << '\n';
return 0;
}
Input / Output
Input
12 18
Output
6
36
Встроенная функция C++
В C++ уже есть готовая функция:
__gcd(a, b)
Пример:
#include <bits/stdc++.h>
using namespace std;
int main() {
int a, b;
cin >> a >> b;
cout << __gcd(a, b);
return 0;
}
std::gcd и std::lcm
В C++17 есть:
std::gcd(a, b)
std::lcm(a, b)
Они находятся в header:
#include <numeric>
Но с bits/stdc++.h они тоже обычно доступны.
#include <bits/stdc++.h>
using namespace std;
int main() {
long long a, b;
cin >> a >> b;
cout << gcd(a, b) << '\n';
cout << lcm(a, b) << '\n';
return 0;
}
НОД нескольких чисел
Если дан массив, НОД всех чисел можно считать постепенно:
ans = gcd(ans, a[2])
ans = gcd(ans, a[3])
...
long long ans = a[0];
for (int i = 1; i < n; i++) {
ans = gcd(ans, a[i]);
}
Код: НОД массива
#include <bits/stdc++.h>
using namespace std;
long long gcd(long long a, long long b) {
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return a;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
cin >> n;
vector<long long> a(n);
for (int i = 0; i < n; i++) {
cin >> a[i];
}
long long ans = a[0];
for (int i = 1; i < n; i++) {
ans = gcd(ans, a[i]);
}
cout << ans;
return 0;
}
НОК нескольких чисел
НОК массива тоже считается постепенно:
ans = lcm(ans, a[2])
ans = lcm(ans, a[3])
...
long long ans = a[0];
for (int i = 1; i < n; i++) {
ans = lcm(ans, a[i]);
}
gcd и отрицательные числа
Обычно НОД считают положительным. Поэтому можно брать модуль:
a = abs(a);
b = abs(b);
Код:
long long gcd(long long a, long long b) {
a = abs(a);
b = abs(b);
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return a;
}
Что если одно число равно 0?
Правила:
gcd(a, 0) = a
gcd(0, b) = b
gcd(0, 0) = 0
Для НОК:
lcm(a, 0) = 0
lcm(0, b) = 0
Поэтому безопасный lcm:
long long lcm(long long a, long long b) {
if (a == 0 || b == 0) {
return 0;
}
return abs(a / gcd(a, b) * b);
}
Взаимно простые числа
Два числа называются взаимно простыми, если их НОД равен 1.
if (gcd(a, b) == 1) {
cout << "YES";
}
Пример:
gcd(8, 15) = 1
Значит 8 и 15 взаимно простые.
Расширенный алгоритм Евклида
Расширенный Евклид находит такие числа x и y, что:
a * x + b * y = gcd(a, b)
Это нужно для:
- линейных диофантовых уравнений
- обратного элемента по модулю
- теории чисел
Код расширенного Евклида
long long extgcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
long long x1, y1;
long long d = extgcd(b, a % b, x1, y1);
x = y1;
y = x1 - (a / b) * y1;
return d;
}
Пример расширенного Евклида
Пусть:
a = 30
b = 18
НОД:
gcd(30, 18) = 6
Можно найти:
x = -1
y = 2
Проверка:
30 * (-1) + 18 * 2 = 6
Полный код расширенного Евклида
#include <bits/stdc++.h>
using namespace std;
long long extgcd(long long a, long long b, long long &x, long long &y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
long long x1, y1;
long long d = extgcd(b, a % b, x1, y1);
x = y1;
y = x1 - (a / b) * y1;
return d;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
long long a, b;
cin >> a >> b;
long long x, y;
long long d = extgcd(a, b, x, y);
cout << d << '\n';
cout << x << ' ' << y << '\n';
return 0;
}
Типичные задачи с gcd
- найти НОД двух чисел
- найти НОК двух чисел
- найти НОД массива
- проверить, являются ли числа взаимно простыми
- сократить дробь
- найти период
- решить уравнение вида ax + by = c
Сокращение дроби
Чтобы сократить дробь:
a / b
нужно поделить числитель и знаменатель на их НОД.
long long d = gcd(a, b);
a /= d;
b /= d;
Пример:
12 / 18 = 2 / 3
Типичные ошибки
- Думать, что НОД — это меньшее число
- Путать НОД и НОК
- Писать медленный перебор вместо Евклида
- Забыть, что gcd(a, 0) = a
- Считать lcm как a * b / gcd и получить overflow
- Использовать int, когда нужен long long
- Не брать abs для отрицательных чисел
Асимптотика
| Метод | Время | Память |
|---|---|---|
| Наивный НОД | O(min(a, b)) | O(1) |
| Алгоритм Евклида | O(log min(a, b)) | O(1) |
| Расширенный Евклид | O(log min(a, b)) | O(log min(a, b)) |
Как думать на задачах?
2. Нужно общее кратное? → lcm
3. Нужно сократить дробь? → gcd
4. Нужно проверить взаимную простоту? → gcd == 1
5. Нужно ax + by = c? → extended gcd
6. Есть несколько чисел? → считаем постепенно
Короткий шаблон
long long gcd(long long a, long long b) {
while (b != 0) {
long long r = a % b;
a = b;
b = r;
}
return abs(a);
}
long long lcm(long long a, long long b) {
if (a == 0 || b == 0) {
return 0;
}
return abs(a / gcd(a, b) * b);
}