← Todos los algoritmos

Fast Exponentiation

Matemáticas · ver código fuente en GitHub

Leer en: English · Português · Español

Categoría: Math

El problema

Elevar un número a una potencia entera. Multiplicar la base por sí misma una vez por cada unidad del exponente — la lectura directa de lo que "exponente" siquiera significa — es O(exponente). Para un exponente grande, eso es una gran cantidad de multiplicaciones para lo que es, matemáticamente, una cantidad mucho menor de información genuinamente nueva.

La solución

Exponenciación por cuadrados: base^n es igual a (base^2)^(n/2) siempre que n sea par, así que elevar la base al cuadrado mientras se reduce el exponente a la mitad llega al mismo resultado — y esa reducción a la mitad se acumula en cada paso, la misma forma de "duplicación en reversa" que Binary Search explota en un array ordenado. Para un exponente impar, primero se extrae un factor de base (multiplicado directamente en el resultado acumulado) para que el exponente restante vuelva a ser par y la reducción a la mitad pueda continuar. Leer los bits del exponente de menos significativo a más significativo — elevando la base al cuadrado una vez por bit, e incorporándola al resultado exactamente cuando ese bit está activo — convierte todo el cálculo en O(log exponente) multiplicaciones en lugar de O(exponente).

flowchart LR
    A["2^10, binary 1010"] --> B["bit 0 (LSB) = 0 → skip; square base to 2^2=4"]
    B --> C["bit 1 = 1 → result *= 4; square base to 4^2=16"]
    C --> D["bit 2 = 0 → skip; square base to 16^2=256"]
    D --> E["bit 3 = 1 → result *= 256 → 4 * 256 = 1024"]

Ejemplo clásico

classic/FastExponentiation implementa power con el bucle de elevación al cuadrado bit a bit descrito arriba, tratando los exponentes negativos como el recíproco del resultado con exponente positivo, más bruteForcePower (una multiplicación por unidad de exponente) incluido específicamente para el benchmark de abajo. FastExponentiationTest usa 2^10 = 1024 específicamente porque 10 en binario es 1010 — una mezcla de bits activos e inactivos en un solo caso — más la identidad de exponente cero, cero elevado a una potencia positiva, exponentes negativos, la fuerza bruta coincidiendo con la exponenciación rápida, y la protección contra el único caso genuinamente indefinido (cero elevado a una potencia negativa).

Ejemplo aplicado: proyección de reserva actuarial de aseguradora

applied/CompoundGrowthCalculator proyecta el valor acumulado de una reserva actuarial después de muchos períodos de capitalización — principal * (1 + periodicRate)^periods — el mismo cálculo de factor de crecimiento, ya sean los períodos meses en una proyección de reserva o años en un cronograma de anualidad a largo plazo. CompoundGrowthCalculatorTest verifica una proyección de tres períodos contra la fórmula de interés compuesto calculada a mano (R$1,000 al 5% por 3 períodos → R$1,157.625), las identidades de período cero y principal cero, y las protecciones (principal negativo, una tasa periódica igual o por debajo de -100%, períodos negativos).

Benchmark

./gradlew :math:fast-exponentiation:jmh

Ejecución real en esta máquina (JMH 1.37, JDK 26.0.2, 2 iteraciones de calentamiento + 3 de medición, 1 fork):

Costo exponent=10,000 exponent=1,000,000 exponent=100,000,000
fast exponentiation 0.015 µs 0.021 µs 0.031 µs
fuerza bruta 16.52 µs 1,643.86 µs 164,002.01 µs

La fuerza bruta siguió su predicción O(exponente) casi con exactitud: un aumento de 100x en el exponente produjo un aumento de 99.5x en el costo (10,000 → 1,000,000), y luego un aumento de 99.8x en el siguiente paso de 100x (1,000,000 → 100,000,000) — una confirmación lineal tan limpia como cualquiera capturada en este repositorio. La exponenciación rápida apenas se movió (1.4x, luego 1.48x) — coincidiendo también de cerca con la predicción O(log n): log2(1,000,000) / log2(10,000) ≈ 1.5, y log2(100,000,000) / log2(1,000,000) ≈ 1.33. En exponent=100,000,000, la fuerza bruta es ~5,290,387x más lenta que la exponenciación rápida para la misma respuesta.

Cuándo no usarlo

Cobertura de pruebas

100% de cobertura de instrucciones, 100% de cobertura de ramas (JaCoCo). Reprodúcelo tú mismo:

./gradlew :math:fast-exponentiation:jacocoTestReport

Reporte en math/fast-exponentiation/build/reports/jacoco/test/html/index.html.

Lectura complementaria

Pruebas unitarias

src/test/java/com/algorithms/math/fastexponentiation/classic/FastExponentiationTest.java
package com.algorithms.math.fastexponentiation.classic;

import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class FastExponentiationTest {

    private static final Offset<Double> TOLERANCE = Offset.offset(1e-9);

    @Test
    void raisesAnEvenAndOddMixOfExponentBitsCorrectly() {
        // 10 in binary is 1010 - exercises both the "bit set" and "bit unset" branches.
        assertThat(FastExponentiation.power(2, 10)).isCloseTo(1024.0, TOLERANCE);
    }

    @Test
    void anyBaseToTheZerothPowerIsOne() {
        assertThat(FastExponentiation.power(7, 0)).isCloseTo(1.0, TOLERANCE);
    }

    @Test
    void zeroToAPositivePowerIsZero() {
        assertThat(FastExponentiation.power(0, 3)).isCloseTo(0.0, TOLERANCE);
    }

    @Test
    void aNegativeExponentIsTheReciprocal() {
        assertThat(FastExponentiation.power(2, -2)).isCloseTo(0.25, TOLERANCE);
    }

    @Test
    void bruteForceAgreesWithFastExponentiation() {
        assertThat(FastExponentiation.bruteForcePower(2, 10))
                .isCloseTo(FastExponentiation.power(2, 10), TOLERANCE);
        assertThat(FastExponentiation.bruteForcePower(2, -2))
                .isCloseTo(FastExponentiation.power(2, -2), TOLERANCE);
    }

    @Test
    void rejectsZeroRaisedToANegativeExponent() {
        assertThatThrownBy(() -> FastExponentiation.power(0, -1)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> FastExponentiation.bruteForcePower(0, -1)).isInstanceOf(IllegalArgumentException.class);
    }
}
src/test/java/com/algorithms/math/fastexponentiation/applied/CompoundGrowthCalculatorTest.java
package com.algorithms.math.fastexponentiation.applied;

import org.assertj.core.data.Offset;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class CompoundGrowthCalculatorTest {

    private static final Offset<Double> TOLERANCE = Offset.offset(1e-9);

    private final CompoundGrowthCalculator calculator = new CompoundGrowthCalculator();

    @Test
    void projectsAReserveOverThreeCompoundingPeriods() {
        // R$1,000 at 5% per period for 3 periods: 1000 * 1.05^3 = 1157.625.
        assertThat(calculator.accumulatedValue(1_000, 0.05, 3)).isCloseTo(1157.625, TOLERANCE);
    }

    @Test
    void zeroPeriodsLeavesThePrincipalUnchanged() {
        assertThat(calculator.accumulatedValue(1_000, 0.05, 0)).isCloseTo(1000.0, TOLERANCE);
    }

    @Test
    void zeroPrincipalStaysZero() {
        assertThat(calculator.accumulatedValue(0, 0.05, 10)).isCloseTo(0.0, TOLERANCE);
    }

    @Test
    void rejectsANegativePrincipal() {
        assertThatThrownBy(() -> calculator.accumulatedValue(-1, 0.05, 3)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsAPeriodicRateOfMinusOneHundredPercentOrWorse() {
        assertThatThrownBy(() -> calculator.accumulatedValue(1_000, -1.0, 3)).isInstanceOf(IllegalArgumentException.class);
        assertThatThrownBy(() -> calculator.accumulatedValue(1_000, -1.5, 3)).isInstanceOf(IllegalArgumentException.class);
    }

    @Test
    void rejectsANegativePeriodCount() {
        assertThatThrownBy(() -> calculator.accumulatedValue(1_000, 0.05, -1)).isInstanceOf(IllegalArgumentException.class);
    }
}

Ver informe completo de cobertura JaCoCo →