НОД. НОК. Алгоритм Евклида
Наибольший общий делитель
Как несложно догадаться, наибольший общий делитель (англ. greatest common divisor) двух целых чисел - наибольшее число, на которое делится каждое из них. Например:
\[\gcd(15, 20) = 5\] \[\gcd(12, 30) = 6\]Сразу заметим важное свойство:
\[\gcd(a, b) = \gcd(b, a)\]НОД играет большую роль как в математике, так и в программировании, и часто встречается в задачах на различные темы.
Алгоритм Евклида
Алгоритм Евклида - один из первых алгоритмов в истории, использовался ещё в Древней Греции, и дошёл до наших дней. В изначальном виде он назывался “взаимным вычитанием”, так как заключался в поочерёдном вычитании меньшего числа из большего, пока одно из них не станет равным 0. Сегодня чаще всего вместо вычитания используется взятие остатка от деления, но суть алгоритма сохранилась.
Алгоритм заключается в построении ряда чисел следующего вида (\(a > b\)):
\[a, b, r_1, r_2, \ldots, r_n\]Где каждое последующее число является остатком от деления предпредыдущего на предыдущее:
\[r_1 = a \bmod b \\ r_2 = b \bmod r_1 \\ \ldots \\ r_n = r_{n - 2} \bmod r_{n - 1}\]Ряд заканчивается, когда остаток от деления предпоследнего числа на последнее становится равным 0:
\[r_{n - 1} \bmod r_n = 0\]В таком случае утверждается, что:
\[\gcd(a, b) = r_n\]Для доказательства этого утверждения сначала докажем следующее: наборы общих делителей пары \((a, b)\) и пары \((b, r_1)\) полностью совпадают. Рассмотрим произвольный (не обязательно наибольший) общий делитель \(a\) и \(b\):
\(t\) - общий делитель \(a\) и \(b\).
\(r_1 = a \bmod b\), или \(a = bq + r_1\).
Докажем, что \(t\) также является общим делителем \(b\) и \(r_1\).
\(b\) делится на \(t\) по определению.
\(r_1 = a - bq = t * ({a \over t} - {b \over t} * q)\), где \({a \over t}\) и \({b \over t}\) целые по определению.
Значит, \(r_1\) также делится на \(t\), что и требовалось доказать.
Из того, что все общие делители пар \((a, b)\) и \((b, r_1)\) совпадают, в частности следует, что \(\gcd(a, b) = \gcd(b, r_1)\).
Далее по индукции можно доказать следующее:
\[\gcd(a, b) = \gcd(b, r_1) = \gcd(r_1, r_2) = \ldots = \gcd(r_{n - 1}, r_n) = \gcd(r_n, 0)\](Нуль в последнем выражении появился из условия \(r_{n - 1} \bmod r_n = 0\)).
Нуль делится на все числа, отличные от нуля, поэтому справедливо следующее свойство:
\[\gcd(x, 0) = x,\ для\ любого\ x \in \mathbb{N}.\]Следовательно,
\[\gcd(a, b) = r_n,\]что и требовалось доказать.
Варианты реализации алгоритма Евклида на C++
Существует несколько вариантов реализации алгоритма Евклида, как итеративных так и рекурсивных. Классическая итеративная реализация (работающая быстрее всех рекурсивных) выглядит так:
1
2
3
4
5
6
7
8
9
10
11
12
int gcd(int a, int b) {
if (a < b) {
swap(a, b);
}
while (b) {
a %= b;
swap(a, b);
}
return a;
}
Рекурсивно это же можно реализовать так:
1
2
3
4
5
6
7
8
9
10
11
int gcd(int a, int b) {
if (a < b) {
swap(a, b);
}
if (b) {
return gcd(b, a % b);
} else {
return a;
}
}
Преимущество рекурсивной реализации заключается в возможности записать её в очень кратком виде (предполагающим, что \(a > b\)):
1
2
3
int gcd(int a, int b) {
return b ? gcd(b, a % b) : a;
}
На практике разница во времени работы итеративного и рекурсивного вариантов не столь значительна, так что вы можете использовать любой из них.
Время работы алгоритма Евклида
Согласно некоторым исследованиям, время работы алгоритма Евклида тесно связано с числами Фибоначчи. Это выражается, в частности, в том, что два последовательных числа Фибоначчи - наихудшие входные данные для алгоритма Евклида. При \(a = F_n, b = F_{n - 1}\), алгоритм Евклида совершит ровно \(n - 2\) итерации. Отсюда можно выразить асимптотическую сложность алгоритма: последовательность Фибоначчи возрастает с экспоненциальной скоростью, поэтому алгоритм Евклида работает за \(O(\log \min(a, b))\).
Наименьшее общее кратное
С понятием НОД связано также понятия наименьшего общего кратного (англ. least common multiple). Наименьшее общее кратное двух натуральных чисел - наименьшее натуральное число, которое делится на каждое из них. Оно обозначается следующим образом:
\[lcm(a, b)\]и связано с НОД формулой:
\[lcm(a, b) = {a * b \over \gcd(a, b)}\]Реализация на C++:
1
2
3
4
5
int lcm(int a, int b) {
return a / gcd(a, b) * b; //используя форму a * b / gcd(a, b),
//можно получить переполнение на этапе a * b,
//поэтому следует выполнять деление до умножения
}
Взаимнопростые числа
Числа \(a\) и \(b\) называются взаимнопростыми тогда и только тогда, когда они не имеют общих делителей отличных от \(1\). То есть в их отношении должно выполняться условие \(\gcd(a, b) = 1\).
НОД и НОК для произвольного количества чисел
Обе функции легко обобщаются для произвольного числа аргументов последовательным применением:
\[\gcd(a, b, c, d) = \gcd(\gcd(\gcd(a, b), c), d)\] \[lcm(a, b, c, d) = lcm(lcm(lcm(a, b), c), d)\]