Aprovechando el entorno en concursos de programación

Esto es una pequeña guía al uso de algunas utilidades poco conocidas de los entornos estándares de Linux usados en concursos de programación tipo ICPC / CUPCAM. Está pensada para dar ideas útiles a concursantes.

1. Cosas sencillas

Medir recursos

Se pueden cronometrar programas, para ver cómo va el programa de tiempo:

$ time ./t < treasure.in > out.txt

real     0m0.032s
user     0m0.005s
sys      0m0.000s

(nota: la E/S requiere mucho tiempo de CPU; es recomendable redirigir la salida para evitar alterar los resultados).

Si se quiere saber cómo va de memoria (el otro recurso limitado), se debe usar una variante:

$ \time ./t < treasure.in > out.txt
0.00user 0.00system 0:00.03elapsed 16%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (0major+115minor)pagefaults 0swaps

(aquí sí que se ve el uso de memoria; en este caso, es insignificante)

Sacar cuentas

La utilidad 'bc' es una calculadora de precisión infinita por línea de comando. Por ejemplo:

$ bc
bc 1.06
Copyright 1991-1994, 1997, 1998, 2000 Free Software Foundation, Inc.
This is free software with ABSOLUTELY NO WARRANTY.
For details type `warranty'.
999999999999999999999999999999999999999999 + 1
1000000000000000000000000000000000000000000
quit

También se puede usar desde la consola, sin necesidad de entrar en el entorno:

$ echo 999999999999999999999999999999999999999999 + 1 | bc
1000000000000000000000000000000000000000000

Factorizar números

La utilidad 'factor' devuelve una lista con los factores de un número cualquiera. Por ejemplo,

$ factor `echo '2^64-1'|bc`
18446744073709551615: 3 5 17 257 641 65537 6700417

Comparar salidas

A menudo programas perfectos fallan por pequeñas imperfecciones en su entrada o salida. La táctica recomendad es, para todos los ficheros de entrada, tener listos ficheros de salida. Y no mirar a ver si la salida está bien 'a ojímetro', sino con la utilidad 'diff' o 'cmp'; y en casos extremos, con 'hexdump'.

$ cmp a.txt b.txt
a.txt b.txt differ: byte 25, line 2
$ diff a.txt b.txt
2c2
< harto curiosa
---
> harto curiosa
$ hexdump -C a.txt
00000000  75 6e 61 20 73 61 6c 69 64 61  0a 68 61 72 74 6f  |una salida.harto|
00000010  20 63 75 72 69 6f 73 61  20 0a                    | curiosa .|
0000001a
$ hexdump -C b.txt
00000000  75 6e 61 20 73 61 6c 69  64 61 0a 68 61 72 74 6f  |una salida.harto|
00000010  20 63 75 72 69 6f 73 61  0a                       | curiosa.|
00000019

Hay un espacio de más al final de la primera cadena; esto puede ocasionar un error en un juez.

2. Causar y examinar 'cores'

Si tu programa causa un Segmentation Fault (violación de segmento, usar memoria que no es suya, como lo quieras llamar), querrás saber dónde se ha producido. Por ejemplo:

$ cat docore.c
#include <stdlib.h>
int main() {
        int *a=(int *)malloc(10*sizeof(int));
        int i;
        for (i=0;i<1000000; i++) a[i] = i;
        return 0;
}
$ gcc -g -Wall docore.c -o c
$ ./c
Segmentation fault (core dumped)

NOTA: si sólo te sale el 'Segmentation fault', y no te sale el 'core dumped', tendrás que ejecutar

$ ulimit -c unlimited

Ahora lo puedes examinar con los siguientes comandos de GDB:

$ ./c
Segmentation fault (core dumped)
$ gdb c
GNU gdb 6.3-debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb) core core
Core was generated by `./c'.
Program terminated with signal 11, Segmentation fault.
warning: current_sos: Can't read pathname for load map: Input/output error

Reading symbols from /lib/tls/libc.so.6...done.
Loaded symbols for /lib/tls/libc.so.6
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0  0x080483c5 in main () at docore.c:5
5          for (i=0;i<1000000; i++) a[i] = i;
(gdb) print i
$1 = 33790
(gdb) quit

Esto no es demasiado útil: el error debería haber saltado nada más llegar a la posición a[10]; no obstante, el programa ha logrado continuar hasta llegar a a[33790].


Para detectar un error tan pronto como sucede, es recomendable usar una librería especializada en depurar malloc()s, como por ejemplo Electric Fence. Abajo, un ejemplo de su uso:

$ gcc -g -Wall docore.c -o c -lefence
$ ./c
  Electric Fence 2.1 Copyright (C) 1987-1998 Bruce Perens.
Segmentation fault (core dumped)
$ gdb c
GNU gdb 6.3-debian
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "i386-linux"...Using host libthread_db library "/lib/tls/libthread_db.so.1".

(gdb) core core
Core was generated by `./c'.
Program terminated with signal 11, Segmentation fault.
warning: current_sos: Can't read pathname for load map: Input/output error

Reading symbols from /usr/lib/libefence.so.0...done.
Loaded symbols for /usr/lib/libefence.so.0
Reading symbols from /lib/tls/libc.so.6...done.
Loaded symbols for /lib/tls/libc.so.6
Reading symbols from /lib/tls/libpthread.so.0...done.
Loaded symbols for /lib/tls/libpthread.so.0
Reading symbols from /lib/ld-linux.so.2...done.
Loaded symbols for /lib/ld-linux.so.2
#0  0x080484b5 in main () at docore.c:5
5          for (i=0;i<1000000; i++) a[i] = i;
(gdb) print i
$1 = 10
(gdb) quit

3. Usar scripts para generar programas / tablas para programas

Usando un "script" adecuado se pueden acelerar partes tediosas necesarias para resolver un programa. Pongamos, por ejemplo, que nuestro programa se vería beneficiado por tener una tabla de números primos entre 1 y 1000 (por ejemplo, nos están pidiendo calcular todos los números expresables como producto de dos primos, o algo así de rebuscado).

Una solución es programar una rutina que lo calcule al comienzo del programa. Otra solución es precalcularlo, e incluirlo como datos estáticos dentro del programa: por ejemplo,

$ echo 'int primos[] = {'; for ((i=0;i<1000;i++)) ; do 
factor $i | sed -ne 's/\(\w*\): \w*$/    \1,/ip' ; 
done ; echo '  -1 };'

(funciona copiando y pegando en un terminal; también se podría pegar dentro de un fichero y ejecutarlo) genera esta lista, y la formatea como un array 'C', listo para ser copiado y pegado en un programa.

También se puede precalcular en un programa C, pero en este caso no tendremos disponibles todas las herramientas que nos ofrece el entorno estándar de Unix.

NOTA: los programas 'precalculados' son legales. No hay ninguna razón para no usarlos cuando puedan ser efectivos. En estos concursos no importa la "elegancia" del código: elegancia es que funcione y se pueda escribir en poco tiempo, no que sea legible o use un algoritmo tremendamente sofisticado.


(c) 2004-2005 Escuela Politécnica Superior, Universidad Autónoma de Madrid
Autor: Manuel Freire Morán