Laberinto ajedrecístico - OIAJ

Hola! Hace tiempo vengo intentando resolver el problema Laberinto ajedrecístico, del Certamen Selectivo de 2009 (http://juez.oia.unsam.edu.ar/#/task/ajedrez/statement), pero se me va de tiempo en los casos más grandes y no veo como optimizarlo más.

Mi pensamiento del problema es el siguiente:
Por cada casilla del tablero, tengo múltiples movimientos. Consideramos que me encuentro en la \color{red}{\text{x}}:

  • Si uso el caballo, puedo ir todas las casillas a las que puedo llegar formando una L. Es decir, todas las combinaciones en las cuales me desplace 2 casillas en una coordenada, y 1 en la otra.
  • Si uso el alfil, serían todas en diagonal. Es decir, aquellas donde la cantida que varío en una coordenada es igual a lo que varío en la otra.
  • Si uso la torre, es en línea recta. Es decir, lo que varío en una coordenada debe ser 0.
\large \begin{array}{|l|l|l|l|l|} \hline \color{yellow}{\text{a}} & \color{lime}{\text{c}} & \color{orange}{\text{t}} & \color{lime}{\text{c}} & \color{yellow}{\text{a}} \\ \hline \color{lime}{\text{c}} & \color{yellow}{\text{a}} & \color{orange}{\text{t}} & \color{yellow}{\text{a}} & \color{lime}{\text{c}} \\ \hline \color{orange}{\text{t}} & \color{orange}{\text{t}} & \color{red}{\text{x}} & \color{orange}{\text{t}} & \color{orange}{\text{t}} \\ \hline \color{lime}{\text{c}} & \color{yellow}{\text{a}} & \color{orange}{\text{t}} & \color{yellow}{\text{a}} & \color{lime}{\text{c}} \\ \hline \color{yellow}{\text{a}} & \color{lime}{\text{c}} & \color{orange}{\text{t}} & \color{lime}{\text{c}} & \color{yellow}{\text{a}}\\ \hline \end{array}

Con los movimientos de caballo no tengo problema, porque llego a lugares fijos. Mientras, con la torre y el alfil puedo ir a todos los posibles casilleros en llínea o en diagonal respectivamente, hasta llegar a un obstáculo. Y yo no puedo suponer que iré hasta el obstáculo, porque puede que un movimiento a medio camino luego me lleve a otros movimientos que resultarán en el coste mínimo. Entonces no me queda otra que fijarme todos los posibles caminos. Si yo considero la grilla como un grafo en el cual cada celda es un nodo, puedo hacer un Dijkstra, que debería obtener el camino más corto.

Si formo el grafo nodo por nodo dados los posibles movimientos en cada uno, se va de tiempo claramente. Mi primer approach era ir metiendo en el priority_queue todas los posibles lugares con los que puedo llegar con cada arista, pero justamente por lo que dije, en los casos más grandes se va de tiempo.
Así que la segunda opción que probé es solo ver con las celdas cercanas a las que puedo ir por cada celda. Es decir, moverme de a pasos lo más chicos posibles, y sumo costo (y cantidad de jugadas) cuando de una casilla a otra me muevo distinto de lo que hice para llegar a la anterior. Entonces así doy prioridad a los caminos de menor costo, y como yo quiero justamente el valor del mínimo costo, apenas llego a destino puedo cortar y tener el resultado. Y por supuesto al probar las posibilidades de cada casilla, me aseguro de no irme fuera del tablero o a algún obstáculo, que los marco en la entrada en un array bidimensional de booleanos.

Esta solución me da 80 ptos. Más allá un caso de prueba que da WA (que probablemente es un caso borde que no tengo en cuenta, caso 2), me da TLE en los casos 16, 17, y 18. Llegué a esto después de unas optimizaciones controlando la cantidad de elementos que llevo en el priority_queue, y no veo ninguna optimización más que pueda hacer.

Dejo mi código:

Código
#include <bits/stdc++.h>

#define forn(i,n) for(int i = 0; i < int(n); i++)
#define forsn(i,s,n) for(int i = int(s); i < int(n); i++)
#define dforn(i,n) for (int i = int(n)-1; i >= 0; i--)
#define dforsn(i,s,n) for(int i = int(n)-1; i >= int(s); i--)
#define all(c) (c).begin(),(c).end()
#define fst first
#define snd second
#define FAST_IO ios::sync_with_stdio(false);cin.tie(nullptr);

using namespace std;
typedef vector<int> vi;
typedef long long ll;
typedef pair<int,int> ii;

const int MAXN = 1e3+10;
const int MAXTM = 16; // máximos tipos de movimientos
const int INF = 1e9+2;

const int X[] = {0,1,0,-1,  2,-2,2,-2,1,-1,1,-1,   1,-1,1,-1}; // X (Torre || Caballo || Alfil)
const int Y[] = {1,0,-1,0,  1,1,-1,-1,2,2,-2,-2,  1,-1,-1,1}; // Y (Torre || Caballo || Alfil)

bool tablero[MAXN][MAXN];
int D[MAXN][MAXN][MAXTM]; // distancia dijkstra
int play[MAXN][MAXN][MAXTM]; // jugadas por casillero
int N;

struct nodo {
    int x,y,p; // vertice, puntos penalización
    int jugadas;
    int cnt, mov; // cantidad consec, tipo, mov específico

    bool operator< (const nodo &o) const{
        return o.p < p;
    }
};

bool valid (int &x, int &y) {
    if (x < 0 or y < 0) return false;
    if (x >= N or y >= N) return false;
    return true;
}

int typ (int &i) {
    if (i > 3)
        if (i > 11)
            return 2;
        else return 1;
    else return 3;
}

void bfsDijkstra(int stX, int stY) {
    priority_queue<nodo> Q;
    forn (i,MAXTM) D[0][0][i] = 0;
    Q.push({stX,stY,0,0,0,-1});

    while (not Q.empty()) {
        auto e = Q.top(); Q.pop();

        if (e.x == N-1 and e.y == N-1) return;

        forn (i,MAXTM) {
            int tipo = typ(i), tipo2 = typ(e.mov);
            bool toAdd = (e.mov != i or tipo == 1); // ¿NUEVA JUGADA?
            if (tipo2 == tipo and e.cnt+toAdd > 2) continue; // no más de dos mov. consec

            int tX = e.x+X[i], tY = e.y+Y[i]; // hacia donde voy
            if (!valid(tX,tY)) continue; // que no me pase del tablero
            if (tablero[tY][tX]) continue; // tampoco me tope con un obstáculo
            //cout << e.x << ' ' << e.y << ' ' << e.mov << ' ' << i << ' ' << tipo2 << ' ' << tipo << ' ' << e.cnt+toAdd << ' ' << D[tY][tX][i] << ' ' << e.p + (toAdd ? tipo : 0) << ' ' << tX << ' ' << tY << endl;

            if (D[tY][tX][i] == -1 or D[tY][tX][i] > e.p + (toAdd ? tipo : 0)) { // ¿acorto haciendo así?
                D[tY][tX][i] = e.p + (toAdd ? tipo : 0); // actualizo
                play[tY][tX][i] = e.jugadas+toAdd;
                //fprintf(stdout,"(%d,%d) -> (%d %d) con %d pen y %d jug\n",e.x,e.y,tX,tY,D[tY][tX][i],play[tY][tX][i]);
                Q.push({tX,tY,D[tY][tX][i],play[tY][tX][i],(tipo2 == tipo ? e.cnt+toAdd : 1),i}); // nuevo camino
            }
        }
    }
}

int main() {
    freopen("ajedrez.in","r",stdin);
    freopen("ajedrez.out","w",stdout);

    memset(D,-1,sizeof(D));

    int c;
    scanf("%d %d",&N,&c);

    forn(i,c) { // marco en el tablero los obstáculos
        int y1,x1,y2,x2;
        scanf("%d %d %d %d",&y1,&x1,&y2,&x2);
        x1--; y1--; x2--; y2--;

        if (abs(x1-x2) == abs(y1-y2)) { // diagonal;
            while (x1 != x2) {
                tablero[y1][x1] = true;
                if (x2 >= x1) x1++; else x1--;
                if (y2 >= y1) y1++; else y1--;
            }
            tablero[y1][x1] = true;
        }
        else {
            if (x1 >= x2 and y1 >= y2) {swap(x1,x2); swap(y1,y2);} // coords más altas en x2 e y2
            if (x1 == x2) { // vertical
                while (y1 <= y2) {
                    tablero[y1][x1] = true;
                    y1++;
                }
            }
            else { // horizontal
                while (x1 <= x2) {
                    tablero[y1][x1] = true;
                    x1++;
                }
            }
        }
    }

    /*forn (i,N) {
        forn(j,N) cout << tablero[i][j];
        cout << endl;
    }*/

    bfsDijkstra(0,0);

    int a = INF, b = INF;
    forn (i,MAXTM)
        if (D[N-1][N-1][i] < a and D[N-1][N-1][i] != -1) {
            a = D[N-1][N-1][i];
            b = play[N-1][N-1][i];
        }

    printf("%d %d",b,a);

    return 0;
}

Espero alguien sepa de alguna optimización o mejora de la idea que pueda aplicar. Gracias!

3 Me gusta

Hola, recuerdo tener que luchar un poco con este problema para evitar el TLE. Te dejo una idea que puede serte de ayuda, porque escribir una solución a este problema con lujo de detalle me llevaría unos días.

Veo que hay un paso clave que prácticamente lo tenés en esta idea de reducir el vecindario, pero que por las dudas voy a dejar explícito acá.

Los casos de movimiento de caballo creo que son claros. Pero en el caso del alfil o de la torre, lo que hacemos es pagar el costo solo la primera vez que comenzamos a hacer el movimiento. Si nos estamos moviendo con una torre o con un alfil en una dirección, podemos seguirnos moviendo en esa dirección sin ningún tipo de costo (cambiar de dirección sí tendría costo aún manteniendo la pieza).

Con esta idea en mente, de un paso al siguiente la distancia no varía mucho. Es decir, las aristas solo puede tener pesos 0, 1, 2 o 3. Las aristas de costo 0 aparecen debido a la idea que mencioné antes. Esto se puede aprovechar en lo que conozco como BFS con bolsas, pero desconozco si ese es el nombre oficial de la técnica. Creo que se va a ver mejor cómo se puede utilizar esto a través de un ejemplito.

Imaginate que tenés a los nodos separados por distancias del nodo inicial, digamos l_0, l_1, l_2, l_3, l_4, l_5, \dots. Al comenzar solo está el nodo inicial en l_0. Suponete por ejemplo que usás una arista de costo 2, entonces tenés que agregar el nodo al que llegaste a l_2. La gracia es siempre procesar a los nodos ordenados por distancia al origen (al igual que hace BFS cuando todas las aristas tienen el mismo peso, o Dijkstra, y usa la priority_queue para encontrar el siguiente nodo a procesar), es decir primero vas a procesar a todos los de l_0, después a todos los de l_1, etcétera.

Ahora, ¿hace falta que tengas todas las listas para ir calculando esto? No, la gracia está en que las aristas tienen costo muy pequeño, concretamente 0, 1, 2 o 3. Entonces al ir procesando en orden, hay a lo sumo 4 listas no vacías en todo momento.

Aún con esta idea, me costó que entre en el tiempo límite de ejecución, si ves mi última submission está relativamente claro el código a mi entender. Básicamente lo que termino haciendo es un BFS con 4 bolsas.

Si te fijás, un BFS normal donde todas las aristas tienen el mismo peso, no es ni más ni menos que un BFS de 2 bolsas (que es la forma en la que me gusta pensar el BFS, con dos bolsas). Lo que ocurre es que como en una cola, los nodos nuevos siempre se ubican al final, se terminan procesando en el orden correcto por distancias al origen, y por eso la implementación estándar de un BFS se hace con una cola.

Ya que estoy comento de un caso que vi en varios jueces online, al cual el BFS con 2 bolsas generaliza muy bien, que es uno en que las aristas solo tienen pesos 0 o 1. Este caso sí lo vi con el nombre de BFS-0-1.

Quizá @santo conozca algún otro consejo respecto de este problema.

¡Éxitos!

4 Me gusta

La verdad que está super bien explicado en la respuesta de Guty :smiley:

Comento que justamente en la wiki, BFS está explicado con “dos bolsas” que se reemplazan sucesivamente, la de la “capa actual” y la de la “capa siguiente”.

Y un detalle importante en la implementación, para no tener problemas con la complejidad, es no reprocesar los nodos varias veces: un mismo nodo puede caer en varias bolsas distintas, porque por ejemplo primero se descubre que se puede llegar en 3 pasos y se pone en l_3, pero luego se pone también en l_1 y entonces el procesamiento “verdadero” se tiene que hacer en la primera bolsa que aparece, digamos en la l_1. Cuando se lo encuentra al procesar la bolsa l_3, como es un nodo que ya fue procesado antes, hay que saltearlo (directamente con un if en la implementación, que consulte si ya fue procesado).

Una forma de saber si ya fue procesado es tener un booleano dedicado por cada nodo a guardar esto, y otra muy común es verificar si la distancia actual, que identifica la bolsa, es la que está guardada como mejor distancia de ese nodo: si no lo es, va a significar necesariamente que ya se procesó antes con la distancia verdadera. Por ejemplo al recorrer l_3, veríamos el nodo que ya fue procesado en l_1, y su distancia calculada sería 1 en lugar de 3 y por eso se puede saltear.

Hacer esto garantiza que cada nodo se procesa exactamente una vez, que es fundamental para garantizar la complejidad lineal del algoritmo. Cuidado con una variante similar que existe, que es “solo lo inserto si mejora la distancia”. Esa variante puede reprocesar nodos varias veces (aunque los procesamientos duplicados no van a agregar nada nuevo, porque sí o sí tendrán peor distancia). Esto arruina la complejidad agregando un factor proporcional a la cantidad de bolsas activas.

4 Me gusta

Muchas gracias por las sugerencias! Justamente me había comentado mi profe de olimpíadas también del BFS por bolsas con el nombre de Leveled BFS, y había también probado dicha solución…pero decidí preguntar por mi solución con Dijkstra ya que es la que me había dado mejor puntaje (además que supuse que era posible porque es uno de los tags del problema). De todas formas, ante lo que dicen volví al código que había hecho con dicho método, y empecé a cambiar cosas.

También me fijé en los códigos que ambos subieron y creo entender lo que hacen. Unos cambios que hice ese que antes usaba queue para cada una de las bolsas, pero dado que como método implica ir de a pequeños pasos, es más convenientes usar vector como veo que hicieron, por su comportamiento LIFO (Last In, First Out) para llegar a todo los movimientos posibles en cierta dirección, ya que todos tienen el mismo costo. Otro cambio que hice el cual nunca había usado, que lo veo bastante útil, es meter varios valores dentro de un mismo entero tipo bitmask, para ahorrar espacio y tiempo en el que uno copia el tamaño de cada valor (y especialmente cuando hay mucho espacio desaprovechado si los valores llegan a valores no muy grandes respecto al tamaño que ocupa un entero). También intenté reducir toda acción redundante que estara haciendo, que pueda gastarme tiempo y espacio.

Ahora bien no termino de hacer que entre en tiempo. Ya desde mis primeros códigos usando Leveled BFS, vengo verificando al ir hacia una celda, si no pasé ya por la misma con el mismo movimiento a ella (me refiero por cada una de las 16 posiblidades con cada una de las piezas). Yo marcaba que lo había pasado sólo cuando lo saco de la bolsa para ver por las posiblidades en él. De acuerdo a lo que dijo @santo, también bajó el tiempo de ejecución cuando agregué el check al sacar uno de la bolsa, así evito pasar por aquellos antes procesados con mejor distancia, pues podría haber guardado posibilidades peores en el tiempo anterior a marcar como procesado.

Con todo esto llegué a los 85ptos, siendo 3 casos de prueba que dan TLE/RE. Digo las dos opciones porque depende de la submission. Esto me da la sensación de que puede ser que ese check que hago no es suficiente y hay muchas posibilidades que se me filtran. Pero el problema es que la diferencia de tiempo cuando da TLE respecto a las que dieron bien, me da más la sensación de que, por alguna razón, estoy entrando en un loop infinito. Entre tantas formas de marcar con nodos hechos llegué a una tal que dejaran de dar error en estos casos, y fue marcando cuando llego a un nodo desde otro, en vez de cuando lo proceso realmente. Pero claramente esto no es correcto porque si después descubro una mejor posibilidad, estaría impidiendo guardarla porque marqué como que no debo pasar por ahí de nuevo. En eso, vi que por ejemplo @Guty en su código tiene un segundo check para verificar si pasó cierta posibilidad por la bolsa correspondiente y las subsiguientes, para marcar para adelante a opciones peores que no las necesitas ya que tenés una mejor. Probé de varias formas con este check, pero simplemente no surte efecto y no arregla estos casos.

Realmente no puedo encontrar donde estaría ocurriendo el problema, ya que la única forma que se me ocurre lo del loop es que esté pasando por los mismos en la bolsa 0, que uso para procesar los movimientos intermedios posibles de una torre o alfil en cierta dirección. Pero mi check ocurre al sacar de la bolsa, por lo que siempre en algún punto debería marcar todo y verificar que no repita para no volver a procesar celdas por las que ya pasé (con las mismas condiciones).

Dejo mi código. Está un poco sucio y desordenado de tanto probar, jeje.

Código
#include <bits/stdc++.h>

#define forn(i,n) for(int i = 0; i < int(n); i++)
#define forsn(i,s,n) for(int i = int(s); i < int(n); i++)
#define dforn(i,n) for (int i = int(n)-1; i >= 0; i--)
#define dforsn(i,s,n) for(int i = int(n)-1; i >= int(s); i--)
#define all(c) (c).begin(),(c).end()
#define pb push_back
#define fst first
#define snd second
#define FAST_IO ios::sync_with_stdio(false);cin.tie(nullptr);

using namespace std;
typedef vector<int> vi;
typedef long long ll;
typedef pair<int,int> ii;

const int MAXN = 1010;
const int MAXTM = 16; // máximos tipos de movimientos
const int INF = 1e9+2;
const int M1 = 1e3, M2 = 1e4, M3 = 1e6, M4 = 1e7;

const int X[] = {0,1,0,-1,  2,-2,2,-2,1,-1,1,-1,   1,-1,1,-1}; // X (Torre || Caballo || Alfil)
const int Y[] = {1,0,-1,0,  1,1,-1,-1,2,2,-2,-2,  1,-1,-1,1}; // Y (Torre || Caballo || Alfil)

bool tablero[MAXN][MAXN], D[MAXN][MAXN][MAXTM+1]; // distancia
int N;

struct nodo {
    int v,r1,r2;

    nodo (int x, int y, int p, int jug, int cnt, int mov) {
        v = x + M1*y + M3*cnt + M4*mov; // vertice, cantidad consec, mov específico
        r1 = p; r2 = jug; // puntaje y jugadas
    }
};

bool valid (int &x, int &y) {
    if (x < 0 or y < 0) return false;
    if (x >= N or y >= N) return false;
    return true;
}

int typ (int &i) {
    if (i > 3)
        if (i > 11)
            return 2;
        else return 1;
    else return 3;
}

ii bfs (int stX, int stY) {
    vector< nodo > Q[4];
    forn (i,MAXTM) D[stY][stX][i] = 0;
    Q[0].pb(nodo({stX,stY,0,0,0,MAXTM}));

    int look = 0;
    while (!Q[0].empty() or !Q[1].empty() or !Q[2].empty() or !Q[3].empty()) {
        while (!Q[look].empty()) {
            auto e = Q[look].back(); Q[look].pop_back();

            int x = e.v%M1, y = (e.v/M1)%M1;
            int p = e.r1, jugadas = e.r2;
            int cnt = (e.v/M3)%10, mov = e.v/M4;

            /*if (done[y][x][mov][cnt]) continue;
            done[y][x][mov][cnt] = true;*/

            if (D[y][x][mov]) continue;
            D[y][x][mov] = true;

            if (x == N-1 and y == N-1) return {jugadas,p};

            int tipo2 = typ(mov);
            forn (i,MAXTM) {
                int tX = x+X[i], tY = y+Y[i];
                //if (e.x == 14 and e.y == 1 and e.mov == 0) cout << tX << ' ' << tY << ' ' << D[tY][tX][i] << endl;
                if (!valid(tX,tY)) continue;
                if (tablero[tY][tX]) continue;
                if (D[tY][tX][i]) continue;

                int tipo = typ(i);
                bool toAdd = (mov != i or tipo == 1); // ¿NUEVA JUGADA?

                //if (tY == 2 and tX == 14 and !i) cout << "OK" << e.x << ' ' << e.y << ' ' << e.mov << ' ' << i << ' ' << e.cnt+toAdd << ' ' << D[e.y][e.x][e.mov] << endl;
                //if (e.mov == 0) cerr << "OK" << tipo << ' ' << tipo2 << ' ' << toAdd << ' ' << tX << ' ' << tY << endl;
                if (tipo2 == tipo and cnt+toAdd > 2) continue; // no más de dos mov. consec

                int add = -1;
                if (!toAdd) add = 0;
                else add = tipo;

                //printf(stderr,"(%d,%d) -> (%d,%d) con %d jug y %d pen | mov: %d y ant: %d\n",e.x,e.y,tX,tY,play[tY][tX][i],D[tY][tX][i],i,e.mov);

                nodo toPush = nodo({tX,tY, p + toAdd*tipo, jugadas+toAdd,
                                   (tipo2 == tipo ? cnt+toAdd : 1),i});
                //cerr << toAdd << endl;

                Q[(look+add)%4].pb(toPush);
            }
        }

        look = (look + 1) % 4;
    }
}
/*
void bfsDijkstra(int stX, int stY) {
    priority_queue<nodo> Q;
    forn (i,MAXTM) D[0][0][i] = 0;
    Q.push({stX,stY,0,0,0,-1});

    while (not Q.empty()) {
        auto e = Q.top(); Q.pop();

        if (e.x == N-1 and e.y == N-1) return;

        forn (i,MAXTM) {
            int tipo = typ(i), tipo2 = typ(e.mov);
            bool toAdd = (e.mov != i or tipo == 1); // ¿NUEVA JUGADA?
            if (tipo2 == tipo and e.cnt+toAdd > 2) continue; // no más de dos mov. consec

            int tX = e.x+X[i], tY = e.y+Y[i]; // hacia donde voy
            if (!valid(tX,tY)) continue; // que no me pase del tablero
            if (tablero[tY][tX]) continue; // tampoco me tope con un obstáculo
            //cout << e.x << ' ' << e.y << ' ' << e.mov << ' ' << i << ' ' << tipo2 << ' ' << tipo << ' ' << e.cnt+toAdd << ' ' << D[tY][tX][i] << ' ' << e.p + (toAdd ? tipo : 0) << ' ' << tX << ' ' << tY << endl;

            if (D[tY][tX][i] == -1 or D[tY][tX][i] > e.p + (toAdd ? tipo : 0)) { // ¿acorto haciendo así?
                D[tY][tX][i] = e.p + (toAdd ? tipo : 0); // actualizo
                play[tY][tX][i] = e.jugadas+toAdd;
                //fprintf(stdout,"(%d,%d) -> (%d %d) con %d pen y %d jug\n",e.x,e.y,tX,tY,D[tY][tX][i],play[tY][tX][i]);
                Q.push({tX,tY,D[tY][tX][i],play[tY][tX][i],(tipo2 == tipo ? e.cnt+toAdd : 1),i}); // nuevo camino
            }
        }
    }
}*/

int main() {
    freopen("ajedrez.in","r",stdin);
    freopen("ajedrez.out","w",stdout);

    int c;
    scanf("%d %d",&N,&c);

    forn(i,c) { // marco en el tablero los obstáculos
        int y1,x1,y2,x2;
        scanf("%d %d %d %d",&y1,&x1,&y2,&x2);
        x1--; y1--; x2--; y2--;

        if (abs(x1-x2) == abs(y1-y2)) { // diagonal;
            while (x1 != x2) {
                tablero[y1][x1] = true;
                if (x2 >= x1) x1++; else x1--;
                if (y2 >= y1) y1++; else y1--;
            }
            tablero[y1][x1] = true;
        }
        else {
            if (x1 >= x2 and y1 >= y2) {swap(x1,x2); swap(y1,y2);} // coords más altas en x2 e y2
            if (x1 == x2) { // vertical
                while (y1 <= y2) {
                    tablero[y1][x1] = true;
                    y1++;
                }
            }
            else { // horizontal
                while (x1 <= x2) {
                    tablero[y1][x1] = true;
                    x1++;
                }
            }
        }
    }

    /*forn (i,N) {
        forn(j,N) cout << tablero[i][j];
        cout << endl;
    }*/

    ii rta = bfs(0,0);

    /*forn (i,N) {
        forn(j,N) {
            int val = INF;
            forn (k,MAXTM) if (D[i][j][k] != -1) val = min(D[i][j][k],val);
            cout << (val == INF ? 0 : val);
        }
        cout << endl;
    }*/

    printf("%d %d",rta.fst,rta.snd);

    return 0;
}

Agradecería mucho si alguien puede revisarlo y si se da cuenta dónde estaría el problema.
Igual repito, gracias por la ayuda!

2 Me gusta

Me respondo a mi mismo, jeje. Pero después de un tiempo quise volver a meterme con el problema, y tras haber pensado un poco mejor, y hacer el uso de booleanos (sin mencionar que justo en un momento le pegué con el uso de ciertos “estados” que me llevó hasta los 95ptos), me puse todas las pilas para no rendirme hasta conseguir el AC.

Un poco de spoilers y experiencia con el problema

Me puse a leer nuevamente sus códigos más con detenimiento, y por fin me entró la idea de guardar la mejor distancia, y en la práctica me resultó mucho más sencillo que el uso de booleanos. Creería tal vez podría reemplazar lo mismo en booleanos fijándome tal vez como marcar por el uso de bolsas, que creo es lo que @Guty había hecho (y recién escribiendo también me termina de encajar :sweat_smile: ), pero creo que verlo como distancias es más intuitivo. Con eso me saqué un poco de encima el problema de marcar más de la cuenta cada nodo, considerando como estados ahora sí (me recordó un poco a DP), condiciones de las cuales “tengo la mismas posibilidades de resultados”: D[y][x][move][consec] siendo x e y las coordenadas de cierta casilla del tablero, move el movimiento mediante el cual llego a dicha casilla (que dada la forma en que recorro las posibilidades “de a una casilla por vez”, es lo mismo que decir de qué casilla vengo) y consec sería la cantidad de movimientos realizados hasta este punto, que de hecho podría ser tal vez tranquilamente un bool, considerando que solo puedo tener como máximo dos movimientos, y realmente solo tengo “0 movimientos” al comienzo del BFS.

Y después hay un detalle clave para que funcione y no se pase de tiempo, y es que para move, unificar todas las 8 posibilidades de un caballo, ya que no importa en qué forma haya llegado, si es un caballo tengo igual de posibilidades dados el resto de los “estados”, y esto se debe a que el caballo puedo saltar de un movimiento y no es un movimiento como “de arrastre” que veo de a un paso por vez, como movimientos con la torre o el alfil. Este detalle clave que cuando en un principio leía los códigos pensaba era una cosa menor, fue fundamental para los 100ptos, que de hecho tuve después de una cuantas horas más renegando :stuck_out_tongue:

Nuevamente remarco muchas gracias por las explicaciones, que me vinieron muy útiles para terminar de entender el problema! Y muy bien hechos jodidos los casos de prueba, jeje, no se les escapaban ni un solo error mío. Con todo lo que estuve con este problema, no me lo olvido más para otros similares que se me puedan presentar.

Por si a alguien le viene útil, dejo mi código a continuación:

Código que probablemente tardó más de 24hs totales en la realización
#include <bits/stdc++.h>

#define forn(i,n) for(int i = 0; i < int(n); i++)
#define forsn(i,s,n) for(int i = int(s); i < int(n); i++)
#define dforn(i,n) for (int i = int(n)-1; i >= 0; i--)
#define dforsn(i,s,n) for(int i = int(n)-1; i >= int(s); i--)
#define all(c) (c).begin(),(c).end()
#define pb push_back
#define fst first
#define snd second
#define FAST_IO ios::sync_with_stdio(false);cin.tie(nullptr);

using namespace std;
typedef vector<int> vi;
typedef long long ll;
typedef pair<int,int> ii;

const int MAXN = 1010;
const int MAXTM = 16; // máximos tipos de movimientos
const int MAXIMP = 13;
const int INF = 2e9+2;
const int M1 = 1e3, M2 = 1e4, M3 = 1e6, M4 = 1e7;

const int X[] = {2,-2,2,-2,1,-1,1,-1,   1,-1,1,-1,  0,1,0,-1,}; // X (Torre || Caballo || Alfil)
const int Y[] = {1,1,-1,-1,2,2,-2,-2,   1,-1,-1,1,  1,0,-1,0,}; // Y (Torre || Caballo || Alfil)
//const int op[] = {2,3,0,1,  7,6,5,4,11,10,9,8,    13,12,15,14}; // opuestos mov
const int typ[] = {1,1,1,1,1,1,1,1,     2,2,2,2,    3,3,3,3};

bool tablero[MAXN][MAXN]; int D[MAXN][MAXN][3][MAXIMP]; // distancia
int N;

struct nodo {
    int v,r2;

    nodo (int x, int y, int jug, int cnt, int mov) {
        v = x + M1*y + M3*cnt + M4*mov; // vertice, cantidad consec, mov específico
        r2 = jug; // puntaje y jugadas
    }
};

bool valid (int &x, int &y) {
    if (x < 0 or y < 0) return false;
    if (x >= N or y >= N) return false;
    return true;
}

ii bfs (int stX, int stY) {
    vector< nodo > Q[4];
    //forn (i,MAXTM) forn (j,4) D[stY][stX][j][i] = INF;
    D[stY][stX][0][0] = 0;
    Q[0].pb(nodo({stX,stY,0,0,0}));

    int look = 0, ptos = 0;
    while (!Q[0].empty() or !Q[1].empty() or !Q[2].empty() or !Q[3].empty()) {
        while (!Q[look].empty()) {
            auto e = Q[look].back(); Q[look].pop_back();

            int x = e.v%M1, y = (e.v/M1)%M1;
            int jugadas = e.r2;
            int cnt = (e.v/M3)%10, mov = e.v/M4;

            if (x == N-1 and y == N-1) return {jugadas,ptos};

            int tipo2 = typ[mov];
            if (ptos != D[y][x][cnt-1][(tipo2 == 1 ? 0 : mov-3)]) continue;

            forn (i,MAXTM) {
                int tX = x+X[i], tY = y+Y[i];
                if (!valid(tX,tY)) continue;
                if (tablero[tY][tX]) continue;

                int tipo = typ[i];
                bool toAdd = (mov != i or tipo == 1); // ¿NUEVA JUGADA?

                int add = -1;
                if (!toAdd) add = 0;
                else add = tipo;

                if ((tipo2 == tipo ? cnt+toAdd : 1) > 2) continue; // no más de dos mov. consec

                if ((ptos+add) >= D[tY][tX][(tipo2 == tipo ? cnt+toAdd : 1)-1][(tipo == 1 ? 0 : i-3)]) continue;
                D[tY][tX][(tipo2 == tipo ? cnt+toAdd : 1)-1][(tipo == 1 ? 0 : i-3)] = ptos+add;

                Q[(look+add)%4].pb(nodo({tX,tY, jugadas+toAdd,
                                   (tipo2 == tipo ? cnt+toAdd : 1),i}));
            }
        }

        ptos++;
        look = (look + 1) % 4;
    }
}


int main() {
    freopen("ajedrez.in","r",stdin);
    freopen("ajedrez.out","w",stdout);

    forn(i,MAXN) forn(j,MAXN) forn(k,3) forn(l,MAXIMP) D[i][j][k][l] = INF;

    int c;
    scanf("%d %d",&N,&c);

    forn(i,c) { // marco en el tablero los obstáculos
        int y1,x1,y2,x2;
        scanf("%d %d %d %d",&y1,&x1,&y2,&x2);
        x1--; y1--; x2--; y2--;

        if (abs(x1-x2) == abs(y1-y2)) { // diagonal;
            while (x1 != x2) {
                tablero[y1][x1] = true;
                if (x2 >= x1) x1++; else x1--;
                if (y2 >= y1) y1++; else y1--;
            }
            tablero[y1][x1] = true;
        }
        else {
            if (x1 >= x2 and y1 >= y2) {swap(x1,x2); swap(y1,y2);} // coords más altas en x2 e y2
            if (x1 == x2) { // vertical
                while (y1 <= y2) {
                    tablero[y1][x1] = true;
                    y1++;
                }
            }
            else { // horizontal
                while (x1 <= x2) {
                    tablero[y1][x1] = true;
                    x1++;
                }
            }
        }
    }

    /*forn (i,N) {
        forn(j,N) cout << tablero[i][j];
        cout << endl;
    }*/

    ii rta = bfs(0,0);

    /*forn (i,N) {
        forn(j,N) {
            int val = INF;
            forn (k,MAXTM) if (D[i][j][k] != -1) val = min(D[i][j][k],val);
            cout << (val == INF ? 0 : val);
        }
        cout << endl;
    }*/

    printf("%d %d",rta.fst,rta.snd);

    return 0;
}
2 Me gusta