Бінарне піднесення до степеня через факторизацію
Розглянемо задачу обчислення за заданими цілими числами , , та , де непарне.
Наведений нижче алгоритм дозволяє розв'язати цю задачу за додавань і бітових операцій та одне множення на .
- Чи модуль є саме степенем двійки (з ), а основа — непарна? (якщо модуль довільний → Бінарне піднесення до степеня)
- Чи критично прибрати множник і обчислити за замість звичайного бінарного степеня?
- Чи ви готові попередньо обчислити таблицю логарифмів за основою (метод спирається на структуру мультиплікативної групи за модулем )?
Завдяки структурі мультиплікативної групи за модулем будь-яке число таке, що , можна подати у вигляді
де . Без втрати загальності припускаємо, що , оскільки випадок можна звести до заміною та . У цьому позначенні подається як
Основна ідея алгоритму полягає в тому, щоб спростити обчислення та , користуючись тим, що ми працюємо за модулем . З причин, які стануть зрозумілими далі, ми працюватимемо з , а не з , але взятим за модулем замість .
У цій статті ми розглянемо реалізацію для -бітних цілих чисел. Нехай
mbin_log_32(r, x)— функція, що обчислює ;mbin_exp_32(r, x)— функція, що обчислює ;mbin_power_odd_32(a, x, y)— функція, що обчислює .
Тоді mbin_power_odd_32 реалізується так:
- C++
- Python
- TypeScript
- Go
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));
}
def mbin_power_odd_32(rem: int, base: int, exp: int) -> int:
if base & 2:
# дільник вважаємо від'ємним (заперечення за модулем 2^32)
base = (-base) & 0xFFFFFFFF
# перевіряємо, чи результат має бути від'ємним
if exp & 1:
rem = (-rem) & 0xFFFFFFFF
# множення на exp також беремо за модулем 2^32
return mbin_exp_32(rem, (mbin_log_32(0, base) * exp) & 0xFFFFFFFF)
// Усі обчислення виконуємо в BigInt і маскуємо до 32 бітів (0xFFFFFFFF),
// щоб точно відтворити поведінку беззнакового uint32 з C++.
function mbinPowerOdd32(rem: bigint, base: bigint, exp: bigint): bigint {
if (base & 2n) {
// дільник вважаємо від'ємним (заперечення за модулем 2^32)
base = -base & 0xFFFFFFFFn;
// перевіряємо, чи результат має бути від'ємним
if (exp & 1n) {
rem = -rem & 0xFFFFFFFFn;
}
}
// множення на exp також беремо за модулем 2^32
return mbinExp32(rem, (mbinLog32(0n, base) * exp) & 0xFFFFFFFFn);
}
// uint32 у Go переповнюється за модулем 2^32 автоматично,
// тож додаткове маскування не потрібне.
func mbinPowerOdd32(rem, base, exp uint32) uint32 {
if base&2 != 0 {
// дільник вважаємо від'ємним
base = -base
// перевіряємо, чи результат має бути від'ємним
if exp&1 != 0 {
rem = -rem
}
}
return mbinExp32(rem, mbinLog32(0, base)*exp)
}
Обчислення 4L(x) за x
Нехай — непарне число таке, що . Його можна подати у вигляді
де . Тут коректно визначене для кожного множника, оскільки всі вони дорівнюють за модулем . Отже,
Таким чином, якщо ми попередньо обчислимо для всіх , то зможемо обчислити для будь-якого числа .
Для 32-бітних цілих чисел можна скористатися такою таблицею:
- C++
- Python
- TypeScript
- Go
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,
};
mbin_log_32_table = [
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,
]
const mbinLog32Table: bigint[] = [
0x00000000n, 0x00000000n, 0xd3cfd984n, 0x9ee62e18n,
0xe83d9070n, 0xb59e81e0n, 0xa17407c0n, 0xce601f80n,
0xf4807f00n, 0xe701fe00n, 0xbe07fc00n, 0xfc1ff800n,
0xf87ff000n, 0xf1ffe000n, 0xe7ffc000n, 0xdfff8000n,
0xffff0000n, 0xfffe0000n, 0xfffc0000n, 0xfff80000n,
0xfff00000n, 0xffe00000n, 0xffc00000n, 0xff800000n,
0xff000000n, 0xfe000000n, 0xfc000000n, 0xf8000000n,
0xf0000000n, 0xe0000000n, 0xc0000000n, 0x80000000n,
];
var mbinLog32Table = [32]uint32{
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,
}
На практиці використовується дещо інший підхід, ніж описаний вище. Замість того щоб шукати факторизацію для , ми послідовно множитимемо на , доки не перетворимо його на за модулем . Таким чином ми знайдемо подання , тобто
Для цього ми перебираємо такі, що . Якщо в поточного встановлено -й біт, ми множимо на , що зручно записується в C++ як x = x + (x << n). Це не змінить бітів, нижчих за , але занулить -й біт, оскільки непарне.
Враховуючи все це, функція mbin_log_32(r, x) реалізується так:
- C++
- Python
- TypeScript
- Go
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;
}
def mbin_log_32(r: int, x: int) -> int:
for n in range(2, 32):
if x & (1 << n):
# усе тримаємо в межах 32 бітів
x = (x + (x << n)) & 0xFFFFFFFF
r = (r - mbin_log_32_table[n]) & 0xFFFFFFFF
return r
function mbinLog32(r: bigint, x: bigint): bigint {
for (let n = 2n; n < 32n; n++) {
if (x & (1n << n)) {
// усе тримаємо в межах 32 бітів
x = (x + (x << n)) & 0xFFFFFFFFn;
r = (r - mbinLog32Table[Number(n)]) & 0xFFFFFFFFn;
}
}
return r;
}
func mbinLog32(r, x uint32) uint32 {
for n := uint(2); n < 32; n++ {
if x&(1<<n) != 0 {
x = x + (x << n)
r -= mbinLog32Table[n]
}
}
return r
}
Зауважимо, що , тому замість того щоб додавати , ми віднімаємо його від , яке спочатку дорівнює .
Обчислення x за 4L(x)
Зауважимо, що для виконується
звідки (послідовним піднесенням до квадрата) можна вивести, що
Застосувавши цей результат до та , дістаємо, що мультиплікативний порядок числа є дільником .
Це, своєю чергою, означає, що має ділитися на , оскільки порядок дорівнює , а порядок дорівнює , де — найвищий степінь , що ділить , тож нам потрібно
отже, має бути більшим або рівним за . Це дещо незручно, і щоб цьому зарадити, ми на початку домовилися множити на . Тепер, якщо ми знаємо , ми можемо однозначно розкласти його на суму , послідовно перевіряючи біти у . Якщо -й біт встановлено в , ми множитимемо результат на та зменшуватимемо поточне на .
Таким чином, mbin_exp_32 реалізується так:
- C++
- Python
- TypeScript
- Go
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;
}
def mbin_exp_32(r: int, x: int) -> int:
for n in range(2, 32):
if x & (1 << n):
# усе тримаємо в межах 32 бітів
r = (r + (r << n)) & 0xFFFFFFFF
x = (x - mbin_log_32_table[n]) & 0xFFFFFFFF
return r
function mbinExp32(r: bigint, x: bigint): bigint {
for (let n = 2n; n < 32n; n++) {
if (x & (1n << n)) {
// усе тримаємо в межах 32 бітів
r = (r + (r << n)) & 0xFFFFFFFFn;
x = (x - mbinLog32Table[Number(n)]) & 0xFFFFFFFFn;
}
}
return r;
}
func mbinExp32(r, x uint32) uint32 {
for n := uint(2); n < 32; n++ {
if x&(1<<n) != 0 {
r = r + (r << n)
x -= mbinLog32Table[n]
}
}
return r
}
Подальші оптимізації
Кількість ітерацій можна вдвічі зменшити, якщо помітити, що і що для виконується
звідки можна вивести, що для . Отже, можна спростити алгоритм, проходячи лише до , а потім скористатися наведеним вище фактом, щоб обчислити решту частини бітовими операціями:
- C++
- Python
- TypeScript
- Go
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;
}
def mbin_log_32(r: int, x: int) -> int:
for n in range(2, 16):
if x & (1 << n):
x = (x + (x << n)) & 0xFFFFFFFF
r = (r - mbin_log_32_table[n]) & 0xFFFFFFFF
r = (r - (x & 0xFFFF0000)) & 0xFFFFFFFF
return r
def mbin_exp_32(r: int, x: int) -> int:
for n in range(2, 16):
if x & (1 << n):
r = (r + (r << n)) & 0xFFFFFFFF
x = (x - mbin_log_32_table[n]) & 0xFFFFFFFF
# множення також тримаємо в межах 32 бітів
r = (r * ((1 - (x & 0xFFFF0000)) & 0xFFFFFFFF)) & 0xFFFFFFFF
return r
function mbinLog32(r: bigint, x: bigint): bigint {
for (let n = 2n; n < 16n; n++) {
if (x & (1n << n)) {
x = (x + (x << n)) & 0xFFFFFFFFn;
r = (r - mbinLog32Table[Number(n)]) & 0xFFFFFFFFn;
}
}
r = (r - (x & 0xFFFF0000n)) & 0xFFFFFFFFn;
return r;
}
function mbinExp32(r: bigint, x: bigint): bigint {
for (let n = 2n; n < 16n; n++) {
if (x & (1n << n)) {
r = (r + (r << n)) & 0xFFFFFFFFn;
x = (x - mbinLog32Table[Number(n)]) & 0xFFFFFFFFn;
}
}
// множення також тримаємо в межах 32 бітів
r = (r * ((1n - (x & 0xFFFF0000n)) & 0xFFFFFFFFn)) & 0xFFFFFFFFn;
return r;
}
func mbinLog32(r, x uint32) uint32 {
for n := uint(2); n != 16; n++ {
if x&(1<<n) != 0 {
x = x + (x << n)
r -= mbinLog32Table[n]
}
}
r -= x & 0xFFFF0000
return r
}
func mbinExp32(r, x uint32) uint32 {
for n := uint(2); n != 16; n++ {
if x&(1<<n) != 0 {
r = r + (r << n)
x -= mbinLog32Table[n]
}
}
r *= 1 - (x & 0xFFFF0000)
return r
}
Обчислення таблиці логарифмів
Щоб обчислити таблицю логарифмів, можна модифікувати алгоритм Поліга–Геллмана для випадку, коли модуль є степенем .
Наше головне завдання тут — обчислити таке, що , де , а — число вигляду .
Підносячи обидві частини до квадрата разів, дістаємо
Зауважимо, що порядок не перевищує (насправді , але для зручності триматимемося ), тож, узявши , ми матимемо в лівій частині або , або , що дозволяє визначити найменший біт , порівнявши з . Тепер припустимо, що , де — відома частина, а ще невідома. Тоді
Помноживши обидві частини на , дістаємо
Тепер, підносячи обидві частини до квадрата разів, ми можемо отримати наступний біт , зрештою відновивши всі його біти.