Programa-Me 2011 Cómo resolver los problemas de Programa-Me Patrocinado por Vicerrectorado de Informatica y Comunicaciones Realizado en IES Antonio de Nebrija. Móstoles 1
Cómo resolver los problemas de Programa-Me 1 Por dónde empezar? Cuando comienza el concurso se da a los participantes el enunciado de un número fijo de problemas (entre 8 y 10) de distinta dificultad. La competición consiste en intentar resolver el mayor número de problemas en el menor tiempo posible. Ganará el equipo que tenga más problemas resueltos correctamente cuando termine el tiempo. Es importante destacar que en caso de empate (dos o más equipos han resuelto el mismo número de problemas) se utiliza el tiempo total para desempatar (también aquí hay que sumar el tiempo por las penalizaciones de envíos incorrectos). Eso se traduce en que es preferible resolver los problemas cuanto antes. Teniendo en cuenta esto, los equipos experimentados en concursos de este tipo suelen comenzar repartiendose los enunciados de los ejercicios para leerselos rápidamente y evaluar de forma rápida cuáles son más fáciles y cuáles son más difíciles. Tras este periodo inicial de lectura, y una vez que se tienen más o menos ordenados los problemas por orden de dificultad, se empieza por el más fácil. De esta forma se pretende tener ejercicios resueltos lo antes posible para, en caso de empate, llevarse la victoria. 2 Qué hacer durante el concurso? Durante el concurso sólo hay un ordenador por equipo, por lo que sólo una persona puede estar tecleando. Es habitual, no obstante, que al menos dos personas estén pensando y programando el ejercicio en cuestión, para evitar errores. Mientras tanto, el otro miembro del equipo puede estar pensando en la solución al siguiente problema, e incluso plantear el programa en un papel para poder trasladarlo más rápidamente al fichero de código fuente cuando el ordenador quede libre. Tened en cuenta que, una vez enviada la solución de un problema, la respuesta de los jueces puede tardar algún tiempo. Mientras tanto el ordenador queda libre y esa tercera persona que estaba pensando en el otro ejercicio puede comenzar a resolverlo (con la ayuda de otro compañero). Es muy normal durante el concurso tener dos o tres problemas abiertos a medio programar. Por ejemplo, si la respuesta de los jueces es negativa y hay algún error, uno de los miembros del equipo puede dedicarse a intentar encontrar el fallo (sobre el papel) mientras los otros dos siguen adelante con la programación de otro ejercicio. 2
3 Cómo son los problemas Los problemas del concurso son de muy distinta índole. Habrá ejercicios cuya única dificultad es la programación mientras que habrá otros donde lo más complicado es saber cómo se resuelve el ejercicio. Por ejemplo, el ejercicio de muestra de los cuadrados diabólicos y esotéricos tiene una solución relativamente sencilla: guardar en una matriz de enteros el contenido del cuadrado y sumar casillas con cuidado para ver si se cumplen las condiciones indicadas. Otros problemas pueden tener una solución corta (desde el punto de vista de programación) pero que es difícil de llegar a ella. El problema de ejemplo de los móviles entra en esta categoría. Una vez entendido el enunciado lo complicado es saber cómo se puede programar para que funcione. Este es un claro ejemplo de enunciado que puede dedicarse a pensar un miembro del equipo mientras los otros dos están utilizando el ordenador resolviendo otro. Nota: Es importante resaltar el hecho de que el juez on-line exige que el main finalice con un return 0. De no hacerse así no acepta el problema y nos notificara que se ha producido un error 4 Entrada de datos Todos los problemas utilizan el mismo esquema: dado un caso de entrada (un cuadrado mágico, la configuración de un móvil, un mensaje encriptado) hay que escribir algo sobre él (el tipo de cuadrado, si está o no equilibrado, el número de vocales). Para que se pueda probar si el programa funciona, éste tendrá que leer numerosos casos de entrada, uno detrás de otro, y dar la respuesta para todos ellos. Para hacerlo, hay dos alternativas: Al principio de la ejecución, el programa lee el número de casos de prueba que se utilizan. El programa va leyendo casos de prueba hasta que se encuentra con un caso de prueba especial (por ejemplo un cuadrado mágico de dimensiones 0,0). Dependiendo de si es una u otra alternativa el esquema general del programa será distinto. Como consejo recomendamos que se utilice una función/método que se encargue de resolver un caso de prueba y que sea llamado desde el programa principal tantas veces como sea necesario. Como ejemplo, una solución en C++ de un problema que comienza con el número de casos de prueba podría tener el siguiente esquema: #i n c l u d e <iostream > using namespace std ; // Resuelve un caso de prueba, leyendo de l a entrada l a // c o n f i g u r a c i o n, y e s c r i b i e n d o l a r e s p u e s t a. 3
void casodeprueba ( ) { //... i n t numcasos ; c i n >> numcasos ; f o r ( i n t i = 0 ; i < numcasos ; i++) casodeprueba ( ) ; return 0 ; Si la entrada del problema en vez de empezar con el número de casos de prueba termina con un caso de prueba especial, podemos utilizar el siguiente: #i n c l u d e <iostream > using namespace std ; // Resuelve un caso de prueba, leyendo de l a entrada l a // c o n f i g u r a c i o n, y e s c r i b i e n d o l a r e s p u e s t a. Si e l caso // de prueba l e i d o es e l que marca e l f i n a l de l a e j e c u c i o n, // l a f u n c i o n devuelve f a l s e para i n d i c a r a l main que hay // que terminar. bool casodeprueba ( ) { //... // Leemos de l a entrada c i n >>... i f ( c a s o e s p e c i a l ) r eturn f a l s e ; //... resolvemos... return true ; bool s e g u i r ; do { 4
s e g u i r = casodeprueba ( ) ; while ( s e g u i r ) ; // Tambien se puede resumir en una unica l i n e a : // while ( casodeprueba ( ) ) ; 5 Ejemplo de ejercicio con casos de prueba limitados Para completar, veamos un ejemplo muy sencillo. Imaginemos que nos piden un problema tan simple como éste 1 : Entrada La primera línea de la entrada contendrá el número de casos de prueba que el programa debe leer. A continuación vendrá uno detraś de otro todos esos casos. Cada uno de ellos consiste en una única línea con un número entero. Salida Para cada caso de prueba el programa escribirá PAR si el caso de prueba es un número par y escribirá IMPAR si el número es impar. Las soluciones en C, C++ y Java aparecen a continuación. Como se ve, en todas ellas se sigue el mismo esquema descrito en la sección anterior. // SOLUCION EN C #i n c l u d e <s t d i o. h> // Resuelve un caso de prueba, leyendo de l a entrada l a // c o n f i g u r a c i o n, y e s c r i b i e n d o l a r e s p u e s t a. void casodeprueba ( ) { i n t num ; s c a n f ( %d\n, &num ) ; i f ( (num % 2) == 0) p r i n t f ( PAR\n ) ; p r i n t f ( IMPAR\n ) ; 1 Los problemas del concurso serán más complicados. Este lo utilizamos como ejemplo para ver cómo sería el esquema de la solución. 5
i n t numcasos ; i n t i ; s c a n f ( %d\n, &numcasos ) ; f o r ( i = 0 ; i < numcasos ; i++) casodeprueba ( ) ; return 0 ; // SOLUCION EN C++ #i n c l u d e <iostream > using namespace std ; // Resuelve un caso de prueba, leyendo de l a entrada l a // c o n f i g u r a c i o n, y e s c r i b i e n d o l a r e s p u e s t a. void casodeprueba ( ) { i n t num ; c i n >> num ; i f ( (num % 2) == 0) cout << PAR\n ; cout << IMPAR\n ; i n t numcasos ; c i n >> numcasos ; f o r ( i n t i = 0 ; i < numcasos ; i++) casodeprueba ( ) ; return 0 ; // SOLUCION EN Java c l a s s s o l u t i o n { 6
s t a t i c java. u t i l. Scanner in ; p u b l i c s t a t i c void casodeprueba ( ) { i n t n ; n = in. nextint ( ) ; i f ( ( n % 2) == 0) System. out. p r i n t l n ( PAR ) ; System. out. p r i n t l n ( IMPAR ) ; p u b l i c s t a t i c void main ( S t r i n g args [ ] ) { in = new java. u t i l. Scanner ( System. in ) ; i n t numcasos ; numcasos = in. nextint ( ) ; f o r ( i n t i = 0 ; i < numcasos ; i++) casodeprueba ( ) ; 6 Ejemplo de ejercicio con casos de prueba ilimitados Los jueces podrían haber puesto el mismo problema pero cambiando el formato de los casos de entrada. En ese caso el enunciado tendría la forma siguiente: Entrada La entrada consistirá en un número indeterminado de casos de prueba. Cada caso de prueba consistirá en un número entero. Los casos de prueba terminarán con el número -1, que marcará el final de la entrada y que no será procesado. Salida Para cada caso de prueba el programa escribirá PAR si el caso de prueba es un número par y escribirá IMPAR si el número es impar. Las soluciones en C, C++ y Java aparecen a continuación. Como se ve, en todas ellas se sigue el mismo esquema descrito más arriba en el documento. // SOLUCION EN C #i n c l u d e <s t d i o. h> 7
i n t casodeprueba ( ) { i n t num ; s c a n f ( %d\n, &num ) ; i f (num == 1) // Marca de f i n de entrada r eturn 0 ; i f ( (num % 2) == 0) p r i n t f ( PAR\n ) ; p r i n t f ( IMPAR\n ) ; return 1 ; while ( casodeprueba ( ) ) ; // SOLUCION EN C++ #i n c l u d e <iostream > using namespace std ; bool casodeprueba ( ) { i n t num ; c i n >> num ; i f (num == 1) r eturn f a l s e ; i f ( (num % 2) == 0) cout << PAR\n ; cout << IMPAR\n ; return true ; 8
while ( casodeprueba ( ) ) ; // SOLUCION EN Java c l a s s s o l u t i o n { s t a t i c java. u t i l. Scanner in ; p u b l i c s t a t i c boolean casodeprueba ( ) { i n t n ; n = in. nextint ( ) ; i f ( n == 1) r eturn f a l s e ; i f ( ( n % 2) == 0) System. out. p r i n t l n ( PAR ) ; System. out. p r i n t l n ( IMPAR ) ; r eturn true ; p u b l i c s t a t i c void main ( S t r i n g args [ ] ) { in = new java. u t i l. Scanner ( System. in ) ; while ( casodeprueba ( ) ) ; 9