Estrategias para entrenar
Está claro que para competir en un concurso, se debe entrenar primero. Por competir no debemos entender simplemente atender al evento, sino tener posibilidades reales de ganarlo. Y para ganar un concurso de nivel competitivo alto, no sólo hace falta entrenar, sino que hay que hacerlo de una manera disciplinada, constante y eficiente.
Al entrenar para un concurso de programación se deben practicar todos los aspectos involucrados. Se deben entrenar la velocidad a la que se resuelven los problemas, se deben estudiar algoritmos más comunes, hay que ir creando un formulario, foguearse en concurso en línea, estudiar estrategias, aprender a programar en papel, entre otros.
Existen varios puntos importantes de entrenar a través concursos en línea, y para poder aprovecharlos se deben tomar con una importancia similar a los concursos internos o regionales. Entre los aspectos más importantes se encuentran:
- Acostumbrarse a estar programando bajo presión por períodos prolongados (incrementar la estamina).
- Conocer la forma óptima en la que trabaja el equipo.
- Revisar la eficacia del formulario (ver si los temas que contiene son importantes).
- Saber como afrontar situaciones adversas (como solucionar los errores de programación y estrategia, como mantenerse templado bajo presión, etc.).
- Conocer el nivel de otros equipos de la misma región.
Al igual que en las competencias deportivas, los entrenamientos deben procurar ser continuos y planeados. Sobre-entrenar poco antes de los concursos generalmente no es aprovechable y puede llegar a ser contraproducente. Idealmente, se deben entrenar dos o tres horas diarias entre semana, aprovechando los fines de semana para concursos en línea y para investigar nuevos algoritmos.
Algo paradójico es que durante un concurso no se debe pensar mucho. Si alguien se encuentra bien preparado, al momento de competir ya está plenamente familiarizado con todos los aspectos que se pueden presentar. Obviamente, es imposible predecir todo lo que va a acontecer durante el concurso, pero entre más cosas se puedan dar por resueltas de antemano, más tiempo se tiene para pensar en el resto.
El aspecto más importante para tener que pensar menos es la experiencia, pero en ocasiones es difícil poder recordar todo lo que se ha entrenado. Para esto se debe utilizar el formulario, donde lo más conveniente es contar con los algoritmos más importantes, complementados con una breve descripción de lo que hacen, cuales son sus entradas, sus salidas y su complejidad.
Es importante notar que el formulario debe ser un complemento a la experiencia, y no un sustituto. No sirve de mucho tener el algoritmo que necesitas cuando no sabes como funciona. Generalmente, en un concurso no hay problemas que se resuelvan exactamente igual al del formulario, por lo que o se tiene que empezar el problema desde cero, o se tiene que adaptar un algoritmo del formulario. Por trivial que sea la modificación, como puede ser un cambio en la inicialización de las variables, al no conocer el funcionamiento del código, no es posible realizarlo. Durante el concurso no es momento para aprender como funcionan los algoritmos.
Existen tres puntos clave al momento de concursar, los cuales son: tener conocimiento pleno de los algoritmos requeridos y la habilidad para aplicarlos a los problemas, poder codificar y debugear correctamente, y aprovechar al máximo los tres integrantes del equipo.
Una vez que se tiene un conocimiento amplio en algoritmia y programación, poder mejorar en estos ámbitos se vuelve cada vez más tardado y complicado, por lo que se llega al punto donde conseguir acoplarse como equipo puede ser lo que defina el lograr mejores lugares.
Al trabajar en equipo es necesario tener una estrategia planeada y tratar de seguirla. Al momento de planear la estrategia se deben tomar en cuenta las características de cada miembro. Aunque se espera que los tres miembros tengan un entendimiento, aunque sea básico, de los temas más importantes de algoritmia y programación, cada uno va a tener preferencias y aptitudes hacia algún tipo específico de problemas. Conocer esto ayuda a saber en que forma se van a repartir los problemas, como se va a acomodar el equipo y que temas son convenientes entrenar individualmente.
A parte de conocer las fortalezas y debilidades de cada quien como programadores, también es importante tener un entendimiento a nivel personal. Muchas veces los equipos inexpertos tienden a perder el tiempo por no tener la confianza como para ponerse a discutir la forma en que se deben resolver los problemas o para definir la estrategia a seguir, mientras que también existen los casos opuestos en que miembros del equipo entran en conflicto y no tienen la experiencia como para poder resolverlos de forma rápida y eficiente.
Algo importante es que la estrategia es algo dinámico que se va perfeccionando a través de la práctica. Poniéndola en uso solamente durante los concursos regionales no se puede llegar al refinamiento necesario. Se debe tomar en cuenta la mayor cantidad de variables posibles, desde como acomodar los formularios y demás material, pasando por la forma en que se repartirán los problemas y cuales se atacarán primero, hasta saber la forma en que se resolverán los inconvenientes (como cuando una solución no es aceptada) y poder acoplar la estrategia a la situaciones que se presenten durante el concurso.
Además de contar con un formulario, también puede ser útil contar con una lista de posibles fallas. De manera similar al formulario, son detalles que se van adquiriendo con el correr del tiempo, principalmente a través de tropiezos propios, y que en momentos de presión puede ser útil tenerlos impresos. En esta lista se pueden incluir cosas como especificaciones del compilador y fallas conocidas, problemas que hayan tenido concursantes en ediciones anteriores, errores comunes por lo que problemas resultan no aceptados, entre otras. Sobre el último punto, se incluyen algunas recomendaciones al respecto posteriormente en esta misma sección.
Para poder estar en óptimas condiciones para el concurso, se necesita estar leyendo continuamente libros de programación, estructuras de datos y matemáticas, para mejorar la capacidad de resolución de problemas. También es conveniente leer ocasionalmente libros que puedan ayudar a crear mejores estrategias, como lo puede ser libros de ingeniería industrial (optimización de recursos) y de filosofía (como el arte de la guerra). Por último, también existen factores como la alimentación y el ejercicio, en los que es difícil dar una estrategia porque son aspectos que varían de persona a persona, pero que es importante notar es que para tener un óptimo rendimiento mental, se debe tener una buena salud física.
Es probable que la capacidad innata de las personas en distintas partes del mundo se distribuya de manera similar. Si en países como Rusia y China hay tantos buenos programadores, no necesariamente todos son más inteligentes que los mexicanos, sino que se tienen mejores métodos de aprendizaje y una preparación más amplia.
Estrategias para concursar
Antes de que inicie el concurso es recomendable contar con todos los elementos necesarios y situarlos de forma que sean accesibles sin que lleguen a estorbar. Lo más importante es tener el formulario acomodado, siendo también recomendable llevar papel en blanco, papel para graficar, lápices y marcadores. Aunque en el concurso existe un área de refrigerios, es recomendable también llevar comida por cuenta propia, principalmente chocolate negro y nueces (o similares), que proporcionan energía a corto plazo para pensar mejor.
Aunque en el mundial y en algunos regionales (cada vez más), ya no es posible ingresar libros, en caso de estar en un concurso que si lo permita, también se debe buscar la forma de ubicarlos. Libros que pueden resultar útiles son de programación, de matemáticas (teoría de números, análisis numérico, probabilidad, de fórmulas, etc.), de algoritmos (como el Cormen o el Sedgewick) y un diccionario inglés-español.
Una vez que inicia el concurso, dos miembros pueden ir leyendo los problemas mientras que el tercero crea todos los archivos *.pas o *.c, y copia todos los ejemplos de entrada a sus respectivos *.txt. Una vez que los dos primeros terminan de leer, le pasan al que está en la computadora todos los algoritmos que creen que van a utilizar para que los vaya capturando. Mientras, los otros dos clasifican los problemas por dificultad. Si se dan cuenta que un problema se puede resolver rápidamente (generalmente hay uno o dos de consolación, o alguno que salga directo del formulario), el de la computadora puede ir resolviéndolo.
Al crear los *.pas o *.c, se pueden ir incluyendo datos que sean comunes en la mayoría de los problemas. Por ejemplo:
Una vez hechos el o los problemas inmediatos, el que estaba de capturista puede ponerse a leer los problemas. En cuanto los tres miembros han leído todos los problemas, estos se deben ordenar por dificultad y se les puede asignar una respuesta tentativa.
Dado que el tiempo es un factor determinante en este tipo de concursos, todos los competidores intentan resolver cualquier problema lo más rápido posible. Esto hace que se presionen y lean superficialmente los problemas, o inclusive, que intenten adivinar lo que preguntan. No leer cuidadosamente conduce a errores de interpretación, a saltarse problemas fáciles, a no entender detalles del problema que pueden cambiar la dificultad del mismo, entre otros.
Antes de empezar con cualquier problema, es mejor tomarse unos cuantos minutos para entender el problema en su totalidad. No es conveniente dejar a la suposición o a la casualidad. Mientras el problema no lo especifique, no se puede dar algo por hecho. Si surge cualquier duda en la descripción de algún problema, es conveniente preguntársela a los jueces desde el principio aunque no se tenga contemplado resolver el problema. También es útil marcar o subrayar toda la información importante al momento de leer el problema, sobre todo las entradas y salidas (variables, formato y rango).
Después de leídos los problemas, estos se reparten según las aptitudes de los integrantes y la estrategia que se haya acordado antes. Independientemente de cómo se hayan repartido los problemas, al momento de solucionarlos se deben tomar en cuenta los siguientes factores.
Sobre el uso eficiente de la computadora:
- Sólo hay una computadora para tres personas, por lo que al menos dos de ellos no van a poder estarla usando. Es importante que al resolver un problema, los códigos se escriban en papel y no se tecleen hasta que estén terminados (o casi).
- Cuando dos personas quieran utilizar la computadora al mismo tiempo, preferentemente debe usarla el que ya tenga la solución en papel. Si los dos ya lo tienen, se puede asignar de acuerdo a quien tiene el problema (o solución) más sencillo, o de acuerdo a quien tenga más experiencia.
- La computadora no debe de utilizarse para “debugear” por más de cinco o diez minutos. Para esto se debe utilizar el código escrito. En caso de que hayan hecho varias modificaciones en la computadora, se puede imprimir el código al momento de mandarlo a revisión para tenerlo listo cuando llegue la respuesta (en caso de que tengan impresora).
- Tampoco es conveniente que en algún momento la computadora no esté siendo utilizada.
Sobre como encontrar una solución:
- Siempre es conveniente resolver varios casos a mano. Esto da ideas de que tan fácilmente puede ser implementado o si existen formas de evitar cálculos. Generalmente es más fácil crear un algoritmo analizando la forma en que se resolvieron los ejemplos, que pensando directamente en el código.
- Lo más importante es que el algoritmo sea correcto y que se pueda codificar rápido, tomando en cuenta que pase las limitantes del problema (tanto en tiempo como en memoria). No se debe perder tiempo en buscar mejores algoritmos o formas de optimización. Aún y conociendo una mejor forma de resolverlo, si no es la diferencia entre que el código sea aceptado o rechazado, hay que apegarse a lo básico.
- No es recomendable utilizar algoritmos de fuerza bruta (búsqueda exhaustiva) o ávidos, ya es probable que los primeros se pasen de tiempo, mientras que los segundos muy posiblemente den salidas erróneas. Hay que tener muy bien analizados los problemas para utilizar cualquiera de los dos.
- Al momento de resolver el problema, es esencial tener una idea clara de que el algoritmo funciona y porque. Generalmente no es necesario tener que demostrarlo, pero ante la duda, lo mejor es intentar hacerlo o buscar la ayuda de un compañero. Si no se quiere demostrar, una alternativa es pensar en casos de prueba complejos y analizarlos antes de empezar a codificar.
- Por más simple que parezca un problema, si después de cierto tiempo no es posible resolverlo, es mejor pasar a otro. No importa que tan “cerca” parezca estar la solución.
Sobre como codificar la solución:
- De manera análoga al algoritmo, lo más importante es que el código sea simple y que funcione. No es necesario utilizar programación robusta, ni códigos elegantes. Habitualmente, lo más rápido de programar es lo mejor.
- Hay que estar acostumbrado a hacer códigos legibles. Esto es trascendental para debugear. Intentar darle formato una vez que no funciona el código, es tiempo que pudo ser mejor empleado.
- Cosas que en condiciones normales pudieran considerarse como malas prácticas de programación, son aceptables durante un concurso si ayuda a resolver más fácilmente un problema. Entre esto se encuentra: sobredimensionar los arreglos, no utilizar programas recursivos si pueden ser implementados de forma iterativa fácilmente o viceversa, no utilizar punteros, no hacer el programa modular, no utilizar clases (de acuerdo al lenguaje), etcétera.
- Aún así, pueden emplearse buenas prácticas de programación, siempre y cuando ya se tenga suficiente experiencia utilizándolas.
- Para los programas en C, hay que saber cuales funciones son ANSI C, ya que si no los son, se corre el riesgo de que no funcionen en diferentes compiladores (por ejemplo, la función itoa).
- Algo muy importante es no mandar un código sin haberlo probado antes, aún y cuando el código sea muy simple o sólo se le haya hecho una pequeña modificación. Con los problemas muy simples a veces es suficiente con sólo utilizar los casos prueba, pero con los demás, por lo menos se deben probar también casos triviales y casos extremos. También es útil recurrir a pruebas aleatorias en las que podamos comprobar el resultado rápidamente.
1.- Cada quien por su cuenta. En la primera estrategia, la idea básica es, una vez que los problemas están separados por dificultad, empezar por los más sencillos y dejar que cada integrante del equipo trabaje sólo en el problema que se adapte a sus gustos o aptitudes. Una vez que un problema es aceptado, se toma otro y se continúa.
Esta estrategia tiene la ventaja de que los problemas más simples son resueltos rápidamente, pero deja muy poco tiempo para resolver los complejos. También tiene el inconveniente de que al estar resolviendo problemas de la misma dificultad, los integrantes necesitarán utilizar la computadora más o menos en los mismos tiempos. Es una buena estrategia para equipos principiantes o para quienes no les interesa definir una estrategia.
2.- El capturista. Se elige a un miembro del equipo para que utilice la computadora, mientras los demás resuelven los problemas. El que está en la computadora es el encargado de escribir todos los programas. Mientras los otros dos miembros están programando el código en papel, el capturista se encarga de las rutinas de lectura y escritura, capturar los programas terminados y hacer debugeo sencillo.
Esta estrategia es útil cuando un miembro del equipo es rápido para teclear y conoce a fondo el lenguaje de programación, mientras los otros dos son buenos para resolver problemas. Tiene la desventaja de que el capturista no es aprovechado para resolver problemas. Una variante pudiera ser cambiar de capturista después de algunas horas, cuando algún miembro de equipo esté cansado.
3.- Pensamiento en grupo (Think Tank). En esta última estrategia, dos personas examinan juntos todos los problemas mientras el tercero captura lo básico y resuelve los problemas inmediatos. Primero se buscan los problemas simples, los cuales se los explican al tercer miembro. Una vez hecho esto, analizan cuidadosamente el resto de los problemas y deciden como los van a resolver. Cada vez que un programa es rechazado, si no se encuentra rápidamente donde está el error, se guarda para después. A las tres horas y media, se analiza la situación actual del equipo, y se determina la estrategia a seguir. A partir de entonces, se procura no tener que empezar nuevos códigos, sino terminar los ya existentes. La persona que resolvió los problemas fáciles puede estar creando entradas, mientras los otros dos revisan y corrigen los códigos.
De esta forma, los problemas difíciles pueden ser atacados desde el principio del concurso por dos personas, mientras que el tercero resuelve los fáciles, con lo que se tienen mejores oportunidades de terminar más códigos. Tiene la desventaja de que el tiempo total no será bueno, por lo que para ganar se tendrán que resolver más problemas. Si al principio o a mediados del concurso no se está entre los primeros lugares, no hay que sucumbir al pánico, simplemente hay que enfocarse a obtener los problemas que pueden resolverse.
Cual estrategia escoger va a depender en gran medida de las características del equipo y de la experiencia que tengan trabajando juntos. Aún así, siempre es conveniente ir ajustando la estrategia de acuerdo a como se desarrolle el concurso, principalmente observando los problemas que están siendo resueltos. Independientemente de la situación, es importante siempre confiar en las capacidades de los demás y buscar la forma en que puedan trabajar de forma óptima. Y sobre todo, si durante un concurso nos encontramos en una situación desfavorable, debemos recordar que la culpa no es de la silla.
Estrategias para “debugear” (“Trouble shooting” o Búsqueda de errores)
Es inevitable que durante un concurso (ya sea en línea o presencial), alguna solución falle. Seguramente la solución parece correcta (sino, ¿para que mandarla?), y teniendo encima la presión del tiempo, se vuelve complicado encontrar el error. Por esto, se debe estar preparado para saber como responder ante tal situación.
Una buena forma de buscar es utilizando una lista de posibles fallas. Con el paso del tiempo (y entrenando), muy posiblemente la lista se vuelva innecesaria, pero es común que durante el concurso obviemos o se nos olviden datos importantes. Con la limitante de hojas que se pueden ingresar como formulario, pudiera parecer un desperdicio de espacio, pero es probable que resulte útil.
A continuación tenemos una lista de errores comunes. Se presentan de forma seguida y con explicaciones para poder analizarlos, pero lo más recomendable al concursar es tener la lista separada por categorías (y subcategorías), si es necesario), tener una sublista de posibles soluciones, y hacerla lo más compacto posible.
- Revisar los casos críticos. Usualmente son los valores más grandes que puede tomar el problema, aunque no necesariamente. Es conveniente utilizar por lo menos una prueba, aunque sea aleatoria, para revisar que no existan problemas con las limitantes de tiempo/memoria.
- Revisar los casos triviales. Generalmente son los valores más pequeños que acepta el problema. Muchas veces estos casos son pasados por alto y esto provoca errores. Por ejemplo, si nos piden el factorial, no considerar que 0! = 1.
- Revisar los rangos de las entradas. Si el problema no lo especifica claramente, es útil preguntárselo a los jueces. No es recomendable asumir de acuerdo a las demás entradas. Un error común es suponer que si la entrada son enteros, que sólo van a ser positivos, o que si son números, que van a ser enteros de 32 bits (principalmente si los casos prueba son así).
- Escoger correctamente el tipo de variable. Lo más conveniente es utilizar longint (long) para enteros y extended (double) para flotantes, a menos que tengamos limitantes de memoria. Esto nos evita posibles problemas de “underflow” (subflujo) u “overflow” (desbordamiento) tanto en la salida como en los datos intermedios. Aún así, esto no evita que lo tengamos que revisar, ya que puede ser que el problema necesite utilizar un int64 (long long int) o números grandes.
- Revisar que el alcance (scope) de las variables sea el correcto. Se debe tener mucho cuidado al escoger si una variable va a ser global o local, o si al utilizar una variable como parámetro se manda sólo el valor o la variable. Esto especialmente en algoritmos recursivos.
- Evitar el uso de números de punto flotante o tener experiencia al usarlos. Lo más conveniente es utilizar números enteros, y si es absolutamente necesario utilizar un flotante, tener en cuenta los errores de redondeo. Nunca hay que comparar dos flotante directamente (se debe utilizar una tolerancia), y hay que tener presente las operaciones las debemos realizar con más dígitos de lo que exige la salida. Un error común con los puntos flotantes en tener salidas del tipo ‘-0.00’ en lugar de ‘0.00’.
- Revisar que los datos sean leídos correctamente. Este problema se presenta mucho en C donde el printf y el scanf pueden ser armas de doble filo, y también cuando tenemos que leer las entradas caracter por caracter, donde pueden haber espacios extras al principio o al final. En Pascal, debemos tener cuidado con el read y el readln, principalmente en entradas que terminan con fin de archivo (eof). Esto se debe a que read puede fallar si hay espacios extras al final, y readln puede fallar si existen líneas vacías.
- Inicializar las variables correctamente. No sólo al principio, sino para cada caso. Y hacerlo no sólo para cuando debemos limpiar las variables, sino también cuando debemos inicializarlas a “infinito” u otro valor (como en las gráficas). Es común que una mala inicialización pase desapercibida si todas las entradas van en orden creciente (o decreciente), por lo que es conveniente probar con entradas grandes y pequeñas aleatoreamente. Una forma fácil de inicializar arreglos es mediante fillchar (o memset), pero debemos tener en cuenta que estas funciones inicializan byte por byte, y no por cada casilla del arreglo.
- No reutilizar variables. Muchos programadores tienen la maña de reutilizar variables para ahorrar espacio en memoria. En la mayoría de las ocasiones, esto no es necesario y puede introducir errores (a veces difíciles de encontrar). Tampoco es sugerible utilizar el mismo nombre para una variable global y una local (a menos de que sean nombre comunes, como i para un ciclo).
- Revisar que las salidas sean exactamente iguales a lo que especifica el problema. Si el concurso es en línea, lo más aconsejable es hacer un copy-paste del formato de la salida para evitar errores de “dedo”. Hay que recordar que en ocasiones hasta por un espacio de más, pueden marcar WA.
- Revisar que estén incluidas todas las librerías. Este es un problema de C y C++. Aunque hay compiladores nos permiten no poner la librería de algunas funciones, debemos recordar que no siempre utilizan el mismo compilador (la misma versión, o banderas de compilación) los jueces.
- Tener cuidado con los paréntesis, y los punto y coma. Tener un paréntesis (o corchete) puesto un poco antes o después, o un punto y coma mal puesto (por ejemplo, después de un for), pueden ocasionar errores difíciles de encontrar.
- Buscar errores de ejecución. Los principales causantes pueden ser divisiones entre cero, acceder a casillas fuera de rango en un arreglo o tener apuntadores perdidos. Estos errores deben ser casi nulos si se cuenta con buenos casos prueba antes de mandar el problema.
- Revisar un TLE. Cuando un programa se excede de tiempo, lo primero que debemos hacer es revisar el cálculo del tiempo de ejecución. Y debe ser claro que se tiene que revisar, no hacerlo por primera vez. Si los cálculos indican que el tiempo es aceptable y el código pasa pruebas con casos críticos, lo más probable es que el programa se pase de tiempo porque se cicla. Esto puede ser porque alguna condición en un ciclo nunca llega a cumplirse, o por circunstancias más difíciles de encontrar, como estar utilizando un arreglo fuera de su rango declarado.
- Revisar que los archivos de entrada y salida sean los correctos. Es posible tener algún error de "dedo" en el nombre de los archivos, o utilizar archivos alternos para hacer pruebas y no cambiarlo al momento de mandar el código, o simplemente tenerlo como comentario. Se debe tener mucho cuidado, ya que no sólo es la penalización, sino el tiempo que se tarda la contestación.
- Ver si otros equipos lo han resuelto. Si hay pocos aceptados, puede ser que el problema sea más difícil de lo que parece o que tenga algún truco.
- Descansar del problema. Si lo demás no funciona, es conveniente dejar el problema un tiempo para refrescar ideas. No necesariamente hay que abandonarlo, sino que otro compañero puede intentar arreglarlo.
Por último, hay que recordar que aunque el concurso haya sido preparado a conciencia, puede haber fallos humanos (y con mucha mayor razón cuando se hacen improvisados, como comúnmente pasa en México). Existe la posibilidad de que el “Wrong Answer” no sea problema ni del algoritmo ni del código sino del juez (ya ha pasado), por lo que no hay que enfrascarse en el problema, y si estás seguro de que está bien (y aunque no estés tan seguro), es conveniente volver mandarlo al final, y con suerte lo revisa otro juez de forma correcta. Problemas no resueltos no generan penalización, así que no hay mucho que perder.