26. Operaciones en aritmética modular

La aritmética modular permite sumar, restar, multiplicar y elevar potencias manteniendo los resultados dentro de un conjunto finito de residuos. Sus reglas hacen posibles cálculos eficientes en ciclos, validaciones y criptografía.

26.1 Introducción

Una vez que sabemos que dos enteros pueden ser congruentes, podemos operar con sus clases de residuos. La idea práctica es simple: realizar una operación y conservar solamente el resto al dividir por el módulo.

Sin embargo, esta simplicidad tiene una excepción importante: la división modular no funciona como la división ordinaria. Entender qué operaciones siempre son válidas y cuándo existe un inverso evita muchos errores al programar.

26.2 Residuos como representantes

Módulo m, cada entero puede reemplazarse por uno de los residuos 0, 1, ..., m - 1. Por ejemplo, módulo 7 podemos reemplazar 31 por 3, porque 31 ≡ 3 (mod 7).

31 mod 7 = 3.
-4 mod 7 = 3.
52 mod 7 = 3.

31, -4 y 52 pertenecen a la misma clase módulo 7.

Elegir el residuo canónico facilita la lectura de los cálculos, pero cualquier número congruente sirve como representante de la misma clase.

26.3 Definición de las operaciones modulares

Para residuos a y b módulo m, las operaciones se definen reduciendo el resultado:

Suma: (a + b) mod m.
Resta: (a - b) mod m.
Producto: (a · b) mod m.
Potencia: an mod m.

Estas definiciones son consistentes: si cambiamos a o b por números congruentes, el residuo final no cambia. Esa propiedad permite reducir durante el cálculo, no solo al final.

26.4 Suma modular

Para sumar módulo m, se suma normalmente y luego se toma el residuo. En un reloj de 12 horas, avanzar 8 horas desde las 7 equivale a calcular 7 + 8 módulo 12.

(7 + 8) mod 12 = 15 mod 12 = 3.

Módulo 5: (4 + 3) mod 5 = 7 mod 5 = 2.
Módulo 5: (2 + 4) mod 5 = 6 mod 5 = 1.

La suma modular es asociativa y conmutativa, igual que la suma de enteros. El elemento neutro es 0, porque a + 0 ≡ a (mod m).

26.5 Resta y opuesto aditivo

Restar módulo m equivale a sumar el opuesto aditivo. El opuesto de a es el residuo que, sumado a a, da 0 módulo m.

Módulo 7, el opuesto de 3 es 4, porque 3 + 4 ≡ 0.

(2 - 5) mod 7 = -3 mod 7 = 4.
También: 2 + 2 = 4, pues el opuesto de 5 módulo 7 es 2.

Todo residuo tiene opuesto aditivo. Por eso la resta siempre está definida en aritmética modular, a diferencia de la división.

26.6 Multiplicación modular

La multiplicación modular se calcula multiplicando y reduciendo. Podemos reducir cualquiera de los factores antes de multiplicar.

(17 · 9) mod 5 = (2 · 4) mod 5 = 8 mod 5 = 3.

(31 · 52) mod 7 = (3 · 3) mod 7 = 2.

La segunda cuenta evita multiplicar 31 por 52. Esta reducción temprana es correcta gracias a que las congruencias se preservan al multiplicar.

26.7 Reducir durante el cálculo

Si a ≡ r (mod m) y b ≡ s (mod m), entonces a + b ≡ r + s y ab ≡ rs (mod m). En términos de programación, es seguro normalizar cada resultado intermedio.

(123 + 456 + 789) mod 10
≡ (3 + 6 + 9) mod 10
= 18 mod 10 = 8.

Reducir no cambia el resultado; solo mantiene los valores pequeños.

La técnica es útil cuando los números crecen rápido, como ocurre con productos repetidos, hashes polinómicos o potencias.

26.8 Implementar suma, resta y producto

function modulo(a, m) {
  if (!Number.isInteger(m) || m <= 0) throw new Error("m debe ser positivo");
  return ((a % m) + m) % m;
}

function sumarMod(a, b, m) {
  return modulo(modulo(a, m) + modulo(b, m), m);
}

function restarMod(a, b, m) {
  return modulo(modulo(a, m) - modulo(b, m), m);
}

function multiplicarMod(a, b, m) {
  return modulo(modulo(a, m) * modulo(b, m), m);
}

console.log(sumarMod(17, 9, 5));        // 1
console.log(restarMod(2, 5, 7));        // 4
console.log(multiplicarMod(31, 52, 7)); // 2

Para enteros pequeños, esta implementación es clara y suficiente. Más adelante veremos por qué los límites de precisión de JavaScript requieren precauciones con valores muy grandes.

26.9 Tabla de suma módulo 4

Una tabla permite ver que la suma modular siempre permanece dentro del conjunto de residuos.

+ mod 40123
00123
11230
22301
33012

Por ejemplo, la última fila muestra que 3 + 1 ≡ 0 y 3 + 2 ≡ 1 módulo 4. La tabla tiene el mismo comportamiento cíclico que un contador que vuelve a cero.

26.10 Potencias modulares

Una potencia modular es el residuo de una potencia ordinaria. La reducción de cada producto impide que los números intermedios crezcan innecesariamente.

31 mod 7 = 3.
32 mod 7 = 2.
33 mod 7 = 6.
34 mod 7 = 4.
35 mod 7 = 5.
36 mod 7 = 1.

Al haber una cantidad finita de residuos, las potencias forman ciclos o alcanzan patrones repetidos. Esto es una de las razones por las que la aritmética modular es tan útil en algoritmos.

26.11 Exponenciación rápida

Calcular an multiplicando a por sí mismo n veces requiere n multiplicaciones. La exponenciación por cuadrados usa la representación binaria del exponente y necesita aproximadamente log2(n) iteraciones.

function potenciaMod(base, exponente, m) {
  if (!Number.isInteger(exponente) || exponente < 0) {
    throw new Error("el exponente debe ser un entero no negativo");
  }

  let resultado = 1;
  let factor = modulo(base, m);

  while (exponente > 0) {
    if (exponente % 2 === 1) resultado = multiplicarMod(resultado, factor, m);
    factor = multiplicarMod(factor, factor, m);
    exponente = Math.floor(exponente / 2);
  }
  return resultado;
}

console.log(potenciaMod(3, 13, 7)); // 3

Cada vez que el exponente es impar, se incorpora el factor actual al resultado. Después se eleva el factor al cuadrado y se divide el exponente por 2 usando división entera.

26.12 La división modular requiere cuidado

En enteros, dividir entre b significa multiplicar por 1/b. En aritmética modular, una división por b solo tiene sentido si existe un número que multiplicado por b dé 1 módulo m. Ese número se llama inverso multiplicativo de b.

Dividir por b módulo m significa multiplicar por b-1 módulo m.

Si b · x ≡ 1 (mod m), entonces x es el inverso de b.

No todos los residuos tienen inverso. Por eso no debemos traducir una división ordinaria a código modular sin verificar esta condición.

26.13 Inversos multiplicativos

Módulo 7, el inverso de 3 es 5, porque 3 · 5 = 15 y 15 ≡ 1 (mod 7). En consecuencia, dividir por 3 módulo 7 equivale a multiplicar por 5.

3-1 ≡ 5 (mod 7).

4 / 3 (mod 7) = 4 · 5 mod 7 = 20 mod 7 = 6.
Comprobación: 3 · 6 = 18 ≡ 4 (mod 7).

Cuando el módulo es primo, todos los residuos no nulos tienen inverso. Con módulos compuestos, algunos residuos no lo tienen.

26.14 Cuándo existe un inverso

Un entero a tiene inverso módulo m si y solo si su máximo común divisor con m es 1. Se dice entonces que a y m son coprimos.

Módulo 8:
3 tiene inverso, porque mcd(3, 8) = 1. De hecho, 3 · 3 ≡ 1.
2 no tiene inverso, porque mcd(2, 8) = 2.

Los productos 2 · r módulo 8 solo dan residuos pares, nunca 1.

El algoritmo de Euclides, que veremos en el próximo tema, calcula el máximo común divisor de forma eficiente y permite decidir esta cuestión.

26.15 Buscar un inverso en ejemplos pequeños

Para módulos pequeños se puede probar cada residuo. Este método ilustra la definición, aunque no es adecuado para módulos enormes.

function inversoPorBusqueda(a, m) {
  a = modulo(a, m);
  for (let candidato = 1; candidato < m; candidato++) {
    if (multiplicarMod(a, candidato, m) === 1) return candidato;
  }
  return null; // no existe inverso
}

console.log(inversoPorBusqueda(3, 7)); // 5
console.log(inversoPorBusqueda(2, 8)); // null

Más adelante reemplazaremos la búsqueda por el algoritmo extendido de Euclides, que encuentra el inverso con mucha mayor eficiencia cuando existe.

26.16 Resolver una ecuación modular sencilla

Para resolver ax ≡ b (mod m), si a tiene inverso se multiplica ambos lados por a-1. Por ejemplo, para 3x ≡ 4 (mod 7), usamos que el inverso de 3 es 5.

3x ≡ 4 (mod 7).
5 · 3x ≡ 5 · 4 (mod 7).
x ≡ 20 ≡ 6 (mod 7).

Comprobación: 3 · 6 = 18 ≡ 4 (mod 7).

Si a no tiene inverso, la ecuación puede no tener solución o tener más de una clase de soluciones. Ese caso depende del máximo común divisor entre a y m.

26.17 Cierre de un conjunto de residuos

Los residuos módulo m están cerrados bajo suma, resta y multiplicación: al operar dos residuos y reducir, obtenemos otro residuo válido entre 0 y m - 1.

Módulo 6:
4 + 5 ≡ 3.
1 - 4 ≡ 3.
4 · 5 ≡ 2.

Todos los resultados pertenecen a {0, 1, 2, 3, 4, 5}.

La división no tiene esta garantía: por ejemplo, 1 no puede dividirse por 2 módulo 6, pues 2 no posee inverso. Esta diferencia separa a los módulos compuestos de los casos más simples con módulo primo.

26.18 Aplicación: hash polinómico

Un hash polinómico combina los valores de los caracteres y reduce cada paso por la capacidad de una tabla. La reducción mantiene el índice dentro de un rango fijo.

function hashSimple(texto, capacidad) {
  let hash = 0;
  const base = 31;

  for (const caracter of texto) {
    hash = sumarMod(multiplicarMod(hash, base, capacidad), caracter.charCodeAt(0), capacidad);
  }
  return hash;
}

console.log(hashSimple("hola", 101)); // un índice entre 0 y 100

Este ejemplo sirve para aprender la técnica, no para seguridad. Las funciones hash criptográficas tienen requisitos mucho más estrictos y deben provenir de bibliotecas confiables.

26.19 Aplicación: acumuladores y comprobaciones

Muchos protocolos y formatos acumulan una suma de bytes y conservan un residuo. Si el emisor y el receptor no obtienen el mismo resultado, saben que hubo una alteración o un error accidental.

Acumulador inicial: 0.
Por cada valor v: acumulador = (acumulador + v) mod 256.

El resultado siempre cabe en un byte: 0 a 255.

Una suma de comprobación simple detecta algunos errores, pero no ofrece protección criptográfica contra modificaciones intencionales. Para seguridad se usan mecanismos diseñados para ese fin.

26.20 Precisión numérica y BigInt

Los valores Number de JavaScript representan exactamente los enteros solo hasta Number.MAX_SAFE_INTEGER. Si un producto excede ese límite, el resto puede ser incorrecto debido al redondeo de punto flotante.

function moduloBigInt(a, m) {
  if (m <= 0n) throw new Error("m debe ser positivo");
  return ((a % m) + m) % m;
}

const resultado = moduloBigInt(12345678901234567890n * 9876543210987654321n, 1000000007n);
console.log(resultado);

Con BigInt debe usarse el sufijo n y no se pueden mezclar valores Number y BigInt en la misma operación. Es una precaución esencial en criptografía y cálculos de gran tamaño.

26.21 Errores frecuentes

  • Olvidar reducir los resultados negativos a un residuo entre 0 y m - 1.
  • Creer que la división modular siempre está definida.
  • Cancelar un factor sin verificar que sea coprimo con el módulo.
  • Calcular potencias enormes antes de tomar el módulo en lugar de reducir en cada paso.
  • Usar Number cuando los productos exceden el rango de enteros seguros.
  • Confundir una suma de comprobación didáctica con una medida de seguridad.

26.22 Qué debes recordar y conclusión

  • La suma, la resta y el producto modulares se calculan reduciendo el resultado módulo m.
  • Es válido reducir valores intermedios para simplificar y controlar el tamaño de los números.
  • La exponenciación rápida calcula potencias modulares en tiempo logarítmico respecto del exponente.
  • Dividir por a módulo m significa multiplicar por el inverso de a.
  • Un inverso existe exactamente cuando a y m son coprimos.
  • Para enteros muy grandes en JavaScript, BigInt evita pérdidas de precisión.

Las operaciones modulares convierten cálculos potencialmente grandes en procesos sobre un conjunto finito de residuos. En el próximo tema estudiaremos el algoritmo de Euclides, la herramienta clásica para hallar máximos comunes divisores y determinar cuándo existen inversos.