Алгоритм Евкліда для обчислення найбільшого спільного дільника
Маючи два невід'ємні цілі числа і , ми повинні знайти їхній НСД (найбільший спільний дільник), тобто найбільше число, яке є дільником і , і . Зазвичай його позначають . Математично він означається так:
(тут символ "" позначає подільність, тобто "" означає " ділить ")
Коли одне з чисел дорівнює нулю, а інше відмінне від нуля, їхнім найбільшим спільним дільником за означенням є друге число. Коли обидва числа дорівнюють нулю, їхній найбільший спільний дільник невизначений (це може бути будь-яке скільки завгодно велике число), але зручно означити його теж як нуль, щоб зберегти асоціативність . Це дає нам просте правило: якщо одне з чисел дорівнює нулю, то найбільший спільний дільник дорівнює іншому числу.
Алгоритм Евкліда, який ми розглянемо нижче, дозволяє знайти найбільший спільний дільник двох чисел і за . Оскільки ця функція асоціативна, щоб знайти НСД більш ніж двох чисел, ми можемо обчислити і так далі.
Уперше алгоритм був описаний у «Началах» Евкліда (близько 300 року до н. е.), але можливо, що алгоритм має навіть давніше походження.
- Чи потрібно знайти лише найбільший спільний дільник (або, через нього, найменше спільне кратне) двох чи кількох чисел?
- Чи вам не потрібні коефіцієнти Безу у ? (якщо потрібні → Розширений алгоритм Евкліда)
- Якщо ви розв'язуєте рівняння у цілих числах — (→ Лінійні діофантові рівняння)?
Алгоритм
Спочатку алгоритм Евкліда був сформульований так: віднімаємо менше число від більшого, доки одне з чисел не стане нулем. Справді, якщо ділить і , то воно ділить і . З іншого боку, якщо ділить і , то воно ділить і , а це означає, що множини спільних дільників і збігаються.
Зауважимо, що залишається більшим числом доти, доки від нього не віднімуть принаймні разів. Тому, щоб прискорити процес, замінюють на . Тоді алгоритм формулюється надзвичайно просто:
Реалізація
- C++
- Python
- TypeScript
- Go
int gcd (int a, int b) {
if (b == 0)
return a;
else
return gcd (b, a % b);
}
def gcd(a: int, b: int) -> int:
if b == 0:
return a
return gcd(b, a % b)
function gcd(a: number, b: number): number {
if (b === 0) return a;
return gcd(b, a % b);
}
func gcd(a, b int) int {
if b == 0 {
return a
}
return gcd(b, a%b)
}
Скориставшись тернарним оператором у C++, ми можемо записати це одним рядком.
int gcd (int a, int b) {
return b ? gcd (b, a % b) : a;
}
І нарешті, ось нерекурсивна реалізація:
- C++
- Python
- TypeScript
- Go
int gcd (int a, int b) {
while (b) {
a %= b;
swap(a, b);
}
return a;
}
def gcd(a: int, b: int) -> int:
while b:
a, b = b, a % b
return a
function gcd(a: number, b: number): number {
while (b) {
[a, b] = [b, a % b];
}
return a;
}
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
return a
}
Зауважимо, що більшість мов постачають функцію НСД у своїй стандартній бібліотеці: у C++17 є std::gcd, у Python — math.gcd, а в Go — big.Int.GCD.
Часова складність
Час роботи алгоритму оцінюється теоремою Ламе, яка встановлює дивовижний зв'язок між алгоритмом Евкліда та послідовністю Фібоначчі:
Якщо і для деякого , то алгоритм Евкліда виконує не більше ніж рекурсивних викликів.
Більш того, можна показати, що верхня межа з цієї теореми є оптимальною. Коли і , виконає рівно рекурсивних викликів. Іншими словами, послідовні числа Фібоначчі є найгіршим випадком вхідних даних для алгоритму Евкліда.
Оскільки числа Фібоначчі зростають експоненційно, ми отримуємо, що алгоритм Евкліда працює за .
Інший спосіб оцінити складність — помітити, що для випадку принаймні у рази менше за , тож більше число зменшується принаймні вдвічі на кожній ітерації алгоритму. Застосовуючи це міркування до випадку, коли ми обчислюємо НСД набору чисел , це також дозволяє нам оцінити загальний час роботи як , а не , оскільки кожна нетривіальна ітерація алгоритму зменшує поточного кандидата на НСД принаймні у рази.
Найменше спільне кратне
Обчислення найменшого спільного кратного (зазвичай позначається НСК) можна звести до обчислення НСД за допомогою такої простої формули:
Отже, НСК можна обчислити за допомогою алгоритму Евкліда з тією самою часовою складністю:
Ось можлива реалізація, яка хитро уникає переповнень цілих чисел, спочатку поділивши на НСД:
- C++
- Python
- TypeScript
- Go
int lcm (int a, int b) {
return a / gcd(a, b) * b;
}
def lcm(a: int, b: int) -> int:
return a // gcd(a, b) * b
function lcm(a: number, b: number): number {
return (a / gcd(a, b)) * b;
}
func lcm(a, b int) int {
return a / gcd(a, b) * b
}
Бінарний НСД
Алгоритм бінарного НСД є оптимізацією звичайного алгоритму Евкліда.
Повільна частина звичайного алгоритму — це операції за модулем. Хоча ми сприймаємо операції за модулем як , вони набагато повільніші за простіші операції на кшталт додавання, віднімання чи бітових операцій. Тому було б краще їх уникати.
Виявляється, що можна сконструювати швидкий алгоритм НСД, який уникає операцій за модулем. Він ґрунтується на кількох властивостях:
- Якщо обидва числа парні, то ми можемо винести двійку з обох і обчислити НСД решти чисел: .
- Якщо одне з чисел парне, а інше непарне, то ми можемо прибрати множник 2 з парного: , якщо непарне.
- Якщо обидва числа непарні, то віднімання одного числа від іншого не змінить НСД:
Використовуючи лише ці властивості та деякі швидкі бітові функції з GCC, ми можемо реалізувати швидку версію:
int gcd(int a, int b) {
if (!a || !b)
return a | b;
unsigned shift = __builtin_ctz(a | b);
a >>= __builtin_ctz(a);
do {
b >>= __builtin_ctz(b);
if (a > b)
swap(a, b);
b -= a;
} while (b);
return a << shift;
}
Зверніть увагу, що така оптимізація зазвичай не потрібна, і більшість мов програмування вже мають функцію НСД у своїх стандартних бібліотеках.
Наприклад, у C++17 є така функція std::gcd у заголовку numeric.