sábado, 30 de noviembre de 2013

Calificacion-1. Detector de cuadrados

Enunciado original
No viene mal comenzar el concurso ganando un poco de confianza, así que bienvenido sea un problema fácil. Un primer paso en el apasionante campo del reconocimiento de formas que a su vez entra en el mundo de la visión artificial.
Traduciendo el enunciado: vamos a tener una cuadrícula con cuadros blancos y negros. Se trata de decir si todos los cuadros negros forman un cuadrado relleno.
El tamaño del tablero máximo que nos proponen (20x20) es suficientemente pequeño como para almacenarlo por completo en memoria así que, en principio, lo más sencillo parece tener este tablero en memoria para trabajar con él.
Vamos a comenzar con una versión previa que lea los datos y los guarde en memoria. Nos quedaría algo así:

#include <iostream>

int main()
{
 bool tabla[21][21]; // Fila-columna adicional para no comprobar bordes
 int T,n,i,j;
 char buffer[21];

 std::cin >> T;
 for (int t=1; t<=T; t++)  // casos
 {
  int nsq=0;  // n. pixels black
  std::cin >> n;  // input
  for (i=0; i<n; i++)
  {
   std::cin >> buffer;
   for (j=0; j<n; j++)
   {
    tabla[i][j]=(buffer[j]=='#');
    if (tabla[i][j]) nsq++;
   }
   tabla[i][n]=false;
  }
  for (j=0; j<=n; j++)
   tabla[n][j]=false;
  bool result=false;
std::cout << "Case #" << t << ": " << (result ? "YES" : "NO") << std::endl;
 }
 return 0;
}

Necesitamos las librerías de entradas/salidas basadas en los streams de C++, así que cargamos sus definiciones con la instrucción include.
Para almacenar las casillas de la cuadrícula he elegido un array de dos dimensiones de valores bool puesto que cada casilla puede tener dos valores (blanco o negro) que asimilaremos por false o true. He asignado una fila y una columna adicional en la cuadrícula (21x21 en lugar de 20x20) para simplificar el algoritmo, pero esto lo explicaré más adelante.
El primer dato que vendrá es el número de casos a analizar, así que lo leemos en la variable T y escribimos un bucle que se repita para cada caso.
Dentro de ese bucle leemos los datos de cada caso. El primer dato que nos vendrá va a ser la dimensión de la cuadrícula, la almacenamos en la variable n. Luego n filas de n caracteres con los cuadros blancos y negros. Los leemos en un bucle controlado por la variable i.
Leeremos cada fila de una vez en el array buffer, dimensionado para poder almacenar los 20 caracteres que como máximo formarán la fila más uno adicional para el carácter especial 0x00 de fin de cadena. Convertimos la secuencia de caracteres leída en los valores bool que guardamos en el array tabla en un bucle anidado controlado por la variable j y vamos dando el valor true a las casillas ocupadas por un pixel negro. Al final de cada fila agregamos un pixel adicional a valor false (blanco) y después de leer la última fila colocamos una fila adicional de pixels blancos.
Además, al principio del primer bucle asignamos el valor cero a la variable nsq. Cada vez que almacenemos un valor true en tabla incrementaremos esta variable nsq con lo que al final en ella tendremos el número de pixels negros de la cuadrícula.
Terminamos el bucle con lo que será el principio y el final del algoritmo de los casos. El principio: definimos una variable de tipo bool en la que calcularemos el resultado del algoritmo (true si los pixels forman un cuadrado, false en caso contrario). El final: el fichero de salida donde convertimos el valor bool de result a los textos YES o NO como nos especifican.

El algoritmo para saber si todos los pixels negros forman un cuadrado es muy sencillo: hay que localizar el primero y a partir de ese comprobar que horizontal y verticalmente todos los pixels negros estén juntos, la longitud horizontal y vertical sea la misma y no haya pixels negros fuera de esta zona.
Localizar el primer pixel negro se reduce a ir comprobando los valores de tabla por filas hasta encontrar el primer valor true. Necesitaremos dos bucles anidados, así:

int x,y;
for (i=0; !result && (i<n); i++)  // localizar esquina
    for (j=0; !result && (j<n); j++)
    {
        result=tabla[i][j];
        if (result)
        {
            x=i; y=j;
        }
    }

En el momento que encontremos el primer pixel negro colocaremos true en la variable result que como está incluida en las condiciones de finalización del bucle hará que se terminen los bucles anidados. Guardamos en las variables x e y la localización de la esquina del supuesto cuadrado.
A partir de esta esquina podríamos ir avanzando hacia el final de la fila para ir contando pixels negros consecutivos. Con el primer pixel blanco terminaríamos la búsqueda y ya sabríamos el número de pixels que tendría el lado del supuesto cuadrado.
Sin embargo, pensando un poco, ya sabemos qué lado tiene que tener el supuesto cuadrado. Para eso hemos contado los pixels negros que tenía nuestra cadrícula. Si todos los pixels negros forman un cuadrado, éste tendrá de lado la raíz cuadrada del número de pixels. Geometría elemental.
Necesitaremos incluir la cabecera de la librería que contiene las funciones matemáticas:


#include <math.h>

Y entonces, al comienzo del algoritmo calcularemos directamente el lado esperado:


int lado=sqrt(nsq);  // lado esperado del cuadrado
Donde sqrt es la función que devuelve la raíz cuadrada de un número. Si el número de pixels no es un cuadrado perfecto ya es imposible que formen un cuadrado por lo que sería una buena idea antes de comenzar ningún bucle comparar esto. Encerramos los bucles de antes (y los que haremos a continuación para comprobar que todos los pixels están formando un cuadrado) en la siguiente comparación:


if ((lado>0) && ((lado*lado)==nsq))  // num pixels correctos
{
}
Sabiendo el lado y que el número de pixels puede ser correcto y teniendo localizada la esquina del supuesto cuadrado sólo queda, con otros dos bucles anidados, muy similares a los primeros, que todos los pixels que forman el hipotético cuadrado son negros:




for (i=0; result && (i<lado); i++)  // comprobar relleno
  for (j=0; result && (j<lado); j++)
    result=tabla[i+x][j+y];
Asignamos los valores del array tabla a la derecha y hacia abajo una distancia de lado a partir del punto (x,y) a la variable result. Como antes, en cuanto se asigne el valor false los bucles terminarán. Para que el cuadrado sea perfecto deben terminarse todas las iteraciones sin que esto ocurra.
Y eso es todo el algoritmo. ¿Pero por qué no he comprobado en ningún momento que no me salía de la cuadrícula? Pues gracias a que he añadido esa falsa fila y columna a mi cuadrícula con todos los valores a false. En el momento que la comparación alcanzara estas casillas, que quedan fuera de la cuadrícula real, el valor va a ser false y terminará el bucle. Siempre que tengo tableros intento usar esta estrategia para eliminar un montón de comparaciones con los bordes del tablero.
Este es el programa completo y aquí el fichero de entrada del concurso.

Esta fue mi implementación, pero ¿podría haberse hecho sin usar el array tabla?. La respuesta es sí. Pensad un poco sobre cómo...
¿Una ayuda? Si tenemos contados los pixels negros y pueden formar un cuadrado y hemos calculado su lado nos basta con sacar las coordenadas mínima y máxima horizontal y vertical de todos los pixels negros. La diferencia entre la máxima y la mínima horizontal y vertical tiene que ser lado-1 para que formen un cuadrado. Así que nos sobra con definir cuatro variables int e ir comparando las coordenadas de los pixels negros que vayamos leyendo para localizar los valores extremos, sobrándonos el array tabla.
Siempre suele haber un algoritmo mejor que el nuestro... lo importante es que el nuestro funcione suficientemente bien.

lunes, 25 de noviembre de 2013

Facebook Hacker Cup 2014. Ronda de calificación

Me encanta la mecánica de este concurso de programación porque es estresante, vertiginosa y ¡eliminatoria!
Y tengo que reconocer que aunque me encante no ha conseguido pasar ningún año de la segunda ronda.
La mecánica es simple:
La competición se organiza en sucesivas rondas para cada una de las cuales se da menos tiempo para resolver problemas más complejos y con una mayor exigencia en los requerimientos para no ser eliminados.
En cada ronda se presentan un conjunto de problemas a resolver mediante un programa del que se proporciona el enunciado y un caso de prueba con la solución (que no resuelto) normalmente explicada. El programa debe leer un fichero con los datos de entrada del problema y escribir un fichero de salida con las soluciones a los datos de entrada ambos ficheros de texto con un formato claramente especificado en el enunciado.
Tú tienes todo el tiempo que desees, dentro de la duración de la ronda, para resolver cada problema con los datos de prueba y los datos que tú te prepares para probar tu programa y una vez que consideras que tu programa está listo y resuelve el problema propuesto en un tiempo aceptable y considerando todos los casos imaginables te descargas el archivo real con los datos de entrada del problema de concurso donde las malvadas mentes promotoras del concurso han pensado los casos más rebuscados para que tu aplicación falle y se inicia una cuenta atrás de seis minutos en los que tienes que ejecutar el programa contra los datos del fichero descargado, corregir los errores que pueda tu programa tener al presentar casos con los que no habías contado y enviar el fichero generado por tu programa con los resultados y el fuente del programa que lo resolvió. Si el programa funciona bien a la primera y el algoritmo es suficientemente eficiente para correr en los digamos cinco minutos que te quedan para cargar y descargar archivos bien. Si el algoritmo no es eficiente y se acaba el tiempo mal. Si el programa te lanza un error entonces a la carrera tratar de ver qué ha pasado, corregirlo y volver a ejecutar, aquí está la parte estresante. Si se acaba el temporizador ya no tienes una segunda oportunidad y este problema queda sin resolver.
En cada ronda se asigna una puntuación a cada problema que depende de la dificultad del mismo, a criterio de los organizadores del concurso, obviamente. El orden de cada participante en la ronda se obtiene, en primer lugar, por la suma de los puntos obtenidos por los problemas enviados con una solución correcta. Los participantes empatados a puntos se ordenan por la suma del tiempo empleado en la resolución de los problemas (evidentemente mejor clasificación cuanto menor tiempo acumulado) computada como el tiempo desde que comenzó la ronda hasta el envío de cada una de las soluciones correctas.
La primera ronda, llamada de calificación, es la más sencilla de pasar. Tiene un plazo de 72 horas para resolver tres problemas propuestos y, para pasar esta ronda no importan ni los puntos obtenidos ni el tiempo empleado. Sólo es necesario resolver bien uno de los problemas para seguir adelante.
Las rondas se organizan en fin de semana (como no podía ser de otra manera para no penalizar a los afortunados que aún tenemos trabajo) por lo que necesitas un punto de sacrificio para sacar tiempo de ocio. Yo tengo que reconocer que fui un pelín vago y no me preocupé de solucionar más que los dos primeros problemas propuestos, como veréis sencillos, y ni pensé en el tercero (aunque voy a tratar de resolverlo, prometo). Así quedé clasificado en la posición 2369 de esta ronda con 55 puntos y 133 horas de tiempo (ya podéis imaginar que no empecé a resolverlos al comienzo de la ronda). Pasaron la ronda 6242 personas de las 7515 que presentaron soluciones.
Desde este vínculo podéis acceder a los tres problemas de la ronda