Бинарное возведение в степень — приём, позволяющий возводить любое число в -ую степень за умножений (вместо умножений при обычном подходе).
#Основная идея
Заметим, что для любого числа и чётного числа выполняется тождество:
Получается, что для любого чётного мы можем свести задачу к вдвое меньшей (), потратив всего одну операцию умножения.
Если же нечётно, то верно следующее:
Получается, для нечетных мы можем сделать сведение к задаче размера , которая уже будет чётной.
Мы фактически нашли рекуррентную формулу для подсчета :
Так как каждые два перехода гарантированно уменьшается хотя бы в два раза, всего будет не более переходов, прежде чем мы придём к . Каждый переход требует ровно одно умножение, и таким образом, мы получили алгоритм, работающий за умножений.
#Реализация
Сразу напрашивается рекурсивная реализация этого алгоритма:
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;
}
}
Эта реализация рекурсивная, что работает долго. Попробуем «развернуть» рекурсию и получить итеративную.
Рассмотрим двоичное представление числа . Результат можно представить как произведение в степенях каких-то степеней двоек. Например, если , то
Чтобы посчитать это произведение итеративно, пройдемся по всем битам числа , поддерживая две переменные: непосредственно результат и текущее значение , где это номер текущей итерации. На каждом шаге будем домножать на текущий результат, если -тый бит числа единичный, и в любом случае возводить её в квадрат, получая для следующей итерации.
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
из стандартной библиотеки реализована только для действительных чисел и использует приближенные методы, и поэтому не дает точных результатов для целочисленных аргументов.
#Обобщения
Бинарное возведение в степень применимо не только к умножению чисел, а вообще к любым ассоциативным операциям — таким, для любых , и выполняется:
Наиболее очевидное обобщение — на остатки по модулю:
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;
}
Вторым самым «популярным» обобщением является произведение матриц, про которое мы подробнее поговорим в следующей статье.
#Геометрические прогрессии
Возводить какие-то объекты в степень часто нужно в контексте комбинаторики, и помимо этого, иногда требуется считать суммы подобного вида:
В простом случае это сумма геометрической прогрессии, и алгебраически она равна , так что можно просто посчитать бинарным возведением в степень, а затем провести деление на . Однако в общем случае так сделать нельзя — например, хотя бы потому, что не по всем модулям будет существовать обратное. Поэтому бинарное возведение в степень для таких формул нужно несколько модифицировать: В самом алгоритме нужно помимо степеней поддерживать ещё и суммы всех степеней от до .