Problema de Josefo
Enunciado
Se dan los números naturales y . Todos los números naturales de a se escriben en un círculo. Primero, se cuenta el -ésimo número empezando desde el primero y se elimina. Luego se cuentan números empezando desde el siguiente y de nuevo se elimina el -ésimo, y así sucesivamente. El proceso se detiene cuando queda un solo número. Se pide hallar el último número.
Esta tarea la planteó Flavio Josefo (Flavius Josephus) en el siglo I (aunque en una formulación algo más estrecha: para ).
Este problema se puede resolver modelando el procedimiento. El modelado por fuerza bruta funciona en . Usando un Árbol de Segmentos, podemos mejorarlo a . Sin embargo queremos algo mejor.
Modelar una solución
Intentaremos hallar un patrón que exprese la respuesta del problema a través de la solución de los problemas anteriores.
Usando modelado por fuerza bruta podemos construir una tabla de valores, por ejemplo la siguiente:
Y aquí podemos ver claramente el siguiente patrón:
Aquí, la indexación desde uno da una fórmula un tanto desordenada; si en cambio se numeran las posiciones desde 0, se obtiene una fórmula muy elegante:
Así, hallamos una solución al problema de Josefo, que trabaja en operaciones.
Implementación
Implementación recursiva sencilla (con indexación desde uno)
int josephus(int n, int k) {
return n > 1 ? (josephus(n-1, k) + k - 1) % n + 1 : 1;
}Forma no recursiva:
int josephus(int n, int k) {
int res = 0;
for (int i = 1; i <= n; ++i)
res = (res + k) % i;
return res + 1;
}Esta fórmula también se puede hallar analíticamente. De nuevo aquí asumimos indexación desde cero. Después de eliminar el primer número, nos quedan números. Cuando repetimos el procedimiento, empezaremos con el número que originalmente tenía el índice . sería la respuesta para el círculo restante, si empezamos a contar en , pero como en realidad empezamos con tenemos .
Modelar una solución
Para relativamente pequeño podemos idear una solución mejor que la solución recursiva anterior en . Si es mucho menor que , entonces podemos eliminar varios números () en una pasada sin dar la vuelta. Después nos quedan números, y empezamos con el -ésimo número. Así que tenemos que desplazar esa cantidad. Podemos notar que es simplemente . Y como eliminamos cada -ésimo número, tenemos que sumar el número de números que eliminamos antes del índice resultado. Lo cual podemos calcular dividiendo el índice resultado por .
También necesitamos manejar el caso en que se vuelve menor que . En este caso, la optimización anterior causaría un bucle infinito.
Implementación (por conveniencia con indexación desde cero):
int josephus(int n, int k) {
if (n == 1)
return 0;
if (k == 1)
return n-1;
if (k > n)
return (josephus(n-1, k) + k) % n;
int cnt = n / k;
int res = josephus(n - cnt, k);
res -= n % k;
if (res < 0)
res += n;
else
res += res / (k - 1);
return res;
}Estimemos la complejidad de este algoritmo. Nótese de inmediato que el caso se analiza con la solución antigua, que en este caso trabajará en . Ahora consideremos el algoritmo en sí. De hecho, después de cada iteración, en lugar de números, nos quedan números, así que el número total de iteraciones del algoritmo se puede hallar de forma aproximada a partir de la siguiente ecuación:
al tomar logaritmo en ambos lados, obtenemos:
usando la descomposición del logaritmo en serie de Taylor, obtenemos una estimación aproximada:
Así, la complejidad del algoritmo es de hecho .
Solución analítica para
En este caso particular (en el que esta tarea la planteó Flavio Josefo) el problema se resuelve mucho más fácil.
En el caso de par obtenemos que todos los números pares se tacharán, y luego quedará un problema restante para , entonces la respuesta para se obtendrá de la respuesta para multiplicando por dos y restando uno (desplazando posiciones):
De manera similar, en el caso de un impar, se tacharán todos los números pares, luego el primer número, y quedará el problema para , y teniendo en cuenta el desplazamiento de posiciones, obtenemos la segunda fórmula:
Podemos usar esta dependencia recurrente directamente en nuestra implementación. Este patrón se puede traducir a otra forma: representa una secuencia de todos los números impares, “reiniciándose” desde uno siempre que resulte ser una potencia de dos. Esto se puede escribir como una sola fórmula:
Solución analítica para
A pesar de la forma sencilla del problema y de un gran número de artículos sobre este y problemas relacionados, todavía no se ha hallado una representación analítica simple de la solución del problema de Josefo. Para pequeño se derivan algunas fórmulas, pero al parecer todas son difíciles de aplicar en la práctica (por ejemplo, véase Halbeisen, Hungerbuhler “The Josephus Problem” y Odlyzko, Wilf “Functional iteration and the Josephus problem”).