Problema rapigrama - OIAJ

Hola, vuelvo a pasar por acá para hacer un pregunta sobre un problema del juez de la OIA. Quise ponerme a hacer el problema Juego con Letras, del Certamen Nacional 2011, de nivel 2 (http://juez.oia.unsam.edu.ar/#/task/rapigrama/statement)
Después de un par de intentos logré a llegar hasta los 65/100 ptos, siendo dos los casos en los cuales mi solución no funciona (siendo uno de ellos el de amyor puntaje).
Para resolver el problema, realizo lo siguiente:

  • Dadas las palabras, guardo la primera letra de las mismas en un set. A su vez, asigno para cada una de tales letras, el prefijo formado por las dos primeras letras de la palabra. Finalmente asigno según tales prefijos, una lista con las palabras originales para cada uno (comprendo que podría hacer un set con todas las palabras, pero resulta que hacer esto ahorra un poco de tiempo de ejecución). Además, para luego poder dar el resultado, hago un mapa donde guardo los índices de las palabras. Como pueden repetirse, por cada una le asigno un vector con los índices de todas las existentes.
  • Recorro el tablero fijándome si alguna de las letras coincide con el comienzo de alguna de mis palabras.
  • Si encuentro una letra que potencialmente sea el comienzo de alguna palabra de la lista, me fijo hacia cada uno de los sentidos de la misma, por la letra adyacente. Si el prefijo formado por la letra en mi posición y el de aquella adyacente que encuentre, continúo en la dirección que la encontré.
  • A medida que avanzo por la dirección mencionada, voy formando una palabra agregando cada letra por la que paso. En cada vuelta, verifico si la palabra que tengo hasta el momento forma parte de mi lista. Paro de buscar cuando llego a alguno de los bordes de la cuadrícula (he pensado en dejar de buscar si lo que tengo no coincide, aunque no sé cómo podría implementar eso, además de que ocuparía bastante memoria).
  • En caso de encontrar una palabra, guardo los índices según las palabras (descartando luego ese índice de mi lista por la palabra) y la dirección. Cuando termino de buscar, ordeno los resultados en función de los números. Ya ordenados, los imprimo en pantalla.

Tengo entendido que debe de haber un problema en la implementación. Espero me puedan ayudar. Dejo mi código:

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

#define forn(i,n) for(int i = 0; i < int(n); i++)
#define fst first
#define snd second

using namespace std;
typedef vector<string> vs;
typedef vector<int> vi;
typedef pair<int,int> ii;

const int X[] = {0,-1,0,1};
const int Y[] = {1,0,-1,0};

struct sorter {
    int num;
    char d;
    bool operator<(const sorter &a) const{
        return a.num > num;
    }
};

vs rapi;
set<char> yes;
map<char,set<string> > prim;
map<string, set<string> > sec;
map<string, vi> refers;
map<ii,char> dir;
vector<sorter> resultado;

void init();
char way(int &x, int &y, int &posX, int &posY);
bool valido(int &x, int &y);

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int D,P;
    freopen("rapigrama.in","r",stdin);
    freopen("rapigrama.out","w",stdout);
    init();

    cin >> D >> P;

    forn (i,D) {
        string x;
        cin >> x;
        rapi.push_back(x);
    }
    forn (i,P) {
        string x,aux;
        cin >> x;
        aux+=x[0];
        aux+=x[1];
        prim[x[0]].insert(aux);
        sec[aux].insert(x);
        refers[x].push_back(i+1);
        yes.insert(x[0]);
    }
    forn (m,rapi.size()) {
        forn (n,rapi[0].size()) {
            char j = rapi[m][n];
            if (yes.count(j)) {
                forn (i,4) {
                    string aux;
                    aux+=j;
                    int tendX = X[i];
                    int tendY = Y[i];
                    int x = n+tendX;
                    int y = m+tendY;
                    if (!valido(x,y)) continue;
                    aux+=rapi[y][x];
                    if (prim[j].count(aux)) {
                        string act = aux;
                        while (true) {
                            if (sec[act].count(aux) and not refers[aux].empty()) {
                                int m = refers[aux].back();
                                refers[aux].pop_back();
                                resultado.push_back({m,way(tendX,tendY,x,y)});
                                break;
                            }
                            char direct = way(tendX,tendY,x,y);
                            if (not valido(x,y)) break;
                            aux += rapi[y][x];
                        }
                    }
                }
            }
        }
    }
    sort (resultado.begin(),resultado.end());
    for (auto &i : resultado) {
        cout << i.num << ' ' << i.d << '\n';
    }
    return 0;
}

void init() {
    dir[{0,1}] = 'S';
    dir[{-1,0}] = 'O';
    dir[{0,-1}] = 'N';
    dir[{1,0}] = 'E';
}

char way(int &x, int &y, int &posX, int &posY) {
    char c = dir[{x,y}];
    switch (c) {
        case 'N' : posY--; break;
        case 'S' : posY++; break;
        case 'O' : posX--; break;
        case 'E' : posX++; break;
    }
    return c;
}

bool valido (int &x, int &y) {
    if (x < 0 or y < 0) return false;
    if (y >= rapi.size() or x >= rapi[0].size()) return false;
    return true;
}
2 Me gusta

Buenas Ariel!

  • Recorro el tablero fijándome si alguna de las letras coincide con el comienzo de alguna de mis palabras.
  • Si encuentro una letra que potencialmente sea el comienzo de alguna palabra de la lista, me fijo hacia cada uno de los sentidos de la misma, por la letra adyacente. Si el prefijo formado por la letra en mi posición y el de aquella adyacente que encuentre, continúo en la dirección que la encontré.

Pensá el caso donde el tablero sea de 100x100 y todas las letras son iguales (todas ‘a’).
Si querés buscar el string:

aaaaaaaaaaaaaaaaaaab

todas las posiciones serán inicios válidos. Probarías buscar el string empezando en cada una de las posibles 10.000 celdas del tablero. Cada inicio válido te forma un string de tamaño 20 como máximo. Así que hasta acá, para buscar esta única palabra, tenemos 10.000 inicios, y de 1 a 20 letras cada string, son casi 200.000 strings. Si además accedemos al map, con cada uno de estos 200.000 strings, vamos a tener un factor log(tamañoMap) extra. Todo esto solo para buscar esta palabra, que nunca será encontrada porque al final tiene una ‘b’. Buscar strings en un map tiene un costo extra, determinado por la longitud del string.

La complejidad a grandes rasgos, sería algo así como:

O(cantPalabras * n^2 * 20 * log(n^2*20))

Que en el peor caso que propuse serían 35.000.000.000 operaciones, sin contar las constantes de map, pero tampoco contando bien la cantidad de strings, está todo medio estimado pero creo que mas o menos es así.

Leí en codeforces que un juez puede procesar en promedio 200.000.000 operaciones por segundo, asi que daría TLE o error de memoria, lo que ocurra primero.

Espero haber entendido bien tu idea :thinking: :sweat_smile:

Saludos!
Gastón

2 Me gusta

Oh wow, gracias por responder. No me había puesto a pensar en la complejidad. Es cierto que la idea se puede mejorar pero, de todas formas, y a pesar de los errores en los casos mencionados, en ningún momento ni se pasa de tiempo, ni tira error de memoria o similar. Por el contrario, tarda máximo 0.276s en el peor de los casos, aunque con errores (por lo que no sé si creer ese tiempo o no). Algo que noté particularmente es que este problema te informa sobre el error con algo de detalle, de la forma como Se esperaba 61 y se encontro 99, y no sé si eso afecta o no informa sobre otros errores, como de memoria.

Si decubrí que, lo que en un principio quería hacer y no sabía cómo implementar, se lo puede hacer con un Trie, a partir de hacer un árbol descomponiendo las palabras según letras, y es mucho más efectivo que el método original. Probé con el modelo del foro y algunos cambios, y llega con nada más 0.04s, pero aún da error en los mismos casos de antes.
A esto lleva la conclusión que hay un pequeño problema de implementación. Dejo mi código ahora modificado, por si lo podés revisar bien:

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

#define forn(i,n) for(int i = 0; i < int(n); i++)
#define fst first
#define snd second

using namespace std;
typedef vector<string> vs;
typedef vector<int> vi;
typedef pair<int,int> ii;

const int MAXN = 100000;
const int X[] = {0,-1,0,1};
const int Y[] = {1,0,-1,0};

struct sorter {
    int num;
    char d;
    bool operator<(const sorter &a) const{
        return a.num > num;
    }
};

int prox; // indice disponible
int sig[MAXN][26]; // posibles letras minúsculas
int prefijos[MAXN][26];
bool final[MAXN]; //lugares donde terminan palabras

void agregar(const string &s, int raiz = 0){
    int actual = raiz;
    forn(i, s.size()){
        int letra = s[i] - 'a';
        if(sig[actual][letra] == -1)
            sig[actual][letra] = prox++;
        actual = sig[actual][letra];
        prefijos[actual][letra]++;
    }
    final[actual] = true;
}

pair<int,char> buscar(const string &s, int raiz = 0){
    int actual = raiz;
    int pref;
    forn(i, s.size()){
        int letra = s[i] - 'a';
        if(sig[actual][letra] == -1)
            return {-1,'N'};
        actual = sig[actual][letra];
        pref = prefijos[actual][letra];
    }
    if(final[actual]) return {actual,'Y'};
    else return {pref,'P'};
}

vs rapi;
set<char> yes;
map<char,set<string> > prim;
map<string, set<string> > sec;
map<string, vi> refers;
map<ii,char> dir;
vector<sorter> resultado;

void init();
char way(int &x, int &y, int &posX, int &posY);
bool valido(int &x, int &y);

int main() {
    memset(sig, -1, sizeof(sig));
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cout.tie(nullptr);

    int D,P;
    freopen("rapigrama.in","r",stdin);
    //freopen("rapigrama.out","w",stdout);
    init();

    cin >> D >> P;

    forn (i,D) {
        string x;
        cin >> x;
        rapi.push_back(x);
    }
    forn (i,P) {
        string x, aux;
        cin >> x;
        aux+=x[0];
        aux+=x[1];
        agregar(x);
        prim[x[0]].insert(aux);
        refers[x].push_back(i+1);
        yes.insert(x[0]);
    }
    forn (m,rapi.size()) {
        forn (n,rapi[0].size()) {
            char j = rapi[m][n];
            if (yes.count(j)) {
                forn (i,4) {
                    string aux;
                    aux+=j;
                    int tendX = X[i];
                    int tendY = Y[i];
                    int x = n+tendX;
                    int y = m+tendY;
                    if (not valido(x,y)) continue;
                    aux+=rapi[y][x];
                    if (prim[j].count(aux)) {
                        while (true) {
                            pair<int,char> t = buscar(aux);
                            if (t.snd == 'N') break;
                            if (t.snd == 'Y' and not refers[aux].empty()) {
                                int m = refers[aux].back();
                                refers[aux].pop_back();
                                resultado.push_back({m,way(tendX,tendY,x,y)});
                                break;
                            }
                            char direct = way(tendX,tendY,x,y);
                            if (not valido(x,y)) break;
                            aux += rapi[y][x];
                        }
                    }
                }
            }
        }
    }
    sort (resultado.begin(),resultado.end());
    for (auto &i : resultado) {
        cout << i.num << ' ' << i.d << '\n';
    }
    return 0;
}

void init() {
    dir[{0,1}] = 'S';
    dir[{-1,0}] = 'O';
    dir[{0,-1}] = 'N';
    dir[{1,0}] = 'E';
}

char way(int &x, int &y, int &posX, int &posY) {
    char c = dir[{x,y}];
    switch (c) {
        case 'N' : posY--; break;
        case 'S' : posY++; break;
        case 'O' : posX--; break;
        case 'E' : posX++; break;
    }
    return c;
}

bool valido (int &x, int &y) {
    if (x < 0 or y < 0) return false;
    if (y >= rapi.size() or x >= rapi[0].size()) return false;
    return true;
}

PD: El código es un poco un lío a este punto, después haré el esfuerzo de acomodarlo a algo más prolijo.

Ah claro, si te da ese error es porque hay palabras que están en el tablero y no las encontrás. Puede que sí haya un error de código o lógica en ese caso. Encontré un caso donde no responde bien:

4 10
palo
alba
mala
epic

pala
palo
palo
pala
pala
ala
pame
palb
pla
alo

Aparentemente hay un problema cuando te preguntan el mismo string varias veces.
¡Lo del Trie va mejor :muscle:! Aunque tendrías que tener en claro qué cosas ponés y qué cosas buscás.

Saludos!

2 Me gusta

Muchas gracias por el caso! Por fin lo pude resolver. Resulta por un lado que, si encontraba una palabra, solo guardaba el resultado para una de las repetidas que me dio y no todas, como es mucho más fácil. Además resulta que si tenía un caso con palabras superpuestas como:

5 2
uyioo
holas
uiols
kcsug
qwhae

hola
holas

Resulta que al estar hola y holas superpuestas, al encontrar la primera, dejaba de buscar, y así nunca encontraba la segunda, por lo que tuve que cambiar a que dejar de buscar esté solo dictado por estar fuera de rango o por el contador de prefijos del Trie. Por demás de todo eso, usaba la función para avanzar en una misma dirección como forma de obtener mi sentido, que solo hacía que en el caso mencionado, después de guardar la palabra, se vaya desfasando y que sacar el stop que antes hacía, de nada útil.

Reitero, gracias por la ayuda!

3 Me gusta