Skip to Content

Exponenciación binaria

La exponenciación binaria (también conocida como exponentiation by squaring) es un truco que permite calcular ana^n, donde nn es un entero no negativo, usando solo O(logn)O(\log n) multiplicaciones (en lugar de las O(n)O(n) multiplicaciones que exige el enfoque ingenuo).

También tiene aplicaciones importantes en muchas tareas que no son aritméticas, ya que se puede usar con cualquier operación que tenga la propiedad de asociatividad:

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

Lo más evidente es que esto aplica a la multiplicación modular, a la multiplicación de matrices y a otros problemas que discutiremos más abajo.

Algoritmo

Elevar aa a la potencia nn se expresa de forma ingenua como multiplicar por aa un total de n1n - 1 veces: an=aaaa^{n} = a \cdot a \cdot \ldots \cdot a. Sin embargo, este enfoque no es práctico para aa o nn grandes.

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

La idea de la exponenciación binaria es partir el trabajo usando la representación binaria del exponente.

Escribamos nn en base 2, por ejemplo:

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

Como el número nn tiene exactamente log2n+1\lfloor \log_2 n \rfloor + 1 dígitos en base 2, solo necesitamos hacer O(logn)O(\log n) multiplicaciones, si conocemos las potencias a1,a2,a4,a8,,a2log2na^1, a^2, a^4, a^8, \dots, a^{2^{\lfloor \log_2 n \rfloor}}.

Así que solo necesitamos una forma rápida de calcularlas. Por suerte es muy fácil, porque un elemento de la secuencia es simplemente el cuadrado del anterior.

31=332=(31)2=32=934=(32)2=92=8138=(34)2=812=656131amp;=332amp;=(31)2=32=934amp;=(32)2=92=8138amp;=(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}

Entonces, para obtener la respuesta final de 3133^{13}, solo hay que multiplicar tres de ellas (saltando 323^2 porque el bit correspondiente de nn no está prendido): 313=6561813=15943233^{13} = 6561 \cdot 81 \cdot 3 = 1594323

La complejidad final de este algoritmo es O(logn)O(\log n): hay que calcular logn\log n potencias de aa, y luego hacer a lo sumo logn\log n multiplicaciones para armar la respuesta final.

El siguiente enfoque recursivo expresa la misma idea:

an={1if n==0(an2)2if n>0 and n even(an12)2aif n>0 and n odda^n = {1amp;if n==0(an2)2amp;if ngt;0 and n even(an12)2aamp;if ngt;0 and n odd\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}

Implementación

Primero el enfoque recursivo, que es una traducción directa de la fórmula recursiva:

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; }

El segundo enfoque resuelve la misma tarea sin recursión. Calcula todas las potencias en un bucle y multiplica las que corresponden a un bit prendido en nn. Aunque la complejidad de ambos enfoques es idéntica, este suele ser más rápido en la práctica porque no tiene el overhead de las llamadas recursivas.

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; }

Aplicaciones

Cálculo eficiente de exponentes grandes módulo un número

Problema: Calcular xnmodmx^n \bmod m. Esta es una operación muy común. Por ejemplo se usa al calcular el inverso multiplicativo modular.

Solución: Como sabemos que el operador módulo no interfiere con las multiplicaciones (ab(amodm)(bmodm)(modm)a \cdot b \equiv (a \bmod m) \cdot (b \bmod m) \pmod m), podemos usar el mismo código y simplemente reemplazar cada multiplicación por una multiplicación modular:

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; }

Nota: Es posible acelerar este algoritmo cuando b>>mb >> m. Si mm es un número positivo y gcd(x,m)=1\gcd(x, m) = 1, entonces xnxnmod(m1)(modm)x^n \equiv x^{n \bmod (m-1)} \pmod{m} para mm primo, y xnxnmodϕ(m)(modm)x^n \equiv x^{n \bmod{\phi(m)}} \pmod{m} para mm compuesto. Esto sigue directamente del pequeño teorema de Fermat y del teorema de Euler; ver el artículo sobre inversos modulares para más detalles.

Cálculo eficiente de números de Fibonacci

Problema: Calcular el nn-ésimo número de Fibonacci FnF_n.

Solución: Para más detalles, ver el artículo de números de Fibonacci. Acá solo damos un panorama del algoritmo. Para calcular el siguiente número de Fibonacci solo hacen falta los dos anteriores, ya que Fn=Fn1+Fn2F_n = F_{n-1} + F_{n-2}. Podemos construir una matriz 2×22 \times 2 que describe esta transformación: el paso de FiF_i y Fi+1F_{i+1} a Fi+1F_{i+1} y Fi+2F_{i+2}. Por ejemplo, aplicar esta transformación al par F0F_0 y F1F_1 lo convierte en F1F_1 y F2F_2. Por lo tanto, podemos elevar esta matriz de transición a la nn-ésima potencia para encontrar FnF_n en complejidad temporal O(logn)O(\log n).

Aplicar una permutación kk veces { data-toc-label=‘Applying a permutation times’ }

Problema: Se da una secuencia de longitud nn. Aplicarle una permutación dada kk veces.

Solución: Simplemente elevar la permutación a la kk-ésima potencia usando exponenciación binaria, y después aplicarla a la secuencia. Esto da una complejidad temporal de 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; }

Nota: Esta tarea se puede resolver de forma más eficiente en tiempo lineal construyendo el grafo de la permutación y considerando cada ciclo por separado. Después se puede calcular kk módulo el tamaño del ciclo y encontrar la posición final de cada número que pertenece a ese ciclo.

Aplicación rápida de un conjunto de operaciones geométricas a un conjunto de puntos

Problema: Dados nn puntos pip_i, aplicar mm transformaciones a cada uno de esos puntos. Cada transformación puede ser una traslación, un escalado o una rotación alrededor de un eje dado por un ángulo dado. También hay una operación de “loop” que aplica una lista dada de transformaciones kk veces (las operaciones “loop” pueden anidarse). Hay que aplicar todas las transformaciones más rápido que O(nlength)O(n \cdot length), donde lengthlength es el número total de transformaciones a aplicar (después de desenrollar las operaciones “loop”).

Solución: Veamos cómo cambian las coordenadas los distintos tipos de transformaciones:

  • Traslación: suma una constante distinta a cada una de las coordenadas.
  • Escalado: multiplica cada una de las coordenadas por una constante distinta.
  • Rotación: la transformación es más complicada (no entramos en detalles acá), pero cada una de las coordenadas nuevas todavía se puede representar como combinación lineal de las viejas.

Como se ve, cada transformación se puede representar como una operación lineal sobre las coordenadas. Así, una transformación se puede escribir como una matriz 4×44 \times 4 de la forma:

(a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44)(a11amp;a12amp;a13amp;a14a21amp;a22amp;a23amp;a24a31amp;a32amp;a33amp;a34a41amp;a42amp;a43amp;a44)\begin{pmatrix} a_{11} &amp; a_ {12} &amp; a_ {13} &amp; a_ {14} \ a_{21} &amp; a_ {22} &amp; a_ {23} &amp; a_ {24} \ a_{31} &amp; a_ {32} &amp; a_ {33} &amp; a_ {34} \ a_{41} &amp; a_ {42} &amp; a_ {43} &amp; a_ {44} \end{pmatrix}

que, al multiplicarse por un vector con las coordenadas viejas y un uno, da un vector nuevo con las coordenadas nuevas y un uno:

(xyz1)(a11a12a13a14a21a22a23a24a31a32a33a34a41a42a43a44)=(xyz1)(xamp;yamp;zamp;1)\begin{pmatrix} x &amp; y &amp; z &amp; 1 \end{pmatrix} \cdot (a11amp;a12amp;a13amp;a14a21amp;a22amp;a23amp;a24a31amp;a32amp;a33amp;a34a41amp;a42amp;a43amp;a44)\begin{pmatrix} a_{11} &amp; a_ {12} &amp; a_ {13} &amp; a_ {14} \ a_{21} &amp; a_ {22} &amp; a_ {23} &amp; a_ {24} \ a_{31} &amp; a_ {32} &amp; a_ {33} &amp; a_ {34} \ a_{41} &amp; a_ {42} &amp; a_ {43} &amp; a_ {44} \end{pmatrix} = \begin{pmatrix} x&#x27; &amp; y&#x27; &amp; z&#x27; &amp; 1 \end{pmatrix}

(¿Por qué introducir una cuarta coordenada ficticia? Esa es la belleza de las coordenadas homogéneas , que tienen gran aplicación en computación gráfica. Sin esto, no sería posible implementar operaciones afines como la traslación como una sola multiplicación de matrices, porque hace falta sumar una constante a las coordenadas. ¡La transformación afín se vuelve una transformación lineal en la dimensión superior!)

Algunos ejemplos de cómo se representan las transformaciones en forma matricial:

  • Traslación: desplazar la coordenada xx en 55, la yy en 77 y la zz en 99.

(1000010000105791)(1amp;0amp;0amp;00amp;1amp;0amp;00amp;0amp;1amp;05amp;7amp;9amp;1)\begin{pmatrix} 1 &amp; 0 &amp; 0 &amp; 0 \ 0 &amp; 1 &amp; 0 &amp; 0 \ 0 &amp; 0 &amp; 1 &amp; 0 \ 5 &amp; 7 &amp; 9 &amp; 1 \end{pmatrix}

  • Escalado: escalar la coordenada xx por 1010 y las otras dos por 55.

(10000050000500001)(10amp;0amp;0amp;00amp;5amp;0amp;00amp;0amp;5amp;00amp;0amp;0amp;1)\begin{pmatrix} 10 &amp; 0 &amp; 0 &amp; 0 \ 0 &amp; 5 &amp; 0 &amp; 0 \ 0 &amp; 0 &amp; 5 &amp; 0 \ 0 &amp; 0 &amp; 0 &amp; 1 \end{pmatrix}

  • Rotación: rotar θ\theta grados alrededor del eje xx siguiendo la regla de la mano derecha (sentido antihorario).

(10000cosθsinθ00sinθcosθ00001)(1amp;0amp;0amp;00amp;cosθamp;sinθamp;00amp;sinθamp;cosθamp;00amp;0amp;0amp;1)\begin{pmatrix} 1 &amp; 0 &amp; 0 &amp; 0 \ 0 &amp; \cos \theta &amp; -\sin \theta &amp; 0 \ 0 &amp; \sin \theta &amp; \cos \theta &amp; 0 \ 0 &amp; 0 &amp; 0 &amp; 1 \end{pmatrix}

Ahora, una vez que cada transformación está descrita como una matriz, la secuencia de transformaciones se describe como un producto de esas matrices, y un “loop” de kk repeticiones se describe como la matriz elevada a la potencia kk (que se puede calcular con exponenciación binaria en O(logk)O(\log{k})). Así, la matriz que representa todas las transformaciones se puede calcular primero en O(mlogk)O(m \log{k}), y después aplicarse a cada uno de los nn puntos en O(n)O(n) para una complejidad total de O(n+mlogk)O(n + m \log{k}).

Número de caminos de longitud kk en un grafo { data-toc-label=‘Number of paths of length in a graph’ }

Problema: Dado un grafo dirigido no ponderado de nn vértices, encontrar la cantidad de caminos de longitud kk de cualquier vértice uu a cualquier otro vértice vv.

Solución: Este problema se trata con más detalle en un artículo separado. El algoritmo consiste en elevar la matriz de adyacencia MM del grafo (una matriz donde mij=1m_{ij} = 1 si hay una arista de ii a jj, o 00 en caso contrario) a la kk-ésima potencia. Ahora mijm_{ij} será la cantidad de caminos de longitud kk de ii a jj. La complejidad temporal de esta solución es O(n3logk)O(n^3 \log k).

Nota: En ese mismo artículo se considera otra variante: cuando las aristas están ponderadas y hay que encontrar el camino de peso mínimo que contiene exactamente kk aristas. Como se muestra allí, este problema también se resuelve por exponenciación de la matriz de adyacencia. La matriz tendría el peso de la arista de ii a jj, o \infty si no existe esa arista. En lugar de la operación usual de multiplicar dos matrices, hay que usar una modificada: en lugar de multiplicación se suman ambos valores, y en lugar de una sumatoria se toma un mínimo. Es decir: resultij=min1  k  n(aik+bkj)result_{ij} = \min\limits_{1\ \leq\ k\ \leq\ n}(a_{ik} + b_{kj}).

Variante de la exponenciación binaria: multiplicar dos números módulo mm { data-toc-label=‘Variation of binary exponentiation: multiplying two numbers modulo ’ }

Problema: Multiplicar dos números aa y bb módulo mm. aa y bb entran en los tipos de datos nativos, pero su producto es demasiado grande para entrar en un entero de 64 bits. La idea es calcular ab(modm)a \cdot b \pmod m sin usar aritmética de bignums.

Solución: Simplemente aplicamos el algoritmo de construcción binaria descrito arriba, pero haciendo sumas en lugar de multiplicaciones. En otras palabras, “expandimos” la multiplicación de dos números a O(logm)O (\log m) operaciones de suma y de multiplicación por dos (que, en esencia, es una suma).

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

Nota: Se puede resolver esta tarea de otra forma usando operaciones de punto flotante. Primero se calcula la expresión abm\frac{a \cdot b}{m} con números de punto flotante y se la castea a un entero sin signo qq. Se resta qmq \cdot m de aba \cdot b usando aritmética de enteros sin signo y se toma módulo mm para encontrar la respuesta. Esta solución parece poco confiable, pero es muy rápida y muy fácil de implementar. Ver aquí  para más información.

Problemas de práctica