2004 - CONICET Santa Fe
2004 - CONICET Santa Fe
2004 - CONICET Santa Fe
You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
Universidad Nacional del Litoral<br />
Facultad de Ingeniería Química<br />
Departamento de Matemáticas<br />
Notas para los cursos de<br />
Computación y Programación<br />
con el lenguaje Pascal<br />
Néstor Aguilera<br />
Año <strong>2004</strong>
Índice general<br />
1. Preliminares 1<br />
1.1. ¿Qué esesto? ............................. 1<br />
1.2. Cómo usar este libro . . . . . . . . . . . . . . . . . . . . . . . . . 1<br />
1.3. Organización y convenciones usadas ................ 3<br />
1.3.1. Versión electrónica ...................... 3<br />
1.4. Para los viejos y/o insomnes . . . . . . . . . . . . . . . . . . . . . 4<br />
1.5. Sobre la versión <strong>2004</strong> . . . . . . . . . . . . . . . . . . . . . . . . . 6<br />
2. El primer contacto 7<br />
2.1. Un poco, muy poco, sobre cómo funciona la computadora .... 7<br />
2.2. Programas: edición, compilación, ejecución ............. 8<br />
2.3. El puntapié inicial . . . . . . . . . . . . . . . . . . . . . . . . . . 10<br />
2.4. Comentarios Bibliográficos ...................... 12<br />
3. Tipos de datos elementales 13<br />
3.1. Tipos, variables e identificadores .................. 13<br />
3.2. Tipos numéricos: entero y real .................... 16<br />
3.2.1. “Readln” . . . . . . . . . . . . . . . . . . . . . . . . . . . 17<br />
3.2.2. Funciones numéricas . . . . . . . . . . . . . . . . . . . . . 18<br />
3.2.3. La codificación de enteros y reales ............. 19<br />
3.3. Variables lógicas . . . . . . . . . . . . . . . . . . . . . . . . . . . 22<br />
3.4. Caracteres . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23<br />
3.5. Comentarios Bibliográficos ...................... 24<br />
4. Tomando control 25<br />
4.1. “If” .................................. 26<br />
4.2. “While” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30<br />
4.3. “Repeat” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34<br />
4.4. “For” ................................. 35<br />
4.5. Ingresando muchos datos: “Eoln” .................. 37<br />
4.6. Comentarios Bibliográficos ...................... 39<br />
5. Aplicaciones 40<br />
5.1. Cálculo numérico elemental . . . . . . . . . . . . . . . . . . . . . 40<br />
5.1.1. Mezclando números grandes y pequeños . . . . . . . . . . 40<br />
5.1.2. Métodos iterativos: puntos fijos ............... 42<br />
5.1.3. El método babilónico . . . . . . . . . . . . . . . . . . . . . 45<br />
5.2. Números enteros y divisibilidad . . . . . . . . . . . . . . . . . . . 48
Pág. ii Índice general<br />
5.2.1. Ecuaciones diofánticas . . . . . . . . . . . . . . . . . . . . 48<br />
5.2.2. Máximo común divisor y el algoritmo de Euclides . . . . . 49<br />
5.2.3. Números primos . . . . . . . . . . . . . . . . . . . . . . . 52<br />
5.3. Problemas Adicionales ........................ 53<br />
5.4. Comentarios Bibliográficos ...................... 54<br />
6. Arreglos 55<br />
6.1. Dimensionamiento de arreglos . . . . . . . . . . . . . . . . . . . . 55<br />
6.2. Búsqueda Lineal ........................... 57<br />
6.3. Cribas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59<br />
6.4. Polinomios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 62<br />
6.5. Problemas Adicionales ........................ 64<br />
6.6. Comentarios Bibliográficos ...................... 65<br />
7. Funciones y Procedimientos 66<br />
7.1. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66<br />
7.2. El método de la bisección ...................... 70<br />
7.3. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 74<br />
7.4. Pasando por valor o por referencia . . . . . . . . . . . . . . . . . 77<br />
7.5. Comentarios Bibliográficos ...................... 80<br />
8. Todos juntos: arreglos, funciones y procedimientos 81<br />
8.1. Definiendo nuestros propios tipos de datos: “type” . . . . . . . . 81<br />
8.2. Ingreso e impresión de arreglos . . . . . . . . . . . . . . . . . . . 82<br />
8.3. La caja de herramientas . . . . . . . . . . . . . . . . . . . . . . . 84<br />
8.4. Arreglos multidimensionales . . . . . . . . . . . . . . . . . . . . . 85<br />
8.5. “Strings” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86<br />
8.6. Manejo elemental de archivos de texto . . . . . . . . . . . . . . . 86<br />
8.7. Problemas Adicionales ........................ 89<br />
8.8. Comentarios Bibliográficos ...................... 90<br />
9. Números Aleatorios y Simulación 91<br />
9.1. Números aleatorios . . . . . . . . . . . . . . . . . . . . . . . . . . 91<br />
9.2. Aplicaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92<br />
9.3. Problemas Adicionales ........................ 94<br />
10.Búsqueda y clasificación 96<br />
10.1. Búsqueda lineal con centinela . . . . . . . . . . . . . . . . . . . . 96<br />
10.2. Búsqueda binaria ........................... 98<br />
10.3. Métodos elementales de clasificación . . . . . . . . . . . . . . . . 101<br />
10.4. Registros (Records) . . . . . . . . . . . . . . . . . . . . . . . . . . 105<br />
10.5. Problemas Adicionales ........................108<br />
10.6. Comentarios Bibliográficos ......................109<br />
11.Recursión 110<br />
11.1. Funciones y procedimientos definidos recursivamente . . . . . . . 110<br />
11.2. Los Grandes Clásicos de la Recursión . . . . . . . . . . . . . . . . 113<br />
11.3. Problemas Adicionales ........................116<br />
11.4. Comentarios Bibliográficos ......................118
Índice general Pág. iii<br />
12.Generando objetos combinatorios 119<br />
12.1. Pilas y colas ..............................119<br />
12.2. Generando Subconjuntos . . . . . . . . . . . . . . . . . . . . . . . 121<br />
12.3. Generando permutaciones ......................124<br />
12.4. Árboles binarios ordenados . . . . . . . . . . . . . . . . . . . . . 126<br />
12.5. Problemas Adicionales ........................131<br />
12.6. Comentarios Bibliográficos ......................132<br />
13.Grafos y árboles 133<br />
13.1. Representación de grafos en la computadora ............136<br />
13.2. Recorriendo un grafo . . . . . . . . . . . . . . . . . . . . . . . . . 137<br />
13.3. Recorrido a lo ancho y en profundidad ...............139<br />
13.4. Camino más corto: Dijkstra . . . . . . . . . . . . . . . . . . . . . 141<br />
13.5. Mínimo árbol generador: Prim . . . . . . . . . . . . . . . . . . . 145<br />
13.6. Problemas Adicionales ........................148<br />
13.6.1. El algoritmo de Kruskal . . . . . . . . . . . . . . . . . . . 149<br />
13.7. Comentarios Bibliográficos ......................154<br />
14.Punteros y listas encadenadas 155<br />
14.1. Punteros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 155<br />
14.2. Listas encadenadas . . . . . . . . . . . . . . . . . . . . . . . . . . 157<br />
14.3. Otras estructuras dinámicas . . . . . . . . . . . . . . . . . . . . . 160<br />
14.4. Problemas Adicionales ........................162<br />
14.5. Comentarios Bibliográficos ......................162<br />
15.Más problemas 163<br />
15.1. Polinomios interpoladores de Lagrange ...............163<br />
15.2. Cuenta de Goldbach . . . . . . . . . . . . . . . . . . . . . . . . . 164<br />
15.3. El juego de “la generala” ......................164<br />
15.4. Visibilidad en el plano ........................164<br />
15.5. Aproximando e con dados ......................164<br />
15.6. Generando “manos” aleatoriamente ................165<br />
15.7. Clasificación por fusión . . . . . . . . . . . . . . . . . . . . . . . 166<br />
15.8. Media, mediana y moda . . . . . . . . . . . . . . . . . . . . . . . 168<br />
15.9. Números de Fibonacci: Zeckendof ..................168<br />
15.10. La Cola de Supermercado . . . . . . . . . . . . . . . . . . . . . 168<br />
15.11. La Producción de la Fábrica ....................169<br />
15.12. Pasos en búsqueda binaria . . . . . . . . . . . . . . . . . . . . . 169<br />
15.13. El Juego de Nim . . . . . . . . . . . . . . . . . . . . . . . . . . 169<br />
15.14. Comentarios Bibliográficos . . . . . . . . . . . . . . . . . . . . . 171<br />
A. Programas mencionados 173<br />
Problema 2.2: holamundo . . . . . . . . . . . . . . . . . . . . . . . . . 173<br />
Problema 3.2: sumardos . . . . . . . . . . . . . . . . . . . . . . . . . . 173<br />
Problema 3.3: leerdato . . . . . . . . . . . . . . . . . . . . . . . . . . . 173<br />
Problema 3.4: raiz .............................174<br />
Problema 3.7: enteroareal . . . . . . . . . . . . . . . . . . . . . . . . . 174<br />
Problema 3.13: segundos . . . . . . . . . . . . . . . . . . . . . . . . . . 175<br />
Problema 3.16: positivo . . . . . . . . . . . . . . . . . . . . . . . . . . . 175<br />
Problema 3.18: caracteres1 . . . . . . . . . . . . . . . . . . . . . . . . . 175
Pág. iv Índice general<br />
Problema 4.2: valorabsoluto ........................176<br />
Problema 4.4: comparar . . . . . . . . . . . . . . . . . . . . . . . . . . 176<br />
Problema 4.5: caracteres2 .........................176<br />
Problema 4.11: resto . . . . . . . . . . . . . . . . . . . . . . . . . . . . 177<br />
Problema 4.12: tablaseno . . . . . . . . . . . . . . . . . . . . . . . . . . 177<br />
Problema 4.13: gauss . . . . . . . . . . . . . . . . . . . . . . . . . . . . 178<br />
Problema 4.16: cifras . . . . . . . . . . . . . . . . . . . . . . . . . . . . 179<br />
Problema 4.17: epsmin ...........................179<br />
Problema 4.18: potencia . . . . . . . . . . . . . . . . . . . . . . . . . . 180<br />
Problema 4.22: eolnprueba .........................180<br />
Problema 4.23: sumardatos .........................181<br />
Problema 5.10: babilonico .........................181<br />
Problema 5.15: euclides ...........................182<br />
Problema 5.20: factoresprimos . . . . . . . . . . . . . . . . . . . . . . . 183<br />
Problema 6.1: unidades ...........................184<br />
Problema 6.2: busquedalineal ........................184<br />
Problema 6.5: eratostenes .........................186<br />
Problema 6.7: flaviojosefo1 .........................187<br />
Problema 7.1: potencias2 . . . . . . . . . . . . . . . . . . . . . . . . . . 188<br />
Problema 7.2: potencias3 . . . . . . . . . . . . . . . . . . . . . . . . . . 188<br />
Problema 7.3: biseccion ...........................189<br />
Problema 7.6: tablaseno2 . . . . . . . . . . . . . . . . . . . . . . . . . . 191<br />
Problema 7.7: intercambio .........................192<br />
Problema 8.2: maximocaracter . . . . . . . . . . . . . . . . . . . . . . . 192<br />
Problema 8.8: deconsolaaarchivo ......................194<br />
Problema 8.8: dearchivoaconsola ......................195<br />
Problema 9.1: dado . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 195<br />
Problema 9.2: dados . . . . . . . . . . . . . . . . . . . . . . . . . . . . 196<br />
Problema 12.6: arbolbinario ........................197<br />
Problema 13.4: anchoprimero . . . . . . . . . . . . . . . . . . . . . . . 199<br />
Problema 13.8: dijkstra ...........................202<br />
Problema 14.1: listas . . . . . . . . . . . . . . . . . . . . . . . . . . . . 206<br />
Problema 14.9: grafos ...........................209<br />
B. Sobre los compiladores Pascal 214<br />
C. Breve referencia de Pascal 215<br />
C.1. Resumen de operadores . . . . . . . . . . . . . . . . . . . . . . . 215<br />
C.1.1. Operadores aritméticos . . . . . . . . . . . . . . . . . . . 215<br />
C.1.2. Operadores relacionales . . . . . . . . . . . . . . . . . . . 215<br />
C.1.3. Operadores lógicos ......................215<br />
C.2. Identificadores estándares ......................215<br />
C.3. Nombres Reservados .........................217<br />
D. Algunas notaciones y símbolos usados 218<br />
D.1. Lógica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218<br />
D.2. Conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218<br />
D.3. Números: conjuntos, relaciones, funciones .............219<br />
D.4. Números importantes en programación . . . . . . . . . . . . . . . 220<br />
D.5. Generales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 220
Índice general Pág. v<br />
Bibliografía 221<br />
Índice de figuras 223<br />
Índice de cuadros 224<br />
Índice alfabético 225
Capítulo 1<br />
Preliminares<br />
1.1. ¿Qué esesto?<br />
Algunos se enloquecen por la cerveza, otros prefieren vino, y también están<br />
los abstemios. Pero todos estamos de acuerdo en que la computadora ocupa, y<br />
cada vez más, un lugar importante en nuestras vidas.<br />
Decimos esto pensando en internet, el acceso a la información, la cuenta<br />
del banco y los impuestos, multimedios como gráficos, sonidos, películas y juegos<br />
(videos), y siguiendo con una larga lista que, sin embargo, difícilmente incluirá<br />
resolución de problemas matemáticos.<br />
Y nuestras creencias están de acuerdo con nuestras experiencias. Los primeros<br />
encuentros con la computadora y sus aplicaciones tienen que ver con hacer<br />
“click” con el “ratón” para acceder a juegos, “browsers” y el correo electrónico.<br />
Después aprendemos algo del funcionamiento del sistema operativo, procesadores<br />
de texto y planillas de cálculo, y el software que se use en nuestro trabajo.<br />
No es ningún misterio que estas aplicaciones están diseñadas y realizadas<br />
por personas, ingenieros y técnicos de sistemas, y programadores. Consecuentemente,<br />
el primer curso de programación usualmente está orientado hacia las<br />
aplicaciones de sistemas informáticos, por ejemplo hacia el manejo de archivos<br />
con vistas a bases de datos, lo cual es muy comprensible.<br />
La propuesta aquí es un tanto diferente: hacer el primer curso de programación<br />
pensando en las aplicaciones matemáticas. Después de todo, el nombre<br />
“computadora” tiene que ver más con matemáticas que con “informática”.<br />
En el libro se va entrelazando la introducción de los elementos de programación<br />
estructurada con la resolución de problemas de análisis y cálculo numérico,<br />
teoría de números, combinatoria y grafos, sirviendo tanto de introducción de<br />
algunos temas como de repaso y fortalecimiento de otros.<br />
Quedan aparte temas de álgebra lineal, ya que la resolución numérica de<br />
problemas lineales requiere un estudio cuidadoso de los errores, que está fuera<br />
del propósito introductorio del libro.<br />
1.2. Cómo usar este libro<br />
Uno puede darse una idea de cómo cocinar mirando un libro de cocina, pero<br />
seguramente la experiencia es mucho más completa y satisfactoria si se tienen
Pág. 2 Preliminares<br />
los ingredientes, las cacerolas y la misma cocina. Del mismo modo, se puede<br />
apreciar qué es la programación mirando un libro, pero nosotros supondremos<br />
que el lector dispone además de una computadora y un compilador Pascal con<br />
los que trabajar.<br />
Así como el aprendiz aprende observando al artesano, en programación es<br />
muy importante copiar (y tomar ideas y estructuras de) programas ya hechos,<br />
aunque a veces no se entienda exactamente del porqué detalocualsintaxis.<br />
Idealmente quien trabaje con los contenidos de este libro copiará (preferentemente<br />
de un diskette o de internet) los programas presentados, y hará variaciones<br />
ocopiarápartesdeunoomás de ellos para resolver los distintos problemas.<br />
Muchos principiantes se ponen ansiosamente a tipear un programa sin tener<br />
un plan específico, quizás por su experiencia previa con la computadora. Una<br />
buena parte de los problemas que planteamos no son sencillos de resolver, y<br />
seguramente no se resuelven probando con distintos “botones”. Más aún, varios<br />
de los problemas no necesitan de la computadora.<br />
Quien quiera sacar provecho de este libro deberá hacer uso del lápiz y papel<br />
para trazarse un plan de resolución antes de copiar o modificar los programas,<br />
pero para eso debe entender primero qué es lo que hacen las distintas partes y<br />
—por sobre todo— a dónde se quiere llegar.<br />
La adquisición de conocimientos no es gratuita y requiere esfuerzo y tiempo,<br />
y las equivocaciones forman parte inevitable de este proceso:<br />
¡habrá que estar dispuesto a pasar un buen rato en<br />
la silla!<br />
Aún cuando no podamos resolver un problema en programación o en matemáticas,<br />
es poco útil que alguien nos cuente la solución si antes no hemos<br />
hecho un trabajo propio: de este modo podremos apreciar, e inclusive criticar,<br />
la que nos proponen.<br />
En algunos problemas se incluyen “sugerencias” para la resolución, que están<br />
puestas como orientación cuando se está “perdido”. La recomendación es “tapar”<br />
la sugerencia, y recurrir a ella en segunda instancia o cuando ya se ha<br />
resuelto el problema para ver si hay otras posibilidades de resolución.<br />
Esto nos trae al tema de que tanto en programación como en matemáticas,<br />
no hay una única forma de hacer los programas o resolver los problemas. Lo<br />
presentado es sólo una posibilidad.<br />
A algunos les parecerá que las sugerencias son oscuras o escasas, a otros<br />
les parecerá que el material presentado es excesivo, y habrá otros que querrán<br />
resolver más problemas: para éstos se incluye una sección de “Problemas Adicionales”<br />
en los capítulos más avanzados, así como todo el capítulo 15.<br />
A todos les recomendamos los libros de Engel [4], Wirth [15, 16] —autordel<br />
lenguaje que usamos, Pascal— y Jensen y Wirth [7] para temas específicos de<br />
Pascal, que forman la base sobre la cual está hecho este libro.<br />
En fin, habrá algunos que querrán seguir avanzando aún más, quizás después<br />
de terminar este libro, y una posibilidad muy interesante es tomar problemas<br />
de las competencias estudiantiles de la ACM (Association for Computing Ma-
1.3. Organización y convenciones usadas Pág. 3<br />
chinery (1) ), en las que los representantes de nuestro país vienen participando<br />
exitosamente desde hace varios años.<br />
1.3. Organización y convenciones usadas<br />
En los capítulos 2 a 15 se presentan los temas y problemas, agrupados en<br />
secciones y a veces subsecciones. Los problemas están numerados comenzando<br />
con 1 en cada capítulo, de modo que el “problema 4.5” se refiere al problema 5<br />
del capítulo 4. De modo similar, la “sección 3.2” se refiere a la sección 2 del<br />
capítulo 3.<br />
En muchos de los problemas se mencionan programas completos, que se han<br />
puesto juntos en el apéndice A, al que le sigue información sobre compiladores<br />
Pascal (apéndice B) disponibles en internet. A modo de glosarios, finalizamos<br />
con una breve referencia de Pascal (apéndice C) y notaciones usadas en el libro<br />
(apéndice D).<br />
A veces intercalaremos texto entre los problemas, por lo que para indicar el<br />
fin del enunciado de un problema colocamos el signo ✄, que puede leerse como<br />
“la cortamos acá”.<br />
Intercalados entre texto y enunciados de problemas, hay algunas notas y<br />
comentarios, en tipo de letra más chico para no distraer demasiado del texto<br />
principal, y puede omitirse su lectura.<br />
En los comentarios, en itálica, se hacen referencias históricas, orientadoras,<br />
curiosidades, etc. Son de la forma<br />
Esto es un comentario.<br />
Por otra parte, las notas son en general de índole más técnica, y son de la<br />
forma<br />
✎ Esto es una nota.<br />
Usamos otros tipos de letra para indicar los nombres de los programas, así,<br />
mientras que lo que escribiremos en la computadora está indicado en “monotipo”,<br />
así, algunas veces entre comillas dobles, para recalcar algún fragmento,<br />
“ como éste ”, reservando las comillas simples para caracteres, como ‘ a ’.<br />
Siguiendo la tradición norteamericana, la computadora expresa los números<br />
poniendo un “punto” decimal en vez de la “coma”, y para no confundirnos<br />
seguiremos esa práctica en todo el texto. Así, 1.589 es un númeroentre1y2,<br />
mientras que 1589 es un número entero, mayor que mil.<br />
También se hace complicado trabajar con acentos o tildes (como en “ á ”o<br />
“ ~n ”) al escribir los programas o mostrar resultados por pantalla, de modo que<br />
en la escritura de códigos los obviaremos (escribiendo “ a ”o“ni ”).<br />
En fin, en el texto indicamos con el pulsado de la tecla “Retorno”<br />
(o “Return” en teclados ingleses) para iniciar un nuevo renglón. Dependiendo<br />
del sistema operativo, es posible que deba pulsarse en cambio la tecla “Intro”<br />
(o “Enter” en inglés).<br />
1.3.1. Versión electrónica<br />
La versión electrónica (2) en formato “pdf” tiene algunas ventajas:<br />
(1) http://icpc.baylor.edu/icpc/<br />
(2) Disponible en http://math.unl.edu.ar/~aguilera/libro<strong>2004</strong>
Pág. 4 Preliminares<br />
• Se puede leer e imprimir desde distintas plataformas (Unix, Windows, etc.)<br />
usando el software gratuito Adobe Reader (anteriormente Adobe Acrobat<br />
Reader) (3) .<br />
• Es muy fácil moverse dentro del documento, subiendo o bajando una página<br />
o ir a una página determinada, y también se puede ver en distintos<br />
tamaños.<br />
• Se puede buscar dentro del documento algún fragmento de texto. . . tratando<br />
de evitar acentos como en “á”, tildes como en “ñ” o “ligaduras”<br />
como en “fi”.<br />
• Eligiendo la “herramienta de texto”, marcada con una gran “T”, es posible<br />
copiar algún fragmento de texto desde un archivo “pdf” a otro documento<br />
hecho con otro software. Esto es especialmente útil para copiar partes de<br />
programas.<br />
• Tiene capacidades de “hipertexto”: referencias en color pueden seleccionarse<br />
para moverse en el documento o internet, y es sencillo volver al lugar<br />
anterior.<br />
Pero. . .<br />
– al imprimir en blanco y negro los colores se ven como grises,<br />
– puede suceder que la posición en la página a la que se llegue esté un<br />
poco corrida hacia arriba o abajo, lo que es especialmente confuso<br />
cuando justo hay un cambio de página,<br />
– o peor, que directamente nos lleve al lugar incorrecto: ¡no sólo los<br />
alumnos y los profes hacen programas imperfectos!<br />
1.4. Para los viejos y/o insomnes<br />
El núcleo del libro es la resolución de problemas para desarrollar el pensamiento<br />
matemático y las conexiones con el pensamiento algorítmico, pasando<br />
de abstracciones al resultado concreto de un programa para la computadora.<br />
Hemos tratado de ubicarnos entre dos extremos. Por un lado el uso de la<br />
computadora como un fin y no como un medio, donde lo importante es la computadora<br />
relegando a un plano secundario, o aún ignorando, las matemáticas.<br />
Por otro lado, el aprendizaje de sistemas avanzados con los que “apretando botones”<br />
obtenemos resultados, muchas veces sin saber de dónde salen ni cuán<br />
confiables son (o de qué estamos hablando).<br />
Y seguramente que queremos apartarnos lo másposibledelapeordetodas<br />
las propuestas, lamentablemente muy difundida: el aprendizaje rutinario de<br />
técnicas de dudoso valor que son rápidamente olvidadas.<br />
Esta relación entre abstracto y concreto se realimenta al usar la computadora<br />
como banco experimental para encontrar patrones, llegar a conjeturas y a su<br />
eventual demostración matemática.<br />
Interpuestos más que nada como comentarios, se tocan temas de eficiencia<br />
y complejidad de algoritmos, pero no se incluyen temas de corrección de<br />
programas.<br />
(3) Disponible en http://www.adobe.com/products/acrobat/readermain.html
1.4. Para los viejos y/o insomnes Pág. 5<br />
Una característica del libro, además de los temas que se presentan, es la<br />
inclusión de muchos programas acabados, pues en programación —mucho más<br />
que en matemáticas— se aprende mirando lo que han hecho otros, y es inclusive<br />
deseable el “reuso” de partes ya hechas. Sin embargo los programas presentados<br />
difícilmente puedan considerarse “estado del arte”, y se han sacrificado la<br />
eficiencia y la brevedad tratando de hacer una presentación clara.<br />
También sólo se usa seudo-código en los capítulos más avanzados (y en mínima<br />
medida), a contra corriente de otros cursos introductorios, con la convicción<br />
de que es muy difícil entender —e imposible concretar— qué seestádiciendo en seudo-código. Es como tratar de aprender una síntesis de los idiomas antes<br />
de aprender a hablar en alguno (4) .<br />
En el libro usamos el lenguaje Pascal. Considerado “obsoleto” por distintas<br />
razones, tiene la gran ventaja de haber sido diseñado especialmente para la<br />
enseñanza de programación por uno de los grandes autores, N. Wirth, y brinda<br />
la posibilidad de una clara formulación de los programas y —mucho más<br />
importante— una sólida base para el futuro.<br />
Habiendo elegido Pascal, hemos tratado de ceñirnos lo más posible a su<br />
estándar pues debe conocerse y apreciarse la importancia de la portabilidad y<br />
consistencia entre diferentes sistemas operativos.<br />
Los capítulos 2, 3, 4, 6, 7, 8, 10 y 11 son más o menos comunes a todos los<br />
cursos de programación, aunque —como mencionamos— hay un fuerte énfasis<br />
hacia las matemáticas dejando de lado la informática. En cambio, los capítulos 5<br />
y 9 cubren temas de matemáticas (teóricas o aplicadas) que normalmente no se<br />
dan.<br />
Aunque pilas y colas es un tema normalmente incluido en cursos de programación,<br />
la perspectiva que damos en el capítulo 12 es bastante distinta:<br />
allí trabajamos con objetos combinatorios, como subconjuntos, permutaciones<br />
yárboles binarios, donde aparece verdaderamente la fuerza de recursión.<br />
Lossiguientesdoscapítulos toman rumbos distintos y son bastante independientes<br />
entre sí. El capítulo 13 está pensado para aquéllos que ya hayan visto<br />
un curso de matemática discreta o teoría de grafos, implementando (en forma<br />
elemental) los primeros algoritmos de grafos. En cambio, el capítulo 14 muestra<br />
las posibilidades de las listas encadenadas, liberándonos de algunas ataduras de<br />
los arreglos.<br />
En el capítulo final hay algunos temas y problemas, algunos más avanzados,<br />
que hemos preferido separar del cuerpo principal del libro.<br />
En la redacción se puede notar la influencia de los libros de Kernighan y<br />
Ritchie [10] yWirth[16] en lo referente a programación, y de Engel [4] yGentile<br />
[5] en cuanto a basar el curso en resolución de problemas (y varios de los<br />
temas considerados). Por otro lado, hemos seguido el estándar Pascal según [7].<br />
Al final de algunos capítulos damos referencias bibliográficas sobre temas y<br />
problemas particulares, pero muchos de ellos pertenecen al “folklore” y es difícil<br />
determinar el origen.<br />
En cuanto a las notas de carácter histórico, muchas han sido basadas en el<br />
material de http://www-groups.dcs.st-and.ac.uk/˜history<br />
(4) Los más viejos recordamos el fracaso de la “matemática moderna”.
Pág. 6 Preliminares<br />
1.5. Sobre la versión <strong>2004</strong><br />
Estas notas se van modificando añoaaño en base a la experiencia que se va<br />
ganando en el dictado del curso. Algunos temas se han eliminado o cambiado,<br />
mientras que otros se han agregado. En particular, para la versión <strong>2004</strong> hemos<br />
agregado muchos más gráficos, y por lo tanto bastante más páginas, así como<br />
el hipertexto en la versión electrónica. También hemos agregado un índice<br />
alfabético que seguramente dista de ser perfecto.<br />
Por supuesto, hemos tratado de corregir los errores encontrados (tipográficos<br />
o conceptuales) en notas de años anteriores, por lo cual debo agradecer<br />
especialmente a Luis Bianculli, Jorge D’Elía y Egle Haye.
Capítulo 2<br />
El primer contacto<br />
2.1. Un poco, muy poco, sobre cómo funciona<br />
la computadora<br />
Conceptualmente, la computadora es una máquina que toma o accede a<br />
datos, los procesa y devuelve resultados. Los datos o entradas y los resultados<br />
o salidas pueden ser simples como números o letras, o mucho más complicados<br />
como una matriz o una base de datos (1) , que podemos esquematizar como<br />
entrada procesamiento salida<br />
En el modelo de computadora con el que trabajaremos (o de von Neumann),<br />
pensaremos que el procesamiento está a cargo de una única unidad, llamada<br />
CPU por “Central Processing Unit” o Unidad Central de Procesamiento, que<br />
accede los datos y retorna los resultados secuencialmente, es decir, de a uno por<br />
vez, y los datos a los que accede se guardan en una lugar denominado memoria.<br />
John von Neumann (1903–1957) se interesó inicialmente en lógica, teoría<br />
de conjuntos, de la medida, y mecánica cuántica, tocando luego temas de análisis<br />
funcional, teoría ergódica, siendo fundador de la teoría de juegos. En sus<br />
últimos años también tuvo influencia decisiva en ecuaciones en derivadas parciales<br />
y en teoría de autómatas, en la que sintetizó sus conocimientos e ideas<br />
de lógica y grandes computadoras electrónicas.<br />
En los programas que haremos normalmente nos comunicaremos con la computadora<br />
entrando los datos con el teclado y recibiendo los resultados en la pantalla,<br />
refiriéndonos en general como terminal o consola al conjunto combinado<br />
de teclado y pantalla. Estos datos que entramos o recibimos no son directamente<br />
procesadosporlaCPU,sinoquesontransferidosaodesdelamemoriamediante<br />
la misma CPU u otro procesador dedicado.<br />
Un esquema del movimiento de datos entre periféricos (consola, discos, impresora,<br />
etc.), memoria y CPU está indicado en la figura 2.1.<br />
Nos imaginaremos que la memoria, en donde se almacenan los datos, está<br />
constituida por muchas cajitas pequeñas llamadas bits por “Binary Digit”<br />
(1) Es como pensar en una máquina de hacer chorizos: ponemos los ingredientes (mezcla,<br />
tripa, etc.), y después de dar vuelta la manija tenemos los chorizos.
Pág. 8 El primer contacto<br />
consola<br />
pantalla<br />
teclado<br />
discos<br />
impresora<br />
otros<br />
memoria<br />
(datos)<br />
CPU<br />
(procesamiento)<br />
Figura 2.1: Esquema de transferencia de datos en la computadora.<br />
odígito binario, en cada una de las cuales sólo se puede guardar un 0 o un<br />
1.Puestoqueestacajaesdemasiadopequeña para guardar información más<br />
complicada que “sí/no” o “blanco/negro”, los bits se agrupan en cajas un poco<br />
más grandes llamadas bytes, que generalmente tienen 8 bits, conceptualmente<br />
alineados, puesto que queremos que 00001111 sea distinto de 11110000. Ver<br />
esquema en la figura 2.2.<br />
<br />
un byte<br />
<br />
1 0 1 0 1 0 0 1<br />
bits<br />
Figura 2.2: Bits y byte.<br />
Problema 2.1. Suponiendo que un byte tenga 8 bits:<br />
a) ¿Cuántas “ristras” distintas de 0’s y 1’s puede tener? Sugerencia: hacer la<br />
cuenta primero para un byte de 1 bit, luego para un byte de 2 bits, luego<br />
para un byte de 3 bits,. . .<br />
b) Si no importara el orden de los bits que forman el byte, y entonces 00001111,<br />
11110000, 10100101 fueran indistinguibles entre sí, ¿cuántos elementos distintos<br />
podría contener un byte? Sugerencia: si el byte tiene 8 bits puede ser<br />
que hayan 8 ceros y ningún uno, o 7 ceros y 1 uno, o. . . ✄<br />
A su vez, para las computadoras más recientes, estas unidades resultan demasiado<br />
pequeñas para alimentar a la CPU, por lo que los bits o bytes se agrupan<br />
formando cajas de, por ejemplo, 32, 64 o 128 bits (usualmente potencias de 2),<br />
siempre conceptualmente alineadas.<br />
2.2. Programas: edición, compilación, ejecución<br />
Por supuesto, queremos que la computadora “haga algo” con los datos que<br />
le damos, pero tenemos que darle instrucciones sobre cómo hacerlo. El conjunto<br />
de instrucciones y datos para realizar determinada tarea es lo que llamaremos<br />
programa, y los mismos programas pueden considerarse como un tipo especial<br />
de datos.
2.2. Programas: edición, compilación, ejecución Pág. 9<br />
En particular, el sistema operativo de la computadora es un programa que<br />
alimenta constantemente a la CPU, y le va a indicar, por ejemplo, que ejecute<br />
(o corra) nuestro programa, leyendo las instrucciones que contiene.<br />
Los lenguajes de programación son “abstracciones” que nos permiten escribir<br />
las instrucciones de un programa de forma que un ser humano puede entender<br />
más fácilmente que ristras de ceros y unos. Las instrucciones para la máquina<br />
se escriben como sentencias —de acuerdo a las reglas del lenguaje— en lo que<br />
se llama programa fuente.<br />
Hay distintos tipos de lenguajes de programación, cada uno con sus defectos<br />
y virtudes. Dentro de los que más se usan en matemáticas, están (en orden más o<br />
menos cronológico) Fortran,“C”,Pascal, “C++”, y otros integrados a sistemas<br />
con posibilidades gráficas y/o simbólicas como Matlab, Maple o Mathematica. En<br />
estas notas usaremos el lenguaje Pascal, que ha sido diseñado para la enseñanza<br />
de programación.<br />
N. Wirth dio el nombre de Pascal al lenguaje en honor al matemático<br />
francés Blaise Pascal (1623–1662), quien fue uno de los primeros en desarrollar<br />
una calculadora mecánica, cuando tenía unos 20 años.<br />
Sin embargo hubo muchas otras calculadoras antes, como las computadoras<br />
lunares y planetarias del astrónomo iraní al-Kashi (1393–1449), o la de W.<br />
Schickard (1592–1635) que multiplicaba y dividía, mientras que la de Pascal<br />
(construida entre los años 1642–1644) sólo sumaba y restaba.<br />
Luego de elegir un lenguaje de programación, al desarrollar un programa lo<br />
usual es primero escribir el programa fuente (el que nosotros entendemos) con<br />
un editor de textos en la computadora, eventualmente guardando lo escrito en<br />
el disco rígido o un diskette.<br />
El programa fuente debe ser traducido a algo que la CPU pueda entender,<br />
es decir las famosas ristras de 0’s y 1’s. Este proceso se llama compilado, dando<br />
por resultado un programa ejecutable, que es el que en realidad va a usar la<br />
computadora.<br />
✎ Esta descripción basta para nuestros propósitos, en realidad todo el proceso<br />
requiere otros pasos intermedios que en nuestro caso serán hechos<br />
automáticamente.<br />
✎ Algunos lenguajes son interpretados, es decir no existe la compilación y no<br />
se crea el ejecutable.<br />
En la mayoría de los casos —aún para gente experimentada— habrá problemas<br />
en la compilación (por ejemplo, por errores de sintaxis), o al ejecutar<br />
el programa los resultados no serán los esperados. Esto da lugar a un ciclo de<br />
trabajo esquematizado en la figura 2.3.<br />
Editar fuente Compilar ejecutable Ejecutar<br />
Corregir<br />
Figura 2.3: Esquema del desarrollo de un programa.<br />
En fin, al momento de usar el programa ejecutable, el sistema operativo lo<br />
aloja en algún lugar disponible de la memoria, quedando un esquema como el<br />
de la figura 2.4.
Pág. 10 El primer contacto<br />
Memoria<br />
programa<br />
ejecutable<br />
datos<br />
instrucciones<br />
Figura 2.4: Esquema del programa ejecutable en la memoria.<br />
2.3. El puntapié inicial<br />
Para lo que sigue, será conveniente ir mirando el primer programa Pascal<br />
con el que trabajaremos: el programa holamundo (pág. 173).<br />
En Pascal, todos los programas (fuentes) se dividen en dos partes o cuerpos.<br />
En la primera, a veces llamada de declaraciones, se coloca el nombre del<br />
programa mediante “ program nombre(input,output)” yotrassentenciasque<br />
iremos viendo.<br />
✎ Siguiendo el estándar Pascal. Muchos compiladores aceptan, sin embargo,<br />
la omisión de “ (input, output) ”.<br />
La segunda parte, a veces llamada principal, empieza con “ begin ”ytermina<br />
con “ end. ” (punto “ . ” incluido), y entre ellos se ponen sentencias para realizar<br />
“acciones”.<br />
En ambas partes, de declaraciones y principal, las sentencias se separan<br />
mediante “ ; ”. En cualquier lugar se pueden agregar comentarios, encerrados<br />
entre “ (* ”y“*) ” que nos ayudan a entender lo que hicimos cuando volvemos<br />
amirardespués de un par de semanas.<br />
✎ También se pueden encerrar comentarios entre “ { ”y“} ”, pero no los<br />
usaremos a fin de seguir una sintaxis más parecida a otros lenguajes como<br />
“C” o Mathematica.<br />
Para evitar confusiones, normalmente se guarda el programa fuente en un<br />
archivo con el mismo nombre que en la sentencia “ program nombre ... ”, y<br />
con extensión “ .p ”o“.pas ”, para indicar que se trata de un programa Pascal.<br />
Así, generalmente guardaremos el programa fuente de nombre pepe en el archivo<br />
“ pepe.p ”o“pepe.pas”. Al compilarlo, el ejecutable cambia la extensión a<br />
“ .exe ”, de modo que obtenemos el archivo “ pepe.exe”.<br />
✎ La extensión para el ejecutable depende del sistema operativo, y puede no<br />
existir. Asimismo, puede no existir la extensión para el programa fuente.<br />
La prueba de fuego es editar, compilar y ejecutar el primer programa. Sin<br />
embargo, los detalles de cómo realizar el ciclo de edición-compilación-ejecución<br />
dependen del sistema operativo y el compilador (la marca del compilador) que<br />
estemos usando, de modo que habrá que seguir las instrucciones de los manuales<br />
o pedir auxilio a algún conocido con este primer paso.<br />
Problema 2.2 (Hola Mundo). Copiar, compilar y ejecutar el programa holamundo,<br />
guardándolo en disco o diskette como “ holamundo.pas”.<br />
✎ Muchos compiladores, entre ellos el muy difundido Turbo Pascal, hacen que<br />
al ejecutar un programa como holamundo se abra una ventana distinta que<br />
se cierra automáticamente al finalizar la ejecución del programa. El proceso
2.3. El puntapié inicial Pág. 11<br />
puede ser tan rápido que apenas nos damos cuenta de que ha sucedido algo.<br />
En estos casos es conveniente agregar un renglón con las instrucciones<br />
writeln(’ para fin’); readln<br />
al terminar el programa, antes de “ end. ” y agregando un “ ; ”alfindel<br />
renglón anterior.<br />
Turbo Pascal y otros compiladores similares no crean el ejecutable automáticamente,<br />
sino que hay que darle instrucciones para que lo hagan.<br />
Para nuestros propósitos, no será necesario crear el ejecutable.<br />
a) Observar con cuidado los signos de puntuación y qué hace cada una de las<br />
instrucciones:<br />
• El renglón inicial que comienza con “ program...”, y que termina en<br />
“ ; ”. En este programa es la única sentencia de la parte declarativa.<br />
• El comentario inmediatamente después de “ program...”, explicando al<br />
que lee el programa fuente cuál es el propósito.<br />
• El cuerpo principal que empieza con “ begin ”yterminaen“end. ”.<br />
• Hay tres sentencias en la parte principal, separadas por dos “ ; ”.<br />
• “ writeln ” escribe un renglón en la pantalla, y el texto a escribir se<br />
encierra entre “ ’ ” (comillas simples). Si no tiene argumentos, “ writeln ”<br />
escribe un renglón “vacío”.<br />
b) Eliminar, repetir o cambiar las instrucciones. Por ejemplo:<br />
i) eliminar el segundo “ writeln ”,<br />
ii) y después también el tercero,<br />
iii) cambiar “ writeln ”por“WRITELN ”, y después por “ Writeln ”,<br />
iv) cambiar “ ’Hola Mundo!’ ”por“’HOLA MUNDO!’ ”,<br />
v) modificar el programa para que se escriba “ bye, bye ”envezde“y<br />
Chau! ”.<br />
c) En general, al escribir el programa usamos indentación o sangrías, i.e. espacios<br />
al comienzo de algunos de los renglones, y a veces renglones enteros<br />
en blanco, para resaltar la estructura del programa. Esto no es necesario, e<br />
inclusive podría escribirse el programa en un único renglón, y un espacio o<br />
varios no hacen diferencia:<br />
i) Eliminar o agregar espacios al comienzo, en el medio y/o al final de<br />
algunos renglones, compilar el programa y verificar que se obtiene el<br />
mismo resultado.<br />
ii) Agregar renglones en blanco o poner dos (o todos los que se quiera)<br />
renglones en uno solo, y verificar que se obtiene el mismo resultado (y<br />
recordar que “ ; ” se usa en Pascal como en castellano: para separar<br />
sentencias).<br />
✎ A los fines del programa fuente, los espacios, tabulaciones (tecla tab o<br />
similar) y renglones son intercambiables (mientras no estén encerrados<br />
entre comillas simples).<br />
d) Agregar y/o eliminar comentarios. Por ejemplo, agregar el comentario “colorin,<br />
colorado” después de writeln(’y Chau!’), en el mismo renglón y/o<br />
en el siguiente. ✄<br />
El uso de “ ; ” en Pascal resulta un poco confuso al principio, pero debe<br />
tenerse en mente que se usa como “ , ”o“; ” se usan al construir una oración
Pág. 12 El primer contacto<br />
en castellano: para separar sentencias. Del mismo modo, el “ . ”enPascaltiene<br />
el mismo sentido que el punto final en castellano. En cambio, la “ , ”seusaen<br />
forma distinta, por ejemplo, para separar argumentos de funciones como se hace<br />
en matemáticas.<br />
Pero no desesperar: por el momento no es necesario entender todo lo que se<br />
hace.<br />
2.4. Comentarios Bibliográficos<br />
El programa holamundo está tomado de [10], y es clásica su inclusión al<br />
aprender a programar.
Capítulo 3<br />
Tipos de datos elementales<br />
Recordemos que la información, incluyendo el programa ejecutable, se guarda<br />
en un lugar de memoria, como ristras de ceros y unos. Como números y<br />
caracteres se representan de esta forma, la computadora al tomar un dato debe<br />
saber si se trata de uno u otro. Esto da lugar a distintos tipos de datos, como<br />
estudiamos en este capítulo.<br />
3.1. Tipos, variables e identificadores<br />
Supongamos que guardamos las letras siguiendo el orden del abecedario, ‘ a ’<br />
como 0, ‘ b ’como1,‘c ’ como 10, y así sucesivamente. Vemos que no podríamos<br />
distinguir entre el par de letras “ ba ”ylaúnica letra “ c ”, pues ambas ser<br />
representarían como 10.<br />
Para evitar esta confusión, se decide que todas las letras ocupen siempre el<br />
mismo espacio, por ejemplo un byte de 8 bits. De esta forma tendremos (ver el<br />
problema 2.1) 2 8 = 256 posibilidades para los caracteres, lo cual es suficiente<br />
para guardar las letras de nuestro alfabeto (pero no los de algunos alfabetos<br />
orientales).<br />
Habiendo decidido esto, nos toca ahora guardar números. Como antes, es<br />
conveniente guardar a todos los números en la misma cantidad de bits. Si usáramos<br />
8 bits como hicimos para las letras, tendríamos sólo 256 números disponibles,<br />
lo cual es bien pobre. Es conveniente que las “cajas” sean más grandes.<br />
Cuando la máquina lea estas cajas que guardan letras o números, tiene que<br />
saber cuántos bits juntos tiene que leer, e interpretar la ristra de bits según sea<br />
una letra o un número. Surgen así cuatro tipos elementales de datos, cada uno<br />
con distinta codificación interna:<br />
• boolean para guardar los valores “ true ” (verdadero) o “ false ”(falso),<br />
• char para guardar caracteres, i.e. letras, signos de puntuación y otros que<br />
veremos más adelante,<br />
• integer para guardar números enteros, como 1, 0, −5, y<br />
• real para guardar números reales, i.e. números como 123.456.<br />
Llamamos a las variables lógicas “booleanas”, en honor a G. Boole (1815–<br />
1864), quien hizo importantes progresos al “algebrizar” la lógica.
Pág. 14 Tipos de datos elementales<br />
En la figura 3.1 ilustramos algunas cajas con datos dentro de la memoria,<br />
con sus tipos correspondientes.<br />
a 1234 123.456<br />
char integer real<br />
verdadero 4321 98.7654<br />
boolean integer real<br />
Figura 3.1: Datos de tipos elementales en la memoria.<br />
Ahora tenemos el problema de cómo acceder a esas cajas que guardan caracteres<br />
o números. Esto es sencillo: les ponemos nombres para identificarlas. Las<br />
cajas así nombradas se llaman variables pues podremos colocar en ellas datos<br />
distintos o (¡ejem!) variables y los nombres que reciben se llaman identificadores.<br />
En la figura 3.2 mostramos algunos nombres posibles para las cajas que<br />
mostramos en la figura 3.1.<br />
a 1234 123.456<br />
codigo a x<br />
verdadero 4321 98.7654<br />
fin m y<br />
Figura 3.2: Los datos con sus identificadores.<br />
Cuando redactamos el programa, debemos indicar al compilador los nombres<br />
y tipos de variables que usaremos, procedimiento llamado de declaración de<br />
variables. En el programa se pone una lista de variables y tipos después de la<br />
palabra clave “ var ”.<br />
Una cosa trae a la otra, y tenemos el problema de que no podemos poner<br />
cualquier nombre. Por ejemplo, no es conveniente usar “ program ”o“writeln ”<br />
que usa Pascal para instrucciones del lenguaje. Además, en Pascal hay nombres<br />
reservados, que no podemos usar como identificadores.<br />
✎ La lista completa de los nombres reservados está en la sección C.3,pág. 217.<br />
Aparte de estas palabras prohibidas, los identificadores en Pascal pueden ser<br />
cualquier sucesión de letras mayúsculas o minúsculas y dígitos, pero<br />
• siempre tienen que empezar con una letra,
3.1. Tipos, variables e identificadores Pág. 15<br />
• no pueden tener espacios entre medio,<br />
• ni pueden tener caracteres como $, ,+,etc.,<br />
• y por supuesto, dos variables distintas no pueden tener el mismo nombre.<br />
A diferencia de otros lenguajes (como “C” o Mathematica), según el estándar<br />
Pascal no se distingue entre mayúsculas o minúsculas tanto en los identificadores<br />
como en palabras claves. Así, podemos poner sin problemas en alguna parte<br />
“ writeln ”yenotra“WriteLn ” (como hemos hecho en el problema 2.2).<br />
✎ Sin embargo, algunos compiladores sí diferencian entre mayúsculas y minúsculas<br />
en identificadores y palabras reservadas.<br />
Problema 3.1. Decidir cuáles de los siguientes son identificadores válidos en<br />
Pascal:<br />
a) mn32xy b) 32xymn c) mn32 xy d) M32nxY e) mn 32 ✄<br />
También tenemos otro problema: cómo colocar los datos en estas cajas o<br />
variables, proceso que se llama asignación.<br />
Una forma de hacerlo es mediante “ := ” (sin espacios intermedios). Así, si<br />
queremos guardar un 2 en la variable n queesdetipoenteroponemos“n := 2”,<br />
y si queremos copiar los contenidos de la caja o variable a en la caja o variable<br />
b, ponemos “ b := a”. Hay que tener un poco de cuidado en este último caso,<br />
pues siempre se pueden hacer asignaciones entre variables del mismo tipo, pero<br />
no es posible hacer asignaciones arbitrarias entre distintos tipos de variables, lo<br />
que veremos con algún detalle en el problema 3.11.<br />
✎ Aunque no debe dejarse espacio entremedio en “ := ”, los espacios a cada<br />
lado no son necesarios. Así, “ n:=2 ”o“n := 2” tienen el mismo efecto.<br />
✎ Otros lenguajes usan otros símbolos para la asignación, en vez de “ := ”. Al<br />
escribir en “seudo código”, muchas veces se usa el símbolo ←, que parece<br />
mucho más apropiado y evita confusiones con el símbolo =.<br />
Otra forma de colocar datos en las respectivas variables es mediante la lectura<br />
de datos, por ejemplo, si a está declarado como “ real ”, la instrucción<br />
“ readln(a)” (que veremos en la sección 3.2.1) lee el número que se ingresa por<br />
terminal y lo guarda en la variable a.<br />
Desde ya que<br />
¡nunca hay que usar el valor de una variable sin antes<br />
haber hecho una asignaciónalavariable!<br />
aunque hay compiladores que automáticamente ponen algún valor al hacer las<br />
declaraciones.<br />
Para tratar de entender esta jerigonza, pasemos a estudiar algunos ejemplos<br />
comenzando con los tipos numéricos “ integer ”y“real ”, para luego considerar<br />
los tipos “ boolean ”y“char ”.
Pág. 16 Tipos de datos elementales<br />
3.2. Tipos numéricos: entero y real<br />
La vida sería un tanto aburrida si sólo pudiéramos cambiar valores de lugar,<br />
es más interesante si podemos realizar operaciones entre estas variables. Así,<br />
suponiendo que las variables a, b y c son del tipo adecuado, una instrucción<br />
como “ a := b + c” hace que la CPU tome los datos que están en las cajas o<br />
variables b y c, las sume, y coloque el resultado en la caja o variable a.<br />
Problema 3.2. Compilar y ejecutar el programa sumardos (pág. 173) analizando<br />
la sintaxis y qué hacen las distintas instrucciones. Por ejemplo:<br />
a) ¿Cuántos comentarios tiene el programa fuente?, ¿qué pasa si se los elimina?<br />
✎ Nosotros pondremos siempre un primer comentario en el programa<br />
fuente para indicar qué hace el programa. A su vez, al ejecutar el<br />
programa trataremos de imprimir un cartel similar para que el usuario<br />
sepa qué es lo que se pretende hacer.<br />
b) La parte declarativa del programa tiene dos sentencias. En la primera estáel<br />
nombre del programa y en la segunda se declaran a y b como de tipo entero,<br />
separando los identificadores con “ , ”.<br />
Ver qué ocurre si se cambia “ var a, b: integer;” por<br />
i) var a; b: integer;<br />
ii) var a: integer; var b: integer;<br />
iii) var a: integer; b: integer;<br />
c) ¿Quépasa si cambiamos el nombre de a por sumardos?<br />
✎ El resultado puede depender del compilador. En general no es aconsejable<br />
usar como nombre del programa una de las palabras reservadas<br />
de Pascal o de una de las variables que aparecen en el mismo programa<br />
fuente, aún cuando el compilador lo acepte.<br />
d) ¿Cuáles son las instrucciones en el cuerpo principal?<br />
e) Observar el uso de la asignación “ := ”, como en “ a := 1” donde se guarda<br />
el valor 1 en a. Cambiar el valor de a para que sea −1 y volver a ejecutar<br />
el programa.<br />
f ) “write ” escribe su/s argumento/s sin terminar un renglón, a diferencia de<br />
“ writeln ”. En cualquier caso, los argumentos (si hay más de uno) están<br />
separados por “ , ”.<br />
i) Cambiar todas las ocurrencias de “ write ”por“writeln ”, y observar<br />
los cambios en la salida del programa.<br />
ii) Cambiar los renglones<br />
write(’La suma de ’, a);<br />
write(’ y ’, b);<br />
writeln(’ es ’, a + b);<br />
por el único renglón<br />
writeln(’La suma de ’, a,’ y ’, b, ’ es ’, a + b);<br />
¿Hay alguna diferencia en la salida?<br />
g) La última línea, “ writeln; writeln(’** Fin **’) ”essólo para avisar<br />
al usuario que el programa ha terminado, y el programa ya no realizará acciones.<br />
Eliminarla y verificar el comportamiento del programa (recordando<br />
la nota al principio del problema 2.2).
3.2. Tipos numéricos: entero y real Pág. 17<br />
h) Cambiar las sentencias de modo de obtener la suma de números reales, i.e.<br />
poner “ real ”envezde“integer ”enladeclaraciónde variables. Compilar<br />
y ejecutar el programa, observando los cambios en la salida.<br />
i) Agregar una variable c, de tipo entero o real según corresponda, hacer la<br />
asignación “ c := a + b” e imprimir c en vez de a + b.<br />
j ) Modificarlo de modo de escribir también la resta (“ - ”), producto (“ * ”) y<br />
división (“ div ” para enteros y “ / ” para reales), tanto en el caso de enteros<br />
como de reales.<br />
k) ¿Quépasacuando se divide por 0? ✄<br />
Conceptualmente hay que distinguir en Pascal entre la variable, elidentificador<br />
yelvalor, i.e. entre la caja, su nombre y lo que contiene.<br />
A fin de no ser demasiado latosos, es común tanto en Pascal como en matemáticas<br />
o la vida real, no distinguir entre identificador, valor y variable, uso<br />
que nosotros seguiremos salvo cuando lleve a confusión: cuando decimos “Josées<br />
simpático”, en general queremos decir “la persona cuyo nombre es José tiene la<br />
cualidad de ser simpática”, y no que el nombre en sí essimpático, en cuyo caso<br />
diríamos “el nombre José essimpático”.<br />
3.2.1. “Readln”<br />
Es un poco tedioso estar cambiando los valores en el programa fuente para<br />
hacer los cálculos. Mucho más razonable es ingresar los datos a nuestro antojo:<br />
Problema 3.3. El programa leerdato (pág. 173) lee un entero ingresado por<br />
terminal y lo imprime. Observar la sintaxis e instrucciones, similares al programa<br />
sumardos, laúnica novedad es el uso de “ readln ”.<br />
a) Compilar y ejecutar el programa, comprobando su funcionamiento.<br />
b) Ingresar, en ejecuciones sucesivas del programa, los siguientes datos (lo que<br />
está entre “ ”, pero sin las comillas) seguidos de , conservando los<br />
espacios intermedios cuando corresponda:<br />
i) “ 23 ”; ii) “2 ”; iii) “2 3”;<br />
iv) “2a ”; v) “a2 ”; vi) “2”.<br />
c) Si“readln ” no tiene argumentos, se lee el renglón entrado —lo escrito<br />
hasta — y lo escrito no se guarda (como la instrucción en la nota<br />
al principio del problema 2.2, enlapág. 10): agregar el renglón<br />
readln;<br />
antes de<br />
write(’Entrar un ...<br />
¿Qué pasasiseingresa“” yluego“1”?,¿ysise<br />
ingresa “ 1” yluego“2”?<br />
d) Combinando los programas sumardos y leerdato, hacer un programa para<br />
ingresar por terminal dos números enteros e imprimir su suma.<br />
✎ Además de “ readln ” existe la función “ read ”, pero postergaremos su uso<br />
para más adelante. Basta decir por ahora que la instrucción “ readln(a) ”<br />
es equivalente a las instrucciones “ read(a); readln ”.
Pág. 18 Tipos de datos elementales<br />
Así como con “ writeln ”y“write ” podemos escribir más de un argumento,<br />
con “ readln ”(y“read ”) podemos leer varios argumentos a la<br />
vez, pero no usaremos esta facilidad para evitar confusiones.<br />
Los comportamientos de “ write ”y“writeln ” para leer, y de “ read ”<br />
y“readln ”sonprácticamente simétricos, aunque existen algunas diferencias<br />
importantes. Por ejemplo, en el tratamiento de los ’s como<br />
hemos visto en el inciso b.vi): “ readln(a) ” los ignora mientras no aparezca<br />
el dato a, pero“writeln(a) ”escribeunúnico renglón por vez. ✄<br />
Cuando se programa profesionalmente, es muy importante que el programa<br />
funcione aún cuando los datos ingresados sean erróneos, por ejemplo si se ingresa<br />
una letra en vez de un número, o el número 0 como divisor de un cociente.<br />
Posiblemente se dedique más tiempo a esta fase, y a la interacción con el usuario,<br />
que a hacer un programa que funcione cuando las entradas son correctas.<br />
Nosotros supondremos que el usuario siempre ingresa<br />
datos apropiados, y no haremos (salvo excepcionalmente)<br />
detección de errores. Y mucho menos<br />
nos preocuparemos por ofrecer una interfase estéticamente<br />
agradable.<br />
En cambio, trataremos de dejar claro mediante carteles<br />
qué hace el programa, qué datos han de ingresarse<br />
en cada momento y dar una señal de finalización.<br />
3.2.2. Funciones numéricas<br />
Pero volvamos a los tipos numéricos.<br />
No sólo podemos sumar o multiplicar números sino que, al igual que en las<br />
calculadoras, hay ya funciones predeterminadas como la raíz cuadrada, que en<br />
Pascal se indica por “ sqrt ”.<br />
Problema 3.4. El programa raiz (pág. 174) calcula la raíz cuadrada de un<br />
númerononegativoentradoporterminal.<br />
a) Compilar y ejecutar el programa, analizando qué hacen las distintas instrucciones.<br />
Probar el programa ingresando números reales, enteros, positivos y<br />
negativos.<br />
b) A fin de escribir en pantalla √ x, no es necesario hacer la asignación previa<br />
“ y := sqrt(x) ”:<br />
i) Reemplazar “ y ”por“sqrt(x) ”enlasentencia“writeln ”, compilar<br />
y ejecutar el programa, verificando que el resultado es el mismo.<br />
ii) Eliminar la variable y del programa por completo (eliminando inclusive<br />
su declaración).<br />
c) ¿Quépasasiseingresa un dato menor que 0?<br />
d) Muchas veces se confunde “ sqrt ”con“sqr ”, que encuentra el cuadrado de<br />
un número, y no su raíz cuadrada. Cambiar “ sqrt ”por“sqr ”yverificar<br />
el comportamiento del programa. ✄
3.2. Tipos numéricos: entero y real Pág. 19<br />
✎ Una lista de las funciones predefinidas en Pascal está enelapéndice C<br />
(pág. 215).<br />
3.2.3. La codificación de enteros y reales<br />
En Pascal, los números ocupan un número fijo de bits, por ejemplo 16 bits<br />
para el tipo “ integer ” y 32 bits para el tipo “ real ” (1) ,porloque<br />
En la máquina sólo pueden representarse un número<br />
finito de números, ya sean enteros o reales.<br />
En otras palabras, “la gran mayoría” de los números<br />
no se pueden representar exactamente en la computadora.<br />
Así, hay un máximo entero representable, que en Pascal se llama maxint,<br />
y hay un menor número real positivo representable que llamaremos εmin ydel<br />
cual hablaremos en el problema 4.17.<br />
Problema 3.5.<br />
a) Suponiendo que el tipo “ integer ” tenga asignados 16 bits, ¿cuántos números<br />
enteros pueden representarse?<br />
b) Análogamente, si el tipo “ real ” tiene asignados 32 bits, ¿cuántos números<br />
reales pueden representarse en la máquina? ✄<br />
Problema 3.6. Hacer un programa para imprimir el valor de maxint correspondiente<br />
al compilador que se usa: no declarar variables y poner el renglón<br />
writeln(’ El entero máximo es: ’, maxint)<br />
✎ En los compiladores más viejos, maxint = 32767. ✄<br />
Sea que las variables de tipo “ integer ”y“real ” ocupen el mismo lugar<br />
o no, su codificación como cadenas de bits es bien distinta. Los enteros se<br />
representan consecutivamente (del menor al mayor), mientras que para representar<br />
los números reales se dividen los bits en dos grupos, uno representando<br />
la mantisa yotroelexponente, como se hace en la notación “científica” al escribir<br />
0.123 × 10 45 (0.123 es la mantisa y 45 el exponente en base 10, pero la<br />
computadora trabaja en base 2).<br />
La representación de números reales mediante mantisa y exponente hace que<br />
—a diferencia de lo que sucede con los números enteros— la distancia entre un<br />
número real que se puede representar y el próximo vaya aumentando a medida<br />
que sus valores absolutos aumentan. Esta propiedad de densidad variable trae<br />
inconvenientes para el cálculo aproximado, como veremos en el problema 4.17<br />
y en la sección 5.1.<br />
(1) La cantidad de bits en cada caso depende del compilador. Incluso ambos tipos podrían<br />
tener asignados la misma cantidad de bits.
Pág. 20 Tipos de datos elementales<br />
1 2 4 8 16<br />
Figura 3.3: Esquema de la “densidad variable” en la codificación de números<br />
reales.<br />
✎ Para entender la “densidad variable”, puede pensarse que hay la misma<br />
cantidad de números representados entre 1 (inclusive) y 2 (exclusive) que<br />
entre2y4,oqueentre4y8,etc.Porejemplo,sihubieransólo4puntos en<br />
cada uno de estos intervalos, tendríamos un gráfico como el de la figura 3.3.<br />
Por el contrario, hay tantos números enteros representados entre 10<br />
(inclusive) y 20 (exclusive), como entre 20 y 30, etc. Es decir, entre 20 y 40<br />
hay el doble de números enteros representados que entre 10 y 20. En este<br />
caso, la “densidad es constante”.<br />
Como todo número entero es en particular un número real, es conveniente<br />
poder pasar de la representación como tipo “ integer ”alarepresentación<br />
como tipo “ real ”. Esto se hace automáticamente en Pascal: si a es de tipo<br />
“ integer ”yx es de tipo “ real ”, la asignación<br />
x := a<br />
hace que automáticamente se guarde el valor que tiene a (un entero) en x ,de<br />
modo que ahora el valor se codifica como real.<br />
✎ En particular, la cantidad de bits (y los valores de éstos) usados en la<br />
representaciones como “ integer ”o“real ” son distintas.<br />
✎ También la conversión se hace automáticamente al ingresar datos: si x es<br />
de tipo real, y tenemos la instrucción “ readln(x) ”, el ingreso de 1 como<br />
dato —un entero— hace que se guarde codificado como real en x.<br />
Problema 3.7. Compilar y ejecutar el programa enteroareal (pág. 174). Observar<br />
la declaración de variables de distinto tipo en el mismo programa. ✄<br />
Problema 3.8. Decir por qué las siguientes declaraciones de variables son incorrectas:<br />
i) var a, b, c: integer; c, d, e: real;<br />
ii) var a, b, c: integer; 2c, d, e: real;<br />
iii) var: a, b, c: integer; d, e: real;<br />
iv) var a, b, c: integer; var d, e: real; ✄<br />
Así como escribimos un entero como real, es posible que queramos pasar de<br />
un real a un entero:<br />
Problema 3.9. Modificar el programa enteroareal de modo de leer x , hacer la<br />
asignación “ a := x” e imprimir a: ¿cuál es el resultado? ✄<br />
Como puede observarse, no es posible cambiar de real a entero directamente:<br />
debe eliminarse primero la parte fraccionaria del real, lo que tradicionalmente<br />
se hace de dos formas distintas.<br />
Una es truncar el real, eliminando la parte decimal. Así, cuando truncamos:<br />
1.1 → 1, 1.9 → 1, −1.9 →−1.<br />
Otra forma es redondear el número real reemplazándolo por el entero más<br />
cercano. Claro que en el redondeo tenemos que decidir qué hacer con números
3.2. Tipos numéricos: entero y real Pág. 21<br />
como 0.5, y adoptamos la convención que se “redondea hacia arriba”: 1.1 → 1,<br />
1.9 → 2, −1.9 →−2. 0.5 → 1, −0.5 →−1.<br />
Problema 3.10. Las funciones de Pascal “ trunc ”, correspondiente a truncar,<br />
y“round ”, correspondiente a redondear, cuando aplicadas a un número real en<br />
la forma “ trunc(x)” o“round(x)” dannúmeros enteros.<br />
✎ Las definiciones de ±, pisoytechoestán en la sección D.3.<br />
a) Hacer un programa para averiguar el comportamiento de estas funciones,<br />
tomando las 8 entradas ±3, ±3.1, ±3.5, ±3.7.<br />
b) ¿Quérelaciónhay entre estas funciones y las funciones piso, ⌊x⌋, ytecho,<br />
⌈x⌉?, es decir, ¿cómo pueden escribirse unas en términos de las otras y<br />
viceversa? ✄<br />
En el próximo problema abundamos sobre las asignaciones entre variables<br />
numéricas de distinto tipo:<br />
Problema 3.11 (Asignaciones entre distintos tipos). Supongamos que<br />
hemos declarado<br />
var n: integer; x: real;<br />
En los siguientes, decir cuál es el valor de la última variable asignada o si se<br />
produce un error (sugerencia: hacer un programa, compilarlo y ejecutarlo):<br />
a) x := 3.7; n := x;<br />
b) x := 3.7; n := trunc(x);<br />
c) x := 3.5; n := trunc(x);<br />
d) x := 3.7; n := trunc(-x);<br />
e) x := 3.5; n := trunc(-x);<br />
f ) x := 3.7; n := round(x);<br />
g) x := 3.5; n := round(x);<br />
h) x := 3.7; n := round(-x);<br />
i) x := 3.5; n := round(-x);<br />
j ) n := maxint div 2; n := n * (n+1) / 2;<br />
k) n := maxint div 2; n := n * (n+1) div 2;<br />
l) n := maxint div 2; x := n * (n+1) / 2; ✄<br />
También es posible hacer operaciones mezclando variables numéricas de distintos<br />
tipos:<br />
Problema 3.12. Hacer un programa donde se declara a de tipo entero y b de<br />
tipo real, se lean a y b, y se escriban en pantalla los resultados de: a + b, a ∗ b,<br />
a/b, b/a. ¿Dequétiposon los resultados en cada caso? Sugerencia: modificar<br />
la declaración de variables en el programa sumardos. ✄<br />
De aquí enmás supondremos que el lector compilará<br />
yejecutará cada programa mencionado en los<br />
problemas, probándolo con distintos datos. En lo sucesivo,<br />
generalmente omitiremos esta instrucción.
Pág. 22 Tipos de datos elementales<br />
Problema 3.13. El programa segundos (pág. 175) permite pasar de segundos<br />
a horas, minutos y segundos, usando la función “ mod ”: “ a mod b ”daesencialmente<br />
el resto de la división de a por b.<br />
✎ El número de segundos a ingresar no debe superar maxint.<br />
a) Agregarle instrucciones de modo de poder verificar si el resultado es correcto,<br />
pasando de horas, minutos y segundos a segundos.<br />
b) ¿Quépasa si se coloca el renglón “ writeln(hs, ’ hs, ’,... ”inmediatamente<br />
después del renglón “ writeln(segs, ’ segundos...” yantesde<br />
“ mins := segs div ... ”? ✄<br />
Problema 3.14.<br />
a) Hacer un programa para averiguar el comportamiento de “ mod ”, tomando<br />
como entradas a = ±7 yb = ±3, calculando “ a mod b ”.<br />
b) Comprobar si el valor de “ (a div b) * b + (a mod b) ” coincide con el<br />
de a. ✄<br />
3.3. Variables lógicas<br />
Estamos acostumbrados a valores numéricos, y no es sorpresa que por ejemplo<br />
las variables del tipo “ integer ” o entero, admitan valores como 1 o −987.<br />
Sin embargo, cuesta acostumbrarse a las variables de tipo lógico o “ boolean ”,<br />
que sólo admiten valores verdadero o “ true ”, y falso o “ false ”. Antes de<br />
trabajar con estas variables en programación, practiquemos un poco:<br />
Problema 3.15. En cada caso, decidir si la expresión es verdadera o falsa:<br />
i) 1=2. ii) 1> 2.<br />
iii) 1≤ 2. iv) 4× 5 > 3<br />
4 .<br />
v) 1< 2 < 3. vi) 1< 2 < 0.<br />
vii) 1< 2o2< 0. viii) 1 = cos 1.<br />
ix ) {a, b, c} = {b, a, c}. x ) {0, 1, 2, 3}⊂N. ✄<br />
Habiendo estirado un poco los músculos, tiene sentido preguntarle a la computadora<br />
si un número es positivo o no, y que guarde el resultado en una variable<br />
lógica:<br />
Problema 3.16. Compilar y ejecutar el programa positivo (pág. 175), analizando<br />
qué hacen las distintas instrucciones.<br />
Agregar una variable pos, declarada como lógica (“ boolean ”), incluir en el<br />
cuerpo del programa la asignación “ pos := a > 0 ” e imprimir pos,envezde<br />
“ a > 0”. ✄<br />
Debemos destacar que al comparar números, Pascal usa a veces una sintaxis<br />
ligeramente diferente a la usual:<br />
Matemáticas Pascal<br />
= =<br />
= <br />
> ><br />
≥ >=<br />
< <<br />
≤
3.4. Caracteres Pág. 23<br />
debiendo tener cuidado en no confundir “ := ”, que indica asignación, con “ = ”,<br />
que pregunta si los valores a ambos lados coinciden:<br />
• “ a := a + 1” significa tomar el valor de a, agregarle 1 y guardar el<br />
resultado en a,<br />
• mientras que “ a = a + 1” es una proposición lógica con valor “falso”, y<br />
no se modifica el valor de a.<br />
Así como para números tenemos la suma, el producto o el inverso aditivo,<br />
para variables lógicas tenemos la conjunción y, a veces simbolizada con ∧, la<br />
disyunción o, simbolizada con ∨, y la negación no, simbolizada con ¬. EnPascal<br />
usamos respectivamente “ and ”, “ or ”y“not ”.<br />
Por ejemplo si a y b son de tipo entero, pondríamos<br />
Matemáticas Pascal<br />
(a >0) ∧ (b >0) (a > 0) and (b > 0)<br />
(a >0) ∨ (b >0) (a > 0) or (b > 0)<br />
¬(a >0) not (a > 0)<br />
pero la expresión matemática a
Pág. 24 Tipos de datos elementales<br />
En Pascal, dado un carácter podemos encontrar su número de orden mediante<br />
“ ord ”yrecíprocamente, dado un entero podemos ver qué carácter le<br />
corresponde mediante la función “ chr ” (2) .Elestándar Pascal establece que:<br />
• Los caracteres ‘ 0 ’,‘ 1 ’,. . . ,‘ 9 ’están numéricamente ordenados y son consecutivos,<br />
pero<br />
• las letras minúsculas, ‘ a ’,‘ b ’,...,‘z ’, si bien están ordenadas, ¡no son<br />
necesariamente consecutivas!<br />
• y lo mismo para las letras mayúsculas.<br />
Afortunadamente, el código ASCII —con el que trabajaremos— satisface<br />
estos requisitos, y más aún, las letras minúsculas ‘ a ’,...,‘z ’ son consecutivas y<br />
lo mismo para las mayúsculas.<br />
Desafortunadamente, si bien para nosotros ‘ ~n ’estáentre‘n ’y‘o ’, esto no es<br />
asíenelcódigo ASCII, pues la letra ‘ ~n ’nisiquieraestácodificada.Niquéhablar<br />
de ch o ll, que se consideran (en cada caso) como dos caracteres distintos. Todo<br />
esto puede traer problemas al clasificar u ordenar alfabéticamente.<br />
Problema 3.18 (Ordinales y caracteres). El programa caracteres1 (pág.<br />
175) toma un carácter como entrada, retorna su número de orden y verifica la<br />
corrección.<br />
a) ¿Quépasa si cambiamos el nombre del programa de “ caracteres1” a<br />
“ char ”?, ¿ya “char1 ”?<br />
✎ Recordar lo dicho sobre identificadores al principio del capítulo y en<br />
el problema 3.2.c).<br />
b) ¿Quépasasiseescribecomoentrada“tonto ”?<br />
c) ¿Cuál es el ordinal correspondiente a los caracteres ‘ a ’, ‘ A ’, ‘ ’ (espacio),<br />
y tabulación (tecla “tab”)?<br />
d) Hacer un programa que, inversamente, dado un número retorne el carácter<br />
correspondiente.<br />
e) Averiguar si hay caracteres correspondientes a números mayores que 127 (el<br />
resultado dependerá delamáquina). ¿Y a números mayores que 255?<br />
f ) ¿Cuál es el carácter correspondiente al número 7 (en ASCII)? ✄<br />
3.5. Comentarios Bibliográficos<br />
Los problemas 3.1 y 3.8 están basados en similares de [3].<br />
(2) ¡No confundir “ char ”con“chr ”!
Capítulo 4<br />
Tomando control<br />
Con las operaciones que hemos visto no podemos hacer mucho más que lo<br />
que hace una calculadora sencilla. Las cosas empiezan a ponerse interesantes<br />
cuando disponemos de estructuras de control de flujo, esto es, instrucciones que<br />
nos permiten tomar decisiones sobre si realizar o no determinadas instrucciones<br />
o realizarlas repetidas veces.<br />
Al disponer de estructuras de control, podremos verdaderamente comenzar a<br />
describir algoritmos, es decir, instrucciones (no necesariamente en un lenguaje de<br />
programación) que nos permiten llegar a determinado resultado, y apuntar hacia<br />
el principal objetivo de este curso: pensar en los algoritmos y cómo traducirlos<br />
a un lenguaje de programación.<br />
✎ Se conocen pocos detalles de la vida de Abu Ja’far Muhammad ibn Musa<br />
al-Khwarizmi’. Presuntamente nació alrededor de 780 en Bagdad (Irak)<br />
—cuando Harun al-Rashid, el califa de “Las mil y una noches”, comenzaba<br />
su califato— y murió en 850.<br />
Al-Mamun hijo y sucesor de al-Rashid, continuó la tradición de su<br />
padre de patrocinar las ciencias y fundó laacademia“Casadelsaber”,a<br />
la que se incorporó Al-Khwarizmi.<br />
Al-Khwarizmi escribió y tradujo varios textos científicos (de matemática,<br />
geografía, etc.) del griego al árabe. El más importante de ellos es “Hisab<br />
al-jabr w’al-muqabala”, de donde surge la palabra “álgebra” conque ahora<br />
designamos a esa rama de la matemática.<br />
Al-Khwarizmi también escribió un tratado sobre números indo-arábigos<br />
que se ha perdido, pero se conservó una traducción al latín llamada<br />
“Algoritmi de numero Indorum”, para indicar su autor, que dio lugar a la<br />
palabra “algoritmo” que usamos.<br />
Los programas irán aumentando en longitud, lo que nos obligará a encarar<br />
de forma diferente su confección, debiendo pensar con más cuidado primero en<br />
qué queremos hacer, luego en cómo lo vamos a hacer, y finalmente pasar a los<br />
detalles. Pascal, y casi todos los lenguajes de programación, son poco “naturales”<br />
en este sentido, exigiendo —por ejemplo— la declaración de variables al<br />
principio del programa fuente, mientras que nosotros pensaremos el programa<br />
de “abajo hacia arriba” (o casi), y recién al final miraremos cómo declarar las<br />
variables necesarias.<br />
En Pascal, las estructuras fundamentales de control son “ if ”, “ while ”,<br />
“ repeat ”y“for ”, que pasamos a estudiar.
Pág. 26 Tomando control<br />
✎ En Pascal hay otras estructuras de control, como “ case ”, que no veremos.<br />
En otros lenguajes de programación existen otras estructuras, pero casi<br />
siempre están los equivalentes de “ if ”y“while ”, con las que se pueden<br />
simular las otras.<br />
4.1. “If”<br />
Supongamos que al cocinar decidimos bajar el fuego si el agua hierve, es decir<br />
realizar cierta acción si se cumplen ciertos requisitos. Podríamos esquematizar<br />
esta decisión con la sentencia:<br />
si el agua hierve entonces bajar el fuego.<br />
A veces queremos realizar una acción si se cumplen ciertos requisitos y<br />
además realizar una acción alternativa si no se cumplen. Por ejemplo, si para<br />
ir al trabajo podemos tomar el colectivo o un taxi —que es más rápido pero<br />
más caro que el colectivo— dependiendo del tiempo que tengamos decidiríamos<br />
tomar uno u otro, que podríamos esquematizar como:<br />
si es temprano entonces tomar el colectivo en otro caso tomar el taxi.<br />
En Pascal podemos tomar este tipo de decisiones, usando la construcción<br />
“ if...then...” para el esquema “si ...entonces ...”, mientras que para la<br />
variante “si ...entonces ...en otro caso ...” usamos“if...then...else...”<br />
Por supuesto, entre “ if ”y“then ” tenemos que poner una condición (una<br />
expresión lógica) que pueda evaluarse como verdadera o falsa.<br />
Problema 4.1.<br />
a) Agregar el siguiente renglón al programa leerdato (pág. 173):<br />
if (a > 10) then writeln(chr(7));<br />
inmediatamente después del renglón “ writeln(’El entero...);”.<br />
¿Cuál es el efecto de agregar este renglón?<br />
b) Modificar el programa leerdato de modo que la computadora emita un sonido<br />
cuando el número entrado es mayor que 0 y menor o igual a 10 (y no haga<br />
nada en otro caso). ✄<br />
Problema 4.2. El valor absoluto para x ∈ R se define como<br />
<br />
x si x ≥ 0,<br />
|x| =<br />
−x en otro caso.<br />
El programa valorabsoluto (pág. 176) realiza este cálculo. Observar en especial<br />
la estructura “ if...then...else...”. ¿Qué pasa si se eliminan los<br />
paréntesis después del “ if ”?,¿ysisecambia“>= ”por“> ” ?, ¿y si se agrega<br />
“ ; ”antesde“else ”?<br />
La función “ abs ” de Pascal hace el cálculo del valor absoluto. Incorporarla<br />
al programa y verificar que se obtienen los mismos resultados. ✄<br />
Problema 4.3. Hacer un programa que, dado un número real, encuentre su<br />
piso y su techo. Sugerencia: recordar el problema 3.10. ✄
4.1. “If” Pág. 27<br />
A veces es conveniente encadenar varios “ if ”:<br />
Problema 4.4. El programa comparar (pág. 176) toma como datos los números<br />
enteros a y b y determina cuál es mayor o si son iguales.<br />
a) ¿Quépasa si se escriben las líneas que empiezan con “ if ”yterminanen<br />
“ ; ”enunsolorenglón?<br />
b) ¿Quépasasiseincluyen“; ” al final de cada uno de los renglones originales?<br />
c) Modificar el programa para que siempre escriba en la salida el número a<br />
primero (i.e. si a = −5 yb = 3 escriba “ -5 es menor que 3 ”envezde“3<br />
es mayor que -5 ”).<br />
d) Modificarloparaquesólo decida si a = b o a = b.<br />
e) Modificarlo para que sólo decida si a>bo a ≤ b. ✄<br />
Problema 4.5 (Comparación de caracteres). El programa caracteres2 (en<br />
la pág. 176) decide si un carácter entrado por terminal es una letra minúscula<br />
o una mayúscula o no es una letra, mediante comparación.<br />
Modificarlo de modo de hacer un programa para clasificar un carácter ingresado<br />
como uno de los siguientes:<br />
a) una letra minúscula vocal, b) una letra minúscula consonante,<br />
c) una letra mayúscula vocal, d) una letra mayúscula consonante,<br />
e) undígito (0, 1,...,9), f )niletranidígito.<br />
✎ Recordando lo dicho al principio de la sección 3.4, no deben ingresarse<br />
vocales acentuadas o “ ~n ”. ✄<br />
Problema 4.6. El Gobierno ha decidido establecer impuestos a las ganancias<br />
en forma escalonada: los ciudadanos con ingresos hasta $10000 no pagarán<br />
impuestos; aquéllos con ingresos superiores a $10000 pero que no sobrepasen<br />
$30000, deberán pagar 10 % de impuestos; aquéllos cuyos ingresos sobrepasen<br />
$30000 pero no sean superiores a $50000 deberán pagar 20 % de impuestos, y<br />
los que tengan ingresos superiores a $50000 deberán pagar 35 % de impuestos.<br />
a) Hacer un programa para calcular el impuesto dado el monto de la ganancia.<br />
¿Qué tipos de datos habrá que usar?<br />
b) Modificarlo para determinar también la ganancia neta (una vez deducidos<br />
los impuestos) del ciudadano.<br />
c) Modificarlo de modo que los impuestos y ganancia neta se impriman hasta<br />
el centavo (y no más). ✄<br />
Problema 4.7 (Años bisiestos). Desarrollar un programa para decidir si un<br />
año dado es o no bisiesto.<br />
Julio César (alrededor de 100–44 a. de C.) cambió el calendario egipcio, que<br />
estaba basado en un año de exactamente 365 días, a un nuevo calendario, el juliano,<br />
conañosbisiestos cada 4 años aproximando mejor la verdadera longitud<br />
del año. Sin embargo, cálculos posteriores mostraron que la longitud del año<br />
es aproximadamente 365.2422 días. Con el paso de los siglos, las diferencias<br />
anuales de 0.0078 días en promedio se fueron acumulando, y hacia el año 1582<br />
el Papa Gregorio comenzó un nuevo calendario (el gregoriano). Por un lado,<br />
se agregaron 10 días a la fecha, de modo que el 5 de octubre de 1582 pasó aser<br />
el 15 de octubre (y nunca existieron los días entre el 6 y el 14 de octubre de<br />
ese año). Por otro lado, se decidió quelosañosbisiestos serían precisamente
Pág. 28 Tomando control<br />
los divisibles por 4, excepto que aquéllos que fueran divisibles por 100 serían<br />
bisiestos sólo cuando fueran divisibles por 400 (así elaño 1700 no es bisiesto<br />
pero sí el 2000). Con esta convención, el año promedio tiene 365.2425 días. No<br />
todos los países del mundo adoptaron este calendario inmediatamente. En Gran<br />
Bretaña y lo que es ahora Estados Unidos de Norteamérica, se adoptó recién<br />
en 1752, por lo que se debieron agregar 11 días más; Japón cambió en 1873,<br />
Rusia en 1917 y Grecia en 1923. Aún hoy algunas iglesias ortodoxas mantienen<br />
el calendario juliano, especialmente para determinar la fecha de las Pascuas.<br />
✎ A veces con un pequeño esfuerzo podemos hacer el cálculo más eficiente.<br />
Un esquema para resolver el problema anterior, si anio es el año ingresado,<br />
es<br />
write(anio);<br />
if (anio mod 400 = 0) then writeln(’ es bisiesto’)<br />
else if (anio mod 100 = 0) then writeln(’ no es bisiesto’)<br />
else if (anio mod 4 = 0) then writeln(’ es bisiesto’)<br />
else writeln(’ no es bisiesto’)<br />
Sin embargo, el esquema<br />
write(anio);<br />
if (anio mod 4 0) then writeln(’ no es bisiesto’)<br />
else if (anio mod 100 0) then writeln(’ es bisiesto’)<br />
else if (anio mod 400 0) then writeln(’ no es bisiesto’)<br />
else writeln(’ es bisiesto’)<br />
es más eficiente, pues siendo que la mayoría de los números no son múltiplos<br />
de 4, en la mayoría de los casos haremos sólo una pregunta con el segundo<br />
esquema pero tres con el primero.<br />
Nosotros no vamos a preocuparnos por la eficiencia del programa, pero<br />
haremos comentarios (¡como éste!) de tanto en tanto. ✄<br />
Problema 4.8. Desarrollar un programa que, tomando como entrada un número<br />
natural entre 1 y 3000, lo escriba en romano (e.g. entrando 2999 se debe<br />
obtener MMCMXCIX). Sugerencia: primero hacer un programa para tratar el<br />
caso entre 1 y 30, y después ir agregando posibilidades.<br />
✎ Las letras usadas para números romanos son I, V, X, L, C, D, M, correspondientes<br />
respectivamente a 1, 5, 10, 50, 100, 500, 1000.<br />
✎ El problema es ciertamente tedioso para escribir, pero es típico de muchas<br />
aplicaciones, en las que no hay atajos. ✄<br />
A veces es conveniente y aún necesario agrupar instrucciones, lo que en<br />
Pascal se hace mediante “ begin...end”, como en la parte principal de todo<br />
programa.<br />
✎ La expresión “ end. ” (con punto) sólo debe aparecer al final del programa<br />
fuente.<br />
Este “agrupar” es similar al uso de paréntesis en expresiones matemáticas,<br />
“ ( ” correspondiendo a “ begin ”y“) ” correspondiendo a “ end ”.<br />
Como “ begin...end” es similar a los paréntesis, “ begin ”y“end ”noson<br />
sentencias en sí, por lo tanto<br />
Es incorrecto poner un “ ; ” inmediatamente antes<br />
de un “ end ”.
4.1. “If” Pág. 29<br />
Veamos un ejemplo donde podríamos usar “ begin...end”:<br />
Problema 4.9. Sea f : R → R la función definida como<br />
<br />
x<br />
f(x) =<br />
2 si x>0,<br />
0 si x ≤ 0.<br />
a) Hacer un esquema del gráfico de la función.<br />
b) Hacer un programa para que, dando x como entrada, se imprima f(x).<br />
c) Supongamos que queremos imprimir además un cartel diciendo si el valor<br />
entrado es positivo (y por lo tanto f(x) =x2 ) o si no lo es (y por lo tanto<br />
f(x) = 0). Una posibilidad es, habiendo declarado a x y y como de tipo<br />
“ real ”, poner dos “ if ”’s consecutivos, por ejemplo:<br />
if (x > 0) then writeln( x, ’ es positivo’)<br />
else writeln( x, ’ no es positivo’);<br />
if (x > 0) then y := x*x else y := 0;<br />
writeln(’El valor de la funcion es ’, y);<br />
Incluir en el programa del inciso anterior estas modificaciones.<br />
d) Otra posibilidad, que puede parecer más coherente, es poner un único “ if ”,<br />
agrupando las instrucciones en cada caso:<br />
if (x > 0) then begin<br />
writeln( x, ’ es positivo’);<br />
y := x*x<br />
end<br />
else begin<br />
writeln( x, ’ no es positivo’);<br />
y := 0<br />
end;<br />
writeln(’El valor de la funcion es ’, y);<br />
Modificar el programa del inciso b) incorporando esta variante. ¿Quépasa<br />
si se elimina el “ end ”antesdel“else ”?,¿ysisepone“; ”entre“end ”<br />
y“else ”? ✄<br />
Otras veces el uso de “ begin...end” evita confusiones:<br />
Problema 4.10. Supongamos que hemos declarado<br />
var a, b, n, z: integer;<br />
y que tenemos el renglón<br />
if (n > 0) then if (a > b) then z := a else z := b;<br />
a) ¿Quétiene de confuso este renglón?<br />
b) Decir cuál es el valor de z inmediatamente después de ejecutar el renglón<br />
original cuando los valores de a, b, n y z son:<br />
i) a =1,b =2,n =1,z =0;<br />
ii) a =1,b =2,n = −1, z =0;<br />
iii) a =2,b =1,n =1,z =0;<br />
iv) a =2,b =1,n = −1, z =0;
Pág. 30 Tomando control<br />
c) Decir si el renglón es equivalente a:<br />
i) if (n > 0) then begin<br />
if (a > b) then z := a else z := b end;<br />
ii) if (n > 0) then begin<br />
if (a > b) then z := a end else z := b; ✄<br />
4.2. “While”<br />
La estructura “ while ” es una estructura para realizar repetidas veces una<br />
misma tarea. Junto con “ repeat ”y“for ” —que veremos más adelante— reciben<br />
el nombre común de lazos o bucles para indicar esta capacidad de repetición.<br />
Supongamos que voy al supermercado con cierto dinero para comprar la<br />
mayor cantidad posible de botellas de cerveza. Podría ir calculando el dinero<br />
que me va quedando a medida que pongo botellas en el carrito: cuando no<br />
alcance para más botellas, iré a la caja. Una forma de poner esquemáticamente<br />
esta acción es<br />
mientras me alcanza el dinero, poner botellas en el carrito.<br />
En Pascal, este esquema se realiza con la construcción “ while...do...”,<br />
donde “ do ” reemplaza a la “ , ” en la sentencia anterior, y entre “ while ”y<br />
“ do ” debe ponerse una expresión lógica.<br />
Observamos desde ya que:<br />
• Si la condición no es cierta al comienzo, nunca se realiza la acción: si el<br />
dinero inicial no me alcanza, no pongo ninguna botella en el carrito.<br />
• En cambio, si la condición es cierta al principio, debe modificarse con<br />
alguna acción posterior, ya que en otro caso llegamos a un lazo “infinito”<br />
que nunca termina.<br />
Por ejemplo, si tomamos un número positivo y le sumamos 1, al resultado<br />
le sumamos 1, y así sucesivamente mientras los resultados sean positivos.<br />
O si tomamos un número positivo y lo dividimos por 2, luego otra vez por<br />
2, etc. mientras el resultado sea positivo.<br />
✎ Al menos en teoría. Como veremos en los problemas 4.15 y 4.17, la<br />
máquina tiene un comportamiento “propio”.<br />
Por cierto, en el ejemplo de las botellas en el supermercado podríamos realizar<br />
directamente el cociente entre el dinero disponible y el precio de cada botella,<br />
en vez de realizar el lazo mientras. Esloquevemosenelpróximo problema:<br />
Problema 4.11. El programa resto (pág. 177) calcula el resto de la división de<br />
a ∈ N por b ∈ N mediante restas sucesivas.<br />
a) Antes de compilar y ejecutar el programa, hacemos una “prueba de escritorio”.<br />
Por ejemplo, si ingresamos a =10yb =3,podríamos poner
4.2. “While” Pág. 31<br />
Paso acción a b r<br />
0 (antes de empezar) sin valor sin valor sin valor<br />
1 leer a 10<br />
2 leer b 3<br />
3 r := a 10<br />
4 r ≥ b: verdadero<br />
5 r := r - b 7<br />
6 r ≥ b: verdadero<br />
7 r := r - b 4<br />
8 r ≥ b: verdadero<br />
9 r := r - b 1<br />
10 r ≥ b: falso<br />
11 imprimir r<br />
donde indicamos los pasos sucesivos que se van realizando y los valores de<br />
las variables a, b y r (el último valor que aparece es el que tiene antes de<br />
ejecutarse el paso correspondiente). Podemos comprobar entonces que los<br />
valores de a y b al terminar el programa son los valores originales, mientras<br />
que r se modifica varias veces.<br />
✎ El lector puede la “prueba de escritorio” como le parezca más clara.<br />
La presentada es sólo una posibilidad.<br />
Las “pruebas de escritorio” sirven para entender el comportamiento<br />
del programa y detectar algunos errores (pero no todos). Son útiles<br />
al comenzar a programar, en programas ni demasiado sencillos, como<br />
los que hemos visto hasta ahora, ni demasiado complicados como<br />
varios de los que veremos en los capítulos siguientes.<br />
Otra forma —bastante más primitiva— de entender el comportamiento<br />
de un programa y eventualmente encontrar errores, es probarlo<br />
con distintas entradas, como hemos hecho desde el comienzo, por ejemplo<br />
en el problema 2.2 yprácticamente en todo el capítulo 3.<br />
Alentamos al lector para que haga las “pruebas de escritorio” de<br />
los restantes problemas de éste y el siguiente capítulos.<br />
b) Hacer una “prueba de escritorio” con otros valores de a y b, por ejemplo<br />
con 0
Pág. 32 Tomando control<br />
valor obtenido en el inciso f )?, ¿podrías explicar por qué?<br />
h) ¿Hay algún ejemplo donde tenga sentido declarar las variables a, b y r como<br />
de tipo real en vez de entero? Sugerencia: pensarenradianesyb =2π. ✄<br />
Problema 4.12. El programa tablaseno (pág. 177) produce una tabla del seno<br />
para ángulos expresados en grados. Tanto el ángulo inicial, el ángulo final como<br />
el incremento, son entrados por el usuario.<br />
a) Observar el uso de “ const ”—porconstante— para dar un valor aproximado<br />
de π, anterior a la declaración de variables. Observar también que no<br />
se hace una asignación a pi, sino que se usa el signo “ = ”. El uso de const<br />
hace que no se puedan hacer asignaciones a pi pues no es una variable (es<br />
una constante): tratar de hacer una asignación a pi yobservarquépasa. b) Antes o después de ejecutar el programa, verificar que se obtienen los mismos<br />
resultados de una “prueba de escritorio” (como en el problema 4.11, ahora<br />
con las variables inicial, final, incremento, grados y radianes).<br />
c) Algunos compiladores Pascal tienen pre-definida la constante pi, peronoes<br />
estándar. Verificar si el compilador usado está en esta categoría comentando<br />
el renglón donde se define pi, i.e. encerrándo el renglón entre “ (* ”y“*) ”.<br />
d) En vez de poner manualmente un valor aproximado de π, escomún usar la<br />
igualdad π =4× arctan 1.<br />
i) Cambiar el valor “ 3.14159265” dadoapi en el programa original,<br />
reemplazándolo por “ 4 * arctan(1)”. En general los compiladores<br />
no aceptan este cambio, pues el estándar Pascal no lo permite.<br />
ii) Eliminar el renglón donde se define a pi como una constante y definir<br />
en cambio pi como una variable de tipo real, y en el cuerpo del programa<br />
hacer la asignación de la igualdad anterior. Comparar con los<br />
resultados obtenidos originalmente.<br />
✎ Podría usarse otro valor similar para π, como2×arccos 0 o 2×arcsen 1,<br />
pero arctan (el arco tangente) es la única función trigonométrica inversa<br />
definida en el estándar Pascal.<br />
e) ¿Qué pasa si el valor de incremento es cero? ¿Y si el valor de incremento<br />
es positivo pero el de final es menor que el de inicial?<br />
f ) Modificar el programa tablaseno para producir en cambio una tabla de logaritmos<br />
en base 10, donde los valores inicial , final e incremento son de tipo<br />
real. Sugerencia: enPascalestá definido ln x =log e x, yparacualquiera, b<br />
y x razonables, log a x =log b x/ log b a. ✄<br />
Otro uso muy común de los lazos (“ while ”, “ repeat ”o“for ”) es el cálculo<br />
de sumas o productos de muchos términos. En forma similar al contador k del<br />
problema 4.11.f ), si el resultado se va a guardar en la variable resultado, ésta se<br />
inicializaa0enelcasodelasumaoa1enelcasodelproductoantes del lazo,<br />
yenéste se van sumando o multiplicando los términos deseados modificando<br />
resultado. Ilustramos esta técnica en el siguiente problema:<br />
Problema 4.13 (Suma de Gauss). Para n ∈ N consideremos la suma<br />
sn =1+2+3+···+ n =<br />
n<br />
i,<br />
i=1
4.2. “While” Pág. 33<br />
que, según la fórmula de Gauss, es<br />
sn =<br />
n × (n +1)<br />
.<br />
2<br />
Según se dice, Karl Friederich Gauss (1777–1855) tenía8años cuando el<br />
maestro de la escuela le dio como tarea sumar los números de 1 a 100 para<br />
mantenerlo ocupado (y que no molestara). Sin embargo, hizo la suma muy<br />
rápidamente al observar que la suma era 50 × 101.<br />
Las contribuciones de Gauss van mucho más allá deestaanécdota, sus<br />
trabajos son tan profundos y en tantas ramas de las matemáticas que le valieron<br />
el apodo de “príncipe de los matemáticos”. Para algunos, fue el más grande<br />
matemático de todos los tiempos.<br />
El programa gauss (pág. 178) calculasnsumando los términos (y no usando<br />
la fórmula de Gauss).<br />
a) ¿Cuál es el valor de n al final del lazo? Sugerencia: hacer una “prueba de<br />
escritorio”.<br />
b) ¿Quépasasiseingresan≤0? c) Lavariablesuma está declarada como de tipo real. Si se declarara como de<br />
tipo entero, ¿cuál sería el máximo n para el cual se podría calcular sn (i.e.<br />
para el cual sn ≤ maxint)?<br />
d) ¿Cuántas asignaciones se realizan (en términos de n) dentro del lazo?<br />
e) Modificar el programa de modo que a la salida exprese también el valor<br />
original de n (algo como “ La suma de 1 a 100 es 5050 ” cuando n =<br />
100).<br />
f )Modificarellazo“while ” para que se sume “al derecho”, i.e. primero los<br />
términos más chicos. ✄<br />
Problema 4.14.<br />
a) Modificar el programa gauss para obtener, dado n ∈ N, sufactorial, denotado<br />
por n! y definido como<br />
n! =1× 2 ×···×n.<br />
Sugerencia: recordar lo dicho antes del problema 4.13 sobre el cálculo de<br />
sumasyproductosdevariostérminos.<br />
b) ¿Hay alguna diferencia entre los resultados multiplicando “al derecho” (de<br />
menor a mayor) y “al revés” (de mayor a menor)?<br />
✎ Es posible que sí cuando n es grande, debido a errores numéricos.<br />
c) Determinar cuántos ceros hay al final de la expresión decimal de 30!, y comparar<br />
con la cantidad de ceros que aparecen en el resultado del programa.<br />
Sugerencia: contar las veces que 2 y 5 son factores de 30!.<br />
✎ 30! tiene 33 cifras decimales. ✄<br />
Problema 4.15. Recordando que la suma y producto de enteros son enteros,<br />
nos preguntamos si el siguiente programa termina o no:<br />
program terminaono(input, output);<br />
var i: integer;<br />
begin
Pág. 34 Tomando control<br />
i := 1;<br />
while (i * i
4.4. “For” Pág. 35<br />
b) ¿Quépasasisecalculaεmaq mediante<br />
eps := 1;<br />
repeat eps := eps/2 until (1 + eps = 1);<br />
eps := 2 * eps;<br />
writeln(’epsmaq es: ’, eps); ?<br />
✎ Es posible que los valores sean distintos, debido a la forma de trabajo<br />
internadelamáquina.<br />
c) Cambiar el lazo “ repeat ”enelcálculo de εmin porunlazo“while ”haciendo<br />
las modificaciones necesarias. ¿Cuál es más natural, “repeat ”o<br />
“ while ”? ✄<br />
Para calcular εmaq y εmin hemos violado justamente la regla de oro:<br />
4.4. “For”<br />
¡Nunca deben compararse números reales por igualdad<br />
sino por diferencias suficientemente pequeñas!<br />
Hay veces que queremos repetir una acción un número fijo de veces. Por<br />
ejemplo, al subir una escalera con 10 escalones haríamos algo como<br />
hacer 10 veces: subir un escalón.<br />
En Pascal no existe una estructura que traduzca esta acción directamente.<br />
✎ Pero sí en Modula, el sucesor de Pascal también desarrollado por N. Wirth.<br />
En cambio, Pascal cuenta con una estructura un poco más flexible que nos<br />
permite imitarla: si contamos los escalones con el “contador” e, quevariaráentre<br />
1 y 10, podríamos poner el esquema<br />
para e desde1a10hacer subir el e-ésimo escalón.<br />
Este esquema se reproduce en Pascal (habiendo declarando e como de tipo<br />
entero) con la estructura<br />
for e := 1 to 10 do subir el e-ésimo escalón<br />
que ciertamente podría cambiarse por un lazo “ while ”:<br />
e := 1;<br />
while (e
Pág. 36 Tomando control<br />
Así, una de las diferencias entre “ while ”(o“repeat ”) y “ for ”esque<br />
no aparece explícitamente una condición lógica. Otra de las diferencias es que,<br />
según el estándar Pascal, el valor de la variable que indica el paso, llamada<br />
variable de control (en el ejemplo e) no está definido al finalizar el lazo “ for ”,<br />
aunque muchos compiladores no respetan esta convención.<br />
En general, usaremos “ for ” cuando sabemos exactamente la cantidad de<br />
veces que hay que pasar por el lazo, por ejemplo para calcular la potencia x n<br />
cuando x ∈ R y n ∈ N, donde debemos multiplicar x por sí mismon veces:<br />
Problema 4.18. El programa potencia (pág. 180) calcula la potencia n-ésima<br />
del real x.<br />
a) Compilarlo y ejecutarlo, verificando su comportamiento.<br />
b) No es posible tomar bases y exponentes muy grandes sin que haya overflow<br />
o underflow, i.e. resultados que no se pueden representar en la máquina por<br />
ser muy grandes o muy pequeños. Probar con distintos valores hasta obtener<br />
este comportamiento, e.g. 10 10 , 100 100 ,(1/10) 10 ,(1/100) 100 ,etc.<br />
c) Modificar el programa de modo de imprimir el valor de i al finalizar el lazo.<br />
✎ Como mencionáramos, según el estándar el valor no está definido, pero<br />
muchos compiladores no respetan esta convención y el valor obtenido<br />
puede depender del compilador: no debe usarse esta variable después<br />
del lazo “ for ” sin antes re-asignarla.<br />
d) Agregar sentencias para considerar también el caso de n negativo. Sugerencia:<br />
a b =1/a −b , usar la función “ abs ” y/o condicional “ if ”.<br />
e) Una variante de “ for...to...do...”, que va incrementando la variable<br />
de control usada de a 1, es usar “ for...downto...do...”, que va disminuyéndola<br />
en 1 en cada paso.<br />
Cambiar el renglón<br />
for i := 1 to n do pot := pot * x;<br />
por el siguiente:<br />
for i := n downto 1 do pot := pot * x;<br />
y verificar que el comportamiento es el mismo. Observar que con “ downto ”<br />
el valor inicial debe ser mayor o igual que el valor final para que se realice<br />
algún paso.<br />
f ) Algunos compiladores admiten la operación “ x ** y”, que no es del estándar<br />
Pascal, para el cálculo de la potencia xy (x ∈ R+, y ∈ R). Verificar si<br />
el compilador usado admite esta sentencia y, en caso afirmativo, comparar<br />
con el resultado anterior cuando y ∈ N.<br />
g) Otra forma de calcular la potencia en general es usar x y = e y×ln x .Incorporar<br />
esta fórmula al programa para comparar con los resultados anteriores.<br />
✎ Es posible que difieran debido a errores numéricos. ✄<br />
Muchos de los problemas que hemos visto usando “ while ”o“repeat ”<br />
pueden hacerse con “ for ” con modificaciones sencillas, observando que para<br />
considerar valores descendientes con “ for ”, se debe usar “ downto ”envezde<br />
“ to ”. Por ejemplo, en el programa gauss (pág. 178) ellazo<br />
while (n > 0) do begin suma := suma + n; n := n-1 end;
4.5. Ingresando muchos datos: “Eoln” Pág. 37<br />
podría reemplazarse por el lazo<br />
for i := n downto 1 do suma := suma + i;<br />
donde la variable i es de tipo entero.<br />
Sin embargo, no siempre se puede o es sencillo usar “ for ”:<br />
Problema 4.19.<br />
a) Modificar los programas tablaseno (pág. 177) ygauss (pág. 178) cambiando<br />
el lazo “ while ” por uno “ for ”.<br />
b) ¿Podría cambiarse el lazo “ while ” por uno “ for ” en el programa resto<br />
(problema 4.11)?, ¿y el lazo “ repeat ” por uno “ for ” en el programa<br />
epsmin (problema 4.17)? ✄<br />
4.5. Ingresando muchos datos: “Eoln”<br />
En esta sección veremos distintas formas de ingresar muchos datos, que es<br />
una operación muy común en las aplicaciones.<br />
Problema 4.20. Hacer un programa que —usando un lazo “ for ”— calcule e<br />
imprima la suma de cierta cantidad de enteros entrados por terminal. El usuario<br />
debe ingresar primero el número de datos y después cada uno de los datos a<br />
sumar. ✄<br />
Problema 4.21. Cuando entramos datos, muchas veces es molesto contar la<br />
cantidad de datos antes de entrarlos: es mucho más cómodo ingresarlos y dar<br />
una señal al programa de que la entrada de datos ha terminado. Una posibilidad<br />
es usar un “dato imposible” como señal, por ejemplo si se ingresa −1 cuando<br />
los datos a sumar son positivos.<br />
Haciendo esta suposición (i.e. el usuario sólo ingresa los datos a sumar que<br />
son positivos, y la señal de fin de datos que es −1), modificar el programa del<br />
problema 4.20 cambiando el lazo “ for ” por uno “ while ”o“repeat ”.<br />
Sugerencia: agregar una variable booleana findatos.<br />
Sugerencia si la anterior no alcanza: usar un esquema como<br />
suma := 0; findatos := false;<br />
repeat<br />
write(’Entrar un dato (fin = -1): ’); readln(x);<br />
if (x = -1) then findatos := true<br />
else suma := suma + x<br />
until (findatos); ✄<br />
Ciertamente el “dato imposible −1” del problema 4.21 no es muy conveniente,<br />
ya que tendremos que modificar el programa si en algún momento queremos<br />
usarlo para sumar datos que pueden ser negativos. Más cómodo sería ingresar<br />
como “dato imposible” alguna señal que no imponga limitaciones sobre los<br />
númerosaentrar(salvoqueseandetipoenterooreal).<br />
Una posibilidad es poner como “dato imposible” un “dato vacío”, es decir,<br />
nada, o más concretamente un sin entrada de datos, usando la función<br />
de Pascal “ eoln ”.<br />
Pero antes de describir esta función, será conveniente que el lector repase un<br />
poco lo dicho —y hecho— al introducir la función “ readln ” en la sección 3.2.1.
Pág. 38 Tomando control<br />
Cuando ingresamos un dato, éste no es leído por el programa hasta que<br />
ingresemos , dándonos la oportunidad de cambiarlo en caso de error<br />
de tipeo.<br />
Si hemos declarado la variable a (de algún tipo), la sentencia “ readln(a)”<br />
hace que el programa lea el primer dato que no sea justamente ylo<br />
guarde en a si el dato es correcto (si es incorrecto, puede pasar cualquier cosa:<br />
ver el problema 3.3.b)).<br />
Si en el programa fuente tenemos la instrucción “ readln ”, sin argumentos,<br />
entonces se lee el renglón ingresado, y si hemos entrado otros datos antes de<br />
, éstos son ignorados (ver el problema 3.3.c)).<br />
Al ingresar nosotros , nosólo estamos indicando que terminamos<br />
de ingresar nuestros datos, sino que se emite una señal especial que llamaremos<br />
fin de línea.<br />
✎ La señal de “fin de línea” depende del sistema operativo, y puede consisir<br />
de uno o más caracteres.<br />
Cuando ponemos “ readln(a) ” lo que hacemos efectivamente es “leer” la<br />
señaldefindelínea además del dato a, yalponer“readln ” (sin argumentos)<br />
loquesehaceesleersólolaseñaldefindelínea que no se guarda en ninguna<br />
variable.<br />
La función de Pascal “ eoln ” (una abreviatura de “end of line” o fin de<br />
línea), sin argumentos, devuelve un valor lógico, i.e. verdadero o falso, según<br />
haya o no un ofindelínea por leer.<br />
Como “ readln ”, “ eoln ” se queda a la espera de ingreso de datos, que<br />
necesariamente deben incluir un . Si se han ingresado datos antes<br />
de , “eoln ”retornafalso ysino,verdadero. Pero a diferencia de<br />
“ readln ”, “ eoln ” no “lee” el fin de línea, y debemos realizar esta “lectura”<br />
con “ readln ” sin argumentos.<br />
✎ Sin embargo, después de evaluarse “ eoln ”, un nuevo “ readln ” con argumentos<br />
hará que se ignoren los ’s que hayamos introducido<br />
aunque no se hayan “leído”, como ya hemos verificado en el problema 3.3.<br />
Problema 4.22. El programa eolnprueba (pág. 180) es una prueba del funcionamiento<br />
de “ eoln ”, en el que se usa la variable a para entender el flujo de las<br />
instrucciones.<br />
a) Compilar y ejecutar el programa, observando que luego de imprimir el valor<br />
de a antes del lazo, queda a la espera del ingreso de datos:<br />
i) Ingresar “”, sin otros caracteres, y observar el valor final<br />
de a. ¿Cuáles de las instrucciones en “ if ” se ejecutaron?, ¿qué valor<br />
ha dado “ eoln ”yporqué?<br />
ii) Si en vez de ingresar sólo “ ”, se ingresa “ ”<br />
(con un espacio antes), ¿cuál será el valor de a queseimprimeal<br />
final? Verificar la respuesta ejecutando el programa con esa entrada.<br />
b) Después del renglón “ writeln(’Ahora el valor... ” agregar los renglones<br />
write(’Entrar un nuevo valor de a: ’);<br />
readln(a);<br />
writeln(’El valor final de a es ’, a);
4.6. Comentarios Bibliográficos Pág. 39<br />
i) Ejecutar el programa e ingresar “ ” y“3” (con<br />
un 3 antes), ¿cuáles son los valores sucesivos de a?, ¿por qué?<br />
ii) Si en la nueva variante ingresamos “ 4” (conun4antes)y<br />
luego “ 5” (con un 5 antes), ¿cuáles son los valores sucesivos<br />
de a?, ¿por qué?<br />
En cada caso ejecutar el programa y verificar la respuesta. ✄<br />
Problema 4.23. El programa sumardatos (pág. 181) calcula la suma de números<br />
reales entrados por terminal, donde para finalizar la entrada de datos se<br />
ingresa sin dato.<br />
✎ Comparar con las sugerencias del problema 4.21.<br />
a) Observar el uso de “ eoln ” para detectar que se ha entrado un <br />
sin dato, y el uso de “ readln ”para“leer”elfindelínea que ha quedado.<br />
¿Qué pasasiseeliminaeste“readln ”?<br />
b) Modificar el programa para que a la salida indique también la cantidad de<br />
datos ingresados.<br />
c) Modificar el programa para que también indique el promedio de los datos<br />
entrados. ✄<br />
Problema 4.24. El lazo “ repeat...until (findatos)” del programa sumardatos:<br />
a) ¿Podría cambiarse por<br />
repeat<br />
write(’Entrar un dato (fin = ): ’);<br />
readln(x); s := s + x<br />
until (eoln) ?<br />
b) ¿Y por<br />
while (not eoln) do begin<br />
write(’Entrar un dato (fin = ): ’);<br />
readln(x); s := s + x<br />
end ?<br />
Explicar en cada caso, eventualmente haciendo un programa para verificar<br />
que las respuestas son correctas. ✄<br />
4.6. Comentarios Bibliográficos<br />
Los comentarios históricos sobre años bisiestos están basados en [13].
Capítulo 5<br />
Aplicaciones<br />
En este capítulo mostramos que se puede hacer bastante matemáticas con<br />
los recursos de programación que hemos visto.<br />
5.1. Cálculo numérico elemental<br />
Ciertamente una de las aplicaciones más importantes de la computadora —y<br />
a la cual debe su nombre— es la obtención de resultados numéricos. A veces es<br />
sencillo obtener los resultados deseados pero a veces —debido a lo complicado<br />
del problema o a los errores numéricos— es sumamente difícil, lo que ha dado<br />
lugar a toda un área de matemáticas llamada “cálculo numérico”.<br />
5.1.1. Mezclando números grandes y pequeños<br />
Sorprendentemente, al trabajar con números de tipo “ real ” pueden pasar<br />
cosas curiosas como:<br />
• al sumar un número grande a uno pequeño (en relación) puede dar nuevamente<br />
el número grande,<br />
• que la diferencia sea 0 aún cuando los números son distintos,<br />
• o que la suma no sea conmutativa.<br />
Por supuesto, ya hemos visto este tipo de comportamiento al calcular εmaq<br />
(problema 4.17), debido fundamentalmente a la densidad variable de números<br />
de tipo “ real ” mencionada en la sección 3.2.3 (pág. 19).<br />
Problema 5.1.<br />
a) Encontrar x de tipo real tal que, para la máquina, ¡x = x +1!.<br />
Sugerencia: buscar una potencia de 2, usando algo como<br />
x := 1; repeat x := 2 * x; y := x + 1 until (x = y)<br />
b) ¿Quérelación hay entre x y1/εmaq? Por ejemplo, ¿es x × εmaq = 1 o<br />
parecido? ✄
5.1. Cálculo numérico elemental Pág. 41<br />
Problema 5.2 (Números armónicos). Para n ∈ N, el n-ésimo número<br />
armónico se definen como<br />
Hn =1+ 1 1 1<br />
+ + ···+<br />
2 3 n =<br />
n 1<br />
i .<br />
i=1<br />
Así, H1 =1,H2 =3/2, H3 =11/6, H4 =25/12, H5 = 137/60.<br />
a) Desarrollar un programa (siguiendo las ideas del problema 4.13 sobre suma<br />
de Gauss), para calcular Hn aproximadamente.<br />
b) En este caso particular, puede demostrarse que es más preciso hacer la<br />
suma “de atrás hacia adelante” que “de adelante hacia atrás”. Comprobar<br />
que para n grande difieren los resultados realizando las sumas en estas dos<br />
formas.<br />
c) En los cursos de análisis o cálculo matemáticosevequeamedidaquen<br />
crece, Hn crece indefinidamente como ln n.<br />
Ver que efectivamente la diferencia Hn − ln n es aproximadamente constante<br />
para valores grandes de n (esta constante es la constante de Euler<br />
γ = 0.5772156649 ...), por ejemplo calculando las diferencias para n =<br />
100, 1000, 10000. ✄<br />
El siguiente problema muestra algunos de los inconvenientes al restar cantidades<br />
grandes y similares.<br />
Problema 5.3 (Problemas numéricos en la ecuación cuadrática). Como<br />
es sabido, la ecuación cuadrática<br />
ax 2 + bx + c =0 (5.1)<br />
donde a, b, c ∈ R son datos con a = 0, tiene soluciones reales si<br />
no es negativo, y están dadas por<br />
x1 = −b + √ d<br />
2a<br />
d = b 2 − 4ac<br />
y x2 = −b − √ d<br />
. (5.2)<br />
2a<br />
a) Hacer un programa que, dados a, b y c, verifiquesia= 0yd ≥ 0, poniendo<br />
un aviso en caso contrario, y en caso afirmativo calcule x1 y x2 usando las<br />
ecuaciones (5.2), y también las cantidades ax2 i + bxi + c, i =1, 2, viendo<br />
cuán cerca están de 0.<br />
b) Cuando d ≈ b2 , i.e. cuando |4ac| ≪b2 , pueden surgir inconvenientes numéricos.<br />
Por ejemplo, calcular las raíces usando el programa del inciso anterior,<br />
cuando a =1,b = 10000 y c = 1, verificando si se satisface la ecuación (5.1)<br />
en cada caso.<br />
✎ Las soluciones con varios dígitos son:<br />
x1 = −0.000100000001000 ... y x2 = −9999.999899999998999 ...<br />
c) Uno de los problemas en el ejemplo del inciso anterior surge de restar números<br />
grandes que son comparables. El siguiente fragmento de programa alivia<br />
un poco la situación:
Pág. 42 Aplicaciones<br />
if (b > 0) then x1 := -(b + sqrt(d))/(2 * a)<br />
else x1 := (sqrt(d) - b)/(2 * a);<br />
x2 := c / (x1 * a);<br />
i) Justificar esta construcción. Sugerencia: recordar que x1 + x2 = −b/a<br />
y x1x2 = c/a.<br />
ii) Hacer un programa con estas modificaciones y comparar con los resultados<br />
en b).<br />
No es importante recordar el “truco”. El ejemplo está aquí para mostrar<br />
cómo el área de cálculo numérico se preocupa en dar soluciones a las<br />
dificultades computacionales. ✄<br />
El siguiente problema muestra dificultades no sólo computacionales sino también<br />
matemáticas al considerar una sucesiónqueseaproximaa1peroquees<br />
elevada a potencias cada vez más grandes.<br />
Problema 5.4. Consideremos la sucesión<br />
<br />
an = 1+ 1<br />
n , para n ∈ N.<br />
n<br />
a) Hacer un programa para generar an dado n (¿de qué tiposserán n y an),<br />
y usarlo para varios valores de n. En base a la experiencia, ¿podrías decir<br />
que an es creciente (i.e. si an
5.1. Cálculo numérico elemental Pág. 43<br />
hacer un programa que ingresando x ∈ R+ y n ∈ N, calcule<br />
<br />
<br />
<br />
√x<br />
··· (= x 1/2n<br />
).<br />
<br />
n raíces<br />
b) Ejecutar el programa para distintos valores de x (positivo) y n (más o menos<br />
grande dependiendo de x). ¿Qué seobserva? ✄<br />
Como el lector habrá comprobado en el problema anterior, a medida que<br />
aumentamos el número de iteraciones, i.e. el valor de n, nos aproximamos cada<br />
vez más a 1. Por supuesto que si empezamos con x = 1, obtendremos siempre<br />
el mismo 1 como resultado, ya que √ 1=1.<br />
En general, si tenemos una función f, un punto x en el rango de f se dice<br />
punto fijo de f si<br />
f(x) =x,<br />
de modo que 1 es un punto fijo de la función f(x) = √ x.<br />
Problema 5.6. ¿Hay algún otro punto fijo de la raíz cuadrada, además de<br />
x =1? ✄<br />
En lo que resta de la sección trabajaremos con funciones continuas, es decir,<br />
funciones que “pueden dibujarse sin levantar el lápiz del papel”. Ejemplos de<br />
funciones continuas son: cualquier polinomio P (x), |x|, cosx, y √ x (para x><br />
0) que acabamos de ver. Para nosotros será suficiente esta idea intuitiva: la<br />
definición rigurosa de función continua se da en los cursos de análisis o cálculo<br />
matemático.<br />
✎ Por supuesto, hay funciones continuas que “no se pueden dibujar” como<br />
• 1/x para x>0, pues cerca de x = 0 se nos acaba el papel,<br />
• sen 1/x para x>0, pues cerca de x = 0 oscila demasiado,<br />
•<br />
f(x) =<br />
x sen 1/x<br />
0<br />
si x = 0,<br />
si x =0<br />
pues cerca de x = 0 oscila demasiado,<br />
y hay funciones que no son continuas como<br />
• signo(x) parax∈Ê, pues pega un “salto” en x =0,<br />
• la función de Dirichlet,<br />
f(x) =<br />
una función imposible de visualizar.<br />
1 si x ∈ É,<br />
0 si x ∈ Ê \ É,<br />
Muchas funciones de interés interactúan de manera interesante con sus puntosfijoscomoenelcasodelaraíz<br />
cuadrada: supongamos que x0 es un punto<br />
dado o inicial y definimos<br />
x1 = f(x0),x2 = f(x1),...,xn = f(xn−1),...<br />
y supongamos que tenemos la suerte que xn se aproxima o converge a ℓ amedida<br />
que n crece, i.e.<br />
xn ≈ ℓ cuando n es muy grande.
Pág. 44 Aplicaciones<br />
1<br />
0.5<br />
y<br />
y = x<br />
y =cosx<br />
0.5 1 1.5<br />
Figura 5.1: Gráficos de y =cosx y y = x.<br />
Puede demostrarse entonces que, si f es continua, ℓ es un punto fijo de f.<br />
Por ejemplo, supongamos que queremos encontrar x tal que cos x = x. Mirando<br />
el gráfico de la figura 5.1, vemos que efectivamente hay un punto fijo de<br />
f(x) =cosx, i.e. un punto x ∗ tal que f(x ∗ )=x ∗ . Podemos apreciar en el gráfico<br />
que el punto buscado está entre0.5 y1,yprobamoslatécnica mencionada:<br />
dado x0 ∈ R definimos<br />
xn+1 =cosxn<br />
x<br />
para n =0, 1, 2,...,<br />
ytratamosdeversixn se aproxima a algún punto cuando n crece.<br />
En la figura 5.2, vemos cómo a partir de x0 = 0, nos vamos aproximando al<br />
punto fijo, donde los trazos horizontales van desde puntos en el gráfico de f a<br />
la diagonal y = x y los verticales vuelven al gráfico de f:<br />
1<br />
0.5<br />
y<br />
0.5 1 1.5<br />
Figura 5.2: Aproximándose al punto fijo de cos x.<br />
Problema 5.7 (Punto fijo de f(x) =cosx). Con las notaciones anteriores<br />
para f y xi:<br />
a) Usando un lazo “ for ”, hacer un programa que dados x0 y n calcule xn, y<br />
observar el comportamiento para distintos valores de x0 y n.<br />
b) Modificar el programa para que también imprima cos xn y comprobar que<br />
para n más o menos grande se tiene xn ≈ cos xn.<br />
c) Modificar el programa para hacer 200 iteraciones, mostrando los resultados<br />
intermedios cada 10. Observar que después de cierta cantidad de iteraciones,<br />
los valores de xk no varían.<br />
x
5.1. Cálculo numérico elemental Pág. 45<br />
✎ El valor de x200 es una buena aproximación al único punto fijo de<br />
f(x) =cosx,aún cuando puede ser que x200 = cosx200(= x201) debido<br />
a errores numéricos. El “verdadero” punto fijo es 0.7390851332 ....<br />
d) Modificar el programa de modo que no se hagan más iteraciones si<br />
|xk+1 − xk| 0esunparámetro entrado por el usuario (e.g. ε =0.00001), aún<br />
cuando k sea menor que n.<br />
Sugerencia: hay que cambiar el lazo “ for ” a uno “ while ”o“repeat ”.<br />
Sugerencia si la anterior no alcanza: suponiendo que x0 es el dato inicial<br />
y n el número máximo de iteraciones, usando un lazo “ while ”podríamos<br />
poner:<br />
k := 1; x := x0; y := cos(x);<br />
while ((k < n) and (abs(y - x) >= eps)) do begin<br />
k := k + 1; x := y; y := cos(x)<br />
end<br />
✎ Observar que la condición |xk+1−xk|
Pág. 46 Aplicaciones<br />
sección es una versión actualizada de una técnica usada por los babilónicos hace<br />
miles de años para aproximar a la raíz cuadrada.<br />
Los babilonios tomaron poder de la Mesopotamia (entre los ríos Tigris y<br />
Éufrates) alrededor de 2000 a. de C., desalojando a los sumerios, quienes<br />
habían estado allí desde 3500 a. de C. Fueron los sumerios los que desarrollaron<br />
el sistema sexagesimal (en base 60) que nos llega a la actualidad en la<br />
división de horas en minutos y segundos, y los babilonios continuaron con el<br />
desarrollo del sistema en base 60, llegando a un sistema que en algunos sentidos<br />
era más avanzado que el decimal nuestro.<br />
Pero los babilonios también estudiaron la resolución de ecuaciones cuadráticas<br />
y cúbicas, llegando al equivalente de la ecuación cuadrática (5.1) ymuy<br />
posiblemente al método que presentamos en esta sección para aproximar raíces<br />
cuadradas. El método estaría descripto en la tableta “Yale”, fechada entre 1800<br />
y 1650 a. de C.<br />
Para llegar a este método, los babilonios habrían usado ideas geométricas<br />
para aproximar la media geométrica de los números x y a/x,<br />
Ö a<br />
x ×<br />
x ,<br />
que es el número √ a buscado, por su media aritmética (o promedio),<br />
1<br />
a<br />
x + .<br />
2 x<br />
Mucho más tarde, Isaac Newton (1642–1727) desarrolló unmétodo basado<br />
en el análisis matemático para encontrar raíces de funciones mucho más generales,<br />
que tiene como caso particular al método babilónico. El método de Newton<br />
también es conocido como de Newton-Raphson, pues J. Raphson (1648–1715)<br />
publicó este resultado unos 50 años antes que se publicara el de Newton.<br />
Variantes del método de Newton-Raphson, que se estudia en los cursos<br />
de cálculo numérico, son los que usan las computadoras y calculadoras para<br />
calcular funciones como el seno o el logaritmo.<br />
Problema 5.9 (Método babilónico I). Este método para aproximar la raíz<br />
cuadrada de a ∈ R+, consiste en aplicar sucesivamente las iteraciones<br />
xn = 1<br />
<br />
xn−1 +<br />
2<br />
a<br />
<br />
para n =1, 2,..., (5.3)<br />
xn−1<br />
a partir de un valor inicial x0 dado (x0 > 0). Es decir xn = fa(xn−1), donde<br />
fa : R+ → R+ está definida como<br />
fa(x) = 1<br />
<br />
2<br />
x + a<br />
x<br />
a) Probar que si a>0yx>0, entonces fa(x) > 0, y si fa(x) =x (x es un<br />
punto fijo de fa) entonces x = √ a.<br />
b) En particular, si x0 > 0 y definimos xn como en la ecuación (5.3), entonces<br />
xn > 0paratodon∈N, ysixn = √ a para algún n, entonces xn = xn−1 =<br />
··· = x0 = √ a. Por lo tanto, si suponemos precisión “infinita” el método<br />
nunca da la respuesta exacta salvo que x0 = √ a.<br />
c) Bosquejar un gráfico similar al segundo del problema 5.7, cuando a =2y<br />
x0 =1.<br />
d) ¿Quépasasia =0?¿Ysix0 =0?<br />
<br />
.
5.1. Cálculo numérico elemental Pág. 47<br />
e) ¿Quépasasix0 < 0ya>0?<br />
f )¿Quépasasia0).<br />
✎ En el programa original se consideran errores absolutos en vez de relativos.<br />
Estos errores se estudian en cursos de cálculo numérico y estadística,<br />
y también en otras ciencias como física o química.<br />
El procedimiento de normalización o escalado que hicimos en este<br />
inciso es esencial en cálculo numérico: trabajar con papas y manzanas<br />
ynoconátomos y planetas, o, más científicamente, con magnitudes<br />
del mismo orden.<br />
Cuando se comparan papas con manzanas en la computadora, se<br />
tiene en cuenta el valor de εmaq, pero al trabajar cerca del 0 hay que<br />
tener en cuenta a εmin.<br />
f ) Verificar el comportamiento del programa en los casos de los incisos d), e)<br />
y f )delproblema5.9.<br />
✎ En el caso a 1, o en forma oscilatoria, si<br />
f(x) =−x 3 y x0 > 1.<br />
Es decir, un método iterativo puede no converger a una solución.<br />
Por otro lado, como hemos visto también en el problema 5.8, aún
Pág. 48 Aplicaciones<br />
cuando un método iterativo converja a una solución, no siempre obtenemos<br />
el punto fijo que buscamos, salvo que empecemos con un valor<br />
“razonablemente” cercano. ✄<br />
5.2. Números enteros y divisibilidad<br />
No sólo podemos obtener resultados numéricos con la computadora, sino que<br />
podemos inferir resultados teóricos experimentando:<br />
Problema 5.11.<br />
a) Hacer un programa para calcular la suma de los primeros n números impares<br />
positivos (n ∈ N),<br />
tn =1+3+···+(2n − 1) =<br />
n<br />
(2k − 1).<br />
b) Obtener el valor para distintos valores de n, y conjeturar una fórmula similar<br />
a la de Gauss en el problema 4.13.<br />
c) Tratar de demostrar la fórmula. Sugerencia: usarlafórmula de Gauss o, si<br />
se conoce el método, usar inducción. ✄<br />
5.2.1. Ecuaciones diofánticas<br />
k=1<br />
Problema 5.12. Queremos resolver el siguiente problema:<br />
Geri y Guille compraron botellas de vino para la reunión. Ambos<br />
querían quedar bien y Guille gastó $5 por botella, mientras que Geri,<br />
que es más sibarita, gastó $8 por botella. Si entre los dos gastaron<br />
$81, ¿cuántas botellas compró cada uno?<br />
a) Hacer un programa para resolver el problema. Sugerencia: se trata de resolver<br />
la ecuación 5x +8y = 81, con x, y ∈ Z, x, y ≥ 0. Por lo tanto,<br />
0 ≤ x ≤⌊81 5 ⌋. Usando un lazo “ for ”, recorrer los valores de x posibles,<br />
x =0, 1,...,16, buscando y ∈ Z, y ≥ 0.<br />
✎ Hay dos soluciones posibles.<br />
✎ En este caso es más eficiente recorrer los valores de y (desde 0 hasta<br />
⌋ = 10) pues hay menos valores a considerar.<br />
⌊ 81<br />
8<br />
b) Generalizar el programa para resolver ecuaciones de la forma ax + by = c,<br />
donde a, b, c ∈ N son dados por el usuario y x, y son incógnitas enteras no<br />
negativas. El programa debe exhibir todas los pares (x, y) de soluciones o<br />
decir que no hay soluciones posibles.<br />
✎ La estrategia de resolución es recorrer todas las posibilidades, una por<br />
una, y por eso se llama de barrido, pero hay algoritmos más eficientes para<br />
este caso basados en que el máximo común divisor entre a y b, mcd(a, b)<br />
puede escribirse como mcd(a, b) =Aa + Bb para A, B ∈ , y que valores<br />
apropiados de A y B pueden obtenerse mediante el algoritmo de Euclides,<br />
que veremos luego. Los detalles se ven en cursos de matemática discreta o<br />
teoría elemental de números. ✄
5.2. Números enteros y divisibilidad Pág. 49<br />
Diofanto de Alejandría (aproximadamente 200–284 d. de C.) fue el primero<br />
en estudiar sistemáticamente problemas de ecuaciones con soluciones enteras,<br />
siendo autor del influyente libro “Aritmética”. En su honor, las ecuaciones<br />
donde las incógnitas son enteros se llaman diofánticas o diofantinas.<br />
Otras ecuaciones diofánticas bien conocidas son las de la forma x n + y n =<br />
z n , donde las incógnitas son x, y, z ∈ Æ y n ∈ Æ está dado. Cuando n =2,las<br />
soluciones (x, y, z) forman una terna pitagórica, y cuando n>2, tenemos la<br />
célebre ecuación de <strong>Fe</strong>rmat-Wiles, que no tiene soluciones.<br />
Justamente <strong>Fe</strong>rmat (1601–1665) fue quien escribió en el margen de su copia<br />
de la “Aritmética” de Diofanto (traducida por Bachet) sobre la imposibilidad<br />
de resolución de la ecuación x n + y n = z n en enteros cuando n>2:<br />
Encontré una demostración verdaderamente destacable, pero el margen<br />
del libro es demasiado pequeño para contenerla.<br />
Wiles demostró el teorema en 1994, ¡casi 350 años después!<br />
La técnica del problema anterior puede extenderse para “barrer” más de dos<br />
números:<br />
Problema 5.13. En los partidos de rugby se consiguen tantos mediante tries<br />
(5 tantos cada try), tries convertidos (7 tantos cada uno) y penales convertidos<br />
(3 tantos cada uno).<br />
Hacer un programa que ingresando la cantidad total de puntos que obtuvo<br />
un equipo al final de un partido, determine las formas posibles de obtener ese<br />
resultado.<br />
Por ejemplo, si un equipo obtuvo 21 tantos, las formas posibles son:<br />
Posibilidad Tries Tries convertidos Penales<br />
1 0 0 7<br />
2 0 3 0<br />
3 1 1 3<br />
4 3 0 2 ✄<br />
También la técnica de “barrido” puede usarse para ecuaciones no lineales:<br />
Problema 5.14.<br />
a) Hacer un programa para que dado n ∈ N determine si existen enteros nonegativos<br />
x, y tales que x2 + y2 = n, exhibiendo en caso positivo un par de<br />
valores de x, y posibles, y en caso contrario imprimiendo un cartel adecuado.<br />
b) En base a los resultados anteriores, ¿cuáles son los n ∈ N que se pueden<br />
escribir como suma de dos cuadrados?<br />
✎ Se puede demostrar (lo que se ve en teoría elemental de números) que<br />
todo entero positivo es suma de cuatro cuadrados (algunos eventualmente<br />
nulos), y que un entero positivo es suma de dos cuadrados si y<br />
sólosiesdelaformaab, donde a es una potencia de 4 (eventualmente<br />
4 0 =1)yb tiene resto 1 al dividirlo por 4. ✄<br />
5.2.2. Máximo común divisor y el algoritmo de Euclides<br />
Dados a, b ∈ N, elmáximo común divisor entre a y b, mcd(a, b), se define (1)<br />
como el máximo elemento del conjunto de divisores comunes de a y b:<br />
(1) ¡Como su nombre lo indica!<br />
mcd(a, b) =máx D,
Pág. 50 Aplicaciones<br />
donde<br />
y<br />
D = {d ∈ Z : d | a y d | b}.<br />
✎ D= ∅, pues 1 ∈D.Además, D está acotado superiormente (por mín{a, b}),<br />
por lo que mcd(a, b) está bien definido.<br />
Cuando a, b ∈ Z, definimos<br />
mcd(a, b) =mcd(|a|, |b|)<br />
mcd(0,z)=mcd(z,0) = |z|z para todo z ∈ Z, z = 0.<br />
No tiene mucho sentido mcd(0, 0), pero por comodidad ponemos<br />
mcd(0, 0) = 0.<br />
Cuando mcd(a, b) = 1, es usual decir que los enteros a y b son primos entre<br />
sí o coprimos.<br />
En la escuela elemental a veces se enseña a calcular el máximo común divisor<br />
efectuando primeramente la descomposición en primos (2) . Sin embargo, la<br />
factorización en primos es computacionalmente difícil (3) y en general bastante<br />
menos eficiente que el algoritmo de Euclides, que aún después de 2000 años, es<br />
el más indicado (con pocas variantes) para calcular el máximo común divisor.<br />
Euclides (alrededor de 325–265 a. de C., aunque hay discusión sobre si se<br />
trata de una persona o un grupo) escribió una serie de libros de enorme influencia<br />
en las matemáticas, inclusive en las actuales. En la proposición 1 del libro VII,<br />
se enuncia:<br />
Dados dos números distintos, y restando sucesiva y continuamente<br />
el menor del mayor, si el número que queda nunca mide el anterior a<br />
él hasta que queda una unidad, los números originales serán primos<br />
entre sí.<br />
Yenlaproposición 2,<br />
Dados dos números y no primos entre sí, encontrar su máxima medida<br />
común.<br />
procediendo a construirlo. En lenguaje moderno, nos dice que para encontrar<br />
el máximo común divisor, “la máxima medida común”, entre a y b, debemos<br />
restar el menor del mayor hasta que los dos sean iguales.<br />
✎ Por supuesto que Euclides con “números” se refiere a lo que nosotros llamamos<br />
números naturales. En los tiempos de Euclides se pensaba en números<br />
como longitudes de segmentos, y la multiplicación de números es un área.<br />
Un poco antes de Euclides, con Pitágoras y el descubrimiento de la<br />
irracionalidad de √ 2, surgió elproblemadelaconmensurabilidad de segmentos,<br />
es decir si dados dos segmentos de longitudes a y b existe otro de<br />
longitud c tal que a y b son múltiplos enteros de c, i.e. c =mcd(a, b). Si<br />
a es irracional, como √ 2, y b =1,entoncesnoexistec, y el algoritmo de<br />
Euclides no termina.<br />
(2) La definición de número primo está en la sección 5.2.3.<br />
(3) Ver también las notas después del problema 5.20.
5.2. Números enteros y divisibilidad Pág. 51<br />
Problema 5.15 (Algoritmo de Euclides). El programa euclides (pág. 182)<br />
es una versión de este algoritmo para encontrar mcd(a, b) cuando a, b ∈ Z, cambiando<br />
las restas sucesivas por el cálculo del resto mediante la función “ mod ”.<br />
✎ En el problema 4.11 hemos hecho el proceso inverso: para calcular el resto<br />
hicimos restas sucesivas.<br />
a) Teniendo en cuenta la descripción original del algoritmo, ¿sería correcto<br />
cambiar el lazo principal por<br />
(* lazo principal *)<br />
repeat<br />
if (a > b) then a := a - b else b := b - a<br />
until (a = b); ?<br />
¿Y por<br />
(* lazo principal *)<br />
repeat<br />
if (a > b) then a := a - b<br />
else if (a < b) then b := b - a<br />
until (a = b); ?<br />
Explicar en cada caso.<br />
b) Ver que el programa termina en un número finito de pasos, por ejemplo en<br />
no más de |a| pasos.<br />
✎ Observar que en el método babilónico (sección 5.1.3), hemos debido<br />
poner “criterios de parada”, pero no aquí. Recordar también la nota<br />
inmediatamente antes de este problema.<br />
c) Modificar el programa de modo que a la salida escriba la cantidad de veces<br />
que realizó ellazo“while ”.<br />
d) ¿Quépasasise cambia la instrucción “ while (b 0) do ”por“while<br />
(b > 0) do”?<br />
e) ¿Quépasa si se eliminan los renglones donde se consideran los casos a
Pág. 52 Aplicaciones<br />
✎ Recordando el tema de la conmensurabilidad mencionado al introducir el<br />
algoritmo de Euclides, no siempre el problema tiene solución. Por ejemplo,<br />
si Pablito hace 1 metro cada 2 pasos y el papá √ 2 metros cada 2 pasos. ✄<br />
Problema 5.17. El mínimo común múltiplo de a, b ∈ N, mcm(a, b), se define<br />
en forma análoga al máximo común divisor: es el menor entero del conjunto<br />
{k ∈ N : a | k y b | k}.<br />
a) En la escuela nos enseñan (4) que si a, b ∈ N entonces<br />
mcm(a, b) × mcd(a, b) =a × b.<br />
Escribir un programa para calcular mcm(a, b) paraa, b ∈ N, usando esta<br />
relación.<br />
b) ¿Cómo podría extenderse la definición de mcm(a, b) paraa, b ∈ Z? ¿Cuál<br />
sería el valor de mcm(0,z)? ✄<br />
Problema 5.18.<br />
a) ¿Quérelación hay entre el máximo común divisor o el mínimo común múltiplo<br />
con el problema 5.16 (de Pablito y su papá)? Sugerencia: recordando<br />
que el problema no siempre tiene solución, volver a mirar la descripción<br />
dada por Euclides de su algoritmo pensando en números cualesquiera, restando<br />
el mayor del menor hasta llegar a una “medida común”, y quizás el<br />
inciso h) delproblema4.11 (pág. 32).<br />
b) Hacer un programa para resolver ese problema, donde las entradas son el<br />
número de pasos y la cantidad de metros recorridos tanto para Pablito como<br />
para su papá.<br />
✎ Como para la computadora todos los números son racionales, el problema<br />
siempre tiene solución. ✄<br />
5.2.3. Números primos<br />
Recordemos que un número n ∈ N es primo si n>1ysusúnicos divisores<br />
(positivos) son 1 y él mismo. Los primeros primos son 2, 3, 5, 7 y 11.<br />
✎ Algunos autores consideran que −2, −3, etc. también son primos, pero<br />
nosotros los consideraremos siempre mayores que 1.<br />
También en sus libros Euclides demuestra muchos teoremas importantes sobre<br />
números primos: en el Libro VII, Proposiciones 31 y 32, que todo número<br />
se puede escribir como producto de primos, y en el Libro IX, Proposición 20<br />
—con una de las demostraciones más hermosas de las matemáticas— que hay<br />
infinitos primos.<br />
Problema 5.19.<br />
a) Probar que si el entero a>1 no es primo, entonces existen enteros b y c,<br />
ambos mayores que 1, tales que b ≤ √ a y a = b × c. Sugerencia: sia = b × c<br />
y1
5.3. Problemas Adicionales Pág. 53<br />
Problema 5.20. El programa factoresprimos (pág. 183) es una elaboración del<br />
programa en el problema 5.19, e imprime todos los factores primos del número<br />
a>1 entrado por terminal.<br />
a) Ver que efectivamente el programa determina todos los factores primos cuando<br />
a>1.<br />
b) Sin embargo, cuando a =1o0elprogramaretornaa, que no es primo.<br />
c) ¿Quépasa cuando a0 entonces:<br />
a) fa(x) − √ a = (x − √ a) 2<br />
.<br />
2x<br />
b) fa(x) 2 − a = (x2 − a) 2<br />
4x2 .<br />
c) Six> √ a,0< (x − √ a)/x < 1, y |fa(x) − √ a| < 1<br />
2 |x − √ a|, i.e. acortamos<br />
más de la mitad la distancia a √ a.<br />
d) x1 ≥ x2 ≥···≥xn ≥···≥ √ a.
Pág. 54 Aplicaciones<br />
✎ Enloscursosdeanálisis o cálculo matemático se ve que por lo tanto,<br />
para n ≥ 1losvaloresdexn decrecen monótonamente hacia un número<br />
b, ycomofa es “continua”, resultará fa(b) =b = √ a.<br />
e) Si x0 = √ a,paran≥1valen las desigualdades<br />
|xn+1 − √ <br />
1<br />
a| < mín<br />
2 |xn − √ a|, |xn − √ a| 2<br />
2 √ <br />
a<br />
y<br />
4a<br />
.<br />
✎ Decimos que la convergencia del método es global y cuadrática puesto<br />
que se verifica una desigualdad como la primera. ✄<br />
|x 2 n+1 − a| < (x2 n − a) 2<br />
Problema 5.23 (Conjetura de Goldbach). La conjetura de Goldbach, originada<br />
en la correspondencia entre Christian Goldbach y Euler en 1742, dice que<br />
todo número par mayor que 4 puede escribirse como la suma de dos números<br />
primos impares (no necesariamente distintos).<br />
✎ Aún no se ha podido demostrar que la conjetura sea verdadera o falsa.<br />
a) Hacer un programa que dado el número n par, n ≥ 6, lo descomponga en<br />
suma de dos primos impares, exhibiéndolos (verificando la conjetura) o en<br />
su defecto diga que no se puede descomponer así.<br />
b) Relacionado con la conjetura, en 1937 Vinogradov demostró que todo número<br />
impar suficientemente grande puede escribirse como suma de tres primos<br />
impares. Hacer un programa (que podría usar el anterior) para verificar que<br />
todo número n impar, n ≥ 9, puede escribirse como suma de tres primos<br />
impares. ✄<br />
5.4. Comentarios Bibliográficos<br />
Las citas de los libros de Euclides están tomadas de [9].
Capítulo 6<br />
Arreglos<br />
A veces queremos tener muchas variables del mismo tipo, por ejemplo una<br />
lista de los 10 primeros números primos. Escribir un nombre para cada una de<br />
las variables ya sería engorroso, pero ¿qué pasa si ahora el problema requiere<br />
una lista de 100 o 1000 en vez de 10 datos? Para estos casos es conveniente<br />
usar una estructura similar a la de vector, que podríamos pensar como v =<br />
(v1,v2,...,vn), para guardar los datos. En programación, esta estructura se<br />
llama arreglo (unidimensional).<br />
Por ejemplo, los 10 primeros números primos son<br />
2, 3, 5, 7, 9, 11, 13, 17, 19, 23,<br />
que podríamos guardar en el vector o arreglo v =(v1,v2,...,v10) donde<br />
v1 =2, v2 =3, v3 =5, v4 =7, v5 =11,<br />
v6 =13, v7 =17, v8 =19, v9 =23, v10 =29.<br />
6.1. Dimensionamiento de arreglos<br />
En presencia de un arreglo, la máquina guarda lugares consecutivos de memoria,<br />
todos del mismo tipo, a los cuales se accede mediante índices (como<br />
un vector) poniendo “ v[i] ”envezdevi en el programa fuente. Puesto que<br />
ocupan un lugar de memoria, y este lugar depende del tipo de sus elementos<br />
ydelacantidadodimensión, debemos hacer una declaración apropiada en el<br />
encabezamiento del programa.<br />
Así, en el caso querer guardar en el arreglo v =(v1,v2,...,v10) los primeros<br />
10 primos (que son enteros), haríamos la declaración<br />
v: array[1..10] of integer;<br />
La parte “ 1..10 ” indica el rango donde se moverán los índices. A diferencia<br />
de los vectores de matemáticaofísica, cuyos índices siempre empiezan con 1,<br />
podemos hacer que los índices tengan cualquier rango, e inclusive que sean negativos,<br />
poniendo por ejemplo “ -30..20 ”, aunque nosotros casi siempre usaremos<br />
arreglos con índices que empiezan en 1 o 0.<br />
Sin embargo,
Pág. 56 Arreglos<br />
Los índices de los arreglos deben ser de tipo entero<br />
(“ integer ”), por lo que los rangos posibles dependen<br />
de maxint.<br />
Para guardar los 10 primeros primos en v, habiéndolo declarado como antes,<br />
haríamos las asignaciones:<br />
v[1] := 2; v[2] := 3; v[3] := 5; v[4] := 7;<br />
v[5] := 11; v[6] := 13; v[7] := 17; v[8] := 19;<br />
v[9] := 23; v[10] := 29;<br />
En la figura 6.1 esquematizamos cómo el arreglo v se guarda en la memoria<br />
de la máquina: las componentes tienen identificadores v[1], v[2], etc., y ocupan<br />
lugares consecutivos del mismo tipo.<br />
v<br />
2<br />
v[1]<br />
3<br />
v[2]<br />
5 7 11 13 17 19 23 29<br />
v[3] v[4] v[5] v[6] v[7] v[8] v[9] v[10]<br />
Figura 6.1: Esquema del arreglo v guardado en memoria.<br />
Problema 6.1. Supongamos que en una serie de números queremos encontrar<br />
cuántos terminan en 0, cuántos en 1, etc.<br />
a) Hacer un programa para realizar esta tarea, sin usar arreglos, donde la<br />
lectura y fin de datos se haga en forma similar al programa sumardatos<br />
(pág. 181). Al terminar de ingresar los datos, el programa debe imprimir la<br />
cantidad de datos que terminaron en 0, 1,...,9.<br />
b) En el programa unidades (pág. 184) se hace el mismo programa, pero usando<br />
arreglos.<br />
• En vez de declarar un contador para cada dígito 0, 1,...,9, declaramos<br />
el arreglo terminaen que nos provee de los 10 contadores necesarios.<br />
Así, la dimensión del arreglo terminaen es 10, y sus índices varían<br />
entre 0 y 9.<br />
• Elusodelarreglonosólo facilita la declaración, sino que después<br />
es mucho más sencillo incrementar el contador pues no tenemos que<br />
hacer una serie de “ if ”’s para determinar el contador adecuado.<br />
• Al comenzar el programa los valores del arreglo no están definidos, y<br />
hay que inicializarlos.<br />
• El ingreso de datos es similar al del programa sumardatos, conunlazo<br />
“ while ”envezde“repeat ” (recordar el problema 4.24).<br />
¿Por qué no se pone directamente “ digito := dato mod 10 ”envez<br />
de “ digito := abs(dato) mod 10 ”paradeterminarelúltimo dígito?<br />
c) El arreglo terminaen está declarado de modo que sus índices puedan variar<br />
entre 0 y 9. Agregar el renglón<br />
writeln(’indice 10: ’, terminaen[10]);
6.2. Búsqueda Lineal Pág. 57<br />
antes del renglón “ writeln; writeln(’** Fin **’) ” y ver que los valores<br />
que se obtienen no tienen sentido (si es que el compilador no se queja). ✄<br />
Muchas veces necesitaremos un arreglo con, digamos, n elementos, pero no<br />
conocemos n de antemano, y es muy fácil cometer errores en la programación<br />
porque podríamos tratar de usar una componente que no existe. Por ejemplo si<br />
declaramos “ a: array[1..10] of integer ”, sería un error poner “ a[0] :=<br />
1 ”o“a[20] := 0 ”, como observamos al final del problema 6.1.<br />
✎ Algunos compiladores pueden agregar instrucciones de modo de comprobar<br />
que cuando se corra el programa se verifique que los índices están dentro<br />
del rango permitido. Por supuesto, esto hace que la ejecución del programa<br />
sea más lenta.<br />
En estos casos tratamos de hacer la declaración para guardar d elementos,<br />
con d prudentemente más grande que el rango con el que pensamos trabajar.<br />
Claro que si hemos declarado el arreglo con d = 100 elementos y usamos sólo<br />
n = 20 lugares, estamos desperdiciando lugar.<br />
Como el cambio de dimensión es frecuente, y sobre todo cuando hay muchos<br />
arreglos con la misma dimensión, es usual declarar una constante, mediante<br />
“ const ”, para indicar las dimensiones y mantenerlas en un lugar destacado del<br />
programa. Así, es conveniente hacer las declaraciones<br />
.<br />
const MAXN = 20;<br />
.<br />
var ..<br />
a: array[1..MAXN] of integer;<br />
.<br />
.<br />
Si queremos usar un arreglo mayor, bastará cambiar el valor de MAXN .<br />
Veamos estas ideas en un ejemplo concreto.<br />
6.2. Búsqueda Lineal<br />
Problema 6.2 (Búsqueda lineal). Una de las primeras aplicaciones de arreglos<br />
es ver si cierto elemento aparece o no en el arreglo, a veces llamado problema<br />
de búsqueda. Más concretamente, dados un arreglo a =(a1,a2,...,an) y<br />
un objeto x (del mismo tipo que los elementos de a), queremos ver si existe i,<br />
1 ≤ i ≤ n, talquex = ai.<br />
La forma más sencilla de hacer la búsqueda es comparar sucesivamente o<br />
linealmente los elementos a1,a2,...,an del arreglo con x, técnica que se llama<br />
de búsqueda lineal.<br />
✎ Como veremos más adelante, podríamos pensar en otros tipos de estrategias<br />
para la búsqueda, como al azar olabinaria.<br />
El programa busquedalineal (pág. 184) muestraestatécnica, donde se ingresa<br />
un arreglo de ndatos enteros y el número x a buscar. Observar que:<br />
• Se leen datos como en el programa sumardatos (pág. 181), donde el fin de<br />
la entrada de datos se señala con sin datos. Como el número
Pág. 58 Arreglos<br />
de dato a ingresar es uno más que el número de datos ya ingresado, se<br />
mantiene ndatos “adelantado” en 1, y al terminar se lo retrocede.<br />
• El programa decide con un lazo “ repeat ”sixesonounelementodelarre glo recorriendo el arreglo linealmente, es decir comparando sucesivamente<br />
a1,a2,...,andatos con x, terminando cuando se encuentra coincidencia o<br />
cuando se han analizado todos los elementos.<br />
a) Sin embargo, al ingresar datos no se verifica si el número de datos leído es<br />
ya MAXN . Modificar el lazo de modo de que esa constante sea tenida en<br />
cuenta (impidiendo que se entren más de MAXN datos).<br />
b) Manteniendo la modificación anterior, cambiar el programa para que en vez<br />
de ingresar un arreglo de longitud máxima 20, se pueda ingresar un arreglo<br />
de longitud máxima 50. Sugerencia: sólo habrá quecambiarunnúmero.<br />
c) Observar que en la impresión del arreglo inicial se usa la función “ mod ”<br />
para imprimir un máximo de 5 datos por renglón.<br />
i) ¿Por quéseponeelrenglón<br />
if ((ndatos mod 5) 0) then writeln; ?<br />
ii) El 5 que indica la cantidad de datos por renglón se usa en dos lugares,<br />
de modo que si se cambia el 5 por otro número, hay que acordarse de<br />
cambiar ambos.<br />
Definir la constante maxrenglon al principio del programa para<br />
eliminar este problema.<br />
iii) Ahora modificar el programa de modo de imprimir 8 datos por renglón<br />
como máximo, cambiando el programa en un único lugar.<br />
d) Para buscar x en el arreglo, se usa un lazo “ repeat ”. Cambiarlo por uno<br />
“ while ” (¡y que el programa siga funcionando correctamente!). Sugerencia:<br />
mirar el programa unidades (pág. 184).<br />
e) Cambiar el lazo principal de modo que, de estar x en el arreglo, se obtenga<br />
el último índice i tal que ai = x, envezdelprimero como hace el programa.<br />
Sugerencia: empezar recorriendo desde atrás.<br />
f ) ¿Podría cambiarse la última parte del programa original por<br />
(* lazo principal *)<br />
i := ndatos;<br />
while ((a[i] x) and (i > 1)) do i := i - 1;<br />
(* resultados *)<br />
if (x a[i]) then writeln(’no se encontro’)<br />
else writeln(’se encontro en el lugar ’, i:1) ?<br />
En caso negativo, decir por qué no, y en caso afirmativo, qué ventajasy<br />
desventajas tendría. Sugerencia: ¿cuál es el último valor de i si x no está en<br />
el arreglo?<br />
g) Modificar el lazo principal de modo que al terminar el programa diga cuántas<br />
veces aparece x en el arreglo, y los índices correspondientes (i.e. los i para<br />
los cuales ai = x).<br />
✎ Observar que el lazo principal cambia sustancialmente.<br />
Sugerencia: una posibilidad es ir imprimiendo i a medida que aparece. Otra<br />
posibilidad es agregar un arreglo para guardar los índices i, por ejemplo:
6.3. Cribas Pág. 59<br />
(* lazo principal *)<br />
veces := 0;<br />
for i := 1 to ndatos do<br />
if (a[i] = x) then begin<br />
veces := veces + 1;<br />
indice[veces] := i<br />
end;<br />
y luego imprimir el arreglo indice (entre 1 y veces si veces > 0). Observar<br />
que en esta variante no es necesaria la variable seencontro. ✄<br />
Problema 6.3. Hacer un programa que dados los arreglos a =(a1,a2,...,am)<br />
y b =(b1,b2,...,bn) (ambos dimensionados por MAXN ), forme un tercer arreglo<br />
c =(c1,c2,...,cm+n) (dimensionado por 2 × MAXN ), consistente en los<br />
elementos de a seguidos por los de b.<br />
Por ejemplo, si<br />
tendremos m =4,n =5,y<br />
a =(1, 3, 5, 3) y b =(7, 1, 5, 8, 3),<br />
c =(1, 3, 5, 3, 7, 1, 5, 8, 3).<br />
Hacer el ingreso de datos (para a y b) repitiendo el esquema del programa<br />
busquedalineal (pág. 184). ✄<br />
Problema 6.4. Hacer un programa que dado el arreglo a lo “purgue”, i.e. elimine<br />
los elementos repetidos. Por ejemplo, si inicialmente a =(3, 5, 2, 6, 2, 1, 3, 2),<br />
al final del programa debe ser a =(3, 5, 2, 6, 1). Sugerencia: “buscar” cada elemento<br />
entre los ya puestos para decidir si agregarlo o no. ✄<br />
6.3. Cribas<br />
Cuando debemos seleccionar en un arreglo todos los elementos que satisfacen<br />
cierto criterio, y el criterio para cada elemento depende de los elementos que le<br />
precedieron, es conveniente usar una criba (o cedazo o tamiz). Quizás la más<br />
conocida de las cribas sea la atribuida a Eratóstenes para encontrar los números<br />
primos entre 1 y n (n ∈ N), donde el criterio para decidir si un número k es<br />
primo o no es la divisibilidad por los números que le precedieron.<br />
Eratóstenes (contemporáneo de Arquímedes y posterior a Euclides) nació en<br />
Cirene (en Libia de hoy) alrededor del 276 a. de C.. Vivió gran parte de su<br />
vida en Alejandría (Egipto), donde murió alrededor de 194 a. de C., después de<br />
haber sido bibliotecario de la famosa biblioteca. Hizo importantes contribuciones<br />
en matemáticas, astronomía, geografía y filosofía. Por ejemplo, fue el primero<br />
en medir correctamente la circunferencia de la Tierra (y pensar que Colón<br />
usaba un huevo, ¡1700 años después!).<br />
Problema 6.5 (Criba de Eratóstenes). Supongamos que queremos encontrar<br />
todos los primos menores o iguales que un dado n ∈ N. Una posibilidad es<br />
verificar si cada uno de los números 2, 3, 4,... es primo, usando la técnica del<br />
problema 5.19. Sin embargo, una forma mucho más eficiente es hacer la criba<br />
de Eratóstenes.<br />
La idea es empezar con la lista
Pág. 60 Arreglos<br />
2 3 4 ... n.<br />
Recuadramos 2, y tachamos los restantes múltiplos de 2 de la lista, quedando<br />
2 3 4 5 6 ...<br />
Ahora miramos al primer número que no está marcado (no tiene recuadro<br />
ni está tachado): 3. Lo recuadramos, y tachamos de la lista todos los múltiplos<br />
de 3 que quedan sin marcar. La lista ahora es<br />
2 3 4 5 6 7 8 9 10 11 12 ...<br />
y seguimos de esta forma, recuadrando y tachando múltiplos hasta agotar la<br />
lista.<br />
a) Ver que el algoritmo es correcto, i.e. los números recuadrados que quedan<br />
finalmente son todos los primos que no superan n.<br />
b) Ver que el programa eratostenes (pág. 186) es una implementación de esta<br />
idea, donde indicamos que un número está tachado o no con el arreglo<br />
esprimo que tiene valores booleanos. Observar también que el arreglo<br />
está dimensionado como “ 2..MAXP ”, es decir, no existe “ esprimo[1]”.<br />
c) ¿Quépasasien el lazo principal se elimina la variable i reemplazándola<br />
directamente por p, poniendo “ for p := 2 to MAXP do ”, etc.?<br />
d) Observamos que podemos mejorar el programa de dos formas:<br />
i) Podemos empezar directamente diciendo que 2 es primo y considerar<br />
sólo la lista de impares para recuadrar y tachar, reduciendo el tamaño<br />
de la lista inicial esencialmente a la mitad.<br />
ii) En el algoritmo original tachamos demasiadas veces un mismo número.<br />
Por ejemplo 105 = 3 × 5 × 7 se tacha al considerar 3, 5 y 7. Vemos<br />
que sólo es necesario tachar los múltiplos de, digamos, 7 a partir de<br />
49 = 7 × 7. Claro que tenemos que tener cuidado de no multiplicar<br />
un entero por sí mismo si el producto supera maxint (recordando los<br />
problemas 3.11 y 4.15).<br />
Introducir estas modificaciones en el programa eratostenes, cambiando o<br />
agregando las siguientes sentencias en lugares apropiados:<br />
• const MAXN = 4999; (* = (MAXP-1) div 2,<br />
= tama~no del arreglo esprimo *)<br />
• maxp, raizdemaxint: integer;<br />
• esprimo: array[1..MAXN] of boolean;<br />
• maxp := 2 * MAXN + 1;<br />
• (* inicializacion *)<br />
for i := 1 to MAXN do esprimo[i] := true;<br />
cuenta := 1; (* el primer primo es 2 *)<br />
raizdemaxint := trunc(sqrt(maxint));<br />
• (* lazo principal *)<br />
for i := 1 to MAXN do<br />
if (esprimo[i]) then begin<br />
(* p = 2i + 1 es primo *)<br />
cuenta := cuenta + 1; p := 2*i + 1;
6.3. Cribas Pág. 61<br />
(* ahora eliminar multiplos impares de p *)<br />
if (p
Pág. 62 Arreglos<br />
1<br />
3<br />
4<br />
5<br />
Figura 6.2: Esquema del problema de Flavio Josefo con n =5ym =3.<br />
4<br />
2<br />
5<br />
3<br />
b) ¿Cuál es el sobreviviente si n = 1234 y m =3?Sugerencia: no imprimir<br />
todos los valores, y tener cuidado con el dimensionamiento.<br />
c) Modificar el programa de modo que responda cuántas vueltas sobrevivió la<br />
persona que estaba inicialmente en el lugar s (el usuario entrará n, m y s).<br />
d) Modificar el programa de modo de imprimir una tabla, dando la posición<br />
inicial y el orden de ejecución, algo como<br />
Posición en el círculo Orden de ejecución<br />
1 2<br />
2 4<br />
3 1<br />
4 5<br />
5 3<br />
cuando n =5ym =3.<br />
Sugerencia: eliminar el arreglo flavio y usar otro, digamos orden, demodo<br />
que “ orden[i]” indicará enquévuelta fue eliminado i. Siseinicializa<br />
“ orden[i] := n ”paratodoi, y consideramos que en cada vuelta<br />
“ orden[i] = n ” indica que vive, tampoco es necesario el arreglo vive ni el<br />
lazo para encontrar el sobreviviente: será elúnico con “ orden[i] = n ”al<br />
terminar.<br />
e) ¿Cómo podría modificarse el programa, de modo que siempre se trabaje<br />
con un arreglo (o sub-arreglo) de longitud (por ejemplo) vivos, demodode<br />
no tener que considerar los que ya no viven? Sugerencia: ir “corriendo” las<br />
posiciones en el arreglo a medida que “se cierra” el círculo.<br />
✎ En el problema 14.5 veremos que las listas encadenadas se adaptan<br />
mejor que los arreglos para esta variante.<br />
f ) El lazo “repeat...until (cuenta = m) ” en el programa original es muy<br />
primitivo. Por ejemplo, si n = 1000, m = 100, cuando quedan 10 sobrevivientes<br />
debemos pasar unas 10 veces por cada uno de los 1000 elementos.<br />
¿Podría mejorarse el lazo usando “ mod ”? Sugerencia: recordar el programa<br />
resto (pág. 177). ✄<br />
6.4. Polinomios<br />
¿Para qué estudiar polinomios? ¿No basta con estudiar las funciones lineales<br />
(y = ax + b) yalosumolascuadráticas (y = ax 2 + bx + c)?<br />
¡Humm! Por un lado, los polinomios son las funciones más sencillas que podemos<br />
considerar, para su cálculo sólo se necesitan sumas y productos. Además<br />
1<br />
2
6.4. Polinomios Pág. 63<br />
los usamos diariamente, por ejemplo un númeroenbase10esenrealidadun<br />
polinomio evaluado en 10.<br />
Pero también los polinomios sirven para aproximar tantocomosedeseea<br />
casi cualquier función, lo que constituye un tema central de las matemáticas, y<br />
se estudia tanto en los cursos teóricos de análisis matemático como en los aplicados<br />
de análisis numérico. A modo de ejemplo visual, en la figura 15.1 mostramos<br />
cómo se podría aproximar a la función seno mediante los denominados polinomios<br />
interpoladores de Lagrange.<br />
Nuestra primer tarea será evaluar un polinomio dado:<br />
Problema 6.8 (Evaluación de Polinomios). Hacer un programa que dada<br />
una lista de coeficientes (reales) de un polinomio, (a0,a1,a2,...,an) yunnúmero<br />
x ∈ R, entrados por terminal, evalúe el polinomio anx n + an−1 x n−1 +<br />
···+ a1x + a0.<br />
Hacerlo de tres formas:<br />
a) Calculando la suma de términos como se indica, calculando x k como en el<br />
problema 4.18.<br />
b) Como el anterior, pero las potencias en la forma x k+1 = x k × x, guardando<br />
x k en cada paso.<br />
c) Usando la regla de Horner<br />
((···((an x + an−1) x + an−2) +···) x + a1) x + a0.<br />
✎ En las dos primeras versiones se hacen n sumas, n productos y se calculan<br />
n − 1 potencias, que en la primera versión representan otros 1 + 2 + ···+<br />
(n − 1) = n(n − 1)/2 productos, mientras que en la segunda, los productos<br />
provenientes de las potencias se reducen a n − 1. Finalmente, en la regla<br />
de Horner, tenemos sólo n sumas y n productos.<br />
William George Horner (1786–1837) fue indudablemente una persona muy<br />
capaz: a los 18 años era director de la escuela Kingswood (en Bristol, Inglaterra).<br />
Sin embargo, sus contribuciones matemáticas no han sido demasiadas, y<br />
conservamos el nombre de “regla de Horner” pues De Morgan la denominó asíy<br />
le dio amplia difusión en los muchos artículos que escribió. Sin embargo, el<br />
método era conocido por Zhu Shijie unos 500 años antes. ✄<br />
Problema 6.9 (Escritura en base entera). La idea de la regla de Horner<br />
se usa también en el caso de escritura en base b (entero > 1). Si n ∈ Z es<br />
no-negativo y<br />
n =<br />
k<br />
aib i , donde ai ∈ Z y0≤ ai
Pág. 64 Arreglos<br />
y para encontrar n dados los coeficientes en base b (si ak es el último coeficiente<br />
no nulo)<br />
j := k; n := a[j];<br />
while (j > 0) do begin j := j-1; n := n * b + a[j] end<br />
a) Implementar dos programas para que dados la base b y n, encontrar los coeficientes<br />
ai, yrecíprocamente, dados la base b y los coeficientes ai, calcular<br />
n usando la regla de Horner.<br />
b) En la ecuación (6.1), ¿cómo se relacionan k ylogbn (si ak = 0)? ✄<br />
Problema 6.10. De la escuela recordamos que todo número racional p/q ∈ Q<br />
tiene una “expresión decimal periódica”. Por ejemplo, 1/7 =0.14285701428 ...<br />
tiene período 142857, 617/4950 = 0.124646 ... tiene período 46, 1/4 =0.25 =<br />
0.2500 ... tiene período 0.<br />
✎ Inclusive es posible que recordemos que hay una regla para reconstruir la<br />
fracción dados el de período y el anteperíodo.<br />
Hacer un programa que dados p, q ∈ N, calcule el período (en base 10) de la<br />
fracción p/q.<br />
Sugerencia: poniendo r0 = resto de dividir p por q, los sucesivos dígitos di de<br />
la parte decimal se obtienen de la ecuación<br />
10rn−1 = dnq + rn,<br />
para n =1, 2,..., y hay que buscar (por ejemplo linealmente) si un resto se<br />
vuelve a repetir.<br />
Sugerencia si la anterior no alcanza: Luego de ingresar p y q, poner<br />
n := 0; r[0] := p mod q;<br />
repeat<br />
c := 10 * r[n];<br />
n := n + 1;<br />
d[n] := c div q;<br />
r[n] := c mod q;<br />
(* ahora ver si r[n] ya estaba en r *)<br />
i := 0;<br />
while (r[i] r[n]) do i := i + 1<br />
until (i < n);<br />
El período está formado por los dígitos di+1,...,dn.<br />
✎ A fin de evitar problemas de dimensionamiento al usar arreglos, considerar<br />
q ≤ 1000: para fracciones de la forma p/q con q ≤ 1000 la longitud del<br />
período y anteperíodo es a lo sumo de 982 (cuando q = 983). ✄<br />
6.5. Problemas Adicionales<br />
Problema 6.11. Modificar la criba de Eratóstenes para determinar cuántos de<br />
los 1000 primeros primos terminan respectivamente en 1, 3, 7 y 9.<br />
✎ Observar que, al menos en el rango considerado, la distribución entre los<br />
queterminanen1,3,7o9esmuypareja.
6.6. Comentarios Bibliográficos Pág. 65<br />
El matemático Johann Peter Gustav Lejeune Dirichlet (1805–1859) nació<br />
enDüren que en aquel entonces pertenecía al imperio francés pero hoy<br />
está enAlemania,ydeallíqueavecessedicequeesfrancés y otras veces<br />
que es alemán. El primer trabajo que publicó fue sobre el teorema de <strong>Fe</strong>rmat-<br />
Wiles, resolviendo el caso n =5, pero también trabajó en temas de análisis<br />
(anticipando las “series de Fourier”).<br />
Dirichlet demostró en 1837 que toda progresión aritmética a + bk, k =<br />
1, 2,..., contiene infinitos primos si mcd(a, b) =1(el resultado fue conjeturado<br />
por Gauss e implica el resultado de Euclides sobre que hay infinitos primos).<br />
En el presente problema, si pensamos que b =10,ya es cualquier número<br />
que no tenga como factores a 2 ni a 5, este teorema de Dirichlet dice que hay<br />
infinitos primos que terminan en 1, infinitos que terminan en 3, etc. ✄<br />
Problema 6.12 (Potencias binarias). Otra variante de la idea de la regla de<br />
Horner (problema 6.8) eselcálculo de una potencia “pura”. Si x ∈ R y n ∈ N,<br />
podemos calcular xn escribiendo n en base 2:<br />
y hacer<br />
n =<br />
k<br />
ai2 i ,<br />
i=0<br />
x n = x a0+2a1+22a2+···+2 k−1 ak−1+2 k ak =<br />
= x a0 · (x 2 ) a1 · (x 4 ) a2 ···(x 2k−1<br />
) ak−1 2<br />
· (x k<br />
) ak .<br />
a) Implementar un programa para el cálculo de potencias “puras” con estas<br />
ideas.<br />
✎ Si los coeficientes se conocen, un esquema es<br />
if (a[0] = 1) then producto := x else producto := 1;<br />
potencia := x;<br />
for j := 1 to k do begin<br />
potencia := potencia * potencia; (* = x**{2**j} *)<br />
if (a[j] = 1) then producto := producto * potencia<br />
end<br />
mientras que si los coeficientes no se conocen (encontrando aj en cada<br />
paso):<br />
m := n; potencia := x;<br />
if (m mod 2 = 1) then producto := x else producto := 1;<br />
while (m > 0) do begin<br />
m := m div 2; potencia := potencia * potencia;<br />
if (m mod 2 = 1) then producto := producto * potencia<br />
end<br />
b) Comparando con el problema 4.18, donde se usaba un lazo “ for ”: ¿cuántos<br />
operaciones aritméticas (productos y divisiones) se realizan en cada caso?<br />
(Recordar el problema 6.9.b)).<br />
✎ Debido a su alta eficiencia, la misma técnica se usa para encriptar mensajes<br />
usando números primos de varias decenas de cifras. ✄<br />
6.6. Comentarios Bibliográficos<br />
Los problemas 6.6 y 6.7 están tomados de [4].
Capítulo 7<br />
Funciones y Procedimientos<br />
Nuestros programas se han hecho cada vez más largos, y a medida que avancemos<br />
lo serán aún más. La longitud y complejidad de los programas profesionales<br />
es tal que una sola persona no puede siquiera escribirlos completamente.<br />
A fin de abordar esta dificultad, se han desarrollado una serie de técnicas, y<br />
en este capítulo daremos los primeros pasos hacia una de ellas, la modularización,<br />
para lo cual Pascal cuenta con dos mecanismos: funciones y procedimientos.<br />
Aunque la ventaja de usar funciones o procedimientos irá quedando más<br />
clara con los ejemplos que veremos en éste y otros capítulos, podemos decir que<br />
en general son convenientes para:<br />
• Poner en un único lugar cálculos idénticos que se realizan en distintas<br />
partes del programa,<br />
• o poner por separado alguna acción permitiendo su fácil reemplazo (y con<br />
menor posibilidad de error),<br />
• y no menos importante, haciendo el programa más fácil de entender, dejando<br />
una visión más global y no tan detallada en cada parte (viendo el<br />
bosque y no el árbol).<br />
El lector seguramente recordará el uso que con el mismo espíritu hemos dado<br />
a“const ”, por ejemplo en el problema 6.2, para hacer un único cambio que<br />
afecta a varias partes del programa.<br />
7.1. Funciones<br />
Como hemos visto, Pascal cuenta con una serie de funciones pre-definidas,<br />
como “cos” o “ln”, de una variable, o la suma, “+”, que se aplica a dos o más<br />
variables. Pero —necesariamente— no puede tener todas las funciones y sólo<br />
algunas se han incluido. Por ejemplo, el cálculo de la potencia x n cuando x ∈ R<br />
y n ∈ N no está incluida en Pascal, pero lo hemos hecho en el problema 4.18.<br />
Supongamos que queremos hacer un programa para comparar los valores x n<br />
y y m , donde x, y ∈ R y n, m ∈ N son datos ingresados por el usuario. Nuestro<br />
programa tendría los siguientes pasos:<br />
1. Poner carteles iniciales.
7.1. Funciones Pág. 67<br />
2. Leer los datos x y n, yluegolosdatosyym. 3. Calcular xn y ym .<br />
4. Comparar el resultado, imprimiendo la decisión.<br />
5. Imprimir un cartel final.<br />
Cada una de las acciones mencionadas no es difícil. Por ejemplo para calcular<br />
xn y ym podríamos poner<br />
xn := 1; for k := 1 to n do xn := xn * x;<br />
ym := 1; for k := 1 to m do ym := ym * y;<br />
Problema 7.1. En el programa potencias2 (pág. 188) hemos implementado<br />
estas ideas. Leerlo, reconociendo la estructura mencionada, y ejecutarlo a fin de<br />
verificar su funcionamiento. ✄<br />
Claro que si contáramos con una función de Pascal para calcular xn para<br />
x ∈ R y n ∈ N, digamos “ potencia(x,n)”, podríamos reemplazar los dos<br />
renglones anteriores por<br />
xn := potencia(x,n); ym := potencia(y,m);<br />
Aunque Pascal no cuenta con una funciónquerealiceestecálculo, nos da un<br />
mecanismo para definirla nosotros.<br />
En Pascal, las funciones (y procedimientos que veremos en un rato) se declaran<br />
en la parte declarativa (1) del programa, formando un bloque de estructura<br />
similar a la de los programas.<br />
Por ejemplo, para definir la función potencia (2) pondríamos antes del cuerpo<br />
principal del programa,<br />
function potencia(a: real; b: integer): real;<br />
var k: integer; z: real;<br />
begin<br />
z := 1;<br />
for k := 1 to b do z := z * a;<br />
potencia := z<br />
end;<br />
Observamos que<br />
• Se comienza poniendo el nombre de la función, poniendo “ function ”(o<br />
“ procedure” para procedimientos) en vez de “ program ”, luego una parte<br />
de declaraciones propias, y después un cuerpo principal encerrado entre<br />
“ begin ”y“end; ”(con“; ”envezde“. ”porquenoeselfinaldel<br />
programa).<br />
• Los argumentos de la función deben ser declarados. En nuestro ejemplo hay<br />
dos argumentos, a, detipo“real ”, y b,detipo“integer ”. Por supuesto,<br />
el orden de los argumentos es importante: 2 3 =3 2 . Estos argumentos son<br />
equivalentes al “ (input, output) ” de un programa Pascal.<br />
• Las funciones en Pascal calculan un valor de alguno de los tipos elementales<br />
boolean, char, integer o real.<br />
(1) ¿Podría ser de otro modo?<br />
(2) Usaremos estetipodeletra para señalar que se trata de una función o procedimiento hecho<br />
por nosotros.
Pág. 68 Funciones y Procedimientos<br />
✎ También podrían ser de tipo “ text ”, que veremos en el capítulo 8,<br />
o puntero, que veremos en el capítulo 14. Según el estándar no deben<br />
ser de otro tipo, como arreglos, aunque muchos compiladores lo<br />
permiten.<br />
El tipo del valor calculado debe ser declarado, lo que se hace mediante<br />
la colocación de “ : ” luego de la declaración de argumentos. En nuestro<br />
ejemplo, el resultado es de tipo “ real ”.<br />
Decimos que la función devuelve o retorna el valor calculado.<br />
• Dentro del bloque correspondiente a la función,elnombredeésta debe<br />
ser asignado, y será el valor “retornado” a otras partes del programa.<br />
✎ Sin embargo, el nombre de la función no es el identificador de una<br />
variable. Ver el problema 7.2.d).<br />
✎ No es importante el lugar de la asignación dentro de la función, y<br />
no tiene por qué serlaúltima instrucción en la función. Inclusive es<br />
posible hacer varias asignaciones, pero al menos debe haber una que<br />
se ejecute antes de que termine la función.<br />
• Además de los argumentos y el valor a retornar, la función puede tener<br />
otras variables propias. En nuestro ejemplo, k de tipo “ integer ”yz de<br />
tipo “ real ”. Todas estas variables propias, en nuestro ejemplo a, b, k y<br />
z, se llaman locales, y se desconocen fuera de la función.<br />
• Finalmente, el valor calculado por la función tiene que ser asignado (o<br />
impreso) dentro de la parte principal del programa.<br />
✎ O en otras funciones o procedimientos declaradas posteriormente.<br />
Veremos ejemplos más adelante.<br />
En estos casos decimos que el programa llama o invoca alafunción. Por<br />
ejemplo, llamaríamos a la función potencia con la instrucción<br />
xn := potencia(x,n)<br />
Problema 7.2. En el programa potencias3 (pág. 188) hemos incorporado la<br />
función “ potencia” y los cambios mencionados. Leerlo, estudiando las distintas<br />
instrucciones, y ejecutarlo.<br />
a) Observar que la variable k ya no está definida al comienzo del programa,<br />
sino sólo en la función “ potencia”: es local alafunción pero desconocida<br />
para el resto del programa.<br />
i) Incluir el renglón<br />
writeln(’ El valor de k es: ’, k:1);<br />
inmediatamente antes del cartel final del programa. El compilador seguramente<br />
rechazará esta acción pues k no está declarada.<br />
ii) Manteniendo el renglón problemático, incluir la declaración de k como<br />
estaba en el programa potencias2, y hacer la asignación “ k := 1”<br />
antes del renglón del inciso anterior. Compilar y ejecutar nuevamente<br />
el programa, observando que ahora no hay inconvenientes.
7.1. Funciones Pág. 69<br />
iii) Repetir los pasos anteriores cambiando k por z, comprobando que z<br />
“vive” sólodentrodelafunción.<br />
b) Observar que al declarar la función, los argumentos a y b se separan con<br />
“ ; ”, pero que al llamar la función se invoca con “ , ” separando los argumentos:<br />
“ potencia(x,n)”.<br />
i) Cambiar “ ; ”por“, ” en la declaración de la función, i.e. poner<br />
function potencia(a: real, b: integer): real;<br />
y comprobar que el compilador no acepta el cambio.<br />
ii) De modo similar, cambiar “ xn := potencia(x,n);” por<br />
xn := potencia(x;n);<br />
y comprobar que este cambio tampoco es aceptado.<br />
✎ Sin embargo, si dos o más argumentos consecutivos son del mismo<br />
tipo, podemos separarlos con una coma juntando las declaraciones.<br />
Para ejemplificar, si a y b son de tipo “ real ”, podemos declarar tanto<br />
function f(a: real; b: real):...<br />
como<br />
function f(a, b: real):...<br />
Es decir, el uso es similar a las “ , ” que se usan al declarar variables<br />
con “ var ”. Veremos un ejemplo en el problema 7.7.<br />
c) Las variables que son argumentos de la función también son locales a ella,<br />
y es posible que coincidan con los nombres de otras variables del programa,<br />
llamadas globales.<br />
i) Cambiar la definición de la función a<br />
function potencia(x: real; n: integer): real;<br />
var k: integer; z: real;<br />
begin<br />
z := 1;<br />
for k := 1 to n do z := z * x;<br />
potencia := z<br />
end;<br />
y comprobar (compilando y ejecutando el programa) que el comportamiento<br />
es el mismo.<br />
ii) La localidad de los argumentos también se traduce en que podemos<br />
modificarlos dentro de la función, sin modificar otras variables con el<br />
mismo identificador del programa.<br />
Por ejemplo, cambiar la declaración de la función poniendo<br />
function potencia(x: real; n: integer): real;<br />
var z: real;<br />
begin<br />
z := 1;<br />
while (n > 0) do begin<br />
z := x * z; n := n - 1 end;<br />
potencia := z<br />
end;
Pág. 70 Funciones y Procedimientos<br />
Observar que el valor final de z es el mismo en uno u otro caso,<br />
pero el valor de la variable n (localalafunción) se va modificando<br />
con la nueva definición.<br />
Ejecutar el programa, y verificar que sin embargo, el valor de n<br />
que se imprime al final es el mismo que el ingresado inicialmente:<br />
corresponde a la variable global n, y no a la variable n local a la<br />
función.<br />
✎ Si bien podemos poner cualquier identificador a los argumentos,<br />
no deben ser los mismos que las otras variables locales a la<br />
función. Por ejemplo no debemos poner<br />
function potencia(x: real; n: integer): real;<br />
var k: integer; x: real;<br />
.<br />
d) Ya hemos mencionado que el nombre de la función debe ser asignado antes<br />
de terminar la función,peronoesunavariable:¿qué pasa si se pone<br />
function potencia(x: real; n: integer): real;<br />
begin<br />
potencia := x;<br />
while (n > 1) do begin<br />
potencia := x * potencia; n := n - 1 end<br />
end; ?<br />
e) Cambiar la función potencia de modo de que el cálculo se haga mediante la<br />
fórmula<br />
x n =exp(nln x),<br />
comprobando que se obtienen los resultados correctos. ✄<br />
7.2. El método de la bisección<br />
Supongamos que tenemos una función continua f definida sobre el intervalo<br />
[a, b] a valores reales (3) ,yquef(a) yf(b) tienen distinto signo, como en la<br />
figura 7.1. Cuando la dibujamos desde el punto (a, f(a)) hasta (b, f(b)), vemos<br />
que en algún momento cruzamos el eje x, yallí encontramos una raíz de f, i.e.<br />
un valor de x tal que f(x) =0.<br />
En el método de la bisección se comienza tomando a0 = a y b0 = b, ypara<br />
i =0, 1, 2,... se va dividiendo sucesivamente en dos el intervalo [ai,bi] tomando<br />
el punto medio ci, y considerando como nuevo intervalo [ai+1,bi+1] alintervalo<br />
[ai,ci] o[ci,bi], manteniendo la propiedad que en los extremos los signos de f<br />
son opuestos (que podemos expresar como f(ai)f(bi) < 0). Se finaliza cuando<br />
se obtiene un valor de x tal que |f(x)| es suficientemente chico, |f(x)|
7.2. El método de la bisección Pág. 71<br />
a = a0<br />
c0 = a1 = a2<br />
c2<br />
c1 = b2<br />
b = b0 = b1<br />
Figura 7.1: Una función continua con distintos signos en los extremos.<br />
✎ También en este sentido, observamos que 2 10 = 1024 ≈ 10 3 y 2 20 =<br />
1048576 ≈ 10 6 , por lo que el intervalo inicial se divide aproximadamente<br />
en 1000 después de 10 iteraciones y en 1000000 = 10 6 después de 20<br />
iteraciones. Es decir, después de 10 iteraciones el intervalo mide el 0.1%<br />
del intervalo original, y después de 20 iteraciones mide el 0.0001 % del intervalo<br />
original: no tiene mucho sentido poner mucho más de 10 iteraciones.<br />
Problema 7.3 (Método de la bisección para encontrar raíces). El programa<br />
biseccion (pág. 189) utiliza el método de bisección para encontrar raíces<br />
en el caso particular<br />
f(x) =x(x +1)(x +2)(x − 4/3).<br />
a) Observar la declaración de la función f en Pascal, y la estructura del cuerpo<br />
principal:<br />
1. Carteles iniciales.<br />
2. Entrada de los extremos del intervalo inicial: la cota inferior se llama<br />
poco ylasuperiormucho.<br />
3. En la inicialización, se calculan los valores de f en ambos extremos,<br />
llamados ypoco y ymucho, y se inicializa a 0 el contador de iteraciones,<br />
iter.<br />
Dado que se sigue iterando si la función toma signos distintos en los<br />
extremos, se define la variable lógica seguir que indica si la condición<br />
es cierta o no.<br />
4. En el lazo principal:<br />
• Se incrementa el contador iter, se calcula el punto medio del intervalo,<br />
medio, yelvalordefen este punto, ymedio.<br />
• Si el valor absoluto de ymedio es suficientemente chico, paramos.<br />
Esto se indica poniendo seguir en falso.<br />
• También paramos si ya hemos llegado al máximo de iteraciones.<br />
• Si no, calculamos el nuevo intervalo, cuidando de que los signos en<br />
losextremosseandistintos.<br />
Observar que sólo necesitamos el signo de ypoco para determinar<br />
el nuevo intervalo, de modo que no hemos conservado el valor de<br />
ymucho.
Pág. 72 Funciones y Procedimientos<br />
5. En la salida tenemos en cuenta las distintas posibilidades por las cuales<br />
seguir es falsa:<br />
• Si la condición de distinto signo en los extremos no se satisfacía<br />
inicialmente, no se han realizado iteraciones (por lo que el valor de<br />
iter es 0), y ponemos un cartel apropiado.<br />
• En otro caso señalamos los valores obtenidos, poniendo un cartel<br />
especial si el error no es suficientemente pequeño (y por lo tanto el<br />
número de iteraciones es máximo).<br />
6. Terminamos poniendo un cartel de “Fin”.<br />
b) Dada la forma de f, en este caso conocemos exactamente las raíces. Bosquejar<br />
el gráfico de f en el intervalo [−3, 2], y dar valores iniciales de poco<br />
y mucho para obtener aproximaciones a cada una de ellas ejecutando el<br />
programa.<br />
c) En caso de que haya más de una raíz en el intervalo inicial, la solución elegida<br />
depende de los datos iniciales. Verificar este comportamiento ejecutando<br />
el programa sucesivamente con los valores .8, 1 y 1.2 paramucho, pero<br />
tomando poco = −3 en todos estos casos.<br />
d) ¿Por quésiponemos poco = −3 ymucho =1obtenemoslaraíz x = −1 en<br />
una iteración?<br />
✎ En general, nunca obtendremos el valor exacto de la raíz: recordar que<br />
en la computadora sólo existen racionales (¡y pocos!).<br />
e) x =0esraíz, pero ¿qué pasa si ponemos poco =0ymucho =1?Modificar<br />
el programa de modo que si f(poco) of(mucho) sean en valor absoluto<br />
suficientemente pequeños, entonces se imprima la correspondiente solución<br />
y no se realice el lazo de bisección.<br />
f ) Agregar también la impresión de carteles apropiados cuando f(poco) ×<br />
f(mucho) ≥ 0ynoseestáenlas condiciones del inciso anterior.<br />
g) El programa no verifica si poco < mucho, ypodría suceder que poco ><br />
mucho. ¿Tieneestoimportancia?<br />
h) Teniendo en cuenta las notas al principio de la sección, ¿tendría sentido<br />
agregar al programa un criterio de modo de parar si los extremos del intervalo<br />
están suficientemente cerca? Si la nueva tolerancia fuera εx, ¿cuántas<br />
iteraciones deben realizarse para alcanzarla, en términos de εx ylosvalores<br />
originales de mucho y poco?<br />
i) Modificar el programa de modo que en vez de considerar hasta un máximo<br />
de iteraciones, el programa termine cuando o bien se ha encontrado x tal que<br />
|f(x)|
7.2. El método de la bisección Pág. 73<br />
a) Resolver aproximadamente las ecuaciones:<br />
i) x 2 − 5x +2=0, ii) x 3 − x 2 − 2x +2=0.<br />
Resolver también estas ecuaciones en forma exacta y comparar con los resultados<br />
obtenidos.<br />
✎ La primera ecuación tiene 2 raíces y la segunda 3.<br />
b) Encontrar una solución aproximada de cos x = x y comparar con los resultados<br />
del problema 5.7.<br />
c) Obtener una solución aproximada de cada una de las ecuaciones<br />
2 − ln x = x y x 3 sen x +1=0.<br />
✎ La primera ecuación tiene una raíz, y la segunda tiene infinitas. ✄<br />
Problema 7.5 (Interés sobre saldo). Consideremos el siguiente problema:<br />
Un artículo cuesta $100 de lista. Puedo comprarlo al contado con un<br />
10 % de descuento, o con un plan de pagos de anticipo y 9 cuotas<br />
mensuales de $10 c/u, ¿cuál es la tasa de interés (nominal anual)<br />
que están cobrando?<br />
Resulta que hay muchas formas de calcularla (4) . En este problema estudiamos<br />
el llamado interés sobre saldo, que da como respuesta a la pregunta anterior<br />
una tasa de 29.07 %.<br />
Supongamos que pedimos prestada una cantidad c (en $) con un tasa de<br />
interés anual (nominal) r (en %), que pagaremos en cuotas mensuales fijas de p<br />
(en $) comenzando al final del primer mes, y que el préstamo tiene las característica<br />
de que el interés se considera sobre el saldo, esto es, si bm es el saldo<br />
adeudado a fin del mes m, justo antes de pagar la cuota m-ésima, y cm es el<br />
saldo inmediatamente después de pagar esa cuota, poniendo t =1+r/(100×12),<br />
tendremos:<br />
y en general<br />
b1 = tc, c1 = b1 − p, b2 = tc1, c2 = b2 − p,...<br />
cm = bm−1 − p = tcm−1 − p, (7.1)<br />
donde inclusive podríamos poner c0 = c.<br />
a) Programar una función saldo(c, r, p, m) que dados el monto inicial c, latasa<br />
r, y el pago mensual p, calcule el saldo inmediatamente después de pagar la<br />
m-ésima cuota, cm = saldo(c, r, p, m) param =1, 2,... Aclaración: nose<br />
pide encontrar una fórmula, sólo escribir la función Pascal.<br />
b) Verificar la corrección de la función anterior (dentro de un programa),<br />
usando los datos de la pregunta al principio del problema (se financian<br />
$90 − $10 = $80 en 9 cuotas de $10, a una tasa de 29.07 %; el saldo correspondiente<br />
debe ser aproximadamente 0).<br />
c) Considerando que c y r están fijos, ¿existe un valor de p de modo que el<br />
saldo sea siempre el mismo, i.e. cm+1 = cm para m =1, 2,...?, ¿cuál?<br />
(4) Yelcontadordirá“¿cuánto querés que te de?”
Pág. 74 Funciones y Procedimientos<br />
d) Para c, r y p fijos, la función saldo(c, r, p, m) esdecreciente con m si p es<br />
mayor que el monto calculado en el inciso c). ¿Cómo es el comportamiento<br />
cuando sólo varía c?, ¿y si sólo varía r?<br />
e) Hacer un programa de modo que dados r, c y p (p mayor que el monto en c))<br />
calcule el número total de cuotas n. Aclaración: todas las cuotas deben ser<br />
iguales, excepto tal vez la última que puede ser menor.<br />
f ) Hacer un programa que usando el método de bisección, y dados c, r y<br />
el número total de cuotas n, calculepde modo que la deuda se cancele<br />
exactamente con n cuotas fijas, es decir cn =0.¿Podríamos usar poco =0<br />
y mucho = a?<br />
g) El resultado anterior, p, en general no podrá pagarse en pesos y centavos<br />
(habrá más de dos decimales). Si los pagos se harán en pesos y centavos,<br />
¿qué tolerancias en p pondremos? Modificar (si es necesario) el programa<br />
anterior para incorporar esta tolerancia.<br />
h) Modificar el programa para que dados r (latasaquemecobran),p (la cuota<br />
que estoy dispuesto a pagar) y n (la cantidad de meses), calcule el monto c<br />
(al que podría acceder).<br />
✎ En calculadoras financieras y “planillas de cálculo” están implementadas<br />
funciones similares.<br />
Por ejemplo, en (la versión inglesa de) Excel están las funciones:<br />
PMT: Para calcular p dados n, r y c: “PMT(r/12,n,c) ”.<br />
PV: Para calcular c dados n, r y p: “PV(r/12,n,p) ”(pnega tivo pues es un pago).<br />
RATE: Para calcular r dados n, c y p: “RATE(n,p,c)*12 ”(p<br />
negativo pues es un pago).<br />
NPER: Calcular n dados r, a y p: “NPER(r/12,p,c) ”(pne gativo pues es un pago).<br />
Así, para calcular el pago mensual si r =8%,n =10yc = $10000,<br />
ponemos “ PMT(8 %/12,10,10000) ” para obtener −1037.03. ✄<br />
7.3. Procedimientos<br />
Los procedimientos y funciones son muy parecidos entre sí, y a veces se<br />
los engloba bajo el nombre común de rutinas (5) . De hecho, en lenguajes como<br />
“C” no hay distinción entre ellos. En Pascal, la diferencia más obvia es que los<br />
procedimientos no tienen “un resultado” visible.<br />
Pascal nos permite mezclar funciones y procedimientos, con la única restricción<br />
de que se deben declarar en el orden en que son usados: una función o<br />
procedimiento no puede llamar a una función o procedimiento que no ha sido<br />
aún declarada.<br />
✎ Una posibilidad intermedia es el uso de “ forward ”, que no veremos.<br />
Más aún, como veremos en el problema 7.6.d) y en el problema 7.8.b), es posible<br />
poner una funciónoprocedimientodentrodeotrafunción o procedimiento,<br />
yaquéllos serán locales aéstos (desconocidos para otras partes del programa).<br />
Esta acción se llama anidamiento de funciones o procedimientos.<br />
(5) Aunque en algunos lenguajes, ¡las rutinas son nuestros procedimientos!
7.3. Procedimientos Pág. 75<br />
Podemos pensar, recordando la figura 2.4, que al alojarse en la memoria el<br />
programa ejecutable, hay un espacio reservado para funciones y procedimientos,<br />
como se indica en la figura 7.2. A su vez, funciones y programas pueden repetir<br />
el esquema si hay anidamiento.<br />
programa<br />
ejecutable<br />
datos<br />
globales<br />
funciones,<br />
procedimientos<br />
instrucciones<br />
(parte<br />
principal)<br />
datos<br />
locales<br />
instrucciones<br />
locales<br />
datos<br />
locales<br />
instrucciones<br />
locales<br />
Figura 7.2: Esquema de programa, funciones y procedimientos en la memoria.<br />
Volvamos al problema 4.12 (pág. 32) donde hicimos una tabla del seno usando<br />
el programa tablaseno (pág. 177). Podemos esquematizar ese programa por<br />
medio de los siguientes pasos:<br />
1. Poner carteles.<br />
2. Leer los datos, en este caso inicial, final e incremento.<br />
3. Hacer e imprimir la tabla.<br />
4. Señalar el fin del programa.<br />
Pascal nos permite poner cada uno de estos pasos como procedimiento, poniendo<br />
en el cuerpo principal del programa (6) :<br />
begin<br />
cartelesiniciales;<br />
leerdatos;<br />
imprimirtabla;<br />
cartelfinal<br />
end.<br />
donde cartelesiniciales, leerdatos, imprimirtabla y cartelfinal son procedimientos<br />
que realizarán las acciones correspondientes.<br />
La ventaja de hacerlo es que podemos preocuparnos por cada uno de los<br />
procedimientos —y si las hubiera, funciones— por separado. Así, podríamos<br />
definir el procedimiento cartelesiniciales como<br />
procedure cartelesiniciales;<br />
begin<br />
writeln(’Hacer una tabla del seno dando valores’);<br />
writeln(’inicial, final, y del incremento (en grados).’);<br />
writeln<br />
end;<br />
(6) Claro que es una exageración. Lo hacemos sólo a modo de ejemplo.
Pág. 76 Funciones y Procedimientos<br />
Problema 7.6. En el programa tablaseno2 (pág. 191) hemos reescrito el programa<br />
tablaseno, con las modificaciones mencionadas.<br />
a) Comparar ambos programas, observando cómo se han declarado los procedimientos<br />
en tablaseno2 ycómo se corresponden con las sentencias de<br />
tablaseno.<br />
Observar que las variables inicial , final e incremento se han declarado<br />
como globales, puesto que se usan en distintas partes, mientras que grados<br />
y radianes son locales al procedimiento imprimirtabla, pueseselúnico que<br />
las usa.<br />
b) Compilar y ejecutar tablaseno2 verificando su comportamiento.<br />
c) En el problema 4.12 nos preocupamos por cómo dar un valor aproximado de<br />
π, teniendo las alternativas de definirlo “manualmente” o usar una fórmula<br />
como π =4× arctan 1, a la que podríamos agregar el uso de la técnica de<br />
punto fijo del problema 5.8.<br />
Eliminar la declaración de pi como constante en el programa tablaseno2,<br />
declararlo en cambio como variable real e incluir el procedimiento:<br />
procedure calculodepi;<br />
begin<br />
pi := 4 * arctan(1)<br />
end;<br />
como primer procedimiento, incluyendo la sentencia “ calculodepi” enel<br />
cuerpo principal del programa. Compilar y ejecutar el programa con estas<br />
modificaciones.<br />
d) En realidad, el valor de pi se usa sólo para pasar de grados a radianes. Eliminar<br />
las declaraciones de pi, calculodepi ylasentencia“calculodepi” del<br />
programa, y en cambio colocarlas dentro del procedimiento imprimirtabla.<br />
Debería quedar algo como:<br />
procedure imprimirtabla;<br />
var<br />
grados: integer;<br />
radianes, pi: real;<br />
procedure calculodepi;<br />
begin pi := 4 * arctan(1) end;<br />
begin<br />
calculodepi;<br />
writeln(’Angulo Seno’);<br />
grados := inicial;<br />
while (grados
7.4. Pasando por valor o por referencia Pág. 77<br />
7.4. Pasando por valor o por referencia<br />
Tanto los procedimientos como las funciones tienen en general uno o más<br />
parámetros que son los argumentos para evaluarlos (7) . Tenemos que distinguir<br />
entre los parámetros que están en la descripción de la función o procedimiento,<br />
llamados parámetros formales y los que se especifican en cada llamada de la<br />
función o procedimiento, llamados parámetros reales.<br />
Los parámetros formales se utilizan solamente dentro del cuerpo de la función<br />
o procedimiento y son locales a él, es decir, son desconocidos fuera de la<br />
función o procedimiento. Por supuesto, los parámetros reales deben tener el<br />
mismo tipo que los formales. De alguna forma, podemos pensar que la función<br />
o procedimiento tiene sus propias “cajas” (los parámetros formales) en las que<br />
alojar las variables que son pasadas (los parámetros reales).<br />
Por ejemplo, volviendo al programa potencias3 (problema 7.2), recordemos<br />
que la función potencia se declaraba como<br />
function potencia(a: real; b: integer):...<br />
En este caso, los parámetros a y b son los parámetros formales, mientras que<br />
al hacer la llamada<br />
xn := potencia(x,n)<br />
x y n se convierten en los parámetros reales. Los valores de x y n —“cajas”<br />
globales del programa— se copian (respectivamente) en las “cajas” locales a y<br />
b.<br />
Pero no siempre esta “copia de valores” es adecuada:<br />
Problema 7.7 (Intercambio de variables). Supongamos que queremos intercambiar<br />
los valores de las variables u y v, es decir poner en u lo que está en<br />
v yrecíprocamente. No podemos poner sencillamente “ u := v; v := u” pues<br />
quedaría u = v (8) . La forma usual de hacer el intercambio es usando una variable<br />
intermedia o temporal t (del mismo tipo que u y v), siguiendo un esquema<br />
como “ t := u; u := v; v := t”, como se indica en la figura 7.3.<br />
2<br />
u v<br />
1 3<br />
t<br />
Figura 7.3: Intercambio de los valores de u y v usando la variable intermedia t.<br />
Los números indican el orden de los pasos a realizar.<br />
Si queremos implementar este intercambio como procedimiento, el primer<br />
impulso es poner (suponiendo variables enteras) un procedimiento como en el<br />
programa intercambio (pág. 192).<br />
✎ Observar la declaración de los argumentos en incorrecto: cuando hay dos<br />
(o más) consecutivos del mismo tipo, en este caso “ integer ”, podemos<br />
separarlos por comas poniendo “ a, b: integer ”, en vez del equivalente<br />
a“a: integer; b: integer ”.<br />
(7) Pero podrían no tenerlos, como en el procedimiento carteles del programa tablaseno2.<br />
(8) Ante la duda, recomendamos hacer “pruebas de escritorio” en este problema.
Pág. 78 Funciones y Procedimientos<br />
a) Ejecutar el programa, y comprobar que los valores de x y y no han cambiado.<br />
El mecanismo de copiar los valores de x, y en las cajas locales a, b es la que<br />
produce el resultado no deseado. Por suerte, existe un mecanismo que al llamar<br />
a la función o procedimiento hace equivalentes las variables correspondientes.<br />
En Pascal, esto se hace incorporando la palabra “ var ” a las variables deseadas:<br />
b) Cambiar el procedimiento incorrecto del programa anterior por el siguiente:<br />
procedure correcto(var a, b: integer);<br />
(* a, b pasados por referencia *)<br />
var t: integer;<br />
begin t := a; a := b; b := t end;<br />
Probarlo y comprobar que los valores finales de x y y son ahora los correctos.<br />
✄<br />
Como hemos visto en el problema anterior, en funciones y procedimientos<br />
podemos indicar si queremos trabajar con una copia de los parámetros reales,<br />
o directamente con éstos. Este mecanismo se llama de “sustitución” y es usual<br />
distinguir las siguientes clases de sustitución de parámetros:<br />
Sustitución por valor: El parámetro real se evalúa, y el valor resultante<br />
sustituye al correspondiente parámetro formal.<br />
Sustitución por referencia: El parámetro real es una variable; los posibles<br />
índices se evalúan, y la variable así identificada sustituye a su<br />
correspondiente parámetro formal. Es utilizada si el parámetro representa<br />
el resultado de un procedimiento.<br />
Por ejemplo, dado el fragmento<br />
var i: integer; a: array[1..2] of integer;<br />
procedure P(x: integer);<br />
begin i := i + 1; x := x + 2 end;<br />
begin (* cuerpo principal *)<br />
a[1] := 10; a[2] := 20; i := 1; P(a[i])<br />
end.<br />
nos preguntamos cuáles son los valores finales del arreglo a.<br />
En este caso, tenemos la sustitución por valor: lavariablexen el procedimiento<br />
P tiene valor inicial 10 = a1. El valor final de a es (10, 20).<br />
Si cambiamos la definición del procedimiento P a<br />
procedure P(var x: integer);<br />
begin i := i + 1; x := x + 2 end;<br />
tendremos la sustitución por referencia: en el procedimiento P , x = a1 yla<br />
sentencia “ x := x + 2” significa “ a[1] := a[1] + 2 ”. Por lo tanto, el valor<br />
final de a es (12, 20).<br />
Recordar entonces que<br />
• En Pascal la sustitución por valor es la usual, si queremos sustitución por<br />
referencia anteponemos la palabra “ var ”alparámetro formal.<br />
• No podemos poner como parámetro real una constante cuando el parámetro<br />
se pasa por referencia. Por ejemplo las declaraciones:
7.4. Pasando por valor o por referencia Pág. 79<br />
.<br />
procedure P(var x: integer):<br />
.<br />
begin (* parte principal *)<br />
.<br />
P(1);<br />
.<br />
.<br />
end.<br />
son incorrectas.<br />
• Si hay varios parámetros, algunos pueden pasarse por valor y otros por<br />
referencia (con “ var ”). Si hay una lista de parámetros separados por<br />
“ , ”, como en “ proc(var a, b: integer; c, d: integer)”, entonces<br />
los parámetros a y b se pasan por referencia mientras que los parámetros<br />
c y d se pasan por valor (aunque es conveniente evitar estas confusiones,<br />
poniendo explícitamente la palabra “ var ” delante de cada parámetro que<br />
se pasará por referencia).<br />
Problema 7.8. En los siguientes programas determinar los valores de los parámetros<br />
de las sentencias “ writeln ” y luego comprobar las respuestas ejecutándolos<br />
(agregando encabezado):<br />
a) var a, b, c: integer;<br />
procedure P(x, y: integer; var z: integer);<br />
begin z := x + y + z; writeln(x, y, z) end;<br />
begin a:= 5; b := 8; c := 3;<br />
P(a, b, c); P(7, a + b + c, a); P( a * b, a div b, c)<br />
end.<br />
b) var i, j, k: integer;<br />
procedure P(var i: integer);<br />
begin i := i + 1; writeln(i, j, k) end;<br />
procedure Q( h: integer; var j: integer);<br />
var i: integer;<br />
procedure R;<br />
begin i := i + 1 end;<br />
begin i := j;<br />
if (h = 0) then P(j) else if h = 1 then P(i) else R;<br />
writeln( i, j, k)<br />
end;<br />
begin i := 0; j := 1; k := 2; Q(0,k); Q(1,i); Q(2,j)<br />
end.
Pág. 80 Funciones y Procedimientos<br />
✎ Observar que el procedimiento “ R ” es local al procedimiento “ Q ”, y desconocido<br />
globalmente. Recordar el problema 7.6.d). ✄<br />
7.5. Comentarios Bibliográficos<br />
La sección 7.4 —incluyendo los problemas— está basada en los libros de<br />
Wirth [15] y[16].
Capítulo 8<br />
Todos juntos: arreglos,<br />
funciones y procedimientos<br />
8.1. Definiendo nuestros propios tipos de datos:<br />
“type”<br />
Hemos visto cuatro tipos de datos elementales —“ boolean”, “ char ”, “ integer<br />
”y“real ”— pero Pascal también nos permite crear nuevos tipos mediante<br />
la palabra “ type ”. Esto es especialmente conveniente cuando trabajamos<br />
con varios arreglos de las mismas características (y como veremos más tarde,<br />
también para otras estructuras).<br />
Supongamos por ejemplo que queremos trabajar con dos arreglos, a y b,<br />
declarados como<br />
var a, b: array[1..100] of integer<br />
Puesto que tienen las mismas características, podríamos definir un tipo para<br />
estos arreglos, por ejemplo<br />
type arregloentero = array[1..100] of integer<br />
después de la declaración de constantes y antes de la de variables. Observar que<br />
usamos el signo “ = ”, como en las constantes, y no “ : ” que usamos al declarar<br />
variables.<br />
Luego podemos declarar (en la parte de “ var ”)<br />
a, b: arregloentero<br />
lo que nos permite hacer asignaciones de la forma “ a := b”, y no tener que<br />
hacer un lazo para la asignación (1) .Más importante, es altamente conveniente y<br />
recomendable usar como argumentos de funciones o procedimientos únicamente<br />
parámetros de los tipos elementales o declarados con “ type ”, a fin de evitar<br />
errores.<br />
La estructura de un programa Pascal —a la que ya no haremos modificaciones—<br />
toma entonces la forma del cuadro 8.1, y la estructura de funciones<br />
(1) No obstante, no es posible hacer la comparación “ a = b”. En este caso habrá que usar<br />
un lazo.
Pág. 82 Todos juntos: arreglos, funciones y procedimientos<br />
y procedimientos es similar (pueden tener constantes, tipos, etc. propios), sólo<br />
que terminan con “ end; ”envezde“end. ”.<br />
1. program nombre (input, output);<br />
2. const si hubiera que definir constantes.<br />
3. type si hubiera definidos tipos propios.<br />
4. var si hay variables.<br />
5. Si las hubiera, funciones (con “ function ”) y procedimientos<br />
(con “ procedure ”). Deben declararse teniendo en cuenta<br />
el orden en que serán usadas.<br />
6. begin<br />
7. Instrucciones.<br />
8. end.<br />
Cuadro 8.1: Estructura de un programa Pascal.<br />
8.2. Ingreso e impresión de arreglos<br />
Ahora que contamos con funciones, es un buen momento para repasar la<br />
entrada de datos del programa busquedalineal del problema 6.2.<br />
Problema 8.1 (Ingreso de arreglos). Supongamos que queremos ingresar<br />
un arreglo a =(a1,a2,...,an) de datos (de algún tipo), ingresando un dato<br />
por renglón, poniendo los carteles en cada paso, y terminando la entrada con<br />
“ sin datos”. Siguiendo el esquema del programa busquedalineal, e<br />
incorporando el control del número de datos, de ahora en más vamos a usar un<br />
procedimiento para englobar esta acción.<br />
Si declaramos “ const MAXN = 100; ” y los tipos<br />
tipodato = integer; (* real, char,... *);<br />
tipoarreglo = array[1..MAXN] of tipodato;<br />
podemos leer arreglos del tipo “ tipoarreglo” mediante el siguiente procedimiento:<br />
procedure leerarreglo(var a: tipoarreglo; var n: integer);<br />
var findatos: boolean;<br />
begin<br />
write(’Entrar numeros enteros,’); (* o reales o... *)<br />
writeln(’ uno por renglon y no mas de ’, MAXN:1, ’.’);<br />
writeln(’ Fin con retorno sin entrada de datos.’);<br />
n := 1; findatos := false;<br />
repeat<br />
if (n > MAXN) then findatos := true<br />
else begin<br />
write(’Entrar el dato ’, n:2);<br />
write(’ (fin = ): ’);<br />
if (eoln) then begin findatos := true; readln end<br />
else begin readln(a[n]); n := n + 1 end<br />
end
8.2. Ingreso e impresión de arreglos Pág. 83<br />
until findatos;<br />
n := n - 1<br />
end;<br />
Podemos también copiar la impresión de arreglos del mismo programa, con<br />
las modificaciones del problema 6.2:<br />
procedure escribirarreglo(a: tipoarreglo; n: integer);<br />
const maxrenglon = 10; (* maxima cantidad por renglon *)<br />
var i: integer;<br />
begin<br />
for i := 1 to n do begin<br />
write(a[i]:6); (* cambiar formato para reales,... *)<br />
if ((i mod maxrenglon) = 0) then writeln<br />
end;<br />
if ((n mod maxrenglon) > 0) then writeln<br />
end;<br />
Hacer un programa con los procedimientos leerarreglo y escribirarreglo (que<br />
acabamos de describir), para leer y luego imprimir un arreglo de enteros, de<br />
modo que el cuerpo principal sea<br />
begin<br />
writeln(’** Prueba de lectura e impresion de arreglos’);<br />
writeln;<br />
leerarreglo( arreglo, ndatos);<br />
writeln(’El arreglo leido es:’);<br />
escribirarreglo( arreglo, ndatos);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Cuando esté funcionando correctamente, cambiar el tipo de datos ingresado<br />
de entero a real. ✄<br />
Problema 8.2. En el programa maximocaracter (pág. 192) se ingresa un arreglo<br />
de caracteres (un renglón), lo reproduce y encuentra su máximo.<br />
Observar la declaración del tipo tiporenglon con “ type ”.<br />
✎ Según el estándar Pascal, son incorrectas la declaraciones del tipo<br />
var s: array[1..10] of char<br />
como hacemos en el programa maximocaracter, ydeberían declararse como<br />
var s: packed array[1..10] of char<br />
Sin embargo, hemos preferido no adherir al estándar en este caso, para<br />
no introducir el concepto de “packed”. Si el compilador no lo admite,<br />
habrá que hacer la declaración según el estándar.<br />
Observar también que el procedimiento para leer el arreglo es ahora diferente<br />
al procedimiento leerarreglo del problema 8.1, pues no queremos entrar una letra<br />
por renglón: debemos cambiar “ readln(c)” por“read(c) ”, que lee el carácter<br />
c, pero no el fin de línea.<br />
✎ Recordando el problema 3.3.b), si pusiéramos “ readln(c) ” leeríamos c y<br />
se ignorarían los restantes caracteres hasta el fin de línea.<br />
a) max es un procedimiento y no una función, ¿por qué?
Pág. 84 Todos juntos: arreglos, funciones y procedimientos<br />
b) Observar que el argumento t en max es del tipo “ tiporenglon” y se pasa<br />
por referencia (mediante “ var ”).<br />
✎ En otro caso, habría que hacer una copia del arreglo (local a la función),<br />
ocupando lugar y tiempo. En los ejemplos con los que estamos<br />
trabajando, esto no tiene mayor importancia, debido al tamaño pequeño.<br />
c) max supone que long > 0. Dar un ejemplo donde es 0 y modificar el programa<br />
para considerar este caso.<br />
d) Modificarlo de modo de encontrar también el mínimo, usando un nuevo<br />
procedimiento.<br />
e) Modificarlo de modo que se intercambien las posiciones del máximo y el<br />
último elemento, imprimiendo el nuevo renglón. ✄<br />
Problema 8.3. Desarrollar un programa que, dado un arreglo de enteros entrado<br />
como en el problema 8.1, y sin usar otro arreglo adicional, lo invierta, i.e.<br />
si el arreglo inicialmente es (a1,a2,...,an), el arreglo final es (an,...,a2,a1).<br />
Sugerencia: hacer los intercambios a1 ↔ an, a2 ↔ an−1,. . . ✄<br />
Problema 8.4. Desarrollar un programa que leyendo un renglón (como en el<br />
programa maximocaracter), lo escriba al revés, e.g. si la entrada es<br />
“ dabale arroz a la zorra el abad ”<br />
el programa escriba<br />
“ daba le arroz al a zorra elabad ”<br />
Sugerencia: ver el problema anterior. ✄<br />
8.3. La caja de herramientas<br />
Iremos presentando cada vez menos programas completos, bajo el entendimiento<br />
de que cuestiones “comunes” ya han sido vistas, como leer o imprimir<br />
arreglos.<br />
Es una buena idea armar una “caja de herramientas” formada por archivos<br />
de texto en el disco, con las funciones o procedimientos que nos parecen más<br />
usados, o quizás más difíciles de reproducir. De esta forma, cuando necesitemos<br />
alguno, podemos simplemente hacer una copia en el programa, haciendo quizás<br />
pequeños cambios, sin necesidad de escribirlos —mucho menos pensarlos— cada<br />
vez.<br />
✎ Los sistemas de programación más avanzados incluyen en bibliotecas estas<br />
funciones o procedimientos, muchas veces en forma binaria difícil de modificar.<br />
Aunque similar, el concepto es ligeramente diferente del de la “caja<br />
de herramientas”.<br />
Por ejemplo, los procedimientos leerarreglo y escribirarreglo del problema 8.1<br />
nos dan una “plantilla” (o “template”) para leer o imprimir arreglos de números<br />
eventualmente modificando el índice inicial, e.g. “ array[1..MAXN]”, o el tipo<br />
de dato del arreglo, e.g “ real ”envezde“integer ”.
8.4. Arreglos multidimensionales Pág. 85<br />
Problema 8.5 (La Caja de Herramientas). Copiar en sendos archivos de<br />
texto (2) , los procedimientos leerarreglo y escribirarreglo del problema 8.1,ypracticar<br />
incorporarlos en un programa para leer y escribir un arreglo de enteros<br />
(como en el problema 8.1). ✄<br />
Seguramente habrá muchos procedimientos o funciones que querrás incorporar<br />
a la caja de herramientas, algunos que ya hemos visto como el método de la<br />
bisección (sección 7.3), y otros que veremos más adelante, como alguno de los<br />
métodos de búsqueda y clasificación del capítulo 10.<br />
8.4. Arreglos multidimensionales<br />
Los arreglos unidimensionales no son apropiados para guardar la información<br />
contenida en una tabla (como la del seno o del logaritmo). En estos casos<br />
es más conveniente usar un “arreglo de arreglos”, también llamado arreglo multidimensional.<br />
Por ejemplo, si queremos guardar una matriz de reales de 2 × 3, podemos<br />
pensar que necesitamos un arreglo de dimensión 2 (cantidad de filas), cuyos<br />
elementos individuales son arreglos de dimensión 3 (cantidad de columnas).<br />
Hacemos entonces la declaración<br />
m: array[1..2] of array[1..3] of real;<br />
o equivalentemente,<br />
m: array[1..2,1..3] of real;<br />
y accedemos al elemento (i, j) usando indistintamente “ m[i][j] ”o“m[i,j] ”.<br />
Las matrices son un ejemplo de estructura de dos dimensiones, pero no hay<br />
inconvenientes en considerar estructuras con cualquier número de dimensiones.<br />
Si en vez de números guardamos caracteres, las filas pueden pensarse como<br />
renglones, como hacemos en el siguiente problema.<br />
Problema 8.6. En este problema consideraremos un texto como una serie de<br />
renglones o líneas no vacías (cada una tiene al menos un carácter). Declaramos<br />
const<br />
MAXC = 255; (* maxima cantidad de caracteres por renglon *)<br />
MAXR = 20; (* maximo numero de renglones *)<br />
type<br />
tiporenglon = array[1..MAXC] of char;<br />
tipotexto = array[1..MAXR] of tiporenglon;<br />
caracteresenrenglon = array[1..MAXR] of integer;<br />
var<br />
nr, (* numero de renglones *)<br />
nc (* numero de caracteres en renglon *)<br />
: integer;<br />
texto: tipotexto;<br />
cenr: caracteresenrenglon;<br />
(2) En algunos sistemas operativos, es conveniente que tengan la extensión “ .txt ”.
Pág. 86 Todos juntos: arreglos, funciones y procedimientos<br />
eventualmente recordando la nota en el problema 8.2.<br />
a) Desarrollar un procedimiento o función para leer no más de MAXR renglones,<br />
con no más de MAXC caracteres por renglón, dando como señal de fin<br />
de entrada un “renglón vacío”.<br />
Sugerencia: copiar las ideas del programa maximocaracter.<br />
Sugerencia si la anterior no alcanza:<br />
nr := 0; (* numero de renglon leido, al ppo. ninguno *)<br />
while (not eoln) do begin<br />
(* mientras el renglon a leer no sea vacio... *)<br />
nr := nr + 1; (* hay un renglon mas *)<br />
nc := 0; (* que todavia no tiene caracteres leidos *)<br />
repeat (* leemos los caracteres *)<br />
nc := nc + 1; (* y los guardamos en el *)<br />
read(texto[nr][nc]) (* lugar correspondiente *)<br />
until (eoln); (* hasta llegar al fin del renglon *)<br />
readln; (* leemos el fin de linea *)<br />
cenr[nr] := nc (* nro. de caracteres en renglon nr *)<br />
end;<br />
b) Incorporar un procedimiento para escribir renglones, y verificar que el proceso<br />
de lectura desarrollada en el inciso anterior es correcto.<br />
✎ A veces arreglos como texto y cenr se dicen paralelos, pues la información<br />
se va actualizando simultáneamente. Cuando veamos registros, en la<br />
sección 10.4, veremos otra forma de guardar información de este tipo. ✄<br />
8.5. “Strings”<br />
Problema 8.7. El tipo “ string ”noesestándar en Pascal, pero existe en casi<br />
todos los compiladores, en particular en Turbo Pascal. Probar el comportamiento<br />
del compilador con un programa que lee un renglón entrado por terminal, vía<br />
“ readln(s) ”, donde se declara s como “ string[100]”, indicando su longitud,<br />
y/o simplemente como “ string ”.<br />
En caso de aceptar alguna de estas variantes, agregar al programa las instrucciones:<br />
a) “writeln(s)”, para escribir s,<br />
b) “length(s) ”, para averiguar su longitud, y<br />
c) “for i := 1 to length(s) do writeln( i:3, ’: ’, s[i]) ”, para escribirlo<br />
carácter por carácter, indicando el índice.<br />
d) Modificar el programa del problema 8.6, cambiando el tipo “ renglon ”por<br />
el tipo “ string ”.<br />
✎ Según el estándar Pascal, el tipo “string” es una denominación genérica<br />
para el tipo char y para arreglos empaquetados de caracteres, como los<br />
mencionados en la nota del problema 8.2. ✄<br />
8.6. Manejo elemental de archivos de texto<br />
A veces resulta útil guardar los resultados obtenidos por un programa, para<br />
después leerlos, imprimirlos o utilizarlos en otros programas. Dado el tamaño
8.6. Manejo elemental de archivos de texto Pág. 87<br />
de las “salidas” de los programas con los que trabajamos, será suficiente para<br />
nosotros trabajar con archivos de texto, es decir, archivos que podemos abrir<br />
(para leer, modificar o imprimir) con un editor de textos.<br />
El lenguaje Pascal no es especialmente apto para leer y escribir archivos, y es<br />
por ello que se han realizado muchas extensiones (que no respetan el estándar)<br />
tratando de mejorarlo, pero trataremos de ceñirnos al estándar. En él se establece<br />
otro tipo elemental de datos, además de los que ya hemos visto: el tipo<br />
“ text ”, o archivo de texto.<br />
✎ Las variables de tipo “ text ” que son argumentos de una función o procedimiento<br />
deben pasarse por referencia, i.e. anteponiendo “ var ”(versección<br />
7.4).<br />
Dos estructuras fundamentales para el manejo de archivos son: copiar de<br />
consola a archivo, y copiar de archivo a consola, que mostramos en los programas<br />
deconsolaaarchivo (pág. 194) ydearchivoaconsola (pág. 195) respectivamente, y<br />
que pasamos a comentar.<br />
• Al leer los programas, observamos que el el archivo a leer o escribir tiene<br />
una variable asignada, curiosamente llamada archivo, declarada como de<br />
tipo “ text ”. Una de las primeras cosas que hemos de hacer es relacionar<br />
este identificador, interno al programa, con el nombre que el archivo tiene<br />
o tendrá en el disco. Para eso, primeramente el usuario ingresa el nombre<br />
tal como aparece o aparecerá eneldisco.<br />
✎ Nos hemos permitido usar “ string ” para la variable correspondiente,<br />
aunque no es estándar y habrá que hacer modificaciones si el<br />
compilador no lo acepta. Recordar la nota en el problema 8.2.<br />
• Luego el nombre interno y el externo se relacionan mediante “ rewrite ”<br />
si el archivo se escribirá, o mediante “ reset ” si el archivo se leerá.<br />
Ha de tenerse cuidado con “ rewrite ”, pues si el archivo no existe, se crea<br />
uno nuevo, pero si ya existe, sus contenidos son borrados.<br />
✎ En compiladores que siguen el modelo de Turbo Pascal para entrada/salida,<br />
hay que modificar los programas, ya que no aceptan el<br />
estándar.<br />
Para escribir un archivo, hay que cambiar el renglón<br />
rewrite(archivo, nombre);<br />
por<br />
assign(archivo, nombre); rewrite(archivo);<br />
en el programa deconsolaaarchivo. Del mismo modo, para leer desde<br />
un archivo, hay que modificar el renglón<br />
reset(archivo, nombre);<br />
por<br />
assign(archivo, nombre); reset(archivo);<br />
en el programa dearchivoaconsola.<br />
Sintetizamos estas diferencias en el cuadro 8.2.
Pág. 88 Todos juntos: arreglos, funciones y procedimientos<br />
Estándar Turbo Pascal<br />
escribir rewrite(archivo, nombre) assign(archivo, nombre);<br />
archivo rewrite(archivo)<br />
leer reset(archivo, nombre) assign(archivo, nombre);<br />
archivo reset(archivo)<br />
Cuadro 8.2: Diferencias entre el estándar y Turbo Pascal para leer o escribir<br />
archivos.<br />
• Luego de leer el nombre externo y relacionarlo con el interno, debemos<br />
leer de consola e ir escribiendo el archivo en un caso, y recíprocamente,<br />
leer del archivo y escribir en consola en el otro.<br />
En el primer caso vemos una estructura similar a la lectura de renglones<br />
del problema 8.6, exceptoquealescribirnousamos“write(c)” sino<br />
“ write(archivo, c) ”paraescribircen el archivo y no la consola, debiendo<br />
incorporar el nombre del archivo. Del mismo modo, “ writeln(archivo)<br />
”(sinelargumentoc) escribeun“findelínea” terminando el<br />
renglón en el archivo.<br />
En el segundo caso, cuando leemos del archivo en dearchivoaconsola eimprimimos<br />
en la terminal, volvemos a encontrar una estructura conocida,<br />
excepto que ahora el “fin de datos” que anteriormente señalábamos con<br />
“ vacío”, ahora se señala de un modo especial para archivos de<br />
textos: el “fin de archivo”.<br />
De modo similar a “ eoln ”, “ eof(archivo)” pregunta si ya hemos llegado<br />
aestaseñal en el archivo que estamos leyendo.<br />
✎ Hay que tener cuidado pues no debe llamarse a “ eoln ”siseha<br />
llegado al final del archivo, ya que un archivo de texto puede no<br />
tener fin de línea: primero siempre debe llamarse a “ eof ”.<br />
También, “ read(c) ” se cambia por “ read(archivo,c)” para leer c desde<br />
el archivo y no la consola.<br />
• Al terminar de leer o escribir el archivo, usamos “ close(archivo)”, lo<br />
que hace que ya no se relacionen el nombre del archivo en el disco y la<br />
variable nombre, y —en el caso de estar escribiendo— coloca en el archivo<br />
la señal de “fin de archivo”.<br />
Problema 8.8 (Archivos de texto).<br />
✎ Recordar los cambios a realizar —usando “ assign ”— si se sigue el modelo<br />
de Turbo Pascal.<br />
a) Compilar y ejecutar el programa deconsoloaarchivo, y verificar su comportamiento<br />
abriendo el archivo creado con un editor de textos.<br />
✎ El “directorio” en el cual el programa creará o buscará el archivo<br />
depende de la instalación del compilador. Las posibilidades son: dar<br />
una ubicación “absoluta”, indicando el “camino” completo; o crear<br />
un archivo y encontrar dónde se instaló, a veces el mismo compilador<br />
permite determinar el directorio donde se trabajará.<br />
✎ En algunos sistemas operativos es conveniente que los archivos de texto<br />
tengan la extensión “ .txt ”.
8.7. Problemas Adicionales Pág. 89<br />
b) Del mismo modo, compilar y ejecutar el programa dearchivoaconsola.<br />
c) Tomando las partes necesarias de los programas deconsolaaarchivo y dearchivoaconsola,<br />
y tal vez del problema 8.6, hacer un programa dearchivoaarchivo<br />
para copiar un archivo en otro (nuevo) archivo, y verificar con un editor de<br />
textos que el nuevo archivo es correcto. Sugerencia: dar distintos nombres<br />
a las variables para el archivo de entrada y el de salida.<br />
✎ Ya que estamos usando “ string ”, podríamos haber leído todo un renglón<br />
en vez de carácter por carácter. Sin embargo, es posible que los “renglones”<br />
tengan más de la longitud permitida por “ string ”, usualmente 256<br />
caracteres. ✄<br />
Problema 8.9.<br />
a) Hacer una función que dado n ∈ N, determine la longitud del período de la<br />
fracción 1/n (recordar el problema 6.10, pág. 64).<br />
b) Usando la función anterior, hacer un programa para crear un archivo con<br />
las longitudes de los períodos de las fracciones 1/n, paran =1, 2,...,100,<br />
escribiendo 10 longitudes por renglón. Verificar el contenido abriendo el<br />
archivo creado con un editor de textos.<br />
c) Hacer otro programa que lea el archivo creado en el inciso anterior, y determine<br />
el promedio de los períodos. ✄<br />
8.7. Problemas Adicionales<br />
Problema 8.10. Hacer un programa que dado un arreglo de n enteros, lo<br />
rote en m posiciones hacia la izquierda, sin usar un arreglo auxiliar. E.g. si<br />
n = 10 y m = 3, el arreglo (0, 1, 2, 3, 4, 5, 6, 7, 8, 9) debe transformarse en<br />
(3, 4, 5, 6, 7, 8, 9, 0, 1, 2).<br />
Sugerencia: el problema consiste en transformar el vector de dos bloques AB<br />
en BA. Si A R es la operación de invertir el arreglo A (ver problema 8.3),<br />
(A R B R ) R = BA. Implementar la inversión como función o procedimiento. ✄<br />
Problema 8.11 (Contando palabras en texto). Al usar un editor de textos<br />
es frecuentemente necesario contar la cantidad de palabras, por ejemplo porque<br />
el informe que estamos haciendo no puede superar cierto número de palabras.<br />
Hacer un programa para leer renglones de caracteres (marcando fin de la<br />
entrada con sin otro dato) y contar el número de palabras ingresados,<br />
donde para simplificar y complicar, supondremos que:<br />
• Sólo se ingresan letras (mayúsculas o minúsculas) sin acentos ni tildes,<br />
números, espacios en blanco o ’s. O sea no se ingresan signos de<br />
puntuación ni tabulación.<br />
• Una palabra es una sucesión máxima de letras y/o números sin espacios<br />
intermedios.<br />
• Pero puede haber varios espacios en blanco consecutivos o al principio o<br />
fin de cada renglón.<br />
Por ejemplo, si ingresamos
Pág. 90 Todos juntos: arreglos, funciones y procedimientos<br />
hay 23 sapos que nacieron tristes<br />
la variable era x27<br />
menos mal que las vacas no vuelan<br />
el programa debe responder que se ingresaron 17 palabras.<br />
Modificarlo de modo de contar las palabras en un archivo de texto, donde<br />
los caracteres que no son letras, números, espacios en blanco o finales de línea<br />
son ignorados, y no hay acentos ni tildes: como en un programa fuente. ✄<br />
8.8. Comentarios Bibliográficos<br />
La descripción de tipos y archivos de textos en Pascal está basada en [7]. El<br />
problema 8.10 está tomado de [4].
Capítulo 9<br />
Números Aleatorios y<br />
Simulación<br />
Muchas veces se piensa que en matemáticas las respuestas son “siempre exactas”,<br />
olvidando que las probabilidades forman parte de ella y que son muchas<br />
las aplicaciones de esta teoría.<br />
Una de estas aplicaciones es la simulación, técnica muy usada por físicos,<br />
ingenieros y economistas cuando es difícil llegar a una fórmula que describa el<br />
sistema o proceso. Así, simulación es usada para cosas tan diversas como el<br />
estudio de las colisiones de partículas en física nuclear y el estudio de cuántos<br />
cajeros poner en el supermercado para que el tiempo de espera de los clientes<br />
en las colas no sea excesivo.<br />
La simulación mediante el uso de la computadora es tan difundida, que hay<br />
lenguajes de programación (en vez del Pascal o “C”) especialmente destinados<br />
aestepropósito.<br />
9.1. Números aleatorios<br />
Cuando en la simulación interviene el azar o la probabilidad, se usan números<br />
generados por la computadora que reciben el nombre de “aleatorios” (o, más<br />
correctamente, “seudo-aleatorios”). No es nuestra intención aquí hacer una descripción<br />
de quésonestosnúmeros o cómo se obtienen: basta con la idea intuitiva<br />
de que la computadora nos da un número en cierto rango, y cualquier número<br />
del rango tiene la misma probabilidad de ser elegido por la computadora.<br />
En general, los números aleatorios se obtienen a partir de un valor inicial o<br />
semilla (“seed” en inglés); de modo que si no se indica lo contrario —cambiando<br />
la semilla— siempre obtenemos la misma sucesión de números aleatorios. Un<br />
método eficaz para obtener “números verdaderamente aleatorios”, es cambiar<br />
la semilla de acuerdo a la hora que indica el reloj de la computadora.<br />
Lamentablemente, en el estándar Pascal no está definida una función para<br />
generar números aleatorios. Aún cuando un compilador tenga implementada<br />
una tal función, el nombre con la que se llama y aún el tipo de resultado —por<br />
ejemplo si son números reales entre 0 y 1 o enteros entre 0 y maxint— dependen<br />
también del compilador.
Pág. 92 Números Aleatorios y Simulación<br />
Dado que muchos compiladores siguen el modelo de Turbo Pascal, nosotros<br />
seguiremos la notación de este compilador para no complicar más de lo necesario,<br />
apartándonos del estándar.<br />
✎ Si no se dispone de una rutina para generar números aleatorios, es necesario<br />
instalar una propia. Sin embargo, obtener números aleatorios con<br />
la computadora no es tan sencillo como tirar dados, y hay mucha teoría<br />
matemática detrás de los “buenos” generadores: el lector interesado puede<br />
consultar el excelente libro de Knuth [11, vol.2].<br />
Una posibilidad es usar las rutinas que aparecen como ejemplo en el<br />
mismo estándar de Pascal extendido (ISO 10206) adaptado al estándar<br />
estándar de Pascal sin extensiones que es el que usamos (1) .<br />
Turbo Pascal cuenta con las funciones “ random ”y“randomize ”, y la variable<br />
randseed .“random ” sin argumentos da un número aleatorio entre 0 y 1;<br />
y cuando incluimos un argumento n ∈ N, “random(n)” daunnúmero aleatorio<br />
entero entre 0 y n−1. La semilla es la variable “ randseed ”, que puede cambiarse<br />
de acuerdo al reloj de la computadora mediante la instrucción “ randomize”.<br />
9.2. Aplicaciones<br />
Problema 9.1. El programa dado (pág. 195) hace una simulación de tirar un<br />
dado mediante números aleatorios obtenidos con la sentencia “ random ”.<br />
a) La sentencia “randomize” sirve para comenzar una nueva serie de números<br />
aleatorios. Eliminarla, comentándola, ejecutar repetidas veces el programa<br />
y comprobar que siempre se obtienen los mismos resultados (o sea no es<br />
muy al azar).<br />
✎ Salvo para programas que tienen un tiempo de ejecución grande, no<br />
debe hacerse más de una llamada a “ randomize ”.<br />
b) Modificar el programa para simular tirar una moneda con resultados “cara”<br />
o “ceca”. ✄<br />
Problema 9.2. El programa dados (pág. 196) hace una simulación para encontrar<br />
la cantidad de veces que se necesita tirar un dado hasta que aparezca un<br />
número prefijado, entrado por el usuario. Gracias a la sentencia “ randomize”, el<br />
resultado en general será distinto con cada ejecución. Observar que si el usuario<br />
entra un número menor que 1 o mayor que 6, el programa no termina nunca.<br />
a) Ejecutar el programa repetidas veces, para tener una idea de cuánto tarda<br />
en aparecer un número.<br />
b) En vez de correr varias veces el programa, modificarlo de modo de realizar<br />
n simulaciones (n entrado por el usuario), mostrando como resultado el<br />
promedio de los tiros en que tardó en aparecer el número predeterminado.<br />
Sugerencia: encerrar el lazo “ repeat ”dentrodeunlazo“for ”.<br />
c) Modificar el programa original a fin de simular que se tiran simultáneamente<br />
dos dados, y contar el número de tiros necesarios hasta obtener un resultado<br />
ingresado por el usuario (entre 2 y 12). ✄<br />
(1) Una copia del estándar en formato pdf puede encontrarse en<br />
http://www.pascal-central.com/standards.html<br />
El ejemplo en el estándar está tomado, a su vez, del artículo de Wichmann and Hill, Building<br />
a Random-Number Generator, Byte, marzo 1987, págs. 127–128.
9.2. Aplicaciones Pág. 93<br />
Problema 9.3. Tomando como base el problema anterior y el programa dados,<br />
hacer un programa que diga cuántas veces debió tirarse un dado hasta que<br />
aparecieron k seis consecutivos, donde k es ingresado por el usuario. Sugerencia:<br />
poner un contador c inicialmente en 0, y dentro de un lazo “ repeat ” (donde<br />
se hace la llamada a “ random ”) el contador se incrementa en 1 si salió unseis<br />
ysinosevuelvea0.<br />
✎ En éste y en el problema 9.2, surge la duda de si el programa terminará alguna<br />
vez, dado que existe la posibilidad de que nunca salga el número<br />
prefijado, o que nunca salga k veces consecutivas. Sin embargo, se puede<br />
demostrar matemáticamente que la probabilidad de que esto suceda es 0<br />
(suponiendo que el generador de números aleatorios sea correcto). ✄<br />
Problema 9.4. En este problema usaremos la sentencia “ random ”(sinargumentos)<br />
de Turbo Pascal que da un número aleatorio real entre 0 y 1.<br />
a) Hacer un programa que elija aleatoriamente el número 1 aproximadamente<br />
el 45 % de las veces, el número 2 el 35 % de las veces, el 3 el 15 % de las<br />
veces y el 4 el 5 % de las veces. Sugerencia: considerar las sumas parciales<br />
.45,.45 + .35,...<br />
b) Generalizar al caso en que en vez de la lista (1, 2, 3, 4) se dé una lista<br />
(a1,a2,...,an) y que en vez de las frecuencias (.45,.35,.15,.5) se dé una<br />
lista (f1,f2,...,fn), con n<br />
i=1 fi = 1. Probarlo para distintos valores y<br />
verificar que las frecuencias son similares a las deseadas. ✄<br />
Problema 9.5. El compilador de Pascal de Ana tiene la sentencia “ aleat ”,<br />
que obtiene números aleatorios r ∈ R con 0 ≤ r
Pág. 94 Números Aleatorios y Simulación<br />
elemento se repite. Sugerencia: agregar un segundo arreglo para contar las<br />
apariciones.<br />
✎ La cantidad de apariciones deberían ser muy similares, aproximadamente<br />
r/s cada uno, cuando r ≫ s. ✄<br />
Problema 9.8 (Dos con el mismo cumpleaños). Mucha gente suele sorprenderse<br />
cuando en un grupo de personas hay dos con el mismo día de cumpleaños:<br />
la probabilidad de que esto suceda es bastante más alta de lo que se<br />
cree normalmente.<br />
Supongamos que en una sala hay n (n ∈ N) personas y supongamos, para<br />
simplificar, que no hay años bisiestos (no existe el 29 de febrero), de modo que<br />
podemos numerar los posibles días de cumpleaños 1, 2,...,365.<br />
a) ¿Para quévalores de n se garantiza que haya al menos dos personas que<br />
cumplen años el mismo día? Sugerencia: recordar el principio del casillero,<br />
también conocido como del palomar o de Dirichlet<br />
✎ El principio de Dirichlet dice que si hay n + 1 objetos repartidos en n<br />
casillas, hay al menos una casilla con 2 o más objetos.<br />
b) Si la sala es un cine al cual van entrando de a una las personas, ¿cuántas<br />
personas, en promedio, entrarán hasta que dos de ellas tengan el mismo<br />
día de cumpleaños? Responder esta pregunta escribiendo un programa que<br />
genere aleatoriamente días de cumpleaños (números entre 1 y 365) hasta que<br />
haya dos que coincidan, retornando la cantidad de “personas” necesarias.<br />
Hacer varias “corridas” para tener una idea más acabada.<br />
Si en el programa se usan arreglos, ¿cuál será ladimensión, de acuerdo<br />
al inciso anterior?<br />
c) Basado en el punto anterior, si en tu curso hay 30 compañeros, ¿apostarías<br />
que hay dos que cumplen años el mismo día? ✄<br />
9.3. Problemas Adicionales<br />
Problema 9.9 (Aguja de Buffon). Una aguja fina (teóricamente un segmento)<br />
de longitud l ≤ 1, es arrojada sobre un tablero dividido por líneas paralelas<br />
donde la distancia entre dos consecutivas es 1. Mostrar, mediante un programa,<br />
que el promedio de veces que la aguja cruzará una de las líneas es aproximadamente<br />
2l/π.<br />
Georges Louis Leclerc, Conde de Buffon (1707–1788) planteó este problema<br />
en 1733, dando vigor a la teoría de probabilidad geométrica. Este problema en<br />
particular es más que nada de interés teórico e introductorio al tema, pues las<br />
aproximaciones experimentales de π no son buenas. ✄<br />
Problema 9.10 (El Show de Televisión). Un problema que causó gran<br />
revuelo hacia 1990 en Estados Unidos es el siguiente:<br />
En un show televisivo el locutor anuncia al participante que detrás<br />
de una de las tres puertas que ve hay un auto 0 km, no habiendo nada<br />
detrás de las otras dos. El locutor le pide al participante que elija<br />
una de las puertas. Sin abrir la puerta elegida por el participante, el<br />
locutor abre otra puerta mostrándole que no hay nada detrás de ésta,<br />
yledalaopción al participante de cambiar su elección (pudiendo
9.3. Problemas Adicionales Pág. 95<br />
optar entre mantener su primera elección o elegir la tercer puerta).<br />
El participante gana el auto si éste está detrás de la puerta que elige<br />
finalmente.<br />
a) Intuitivamente y sin dar una justificación rigurosa, ¿es más conveniente para<br />
el participante mantener su primera elección, cambiarla o es indiferente?<br />
b) Hacer un programa que simule el juego para tener mejor idea: i) el programa<br />
elige una de las puertas donde estará el auto (sin que nosotros sepamos<br />
el resultado); ii) nosotros hagamos una elección; iii) que en base a esta<br />
elección, el “locutor” elija una segunda puerta detrás de la cual no está el<br />
auto, al azar si hay dos posibilidades; iv) que nosotros mantengamos o<br />
cambiemos la elección; y v) que diga si ganamos o no el auto.<br />
c) Modificar el programa anterior de modo de hacer automáticamente n juegos<br />
en los que siempre elegimos una puerta al azar y luego cambiamos,<br />
retornando el número de veces que ganamos. Correrlo para n = 1000 (¡primero<br />
hacerlo para n =5paraversiestáfuncionando bien!). Comparar el<br />
resultado con la respuesta en a). ✄
Capítulo 10<br />
Búsqueda y clasificación<br />
Siempre estamos buscando algo y es mucho más fácil encontrarlo si los datos<br />
están clasificados u ordenados. No es sorpresa que búsqueda y clasificación sean<br />
temas centrales en informática y que haya una enorme cantidad de material<br />
escrito al respecto. Por ejemplo, en sus clásicos libros [11] Knuth dedica al tema<br />
todo el Volumen 3 (que por supuesto, usa material de los volúmenes anteriores).<br />
Acá hacemos una introducción al tema siguiendo, en mínima proporción, la<br />
presentacióndeWirthen[16].<br />
10.1. Búsqueda lineal con centinela<br />
Empecemos recordando lo hecho en el problema 6.2 y el programa busquedalineal<br />
en el que recorríamos “linealmente” el arreglo a =(a1,a2,...,an) buscando<br />
x (suponiendo n>0).<br />
Problema 10.1. Al buscar x en el arreglo (a1,a2,...,an), el programa busquedalineal<br />
(pág. 184) usaunlazo“repeat ”:<br />
i := 0;<br />
repeat i := i + 1; seencontro := (a[i] = x)<br />
until (seencontro or (i = n));<br />
if (seencontro) then... (* se encontro en la posicion i *)<br />
¿Con cuáles de las siguientes alternativas se podría reemplazar este lazo?:<br />
a) i := 0;<br />
repeat i := i + 1 until ((i = n) or (x = a[i]));<br />
if (x = a[i]) then... (* se encontro en la posicion i *)<br />
b) i := 1;<br />
while ((i
10.1. Búsqueda lineal con centinela Pág. 97<br />
while ((a[i] x) and (i > 1)) do i := i - 1;<br />
if (x = a[i]) then... (* se encontro en la posicion i *) ✄<br />
En el lazo “ while ”o“repeat ” de los esquemas en el problema anterior para<br />
búsqueda lineal se hacen dos comparaciones, pero podemos mejorarlo haciendo<br />
una sola colocando a x como “centinela” en alguna posición de modo que siempre<br />
lo encontremos.<br />
Por ejemplo, una modificación del esquema del inciso d) del problema anterior<br />
—que es correcto— suponiendo que el arreglo a está dimensionado adecuadamente,<br />
es:<br />
(* a debe ser declarado de modo de admitir a[0] *)<br />
a[0] := x; i := n;<br />
while (x a[i]) do i := i - 1;<br />
if (i > 0) then... (* se encontro en la posicion i *)<br />
Observamos que siempre termina, ya sea porque x es un elemento del arreglo<br />
original, en cuyo caso i es el lugar que ocupa, o bien porque no se encontró, en<br />
cuyo caso i =0.<br />
✎ No hay nada misterioso en ir desde atrás hacia adelante, podríamos haber<br />
puesto el “centinela” en la posición n+1 y recorrer de adelante hacia atrás.<br />
Problema 10.2 (Búsqueda lineal con centinela). Hacer una implementación<br />
con ambas variantes (con y sin centinela) como procedimientos, incluyendo<br />
un contador para la cantidad de comparaciones en cada una, y probar el comportamiento<br />
en distintos ejemplos (entrar el arreglo como en el procedimiento<br />
leerarreglo, pág. 82, o generar uno aleatoriamente). ✄<br />
Problema 10.3. En este problema queremos hacer un procedimiento para eliminar<br />
elementos repetidos del arreglo de enteros a = (a1,a2,...,an), como<br />
en el problema 6.4, pero cuando a está ordenado de menor a mayor, e.g. si<br />
a =(1, 2, 2, 5, 6, 6, 9), queremos que al fin del procedimiento sea a =(1, 2, 5, 6, 9).<br />
En este caso podemos poner algo como<br />
procedure purgarordenado(var a: arreglo; var n: integer);<br />
(* sacar elementos repetidos del arreglo<br />
ordenado a de longitud n. *)<br />
var i, m: integer;<br />
begin<br />
m := 1; (* a[1],...,a[m] son los elementos sin repetir *)<br />
for i := 2 to n do<br />
(* incluir a[i] si no es a[m] *)<br />
if (a[m] < a[i]) then begin<br />
m := m + 1; a[m] := a[i] end;<br />
n := m<br />
end;<br />
Hacer un programa para verificar el comportamiento de este procedimiento.<br />
✄<br />
Problema 10.4 (Fusión o mezcla de arreglos ordenados). Supongamos<br />
que tenemos dos arreglos a = (a1,a2,...,an) yb = (b1,b2,...,bm) ordenados<br />
de menor a mayor (con n, m ≥ 1), y queremos construir un arreglo c =
Pág. 98 Búsqueda y clasificación<br />
(c1,c2,...,ck) queestétambién ordenado y de modo que tenga los elementos<br />
de a y b. Por ejemplo, si a = (1, 2, 5, 5) y b =(2, 3, 4), deberá serc =<br />
(1, 2, 2, 3, 4, 5, 5).<br />
✎ Recordar el problema 6.3.<br />
Hacer un procedimiento para obtener c, usando un esquema como<br />
i := 1; j := 1; k := 0;<br />
while ((i
10.2. Búsqueda binaria Pág. 99<br />
i) Verificar que (10.1) implica (10.2).<br />
ii) Verificar que si en cambio, mucho ≥ poco +2, entonces poco < medio <<br />
mucho.<br />
c) Si se cambiara el esquema del inciso a) a<br />
poco := 1; mucho := n;<br />
while (poco + 1 < mucho) do begin<br />
medio := (poco + mucho) div 2;<br />
if (a[medio] < x) then poco := medio<br />
else mucho := medio<br />
end;<br />
(* a continuación comparar x<br />
con a[mucho] y con a[poco] *)<br />
¿sería ahora correcto?<br />
d) Si en vez de considerar poco + 1 en la condición del lazo del inciso anterior,<br />
lo incorporamos a la asignación, llegamos a:<br />
poco := 1; mucho := n;<br />
while (poco < mucho) do begin<br />
medio := (poco + mucho) div 2;<br />
if (a[medio] < x) then poco := medio + 1<br />
else mucho := medio<br />
end<br />
(* a continuación comparar x con a[mucho] *)<br />
En este inciso veremos que el nuevo esquema es correcto:<br />
i) En todo momento poco ≤ mucho, la distancia entre ellos se acorta en<br />
cada “vuelta” del lazo, y al terminar es poco = mucho (y no puede ser<br />
poco > mucho) (recordar el inciso b.ii)).<br />
ii) Si x ≤ a1, poco queda fijo, y al final se compara x con a1.<br />
iii) Si x>an, mucho queda fijo en las siguientes iteraciones, y al final se<br />
compara x con an (y obtendremos que x/∈ a).<br />
iv) Si apoco
Pág. 100 Búsqueda y clasificación<br />
until ((a[k] = x) or (i >= j))<br />
b) i := 1; j := n;<br />
repeat<br />
k := (i + j) div 2;<br />
if (x = j)<br />
d) Muchos autores presentan un algoritmo un poco más ineficiente que el del<br />
problema 10.5.d), comparando x con amedio en cada paso de modo que hay<br />
dos comparaciones por “vuelta” de lazo:<br />
poco := 1; mucho := n; encontrado := false;<br />
while ((poco a[medio]) then poco := medio + 1<br />
else mucho := medio - 1<br />
end;<br />
Ver que el esquema es correcto. ✄<br />
Problema 10.7. Se ha roto un cable maestro de electricidad en algún punto<br />
de su recorrido subterráneo de 50 cuadras. La compañía local de electricidad<br />
puede hacer un pozo en cualquier lugar para comprobar si hasta allí elcable<br />
está sano, y bastará con detectar el lugar de la falla con una precisión de 5m.<br />
Por supuesto, una posibilidad es ir haciendo pozos cada 5m, pero el encargado<br />
no está muy entusiasmado con la idea de hacer tantos pozos, porque hacer<br />
(y después tapar) los pozos cuesta tiempo y dinero, y los vecinos siempre se<br />
quejan por el tránsito, que no tienen luz, etc.<br />
¿Qué lepodrías sugerir al encargado? ✄<br />
Problema 10.8 (El regalo en las cajas). Propongamos el siguiente juego:<br />
Se esconde un “regalo” en una de diez cajas alineadas de izquierda<br />
a derecha, y nos dan cuatro oportunidades para acertar. Después de<br />
cada intento nuestro, nos dicen si ganamos o si el regalo está hacia<br />
la derecha o izquierda.<br />
a) Ver que siempre se puede ganar si nos dan cuatro oportunidades.<br />
b) Simular este juego en la computadora: la computadora elige aleatoriamente<br />
una ubicación para el regalo, y luego en cada elección del usuario el programa<br />
responde si el regalo está en la caja elegida (y termina), a la derecha o a la<br />
izquierda, orientando la búsqueda. Si después de cuatro oportunidades no<br />
se acertó la ubicación, el programa termina dando el número de caja donde<br />
estaba el regalo.
10.3. Métodos elementales de clasificación Pág. 101<br />
c) Cambiar el programa de cuatro a tres oportunidades, y ver que no siempre<br />
se puede ganar.<br />
d) ¿Cuántas oportunidades habrá que dar para n cajas, suponiendo una estrategia<br />
de búsqueda binaria? Sugerencia: la respuesta involucra las funciones<br />
techo y log2. ✄<br />
10.3. Métodos elementales de clasificación<br />
Nuestra próxima tarea consistirá en ver cómo clasificar u ordenar un arreglo<br />
a =(a1,a2,...,an) de elementos no necesariamente distintos, poniendo el<br />
resultado en el mismo arreglo a —tratando de evitar usar otro arreglo—, pero<br />
con sus elementos ordenados de menor a mayor. Para ello veremos tres métodos<br />
elementales para clasificar que podemos relacionar con la forma con que<br />
generalmente cada uno ordena las cartas de un juego:<br />
Inserción directa: Levantamos una carta, buscamos su posición de derecha<br />
a izquierda en el abanico que tenemos en la mano, la colocamos en<br />
el lugar correspondiente y continuamos levantando la próxima carta.<br />
Podemos escribir el algoritmo resultante como:<br />
for i := 2 to n do begin<br />
x := a[i]; a[0] := x; j := i;<br />
while (x < a[j-1]) do begin<br />
a[j] := a[j-1]; j := j-1<br />
end;<br />
a[j] := x<br />
end<br />
✎ Observar el uso de centinela: a debe ser declarado de modo de<br />
admitir a0.<br />
Selección directa: Levantamos las cartas al mismo tiempo (todas juntas),<br />
las abrimos en abanico, y comenzando desde la izquierda buscamos la<br />
menor y la colocamos al principio, luego buscamos la segunda menor<br />
y la ubicamos en la segunda posición, etc.<br />
Acá podemosponer:<br />
for i := 1 to n-1 do begin<br />
k := i; x := a[i];<br />
for j := i+1 to n do<br />
if (x > a[j]) then begin<br />
(* nuevo maximo *)<br />
k := j; x := a[k] end;<br />
a[k] := a[i]; a[i] := x<br />
end<br />
✎ Comparar con el inciso e) delproblema8.2.<br />
Intercambio directo o burbujeo: Aquítambién levantamos las cartas al<br />
mismo tiempo y las abrimos en abanico, pero vamos mirando de derecha<br />
a izquierda buscando un par de cartas consecutivas fuera de<br />
orden y cuando lo encontramos lo invertimos. Seguimos recorriendo
Pág. 102 Búsqueda y clasificación<br />
con la mirada el abanico hasta que no haya ningún par fuera de orden.<br />
Un posible esquema es:<br />
for i := 2 to n do<br />
for j := n downto i do<br />
if (a[j-1] > a[j]) then begin<br />
(* intercambiar *)<br />
x := a[j-1]; a[j-1] := a[j]; a[j] := x<br />
end<br />
✎ Por supuesto en cada uno de los métodos podemos cambiar el sentido del<br />
recorrido, empezando desde la derecha o desde la izquierda.<br />
Problema 10.9 (Métodos elementales de clasificación). Implementar los<br />
algoritmos anteriores como procedimientos en un único programa. Agregar en<br />
cada uno contadores para el número de asignaciones y comparaciones (entre<br />
datos como x o ak ynoentreíndices como i, j o k) hechas en cada caso, y<br />
comparar estas cantidades cuando:<br />
a =(1, 2, 3, 4, 5, 6), a =(6, 5, 4, 3, 2, 1), a =(2, 6, 8, 7, 4, 5, 1, 3, 6, 4). ✄<br />
En el cuadro 10.1 mostramos una comparación entre los tres métodos y el de<br />
“clasificación por fusión” —un método avanzado que explicamos en el problema<br />
15.7— con arreglos de 10 000 enteros construidos de tres formas: ordenado,<br />
ordenado al revés, y aleatorio. Los tiempos en el cuadro son para una máquina<br />
particular (y no demasiado rápida para los estándares cuando se realizaron las<br />
pruebas) y determinado compilador, y debe considerarse sólo la proporción entre<br />
ellos (la cifra 0.00 para el tiempo indica que la fracción de segundo es menor<br />
a1/60 = 0.0166 ...). Se contaron las comparaciones y asignaciones de arreglos<br />
(y no cuando se comparan o asignan índices).<br />
Observamos en la tabla que tanto selección directa como intercambio directo<br />
realizan siempre el mismo número de comparaciones, n(n − 1)/2, y que aún<br />
cuando no se hacen asignaciones (como en el caso de intercambio directo, cuando<br />
el arreglo está ordenado), la enorme cantidad de comparaciones lleva tiempo.<br />
En cuanto a las asignaciones, hemos de destacar que hemos tomado arreglos<br />
de enteros, por lo que individualmente no llevan demasiado tiempo, y en el<br />
caso de inserción directa con un arreglo aleatorio, hay muchas asignaciones pero<br />
—comparando con los otros métodos elementales— no llevan tanto tiempo.<br />
Sería distinto si los elementos fueran arreglos, por ejemplo “strings” al clasificar<br />
palabras, o registros (que veremos un poco más adelante).<br />
Algunos autores usan búsqueda binaria en vez de búsqueda lineal en inserción<br />
directa, disminuyendo en principio el número de comparaciones. Pero las<br />
ventajas son marginales y aún pueden empeorar el algoritmo (ver [16, pág. 87]).<br />
Muchos libros de programación insisten en presentar el método de intercambio<br />
directo o burbujeo —y a veces ningún otro— que es claramente inferior a<br />
cualquiera de los otros: aún en el caso del arreglo ordenado, se hace la misma<br />
cantidad de comparaciones que selección directa, y ninguna asignación, pero ¡el<br />
tiempo es superior! Hemos incluido este método sólo para que el lector sepa<br />
que existe y que es bastante malo: nosotros casi siempre elegiremos el de selección<br />
directa o inserción directa: selección directa hace relativamente pocas<br />
asignaciones, inserción directa relativamente pocas comparaciones.
10.3. Métodos elementales de clasificación Pág. 103<br />
Arreglo ya ordenado<br />
fusión selección intercambio inserción<br />
comparaciones 71 712 49 995 000 49 995 000 9 999<br />
asignaciones 140 000 29 997 0 29 997<br />
tiempo (segs.) 0.00 1.10 1.43 0.00<br />
Arreglo ordenado al revés<br />
fusión selección intercambio inserción<br />
comparaciones 64 608 49 995 000 49 995 000 50 004 999<br />
asignaciones 140 000 25 029 997 149 985 000 50 024 997<br />
tiempo (segs.) 0.00 1.27 1.87 1.45<br />
Arreglo aleatorio<br />
fusión selección intercambio inserción<br />
comparaciones 123 582 49 995 000 49 995 000 25 023 757<br />
asignaciones 140 000 103 894 75 041 274 25 043 755<br />
tiempo (segs.) 0.00 1.10 1.75 0.72<br />
Cuadro 10.1: Comparación de algoritmos de clasificación, para arreglos enteros<br />
de longitud 10000.<br />
Problema 10.10 (Clasificación por conteo). La clasificación es muy sencilla<br />
si sabemos de antemano que el rango de los elementos de a =(a1,a2,...,an) es<br />
pequeño, digamos 1 ≤ ai ≤ m para i =1, 2,...,n.<br />
En efecto, imaginemos que tenemos n bolitas alineadas cada una con un<br />
númeroentre1ym: armamos m cajas numeradas de 1 a m, luego colocamos<br />
cadabolitaenlacajaquetienesunúmero, y finalmente será cuestión de vaciar<br />
los contenidos de las cajas ordenadamente, empezando desde la primera, alineando<br />
las bolitas a medida que las sacamos, como ilustramos en la figura 10.1.<br />
① ③ ③ ② ① ② ① ② ① ③<br />
disposición inicial<br />
①<br />
① ② ③<br />
① ② ③<br />
① ② ③<br />
1 2 3<br />
poniendo en cajas<br />
① ① ① ① ② ② ② ③ ③ ③<br />
disposición final<br />
Figura 10.1: Ordenando por conteo.<br />
En el algoritmo, en vez de “colocar cada bolita en su caja”, contamos las<br />
veces que apareció:
Pág. 104 Búsqueda y clasificación<br />
for k := 1 to m do cuenta[k] := 0;<br />
for i := 1 to n do cuenta[a[i]] := cuenta[a[i]] + 1;<br />
i := 0;<br />
for k := 1 to m do<br />
while (cuenta[k] > 0) do begin<br />
i := i + 1; a[i] := k; cuenta[k] := cuenta[k] - 1 end<br />
a) Ver que el algoritmo es correcto, i.e. al finalizar el arreglo a está ordenado,<br />
aún cuando haya valores r, 1≤ r ≤ m tales que ai = r para todo i.<br />
b) Implementar el algoritmo y comparar su eficiencia con los otros métodos,<br />
poniendo contadores para el número de asignaciones y comparaciones.<br />
c) ¿A lo sumo cuántas asignaciones de arreglos se hacen en este algoritmo (la<br />
respuesta involucra n y m)? ¿Cuándo lo usarías? ✄<br />
Problema 10.11 (Arreglos como conjuntos). Cuando queremos tratar a<br />
un arreglo a =(a1,a2,...,an) como un conjunto, es decir no nos interesan los<br />
elementos repetidos ni el orden, es conveniente primero ordenarlo (de menor a<br />
mayor) y quitar sus elementos repetidos, por ejemplo con el procedimiento del<br />
problema 10.3, para facilitar las operaciones entre conjuntos.<br />
a) Hacer un procedimiento, que a su vez usa un procedimiento de clasificar<br />
y otro de purgar, para “pasar el arreglo a a conjunto” ordenándolo<br />
y “purgándolo”.<br />
✎ Aunque se puede hacer un único procedimiento modificando un procedimiento<br />
de clasificación elemental de modo de eliminar elementos<br />
repetidos, es más sencillo, y desde el punto de vista de la modularidad<br />
más razonable, el llamar a dos procedimientos separados, uno para<br />
clasificar y otro para “purgar”. Así, se puede cambiar, por ejemplo, el<br />
procedimiento de clasificación por uno más conveniente sin tener que<br />
revisar todo el procedimiento de este inciso.<br />
Además, con métodos más avanzados es más eficiente primero clasificar<br />
y luego “purgar” que al revés.<br />
b) Dados dos arreglos a =(a1,a2,...,an) yb =(b1,b2,...,bm), considerados<br />
como conjuntos, hacer un procedimiento para construir el “conjunto intersección”,<br />
i.e. un arreglo ordenado c =(c1,c2,...,cr) con todos los elementos<br />
que están en a y b simultáneamente (pero aparecen en c una sola vez).<br />
Sugerencia: suponiendo que a =(a1,a2,...,an) yb =(b1,b2,...,bm) ya<br />
están ordenados y purgados, una posibilidad es considerar el esquema<br />
i := 1; j := 1; r := 0;<br />
repeat<br />
if (a[i] > b[j]) then j := j + 1<br />
else if (a[i] < b[j]) then i := i + 1<br />
else begin (* a[i] = b[j]: ponerlo en c *)<br />
r := r + 1;<br />
c[r] := a[i];<br />
i := i + 1;<br />
j := j + 1<br />
end<br />
until ((i > n) or (j > m))<br />
Cuando r = 0, los arreglos originales no tienen elementos comunes.
10.4. Registros (Records) Pág. 105<br />
c) Del mismo modo, hacer un procedimiento para construir el “conjunto unión”,<br />
i.e. un arreglo ordenado c =(c1,c2,...,cs) con todos los elementos<br />
que están en a o b (pero aparecen en c una sola vez).<br />
Sugerencia: podría ponerse un arreglo a continuación del otro, haciendo<br />
for i := 1 to n do c[i] := a[i];<br />
for i := 1 to m do c[i+n] := b[i];<br />
y luego “purgar” el arreglo c que tiene longitud m + n.<br />
Otra posibilidad es, si a y b ya están ordenados y clasificados, usar la<br />
“fusión” o “mezcla” del problema 10.4. ✄<br />
10.4. Registros (Records)<br />
En aplicaciones informáticas, y aún en algunas matemáticas como veremos,<br />
es conveniente agrupar la información que tenemos sobre un objeto. Por ejemplo,<br />
para una persona podríamos tener su apellido, nombres, número de identidad,<br />
etc. Una forma de hacerlo es mediante arreglos paralelos, e.g. un arreglo para<br />
los apellidos, otro para los nombres, etc., donde una persona tendría el mismo<br />
índice para cada uno de los arreglos. Otra forma más razonable es el uso de<br />
registros.<br />
Un registro es una estructura que consiste en un número fijo de componentes<br />
llamadas campos. A diferencia del arreglo, las componentes de un registro pueden<br />
ser de diferentes tipos, pero no pueden ser indexados.<br />
Al definir un tipo de registro, se especifica el tipo de cada componente y su<br />
identificador. Por ejemplo, si que queremos trabajar con números complejos de<br />
la forma a + bi,cona, b ∈ R, podemosponer:<br />
type complejo = record re, im: real end;<br />
var z: complejo;<br />
✎ Observar que la declaración del registro termina con “ end ”, pero no hay<br />
un “ begin ”.<br />
El registro ocupa un lugar de memoria donde los campos son consecutivos,<br />
de modo que podemos pensar que el complejo z = a + bi se guarda en una caja<br />
como se muestra en la figura 10.2. Enelejemplo,cada“sub-caja”ocampoes<br />
de tipo real.<br />
a b<br />
re im<br />
Figura 10.2: Esquema del registro de tipo “ complejo ”enmemoria.<br />
Para acceder a una componente del registro, el nombre del registro es seguido<br />
por un punto (“ . ”) y el correspondiente identificador del campo. Así, si<br />
quisiéramos asignar a z el valor 3 + 5 i, pondríamos<br />
z.re := 3; z.im := 5;<br />
Si z ′ es otro número complejo, podemos hacer la asignación<br />
zp := z;
Pág. 106 Búsqueda y clasificación<br />
mientras que la suma z ′′ = z + z ′ puede expresarse como:<br />
zpp.re := z.re + zp.re; zpp.im := z.im + zp.im;<br />
Las dos operaciones válidas para variables de registro<br />
(completas) son la selección de componentes y la<br />
asignación.<br />
Es importante destacar también la localidad de los nombres de los campos<br />
de un registro. La declaración<br />
var a: array[2..8] of integer;<br />
a: real;<br />
es incorrecta, pero la declaración<br />
var a: array[2..8] of integer;<br />
b: record<br />
a: real;<br />
b: boolean<br />
end;<br />
es correcta, pues “ b.a ” indica la componente “ a ”de“b ”, mientras que “ a ”<br />
indica un arreglo.<br />
Cuando trabajamos con un registro en Pascal, es posible acceder directamente<br />
a sus componentes mediante la sentencia “ with ” (en castellano, con).<br />
Por ejemplo, para asignar a z el valor 3 + 5 i, podríamos poner:<br />
with z do begin re := 3; im := 5 end;<br />
Problema 10.12. Suponiendo que declaramos el tipo “ complejo” comomás<br />
arriba:<br />
a) Hacer procedimientos para leer y escribir un complejo por terminal.<br />
b) Recordando que el producto de complejos se define como<br />
(a + bi) × (c + di)=(ac − bd)+(ad + bc) i,<br />
hacer un programa para calcular la suma y el producto de dos complejos<br />
entrados por terminal. ✄<br />
En los dos siguientes problemas, suponemos que el compilador admite el tipo<br />
“ string ”.<br />
Problema 10.13. Supongamos que queremos trabajar con un arreglo de registros,<br />
en cada uno de los cuales se guarda un nombre y un número de identificación.<br />
Declaramos<br />
const MAXN = 20;<br />
type<br />
tipoinfo = record nombre: string; nroid: integer end;<br />
tipoarreglo = array[0..MAXN] of tipoinfo;
10.4. Registros (Records) Pág. 107<br />
Una variante del procedimiento leerarreglo (capítulo 8, pág. 82), para leer<br />
datos es:<br />
procedure leerdatos(var a: tipoarreglo; var n: integer);<br />
function nuevodato: boolean;<br />
begin<br />
if (n > MAXN) then nuevodato := false<br />
else begin<br />
writeln;<br />
write(’Entrar el dato ’, n:2);<br />
writeln(’ (fin = ): ’);<br />
write(’ Entrar el nombre: ’);<br />
if (eoln) then begin readln; nuevodato := false end<br />
else begin<br />
with a[n] do begin<br />
readln(nombre);<br />
write(’ Entrar el numero de id: ’);<br />
readln(nroid)<br />
end;<br />
nuevodato := true<br />
end<br />
end<br />
end;<br />
begin<br />
writeln(’** Entrada de datos’);<br />
n := 1;<br />
while (nuevodato) do n := n + 1;<br />
n := n - 1<br />
end;<br />
Hacer un procedimiento para escribir arreglos de este tipo, y hacer un programa<br />
incorporando ambos procedimientos para probarlos. ✄<br />
Problema 10.14 (Clasificación de arreglos de registros). Supongamos<br />
que hemos hecho las declaraciones del problema 10.13, y queremos clasificar el<br />
arreglo a de tipo “ tipoarreglo”, que puede ser según nombre osegún nroid.<br />
La componente por la cual se clasifica se denomina llave o clave (en inglés<br />
“key”). Por ejemplo si tenemos el registro x con el nombre “ Geri ”yelnúmero<br />
de identidad “ 5 ”, y el registro y con el nombre “ Ana ”ynúmero “ 10 ”, ordenándolos<br />
(de menor a mayor) por nombre vendrá primero y antes que x, pero<br />
si los ordenamos por nroid vendrá primero x.<br />
✎ La comparación de strings se hace como la comparación entre números<br />
(en Pascal), e.g. “ s1 y.z ” donde z puede ser nombre o nroid según el valor de llave.
Pág. 108 Búsqueda y clasificación<br />
b) Modificar alguno de los métodos de clasificación vistos, de modo de poder<br />
clasificar un arreglo de registros, cambiando la comparación entre datos de<br />
la forma x>ypor “ mayor(x, y, llave) ”.<br />
c) Hacer un programa para ingresar el arreglo a y clasificarlo según nombre o<br />
nroid a elección del usuario, escribiendo por pantalla el resultado. ✄<br />
Problema 10.15. Queremos escribir un programa que tome como entradas<br />
caracteres (un carácter por línea y fin de entrada con sin datos),<br />
y como salida escriba los caracteres ingresados y la cantidad de apariciones,<br />
ordenados decrecientemente según la cantidad de apariciones.<br />
Por ejemplo, si la entrada son los caracteres ‘ a ’, ‘ b ’, ‘ c ’, ‘ c ’, ‘ d ’, ‘ b ’, ‘ c ’,<br />
‘ d ’, ‘ c ’, ‘ b ’, ‘ b ’, ‘ c ’, (uno por línea), la salida debe ser algo como:<br />
Caracter Cantidad de apariciones<br />
c 5<br />
b 4<br />
d 2<br />
a 1<br />
Para esta tarea vamos a construir los tipos de datos<br />
type<br />
tipoinfo = record letra: char; cuenta: integer end;<br />
tipoarreglo = array[1..MAXN] of tipoinfo;<br />
A medida que se ingresa una letra la comparamos con las letras ya ingresadas<br />
mediante búsqueda lineal (eventualmente con centinela al final). Si la letra ya<br />
apareció, se incrementa en 1 el campo cuenta correspondiente, en otro caso, se<br />
agrega al arreglo un nuevo registro con la nueva letra, poniendo cuenta =1.<br />
Luego se puede ordenar el arreglo de registros según el campo veces,enforma<br />
similar a la del problema 10.14.<br />
Hacer un programa con estas ideas. ✄<br />
10.5. Problemas Adicionales<br />
Problema 10.16. Hacer un programa para un juego como el del regalo en las<br />
cajas (problema 10.8), pero ahora jugando en un tablero de m × n. El jugador<br />
da las coordenadas (i, j), 1 ≤ i ≤ m, 1≤ j ≤ n, de la casilla que elige y la<br />
búsqueda se orienta dando direcciones hacia donde está el regalo: N, NE, E, SE,<br />
S, SO, O, NO.<br />
Recordando el problema del regalo en las cajas, ¿cuántas oportunidades<br />
habrá que dar en el caso de las m × n casillas? ✄<br />
Problema 10.17. Otra variante del juego de las cajas (problema 10.8) esla<br />
siguiente: Dado un número (entero) entre 1 y 100 (generado aleatoriamente),<br />
tratar de encontrarlo mediante “encajonamiento”, o sea vamos dando números a<br />
y b (a ≤ b) y la computadora nos va respondiendo si el número está a la izquierda<br />
de a, “atrapado” entre a y b, oaladerechadeb. Elnúmero será encontrado<br />
cuando a y b coincidan con el número a acertar, terminando el juego. Hacer un<br />
programa para este juego, y pensar una estrategia para encontrar el número en<br />
“pocas” jugadas. ✄
10.6. Comentarios Bibliográficos Pág. 109<br />
Problema 10.18 (Estabilidad para clasificación). Un método de clasificación<br />
se llama estable si el orden relativo de elementos con llaves iguales<br />
permanece inalterado en el proceso de clasificación.<br />
a) Decidir si el método de clasificación usado en el programa en el problema<br />
10.14 es estable o no, e.g. si ordenando primero por nroid ydespués<br />
por nombre, personas con el mismo nombre aparecen en orden creciente de<br />
nroid.<br />
b) Decidir cuáles de los métodos elementales de clasificación que hemos visto<br />
(inserción directa, selección directa, intercambio directo) son estables. ✄<br />
10.6. Comentarios Bibliográficos<br />
Los temas de búsqueda (con centinela y binaria) y clasificación están tomados<br />
de [16] como mencionamos. Partes de la sección 10.4 están basadas en [7,Cap.7].
Capítulo 11<br />
Recursión<br />
Teniendo en mente las sumas de Gauss (problema 4.13), supongamos que<br />
queremos encontrar todas las sumas<br />
s1 =1, s2 =1+2, s3 =1+2+3, ... sn =1+2+···+ n. (11.1)<br />
Podríamos calcular cada una de ellas separadamente, por ejemplo usando la<br />
fórmula de Gauss an = n × (n +1)/2, pero también podríamos poner<br />
s1 =1, s2 = s1 +2, s3 = s2 +3, ... sn = sn−1 + n. (11.2)<br />
Claro que no habría mucha diferencia con el esquema que vimos en el problema<br />
4.13, en el que cambiando la variable suma por un arreglo tendríamos<br />
suma[0] := 0; for i := 1 to n do suma[i] := suma[i-1] + i<br />
calculando en cada paso del lazo “ for ”lacantidadsi.<br />
Cambiando suma por producto en las ecuaciones (11.1) o(11.2), obtenemos<br />
el factorial (recordar el problema 4.14), y en realidad es usual definir n! como<br />
1! = 1 y n! =n × (n − 1)! para n ∈ N, n>1. (11.3)<br />
Esto es típico de inducción en matemáticas o de recursión en programación:<br />
dar un valor inicial y una “fórmula” para calcular los valores subsiguientes.<br />
Por ejemplo, si queremos calcular 5! usando la ecuación (11.3), tendríamos<br />
sucesivamente:<br />
5! = 5 × 4! = 5 × (4 × 3!) = 5 × 4 × (3 × 2!) =<br />
=5× 4 × 3 × (2 × 1!) = 5 × 4 × 3 × 2 × 1 = 120.<br />
En fin, ya hemos visto este esquema anteriormente, por ejemplo al calcular la<br />
función cm = saldo(c, r, p, m) enelproblema7.5 según la ecuación (7.1), donde<br />
c0 = c.<br />
11.1. Funciones y procedimientos definidos recursivamente<br />
En los problemas siguientes vamos a encontrar varios ejemplos que anteriormente<br />
resolvíamos con un lazo “ for ” o similar, ahora reescritos en forma<br />
recursiva.
11.1. Funciones y procedimientos definidos recursivamente Pág. 111<br />
Problema 11.1 (Factorial recursivo). El siguiente esquema muestra una<br />
función recursiva, i.e. que se llama a sí misma, para calcular n!, basado en la<br />
ecuación (11.3):<br />
function factorial(n: integer): integer;<br />
begin<br />
if (n > 1) then factorial := n * factorial(n - 1)<br />
else factorial := 1<br />
end;<br />
✎ Observar que la variable “sobre la que se hace la recursión”, en este caso<br />
n, no tiene antepuesta la palabra “ var ”enladefiniciónde la función. No<br />
tiene sentido hacerlo, pues cuando llamamos factorial(n−1), n−1 notiene<br />
asignado un lugar (aunque lo tenga n): n − 1 tiene que pasarse por valor y<br />
no referencia.<br />
✎ Ahora podemos apreciar un poco más por qué la función (o procedimiento)<br />
no es una variable local a la función (recordar problema 7.2.d)).<br />
a) Hacer un programa para calcular n! usando la función anterior.<br />
b) Obtener el máximo valor de n para el cual se puede calcular n! (i.e. n! ≤<br />
maxint) enlaversióndel inciso a).<br />
En vista de este resultado, es conveniente cambiar el tipo de los valores<br />
retornados por factorial de “ integer ”a“real ” en la función en a) (como<br />
hemos hecho en los problemas 4.13 y 4.14).<br />
c) Lafórmula de Stirling establece que cuando n es bastante grande,<br />
n! ≈ n n e −n√ 2πn.<br />
Hacer un programa para calcular esta aproximación, y probarla con n =<br />
10, 100, 1000. Comparar con los resultados obtenidos en a) (habiendo hecho<br />
las modificaciones indicadas en la nota del inciso b)). ✄<br />
Expliquemos un poco cómo funciona recursión.<br />
Una función o procedimiento que no usa recursión(nosellamaasímisma),<br />
ocupa un lugar en la memoria al momento de correr el programa. Ese<br />
lugar contiene las “cajas” correspondientes a sus variables locales, y también<br />
las instrucciones que debe seguir (recordar la figura 7.2, pág. 75).<br />
Cuando la función o procedimiento se llama a sí misma, podemos pensar<br />
que al ejecutarse el programa se generan automáticamente copias de la función<br />
(tantas como sean necesarias), cada copia con sus propios lugares para código<br />
y variables locales. Cuando la copia de la función o procedimiento que ha sido<br />
llamada por la recursión termina su tarea, el espacio es liberado. Este espacio de<br />
memoria usado por recursión no está reservado por el programa en el momento<br />
de compilar (como sucede con los arreglos y las funciones declaradas), pues no<br />
puede saber de antemano cuántas veces se usará la recursión. Por ejemplo, para<br />
calcular n!, se necesitan unas n copias de la función: cambiando n cambiamos el<br />
número de copias necesarias. Este espacio de memoria especial se llama “stack”<br />
o “pila”, estructura que veremos también en el problema 11.5 yunpocomás<br />
formalmente en la sección 12.1.<br />
✎ Un resultado de computación teórica dice que toda función o procedimiento<br />
recursiva puede reescribirse sin usar recursión con lazos “ while ” y arreglos<br />
(que se usan como “pilas” para ir guardando los datos intermedios).
Pág. 112 Recursión<br />
Problema 11.2. Rehacer el problema 7.5, cambiando la definición de la función<br />
saldo a una recursiva, según la ecuación (7.1). ✄<br />
La recursión también puede usarse en procedimientos:<br />
Problema 11.3. Hacer un programa implementando la clasificación por selección<br />
directa (pág. 101) como un procedimiento recursivo, declarando el procedimiento<br />
como “ seleccion(var a: arreglo; n: integer) ” y haciendo recursión<br />
en n, colocando el mayor elemento en la posición final antes de llamar al<br />
procedimiento con n − 1(vertambién el problema 8.2.e)). ✄<br />
La recursión se puede usar en más de un argumento, i.e. no siempre se “llama<br />
a la función con n − 1” (ver también el problema 11.7):<br />
Problema 11.4. Para m, n ∈ N, consideremos una cuadrícula rectangular de<br />
dimensiones m × n (4 × 3enlafigura11.1), e imaginémosnos que se trata<br />
de un mapa, donde los segmentos son calles y los puntos remarcados son las<br />
intersecciones.<br />
1 4 10 20 35<br />
1<br />
1<br />
3<br />
2<br />
6<br />
3<br />
10<br />
1 1 1 1<br />
Figura 11.1: Contando la cantidad de caminos posibles.<br />
Nos preguntamos de cuántas maneras podremos ir desde la esquina más<br />
hacia el sudoeste, de coordenadas (0, 0), a la esquina más hacia el noreste, de<br />
coordenadas (m, n), si estamos limitados a recorrer las calles únicamente en<br />
sentido oeste–este o sur–norte, según corresponda.<br />
Para resolver el problema, podemos pensar que para llegar a una intersección<br />
hay que hacerlo desde el oeste o desde el sur (salvo cuando la intersección estáen<br />
el borde oeste o sur), y por lo tanto la cantidad de caminos para llegar a la<br />
intersección es la suma de la cantidad de caminos llegando desde el oeste (si<br />
se puede) más la cantidad de caminos llegando desde el sur (si se puede). Los<br />
números en la figura 11.1 indican, para cada intersección, la cantidad de caminos<br />
para llegar allí desde(0, 0) mediante movimientos permitidos.<br />
a) Hacer un programa para calcular la cantidad h(m, n) de caminos para llegar<br />
desde (0, 0) a (m, n), donde m y n son ingresados por el usuario.<br />
b) En cursos de matemática discreta se demuestra que<br />
h(m, n) =<br />
(m + n)!<br />
m! n!<br />
4<br />
(m + n) × (m + n − 1) ×···×(m +1)<br />
= .<br />
n × (n − 1) ×···×1<br />
Incorporar al programa del inciso anterior una función con el cálculo de<br />
h(m, n) (por ejemplo, con un lazo) y comparar con el obtenido anteriormente.<br />
15<br />
5
11.2. Los Grandes Clásicos de la Recursión Pág. 113<br />
c) Modificar el programa del inciso a) demododecalcularlacantidadde<br />
caminos cuando la intersección (r, s) estábloqueada y no se puede pasar<br />
por allí, donde r y s son ingresados por el usuario (0
Pág. 114 Recursión<br />
Tapa del juego original Fotografía del juego<br />
Figura 11.2: Las torres de Hanoi.<br />
type aguja = array[0..10] of integer<br />
y declarar, por ejemplo, var a, b, c: aguja. Usamos la posición 0 para guardar<br />
la cantidad de discos en la aguja (e.g a0 = na), y las siguientes posiciones,<br />
(a1,a2,...,ana), para indicar que en la posición 1 (la de más abajo) de la aguja a<br />
estáeldiscoa1, enla2eldiscoa2, etc. Inicialmente será a =(n, n, n−1,...,2, 1)<br />
(de mayor a menor), b =(0,...)yc =(0,...).<br />
Si hay n discos en la aguja a, y ninguno en la agujas b y c, para poner el<br />
disco más grande en la aguja b, habráque poner los n − 1 restantes en la aguja<br />
c primero, después colocar la n-ésima en la aguja b, y volver a poner los n − 1<br />
en la aguja b. Paramoverlosn−1delaaguja a a la aguja c, habráquemover<br />
n − 2 a la aguja b, pasar el n − 1alaagujac,...<br />
a) Definir un procedimiento pasar (k, x, y, z) que “pase” k discos de la aguja<br />
x alay usando la z (2) , imprimiendo los discos que hay en cada aguja al<br />
terminar.<br />
Sugerencia: parak =1sólo hay que mover la aguja que está enlacimade<br />
x hacia y, yparak>1, podemos poner pasar (k, x, y, z) como consistente<br />
de los siguientes pasos:<br />
pasar(k-1,x,z,y); pasar(1,x,y,z); pasar(k-1,z,y,x)<br />
Sugerencia si la anterior no alcanza:<br />
procedure pasar( k: integer; var x, y, z: aguja);<br />
(* pasar k discos de x a y, usando z *)<br />
begin<br />
if (k > 1) then begin<br />
pasar(k-1, x, z, y);<br />
pasar(1, x, y, z);<br />
pasar(k-1, z, y, x)<br />
end<br />
else begin (* pasar una de x a y *)<br />
y[0] := y[0] + 1; y[y[0]] := x[x[0]]; x[0] := x[0] - 1;<br />
imprimir<br />
end<br />
end;<br />
donde hemos supuesto un procedimiento imprimir que imprime los elemen-<br />
(2) Con movimientos permitidos: recordar que el disco a pasar de una aguja a otra debe ser<br />
menor que cualquier otro disco en esas agujas.
11.2. Los Grandes Clásicos de la Recursión Pág. 115<br />
tos que tiene cada arreglo (hasta la longitud correspondiente). Observar que<br />
k se pasa por valor, pero x, y y z se pasan por referencia: los arreglos pueden<br />
modificarse en el procedimiento.<br />
b) Usar el procedimiento del inciso anterior en un programa que imprima una<br />
sucesión de movimientos para transferir n discos de una aguja a otra. Verificar<br />
el programa para n =1, 2, 3, 4, 5.<br />
c) Agregar un contador para contar la cantidad de veces que se transfiere un<br />
disco de una aguja a otra (en pasar ), e imprimirlo al terminar el programa.<br />
En base a este resultado (para n =1, 2, 3, 4, 5) conjeturar la cantidad<br />
de movimientos necesarios para transferir n discos de una aguja a otra, y<br />
demostrarlo.<br />
Sugerencia: 2 n − 1=1+2+4+···+2 n−1 =1+2(1+2+···+2 n−2 )=<br />
1+2(2 n−1 − 1).<br />
d) Suponiendo que transfieren un disco por segundo, ¿cuánto tardarán los monjes<br />
en transferir los 64 discos? ¿Cuántos años tardaría una computadora en<br />
calcular la solución para n = 64, suponiendo que tarda un nanosegundo por<br />
movimiento (3) (nano = dividir por mil millones)? Bajo la misma suposición<br />
sobre la velocidad de la computadora, ¿cuál es el valor máximo de n para<br />
calcular los movimientos en 1 minuto?<br />
✎ La suposición que lo que tarda la computadora por movimiento es<br />
independiente de n es una aproximación muy buena. Si es posible,<br />
comprobarla (tomando por ejemplo n =19, 20,...), sin imprimir los<br />
movimientos, y estimar lo que tarda la computadora por movimiento<br />
en cada caso (del orden de micro segundo, micro = dividir por un<br />
millón).<br />
✎ En el problema 11.9 se da una variante para resolver el problema de las<br />
torres de Hanoi sin usar arreglos.<br />
✎ La estructura que estamos usando para los arreglos, agregando atrás y<br />
sacando también desde atrás, es un ejemplo de pila (como la de platos),<br />
sobre la que hablaremos más en la sección 12.1.<br />
Hay muchas variantes del problema de las torres de Hanoi, por ejemplo:<br />
¿qué pasa si los discos no están inicialmente todos sobre una misma aguja<br />
(pero respetan la distribución de mayor a menor)?, ¿qué pasa si hay más de<br />
tres agujas? ✄<br />
Problema 11.6 (Números de Fibonacci). En 1202 Fibonacci propuso el<br />
siguiente problema en su libro Liber Abaci:<br />
¿Cuántos pares de conejos se producen a partir de una única pareja<br />
en un año si cada mes cada par de conejos da nacimiento a un nuevo<br />
par, el que después del segundo mes se reproduce, y no hay muertes?<br />
a) Resolver el problema.<br />
Los números que aparecen en la solución de este problema se conocen como<br />
números de Fibonacci, definidos recursivamente como:<br />
f1 =1, f2 =1, y fn = fn−1 + fn−2 para n ≥ 3.<br />
(3) ¡Y que no hay cortes de luz!
Pág. 116 Recursión<br />
b) Hacer un programa para calcular los números de Fibonacci:<br />
i) Usando recursión sobre dos parámetros con dos condiciones iniciales.<br />
ii) Sin usar recursión. Sugerencia: un esquema posible como función es<br />
a := 1; b := 1;<br />
for i := 3 to n do begin c := a + b; b := a; a := c end;<br />
fibonacci := a<br />
c) La fórmula de Euler-Binet establece que<br />
2n √ . (11.4)<br />
5<br />
Créase o no, el miembro derecho es un número entero (¿podrías decir por<br />
qué, si no supieras el resultado?).<br />
Implementar el miembro derecho de esta igualdad como función y comparar<br />
con los resultados del inciso anterior.<br />
d) Según la fórmula 11.4, fn crece exponencialmente, porloquerápidamente<br />
toma valores muy grandes, y al implementarse como función debe retornar<br />
valores de tipo “ real ” (como en el caso de n!). Encontrar el máximo n tal<br />
que fn ≤ maxint.<br />
fn = (1 + √ 5) n − (1 − √ 5) n<br />
Leonardo Bigollo es el verdadero nombre de Leonardo de Pisa (1180–1250),<br />
también conocido como Fibonacci (contracción de las palabras “hijo de Bonacci”).<br />
Además de “Liber Abaci”, publicó numerosos tratados sobre teoría de<br />
números, geometría y la relación entre ellos. Sin embargo, fue prácticamente<br />
ignorado durante la Edad Media, apareciendo sus resultados nuevamente publicados<br />
(por Maurico) unos 300 años después.<br />
Jacques Binet (1786–1856) publicó lafórmula para los números de Fibonacci<br />
en 1843, pero ya había sido publicada por Leonhard Euler (1707–1783)<br />
en 1765. Sin embargo, aún antes A. de Moivre (1667–1754) la había publicado<br />
(y en forma más general) en 1730.<br />
Mucho después de Fibonacci, se observó quelosnúmeros fn aparecen en<br />
muy diversos contextos, algunos insospechados como en la forma de las flores<br />
del girasol, y son de importancia tanto en las aplicaciones prácticas como<br />
teóricas. Por ejemplo, han sido usados para resolver problemas de confiabilidad<br />
de comunicaciones.<br />
En cuanto a aplicaciones teóricas, vale la pena mencionar que en el Congreso<br />
Internacional de Matemáticas de 1900, David Hilbert propuso una serie<br />
de problemas que consideró de importancia para resolver durante el siglo XX.<br />
Entre ellos, el décimo pide encontrar un algoritmo para determinar si un polinomio<br />
con coeficientes enteros, arbitrariamente prescripto, tiene raíces enteras<br />
(resolver la ecuación diofántica asociada). Recién en 1970 pudo obtenerse una<br />
respuesta al problema, cuando el matemático ruso Yuri Matijasevich (quien<br />
tenía 22 años) demostró que el problema es irresoluble, es decir, no existe algoritmo<br />
que pueda determinar si una ecuación diofántica polinomial arbitraria<br />
tiene soluciones enteras. En su demostración, Matijasevich usa la tasa de crecimiento<br />
de los números de Fibonacci. ✄<br />
11.3. Problemas Adicionales<br />
Problema 11.7. Usando recursión, reescribir el algoritmo de Euclides (sección<br />
5.2.2) para encontrar el máximo común divisor entre dos enteros positivos.<br />
Sugerencia: mcd(a, b) =mcd(a − b, b) sia>b. ✄
11.3. Problemas Adicionales Pág. 117<br />
Problema 11.8. En este problema queremos encontrar los factores primos de<br />
n ∈ N usando recursión.<br />
a) Podemos probar si 2 divide a n y en caso afirmativo probar nuevamente si<br />
2 divide a n/2, o bien probar con 3, y así sucesivamente, imprimiendo cada<br />
factor encontrado:<br />
procedure factorizar(n, k: integer);<br />
begin<br />
if (n > 1) then<br />
if ((n mod k) = 0) then begin<br />
write(k:10);<br />
factorizar(n div k, k)<br />
end<br />
else factorizar(n, k+1)<br />
end;<br />
haciendo la llamada “ factorizar(n,2)” enlaparteprincipal.<br />
✎ Refiriéndonos a comentarios anteriores, observar que en el procedimiento<br />
n y k son llamados por valor y no referencia.<br />
i) Ver que el procedimiento es correcto y que, en particular, los números<br />
que se imprimen son siempre primos.<br />
ii) Hacer un programa para escribir los factores de n ∈ N, n>1, usando<br />
este procedimiento.<br />
b) Recordando los problemas 5.19 y 5.20, ver que se puede mejorar el procedimiento<br />
del inciso anterior a:<br />
procedure factorizar(n, inic: integer);<br />
var x: real;<br />
begin<br />
x := sqrt(n);<br />
while ((inic x) then writeln(n:10) (* n es primo *)<br />
else begin<br />
write(inic:10);<br />
factorizar(n div inic, inic)<br />
end<br />
end; ✄<br />
Problema 11.9 (Torres de Hanoi sin arreglos). Flor, que siempre le lleva<br />
la contra al profe y está compitiendo con él, hizo un programa para resolver el<br />
problema de las torres de Hanoi sin usar arreglos. Más aún, ¡cambió arreglos<br />
por caracteres!:<br />
program hanoisinarreglos(input, output);<br />
(*<br />
Solucion recursiva de las torres de Hanoi,<br />
sin usar arreglos.<br />
*)<br />
type aguja = char;
Pág. 118 Recursión<br />
var n: integer;<br />
procedure pasar( n: integer; x, y, z: aguja);<br />
(* pasar n discos de x a y usando z *)<br />
begin<br />
if (n > 1) then begin<br />
pasar(n-1, x, z, y);<br />
write(’pasar el disco ’, n:1);<br />
writeln(’ de "’, x, ’" a "’, y,’"’);<br />
pasar(n-1, z, y, x)<br />
end<br />
else begin<br />
write(’pasar el disco 1’);<br />
writeln(’ de "’, x, ’" a "’, y,’"’)<br />
end<br />
end;<br />
begin<br />
writeln(’** Problema de las torres de Hanoi:’);<br />
writeln(’ Pasar n discos de la aguja "a" a la "b"’);<br />
write(’ usando la "c", mediante movimientos ’);<br />
writeln(’ permitidos.’);<br />
writeln;<br />
write(’ Entrar el numero de discos: ’); readln(n);<br />
writeln;<br />
pasar(n, ’a’, ’b’, ’c’);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
¿Es correcto el programa de Flor? ¿Por qué? ✄<br />
Problema 11.10. Recordando la fórmula de Euler-Binet (11.4), comparar el<br />
número de Fibonacci fn con el redondeo (“ round ”) de<br />
(1 + √ 5) n<br />
2n √ .<br />
5<br />
¿A partir de qué n son iguales?, ¿podrías decir por qué? ✄<br />
11.4. Comentarios Bibliográficos<br />
y<br />
Las fotografías en la figura 11.2 están tomadas de<br />
http://www.cs.wm.edu/~pkstoc/<br />
http://en.wikipedia.org/wiki/Tower of Hanoi<br />
respectivamente.
Capítulo 12<br />
Generando objetos<br />
combinatorios<br />
Muchos de los problemas que vimos en el capítulo anterior se pueden reescribir<br />
sin recursión con lazos “ for ”, como el factorial, selección directa, los<br />
números de Fibonacci, o aún la cantidad de caminos del problema 11.4, ynos<br />
quedamos con la impresión de que recursión no es muy útil. Otro problema muy<br />
distinto al de contar objetos es generarlos, por ejemplo para encontrar alguno o<br />
todos los que satisfacen cierto criterio, y aquí es donde recursión muestra toda<br />
su potencia.<br />
Puesto que el número de objetos a generar puede ser muy grande, como<br />
2 n o n!, es conveniente no tener —por ejemplo— un arreglo para cada uno de<br />
ellos, sino tener uno solo que se va modificando a medida que vamos creando<br />
los objetos. Una estructura particularmente útil para esta modificación es la de<br />
“pila”, con la que comenzamos.<br />
12.1. Pilas y colas<br />
Las pilas, como las de platos, y las colas, como las de supermercados, son dos<br />
estructuras familiares que trataremos de formalizar un poco aquí, más que nada<br />
para poner en perspectiva lo que estamos haciendo y no siendo estrictamente<br />
rigurosos.<br />
Tanto en pilas como en colas podemos pensar que tenemos una serie de<br />
objetos —todos del mismo tipo— esperando en línea a ser tratados. En el caso<br />
de la pila, o cola lifo, por “last in first out”, el último en llegar es el primero en<br />
ser tratado. En cambio en lo que denominamos comúnmente cola (sin aditivos),<br />
omás formalmente cola fifo, por “first in first out”, el primero en llegar es el<br />
primero en ser tratado.<br />
No es difícil imaginar distintas acciones comunes:<br />
Crear o inicializar la cola, construyendo la cola “vacía”.<br />
Agregar un elemento, ubicándolo al final y actualizando la cola.<br />
Quitar un elemento, tomando, según corresponda, el primero o el último<br />
y actualizando la cola.
Pág. 120 Generando objetos combinatorios<br />
Destruir la cola, cuando no la necesitamos más, limpiando lo que ensuciamos.<br />
En fin, podríamos agregar otras, como averiguar el estado de la cola (si<br />
está vacía o llena).<br />
Es tradicional en computación llamar con distintos nombres a estas acciones,<br />
según se trate de una pila o cola. Por ejemplo, agregar el elemento a en una pila<br />
se llama push yenlacolaput. En cambio, el quitar un elemento se llama pop<br />
para pilas y get para colas.<br />
No es casualidad que put y get sean nombres en Pascal de funciones o procedimientos<br />
para entrada o salida (que no cubriremos, siendo más “primitivas”<br />
que read y write). Efectivamente, la entrada y salida en la computadora se<br />
implementan como colas fifo: los datos se leen o escriben en el orden en que<br />
van surgiendo. Dado que put y get tienen significados especiales en Pascal, es<br />
conveniente evitar estos nombres.<br />
El concepto de cola, que tiene muchísimas generalizaciones, constituye un<br />
tipo de datos abstracto (TDA): no importa tanto cómo se va a implementar o<br />
representar en la máquina, sino saber que contamos con las operaciones de crear<br />
o destruir la cola, agregarle o quitarle un elemento, etc.<br />
Sin embargo, la implementación de una cola como “fifo” o “lifo” puede tener<br />
consecuencias decisivas en los algoritmos, como veremos en el capítulo 13.<br />
Además, en lenguajes no tan abstractos como Pascal, debemos tener cuidado en<br />
la implementación de TDA’s. En el capítulo 14 introduciremos punteros y listas<br />
encadenadas de Pascal, que son más apropiados para implementar colas, pero a<br />
fin de no complicar la presentación, por ahora “nos arreglaremos con arreglos”.<br />
Como sabemos, si queremos trabajar con un arreglo tenemos que declararlo<br />
al comienzo, dando su dimensión y el tipo de elementos. Por lo tanto, si vamos a<br />
representar una cola con un arreglo, no podemos “crearlo de la nada”, y tenemos<br />
que prever su existencia y dar una dimensión adecuada al problema. Del mismo<br />
modo, no podemos “destruirlo”, y nos limitaremos a ignorarlo cuando no lo<br />
necesitemos más.<br />
Suponiendo que hemos declarado el tipo “ dato ”, que puede ser un registro<br />
u otro arreglo, podemos implementar una pila declarando<br />
pila: array[1..MAXP] of dato<br />
donde MAXP es una constante apropiada. Si npila es la cantidad de elementos,<br />
declarado con la misma localidad que pila, podemos hacer:<br />
Crear la pila:<br />
procedure inicializar;<br />
begin npila := 0 end;<br />
Agregar d:<br />
procedure push(d: dato);<br />
begin<br />
if (npila < MAXP) then begin<br />
npila := npila + 1; pila[npila] := d<br />
end<br />
end;
12.2. Generando Subconjuntos Pág. 121<br />
✎ Siguiendo con la filosofía de no poner en general mensajes de<br />
aviso, tratando de ver el bosque y no el árbol, no hemos agregado<br />
acciones para el caso en que npila = MAXP antes de<br />
incorporar el dato.<br />
Quitar un elemento:<br />
function pop: dato;<br />
begin<br />
if (npila > 0) then begin<br />
pop := pila[npila]; npila := npila - 1<br />
end<br />
end;<br />
✎ Valen los mismos comentarios anteriores: no hemos previsto acciones<br />
para cuando se quiera sacar un elemento de una cola<br />
vacía. En general, preguntaremos si hay elementos antes de sacar.<br />
Observar la simetría entre las acciones de push y pop: una realiza exactamente<br />
los pasos inversos de la otra. Esta simetría se traslada a que push es un<br />
procedimiento con el dato como parámetro, mientras que pop es una función<br />
que devuelve el dato (y por lo tanto debe hacerse alguna asignación o similar).<br />
Para colas (fifo), las cosas son ligeramente diferentes. En vez de tener un índice<br />
como en el caso de la pila, mantenemos dos: uno, digamos ppocola,señalando<br />
el principio de la cola en el arreglo, y otro, digamos fincola, señalando el final.<br />
fincola se incrementa al agregar un dato, mientras que ppocola aumenta cuando<br />
se extrae un dato, de modo que los elementos “vivos” en principio estarán entre<br />
ppcola y fincola (inclusivo en ambos casos).<br />
Claro que si la cola está definida como un arreglo de MAXC elementos,<br />
las cosas se complican cuando agregamos más de MAXC elementos a la cola<br />
(aún cuando hayamos quitado algunos), y necesitamos usar aritmética módulo<br />
MAXC . No entramos en detalles porque en los ejemplos que daremos en<br />
los capítulos posteriores supondremos que MAXC es lo suficientemente grande<br />
como para evitar este problema.<br />
Volviendo a las pilas, no podemos dejar de observar que los procedimientos<br />
inicializar y push son viejos conocidos que usamos al leer los datos de un arreglo,<br />
por ejemplo en el programa maximocaracter (pág. 192). También hemos visto<br />
un atisbo del uso de pilas en el problema 11.5 de las torres de Hanoi, donde las<br />
agujas eran pilas en sí.<br />
En la próxima sección veremos que la técnica de mezclar recursión con pilas<br />
puede usarse para generar distintos objetos combinatorios, aunque las pilas no<br />
siempre aparecerán explícitamente.<br />
12.2. Generando Subconjuntos<br />
Problema 12.1. Supongamos que queremos imprimir todas las cadenas de bits<br />
(arreglos de 0’s y 1’s) de longitud n. Dado que las cadenas de bits de longitud<br />
n se obtienen agregando un 0 o un 1 a las cadenas de longitud n − 1, podríamos<br />
declarar un arreglo global a como<br />
a: array[1..10] of integer,
Pág. 122 Generando objetos combinatorios<br />
donde iremos construyendo las cadenas, y hacer la llamada en el cuerpo principal<br />
a“cadena(1)”, donde cadena está dadapor:<br />
procedure cadena(k: integer);<br />
var i, j: integer;<br />
begin<br />
for i := 0 to 1 do begin<br />
a[k] := i; (* poner 0 o 1 en el lugar k *)<br />
if (k < n) then cadena(k+1)<br />
else begin<br />
(* imprimir la cadena cuando k = n *)<br />
write(’ ’);<br />
for j := 1 to n do write(a[j]:1);<br />
writeln<br />
end (* else *)<br />
end (* for *)<br />
end;<br />
Observar que usamos al arreglo a como una pila. Para k fijo, el elemento<br />
en la posición k tiene un valor de 0 cuando i = 0, que se mantiene para valores<br />
superiores a k pero luego es cambiado para i = 1, y obviamente cambia<br />
varias veces cuando es llamado desde valores inferiores a k. Sin embargo, k es<br />
el parámetro del procedimiento y no una variable global.<br />
a) Hacer una prueba de escritorio del procedimiento cuando n =3.<br />
b) Hacer un programa que dado n ∈ N imprima todas las cadenas de bits de<br />
longitud n, siguiendo las indicaciones anteriores.<br />
c) Agregar un contador (global) para contar las cadenas de longitud n, yver<br />
que la cantidad de cadenas es 2n .<br />
d) En el procedimiento cadena propuesto, se va “hacia adelante”, llamando<br />
a“cadena(1) ” en el cuerpo principal y se va aumentando el valor del<br />
parámetro k en cada llamada del procedimiento hasta llegar a k = n, ya<br />
que n es global. Esto contrasta con el uso de recursión de, por ejemplo, el<br />
factorial, donde vamos “hacia atrás” disminuyendo el valor del parámetro en<br />
cada llamada. ¿Cómo podría redefinirse el procedimiento de modo de hacer<br />
la llamada “ cadena(n)” en el cuerpo principal, y que en vez comparar k<br />
con n se compare con 0 o 1?<br />
✎ En el problema 12.7 vemos otra forma de encontrar las cadenas de bits, sin<br />
usar recursión. ✄<br />
Problema 12.2 (Subconjuntos). En el problema anterior esencialmente se<br />
construyen todos los subconjuntos de {1, 2,...,n}, ya que las cadenas de bits de<br />
longitud n se pueden considerar como vectores característicos. Dado un conjunto<br />
A ⊂ {1, 2,...,n} definimos su vector característico, b(A) = (b1,b2,...,bn),<br />
mediante<br />
<br />
1 si i ∈ A,<br />
bi =<br />
0 si no.<br />
Es claro que dos conjuntos distintos tienen vectores característicos distintos,<br />
y que por otro lado, dada una cadena de bits podemos encontrar un conjunto<br />
A tal que b(A) sea esa cadena. Por lo tanto, hay una correspondencia biunívoca<br />
entre cadenas de bits de longitud n y subconjuntos de {1, 2,...,n}.
12.2. Generando Subconjuntos Pág. 123<br />
Modificar el programa del problema 12.1 para que en vez de imprimir cadenas<br />
de bits, imprima el subconjunto correspondiente en {1, 2,...,n}, representando<br />
al conjunto vacío con una raya “ - ”.<br />
Por ejemplo, si n = 2 la salida debería ser algo como:<br />
- 1 2 1 2 ✄<br />
Una vez que sabemos cómo generar una familia de objetos, es más sencillo<br />
generar o contar objetos de la familia con características particulares. Por<br />
ejemplo:<br />
Problema 12.3. Supongamos que queremos contar la cantidad h(n) de cadenas<br />
de n bits que no contienen dos 0’s sucesivos:<br />
a) Hacer un programa para calcular h(n) paran∈N, usando el programa<br />
del problema 12.1 y una variante de búsqueda lineal para encontrar dos 0’s<br />
consecutivos en cada cadena construida, verificando si se trata de una cadena<br />
válida antes de aumentar el contador. Sugerencia: recordar el problema 9.3.<br />
b) Comparar el número h(n) obtenido anteriormente con los números de Fibonacci<br />
y hacer un nuevo programa para calcular h(n) directamente. ✄<br />
Problema 12.4. En este problema imprimiremos todos los caminos que hemos<br />
contado en el problema 11.4. Para ello consideramos una arreglo global camino<br />
en el que guardaremos las “intersecciones” o “esquinas” por las que hay que<br />
pasar. Como éstas tienen dos coordenadas, digamos x y y, declaramos<br />
type esquina = record x, y: integer end;<br />
y<br />
camino: array[1..MAXK] of esquina;<br />
donde MAXK es una constante para la máxima longitud del camino. En el problema<br />
original todos los caminos tienen longitud m+n,peropodrían ser distintas<br />
si, por ejemplo, también consideráramos la posibilidad de ir en diagonales de<br />
suroeste a noreste. Para nuestros ejemplos, 10 o 20 son valores razonables para<br />
MAXK .<br />
También consideramos a los datos m y n como variables globales, y agregamos<br />
la variable global k que indicará la longitud del camino construido. Inicialmente<br />
tendremos k = 0, puesto que no tenemos camino.<br />
El trabajo lo hará el procedimiento llegardesde, haciendo la única llamada<br />
“ llegardesde(0,0)” enelcuerpoprincipal:<br />
procedure llegardesde(i, j: integer);<br />
var r: integer;<br />
begin<br />
k := k + 1;<br />
with camino[k] do begin x := i; y := j end;<br />
if (i < m) then llegardesde(i+1,j);<br />
if (j < n) then llegardesde(i,j+1);<br />
if ((i = m) and (j = n)) then (* llegamos *)<br />
begin<br />
(* imprimir *)<br />
write(’ ’);<br />
for r := 1 to k do
Pág. 124 Generando objetos combinatorios<br />
with camino[r] do<br />
write(’ (’, x:1, ’,’, y:1, ’)’);<br />
writeln<br />
end; (* if i = m and j = n *)<br />
k := k - 1<br />
end;<br />
Estudiemos este procedimiento, haciendo una “prueba de escritorio” en todo<br />
caso:<br />
• k, inicialmente en 0, se incrementa en 1 al entrar al procedimiento, y se<br />
incorpora la intersección (i, j) alcamino en la (nueva) posición k.<br />
Es decir, k indica la cantidad de objetos en la pila camino. Al comienzo,<br />
la pila está vacía, y se van incorporando elementos atrás.<br />
A diferencia del problema 12.1, k es global y no el parámetro del procedimiento.<br />
• Si i
12.3. Generando permutaciones Pág. 125<br />
y por lo tanto no lo haremos aquí, que si el conjunto tiene n elementos entonces<br />
hay n! permutaciones posibles.<br />
Problema 12.5 (Generación de permutaciones). En este problema, copiando<br />
las ideas de los problemas anteriores, generamos todas las permutaciones<br />
de {1, 2,...,n} usando el procedimiento poner :<br />
procedure poner(k: integer);<br />
(* poner en la posicion k los que faltan *)<br />
var j: integer;<br />
begin<br />
if (k < n) then<br />
(* buscar y agregar los que faltan *)<br />
for j := 1 to n do begin<br />
if (falta[j]) then begin<br />
falta[j] := false; a[k] := j;<br />
poner(k+1); (* ir a proxima posicion *)<br />
falta[j] := true (* borrar huellas *)<br />
end (* if *)<br />
end (* for *)<br />
else (* k = n: nueva permutacion *)<br />
begin<br />
(* buscar y agregar el que falta *)<br />
j := 0; repeat j := j + 1 until falta[j];<br />
a[n] := j;<br />
(* imprimir *)<br />
write(’ ’); for j := 1 to n do write(a[j]:3);<br />
writeln<br />
end<br />
end;<br />
donde a es un arreglo global en el que guardamos la permutación que estamos<br />
construyendo, y falta es un arreglo cuyos elementos son de tipo “ boolean ”(y<br />
tiene la misma longitud de a), de modo que falta i es falso o verdadero de acuerdo<br />
asielelementoi ya ha sido colocado o no (respectivamente) en la permutación.<br />
Observamos que<br />
• Los arreglos a y falta deben tener una longitud adecuada. 10 es suficiente<br />
para nosotros.<br />
• n debe ser una variable global. En cambio, k es el parámetro del procedimiento,comoenelproblema12.1.<br />
• Inicialmente “faltan” todos los elementos en la permutación, i.e. falta i es<br />
verdadero para todo i =1,...,n.<br />
• En cualquier paso, cuando se agrega el elemento j en la posición k (k
Pág. 126 Generando objetos combinatorios<br />
los valores en lugares arbitrarios y en cada momento son importantes todos<br />
los valores, no sólo los primeros.<br />
• Se hace la única llamada “ poner(1) ”enelcuerpoprincipal.<br />
a) Usando las ideas anteriores, hacer un programa para generar todas las permutaciones<br />
de n elementos.<br />
b) En el procedimiento poner , al buscar los elementos que faltan se pone<br />
if (k < n) then<br />
for j := 1 to n do begin<br />
if (falta[j]) then begin<br />
.<br />
end (* if *)<br />
end (* for *)<br />
¿Qué función cumple el par “ begin-end” que encierra a “ if ” (el primer<br />
“ begin ”conelúltimo “ end ”) en este caso?, ¿es redundante?<br />
¿Sería equivalente poner<br />
if (k < n) then begin<br />
for j := 1 to n do<br />
if (falta[j]) then begin<br />
.<br />
end (* if *)<br />
end (* if *) ?<br />
c) Eliminar (comentándolo) el renglón “ falta[j] := true ” del procedimiento<br />
poner , y comprobar el comportamiento.<br />
d) Agregar un contador, como en los problemas anteriores, a fin de ir contando<br />
las permutaciones obtenidas, imprimiendo cada permutaciónconsunúmero<br />
de orden.<br />
✎ Hay varios algoritmos para generar las permutaciones de {1,...,n}, algunos<br />
bastante diferentes al que presentamos, y otros más eficientes. Observar<br />
que con el presentado, las permutaciones obtenidas están ordenadas “lexicográficamente”,<br />
cosa que no ocurre en otros algoritmos. ✄<br />
12.4. Árboles binarios ordenados<br />
Como mencionáramos al presentar los métodos de clasificación en la sección<br />
10.3, una de las mayores causas de ineficiencia al clasificar es el “movimiento”<br />
de datos, problema que se agudiza al mover “grandes” datos como<br />
arreglos o registros. En esta sección nos preocuparemos por dar un método donde<br />
clasificamos datos pero una vez construido un registro éste “no se mueve”.<br />
Hasta ahora nos hemos concentrado en la estructura “lineal” de los arreglos<br />
para guardar información, como se ilustra en la figura 12.1, donde cada punto<br />
o nodo representa un elemento del arreglo, y las flechas señalan al siguiente<br />
elemento.<br />
Figura 12.1: Una estructura “lineal”.
12.4. Árboles binarios ordenados Pág. 127<br />
Pero podemos pensar en una configuración como la de la figura 12.2, donde<br />
también tenemos nodos unidos por flechas. Los nodos están en distintos “pisos”<br />
o niveles, marcados por las rectas punteadas (sí, a medida que bajamos el nivel<br />
aumenta).<br />
nivel 0<br />
nivel 1<br />
nivel 2<br />
nivel 3<br />
Figura 12.2: Un árbol binario ordenado.<br />
Si tuviéramos nodos hasta un nivel k, vemos que para ir desde el punto<br />
superior a cualquier otro usando los segmentos, necesitamos a lo sumo k pasos.<br />
Por lo tanto, podemos ir desde cualquier punto a otro cualquiera en a lo sumo<br />
2k pasos, aún cuando puede haber hasta<br />
1+2+4+···+2 k =2 k+1 − 1<br />
nodos. Esta observación es muy importante si guardamos y buscamos datos en<br />
los nodos: la diferencia entre las estructuras de las figuras 12.2 y 12.1 es similar<br />
a la que hay entre búsqueda binaria y búsqueda lineal.<br />
Unaestructuracomoladelafigura12.2 se llama árbol binario ordenado<br />
o simplemente árbol binario (1) .Elnododemás arriba (en el nivel 0) se llama<br />
raíz, y cada nodo está conectadohaciaabajocon0,1o2(ynomás) hijos, ala<br />
izquierdaoaladerecha.<br />
✎ El nombre de árbol binario ordenado parece un poco largo. Informalmente,<br />
se llama árbol por la forma de “ramas”, aunque la raíz se representa arriba,<br />
binario porque cada rama se divide en a lo sumo 2, y ordenado porque<br />
distinguimos entre el hijo a la izquierda y a la derecha.<br />
✎ Como vimos, un árbol binario con k niveles puede tener hasta 2 k+1 − 1<br />
nodos. En contrapartida, si tiene exactamente k niveles, no puede tener<br />
menos de k + 1 nodos.<br />
Veamos cómo usar esta estructura para guardar información.<br />
Supongamos que, como en el problema 10.15, ingresamos letras, por ejemplo<br />
‘ b ’, ‘ d ’, ‘ c ’, ‘ d ’, ‘ a ’, ‘ d ’, ‘ a ’, ‘ a ’, ‘ d ’, ‘ e ’, ‘ d ’, ‘ e ’, ‘ a ’, ‘ b ’, ‘ a ’,<br />
en ese orden, y queremos imprimir la cantidad de veces que apareció cada una<br />
de ellas, pero ordenadas alfabéticamente, i.e. queremos una salida como<br />
Caracter Cantidad de apariciones<br />
a 5<br />
b 2<br />
c 1<br />
d 5<br />
e 2<br />
(1) Sólo veremos árboles binarios ordenados.
Pág. 128 Generando objetos combinatorios<br />
Si declaramos, en forma similar al problema 10.15,<br />
type<br />
tipodato = char; (* el tipo de datos a ingresar *)<br />
nodo = record<br />
llave: tipodato; (* dato *)<br />
cuenta: integer (* veces que aparecio *)<br />
end;<br />
tipoarreglo = array[1..MAXN] of nodo;<br />
y guardamos los datos en un arreglo de tipo “ tipoarreglo”, digamos arreglo,<br />
después de ingresar los datos como en el aquel problema, y antes de clasificarlos,<br />
tendríamos una disposición como en la figura 12.3.<br />
‘ b ’ 2 ‘ d ’ 5 ‘ c ’ 1 ‘ a ’ 5 ‘ e ’ 2<br />
arreglo 1 arreglo 2 arreglo 3 arreglo 4 arreglo 5<br />
Figura 12.3: Disposición del arreglo de registros luego de ingresar los datos.<br />
Para dar una estructura de árbol binario a los datos, hacemos lo siguiente: el<br />
primer dato ingresado es la raíz del árbol, y al ingresar cada dato subsiguiente lo<br />
comparamos con los ya existentes, yendo hacia abajo a la izquierda si el nuevo<br />
dato es menor o hacia la derecha si es mayor (o incrementando cuenta si es<br />
igual). Así, al terminar de ingresar los datos en nuestro ejemplo quedaría un<br />
árbol binario como el de la figura 12.4.<br />
‘ b ’ 2<br />
‘ a ’ 5 ‘ d ’ 5<br />
‘ c ’ 1 ‘ e ’ 2<br />
Figura 12.4: Disposición del árbol binario luego de ingresar los datos.<br />
Después de mirar un rato esta figura, nos damos cuenta que si “proyectamos”<br />
los registros sobre una recta horizontal como en la figura 12.5, obtenemos los<br />
registros clasificados alfabéticamente. Después de pensar otro rato más, nos<br />
damos cuenta que no se trata de una casualidad: en la figura 12.4 todo lo<br />
que queda a la izquierda y abajo de un registro tiene que tener menor llave<br />
yanálogamente hacia la derecha y abajo. Así, en nuestro ejemplo, todo lo que<br />
es menor que ‘ b ’, i.e. ‘ a ’, está a la izquierda, mientras que ‘ c ’, ‘ d ’y‘e ’están<br />
a la derecha. Cuando miramos a ‘ d ’, ‘ c ’está a la izquierda, pero también ‘ b ’<br />
(que tenía a ‘ d ’asuderecha)y‘a ’ (que estaba a la izquierda de ‘ b ’), etc.<br />
‘ a ’ 5 ‘ b ’ 2 ‘ c ’ 1 ‘ d ’ 5 ‘ e ’ 2<br />
Figura 12.5: Los registros de la figura 12.4 “proyectados”.<br />
Nos queda la inquietud de cómo guardar las “flechas” en la computadora,<br />
i.e. la información sobre los hijos a izquierda y derecha. Para esto agregamos a<br />
los registros dos campos donde se guardará elíndice del hijo correspondiente:
12.4. Árboles binarios ordenados Pág. 129<br />
type<br />
indice = integer; (* para senialar los indices *)<br />
tipodato = char; (* el tipo de datos a ingresar *)<br />
nodo = record<br />
llave: tipodato; (* dato *)<br />
cuenta: integer; (* veces que aparecio *)<br />
izquierda: indice; (* hijo a la izquierda *)<br />
derecha: indice (* hijo a la derecha *)<br />
end;<br />
tipoarbol = array[1..MAXN] of nodo;<br />
Aún cuando el arreglo con los datos ingresados sigue formando una pila,<br />
ahora nos queda una disposición como en la figura 12.6, donde con 0 indicamos<br />
que no hay hijos (a izquierda o derecha) y, claro, dibujamos las flechas sólo<br />
para orientarnos (no están en la computadora). Observar que esta figura es<br />
básicamente la figura 12.3 a la que hemos agregado flechas, pero también es<br />
equivalente a la figura 12.4.<br />
‘ b ’ 2 4 2 ‘ d ’ 5 3 5 ‘ c ’ 1 0 0 ‘ a ’ 5 0 0 ‘ e ’ 2 0 0<br />
Figura 12.6: Los registros con índices para la estructura de árbol binario.<br />
Ya estamos en condiciones de abordar el siguiente problema:<br />
Problema 12.6. El programa arbolbinario, muestra la construcción de un árbol<br />
binario ordenado usando arreglos como indicamos anteriormente.<br />
En los nodos se guarda información, que en nuestro ejemplo es llave y cuenta.<br />
En llave guardamos un entero, pero podría ser una palabra de un texto, o<br />
un apellido, etc., mientras que en cuenta guardamos el número de veces que<br />
apareció esedato.Enelárbol, un nodo tiene a su izquierda un subárbol cuyos<br />
nodos tienen menor llave, y a su derecha nodos con mayor llave.<br />
A la salida se imprimen los datos ordenados por orden creciente de llave y<br />
las veces que apareció el dato.<br />
Estudiando el programa, observamos que<br />
• Cada nodo guardará cuatro datos: la llave, la cantidad de veces que apareció,<br />
y cómo encontrar a sus hijos. Por lo tanto necesitamos un registro<br />
con 4 campos.<br />
• Los nodos con la información se guardan en un arreglo de nodos llamado<br />
(¡curiosamente!) arbol.<br />
• Hay una función booleana entrardatos, modificando un poco lo hecho en<br />
el procedimiento leerarreglo del problema 8.1.<br />
• El árbol se va construyendo en el arreglo arbol como pila con narbol elementos:<br />
a medida que llega nueva información se aumenta narbol yse<br />
incorpora al final.
Pág. 130 Generando objetos combinatorios<br />
• Pero la información de la estructura de árbol —bien diferente a la de<br />
pila— se guarda en los registros: en izquierda (o derecha) colocamos la<br />
posición en el arreglo del hijo a la izquierda (o derecha). Para indicar que<br />
no hay un hijo (a izquierda o derecha) introducimos la constante nada,que<br />
podría tomar cualquier valor “imposible” para índices del arreglo arbol y<br />
que hemos puesto en 0.<br />
• Para procesar los datos ingresados, usamos el procedimiento recursivo<br />
binario que primero se fija si se está en una posición “vacía”, i.e. sin<br />
datos, en cuyo caso se agrega un nodo al árbol, se guarda el dato, se<br />
coloca cuenta en 1, y se inicializan los hijos a nada pues están vacíos.<br />
Si la posición no está vacía, se pregunta si el dato es el mismo que se<br />
guarda en ese nodo, en cuyo caso se incrementa cuenta, osidebeirala<br />
izquierdaencasoqueseamenoroaladerecha.<br />
• El procedimiento enorden imprime los datos en (¡ejem!) orden con la cantidad<br />
de apariciones en forma recursiva: para mantener el orden, deben<br />
imprimirse primero los descendientes a la izquierda, luego el nodo mismo,<br />
y finalmente los descendientes a derecha. La condición “de parada” para<br />
la recursión es encontrar el valor nada.<br />
Observar que no necesitamos intercambiar los registros para poder imprimirlos<br />
ordenadamente (recordar la “proyección” mencionada anteriormente).<br />
a) Cambiar la impresión en orden por post orden (primero se imprimen los<br />
subárboles y después el nodo) y por pre orden (primero el nodo y después<br />
los subárboles).<br />
b) Cambiar el procedimiento enorden para imprimir los datos clasificados de<br />
mayor a menor (en vez de menor a mayor).<br />
c) Cambiar el procedimiento enorden para imprimir también el nivel en que<br />
se ha alojado la información.<br />
Sugerencia: agregar un argumento para el nivel, formando parte de la recursión,<br />
o, alternativamente, redefinir los nodos de modo de incluir el nivel<br />
cuando se construyen.<br />
Sugerencia si la anterior no alcanza: paralaprimervariantepodríamos<br />
poner<br />
procedure enorden(w: indice; nivel: integer);<br />
begin<br />
if (w nada) then<br />
with arbol[w] do begin<br />
enorden(izquierda, nivel+1);<br />
writeln(llave:10, cuenta:20, nivel:20);<br />
enorden(derecha, nivel+1)<br />
end<br />
end;<br />
haciendo la llamada “ enorden(raiz, 0) ” en la parte principal.<br />
d) Agregar al programa la impresión de la profundidad (= máximo nivel) del<br />
árbol al terminar de ingresar los datos.
12.5. Problemas Adicionales Pág. 131<br />
e) Ver que si los datos se ingresan ya ordenados, el árbol correspondiente se<br />
reduce básicamente a un arreglo unidimensional: dar un ejemplo con 5 caracteres<br />
distintos, donde la profundidad sea 4.<br />
✎ Para eliminar este problema, se han diseñado una serie de variantes,<br />
como árboles balanceados, árboles “B”, etc. Observar que sacar nodos<br />
de un árbol es un proceso bastante más complicado que sacar nodos<br />
de una lista (ver [16, Cap.4]). ✄<br />
El lector se preguntará porqué hemos incluido el tema de árboles binarios<br />
en este capítulo de “objetos combinatorios”: créase o no, en este capítulo<br />
ya habíamos visto y recorrido árboles binarios, aunque tenían una estructura<br />
particular que nos permitía ahorrar espacio.<br />
Porejemplo,enelproblema12.1 construíamos todas las cadenas de bits<br />
de longitud n o, equivalentemente, los subconjuntos de {1,...,n}. Dada una<br />
cadena c de bits de longitud k, podemos considerar el “hijo a izquierda” como<br />
la cadena c0 de longitud k + 1 que se obtiene de c agregando a la derecha un 0, y<br />
de la misma forma considerar el “hijo a derecha” como la cadena c1 de longitud<br />
k + 1 que se obtiene agregando un 1. En la función recursiva del problema 12.1<br />
el nivel que estábamos construyendo era precisamente k, y cuando llegábamos<br />
a k = n, imprimíamos la cadena.<br />
También hay “árboles” en los problemas 12.4 y 12.5, sólo que los nodos<br />
pueden tener más de dos hijos. A diferencia del problema de los subconjuntos o<br />
cadenas de bits, el “recorrido” del árbol (que no es binario) es más complicado<br />
porque la descripción de los nodos lo es.<br />
✎ La variable k del procedimiento llegardesde oenelprocedimientoponer<br />
indica también el nivel que estamos recorriendo. Las instrucciones “ k :=<br />
k - 1”y“falta[j] := true ”, respectivamente, hacen que vayamos “un<br />
nivel hacia arriba” antes de descender nuevamente, y por eso la técnica de<br />
“borrar las huellas” que usamos en ambos casos es conocida como backtracking<br />
o “rastreo inverso”.<br />
12.5. Problemas Adicionales<br />
Problema 12.7. Resolver el problema 12.1 sin usar recursión, usando que las<br />
cadenas de bits de longitud n pueden pensarse como los coeficientes en base 2<br />
de los números entre 0 y 2 n − 1. Sugerencia: parak =0,...,2 n − 1, construir<br />
la lista de coeficientes e imprimirla. ✄<br />
Problema 12.8. Hacer un programa que dados n, k ∈ N imprima todos los<br />
subconjuntos de {1, 2,...,n} que tienen exactamente k elementos. ✄<br />
Problema 12.9 (Problema del Viajante). Supongamos que un viajante<br />
tiene que recorrer n ciudades (exactamente) volviendo a la de partida, sin repetir<br />
su visita a ninguna (salvo la inicial), y que el costo de viajar desde la ciudad i<br />
a la ciudad j es cij ≥ 0. El problema del viajante es encontrar una permutación<br />
(a1,a2,...,an) de(1, 2,...,n) de modo que el costo total del recorrido, ca1a2 +<br />
ca2a3 + ···+ can−1an + cana1, seamínimo. Como se recorre un ciclo, es suficiente<br />
tomar a1 =1.<br />
a) Usando una variante del programa del problema 12.5, hacer un programa<br />
para encontrar una solución al problema cuando el costo cij es:
Pág. 132 Generando objetos combinatorios<br />
i) i + j.<br />
ii) |i − j|.<br />
iii) i × j.<br />
iv) dado como entero aleatorio entre 1 y 10.<br />
Sugerencia: definir una variable min de tipo “ real ” y un arreglo opt donde<br />
se guardarán el costo más chico obtenido y la permutación correspondiente,<br />
y<br />
<br />
poner<br />
<br />
inicialmente min a un valor muy grande e inalcanzable como 1 +<br />
i j cij. A medida que se van recorriendo las permutaciones, calcular<br />
el costo de la permutación y compararlo con min, cambiando min y opt<br />
acordemente. Tomar n pequeño, 4 o 5 al principio, mientras se prueba el<br />
programa, y no mayor que 10–12 en cualquier caso.<br />
b) Ver que en el caso i) del inciso anterior, el costo es el mismo para cualquier<br />
permutación.<br />
c) ¿Podrías decir cuál es el óptimo en el caso ii) para cualquier n? Sugerencia:<br />
podemos pensar que los puntos 1,...,n están sobre una recta y los costos<br />
son las distancias.<br />
✎ En los ejemplos i–iii) los costos son simétricos, i.e. cij = cji. Como recorrer<br />
un ciclo en uno u otro sentido no cambia el costo, estamos trabajando (al<br />
menos) el doble de lo necesario con la propuesta.<br />
El problema del viajante y otros similares son sumamente importantes y<br />
se aplican a problemas de recorridos en general. Por ejemplo, para “ruteo” de<br />
vehículos, máquinas para perforar o atornillar, circuitos impresos y “chips”,<br />
etc., donde el costo puede medirse en dinero, tiempo, longitud, etc. Consecuentemente,<br />
hay toda una área de las matemáticas y computación dedicada al<br />
estudio de su resolución eficiente. Hasta el momento no se conocen algoritmos<br />
verdaderamente buenos, resolviéndose exactamente el caso de unos pocos miles<br />
de “ciudades” y costos arbitrarios. Aunque se sospecha que no hay algoritmos<br />
“eficientes”, aún no se ha demostrado que no los hay (para esto, hay que desarrollar<br />
toda una teoría matemática de qué significa “eficiente”). El algoritmo<br />
propuesto acá esquizás el menos eficiente, pues analiza todas las soluciones<br />
posibles, y por eso se lo llama de búsqueda exhaustiva. ✄<br />
12.6. Comentarios Bibliográficos<br />
El programa arbolbinario está basado en las presentaciones de [16, pág. 210]<br />
y[10, pág. 153].
Capítulo 13<br />
Grafos y árboles<br />
Si tomamos un conjunto de ciudades y las carreteras que las unen, y representamos<br />
gráficamente a las ciudades como puntos y a las carreteras como<br />
segmentos o curvas uniendo esos puntos, obtenemos una figura similar a la 11.4,<br />
en la cual pensábamos que los segmentos eran calles y los puntos las intersecciones.<br />
La idea subyacente en ambos casos es la de grafo, unconjuntodevértices<br />
(las ciudades o intersecciones) que indicaremos por V , y un conjunto de aristas<br />
(las rutas o calles), E, cada una de las cuales queda definida por dos vértices.<br />
No sólo los grafos están relacionados con calles o rutas. En comunicaciones<br />
también podemos pensar que los vértices son computadoras y las aristas representan<br />
la posibilidad de que dos computadoras se conecten entre sí. O, saliendo<br />
de las comunicaciones, podemos pensar en un árbol genealógico, donde los vértices<br />
son las personas y las aristas relacionan padres con hijos. En fin, los ejemplos<br />
de aplicaciones de grafos son muy variadas, y muchas veces sorprendentes.<br />
Euler (1707–1783) fue uno de los más grandes y prolíficos matemáticos,<br />
y ya hemos mencionado su nombre en conexión con análisis (la constante de<br />
Euler), teoría de números (la función φ ylafórmula de Euler-Binet), aunque<br />
éstas son comparativamente contribuciones menores suyas. Fue tan grande su<br />
influencia en las matemáticas que se unificaron notaciones que el ideó, como<br />
la de π (= 3.14159 ...) (del griego “perypheria” o circunferencia), i (= √ −1)<br />
(por imaginario), y e (= 2.71828 ...) (del alemán “einhalt” o unidad).<br />
Entre otras tantas, Euler fue quien originó el estudio de teoría de grafos y<br />
la topología al resolver en 1736 el famoso problema de los puentes de Königsberg<br />
(hoy Kaliningrad, en Rusia), donde el río Pregel se bifurcaba dejando dos<br />
islas (y dos costas), y las islas y las costas se unían por siete puentes. Euler<br />
resolvió el problema, demostrando que no se podían recorrer todos los puentes<br />
pasando una única vez por ellos, demostrando el teorema que hoy llamamos<br />
“de grafos eulerianos” y que mencionamos en el problema 13.7.<br />
En este capítulo veremos algunas propiedades básicas y algunos algoritmos<br />
elementales para grafos. Nos contentaremos con dar una simple, y seguramente<br />
demasiado breve, descripción de los términos y propiedades que usaremos,<br />
dejando para los cursos de matemática discreta las definiciones rigurosas y las<br />
demostraciones.<br />
✎ Lamentablemente no hay una nomenclatura ni notación uniforme de los<br />
muchos conceptos asociados a grafos, de modo que las nuestras pueden<br />
diferir de las de otros autores.
Pág. 134 Grafos y árboles<br />
A fin de distinguir los vértices entre sí es conveniente darles nombres, pero<br />
para el uso computacional supondremos que si hay n vértices, éstos se denominan<br />
1, 2,...,n, i.e. V = {1, 2,...,n}. Más aún, casi siempre usaremos el nombre<br />
n para el cardinal de V .Asícomon es el “nombre oficial” de |V |, el“nombre<br />
oficial” para |E| es m.<br />
Sólo consideraremos grafos simples, para los que no hay una arista de un<br />
vértice en sí mismo, ni aristas “paralelas” uniendo los mismos vértices. En este<br />
caso, podemos relacionar n = |V | y m = |E|: sihayn elementos, hay c(n, 2) =<br />
n(n − 1)/2 subconjuntos de 2 elementos, de modo que m ≤ c(n, 2).<br />
Las aristas están formadas por un par de vértices, y como el orden no importa,<br />
indicaremos la arista que une el vértice a con el vértice b por {a, b}. Claro<br />
que decir que {a, b} ∈E es lo mismo que decir que {b, a} ∈E.<br />
Si e = {a, b} ∈E, diremosquea y b son vecinos o adyacentes, quea y b<br />
son los extremos de e, oquee incide sobre a (y b). A veces, un vértice no tiene<br />
vecinos —no hay aristas que inciden sobre él— y entonces decimos que es un<br />
vértice aislado.<br />
En la figura 13.1, mostramos un ejemplo de grafo donde n =6,<br />
E = {{1, 2}, {1, 3}, {2, 3}, {2, 6}, {3, 4}, {3, 6}, {4, 6}}.<br />
yporlotantom =7,yelvértice 5 es aislado.<br />
3<br />
2<br />
1<br />
4 5<br />
Figura 13.1: Un grafo con 6 vértices y 7 aristas, el vértice 5 es aislado.<br />
Siguiendo con la analogía de calles y rutas, es común hablar de camino,<br />
una sucesión de vértices (el orden es importante) de la forma (v1,v2,...,vk),<br />
donde {vi,vi+1} ∈E para i =1,...,k− 1. Un camino en principio puede tener<br />
vértices repetidos, y si se cierra sobre sí mismo de modo que v1 = vk, decimos<br />
que se trata de un camino cerrado, mientras que si no tiene vértices repetidos<br />
decimos que es un camino simple. Por ejemplo, en la figura 13.1, {3, 2, 6, 3, 4}<br />
es un camino, {1, 2, 3} es un camino simple y {4, 3, 2, 1, 3, 6, 4} es un camino<br />
cerrado. Claro que podemos describir un camino tanto por los vértices como por<br />
las aristas intermedias, y en vez de poner (v1,v2,...,vk) podríamos considerar<br />
({v1,v2}, {v2,v3},...,{vk−1,vk}).<br />
Un ciclo es un camino cerrado sin aristas repetidas (pero puede tener varios<br />
vértices repetidos). Por ejemplo, {4, 3, 2, 1, 3, 6, 4} es un ciclo en el grafo de la<br />
figura 13.1.<br />
De fundamental importancia es reconocer si un grafo es conexo, es decir, si<br />
existe un camino desde cualquier vértice a cualquier otro vértice. Por otra parte,<br />
6
si un grafo es conexo pero no tiene ciclos, decimos que es un árbol.<br />
Por ejemplo, el grafo de la figura 13.1 no es conexo, pues tiene un vértice<br />
aislado. En la figura 13.2.a) mostramos otro grafo no conexo, y en b) unárbol.<br />
a) grafo no conexo con 9 vértices y 9 aristas. b) árbol con 7 vértices.<br />
Figura 13.2: Un grafo no conexo y un árbol.<br />
Si además el grafo es conexo (y simple), como se puede unir un vértice con<br />
los n − 1 restantes, debe haber al menos n − 1 aristas. De modo que para un<br />
grafo (simple) conexo, m tiene que estar básicamente entre n y n2 .<br />
A veces se consideran grafos dirigidos o digrafos, en los que las aristas están<br />
orientadas, y por lo tanto se indican como (a, b), y se distingue entre (a, b) y<br />
(b, a). Nosotros no estudiaremos este tipo de grafos, aunque en realidad hemos<br />
trabajado con el grafo de la figura 11.4 como si fuera un digrafo: las calles sólo<br />
iban de sur a norte o de oeste a este.<br />
Dada su estructura, es más sencillo trabajar con árboles que con grafos.<br />
Como hemos dicho, un árbol es un grafo (simple) conexo y sin ciclos, pero hay<br />
muchas formas equivalentes de describirlo, algunas de las cuales enunciamos<br />
como teorema (que por supuesto creeremos):<br />
Teorema 13.1 (Caracterizaciones de árboles). Un grafo simple G =(V,E)<br />
con |V | = n es un árbol si y sólosisecumplealgunadelassiguientescondiciones:<br />
a) Para cualquier a, b ∈ V existe un único camino que los une.<br />
b) G es conexo y |E| = n − 1.<br />
c) G no tiene ciclos y |E| = n − 1.<br />
d) G es conexo, y si se agrega una arista entre dos vértices cualesquiera, se<br />
crea un único ciclo.<br />
e) G es conexo, y si se quita cualquier arista —pero no los vértices en los<br />
que incide— queda no conexo.<br />
A veces en un árbol consideramos un vértice particular como raíz, y miramos<br />
a los otros vértices como “descendientes” de la raíz: los que se conectan mediante<br />
unaaristaalaraíz son los “hijos”, los que se conectan con un camino de 2 aristas<br />
son los “nietos” y así sucesivamente. Dado que hay un único camino de la raíz<br />
a cualquier otro vértice, podemos clasificar a los vértices según niveles: laraíz<br />
tiene nivel 0, los hijos nivel 1, los nietos nivel 2, etc., como hemos visto al estudiar<br />
árboles binarios ordenados en la sección 12.4.<br />
Por supuesto, podemos pensar que los nietos son hijos de los hijos, los hijos<br />
padres de los nietos, etc., de modo que —en un árbol con raíz— hablaremos<br />
de padres, hijos, ascendientes y descendientes de un vértice. Habrá uno o más<br />
vértices sin descendientes, llamados hojas, mientras que la raíz será elúnico<br />
vértice sin ascendientes. También es común referirse al conjunto formado por<br />
un vértice (aunque el vértice no sea la raíz) y sus descendientes como una rama<br />
del árbol.<br />
Pág. 135
Pág. 136 Grafos y árboles<br />
13.1. Representación de grafos en la computadora<br />
Antes de meternos de lleno con los algoritmos, nos falta una “cosita” más:<br />
¿cómo guardar la información de un grafo en la computadora? Ya sabemos que<br />
los vértices son 1, 2,...,n, pero ¿y las aristas?<br />
Hay muchas formas de representar un grafo en general, y nosotros en este<br />
capítulo nos limitaremos a dos: dar la lista de aristas, y dar la matriz de adyacencias,<br />
una matriz cuyas entradas son sólo0o1ydemodoquelaentradaij<br />
es 1 si y sólo si {i, j} ∈E.<br />
La representación mediante matriz de adyacencias es cómoda, y relativamente<br />
fácil de entender. Quizás sería más eficiente —para los algoritmos que<br />
veremos— dar para cada vértice una lista de sus vecinos. Esta tercera forma<br />
puede implementarse mediante arreglos, pero es mucho más natural usar listas<br />
encadenadas que veremos en el problema 14.9.<br />
En lo que resta de este capítulo, supondremos las declaraciones:<br />
const<br />
MAXN = 20; (* maximo numero de vértices *)<br />
MAXM = 100; (* maximo numero de aristas *)<br />
type<br />
arreglodevertices = array[1..MAXN] of integer;<br />
tipoarista = record i, j: integer end;<br />
arreglodearistas = array[1..MAXM] of tipoarista;<br />
matrizNN = array[1..MAXN,1..MAXN] of integer;<br />
var<br />
ngrafo, mgrafo: integer;<br />
aristasgrafo: arreglodearistas;<br />
adyacencias: matrizNN;<br />
usando para las aristas una representación como arreglo de registros.<br />
Usualmente el ingreso de datos es más sencillo mediante la lista de aristas,<br />
pues la matriz de adyacencias tiene n2 elementos y en general m ≪ n2 (y siempre<br />
m ≤ n(n − 1)/2 para grafos simples). De cualquier modo, es conveniente tener<br />
amanoprocedimientosparalalecturaypasardeunaaotrarepresentación:<br />
Problema 13.2. En nuestros programas supondremos que ngrafo, elnúmero<br />
de vértices del grafo, es entrado explícitamente (recordando que los vértices<br />
serán entonces 1, 2,...,ngrafo).<br />
a) Hacer un procedimiento para leer las aristas ingresadas por terminal, donde<br />
cada arista consta de dos números enteros, formando un arreglo de longitud<br />
mgrafo.<br />
✎ Como es usual, supondremos que el usuario ingresa correctamente los<br />
datos: los vértices de las aristas son enteros entre 1 y ngrafo, nohay<br />
aristas repetidas o de la forma {i, i}, y no hay más de MAXM aristas.<br />
b) Considerando el procedimiento dearistasaadyacencias,<br />
procedure dearistasaadyacencias;<br />
var i, j, k: integer;
13.2. Recorriendo un grafo Pág. 137<br />
begin<br />
for i := 1 to ngrafo do<br />
for j := 1 to ngrafo do<br />
adyacencias[i,j] := 0;<br />
for k := 1 to mgrafo do<br />
with aristasgrafo[k] do begin<br />
adyacencias[i,j] := 1;<br />
adyacencias[j,i] := 1<br />
end<br />
end;<br />
hacer un programa que lea el número de vértices, las aristas (como en el inciso<br />
anterior), calcule la matriz de adyacencias, e imprima para cada vértice,<br />
los vértices que son adyacentes. Por ejemplo, si la entrada son las aristas<br />
{1, 5}, {2, 5} y {4, 2}, deberá imprimir<br />
Vertice Vecinos<br />
1 5<br />
2 4 5<br />
3<br />
4 2<br />
5 1 2<br />
c) Hacer un procedimiento que, dada la matriz de adyacencias, construya el<br />
arreglo de aristas (a fin de evitar repeticiones, es conveniente construir sólo<br />
aristas de la forma {i, j} con i
Pág. 138 Grafos y árboles<br />
Claro que si el grafo no es conexo, no será posible hacerlo. Por suerte, con<br />
un pequeño esfuerzo extra, los algoritmos que veremos también nos dirán si el<br />
grafo es conexo o no.<br />
Estos algoritmos se encuadran en la estructura del algoritmo visitar, que<br />
mostramos en “seudo-código” en el cuadro 13.1, donde con “←” denotamos la<br />
asignación y con la indentación implícitamente señalamos un par “begin-end” o<br />
“comienzo-fin” para agrupar instrucciones.<br />
Algoritmo visitar<br />
Entrada: un grafo G =(V,E), y un vértice i0 ∈ V .<br />
Salida: la lista de vértices que se pueden alcanzar desde i0 “visitados”.<br />
comienzo<br />
Q ←{i0};<br />
mientras Q = ∅ hacer<br />
sea i ∈ Q;<br />
sacar i de Q;<br />
“visitar” i;<br />
para todo j adyacente a i hacer<br />
si j no está “visitado”yj/∈ Q entonces agregar j a Q<br />
fin<br />
Cuadro 13.1: Esquema del algoritmo visitar.<br />
En el algoritmo se guardan en la cola Q los vértices que pueden visitarse, esto<br />
es, son adyacentes a algún vértice ya visitado pero aún no han sido visitados. Así,<br />
en todo momento del algoritmo habrá tresclasesdevértices: los ya visitados, los<br />
que están en la cola (y todavía no se han visitado), y los que no fueron visitados<br />
ni están en la cola (porque todavía no visitamos ninguno de sus vecinos).<br />
Como no queremos agregar a la cola un vértice ya agregado, debemos guardar<br />
información que nos diga si un vértice ha estado en la cola o no. Para los<br />
algoritmos que haremos será conveniente usar un arreglo padre, inicialmenteen<br />
0, y al incluir el vértice j en la cola porque es vecino del vértice i que estamos<br />
visitando, pondremos padre j = i (> 0). Así, la condición padre i > 0 indicaráque<br />
i se ha puesto alguna vez en la cola, aunque ya no esté.<br />
A medida que recorremos el grafo, las aristas que usamos y los vértices que<br />
visitamos van formando un árbol (1) que —cuando el grafo es conexo— se llama<br />
generador o de expansión del grafo, pues contiene a todos los vértices del grafo<br />
original y las aristas del árbol son también aristas del grafo original (pero pueden<br />
no ser todas).<br />
Por supuesto que si el grafo que queremos recorrer es ya un árbol, no se formará<br />
uno nuevo. Pero sí que podemos tomar al vértice i0, con el que empezamos<br />
el recorrido, como raíz, y el arreglo padre justamente nos dirá quién es el padre<br />
de cada vértice en el árbol que se forma.<br />
(1) Esto irá quedando más claro con los ejemplos. Es una propiedad que no demostraremos,<br />
¡como tantas otras!
13.3. Recorrido a lo ancho y en profundidad Pág. 139<br />
Pondremos padre i0 = i0 para indicar que i0 es justamente la raíz y no podemos<br />
seguir “más arriba”, de modo que también para i = i0 la condición<br />
padre i > 0 es equivalente a que el vértice i se ha incoporado alguna vez a la<br />
cola.<br />
13.3. Recorrido a lo ancho y en profundidad<br />
En el recorrido “a lo ancho” o “ancho primero”, visitamos primero el vértice<br />
i0, luego sus vecinos, luego los vecinos de los vecinos, etc., sin volver a visitar a<br />
alguien ya visitado. O sea que, pensando en el árbol que se formará, visitaremos<br />
primerolaraíz, después todos sus hijos, después todos sus nietos, etc., en el mismo<br />
orden en que los vamos encontrando. Es entonces conveniente implementar<br />
la cola Q como cola fifo (como en la sección 12.1): el que llegó antes sale antes.<br />
Problema 13.4 (Recorrido a lo ancho). Implementamos el algoritmo en el<br />
programa anchoprimero (pág. 199), y observamos que:<br />
• Además de las variables mencionadas en la sección 13.1, incorporamos<br />
nvisitado y marbol que nos indicarán la cantidad de vértices visitados<br />
y aristas en el árbol que forman. Por supuesto, al finalizar tendremos<br />
nvisitado = ngrafo si y sólo si hemos recorrido todos los vértices, o equivalentemente,<br />
si el grafo original es conexo.<br />
También agregamos el arreglo aristasarbol , donde guardaremos las aristas<br />
del árbol construido, y el arreglo de vértices visitado, donde guardaremos<br />
el orden en que han sido visitados los vértices.<br />
Es posible que sólo querramos saber si el grafo es conexo. En este caso<br />
los arreglos aristasarbol y visitado no son necesarios, y sólo habrá que<br />
mantener nvisitado y compararlo al terminar con el valor de ngrafo.<br />
• El procedimiento ancho es el que realiza el trabajo. Hemos tomado como<br />
vértice inicial i0 = 1, guardando la cola (fifo) con los vértices a visitar en<br />
el arreglo avisitar,entreppo y fin.<br />
Cuando visitamos un vértice i, guardamos el número correlativo de visita<br />
en visitado. Luego recorremos los vecinos, reconocidos por la condición<br />
adyacencias ij > 0. Si un vecino j de i tiene padre j = 0, quiere decir que<br />
no ha sido agregado a la cola, y entonces se lo coloca en la cola y actualiza<br />
el valor de padre j .<br />
• A fin de reconocer las aristas del árbol, usamos el arreglo padre en el<br />
procedimiento hacerarbol.<br />
a) Ejecutar el programa, tomando como entrada el grafo de la figura 13.1,<br />
agregando las aristas {3, 5} y {4, 5}. Losvértices se recorren en el orden<br />
1, 2, 3, 6, 4, 5.<br />
b) Cambiar el programa de modo de que la raíz sea un vértice arbitrario r (en<br />
vez de 1), ingresado por el usuario.<br />
✎ Una fuente de ineficiencia en la implementación es el uso de la matriz de<br />
adyacencias para reconocer a los vecinos. Esto hace que nuestro programa<br />
haga del orden de n 2 pasos para recorrer el grafo. Si m ≪ n 2 ,esmás conveniente<br />
usar directamente una lista de vecinos para cada vértice, puesto<br />
que entonces el algoritmo hará delordendempasos.
Pág. 140 Grafos y árboles<br />
✎ El orden en que se recorren los vértices —tanto en ancho primero como en<br />
profundidad primero que veremos luego— está determinado también por<br />
el orden que se dan a los vecinos. Acá, al usar la matriz de adyacencias<br />
para buscar los vecinos, seguimos el orden de los naturales, pero podría ser<br />
otro. En la mayoría de las aplicaciones, la numeración dada a los vértices<br />
y sus vecinos no es importante: si lo fuera, hay que sospechar del modelo. ✄<br />
En general, aún cuando el grafo sea un árbol binario ordenado (y con raíz<br />
1), el recorrido a lo ancho es distinto de los recorridos “en orden”, “pre orden” o<br />
“post orden” que hemos visto en el problema 12.6, pues ahora visitamos los vecinos<br />
de la raíz, luego los vecinos de los vecinos, etc., en otras palabras, visitamos<br />
el árbol por niveles.<br />
Problema 13.5. Agregar un procedimiento al programa arbolbinario para imprimir<br />
el árbol construido por niveles: primero la información del vértice en el<br />
nivel 0, luego la de los vértices en el nivel 1, etc., hasta la profundidad. ✄<br />
Si en el algoritmo visitar, en vez de implementar la cola como fifo, la implementamos<br />
como lifo (pila), visitaremos primero los vértices que se han incorporando<br />
más recientemente a la pila. Si el grafo fuera ya un árbol, resultará que<br />
primero visitaremos toda una rama hasta el fin antes de recorrer otra (2) ,loque<br />
hace que este tipo de recorrido se llame “en profundidad” o de “profundidad<br />
primero”.<br />
Problema 13.6 (Recorrido en profundidad).<br />
a) Cambiar el procedimiento ancho del programa anchoprimero de modo de<br />
implementar una pila (cola lifo) en vez de una cola fifo, para realizar el<br />
recorrido en profundidad. Si la entrada es la misma que en el problema 13.4<br />
(agregando las aristas {3, 5} y {4, 5}), los vértices se visitan en el orden<br />
1, 3, 6, 5, 4, 2.<br />
b) Algunos autores (por ejemplo [8]) consideran que la búsqueda en profundidad<br />
consiste en quitar el vértice de la cola sólo cuando se han visitado todos<br />
los vecinos que están después en la cola, lo que en el ejemplo anterior daría<br />
por resultado el orden de visita 1, 2, 3, 4, 5, 6.<br />
Hacer un programa con esta nueva variante, reemplazando en los lugares<br />
apropiados las instrucciones por:<br />
(* al principio solo esta 1 *)<br />
padre[1] := 1;<br />
hay := 1; (* la cantidad de vertices en la pila *)<br />
avisitar[1] := 1;<br />
nvisitado := 1; visitado[1] := 1;<br />
while (hay > 0) do (* mientras la pila no es vacia *)<br />
begin<br />
i := avisitar[hay]; (* tomar el ultimo vertice *)<br />
(* y buscar vecino que no tenga padre *)<br />
j := 1; buscar := true;<br />
while (buscar) do begin<br />
if ((adyacencias[i,j] > 0) and (padre[j] = 0))<br />
(2) Bah, que nos vamos por las ramas.
13.4. Camino más corto: Dijkstra Pág. 141<br />
then<br />
(* se encontro *)<br />
begin<br />
buscar := false;<br />
(* agregar arista (i,j) al arbol *)<br />
padre[j] := i;<br />
(* registrar el orden de visita *)<br />
nvisitado := nvisitado + 1;<br />
visitado[nvisitado] := j;<br />
(* y agregar j a la cola *)<br />
hay := hay + 1;<br />
avisitar[hay] := j<br />
end<br />
else (* probar con el siguiente *)<br />
j := j + 1;<br />
if (j = ngrafo + 1) then begin<br />
(* todos los vecinos ya tienen padre *)<br />
buscar := false;<br />
(* sacar i de la pila *)<br />
hay := hay - 1<br />
end<br />
end (* buscar *)<br />
end (* hay *)<br />
donde la variable buscar es booleana.<br />
c) Hacer un gráfico de los árboles obtenidos en cada caso para el ejemplo dado<br />
y compararlos.<br />
✎ Si el grafo a recorrer es ya un árbol, los resultados obtenidos por estas dos<br />
variantes es el mismo. Pero nuestra implementación de la segunda variante<br />
es mucho más ineficiente que la primera, pues recorremos varias veces las<br />
adyacencias de un mismo vértice buscando un vecino para agregar.<br />
Recordar también la nota sobre la numeración de los vecinos al final<br />
del problema 13.4. ✄<br />
Problema 13.7. Un célebre teorema de Euler dice que un grafo tiene un ciclo<br />
que pasa por todas las aristas exactamente una vez, llamado ciclo de Euler,<br />
si y sólo si el grafo es conexo y el grado de cada vértice es par (recordar el<br />
problema 13.3).<br />
Modificar el programa anchoprimero para que a la salida determine también<br />
si el grafo tiene o no un ciclo de Euler usando el teorema.<br />
✎ Un problema muy distinto es encontrar un ciclo de Euler en caso de existir.<br />
Esto es lo que hacemos en el problema 14.10. ✄<br />
13.4. Camino más corto: Dijkstra<br />
Algunas veces la información interesante de un grafo está enlosvértices,<br />
y las aristas nos dicen que, por alguna razón, dos vértices están relacionados<br />
ynadamás. Tal es el caso de los árboles binarios de la sección 12.4, donde la<br />
conexión estaba asociada al orden entre los datos guardados en los vértices.
Pág. 142 Grafos y árboles<br />
Otras veces, las aristas tienen información adicional que queremos considerar.<br />
Por ejemplo si las aristas indican rutas entre ciudades, podría ser la distancia<br />
entre las ciudades. Cuando cada arista e ∈ E de un grafo tiene un costo o peso<br />
asociado, we, decimos que se trata de un grafo con pesos o pesado (nosotros sólo<br />
consideraremos el caso we > 0paratodoe ∈ E).<br />
En la figura 13.3 damos un ejemplo de grafo con pesos, marcados con recuadros<br />
en las aristas correspondientes, pero por comodidad reproducimos los<br />
datos en el cuadro al costado.<br />
1<br />
2<br />
3<br />
2<br />
1<br />
1<br />
2<br />
1<br />
3<br />
2<br />
8<br />
4 5<br />
1<br />
3<br />
6<br />
Arista e peso we<br />
{1,2} 2<br />
{1,4} 3<br />
{1,5} 8<br />
{2,3} 2<br />
{2,4} 1<br />
{3,4} 1<br />
{3,5} 2<br />
{3,6} 1<br />
{4,6} 1<br />
{5,6} 3<br />
Figura 13.3: Un grafo con pesos en las aristas.<br />
Si tuviéramos que ir de una ciudad a otra, teniendo distintas rutas alternativas<br />
para elegir, es razonable preguntarse cuál de ellas será lamás corta (o la<br />
más barata). Éste es el problema del camino más corto: enungrafopesado,y<br />
dados dos vértices s, t ∈ V , encontrar un camino (v0 = s, v1,...,vk = t) con<br />
peso total mínimo, donde el peso total de un camino se define como<br />
w {v0,v1} + w {v1,v2} + ···+ w {vk−1,vk} =<br />
k<br />
w {vi−1,vi}.<br />
Observar que el valor de k no está fijo: no nos interesa si tenemos que usar<br />
una arista o cien, sólo nos interesa que la distancia total para ir de s a t sea<br />
mínima.<br />
Por ejemplo, en el grafo de la figura 13.3 podemos usar varios caminos para<br />
ir del vértice 1 al 5: el camino (1, 5) usa una única arista (k = 1) y tiene peso 8,<br />
el camino (1, 2, 3, 5) usa 3 aristas y tiene costo total 2 + 2 + 1 = 5, y en realidad<br />
no hay otro con menor costo.<br />
Tal vez el algoritmo más conocido para resolver este problema sea el de<br />
Dijkstra, que sigue la estructura del algoritmo visitar: se comienza desde un<br />
vértice, en este caso s, se lo coloca en una cola, y se visitan los vértices de la<br />
cola.<br />
E. W. Dijkstra (1930–2002) nació ymurióenHolanda. Fue uno de los más<br />
grandes intelectos que contribuyeron a la lógica matemática subyacente en los<br />
programas de computación y sistemas operativos. Entre sus muchas y destacadas<br />
contribuciones está el algoritmo para el camino más corto que presentamos,<br />
publicado en 1959.<br />
Nuevamente habrá tresclasesdevértices: los visitados, los que están en la<br />
cola y no han sido aún visitados, y los que nunca estuvieron en la cola. Como<br />
i=1
13.4. Camino más corto: Dijkstra Pág. 143<br />
novedad, para cada i ∈ V consideraremos el valor di de la distancia más corta de<br />
s a i mediante caminos de la forma (s, v1,v2,...,vk,i) donde todos los vértices<br />
excepto i ya han sido visitados. A fin de indicar que no existe tal camino,<br />
pondremos di = ∞ (siendo éste el valor inicial para todo i).<br />
A diferencia del recorrido a lo ancho, donde se visitan los vértices en el orden<br />
de llegada a la cola, en el algoritmo de Dijkstra elegimos para visitar el vértice<br />
de la cola con menor distancia di. El algoritmo termina cuando t es el próximo<br />
vértice a visitar (y entonces dt es la distancia del menor camino de s a t), o<br />
cuando la cola es vacía,encuyocasonoexisteuncaminodesdeshacia t (y<br />
necesariamente el grafo no es conexo).<br />
Al visitar un vértice i yexaminarunvértice vecino j, verificamos si<br />
di + w {i,j} 0paratodoe∈E. Nosotros dejaremos estas<br />
propiedades para cursos de matemática discreta o teoría de grafos.<br />
Podemos esquematizar el algoritmo en seudo-código como en el cuadro 13.2.<br />
Algoritmo de Dijkstra<br />
Entrada: un grafo pesado G =(V,E,W), y dos vértices s, t ∈ V .<br />
Salida: la distancia dt de un camino más corto entre s y t (dt =<br />
∞, sitalcaminonoexiste).<br />
comienzo<br />
para todo i ∈ V hacer di ←∞;<br />
ds ← 0; Q ←{s};<br />
repetir<br />
sea i ∈ Q tal que di =mínj∈Q dj;<br />
si i = t entonces<br />
sacar i de Q;<br />
para todo j adyacente a i hacer<br />
si di + w {i,j}
Pág. 144 Grafos y árboles<br />
∞, ycomolosvértices j que no han ingresado en la cola son los que satisfacen<br />
dj = ∞, elúltimo lazo interno puede simplificarse a<br />
para todo j ∈ V hacer<br />
si di + w {i,j}
13.5. Mínimo árbol generador: Prim Pág. 145<br />
4. Una vez que determinamos el vértice a visitar, i, lo comparamos con t,<br />
el vértice al cual queremos llegar. Si i = t, hemos llegado y se termina<br />
el procedimiento.<br />
5. En cambio, si i = t, recorremos los vecinos de i, indicados con j,<br />
actualizando dist, padre y la cola si fuera necesario, de acuerdo a la<br />
desigualdad (13.1). Antes de cambiar dist j, sin embargo, preguntamos<br />
si j ha estado en la cola comparando dist j con infinito.<br />
• Para construir e imprimir un camino mínimo, usamos el arreglo padre en<br />
el procedimiento hacercamino: formamos un arreglo recorriendo los antecesores<br />
de t hasta llegar a s. El problema es que, como vamos agregando<br />
atrás, nos queda el camino (t, padre t,...,s), en orden inverso.<br />
Ésto puede no ser problema, pero es fácil imprimirlo de atrás para adelante<br />
de modo de mostrarlo “al derecho”, como hemos hecho. Si fuera<br />
necesario, se puede invertir el arreglo usando un procedimiento como el<br />
del problema 8.3.<br />
a) Ejecutar el programa, tomando como entrada el grafo pesado de la figura<br />
13.3, vértice de partida 1 y vértice de llegada 4.<br />
b) Cambiar el programa de modo que calcule las distancias mínimas de s (ingresado<br />
por el usuario) a todos los otros vértices del grafo (sólo las distancias,<br />
no los caminos).<br />
c) Modificarlo de modo que calcule todas las distancias de i a j, para1≤i< j ≤ ngrafo.<br />
✎ Técnicamente el tipo de cola que usamos en los programas dijkstra y prim<br />
—en la próxima sección— se llama cola de prioridad, en vez de “lifo” o<br />
“fifo”: no se elige ni el primero ni el último sino que se usa otro criterio<br />
“de valor” o “prioridad” para sacar un elemento de la cola.<br />
Nuestra implementación de este tipo de colas es un tanto rudimentaria<br />
a fin de mantener la claridad de los programas. El uso de la variante de<br />
selección directa tarda del orden de |V | pasos cada vez que se elige el<br />
próximo vértice a visitar. En cambio, el uso de variantes de algoritmos<br />
de clasificación más avanzados hace que se tarde a lo sumo del orden de<br />
log2 (|V |) pasos para esa elección.<br />
✎ Otra fuente de ineficiencia es el uso de la matriz de adyacencias (o costos)<br />
si m ≪ n 2 , como hemos observado en el problema 13.4.<br />
✎ Hemos tratado de mantener la presentación de los algoritmos y programas<br />
de este capítulo — algoritmo visitar y los programas anchoprimero, dijkstra<br />
y prim— lomásparecidas entre sí a fin de resaltar las semejanzas, lo que<br />
también afecta (un poquito) a la eficiencia. Es muy posible que el lector<br />
los vea con un “disfraz” bastante distinto en otras referencias. ✄<br />
13.5. Mínimo árbol generador: Prim<br />
Si tuviéramos una cierta cantidad de ciudades y las distancias entre ellas,<br />
un problema interesante es ver cómo construir carreteras de modo de que todas<br />
las ciudades puedan unirse entre sí mediante estas nuevas rutas. Dado que no<br />
nos interesan ciclos, pero el grafo formado por las carreteras debe ser conexo,<br />
estamos buscando un árbol generador.
Pág. 146 Grafos y árboles<br />
El teorema 13.1 nos dice que cuando tenemos un grafo conexo que no es<br />
un árbol, tendremos más de un árbol generador: será cuestión de tomar un<br />
ciclo en el grafo original, sacar cualquiera de las aristas del ciclo, y repetir el<br />
procedimiento hasta que no haya más ciclos, manteniendo la conexión. Como<br />
las aristas que sacamos son bastante arbitrarias, en general hay muchos árboles<br />
generadores de un mismo grafo.<br />
Cuando hay pesos (o costos) asociados a las aristas, como en el caso de la<br />
construcción de las carreteras, nos interesa encontrar entre todos los árboles generadores<br />
uno que minimice la suma de los pesos de las aristas que lo componen,<br />
<br />
we,<br />
e en el árbol<br />
llamado árbol generador mínimo (es posible que haya más de un árbol con esta<br />
propiedad, por ejemplo si los costos de todas las aristas son 1).<br />
Volviendo al grafo de la figura 13.3, podemos formar un árbol generador con<br />
las aristas {1, 2}, {1, 4}, {2, 3}, {3, 6}, {6, 5} (siempre un árbol generador debe<br />
tener n − 1 aristas), con peso total 2 + 3 + 2 + 1 + 3 = 11, y si reemplazamos la<br />
arista {5, 6} por la arista {3, 5}, reducimos el costo en 1.<br />
Hay varios algoritmos para encontrar un árbol generador mínimo, y nosotros<br />
veremos aquí el debido a Prim, pues sigue la estructura del algoritmo visitar,<br />
siendo por lo tanto muy parecido al recorrido a lo ancho o al algoritmo de<br />
Dijkstra para el camino más corto. Otro algoritmo, más eficiente en la mayoría<br />
de los casos prácticos, es el de Kruskal, que dejamos para la sección de Problemas<br />
Adicionales pues su implementación es más elaborada.<br />
El algoritmo de Kruskal fue publicado en 1956, mientras que el de Prim fue<br />
publicado en 1959. Dijkstra también obtuvo en forma independiente el algoritmo<br />
de Prim y lo publicó en 1959, en el mismo trabajo donde presenta su algoritmo<br />
para el camino más corto, lo que no es sorpresa dada la similitud.<br />
Recordemos que en el algoritmo visitar manteníamos una cola con los vértices<br />
a visitar, y se formaban tres clases de vértices: los visitados, los que estaban<br />
en la cola, y los que nunca habían ingresado en la cola.<br />
En el algoritmo de Prim se sigue la misma idea, sólo que un vértice en<br />
la cola se visita cuando su “distancia” a los vértices ya visitados es la menor<br />
entre los vértices de la cola. De modo que hay que hacer pocas modificaciones al<br />
algoritmo de Dijkstra: cambiar la definición de la distancia y, en vez de construir<br />
el camino, construir el árbol correspondiente. A diferencia del algoritmo de<br />
Dijkstra, tenemos que mantener información sobre los vértices visitados. Como<br />
el arreglo padre se irá modificando en los sucesivos pasos (en el de Dijkstra<br />
también, pero no en el recorrido a lo ancho), introducimos un nuevo vector con<br />
valores lógicos visitado.<br />
✎ Como con el algoritmo de Dijkstra, se necesita que we > 0paratodoe ∈ E.<br />
Asimismo, dejamos la demostración de que el algoritmo da efectivamente<br />
un árbol generador mínimo para los cursos de matemática discreta o teoría<br />
de grafos.<br />
Problema 13.9 (Algoritmo de Prim para el mínimo árbol generador).<br />
A fin de implementar el algoritmo de Prim, observamos las similitudes y diferencias<br />
con el de Dijkstra:<br />
• La matriz de costos y el valor infinito son idénticos.
13.5. Mínimo árbol generador: Prim Pág. 147<br />
• En el algoritmo de Prim “no existen” s y t, ni por supuesto el camino que<br />
los une.<br />
• Mientras que en el programa dijkstra tomamos a s como “raíz”, en el<br />
programa prim el vértice 1 es el primero en ingresar a la cola, como en el<br />
programa anchoprimero.<br />
• Guardamos información sobre si un vértice ha sido visitado o no en el<br />
arreglo booleano visitado.<br />
✎ Observar que es diferente al usado en el programa anchoprimero, en<br />
el cual guardábamos en visitado el orden de visita.<br />
La distancia se actualiza comparando dist j con w {i,j} envezdeconw {i,j}+<br />
dist i. Por lo tanto, el procedimiento mascorto del programa dijkstra se<br />
puede cambiar a<br />
procedure arbolminimo;<br />
var<br />
i, j, k, kmin, hay: integer;<br />
d, dmin: costo;<br />
avisitar: arreglodevertices;<br />
visitado: array[1..MAXN] of boolean;<br />
begin<br />
(* inicializacion *)<br />
dearistasacostos;<br />
for i := 1 to ngrafo do begin<br />
padre[i] := 0;<br />
dist[i] := infinito;<br />
visitado[i] := false<br />
end;<br />
(* 1 es la "raiz" *)<br />
padre[1] := 1; dist[1] := 0;<br />
(* cola inicial *)<br />
hay := 1; avisitar[1] := 1;<br />
repeat (* aca hay > 0 *)<br />
(* nuevo i: el de minima distancia en la cola *)<br />
kmin := hay; i := avisitar[hay]; dmin := dist[i];<br />
for k := 1 to hay - 1 do begin<br />
j := avisitar[k]; d := dist[j];<br />
if (d < dmin) then begin<br />
kmin := k; i := j; dmin := d end<br />
end;<br />
(* poner i al final de la cola y sacarlo *)<br />
if (kmin < hay) then<br />
avisitar[kmin] := avisitar[hay];<br />
hay := hay - 1;
Pág. 148 Grafos y árboles<br />
(* la distancia de i ya no debe modificarse *)<br />
visitado[i] := true;<br />
(* examinar vecinos de i *)<br />
for j := 1 to ngrafo do<br />
if ((not visitado[j]) and<br />
(costos[i,j] < dist[j])) then begin<br />
(* si j no se agrego, agregarlo a la cola *)<br />
if (dist[j] = infinito) then begin<br />
hay := hay + 1; avisitar[hay] := j end;<br />
(* actualizar dist y padre *)<br />
dist[j] := costos[i,j];<br />
padre[j] := i<br />
end<br />
until (hay = 0)<br />
end;<br />
• En dijkstra buscábamos un camino, y ahora debemos construir un árbol,<br />
i.e. encontrar las aristas correspondientes. Esto lo podremos hacer usando<br />
nuevamente padre, agregando los pesos mediante dist. Así, el procedimiento<br />
hacercamino en dijkstra puede reemplazarse por una variante del<br />
procedimiento hacerarbol del programa anchoprimero, observando que el<br />
peso w de la arista {k, padre k } es dist k.<br />
• A la salida, debemos imprimir el árbol encontrado y un cartel en caso que<br />
el grafo original no sea conexo. Esto puede hacerse modificando las partes<br />
correspondientes de los programas anchoprimero o dijkstra.<br />
a) Hacer un programa para implementar el algoritmo de Prim usando las<br />
ideas expuestas. En el grafo de la figura 13.3, se obtiene el árbol de aristas<br />
{1, 2}, {3, 4}, {2, 4}, {3, 5}, {4, 6}.<br />
b) Incluir también instrucciones a fin de imprimir el peso total del árbol resultante.<br />
c) El algoritmo expuesto empieza con el vértice 1. Cambiarlo de modo de poder<br />
comenzar con un vértice arbitrario r ingresado por el usuario. Observar que<br />
se pueden obtener distintos árboles de mínimo costo (aunque siempre con<br />
el mismo costo total).<br />
✎ Dado la similitud entre nuestras implementaciones de los algoritmos de<br />
“ancho primero”, Dijkstra y Prim, no es sorprendente que valgan las mismas<br />
observaciones hechas al final de los problemas 13.4 y 13.8. ✄<br />
13.6. Problemas Adicionales<br />
Problema 13.10. Modificar el programa anchoprimero de modo que al terminar,<br />
además de decidir si el grafo es conexo o no, decida<br />
a) si es un árbol o no,<br />
b) si tiene un ciclo o no,<br />
c) y en ese caso, exhibir un ciclo.<br />
Sugerencia: incorporar las modificaciones una por vez. ✄
13.6. Problemas Adicionales Pág. 149<br />
13.6.1. El algoritmo de Kruskal<br />
El algoritmo de Kruskal para encontrar un árbol generador de mínimo peso<br />
se aparta un poco del esquema del algoritmo visitar, ymás bien se acerca al<br />
denominado algoritmo del codicioso pues busca incorporar la mejor arista de las<br />
que quedan.<br />
Antes de describir el algoritmo es conveniente introducir el concepto de componente<br />
conexa (o simplemente componente cuando quede claro que es la conexa):<br />
dado un grafo G =(V,E), vamos a dividir, o más correctamente, particionar,<br />
elconjuntodevértices V en subconjuntos, digamos A1,A2,...,Ac, que<br />
llamaremos componentes conexas de G, de modo que dos vértices u, v ∈ V están<br />
en la misma componente si (y sólo si) existe un camino de u a v. Deestaforma<br />
resulta que las componentes A1,A2,...,Ac satisfacen:<br />
1. V = ∪1≤i≤cAi.<br />
2. Si c>1, Ai ∩ Aj = ∅ para 1 ≤ i
Pág. 150 Grafos y árboles<br />
Algoritmo de Kruskal<br />
Entrada: un grafo G con vértices V ,aristasE ypesos{we}e∈E.<br />
Salida: las componentes conexas (vértices) de G, C, y un conjunto<br />
F ⊂ E tal que H =(V,F) tiene las mismas componentes<br />
conexas que G, y con el mínimo peso entre todos los<br />
grafos con esta propiedad. En particular si G es conexo,<br />
H es un árbol generador de mínimo peso.<br />
comienzo<br />
para todo i ∈ V hacer Ci ←{i};<br />
C←{Ci}i∈V ;<br />
F ←∅;<br />
Q ← E;<br />
mientras (E = ∅) y(|C| > 1) hacer<br />
sea e = {i, j} una arista de menor peso en Q;<br />
sacar e de Q;<br />
si Ci = Cj entonces<br />
C ← Ci ∪ Cj;<br />
eliminar Ci y Cj de C y agregar C a C;<br />
Ci ← C; Cj ← C;<br />
F ← F ∪{e}<br />
fin<br />
Cuadro 13.3: Esquema del algoritmo de Kruskal.<br />
una estructura eficiente para ello que —conservando el inglés— llamaremos<br />
union-find (por “unión-encontrar”) y que pasamos a describir.<br />
En cada etapa del algoritmo, vamos a considerar para cada componente<br />
C ∈Cun vértice r ∈ C representante de C, y una función f : V → V tal que<br />
f(i) es el representante de la componente en la que está i, Ci. Enparticular,si<br />
r es el representante de C, tendremos f(r) =r.<br />
f irá cambiando en cada etapa, pues se van fusionando las componentes<br />
conexas, pero como inicialmente tenemos Ci = {i} para todo i ∈ V ,inicialmente<br />
tendremos f(i) =i.<br />
Cuando en alguna etapa se unen C y C ′ —con representantes r y r ′ respectivamente—<br />
para formar C ′′ , por simplicidad elegiremos a uno de ellos, digamos<br />
r, como representante de C ′′ . Es decir que al finalizar esa etapa f(r) sigue siendo<br />
r pero f(r ′ )=r, y en realidad f se ha modificado en todos los vértices de C ′ .<br />
Hacer la redefinición de f explícitamente en cada etapa para todos los vértices<br />
es muy costosa, por lo que se considera otra función auxiliar, g, que inicialmente<br />
toma los mismos valores de f, i.e. inicialmente g(i) =i para todo<br />
i ∈ V .<br />
g también se va modificando en cada etapa, pero lo haremos únicamente<br />
sobre el representante que ha dejado de serlo. Con las notaciones anteriores de<br />
C, C ′ , r y r ′ , y habiendo elegido a r como representante de C ∪ C ′ , pondríamos<br />
g(r ′ )=r. Observemos que una vez que un vértice i deja de ser representante<br />
de la clase Ci, el valor g(i) nosemodifica(g sólo se modifica sobre algunos
13.6. Problemas Adicionales Pág. 151<br />
representantes). Es decir, o bien g(i) no se modifica nunca, o bien se modifica<br />
una única vez.<br />
Para encontrar f(i) en cualquier momento, preguntamos si g(i) =i. Encaso<br />
afirmativo, quiere decir que, o bien la componente Ci no se unió con ninguna<br />
otra, o bien cada vez que se unió con otra resultó que elegimos a i como<br />
representante de la nueva componente, y debe ser f(i) =i.<br />
Por otro lado, si g(i) =j = i, quiere decir que en algún momento Ci se<br />
unióconCjy tomamos como representante a j en vez de i,yporlotantoCi = Cj<br />
desde ese momento en adelante. Podría ser que g(j) = j, en cuyo caso repetimos<br />
el argumento, considerando los valores sucesivos g(i),g(g(i)),g(g(g(i))),...<br />
hasta llegar a algún valor k para el cual g(k) =k, que debe ser el representante<br />
de la clase, por lo que f(i) =k.<br />
✎ El uso de g es análogo al del arreglo padre en el procedimiento hacercamino<br />
del programa dijkstra. De hecho, estamos pensando a cada componente<br />
conexa como árbol con raíz, que se reconoce por g(r) =r y las aristas<br />
son de la forma {i, g(i)} con g(i) = i. Sin embargo, las aristas {i, g(i)} no<br />
tienen por qué estar en el conjunto de aristas originales E.<br />
En la implementación, llamaremos find (por “encontrar”) a este proceso<br />
iterativo para calcular la función f, guardaremos los valores de g en el arreglo<br />
repre (por “representante”, claro), que por simplicidad supondremos definido<br />
globalmente, y pondremos<br />
function find(i: integer): integer;<br />
var r: integer;<br />
begin<br />
r := i;<br />
while (r repre[r]) do r := repre[r];<br />
find := r<br />
end;<br />
Ahora tenemos que resolver cómo elegir el representante cuando se unen<br />
las componentes C y C ′ con representantes r y r ′ respectivamente, o en otras<br />
palabras, cómo actualizar g (o el arreglo repre). En principio podríamos elegir<br />
cualquiera de g(r) =r ′ o g(r ′ )=r, pero podemos hacer la elección eficientemente<br />
si tenemos en cuenta que g se usará para buscar, dado i, alrepresentante<br />
de Ci con el procedimiento iterativo de find, y nos interesa mantener el número<br />
de iteraciones lo más bajo posible.<br />
Si una componente C tiene como representante a r, llamemos ℓ(r) alnúmero<br />
máximo de iteraciones (del lazo “ while ” en el procedimiento find) paracualquier<br />
vértice de esa componente C. Inicialmente tendremos ℓ(r) =0paratodo<br />
r, pero este valor se irá modificando a medida que fusionemos componentes.<br />
Cuando se juntan las componentes C y C ′ con representantes r y r ′ , y<br />
elegimos como representante de la nueva clase a (por ejemplo) r, elnúmero de<br />
iteraciones máximas para la nueva componente será:<br />
<br />
ℓ(r ′ ) + 1 para los vértices cuyo representante era r ′ ,<br />
ℓ(r) para los vértices cuyo representante era r,<br />
de modo que el nuevo valor de ℓ(r) serámáx{ℓ(r ′ )+1,ℓ(r)}.<br />
Si queremos que los valores de ℓ aumenten lo menos posible, podemos elegir<br />
r o r ′ como nuevo representante comparando máx{ℓ(r ′ )+1,ℓ(r)} ymáx{ℓ(r)+
Pág. 152 Grafos y árboles<br />
1,ℓ(r ′ )}, o equivalentemente, ℓ(r) yℓ(r ′ ):<br />
<br />
g(r ′ )=g(r) si ℓ(r ′ )
13.6. Problemas Adicionales Pág. 153<br />
• Para la salida necesitaremos un procedimiento para imprimir las aristas<br />
elegidas, similar al de escribirarbol en el programa anchoprimero.<br />
• Luego habrá que declarar las funciones de “union-find”, como hemos descripto<br />
anteriormente.<br />
• Podemos hacer el trabajo principal en el procedimiento arbolminimo, donde<br />
elegimos la arista de menor peso haciendo parcialmente selección directa<br />
(como hicimos en dijkstra al elegir la menor distancia entre los que estaban<br />
en la cola).<br />
No es necesario mantener una cola explícita para las aristas si se usa el<br />
mismo arreglo de aristas originales (en todo caso, si es necesario mantener<br />
la lista original, se puede hacer una copia, o directamente pasar el arreglo<br />
de aristas como argumento con lo que la copia se hace automáticamente).<br />
En esta implementación, no “se pierden” las aristas originales, sino que<br />
cambian de lugar (quedando parcialmente ordenadas de mayor a menor).<br />
procedure arbolminimo;<br />
var<br />
hay, ri, rj, k, kmin: integer;<br />
wmin : costo;<br />
e: tipoarista;<br />
begin<br />
(* inicializacion *)<br />
inicializaruf; (* inicializar union-find *)<br />
nconexas := ngrafo; (* numero de componentes *)<br />
marbol := 0; (* en el bosque no hay aristas *)<br />
hay := mgrafo; (* cantidad de aristas a examinar *)<br />
while ((nconexas > 1) and (hay > 0)) do begin<br />
(* tomar la arista de menor peso<br />
y sacarla de la pila *)<br />
kmin := hay;<br />
e := aristasgrafo[hay];<br />
wmin := e.w;<br />
for k := 1 to hay - 1 do<br />
with aristasgrafo[k] do<br />
if (w < wmin) then begin<br />
kmin := k; wmin := w end;<br />
if (kmin < hay) then begin<br />
e := aristasgrafo[kmin];<br />
aristasgrafo[kmin] := aristasgrafo[hay]<br />
end;<br />
hay := hay - 1;<br />
(* miramos si los extremos estan<br />
en la misma componente *)<br />
with e do begin ri := find(i); rj := find(j) end;<br />
if (ri rj) then begin
Pág. 154 Grafos y árboles<br />
end;<br />
(* distintas componentes *)<br />
(* agregar arista a arbol *)<br />
marbol := marbol + 1;<br />
aristasarbol[marbol] := e;<br />
(* juntar componentes conexas *)<br />
union(ri, rj); nconexas := nconexas - 1<br />
end (* if ri rj *)<br />
end (* while nconexas y hay *)<br />
• Al finalizar, además de las aristas, podemos imprimir la cantidad de componentes<br />
conexas, nconexas, donde sabemos que |nconexas| =1⇔ el grafo<br />
es conexo y las aristas elegidas forman un árbol.<br />
Problema 13.11 (Algoritmo de Kruskal para el mínimo árbol generador).<br />
Hacer un programa con las indicaciones dadas, y verificarlo con las<br />
mismas entradas que en el problema 13.9 (figura 13.3). Es posible que la solución<br />
sea distinta. ✄<br />
✎ En nuestra implementación de Kruskal, la fuente de mayor ineficiencia<br />
está enlaselección de la arista de menor peso. En nuestra variante se tarda<br />
alrededor de m pasos en ubicarla, mientras que puede hacerse en el orden de<br />
log 2 m pasos. Tal cual está planteado, el algoritmo puede tardar del orden<br />
de m 2 pasos por el ordenamiento, pero en variantes más eficientes puede<br />
tardar del orden de m log 2 m pasos. Aún con nuestra implementación, el<br />
comportamiento para “grafos típicos” es mejor que el de Prim.<br />
13.7. Comentarios Bibliográficos<br />
La presentación unificada de los algoritmos visitar, Dijkstra y Prim está basada<br />
en [12].
Capítulo 14<br />
Punteros y listas<br />
encadenadas<br />
La lista encadenada surge de la necesidad de evitar la rigidez de la estructura<br />
de arreglo. Recordemos que los arreglos tienen una dimensión predefinida (en<br />
el programa fuente) que a veces resultará excesiva o escasa, dependiendo de la<br />
aplicación. También es muy difícil eliminar o agregar un elemento intermedio,<br />
pues debemos hacer un corrimiento de varios valores del arreglo (por ejemplo,<br />
en el método de clasificación por inserción directa).<br />
En contraste, veremos que la lista encadenada es una estructura dinámica,<br />
que se va construyendo y modificando de acuerdo a los datos que se ingresan.<br />
Para construir una lista encadenada, necesitamos la noción de puntero o<br />
apuntador, que introducimos a continuación.<br />
14.1. Punteros<br />
Recordemos que las variables que hemos usado hasta ahora ocupan un lugar<br />
de memoria, que será más grande o más chico dependiendo de su tipo, y que<br />
pensamos como “cajas”. Para reconocer una variable le hemos dado un nombre<br />
—su identificador— que tienen sentido para nosotros. Pero para la máquina, una<br />
vez compilado el programa, los nombres desaparecen y en cambio identifica cada<br />
variable por la dirección de memoria en la que empieza la caja y su contenido<br />
(y por lo tanto su longitud).<br />
Podemos pensar que un puntero es una variable donde nosotros guardaremos<br />
direcciones de memoria donde empiezan cajas. Aunque todas las direcciones<br />
(para una máquina y sistema operativo dados) ocupan el mismo espacio, en<br />
Pascal debemos agregar en la declaración de un puntero el tipo al cual “apunta”<br />
o “referencia” (esto es una propiedad del lenguaje, protegiéndonos de posibles<br />
errores).<br />
Por ejemplo, si queremos alojar en el puntero p una dirección de memoria<br />
en la que se guardará un entero, declaramos<br />
var p: ^integer<br />
✎ El símbolo ‘ ^ ’ es una abreviatura de ‘ ↑ ’, que normalmente no existe en<br />
los teclados.
Pág. 156 Punteros y listas encadenadas<br />
En este caso, al comienzo del programa p no tiene asignado valor alguno<br />
(como sucede con variables de otros tipos). Para indicar que p no tiene una caja<br />
relacionada, es muy conveniente y aconsejable (para evitar problemas) asignarle<br />
una dirección “imposible” que en Pascal se denomina nil:<br />
p := nil<br />
Podemos interpretar a nil como “un cable a tierra”, que indica que no hay<br />
“nada”. Recordemos que justamente hemos usado con el mismo sentido a la<br />
variable nada en el programa arbolbinario (pág. 197).<br />
A pesar de que, habiéndolo declarado, p ocupa un lugar de memoria, ese<br />
lugar es sólo para guardar direcciones, y puede no coincidir con la cantidad de<br />
espacio necesaria para guardar, por ejemplo, un entero. Si además queremos<br />
reservar un lugar para guardar un entero, usamos “ new ”:<br />
new(p)<br />
Esta instrucción hace que se reserve un lugar en memoria —la famosa “caja”—<br />
para alojar un dato de tipo entero (en este caso), y guarda en p la dirección<br />
de ese lugar, dejando a la caja disponible pero sin valor asignado.<br />
✎ Es posible que no haya más memoria para reservar, cosa improbable con<br />
las computadoras actuales y los programas que nosotros usaremos, por lo<br />
que no nos preocuparemos por esta eventualidad. En programación más<br />
“seria” también debemos preocuparnos por dejar libres los lugares obtenidos<br />
mediante “ new ”yqueyanoserán usados, que en Pascal se hace<br />
mediante “ dispose ”(quenoveremos).<br />
La variable (entera) a la que ahora “apunta” p se indica por pˆ. Así, si<br />
queremos poner el número3allí, hacemos<br />
p^ := 3;<br />
Es un error hacer una asignación a pˆ sin haber<br />
hecho antes una reserva de espacio con “ new ”.<br />
En la figura 14.1 esquematizamos la variable p yla“caja”pˆ alaque<br />
apunta. El valor alojado en p será asignado por la máquina al hacer “ new(p) ”<br />
y desconocido por nosotros, mientras que pˆ tiene alojado el valor 3 hecho<br />
por la asignación “ p^:= 3 ”. Los tamaños de las “cajas” p y pˆ notienenpor<br />
qué coincidir.<br />
p<br />
3<br />
pˆ<br />
Figura 14.1: El puntero p y la variable apuntada pˆ.
14.2. Listas encadenadas Pág. 157<br />
En (estándar) Pascal no es posible obtener la dirección de memoria de una<br />
variable. Por ejemplo si n es una variable entera, no podemos asignar a p la<br />
dirección de n. Por supuesto que no podemos hacer<br />
p := n (* incorrecto! *)<br />
pero si p y q son punteros del mismo tipo (declarado mediante “ type ”), es legal<br />
hacer<br />
q := p<br />
aún cuando p o q no hagan referencia a dato alguno, ya que estamos copiando<br />
en q la dirección guardada en p. Por otro lado, supuesto que p y q sean del<br />
mismo tipo y que ambos se hayan “inicializado” con new, podemos copiar los<br />
contenidos de una caja a la otra mediante<br />
q^ := p^<br />
Nuestro interés en este tipo de variables es en la creación de listas encadenadas.<br />
14.2. Listas encadenadas<br />
Una lista encadenada, o simplemente lista (1) , consiste en una serie de elementos,<br />
llamados nodos (2) unidos entre sí. Los nodos son registros (“ record ”’s),<br />
uno de cuyos campos es un puntero que señala al próximo elemento de la lista.<br />
Una declaración típica para los nodos es:<br />
type<br />
ptrnodo = ^nodo;<br />
nodo = record<br />
info: integer; (* puede haber mas de un<br />
campo de este estilo *)<br />
siguiente: ptrnodo<br />
end;<br />
Observar que la definición de los nodos es de alguna forma recursiva, ya que<br />
el tipo “ ptrnodo ”sólo puede definirse si está definido el tipo “ nodo ”, que a<br />
su vez sólo puede definirse si está definido el tipo “ ptrnodo ”. Sin embargo esta<br />
recursión es un tanto aparente pues las direcciones de memoria siempre ocupan<br />
el mismo lugar (que dependerá de la computadora y sistema operativo). Así, la<br />
reserva de espacio para “ ptrnodo ” es siempre la misma, independientemente<br />
del espacio que deba reservarse para una variable de tipo “ nodo ”.<br />
Las listas encadenadas tienen asociados un puntero donde se guarda la dirección<br />
donde comienza la lista. En nuestra implementación consideraremos que<br />
todos los nodos de la lista tienen información (3) , por lo que la lista “vacía” tiene<br />
el puntero asociado en nil (como nada en el programa arbolbinario). Del mismo<br />
modo, si la lista no es vacía, su último nodo apunta a nil como sucesor.<br />
(1) A veces, para confundir, también hablamos de listas cuando nos referimos a arreglos.<br />
(2) Como en los árboles binarios o grafos dirigidos. En realidad, las listas encadenadas se<br />
pueden pensar como grafos dirigidos.<br />
(3) Nuestra presentación difiere de otras donde siempre hay al menos un nodo.
Pág. 158 Punteros y listas encadenadas<br />
En forma similar a la descripción que hicimos de pilas o colas en general en<br />
la sección 12.1 (pág. 119), las operaciones fundamentales con listas son: inicialización,<br />
agregar nodos a la lista, sacar nodos y recorrer la lista.<br />
Suponiendo que lista es la variable declarada como “ ptrnodo ” que guarda<br />
el comienzo de la lista, tenemos:<br />
Inicialización de lista. Al comienzo la lista no tiene nodos, y ponemos<br />
lista := nil<br />
Agregar un nodo a la lista. Si queremos agregar el nodo p de tipo ptrnodo<br />
“adelante” en la lista, ponemos<br />
p^.siguiente := lista; lista := p<br />
pero puede ser que querramos construir la lista de otra forma, por ejemplo<br />
si queremos que esté ordenada.<br />
Si queremos agregar siempre atrás, es conveniente guardar también la dirección<br />
del último nodo de la lista, y debe preverse en la inicialización.<br />
Agregar un nodo antes o después de otro. Supongamos que p y q son del<br />
tipo “ ptrnodo ”, que pˆ esunelementodelalistayqˆ debe agregarse<br />
después de pˆ. El esquema simple es:<br />
q^.siguiente := p^.siguiente; p^.siguiente := q<br />
Si se desea insertar qˆ antes que pˆ, y no conocemos el antecesor de pˆ,<br />
podemos insertar qˆ después de pˆ y luego intercambiar las datos correspondientes:<br />
q^.siguiente := p^.siguiente; p^.siguiente := q;<br />
temp := p^.info; p^.info := q^.info; q^.info := temp<br />
Eliminar un elemento. Para sacar el nodo siguiente a pˆ, podemos hacer:<br />
q := p^.siguiente; p^.siguiente := q^.siguiente<br />
No podemos eliminar el nodo pˆ si no conocemos su antecesor (salvo que<br />
sea el primero de la lista). En este caso habrá que recorrer la lista hasta<br />
encontrar pˆ, guardando información sobre los antecesores.<br />
Recorrer la lista. Es bastante sencillo en nuestro caso (donde no hay nodos<br />
especiales para el principio o fin de lista):<br />
p := lista;<br />
while (p nil) do begin<br />
haceralgo; (* haceralgo con p o p^ *);<br />
p := p^.siguiente<br />
end
14.2. Listas encadenadas Pág. 159<br />
Problema 14.1. En el programa listas (pág. 206) se ven algunas posibilidades<br />
para construir listas, tanto agregando nodos adelante como en el medio para<br />
formar una lista ordenada por id, y el recorrido de una lista, para buscar una<br />
información o imprimir. Al final del listado del programa damos un ejemplo de<br />
salida (pág. 209).<br />
a) Estudiar cuidadosamente los distintos tipos de listas y la salida.<br />
b) Agregar un procedimiento o función para contar los elementos de una lista,<br />
imprimiendo luego el número. ✄<br />
Problema 14.2 (Listas recursivas). La definición de alguna forma recursiva<br />
del tipo “ ptrnodo ” nos hace pensar que podemos usar recursión para varias de<br />
las tareas.<br />
a) Por ejemplo, para imprimir una lista podríamos usar:<br />
procedure enorden (p: ptrnodo);<br />
begin<br />
if (p nil) then begin<br />
with p^.info do writeln(nombre:tamanio, id:10);<br />
enorden(p^.siguiente)<br />
end<br />
end;<br />
haciendo la llamada a “ enorden(lista)” (eventualmente imprimiendo un<br />
cartel antes si la lista es vacía). Agregar este procedimiento para comprobar<br />
su comportamiento.<br />
b) Cambiar el procedimiento enorden por enordeninverso, enelqueprimero<br />
se hace la llamada recursiva y luego se imprime info, y verificar su comportamiento.<br />
✄<br />
Problema 14.3. Hacer un procedimiento para construir una copia de una lista<br />
encadenada, es decir, una lista con tantos nodos como la original, y en la cual<br />
la información se guarda en el mismo orden que la lista original. ✄<br />
Problema 14.4. Hacer un procedimiento para “invertir” una lista de modo de<br />
que el nodo que estaba en primer lugar esté al final en la nueva lista y viceversa,<br />
etc., sin crear nuevos nodos (i.e. sin usar “ new ”).<br />
Sugerencia: ir formando una nueva lista en la que se incorpora el primer nodo<br />
de la vieja, borrándolo de la vieja, y continuar hasta terminar la lista original.<br />
Sugerencia si la anterior no alcanza: usar el esquema<br />
procedure darvuelta(var lista: ptrnodo);<br />
var p, q: ptrnodo;<br />
begin<br />
q := lista; lista := nil;<br />
while (q nil) do begin<br />
p := q; q := q^.siguiente;<br />
p^.siguiente := lista; lista := p<br />
end<br />
end;<br />
✎ El problema 8.3 hacía lo mismo para un vector, pero la técnica es muy<br />
diferente. En cambio, el esquema propuesto usa la idea del procedimiento<br />
“ enordeninverso ”delproblema14.2, agregando el nodo adelante en vez<br />
de imprimir. ✄
Pág. 160 Punteros y listas encadenadas<br />
Problema 14.5 (El problema de Flavio Josefo II). Implementar una solución<br />
a este problema (problema 6.7) usando en vez de arreglos una lista circular,<br />
i.e. el “último” nodo se encadena al “primero”, donde cada nodo representa una<br />
persona, y la eliminación se traduce en eliminar el nodo de la lista. El programa<br />
debe imprimir la permutación de Flavio Josefo.<br />
Sugerencia: crear una lista quedan de los que “quedan”, inicialmente con los valores<br />
1,...,n, donde el último nodo apunta al primero en vez de a nil. A medida<br />
que se eliminan nodos de quedan, ir formando la permutación de Flavio Josefo.<br />
En cada paso de “eliminación”, guardar información sobre el nodo anterior al<br />
eliminado, para eliminar el nodo y poder contar m de los que quedan a partir<br />
de él.<br />
✎ Recordar también el uso de “ mod ”enelproblemaoriginal. ✄<br />
Problema 14.6. Siguiendo las ideas del problema 14.4 y del procedimiento<br />
ordenada en el programa listas (pág. 206), hacer un procedimiento para ordenar<br />
(por alguna llave) una lista ya ingresada sin generar nuevos nodos. ✄<br />
Problema 14.7. Si una lista se modifica sólo agregando adelante o quitando<br />
el primer elemento, tenemos la estructura de “pila” que hemos usado para resolver<br />
el problema de las torres de Hanoi (problema 11.5). Hacer un programa<br />
resolviendo ese problema usando listas encadenadas en vez de arreglos. ✄<br />
14.3. Otras estructuras dinámicas<br />
En la construcción de listas, hemos colocado un puntero en los registros<br />
(“records”) para apuntar al siguiente elemento en la lista terminando con nil<br />
o volviendo a apuntar al principio de la lista para formar una lista “circular”.<br />
También podemos pensar en agregar en cada registro un puntero para señalar<br />
al elemento anterior (formando una lista doblemente encadenada), pero siempre<br />
podemos visualizar estas listas como “lineales” o “unidimensionales”.<br />
Podemos pensar, en cambio, en agregar punteros en los registros de modo<br />
de señalar a otras listas, que pueden tener el mismo tipo de registros o no.<br />
Si por ejemplo en el programa listas en vez de tener los nodos un campo<br />
para punteros, tenemos dos, y llamamos al primero “izquierdo” y al segundo<br />
“derecho”, obtenemos otra implementación de los árboles binarios ordenados<br />
que vimos en la sección 12.6, reemplazando la “pila” del arreglo por listas encadenadas.<br />
Problema 14.8. Declarando<br />
type<br />
ptrnodo = ^nodo;<br />
nodo = record<br />
llave: integer; (* numero leido *)<br />
cuenta: integer; (* veces que aparecio *)<br />
izquierda: ptrnodo; (* hijo a la izquierda *)<br />
derecha: ptrnodo (* hijo a la derecha *)<br />
end;<br />
rehacer el programa arbolbinario (pág. 197) reemplazando los arreglos por listas<br />
encadenadas, y luego realizar los incisos del problema 12.6. ✄
14.3. Otras estructuras dinámicas Pág. 161<br />
Recordemos que en el problema 8.6 estudiamos un arreglo bidimensional<br />
donde el primer índice indicaba el número de renglón, mientras que el segundo<br />
índice indicaba el número de carácter dentro de ese renglón. Al tener la estructura<br />
de arreglo una dimensión fija, estamos desperdiciando mucho espacio si<br />
queremos tener renglones de tamaños muy diferentes, o si no tenemos mucha<br />
idea de cuántos renglones habrá en el texto completo. Es mucho más apropiado<br />
tomar una lista encadenada, representando cada renglón, y donde sus nodos<br />
apuntan no sólo al siguiente renglón sino también a una lista con el texto de ese<br />
renglón. Los editores de textos trabajan con una estructura similar (escribiendo<br />
luego secuencialmente en el disco cuando guardamos el archivo).<br />
Un problema matemático donde se puede usar una idea similar es en grafos,<br />
al tener la descripción por lista de aristas o por vértices adyacentes a otro dado.<br />
Problema 14.9. Como hemos visto en el capítulo anterior, un grafo G consiste<br />
de un conjunto de vértices V = {1, 2,...,n}, y un conjunto de aristas E, donde<br />
cada arista es de la forma {u, v} con u, v ∈ V , u = v (y supondremos que no<br />
hay aristas repetidas).<br />
Es usual representar los vértices mediante puntos y las aristas con segmentos<br />
o curvas que unen los vértices, como se muestra en la figura 13.1 (pág. 134), en<br />
donde n =6yE = {{1, 2}, {1, 3}, {2, 3}, {2, 6}, {3, 4}, {3, 6}, {4, 6}}.<br />
Por otra parte, en vez de describir G mediante E (y V ), podríamos hacerlo<br />
a partir de las listas de adyacencias o vecinos: para cada u ∈ V el conjunto de<br />
vértices {v ∈ V : {u, v} ∈E}. En el ejemplo anterior, podríamos poner:<br />
1 →{2, 3}, 2 →{1, 3, 6}, 3 →{1, 2, 4, 6}, 4 →{3, 6}, 5 →∅, 6 →{2, 3, 4}.<br />
Para representar el grafo en la computadora usando listas encadenadas (4) ,<br />
podemos recurrir a las estructuras:<br />
Para lista de aristas: Una lista de aristas, donde cada arista se representa<br />
mediante<br />
ptrarista = ^arista;<br />
arista = record u, v: integer; s: ptrarista end;<br />
Para lista de adyacencias: Una lista de vértices, donde cada vértice<br />
tiene un puntero a una lista de vecinos:<br />
ptrvecino = ^vecino;<br />
vecino = record id: integer; s: ptrvecino end;<br />
ptrvertice = ^vertice;<br />
vertice = record<br />
id: integer;<br />
s: ptrvertice;<br />
vecinos: ptrvecino<br />
end;<br />
El programa grafos (pág. 209) imprime la listas de adyacencias si el grafo<br />
es ingresado dando n y E. Estudiarlo y modificarlo de modo de realizar la<br />
operación inversa, i.e. imprimir los aristas cuando el grafo se ingresa dando n y<br />
las listas de adyacencias. ✄<br />
(4) En el capítulo 13 hemos considerado algunas representaciones con arreglos y matrices.
Pág. 162 Punteros y listas encadenadas<br />
14.4. Problemas Adicionales<br />
Problema 14.10 (Ciclo de Euler). En el problema 13.7 nos hemos referido<br />
a la existencia de ciclos de Euler, y nos preocuparemos ahora por encontrar<br />
efectivamente uno.<br />
Para demostrar que un grafo conexo con todos los vértices de grado par tiene<br />
unciclodeEuler,secomienzaapartirdeunvértice y se van recorriendo aristas<br />
y borrándolas hasta que volvamos al vértice original, formando un ciclo. Esto<br />
debe suceder porque todos los vértices tienen grado par. Puede ser que el ciclo<br />
no cubra a todas las aristas, pero como el grafo es conexo, debe haber un vértice<br />
en el ciclo construido que tenga una arista (aún no eliminada) incidente en él, a<br />
partir del cual podemos formar un nuevo ciclo, agregarlo al anterior y continuar<br />
con el procedimiento hasta haber eliminado recorrido todas las aristas.<br />
Esta demostración es bien constructiva, y podemos implementar los ciclos<br />
como listas encadenadas. La implementación de aristas, etc. puede hacerse con<br />
arreglos o matrices.<br />
Hacer un programa para decidir si un grafo ingresado como en el programa<br />
anchoprimero (pág. 199) es conexo y todos los vértices tienen grado par, y<br />
en caso afirmativo construir e imprimir un ciclo de Euler. ✄<br />
14.5. Comentarios Bibliográficos<br />
La presentación de punteros y listas encadenadas está basada en [16], tomando<br />
también ideas de [10].
Capítulo 15<br />
Más problemas<br />
En este capítulo agregamos más problemas para los entusiastas o los que simplemente<br />
quieran practicar más (¡por ejemplo para algún examen!), no estando<br />
ordenados ni por tema ni por dificultad.<br />
Problema 15.1 (Polinomios interpoladores de Lagrange). Un polinomio<br />
P (x) =anx n + an−1x n−1 + ···+ a1x + a0, de grado a lo sumo n, está determinado<br />
por los n + 1 coeficientes. Supongamos que no conocemos los coeficientes,<br />
pero podemos conocer los valores de P (x) en algunos puntos, ¿cuántos puntos<br />
necesitaremos para determinar los coeficientes? Como hay n + 1 coeficientes,<br />
es natural pensar que quedarán determinados por n + 1 ecuaciones, i.e. que<br />
bastarán n + 1 puntos.<br />
Este resultado es cierto, y se puede demostrar planteando un sistema de n+1<br />
ecuaciones lineales, y viendo que su determinante —de Van der Monde— que<br />
se estudia en Álgebra Lineal es no nulo.<br />
Dados (xi,yi), i =1,...,n+1, elpolinomio interpolador de Lagrange se<br />
define como<br />
n+1 <br />
P (x) =<br />
<br />
yi<br />
xi − xj<br />
i=1 j=i<br />
x − xj<br />
. (15.1)<br />
✎ El polinomio en general resulta de grado ≤ n, y no necesariamente n.<br />
Pensar, por ejemplo, en 3 puntos sobre una recta: determinan un polinomio<br />
de grado 1 y no 2.<br />
✎ En cálculo numérico se ven formas más eficientes (esencialmente extensiones<br />
de la regla de Horner) para el cómputo de estos polinomios interpoladores.<br />
1<br />
0.5<br />
π<br />
6<br />
π<br />
4<br />
π<br />
2<br />
Figura 15.1: Aproximación de sen x (en trazo discontinuo) mediante un polinomio<br />
de grado 3.<br />
π
Pág. 164 Más problemas<br />
a) Ver que efectivamente, P (x) definido por la ecuación (15.1)satisfaceP (xi) =<br />
yi para i =1,...,n+1.<br />
b) Desarrollar un procedimiento para evaluar P (x) dado por la ecuación (15.1),<br />
donde los datos son (xi,yi), 1 ≤ i ≤ n +1, y x. Aclaración: sólose pide una<br />
traducción “literal” de la ecuación (15.1).<br />
c) Utilizarlo en un programa que calcule los coeficientes del polinomio de grado<br />
a lo sumo 3 que pasa por los puntos (−1, 0), (0, 1), (1, 0), (2, 2),enelpunto<br />
x = −2.<br />
d) Ampliar el programa para calcular una aproximación de sen π/4, usando los<br />
valores del seno para 0,π/6,π/2yπ.<br />
✎ Es usual poner π =4×arctan 1, aunque en este problema en particular<br />
no se necesita un valor específico de π (considerar la función sen πx en<br />
vez de sen x).<br />
La figura 15.1 muestra cómo se parecen las gráficas de sen x y el polinomio<br />
en el intervalo [0,π].<br />
Aunque considerado como francés, Joseph-Louis Lagrange (1736–1813) nació<br />
enTurín (Italia) como Giuseppe Lodovico Lagrangia.<br />
Lagrange fue uno de los fundadores del “cálculo de variaciones” (área relacionada<br />
con la mecánica) y las probabilidades, e hizo numerosas contribuciones<br />
en otras áreas como astronomía y ecuaciones diferenciales. ✄<br />
Problema 15.2 (Cuenta de Goldbach). En el problema 5.23 hablamos de<br />
la conjetura de Golbach, que todo número par mayor o igual a 6 puede escribirse<br />
como suma de dos primos impares.<br />
La cuenta de Goldbach es la cantidad de formas en las que un número par<br />
puede escribirse como suma de dos primos impares. Por ejemplo, 20 = 13 + 7 =<br />
17 + 3, por lo que la cuenta de Goldbach de 20 es 2.<br />
Hacer un programa para determinar la cuenta de Goldbach de todos los<br />
pares entre 2 y 10000, y encontrar el máximo. ✄<br />
Problema 15.3. Hacer un programa para simular el juego de la generala en el<br />
que se tiran 5 dados (una única vez), y se tiene “full” cuando hay 3 dados con<br />
un mismo número y los otros 2 con otro número, “póker” cuando hay 4 dados<br />
iguales, “escalera” cuando hay 5 números consecutivos, y “generala” cuando los<br />
5 dados son iguales. Sugerencia: una vez “tirados” los dados, clasificarlos para<br />
determinar si se ha obtenido alguno de los juegos. ✄<br />
Problema 15.4 (Visibilidad en el plano). Consideremos el reticulado C<br />
definido por los puntos del plano con ambas coordenadas enteras. Diremos que<br />
el punto P ∈Ces visible (desde el origen O) si el segmento que une P y O no<br />
contiene otros puntos de C distintos de P y O.<br />
a) Si a, b ∈ N, entonces (a, b) es visible ⇔ mcd(a, b) =1.<br />
b) Desarrollar un programa que dado n ∈ N calcule sn, la cantidad de puntos<br />
visibles en el cuadrado 1 ≤ a, b ≤ n.<br />
c) Se puede demostrar que a medida que n crece, pn = sn/n2 se aproxima<br />
a p =6/π2 =0.6079 ....Calcularpnpara distintos valores de n para ver<br />
que este es el caso. Sugerencia: empezar con n pequeño e ir aumentando<br />
paulatinamente su valor. ✄
Problema 15.5 (Aproximando e con dados). Hacer un programa para<br />
simular una máquina que emite números al azar (uniformemente distribuidos)<br />
en el intervalo (0, 1) uno tras otro hasta que su suma excede 1. Comprobar que<br />
al usar la máquina muchas veces, la cantidad promedio de números emitidos es<br />
aproximadamente e =2.71828 ....<br />
✎<br />
Éste es otro ejemplo del método de Monte-Carlo, esta vez para aproximar<br />
e. ✄<br />
Problema 15.6 (Generando “manos” aleatoriamente). Queremos resolver<br />
el problema de encontrar una “mano” de los números enteros entre 1 y n, es<br />
decir, queremos “mezclar” un mazo de n cartas de un mismo palo (matemáticamente,<br />
obtener una permutación aleatoria de los números entre 1 y n).<br />
Presentamos varias posibilidades:<br />
a) Imaginando que empezamos con la cartas numeradas de 1 a n, ordenadas<br />
crecientemente, hacemos el siguiente procedimiento: sacamos una carta al<br />
azar entre 1 y n y la separamos, de las restantes sacamos una al azar (entre<br />
1yn − 1) y la separamos, y así sucesivamente. Sin embargo, computacionalmente<br />
esta tarea no es sencilla, debido al manejo que debemos hacer de<br />
listas.<br />
b) Una variante es: al elegir la primera carta la colocamos adelante y ponemos<br />
la primera carta en el lugar de la elegida. Luego elegimos al azar alguna de<br />
las cartas que están en las posiciones entre 2 y n y la intercambiamos con la<br />
segunda, y así sucesivamente. Implementar esta idea computacionalmente.<br />
c) En el algoritmo anterior, podríamos reemplazar el intercambiar la carta<br />
elegida con la primera, después con la segunda, etc., por intercambiar con<br />
la última, después con la ante-última, etc. Implementar también esta idea<br />
computacionalmente.<br />
d) Usando alguno de los algoritmos anteriores, hacer un programa para simular<br />
una “mano” de m cartas de un total de n. Por ejemplo, para un jugador en<br />
el truco elegiríamos m =3yn = 40.<br />
✎ ¡Noesnecesariogenerartodalapermutación!<br />
Aunque el problema de las “manos” es muy sencillo de plantear, y se usa<br />
con mucha frecuencia, encontrar algoritmos correctos y eficientes no es sencillo.<br />
La corrección de un algoritmo se refiere a que 1) todas las permutaciones<br />
de 1,...,n se pueden generar de esta forma, y 2) con igual probabilidad. Otro<br />
problema es el de decidir su eficiencia: cuánto tiempo e intercambios realiza en<br />
términos de n. Ninguna de estas preguntas es fácil de responder.<br />
✎ Observar la similitud entre la clasificación por selección directa o inserción<br />
directa con la obtención de “manos” aleatorias en los incisos b) ya)<br />
respectivamente.<br />
Estas similitudes son la base para el estudio teórico de los métodos de<br />
clasificación: peor caso, caso promedio, etc.<br />
✎ El algoritmo en b) estápresentadoen[11, Vol.2,pág. 126], donde se hacen<br />
los análisis de corrección y eficiencia. Es atribuído independientemente a<br />
L. E. Moses y R. V. Oakford (1963) y R. Durstenfeld (1964).<br />
✎ Existen muchos otros algoritmos relacionados con este problema. Los interesados<br />
pueden consultar el libro de Knuth ya mencionado. Otro algoritmo<br />
interesante, debido a R. Floyd, está descripto en [2]. ✄<br />
Pág. 165
Pág. 166 Más problemas<br />
Problema 15.7 (Clasificación por fusión). Los métodos elementales usan<br />
del orden de n 2 comparaciones y/o asignaciones para clasificar un arreglo de<br />
longitud n. En contraposición, los métodos más avanzados usan del orden de<br />
n × log 2 n operaciones. Entre los más “populares” de este tipo podemos mencionar<br />
al heapsort o clasificación por montón, quicksort o clasificación rápida, y<br />
clasificación por fusión o mezcla (ya mencionado en la sección 10.3), que veremos<br />
ahora.<br />
No es difícil explicar este último método, cuya idea es dividir el arreglo<br />
de a pares, y luego ir fusionando —como en el problema 10.4— los subarreglos:<br />
primero juntar pares consecutivos, luego cuaternas consecutivas, etc. hasta llegar<br />
obtener el arreglo completo. Por ejemplo, el método realiza los siguientes pasos<br />
al clasificar (3, 6, 7, 5, 4, 1, 2):<br />
1. 3 6 75412 se juntan de a pares (o lo que se pueda),<br />
2. 3 6 57142 se ordena cada par (i.e. se “fusiona” cada grupo),<br />
3. 3 6 57142 se agrupan de a cuatro (o lo que se pueda),<br />
4. 3567124 se “fusiona” cada grupo,<br />
5. 3567124 se agrupan de a ocho (o lo que se pueda),<br />
6. 1234567 y se fusionan.<br />
A fin de implementar este algoritmo, usamos el procedimiento clasificar que<br />
llama, a su vez, al fusionar:<br />
procedure fusionar(<br />
var a, b: arreglo; (* entra a, sale b *)<br />
inic, medio, fin: integer (* partes a fusionar *)<br />
);<br />
var<br />
i, j, k: integer;<br />
begin<br />
i := inic; j := medio + 1; k := inic - 1;<br />
repeat<br />
k := k + 1;<br />
if (a[i] medio) or (j > fin));<br />
(* copiar lo que falta *)<br />
while (j
a2: arreglo;<br />
begin<br />
k := 1; (* tamanio de subarreglos a fusionar *)<br />
veces := 0; (* las veces que paso por el lazo *)<br />
while (k < n) do begin<br />
veces := veces + 1; k2 := 2 * k;<br />
i := 1; m := k; f := k2;<br />
if ((veces mod 2) = 0) then begin<br />
while (f
Pág. 168 Más problemas<br />
ficar que su comportamiento es correcto, y cotejar el número de asignaciones<br />
(entre arreglos) y comparaciones con los otros algoritmos elementales a fin<br />
de obtener un cuadro similar al 10.1 (sin los tiempos, y para arreglos más<br />
chicos). ✄<br />
Problema 15.8. Al estudiar los datos numéricos (a1,a2,...,an) estadísticamente,<br />
se consideran (entre otros) tres tipos de “medidas”:<br />
La media o promedio: 1<br />
n<br />
ai.<br />
n<br />
i=1<br />
La mediana: Intuitivamente es un valor aℓ tal que la mitad de los datos<br />
son menores o iguales que aℓ y la otra mitad son mayores o iguales que<br />
aℓ. Para obtenerla, se ordenan los datos y la mediana es el elemento<br />
del medio si n es impar y es el promedio de los dos datos en el medio<br />
si hay un número par de datos.<br />
✎ En realidad no es necesario ordenar todos los datos para obtener<br />
la mediana. Por otra parte, observar que la mediana puede no<br />
serundatoenelcasoden par.<br />
La moda: Es un valor aℓ con frecuencia máxima, y puede haber más de<br />
una moda.<br />
Por ejemplo, si los datos son 8, 0, 4, 6, 7, 8, 3, 2, 7, 4, la media es 4.9, la mediana<br />
es 5, y las modas son 4, 7 y 8.<br />
Hacer un programa para generar aleatoriamente un arreglo de longitud n<br />
de números entre 0 y 9, y luego encontrar la media, mediana y moda/s (n es<br />
ingresado por el usuario). ✄<br />
Problema 15.9 (Números de Fibonacci: Zeckendof (1972)). Todo número<br />
entero se puede escribir, de forma única, como suma de (uno o más) números<br />
de Fibonacci no consecutivos, es decir para todo n ∈ N existe una única representación<br />
de la forma<br />
n = b2f2 + b3f3 + ···+ bmfm,<br />
donde bm =1,bk ∈{0, 1}, ybk · bk+1 =0parak =2,...,m− 1.<br />
Por ejemplo, 10 = 8 + 2 = f6 + f3,27=21+5+1=f8 + f5 + f1.<br />
Hacer un programa para calcular esa representación (dando por ejemplo una<br />
lista de los índices k para los que bk =1en(15.9)). Comprobar con algunos<br />
ejemplos la veracidad de la salida. ✄<br />
Problema 15.10 (La Cola del Supermercado). Hacer un programa que<br />
simule una “cola”, donde el tiempo entre dos llegadas consecutivas de clientes<br />
puede considerarse como un número entero (aleatorio y uniformemente distribuido)<br />
de entre 1 y 5 minutos. Suponer que la atención de cada cliente dura<br />
un tiempo fijo de 2 minutos. Hacer la simulación por un período de 8 horas,<br />
computando el porcentaje de uso del servicio, el tiempo que no se ha usado,<br />
la longitud promedio de la cola, el tiempo de espera promedio de los clientes<br />
ycuántos clientes no fueron atendidos al final del período (se supone que los<br />
clientes pueden comenzar a ser atendidos hasta el último instante). Variar los<br />
datos de frecuencia de clientes y tiempo de atención para ver cómo varía el<br />
comportamiento de la “cola”, por ejemplo incrementar el tiempo de atención de<br />
2 a 5 minutos. Discutir estos resultados, experimentando: tiempo de atención
para que los clientes no esperen más de 3 minutos (en promedio), o para que la<br />
cola no crezca indefinidamente, etc. ✄<br />
Problema 15.11 (La Producción de la Fábrica). Supongamos que una<br />
pequeña empresa fabrica un cierto tipo de producto. La empresa quiere contratar<br />
obreros, cada uno de los cuales, con las herramientas disponibles, podrá fabricar<br />
28 unidades diarias del producto. Nuestro objetivo es, mediante simulación,<br />
determinar la cantidad de obreros necesarios para que el atraso en la producción<br />
sea razonable, sin tener mano de obra ociosa excesiva. (Suponemos que no se<br />
mantiene stock).<br />
De experiencia anterior se sabe que los pedidos diarios del producto (la<br />
demanda) sigue en promedio una distribución como se indica:<br />
promedio pedido (unidades) 100 200 300 400 500 600<br />
frecuencia 0.15 0.25 0.18 0.22 0.13 0.07<br />
a) Hacer un programa para analizar la producción diaria durante un cierto<br />
período, digamos durante 30 ó60días, para ver qué cantidad de obreros<br />
será necesaria.<br />
b) Comprobar que los pedidos siguen (aproximadamente) la ley dada por las<br />
frecuencias. (O sea que hay pedidos de 100 el 15 % de las veces, de 200 el<br />
40 %, etc.). Tal vez sea conveniente aumentar la longitud del período de<br />
simulación. ✄<br />
Problema 15.12 (Pasos en búsqueda binaria). La función f que estudiamos<br />
en este problema está relacionada con la cantidad de pasos que se realizan<br />
en la búsqueda binaria cuando hay n elementos.<br />
Consideremos f : N → N definida por f(n) =⌊(n +1)/2⌋, yparak ∈ N,<br />
F (k, n) =f k (n) =f(···(f(n))<br />
···).<br />
<br />
k veces<br />
a) Hacer un programa para calcular F (k, n) usando un lazo “ for ”, recordando<br />
que para n ∈ N, ⌊(n +1)/2⌋puede escribirse en Pascal como (n + 1) div<br />
2.<br />
b) ¿Cuál es el valor de F (k, 2k )? (probar con el programa para algunos valores<br />
de k, hacer una conjetura y tratar de demostrarla).<br />
c) Para cada n>2, existe j = g(n) talqueF (k, n) > 1sik
Pág. 170 Más problemas<br />
A, toma cualquier número de fósforos de un fila, pudiendo tomar<br />
uno, dos o hasta toda la fila, pero sólo debe modificar una fila. El<br />
jugador B juega de manera similar con los fósforos que quedan, y<br />
los jugadores van alternándose en sus jugadas. Gana el jugador que<br />
saca el último fósforo.<br />
Veremos que, dependiendo de la posición inicial, uno u otro jugador tiene<br />
siempre una estrategia ganadora. Para esto, digamos que una disposición de los<br />
fósforos es una posición ganadora para el jugador X, sidejando X los fósforos<br />
en esa posición, entonces no importa cual sea la jugada del oponente, X puede<br />
jugar de forma tal de ganar el juego. Por ejemplo, la posición en la cual hay dos<br />
filas con dos fósforos cada una, es ganadora: si A deja esta posición a B, B debe<br />
tomarunoodosfósforos de una fila. Si B toma 2, A toma los dos restantes. Si<br />
B toma uno, A toma uno de la otra fila. En cualquier caso, A gana.<br />
a) Ver que la posición en donde hay 3 filas con 1, 2 y 3 fósforos respectivamente,<br />
es ganadora.<br />
Para encontrar una estrategia ganadora, formamos una tabla expresando el<br />
número de fósforos de cada fila en binario, uno bajo el otro, poniendo en una<br />
fila final “ P ” si la suma total de la columna correspondiente es par, e “ I ”en<br />
otro caso. Por ejemplo, en las posiciones anteriores haríamos:<br />
1 0<br />
1 0<br />
P P<br />
y<br />
0 1<br />
1 0<br />
1 1<br />
P P<br />
En el último ejemplo, es conveniente poner 01 en vez de sólo 1 en la primer<br />
fila a fin de tener todas las filas con igual longitud.<br />
Si la suma de cada columna es par decimos que la posición es correcta, y<br />
en cualquier otro caso decimos que la posición es incorrecta. Por ejemplo, la<br />
posición donde hay 1, 3 y 4 fósforos es incorrecta:<br />
| = 0 0 1<br />
| | | = 0 1 1<br />
| | | | = 1 0 0<br />
I I P<br />
b) En este inciso, veremos que una posición en Nim es ganadora si y sólo si es<br />
correcta.<br />
i) Si ninguna fila tiene más de un fósforo, la posición es ganadora ⇔ hay<br />
en total un número par de fósforos ⇔ la posición es correcta.<br />
ii) Si un número a ∈ N, expresado en binario por (an,an−1,...,a1,a0)<br />
(permitiendo an = 0) es reemplado por otro menor b (entero ≥ 0),<br />
expresado en binario como (bn,bn−1,...,b1,b0), entonces para algún<br />
i, 0≤ i ≤ n, las paridades de ai y bi son distintas.<br />
iii) Por lo tanto, si el jugador X recibe una posición correcta, necesariamente<br />
la transforma en incorrecta.<br />
iv) Supongamos que estamos en una posición incorrecta, es decir, al menos<br />
la suma de una columna es impar. Para fijar ideas supongamos que las<br />
paridades de las columnas son
P P I P I P<br />
Entonces hay al menos un 1 en la tercera columna (la primera con<br />
suma impar). Supongamos, otra vez para fijar ideas, que una fila en la<br />
cual esto pasa es (en binario)<br />
- -<br />
0 1 1 1 0 1<br />
donde marcamos con “ - ”quelosnúmeros debajo están en columnas<br />
de suma impar. Cambiando 0’s y 1’s por 1’s y 0’s en las posiciones<br />
marcadas, obtenemos el número (menor que el original, expresado en<br />
binario):<br />
- -<br />
0 1 0 1 1 1<br />
Estáclaroqueestecambiocorrespondeaunmovimientopermitido,<br />
haciendo la suma de cada columna par, y que el argumento es general.<br />
v) Por lo tanto, si el jugador X recibe una posición incorrecta, puede<br />
moverdemododedejarunaposición correcta.<br />
vi) Si A deja una posición correcta, B necesariamente la convierte en<br />
incorrecta y A puede jugar dejando una posición correcta. Este proceso<br />
continuará hasta que cada fila quede vacía o contenga un único fósforo<br />
(el caso i)).<br />
c) En base a los incisos anteriores, y suponiendo que A y B siempre juegan de<br />
modo óptimo, ¿quién ganará?<br />
d) Ver que la “jugada ganadora” del inciso b.iv) nosiempreesúnica.<br />
e) Desarrollar un programa que, ingresada una posición inicial en la forma de<br />
una lista con el número de fósforos en cada fila, decida si la posición es<br />
correcta o incorrecta, y en este caso encuentre una jugada ganadora.<br />
f )¿cuál es la “estrategia” si el que saca el último fósforo es el que pierde? ✄<br />
Comentarios Bibliográficos<br />
El problema 15.4 está tomado de [4]. Para el problema 15.6, ver las citas<br />
allí mencionadas. El problema 15.10 está tomado de [14], y el problema 15.11<br />
de [1]. El juego de Nim (problema 15.13) y su solución está tomado de [6].<br />
Pág. 171
Apéndice A<br />
Programas mencionados<br />
Problema 2.2: holamundo (pág. 10)<br />
program holamundo(input, output);<br />
(* primer programa *)<br />
begin<br />
writeln(’Hola Mundo!’);<br />
writeln;<br />
writeln(’y Chau!’)<br />
end.<br />
Problema 3.2: sumardos (pág. 16)<br />
program sumardos(input, output);<br />
(* sumar dos numeros enteros *)<br />
var a, b: integer;<br />
begin<br />
writeln(’** Programa para sumar dos numeros enteros’);<br />
writeln;<br />
a := 1;<br />
b := 2;<br />
write(’La suma de ’, a);<br />
write(’ y ’, b);<br />
writeln(’ es ’, a + b);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 3.3: leerdato (pág. 17)<br />
program leerdato(input, output);<br />
(* Leer un entero entrado por terminal *)<br />
var a: integer;
Pág. 174 Programas mencionados<br />
begin<br />
writeln(’** Programa para leer un dato entero’);<br />
writeln;<br />
write(’Entrar un entero: ’); readln(a);<br />
writeln;<br />
writeln(’El entero leido es ’, a);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 3.4: raiz (pág. 18)<br />
program raiz(input, output);<br />
(*<br />
Obtener la raiz cuadrada de un numero real x.<br />
Se supone que x no es negativo.<br />
*)<br />
var x, y: real;<br />
begin<br />
writeln(’** Calcular la raiz cuadrada de x **’);<br />
writeln;<br />
write(’Entrar x (> 0): ’); readln(x);<br />
y := sqrt(x);<br />
writeln;<br />
writeln(’La raiz cuadrada de ’, x, ’ es ’, y);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 3.7: enteroareal (pág. 20)<br />
program enteroareal(input, output);<br />
(* Asignar un entero a un real *)<br />
var<br />
a: integer;<br />
x: real;<br />
begin<br />
writeln(’** Asignar un entero a un real’);<br />
writeln;<br />
write(’Entrar el valor del entero: ’); readln(a);<br />
x := a;<br />
writeln;<br />
writeln( a, ’ cambiado a real es: ’, x);<br />
writeln; writeln(’** Fin **’)<br />
end.
segundos (Problema 3.13) Pág. 175<br />
Problema 3.13: segundos (pág. 22)<br />
program segundos(input, output);<br />
(* Pasar de segundos a horas, minutos y segundos *)<br />
var hs, mins, segs: integer;<br />
begin<br />
write(’** Programa para pasar de segundos’);<br />
writeln(’ a horas, minutos y segundos’);<br />
writeln;<br />
write(’Entrar la cantidad de segundos: ’); readln(segs);<br />
writeln;<br />
writeln(segs, ’ segundos son equivalentes a ’);<br />
mins := segs div 60; segs := segs mod 60;<br />
hs := mins div 60; mins := mins mod 60;<br />
writeln(hs, ’ hs, ’, mins, ’ mins, ’, segs, ’ segs.’);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 3.16: positivo (pág. 22)<br />
program positivo(input, output);<br />
(* dice si un numero entero es positivo *)<br />
var a: integer;<br />
begin<br />
writeln(’** Ver si un numero entero es positivo’);<br />
writeln;<br />
write(’Entrar un numero: ’); readln(a);<br />
writeln;<br />
writeln(’es positivo?: ’, a > 0);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 3.18: caracteres1 (pág. 24)<br />
program caracteres1(input, output);<br />
(* pasar de caracter a numero y viceversa *)<br />
var c: char; i: integer;<br />
begin<br />
writeln(’** Pasar de caracter a numero y viceversa’);<br />
writeln;<br />
write(’ Entrar un caracter: ’); readln(c);<br />
writeln;<br />
i := ord(c);<br />
writeln(’ El numero de orden de ’, c, ’ es ’, i:1);
Pág. 176 Programas mencionados<br />
(* i:1 indica que escribira i en un solo espacio,<br />
o los que sean necesarios *)<br />
write(’ Verificacion:’);<br />
write(’ el caracter correspondiente a ’, i:1);<br />
writeln(’ es ’, chr(i));<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.2: valorabsoluto (pág. 26)<br />
program valorabsoluto(input, output);<br />
(* encuentra el valor absoluto del numero real x *)<br />
var x, y: real;<br />
begin<br />
writeln(’** Encontrar el valor absoluto de un numero real’);<br />
writeln;<br />
write(’Entrar el numero: ’); readln(x);<br />
if (x >= 0) then y := x else y := -x;<br />
writeln;<br />
writeln(’El valor absoluto de ’, x, ’ es ’, y);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.4: comparar (pág. 27)<br />
program comparar(input, output);<br />
(* decidir cual numero entero es mayor o si son iguales *)<br />
var a, b: integer;<br />
begin<br />
writeln(’** Comparar dos numeros enteros’);<br />
writeln;<br />
write(’Entrar un numero entero: ’); readln(a);<br />
write(’Entrar otro numero entero: ’); readln(b);<br />
writeln;<br />
if (a > b) then<br />
writeln(a, ’ es mayor que ’, b)<br />
else if (a < b) then<br />
writeln(b, ’ es mayor que ’, a)<br />
else<br />
writeln(a, ’ es igual a ’, b);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.5: caracteres2 (pág. 27)<br />
program caracteres2(input, output);
esto (Problema 4.11) Pág. 177<br />
(* decidir si un caracter es una letra mayuscula o<br />
minuscula, o no es letra, comparando caracteres *)<br />
var c: char;<br />
begin<br />
writeln(’** Decide si un caracter es una letra ’);<br />
writeln(’ mayuscula o minuscula o no es letra’);<br />
writeln;<br />
write(’ Entrar un caracter: ’); readln(c); writeln;<br />
write(c);<br />
if (’a’
Pág. 178 Programas mencionados<br />
(* hacer una tabla del seno donde los angulos<br />
estan dados en grados *)<br />
const pi = 3.14159265;<br />
var<br />
inicial, final, incremento, grados: integer;<br />
radianes: real;<br />
begin<br />
writeln(’** Hacer una tabla del seno dando valores’);<br />
writeln(’ inicial, final, y del incremento (en grados).’);<br />
writeln;<br />
write(’Entrar el valor inicial (en grados): ’);<br />
readln(inicial);<br />
write(’Entrar el valor final (en grados): ’);<br />
readln(final);<br />
write(’Entrar el valor de incremento (en grados): ’);<br />
readln(incremento);<br />
writeln;<br />
writeln(’Angulo Seno’);<br />
grados := inicial;<br />
while (grados
cifras (Problema 4.16) Pág. 179<br />
while (n > 0) do begin suma := suma + n; n := n-1 end;<br />
writeln;<br />
writeln(’ suma con while al reves: ’, suma:10:0);<br />
(* 10:0 indica que el numero se escribe en<br />
al menos 10 espacios, sin decimales *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.16: cifras (pág. 34)<br />
program cifras(input, output);<br />
(* determinar la cantidad de cifras significativas<br />
(en base 10, ignorando el signo) de un entero<br />
entrado por terminal.<br />
E.g. 246 -> 3, -908 -> 3, pero 0 -> 1 *)<br />
var a, b, c: integer;<br />
begin<br />
writeln(’** Determinar la cantidad de cifras de un’);<br />
writeln(’ entero (sin tener en cuenta el signo)’);<br />
writeln;<br />
write(’Entrar el numero: ’); readln(a); writeln;<br />
if (a < 0) then b := -a else b := a;<br />
c := 0;<br />
repeat<br />
c := c + 1;<br />
b := b div 10<br />
until b = 0;<br />
writeln(’La cantidad de cifras de ’, a:1, ’ es ’, c:1);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.17: epsmin (pág. 34)<br />
program epsmin(input, output);<br />
(*<br />
epsmin = menor numero real mayor que 0.<br />
epsmaq = menor numero real que sumado a 1 da mayor que 1.<br />
*)<br />
var eps, x: real;
Pág. 180 Programas mencionados<br />
begin<br />
writeln(’** Determinacion de epsmin y epsmaq’); writeln;<br />
x := 1;<br />
repeat eps := x; x := x / 2 until (x = 0);<br />
writeln(’epsmin es: ’, eps);<br />
eps := 1;<br />
while (1 + eps > 1) do eps := eps/2;<br />
eps := 2 * eps;<br />
writeln(’epsmaq es: ’, eps);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.18: potencia (pág. 36)<br />
program potencia(input, output);<br />
(* calculo de potencias con exponente natural *)<br />
var<br />
x, pot: real;<br />
n, i: integer;<br />
begin<br />
writeln(’** Calculo de la potencia x a la n’);<br />
writeln;<br />
write(’Entrar x (real): ’); readln(x);<br />
write(’Entrar n (entero positivo): ’); readln(n);<br />
pot := 1;<br />
for i := 1 to n do pot := pot * x;<br />
writeln;<br />
writeln(x, ’ a la ’, n:1, ’ es ’, pot);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.22: eolnprueba (pág. 38)<br />
program eolnprueba(input, output);<br />
(* prueba del funcionamiento de eoln *)<br />
var a: integer;<br />
begin
sumardatos (Problema 4.23) Pág. 181<br />
writeln(’** Prueba del funcionamiento de eoln’);<br />
writeln;<br />
a := 1; writeln(’El valor de a es ’, a:1);<br />
if (eoln) then a := 2 else a := 3;<br />
writeln(’Ahora el valor de a es ’, a:1);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 4.23: sumardatos (pág. 39)<br />
program sumardatos(input, output);<br />
(* Calcular la suma de datos entrados por terminal, donde<br />
el fin de datos se indica con sin datos *)<br />
var<br />
s, x: real;<br />
findatos: boolean;<br />
begin<br />
writeln(’** Calcular la suma de los datos entrados’);<br />
writeln(’ indicando el fin de datos con doble ’);<br />
writeln;<br />
s := 0; findatos := false; (* inicializacion *)<br />
repeat<br />
write(’Entrar un dato (fin = ): ’);<br />
if (eoln) then findatos := true<br />
else begin readln(x); s := s + x end<br />
until (findatos);<br />
readln; (* para leer el ultimo fin de datos,<br />
en este caso innecesario *)<br />
writeln;<br />
writeln(’La suma de los datos es ’, s);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 5.10: babilonico (pág. 47)<br />
program babilonico(input, output);<br />
(*<br />
Metodo babilonico o de Newton para aproximar<br />
la raiz cuadrada de un real positivo<br />
*)
Pág. 182 Programas mencionados<br />
const<br />
itmax = 10;<br />
tol = 10e-5;<br />
x0 = 1.0;<br />
var<br />
it: integer;<br />
a, x, y: real;<br />
begin<br />
(* carteles *)<br />
writeln(’** Metodo babilonico o de Newton para’);<br />
writeln(’ aproximar la raiz cuadrada del numero’);<br />
writeln(’ real positivo "a"’); writeln;<br />
(* datos de entrada *)<br />
write(’ Entrar a (> 0): ’); readln(a); writeln;<br />
(* inicializacion *)<br />
it := 1; x := x0; y := (x + a/x)/2;<br />
(* lazo principal *)<br />
while ((it tol)) do<br />
begin x := y; y := (x + a/x)/2; it := it + 1 end;<br />
(* salida *)<br />
writeln(’Iteraciones realizadas: ’, it);<br />
if (it > itmax) then<br />
writeln(’** Iteraciones maximas excedidas **’);<br />
writeln(’Solucion aproximada obtenida: ’, y:20:10);<br />
(* fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 5.15: euclides (pág. 51)<br />
program euclides(input, output);<br />
(*<br />
Algoritmo de Euclides, usando restos<br />
en vez de restas sucesivas<br />
*)<br />
var a, b, r: integer;<br />
begin<br />
(* cartel de info *)<br />
writeln(’** Algoritmo de Euclides para encontrar el ’);<br />
write(’ maximo comun divisor entre dos enteros, ’);<br />
writeln(’ mcd(a,b)’); writeln;
factoresprimos (Problema 5.20) Pág. 183<br />
writeln(’Nota: mcd(0,0) definido como 0’); writeln;<br />
(* datos de entrada *)<br />
write(’ Entrar a: ’); readln(a);<br />
write(’ Entrar b: ’); readln(b); writeln;<br />
(* inicializacion: trabajar con no negativos *)<br />
if (a < 0) then a := -a;<br />
if (b < 0) then b := -b;<br />
(* lazo principal *)<br />
while (b 0) do begin r := a mod b; a := b; b := r end;<br />
(* salida *)<br />
writeln(’El maximo comun divisor es: ’, a:2);<br />
(* fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 5.20: factoresprimos (pág. 53)<br />
program factoresprimos(input, output);<br />
(* Encontrar factores primos de un entero positivo *)<br />
var<br />
a, b, s, t: integer;<br />
tesprimo: boolean;<br />
begin<br />
(* carteles *)<br />
writeln(’** Encontrar los factores primos de un entero’);<br />
writeln;<br />
(* datos de entrada *)<br />
write(’Entrar el entero (> 1): ’); readln(a); writeln;<br />
(* inicializacion, lazo e impresion *)<br />
t := a; tesprimo := false; b := 2;<br />
repeat<br />
s := trunc(sqrt(t));<br />
while (((t mod b) 0) and (b s) then tesprimo := true<br />
else begin (* b es factor de t (=> de a) *)<br />
write(b:1, ’, ’);<br />
t := t div b<br />
end<br />
until tesprimo;<br />
writeln(t:1);
Pág. 184 Programas mencionados<br />
(* fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 6.1: unidades (pág. 56)<br />
program unidades(input, output);<br />
(*<br />
contar cuantos de los numeros ingresados<br />
terminan en 0,1,...,9<br />
*)<br />
var<br />
dato, digito: integer;<br />
terminaen: array[0..9] of integer;<br />
begin<br />
writeln(’** Contar cuantos de los numeros ingresados’);<br />
writeln(’ terminan en 0,1,...,9’);<br />
writeln;<br />
(* inicializar el arreglo *)<br />
for digito := 0 to 9 do terminaen[digito] := 0;<br />
(* entrar datos *)<br />
write(’Entrar un entero (fin = ): ’);<br />
while (not eoln) do begin<br />
readln(dato);<br />
digito := abs(dato) mod 10;<br />
terminaen[digito] := terminaen[digito] + 1;<br />
write(’Entrar un entero (fin = ): ’)<br />
end;<br />
readln;<br />
(* salida *)<br />
writeln;<br />
writeln(’ Digito numero de datos’);<br />
for digito := 0 to 9 do<br />
writeln( digito:4, terminaen[digito]:20);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 6.2: busquedalineal (pág. 57)<br />
program busquedalineal(input, output);<br />
(*<br />
Dado un arreglo a1, a2,... y un elemento x, ver<br />
si x = ai para algun i.
usquedalineal (Problema 6.2) Pág. 185<br />
*)<br />
const MAXN = 20;<br />
var<br />
i, ndatos, x: integer;<br />
findatos, seencontro: boolean;<br />
a: array[1..MAXN] of integer;<br />
begin<br />
(* carteles *)<br />
writeln(’** Dado un arreglo a1, a2,...’);<br />
writeln(’ ver si x = ai para algun i’);<br />
writeln;<br />
(* leer el arreglo *)<br />
write(’Entrar enteros, senialando fin’);<br />
writeln(’ con sin datos.’);<br />
ndatos := 1; findatos := false;<br />
repeat<br />
write(’Entrar el dato ’, ndatos:1);<br />
write(’ (fin = ): ’);<br />
if (eoln) then begin (* no hay mas datos *)<br />
findatos := true; readln end<br />
else begin (* nuevo dato *)<br />
readln(a[ndatos]); (* incorporarlo al arreglo *)<br />
ndatos := ndatos + 1 (* para el proximo *)<br />
end<br />
until (findatos);<br />
ndatos := ndatos - 1;<br />
(* imprimir el arreglo *)<br />
writeln(’El arreglo es: ’); writeln;<br />
for i := 1 to ndatos do begin<br />
write(a[i]:10);<br />
if ((i mod 5) = 0) then writeln<br />
end;<br />
if ((ndatos mod 5) 0) then writeln;<br />
writeln;<br />
(* elemento a buscar *)<br />
write(’Entrar el entero x a buscar: ’); readln(x);<br />
writeln;<br />
(* lazo principal: suponemos ndatos > 1 *)<br />
i := 0;<br />
repeat<br />
i := i + 1;<br />
seencontro := (a[i] = x)<br />
until (seencontro or (i = ndatos));
Pág. 186 Programas mencionados<br />
(* resultados y fin *)<br />
if (seencontro)<br />
then writeln(’Se encontro en la posicion ’, i:1)<br />
else<br />
writeln(’No se encontro’);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 6.5: eratostenes (pág. 59)<br />
program eratostenes(input, output);<br />
(* Criba de Eratostenes para encontrar primos *)<br />
const MAXP = 10000; (* maximo primo *)<br />
var<br />
p, i, cuenta, k: integer;<br />
esprimo: array[2..MAXP] of boolean;<br />
begin<br />
writeln(’** Criba de Eratostenes para encontrar’);<br />
writeln(’ los primos entre 2 y ’, MAXP:1);<br />
writeln;<br />
(* inicializacion *)<br />
for i := 2 to MAXP do esprimo[i] := true;<br />
cuenta := 0;<br />
(* lazo principal *)<br />
for i := 2 to MAXP do<br />
if (esprimo[i]) then begin (* nuevo primo *)<br />
cuenta := cuenta + 1;<br />
p := i; (* el nuevo primo *)<br />
(* ahora eliminar multiplos de p *)<br />
k := p + p;<br />
while (k
flaviojosefo1 (Problema 6.7) Pág. 187<br />
Problema 6.7: flaviojosefo1 (pág. 61)<br />
program flaviojosefo1(input, output);<br />
(* Problema de Flavio Josefo con arreglos *)<br />
const MAXN = 100;<br />
var<br />
m, n, i, vuelta, cuenta: integer;<br />
vive: array[1..MAXN] of boolean;<br />
flavio: array[1..MAXN] of integer;<br />
begin<br />
(* carteles *)<br />
writeln(’** Problema de Flavio Josefo’);<br />
writeln;<br />
writeln(’En un circulo de n, se van eliminando de a m’);<br />
writeln;<br />
(* datos *)<br />
write(’Entrar n: ’); readln(n);<br />
write(’Entrar m: ’); readln(m);<br />
writeln;<br />
(* inicializacion *)<br />
for i := 1 to n do vive[i] := true; (* aun vive *)<br />
i := 0;<br />
(* parte principal *)<br />
for vuelta := 1 to n-1 do begin<br />
(* en cada vuelta, i es el ultimo eliminado *)<br />
cuenta := 0; (* contamos m sobrevivientes desde i *)<br />
repeat<br />
if (i n) then i := i + 1 else i := 1;<br />
if (vive[i]) then cuenta := cuenta + 1<br />
until (cuenta = m);<br />
vive[i] := false; (* lo eliminamos *)<br />
flavio[vuelta] := i<br />
end;<br />
(* encontrar el sobreviviente *)<br />
i := 0; repeat i := i + 1 until vive[i];<br />
flavio[n] := i;<br />
(* salida y fin *)<br />
writeln(’ La permutacion de Flavio Josefo es’); writeln;<br />
for i := 1 to n do begin<br />
write(flavio[i]:5);<br />
if ((i mod 10) = 0) then writeln<br />
end;
Pág. 188 Programas mencionados<br />
if ((n mod 10) 0) then writeln;<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 7.1: potencias2 (pág. 67)<br />
program potencias2(input, output);<br />
(* Comparar x a la n con y a la m. *)<br />
var<br />
n, m, k: integer;<br />
x, y, xn, ym: real;<br />
begin<br />
(* carteles iniciales *)<br />
writeln(’** Comparar x a la n con y a la m’);<br />
writeln;<br />
(* entrar datos *)<br />
write(’Entrar x (real): ’); readln(x);<br />
write(’Entrar n (entero): ’); readln(n);<br />
write(’Entrar y (real): ’); readln(y);<br />
write(’Entrar m (entero): ’); readln(m);<br />
(* calcular x a la n, y a la m *)<br />
xn := 1; for k := 1 to n do xn := xn * x;<br />
ym := 1; for k := 1 to m do ym := ym * y;<br />
(* comparar e imprimir *)<br />
write(x:10:5, ’ a la ’, n:1);<br />
if (xn < ym) then write(’ es menor que ’)<br />
else if (xn > ym) then write(’ es mayor que ’)<br />
else write(’ es igual a ’);<br />
writeln(y:10:5, ’ a la ’, m:1);<br />
(* cartel final *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 7.2: potencias3 (pág. 68)<br />
program potencias3(input, output);<br />
(*<br />
Primer ejemplo de uso de funciones,<br />
definiendo potencia(x,n) para comparar<br />
x a la n con y a la m.<br />
*)<br />
var
iseccion (Problema 7.3) Pág. 189<br />
n, m: integer;<br />
x, y, xn, ym: real;<br />
function potencia(a: real; b: integer): real;<br />
var k: integer; z: real;<br />
begin<br />
z := 1;<br />
for k := 1 to b do z := z * a;<br />
potencia := z<br />
end;<br />
begin<br />
(* carteles iniciales *)<br />
writeln(’** Comparar x a la n con y a la m’);<br />
writeln;<br />
(* entrar datos *)<br />
write(’Entrar x (real): ’); readln(x);<br />
write(’Entrar n (entero): ’); readln(n);<br />
write(’Entrar y (real): ’); readln(y);<br />
write(’Entrar m (entero): ’); readln(m);<br />
(* calcular x a la n, y a la m *)<br />
xn := potencia(x,n); ym := potencia(y,m);<br />
(* comparar e imprimir *)<br />
write(x:10:5, ’ a la ’, n:1);<br />
if (xn < ym) then write(’ es menor que ’)<br />
else if (xn > ym) then write(’ es mayor que ’)<br />
else write(’ es igual a ’);<br />
writeln(y:10:5, ’ a la ’, m:1);<br />
(* cartel final *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 7.3: biseccion (pág. 71)<br />
program biseccion(input, output);<br />
(*<br />
Metodo de biseccion para encontrar raices de funciones<br />
reales y continuas.<br />
*)<br />
const<br />
Cambiar la funcion cuando se investigue otra ecuacion.<br />
Version simplificada que se va mejorando<br />
en los trabajos practicos.
Pág. 190 Programas mencionados<br />
dify = 1.0e-6; (* tolerancia en y *)<br />
maxiter = 30; (* maximo numero de iteraciones *)<br />
var<br />
iter: integer;<br />
poco, mucho, x, (* valores en x *)<br />
ypoco, ymucho, y (* correspondientes valores en y *)<br />
: real;<br />
seguir (* indica si seguir iterando *)<br />
: boolean;<br />
(* funcion para la que se quiere encontrar una raiz *)<br />
function f(x: real): real;<br />
begin<br />
f := x * (x + 1) * (x + 2) * (x - 4/3)<br />
end;<br />
begin<br />
(* carteles *)<br />
writeln(’** Metodo de biseccion para encontrar raices’);<br />
writeln;<br />
write(’Implementacion para ’);<br />
writeln(’f(x) = x (x + 1) (x + 2) (x - 4/3)’);<br />
writeln;<br />
(* datos de entrada *)<br />
write(’ Entrar cota inferior para x: ’); readln(poco);<br />
write(’ Entrar cota superior para x: ’); readln(mucho);<br />
writeln;<br />
(* inicializacion *)<br />
ypoco := f(poco); ymucho := f(mucho);<br />
seguir := (ypoco * ymucho < 0);<br />
iter := 0;<br />
(* lazo principal *)<br />
while (seguir) do begin<br />
iter := iter + 1;<br />
x := (poco + mucho) / 2;<br />
y := f(x);<br />
if (abs(y) < dify) then seguir := false<br />
else if (iter = maxiter) then seguir := false<br />
else if (y * ypoco < 0) then mucho := x<br />
else begin poco := x; ypoco := y end<br />
end;<br />
(* salida *)<br />
if (iter > 0) then (* hay una solucion, aceptable o no *)<br />
begin<br />
writeln(’ Solucion obtenida: ’, x);
tablaseno2 (Problema 7.6) Pág. 191<br />
writeln(’ Valor de f: ’, y);<br />
writeln(’ Iteraciones realizadas: ’, iter:1);<br />
if (abs(y) > dify) then begin<br />
writeln;<br />
write(’* La respuesta puede no estar ’);<br />
writeln(’cerca de una raiz *’)<br />
end<br />
end;<br />
(* fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 7.6: tablaseno2 (pág. 76)<br />
program tablaseno2(input, output);<br />
(* hacer una tabla del seno donde los angulos<br />
estan dados en grados *)<br />
const pi = 3.14159265;<br />
var inicial, final, incremento: integer;<br />
procedure cartelesiniciales;<br />
begin<br />
writeln(’** Hacer una tabla del seno dando valores’);<br />
writeln(’ inicial, final, y del incremento (en grados).’);<br />
writeln<br />
end;<br />
procedure leerdatos;<br />
begin<br />
write(’Entrar el valor inicial (en grados): ’);<br />
readln(inicial);<br />
write(’Entrar el valor final (en grados): ’);<br />
readln(final);<br />
write(’Entrar el valor de incremento (en grados): ’);<br />
readln(incremento);<br />
writeln<br />
end;<br />
procedure imprimirtabla;<br />
var<br />
grados: integer;<br />
radianes: real;<br />
begin<br />
writeln(’Angulo Seno’);<br />
grados := inicial;<br />
while (grados
Pág. 192 Programas mencionados<br />
end;<br />
writeln(grados:5, sin(radianes):15:5);<br />
grados := grados + incremento<br />
end<br />
procedure cartelfinal;<br />
begin writeln; writeln(’** Fin **’) end;<br />
begin<br />
cartelesiniciales;<br />
leerdatos;<br />
imprimirtabla;<br />
cartelfinal<br />
end.<br />
Problema 7.7: intercambio (pág. 77)<br />
program intercambio(input, output);<br />
(* prueba de intercambio de valores entre variables *)<br />
var x, y: integer;<br />
procedure incorrecto(a, b: integer);<br />
(* a, b pasados por valor *)<br />
var t: integer;<br />
begin t := a; a := b; b := t end;<br />
begin<br />
writeln(’** Prueba de intercambio de variables’);<br />
writeln;<br />
x := 5; y := 2;<br />
writeln(’Antes: x = ’, x:2, ’, y = ’, y:2);<br />
incorrecto(a,b);<br />
writeln(’Despues: x = ’, x:2, ’, y = ’, y:2);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 8.2: maximocaracter (pág. 83)<br />
program maximocaracter(input, output);<br />
(*<br />
Encontrar el maximo caracter en<br />
un renglon entrado por terminal<br />
*)<br />
const MAXC = 255; (* maximo tamanio de arreglo *)<br />
type tiporenglon = array[1..MAXC] of char;
maximocaracter (Problema 8.2) Pág. 193<br />
var<br />
p, (* posicion en el renglon *)<br />
n (* numero de caracteres en el renglon *)<br />
: integer;<br />
t (* el caracter de maximo ordinal *)<br />
: char;<br />
renglon (* el renglon a procesar *)<br />
: tiporenglon;<br />
procedure leerrenglon( var s: tiporenglon; var n: integer);<br />
(* aqui suponemos renglon: array[1..MAXC] of char *)<br />
begin<br />
write(’Escribir algo ’);<br />
writeln(’(no mas de ’, MAXC:1, ’ caracteres.):’);<br />
n := 0;<br />
while ((not eoln) and (n < MAXC)) do begin<br />
n := n + 1; read(s[n]) (* aca es read y no readln *)<br />
end;<br />
readln (* para leer el ultimo fin de linea *)<br />
end;<br />
procedure escribirrenglon( s: tiporenglon; n: integer);<br />
var i: integer;<br />
begin<br />
for i := 1 to n do write(s[i]);<br />
writeln<br />
end;<br />
procedure max(<br />
var t: tiporenglon;<br />
long: integer;<br />
var tmax: char;<br />
var indice: integer);<br />
var i: integer;<br />
begin (* suponemos long positivo *)<br />
tmax := t[1]; indice := 1;<br />
for i := 2 to long do<br />
if (t[i] > tmax) then begin<br />
tmax := t[i]; indice := i end<br />
end; (* fin de procedure max *)<br />
begin<br />
(* titulo *)<br />
write(’** Programa para encontrar el maximo en un ’);<br />
write(’ arreglo de caracteres entrado por terminal’);<br />
writeln;<br />
(* entrada *)<br />
leerrenglon(renglon, n);
Pág. 194 Programas mencionados<br />
(* verificacion *)<br />
writeln; writeln(’El renglon entrado es: ’);<br />
writeln;<br />
escribirrenglon(renglon, n);<br />
(* procesamiento *)<br />
max(renglon, n, t, p);<br />
(* salida y fin *)<br />
writeln; write(’El maximo es "’, t, ’",’);<br />
writeln(’ alcanzado en la posicion ’, p:1);<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 8.8: deconsolaaarchivo (pág. 88)<br />
program deconsolaaarchivo(input, output);<br />
(*<br />
Copiar de consola a archivo<br />
*)<br />
En la entrada no deben haber renglones "vacios"<br />
var<br />
archivo: text;<br />
c: char;<br />
nombre: string;<br />
begin<br />
(* carteles *)<br />
writeln(’** Copiar de consola a archivo’);<br />
writeln;<br />
(* nombre de archivo a escribir *)<br />
writeln(’ Entrar el nombre del archivo a escribir’);<br />
writeln(’ Atencion: si ya existe se borra!’);<br />
write(’ Archivo: ’); readln(nombre);<br />
rewrite(archivo, nombre);<br />
(* copiar de consola a archivo *)<br />
writeln;<br />
write(’Entrar datos en ’, nombre);<br />
writeln(’ (Fin con retorno "vacio")’);<br />
while (not eoln) do begin<br />
repeat<br />
read(c); write(archivo, c)<br />
until eoln;<br />
readln;<br />
writeln(archivo)
dearchivoaconsola (Problema 8.8) Pág. 195<br />
end;<br />
close(archivo);<br />
(* Fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 8.8: dearchivoaconsola (pág. 88)<br />
program dearchivoaconsola(input, output);<br />
(* Copiar de archivo a consola *)<br />
var<br />
archivo: text;<br />
c: char;<br />
nombre: string;<br />
begin<br />
(* carteles *)<br />
writeln(’** Copiar de archivo a consola’);<br />
writeln;<br />
(* nombre de archivo a leer *)<br />
write(’ Entrar el nombre del archivo a leer: ’);<br />
readln(nombre);<br />
reset(archivo, nombre);<br />
writeln;<br />
(* copiar datos en pantalla *)<br />
writeln(’Estos son los datos en ’, nombre, ’:’);<br />
writeln;<br />
while not eof(archivo) do<br />
if (eoln(archivo)) then begin<br />
readln(archivo); writeln end<br />
else begin<br />
read(archivo, c); write(c) end;<br />
close(archivo);<br />
(* Fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 9.1: dado (pág. 92)<br />
program dado(input, output);<br />
(*<br />
Simular tirar un dado.
Pág. 196 Programas mencionados<br />
*)<br />
Usa la sentencia "random" de Turbo Pascal.<br />
var d: integer;<br />
begin<br />
writeln(’** Simular tirar un dado’);<br />
writeln;<br />
randomize;<br />
d := 1 + random(6);<br />
writeln(’El dado que salio es ’, d:1);<br />
writeln;<br />
writeln(’** para Fin **’); readln<br />
end.<br />
Problema 9.2: dados (pág. 92)<br />
program dados(input, output);<br />
(*<br />
Contar las veces que se tira un dado<br />
hasta que aparece un numero prefijado.<br />
*)<br />
Usa la sentencia "random" de Turbo Pascal.<br />
var numero, tiros: integer;<br />
begin<br />
writeln(’** Contar las veces que se tira un dado’);<br />
writeln(’ hasta que aparece un numero prefijado’);<br />
writeln;<br />
write(’Entrar el valor que debe aparecer ’);<br />
write(’(entre 1 y 6): ’);<br />
readln(numero);<br />
randomize;<br />
tiros := 0;<br />
repeat tiros := tiros + 1<br />
until (1 + random(6) = numero);<br />
writeln;<br />
write(’El numero ’, numero:1);<br />
writeln(’ tardo ’, tiros, ’ tiros hasta aparecer.’);<br />
writeln;<br />
writeln(’** para Fin **’); readln
arbolbinario (Problema 12.6) Pág. 197<br />
end.<br />
Problema 12.6: arbolbinario (pág. 129)<br />
program arbolbinario(input, output);<br />
(*<br />
Arbol binario parcialmente ordenado guardando los<br />
nodos en un arreglo. Variante de las versiones con<br />
punteros de Wirth87, p. 210, y K&R, p. 153.<br />
*)<br />
Aplicacion al problema de hacer un listado<br />
ordenado de caracteres entrados por terminal,<br />
contando las apariciones.<br />
const<br />
MAXN = 100; (* maximo numero de datos a guardar *)<br />
nada = 0; (* para cuando no hay hijos *)<br />
type<br />
indice = integer; (* para senialar los indices *)<br />
tipodato = char; (* el tipo de datos a ingresar *)<br />
nodo = record<br />
llave: tipodato; (* dato *)<br />
cuenta: integer; (* veces que aparecio *)<br />
izquierda: indice; (* hijo a la izquierda *)<br />
derecha: indice (* hijo a la derecha *)<br />
end;<br />
tipoarbol = array[1..MAXN] of nodo;<br />
var<br />
narbol: integer; (* numero de nodos en el arbol *)<br />
arbol: tipoarbol; (* el arbol! *)<br />
raiz: indice; (* donde empieza el arbol *)<br />
dato: tipodato; (* dato leido *)<br />
function entrardatos(var dato: tipodato): boolean;<br />
begin<br />
write(’Entrar el numero (fin = ): ’);<br />
if (eoln) then<br />
begin entrardatos := false; readln end<br />
else<br />
begin entrardatos := true; readln(dato) end<br />
end;<br />
procedure binario(x: tipodato; var p: indice);<br />
var y: tipodato;<br />
begin<br />
if (p = nada) then begin (* nueva entrada *)<br />
narbol := narbol + 1;
Pág. 198 Programas mencionados<br />
p := narbol;<br />
with arbol[p] do begin<br />
llave := x; cuenta := 1;<br />
izquierda := nada; derecha := nada<br />
end<br />
end<br />
else begin<br />
y := arbol[p].llave;<br />
if (x = y) then (* dato existente *)<br />
arbol[p].cuenta := arbol[p].cuenta + 1<br />
else if (x < y) then (* ir a la izquierda *)<br />
binario(x, arbol[p].izquierda)<br />
else (* ir a la derecha *)<br />
binario(x, arbol[p].derecha)<br />
end<br />
end;<br />
procedure enorden(w: indice);<br />
begin<br />
if (w nada) then<br />
with arbol[w] do begin<br />
enorden(izquierda);<br />
writeln(llave:10, cuenta:20);<br />
enorden(derecha)<br />
end<br />
end;<br />
begin<br />
(* carteles *)<br />
writeln(’** Arbol binario’); writeln;<br />
write(’ Entrar caracteres y contar’);<br />
writeln(’ las veces que aparecieron’);<br />
writeln;<br />
(* inicializacion *)<br />
narbol := 0;<br />
raiz := nada;<br />
(* lectura de datos y construccion del arbol *)<br />
while (entrardatos(dato)) do binario(dato, raiz);<br />
(* salida y fin *)<br />
writeln;<br />
writeln(’Impresion en orden:’);<br />
writeln;<br />
writeln(’ Caracter Cantidad de Apariciones’);<br />
enorden(raiz);<br />
writeln; writeln(’** Fin **’)<br />
end.
anchoprimero (Problema 13.4) Pág. 199<br />
Problema 13.4: anchoprimero (pág. 139)<br />
program anchoprimero(input, output);<br />
(*<br />
Recorrer un grafo a lo ancho para<br />
determinar un arbol de expansion.<br />
*)<br />
Entrada: un grafo dado por lista de aristas y el<br />
numero de vertices, n.<br />
Los vertices son todos los enteros positivos<br />
entre 1 y n. Si alguno no aparece en ninguna<br />
arista, es un vertice aislado.<br />
Salida: las aristas del arbol y el orden en que<br />
fueron visitados los vertices.<br />
Si el arbol no es conexo, se imprime un mensaje.<br />
Implementacion con una cola fifo de vertices a visitar.<br />
const<br />
MAXN = 20; (* maximo numero de vertices *)<br />
MAXM = 100; (* maximo numero de aristas *)<br />
type<br />
arreglodevertices = array[1..MAXN] of integer;<br />
tipoarista = record i, j: integer end;<br />
arreglodearistas = array[1..MAXM] of tipoarista;<br />
matrizNN = array[1..MAXN,1..MAXN] of integer;<br />
var<br />
ngrafo, mgrafo, nvisitado, marbol: integer;<br />
padre, visitado: arreglodevertices;<br />
aristasgrafo, aristasarbol: arreglodearistas;<br />
adyacencias: matrizNN;<br />
function nuevaarista: boolean;<br />
begin<br />
writeln;<br />
write(’ Entrar la arista ’, mgrafo:2);<br />
writeln(’ (fin = ):’);<br />
write(’ Entrar el primer vertice: ’);<br />
if (not eoln) then begin<br />
nuevaarista := true;<br />
with aristasgrafo[mgrafo] do begin<br />
read(i);<br />
write(’ Entrar el segundo vertice: ’);<br />
readln(j)<br />
end (* with *)<br />
end (* if not eoln *)
Pág. 200 Programas mencionados<br />
else begin nuevaarista := false; readln end<br />
end;<br />
procedure entrardatos;<br />
begin<br />
(* vertices *)<br />
writeln;<br />
write(’Entrar el numero de vertices: ’);<br />
readln(ngrafo);<br />
(* aristas *)<br />
writeln;<br />
writeln(’Entrar las aristas:’);<br />
mgrafo := 1;<br />
while (nuevaarista) do mgrafo := mgrafo + 1;<br />
mgrafo := mgrafo - 1<br />
end;<br />
procedure escribiraristas( a: arreglodearistas; m: integer);<br />
const maxrenglon = 10; (* maxima cantidad por renglon *)<br />
var k: integer;<br />
begin<br />
for k := 1 to m do begin<br />
write(’ (’, a[k].i:1, ’,’, a[k].j:1, ’)’);<br />
if ((k mod maxrenglon) = 0) then writeln<br />
end;<br />
if ((m mod maxrenglon) > 0) then writeln<br />
end;<br />
procedure dearistasaadyacencias;<br />
var i, j, k: integer;<br />
begin<br />
for i := 1 to ngrafo do<br />
for j := 1 to ngrafo do<br />
adyacencias[i,j] := 0;<br />
for k := 1 to mgrafo do<br />
with aristasgrafo[k] do begin<br />
adyacencias[i,j] := 1; adyacencias[j,i] := 1<br />
end<br />
end;<br />
procedure ancho;<br />
var<br />
i, j, ppo, fin: integer;<br />
avisitar: arreglodevertices;<br />
begin<br />
(* inicializacion *)<br />
dearistasaadyacencias;<br />
for i := 1 to ngrafo do padre[i] := 0;<br />
nvisitado := 0;
anchoprimero (Problema 13.4) Pág. 201<br />
(* al principio solo esta 1 *)<br />
padre[1] := 1;<br />
ppo := 1; fin := 1; (* los extremos de la cola *)<br />
avisitar[1] := 1;<br />
while (ppo 0) and (padre[j] = 0)) then<br />
begin (* agregar j a la cola *)<br />
padre[j] := i;<br />
fin := fin + 1;<br />
avisitar[fin] := j<br />
end (* if adyacencias > 0 *)<br />
end (* while *)<br />
end;<br />
procedure hacerarbol;<br />
(* Encontrar las aristas del arbol generado usando padre *)<br />
var k: integer;<br />
begin<br />
marbol := 0;<br />
for k := 2 to nvisitado do (* 1 es "raiz" *)<br />
if (padre[k] > 0) then begin<br />
marbol := marbol + 1;<br />
with aristasarbol[marbol] do begin<br />
i := k; j := padre[k] end<br />
end<br />
end;<br />
procedure escribirvisitado;<br />
const maxrenglon = 10; (* maxima cantidad por renglon *)<br />
var i: integer;<br />
begin<br />
for i := 1 to nvisitado do begin<br />
write(visitado[i]:5);<br />
if ((i mod maxrenglon) = 0) then writeln<br />
end;<br />
if ((nvisitado mod maxrenglon) 0) then writeln<br />
end;<br />
begin<br />
(* Carteles *)
Pág. 202 Programas mencionados<br />
writeln(’** Recorrer un grafo a lo ancho para determinar’);<br />
writeln(’ un arbol de expansion entrando las aristas,’);<br />
writeln(’ tomando como raiz a 1.’);<br />
writeln;<br />
(* Datos e inicializacion *)<br />
entrardatos;<br />
writeln;<br />
writeln(’ Las aristas originales son:’);<br />
escribiraristas( aristasgrafo, mgrafo);<br />
(* Procesamiento y salida *)<br />
ancho;<br />
hacerarbol;<br />
writeln;<br />
writeln(’ Las aristas del arbol resultante son:’);<br />
escribiraristas(aristasarbol, marbol);<br />
writeln;<br />
writeln(’Los vertices se visitaron en el siguiente orden:’);<br />
escribirvisitado;<br />
if (nvisitado < ngrafo) then begin<br />
writeln; writeln(’** El grafo no es conexo **’) end;<br />
(* Fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 13.8: dijkstra (pág. 144)<br />
program dijkstra(input, output);<br />
(*<br />
Implementacion del algoritmo de Dijkstra para<br />
encontrar un camino mas corto entre dos vertices<br />
en un grafo pesado.<br />
*)<br />
Entrada: un grafo pesado dado por lista de aristas,<br />
el numero de vertices, n, y los vertices a unir, s y t.<br />
Los vertices son todos los enteros positivos<br />
entre 1 y n. Si alguno no aparece en ninguna<br />
arista, es un vertice aislado.<br />
Los pesos de cada arista son enteros positivos.<br />
Salida: la longitud de un camino mas corto, y el<br />
camino que lo realiza como lista de vertices. Si<br />
los vertices no se pueden conectar, se imprime un<br />
mensaje.<br />
const<br />
MAXN = 20; (* maximo numero de vertices *)
dijkstra (Problema 13.8) Pág. 203<br />
MAXM = 100; (* maximo numero de aristas *)<br />
type<br />
costo = integer; (* podria ser real *)<br />
arreglodevertices = array[1..MAXN] of integer;<br />
tipoarista = record<br />
i, j: integer; (* los extremos *)<br />
w: costo (* el costo *)<br />
end;<br />
arreglodearistas = array[1..MAXM] of tipoarista;<br />
matrizNN = array[1..MAXN,1..MAXN] of costo;<br />
var<br />
ngrafo, mgrafo, s, t: integer;<br />
infinito: costo;<br />
padre: arreglodevertices;<br />
dist: array[1..MAXN] of costo;<br />
aristasgrafo: arreglodearistas;<br />
costos: matrizNN;<br />
function nuevaarista: boolean;<br />
begin<br />
writeln;<br />
write(’ Entrar la arista ’, mgrafo:2);<br />
writeln(’ (fin = ): ’);<br />
write(’ Entrar el primer vertice: ’);<br />
if (not eoln) then begin<br />
nuevaarista := true;<br />
with aristasgrafo[mgrafo] do begin<br />
read(i);<br />
write(’ Entrar el segundo vertice: ’);<br />
read(j);<br />
write(’ Entrar el costo (entero > 0): ’);<br />
readln(w);<br />
infinito := infinito + w<br />
end (* with *)<br />
end (* if not eoln *)<br />
else begin nuevaarista := false; readln end<br />
end;<br />
procedure entrardatos;<br />
begin<br />
(* vertices *)<br />
writeln;<br />
write(’Entrar el numero de vertices: ’);<br />
readln(ngrafo);<br />
(* aristas *)<br />
writeln;<br />
writeln(’Entrar las aristas:’);<br />
mgrafo := 1; infinito := 1;
Pág. 204 Programas mencionados<br />
while (nuevaarista) do mgrafo := mgrafo + 1;<br />
mgrafo := mgrafo - 1;<br />
(* vertices a unir *)<br />
writeln;<br />
write(’Entrar el vertice de partida: ’); readln(s);<br />
write(’Entrar el vertice de llegada: ’); readln(t)<br />
end;<br />
procedure escribiraristas(a: arreglodearistas; m: integer);<br />
var k: integer;<br />
begin<br />
writeln;<br />
writeln(’ Vertices Costo’);<br />
for k := 1 to m do<br />
with a[k] do writeln(i:5, j:6, w:9)<br />
end;<br />
procedure dearistasacostos;<br />
var i, j, k: integer;<br />
begin<br />
for i := 1 to ngrafo do<br />
for j := 1 to ngrafo do<br />
costos[i,j] := infinito;<br />
for k := 1 to mgrafo do<br />
with aristasgrafo[k] do begin<br />
costos[i,j] := w; costos[j,i] := w<br />
end<br />
end;<br />
procedure mascorto;<br />
var<br />
i, j, k, kmin, hay: integer;<br />
d, dmin: costo;<br />
avisitar: arreglodevertices;<br />
begin<br />
(* inicializacion *)<br />
dearistasacostos;<br />
for i := 1 to ngrafo do begin<br />
padre[i] := 0; dist[i] := infinito end;<br />
(* s es la "raiz" y el unico en la cola al comenzar *)<br />
padre[s] := s; dist[s] := 0; hay := 1; avisitar[1] := s;<br />
repeat (* aca hay > 0 *)<br />
(* nuevo i: el de minima distancia en la cola *)<br />
kmin := hay; i := avisitar[hay]; dmin := dist[i];<br />
for k := 1 to hay - 1 do begin<br />
j := avisitar[k]; d := dist[j];<br />
if (d < dmin) then begin
dijkstra (Problema 13.8) Pág. 205<br />
kmin := k; i := j; dmin := d end<br />
end;<br />
(* ahora tenemos el nuevo i *)<br />
if (i t) then begin<br />
(* poner i al fin de la cola y sacarlo *)<br />
if (kmin < hay) then avisitar[kmin] := avisitar[hay];<br />
hay := hay - 1;<br />
(* examinar vecinos de i *)<br />
for j := 1 to ngrafo do<br />
if (dist[i] + costos[i,j] < dist[j]) then begin<br />
(* si j no se agrego, agregarlo a la cola *)<br />
if (dist[j] = infinito) then begin<br />
hay := hay + 1; avisitar[hay] := j end;<br />
(* actualizar dist y padre *)<br />
dist[j] := dist[i] + costos[i,j];<br />
padre[j] := i<br />
end<br />
end (* if i t *)<br />
until ((i = t) or (hay = 0))<br />
end;<br />
procedure hacercamino;<br />
(* Construir e imprimir el camino mas corto usando padre. *)<br />
const maxrenglon = 10; (* maxima cantidad / renglon *)<br />
var<br />
i, ncamino: integer;<br />
camino: arreglodevertices;<br />
begin<br />
ncamino := 1; camino[1] := t; i := t;<br />
while (i s) do begin<br />
i := padre[i];<br />
ncamino := ncamino + 1;<br />
camino[ncamino] := i<br />
end;<br />
(* imprimir "dando vuelta" el arreglo *);<br />
writeln(’ Los vertices en el camino son:’);<br />
for i := ncamino downto 1 do begin<br />
write(camino[i]:5);<br />
if ((i mod maxrenglon) = 0) then writeln<br />
end;<br />
if ((ncamino mod maxrenglon) 0) then writeln<br />
end;<br />
begin<br />
(* Carteles *)<br />
writeln(’** Busqueda del camino mas corto entre dos’);<br />
writeln(’ vertices con el algoritmo de Dijkstra.’);<br />
writeln(’ El grafo se entra mediante lista de aristas.’);<br />
(* Datos e inicializacion *)
Pág. 206 Programas mencionados<br />
entrardatos;<br />
writeln; writeln(’Las aristas del grafo son:’);<br />
escribiraristas(aristasgrafo, mgrafo);<br />
writeln; writeln(’Los vertices a unir son:’);<br />
writeln(’ partida: ’, s:1);<br />
writeln(’ llegada: ’, t:1);<br />
(* Procesamiento y salida *)<br />
mascorto;<br />
writeln;<br />
if (padre[t] > 0) then begin<br />
writeln(’ La longitud del camino es ’, dist[t]:1);<br />
hacercamino<br />
end<br />
else begin<br />
writeln;<br />
writeln(’** ’, s:1, ’ y ’ , t:1, ’ no se conectan **’)<br />
end;<br />
(* Fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Problema 14.1: listas (pág. 159)<br />
program listas(input, output);<br />
(*<br />
Listas encadenadas con punteros: con los datos ingresados<br />
formamos dos listas, una agregando adelante y otra ordenada.<br />
*)<br />
El fin de lista se indica con "nil", pero podria ser otro<br />
nodo, y tambien podria haber un nodo al principio sin datos.<br />
listaadelante: se agrega nueva info adelante.<br />
listaordenada: lista ordenada por "id". Recorremos toda la<br />
lista buscando donde ubicar la nueva info, que se inserta<br />
"despues" del elemento que ya estaba si coinciden las<br />
llaves de comparacion, para insertar "antes" cambiar<br />
"
listas (Problema 14.1) Pág. 207<br />
nodo = record<br />
info: info;<br />
siguiente: ptrnodo<br />
end;<br />
var<br />
nuevainfo: info;<br />
listaadelante, listaordenada: ptrnodo;<br />
function entrardatos(var datos: info): boolean;<br />
begin<br />
write(’Entrar el nombre (fin = ): ’);<br />
if (not eoln) then begin<br />
entrardatos := true;<br />
with datos do begin<br />
readln(nombre);<br />
write(’Entrar numero de id para "’, nombre, ’": ’);<br />
readln(id)<br />
end<br />
end<br />
else begin entrardatos := false; readln end<br />
end;<br />
procedure adelante (var lista: ptrnodo; nuevainfo: info);<br />
(*<br />
agregar nueva info adelante en lista,<br />
lista puede ser "nil"<br />
*)<br />
var p: ptrnodo;<br />
begin<br />
new(p);<br />
p^.info := nuevainfo; p^.siguiente := lista;<br />
lista := p<br />
end;<br />
procedure insertaradelante( p, q: ptrnodo);<br />
(* insertar q delante de p, ninguno debe ser "nil" *)<br />
var temp: info;<br />
begin<br />
q^.siguiente := p^.siguiente; p^.siguiente := q;<br />
temp := p^.info; p^.info := q^.info; q^.info := temp<br />
end;<br />
procedure insertardetras( p, q: ptrnodo);<br />
(* insertar q detras de p, ninguno debe ser "nil" *)<br />
begin<br />
q^.siguiente := p^.siguiente; p^.siguiente := q<br />
end;<br />
procedure ordenada (var lista: ptrnodo; nuevainfo: info);
Pág. 208 Programas mencionados<br />
(* agregar elementos a una lista ordenada por id *)<br />
var p, r: ptrnodo;<br />
begin<br />
(* nuevo nodo con nueva info *)<br />
new(r); r^.info := nuevainfo;<br />
(* buscar donde ubicar r *)<br />
if (lista = nil) then begin<br />
(* r es el primer nodo *)<br />
lista := r; r^.siguiente := nil end<br />
else begin (* recorrer la lista *)<br />
p := lista;<br />
while ((p^.siguiente nil) and<br />
(p^.info.id nuevainfo.id)<br />
(* cambiar a >= para antes *)<br />
then insertaradelante(p, r)<br />
else (* p es el ultimo de la lista *)<br />
insertardetras(p, r)<br />
end<br />
end;<br />
procedure imprimir (lista: ptrnodo);<br />
var p: ptrnodo;<br />
begin<br />
if (lista = nil) then writeln(’ Lista vacia’);<br />
p := lista;<br />
while (p nil) do begin<br />
with p^.info do writeln(nombre:tamanio, id:10);<br />
p := p^.siguiente<br />
end<br />
end;<br />
begin<br />
(* carteles *)<br />
writeln(’** Prueba de listas encadenadas’);<br />
writeln;<br />
(* inicializacion de listas *)<br />
listaadelante := nil;<br />
listaordenada := nil;<br />
(* entrada de datos y construccion de listas *)<br />
while (entrardatos(nuevainfo)) do begin<br />
adelante( listaadelante, nuevainfo);<br />
ordenada( listaordenada, nuevainfo)<br />
end;
grafos (Problema 14.9) Pág. 209<br />
(* imprimir listas *)<br />
writeln; writeln(’Las listas son:’); writeln;<br />
writeln(’ Lista agregando adelante:’);<br />
imprimir(listaadelante); writeln;<br />
writeln(’ Lista ordenada por id:’);<br />
imprimir(listaordenada);<br />
(* Fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.<br />
Ejemplo de Salida<br />
** Prueba de listas encadenadas **<br />
Entrar el nombre (fin = ): Geri<br />
Entrar numero de id para "Geri": 2<br />
Entrar el nombre (fin = ): Pablito<br />
Entrar numero de id para "Pablito": 1<br />
Entrar el nombre (fin = ): Guille<br />
Entrar numero de id para "Guille": 2<br />
Entrar el nombre (fin = ): Robin Hood<br />
Entrar numero de id para "Robin Hood": 3<br />
Entrar el nombre (fin = ):<br />
Las listas son:<br />
Lista agregando adelante:<br />
Lista ordenada por id:<br />
** Fin **<br />
Problema 14.9: grafos (pág. 161)<br />
Robin Hood 3<br />
Guille 2<br />
Pablito 1<br />
Geri 2<br />
Pablito 1<br />
Geri 2<br />
Guille 2<br />
Robin Hood 3<br />
program grafos(input, output);<br />
(*<br />
Representaciones de grafos dirigidos donde<br />
V = (1,...,n) por lista de aristas o por<br />
listas de vecinos.
Pág. 210 Programas mencionados<br />
*)<br />
Siempre n es dato.<br />
Las listas terminan en "nil".<br />
type<br />
ptrarista = ^arista;<br />
arista = record u, v: integer; s: ptrarista end;<br />
ptrvecino = ^vecino;<br />
vecino = record id: integer; s: ptrvecino end;<br />
ptrvertice = ^vertice;<br />
vertice = record<br />
id: integer;<br />
s: ptrvertice;<br />
vecinos: ptrvecino<br />
end;<br />
var<br />
ngrafo: integer;<br />
aristas: ptrarista;<br />
vertices: ptrvertice;<br />
(***************************************<br />
Aristas<br />
***************************************)<br />
function nuevaarista(var x, y: integer): boolean;<br />
begin<br />
writeln;<br />
writeln(’Entrar una arista (fin = ): ’);<br />
write(’ Entrar el primer vertice: ’);<br />
if (not eoln) then begin<br />
nuevaarista := true;<br />
read(x);<br />
write(’ Entrar el segundo vertice: ’);<br />
readln(y)<br />
end (* if not eoln *)<br />
else begin nuevaarista := false; readln end<br />
end;<br />
procedure agregararista(x, y: integer);<br />
(* agregamos arista adelante *)<br />
var parista: ptrarista;<br />
begin<br />
new(parista);<br />
with parista^ do begin u := x; v := y; s := aristas end;<br />
aristas := parista
grafos (Problema 14.9) Pág. 211<br />
end;<br />
procedure entrararistas;<br />
(* inicializar e ingresar aristas *)<br />
var x, y: integer;<br />
begin<br />
aristas := nil;<br />
while nuevaarista(x,y) do agregararista(x,y)<br />
end;<br />
procedure escribiraristas;<br />
const maxrenglon = 10; (* maxima cantidad por renglon *)<br />
var<br />
k: integer;<br />
p: ptrarista;<br />
begin<br />
if (aristas = nil) then writeln(’** lista vacia’)<br />
else begin<br />
p := aristas; k := 0;<br />
repeat<br />
k := k + 1;<br />
with p^ do write(’ (’, u:1, ’,’, v:1, ’)’);<br />
if ((k mod maxrenglon) = 0) then writeln;<br />
p := p^.s<br />
until (p = nil)<br />
end;<br />
if ((k mod maxrenglon) > 0) then writeln<br />
end;<br />
(***************************************<br />
Vertices y sus vecinos<br />
***************************************)<br />
procedure inicializarvecinos;<br />
var<br />
k: integer;<br />
p: ptrvertice;<br />
begin<br />
vertices := nil;<br />
(* la construimos "al reves" para que quede ordenada *)<br />
for k := ngrafo downto 1 do begin<br />
new(p);<br />
with p^ do begin<br />
id := k; s := vertices; vecinos := nil end;<br />
vertices := p<br />
end (* for k *)<br />
end;<br />
procedure agregarvecino(x, y: integer);<br />
(* agregar y a los vecinos de x *)
Pág. 212 Programas mencionados<br />
var<br />
pvertice: ptrvertice;<br />
pvecino: ptrvecino;<br />
begin<br />
(* buscar x entre los vertices *)<br />
pvertice := vertices; (* aca debe ser vertices nil *)<br />
while (pvertice^.id x) do pvertice := pvertice^.s;<br />
(* actualizar vecinos de "x", agregando "y" adelante *)<br />
new(pvecino);<br />
with pvecino^ do begin s := pvertice^.vecinos; id := y end;<br />
pvertice^.vecinos := pvecino<br />
end;<br />
procedure escribirvecinos;<br />
var<br />
pvertice: ptrvertice;<br />
pvecino: ptrvecino;<br />
begin<br />
(* adyacencias *)<br />
pvertice := vertices;<br />
while (pvertice nil) do begin<br />
write(pvertice^.id:5, ’ -> ’);<br />
pvecino := pvertice^.vecinos;<br />
if (pvecino = nil) then write(’ {}’)<br />
else<br />
repeat<br />
write(pvecino^.id:3);<br />
pvecino := pvecino^.s<br />
until (pvecino = nil);<br />
writeln;<br />
pvertice := pvertice^.s<br />
end<br />
end;<br />
(***************************************<br />
De aristas a vertices<br />
***************************************)<br />
procedure dearistasaadyacencias;<br />
(* construir listas de adyacencias<br />
recorriendo la lista de aristas *)<br />
var parista: ptrarista;<br />
begin<br />
parista := aristas;<br />
while parista nil do begin<br />
with parista^ do begin<br />
agregarvecino(u, v);<br />
agregarvecino(v, u)<br />
end;
grafos (Problema 14.9) Pág. 213<br />
end;<br />
(* proxima arista *)<br />
parista := parista^.s<br />
end<br />
(**************************************<br />
Parte principal<br />
**************************************)<br />
begin<br />
(* carteles *)<br />
writeln(’** Pasar de aristas a adyacencias usando’);<br />
writeln(’ listas encadenadas.’); writeln;<br />
(* Inicializacion y entrada de datos *)<br />
(* vertices *)<br />
write(’ Entrar n (la cantidad de vertices): ’);<br />
readln(ngrafo);<br />
inicializarvecinos; writeln;<br />
(* aristas *)<br />
entrararistas;<br />
(* construccion de listas de adyacencias *)<br />
dearistasaadyacencias;<br />
(* imprimir resultados *)<br />
writeln; writeln(’** Resultados **’); writeln;<br />
writeln(’ Lista de aristas:’);<br />
escribiraristas; writeln;<br />
writeln(’ Listas de vecinos:’);<br />
escribirvecinos;<br />
(* fin *)<br />
writeln; writeln(’** Fin **’)<br />
end.
Apéndice B<br />
Sobre los compiladores<br />
Pascal<br />
Suponiendo que se tenga acceso a una computadora, el próximo problema es<br />
usar un compilador Pascal. Si bien los compiladores comerciales tienen menos<br />
problemas y tienen documentación adecuada, afortunadamente hay varias posibilidades<br />
de compiladores disponibles gratis de internet, y pasamos a presentar<br />
algunas (1) :<br />
• Aunque no se adhiere estrictamente al estándar Pascal, el compilador<br />
“Turbo Pascal” de Borland (para DOS) es muy “robusto” (no tiene problemas),<br />
y su popularidad ha hecho que muchos otros compiladores copiaran<br />
su sintaxis.<br />
La versión 5.5 de este compilador se puede conseguir gratis para uso personal<br />
en Borland Museum (2) . Versiones más recientes de Turbo Pascal o<br />
Delphi (como Turbo Pascal pero para Windows), también para uso personal<br />
pero en francés, pueden conseguirse en Borland Francia (3) .<br />
• Free Pascal (4) , es un compilador gratuito para varias plataformas, incluyendo<br />
DOS, Win32 y Linux, entre otras. La página principal de Free Pascal<br />
(5) tiene más datos sobre Free Pascal y Pascal en general.<br />
• Thefreecountry.com (6) presenta varias posibilidades de compiladores gratis,<br />
incluyendo Turbo Pascal y Delphi.<br />
• Para Macintosh, puede consultarse la página de Pascal Central (7) para<br />
distintas posibilidades, incluyendo programas hechos.<br />
Claro que una vez obtenido el compilador, debe instalarse en la computadora<br />
siguiendo las instrucciones para cada caso.<br />
(1) La información está actualizada a febrero de <strong>2004</strong>.<br />
(2) http://community.borland.com/museum/<br />
(3) http://www.inprise.fr/download/compilateurs/<br />
(4) http://www.freepascal.org/download.html<br />
(5) http://www.freepascal.org/<br />
(6) http://www.thefreecountry.com/developercity/pascal.shtml<br />
(7) http://www.pascal-central.com/
Apéndice C<br />
Breve referencia de Pascal<br />
C.1. Resumen de operadores<br />
C.1.1. Operadores aritméticos<br />
Operador operación operando resultado<br />
+ (unitario) identidad entero o real mismo que operando<br />
- (unitario) inversión de signo entero o real mismo que operando<br />
+ (binario) suma entero o real entero o real<br />
- (binario) resta entero o real entero o real<br />
* producto entero o real entero o real<br />
div división entera entero entero<br />
/ división entero o real real<br />
mod resto entero entero<br />
C.1.2. Operadores relacionales<br />
Operador operación operando resultado<br />
= igualdad simple, string o puntero lógico<br />
desigualdad simple, string o puntero lógico<br />
< menor simple o string lógico<br />
> mayor simple o string lógico<br />
= mayor o igual simple o string lógico<br />
C.1.3. Operadores lógicos<br />
Operador operación operando resultado<br />
not negación lógico lógico<br />
or disyunción lógico lógico<br />
and conjunción lógico lógico<br />
C.2. Identificadores estándares<br />
Constantes
Pág. 216 Breve referencia de Pascal<br />
Tipos<br />
Variables<br />
false maxint true<br />
boolean char integer real text<br />
input output<br />
Funciones con argumentos numéricos<br />
Función operación argumento resultado<br />
abs valor absoluto entero o real entero o real<br />
arctan arco tangente entero o real real<br />
chr carácter en orden entero carácter<br />
exp exponencial (e x ) entero o real real<br />
ln logaritmo (base e) entero o real real<br />
odd (∗) si impar entero lógico<br />
round (†) redondeo real entero<br />
sin seno real o entero real<br />
sqr cuadrado real o entero real o entero<br />
sqrt raíz cuadrada real o entero (≥ 0) real<br />
trunc (‡) truncar real entero<br />
(∗) odd (x) es verdadero si x es impar.<br />
(†)<br />
round (x) =trunc(x +0.5) para x ≥ 0, y round (x) =trunc(x − 0.5) para<br />
x ≤ 0.<br />
Otras funciones<br />
(‡) trunc(x) = ⌊x⌋ si x ≥ 0,<br />
⌈x⌉ si x
C.3. Nombres Reservados Pág. 217<br />
C.3. Nombres Reservados<br />
and array begin case const div<br />
downto do else end file for<br />
function goto if in label mod<br />
nil not of or packed procedure<br />
program record repeat set then to<br />
type until var while with
Apéndice D<br />
Algunas notaciones y<br />
símbolos usados<br />
Ponemos aquí algunas notaciones, abreviaturas y expresiones usadas (que<br />
pueden diferir de algunas ya conocidas), sólo como referencia: el lector debería<br />
mirarlo rápidamente y volver cuando surja alguna duda.<br />
D.1. Lógica<br />
⇒ implica o entonces. x > 0 ⇒ x = √ x 2 puede leerse como “si x es<br />
positivo, entonces. . . ”.<br />
⇔ si y sólo si. Significa que las condiciones a ambos lados son equivalentes.<br />
Por ejemplo x ≥ 0 ⇔|x| = x se lee “x es positivo si y sólo si. . . ”.<br />
∃ existe. ∃ k ∈ Z tal que. . . se lee “existe k entero tal que...”.<br />
∀ para todo. ∀ x>0, x = √ x 2 se lee “para todo x positivo,...”.<br />
¬ La negación lógica no. Sip es una proposición lógica, ¬p se lee “no p”.<br />
¬p es verdadera ⇔ p es falsa.<br />
∧ La conjunción lógica y. Sip y q son proposiciones lógicas, p ∧ q es<br />
verdadera ⇔ tanto p como q son verdaderas.<br />
∨ La disyunción lógica o. Sip y q son proposiciones lógicas, p ∨ q es<br />
verdadera ⇔ obienp es verdadera o bien q es verdadera.<br />
D.2. Conjuntos<br />
∈ pertenece. x ∈ A significa que x es un elemento de A.<br />
/∈ no pertenece. x ∈ A significa que x no es un elemento de A.<br />
∪ unión de conjuntos, A ∪ B = {x : x ∈ A o x ∈ B}.<br />
∩ intersección de conjuntos, A ∩ B = {x : x ∈ A y x ∈ B}.
D.3. Números: conjuntos, relaciones, funciones Pág. 219<br />
\ diferencia de conjuntos, A \ B = {x : x ∈ A y x/∈ B}.<br />
||,<br />
# cardinal, |A|, o a veces #(A), es la cantidad de elementos en el conjunto<br />
A.<br />
∅ El conjunto vacío, #(∅) =0.<br />
D.3. Números: conjuntos, relaciones, funciones<br />
N El conjunto de números naturales, N = {1, 2, 3,...}. Observar que para<br />
nosotros 0 /∈ N.<br />
Z Los enteros, Z = {0, 1, −1, 2, −2,...}.<br />
Q Los racionales p/q, donde p, q ∈ Z, q = 0.<br />
R Los reales. Son todos los racionales más números como √ 2, π, etc.,que<br />
no tienen una expresión decimal periódica.<br />
R+<br />
Los reales positivos, R+ = {x ∈ R : x>0}.<br />
C Los complejos, de la forma z = a + bi, cona, b ∈ R y donde i es la<br />
unidad imaginaria, “ i = √ −1”.<br />
±x Dado el número x, ±x representa dos números: x y −x.<br />
| Para m, n ∈ Z, m | n se lee m divide a n, on es múltiplo de m,ysignifica<br />
que existe k ∈ Z tal que n = km.<br />
≈ aproximadamente. x ≈ y se lee “x es aproximadamente igual a y”.<br />
≪ mucho menor. x ≪ y se lee “x es mucho menor que y”.<br />
≫ mucho mayor. x ≫ y se lee “x es mucho mayor que y”.<br />
|x| El valor<br />
<br />
absoluto o módulo del número x. Siz = a+bi∈ C con a, b ∈ R,<br />
|z| = a2 + b2 .<br />
⌊x⌋ El piso de x, x ∈ R. Es el mayor entero que no supera a x, porloque<br />
⌊x⌋ ≤x0, o logaritmo en base e, lnx =<br />
log e x. Es la inversa de la exponencial, y =lnx ⇔ e y = x.
Pág. 220 Algunas notaciones y símbolos usados<br />
✎ Para no confundir con los logaritmos en base 10, evitaremos la<br />
notación log x: si nos referimos a la base e usaremos ln x, yen<br />
otras bases log b x.<br />
sen x,<br />
sin x Las función trigonométrica seno, definida para x ∈ R. Su inversa es la<br />
función arcsen (arcsin en inglés).<br />
cos x Las función trigonométrica coseno, definida para x ∈ R. Su inversa es<br />
la función arccos.<br />
tan x Las función trigonométrica tangente, definida como tan x =senx/ cos x.<br />
Su inversa es la función arctan.<br />
arcsen x,<br />
arccos x,<br />
arctan x Funciones trigonométricas inversas respectivamente de sen, cos y tan.<br />
sgn(x),<br />
signo(x) Las función signo, definida para x ∈ R por<br />
⎧<br />
⎪⎨ 1 si x>0,<br />
signo(x) = 0 si x =0,<br />
⎪⎩<br />
−1 si x
Bibliografía<br />
[1] E. M. Baras: Lotus 1-2-3 — Guía del Usuario (2. a ed.), Osborne - Mc-<br />
Graw Hill, 1990. (Citado en pág. 171.)<br />
[2] J. L. Bentley: More Programming Pearls, Addison-Wesley, 1988. (Citado<br />
en pág. 165.)<br />
[3] N. L. Biggs: Introduction to Computing with Pascal, Oxford Science Publications,<br />
1989. (Citado en pág. 24.)<br />
[4] A. Engel: Exploring Mathematics with your computer, The Mathematical<br />
Association of America, 1993. (Citado en págs. 2, 5, 65, 90 y 171.)<br />
[5] E. Gentile: Aritmética elemental en la formación matemática, Red<br />
Olímpica, 1991. (Citado en pág. 5.)<br />
[6] G. H. Hardy y E. M. Wright: An Introduction to the Theory of Numbers<br />
(4. a ed.), Oxford University Press, 1975. (Citado en pág. 171.)<br />
[7] K. Jensen y N. Wirth: Pascal—User Manual and Report, Springer Verlag,<br />
1985. (Citado en págs. 2, 5, 90 y 109.)<br />
[8] R. Johnsonbaugh: Matemáticas Discretas (4. a ed.), Prentice Hall, 1999.<br />
(Citado en pág. 140.)<br />
[9] SirT.L.Heath: The thirteen books of Euclid’s Elements, vol.2,Dover<br />
Publications, 1956. (Citado en pág. 54.)<br />
[10] B. W. Kernighan y D. M. Ritchie: El lenguaje de programación C<br />
(2. a ed.), Prentice-Hall Hispanoamericana, 1991. (Citado en págs. 5, 12,<br />
132 y 162.)<br />
[11] D. E. Knuth: The Art of Computer Programming, Addison-Wesley. Vol. 1<br />
(3. a ed.), 1997; vol. 2 (3. a ed.), 1997; vol. 3 (2. a ed.), 1997. (Citado en págs.<br />
92, 96 y 165.)<br />
[12] C. H. Papadimitriou y K. Steiglitz: Combinatorial Optimization, Algorithms<br />
and Complexity, Dover, 1998. (Citado en pág. 154.)<br />
[13] K. H. Rosen: Elementary Number Theory and its Applications (3. a ed.),<br />
Addison Wesley, 1993. (Citado en pág. 39.)<br />
[14] H. A. Taha: Operations Research — An Introduction (4. a ed.), MacMillan<br />
Publishing Co., 1987. (Citado en pág. 171.)
Pág. 222 Bibliografía<br />
[15] N. Wirth: Introducción a la Programación Sistemática, El Ateneo, 1984.<br />
(Citado en págs. 2 y 80.)<br />
[16] N. Wirth: Algoritmos y Estructuras de Datos, Prentice-Hall Hispanoamericana,<br />
1987. (Citado en págs. 2, 5, 80, 96, 102, 109, 131, 132 y 162.)
Índice de figuras<br />
2.1. Esquema de transferencia de datos en la computadora. ...... 8<br />
2.2. Bits y byte. .............................. 8<br />
2.3. Esquema del desarrollo de un programa. . . . . . . . . . . . . . . 9<br />
2.4. Esquema del programa ejecutable en la memoria. . . . . . . . . . 10<br />
3.1. Datos de tipos elementales en la memoria. ............. 14<br />
3.2. Los datos con sus identificadores. .................. 14<br />
3.3. Esquema de la “densidad variable”. . . . . . . . . . . . . . . . . . 20<br />
5.1. Gráficos de y =cosx y y = x. .................... 44<br />
5.2. Aproximándose al punto fijo de cos x. ............... 44<br />
6.1. Esquema del arreglo v guardado en memoria. . . . . . . . . . . . 56<br />
6.2. Esquema del problema de Flavio Josefo con n =5ym =3. . . . 62<br />
7.1. Una función continua con distintos signos en los extremos. .... 71<br />
7.2. Programa, funciones y procedimientos en la memoria. ...... 75<br />
7.3. Intercambio de los valores de u y v. . . . . . . . . . . . . . . . . . 77<br />
10.1. Ordenando por conteo. ........................103<br />
10.2. Esquema del registro de tipo “ complejo” enmemoria.......105<br />
11.1. Contando la cantidad de caminos posibles. .............112<br />
11.2. Las torres de Hanoi. . . . . . . . . . . . . . . . . . . . . . . . . . 114<br />
12.1. Una estructura “lineal”. . . . . . . . . . . . . . . . . . . . . . . . 126<br />
12.2. Un árbol binario ordenado. . . . . . . . . . . . . . . . . . . . . . 127<br />
12.3. Disposición del arreglo de registros luego de ingresar los datos. . 128<br />
12.4. Disposición del árbol binario luego de ingresar los datos. . . . . . 128<br />
12.5. Los registros de la figura 12.4 “proyectados”. . . . . . . . . . . . 128<br />
12.6. Los registros con índices para la estructura de árbol binario. . . . 129<br />
13.1. Un grafo con 6 vértices y 7 aristas. . . . . . . . . . . . . . . . . . 134<br />
13.2. Un grafo no conexo y un árbol. . . . . . . . . . . . . . . . . . . . 135<br />
13.3. Un grafo con pesos en las aristas. ..................142<br />
14.1. El puntero p y la variable apuntada pˆ. . . . . . . . . . . . . . . 156<br />
15.1. Aproximación de sen x mediante un polinomio. . . . . . . . . . . 163
Índice de cuadros<br />
8.1. Estructura de un programa Pascal. . . . . . . . . . . . . . . . . . 82<br />
8.2. Diferencias entre el estándar y Turbo Pascal. ........... 88<br />
10.1. Comparación de algoritmos de clasificación. . . . . . . . . . . . . 103<br />
13.1. Esquema del algoritmo visitar. . . . . . . . . . . . . . . . . . . . 138<br />
13.2. Esquema del algoritmo de Dijkstra. . . . . . . . . . . . . . . . . . 143<br />
13.3. Esquema del algoritmo de Kruskal. . . . . . . . . . . . . . . . . . 150
Índice alfabético<br />
εmaq, 34, 220<br />
εmin, 19, 34, 220<br />
φ (de Euler), 53<br />
adyacencia<br />
lista de, 161<br />
matriz de, 136<br />
adyacente (vértice), 134<br />
aguja<br />
de Buffon, 94<br />
Hanoi, 113, 121<br />
Al-Khwarizmi, 25<br />
algoritmo, 25<br />
de búsqueda, ver búsqueda<br />
de clasificación, ver clasificación<br />
de Eratóstenes, 60<br />
de Euclides, 49<br />
de Prim, 145<br />
Dijkstra, 141<br />
para grafos, ver grafo<br />
para permutaciones, 124<br />
para subconjuntos, 121<br />
visitar, 137<br />
and, 23<br />
aproximación, ver también convergencia,<br />
método de Monte<br />
Carlo<br />
acantidaddeprimos,61<br />
de n!, 111<br />
de sen x, 164<br />
de número armónico Hn, 41<br />
de raíz cuadrada, 45<br />
de raíz de ecuación, 70<br />
árbol<br />
binario ordenado, 126<br />
caracterización, 135<br />
definición, 135<br />
hoja, 135<br />
rama, 135<br />
archivo<br />
caja de herramientas, 84<br />
de texto, 86, ver también editor<br />
de textos<br />
para guardar programa, 10<br />
arista<br />
de grafo, 133<br />
extremo, 134<br />
lista de, 136, 161<br />
Arquímedes, 59<br />
array, ver arreglo<br />
arreglo, 55, ver también lista<br />
como conjunto, 104<br />
dimensión, 55<br />
ASCII (código), 23, 24<br />
año bisiesto, 27<br />
begin-end<br />
en parte principal, 10<br />
para agrupar, 28<br />
para funciones y procedimientos,<br />
67<br />
Bigollo, ver Fibonacci<br />
Binet, 116<br />
bisección (método), 70<br />
bit, 7<br />
Boole, 13<br />
boolean, 13<br />
bucle (lazo), 30<br />
Buffon, 94<br />
búsqueda<br />
aloancho,139<br />
binaria, 98, 169<br />
en grafo, 137<br />
en profundidad, 140<br />
exhaustiva, 132<br />
lineal, 57<br />
lineal con centinela, 96<br />
byte, 8<br />
caja de herramientas, 84<br />
cálculo
Pág. 226 Índice alfabético<br />
de suma o producto, 32<br />
numérico, ver sec. 5.1<br />
calendario (juliano y gregoriano), 27<br />
camino en grafo, 134<br />
carácter<br />
comparación, 27<br />
imprimible, 23<br />
yordinal,24<br />
case, 26<br />
caótico, 47<br />
char, 13, ver también carácter<br />
chr, 24<br />
ciclo<br />
de Euler, 162<br />
en grafo, 134<br />
clasificación<br />
comparación de métodos, 103<br />
de caracteres, 27<br />
de registros, 107, 108<br />
estabilidad, 109<br />
inserción directa, 101<br />
intercambio directo, 101<br />
lista encadenada, 159, 160<br />
métodos, 101<br />
por conteo, 103<br />
por fusión, 102, 166<br />
recursiva, 112<br />
selección directa, 101<br />
yárbol binario, 126<br />
cola, 119<br />
de prioridad, 145<br />
del supermercado, 168<br />
fifo, 119, 140<br />
lifo (pila), 119, 140<br />
consola, 7<br />
const, 32, 57, 66<br />
contador, 31, 32, 34, 93, 122–124,<br />
126<br />
en arreglo, 56<br />
en lazo, 71, 97, 102, 104<br />
en “ for ”, ver variable de control<br />
convergencia, 42, 43, ver también sec.<br />
5.1<br />
cuadrática, 54<br />
de n/ ln n, 61<br />
de polinomios, 63<br />
de punto fijo, 47<br />
global, 54<br />
método de punto fijo, 42<br />
CPU, 7<br />
criba, 59<br />
de Eratóstenes, 59<br />
criterio de parada, 47, 51, 70, 72<br />
de Moivre, A., 116<br />
De Morgan, 63<br />
leyes de, 23<br />
densidad, 19<br />
Dijkstra, 142, 146<br />
algoritmo, 141<br />
Diofanto, 49<br />
Dirichlet, 43, 65<br />
principio de, 94<br />
dispose, 156<br />
div, 17<br />
do<br />
y for, 35<br />
y while, 30<br />
downto, 36, ver también for-do<br />
e (= 2.718 ...), 36, 42, 111, 219<br />
Monte Carlo, 164<br />
ecuación<br />
cuadrática, 41<br />
diofántica, 48<br />
no lineal, 49<br />
editor de textos, 9, 88, 89, 161, ver<br />
también archivo de texto<br />
else, ver if-then<br />
end, ver también begin-end<br />
“ ; ”antesde,29<br />
sin begin, 105<br />
eof, 88<br />
eoln, 37<br />
epsilon<br />
de máquina (εmaq), 34<br />
mínimo (εmin), 34<br />
Eratóstenes, 59<br />
criba, 59<br />
error<br />
absoluto y relativo, 47<br />
numérico, 33, 34, 36, 41, 45, 72<br />
estrategia, ver método<br />
Euclides, 50, 59<br />
algoritmo para mcd, 48–50<br />
Euler, 54, 116, 133<br />
ciclo de, 141, 162<br />
constante de (γ), 41<br />
función φ, 53
Índice alfabético Pág. 227<br />
factorial, 33<br />
recursivo, 111<br />
<strong>Fe</strong>rmat, 49, 65<br />
Fibonacci, 116<br />
número de, 115, 123, 168<br />
Flavio Josefo, 61, 160<br />
for-do, 35<br />
fórmula<br />
de Gauss, 33, 48, 110<br />
de Stirling para n!, 111<br />
Euler-Binet, 116<br />
para x y , 36<br />
forward, 74<br />
función continua, 54<br />
function, ver cap. 7<br />
Gauss, 33<br />
suma de, 32, 41, 110<br />
get (en Pascal), 120<br />
get (en cola), 120<br />
Goldbach, 54<br />
cuenta de, 164<br />
grafo, 133, 137<br />
arista, 133<br />
conexo, 134<br />
dirigido (digrafo), 135<br />
representación, 136<br />
simple, 134<br />
vértice, 133<br />
y lista encadenada, 161<br />
Gregorio (Papa), 27<br />
Hanoi (torres de), 113<br />
Horner, 63<br />
regla de, 63<br />
identificador (de variable), 13<br />
if-then, 26<br />
input al comienzo de programa, 10<br />
integer, 13<br />
juego, ver también problema<br />
cartas (clasificar), 101<br />
de Nim, 169<br />
generala, 164<br />
Julio César, 27<br />
Kruskal, 146<br />
algoritmo, 149<br />
Lagrange, 163, 164<br />
lazo, 30<br />
Leonardo de Pisa, ver Fibonacci<br />
lineal<br />
estructura de arreglo, 126<br />
lista, 160<br />
lista, ver también arreglo<br />
de aristas, 136<br />
encadenada, 157<br />
matriz<br />
arreglo multidimensional, 85<br />
de adyacencias, 136<br />
de costos, 144<br />
máximo común divisor, ver mcd<br />
maxint, 19, 220, ver también integer<br />
mcd, 48, 49, 53, 65, 164<br />
recursivo, 116<br />
media (indicador estadístico), 168<br />
mediana (indicador estadístico), 168<br />
memoria (de computadora), 7<br />
método<br />
babilónico, 45, 53<br />
de barrido, 48<br />
de bisección, 70<br />
de clasificación ver clasificación,<br />
101<br />
de Monte Carlo<br />
para e, 164<br />
para π, 93, 94<br />
de Newton-Raphson, 46<br />
de punto fijo, 42<br />
mínimo común múltiplo (mcm), 52<br />
mod, 22<br />
moda (indicador estadístico), 168<br />
new, 156<br />
Newton, 46<br />
nil, 156<br />
nivel (en árbol), 135<br />
normalización, 47<br />
not, 23<br />
número<br />
armónico (Hn), 41<br />
de Fibonacci, ver Fibonacci<br />
entero, ver integer<br />
primo, ver primo<br />
real, ver real<br />
or, 23
Pág. 228 Índice alfabético<br />
ord, 24<br />
ordinal y carácter, 24<br />
output al comienzo de programa, 10<br />
packed, 83<br />
parámetro<br />
formal, 77<br />
real, 77<br />
pasar (por valor o referencia), 77<br />
Pascal (Blaise), 9<br />
período (en expresión decimal), 64,<br />
89<br />
pi (π), 32, 164<br />
en programa, 32, 76<br />
Monte Carlo, 93, 94<br />
punto fijo, 45<br />
pila, 119<br />
Pitágoras, 50<br />
terna pitagórica, 49<br />
polinomio, 62<br />
de Lagrange, 63, 163<br />
pop (en pila), 120<br />
potencia (cálculo de), 36, 65–68<br />
Prim, 146<br />
algoritmo, 145<br />
primo, 52, 54, 55, 65, 113, ver también<br />
criba de Eratóstenes<br />
Euclides, 52<br />
existencia de infinitos, 52<br />
factorización, 50, 52<br />
y recursión, 117<br />
primos entre sí (coprimos),50<br />
principio<br />
de Dirichlet, 94<br />
del casillero, 94<br />
del palomar, 94<br />
problema<br />
cola del supermercado, 168<br />
contando palabras en texto, 89<br />
cuenta de Goldbach, 164<br />
de “manos”, 165<br />
de Flavio Josefo, 61, 160<br />
de las cajas, 100, 108<br />
del cumpleaños, 94<br />
del viajante, 131<br />
interés sobre saldo, 73<br />
numérico, 41<br />
producción de fábrica, 169<br />
show de televisión, 94<br />
torres de Hanoi, 113, 117<br />
visibilidad en el plano, 164<br />
procedure, ver cap. 7<br />
programa, 8<br />
ejecutable, 9<br />
fuente, 9<br />
mencionado<br />
anchoprimero, 139–141, 144,<br />
145, 147, 148, 153, 162, 199<br />
arbolbinario, 129, 140, 156, 157,<br />
160, 197<br />
babilonico, 47, 181<br />
biseccion, 71, 72, 189<br />
busquedalineal, 57, 59, 82, 96,<br />
184<br />
caracteres1, 24, 175<br />
caracteres2, 27, 176<br />
cifras, 34, 179<br />
comparar, 27, 176<br />
dado, 92, 195<br />
dados, 92, 93, 196<br />
dearchivoaconsola, 87, 89, 195<br />
deconsolaaarchivo, 194<br />
deconsolaarchivo, 87–89<br />
dijkstra, 144, 145, 147, 148,<br />
151–153, 202<br />
enteroareal, 20, 174<br />
eolnprueba, 38, 180<br />
epsmin, 34, 37, 179<br />
eratostenes, 60, 186<br />
euclides, 51, 182<br />
factoresprimos, 53, 183<br />
flaviojosefo1, 61, 187<br />
gauss, 33, 36, 37, 178<br />
grafos, 161, 209<br />
holamundo, 10, 173<br />
intercambio, 77, 192<br />
leerdato, 17, 26, 173<br />
listas, 159, 160, 206<br />
maximocaracter, 83, 84, 86, 121,<br />
192<br />
positivo, 22, 23, 175<br />
potencia, 36, 180<br />
potencias2, 67, 68, 188<br />
potencias3, 68, 77, 188<br />
raiz, 18, 174<br />
resto, 30, 37, 62, 177<br />
segundos, 22, 175<br />
sumardatos, 39, 56, 57, 181<br />
sumardos, 16, 17, 21, 173<br />
tablaseno, 32, 37, 75, 76, 177
Índice alfabético Pág. 229<br />
tablaseno2, 76, 77, 191<br />
unidades, 56, 58, 184<br />
valorabsoluto, 26, 176<br />
prueba de escritorio, 30<br />
puntero, 155<br />
push (en pila), 120<br />
put (en Pascal), 120<br />
put (en cola), 120<br />
Raphson, 46<br />
read, 17, 83<br />
readln, 17<br />
real, 13<br />
record, 105<br />
recorrido de grafo, 137<br />
registro, 105<br />
repeat-until, 34<br />
reset, 87<br />
rewrite, 87<br />
signo (función), 43, 220<br />
Stirling, 111<br />
suma de Gauss, ver Gauss<br />
técnica, ver método<br />
teorema<br />
caracterización de árboles, 135,<br />
146<br />
de Dirichlet, 65<br />
de Euler, 133, 141<br />
de los números primos, 61<br />
suma de grados, 137<br />
terna pitagórica, 49<br />
text<br />
como argumento, 87<br />
tipo, 68, 86<br />
texto, ver editor de, archivo de<br />
then, ver if-then<br />
tipo (de variable), 13<br />
to, ver for-do<br />
type, 81<br />
until, ver repeat-until<br />
Van der Monde, 163<br />
var<br />
declaración de variable, 14<br />
pasar por referencia, 78<br />
variable, 13<br />
carácter (char), 13, 23<br />
de control en “ for ”, 36<br />
elemental, 13<br />
entera (integer), 13, 16<br />
global, 69<br />
local, 68<br />
lógica (boolean), 13, 22<br />
real (real), 13, 16<br />
vértice<br />
adyacente, 134, 161<br />
aislado, 134<br />
de grafo, 133<br />
grado, 137<br />
von Neumann, 7<br />
while-do, 30<br />
Wiles, 49, 65<br />
with, 106<br />
write, 16<br />
writeln, 11<br />
Zeckendof, 168