Бинарное возведение в степень - Алгоритмика
Бинарное возведение в степень

Бинарное возведение в степень

авторы Сергей Слотин Максим Иванов

Бинарное возведение в степень — приём, позволяющий возводить любое число в nn-ую степень за O(logn)O(\log n) умножений (вместо nn умножений при обычном подходе).

#Основная идея

Заметим, что для любого числа aa и чётного числа nn выполняется тождество:

an=(an/2)2=an/2an/2 a^n = (a^{n/2})^2 = a^{n/2} \cdot a^{n/2}

Получается, что для любого чётного nn мы можем свести задачу к вдвое меньшей (n=n2n’ = \frac{n}{2}), потратив всего одну операцию умножения.

Если же nn нечётно, то верно следующее:

an=an1a a^n = a^{n-1} \cdot a

Получается, для нечетных nn мы можем сделать сведение к задаче размера (n1)(n-1), которая уже будет чётной.

Мы фактически нашли рекуррентную формулу для подсчета ana^n:

an=f(a,n)={1,n=0f(a,n2)2,2nf(a,n1)a,2n 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}

Так как каждые два перехода nn гарантированно уменьшается хотя бы в два раза, всего будет не более 2log2n2 \cdot \log_2 n переходов, прежде чем мы придём к n=0n = 0. Каждый переход требует ровно одно умножение, и таким образом, мы получили алгоритм, работающий за O(logn)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;
    }
}

Эта реализация рекурсивная, что работает долго. Попробуем «развернуть» рекурсию и получить итеративную.

Рассмотрим двоичное представление числа nn. Результат ana^n можно представить как произведение aa в степенях каких-то степеней двоек. Например, если n=42=32+8+2n = 42 = 32 + 8 + 2, то

a42=a32+8+2=a32a8a2 a^{42} = a^{32+8+2} = a^{32} \cdot a^8 \cdot a^2

Чтобы посчитать это произведение итеративно, пройдемся по всем битам числа nn, поддерживая две переменные: непосредственно результат и текущее значение a2ka^{2^k}, где kk это номер текущей итерации. На каждом шаге будем домножать a2ka^{2^k} на текущий результат, если kk-тый бит числа nn единичный, и в любом случае возводить её в квадрат, получая a2k2=a2k+1a^{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 из стандартной библиотеки реализована только для действительных чисел и использует приближенные методы, и поэтому не дает точных результатов для целочисленных аргументов.

#Обобщения

Бинарное возведение в степень применимо не только к умножению чисел, а вообще к любым ассоциативным операциям — таким, для любых aa, bb и cc выполняется:

(ab)c=a(bc) (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+a2+an1 S = 1 + a + a^2 + \ldots a^{n-1} В простом случае это сумма геометрической прогрессии, и алгебраически она равна 1an1a\frac{1-a^n}{1-a}, так что можно просто посчитать ana^n бинарным возведением в степень, а затем провести деление (1an)(1-a^n) на (1a)(1-a). Однако в общем случае так сделать нельзя — например, хотя бы потому, что не по всем модулям будет существовать обратное. Поэтому бинарное возведение в степень для таких формул нужно несколько модифицировать: f(a,n)=1+a+a2+an1=(1+a++an/2)+an/2(1+a+an/2)=(1+an/2)f(a,n) \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} В самом алгоритме нужно помимо степеней aa поддерживать ещё и суммы всех степеней от 11 до aka^k.