Circular Barn
Análisis oficial (C++, Python)
Explicación
Primero, consideremos el caso , cuando ambos granjeros quitan vacas de una sola habitación.
Cada granjero puede quitar , , , pero no ni ningún múltiplo de 4. Esto nos motiva a considerar en nuestro juego. El jugador que empieza con debería tomar vacas para forzar al otro jugador a , quien siempre le dará al primer jugador un , ya que no puede tomar ningún múltiplo de para mantener el resto . El primer jugador ahora puede repetir el proceso hasta que , lo que resulta en una victoria.
Por tanto, cuando , el Granjero John gana. En caso contrario, gana el Granjero Nhoj.
Ahora consideramos el caso . Como cada habitación es independiente, tratamos el juego global como juegos individuales. Para cada habitación, definimos al atacante como el ganador garantizado y al defensor como el perdedor garantizado.
El siguiente par de movimientos en una habitación solo se hará cuando se hayan visitado todas las demás habitaciones. Así, los jugadores usan las siguientes estrategias:
- El atacante de una habitación reduce la cantidad de vacas lo más rápido posible para volver a esta habitación y potencialmente ganar antes.
- El defensor preferiría que esto se juegue lento para maximizar las chances de ganar temprano en otra habitación. Sea la cantidad total de operaciones para terminar la habitación de forma individual. El ganador del juego será el ganador de la habitación con el mínimo (si hay empate, la de menor número de habitación).
Para minimizar la cantidad de rondas, el atacante puede usar una versión modificada de su estrategia cuando . En lugar de quitar vacas, puede quitar cualquier cantidad de vacas siempre que sea primo y menor que . Para calcular el mayor tal que , primero usamos una criba para hallar todos los primos, y mantenemos un arreglo con para el primo más reciente que encontramos donde . Al iterar sobre valores de , guardamos para representar el primo equivalente más grande.
Por otro lado, el defensor puede bloquear esta aceleración aprovechando que no hay primos que cumplan , lo que implica que no hay aceleraciones cuando . Si el defensor toma vacas de su y devuelve cada vez, el atacante ya no podrá acelerar el juego.
Así:
- Cuando , la única aceleración que el atacante puede aplicar es quitar al inicio, lo que da .
- En caso contrario, no hay aceleraciones y .
Primero calculamos para cada habitación, y luego hallamos el mínimo de (si hay empate, con el menor ), y emitimos el ganador de esa habitación.
Implementación
Complejidad temporal:
#include <cstring>
#include <iostream>
#include <vector>
using namespace std;
constexpr int MAX_NUMBER = 5e6;
constexpr int MAX_INT = 2e9;
int main() {
vector<bool> is_prime(MAX_NUMBER + 1, true);
// Esto es m[] en la explicación.
vector<int> largest_prime_equiv(MAX_NUMBER + 1);
is_prime[1] = false;
// 1 se considera primo en este problema, aunque
// no lo cribemos; así que asignamos 1 al inicio.
// Esto es t[] en la explicación.
int largest_prime_mod[] = {-1, 1, -1, -1};
largest_prime_equiv[1] = 1;
// Algoritmo de criba
for (int i = 2; i <= MAX_NUMBER; i++) {
if (is_prime[i]) {
largest_prime_mod[i % 4] = i;
for (int j = i; j < MAX_NUMBER; j += i) is_prime[j] = false;
}
largest_prime_equiv[i] = largest_prime_mod[i % 4];
}
int test_cases;
cin >> test_cases;
for (int t = 0; t < test_cases; t++) {
int n;
cin >> n;
// Mínimo de turnos, es decir, operaciones / 2
int min_turn = MAX_INT;
// Cantidad de operaciones en la habitación de turnos mínimos.
int min_turn_ops = MAX_INT;
for (int i = 1; i <= n; i++) {
int cows;
cin >> cows;
int operations;
if (cows % 2 == 0) {
operations = cows / 2;
} else {
operations = 1 + (cows - largest_prime_equiv[cows]) / 2;
}
if (operations / 2 < min_turn) {
min_turn = operations / 2;
min_turn_ops = operations;
}
}
cout << ((min_turn_ops % 2 != 0) ? "Farmer John" : "Farmer Nhoj") << endl;
}
return 0;
}