Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
Computación y Programación
con el lenguaje Pascal
Néstor E. Aguilera
2003
Índice general
1. Preliminares 1
1.1. ¿Qué es esto? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Cómo usar este libro . . . . . . . . . . . . . . . . . . . . . . . . . 2
1.3. Organización y convenciones usadas . . . . . . . . . . . . . . . . 3
2. El primer contacto 5
2.1. Un poco, muy poco, sobre cómo funciona la computadora . . . . 5
2.2. El puntapié inicial . . . . . . . . . . . . . . . . . . . . . . . . . . 6
4. Tomando control 21
4.1. “If” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 21
4.2. “While” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
4.3. “Repeat” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 28
4.4. “For” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30
4.5. “Eoln” . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
5. Aplicaciones 35
5.1. Cálculo numérico elemental . . . . . . . . . . . . . . . . . . . . . 35
5.1.1. Mezclando números grandes y pequeños . . . . . . . . . . 35
5.1.2. Métodos iterativos: puntos fijos . . . . . . . . . . . . . . . 37
5.1.3. El método babilónico . . . . . . . . . . . . . . . . . . . . . 40
5.2. Números enteros y divisibilidad . . . . . . . . . . . . . . . . . . . 42
5.2.1. Ecuaciones diofánticas . . . . . . . . . . . . . . . . . . . . 42
5.2.2. Máximo común divisor y el algoritmo de Euclides . . . . . 43
5.2.3. Números primos . . . . . . . . . . . . . . . . . . . . . . . 45
5.3. Problemas Adicionales . . . . . . . . . . . . . . . . . . . . . . . . 47
Pág. ii Índice general
6. Arreglos 49
6.1. Dimensionamiento de arreglos . . . . . . . . . . . . . . . . . . . . 49
6.2. Búsqueda Lineal . . . . . . . . . . . . . . . . . . . . . . . . . . . 50
6.3. Cribas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52
6.4. Polinomios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 55
6.5. Problemas Adicionales . . . . . . . . . . . . . . . . . . . . . . . . 56
7. Funciones y Procedimientos 59
7.1. Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
7.2. El método de la bisección . . . . . . . . . . . . . . . . . . . . . . 63
7.3. Procedimientos . . . . . . . . . . . . . . . . . . . . . . . . . . . . 67
7.4. Pasando por valor o por referencia . . . . . . . . . . . . . . . . . 68
10.Búsqueda y clasificación 93
10.1. Búsqueda lineal con centinela . . . . . . . . . . . . . . . . . . . . 93
10.2. Búsqueda binaria . . . . . . . . . . . . . . . . . . . . . . . . . . . 95
10.3. Métodos elementales de clasificación . . . . . . . . . . . . . . . . 98
10.4. Registros (Records) . . . . . . . . . . . . . . . . . . . . . . . . . . 102
10.5. Problemas Adicionales . . . . . . . . . . . . . . . . . . . . . . . . 104
11.Recursión 109
11.1. Funciones y procedimientos definidas recursivamente . . . . . . . 109
11.2. Los Grandes Clásicos de la Recursión . . . . . . . . . . . . . . . . 112
11.3. Problemas Adicionales . . . . . . . . . . . . . . . . . . . . . . . . 115
Bibliografı́a 209
Capı́tulo 1
Preliminares
El primer contacto
A su vez, para las computadoras más recientes, estas unidades resultan dema-
siado pequeñas para alimentar a la CPU, por lo que los bits o bytes se agrupan
formando cajas de, por ejemplo, 32, 64 o 128 bits (usualmente potencias de 2),
siempre conceptualmente alineadas.
Los mismos programas pueden considerarse como un tipo especial de datos,
con “instrucciones” que dicen a la CPU qué hacer. En particular, el sistema
operativo de la computadora es un programa que alimenta constantemente a la
CPU, y le va a indicar, por ejemplo, que ejecute nuestro programa, leyendo las
instrucciones que contiene.
“ (input, output) ”.
2.2. El puntapié inicial Pág. 7
• char para guardar caracteres, i.e. letras, signos de puntuación y otros que
veremos más adelante,
Ahora tenemos el problema de cómo acceder a esas cajas que guardan carac-
teres o números. Esto es sencillo: les ponemos nombres para identificarlas. Las
Pág. 10 Tipos de datos elementales
cajas ası́ nombradas se llaman variables pues podremos colocar en ellas datos
distintos o (¡ejem!) variables y los nombres que reciben se llaman identificadores.
Cuando redactamos el programa, debemos indicar al compilador los nom-
bres y tipos de variables que usaremos, procedimiento llamado declaración de
variables. En el programa se pone una lista de variables y tipos después de la
palabra clave “ var ”.
Una cosa trae a la otra, y tenemos el problema de que no podemos poner
cualquier nombre. Por ejemplo, no es conveniente usar “ writeln ” o “ write ”,
que son usados por Pascal para instrucciones del lenguaje. Además, en Pascal
hay nombres reservados, que no podemos usar como identificadores1 . Aparte
de estas palabras prohibidas, los identificadores en Pascal pueden ser cualquier
sucesión de letras mayúsculas o minúsculas y dı́gitos, pero
Desde ya que
aunque hay compiladores que automáticamente ponen algún valor al hacer las
declaraciones.
Para tratar de entender esta jeringoza, pasemos a estudiar algunos ejemplos
comenzando con los tipos numéricos “ integer ” y “ real ”, para luego conside-
rar los tipos “ boolean ” y “ char ”.
3.2.1. “Readln”
Es un poco tedioso estar cambiando los valores en el programa fuente para
hacer los cálculos. Mucho más razonable es ingresar los datos a nuestro antojo:
Problema 3.3. El programa leerdato (pág. 159) lee un entero ingresado por
terminal y lo imprime. Observar la sintaxis e instrucciones, similares al programa
sumardos, la única novedad es el uso de “ readln ”.
a) Compilar y ejecutar el programa, comprobando su funcionamiento.
b) Ingresar, en ejecuciones sucesivas del programa, los siguientes datos (sin las
comillas) seguidos de <retorno>, conservando los espacios intermedios
cuando corresponda:
i) “ 23 ”; ii) “ 2 ”; iii) “ 2 3 ”;
iv ) “ 2a ”; v ) “ a2 ”; vi) “ <retorno><retorno>2”.
3.2. Tipos numéricos: entero y real Pág. 13
ya sean enteros o reales. Ası́, hay un máximo entero representable, que en Pascal
se llama maxint, y hay un menor número positivo representable que llamaremos
εmin y del cual hablaremos en el problema 4.14.
Problema 3.5.
a) Suponiendo que el tipo “ integer ” tenga asignados 16 bits, ¿cuántos núme-
ros enteros pueden representarse?
b) Análogamente, si el tipo “ real ” tiene asignados 32 bits, ¿cuántos números
reales pueden representarse en la máquina? ✄
Problema 3.6. Hacer un programa para imprimir el valor de maxint corres-
pondiente al compilador que se usa.
Sugerencia: no declarar variables y poner el renglón
writeln(’ El entero máximo es: ’, maxint)
Nota: En los compiladores más viejos, maxint = 32767. ✄
Sea que las variables de tipo “ integer ” y “ real ” ocupen el mismo lugar o
no, su codificación como cadenas de bits es bien distinta. Los enteros se represen-
tan consecutivamente (del menor al mayor), mientras que para representar los
números reales se dividen los bits en dos grupos, uno representando la mantisa y
otro el exponente, como se hace en la notación “cientı́fica” al escribir 0.123×1045
(.123 es la mantisa y 45 el exponente en base 10, pero la computadora trabaja
en base 2).
2 La cantidad de bits en cada caso depende del compilador. Incluso ambos tipos podrı́an
Problema 3.14.
a) Hacer un programa para averiguar el comportamiento de “ mod ”, tomando
como entradas a = ±7 y b = ±3.
b) Comprobar si el valor de “ (a div b) * b + (a mod b) ” coincide con el
de a. ✄
3.3. Variables lógicas Pág. 17
Debemos destacar que al comparar números, Pascal usa a veces una sintaxis
ligeramente diferente a la usual:
Matemáticas Pascal
= =
6= <>
> >
≥ >=
< <
≤ <=
Matemáticas Pascal
(a > 0) ∧ (b > 0) (a > 0) and (b > 0)
(a > 0) ∨ (b > 0) (a > 0) or (b > 0)
¬(a > 0) not (a > 0)
pero la expresión matemática a < b < c debe escribirse en Pascal como “ (a <
b) and (b < c) ”. También observamos que para preguntar si a 6= b en Pascal
puede ponerse tanto “ a <> b ” o como “ not (a = b) ”, y de modo similar para
las desigualdades.
3.4. Caracteres
Ası́ como la computadora guarda internamente los números como ristras de
ceros y unos en la computadora, también las letras se guardan como ristras de
ceros y unos. La forma en que se hace esta codificación depende del sistema
operativo, pero un código particularmente usado es el ASCII con el que se con-
vierten a números entre 0 y 127 algunos caracteres: letras como ‘ a ’, ‘ b ’,. . . ,‘ z ’
o ‘ A ’,. . . ,‘ Z ’, números (dı́gitos) como 0,1,. . . ,9, y sı́mbolos como ‘ + ’, ‘ $ ’, etc.
En ASCII, los caracteres imprimibles como letras y sı́mbolos empiezan a par-
tir de 32, pero no todas las letras están representadas, como la ‘ ~ n ’ o las vocales
acentuadas del castellano. A fin de codificar también estos caracteres, hay ex-
tensiones para cubrir caracteres con números hasta 255, pero estas extensiones
no son del estándar ASCII y dependen en general del sistema operativo.
En Pascal, dado un carácter podemos encontrar su número de orden me-
diante “ ord ” y recı́procamente, dado un entero podemos ver qué carácter le
corresponde mediante la función “ chr ”3 . El estándar Pascal establece que:
• las letras minúsculas, ‘ a ’,‘ b ’,. . . ,‘ z ’, si bien están ordenadas, ¡no son
necesariamente consecutivas!
Tomando control
Con las operaciones que hemos visto, no podemos hacer mucho más que lo
que hace una calculadora sencilla. Las cosas empiezan a ponerse interesantes
cuando disponemos de estructuras de control de flujo, esto es, instrucciones que
nos permiten tomar decisiones sobre si realizar o no determinadas instrucciones
o realizarlas repetidas veces.
Al contar con estructuras de control, podremos verdaderamente comenzar a
describir algoritmos, es decir, instrucciones (no necesariamente en un lenguaje
de programación) que nos permiten llegar a determinado resultado, y encarar
el principal objetivo de este curso: pensar en los algoritmos y cómo traducirlos
a un lenguaje de programación.
Los programas irán aumentando en longitud, lo que nos obligará a encarer
de forma diferente su confección, debiendo pensar con más cuidado primero en
qué queremos hacer, luego en cómo lo vamos a hacer, y finalmente pasar a los
detalles. Pascal, y casi todos los lenguajes de programación, son poco “natu-
rales” en este sentido, exigiendo —por ejemplo— la declaración de variables al
principio del programa fuente, mientras que nosotros pensaremos el programa
de “abajo hacia arriba” (o casi), y recién al final miraremos cómo declarar las
variables necesarias.
En Pascal, las estructuras fundamentales de control son “ if ”, “ while ”,
“ repeat ” y “ for ”, que pasamos a estudiar.
Nota: En Pascal hay otras estructuras de control, como “ cases ”, que no ve-
remos. En otros lenguajes de programación existen otras estructuras, pero casi
siempre están los equivalentes de “ if ” y “ while ”, con las que se pueden
simular las otras.
4.1. “If”
Supongamos que al cocinar decidimos bajar el fuego si el agua hierve, es decir
realizar cierta acción si se cumplen ciertos requisitos. Podrı́amos esquematizar
esta decisión con la sentencia:
El programa valorabsoluto (pág. 162) realiza este cálculo. Observar qué rea-
lizan las distintas instrucciones. ¿Qué pasa si se eliminan los paréntesis después
del “ if ” ?, ¿y si se cambia “ >= ” por “ > ” ?, y si se agrega “ ; ” antes de “ else ”?
La función “ abs ” de Pascal hace el cálculo del valor absoluto. Incorporarla
al programa y verificar que se obtienen los mismos resultados. ✄
Problema 4.2. El programa comparar (pág. 162) toma como datos los números
enteros a y b y determina cuál es mayor o si son iguales.
a) ¿Qué pasa si se escriben las lı́neas que empiezan con “ if ” y terminan en
“ ; ” en un solo renglón?
b) ¿Qué pasa si se incluyen “ ; ” al final de cada uno de los renglones?
c) ¿Qué pasa si se ingresan números reales o letras en vez de enteros?
d ) Modificar el programa para que siempre escriba en la salida el número a
primero (i.e. si a = −5 y b = 3 escriba “ -5 es menor que 3 ” en vez de “ 3
es mayor que -5 ”).
e) Modificarlo para que sólo decida si a = b o a 6= b.
f ) Modificarlo para que sólo decida si a > b o a ≤ b. ✄
programa fuente.
4.2. “While” Pág. 25
4.2. “While”
La estructura “ while ” es una estructura para realizar repetidas veces una
misma tarea. Junto con “ repeat ” y “ for ” —que veremos más adelante— reci-
ben el nombre común de lazos o bucles para indicar esta capacidad de repetición.
Supongamos que voy al supermercado con cierto dinero para comprar la
mayor cantidad posible de botellas de cerveza. Podrı́amos ir calculando el di-
nero que nos va quedando a medida que vamos poniendo botellas en el carrito:
cuando no alcance para más botellas, vamos a la caja. Una forma de poner
esquemáticamente esta acción como
diferente.
4.2. “While” Pág. 27
X
n
sn = 1 + 2 + 3 + · · · + n = i,
i=1
n × (n + 1)
sn = .
2
Según se dice, Karl Friederich Gauss (1777–1855) tenı́a 8 años cuando el
maestro de la escuela le dio como tarea sumar los números de 1 a 100 para
mantenerlo ocupado (y que no molestara). Sin embargo, hizo la suma muy
rápidamente al observar que la suma era 50 × 101.
El programa gauss (pág. 164) calcula sn sumando los términos (y no usando
la fórmula de Gauss).
a) ¿Cuál es el valor de n al final del lazo? Sugerencia: hacer una “prueba de
escritorio”.
b) ¿Qué pasa si se ingresa n ≤ 0?
c) La variable suma está declarada como de tipo real. Si se declarara como de
tipo entero, ¿cuál serı́a el máximo n para el cual se podrı́a calcular sn (i.e.
para el cual sn ≤ maxint )?
d ) ¿Cuántas asignaciones se realizan (en términos de n) dentro del lazo?
e) Modificar el programa de modo que a la salida exprese también el valor
original de n (algo como “ La suma de 1 a 100 es 5050 ” cuando n =
100).
f ) Modificar el lazo “ while ” para que se sume “al derecho”, i.e. primero los
términos más chicos. ✄
4.3. “Repeat”
Si queremos ir a la facultad caminando, podrı́amos poner
4.3. “Repeat” Pág. 29
4.4. “For”
Hay veces que queremos repetir una acción un número fijo de veces. Por
ejemplo, al subir una escalera con 10 escalones harı́amos algo como
b) No es posible tomar bases y exponentes muy grandes sin que haya overflow
o underflow, i.e. resultados que no se pueden representar en la máquina por
ser muy grandes o muy pequeños. Probar con distintos valores hasta obtener
este comportamiento, e.g. 1010 , 100100, (1/10)10 , (1/100)100, etc.
c) Modificar el programa de modo de imprimir el valor de i al finalizar el lazo.
Nota: Como mencionáramos, según el estándar el valor no está definido,
pero muchos compiladores no respetan esta convención y el valor obtenido
puede depender del compilador: no debe usarse esta variable después del
lazo “ for ” sin antes re-asignarla.
d ) Agregar sentencias para considerar también el caso de n negativo. Sugeren-
cia: ab = 1/a−b , usar la función “ abs ” y condicional “ if ”.
e) Algunos compiladores admiten la operación “ x ** y ”, que no es del están-
dar Pascal, para el cálculo de la potencia xy (x ∈ R+ , y ∈ R). Verificar si
el compilador usado admite esta sentencia y, en caso afirmativo, comparar
con el resultado anterior cuando y ∈ N.
f ) Otra forma de calcular la potencia en general es usar xy = ey×ln x . Incorpo-
rar esta fórmula al programa para comparar con los resultados anteriores.
Nota: Es posible que difieran debido a errores numéricos. ✄
Problema 4.16.
a) Modificar los programas tablaseno (pág. 163) y gauss (pág. 164) cambiando
el lazo “ while ” por uno “ for ”.
b) ¿Podrı́a cambiarse el lazo “ while ” por uno “ for ” en el programa resto
(problema 4.9)?, ¿y el lazo “ repeat ” por uno “ for ” en el programa epsmin
(problema 4.14)? ✄
Un problema muy común es entrar datos por terminal para realizar alguna
acción, como sumarlos:
Problema 4.17.
4.5. “Eoln”
Ciertamente es molesto el “dato imposible −1” del problema 4.17, ya que
tendremos que modificar el programa si en algún momento queremos usarlo para
sumar datos que pueden ser negativos. Más cómodo serı́a ingresar como “dato
imposible” alguna señal que no imponga limitaciones sobre los números a entrar
(salvo que sean de tipo entero o real).
Una posibilidad es poner como “dato imposible” un “dato vacı́o”, es decir,
nada, o más correctamente un <retorno> sin entrada de datos, usando la
función de Pascal “ eoln ”.
Pero antes de describir esta función, será conveniente repasar un poco lo di-
cho —y hecho— al introducir la función “ readln ” en la sección 3.2.1 (pág. 12).
Cuando ingresamos un dato, éste no es leı́do por el programa hasta que
ingresemos <retorno>, dándonos la oportunidad de cambiarlo5. Si hemos de-
clarado la variable a (de algún tipo), la sentencia “ readln(a) ” hace que el
programa lea el primer dato que no sea justamente <retorno> y lo guarde
en a si el dato es correcto (si es incorrecto, puede pasar cualquier cosa: ver el
problema 3.3.b)).
Si en el programa fuente tenemos la instrucción “ readln ”, sin argumentos,
entonces se lee el renglón, y si hemos entrado otros datos antes de <retorno>,
éstos son ignorados (ver el inciso c) del problema citado).
Al ingresar nosotros <retorno>, no sólo estamos indicando que terminamos
de ingresar nuestros datos, sino que se emite una señal especial que llamaremos
fin de lı́nea 6 . Cuando ponemos “ readln(a) ” lo que hacemos efectivamente es
“leer” la señal de fin de lı́nea además del dato a.
La función de Pascal “ eoln ” (una abreviatura de “end of line” o fin de lı́nea),
sin argumentos, devuelve un valor lógico, i.e. verdadero o falso. Como “ readln ”,
“ eoln ” se queda a la espera de ingreso de datos, que necesariamente deben
incluir un <retorno>. Si hay otros datos además de <retorno>, “ eoln ”
retorna falso y si no, verdadero. Pero a diferencia de “ readln ”, no “lee” el fin
de lı́nea, y debemos realizar esta “lectura” con “ readln ” sin argumentos.
Nota: Sin embargo, después de evaluarse “ eoln ”, un nuevo “ readln ” con
< >
argumentos hará que se ignoren los retorno ’s que hayamos introducido
aunque no se hayan “leı́do”, como ya hemos verificado en el problema 3.3.
Problema 4.18. El programa eolnprueba (pág. 166) es una prueba del funcio-
namiento de “ eoln ”, en el que se usa la variable a para entender el flujo de las
instrucciones.
a) Compilar y ejecutar el programa, observando que luego de imprimir el valor
de a antes del lazo, queda a la espera del ingreso de datos:
i) Ingresar “ <retorno> ”, sin otros caracteres, y observar el valor final
de a. ¿Cuáles de las instrucciones en “ if ” se ejecutaron?, ¿qué valor
ha dado “ eoln ” y por qué?
ii) Si en vez de ingresar sólo “ <retorno> ”, se ingresa “ <retorno> ”
(con un espacio antes), ¿cuál será el valor de a que se imprime al
final? Verificar la respuesta ejecutando el programa con esa entrada.
5 Al menos para compiladores “razonables”.
6 La señal de “fin de lı́nea” depende del sistema operativo.
4.5. “Eoln” Pág. 33
Aplicaciones
Problema 5.1.
a) Encontrar x de tipo real tal que, para la máquina, ¡x = x + 1!.
Sugerencia: buscar una potencia de 2, usando algo como
x := 1; repeat x := 2 * x; y := x + 1 until (x = y)
ax2 + bx + c = 0 (5.1)
x1 = −0.00010000000100000 . . . y x2 = −9999.99989999999899999 . . .
c) Uno de los problemas en el ejemplo del inciso anterior surge de restar núme-
ros grandes que son comparables. El siguiente fragmento de programa alivia
un poco la situación:
if (b > 0) then x1 := -(b + sqrt(d))/(2 * a)
else x1 := (sqrt(d) - b)/(2 * a);
x2 := c / (x * a);
5.1. Cálculo numérico elemental Pág. 37
Nota: Por supuesto, hay funciones continuas que “no se pueden dibujar” como
1/x para x > 0, pues cerca de x = 0 se nos acaba el papel,
sen 1/x para x > 0, pues cerca de x = 0 oscila demasiado,
(
x sen 1/x si x 6= 0,
f (x) = pues cerca de x = 0 oscila demasiado.
0 si x = 0,
Otra forma de describir que una función es continua es decir que a cambios
“pequeños” en x corresponden cambios “pequeños” en f (x), aunque la “pe-
√
queñez” relativa sea muy distinta (por ejemplo, f (x) = 3 x es continua, pero
si cambiamos de 0 a 0.001 en x, hay sólo un cambio de 0.1 de f (0) a f (0.001)).
Tener en cuenta también que puede ser que a pequeños cambios de f (x)
pueden no corresponder pequeños cambios en x. Por ejemplo una función cons-
tante es continua.
Muchas funciones de interés interactúan de manera interesante con sus pun-
tos fijos como en el caso de la raı́z cuadrada: supongamos que x0 es un punto
dado y definimos
f (`) = `,
y
y=x
1
0.5 y = cos x
x
0.5 1 1.5
0.5
x
0.5 1 1.5
Problema 5.6 (Punto fijo de f (x) = cos x). Con las notaciones anteriores
para f y xi :
a) Hacer un programa que dados x0 y n calcule xn , y observar el comporta-
miento para distintos valores de x0 y n. Sugerencia: usar un lazo “ for ”
como en el problema 5.4.
b) Modificar el programa para que también imprima cos xn y comprobar que
para n más o menos grande se tiene xn ≈ cos xn .
c) Modificar el programa para hacer 200 iteraciones, mostrando los resultados
intermedios cada 10. Observar que después de cierta cantidad de iteraciones,
los valores de xk no varı́an.
Nota: El valor de x200 es una buena aproximación al único punto fijo de
f (x) = cos x, aún cuando puede ser que x200 6= cos x200 (= x201 ) debido a
errores numéricos. El “verdadero” punto fijo es 0.7390851332 . . ..
d ) Modificar el programa de modo que no se hagan más iteraciones si
|xk+1 − xk | < ε,
El método era conocido por los babilonios (entre 2000 y 200 años a. de C.),
quienes usaron ideas geométricas para aproximar la media geométrica de los
números x y a/x, r
a
x× ,
x
√
que es el número a buscado, por su media aritmética (o promedio),
1 a
x+ .
2 x
Mucho más tarde, Isaac Newton (1642–1727) desarrolló un método basado
en el análisis matemático para encontrar raı́ces de funciones mucho más gene-
rales, que tiene como caso particular al método babilónico. El método de Newton
también es conocido como de Newton-Raphson, pues J. Raphson (1648–1715)
publicó este resultado unos 50 años antes que se publicara el de Newton.
a) Probar que si a > 0 y x > 0, entonces
√ fa (x) > 0, y si fa (x) = x (x es un
punto fijo de fa ) entonces x = a.
b) En particular, si x0 > 0 y definimos√xn como en la ecuación (5.3), entonces
xn > 0 para √todo n ∈ N, y si xn = a para algún n, entonces xn = xn−1 =
· · · = x0 = a. Por lo tanto, si suponemos precisión
√ “infinita” el método
nunca da la respuesta exacta salvo que x0 = a.
c) Bosquejar un gráfico similar al segundo del problema 5.6, cuando a = 2 y
x0 = 1.
d ) ¿Qué pasa si a = 0? ¿Y si x0 = 0?
e) ¿Qué pasa si x0 < 0 y a > 0?
f ) ¿Qué pasa si a < 0 y x0 ∈ R? ✄
Problema 5.9 (Método babilónico II). El programa babilonico (pág. 167) es
una versión para aplicar el método babilónico estudiado en el problema anterior.
a) Correr el programa para distintos valores de a, y luego modificarlo para
comparar el resultado con el valor dado por “ sqrt(a) ”.
b) En el programa presentado, el valor inicial x0 , el número máximo de itera-
ciones y la tolerancia están dados como constantes. Modificar el programa
para que estos datos puedan ser entrados interactivamente.
c) Uno de los “criterios de parada” usados en el programa es que la diferencia
de dos valores consecutivos “en x” sea suficientemente pequeña. Agregar
también un criterio para parar si la diferencia “en y”, |x2 − a|, es suficien-
temente pequeña, con tolerancia eventualmente distinta a la anterior.
d ) Modificar el programa para que imprima el criterio que se ha usado para
terminar. Ver cuál es este criterio cuando x0 = 1 y ambas tolerancias son
10−5 , para a = .0001, .01, 100, 10000.
e) En el programa se √consideran errores absolutos √en vez de relativos. Por
ejemplo, aproximar 2 es equivalente a aproximar 200, sólo hay que correr
en un lugar la coma decimal en la solución, pero en el programa hemos
tomado la misma tolerancia para las diferencias.
i) Verificar el comportamiento del programa para los valores a = 2 y
a = 200, tomando el mismo valor inicial y tolerancias.
ii) Modificar el programa de modo que internamente se aproximen las
raı́ces cuadradas sólo de números entre 1 y 100, pero que después el
Pág. 42 Aplicaciones
Problema 5.10.
a) Hacer un programa para calcular la suma de los primeros n números impares
positivos (n ∈ N),
X
n
tn = 1 + 3 + · · · + (2n − 1) = (2k − 1).
k=1
Y en la proposición 2,
e) ¿Qué pasa si se eliminan los renglones donde se consideran los casos a < 0
y b < 0?
f ) ¿Y si se hacen los cambios d ) y e) simultáneamente?
g) Modificar el programa de modo que a la salida escriba también los valores
originales de a y b, e.g. “ El máximo común divisor entre 12 y 8 es 4 ”
si las entradas son a = 12 y b = 8.
h) Una de las primeras aplicaciones del mcd es “simplificar” números raciona-
les, e.g. escribir 12 3
8 como 2 . Hacer un programa que dados los enteros p y q,
con q 6= 0, encuentre m ∈ Z y n ∈ N de modo que pq = m n y mcd(m, n) = 1.
Nota: ¡Atención con los signos de p y q! ✄
Problema 5.14. Pablito y su papá caminan juntos tomados de la mano. Pa-
blito camina 2 metros en exactamente 5 pasos, mientras que su padre lo hace en
exactamente 3 pasos. Si empiezan a caminar juntos, ¿cuántos metros recorrerán
hasta marcar nuevamente el paso juntos?, ¿y si el padre caminara 2 21 metros
en 3 pasos? Aclaración: Se pregunta si habiendo en algún momento apoyado si-
multáneamente los pies izquierdos, cuántos metros después volverán a apoyarlos
simultáneamente.
Nota: Recordando el tema de la conmensurabilidad mencionado al introducir
el algoritmo de Euclides, no siempre el problema
√ tiene solución. Por ejemplo,
si Pablito hace 1 metro cada 2 pasos y el papá 2 metros cada 2 pasos. ✄
Problema 5.15. El mı́nimo común múltiplo de a, b ∈ N, mcm(a, b), se define
en forma análoga al máximo común divisor: es el menor entero del conjunto
{k ∈ N : a | k y b | k}.
a) Demostrar que si a, b ∈ N entonces mcm(a, b) × mcd(a, b) = a × b.
b) Escribir un programa para calcular mcm(a, b) para a, b ∈ N.
c) ¿Cómo podrı́a extenderse la definición de mcm para a, b ∈ Z? ¿Cuál serı́a
el valor de mcm(0, z)? ✄
Problema 5.16.
a) ¿Qué relación hay entre el máximo común divisor o el mı́nimo común múlti-
plo con el problema 5.14 (de Pablito y su papá)? Sugerencia: recordando
que el problema no siempre tiene solución, volver a mirar la descripción
dada por Euclides de su algoritmo pensando en números cualesquiera, res-
tando el mayor del menor hasta llegar a una “medida común”, y quizás el
inciso h) del problema 4.9 (pág. 27).
b) Hacer un programa para resolver ese problema, donde las entradas son el
número de pasos y la cantidad de metros recorridos tanto para Pablito como
para su papá.
Nota: Como para la computadora todos los números son racionales, el
problema siempre tiene solución. ✄
a = p1 × p2 × · · · × pk
debe tener algún factor primo que lo divida, digamos pi (para algún i entre 1
y k) y tendremos
pi | a y pi | ap1 × p2 × · · · × pk ,
Problema 5.17.
a) Probar que si el entero a > 1 no es√primo, entonces existen enteros b y c,
√ que 1, tales que b ≤ a y a = b × c. Sugerencia: si a = b × c
ambos mayores
y 1 < b, c < a, entonces b × c < . . .
b) Usando el resultado anterior, hacer un programa que determine si el en-
tero a > 1√entrado por terminal es primo o no, viendo si los números
2, 3, 4, . . . , b ac lo dividen o no. ✄
Problema 5.18. El programa factoresprimos (pág. 169) es una elaboración del
programa en el problema 5.17, e imprime todos los factores primos del número
a > 1 entrado por terminal.
a) Ver que efectivamente el programa determina todos los factores primos cuan-
do a > 1.
b) Sin embargo, cuando a = 1 o 0 el programa retorna a, que no es primo.
c) ¿Qué pasa cuando a < 0?
d ) Como el programa busca factores primos y el único primo par es 2, modificar
el programa de modo de primero eliminar los factores 2, y luego a partir de
b = 3, incrementar b de a dos en vez de a uno.
e) Modificar el programa de modo que no imprima repetidas veces el mismo
factor primo, sino que imprima el primo seguido de su multiplicidad. Por
ejemplo, si a = 456 = 23 × 3 × 19, se imprima
Factor Multiplicidad
2 3
3 1
19 1
✄
Arreglos
A veces queremos tener muchas variables del mismo tipo, por ejemplo una
lista de los 100 primeros números primos. Escribir un nombre para cada una de
las variables ya serı́a engorroso, pero ¿qué pasa si ahora el problema requiere una
lista de 1000 en vez de 100 datos? Para estos casos es conveniente usar una es-
tructura similar a la de vector, que podrı́amos pensar como v = (v1 , v2 , . . . , vn ),
para guardar los datos. En programación, esta estructura se llama arreglo (uni-
dimensional).
• Al comenzar el programa los valores del arreglo no están definidos, y hay que
inicializarlos.
• El ingreso de datos está tomado del programa sumardatos (pág. 167), excepto
que al entrar un dato se determinar el dı́gito de las unidades del dato, d, y se
incrementa el contador correspondiente.
¿Por qué no se pone directamente “ digito := dato mod 10 ” en vez de
“ digito := abs(dato) mod 10 ” para determinar el dı́gito? ✄
6.3. Cribas
Cuando debemos seleccionar en un arreglo todos los elementos que satisfacen
cierto criterio, y el criterio para cada elemento depende de los elementos que le
precedieron, es conveniente usar una criba (o cedazo o tamiz). Quizás la más
conocida de las cribas sea la atribuı́da a Eratóstenes para encontrar los números
primos entre 1 y n (n ∈ N), donde el criterio para decidir si un número k es
primo o no es la divisibilidad por los números que le precedieron.
Eratóstenes (contemporáneo de Arquı́medes y posterior a Euclides) nació en
Cirene (en Libia de hoy) alrededor del 276 a. de C.. Vivió gran parte de su
vida en Alejandrı́a (Egipto), donde murió alrededor de 194 a. de C., después de
haber sido bibliotecario de la famosa biblioteca. Hizo importantes contribuciones
en matemáticas, astronomı́a, geografı́a y filosofı́a. Por ejemplo, fue el primero
en medir correctamente la circunferencia de la Tierra (y pensar que Colón
usaba un huevo, ¡1700 años después!).
2 3 4 . . . n.
2 3 64 5 66 . . .
Ahora miramos al primer número que no está marcado (no tiene recuadro
ni está tachado): 3. Lo recuadramos, y tachamos de la lista todos los múltiplos
de 3 que quedan sin marcar. La lista ahora es
2 3 64 5 66 7 68 6 9 16 0 11 16 2 . . .
comenzando con todas las celdas cerradas. Un prisionero era liberado si al final
de este proceso su puerta estaba abierta. ¿Qué prisioneros fueron liberados?
Sugerencia: ¡no pensar, hacer el programa! ✄
Problema 6.5 (El problema de Flavio Josefo I). Durante la rebelión judı́a
contra Roma (unos 70 años d. de C.), 40 judı́os quedaron atrapados en una
cueva. Prefiriendo la muerte antes que la captura, decidieron formar un cı́rculo,
matando cada 3 de los que quedaran, hasta que quedara uno solo, quien se
suicidarı́a. Conocemos esta historia por Flavio Josefo (historiador famoso), quien
siendo el último de los sobrevivientes del cı́rculo, no se suicidó. El problema es
ver en qué posición debió colocarse Flavio Josefo dentro del cı́rculo para quedar
como último sobreviviente.
En general, supondremos un cı́rculo de n personas, numeradas de 1 a n,
y vamos eliminando la m-ésima comenzando desde la primera, recorriendo los
cı́rculos—cada vez más reducidos—siempre en el mismo sentido. Por ejemplo si
n = 5 y m = 3, se eliminarán sucesivamente las numeradas 3, 1, 5, 2, sobrevi-
viendo 4 (¡hacer un dibujo!).
El programa flaviojosefo1 (pág. 172) implementa una criba para resolver el
problema. El usuario entra n y m, y el programa escribe la permutación de
Flavio Josefo, es decir la secuencia en que van siendo eliminados incluyendo el
sobreviviente al final (3, 1, 5, 2, 4 en el ejemplo).
a) ¿Qué posición ocupaba en el cı́rculo inicial Flavio Josefo (en la versión
original del problema)?
b) ¿Cuál es el sobreviviente si n = 1234 y m = 3? Sugerencia: no imprimir
todos los valores, y tener cuidado con el dimensionamiento.
c) Modificar el programa de modo que responda cuántas vueltas sobrevivió la
persona que estaba inicialmente en el lugar s (el usuario entrará n, m y s).
d ) Modificar el programa de modo de imprimir una tabla, dando la posición
inicial y el orden de ejecución, algo como
Posición en el cı́rculo Orden de ejecución
1 2
2 4
6.4. Polinomios Pág. 55
3 1
4 5
5 3
cuando n = 5 y m = 3.
Sugerencia: eliminar el arreglo flavio y usar otro, digamos orden, de mo-
do que “ orden[i] ” indicará en qué vuelta fue eliminado i. Si se inicia-
liza “ orden[i] := n ” para todo i, y consideramos que en cada vuelta
“ orden[i] = n ” indica que vive, tampoco es necesario el arreglo vive ni el
lazo para encontrar el sobreviviente: será el único con “ orden[i] = n ” al
terminar.
e) ¿Cómo podrı́a modificarse el programa, de modo que siempre se trabaje
con un arreglo (o sub-arreglo) de longitud (por ejemplo) vivos, de modo de
no tener que considerar los que ya no viven? Sugerencia: ir “corriendo” las
posiciones en el arreglo a medida que “se cierra” el cı́rculo.
Nota: En el capı́tulo 14 veremos que las listas encadenadas se adaptan
mejor que los arreglos para esta variante.
f ) El lazo “ repeat...until (cuenta = m) ” en el programa original es muy
primitivo. Por ejemplo, si n = 1000, m = 100, cuando quedan 10 sobrevi-
vientes debemos pasar unas 10 veces por cada uno de los 1000 elementos.
¿Podrı́a mejorarse el lazo usando “ mod ”? Sugerencia: recordar el programa
resto (pág. 163). ✄
6.4. Polinomios
¿Para qué estudiar polinomios? ¿No basta con estudiar las funciones lineales
(y = ax + b) y a lo sumo las cuadráticas (y = ax2 + bx + c)?
¡Humm! Por un lado, los polinomios son las funciones más sencillas que po-
demos considerar, para su cálculo sólo se necesitan sumas y productos. Además
los usamos diariamente, por ejemplo un número en base 10 es en realidad un
polinomio evaluado en 10.
Pero también los polinomios sirven para aproximar tanto como se desee a
casi cualquier función, lo que constituye un tema central de las matemáticas,
y se estudia tanto en los cursos teóricos de Análisis Matemático como en los
aplicados de Análisis Numérico. A modo de ejemplo visual, en el problema 8.11
mostramos cómo se podrı́a aproximar a la función seno mediante los denomina-
dos polinomios interpoladores de Lagrange.
Nuestra primer tarea será evaluar un polinomio dado:
X
k
n= ai b i , donde ai ∈ Z y 0 ≤ ai < b, (6.1)
i=0
a) Implementar dos programas para que dados la base b y n, encontrar los coe-
ficientes ai , y recı́procamente, dados la base b y los coeficientes ai , calcular
n usando la regla de Horner.
b) En la ecuación (6.1), ¿cómo se relacionan k y logb n (si ak 6= 0)? ✄
X
k
n= ai 2 i ,
i=0
y haciendo
2
a2 +···+2k−1 ak−1 +2k ak
xn = xa0 +2a1 +2
k−1 k
= xa0 · (x2 )a1 · (x4 )a2 · · · (x2 )ak−1 · (x2 )ak .
6.5. Problemas Adicionales Pág. 57
Funciones y Procedimientos
Nuestros programas se han hecho cada vez más largos, y a medida que avan-
cemos lo serán aún más. La longitud y complejidad de los programas profesio-
nales es tal que una sola persona no puede siquiera escribirlos completamente.
A fin de abordar esta dificultad, se han desarrollado una serie de técnicas, y
en este capı́tulo daremos los primeros pasos hacia una de ellas, la modularización,
para lo cual Pascal cuenta con dos mecanismos: funciones y procedimientos.
Aunque la ventaja de usar funciones o procedimientos irá quedando más
clara con los ejemplos que veremos en éste y otros capı́tulos, podemos decir que
en general son convenientes para:
El lector seguramente recordará el uso que con el mismo espı́ritu hemos dado
a “ const ”, por ejemplo en el problema 6.2, para hacer un único cambio que
afecta a varias partes del programa.
7.1. Funciones
Como hemos visto, Pascal cuenta con una serie de funciones pre-definidas,
como “cos” o “ln”, de una variable, o la suma, “+”, que se aplica a dos o más
variables. Pero —necesariamente— no puede tener todas las funciones y sólo
algunas se han incluido. Por ejemplo, el cálculo de la potencia xn cuando x ∈ R
y n ∈ N no está incluida en Pascal, pero lo hemos hecho en el problema 4.15.
Supongamos que queremos hacer un programa para comparar los valores xn
y y , donde x, y ∈ R y n, m ∈ N son datos ingresados por el usuario. Nuestro
m
Claro que si contáramos con una función de Pascal para calcular xn para
x ∈ R y n ∈ N, digamos “ potencia(x,n) ”, podrı́amos reemplazar los dos
renglones anteriores por
xn := potencia(x,n); ym := potencia(y,m);
Aunque Pascal no cuenta con una función que realice este cálculo, nos da un
mecanismo para definirla nosotros.
En Pascal, las funciones y procedimientos (que veremos en un rato) se de-
claran en la parte declarativa1 del programa y tienen la misma estructura que
los programas. Se comienza poniendo el nombre, cambiando “ program ” por
“ function ” para funciones o “ procedure ” para procedimientos, luego una
parte de declaraciones propias, y después un cuerpo principal encerrado entre
“ begin ” y “ end; ” (con “ ; ” en vez de “ . ”).
Ası́, para definir la función “ potencia ” pondrı́amos
function potencia(a: real; b: integer): real;
var k: integer; z: real;
begin
z := 1;
for k := 1 to b do z := z * a;
potencia := z
end;
Observamos que
a = a0 c0 = a1 = a2 c1 = b2 b = b0 = b1
c2
Figura 7.1: una función continua con distintos signos en los extremos.
g) El programa no verifica si poco < mucho, y podrı́a suceder que poco >
mucho. ¿Tiene esto importancia?
h) Teniendo en cuenta las notas al principio de la sección (pág. 63), ¿tendrı́a
sentido agregar al programa un criterio de modo de parar si los extremos
del intervalo están suficientemente cerca? Si la nueva tolerancia fuera εx ,
¿cuántas iteraciones deben realizarse para alcanzarla, en términos de εx y
los valores originales de mucho y poco?
i) Modificar el programa de modo que en vez de considerar hasta un máximo
de iteraciones, el programa termine cuando o bien se ha encontrado x tal que
|f (x)| < εy o bien se llega a poco y mucho de modo que |poco − mucho| <
εx . ✄
El método de la bisección es bien general y permite encontrar las raı́ces de
muchas funciones. Al programarlo, hemos separado la declaración de la función
f de modo de poder cambiarla fácilmente según la aplicación, sin necesidad de
recorrer todo el programa buscando las apariciones de f (habrá, sı́, que cambiar
también los carteles iniciales).
Problema 7.4. Cambiar la definición de la función en el programa biseccion,
para responder a los siguientes ejemplos (hacer primero un bosquejo del gráfico
para estimar valores iniciales de poco y mucho):
a) Resolver aproximadamente las ecuaciones:
x2 − 5x + 2 = 0 y x3 − x2 − 2x + 2 = 0.
2 − ln x = x y x3 sen x + 1 = 0.
Nota: La primera ecuación tiene una raı́z, y la segunda tiene infinitas. ✄
Problema 7.5 (Interés sobre saldo). Consideremos el siguiente problema:
7.3. Procedimientos
Los procedimientos y funciones son muy parecidos entre sı́, y a veces se
los engloba bajo el nombre común de rutinas 5 . De hecho, en lenguajes como
“C” no hay distinción entre ellos. En Pascal, la diferencia más obvia es que los
procedimientos no tienen “un resultado” visible.
Pascal nos permite mezclar funciones y procedimientos, con la única restric-
ción de que se deben declarar en el orden en que son usados: una función o
procedimiento no puede llamar a una función o procedimiento que no ha sido
aún declarada.
Nota: Una posibilidad intermedia el uso de “ forward ”, que no veremos.
Más aún, como veremos en el próximo problema, es posible poner una función
o procedimiento dentro de otra función o procedimiento.
Volvamos al problema 4.10 (pág. 27) donde hicimos una tabla del seno usan-
do el programa tablaseno (pág. 163). Podemos esquematizar ese programa por
medio de los siguientes pasos:
1. Poner carteles.
2. Leer los datos, en este caso inicial , final e incremento.
3. Hacer e imprimir la tabla.
4. Señalar el fin del programa.
Pascal nos permite poner cada uno de estos pasos como procedimiento, po-
niendo en el cuerpo principal del programa6:
begin
cartelesiniciales;
leerdatos;
imprimirtabla;
cartelfinal
end.
donde cartelesiniciales , leerdatos, imprimirtabla y cartelfinal son procedimientos
que realizarán las acciones correspondientes7 .
La ventaja de hacerlo es que podemos preocuparnos por cada uno de los
procedimientos —y si las hubiera, funciones— por separado. En nuestro ejem-
plo de la tabla del seno, usamos sólo procedimientos puesto que no tenemos
asignaciones explı́citas.
Ası́, podrı́amos definir el procedimiento cartelesiniciales como
procedure cartelesiniciales;
begin
writeln(’Hacer una tabla del seno dando valores’);
writeln(’inicial, final, y del incremento (en grados).’);
writeln
end;
procedure calculodepi;
begin pi := 4 * arctan(1) end;
begin
calculodepi;
writeln(’Angulo Seno’);
grados := inicial;
while (grados <= final) do begin
radianes := grados * pi/180;
writeln(grados:5, sin(radianes):15:5);
grados := grados + incremento
end
end;
mientras que el cuerpo principal y las variables globales son como el en la
versión original de tablaseno2. ✄
son incorrectas.
• Si hay varios parámetros, algunos pueden pasarse por valor y otros por
referencia (con “ var ”). Si hay una lista de parámetros separados por “ , ”,
como en “ proc (var a, b: integer; c, d: integer) ”, entonces los
7.4. Pasando por valor o por referencia Pág. 71
begin a:= 5; b := 8; c := 3;
P(a, b, c); P(7, a + b + c, a); P( a * b, a div b, c)
end.
b) var i, j, k: integer;
procedure R;
begin i := i + 1 end;
begin i := j;
if (h = 0) then P(j) else if h = 1 then P(i) else R;
writeln( i, j, k)
end;
un lazo.
Pág. 74 Todos juntos: arreglos, funciones y procedimientos
c) max supone que long > 0. Dar un ejemplo donde es 0 y modificar el pro-
grama para considerar este caso.
d ) Modificarlo de modo de encontrar también el mı́nimo, usando un nuevo
procedimiento.
e) Modificarlo de modo que se intercambien las posiciones del máximo y el
último elemento, imprimiendo el nuevo renglón. ✄
Problema 8.3. Desarrollar un programa que, dado un arreglo de enteros en-
trado como en el problema 8.1, y sin usar otro arreglo adicional, lo invierta, i.e.
si el arreglo inicialmente es (a1 , a2 , . . . , an ), el arreglo final es (an , . . . , a2 , a1 ).
Sugerencia: usar “ swap ” para intercambiar a1 ↔ an , a2 ↔ an−1 ,. . . ✄
Problema 8.4. Desarrollar un programa que leyendo un renglón (como en el
programa maximo), lo escriba al revés, e.g. si la entrada es
“ dabale arroz a la zorra el abad ”
el programa escriba
“ daba le arroz al a zorra elabad ”
Sugerencia: ver el problema anterior. ✄
type
tiporenglon = array[1..MAXC] of char;
tipotexto = array[1..MAXR] of tiporenglon;
caracteresenrenglon = array[1..MAXR] of integer;
var
nr, (* numero de renglones *)
nc (* numero de caracteres en renglon *)
: integer;
texto: tipotexto;
cenr: caracteresenrenglon;
eventualmente recordando la Nota en la pág. 75.
a) Desarrollar un procedimiento o función para leer no más de MAXR renglo-
nes, con no más de MAXC caracteres por renglón, dando como señal de fin
de entrada un “renglón vacı́o”.
Sugerencia: copiar las ideas del programa maximo.
Sugerencia si la anterior no alcanza:
nr := 0; (* numero de renglon leido, al ppo. ninguno *)
while (not eoln) do begin
(* mientras el renglon a leer no sea vacio... *)
nr := nr + 1; (* hay un renglon mas *)
nc := 0; (* que todavia no tiene caracteres leidos *)
Pág. 78 Todos juntos: arreglos, funciones y procedimientos
8.5. “Strings”
Problema 8.7. El tipo “ string ” no es estándar en Pascal, pero existe en casi
todos los compiladores, en particular en Turbo Pascal. Probar el comportamien-
to del compilador con un programa que lee un renglón entrado por terminal, vı́a
“ readln(s) ”, donde se declara s como “ string[100] ”, indicando su longitud,
y/o simplemente como “ string ”.
En caso de aceptar alguna de estas variantes, agregar al programa las ins-
trucciones:
a) “ writeln(s) ”, para escribir s,
b) “ length(s) ”, para averiguar su longitud, y
c) “ for i := 1 to length(s) do writeln( i:3, ’: ’, s[i]) ”, para es-
cribirlo carácter por carácter, indicando el ı́ndice.
d ) Modificar el programa del problema 8.6, cambiando el tipo “ renglon ” por
el tipo “ string ”.
Nota: Según el estándar Pascal, el tipo “string” es una denominación genéri-
ca para el tipo char y para arreglos empaquetados de caracteres, como los
mencionados en la nota del problema 8.2. ✄
si mcd(a, b) = 1 (lo que implica el resultado de Euclides sobre que hay infinitos
primos). En el inciso b), si pensamos que b = 10, y a es cualquier número que
no tenga como factores a 2 ni a 5, el teorema de Dirichlet dice que hay infinitos
primos que terminan en 1, infinitos que terminan en 3, etc. ✄
X
n+1 Y x − xj
P (x) = yi . (8.1)
i=1
xi − xj
j6=i
0.5
π π π
6 4 2
π
0 1
1 0
1 0
1 0 y
1 1
P P
P P
En el último ejemplo, es conveniente poner 01 en vez de sólo 1 en la primer
fila a fin de tener todas las filas con igual longitud.
3 Tomado de G. H. Hardy y E. M. Wright, An Introduction to the Theory of Numbers, 4.a
| = 0 0 1
| | | = 0 1 1
| | | | = 1 0 0
I I P
b) En este inciso, veremos que una posición en Nim es ganadora si y sólo si es
correcta.
i) Si ninguna fila tiene más de un fósforo, la posición es ganadora ⇔ hay
en total un número par de fósforos ⇔ la posición es correcta.
ii) Si un número a ∈ N, expresado en binario por (an , an−1 , . . . , a1 , a0 )
(permitiendo an = 0) es reemplado por otro menor b (entero ≥ 0),
expresado en binario como (bn , bn−1 , . . . , b1 , b0 ), entonces para algún
i, 0 ≤ i ≤ n, las paridades de ai y bi son distintas.
iii) Por lo tanto, si el jugador X recibe una posición correcta, necesaria-
mente la transforma en incorrecta.
iv ) Supongamos que estamos en una posición incorrecta, es decir, al menos
la suma de una columna es impar. Para fijar ideas supongamos que las
paridades de las columnas son
P P I P I P
Entonces hay al menos un 1 en la tercera columna (la primera con
suma impar). Supongamos, otra vez para fijar ideas, que una fila en la
cual esto pasa es (en binario)
- -
0 1 1 1 0 1
donde marcamos con “ - ” que los números debajo están en columnas
de suma impar. Cambiando 0’s y 1’s por 1’s y 0’s en las posiciones
marcadas, obtenemos el número (menor que el original, expresado en
binario):
- -
0 1 0 1 1 1
Está claro que este cambio corresponde a un movimiento permitido,
haciendo la suma de cada columna par, y que el argumento es general.
v ) Por lo tanto, si el jugador X recibe una posición incorrecta, puede
mover de modo de dejar una posición correcta.
vi) Si A deja una posición correcta, B necesariamente la convierte en
incorrecta y A puede jugar dejando una posición correcta. Este proceso
continuará hasta que cada fila quede vacı́a o contenga un único fósforo
(el caso i)).
c) En base a los incisos anteriores, y suponiendo que A y B siempre juegan de
modo óptimo, ¿quién ganará?
d ) Ver que la “jugada ganadora” del inciso b.iv ) no siempre es única.
e) Desarrollar un programa que, ingresada una posición inicial en la forma de
Pág. 84 Todos juntos: arreglos, funciones y procedimientos
Números Aleatorios y
Simulación
Muchas veces se piensa que en matemáticas las respuestas son “siempre exac-
tas”, olvidando que las probabilidades forman parte de ella y que son muchas
las aplicaciones de esta teorı́a.
Una de estas aplicaciones es la simulación, técnica muy usada por fı́sicos,
ingenieros y economistas cuando es difı́cil llegar a una fórmula que describa el
sistema o proceso. Ası́, simulación es usada para cosas tan diversas como el
estudio de las colisiones de partı́culas en fı́sica nuclear y el estudio de cuántos
cajeros poner en el supermercado para que el tiempo de espera de los clientes
en las colas no sea excesivo.
La simulación mediante el uso de la computadora es tan difundida, que hay
lenguajes de programación (en vez del Pascal o “C”) especialmente destinados
a este propósito.
9.2. Aplicaciones
Problema 9.1. El programa dado (pág. 181) hace una simulación de tirar un
dado mediante números aleatorios obtenidos con la sentencia “ random ”.
a) La sentencia “ randomize ” sirve para comenzar una nueva serie de números
aleatorios. Eliminarla, comentándola, ejecutar repetidas veces el programa
y comprobar que siempre se obtienen los mismos resultados (o sea no es
muy al azar).
Nota: Salvo para programas que tienen un tiempo de ejecución grande, no
debe hacerse más de una llamada a “ randomize ”.
b) Modificar el programa para simular tirar una moneda con resultados “cara”
o ”ceca”. ✄
Problema 9.2. El programa dados (pág. 182) hace una simulación para en-
contrar la cantidad de veces que se necesita tirar un dado hasta que aparezca un
número prefijado, entrado por el usuario. Gracias a la sentencia “ randomize ”, el
resultado en general será distinto con cada ejecución. Observar que si el usuario
entra un número menor que 1 o mayor que 6, el programa no termina nunca.
a) Ejecutar el programa repetidas veces, para tener una idea de cuánto tarda
en aparecer un número.
b) En vez de correr varias veces el programa, modificarlo de modo de realizar
n simulaciones (n entrado por el usuario), mostrando como resultado el
promedio de los tiros en que tardó en aparecer el número predeterminado.
Sugerencia: encerrar el lazo “ repeat ” dentro de un lazo “ for ”.
c) Modificar el programa original a fin de simular que se tiran simultáneamente
dos dados, y contar el número de tiros necesarios hasta obtener un resultado
ingresado por el usuario (entre 2 y 12). ✄
prefijado, o que nunca salga k veces consecutivas. Sin embargo, se puede demos-
trar matemáticamente que la probabilidad de que esto suceda es 0 (suponiendo
que el generador de números aleatorios sea correcto). ✄
Problema 9.4. El compilador de Pascal de Ana tiene la sentencia “ aleat ”,
que obtiene números aletorios r ∈ R con 0 ≤ r < 1, i.e. como la sentencia
“ random ” de Turbo Pascal sin argumentos.
a) ¿Qué instrucción debe poner Ana en vez de “ random(n) ” (que no es re-
conocida por su compilador) para hacer los problemas anteriores sobre los
dados? Sugerencia: multiplicar y usar “ trunc ”.
b) El profesor le ha dado un problema donde tiene que obtener 1 o −1 con
igual probabilidad. ¿Qué sentencias deberı́a usar?
c) ¿Y si necesitara números reales entre a y b (que pueden ser a pero no b),
donde a < b? ✄
Existen muchos métodos, que reciben el nombre de métodos de Monte-Carlo,
para aproximar cantidades determinı́sticas, i.e. no aleatorias, mediante proba-
bilidades. El próximo problema es un ejemplo de la técnica.
Problema 9.5. Hacer un programa para calcular π tomando n pares de núme-
ros aleatorios (a, b), con a y b entre −1 y 1, contar cuántos de ellos están dentro
del cı́rculo unidad, i.e. a2 + b2 < 1. El cociente entre este número y n (ingresado
por el usuario), es aproximadamente la proporción entre las áreas del cı́rculo de
radio 1 y el cuadrado de lado 2. ✄
Problema 9.6.
a) Desarrollar un programa para hacer una lista de r números enteros, elegidos
“aleatoria y uniformemente” entre 0 y s − 1, donde r, s ∈ N son entrados
por el usuario.
b) Modificar el programa de modo que, recorriendo linealmente la lista genera-
da en el inciso anterior, al terminar imprima la cantidad de veces que cada
elemento se repite cuando r s. Sugerencia: agregar un segundo arreglo
para contar las apariciones.
Nota: La cantidad de apariciones deberı́an ser muy similares, aproxima-
damente r/s cada uno. ✄
Problema 9.7 (Dos con el mismo cumpleaños). Mucha gente suele sor-
prenderse cuando en un grupo de personas hay dos con el mismo dı́a de cum-
pleaños: la probabilidad de que esto suceda es bastante más alta de lo que se
cree normalmente.
Supongamos que en una sala hay n (n ∈ N) personas y supongamos, para
simplificar, que no hay años bisiestos (no existe el 29 de febrero), de modo que
podemos numerar los posibles dı́as de cumpleaños 1, 2, . . . , 365.
a) ¿Para qué valores de n se garantiza que haya al menos dos personas que
cumplen años el mismo dı́a? Sugerencia: recordar el principio del casillero,
también conocido como del palomar o de Dirichlet.
Nota: El principio de Dirichlet dice que si hay n + 1 objetos repartidos en
n casillas, hay al menos una casilla con 2 o más objetos.
b) Si la sala es un cine al cual van entrando de a una las personas, ¿cuántas
personas, en promedio, entrarán hasta que dos de ellas tengan el mismo
Pág. 88 Números Aleatorios y Simulación
Problema 9.9 (El Show de Televisión). Un problema que causó gran re-
vuelo hacia 1990 en Estados Unidos es el siguiente:
a) Intuitivamente y sin dar una justificación rigurosa, ¿es más conveniente para
el participante mantener su primera elección, cambiarla o es indiferente?
b) Hacer un programa que simule el juego para tener mejor idea: i) el programa
elige una de las puertas donde estará el auto (sin que nosotros sepamos
el resultado); ii) nosotros hagamos una elección; iii) que en base a esta
elección, el “locutor” elija una segunda puerta detrás de la cual no está el
auto, al azar si hay dos posibilidades; iv ) que nosotros mantengamos o
cambiemos la elección; y v ) que diga si ganamos o no el auto.
c) Modificar el programa anterior de modo de hacer automáticamente n jue-
gos en los que siempre elegimos una puerta al azar y luego cambiamos,
retornando el número de veces que ganamos. Correrlo para n = 1000 (¡pri-
mero hacerlo para n = 5 para ver si está funcionando bien!). Comparar el
resultado con la respuesta en a). ✄
programa, que el promedio de veces que la aguja cruzará una de las lı́neas es
aproximadamente 2l/π.
Georges Louis Leclerc, Conde de Buffon (1707–1788) planteó este problema
en 1733, dando vigor a la teorı́a de probabilidad geométrica. El problema es
más que nada de interés teórico e introductorio al tema, pues las aproximacio-
nes experimentales de π no son buenas.
var
x1, x2, x3: integer;
temp: real;
begin
(* ajustar semillas *)
randsemilla1 := randsemilla1 mod p1;
if (randsemilla1 < 1) then randsemilla1 := 1;
(* primer generador *)
x1 := m1*(randsemilla1 mod 177) - 2*(randsemilla1 div 177);
if x1 < 0 then x1 := x1 + p1;
(* segundo generador *)
x2 := m2*(randsemilla2 mod 176) - 35*(randsemilla2 div 176);
if x2 < 0 then x2 := x2 + p2;
(* tercer generador *)
Pág. 92 Números Aleatorios y Simulación
random := trunc((temp-trunc(temp)) * n)
procedure cambiarsemilla;
begin
write(’** El valor actual de la semilla es ’);
writeln(randsemilla1);
write(’ Entrar el nuevo valor (entero): ’);
readln(randsemilla1)
end;
Búsqueda y clasificación
Siempre estamos buscando algo y es mucho más fácil encontrarlo si los datos
están clasificados u ordenados. No es sorpresa que búsqueda y clasificación sean
temas centrales en informática y que haya una enorme cantidad de material
escrito al respecto. Por ejemplo, en sus clásicos libros [4] Knuth dedica al tema
todo el Volumen 3 de (que por supuesto, usa material de los volúmenes anterio-
res). Acá hacemos una introducción al tema siguiendo, en mı́nima proporción,
la presentación de Wirth en [6].
d ) i := n;
while ((a[i] <> x) and (i > 1)) do i := i - 1;
if (x = a[i]) then ... (* se encontro en la posicion i *) ✄
obtenemos
similar al hecho con el esquema presentado. Suponemos que los datos son el
arreglo a = (a1 , a2 , . . . , an ) ordenado no-decrecientemente y x, y que al terminar
se hace la comparación entre ak y x.
a) i := 1; j := n;
repeat
k := (i + j) div 2;
if (a[k] < x) then i := k else j := k
until ((a[k] = x) or (i >= j))
b) i := 1; j := n;
repeat
k := (i + j) div 2;
if (x <= a[k]) then j := k - 1;
if (a[k] <= x) then i := k + 1
until (i > j)
c) i := 1; j := n;
repeat
k := (i + j) div 2;
if (x < a[k]) then j := k else i := k + 1
until (i >= j)
d ) Muchos autores presentan un algoritmo un poco más ineficiente que el del
problema 10.5.c), comparando x con amedio en cada paso de modo que hay
dos comparaciones por “vuelta” de lazo:
poco := 1; mucho := n; encontrado := false;
while ((poco <= mucho) and (not encontrado)) do begin
medio := (poco + mucho) div 2;
if (x = a[medio]) then begin
i := medio; encontrado := true end
else if (x > a[medio]) then poco := medio + 1
else mucho := medio - 1
end;
Ver que el esquema es correcto. ✄
for j := i+1 to n do
if (x > a[j]) then begin k := j; x := a[k] end;
a[k] := a[i]; a[i] := x
end
Nota: Comparar con el inciso e) del problema 8.2.
Intercambio directo o burbujeo: Aquı́ también levantamos las cartas al
mismo tiempo y las abrimos en abanico, pero vamos mirando de iz-
quierda a derecha (o de derecha a izquierda) buscando un par de
cartas consecutivas fuera de orden y cuando lo encontramos lo inver-
timos. Seguimos recorriendo con la mirada el abanico hasta que no
haya ningún par fuera de orden. Un posible esquema es:
for i := 2 to n do
for j := n downto i do
if (a[j-1] > a[j]) then begin (* swap *)
x := a[j-1]; a[j-1] := a[j]; a[j] := x
end
Nota: Observar la similitud entre la clasificación por selección directa o inser-
ción directa con la obtención de “manos” aleatorias, como en el problema 9.12,
incisos b) y a) respectivamente. Esto permite estudiar estos métodos teórica-
mente.
Arreglo ya ordenado
mergesort selección intercambio inserción
comparaciones 71 712 49 995 000 49 995 000 9 999
asignaciones 140 000 29 997 0 29 997
tiempo (segs.) 0.00 1.10 1.43 0.00
Arreglo aleatorio
mergesort selección intercambio inserción
comparaciones 123 582 49 995 000 49 995 000 25 023 757
asignaciones 140 000 103 894 75 041 274 25 043 755
tiempo (segs.) 0.00 1.10 1.75 0.72
Nota: Observar que la declaración del registro termina con “ end; ”, pero no
hay un “ begin ”.
Para acceder a una componente del registro, el nombre del registro es se-
guido por un punto (“ . ”) y el correspondiente identificador del campo. Ası́, si
quisiéramos asignar a z el valor 3 + 5i, pondrı́amos
z.re := 3; z.im := 5;
Si z 0 es otro número complejo, podemos hacer la asignación
zp := z;
mientras que la suma z 00 = z + z 0 puede expresarse como:
zpp.re := z.re + zp.re; zpp.im := z.im + zp.im;
type
tipoinfo = record nombre: string; nroid: integer end;
tipoarreglo = array[0..MAXN] of tipoinfo;
Una variante del procedimiento leerarreglo (capı́tulo 8, pág. 74), para leer
datos es:
procedure leerdatos(var a: tipoarreglo; var n: integer);
begin
writeln(’** Entrada de datos’);
n := 1;
Pág. 104 Búsqueda y clasificación
while (nuevodato) do n := n + 1;
n := n - 1
end;
donde la variable findatos es de tipo “ boolean ”.
Hacer un procedimiento para escribir arreglos de este tipo, y hacer un pro-
grama incorporando ambos procedimientos para probarlos. ✄
Problema 10.15 (Clasificación de arreglos de registros). Supongamos
que hemos hecho las declaraciones del problema 10.14, y queremos clasificar el
arreglo a de tipo “ tipoarreglo ”, que puede ser según nombre o según nroid .
La componente por la cual se clasifica se denomina llave o clave (en inglés
“key”). Por ejemplo si tenemos el registro x con el nombre “ Geri ” y el número
de identidad “ 5 ”, y el registro y con el nombre “ Ana ” y número “ 10 ”, or-
denándolos (de menor a mayor) por nombre vendrá primero y antes que x, pero
si los ordenamos por nroid vendrá primero x.
Nota: La comparación de strings se hace como la comparación entre números
(en Pascal), e.g. “ s1 <= s2 ”, obteniendo un valor booleano (verdadero o falso).
a) Hacer una función “ mayor(x, y, llave) ” que retorna un valor lógico, y
donde x, y son del tipo “ info ” y llave es del tipo “ integer ”, tomando los
valores ‘ 1 ’ o ‘ 2 ’, de modo que el resultado de mayor sea el valor de “ x.z
> y.z ” donde z puede ser nombre o nroid según el valor de llave.
b) Modificar alguno de los métodos de clasificación vistos, de modo de poder
clasificar un arreglo de registros, cambiando la comparación entre datos de
la forma x > y por “ mayor(x, y, llave) ”.
c) Hacer un programa para ingresar el arreglo a y clasificarlo según nombre o
nroid a elección del usuario, escribiendo por pantalla el resultado. ✄
end;
m := mucho - 1;
Sugerencia: realizar un análisis como en el hecho para Búsqueda Binaria,
viendo que i) el algoritmo
√ termina, ii) que al terminar poco = mucho, y iii)
siempre es poco − 1 ≤ b nc < mucho.
b) Hacer un procedimiento y programa implementándolo. Comprobar la co-
rrección comparando el valor obtenido con “ trunc(sqrt(n)) ”.
√
c) ¿Qué pasa si b(n + 1)/2c > maxint en el programa anterior?, ¿por qué?,
¿podrı́a arreglarse el esquema en a)?, ¿podrı́a usarse un escalamiento como
en el problema 5.9 sobre el método babilónico, para evitar que mucho ×
mucho > maxint?
Nota: El método babilónico también hace sólo operaciones aritméticas elemen-
tales y “acorta al menos en dos la distancia” en cada paso. El esquema
m := 1;
repeat m := (m + (n div m)) div 2 until ((m * m) <= n)
es correcto, excepto si m2 > maxint en algún momento. Es decir, valen las
mismas
√ objeciones hechas en c) al esquema en a): es correcto si n + 1 ≤
2 maxint. ✄
Problema 10.18 (Estabilidad para clasificación). Un método de clasifica-
ción se llama estable si el orden relativo de elementos con llaves iguales perma-
nece inalterado en el proceso de clasificación.
a) Decidir si el método de clasificación usado en el programa en el proble-
ma 10.15 es estable o no, e.g. si ordenando primero por nroid y después
por nombre, personas con el mismo nombre aparecen en orden creciente de
nroid .
b) Decidir cuáles de los métodos elementales de clasificación que hemos visto
(inserción directa, selección directa, intercambio directo) son estables. ✄
Problema 10.19 (Mergesort). Los métodos elementales usan del orden de
n2 comparaciones y/o asignaciones para clasificar un arreglo de longitud n. En
contraposición, los métodos más avanzados usan del orden de n × log2 n opera-
ciones. Entre los más “populares” de este tipo podemos mencionar al heapsort
o clasificación por montón, quicksort o clasificación rápida, y mergesort o clasi-
ficación por fusión o mezcla.
No es difı́cil explicar este último método, cuya idea es dividir el arreglo
de a pares, y luego ir fusionando —como en el problema 10.4— los subarreglos:
primero juntar pares consecutivos, luego cuaternas consecutivas, etc. hasta llegar
obtener el arreglo completo. Por ejemplo, el método realiza los siguientes pasos
al clasificar (3, 6, 7, 5, 4, 1, 2):
6. 1234567 y se fusionan.
A fin de implementar este algoritmo, usamos el procedimiento clasificar que
llama, a su vez, al fusionar :
procedure fusionar(
var a, b: arreglo; (* entra a, sale b *)
inic, medio, fin: integer (* partes a fusionar *)
);
var
i, j, k: integer;
begin
i := inic; j := medio + 1; k := inic - 1;
repeat
k := k + 1;
if (a[i] <= a[j]) then begin
b[k] := a[i]; i := i + 1 end
else begin
b[k] := a[j]; j := j + 1 end
until ((i > medio) or (j > fin));
(* copiar lo que falta *)
while (j <= fin) do begin
k := k + 1; b[k] := a[j]; j := j + 1 end;
while (i <= medio) do begin
k := k + 1; b[k] := a[i]; i := i + 1 end
end;
Recursión
s1 = 1, s2 = 1 + 2, s3 = 1 + 2 + 3, ... sn = 1 + 2 + · · · + n, ...
1 4 10 20 35
1 3 6 10 15
1 2 3 4 5
1 1 1 1
Para resolver el problema, podemos pensar que para llegar a una intersección
hay que hacerlo desde el oeste o desde el sur (salvo cuando la intersección está en
el borde oeste o sur), y por lo tanto la cantidad de caminos para llegar a la
intersección es la suma de la cantidad de caminos llegando desde el oeste (si
se puede) más la cantidad de caminos llegando desde el sur (si se puede). Los
números en la figura 11.1 indican, para cada intersección, la cantidad de caminos
para llegar allı́ desde (0, 0) mediante movimientos permitidos.
a) Hacer un programa para calcular la cantidad de caminos para llegar desde
(0, 0) a (m, n), donde m y n son ingresados por el usuario.
b) En cursos de matemática discreta se demuestra que el número de caminos
es
(m + n)! (m + n) × (m + n − 1) × · · · × (m + 1)
c(m, n) = = .
m! n! n × (n − 1) × · · · × 1
Incorporar al programa del inciso anterior una función con el cálculo de
c(m, n) (por ejemplo, con un lazo) y comparar con el obtenido anteriormen-
te.
c) Modificar el programa del inciso a) de modo de calcular la cantidad de
caminos cuando la intersección (r, s) está bloqueada y no se puede pasar
por allı́, donde r y s son ingresados por el usuario (0 < r < m y 0 < s < n).
Sugerencia: poner c(r, s) = 0.
d ) Supongamos ahora que, al revés del inciso anterior, para ir de (0, 0) a (m, n)
tenemos que pasar por (r, s) (por ejemplo, para llevar a (m, n) la pizza
que compramos en la esquina (r, s)). Hacer un programa para esta nueva
posibilidad. Sugerencia: puedo armar un camino de (0, 0) a (m, n) tomando
cualquier camino de (0, 0) a (r, s) y después cualquier camino de (r, s) a
(m, n).
e) De acuerdo al inciso b), la cantidad de caminos en el inciso d ) es c(r, s) ×
c(m − r, n − s). Verificar que esto es cierto.
f ) Análogamente, la cantidad de caminos del inciso c) es c(m, n) − c(r, s) ×
c(m − r, n − s). Verificar que esto es cierto en el mismo programa. ✄
Hay muchas variantes del problema de las torres de Hanoi, por ejemplo:
¿qué pasa si los discos no están inicialmente todos sobre una misma aguja
(pero respetan la distribución de mayor a menor)?, ¿qué pasa si hay más de
tres agujas? ✄
a) Resolver el problema.
Los números que aparecen en la solución de este problema se conocen como
números de Fibonacci, definidos recursivamente como:
a := 1; b := 1;
for i := 3 to n do begin c := a + b; b := a; a := c end;
fibonacci := a
var n: integer;
begin
writeln(’** Solucion recursiva de las torres de Hanoi:’);
writeln(’ Pasar n discos de la aguja "a" a la "b"’);
write(’ usando la "c", mediante movimientos ’);
writeln(’ permitidos.’);
writeln;
Generando objetos
combinatorios
Quitar un elemento:
function pop: dato;
begin
if (npila > 0) then begin
pop := pila[npila]; npila := npila - 1
end
end;
Nota: Valen los mismos comentarios anteriores: no hemos previsto
acciones para cuando se quiera sacar un elemento de una cola vacı́a.
En general, preguntaremos si hay elementos antes de sacar.
Observar la simetrı́a entre las acciones de push y pop: una realiza exacta-
mente los pasos inversos de la otra. Esta simetrı́a se traslada a que push es un
procedimiento con el dato como parámetro, mientras que pop es una función
que devuelve el dato (y por lo tanto debe hacerse alguna asignación o similar).
Para colas (fifo), las cosas son ligeramente diferentes. En vez de tener un ı́ndi-
ce como en el caso de la pila, mantenemos dos: uno, digamos ppocola, señalando
el principio de la cola en el arreglo, y otro, digamos fincola, señalando el final.
fincola se incrementa al agregar un dato, mientras que ppocola aumenta cuando
se extrae un dato, de modo que los elementos “vivos” en principio estarán entre
ppcola y fincola (inclusivo en ambos casos).
Claro que si la cola está definida como un arreglo de MAXC elementos,
las cosas se complican cuando agregamos más de MAXC elementos a la cola
(aún cuando hayamos quitado algunos), y necesitamos usar aritmética módu-
lo MAXC . No entramos en detalles porque en los ejemplos que daremos en
los capı́tulos posteriores supondremos que MAXC es lo suficientemente grande
como para evitar este problema.
Volviendo a las pilas, no podemos dejar de observar que los procedimientos
inicializar y push son viejos conocidos que usamos al leer los datos de un arreglo,
por ejemplo en el programa maximo (pág. 178). También hemos visto un atisbo
del uso de pilas en el problema 11.6 de las torres de Hanoi, donde las agujas
eran pilas en sı́.
En la próxima sección veremos que la técnica de mezclar recursión con pilas
puede usarse para generar distintos objetos combinatorios, aunque las pilas no
aparecerán explı́citamente.
begin
for i := 0 to 1 do begin
a[k] := i; (* poner 0 o 1 en el lugar k *)
if (k < n) then cadena(k+1) else begin
(* imprimir la cadena cuando k = n *)
write(’ ’);
for i := 1 to n do write(a[i]:1);
writeln
end (* if *)
end (* for *)
end;
Observar que usamos al arreglo a como una pila. Para k fijo, el elemento
en la posición k tiene un valor de 0 cuando i = 0, que se mantiene para va-
lores superiores a k pero luego es cambiado para i = 1, y obviamente cambia
varias veces cuando es llamado desde valores inferiores a k. Sin embargo, k es
el parámetro del procedimiento y no una variable global.
a) Hacer una prueba de escritorio del procedimiento cuando n = 3.
b) Hacer un programa que dado n ∈ N imprima todas las cadenas de bits de
longitud n, siguiendo las indicaciones anteriores.
c) Agregar un contador (global) para contar las cadenas de longitud n, y ver
que la cantidad de cadenas es 2n .
d ) En el procedimiento cadena propuesto, se va “hacia adelante”, llamando
a “ cadena(1) ” en el cuerpo principal y se va aumentando el valor del
parámetro k en cada llamada del procedimiento hasta llegar a k = n, ya
que n es global. Esto contrasta con el uso de recursión de, por ejemplo, el
factorial, donde vamos “hacia atrás” disminuyendo el valor del parámetro en
cada llamada. ¿Cómo podrı́a redefinirse el procedimiento de modo de hacer
la llamada “ cadena(n) ” en el cuerpo principal, y que en vez comparar k
con n se compare con 0 o 1?
Nota: En el problema 12.7 vemos otra forma de encontrar las cadenas de bits,
sin usar recursión. ✄
- 1 2 1 2 ✄
Una vez que sabemos cómo generar una familia de objetos, es más senci-
llo generar o contar objetos de la familia con caracterı́sticas particulares. Por
ejemplo:
Problema 12.3. Supongamos que queremos contar la cantidad c(n) de cadenas
de bits que no contienen dos 0’s sucesivos:
a) Hacer un programa para calcular c(n) para n ∈ N, usando el programa
del problema 12.1 y una variante de búsqueda lineal para encontrar dos
0’s consecutivos en cada cadena construida, verificando si se trata de una
cadena válida antes de aumentar el contador.
b) Comparar el número c(n) obtenido anteriormente con los números de Fibo-
nacci, y hacer un nuevo programa para calcular c(n) directamente. ✄
Problema 12.4. En este problema imprimiremos todos los caminos que hemos
contado en el problema 11.5. Para ello consideramos una arreglo global camino
en el que guardaremos las “intersecciones” o “esquinas” por las que hay que
pasar. Como éstas tienen dos coordenadas, digamos x y y, declaramos
type esquina = record x, y: integer end;
y
camino: array[1..MAXK] of esquina;
donde MAXK es una constante para la máxima longitud del camino. En el pro-
blema original todos los caminos tienen longitud m+n, pero podrı́an ser distintas
si, por ejemplo, también consideráramos la posibilidad de ir en diagonales de
suroeste a noreste. Para nuestros ejemplos, 10 o 20 son valores razonables para
MAXK .
También consideramos a los datos m y n como variables globales, y agrega-
mos la variable global k que indicará la longitud del camino construido. Inicial-
mente tendremos k = 0, puesto que no tenemos camino.
El trabajo lo hará el procedimiento llegardesde, haciendo la única llamada
“ llegardesde(0,0) ” en el cuerpo principal:
procedure llegardesde(i, j: integer);
var r: integer;
begin
k := k + 1;
with camino[k] do begin x := i; y := j end;
if (i < m) then llegardesde(i+1,j);
if (j < n) then llegardesde(i,j+1);
if ((i = m) and (j = n)) then (* llegamos *)
begin
(* imprimir *)
write(’ ’);
for r := 1 to k do
with camino[r] do
write(’ (’, x:1, ’,’, y:1, ’)’);
writeln
end; (* if i = m and j = n *)
Pág. 122 Generando objetos combinatorios
k := k - 1
end;
Estudiemos este procedimiento, haciendo una “prueba de escritorio” en todo
caso:
• k , inicialmente en 0, se incrementa en 1 al entrar al procedimiento, y se
incorpora la intersección (i, j) al camino en la (nueva) posición k .
Es decir, k indica la cantidad de objetos en la pila camino. Al comienzo,
la pila está vacı́a, y se van incorporando elementos atrás.
A diferencia con el problema 12.1, k es global y no el parámetro del
procedimiento.
• Si i < m o j < n, se puede continuar (hacia la derecha o hacia arriba), por
lo que llamamos al procedimiento incrementando los valores respectivos.
Observar que no hay un “ else ”: queremos que cada caso contribuya con
un camino distinto.
• Si i = m y j = n, hemos llegado a la esquina deseada, e imprimimos el
camino.
• Una vez recorridos los caminos que siguen hacia el este, o el norte, o impre-
so el camino, debemos retornar, borrando nuestras huellas para permitir
que la posición k pueda ser ocupada por otra esquina. Por eso se pone la
instrucción “ k := k - 1 ” al terminar el procedimiento.
En otras palabras, al terminar quitamos de la pila camino el elemento
que habı́amos agregado al comienzo.
Hay 3 caminos
cuando m = 2 y n = 1. ✄
a) Usando las ideas anteriores, hacer un programa para generar todas las per-
mutaciones de n elementos.
b) En el procedimiento poner , al buscar los elementos que faltan se pone
if (k < n) then
for j := 1 to n do begin
if (falta[j]) then begin
.
.
.
end (* if *)
end (* for *)
¿Qué función cumple el par begin–end que encierra a “ if ” en este
caso?, ¿es redundante?
¿Serı́a equivalente poner
if (k < n) then begin
for j := 1 to n do
if (falta[j]) then begin
.
.
.
end (* if *)
end (* if *) ?
nivel 0
nivel 1
nivel 2
nivel 3
Problema 12.6. El programa arbolbinario (pág. 182), que toma ideas de [6,
pág. 210] y de [3, pág. 153], muestra la construcción de un árbol binario ordenado
usando arreglos.
En los nodos se guarda información, que en nuestro ejemplo es llave y cuenta.
En llave guardamos un entero, pero podrı́a ser una palabra de un texto, o
un apellido, etc., mientras que en cuenta guardamos el número de veces que
apareció ese dato. En el árbol, un nodo tiene a su izquierda un subárbol cuyos
nodos tienen menor llave, y a su derecha nodos con mayor llave.
A la salida se imprimen los datos ordenados por orden creciente de llave
y las veces que apareció el dato, como se muestra en el ejemplo de salida al
finalizar el listado.
Estudiando el programa, observamos que
• Cada nodo guardará cuatro datos: la llave, la cantidad de veces que apa-
reció, y cómo encontrar a sus hijos. Por lo tanto necesitamos un registro
con 4 campos.
Créase o no, ya hemos visto y recorrido árboles binarios ordenados con an-
terioridad, aunque tenı́an una estructura particular que nos permitı́a ahorrar
espacio.
Por ejemplo, en el problema 12.1 construı́amos todas las cadenas de bits
de longitud n o, equivalentemente, los subconjuntos de {1, . . . , n}. Dada una
cadena c de bits de longitud k, podemos considerar el “hijo a izquierda” como
la cadena c0 de longitud k + 1 que se obtiene de c agregando a la derecha un 0, y
de la misma forma considerar el “hijo a derecha” como la cadena c1 de longitud
k + 1 que se obtiene agregando un 1. En la función recursiva del problema 12.1
el nivel que estábamos construyendo era precisamente k, y cuando llegábamos
a k = n, imprimı́amos la cadena.
12.5. Problemas Adicionales Pág. 127
Nota: También hay “árboles” en los problemas 12.4 y 12.5, sólo que los nodos
pueden tener más de dos hijos. A diferencia del problema de los subconjun-
tos o cadenas de bits, el “recorrido” del árbol es más complicado porque la
descripción de los nodos lo es.
La variable k del procedimiento llegardesde o en el procedimiento poner
indica también el nivel que estamos recorriendo. Las instrucciones “ k := k
- 1 ” y “ falta[j] := true ”, respectivamente, hacen que vayamos “un nivel
hacia arriba” antes de descender nuevamente, y por eso la técnica de “borrar las
huellas” que usamos en ambos casos es conocida como backtracking o “rastreo
inverso”.
Grafos y árboles
E = {{1, 2}, {1, 3}, {2, 3}, {2, 6}, {3, 4}, {3, 6}, {4, 6}}.
2 1
3 6
4 5
Sólo consideraremos grafos simples, para los que no hay una arista de un
nodo en sı́ mismo, ni aristas paralelas uniendo los mismos nodos. En este caso,
podemos relacionar n = |V | y m = |E|: si hay n elementos, hay c(n, 2) =
n(n − 1)/2 subconjuntos de 2 elementos, de modo que m ≤ c(n, 2).
Si además el grafo es conexo (y simple), como se puede unir un nodo con los
n − 1 restantes, debe haber al menos n − 1 aristas. De modo que para un grafo
(simple) conexo, m tiene que estar básicamente entre n y n2 .
A veces se consideran grafos dirigidos o digrafos, en los que las aristas están
orientadas, y por lo tanto se indican como (a, b), y se distingue entre (a, b) y
(b, a). Nosotros no estudiaremos este tipo de grafos, aunque en realidad hemos
trabajado con el grafo de la figura 11.5 como si fuera un digrafo: las calles sólo
iban de sur a norte o de oeste a este.
Dada su estructura, es más sencillo trabajar con árboles que con grafos.
Como hemos dicho, un árbol es un grafo (simple) conexo y sin ciclos, pero hay
muchas formas equivalentes de describirlo, algunas de las cuales enunciamos
como teorema (que por supuesto creeremos):
en el problema 12.6.
Pág. 132 Grafos y árboles
Por supuesto, podemos pensar que los nietos son hijos de los hijos, los hijos
padres de los nietos, etc., de modo que —en un árbol con raı́z— hablaremos
de padres, hijos, ascendientes y descendientes de un nodo. La raı́z será el único
nodo sin ascendientes, mientras que habrá uno o más nodos sin descendientes.
También es común referirse al conjunto de descendientes de un nodo (aunque
el nodo no sea la raı́z) como una rama del árbol.
type
arreglodevertices = array[1..MAXN] of integer;
tipoarista = record i, j: integer end;
arreglodearistas = array[1..MAXM] of tipoarista;
matrizNN = array[1..MAXN,1..MAXN] of integer;
var
ngrafo, mgrafo: integer;
aristasgrafo: arreglodearistas;
adyacencias: matrizNN;
usando para las aristas una representación como arreglo de registros (pudiendo
haber declaraciones adicionales, por supuesto).
Usualmente el ingreso de datos es más sencillo mediante la lista de aristas,
pues la matriz de adyacencias tiene n2 elementos y en general m n2 (y siempre
m ≤ n(n − 1)/2 para grafos simples). De cualquier modo, es conveniente tener
a mano procedimientos para la lectura y pasar de una a otra representación:
a) Hacer un procedimiento para leer las aristas ingresadas por terminal, donde
cada arista consta de dos números enteros, formando un arreglo de longitud
mgrafo.
Nota: Como es usual, supondremos que el usuario ingresa correctamente
los datos: los vértices de las aristas son enteros entre 1 y ngrafo, no hay
aristas repetidas o de la forma {i, i}, y no hay más de MAXM aristas.
b) Considerando el procedimiento dearistasaadyacencias ,
procedure dearistasaadyacencias;
var i, j, k: integer;
begin
for i := 1 to ngrafo do
for j := 1 to ngrafo do
adyacencias[i,j] := 0;
for k := 1 to mgrafo do
with aristasgrafo[k] do begin
adyacencias[i,j] := 1;
adyacencias[j,i] := 1
end
end;
hacer un programa que lea el número de nodos, las aristas (como en el inciso
anterior), calcule la matriz de adyacencias, e imprima para cada nodo, los
nodos que son adyacentes. Por ejemplo, si la entrada son las aristas {1, 5},
{2, 5} y {4, 2}, deberá imprimir
Nodo Vecinos
1 5
2 4 5
3
4 2
5 1 2
Problema 13.3 (Grado de vértices). Dado un grafo G = (V, E), para cada
nodo v ∈ V se define su grado o valencia, δ(v), como la cantidad de aristas que
inciden en v, o equivalentemente, la cantidad de vecinos de v (excluyendo al
mismo v).
Por ejemplo, en el grafo de la figura 13.1, los grados son δ(1) = 2, δ(2) =
3, δ(3) = 4, δ(4) = 2, δ(5) = 0, δ(6) = 3.
Tomando como base el problema anterior:
a) Hacer un programa que ingrese un grafo dado por sus aristas y calcule δ(v)
para todo v ∈ V .
b) Uno de los primeros teoremas que se ven en teorı́a de grafos dice que si U
es el conjunto de nodos de grado impar, entonces
X
δ(v) es par.
v∈U
Pág. 134 Grafos y árboles
Algoritmo visitar
Cuadro 13.1: esquema del algoritmo visitar para recorrer todos los nodos a los
que se puede llegar desde i0 .
En general, aún cuando el grafo sea un árbol binario ordenado (y con raı́z
1), el recorrido a lo ancho es distinto de los recorridos “en orden”, “pre orden” o
“post orden” que hemos visto en el problema 12.6, pues ahora visitamos los veci-
nos de la raı́z, luego los vecinos de los vecinos, etc., en otras palabras, visitamos
el árbol por niveles.
Problema 13.7. Un célebre teorema de Euler dice que un grafo tiene un ciclo
que pasa por todas las aristas exactamente una vez, llamado ciclo de Euler,
si y sólo si el grafo es conexo y el grado de cada vértice es par (recordar el
problema 13.3).
Modificar el programa anchoprimero para que a la salida determine también
si el grafo tiene o no un ciclo de Euler usando el teorema.
Nota: Un problema muy distinto es encontrar un ciclo de Euler en caso de
existir. Esto es lo que hacemos en el problema 14.10. ✄
3 Bah, que nos vamos por las ramas.
13.4. Camino más corto: Dijkstra Pág. 137
2 2 3 Arista e peso we
{1,2} 2
2 1
{1,4} 3
1 {1,5} 8
2
1 {2,3} 2
1 1 6 {2,4} 1
{3,4} 1
{3,5} 2
3 3 {3,6} 1
{4,6} 1
8
4 5 {5,6} 3
Observar que el valor de k no está fijo: no nos interesa si tenemos que usar
una ruta o cien, sólo nos interesa que la distancia total para ir de s a t sea
mı́nima.
Por ejemplo, en el grafo de la figura 13.3 podemos usar varios caminos para
ir del vértice 1 al 5: el camino (1, 5) usa una única arista (k = 1) y tiene peso 8,
el camino (1, 2, 3, 5) usa 3 aristas y tiene costo total 2 + 2 + 1 = 5, y en realidad
no hay otro con menor costo.
Tal vez el algoritmo más conocido para resolver este problema sea el de
Dijkstra, que sigue la estructura del algoritmo visitar : se comienza desde un
nodo, en este caso s, se lo coloca en una cola, y se visitan los nodos de la cola.
E. W. Dijkstra (1930–2002) nació y murió en Holanda. Fue uno de los más
grandes intelectos que contribuyeron a la lógica matemática subyacente en los
Pág. 138 Grafos y árboles
Si esta desigualdad es válida, quiere decir que el camino más corto para ir desde
s a j (sólo por nodos ya visitados) es ir desde s a i con el camino para i, y luego
usar la arista {i, j}. Por lo tanto, actualizamos dj poniendo dj = di + w{i,j} .
También agregaremos j a la cola si no se ha agregado aún.
Es una propiedad del algoritmo, que no demostraremos, que si j es un vecino
de i —el nodo que se está visitando— y j ya se ha visitado, la desigualdad (13.1)
no puede darse, por lo que, a diferencia del algoritmo visitar no es necesario
verificar si j ya a sido visitado.
Nota: Para demostrar esta propiedad —y que el algoritmo es correcto— se
usa de forma esencial que we > 0 para todo e ∈ E. Nosotros dejaremos estas
propiedades para cursos de matemática discreta o teorı́a de grafos.
Podemos esquematizar el algoritmo en seudo-código como:
para todo i ∈ V hacer di ← ∞;
ds ← 0; Q ← {s};
repetir
sea i ∈ Q tal que di = mı́nj∈Q dj ;
si i 6= t entonces
sacar i de Q;
para todo j adyacente a i hacer
si di + w{i,j} < dj entonces
dj ← di + w{i,j} ;
si j ∈
/ Q entonces agregar j a Q
hasta que i = t o Q = ∅
Una simplificación puede hacerse si convenimos que w{i,j} = ∞ cuando
{i, j} ∈
/ E. En este caso, los nodos j adyacentes a i se caracterizan por w{i,j} <
∞, y como los nodos j que no han ingresado en la cola son los que satisfacen
dj = ∞, el último lazo interno puede simplificarse a
para todo j ∈ V hacer
si di + w{i,j} < dj entonces
si dj = ∞ entonces agregar j a Q;
13.4. Camino más corto: Dijkstra Pág. 139
dj ← di + w{i,j} ;
• Las aristas se guardan, como antes, en registros que ahora tienen un campo
más, w , para guardar el peso correspondiente.
y (
s si i = s,
padre i =
0 en otro caso.
2. La cola de nodos a visitar se guarda en el arreglo avisitar , que inicial-
mente sólo contiene al nodo s.
3. En el lazo principal, se busca en la cola y se coloca al final el vértice
con menor valor de dist , que será el próximo nodo a visitar. El pro-
cedimiento para encontrar el mı́nimo y modificar la cola es similar al
proceso de selección directa (pág. 98, ver también el problema 8.2.e)).
4. Una vez que determinamos el nodo a visitar, i, lo comparamos con t,
el nodo al cual queremos llegar. Si i = t, hemos llegado y se termina
el procedimiento.
Pág. 140 Grafos y árboles
las aristas que sacamos son bastante arbitrarias, en general hay muchos árboles
generadores de un mismo grafo.
Cuando hay pesos (o costos) asociados a las aristas, como en el caso de la
construcción de las carreteras, nos interesa encontrar entre todos los árboles ge-
neradores uno que minimice la suma de los pesos de las aristas que lo componen,
X
we ,
e en el árbol
llamado árbol generador mı́nimo (es posible que haya más de un árbol con esta
propiedad, por ejemplo si los costos de todas las aristas son 1).
Por ejemplo, volviendo al grafo de la figura 13.3, podemos formar un árbol
generador con las aristas {1, 2}, {1, 4}, {2, 3}, {3, 6}, {6, 5} (siempre un árbol ge-
nerador debe tener n − 1 aristas), con peso total 2 + 3 + 2 + 1 + 3 = 11, y si
reemplazamos cambiamos la arista {5, 6} por la arista {3, 5}, reducimos el costo
en 1.
Hay varios algoritmos para encontrar un árbol generador mı́nimo, y nosotros
veremos aquı́ el debido a Prim, pues sigue la estructura del algoritmo visitar,
siendo por lo tanto muy similar al recorrido a lo ancho o al algoritmo de Dijkstra
para el camino más corto. Otro algoritmo, más eficiente en la mayorı́a de los
casos prácticos, es el de Kruskal, que dejamos para la sección de Problemas
Adicionales pues su implementación es más elaborada.
El algoritmo de Kruskal fue publicado en 1956, mientras que el de Prim fue
publicado en 1959. Kruskal también obtuvo en forma independiente el algoritmo
de Prim y lo publicó en 1959 en el mismo trabajo donde presenenta su algoritmo
para el camino más corto, lo que no es sorpresa por la similitud.
Recordemos que en el algoritmo visitar mantenı́amos una cola con los nodos
a visitar, y se formaban tres clases de nodos: los visitados, los que estaban en
la cola, y los que nunca habı́an ingresado en la cola.
En el algoritmo de Prim se sigue la misma idea, sólo que un nodo en la cola
se visita cuando su “distancia” a los nodos ya visitados es la menor entre los
nodos de la cola. De modo que hay que hacer pocas modificaciones al algoritmo
de Dijkstra: cambiar la definición de la distancia y, en vez de construir el cami-
no, construir el árbol correspondiente. A diferencia del algoritmo de Dijkstra,
tenemos que mantener información sobre los nodos visitados. Como el arreglo
padre se irá modificando en los sucesivos pasos (en el Dijkstra también, pero
no en el recorrido a lo ancho), introducimos un nuevo vector con valores lógicos
visitado.
Nota: Como con el algoritmo de Dijkstra, se necesita que we > 0 para todo
e ∈ E. Asimismo, dejamos la demostración de que el algoritmo da efectivamente
un árbol generador mı́nimo para los cursos de matemática discreta o teorı́a de
grafos.
procedure arbolminimo;
var
i, j, k, kmin, hay: integer;
d, dmin: costo;
avisitar: arreglodevertices;
visitado: array[1..MAXN] of boolean;
begin
(* inicializacion *)
dearistasacostos;
for i := 1 to ngrafo do begin
padre[i] := 0;
dist[i] := infinito;
visitado[i] := false
end;
(* 1 es la "raiz" *)
padre[1] := 1; dist[1] := 0; hay := 1;
avisitar[1] := 1;
(* examinar vecinos de i *)
for j := 1 to ngrafo do
if ((not visitado[j]) and
(costos[i,j] < dist[j])) then begin
(* si j no se agrego, agregarlo a la cola *)
13.6. Problemas Adicionales: Kruskal Pág. 143
1. V = ∪1≤i≤c Ai .
Dado u ∈ V , podemos encontrar todos los nodos que se conectan con él
mediante el algoritmo visitar (pág. 134), encontrando ası́ la componente a la
cual pertenece. Claro que si el grafo G es conexo, hay una única componente
(c = 1), y recı́procamente.
Nota: A fin de describir el algoritmo de Kruskal, consideramos que las compo-
nentes conexas son conjuntos de nodos. En otras variantes de la definición de
componente conexa, también se incluyen las aristas que unen a los nodos.
En el algoritmo de Kruskal, que esquematizamos en el cuadro 13.2, dado el
grafo G = (V, E) vamos construyendo por etapas un grafo (V, F ), con F ⊂ E,
examinando ordenadamente cada arista de E para decidir si la agregaremos o
no a F .
En el algoritmo, conservamos en cada etapa las componentes conexas de
(V, F ) en C, e indicamos mediante mediante Ci a la componente conexa que tiene
al nodo i. Inicialmente no habrá aristas, es decir, tendremos F = ∅, Ci = {i}
para i ∈ V , y C = {Ci }i∈V .
Todas las aristas en el grafo original se ponen en una cola, Q, y en cada paso
extraemos de Q la arista de menor peso, digamos e = {i, j}. Si i y j están en
la misma componente conexa (hasta ese momento), i.e. si Ci = Cj , pasamos a
la próxima arista. En cambio, si Ci 6= Cj , podemos unir las componentes Ci y
Cj mediante e formando una nueva componente conexa, y guardamos e en el
conjunto F .
En principio terminaremos cuando no haya más aristas en la cola Q, pero si
en algún momento llegamos a una única componente conexa, o sea si |C| = 1,
entonces sabemos que ya no agregaremos más aristas (pues Ci = Cj para todo
i, j ∈ V ) y podemos terminar aún cuando Q 6= ∅.
Al finalizar el algoritmo, las componentes conexas de H = (V, F ) y de G =
(V, F ) serán las mismas, C, y H será el grafo de menor peso con esta propiedad.
Claro que si hay una única componente conexa (de G o de H porque coinciden),
H será un árbol generador mı́nimo de G.
Nota: Como en los caso anteriores, “creeremos” que el algoritmo es correcto.
La mayor dificultad para implementar el algoritmo de Kruskal es mantener la
información sobre los conjuntos Ci a medida que van cambiando, y se ha ideado
una estructura eficiente para ello que —conservando el inglés— llamaremos
union-find (por “unión-encontrar”) y que pasamos a describir.
En cada etapa del algoritmo, vamos a considerar para cada componente
C ∈ C un nodo r ∈ C representante de C, y una función f : V → V tal que f (i)
es el representante de la componente en la que está i, Ci . En particular, si r es
el representante de C, tendremos f (r) = r.
f irá cambiando en cada etapa, pues se van fusionando las componentes
conexas, pero como inicialmente tenemos Ci = {i} para todo i ∈ V , inicialmente
tendremos f (i) = i.
Cuando en alguna etapa se unen C y C 0 —con representantes r y r0 respecti-
vamente— para formar C 00 , por simplicidad elegiremos a uno de ellos, digamos
r, como representante de C 00 . Es decir que al finalizar esa etapa f (r) sigue siendo
r pero f (r0 ) = r, y en realidad f se ha modificado en todos los nodos de C 0 .
Hacer la redefinición de f explı́citamente en cada etapa para todos los nodos
es muy costosa, por lo que se considera otra función auxiliar, g, que inicialmente
toma los mismos valores de f , i.e. inicialmente g(i) = i para todo i ∈ V .
g también se va modificando en cada etapa, pero lo haremos únicamente
13.6. Problemas Adicionales: Kruskal Pág. 145
Algoritmo de Kruskal
Nota: Esta elección hace que el procedimiento find no haga más de dlog 2 |V |e
iteraciones.
Manteniendo el arreglo long (definido globalmente por simplicidad) para
guardar los valores de `(r), podemos escribir el procedimiento para juntar dos
componentes como:
procedure union(r, rp: integer);
var l, lp: integer;
begin
l := long[r]; lp := long[rp];
if (lp < l) then begin
repre[rp] := r;
if (l < lp + 1) then long[r] := lp + 1
13.6. Problemas Adicionales: Kruskal Pág. 147
end
else begin
repre[r] := rp;
if (lp < l + 1) then long[rp] := l + 1
end
end;
Por completitud, incluimos la inicialización de la estructura “union-find”:
procedure inicializaruf;
var i: integer;
begin
for i := 1 to ngrafo do begin
repre[i] := i; long[i] := 0 end
end;
En resumen, para implementar “union-find” necesitamos tres funciones o
procedimientos, inicializaruf , find y union, y dos arreglos, repre y long, que
por simplicidad hemos considerado como globales.
Ya estamos en condiciones de implementar el algoritmo, con las siguientes
observaciones
• Luego habrá que declarar las funciones de “union-find”, como hemos des-
cripto anteriormente.
procedure arbolminimo;
var
hay, ri, rj, k, kmin: integer;
wmin : costo;
e: tipoarista;
begin
(* inicializacion *)
inicializaruf; (* inicializar union-find *)
nconexas := ngrafo; (* numero de componentes *)
marbol := 0; (* en el bosque no hay aristas *)
hay := mgrafo; (* cantidad de aristas a examinar *)
Punteros y listas
encadenadas
14.1. Punteros
Recordemos que las variables que hemos usado hasta ahora ocupan un lugar
de memoria, que será más grande o más chico dependiendo de su tipo, y que
pensamos como “cajas”. Para reconocer una variable le hemos dado un nombre,
su identificador, que tienen sentido para nosotros, pero para la máquina —una
vez compilado el programa— los nombres desaparecen y en cambio los identifica
por la dirección de memoria en la que empieza la caja y su contenido (y por lo
tanto su longitud).
Podemos pensar que un puntero es una variable donde nosotros guardaremos
direcciones de memoria donde empiezan cajas. Aunque todas las direcciones
(para una máquina y sistema operativo dados) ocupan el mismo espacio, en
Pascal debemos agregar en la declaración de un puntero el tipo al cual “apunta”
o “referencia” (esto es una propiedad del lenguaje, protegiéndonos de posibles
errores).
Por ejemplo, si queremos alojar en el puntero p una dirección de memoria
en la que se guardará un entero, declaramos
var p: ^integer
En este caso, al comienzo del programa p no tiene asignado valor alguno.
Pág. 152 Punteros y listas encadenadas
p := nil
Podemos interpretar a nil como “un cable a tierra”, que indica que no hay
“nada”. Recordemos que justamente hemos usado con el mismo sentido a la
variable nada en el programa arbolbinario (pág. 182).
A pesar de que, habiéndolo declarado, p ocupa un lugar de memoria, ese
lugar es sólo para guardar direcciones, y puede no coincidir con la cantidad de
espacio necesaria para guardar, por ejemplo, un entero. Si además queremos
reservar un lugar para guardar un entero, usamos “ new ”:
new(p)
Esta instrucción hace que se reserve un lugar en memoria —la famosa “ca-
ja”— para alojar un dato de tipo entero (en este caso), y guarda en p la dirección
de ese lugar, dejando a la caja disponible pero sin valor asignado.
Nota: Es posible que no haya más memoria para reservar, cosa improbable con
las computadoras actuales y los programas que nosotros usaremos, por lo que
no nos preocuparemos por esta eventualidad. En programación más “seria”
también debemos preocuparnos por dejar libres los lugares obtenidos mediante
“ new ”, que en Pascal se hace mediante “ dispose ” (que no veremos).
La variable (entera) a la que ahora “apunta” p se indica por pˆ. Ası́, si
queremos poner el número 3 allı́, hacemos
p^ := 3;
p := n (* incorrecto! *)
pero si p y q son punteros del mismo tipo (declarado mediante “ type ”), es legal
hacer
q := p
aún cuando p o q no referencien dato alguno, ya que estamos copiando en q la
dirección guardada en p. Por otro lado, supuesto que p y q sean del mismo tipo
y que ambos se hayan “inicializado” con new , podemos copiar los contenidos de
una caja a la otra mediante
q^ := p^
Nuestro interés en este tipo de variables es en la creación de listas encade-
nadas.
14.2. Listas encadenadas Pág. 153
lista := nil
pero puede ser que querramos construir la lista de otra forma, por ejemplo
si queremos que esté ordenada.
Si queremos agregar siempre atrás, es conveniente guardar también la di-
rección del último nodo de la lista, y debe preverse en la inicialización.
1 A veces, para confundir, también hablamos de listas cuando nos referimos a arreglos.
2 Como en grafos. En realidad, las listas encadenadas se pueden pensar como grafos diri-
gidos.
3 Nuestra presentación difiere de otras donde siempre hay al menos un nodo.
Pág. 154 Punteros y listas encadenadas
p := lista;
while (p <> nil) do begin
haceralgo; (* haceralgo con p o p^ *);
p := p^.siguiente
end
Problema 14.3. Hacer un procedimiento para construir una copia de una lista
encadenada, es decir, una lista con tantos nodos como la original, y en la cual
la información se guarda en el mismo orden que la lista original.
Nota: Observar que si a y b son arreglos del mismo tipo (que se ha declarado
con “ type ”), para copiar a en b se puede hacer simplemente la asignación “ b
:= a ”, pero con listas encadenadas tenemos que recorrer todos los nodos. ✄
Nota: El problema 8.3 hacı́a lo mismo para un vector, pero la técnica es muy
diferente. En cambio, el esquema propuesto usa la idea del procedimiento
“ enordeninverso ” del problema 14.2, agregando el nodo adelante en vez de
imprimir. ✄
Problema 14.5 (El problema de Flavio Josefo II). Implementar una solu-
ción a este problema (problema 6.5) usando en vez de arreglos una lista circular,
i.e. el “último” nodo se encadena al “primero”, donde cada nodo representa una
persona, y la eliminación se traduce en eliminar el nodo de la lista. El programa
debe imprimir la permutación de Flavio Josefo.
Sugerencia: crear una lista quedan de los que “quedan”, inicialmente con los va-
lores 1, . . . , n, donde el último nodo apunta al primero en vez de a nil . A medida
que se eliminan nodos de quedan, ir formando la permutación de Flavio Josefo.
En cada paso de “eliminación”, guardar información sobre el nodo anterior al
eliminado, para eliminar el nodo y poder contar m de los que quedan a partir
de él. ✄
Problema 14.6. Siguiendo las ideas del problema 14.4 y del procedimiento
ordenada en el programa listas (pág. 192), hacer un procedimiento para ordenar
(por alguna llave) una lista ya ingresada sin generar nuevos nodos. ✄
Pág. 156 Punteros y listas encadenadas
Problema 14.8.
a) Declarando
type
ptrnodo = ^nodo;
nodo = record
llave: integer; (* numero leido *)
cuenta: integer; (* veces que aparecio *)
izquierda: ptrnodo; (* hijo a la izquierda *)
derecha: ptrnodo (* hijo a la derecha *)
end;
rehacer el programa arbolbinario (pág. 182) reemplazando los arreglos por
listas encadenadas, y luego realizar los incisos del problema 12.6.
b) Ver que si los datos se ingresan ya ordenados, el árbol correspondiente se
reduce básicamente a un arreglo unidimensional.
Nota: Para eliminar este problema, se han diseñado una serie de variantes,
como árboles balanceados, árboles “B”, etc. Observar que sacar nodos de
un árbol es un proceso bastante más complicado que sacar nodos de una
lista (ver [6, Cap. 4]). ✄
renglón. Los editores de texto trabajan con una estructura similar (escribiendo
luego secuencialmente en el disco cuando guardamos el archivo).
Un problema matemático donde se puede usar una idea similar es en grafos,
al tener la descripción por lista de aristas o por nodos adyacentes a un nodo
dado.
Problema 14.9. Como hemos visto en el capı́tulo anterior, un grafo G consiste
de un conjunto de nodos o vértices V = {1, 2, . . . , n}, y un conjunto de aristas
E, donde cada arista es de la forma {u, v} con u, v ∈ V , u 6= v (y supondremos
que no hay aristas repetidas).
Es usual representar los nodos mediante puntos y las aristas con segmentos
o curvas que unen los nodos, como se muestra en la figura 13.1 (pág. 130), en
donde n = 6 y E = {{1, 2}, {1, 3}, {2, 3}, {2, 6}, {3, 4}, {3, 6}, {4, 6}}.
Por otra parte, en vez de describir G mediante E (y V ), podrı́amos hacerlo
a partir de las listas de adyacencias o vecinos: para cada u ∈ V el conjunto de
nodos {v ∈ V : {u, v} ∈ E}. En el ejemplo anterior, podrı́amos poner:
1 → {2, 3}, 2 → {1, 3, 6}, 3 → {1, 2, 4, 6}, 4 → {3, 6}, 5 → ∅, 6 → {2, 3, 4}.
ptrvertice = ^vertice;
vertice = record
id: integer;
s: ptrvertice;
vecinos: ptrvecino
end;
Para demostrar que un grafo conexo con todos los nodos de grado par tiene
un ciclo de Euler, se comienza a partir de un nodo y se van recorriendo aristas y
borrándolas hasta que volvamos al nodo, formando un ciclo. Esto debe suceder
porque todos los nodos tienen grado par. Puede ser que el ciclo no cubra a
todas las aristas, pero como el grafo es conexo, debe haber un vértice en el ciclo
construido que tenga una arista (aún no eliminada) incidente en él, a partir del
cual podemos formar un nuevo ciclo, agregarlo al anterior y continuar con el
procedimiento hasta haber eliminado recorrido todas las aristas.
Esta demostración es bien constructiva, y podemos implementar los ciclos
como listas encadenadas. La implementación de aristas, etc. puede hacerse con
arreglos o matrices.
Hacer un programa para decidir si un grafo entrado por n, la cantidad de
vértices, y en caso de ser conexo y con todos los vértices de grado par, imprimir
un ciclo de Euler. ✄
Apéndice A
Programas mencionados
begin
writeln(’Hola Mundo!’);
writeln;
writeln(’y Chau!’)
end.
var a, b: integer;
begin
writeln(’** Programa para sumar dos numeros enteros’);
writeln;
a := 1;
b := 2;
write(’La suma de ’, a);
write(’ y ’, b);
writeln(’ es ’, a + b);
writeln; writeln(’** Fin **’)
end.
var a: integer;
Pág. 160 Programas mencionados
begin
writeln(’** Programa para leer un dato entero’);
writeln;
write(’Entrar un entero: ’); readln(a);
writeln;
writeln(’El entero leido es ’, a);
writeln; writeln(’** Fin **’)
end.
var x, y: real;
begin
writeln(’** Calcular la raiz cuadrada de x **’);
writeln;
write(’Entrar x (> 0): ’); readln(x);
y := sqrt(x);
writeln;
writeln(’La raiz cuadrada de ’, x, ’ es ’, y);
writeln; writeln(’** Fin **’)
end.
var
a: integer;
x: real;
begin
writeln(’** Asignar un entero a un real’);
writeln;
write(’Entrar el valor del entero: ’); readln(a);
x := a;
writeln;
writeln( a, ’ cambiado a real es: ’, x);
writeln; writeln(’** Fin **’)
end.
segundos (Problema 3.13) Pág. 161
begin
write(’** Programa para pasar de segundos’);
writeln(’ a horas, minutos y segundos **’);
writeln;
write(’Entrar la cantidad de segundos: ’); readln(segs);
writeln;
writeln(segs, ’ segundos son equivalentes a ’);
mins := segs div 60; segs := segs mod 60;
hs := mins div 60; mins := mins mod 60;
writeln(hs, ’ hs, ’, mins, ’ mins, ’, segs, ’ segs.’);
writeln; writeln(’** Fin **’)
end.
var a: integer;
begin
writeln(’Ver si un numero entero es positivo’);
writeln;
write(’Entrar un numero: ’); readln(a);
writeln;
writeln(’es positivo?: ’, a > 0);
writeln; writeln(’** Fin **’)
end.
begin
writeln(’ ** Pasar de caracter a numero y viceversa **’);
write(’ Entrar un caracter: ’); readln(c);
writeln;
i := ord(c);
writeln(’ El numero de orden de ’, c, ’ es ’, i:1);
(* i:1 indica que escribira i en un solo espacio,
Pág. 162 Programas mencionados
var x, y: real;
begin
writeln(’** Encontrar el valor absoluto de un numero real’);
writeln;
write(’Entrar el numero: ’); readln(x);
if (x >= 0) then y := x else y := -x;
writeln;
writeln(’El valor absoluto de ’, x, ’ es ’, y);
writeln; writeln(’** Fin **’)
end.
var a, b: integer;
begin
writeln(’** Comparar dos numeros enteros’);
writeln;
write(’Entrar un numero entero: ’); readln(a);
write(’Entrar otro numero entero: ’); readln(b);
writeln;
if (a > b) then
writeln(a, ’ es mayor que ’, b)
else if (a < b) then
writeln(b, ’ es mayor que ’, a)
else
writeln(a, ’ es igual a ’, b);
writeln; writeln(’** Fin **’)
end.
var c: char;
begin
write(’** Decide si un caracter es una letra ’);
writeln(’mayuscula o minuscula o no es letra **’);
write(’ Entrar un caracter: ’); readln(c); writeln;
write(c);
if (’a’ <= c) and (c <= ’z’) then
writeln(’ es una letra minuscula’)
else if (’A’ <= c) and (c <= ’Z’) then
writeln(’ es una letra mayuscula’)
else
writeln(’ no es una letra’);
writeln; writeln(’** Fin **’)
end.
var a, b, r: integer;
begin
writeln(’** Hallar el resto de la division entre a y b,’);
writeln(’(enteros positivos) mediante restas sucesivas **’);
writeln;
r := a;
while (r >= b) do r := r - b;
writeln;
write(’El resto de dividir ’, a:1);
writeln(’ por ’, b:1, ’ es ’, r:1);
const pi = 3.14159265;
var
inicial, final, incremento, grados: integer;
radianes: real;
begin
writeln(’Hacer una tabla del seno dando valores’);
writeln(’inicial, final, y del incremento (en grados).’);
writeln;
writeln(’Angulo Seno’);
grados := inicial;
while (grados <= final) do begin
radianes := grados * pi/180;
writeln(grados:5, sin(radianes):15:5);
grados := grados + incremento
end;
var
n: integer;
suma: real; (* ponemos real por si es muy grande *)
begin
writeln(’** Sumar 1 + 2 +...+ n **’);
writeln;
write(’ Entrar n (maxint = ’, maxint, ’): ’); readln(n);
suma := 0;
writeln;
writeln(’ suma con while al reves: ’, suma:10:0);
(* 10:0 indica que el numero se escribe en
al menos 10 espacios, sin decimales *)
var a, b, c: integer;
begin
writeln(’Determina la cantidad de cifras de un entero’);
writeln(’(sin tener en cuenta el signo)’);
writeln;
c := 0;
repeat
c := c + 1;
b := b div 10
until b = 0;
begin
writeln(’** Determinacion de epsmin y epsmaq **’); writeln;
x := 1;
repeat eps := x; x := x / 2 until (x = 0);
writeln(’epsmin es: ’, eps);
eps := 1;
while (1 + eps > 1) do eps := eps/2;
eps := 2 * eps;
writeln(’epsmaq es: ’, eps);
var
x, pot: real;
n, i: integer;
begin
writeln(’** Calculo de la potencia x a la n’);
writeln;
pot := 1;
for i := 1 to n do pot := pot * x;
writeln;
writeln(x, ’ a la ’, n:1, ’ es ’, pot);
var a: integer;
begin
writeln(’** Prueba del funcionamiento de eoln’);
sumardatos (Problema 4.19) Pág. 167
writeln;
var
s, x: real;
findatos: boolean;
begin
writeln(’** Calcular la suma de los datos entrados’);
writeln(’indicando el fin de datos con doble <retorno>’);
writeln;
writeln;
writeln(’La suma de los datos es ’, s);
const
itmax = 10;
tol = 10e-5;
x0 = 1.0;
Pág. 168 Programas mencionados
var
it: integer;
a, x, y: real;
begin
(* carteles *)
writeln(’** Metodo babilonico o de Newton para’);
writeln(’ aproximar la raiz cuadrada de un numero’);
writeln(’ real positivo "a" **’); writeln;
(* datos de entrada *)
write(’ Entrar a: ’); readln(a); writeln;
(* inicializacion *)
it := 1; x := x0; y := (x + a/x)/2;
(* lazo principal *)
while ((it <= itmax) and (abs(x - y) > tol)) do
begin x := y; y := (x + a/x)/2; it := it + 1 end;
(* salida *)
writeln(’Iteraciones realizadas: ’, it);
if (it > itmax) then
writeln(’** Iteraciones maximas excedidas **’);
writeln(’Solucion aproximada obtenida: ’, y:20:10);
(* fin *)
writeln; writeln(’** Fin **’)
end.
var a, b, r: integer;
begin
(* cartel de info *)
writeln(’** Algoritmo de Euclides para encontrar el ’);
write(’ maximo comun divisor entre dos enteros, ’);
writeln(’mcd(a,b) **’); writeln;
writeln(’Nota: mcd(0,0) definido como 0’); writeln;
(* datos de entrada *)
write(’ Entrar a: ’); readln(a);
factoresprimos (Problema 5.18) Pág. 169
(* lazo principal *)
while (b <> 0) do begin r := a mod b; a := b; b := r end;
(* salida *)
writeln(’El maximo comun divisor es: ’, a:2);
(* fin *)
writeln; writeln(’** Fin **’)
end.
var
a, b, s, t: integer;
tesprimo: boolean;
begin
(* carteles *)
writeln(’Encontrar los factores primos del entero a > 1’);
writeln;
(* datos de entrada *)
write(’Entrar a (a > 1): ’); readln(a); writeln;
(* fin *)
writeln; writeln(’** Fin **’)
end.
Pág. 170 Programas mencionados
var
dato, digito: integer;
terminaen: array[0..9] of integer;
begin
writeln(’** Contar cuantos de los numeros ingresados’);
writeln(’ terminan en 0,1,...,9’);
writeln;
(* inicializar el arreglo *)
for digito := 0 to 9 do terminaen[digito] := 0;
(* entrar datos *)
write(’Entrar un entero (fin = <retorno>): ’);
while (not eoln) do begin
readln(dato);
digito := abs(dato) mod 10;
terminaen[digito] := terminaen[digito] + 1;
write(’Entrar un entero (fin = <retorno>): ’)
end;
readln;
(* salida *)
writeln;
writeln(’ Digito numero de datos’);
for digito := 0 to 9 do
writeln( digito:4, terminaen[digito]:20);
var
busquedalineal (Problema 6.2) Pág. 171
i, ndatos, x: integer;
findatos, seencontro: boolean;
a: array[1..MAXN] of integer;
begin
(* carteles *)
writeln(’** Dado un arreglo a1, a2,...’);
writeln(’ ver si x = ai para algun i **’);
writeln;
(* leer el arreglo *)
write(’Entrar enteros, senialando fin’);
writeln(’ con <retorno> sin datos.’);
ndatos := 1; findatos := false;
repeat
write(’Entrar el dato ’, ndatos:1);
write(’ (fin = <retorno>): ’);
if (eoln) then begin (* no hay mas datos *)
findatos := true; readln end
else begin (* nuevo dato *)
readln(a[ndatos]); (* incorporarlo al arreglo *)
ndatos := ndatos + 1 (* para el proximo *)
end
until (findatos);
ndatos := ndatos - 1;
(* imprimir el arreglo *)
writeln(’El arreglo es: ’); writeln;
for i := 1 to ndatos do begin
write(a[i]:10);
if ((i mod 5) = 0) then writeln
end;
if ((ndatos mod 5) <> 0) then writeln;
writeln;
(* elemento a buscar *)
write(’Entrar el entero x a buscar: ’); readln(x);
writeln;
(* lazo principal *)
i := 0;
repeat
i := i + 1;
seencontro := (a[i] = x)
until (seencontro or (i = ndatos));
(* resultados y fin *)
if (seencontro)
then writeln(’Se encontro en la posicion ’, i:1)
else
Pág. 172 Programas mencionados
writeln(’No se encontro’);
var
p, i, cuenta, k: integer;
esprimo: array[2..MAXP] of boolean;
begin
writeln(’** Criba de Eratostenes para encontrar’);
writeln(’ los primos entre 1 y ’, MAXP:1);
writeln;
(* inicializacion *)
for i := 2 to MAXP do esprimo[i] := true;
cuenta := 0;
(* lazo principal *)
for i := 2 to MAXP do
if (esprimo[i]) then begin (* nuevo primo *)
p := i; cuenta := cuenta + 1;
(* ahora eliminar multiplos de p *)
k := p + p;
while (k <= MAXP) do begin
esprimo[k] := false;
k := k + p
end
end;
(* salida y fin *)
write(’ Se encontraron ’, cuenta:1);
writeln(’ primos que no superan ’, MAXP:2);
writeln; writeln(’** Fin **’)
end.
var
m, n, i, vuelta, cuenta: integer;
vive: array[1..MAXN] of boolean;
flavio: array[1..MAXN] of integer;
begin
(* carteles *)
writeln(’** Problema de Flavio Josefo **’); writeln;
writeln(’En un circulo de n, se van eliminando de a m’);
writeln;
(* datos *)
write(’Entrar n: ’); readln(n);
write(’Entrar m: ’); readln(m); writeln;
(* inicializacion *)
for i := 1 to n do vive[i] := true; (* aun vive *)
i := 0;
(* parte principal *)
for vuelta := 1 to n-1 do begin
(* en cada vuelta, i es el ultimo eliminado *)
cuenta := 0; (* contamos m sobrevivientes desde i *)
repeat
if (i <> n) then i := i + 1 else i := 1;
if (vive[i]) then cuenta := cuenta + 1
until (cuenta = m);
vive[i] := false; (* lo eliminamos *)
flavio[vuelta] := i
end;
(* encontrar el sobreviviente *)
i := 0; repeat i := i + 1 until vive[i];
flavio[n] := i;
(* salida y fin *)
writeln(’ La permutacion de Flavio Josefo es’); writeln;
for i := 1 to n do begin
write(flavio[i]:5);
if ((i mod 10) = 0) then writeln
end;
if ((n mod 10) <> 0) then writeln;
writeln; writeln(’** Fin **’)
end.
var
n, m, k: integer;
x, y, xn, ym: real;
begin
(* carteles iniciales *)
writeln(’** Comparar x a la n con y a la m’);
writeln;
(* entrar datos *)
write(’Entrar x (real): ’); readln(x);
write(’Entrar n (entero): ’); readln(n);
write(’Entrar y (real): ’); readln(y);
write(’Entrar m (entero): ’); readln(m);
(* calcular x a la n, y a la m *)
xn := 1; for k := 1 to n do xn := xn * x;
ym := 1; for k := 1 to m do ym := ym * y;
(* comparar e imprimir *)
write(x:10:5, ’ a la ’, n:1);
if (xn < ym) then write(’ es menor que ’)
else if (xn > ym) then write(’ es mayor que ’)
else write(’ es igual a ’);
writeln(y:10:5, ’ a la ’, m:1);
(* cartel final *)
writeln; writeln(’** Fin **’)
end.
var
n, m: integer;
x, y, xn, ym: real;
end;
begin
(* carteles iniciales *)
writeln(’** Comparar x a la n con y a la m’);
writeln;
(* entrar datos *)
write(’Entrar x (real): ’); readln(x);
write(’Entrar n (entero): ’); readln(n);
write(’Entrar y (real): ’); readln(y);
write(’Entrar m (entero): ’); readln(m);
(* calcular x a la n, y a la m *)
xn := potencia(x,n); ym := potencia(y,m);
(* comparar e imprimir *)
write(x:10:5, ’ a la ’, n:1);
if (xn < ym) then write(’ es menor que ’)
else if (xn > ym) then write(’ es mayor que ’)
else write(’ es igual a ’);
writeln(y:10:5, ’ a la ’, m:1);
(* cartel final *)
writeln; writeln(’** Fin **’)
end.
const
dify = 1.0e-6; (* tolerancia en y *)
maxiter = 30; (* maximo numero de iteraciones *)
var
iter: integer;
poco, mucho, x, (* valores en x *)
ypoco, ymucho, y (* correspondientes valores en y *)
: real;
seguir (* indica si seguir iterando *)
Pág. 176 Programas mencionados
: boolean;
begin
(* carteles *)
writeln(’* Metodo de biseccion para encontrar raices *’);
writeln;
write(’Implementacion para ’);
writeln(’f(x) = x (x + 1) (x + 2) (x - 4/3)’);
writeln;
(* datos de entrada *)
write(’ Entrar cota inferior para x: ’); readln(poco);
write(’ Entrar cota superior para x: ’); readln(mucho);
writeln;
(* inicializacion *)
ypoco := f(poco); ymucho := f(mucho);
seguir := (ypoco * ymucho < 0);
iter := 0;
(* lazo principal *)
while (seguir) do begin
iter := iter + 1;
x := (poco + mucho) / 2;
y := f(x);
if (abs(y) < dify) then seguir := false
else if (iter = maxiter) then seguir := false
else if (y * ypoco < 0) then mucho := x
else begin poco := x; ypoco := y end
end;
(* salida *)
if (iter > 0) then (* hay una solucion, aceptable o no *)
begin
writeln(’ Solucion obtenida: ’, x);
writeln(’ Valor de f: ’, y);
writeln(’ Iteraciones realizadas: ’, iter:1);
if (abs(y) > dify) then begin
writeln;
write(’* La respuesta puede no estar ’);
writeln(’cerca de una raiz *’)
end
end;
tablaseno2 (Problema 7.6) Pág. 177
(* fin *)
writeln; writeln(’** Fin **’)
end.
const pi = 3.14159265;
procedure cartelesiniciales;
begin
writeln(’Hacer una tabla del seno dando valores’);
writeln(’inicial, final, y del incremento (en grados).’);
writeln
end;
procedure leerdatos;
begin
write(’Entrar el valor inicial (en grados): ’);
readln(inicial);
write(’Entrar el valor final (en grados): ’);
readln(final);
write(’Entrar el valor de incremento (en grados): ’);
readln(incremento);
writeln
end;
procedure imprimirtabla;
var
grados: integer;
radianes: real;
begin
writeln(’Angulo Seno’);
grados := inicial;
while (grados <= final) do begin
radianes := grados * pi/180;
writeln(grados:5, sin(radianes):15:5);
grados := grados + incremento
end
end;
procedure cartelfinal;
begin writeln; writeln(’** Fin **’) end;
begin
Pág. 178 Programas mencionados
cartelesiniciales;
leerdatos;
imprimirtabla;
cartelfinal
end.
var a, b: integer;
begin
a := 5; b := 2;
writeln(’Inicialmente: a = ’, a:2, ’, b = ’, b:2);
swapincorrecto(a,b);
writeln(’Despues de swap: a = ’, a:2, ’, b = ’, b:2);
writeln; writeln(’** Fin **’)
end.
var
p, (* posicion en el renglon *)
n (* numero de caracteres en el renglon *)
: integer;
t (* el caracter de maximo ordinal *)
: char;
renglon (* el renglon a procesar *)
: tiporenglon;
n := 0;
while ((not eoln) and (n < MAXC)) do begin
n := n + 1; read(s[n]) (* aca es read y no readln *)
end;
readln (* para leer el ultimo fin de linea *)
end;
procedure max(
var t: tiporenglon;
long: integer;
var tmax: char;
var indice: integer);
var i: integer;
begin (* suponemos long positivo *)
tmax := t[1]; indice := 1;
for i := 2 to long do
if (t[i] > tmax) then begin
tmax := t[i]; indice := i end
end; (* fin de procedure max *)
begin
(* titulo *)
write(’** Programa para encontrar el maximo en un ’);
write(’arreglo de caracteres entrado por terminal’);
writeln;
(* entrada *)
leerrenglon(renglon, n);
(* verificacion *)
writeln; writeln(’El renglon entrado es: ’);
writeln;
escribirrenglon(renglon, n);
(* procesamiento *)
max(renglon, n, t, p);
(* salida y fin *)
writeln; write(’El maximo es "’, t, ’",’);
writeln(’ alcanzado en la posicion ’, p:1);
var
archivo: text;
c: char;
nombre: string;
begin
(* carteles *)
writeln(’** Copiar de consola a archivo **’);
writeln;
close(archivo);
(* Fin *)
writeln; writeln(’** Fin **’)
end.
var
archivo: text;
c: char;
nombre: string;
dado (Problema 9.1) Pág. 181
begin
(* carteles *)
writeln(’** Copiar de archivo a consola **’);
writeln;
close(archivo);
(* Fin *)
writeln; writeln(’** Fin **’)
end.
var d: integer;
begin
writeln(’** Simular tirar un dado **’);
writeln;
randomize;
d := 1 + random(6);
writeln(’El dado que salio es ’, d:1);
writeln;
writeln(’** <retorno> para Fin **’); readln
end.
Pág. 182 Programas mencionados
begin
writeln(’** Contar las veces que se tira un dado’);
writeln(’ hasta que aparece un numero prefijado **’);
writeln;
randomize;
tiros := 0;
repeat tiros := tiros + 1
until (1 + random(6) = numero);
writeln;
write(’El numero ’, numero:1);
writeln(’ tardo ’, tiros, ’ tiros hasta aparecer.’);
writeln;
writeln(’** <retorno> para Fin **’); readln
end.
const
MAXN = 100; (* maximo numero de datos a guardar *)
nada = 0; (* para cuando no hay hijos *)
arbolbinario (Problema 12.6) Pág. 183
type
indice = integer; (* para senialar los arreglos *)
tipodato = integer; (* el tipo de datos a ingresar *)
nodo = record
llave: tipodato; (* dato *)
cuenta: integer; (* veces que aparecio *)
izquierda: indice; (* hijo a la izquierda *)
derecha: indice (* hijo a la derecha *)
end;
tipoarbol = array[0..MAXN] of nodo;
var
narbol: integer; (* numero de nodos en el arbol *)
arbol: tipoarbol; (* el arbol! *)
raiz: indice; (* donde empieza el arbol *)
dato: tipodato; (* dato leido *)
begin
if (w <> nada) then
with arbol[w] do begin
enorden(izquierda);
writeln(llave:10, cuenta:20);
enorden(derecha)
end
end;
begin
(* carteles *)
writeln(’** Arbol binario **’); writeln;
write(’Entrar numeros enteros y contar’);
writeln(’ las veces que aparecieron’);
writeln;
(* inicializacion *)
narbol := 0;
raiz := nada;
(* salida y fin *)
writeln; writeln(’Impresion en orden:’);
writeln;
writeln(’ Numero Cantidad de Apariciones’);
enorden(raiz);
Ejemplo de Salida
** Arbol binario **
Impresion en orden:
3 1
** Fin **
const
MAXN = 20; (* maximo numero de nodos *)
MAXM = 100; (* maximo numero de aristas *)
type
arreglodevertices = array[1..MAXN] of integer;
tipoarista = record i, j: integer end;
arreglodearistas = array[1..MAXM] of tipoarista;
matrizNN = array[1..MAXN,1..MAXN] of integer;
var
ngrafo, mgrafo, narbol, marbol: integer;
padre, orden: arreglodevertices;
aristasgrafo, aristasarbol: arreglodearistas;
adyacencias: matrizNN;
procedure entrardatos;
begin
(* nodos *)
writeln;
write(’Entrar el numero de nodos: ’); readln(ngrafo);
(* aristas *)
writeln; writeln(’Entrar las aristas:’);
mgrafo := 1;
while (nuevodato) do mgrafo := mgrafo + 1;
mgrafo := mgrafo - 1
end;
procedure dearistasaadyacencias;
var i, j, k: integer;
begin
for i := 1 to ngrafo do
for j := 1 to ngrafo do
adyacencias[i,j] := 0;
for k := 1 to mgrafo do
with aristasgrafo[k] do begin
adyacencias[i,j] := 1; adyacencias[j,i] := 1
end
end;
procedure ancho;
var
i, j, ppo, fin: integer;
avisitar: arreglodevertices;
begin
(* inicializacion *)
dearistasaadyacencias;
anchoprimero (Problema 13.4) Pág. 187
procedure hacerarbol;
(* Construir el arbol generado usando padre *)
var k: integer;
begin
marbol := 0;
for k := 2 to ngrafo do (* 1 es "raiz" *)
if (padre[k] > 0) then begin
marbol := marbol + 1;
with aristasarbol[marbol] do begin
i := k; j := padre[k] end
end
end;
procedure escribirorden;
const maxrenglon = 10; (* maxima cantidad por renglon *)
var i: integer;
begin
for i := 1 to narbol do begin
write(orden[i]:5);
if ((i mod maxrenglon) = 0) then writeln
end;
if ((narbol mod maxrenglon) <> 0) then writeln
end;
Pág. 188 Programas mencionados
begin
(* Carteles *)
writeln(’** Recorrer un grafo a lo ancho para determinar’);
writeln(’ un arbol de expansion entrando las aristas.’);
writeln(’ La busqueda se hace en el orden de los enteros,’);
writeln(’ tomando como raiz a 1.’); writeln;
(* Datos e inicializacion *)
entrardatos;
writeln; writeln(’ Las aristas originales son:’);
escribiraristas( aristasgrafo, mgrafo);
(* Procesamiento y salida *)
ancho;
hacerarbol;
writeln; writeln(’ Las aristas del arbol resultante son:’);
escribiraristas(aristasarbol, marbol);
writeln;
writeln(’Los nodos se visitaron en el siguiente orden:’);
escribirorden;
if (narbol < ngrafo) then begin
writeln; writeln(’** El grafo no es conexo **’) end;
(* Fin *)
writeln; writeln(’** Fin **’)
end.
const
MAXN = 20; (* maximo numero de nodos *)
dijkstra (Problema 13.8) Pág. 189
type
costo = integer; (* podria ser real *)
arreglodevertices = array[1..MAXN] of integer;
tipoarista = record
i, j: integer; (* los extremos *)
w: costo (* el costo *)
end;
arreglodearistas = array[1..MAXM] of tipoarista;
matrizNN = array[1..MAXN,1..MAXN] of costo;
var
ngrafo, mgrafo, s, t: integer;
infinito: costo;
padre: arreglodevertices;
dist: array[1..MAXN] of costo;
aristasgrafo: arreglodearistas;
costos: matrizNN;
procedure entrardatos;
begin
(* nodos *)
writeln;
write(’Entrar el numero de nodos: ’);
readln(ngrafo);
(* aristas *)
writeln;
writeln(’Entrar las aristas:’);
mgrafo := 1; infinito := 1;
Pág. 190 Programas mencionados
procedure dearistasacostos;
var i, j, k: integer;
begin
for i := 1 to ngrafo do
for j := 1 to ngrafo do
costos[i,j] := infinito;
for k := 1 to mgrafo do
with aristasgrafo[k] do begin
costos[i,j] := w; costos[j,i] := w
end
end;
procedure mascorto;
var
i, j, k, kmin, hay: integer;
d, dmin: costo;
avisitar: arreglodevertices;
begin
(* inicializacion *)
dearistasacostos;
for i := 1 to ngrafo do begin
padre[i] := 0; dist[i] := infinito end;
(* examinar vecinos de i *)
for j := 1 to ngrafo do
if (dist[i] + costos[i,j] < dist[j]) then begin
(* si j no se agrego, agregarlo a la cola *)
if (dist[j] = infinito) then begin
hay := hay + 1; avisitar[hay] := j end;
(* actualizar dist y padre *)
dist[j] := dist[i] + costos[i,j];
padre[j] := i
end
end (* if i <> t *)
until ((i = t) or (hay = 0))
end;
procedure hacercamino;
(* Construir e imprimir el camino mas corto usando padre. *)
const maxrenglon = 10; (* maxima cantidad / renglon *)
var
i, ncamino: integer;
camino: arreglodevertices;
begin
ncamino := 1; camino[1] := t; i := t;
repeat
i := padre[i];
ncamino := ncamino + 1;
camino[ncamino] := i
until (i = s);
(* imprimir "dando vuelta" el arreglo *);
writeln(’ Los vertices en el camino son:’);
for i := ncamino downto 1 do begin
write(camino[i]:5);
if ((i mod maxrenglon) = 0) then writeln
end;
if ((ncamino mod maxrenglon) <> 0) then writeln
end;
begin
(* Carteles *)
writeln(’** Busqueda del camino mas corto entre dos’);
writeln(’ vertices con el algoritmo de Dijkstra.’);
writeln(’ El grafo se entra mediante lista de aristas.’);
Pág. 192 Programas mencionados
(* Datos e inicializacion *)
entrardatos;
writeln; writeln(’Las aristas del grafo son:’);
escribiraristas(aristasgrafo, mgrafo);
writeln; writeln(’Los nodos a unir son:’);
writeln(’ partida: ’, s:1);
writeln(’ llegada: ’, t:1);
(* Procesamiento y salida *)
mascorto;
writeln;
if (padre[t] > 0) then begin
writeln(’ La longitud del camino es ’, dist[t]:1);
hacercamino
end
else begin
writeln;
writeln(’*** ’, s:1, ’ y ’ , t:1, ’ no se conectan ***’)
end;
(* Fin *)
writeln; writeln(’** Fin **’)
end.
type
info = record
nombre: string;
id: integer
end;
listas (Problema 14.1) Pág. 193
ptrnodo = ^nodo;
nodo = record
info: info;
siguiente: ptrnodo
end;
var
nuevainfo: info;
listaadelante, listaordenada: ptrnodo;
begin
(* carteles *)
writeln(’** Prueba de listas encadenadas **’);
writeln;
(* inicializacion de listas *)
listaadelante := nil;
listaordenada := nil;
(* imprimir listas *)
writeln; writeln(’Las listas son:’); writeln;
(* Fin *)
writeln; writeln(’** Fin **’)
end.
Ejemplo de Salida
** Fin **
listas de vecinos.
Siempre n es dato.
type
ptrarista = ^arista;
arista = record u, v: integer; s: ptrarista end;
ptrvecino = ^vecino;
vecino = record id: integer; s: ptrvecino end;
ptrvertice = ^vertice;
vertice = record
id: integer;
s: ptrvertice;
vecinos: ptrvecino
end;
var
ngrafo: integer;
aristas: ptrarista;
vertices: ptrvertice;
(***************************************
Aristas
***************************************)
aristas := parista
end;
procedure entrararistas;
(* inicializar e ingresar aristas *)
var x, y: integer;
begin
aristas := nil;
while nuevaarista(x,y) do agregararista(x,y)
end;
procedure escribiraristas;
const maxrenglon = 10; (* maxima cantidad por renglon *)
var
k: integer;
p: ptrarista;
begin
if (aristas = nil) then writeln(’** lista vacia’)
else begin
p := aristas; k := 0;
repeat
k := k + 1;
with p^ do write(’ (’, u:1, ’,’, v:1, ’)’);
if ((k mod maxrenglon) = 0) then writeln;
p := p^.s
until (p = nil)
end;
if ((k mod maxrenglon) > 0) then writeln
end;
(***************************************
Vertices y sus vecinos
***************************************)
procedure inicializarvecinos;
var
k: integer;
p: ptrvertice;
begin
vertices := nil;
(* la construimos "al reves" para que quede ordenada *)
for k := ngrafo downto 1 do begin
new(p);
with p^ do begin
id := k; s := vertices; vecinos := nil end;
vertices := p
end (* for k *)
end;
procedure escribirvecinos;
var
pvertice: ptrvertice;
pvecino: ptrvecino;
begin
(* adyacencias *)
pvertice := vertices;
while (pvertice <> nil) do begin
write(pvertice^.id:5, ’ -> ’);
pvecino := pvertice^.vecinos;
if (pvecino = nil) then write(’ {}’)
else
repeat
write(pvecino^.id:3);
pvecino := pvecino^.s
until (pvecino = nil);
writeln;
pvertice := pvertice^.s
end
end;
(***************************************
De aristas a vertices
***************************************)
procedure dearistasaadyacencias;
(* construir listas de adyacencias
recorriendo la lista de aristas *)
var parista: ptrarista;
begin
parista := aristas;
while parista <> nil do begin
with parista^ do begin
agregarvecino(u, v);
agregarvecino(v, u)
grafos (Problema 14.9) Pág. 199
end;
(* proxima arista *)
parista := parista^.s
end
end;
(**************************************
Parte principal
**************************************)
begin
(* carteles *)
writeln(’** Pasar de aristas a adyacencias usando’);
writeln(’ listas encadenadas.’); writeln;
(* aristas *)
entrararistas;
(* imprimir resultados *)
writeln; writeln(’** Resultados **’); writeln;
writeln(’ Lista de aristas:’);
escribiraristas; writeln;
writeln(’ Listas de vecinos:’);
escribirvecinos;
(* fin *)
writeln; writeln(’** Fin **’)
end.
Apéndice B
(∗)
odd (x) es verdadero si x es impar.
(†)
round (x) = trunc(x + 0.5).
(
(‡) bxc si x ≥ 0,
trunc(x ) =
dxe si x < 0.
Otras funciones
eof eoln ord pred succ
Procedimientos
dispose get new pack page put
read readln reset rewrite unpack write
writeln
Algunas notaciones y
sı́mbolos usados
C.1. Lógica
√
⇒ implica o entonces. x > 0 ⇒ x = x2 puede leerse como “si x es
positivo, entonces. . . ”.
⇔ si y sólo si. Significa que las condiciones a ambos lados son equivalentes.
Por ejemplo x ≥ 0 ⇔ |x| = x se lee “x es positivo si y sólo si. . . ”.
C.2. Conjuntos
∈ pertenece. x ∈ A significa que x es un elemento de A.
\ diferencia de conjuntos, A \ B = {x : x ∈ A y x ∈
/ B}.
Pág. 204 Algunas notaciones y sı́mbolos usados
| |,
# cardinal, |A|, o a veces #(A), es la cantidad de elementos en el conjunto
A.
∅ El conjunto vacı́o, #(∅) = 0.
C.3. Números
N El conjunto de números naturales, N = {1, 2, 3, . . . }. Observar que para
nosotros 0 ∈
/ N.
Z Los enteros, Z = {0, 1, −1, 2, −2, . . . }.
Q Los racionales p/q, donde p, q ∈ Z, q 6= 0.
√
R Los reales. Son todos los racionales más números como 2, π, etc., que
no tienen una expresión decimal periódica.
R+ Los reales positivos, R+ = {x ∈ R : x > 0}.
C √ z = a + bi, con a, b ∈ R y donde i es la
Los complejos, de la forma
unidad imaginaria, “ i = −1 ”.
| Para m, n ∈ Z, m | n se lee m divide a n, o n es múltiplo de m, y significa
que existe k ∈ Z tal que n = km.
≈ aproximadamente. x ≈ y se lee “x es aproximadamente igual a y”.
mucho menor. x y se lee “x es mucho menor que y”.
mucho mayor. x y se lee “x es mucho mayor que y”.
|x| √ absoluto o módulo del número x. Si z = a+ bi ∈ C con a, b ∈ R,
El valor
|z| = a2 + b2 .
bxc El piso de x, x ∈ R. Es el mayor entero que no supera a x, por lo que
bxc ≤ x < bxc + 1: bπc = 3, b−πc = −4, bzc = z ∀ z ∈ Z.
[x] La parte entera de x, x ∈ R. [x] = bxc. Nosotros usaremos la notación
bxc, siguiendo la costumbre en las áreas relacionadas con la computa-
ción.
dxe El techo de x, x ∈ R. Es el primer entero que no es menor que x, por
lo que dxe − 1 < x ≤ dxe: dπe = 4, d−πe = −3, dze = z ∀ z ∈ Z.
ex ,
exp(x) La función exponencial de base e = 2.718281828459 . . .
logb x El logaritmo de x ∈ R, x > 0, en base b. y = logb x ⇔ by = x.
ln x El logaritmo natural de x ∈ R, x > 0, o logaritmo en base e, ln x =
loge x. Es la inversa de la exponencial, y = ln x ⇔ ey = x.
Nota: Para no confundir con los logaritmos en base 10, evitaremos la
notación log x: si nos referimos a la base e usaremos ln x, y en otras
bases logb x.
C.4. Números importantes en programación Pág. 205
sen x,
sin x Las función trigonométrica seno, definida para x ∈ R.
C.5. Generales
i.e. es decir o esto es, del latı́n “id est”.
e.g. por ejemplo, del latı́n “exempli gratia”.
Apéndice D
glés).
7 http://www.pascal-central.com/ (la página está en inglés).
Pág. 208 Sobre los compiladores Pascal