Розширений алгоритм Евкліда
Якщо алгоритм Евкліда обчислює лише найбільший спільний дільник (НСД) двох невід'ємних цілих чисел і , то розширена версія додатково знаходить спосіб подати НСД через і , тобто коефіцієнти і , для яких:
Важливо зауважити, що за тотожністю Безу таке подання завжди можна знайти. Наприклад, , тому ми можемо подати як лінійну комбінацію доданків і :
Загальнішу форму цієї задачі розглянуто у статті про лінійні діофантові рівняння. Вона ґрунтуватиметься на цьому алгоритмі.
- Чи окрім вам потрібні самі коефіцієнти такі, що ? (якщо потрібен лише НСД → Алгоритм Евкліда)
- Чи кінцева мета — обернений елемент за модулем для необов'язково простого ? (інакше для простого простіше через мала теорема Ферма)
- Чи ви розв'язуєте лінійне діофантове рівняння ? (→ Лінійні діофантові рівняння)
Алгоритм
У цьому розділі позначатимемо НСД чисел і через .
Зміни до вихідного алгоритму дуже прості. Якщо пригадати алгоритм, то можна побачити, що він завершується при і . Для цих параметрів коефіцієнти знаходяться легко, а саме .
Починаючи з цих коефіцієнтів , ми можемо рухатися назад вгору рекурсивними викликами. Усе, що нам потрібно, — з'ясувати, як змінюються коефіцієнти і під час переходу від до .
Припустимо, що ми знайшли коефіцієнти для :
і хочемо знайти пару для :
Ми можемо подати так:
Підставляючи цей вираз у рівняння коефіцієнтів для , отримуємо:
а після перегрупування доданків:
Ми знайшли значення і :
Реалізація
- C++
- Python
- TypeScript
- Go
int gcd(int a, int b, int& x, int& y) {
if (b == 0) {
x = 1;
y = 0;
return a;
}
int x1, y1;
int d = gcd(b, a % b, x1, y1);
x = y1;
y = x1 - y1 * (a / b);
return d;
}
def gcd(a: int, b: int) -> tuple[int, int, int]:
# Повертає (g, x, y), де a*x + b*y = g
if b == 0:
return a, 1, 0
d, x1, y1 = gcd(b, a % b)
# Перераховуємо коефіцієнти під час повернення вгору
return d, y1, x1 - y1 * (a // b)
// Повертає [g, x, y], де a*x + b*y = g
function gcd(a: number, b: number): [number, number, number] {
if (b === 0) {
return [a, 1, 0];
}
const [d, x1, y1] = gcd(b, a % b);
// Перераховуємо коефіцієнти під час повернення вгору
return [d, y1, x1 - y1 * Math.floor(a / b)];
}
// gcd повертає (g, x, y), де a*x + b*y = g
func gcd(a, b int) (int, int, int) {
if b == 0 {
return a, 1, 0
}
d, x1, y1 := gcd(b, a%b)
// Перераховуємо коефіцієнти під час повернення вгору
return d, y1, x1 - y1*(a/b)
}
Наведена вище рекурсивна функція повертає НСД, а значення коефіцієнтів записує у x і y (які передаються у функцію за посиланням).
Ця реалізація розширеного алгоритму Евкліда дає правильні результати і для від'ємних цілих чисел.
Ітеративна версія
Розширений алгоритм Евкліда також можна записати ітеративно. Оскільки в такому варіанті немає рекурсії, код працюватиме трохи швидше за рекурсивний.
- C++
- Python
- TypeScript
- Go
int gcd(int a, int b, int& x, int& y) {
x = 1, y = 0;
int x1 = 0, y1 = 1, a1 = a, b1 = b;
while (b1) {
int q = a1 / b1;
tie(x, x1) = make_tuple(x1, x - q * x1);
tie(y, y1) = make_tuple(y1, y - q * y1);
tie(a1, b1) = make_tuple(b1, a1 - q * b1);
}
return a1;
}
def gcd(a: int, b: int) -> tuple[int, int, int]:
# Повертає (g, x, y), де a*x + b*y = g
x, y = 1, 0
x1, y1, a1, b1 = 0, 1, a, b
while b1:
q = a1 // b1
x, x1 = x1, x - q * x1
y, y1 = y1, y - q * y1
a1, b1 = b1, a1 - q * b1
return a1, x, y
// Повертає [g, x, y], де a*x + b*y = g
function gcd(a: number, b: number): [number, number, number] {
let x = 1;
let y = 0;
let x1 = 0;
let y1 = 1;
let a1 = a;
let b1 = b;
while (b1 !== 0) {
const q = Math.floor(a1 / b1);
[x, x1] = [x1, x - q * x1];
[y, y1] = [y1, y - q * y1];
[a1, b1] = [b1, a1 - q * b1];
}
return [a1, x, y];
}
// gcd повертає (g, x, y), де a*x + b*y = g
func gcd(a, b int) (int, int, int) {
x, y := 1, 0
x1, y1, a1, b1 := 0, 1, a, b
for b1 != 0 {
q := a1 / b1
x, x1 = x1, x-q*x1
y, y1 = y1, y-q*y1
a1, b1 = b1, a1-q*b1
}
return a1, x, y
}
Якщо уважно придивитися до змінних a1 і b1, то можна помітити, що вони набувають точнісінько таких самих значень, як і в ітеративній версії звичайного алгоритму Евкліда. Отже, алгоритм принаймні обчислить правильний НСД.
Щоб зрозуміти, чому алгоритм обчислює правильні коефіцієнти, зауважимо, що в будь-який момент (перед початком циклу while і наприкінці кожної ітерації) виконуються такі інваріанти:
Позначмо значення наприкінці ітерації штрихом () і припустимо, що . З алгоритму Евкліда маємо:
Щоб виконувався перший інваріант, має бути правильно, що:
Аналогічно для другого інваріанта має виконуватися:
Порівнюючи коефіцієнти при і , можна вивести рівняння оновлення для кожної змінної, які гарантують, що інваріанти зберігаються протягом усього алгоритму.
Наприкінці ми знаємо, що містить НСД, тож . Це означає, що ми знайшли потрібні коефіцієнти.
Код можна оптимізувати ще більше: прибрати змінні і та просто повторно використати і . Однак якщо так зробити, то ми втратимо можливість міркувати про інваріанти.