Aritmética modular

Para programación competitiva y también en general.

Si usted vino buscando un resumen, al final del post lo encontrará.

El residuo #

Seguramente cuando usted estaba en la primaria le enseñaron a dividir de una manera similar a la siguiente:

División

Donde se cumple que:

$$ \text{dividendo} = \text{divisor} \cdot \text{cociente} + \text{residuo} $$

Aunque cuando dividimos usualmente nos interesa el cociente, en este caso nos va a interesar el residuo.

En la mayoría de lenguajes de programación tenemos el operador $%$ para calcular el residuo de una división. Algunos ejemplos:

int r = 8 % 3; // r = 2
int r = 21 % 4; // r = 1
int r = 7 % 10; // r = 7
int r = 30 % 6; // r = 0

Algunas propiedades. Sea $r = k % m$:

  • $0 \leq r < m$
  • Si $0 \leq k < m$, entonces $r = k$
  • Si $k$ es múltiplo de $m$, entonces $r = 0$

Cuidado con los números negativos #

Miremos este ejemplo para ver lo que pasa en los lenguajes de programación con números negativos y el operador $%$:

int m = 6;
int d = -16 / m; // d = -2
int r = -16 % m; // r = -4

Esto tiene sentido, ya que se cumple que -16 == 6 * (-2) + (-4).

Sin embargo, para hacer cálculos de aritmética modular preferimos que el residuo sea un número que cumpla con $0 \leq r < m$. Para conseguir este residuo no-negativo, hacemos $r = (k % m + m) % m$. En el ejemplo anterior nos quedaría:

int m = 6;
int r = (-16 % m + m) % m; // r = 2

Y la expresión quedaría -16 == 6 * (-3) + 2.

Congruencias #

Si dos números $x$ y $y$ tienen el mismo residuo con cierto $m$, decimos que $x$ y $y$ son congruentes módulo $m$, y lo denotamos de la siguiente manera:

$$ x \equiv y \pmod{m} $$

Algunos ejemplos:

  • $7 \equiv 25 \pmod{3}$ ya que $7 % 3 = 1$ y $25 % 3 = 1$
  • $20 \equiv 44 \pmod{8}$ ya que $20 % 8 = 4$ y $44 % 8 = 4$
  • $52 \equiv 24 \pmod{7}$ ya que $52 % 7 = 3$ y $24 % 7 = 3$

Inverso modular #

El inverso modular de $x$ módulo $m$ es un número que denotamos como $x^{-1}$, y que al multiplicarlo por $x$, obtenemos que $x \cdot x^{-1} \equiv 1 \pmod{m}$

Algo que hay que anotar es que este inverso modular existe solamente si $x$ y $m$ son coprimos (es decir que no tienen factores en común).

Veamos algunos ejemplos:

  • El inverso de $3$ módulo $26$ es $9$, ya que $9 \cdot 3 = 27$ y $27 % 26 = 1$
  • El inverso de $11$ módulo $36$ es $23$, ya que $11 \cdot 23 = 253$ y $253 % 36 = 1$
  • El inverso de $5$ módulo $8$ es $5$, ya que $5 \cdot 5 = 25$ y $25 % 8 = 1$
  • El inverso de $6$ módulo $14$ no existe, ya que no son coprimos (tienen a $2$ como factor común)

Ahora la pregunta es, ¿cómo hallamos este inverso modular?

Pequeño teorema de Fermat #

No confundir con el Último Teorema de Fermat.

Este pequeño teorema establece que:

$$ \text{Si } m \text{ es primo, } a^{m-1} \equiv 1 \pmod{m} $$

Notemos que $a^{m-1} = a \cdot a^{m-2}$. Nos queda que:

$$ a \cdot a^{m-2} \equiv 1 \pmod{m} $$

Entonces por definición $a^{m-2}$ es el inverso modular de $a$.

Como dijimos anteriormente, el inverso modular sólo existe si $a$ y $m$ son coprimos. Como en este caso $m$ es primo, todos los $a$ tal que $0 < a < m$ son coprimos con $m$, y por lo tanto tienen inverso. Si $a > m$, sólo se cumplirá el teorema cuando sean coprimos.

Usamos la exponenciación logarítmica para encontrar $a^{m-2}$ rápidamente.

public static int MOD = 1000000007;

public static int expLogMod(int b, int e) {
    if(e == 0) return 1;

    int p = expLogMod(b, e/2);
    long q;

    if(e % 2 == 0) {
        q = (long)p * p;
    } else {
        q = ((long)p * p) % MOD * b;
    }
    return (int)(q % MOD);
}

public static int invMod(int a) {
    return expLogMod(a, MOD - 2);
}

Recomiendo leer el post hasta el final y luego regresar y corroborar este código.

Algoritmo de Euclides extendido #

Con este algoritmo podemos encontrar el inverso modular cuando $m$ no es primo. Para no alargar demasiado este post no lo voy a incluir aquí. El lector interesado puede ver una explicación e implementación en este link.

Propiedad distributiva #

El módulo tiene varias propiedades con la suma y la multiplicación, pero una que nos va a resultar especialmente útil es la distributiva:

  • $(a_0 ​+ a_1 ​+ … + a_n​) % m = (a_0 ​% m + a_1 % m + … + a_n ​% m) % m$
  • $(a_0 ​\cdot a_1 ​\cdot … \cdot a_n​) % m = (a_0 ​% m \cdot a_1 % m \cdot … \cdot a_n ​% m) % m$

En programación competitiva #

En algunos problemas de programación competitiva piden imprimir la respuesta “módulo $10^9 + 7$”. Esto sucede cuando la respuesta es un número muy grande. El máximo número que cabe en un int está alrededor de $10^9$, y en un long está alrededor de $10^{18}$. Imaginemos un problema que nos pide calcular $50!$. Esto no lo podremos hacer ni siquiera con long, ya que $50!$ está en el orden de $10^{64}$.

Por ende, los problemas no piden la solución exacta, sino la respuesta módulo $10^9 + 7$. Como $10^9 + 7$ cabe en un int, podemos almacenarla perfectamente. Además, $10^9 + 7$ es primo, por lo cual podemos usar el Pequeño Teorema de Fermat para calcular inversos modulares en caso de ser necesario.

Si intentamos calcular toda la respuesta del problema y tomar el módulo únicamente al final, vamos a tener overflow en los pasos intermedios y por tanto la respuesta incorrecta. La clave es usar la propiedad distributiva e ir tomando el módulo en cada paso. Veamos ejemplos:

La suma #

public static int MOD = 1000000007;

// MAL
public static int sum(int[] a) {
    int s = 0;
    for(int i=0; i<a.length; i++) {
        s = s + a[i]; // Puede dar overflow
    }
    return s % MOD;
}

// BIEN
public static int sum(int[] a) {
    int s = 0;
    for(int i=0; i<a.length; i++) {
        s = (s % MOD + a[i] % MOD) % MOD; // Módulo en cada paso
    }
    return s % MOD;
}

La resta #

Matemáticamente la propiedad distributiva de la suma se sigue cumpliendo para la resta ($(a - b) % m = (a % m - b % m) % m$). Sin embargo, a la hora de programar, el operador $%$ nos puede arrojar un número negativo como ya vimos anteriormente. Para obtener siempre un número positivo, hacemos:

$$ (a - b) % m = (a % m - b % m + m) % m $$

La multiplicación #

Con la multiplicación hay que tener cuidado: tanto $a % m$ como $b % m$ caben en un int, pero al hacer $(a % m \cdot b % m)$, es posible que haya overflow. Debemos utilizar un long para almacenar este producto. Luego, al sacarle el módulo, ya volvemos a obtener un valor que cabe en un int.

Por ejemplo, miremos esta función para calcular el factorial:

public static int MOD = 1000000007;

// MAL
public static int factorial(int n) {
    int f = 1;
    for(int i=2; i<=n; i++) {
        // Esto nos va a dar overflow y el resultado incorrecto
        f = f * i;
    }
    return f % MOD;
}

// BIEN
public static int factorial(int n) {
    // Debemos usar long para almacenar los pasos intermedios
    long f = 1;
    for(int i=2; i<=n; i++) {
        // Hacemos la multiplicación con módulo en cada paso
        f = (f * i) % MOD;
    }
    return (int)(f % MOD);
}

La división #

Incluir el inverso modular en el post no fue gratuito. Resulta que a diferencia de la suma, la multiplicación y la resta, la división no es tan sencilla:

$$ \left( \frac{a}{b} \right) % m \neq \left( \frac{a % m}{b % m} \right) % m $$

Para poder hacerlo, debemos encontrar el inverso modular de $b$ (módulo $m$), y usar la siguiente propiedad:

$$ \left( \frac{a}{b} \right) % m = ( a \cdot b^{-1} ) % m = ( a % m \cdot b^{-1} % m ) % m $$

Ya que $10^9 + 7$ es primo, podemos usar el Pequeño Teorema de Fermat para hallar el inverso modular de $b$. Si el problema pide otro módulo, hay que asegurarse de que sea primo, y en caso contrario, usar el Algoritmo de Euclides Extendido u otro algoritmo.

Resumen #

  • $(a + b) % m = (a % m + b % m) % m$
  • $(a - b) % m = (a % m - b % m + m) % m$
  • $(a \cdot b) % m = (a % m \cdot b % m) % m$
  • Para calcular $(\frac{a}{b}) % m$, debemos encontrar el inverso modular de $b$ (módulo $m$) y hacer $(a \cdot b^{-1}) % m = (a % m \cdot b^{-1} % m) % m$