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

Бінарне піднесення до степеня

Бінарне піднесення до степеня (також відоме як піднесення до степеня через квадрати) — це прийом, який дозволяє обчислити ana^n лише за O(logn)O(\log n) множень (замість O(n)O(n) множень, потрібних для наївного підходу).

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

(XY)Z=X(YZ)(X \cdot Y) \cdot Z = X \cdot (Y \cdot Z)

Найочевидніше це стосується множення за модулем, множення матриць та інших задач, які ми обговоримо нижче.

Коли підходить цей алгоритм?
  • Чи потрібно обчислити ana^n (або застосувати ту саму операцію nn разів) за великого nn, де лінійні O(n)O(n) множень не вкладаються в ліміт часу?
  • Чи операція, яку ви повторюєте, асоціативна (множення за модулем, множення матриць, композиція перетворень)? (якщо ні → бінарне піднесення незастосовне)
  • Якщо це множення матриць для лінійної рекурентності — чи розмір матриці малий, щоб O(k3logn)O(k^3 \log n) було прийнятним?

Алгоритм

Піднесення aa до степеня nn наївно виражається як множення на aa, виконане n1n - 1 разів: an=aaaa^{n} = a \cdot a \cdot \ldots \cdot a. Однак цей підхід непрактичний для великих aa чи nn.

ab+c=abaca^{b+c} = a^b \cdot a^c та a2b=abab=(ab)2a^{2b} = a^b \cdot a^b = (a^b)^2.

Ідея бінарного піднесення до степеня в тому, що ми розбиваємо роботу, використовуючи двійкове представлення показника.

Запишемо nn у системі числення за основою 2, наприклад:

313=311012=3834313^{13} = 3^{1101_2} = 3^8 \cdot 3^4 \cdot 3^1

Оскільки число nn має рівно log2n+1\lfloor \log_2 n \rfloor + 1 цифр у двійковій системі, нам потрібно виконати лише O(logn)O(\log n) множень, якщо ми знаємо степені a1,a2,a4,a8,,a2log2na^1, a^2, a^4, a^8, \dots, a^{2^{\lfloor \log_2 n \rfloor}}.

Отже, нам потрібно лише знати швидкий спосіб їх обчислити. На щастя, це дуже просто, оскільки кожен елемент послідовності — це просто квадрат попереднього елемента.

31=332=(31)2=32=934=(32)2=92=8138=(34)2=812=6561\begin{align} 3^1 &= 3 \\ 3^2 &= \left(3^1\right)^2 = 3^2 = 9 \\ 3^4 &= \left(3^2\right)^2 = 9^2 = 81 \\ 3^8 &= \left(3^4\right)^2 = 81^2 = 6561 \end{align}

Отже, щоб отримати остаточну відповідь для 3133^{13}, нам потрібно перемножити лише три з них (пропускаючи 323^2, бо відповідний біт у nn не встановлений): 313=6561813=15943233^{13} = 6561 \cdot 81 \cdot 3 = 1594323

Остаточна складність цього алгоритму — O(logn)O(\log n): нам треба обчислити logn\log n степенів aa, а потім виконати щонайбільше logn\log n множень, щоб отримати з них остаточну відповідь.

Наступний рекурсивний підхід виражає ту саму ідею:

an={1if n==0(an2)2if n>0 and n even(an12)2aif n>0 and n odda^n = \begin{cases} 1 &\text{if } n == 0 \\ \left(a^{\frac{n}{2}}\right)^2 &\text{if } n > 0 \text{ and } n \text{ even}\\ \left(a^{\frac{n - 1}{2}}\right)^2 \cdot a &\text{if } n > 0 \text{ and } n \text{ odd}\\ \end{cases}

Реалізація

Спочатку рекурсивний підхід, який є прямим перекладом рекурсивної формули:

long long binpow(long long a, long long b) {
if (b == 0)
return 1;
long long res = binpow(a, b / 2);
if (b % 2)
return res * res * a;
else
return res * res;
}

Другий підхід виконує те саме завдання без рекурсії. Він обчислює всі степені в циклі та перемножує ті з них, які мають відповідний одиничний біт у nn. Хоча складність обох підходів однакова, цей підхід буде швидшим на практиці, оскільки в нас немає накладних витрат на рекурсивні виклики.

long long binpow(long long a, long long b) {
long long res = 1;
while (b > 0) {
if (b & 1)
res = res * a;
a = a * a;
b >>= 1;
}
return res;
}

Застосування

Ефективне обчислення великих степенів за модулем числа

Задача: Обчислити xnmodmx^n \bmod m. Це дуже поширена операція. Наприклад, вона використовується при обчисленні оберненого елемента за модулем.

Розв'язок: Оскільки ми знаємо, що операція взяття за модулем не заважає множенням (ab(amodm)(bmodm)(modm)a \cdot b \equiv (a \bmod m) \cdot (b \bmod m) \pmod m), ми можемо безпосередньо використати той самий код, лише замінивши кожне множення на множення за модулем:

long long binpow(long long a, long long b, long long m) {
a %= m;
long long res = 1;
while (b > 0) {
if (b & 1)
res = res * a % m;
a = a * a % m;
b >>= 1;
}
return res;
}

Зауваження: Цей алгоритм можна прискорити для великих b>>mb >> m. Якщо mm — додатне число і gcd(x,m)=1\gcd(x, m) = 1, то xnxnmod(m1)(modm)x^n \equiv x^{n \bmod (m-1)} \pmod{m} для простого mm і xnxnmodϕ(m)(modm)x^n \equiv x^{n \bmod{\phi(m)}} \pmod{m} для складеного mm. Це безпосередньо випливає з малої теореми Ферма та теореми Ейлера, докладніше див. статтю про обернені елементи за модулем.

Ефективне обчислення чисел Фібоначчі

Задача: Обчислити nn-те число Фібоначчі FnF_n.

Розв'язок: Докладніше див. статтю про числа Фібоначчі. Ми лише оглянемо алгоритм у загальних рисах. Щоб обчислити наступне число Фібоначчі, потрібні лише два попередні, оскільки Fn=Fn1+Fn2F_n = F_{n-1} + F_{n-2}. Ми можемо побудувати матрицю 2×22 \times 2, яка описує це перетворення: перехід від FiF_i та Fi+1F_{i+1} до Fi+1F_{i+1} та Fi+2F_{i+2}. Наприклад, застосування цього перетворення до пари F0F_0 та F1F_1 змінить її на F1F_1 та F2F_2. Тому ми можемо піднести цю матрицю перетворення до nn-го степеня, щоб знайти FnF_n з часовою складністю O(logn)O(\log n).

Застосування перестановки kk разів

Задача: Вам дано послідовність довжини nn. Застосуйте до неї задану перестановку kk разів.

Розв'язок: Просто піднесіть перестановку до kk-го степеня за допомогою бінарного піднесення до степеня, а потім застосуйте її до послідовності. Це дасть вам часову складність O(nlogk)O(n \log k).

vector<int> applyPermutation(vector<int> sequence, vector<int> permutation) {
vector<int> newSequence(sequence.size());
for(int i = 0; i < sequence.size(); i++) {
newSequence[i] = sequence[permutation[i]];
}
return newSequence;
}

vector<int> permute(vector<int> sequence, vector<int> permutation, long long k) {
while (k > 0) {
if (k & 1) {
sequence = applyPermutation(sequence, permutation);
}
permutation = applyPermutation(permutation, permutation);
k >>= 1;
}
return sequence;
}

Зауваження: Цю задачу можна розв'язати ефективніше за лінійний час, побудувавши граф перестановки і розглянувши кожен цикл окремо. Тоді можна обчислити kk за модулем розміру циклу і знайти остаточну позицію для кожного числа, яке є частиною цього циклу.

Швидке застосування набору геометричних операцій до набору точок

Задача: Дано nn точок pip_i, застосуйте mm перетворень до кожної з цих точок. Кожне перетворення може бути зсувом, масштабуванням або поворотом навколо заданої осі на заданий кут. Існує також операція «цикл», яка застосовує заданий список перетворень kk разів (операції «цикл» можуть бути вкладеними). Ви маєте застосувати всі перетворення швидше, ніж за O(nlength)O(n \cdot length), де lengthlength — загальна кількість перетворень, які треба застосувати (після розгортання операцій «цикл»).

Розв'язок: Подивимося, як різні типи перетворень змінюють координати:

  • Операція зсуву: додає до кожної з координат свою константу.
  • Операція масштабування: множить кожну з координат на свою константу.
  • Операція повороту: перетворення складніше (тут ми не вдаватимемося в деталі), але кожну з нових координат усе одно можна подати як лінійну комбінацію старих.

Як бачите, кожне з перетворень можна подати як лінійну операцію над координатами. Таким чином, перетворення можна записати як матрицю 4×44 \times 4 вигляду:

(a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44)\begin{pmatrix} a_{11} & a_ {12} & a_ {13} & a_ {14} \\ a_{21} & a_ {22} & a_ {23} & a_ {24} \\ a_{31} & a_ {32} & a_ {33} & a_ {34} \\ a_{41} & a_ {42} & a_ {43} & a_ {44} \end{pmatrix}

яка при множенні на вектор зі старими координатами та одиницею дає новий вектор з новими координатами та одиницею:

(xyz1)(a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44)=(xyz1)\begin{pmatrix} x & y & z & 1 \end{pmatrix} \cdot \begin{pmatrix} a_{11} & a_ {12} & a_ {13} & a_ {14} \\ a_{21} & a_ {22} & a_ {23} & a_ {24} \\ a_{31} & a_ {32} & a_ {33} & a_ {34} \\ a_{41} & a_ {42} & a_ {43} & a_ {44} \end{pmatrix} = \begin{pmatrix} x' & y' & z' & 1 \end{pmatrix}

(Чому ми вводимо фіктивну четверту координату, спитаєте ви? У цьому й полягає краса однорідних координат, які знаходять чудове застосування в комп'ютерній графіці. Без цього було б неможливо реалізувати афінні операції на кшталт операції зсуву як одне множення матриць, оскільки воно вимагає додати константу до координат. Афінне перетворення стає лінійним перетворенням у вищій вимірності!)

Ось кілька прикладів того, як перетворення подаються в матричній формі:

  • Операція зсуву: зсунути координату xx на 55, координату yy на 77 і координату zz на 99.
(1000010000105791)\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & 1 & 0 \\ 5 & 7 & 9 & 1 \end{pmatrix}
  • Операція масштабування: масштабувати координату xx на 1010, а дві інші — на 55.
(10000050000500001)\begin{pmatrix} 10 & 0 & 0 & 0 \\ 0 & 5 & 0 & 0 \\ 0 & 0 & 5 & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}
  • Операція повороту: повернути на θ\theta градусів навколо осі xx за правилом правої руки (проти годинникової стрілки).
(10000cosθsinθ00sinθcosθ00001)\begin{pmatrix} 1 & 0 & 0 & 0 \\ 0 & \cos \theta & -\sin \theta & 0 \\ 0 & \sin \theta & \cos \theta & 0 \\ 0 & 0 & 0 & 1 \end{pmatrix}

Тепер, коли кожне перетворення описане як матриця, послідовність перетворень можна описати як добуток цих матриць, а «цикл» із kk повторень можна описати як матрицю, піднесену до степеня kk (що можна обчислити за допомогою бінарного піднесення до степеня за O(logk)O(\log{k})). Таким чином, матрицю, яка представляє всі перетворення, можна спочатку обчислити за O(mlogk)O(m \log{k}), а потім застосувати її до кожної з nn точок за O(n)O(n), що дає загальну складність O(n+mlogk)O(n + m \log{k}).

Кількість шляхів довжини kk у графі

Задача: Дано орієнтований незважений граф з nn вершин, знайти кількість шляхів довжини kk з будь-якої вершини uu до будь-якої іншої вершини vv.

Розв'язок: Ця задача розглядається докладніше в окремій статті. Алгоритм полягає в піднесенні матриці суміжності MM графа (матриці, де mij=1m_{ij} = 1, якщо є ребро з ii до jj, або 00 в іншому випадку) до kk-го степеня. Тоді mijm_{ij} буде кількістю шляхів довжини kk з ii до jj. Часова складність цього розв'язку — O(n3logk)O(n^3 \log k).

Зауваження: У тій самій статті розглядається ще одна варіація цієї задачі: коли ребра зважені й потрібно знайти шлях мінімальної ваги, що містить рівно kk ребер. Як показано в тій статті, ця задача також розв'язується піднесенням матриці суміжності до степеня. Матриця міститиме вагу ребра з ii до jj, або \infty, якщо такого ребра немає. Замість звичайної операції множення двох матриць слід використати модифіковану: замість множення обидва значення додаються, а замість підсумовування береться мінімум. Тобто: resultij=min1  k  n(aik+bkj)result_{ij} = \min\limits_{1\ \leq\ k\ \leq\ n}(a_{ik} + b_{kj}).

Варіація бінарного піднесення до степеня: множення двох чисел за модулем mm

Задача: Перемножити два числа aa та bb за модулем mm. aa і bb вміщуються у вбудовані типи даних, але їхній добуток завеликий, щоб уміститися в 64-бітне ціле число. Ідея полягає в тому, щоб обчислити ab(modm)a \cdot b \pmod m без використання довгої арифметики.

Розв'язок: Ми просто застосовуємо описаний вище алгоритм бінарної побудови, лише виконуючи додавання замість множення. Іншими словами, ми «розклали» множення двох чисел на O(logm)O (\log m) операцій додавання та множення на два (яке, по суті, є додаванням).

ab={0if a=02a2bif a>0 and a even2a12b+bif a>0 and a odda \cdot b = \begin{cases} 0 &\text{if }a = 0 \\ 2 \cdot \frac{a}{2} \cdot b &\text{if }a > 0 \text{ and }a \text{ even} \\ 2 \cdot \frac{a-1}{2} \cdot b + b &\text{if }a > 0 \text{ and }a \text{ odd} \end{cases}

Зауваження: Цю задачу можна розв'язати іншим способом, використовуючи операції з рухомою комою. Спочатку обчисліть вираз abm\frac{a \cdot b}{m} за допомогою чисел з рухомою комою і зведіть його до беззнакового цілого qq. Відніміть qmq \cdot m від aba \cdot b за допомогою беззнакової цілочисельної арифметики і візьміть це за модулем mm, щоб знайти відповідь. Цей розв'язок виглядає доволі ненадійним, але він дуже швидкий і дуже простий у реалізації. Докладніше див. тут.

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

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