Расширенный алгоритм Евклида
Тот же Евклид, но по дороге он находит коэффициенты уравнения ax + by = НОД. Отсюда — обратные элементы и все целые решения линейных уравнений.
6 мин
Обычный Евклид отвечает на вопрос «чему равен НОД». Расширенный отвечает на более сильный: как выразить НОД через сами числа.
Формально: даны целые и , не равные нулю одновременно. Нужно найти такие целые и , что
Уравнение такого вида называется диофантовым — это уравнение, решения которого ищут в целых числах.
Первое, что здесь неочевидно: решение существует всегда. Второе — что находится оно тем же алгоритмом Евклида, просто раскрученным в обратную сторону.
Откуда берутся формулы
Евклид спускается по цепочке
Внизу цепочки задача решается глазами: для пары подходит , , потому что .
Осталось научиться подниматься на шаг вверх. Пусть для пары мы уже знаем ответ:
Подставим определение остатка :
Раскроем скобки и соберём отдельно то, что стоит при , и то, что стоит при :
Слева получилось ровно то, что нужно, — комбинация и . Значит,
Это весь алгоритм. Никакой отдельной идеи в нём нет — только аккуратно раскрытые скобки.
flowchart TD
A["gcd(4, 3)"] --> B["gcd(3, 1)"]
B --> C["gcd(1, 0)"]
C -->|"x=1, y=0"| D["база: 1·1 + 0·0 = 1"]
D -->|"поднимаемся"| E["gcd(3,1): x=0, y=1<br/>3·0 + 1·1 = 1"]
E -->|"поднимаемся"| F["gcd(4,3): x=1, y=-1<br/>4·1 + 3·(-1) = 1"]
Спуск ничем не отличается от обычного Евклида. Вся работа происходит на подъёме: каждый возврат из рекурсии пересчитывает пару коэффициентов по формулам выше.
Код
long long gcdExt(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 = gcdExt(b, a % b, x1, y1);
x = y1;
y = x1 - (a / b) * y1;
return d;
}
Здесь x и y передаются по ссылке: функция должна вернуть три числа сразу — сам НОД и два коэффициента, — а return в C++ один. Амперсанд означает, что внутри функции мы меняем не копию, а сами переменные вызывающего.
Отдельные x1, y1 нужны обязательно. Новые значения зависят и от старого , и от старого одновременно, поэтому перезаписывать их на месте нельзя — второе присваивание увидело бы уже испорченное первое.
И да: в отличие от обычного Евклида, этот алгоритм пишут именно рекурсивно. Итеративная версия существует, но читается заметно хуже, а выигрыш нулевой — глубина рекурсии здесь логарифмическая, до сорока кадров для чисел до .
Как это разворачивается
Возьмём и проследим за стеком.
| вызов | что вернулось снизу | проверка | ||
|---|---|---|---|---|
gcdExt(1, 0) |
база | |||
gcdExt(3, 1) |
||||
gcdExt(4, 3) |
Обратите внимание: равенство выполняется на каждом уровне, а не только в конце. Это удобно при отладке — можно поставить проверку внутрь функции и сразу увидеть, на каком шаге сломалось.
Заодно видно, что коэффициенты бывают отрицательными. Это нормально и неизбежно: если и положительны, а НОД меньше обоих, то без минуса комбинацию не собрать.
Общее уравнение ax + by = c
На практике справа стоит не НОД, а произвольное . Тогда:
Решение существует тогда и только тогда, когда делится на . В одну сторону очевидно: левая часть всегда кратна . В другую — если , домножим найденную пару на .
// решает a*x + b*y = c; возвращает false, если решений нет
bool diophantine(long long a, long long b, long long c, long long &x, long long &y) {
long long g = gcdExt(a, b, x, y);
if (c % g != 0) return false;
x *= c / g;
y *= c / g;
return true;
}
Найденное решение — не единственное. Все остальные получаются сдвигом:
Проверяется подстановкой: добавка к левой части равна . Делить нужно именно на — с меньшим шагом получатся не все решения, с бо́льшим часть потеряется.
Отсюда решаются типовые постановки: «найдите решение с наименьшим неотрицательным » — берём ; «сколько решений в заданном отрезке» — считаем допустимые через два неравенства.
Зачем это нужно на самом деле
Обратный элемент по модулю. Если , то из следует , то есть — обратный к . В отличие от способа через малую теорему Ферма, здесь модуль не обязан быть простым — достаточно взаимной простоты. Это единственный доступный путь, когда модуль вида или просто составной.
Задачи про кувшины и гири. «Есть сосуды на 7 и 11 литров, отмерьте 5» — это буквально , где знак означает направление переливания.
Линейные сравнения и китайская теорема — целиком стоят на расширенном Евклиде, про них отдельная статья.
Где ломается
Главная ловушка — переполнение. Промежуточные и могут по модулю доходить до , а после домножения на — вылететь за long long. Если , и порядка , считать нужно в __int128 или сразу приводить по модулю.
Вторая — отрицательные входные данные. Формулы верны и для них, но % в C++ возвращает остаток со знаком делимого, и «наименьшее неотрицательное решение» придётся нормализовать вручную: ((x % n) + n) % n.