Бинарное возведение в степень — приём, позволяющий возводить любое число в $n$-ую степень за $O(\log n)$ умножений (вместо $n$ умножений при обычном подходе).
#Основная идея
Заметим, что для любого числа $a$ и чётного числа $n$ выполняется тождество:
$$ a^n = (a^{n/2})^2 = a^{n/2} \cdot a^{n/2} $$Получается, что для любого чётного $n$ мы можем свести задачу к вдвое меньшей ($n’ = \frac{n}{2}$), потратив всего одну операцию умножения.
Если же $n$ нечётно, то верно следующее:
$$ a^n = a^{n-1} \cdot a $$Получается, для нечетных $n$ мы можем сделать сведение к задаче размера $(n-1)$, которая уже будет чётной.
Мы фактически нашли рекуррентную формулу для подсчета $a^n$:
$$ a^n = f(a, n) = \begin{cases} 1, && n = 0 \\ f(a, \frac{n}{2})^2, && 2 \mid n \\ f(a, n - 1) \cdot a, && 2 \nmid n \end{cases} $$Так как каждые два перехода $n$ гарантированно уменьшается хотя бы в два раза, всего будет не более $2 \cdot \log_2 n$ переходов, прежде чем мы придём к $n = 0$. Каждый переход требует ровно одно умножение, и таким образом, мы получили алгоритм, работающий за $O(\log n)$ умножений.
#Реализация
Сразу напрашивается рекурсивная реализация этого алгоритма:
int binpow(int a, int n) {
if (n == 0)
return 1;
if (n % 2 == 1)
return binpow(a, n - 1) * a;
else {
int b = binpow(a, n / 2);
return b * b;
}
}
Эта реализация рекурсивная, что работает долго. Попробуем «развернуть» рекурсию и получить итеративную.
Рассмотрим двоичное представление числа $n$. Результат $a^n$ можно представить как произведение $a$ в степенях каких-то степеней двоек. Например, если $n = 42 = 32 + 8 + 2$, то
$$ a^{42} = a^{32+8+2} = a^{32} \cdot a^8 \cdot a^2 $$Чтобы посчитать это произведение итеративно, пройдемся по всем битам числа $n$, поддерживая две переменные: непосредственно результат и текущее значение $a^{2^k}$, где $k$ это номер текущей итерации. На каждом шаге будем домножать $a^{2^k}$ на текущий результат, если $k$-тый бит числа $n$ единичный, и в любом случае возводить её в квадрат, получая $a^{2^k \cdot 2} = a^{2^{k+1}}$ для следующей итерации.
int binpow(int a, int n) {
int res = 1;
while (n != 0) {
if (n & 1)
res = res * a;
a = a * a;
n >>= 1;
}
return res;
}
Стоит отметить, что во многих языках программирования бинарное возведение в степень уже реализовано. Но не в C++: функция pow
из стандартной библиотеки реализована только для действительных чисел и использует приближенные методы, и поэтому не дает точных результатов для целочисленных аргументов.
#Обобщения
Бинарное возведение в степень применимо не только к умножению чисел, а вообще к любым ассоциативным операциям — таким, для любых $a$, $b$ и $c$ выполняется:
$$ (a \circ b) \circ c = a \circ (b \circ c) $$Наиболее очевидное обобщение — на остатки по модулю:
const int mod = 1e9 + 7;
long long binpow(long long a, int n) {
long long res = 1;
while (n != 0) {
if (n & 1)
res = res * a % mod;
a = a * a % mod;
n >>= 1;
}
return res;
}
Вторым самым «популярным» обобщением является произведение матриц, про которое мы подробнее поговорим в следующей статье.
#Геометрические прогрессии
Возводить какие-то объекты в степень часто нужно в контексте комбинаторики, и помимо этого, иногда требуется считать суммы подобного вида:
$$ S = 1 + a + a^2 + \ldots a^{n-1} $$ В простом случае это сумма геометрической прогрессии, и алгебраически она равна $\frac{1-a^n}{1-a}$, так что можно просто посчитать $a^n$ бинарным возведением в степень, а затем провести деление $(1-a^n)$ на $(1-a)$. Однако в общем случае так сделать нельзя — например, хотя бы потому, что не по всем модулям будет существовать обратное. Поэтому бинарное возведение в степень для таких формул нужно несколько модифицировать: $$ \begin{aligned} f(a, n) &= 1 + a + a^2 + \ldots a^{n-1} \\ &= (1 + a + \ldots + a^{n/2}) + a^{n/2} \cdot (1 + a + \ldots a^{n/2}) \\ &= (1 + a^{n/2}) \cdot f(a, n) \end{aligned} $$ В самом алгоритме нужно помимо степеней $a$ поддерживать ещё и суммы всех степеней от $1$ до $a^k$.