Перейти до основного вмісту

Розширений алгоритм Евкліда

Якщо алгоритм Евкліда обчислює лише найбільший спільний дільник (НСД) двох невід'ємних цілих чисел aa і bb, то розширена версія додатково знаходить спосіб подати НСД через aa і bb, тобто коефіцієнти xx і yy, для яких:

ax+by=gcd(a,b)a \cdot x + b \cdot y = \gcd(a, b)

Важливо зауважити, що за тотожністю Безу таке подання завжди можна знайти. Наприклад, gcd(55,80)=5\gcd(55, 80) = 5, тому ми можемо подати 55 як лінійну комбінацію доданків 5555 і 8080: 553+80(2)=555 \cdot 3 + 80 \cdot (-2) = 5

Загальнішу форму цієї задачі розглянуто у статті про лінійні діофантові рівняння. Вона ґрунтуватиметься на цьому алгоритмі.

Коли підходить цей алгоритм?
  • Чи окрім gcd(a,b)\gcd(a,b) вам потрібні самі коефіцієнти x,yx, y такі, що ax+by=gcd(a,b)ax + by = \gcd(a,b)? (якщо потрібен лише НСД → Алгоритм Евкліда)
  • Чи кінцева мета — обернений елемент за модулем a1modma^{-1} \bmod m для необов'язково простого mm? (інакше для простого mm простіше через мала теорема Ферма)
  • Чи ви розв'язуєте лінійне діофантове рівняння ax+by=cax + by = c? (→ Лінійні діофантові рівняння)

Алгоритм

У цьому розділі позначатимемо НСД чисел aa і bb через gg.

Зміни до вихідного алгоритму дуже прості. Якщо пригадати алгоритм, то можна побачити, що він завершується при b=0b = 0 і a=ga = g. Для цих параметрів коефіцієнти знаходяться легко, а саме g1+00=gg \cdot 1 + 0 \cdot 0 = g.

Починаючи з цих коефіцієнтів (x,y)=(1,0)(x, y) = (1, 0), ми можемо рухатися назад вгору рекурсивними викликами. Усе, що нам потрібно, — з'ясувати, як змінюються коефіцієнти xx і yy під час переходу від (a,b)(a, b) до (b,amodb)(b, a \bmod b).

Припустимо, що ми знайшли коефіцієнти (x1,y1)(x_1, y_1) для (b,amodb)(b, a \bmod b):

bx1+(amodb)y1=gb \cdot x_1 + (a \bmod b) \cdot y_1 = g

і хочемо знайти пару (x,y)(x, y) для (a,b)(a, b):

ax+by=ga \cdot x + b \cdot y = g

Ми можемо подати amodba \bmod b так:

amodb=aabba \bmod b = a - \left\lfloor \frac{a}{b} \right\rfloor \cdot b

Підставляючи цей вираз у рівняння коефіцієнтів для (x1,y1)(x_1, y_1), отримуємо:

g=bx1+(amodb)y1=bx1+(aabb)y1g = b \cdot x_1 + (a \bmod b) \cdot y_1 = b \cdot x_1 + \left(a - \left\lfloor \frac{a}{b} \right\rfloor \cdot b \right) \cdot y_1

а після перегрупування доданків:

g=ay1+b(x1y1ab)g = a \cdot y_1 + b \cdot \left( x_1 - y_1 \cdot \left\lfloor \frac{a}{b} \right\rfloor \right)

Ми знайшли значення xx і yy:

{x=y1y=x1y1ab\begin{cases} x = y_1 \\ y = x_1 - y_1 \cdot \left\lfloor \frac{a}{b} \right\rfloor \end{cases}

Реалізація

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;
}

Наведена вище рекурсивна функція повертає НСД, а значення коефіцієнтів записує у x і y (які передаються у функцію за посиланням).

Ця реалізація розширеного алгоритму Евкліда дає правильні результати і для від'ємних цілих чисел.

Ітеративна версія

Розширений алгоритм Евкліда також можна записати ітеративно. Оскільки в такому варіанті немає рекурсії, код працюватиме трохи швидше за рекурсивний.

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;
}

Якщо уважно придивитися до змінних a1 і b1, то можна помітити, що вони набувають точнісінько таких самих значень, як і в ітеративній версії звичайного алгоритму Евкліда. Отже, алгоритм принаймні обчислить правильний НСД.

Щоб зрозуміти, чому алгоритм обчислює правильні коефіцієнти, зауважимо, що в будь-який момент (перед початком циклу while і наприкінці кожної ітерації) виконуються такі інваріанти:

xa+yb=a1x \cdot a + y \cdot b = a_1 x1a+y1b=b1x_1 \cdot a + y_1 \cdot b = b_1

Позначмо значення наприкінці ітерації штрихом (') і припустимо, що q=a1b1q = \frac{a_1}{b_1}. З алгоритму Евкліда маємо:

a1=b1a_1' = b_1 b1=a1qb1b_1' = a_1 - q \cdot b_1

Щоб виконувався перший інваріант, має бути правильно, що:

xa+yb=a1=b1x' \cdot a + y' \cdot b = a_1' = b_1 xa+yb=x1a+y1bx' \cdot a + y' \cdot b = x_1 \cdot a + y_1 \cdot b

Аналогічно для другого інваріанта має виконуватися:

x1a+y1b=a1qb1x_1' \cdot a + y_1' \cdot b = a_1 - q \cdot b_1 x1a+y1b=(xqx1)a+(yqy1)bx_1' \cdot a + y_1' \cdot b = (x - q \cdot x_1) \cdot a + (y - q \cdot y_1) \cdot b

Порівнюючи коефіцієнти при aa і bb, можна вивести рівняння оновлення для кожної змінної, які гарантують, що інваріанти зберігаються протягом усього алгоритму.

Наприкінці ми знаємо, що a1a_1 містить НСД, тож xa+yb=gx \cdot a + y \cdot b = g. Це означає, що ми знайшли потрібні коефіцієнти.

Код можна оптимізувати ще більше: прибрати змінні a1a_1 і b1b_1 та просто повторно використати aa і bb. Однак якщо так зробити, то ми втратимо можливість міркувати про інваріанти.

Задачі для практики

Відеоматеріали