Solución de "Aventureros"

Hola buenas, vuelvo a escribir acá por otro problema del envío 5 (espero estar publicando en la sección correcta).

Esta vez es por el problema “aventureros”. Lo intenté resolver pero tengo un par de problemas. Esto es lo que traté de hacer:

La idea es calcular cuántas personas fueron eliminadas dentro del rango entre mi posición actual pos y mi posición actual más la longitud del salto (pos + L). Esto lo hago con un Fenwick Tree que setea en 1 las posiciones correspondientes a personas eliminadas, y obtiene la suma de todos los ‘1s’ dentro del rango que mencioné recién.

Primero me fijo si hacer ese salto excede la longitud de la fila de personas. Si es así, hago dos queries separadas: una entre mi posición de la fila y el final:

query(pos,N)

…y otra entre el principio de la fila y la posición en la que hubiera quedado en módulo N (siendo N la cantidad de personas original):

query2 ( 0 , (pos+C)%N )

A la suma de estas dos queries la multiplico por la parte entera de dividir la posición en la que hubiera quedado por N:

(query2 * floor( (pos+C) / N )

…por si “da varias vueltas”.

Mi nueva cantidad de saltos va a ser la suma de los resultados de esas dos queries y la cantidad de saltos original. Mi nueva posición va a ser la suma de todo lo anterior y mi posición original, todo en módulo N.

El problema viene ahora: ¿Qué pasa si caigo en una posición en la que la persona ya fue eliminada? Lo que me se me ocurre es pasar a la siguiente posición hasta encontrar una persona, pero tiene que haber una solución mejor.

Dejo mi código con esta solución fea (que encima tira seg fault en muchos casos):

Código
#include <iostream>
#include <cstdio>
#include <queue>
#include <cmath>

#define forn(i,N) for(int i=0;i<N;i++)

using namespace std;

int N;
int maxN = 2<<15;

int pos=0;

int fenTree[100000]; /// jumps
int eliminado[100000];
queue <int> queries;

void pointUpdate( int x, int v ){

    for (int i=x; i<maxN; i += i & -i){
        fenTree[i] += v;
    }
}

int rangeQuery( int x ){

    int v=0;
    for (int i=x; i; i -= i & -i){
        v+=fenTree[i];
    }
    return v;
}

void processJump( int j ){

    int newPos = pos+j, newJumps = 0;
    int firstQuery = 0, secondQuery = 0;

    if (newPos >= N){

        firstQuery = rangeQuery(N) - rangeQuery(pos);
        secondQuery = rangeQuery (newPos%N);
        newJumps = (firstQuery + secondQuery) * floor(newPos/N);

    } else {

        newJumps = rangeQuery ( newPos  ) - rangeQuery (pos);
    }

    newPos = (newPos + newJumps)%N;

    while (eliminado[newPos]){

        newPos = (newPos + 1)%N;
    }

    pointUpdate(newPos+1,1);
    eliminado[newPos] = 1;

    pos = newPos+1;
    cout << pos << " ";
}

void processQueries(){

    while (queries.size()){

        int q = queries.front();
        queries.pop();

        processJump( q );
    }
}

int main(){

    //freopen("aventureros.txt","r",stdin);
    freopen("aventureros.in","r",stdin);
    freopen("aventureros.out","w",stdout);

    cin >> N;

    int buffQ;
    forn(i,N-1){

        cin >> buffQ;
        queries.push(buffQ);
    }

    processQueries();
    forn(i,N){
        if (!eliminado[i]){
            cout << endl << i+1 << endl;
            break;
        }
    }
    
    return 0;
}

Puede ser también que las queries al fenwick tree no tengan que estar indexadas en 0 sino en 1, pero de ambas formas el resultado es el mismo en el juez.

Ambos casos ejemplo me dan bien, pero al evaluador no le gusta para nada. Quisiera saber qué puedo hacer para solucionar esos casos (y si puede ser también dónde estoy accediendo a una dirección de memoria que no debería).

Muchas gracias!

1 me gusta

Voy a tirar un par de pistas generales para el problema (son los pasitos que fui pensando hasta llegar a una solución)

Pista 1

En el i-ésimo paso, ¿cuántos saltitos al siguiente tengo que dar para volver al mismo lugar?¿Importan las posiciones que fueron elminadas anteriormente?

Pista 2 (incluye respuesta a Pista 1, mirar después de pensar la Pista 1)

En el i-ésimo paso, al cabo de n-i pasos volvemos a la posición donde estamos y NO IMPORTA QUIÉNES FUERON ELMINADOS EN PASOS ANTERIORES (comenzando desde el paso 0, si comenzamos desde el paso 1 será n-i+1).

Entonces, puedo mirar a_i módulo (n-i), y ahora hay que avanzar esa cantidad de pasos

Pista 3

En vez de guardarte con un 1 los que fueron eliminados, guardate con 1 los que todavían siguen en la ronda. Por la pista 2, sabés cuántas personas que están en la ronda tenés que avanzar desde ahí, ¿Cómo podés avanzar rápido?

Edit: Agrego pista muy quemadora.

Pista 4

Si tenemos el arreglo binario con un 1 en las posiciones originales de los que todavía están en la ronda y lo vamos actualizando acorde. ¿Qué nos dice la suma en un rango?

Respuesta

Nos dice la cantidad de personas que todavía están en la ronda con índices en ese rango. Entonces, si queremos avanzar una cantidad de posiciones, hay que conseguir un rango que contenga esa cantidad de personas.

1 me gusta

Perdón por tardar tanto en responder, pero la verdad es que estoy muy perdido con este problema.

Haciendo la cantidad de saltos módulo (n-i) obtengo la “verdadera” cantidad de pasos, pero todavía sin tener en cuenta las personas eliminadas sobre las que voy a pasar.

A partir de la pista 3 se me ocurre solamente algo parecido a lo que hice. Si tengo que avanzar P pasos, obtengo la suma de todos los “1” en un rango de longitud P a partir de mi posición. Si P es igual a esa suma, significa que puedo avanzar sin problemas. Si no, tengo que seguir moviendome hasta que la suma sea igual a P.

Sobre la pista 4, ¿obtener rangos a partir de cantidades de personas? Se me ocurre que podría hacer algo con una binary search. Digamos que empiezo desde la posición X, me fijo cuantas personas hay entre X+P y N. Si esa cantidad, sumada a la cantidad de personas entre X y X+P, es mayor a P, significa que me pasé y muevo la cota superior de la búsqueda a (X+P+N)/2. Si es menor a P, muevo la cota inferior a (X+P+N)/2. Si coincide, encontré mi nueva posición final. ¿Será mejor esto que simplemente moverme hasta alcanzar la cantidad de personas necesaria?

Se me cruzó por la cabeza armar el fenwick tree “al reves”. En vez de obtener la cantidad de personas en un rango, obtengo los rangos para una cantidad de personas, pero no se si es viable. Me parece que no estoy llegando a ningún lado. ¿Hay algo que no vi?

1 me gusta

@DeCicco Bueno, como que todo lo que contás parece bien encaminado… falta juntar todo y resolver el problema.

Voy a intentar explicar todo lo anterior, pero en el segundo ejemplo que viene en el enunciado. La flechita indica dónde está el mate en el paso actual.

Paso 0 (inicialmente están todos en la ronda)

n = 5

\begin{array}{|c|c|c|c|c|} \hline \downarrow & & & & \\ \hline 1 & 1 & 1 & 1 &1 \\ \hline \end{array}

Paso 1

i = 1
a_i = 1 \Rightarrow \boxed{{\bf a_i \equiv 1} \pmod{\overbrace{n-i+1}^5}}

Entonces, el mate avanza una persona hacia adelante. Esa persona se va de la ronda y el mate queda en la siguiente persona.

\begin{array}{|c|c|c|c|c|} \hline \downarrow & & & & \\ \hline 1 & 1 & 1 & 1 &1 \\ \hline \end{array} \rightarrow \begin{array}{|c|c|c|c|c|} \hline & & \downarrow & & \\ \hline 1 & 0 & 1 & 1 &1 \\ \hline \end{array}

Paso 2

i = 2
a_i = 2 \Rightarrow \boxed{{\bf a_i \equiv 2} \pmod{\overbrace{n-i+1}^4}}

Entonces, el mate avanza dos personas hacia adelante. Esa persona se va de la ronda y el mate queda en la siguiente persona.

Veamos ahora cómo hacer esto en los términos que vimos antes.

Notar que en todos los pasos esto se puede separar en dos problemas.

  • Encontrar la \color{orange}{\text{persona que se va de la ronda}}. Si el mate está en i, queremos un rango a partir de i+1 que sea lo más chico posible, pero que sume al menos a_i, que en este caso es \color{orange}{2}.
  • Encontrar la \color{lime}{\text{siguiente persona que va a recibir el mate}}. Es el mismo problema, queremos un rango a partir de i+1 que sea lo más chico posible, pero que esta vez sume al menos a_i+1, que en este caso es \color{lime}{3}.

Normalmente sería un intervalo de la forma [i+1,r) y buscamos el r más chico con suma al menos a_i. Como el arreglo es circular podemos primero chequear si alcanza con todo lo que resta hasta el final, de ser así, hacemos la búsqueda allí. Sino buscamos a_i-\texttt{sumaSufijo} en la primera parte. Notar que como tomamos módulo nunca “pega más de una vuelta”.

\begin{array}{|c|c|c|c|c|} \hline & & \downarrow & & \\ \hline 1 & 0 & 1 & \color{orange}1 & \color{orange}1 \\ \hline \end{array} \rightarrow \begin{array}{|c|c|c|c|c|} \hline & & \downarrow & & \\ \hline 1 & 0 & 1 & 1 & \color{red}0 \\ \hline \end{array}
\begin{array}{|c|c|c|c|c|} \hline & & \downarrow & & \\ \hline \color{lime}{1} & 0 & 1 & \color{lime}1 & \color{lime}1 \\ \hline \end{array} \rightarrow \begin{array}{|c|c|c|c|c|} \hline \color{lime}\downarrow & & & & \\ \hline 1 & 0 & 1 & 1 & \color{red}0 \\ \hline \end{array}

Por ejemplo, en este caso, no alcanzó para sumar 3 con todo los que restaban hasta el final en verde. Por lo tanto, hay que buscar un rango que sume 3-2 = 1 comenzando desde el principio.

Paso 3

i = 3
a_i = 3 \Rightarrow \boxed{{\bf a_i \equiv 0} \pmod{\overbrace{n-i+1}^3}}

Entonces, la persona que tiene el mate se va de la ronda y el mate pasa a la siguiente persona. Para pasar a la siguiente persona, debemos encontrar el rango más chico que sume al menos 1.

\begin{array}{|c|c|c|c|c|} \hline \downarrow & & & & \\ \hline 1 & 0 & 1 & 1 & 0 \\ \hline \end{array} \rightarrow \begin{array}{|c|c|c|c|c|} \hline \downarrow & & & & \\ \hline \color{red}0 & 0 & 1 & 1 & 0 \\ \hline \end{array}
\begin{array}{|c|c|c|c|c|} \hline \downarrow & & & & \\ \hline 1 & \color{lime}0 & \color{lime}1 & 1 & 0 \\ \hline \end{array} \rightarrow \begin{array}{|c|c|c|c|c|} \hline & & \color{lime}\downarrow & & \\ \hline \color{red}0 & 0 & 1 & 1 & 0 \\ \hline \end{array}

Y se sigue de la misma forma.

Edit: Normalmente pondría todo esto en un gran Spoiler. Pero si hago eso no se ven las fórmulas en LaTeX :frowning:

1 me gusta

Muy buena explicación! La verdad que me vinieron de 10 las tablas con el paso a paso. Me parece que para terminar de cerrarlo me faltaba la parte de obtener rangos que cumplan cierta propiedad (en este caso, que la suma de sus elementos sea la cantidad de saltos que necesito dar). Encontré cómo hacer esto sin binary search “invirtiendo” el fenwick tree.

Además, me parece que lo facilita mucho pensar que tanto para encontrar a la persona que se va de la ronda, como a la que recibe el mate se hace practicamente lo mismo:

Voy a probar de hacerlo de vuelta (como por 4ta vez) y ver qué sale.

Muchisimas gracias!

P.D: las diapositivas de la charla sobre Fenwick Trees del año pasado explican de todo, y ahí encontré cómo “invertir” el fenwick tree para encontrar rangos que sumen una cantidad, en vez de la suma de los elementos de un rango. Por si a alguien le sirve, las pueden ver acá.

1 me gusta

Probé de vuelta y no salió tan bien como esperaba. Solamente 28 puntos, principalmente por “Execution killed with signal 11”. Resuelve los mismos casos que mi intento anterior, además de un par que antes no tenía en cuenta (cuando eliminaba a alguien, me paraba en la casilla inmediatamente a la derecha, sin pensar que esa posición podía estar vacía). Estaba armando la respuesta planteando el problema cuando me puse a pensar por qué podría estar teniendo el mismo problema en ambos intentos.

Resulta que estaba definiendo mal las cotas del problema. 2 << 15 (mi maxN) es solamente 65536 y le di solamente 100000 espacios al array para el fenwick tree, mientras que el enunciado dice que pueden ser hasta 400000 personas. Subí maxN a 2<<18 y le dí 600000 espacios al array y se solucionó todo, 100 puntos.

Me sorprende lo rápido que salió el problema teniendo todo planteado de antemano.

Acá está el código por las dudas:

Código
#include <iostream>
#include <cstdio>
#include <queue>
#include <cmath>

#define forn(i,N) for(int i=0;i<N;i++)

using namespace std;

int N;
int maxN = 2<<18;
int logMaxN = log2(maxN);
int pos=1, step=0;

int fenTree[600000];

queue <int> queries;

void pointUpdate( int x, int v ){

    for (int i=x; i<maxN; i += i & -i){
        fenTree[i] += v;
    }
}

int rangeQuery( int x ){

    int v=0;
    for (int i=x; i; i -= i & -i){
        v+=fenTree[i];
    }
    return v;
}

int getRange(int v){
    int x=0;
    for (int d=1 <<(logMaxN-1); d; d>>=1 ){
        if (fenTree[x|d] < v){
            x |= d;
            v -= fenTree[x];
        }
    }
    return x+1;
}

void processJump( int jumps ){

    int newJumps = jumps%(N-step), nextJumps = (jumps+1)%(N-step);
    int peopleLeft = rangeQuery(N) - rangeQuery (pos);
    int deletePos, nextPos;

    if (peopleLeft >= newJumps){
        deletePos = getRange( rangeQuery(pos) + newJumps );
    } else {
        deletePos = getRange( newJumps - peopleLeft );
    }

    if (peopleLeft >= nextJumps){
        nextPos = getRange( rangeQuery(pos) + nextJumps);
    } else {
        nextPos = getRange( nextJumps - peopleLeft );
    }

    pos = nextPos;
    cout << deletePos << " ";
    pointUpdate( deletePos, -1);
}

void processQueries(){

    forn(i,N){
        pointUpdate(i+1,1);
    }

    while (queries.size()){

        int q = queries.front();
        queries.pop();

        processJump( q );
        step++;
    }

    cout << endl << pos << endl;
}

int main(){

    //freopen("aventureros.txt","r",stdin);
    freopen("aventureros.in","r",stdin);
    freopen("aventureros.out","w",stdout);

    cin >> N;

    int buffQ;
    forn(i,N-1){

        cin >> buffQ;
        queries.push(buffQ);
    }

    processQueries();

    return 0;
}
1 me gusta