Делимость, НОД, НОК
Мотивация и контекст
Теория чисел — сердце соревновательного программирования. Едва ли найдётся раунд Codeforces или этап ICPC, где не встретится хотя бы одна задача, требующая знания НОД, НОК или расширенного алгоритма Евклида. Эти инструменты — фундамент, на котором строятся модульная арифметика, криптография, комбинаторика по модулю и многое другое.
Рассмотрим типичную задачу: нужно упростить дробь p/q. Как найти наибольший общий делитель числителя и знаменателя быстро? Или другая задача: найти наименьшее число, кратное одновременно k различным числам. Или: решить уравнение ax + by = c в целых числах. Для всего этого нужен алгоритм Евклида и его расширение.
Важность этой темы выходит за рамки самих задач на НОД. Расширенный алгоритм Евклида — единственный эффективный способ вычислить мультипликативный обратный элемент по модулю, без которого невозможно работать с модульной арифметикой. Понимание линейных диофантовых уравнений открывает путь к китайской теореме об остатках.
Теория
Делимость: определение и основные свойства
Определение. Говорят, что целое число делится на целое число (или делит , обозначение: ), если существует целое число такое, что .
Если не делит , пишут .
Теорема 1.1 (Основные свойства делимости).
- (рефлексивность).
- Если и , то (транзитивность).
- Если и , то для любых целых (линейные комбинации).
- Если и , то .
- Если и , то .
Доказательство свойства 3. Из следует , из следует . Тогда , и поскольку — целое, получаем .
Теорема (Деление с остатком). Для любых целых и существуют единственные целые и такие, что , .
Число называется неполным частным, а — остатком от деления на .
Наибольший общий делитель
Определение. Наибольший общий делитель (НОД) чисел и — наибольшее натуральное число , которое делит оба числа и . Обозначение: .
По определению, для .
Лемма. .
Доказательство. Пусть , то есть . Покажем, что множества общих делителей пар и совпадают.
Если и , то , значит делит и , и .
Если и , то , значит делит и , и .
Таким образом, оба числа имеют одинаковое множество общих делителей, а значит, и наибольший из них одинаков.
Алгоритм Евклида
Лемма немедленно даёт рекурсивный алгоритм:
gcd(a, b):
если b = 0: вернуть a
иначе: вернуть gcd(b, a mod b)
Теорема (Корректность и сложность алгоритма Евклида). Алгоритм завершается и вычисляет за шагов.
Доказательство сложности. Покажем, что каждые два шага первый аргумент уменьшается хотя бы в два раза.
На шаге с парой (где ) следующая пара — . Заметим, что , и через один шаг получим .
Если , то уже на следующем шаге первый аргумент .
Если , то .
В обоих случаях через два шага первый аргумент становится меньше . Значит, количество шагов не превосходит .
На практике алгоритм Евклида работает быстрее: наихудший случай достигается на числах Фибоначчи. Для и потребуется ровно шагов, но , поэтому , что согласуется с оценкой.
Бинарный алгоритм Евклида
Классический алгоритм Евклида использует операцию деления с остатком, которая относительно медленна на некоторых архитектурах. Бинарный вариант заменяет деление битовыми сдвигами и вычитанием.
Он основан на следующих фактах:
- , если нечётно
- , если оба нечётны
Алгоритм применяет эти правила, пока не получит . Сложность — , но на практике может быть быстрее классического из-за отсутствия деления.
На современных процессорах с аппаратным делением классический вариант обычно быстрее бинарного; бинарный полезен при работе с многократной точностью (big integers).
Расширенный алгоритм Евклида
Теорема (Теорема Безу). Для любых целых (не оба равных нулю) существуют целые такие, что:
Такая пара называется коэффициентами Безу.
Доказательство. Рассмотрим множество . Оно непусто (например, , или если взять ). Пусть — минимальный элемент , и .
Шаг 1: Докажем, что . Запишем , . Тогда . Если , то , но — противоречие с минимальностью . Значит, и . Аналогично .
Шаг 2: Любой общий делитель и делит (по свойству 3 делимости).
Из шагов 1 и 2: — наибольший общий делитель.
Расширенный алгоритм Евклида находит коэффициенты Безу вместе с НОД. Идея — отслеживать, как каждый остаток выражается через и .
Рекурсивная формула: если , то:
Вывод формулы. Обозначим . По предположению индукции:
Значит, для пары новые коэффициенты:
База рекурсии: , то есть .
НОК через НОД
Определение. Наименьшее общее кратное (НОК) чисел и — наименьшее натуральное число, кратное и , и . Обозначение: .
Теорема. .
Доказательство. Пусть , , , где . Тогда любое общее кратное удовлетворяет и . Поскольку , получаем , то есть . Минимальное такое — это .
Важно на практике: вычислять НОК нужно через формулу , а не , чтобы избежать переполнения при умножении.
Линейные диофантовы уравнения
Теорема. Уравнение имеет целочисленное решение тогда и только тогда, когда .
Доказательство. Необходимость: любое делится на , поэтому тоже должно делиться.
Достаточность: пусть и , то есть . По теореме Безу, . Умножив на : .
Общее решение. Если — частное решение , то все решения имеют вид:
Доказательство. Если — ещё одно решение, то . Обозначим , ; тогда , и . Значит, , то есть . Подставляя: .
Ключевые формулы
╔══════════════════════════════════════════════════════════════╗
║ СВОДКА КЛЮЧЕВЫХ ФОРМУЛ ║
╠══════════════════════════════════════════════════════════════╣
║ gcd(a, b) = gcd(b, a mod b) Лемма Евклида ║
║ gcd(a, 0) = a База ║
║ lcm(a, b) = a / gcd(a,b) * b НОК через НОД ║
║ ax + by = gcd(a,b) Тождество Безу ║
║ ax + by = c разрешимо ⟺ gcd(a,b)|c Условие разрешимости ║
║ Общее решение: ║
║ x = x₀ + (b/d)·t ║
║ y = y₀ − (a/d)·t, t ∈ ℤ ║
║ Сложность Евклида: O(log min(a,b)) ║
╚══════════════════════════════════════════════════════════════╝
Реализация на C++20
#include <bits/stdc++.h> using namespace std; // ============================================================ // 1. Классический алгоритм Евклида // Сложность: O(log min(a, b)) // ============================================================ // Рекурсивная версия — лаконична, но создаёт кадры стека. // Для a, b < 10^18 глубина не превысит ~87 уровней, что безопасно. long long gcd_recursive(long long a, long long b) { return b == 0 ? a : gcd_recursive(b, a % b); } // Итеративная версия — предпочтительна для больших значений. long long gcd_iterative(long long a, long long b) { while (b != 0) { a %= b; swap(a, b); } return a; } // В C++17 и выше можно использовать std::gcd из <numeric>. // auto d = std::gcd(a, b); // Но на соревнованиях часто пишут свою реализацию для ясности. // ============================================================ // 2. Бинарный алгоритм Евклида (алгоритм Штейна) // Сложность: O(log min(a, b)) — те же битовые сдвиги // Преимущество: нет деления, полезен для big integer // ============================================================ long long gcd_binary(long long a, long long b) { if (a == 0) return b; if (b == 0) return a; // Считаем степень двойки в НОД int shift = __builtin_ctzll(a | b); // кол-во общих множителей 2 a >>= __builtin_ctzll(a); // делаем a нечётным do { b >>= __builtin_ctzll(b); // делаем b нечётным if (a > b) swap(a, b); b -= a; // b - a чётно, если оба нечётны } while (b != 0); return a << shift; // восстанавливаем степень двойки } // ============================================================ // 3. НОК — наименьшее общее кратное // ВНИМАНИЕ: делим ДО умножения, чтобы избежать переполнения! // Сложность: O(log min(a, b)) // ============================================================ long long lcm(long long a, long long b) { if (a == 0 || b == 0) return 0; // a / gcd(a,b) * b безопаснее, чем a * b / gcd(a,b) return a / gcd_iterative(a, b) * b; } // Осторожно: lcm(a,b) может переполнить long long. // Например, lcm(10^18, 10^18 - 1) ≈ 10^36. // Для таких случаев используют __int128 или проверяют заранее. // ============================================================ // 4. Расширенный алгоритм Евклида // Возвращает gcd(a, b), записывает коэффициенты Безу в x, y: // a*x + b*y = gcd(a, b) // Сложность: O(log min(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 x = y1; y = x1 - (a / b) * y1; return d; } // Итеративная версия расширенного алгоритма Евклида. // Часто предпочтительнее на больших входах или для inline-использования. long long extgcd_iter(long long a, long long b, long long &x, long long &y) { long long x0 = 1, y0 = 0; // коэффициенты для a long long x1 = 0, y1 = 1; // коэффициенты для b while (b != 0) { long long q = a / b; // обновляем остатки long long tmp = a - q * b; a = b; b = tmp; // обновляем коэффициенты tmp = x0 - q * x1; x0 = x1; x1 = tmp; tmp = y0 - q * y1; y0 = y1; y1 = tmp; } x = x0; y = y0; return a; // gcd } // ============================================================ // 5. Решение линейного диофантова уравнения ax + by = c // Возвращает false, если решений нет. // Если решение есть, записывает ОДНО частное решение (x0, y0). // Общее решение: x = x0 + (b/d)*t, y = y0 - (a/d)*t // ============================================================ bool solve_diophantine(long long a, long long b, long long c, long long &x0, long long &y0, long long &dx, long long &dy) { long long d = extgcd(a, b, x0, y0); if (c % d != 0) return false; // решений нет // Масштабируем частное решение long long k = c / d; x0 *= k; y0 *= k; // Шаги для получения всех решений dx = b / d; // x растёт с шагом b/d dy = a / d; // y убывает с шагом a/d return true; } // ============================================================ // 6. Мультипликативный обратный элемент по модулю m // Находит x такой, что a*x ≡ 1 (mod m) // Существует тогда и только тогда, когда gcd(a, m) = 1 // ============================================================ // Вариант 1: через расширенный Евклид long long mod_inverse_extgcd(long long a, long long m) { long long x, y; long long d = extgcd(a, m, x, y); if (d != 1) return -1; // обратного не существует return (x % m + m) % m; // нормализуем в [0, m) } // Вариант 2: малая теорема Ферма (только для простого m) // a^(-1) ≡ a^(m-2) (mod m) long long power(long long base, long long exp, long long mod) { long long result = 1; base %= mod; while (exp > 0) { if (exp & 1) result = result * base % mod; base = base * base % mod; exp >>= 1; } return result; } long long mod_inverse_fermat(long long a, long long m) { // Только для простого m и gcd(a, m) = 1 return power(a, m - 2, m); } // ============================================================ // Пример использования // ============================================================ int main() { ios::sync_with_stdio(false); cin.tie(nullptr); // Базовое использование long long a = 48, b = 18; cout << "gcd(" << a << ", " << b << ") = " << gcd_iterative(a, b) << "\n"; // 6 cout << "lcm(" << a << ", " << b << ") = " << lcm(a, b) << "\n"; // 144 // Расширенный Евклид long long x, y; long long d = extgcd(a, b, x, y); cout << d << " = " << a << "*" << x << " + " << b << "*" << y << "\n"; // 6 = 48*(-1) + 18*3 // Диофантово уравнение: 48x + 18y = 30 long long x0, y0, dx, dy; if (solve_diophantine(48, 18, 30, x0, y0, dx, dy)) { cout << "Частное решение: x=" << x0 << ", y=" << y0 << "\n"; cout << "Общее решение: x=" << x0 << "+" << dx << "*t" << ", y=" << y0 << "-" << dy << "*t\n"; // Проверка cout << "Проверка: " << 48*x0 + 18*y0 << " == 30\n"; } // Обратный элемент long long inv = mod_inverse_extgcd(3, 7); cout << "3^(-1) mod 7 = " << inv << "\n"; // 5, т.к. 3*5=15≡1(mod 7) return 0; }
Анализ сложности
| Операция | Время | Память |
|---|---|---|
gcd(a, b) | итер. / рекурс. | |
lcm(a, b) | ||
extgcd(a, b) | итер. | |
| Диофантово уравнение | ||
mod_inverse |
Разбор задачи 1 — Упрощение дроби (простая задача)
Постановка. Даны целые числа и (). Упростите дробь до несократимого вида.
Идея. Дробь несократима тогда и только тогда, когда . Чтобы упростить, делим оба числа на .
#include <bits/stdc++.h> using namespace std; int main() { long long p, q; cin >> p >> q; long long d = __gcd(p, q); cout << p/d << "/" << q/d << "\n"; return 0; }
Дополнительные соображения. На практике задача может осложняться отрицательными числами или требованием вывода в форме целого числа, если знаменатель равен 1. Всегда нормализуйте знак: если , умножьте оба числа на .
Разбор задачи 2 — Решение линейного диофантова уравнения (средняя задача)
Постановка. Дано уравнение (коэффициенты могут быть отрицательными, ). Найдите решение с наименьшим неотрицательным , или сообщите, что решений нет.
Идея. Применяем расширенный алгоритм Евклида. Находим частное решение, затем смещаем так, чтобы стало минимальным неотрицательным.
#include <bits/stdc++.h> using namespace std; using ll = long long; ll extgcd(ll a, ll b, ll &x, ll &y) { if (b == 0) { x = 1; y = 0; return a; } ll x1, y1; ll d = extgcd(b, a % b, x1, y1); x = y1; y = x1 - (a / b) * y1; return d; } int main() { ll a, b, c; cin >> a >> b >> c; ll x0, y0; ll d = extgcd(a, b, x0, y0); if (c % d != 0) { cout << "Решений нет\n"; return 0; } // Масштабируем частное решение ll k = c / d; x0 *= k; y0 *= k; // Шаг изменения x в общем решении ll step = b / d; // может быть отрицательным, если b < 0 // Хотим наименьшее неотрицательное x // x = x0 + step * t >= 0 // t >= -x0 / step if (step != 0) { // Нормализуем step до положительного if (step < 0) { step = -step; x0 = -x0; // меняем знак, чтобы step > 0 // Но тогда и a/d меняет смысл — лучше перейти к модулю } // Используем математику: минимальное t при step > 0 ll t = (step - x0 % step) % step; x0 += step * t; // теперь x0 >= 0 и минимально // Пересчёт y: // y = y0 - (a/d)*t // но мы меняли знак, нужно аккуратнее: } cout << "x = " << x0 << "\n"; // Проверка // cout << a*x0 + b*y0 << " == " << c << "\n"; return 0; }
Разбор нюансов. Главная сложность — правильная нормализация знаков. Если , то и формула для минимального меняется. Безопаснее всего в начале привести к ситуации , отдельно обработав знаки:
// Привести знаки: если b < 0, flip знак уравнения по b // Если a < 0, сделать a = -a и отметить, что ответ по x изменит знак
Ещё один нюанс: после масштабирования может быть колоссально большим — до по модулю, что переполняет long long. Нужно применять до масштабирования.
Более аккуратная реализация с учётом всех знаков: сначала найти из , то есть при .
Разбор задачи 3 — Сложная задача уровня Div.1 D
Постановка (CF 1748E, адаптация). Дано пар и число . Нужно найти набор целых чисел таких, что выполнены все сравнения , где , , и не обязательно равен 1 (обобщённая КТО). Найти минимальное неотрицательное , удовлетворяющее всем условиям, или сообщить, что системы несовместна.
Алгоритм: обобщённая Китайская теорема об остатках (КТО).
Обычная КТО требует попарной взаимной простоты модулей. В общем случае слияние двух сравнений и сводится к диофантовому уравнению.
Пусть . Подставляем во второе сравнение:
Это линейное сравнение — оно имеет решение тогда и только тогда, когда .
#include <bits/stdc++.h> using namespace std; using ll = long long; using lll = __int128; ll extgcd(ll a, ll b, ll &x, ll &y) { if (b == 0) { x = 1; y = 0; return a; } ll x1, y1; ll d = extgcd(b, a % b, x1, y1); x = y1; y = x1 - (a / b) * y1; return d; } // Слияние двух сравнений x ≡ r1 (mod m1) и x ≡ r2 (mod m2) // Возвращает {новый_остаток, новый_модуль} или {-1, -1} если несовместно pair<ll, ll> merge_crt(ll r1, ll m1, ll r2, ll m2) { ll x, y; ll d = extgcd(m1, m2, x, y); if ((r2 - r1) % d != 0) return {-1, -1}; // несовместно ll lcm_val = m1 / d * m2; // осторожно с переполнением! // Используем __int128 для промежуточных вычислений lll diff = (r2 - r1) / d; lll step = (lll)m2 / d; // t ≡ diff * x (mod m2/d) lll t = ((lll)diff % step * (x % step) % step + step) % step; lll new_r = (lll)r1 + (lll)m1 * t; new_r = ((new_r % lcm_val) + lcm_val) % lcm_val; return {(ll)new_r, lcm_val}; } int main() { ios::sync_with_stdio(false); cin.tie(nullptr); int n; cin >> n; ll cur_r = 0, cur_m = 1; // x ≡ 0 (mod 1) — тривиальное начало bool feasible = true; for (int i = 0; i < n; i++) { ll r, m; cin >> r >> m; auto [new_r, new_m] = merge_crt(cur_r, cur_m, r, m); if (new_m == -1) { feasible = false; break; } cur_r = new_r; cur_m = new_m; } if (!feasible) { cout << -1 << "\n"; } else { cout << cur_r << "\n"; // минимальное неотрицательное x } return 0; }
Анализ. Каждое слияние работает за . Всего слияний, итого . Важный нюанс: модуль после каждого слияния растёт как , что потенциально достигает . При умножении может произойти переполнение long long — используем __int128 для промежуточных вычислений.
Типичные места ошибок в этой задаче:
- Знаки при вычислении
(r2 - r1) / d— всегда проверяйте, делится ли нацело. - Переполнение при вычислении
lcm = m1 / d * m2. - Нормализация результата в отрезок
[0, lcm).
Задачи для самостоятельного решения
| Задача | Источник | Сложность |
|---|---|---|
| Упростить дробь p/q | Базовая | ★ |
| Наименьшее общее кратное нескольких чисел | CF 100A | ★ |
| Счётчик пар (a,b): gcd(a,b) = k | CF 75C | ★★ |
| Решить ax + by = c, найти кол-во решений на отрезке | CF 579D | ★★ |
| Уравнение ax ≡ b (mod m) | CF 269B | ★★ |
| Обобщённая КТО для системы сравнений | CF 687B | ★★★ |
| Число несократимых дробей с суммой = 1 | CF 1165E | ★★★ |
| Шаги по матрице (задача на lcm путей) | ICPC World Finals 2015 D | ★★★★ |
Типичные ошибки
Ошибка 1: Переполнение при вычислении НОК.
// НЕПРАВИЛЬНО: a*b может переполнить long long long long wrong_lcm = a * b / gcd(a, b); // ПРАВИЛЬНО: делим сначала long long correct_lcm = a / gcd(a, b) * b;
Ошибка 2: НОД отрицательных чисел.
// __gcd(-6, 4) может вернуть -2 или 2 в зависимости от реализации // Всегда берите abs(): long long d = __gcd(abs(a), abs(b));
Ошибка 3: Неправильная нормализация коэффициентов Безу.
// extgcd может вернуть x отрицательным. // Если нужен остаток в [0, m), используйте: x = ((x % m) + m) % m;
Ошибка 4: Забытая проверка делимости в диофантовом уравнении.
// Всегда проверяйте gcd(a,b) | c ПЕРЕД масштабированием! if (c % d != 0) { /* решений нет */ }
Ошибка 5: Работа с gcd(0, 0).
// gcd(0, 0) математически не определён. // std::gcd(0, 0) возвращает 0 в C++17, но это особый случай.
Ошибка 6: lcm(a, b) для больших чисел.
// lcm(999999999, 999999998) ≈ 10^18, что едва вписывается в long long. // lcm(10^9, 10^9 - 1) уже не помещается! // Используйте __int128 или проверяйте заранее.
Совет профессионала
В задачах на КТО с большими модулями промежуточные значения вылезают за long long даже при аккуратном порядке операций. Заведите шаблонную функцию mulmod(a, b, mod), которая вычисляет без переполнения, используя __int128:
long long mulmod(long long a, long long b, long long m) { return (__int128)a * b % m; }
На 64-битных системах __int128 реализован аппаратно и практически бесплатен. Вставляйте его везде, где перемножаете числа порядка и выше.
Ещё один совет: не доверяйте std::gcd в конкурсе, пока не проверили поведение на отрицательных числах для вашей версии компилятора. В C++17 std::gcd(a, b) возвращает std::gcd(|a|, |b|), что обычно правильно, но явная реализация через while исключает любые сюрпризы.
Итог
Алгоритм Евклида — один из старейших алгоритмов в истории математики (III век до н.э.) и одновременно один из наиболее часто используемых в конкурсном программировании. Его расширение даёт мощный инструмент для решения линейных диофантовых уравнений и систем сравнений.
Ключевые итоги:
- вычисляется за шагов — это оптимально.
- Теорема Безу гарантирует представление НОД в виде линейной комбинации, которое конструктивно строит расширенный алгоритм.
- НОК выражается через НОД; вычислять надо в порядке «сначала делить, потом умножать».
- Диофантово уравнение разрешимо тогда и только тогда, когда ; общее решение — однопараметрическое семейство.
- Обобщённая КТО через итеративное слияние сравнений работает без условия взаимной простоты модулей.