Logaritmo discreto
El logaritmo discreto es un entero que satisface la ecuación
para enteros dados , y .
El logaritmo discreto no siempre existe; por ejemplo, no hay solución de . No hay una condición sencilla para determinar si el logaritmo discreto existe.
En este artículo describimos el algoritmo Baby-step giant-step (pasos de bebé y de gigante), un algoritmo para calcular el logaritmo discreto propuesto por Shanks en 1971, que tiene complejidad temporal . Es un algoritmo de meet-in-the-middle porque usa la técnica de separar las tareas por la mitad.
Algoritmo
Consideremos la ecuación:
donde y son coprimos.
Sea , donde es alguna constante preseleccionada (más adelante describiremos cómo elegir ). se conoce como paso gigante (giant step), ya que incrementarlo en uno incrementa en . De forma análoga, se conoce como paso bebé (baby step).
Obviamente, cualquier número en el intervalo se puede representar de esta forma, donde y .
Entonces la ecuación queda:
Usando el hecho de que y son coprimos, obtenemos:
Esta nueva ecuación se puede reescribir de forma simplificada:
Este problema se puede resolver con el método meet-in-the-middle de la siguiente manera:
- Calcular para todos los argumentos posibles . Ordenar el arreglo de pares valor-argumento.
- Para todos los argumentos posibles , calcular y buscar el correspondiente en el arreglo ordenado usando búsqueda binaria.
Complejidad
Podemos calcular en usando el algoritmo de exponenciación binaria. De forma análoga para .
En el primer paso del algoritmo, hay que calcular para cada argumento posible y luego ordenar los valores. Así, este paso tiene complejidad:
En el segundo paso del algoritmo, hay que calcular para cada argumento posible y luego hacer una búsqueda binaria sobre el arreglo de valores de ; así, este paso tiene complejidad:
Ahora, al sumar estas dos complejidades, obtenemos multiplicado por la suma de y , que es mínima cuando , lo que significa que, para alcanzar el rendimiento óptimo, debe elegirse de modo que:
Entonces la complejidad del algoritmo queda:
Implementación
La implementación más simple
En el siguiente código, la función powmod calcula y la función solve produce una solución adecuada del problema.
Devuelve si no hay solución y, en caso contrario, devuelve una de las soluciones posibles.
int powmod(int a, int b, int m) {
int res = 1;
while (b > 0) {
if (b & 1) {
res = (res * 1ll * a) % m;
}
a = (a * 1ll * a) % m;
b >>= 1;
}
return res;
}
int solve(int a, int b, int m) {
a %= m, b %= m;
int n = sqrt(m) + 1;
map<int, int> vals;
for (int p = 1; p <= n; ++p)
vals[powmod(a, p * n, m)] = p;
for (int q = 0; q <= n; ++q) {
int cur = (powmod(a, q, m) * 1ll * b) % m;
if (vals.count(cur)) {
int ans = vals[cur] * n - q;
return ans;
}
}
return -1;
}En este código usamos map de la biblioteca estándar de C++ para almacenar los valores de .
Internamente, map usa un árbol rojo-negro para guardar los valores.
Así, este código es un poco más lento que si hubiéramos usado un arreglo y búsqueda binaria, pero es mucho más fácil de escribir.
Nótese que nuestro código asume , es decir, el código calculará como solución de la ecuación y también como solución de . Esta es una convención usada a menudo en álgebra, pero no está universalmente aceptada en todas las áreas. A veces simplemente no está definido. Si no se comparte esa convención, hay que tratar el caso por separado:
if (a == 0)
return b == 0 ? 1 : -1;Otra cosa a notar es que, si hay varios argumentos que corresponden al mismo valor de , solo almacenamos uno de esos argumentos.
Esto funciona en este caso porque solo queremos devolver una solución posible.
Si necesitamos devolver todas las soluciones posibles, hay que cambiar map<int, int> por, por ejemplo, map<int, vector<int>>.
También hay que cambiar el segundo paso de forma correspondiente.
Implementación mejorada
Una posible mejora es deshacerse de la exponenciación binaria.
Esto se puede hacer manteniendo una variable que se multiplica por cada vez que incrementamos y una variable que se multiplica por cada vez que incrementamos .
Con este cambio, la complejidad del algoritmo sigue siendo la misma, pero ahora el factor es solo por el map.
En lugar de un map, también podemos usar una tabla hash (unordered_map en C++), que tiene complejidad temporal promedio para insertar y buscar.
Los problemas a menudo piden el mínimo que satisface la solución.
Es posible obtener todas las respuestas y tomar el mínimo, o reducir la primera respuesta encontrada usando el teorema de Euler, pero podemos ser inteligentes con el orden en que calculamos los valores y asegurar que la primera respuesta que encontremos sea la mínima.
// Devuelve el mínimo x para el cual a ^ x % m = b % m; a y m son coprimos.
int solve(int a, int b, int m) {
a %= m, b %= m;
int n = sqrt(m) + 1;
int an = 1;
for (int i = 0; i < n; ++i)
an = (an * 1ll * a) % m;
unordered_map<int, int> vals;
for (int q = 0, cur = b; q <= n; ++q) {
vals[cur] = q;
cur = (cur * 1ll * a) % m;
}
for (int p = 1, cur = 1; p <= n; ++p) {
cur = (cur * 1ll * an) % m;
if (vals.count(cur)) {
int ans = n * p - vals[cur];
return ans;
}
}
return -1;
}La complejidad es usando unordered_map.
Cuando y no son coprimos { data-toc-label=‘Cuando a y m no son coprimos’ }
Sea , y . Claramente para todo será divisible por .
Si , no hay solución para .
Si , sean .
(g \alpha) a^{x - 1} & \equiv g \beta \mod g \nu \
\alpha a^{x-1} & \equiv \beta \mod \nu
\end{aligned}
El algoritmo baby-step giant-step se puede extender fácilmente para resolver para .
// Devuelve el mínimo x para el cual a ^ x % m = b % m.
int solve(int a, int b, int m) {
a %= m, b %= m;
int k = 1, add = 0, g;
while ((g = gcd(a, m)) > 1) {
if (b == k)
return add;
if (b % g)
return -1;
b /= g, m /= g, ++add;
k = (k * 1ll * a / g) % m;
}
int n = sqrt(m) + 1;
int an = 1;
for (int i = 0; i < n; ++i)
an = (an * 1ll * a) % m;
unordered_map<int, int> vals;
for (int q = 0, cur = b; q <= n; ++q) {
vals[cur] = q;
cur = (cur * 1ll * a) % m;
}
for (int p = 1, cur = k; p <= n; ++p) {
cur = (cur * 1ll * an) % m;
if (vals.count(cur)) {
int ans = n * p - vals[cur] + add;
return ans;
}
}
return -1;
}La complejidad temporal sigue siendo como antes, ya que la reducción inicial a y coprimos se hace en .
Problemas de práctica
- Spoj - Power Modulo Inverted
- Topcoder - SplittingFoxes3
- CodeChef - Inverse of a Function
- Hard Equation (asume que no está definido)
- CodeChef - Chef and Modular Sequence