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

Бінарне піднесення до степеня через факторизацію

Розглянемо задачу обчислення axy(mod2d)ax^y \pmod{2^d} за заданими цілими числами aa, xx, yy та d3d \geq 3, де xx непарне.

Наведений нижче алгоритм дозволяє розв'язати цю задачу за O(d)O(d) додавань і бітових операцій та одне множення на yy.

Коли підходить цей алгоритм?
  • Чи модуль є саме степенем двійки 2d2^dd3d \geq 3), а основа xxнепарна? (якщо модуль довільний → Бінарне піднесення до степеня)
  • Чи критично прибрати множник log\log і обчислити axymod2dax^y \bmod 2^d за O(d)O(d) замість O(dlogy)O(d \log y) звичайного бінарного степеня?
  • Чи ви готові попередньо обчислити таблицю логарифмів за основою 55 (метод спирається на структуру мультиплікативної групи за модулем 2d2^d)?

Завдяки структурі мультиплікативної групи за модулем 2d2^d будь-яке число xx таке, що x1(mod4)x \equiv 1 \pmod 4, можна подати у вигляді

xbL(x)(mod2d),x \equiv b^{L(x)} \pmod{2^d},

де b5(mod8)b \equiv 5 \pmod 8. Без втрати загальності припускаємо, що x1(mod4)x \equiv 1 \pmod 4, оскільки випадок x3(mod4)x \equiv 3 \pmod 4 можна звести до x1(mod4)x \equiv 1 \pmod 4 заміною xxx \mapsto -x та a(1)yaa \mapsto (-1)^{y} a. У цьому позначенні axyax^y подається як

axyabyL(x)(mod2d).a x^y \equiv a b^{yL(x)} \pmod{2^d}.

Основна ідея алгоритму полягає в тому, щоб спростити обчислення L(x)L(x) та byL(x)b^{y L(x)}, користуючись тим, що ми працюємо за модулем 2d2^d. З причин, які стануть зрозумілими далі, ми працюватимемо з 4L(x)4L(x), а не з L(x)L(x), але взятим за модулем 2d2^d замість 2d22^{d-2}.

У цій статті ми розглянемо реалізацію для 3232-бітних цілих чисел. Нехай

  • mbin_log_32(r, x) — функція, що обчислює r+4L(x)(mod2d)r+4L(x) \pmod{2^d};
  • mbin_exp_32(r, x) — функція, що обчислює rbx4(mod2d)r b^{\frac{x}{4}} \pmod{2^d};
  • mbin_power_odd_32(a, x, y) — функція, що обчислює axy(mod2d)ax^y \pmod{2^d}.

Тоді mbin_power_odd_32 реалізується так:

uint32_t mbin_power_odd_32(uint32_t rem, uint32_t base, uint32_t exp) {
if (base & 2) {
/* дільник вважаємо від'ємним */
base = -base;
/* перевіряємо, чи результат має бути від'ємним */
if (exp & 1) {
rem = -rem;
}
}
return (mbin_exp_32(rem, mbin_log_32(0, base) * exp));
}

Обчислення 4L(x) за x

Нехай xx — непарне число таке, що x1(mod4)x \equiv 1 \pmod 4. Його можна подати у вигляді

x(2a1+1)(2ak+1)(mod2d),x \equiv (2^{a_1}+1)\dots(2^{a_k}+1) \pmod{2^d},

де 1<a1<<ak<d1 < a_1 < \dots < a_k < d. Тут L()L(\cdot) коректно визначене для кожного множника, оскільки всі вони дорівнюють 11 за модулем 44. Отже,

4L(x)4L(2a1+1)++4L(2ak+1)(mod2d).4L(x) \equiv 4L(2^{a_1}+1)+\dots+4L(2^{a_k}+1) \pmod{2^{d}}.

Таким чином, якщо ми попередньо обчислимо tk=4L(2n+1)t_k = 4L(2^n+1) для всіх 1<k<d1 < k < d, то зможемо обчислити 4L(x)4L(x) для будь-якого числа xx.

Для 32-бітних цілих чисел можна скористатися такою таблицею:

const uint32_t mbin_log_32_table[32] = {
0x00000000, 0x00000000, 0xd3cfd984, 0x9ee62e18,
0xe83d9070, 0xb59e81e0, 0xa17407c0, 0xce601f80,
0xf4807f00, 0xe701fe00, 0xbe07fc00, 0xfc1ff800,
0xf87ff000, 0xf1ffe000, 0xe7ffc000, 0xdfff8000,
0xffff0000, 0xfffe0000, 0xfffc0000, 0xfff80000,
0xfff00000, 0xffe00000, 0xffc00000, 0xff800000,
0xff000000, 0xfe000000, 0xfc000000, 0xf8000000,
0xf0000000, 0xe0000000, 0xc0000000, 0x80000000,
};

На практиці використовується дещо інший підхід, ніж описаний вище. Замість того щоб шукати факторизацію для xx, ми послідовно множитимемо xx на 2n+12^n+1, доки не перетворимо його на 11 за модулем 2d2^d. Таким чином ми знайдемо подання x1x^{-1}, тобто

x(2a1+1)(2ak+1)1(mod2d).x (2^{a_1}+1)\dots(2^{a_k}+1) \equiv 1 \pmod {2^d}.

Для цього ми перебираємо nn такі, що 1<n<d1 < n < d. Якщо в поточного xx встановлено nn-й біт, ми множимо xx на 2n+12^n+1, що зручно записується в C++ як x = x + (x << n). Це не змінить бітів, нижчих за nn, але занулить nn-й біт, оскільки xx непарне.

Враховуючи все це, функція mbin_log_32(r, x) реалізується так:

uint32_t mbin_log_32(uint32_t r, uint32_t x) {
uint8_t n;

for (n = 2; n < 32; n++) {
if (x & (1 << n)) {
x = x + (x << n);
r -= mbin_log_32_table[n];
}
}

return r;
}

Зауважимо, що 4L(x)=4L(x1)4L(x) = -4L(x^{-1}), тому замість того щоб додавати 4L(2n+1)4L(2^n+1), ми віднімаємо його від rr, яке спочатку дорівнює 00.

Обчислення x за 4L(x)

Зауважимо, що для k1k \geq 1 виконується

(a2k+1)2=a222k+a2k+1+1=b2k+1+1,(a 2^{k}+1)^2 = a^2 2^{2k} +a 2^{k+1}+1 = b2^{k+1}+1,

звідки (послідовним піднесенням до квадрата) можна вивести, що

(2a+1)2b1(mod2a+b).(2^a+1)^{2^b} \equiv 1 \pmod{2^{a+b}}.

Застосувавши цей результат до a=2n+1a=2^n+1 та b=dkb=d-k, дістаємо, що мультиплікативний порядок числа 2n+12^n+1 є дільником 2dn2^{d-n}.

Це, своєю чергою, означає, що L(2n+1)L(2^n+1) має ділитися на 2n2^{n}, оскільки порядок bb дорівнює 2d22^{d-2}, а порядок byb^y дорівнює 2d2v2^{d-2-v}, де 2v2^v — найвищий степінь 22, що ділить yy, тож нам потрібно

2dk0(mod2d2v),2^{d-k} \equiv 0 \pmod{2^{d-2-v}},

отже, vv має бути більшим або рівним за k2k-2. Це дещо незручно, і щоб цьому зарадити, ми на початку домовилися множити L(x)L(x) на 44. Тепер, якщо ми знаємо 4L(x)4L(x), ми можемо однозначно розкласти його на суму 4L(2n+1)4L(2^n+1), послідовно перевіряючи біти у 4L(x)4L(x). Якщо nn-й біт встановлено в 11, ми множитимемо результат на 2n+12^n+1 та зменшуватимемо поточне 4L(x)4L(x) на 4L(2n+1)4L(2^n+1).

Таким чином, mbin_exp_32 реалізується так:

uint32_t mbin_exp_32(uint32_t r, uint32_t x) {
uint8_t n;

for (n = 2; n < 32; n++) {
if (x & (1 << n)) {
r = r + (r << n);
x -= mbin_log_32_table[n];
}
}

return r;
}

Подальші оптимізації

Кількість ітерацій можна вдвічі зменшити, якщо помітити, що 4L(2d1+1)=2d14L(2^{d-1}+1)=2^{d-1} і що для 2kd2k \geq d виконується

(2n+1)222n+2n+1+12n+1+1(mod2d),(2^n+1)^2 \equiv 2^{2n} + 2^{n+1}+1 \equiv 2^{n+1}+1 \pmod{2^d},

звідки можна вивести, що 4L(2n+1)=2n4L(2^n+1)=2^n для 2nd2n \geq d. Отже, можна спростити алгоритм, проходячи лише до d2\frac{d}{2}, а потім скористатися наведеним вище фактом, щоб обчислити решту частини бітовими операціями:

uint32_t mbin_log_32(uint32_t r, uint32_t x) {
uint8_t n;

for (n = 2; n != 16; n++) {
if (x & (1 << n)) {
x = x + (x << n);
r -= mbin_log_32_table[n];
}
}

r -= (x & 0xFFFF0000);

return r;
}

uint32_t mbin_exp_32(uint32_t r, uint32_t x) {
uint8_t n;

for (n = 2; n != 16; n++) {
if (x & (1 << n)) {
r = r + (r << n);
x -= mbin_log_32_table[n];
}
}

r *= 1 - (x & 0xFFFF0000);

return r;
}

Обчислення таблиці логарифмів

Щоб обчислити таблицю логарифмів, можна модифікувати алгоритм Поліга–Геллмана для випадку, коли модуль є степенем 22.

Наше головне завдання тут — обчислити xx таке, що gxy(mod2d)g^x \equiv y \pmod{2^d}, де g=5g=5, а yy — число вигляду 2n+12^n+1.

Підносячи обидві частини до квадрата kk разів, дістаємо

g2kxy2k(mod2d).g^{2^k x} \equiv y^{2^k} \pmod{2^d}.

Зауважимо, що порядок gg не перевищує 2d2^{d} (насправді 2d22^{d-2}, але для зручності триматимемося 2d2^d), тож, узявши k=d1k=d-1, ми матимемо в лівій частині або g1g^1, або g0g^0, що дозволяє визначити найменший біт xx, порівнявши y2ky^{2^k} з gg. Тепер припустимо, що x=x0+2kx1x=x_0 + 2^k x_1, де x0x_0 — відома частина, а x1x_1 ще невідома. Тоді

gx0+2kx1y(mod2d).g^{x_0+2^k x_1} \equiv y \pmod{2^d}.

Помноживши обидві частини на gx0g^{-x_0}, дістаємо

g2kx1(gx0y)(mod2d).g^{2^k x_1} \equiv (g^{-x_0} y) \pmod{2^d}.

Тепер, підносячи обидві частини до квадрата dk1d-k-1 разів, ми можемо отримати наступний біт xx, зрештою відновивши всі його біти.

Джерела