Académique Documents
Professionnel Documents
Culture Documents
Andr es Marzal
Mara Jos e Castro
Pablo Aibar
Borrador
28 de septiembre de 2009
a
l
o
m o
r
s
g
i t
I
Ingeniera Informatica
II24 Algortmica
2008/2009Universitat Jaume I de Castell o
Apuntes de Algortmica
Andres Marzal, Mara Jose Castro, Pablo Aibar
Temas 1 a 5 y apendices
INDICE GENERAL
Indice general I
1 Introducci on 1
1.1. Un ejemplo ilustrativo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1
1.2. Algoritmia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6
1.2.1 Problema, instancias, soluciones y restricciones, 6. 1.2.2 Algoritmo, 8.
1.3 El ciclo de desarrollo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3.1 Especicaci on, 10. 1.3.2 Dise no, 12. 1.3.3 Vericaci on, 14. 1.3.4 An ali-
sis, 14. 1.3.5 Codicaci on, 15. 1.3.6 Evaluaci on, 16.
1.4 Dise no de algoritmos basado en esquemas algortmicos . . . . . . . . . . . 16
2 An alisis de algoritmos 19
2.1 Medici on de tiempo de ejecuci on . . . . . . . . . . . . . . . . . . . . . . . . 20
2.2 Talla de una instancia . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
2.3 Perl de ejecuci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 27
2.4 Conteo de pasos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 35
2.4.1 Conteo de pasos en algoritmos iterativos, 35. 2.4.2 Conteo de pasos en
algoritmos recursivos, 38.
2.5 Expresi on del coste temporal de los algoritmos con notaci on asint otica . . 39
2.5.1 Mejor y peor casos, 39. 2.5.2 Coste promedio, 42. 2.5.3 Coste amortizado, 43.
2.5.4 Una comparaci on de costes, 47. 2.5.5 Una ultima consideraci on: conteo de
operaciones muy relevantes, 50.
2.6 Coste espacial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51
2.7 Complejidad de problemas . . . . . . . . . . . . . . . . . . . . . . . . . . . 53
2.7.1 Problemas difciles, 53. 2.7.2 Problemas presumiblemente difciles, 54.
2.7.3 Problemas irresolubles, 55.
3 Algunas estructuras de datos 57
3.1 Clases abstractas b asicas para colecciones . . . . . . . . . . . . . . . . . . . 57
3.2 Secuencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 59
I
II Apuntes de Algoritmia 28 de septiembre de 2009
3.2.1 Vector, 60. 3.2.2 Lista enlazada, 63.
3.3 Colas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66
3.3.1 Cola FIFO, 67. 3.3.2 Pila o cola LIFO, 69.
3.4 Colecciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.5 Vectores asociativos, mapeos o diccionarios . . . . . . . . . . . . . . . . . . 73
3.5.1 Tabla de dispersion, 74. 3.5.2 Vectores asociativos con claves enteras en un
rango, 78.
3.6 Conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 79
3.6.1 Mediante vectores, 81. 3.6.2 Mediante listas enlazadas, 82. 3.6.3 Mediante
tablas de dispersi on, 82. 3.6.4 Mediante arboles equilibrados, 84. 3.6.5 Mediante
vectores caractersticos, 84.
3.7 Digrafos y grafos no dirigidos . . . . . . . . . . . . . . . . . . . . . . . . . . 85
3.7.1 Sobre una implementaci on naf de los digrafos, 87. 3.7.2 Digrafos represen-
tados con mapeo a conjuntos de adyacencia, 88. 3.7.3 Digrafos implementados
con matriz de adyacencia, 92. 3.7.4 Digrafos con tabla de dispersi on en la co-
rrespondencia de v ertices a conjuntos de sucesores. . . , 93. 3.7.5 Digrafos con
conjuntos de adyacencia con inversa, 96. 3.7.6 Un estudio comparativo y algunas
conclusiones, 97. 3.7.7 Digrafo ponderado, 101.
3.8
Arbol . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103
3.9
Arbol con raz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 104
3.9.1 Implementaci on basada en grafos, 105. 3.9.2 Implementaci on basada
en lista de listas, 105. 3.9.3 Implementaci on con referencias a padres, 107.
3.9.4 Implementaci on vectorial de arboles con raz y aridad acotada, 108.
3.9.5 Recorrido de arboles con raz, 111.
3.10 Cola de prioridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118
3.10.1 Montculos, 119. 3.10.2 Una aplicaci on: el m etodo de ordenaci on heap-
sort, 127.
3.11 Cola de prioridad con dos extremos . . . . . . . . . . . . . . . . . . . . . . 127
3.11.1 Montculo de intervalos, 128. 3.11.2 Una aplicaci on: rango de los k mejores
elementos de un serie potencialmente innita, 135.
3.12 Diccionario de prioridad . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136
3.12.1 Diccionario de prioridad basado en montculo, 137. 3.12.2 Diccionario de
prioridad basado en montculo de Fibonacci, 142.
3.13 Diccionarios de prioridad con dos extremos . . . . . . . . . . . . . . . . . . 150
3.14 Conjuntos disjuntos (particiones): MFSET . . . . . . . . . . . . . . . . . . . 157
3.14.1 Una aplicaci on: componentes conexos en una imagen, 158. 3.14.2 Estruc-
tura de datos, 159. 3.14.3 Bosque de conjuntos disjuntos, 160. 3.14.4 Heursticos
para la reducci on del coste, 162.
4 Algoritmos sobre grafos 171
4.1 Exploraci on de grafos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 171
4.1.1 Exploraci on de grafos por primero en anchura, 172. 4.1.2 Exploraci on de
grafos por primero en profundidad, 177.
4.2 Componentes conexos en grafos no dirigidos . . . . . . . . . . . . . . . . . 184
28 de septiembre de 2009 Captulo -1. III
4.3
Arboles de recubrimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . 186
4.3.1
Arbol de recubrimiento a partir del recorrido del grafo, 188. 4.3.2
Arbol de
recubrimiento de coste mnimo, 190.
4.4 Componentes fuertemente conexos . . . . . . . . . . . . . . . . . . . . . . . 193
4.5 Ordenaci on topol ogica de un digrafo acclico . . . . . . . . . . . . . . . . . 196
4.6 Clausura transitiva de un digrafo . . . . . . . . . . . . . . . . . . . . . . . . 199
4.6.1 Cierre transitivo de una matriz booleana, 200. 4.6.2 M etodo basado en la
detecci on de componentes fuertemente conexos, 206.
4.7 Camino con menor n umero de aristas . . . . . . . . . . . . . . . . . . . . . 206
4.7.1 El camino de menor longitud entre dos v ertices, 208. 4.7.2 Los caminos de
menor longitud de un v ertice a cualquier otro, 212. 4.7.3 Los caminos de menor
longitud desde cualquier v ertice de un conjunto a todos los dem as, 213. 4.7.4 El
camino de menor longitud entre dos conjuntos de v ertices, 215.
4.8 Caminos m as cortos en digrafos ponderados . . . . . . . . . . . . . . . . . 217
4.8.1 Camino m as corto en un digrafo ponderado acclico, 219. 4.8.2 Camino m as
corto formado por k aristas, 227. 4.8.3 Camino m as corto en un digrafo ponderado
cualquiera, 231. 4.8.4 Camino m as corto en un digrafo ponderado positivo, 235.
4.9 El camino m as corto en un digrafo ponderado eucldeo . . . . . . . . . . . 243
4.9.1 Sobre la complejidad computacional, 246.
4.10 Caminos m as cortos entre todo par de v ertices de un digrafo . . . . . . . . 247
4.11 Algunas consideraciones sobre los algoritmos para el c alculo del camino
m as corto . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 252
5 Divide y vencer as 255
5.1 Ordenaci on por fusi on . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 256
5.1.1 Una primera soluci on basada en divide y vencer as, 256. 5.1.2 Correcci on
del algoritmo, 259. 5.1.3 Complejidad computacional de la ordenaci on por
fusi on, 259. 5.1.4 Un renamiento: ordenaci on in situ y reducci on del consumo
de memoria, 262.
5.2 La estrategia divide y vencer as . . . . . . . . . . . . . . . . . . . . . . . . 265
5.2.1 El esquema algortmico, 265. 5.2.2 Correcci on, 268. 5.2.3 Complejidad
temporal, 268. 5.2.4 Complejidad espacial, 272. 5.2.5 Algunas consideraciones
sobre la eciencia, 272. 5.2.6 Algunas consideraciones sobre el esquema y su uso
en el dise no de un algoritmo, 274.
5.3 Un caso especial: reduce y vencer as . . . . . . . . . . . . . . . . . . . . . 275
5.3.1 Coste computacional, 276. 5.3.2 Eliminaci on de la recursi on por cola, 276.
5.4 B usqueda binaria o dicot omica . . . . . . . . . . . . . . . . . . . . . . . . . 277
5.4.1 Una versi on con umbral de recursi on, 280. 5.4.2 Una versi on iterativa de la
b usqueda dicot omica, 280.
5.5 Potencia entera de un n umero . . . . . . . . . . . . . . . . . . . . . . . . . . 281
5.6 Mnimo y m aximo de un vector . . . . . . . . . . . . . . . . . . . . . . . . . 283
5.7 Mnimo de un vector convexo . . . . . . . . . . . . . . . . . . . . . . . . . . 285
5.8 El algoritmo de ordenaci on quicksort . . . . . . . . . . . . . . . . . . . . . . 288
5.8.1 Ordenaci on in situ, 291. 5.8.2 Complejidad computacional, 292. 5.8.3 Alea-
IV Apuntes de Algoritmia 28 de septiembre de 2009
torizaci on de quicksort, 296. 5.8.4 Otras t ecnicas de selecci on del pivote, 297.
5.8.5 Eliminaci on de recursi on por cola, 298. 5.8.6 La eciencia de quicksort en la
pr actica, 299.
5.9 Selecci on del k- esimo menor elemento . . . . . . . . . . . . . . . . . . . . . 303
5.10 El par de puntos m as pr oximos . . . . . . . . . . . . . . . . . . . . . . . . . 307
5.11 La envolvente convexa de un conjunto de puntos en el plano . . . . . . . . 314
5.11.1 El algoritmo QuickHull, 314. 5.11.2 Complejidad computacional, 318.
5.12 Producto de enteros grandes . . . . . . . . . . . . . . . . . . . . . . . . . . . 319
5.13 Producto de matrices cuadradas . . . . . . . . . . . . . . . . . . . . . . . . 323
5.14 La transformada r apida de Fourier . . . . . . . . . . . . . . . . . . . . . . . 326
5.14.1 Algunos conceptos previos, 326. 5.14.2 Un algoritmo divide y ven-
cer as, 328. 5.14.3 Una versi on in situ iterativa, 330.
A Python 337
A.1 Formato . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 337
A.2 Salida por pantalla . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
A.3 Variables . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 339
A.4 Tipos b asicos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 340
A.4.1 N umeros, 340. A.4.2 Otros escalares, 341. A.4.3 Colecciones, 341.
A.4.4 Coerci on, 347.
A.5 Expresiones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 348
A.5.1 Operadores, 348. A.5.2 Sobre la asignaci on, 349.
A.6 Estructuras de control . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 350
A.6.1 De selecci on, 350. A.6.2 De repetici on (o bucle), 350. A.6.3 Excepcio-
nes, 351.
A.7 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
A.7.1 Funciones con nombre, 353. A.7.2 Algunas funciones predenidas, 357.
A.7.3 Funciones an onimas, 358.
A.8 Clases . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 358
A.8.1 Denici on de clases e instanciaci on de objetos, 358. A.8.2 Algunos m etodos
especiales, 359. A.8.3 Herencia, 361. A.8.4 M etodos y campos privados y p ubli-
cos, 362. A.8.5 Creaci on de alias para m etodos, 362. A.8.6 M etodos est aticos, 363.
A.8.7 Clases con consumo de memoria ajustado, 363. A.8.8 Propiedades, 363.
A.9 M odulos y paquetes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 364
A.9.1 M odulos, 364. A.9.2 Paquetes, 366.
A.10 Interfaces (clases abstractas) . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
A.11 Tuplas con campos con nombre . . . . . . . . . . . . . . . . . . . . . . . . . 367
A.12 Iteradores y generadores . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 367
A.12.1 Algunas funciones auxiliares para iterables, 370.
A.13 Ficheros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 373
A.14 Inyecci on de dependencias mediante factoras . . . . . . . . . . . . . . . . 374
B Conceptos matem aticos 379
B.1 C alculo proposicional . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
28 de septiembre de 2009 Captulo -1. V
B.2 Conjuntos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
B.3 Relaciones binarias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
B.4 Funciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
B.5 Estructuras algebraicas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
B.5.1 Monoide, 384. B.5.2 Monoide conmutativo, 384. B.5.3 Semianillo, 384.
B.6 M etodos de demostraci on . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
B.6.1 Inducci on matem atica, 385. B.6.2 Demostraci on por contradicci on, 388.
B.6.3 Demostraci on por contraejemplo, 388.
B.7 Las funciones enteras techo y suelo . . . . . . . . . . . . . . . . . . . . . . . 388
B.8 Potencias y logaritmos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 389
B.9 Sucesiones y sumatorios . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
B.9.1 Sucesiones, 390. B.9.2 Sumatorios, 390. B.9.3 Sumatorio de una progresi on
aritm etica, 391. B.9.4 Sumatorio de una progresi on geom etrica, 391. B.9.5 Algu-
nos sumatorios de inter es, 392. B.9.6 Los n umeros de Fibonacci, 393. B.9.7 Los
n umeros arm onicos, 393.
B.10 Productorios y factorial . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
B.11 Permutaciones . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 394
B.12 N umeros complejos . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395
B.12.1 Notaci on: formas bin omica y polar, 395. B.12.2 Producto y exponenciaci on
de complejos en forma polar, 396. B.12.3 Races de la unidad, 397.
B.13 Notaci on asint otica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 397
B.13.1 Cota asint otica superior: notaci on orden, 397. B.13.2 Cota asint otica
inferior: notaci on omega, 400. B.13.3 Cota asint otica superior e inferior:
notaci on zeta, 401.
B.14 Recurrencias . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 402
B.14.1 El m etodo del desplegado, 402. B.14.2 La demostraci on por inducci on, 404.
B.14.3 Los n umeros de Catalan, 405.
B.15 Digrafos y grafos (no dirigidos) . . . . . . . . . . . . . . . . . . . . . . . . . 406
B.15.1 Grafos dirigidos o digrafos, 406. B.15.2 Grafos no dirigidos, 408.
B.15.3 Grafos etiquetados y grafos ponderados, 409. B.15.4 Caminos, 410.
B.15.5 Subgrafos, 413. B.15.6 Conectividad, 414. B.15.7 Densidad, 414.
B.15.8 Multigrafos, 415. B.15.9 Algunos tipos especiales de grafo, 415.
B.16 Teora de lenguajes formales . . . . . . . . . . . . . . . . . . . . . . . . . . . 420
B.16.1 Gram aticas, 420. B.16.2 An alisis, 421.
Captulo 1
INTRODUCCI
ON
Presentemos el campo de estudio del que nos ocupamos con un ejemplo cotidiano: la
b usqueda de la denici on de una palabra en un diccionario. Nos servir a para introducir
intuitivamente el concepto de algoritmo y el estudio de la eciencia computacional.
1.1. Un ejemplo ilustrativo
Seguro que el lector se habr a enfrentado en numerosas ocasiones al problema de buscar
una palabra en un diccionario. Especiquemos bien el problema: dada una palabra y un
diccionario que contiene su denici on, deseamos conocer esta. Un m etodo sencillo para
resolver este problema puede describirse as:
1. Poner el dedo en la primera voz del diccionario.
2. Repetir estos pasos:
a) Si el vocablo se nalado y el buscado coinciden, anotar su denici on.
b) Si no era la ultima palabra del diccionario, mover el dedo para que se nale al siguiente
vocablo. Si era la ultima, pasar al paso 3.
3. Proporcionar como respuesta la denici on anotada.
Llamaremos a este m etodo b usqueda secuencial naf, pues tiene un punto absurdo:
sigue recorriendo la lista de palabras del diccionario aun cuando ya ha encontrado la
palabra buscada. Cu anto tardaremos en proporcionar una respuesta si aplicamos este
procedimiento? Depende, claro est a, de lo r apidos que seamos ejecutando cada uno de
los pasos. Solucionemos esta cuesti on midiendo el tiempo en funci on de una operaci on
especial: la comparaci on y movimiento de dedo, que denotaremos con las siglas CMD.
1
2 Apuntes de Algoritmia 28 de septiembre de 2009
Estudiemos primero el n umero de CMD necesarias para encontrar la palabra y luego
nos ocuparemos del tiempo que tardamos en aplicar el procedimiento multiplicando es-
ta cantidad por el tiempo necesario para realizar una CMD. De qu e depende el n umero
de CMD necesarias para encontrar el vocablo? Obviamente del n umero de palabras exis-
tentes en el diccionario: si un diccionario tiene 1 000 palabras y otro tiene 2 000, buscar
un vocablo en el segundo supone efectuar el doble de CMD que hacerlo en el primero.
El n umero de voces del diccionario guarda relaci on con la dicultad que supone efectuar
una b usqueda en el: es la talla del problema y el n umero de CMD para resolver el proble-
ma es una funci on f
0
: N R
0
. Como el m etodo nos obliga a recorrer completamente
el diccionario, est a claro que necesitamos efectuar n CMD para buscar una palabra en un
diccionario con n palabras, o sea, f
0
(n) = n.
El m etodo de b usqueda propuesto es maniestamente mejorable, ya que nos hace
seguir buscando aun cuando hayamos encontrado el vocablo. He aqu un m etodo de
b usqueda renado al que denominaremos b usqueda secuencial:
1. Poner el dedo en la primera voz del diccionario.
2. Repetir estos pasos:
a) Si el vocablo se nalado y el buscado coinciden, devolver su denici on y nalizar.
b) En otro caso, avanzar el dedo para que se nale al siguiente vocablo.
Estudiemos el n umero de CMD que hemos de efectuar al ejecutar el nuevo procedi-
miento. Lo primero que llama la atenci on es que dicho n umero no depende unicamente de
la talla del problema: vara adem as con la ubicaci on del vocablo buscado en el dicciona-
rio. El n umero de CMD que hemos de ejecutar para problemas de una determinada talla
no es una funci on de la talla, pues a una misma talla pueden corresponder distintos valo-
res. Cuando estudiemos el coste de m etodos como este deberemos considerar diferentes
casos:
El n umero de CMD necesarias en el mejor de los casos. Para este problema el mejor
caso es aquel en el que el vocablo buscado ocupa la primera posici on. Entonces una
CMD basta para resolver el problema, es decir, el n umero de CMD en el mejor de
los casos viene dado por la funci on f
1
(n) = 1.
El n umero de CMD necesarias en el peor de los casos, que es el que se da cuando
buscamos la ultima palabra del diccionario. La funci on que proporciona el n umero
de CMD en funci on de n para el peor de los casos es f
2
(n) = n, el mismo n umero
de CMD que requiere, en cualquier caso, la b usqueda secuencial naf.
El n umero de CMD necesarias en promedio. En nuestro caso, estimar el n umero de
CMDen promedio requiere conocer la probabilidad de que busquemos cada una de
la palabras del diccionario. Supongamos que existe la misma probabilidad de que
busquemos cualquiera de ellas, es decir, que la probabilidad de que busquemos una
voz cualquiera sea 1/n. Cuando buscamos la que aparece en la posici on i- esima
28 de septiembre de 2009 Captulo 1. Introducci on 3
efectuamos i CMD. El n umero de CMD promedio se puede calcular sumando el
n umero de CMD que cuesta encontrar cada palabra por la probabilidad de que
busquemos esa palabra en particular:
f
3
(n) =
n
i=1
1
n
i =
1
n
n
i=1
i =
1
n
n(n +1)
2
=
n +1
2
.
As pues, efectuar la b usqueda en un diccionario con 1 000 palabras requerira un
promedio de 500.5 CMD, cerca de la mitad de lo que costaba efectuar una b usque-
da con el m etodo anterior. Y buscar en un diccionario con 2 000 palabras requiere,
en promedio, 1 000.5 CMD, aproximadamente el doble de CMD esperadas que
hacerlo en uno con s olo 1 000.
El mejor de los casos describe la situaci on id onea para cualquier valor dado
de n. Considerar que el mejor de los casos se da cuando n vale 0 es un error
conceptual grave. Si te propones encontrar el mejor caso, piensa: Dado un valor
jo de n, qu e datos de entrada de esa talla me lo ponen m as f acil?.
Cabe esperar, pues, que el segundo m etodo de b usqueda sea el doble de r apido que
el primero y, en el peor caso, se comporta igual que el primero. Podemos concluir que el
segundo m etodo es mejor que el primero. Pero, es el mejor m etodo de b usqueda posible?
No. Hay uno mejor que aprovecha de una forma ingeniosa el orden alfab etico con que
aparecen las palabras en un diccionario:
1. Repetir mientras el (trozo de) diccionario contenga una o m as palabras:
a) Poner el dedo en la palabra que se encuentra justo en la mitad de (lo que queda de) el
diccionario.
b) Comparar la palabra buscada con la se nalada:
b.1) Si coinciden, ha acabado la b usqueda y debe proporcionarse como respuesta la
denici on asociada.
b.2) Si no coinciden y el vocablo buscado debe ocupar una posici on anterior al se nalado,
romper el diccionario en dos mitades y descartar la segunda parte (que incluye el
vocablo reci en comparado).
b.3) Y si no coinciden pero el vocablo buscado debe ir detr as del se nalado, romper el
diccionario en dos mitades y descartar la primera parte (incluyendo el vocablo reci en
comparado).
Este m etodo se denomina b usqueda binaria porque en cada paso se divide el proble-
ma en dos problemas que son la mitad de difciles. (Tambi en se conoce por b usqueda
logartmica, b usqueda dicot omica, m etodo de la bisecci on y m etodo de la bipartici on.)
No se basa en la operaci on comparaci on y movimiento de dedo, as que hemos de
cambiar de unidad elemental de medida de tiempo. Usaremos ahora la comparaci on y
4 Apuntes de Algoritmia 28 de septiembre de 2009
rotura de diccionario (CRD). En el mejor de los casos una sola CRD basta para encontrar
la palabra buscada. Esta situaci on se da cuando la palabra buscada ocupa la posici on que
est a justo en mitad del diccionario. El peor de los casos es m as difcil de analizar. Supon-
gamos que hay n palabras en el diccionario. Si no encontramos la que buscamos con la
primera CRD, tendremos que buscar en una de las dos mitades del diccionario, es decir,
en un nuevo diccionario con menos palabras. Con cu antas? Si n es impar, con n/2,
y si n es par, con n/2 1 o n/2. Es decir, la mitad mayor tiene n/2 palabras. Si fallamos
nuevamente, la b usqueda se restringir a a, como mucho, n/4 palabras, y as sucesiva-
mente. Lo peor que nos puede pasar es que encontremos el vocablo buscado cuando lo
que queda del diccionario s olo contiene una palabra. Cu antas CRD habremos efectua-
do? No m as de 1 +lg n CRD. Eso signica, por ejemplo, que buscar una palabra entre
2 000 s olo puede requerir una CRD m as que buscarla entre 1 000!
En la tabla 1.1 se puede comparar el n umero de CRD de este m etodo y el de CMD de
la b usqueda secuencial para sus respectivos peores casos y valores crecientes de n.
Tabla 1.1: N umero de CMD y CRD en el peor de los
casos al efectuar la b usqueda secuencial y la b usqueda
binaria para diferentes valores de n.
n B usqueda secuencial B usqueda binaria
2 2 CMD 2 CRD
4 4 CMD 3 CRD
8 8 CMD 4 CRD
16 16 CMD 5 CRD
32 32 CMD 6 CRD
64 64 CMD 7 CRD
128 128 CMD 8 CRD
256 256 CMD 9 CRD
512 512 CMD 10 CRD
Ajuzgar por la tabla, el m etodo de b usqueda binaria es m as eciente que el m etodo de
b usqueda secuencial. Se podra objetar que llevar a cabo una CRD requiere m as tiempo
que una CMD y que este mayor tiempo podra suponer que, en el fondo, el segundo
procedimiento fuera m as lento. Esto es posible para valores peque nos de n, pero no para
valores de n sucientemente grandes. Ve amoslo con un ejemplo. La tabla 1.2 muestra el
tiempo necesario para buscar con cada m etodo en el peor de los casos suponiendo que
una CMD cuesta medio segundo y una CRD cuesta dos segundos.
Tabla 1.2: Tiempo necesario en el peor de los casos para
efectuar las b usquedas secuencial y binaria en funci on
de n, suponiendo que una CMD y una CRD requieren
medio segundo y dos segundos respectivamente.
n B usqueda secuencial B usqueda binaria
2 1 s 4 s
4 2 s 6 s
8 4 s 8 s
16 8 s 10 s
32 16 s 12 s
64 32 s 14 s
128 64 s 16 s
256 128 s 18 s
512 256 s 20 s
28 de septiembre de 2009 Captulo 1. Introducci on 5
A partir de 32 palabras es m as r apido el segundo m etodo. Y si una CRD costara 12
segundos? La tabla 1.3 muestra el tiempo necesario para ejecutar cada procedimiento en
este supuesto. En este caso necesitamos un diccionario con al menos 256 palabras para
que el m etodo de b usqueda binaria sea m as r apido que el de b usqueda secuencial. Pero
para un diccionario de esa talla y para cualquier otro de talla superior, el m etodo bina-
rio es m as eciente. Y mucho m as eciente cuanto mayor sea el diccionario. No importa
cu anto m as lenta sea una CRD que una CMD: para un n umero de palabras suciente-
mente grande el m etodo binario siempre acaba por ser mejor.
n B usqueda secuencial B usqueda binaria
2 1 s 24 s
4 2 s 36 s
8 4 s 48 s
16 8 s 60 s
32 16 s 72 s
64 32 s 84 s
128 64 s 96 s
256 128 s 108 s
512 256 s 120 s
1 024 512 s 132 s
2 048 1 024 s 144 s
Tabla 1.3: Tiempo necesario en el peor de los ca-
sos para efectuar las b usquedas secuencial y bina-
ria en funci on de n, suponiendo que una CMD y
una CRD requieren medio segundo y doce segundos
respectivamente.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1 Queremos averiguar qui en es la persona m as joven en una la de n personas, donde n > 0.
Tenemos dos m etodos para averiguarlo y queremos saber cu al es m as eciente. El primer m etodo
consiste en:
1. Tomamos al primero de la la como candidato a persona m as joven.
2. Preguntamos al candidato por su fecha de nacimiento y nombre y apuntamos estos datos en una
hoja de papel. A continuaci on, preguntamos a todos los dem as, de uno en uno, si nacieron antes
o despu es de la fecha que les decimos en voz alta.
3. Si todos nos dijeron antes, hemos acabado y ya sabemos qui en es la persona m as joven: su
nombre est a apuntado en la hoja de papel. Si no, consideramos candidato a la persona que sigue
en la la al actual candidato y volvemos al paso 2.
Y el segundo m etodo consiste en:
1. Preguntamos al primero por su fecha de nacimiento y nombre y apuntamos ambos datos en una
hoja de papel.
2. Si no hay nadie m as en la la, vamos al paso 3. Si queda gente, pasamos al siguiente y le pre-
guntamos por su fecha de nacimiento y nombre. Si nos da una fecha posterior a la que tenemos
apuntada, borramos los datos de la hoja de papel y escribimos los que nos acaban de dar. Repe-
timos el paso 2.
3. La persona m as joven es aquella cuyo nombre gura en la hoja de papel.
Cu ales son los casos mejor y peor para cada m etodo? De qu e operaciones b asicas depende cada
algoritmo? Cu al de los dos m etodos es m as eciente en el peor caso? Y en caso promedio (supo-
niendo que no hay relaci on alguna entre la posici on ocupada en la la y la fecha de nacimiento)?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6 Apuntes de Algoritmia 28 de septiembre de 2009
1.2. Algoritmia
El anterior ejemplo ilustra el campo de estudio del que se ocupa este texto: del dise no
de algoritmos para la resoluci on de problemas y del an alisis de su eciencia, es decir, de
la algoritmia. Todos tenemos una idea intuitiva de lo que entendemos por problema
y algoritmo, pero conviene que denamos con cierta precisi on estos conceptos y otros
relacionados con ellos.
1.2.1. Problema, instancias, soluciones y restricciones
Un problema es una pregunta general que puede poseer varios par ametros o variables
libres cuyos valores no est an especicados. Por ejemplo, el enunciado buscar la de-
nici on asociada a una palabra en un diccionario describe un problema. N otese que la
palabra buscada y el diccionario en el que efectuar la b usqueda son par ametros sin un
valor asignado a priori.
Hay una familia de problemas que nos interesar a especialmente: los denominados
problemas de optimizaci on. Un problema de optimizaci on propone la b usqueda del
valor optimo (m aximo o mnimo) de cierta funci on objetivo f denida en un dominio
X determinado:
v = opt
xX
f (x).
(Por regla general, el rango de la funci on f son los reales, aunque basta con que sea un
conjunto dotado de un orden total.) Los problemas de optimizaci on en los que buscamos
un valor m aximo se denominan problemas de maximizaci on, y aquellos en los que busca-
mos un valor mnimo, problemas de minimizaci on. Muchos problemas de optimizaci on
se formulan con una variante: se propone buscar un elemento x X que proporciona el
valor optimo de f (), es decir,
x = arg opt
xX
f (x).
El elemento x (o elementos, pues puede haber m as de uno) recibe el nombre de soluci on
optima, y X suele recibir el nombre de conjunto de soluciones factibles. N otese que re-
solver este tipo de problema implica resolver el primero, pues si se conoce x tambi en co-
nocemos f ( x) = v. En adelante, y salvo indicaci on en sentido contrario, nos referiremos
con problema de optimizaci on a este segundo tipo de problema, en el que se propone la
b usqueda de la soluci on optima, y no (s olo) del valor optimo.
Con argopt
xX
f (x) obtenemos el argumento que hace optimo el valor de
f (x) en un conjunto de valores dado. Por ejemplo
arg mn
x{4,3,4,5}
x
2
es el valor 3 pues 3
2
es menor que (4)
2
, 4
2
y 5
2
.
Consideremos el siguiente ejemplo de problema de optimizaci on: Cu al es el n ume-
ro de cada tipo de moneda y billete con que podemos entregar, en un sistema monetario
28 de septiembre de 2009 Captulo 1. Introducci on 7
dado, una determinada cantidad entera de euros Q usando el menor n umero de pie-
zas?. Se trata de un problema de minimizaci on, es decir, opt es mn. El conjunto X
es el conjunto de posibles desgloses de moneda y la funci on objetivo indica el n umero de
piezas empleadas en un desglose. Un problema relacionado propone averiguar, en un
sistema monetario dado, el menor n umero de piezas con el que se puede entregar una
determinada cantidad entera de euros Q. El primer problema propone la b usqueda de
la soluci on optima, mientras que el segundo se conforma con hallar el valor optimo.
Una instancia o caso particular del problema es la formulaci on del mismo para unos
datos concretos, es decir, la descripci on de una asignaci on de valores para cada uno de
los par ametros del problema. Por ejemplo, buscar la palabra algoritmo en la vig esima
segunda edici on del Diccionario de la Lengua Espa nola es una instancia del problema
de la b usqueda de una palabra en un diccionario. En el caso del problema de optimiza-
ci on que hemos propuesto, una instancia sera, por ejemplo, averiguar, en un sistema
con monedas de 1 y 2 euros y billetes de 5, 10, 20, 50, 100, 200 y 500 euros, el n umero de
monedas y billetes de cada tipo con el que se puede entregar 9 euros usando la menor
cantidad posible de piezas.
Cada instancia lleva asociada una respuesta: su soluci on. En la instancia del problema
de la b usqueda de una palabra en un diccionario que estamos considerando, la denici on
de la palabra algoritmo, y por tanto su soluci on, es:
algoritmo.
(Quiz a del lat. tardo *algobarismus, y este abrev. del ar. cl as. h
.
is abu l gub ar,
c alculo mediante cifras ar abigas).
1. m. Conjunto ordenado y nito de operaciones que permite hallar la solu-
ci on de un problema.
2. m. M etodo y notaci on en las distintas formas del c alculo.
tal que X X
. El conjunto X
1iN
a
i
, donde
X =
{
(a
1
, a
2
, . . . , a
N
) (Z
0
)
N
1iN
a
i
v
i
= Q
}
.
La instancia del problema del cambio optimo de moneda que hemos denido antes
a modo de ejemplo se describira con Q = 9 y V = (1, 2, 5, 10, 20, 50, 100, 200, 500). Y la
soluci on optima con la tupla (0, 2, 1, 0, 0, 0, 0, 0, 0).
No es el unico modo de formalizar el problema. En lugar de una tupla de valores
podramos haber usado un conjunto de valores para especicar el sistema de monedas.
Y la soluci on podra expresarse, en un lugar de con una tupla con N valores enteros, con
una secuencia de pares (cantidad de monedas o billetes, valor de la moneda o billete).
Entrada: Q Ny V = {v
1
, v
2
, . . . , v
N
} N.
Salida: (( a
1
,
b
1
), ( a
2
,
b
2
), . . . , ( a
M
,
b
M
)) = arg opt
((a
1
,b
1
),(a
2
,b
2
),...,(a
M
,b
M
))X
1iN
a
i
, donde
X =
{
((a
1
, b
1
), (a
2
, b
2
), . . . , (a
M
, b
M
)) (NV)
1iN
a
i
b
i
= Q
}
.
Y la instancia del problema del cambio optimo de moneda se describira es este caso
con Q = 9 y V = {1, 2, 5, 10, 20, 50, 100, 200, 500}, y su soluci on optima con la secuencia
((2, 2), (1, 5)), que expresa la idea de 2 monedas de 2 euros y 1 billete de 5 euros.
O, usando la misma especicaci on de la entrada, podemos expresar la salida como
una secuencia de valores de monedas o billetes de modo que el n umero de monedas o
billetes usado sea la talla de dicha secuencia.
Entrada: Q Ny V = {v
1
, v
2
, . . . , v
N
} N.
Salida: (
b
1
,
b
2
, . . . ,
b
M
) = arg opt
(b
1
,b
2
,...,b
M
)X
M, donde
X =
{
(b
1
, b
2
, . . . , b
M
) V
1iM
b
i
= Q
}
.
Y la instancia del problema del cambio optimo de moneda se describira es este caso
con Q = 9 y V = {1, 2, 5, 10, 20, 50, 100, 200, 500}, y su soluci on optima con la secuencia
(2, 2, 5), que expresa la idea de una moneda de dos euros, otra moneda de dos euros y un
billete de cinco euros.
No hay una especicaci on m as correcta que las otras. Las tres que hemos dado son
igualmente v alidas. Pero s ha de tenerse en cuenta que cada una de ellas invita a ciertas
estructuras de datos para la expresi on de los datos de entrada y salida. Y, como veremos
m as adelante, tambi en a diferentes m etodos resolutivos.
12 Apuntes de Algoritmia 28 de septiembre de 2009
1.3.2. Dise no
En el proceso de desarrollo de programas que hemos ilustrado en la gura 1.1 hay un
paso que, a priori, est a poco sistematizado: el dise no del algoritmo a partir de la especi-
caci on del problema. En principio, cada problema requiere una aproximaci on particular
al dise no de algoritmos que le den soluci on y la creatividad juega un papel fundamental.
Desde ese punto de vista, el dise no de algoritmos es un arte y no una t ecnica.
Este arte puede englobarse en el campo de la resoluci on de problemas. Gy orgy
P olya, un matem atico de origen h ungaro, trat o de sistematizar los procesos que segui-
mos al tratar de resolver un problema (normalmente de matem aticas, aunque no solo) y
expuso en How to Solve It una serie de 4 pasos:
1. Comprende el problema.
2. Traza un plan.
3. Ejecuta el plan.
4. Reexiona sobre el trabajo realizado.
ALISIS DE ALGORITMOS
El an alisis de algoritmos es el campo de la algoritmia que estudia la eciencia de los al-
goritmos: el tiempo necesario para su ejecuci on y la cantidad de memoria que consumen.
El estudio de la eciencia nos interesa:
para estimar los recursos computacionales (tiempo de proceso y cantidad de memo-
ria o espacio) que consume un algoritmo en funci on de la talla de las instancias
del problema que resuelve,
y para comparar dos algoritmos que resuelven un mismo problema de modo que
podamos seleccionar el m as eciente.
Centraremos el discurso en el consumo de tiempo y s olo al nal del captulo nos
ocuparemos del consumo de espacio. Hablaremos de complejidad o coste temporal y
espacial para referirnos al consumo de estos recursos.
Una primera idea consiste en estudiar el tiempo real necesario para resolver instancias
concretas del problema con una implementaci on particular del algoritmo ejecut andose
en un sistema computador concreto. Surge entonces una primera cuesti on: con qu e len-
guaje de programaci on hemos de implementar el algoritmo? En principio, el lenguaje de
programaci on resulta indiferente si el objetivo es la comparaci on de dos o m as algoritmos
y todos se implementan con un mismo lenguaje de prop osito general. Pero no basta con
usar el mismo lenguaje para garantizar una comparaci on justa: hemos de ser cuidadosos
y realizar todas las implementaciones con el mismo grado de competencia. No sera justo
implementar uno de los programas de forma directa y burda cuando otro se desarrolla
prestando atenci on a todo tipo de detalles.
En cualquier caso, los algoritmos son independientes de los lenguajes de programa-
ci on y de los sistemas computadores, as que el an alisis del tiempo y la memoria nece-
sarios para la ejecuci on deberan ser ajenos a estos elementos. C omo podemos obtener
an alisis de tiempos de ejecuci on y consumo de memoria sin tener en cuenta detalles
como el computador o el lenguaje de programaci on usados? Un enfoque del an alisis de
19
20 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmos se centra en el estudio de la evoluci on asint otica del n umero de operaciones
elementales y del n umero de celdas de memoria en funci on de la talla del problema. Cier-
tas t ecnicas que estudiaremos permiten establecer algunos resultados sobre el comporta-
miento de un algoritmo a partir de estimaciones muy groseras del n umero de operacio-
nes que han de ejecutarse o del n umero de celdas de memoria consumidas. Estudiaremos,
fundamentalmente, cotas al coste en el mejor y el peor de los casos. Tambi en considera-
remos otras caractersticas del coste temporal y espacial de los algoritmos, como el coste
promedio o el coste amortizado.
Los par ametros del m etodo (a excepci on de self ) aparecen marcados con
informaci on de tipo en una notaci on informal: una cadena que aparece tras
los dos puntos que siguen a cada par ametro. Del mismo modo, el valor de retorno
del m etodo se se nala informalmente al cerrar la lista de par ametros y siguiendo a
una echa (->).
Python permite que el usuario trate esta informaci on que acompa na a los
par ametros o al valor de retorno, pero en nuestro caso no son m as que comentarios
que Python ignora y que presentamos para aumentar la legibilidad del c odigo.
Cada una de estas tres clases implementan uno de los algoritmos descritos antes, y
todas lo hacen implementando la interfaz ISearcher:
22 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/problems/searching.py
class NaiveSequentialSearcher(ISearcher):
def index of (self , a: "sequence<T>", x: "T") -> "int or None":
index = None
for i in range(len(a)):
if x == a[i]:
index = i
return index
class SequentialSearcher(ISearcher):
def index of (self , a: "sequence<T>", x: "T") -> "int or None":
for i in range(len(a)):
if x == a[i]:
return i
elif x < a[i]:
return None
return None
class BinarySearcher(ISearcher):
def index of (self , a: "sequence<T>", x: "T") -> "int or None":
left, right = 0, len(a)
while left < right:
i = (right + left) // 2
if x == a[i]:
return i
elif x < a[i]:
right = i
else:
left = i + 1
return None
La funci on clock
Ocup emonos ya de la cuesti on t ecnica de la medici on del tiempo de ejecuci on. El m odulo
time de la colecci on de bibliotecas est andar de Python ofrece una funci on para la medi-
ci on de tiempos. Su nombre es clock y devuelve el n umero de segundos (un valor en coma
otante) que se ha dedicado a la ejecuci on del programa hasta el punto en que se efect ua
la llamada a la funci on.
log
10
n
. Como log
10
n = lg n/lg 10, el n umero de
cifras necesarias en base 2 s olo es unas 3.32 veces mayor que en base 10. Si bien se trata
de una reducci on del n umero de smbolos, esta se mantiene constante para cualquier va-
lor de n. En cambio, el ratio entre el n umero de smbolos necesarios al codicar en base
1 o en otra base no es constante: la cantidad de smbolos necesarios en base 1 aumenta
exponencialmente con respecto a la de smbolos necesarios en cualquier otra base para
valores crecientes del n umero que se desea codicar.
Llamamos codicaci on razonable a toda codicaci on que permite representar la in-
formaci on con un n umero de smbolos proporcional al requerido con el sistema binario.
N otese que es necesario que el alfabeto tenga al menos dos smbolos para que sea razo-
nable. Si usamos una codicaci on razonable y medimos la talla de la instancia en funci on
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 27
n s
1
(n) s
2
(n)
0 0 1
1 1 1
2 2 2
3 3 2
4 4 3
5 5 3
6 6 3
7 7 3
8 8 4
9 9 4
10 10 4
11 11 4
.
.
.
.
.
.
.
.
.
1 023 1 023 10
.
.
.
.
.
.
.
.
.
1 048 575 1 048 575 20
.
.
.
.
.
.
.
.
.
1 099 511 627 775 1 099 511 627 775 40
.
.
.
.
.
.
.
.
.
Tabla 2.1: Smbolos necesarios para expresar un entero po-
sitivo n usando una codicaci on unaria (s
1
(n)) y una co-
dicaci on en binario natural (s
2
(n)).
del n umero de smbolos necesarios para expresarla, diremos que hacemos uso del crite-
rio del coste logartmico: un n umero entero positivo puede expresarse con un n umero
de smbolos proporcional a su logaritmo; los elementos de un conjunto con n valores
pueden expresarse con un n umero de smbolos proporcional al logaritmo de n.
Los computadores actuales suelen usar una cantidad de memoria ja para represen-
tar valores escalares. Es habitual, por ejemplo, usar 32 o 64 bits para codicar los n ume-
ros enteros sin signo. Cualquier cantidad que podamos expresar con la cantidad de bits
correspondiente ocupa el mismo n umero de celdas de memoria. El criterio del coste
uniforme es una simplicaci on inspirada en este hecho y que asume que toda cantidad
num erica puede expresarse usando una unica celda de memoria. As, un vector con n
enteros positivos, por ejemplo, ocupar a n celdas de memoria. El criterio del coste unifor-
me ayuda notablemente a simplicar los an alisis de coste que efectuaremos, as que lo
adoptaremos en adelante salvo cuando indiquemos lo contrario.
Cabe decir, nalmente, que la talla de un problema no tiene por qu e venir determina-
da por un unico par ametro. Si, por ejemplo, multiplicamos dos matrices, una de dimen-
si on p q y otra de dimensi on q r, la talla del problema depende de los tres valores: p,
q y r.
2.3. Perl de ejecuci on
Hemos de decidir qu e entendemos por talla de una instancia en el contexto del pro-
blema de b usqueda que estamos resolviendo por tres procedimientos diferentes. Asumi-
remos el criterio del coste uniforme, as que no nos preocuparemos por la magnitud de
los valores del vector ni, en consecuencia, por el n umero de bits necesarios para codicar
28 Apuntes de Algoritmia 28 de septiembre de 2009
cada uno de ellos. La talla del problema, que denotaremos con la letra n, vendr a deter-
minada unicamente por el tama no del vector sobre el que se efect ua la b usqueda.
Evaluaci on del tiempo de ejecuci on en funci on de la talla
Estudiaremos el perl del tiempo de ejecuci on del programa, esto es, la evoluci on del
tiempo de ejecuci on con el par ametro n.
experiments/searching/timing4.py
from algoritmia.searching import NaiveSequentialSearcher
from time import clock
from random import seed, randrange, sample
tmin = 1 # Tiempo mnimo de ejecuci on: un segundo.
seed(0)
searcher = NaiveSequentialSearcher()
for n in range(1, 11):
a = sorted(sample(range(5*n), n))
x = a[randrange(n)]
t1 = t2 = clock()
r = 0
while t2 - t1 < tmin:
index = searcher.index of (a, x)
t2 = clock()
r += 1
t3 = t4 = clock()
aux = 0
while aux < r:
t4 - t3
index = 0
t4 = clock()
aux += 1
t = ((t2 - t1) - (t4 - t3)) / r
print(Tiempo medio por ejecucion n={:2d}: {:.8f} segundos.format(n, t))
La gura 2.1 (a) muestra gr acamente el resultado de esta medici on de tiempos de
ejecuci on de NaiveSequentialSearcher.index of . Se puede apreciar que una lnea recta se
ajustara razonablemente bien a los puntos de la gr aca: el tiempo necesario crece propor-
cionalmente con la longitud del vector. Si repetimos el experimento con SequentialSear-
cher.index of obtenemos los resultados que se muestran gr acamente en la gura 2.1 (b).
Se puede apreciar una mayor variabilidad en el tiempo medido. Ello se debe a que el
m etodo de b usqueda secuencial es sensible a la ubicaci on en el vector del valor buscado:
por ejemplo, acaba m as r apidamente si el elemento buscado ocupa las primeras posicio-
nes y tarda m as si ocupa las ultimas.
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 29
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
(a) (b)
Figura 2.1: Medici on de tiempo de ejecuci on de (a) NaiveSequentialSearcher.index of y (b) SequentialSear-
cher.index of para vectores de tama no entre 1 y 10 y la b usqueda de un elemento cualquiera de cada uno de los
vectores.
Podemos efectuar una medida de tiempo de ejecuci on de SequentialSearcher.index of
para cada una de las posibles ubicaciones del elemento buscado. El resultado se muestra
en la gura 2.2 (a). El tiempo mnimo para cada valor de n corresponde a la b usqueda
del valor que ocupa la primera posici on del vector. Es su mejor caso. El tiempo m aximo
corresponde a la b usqueda del valor que est a en ultima posici on, y es el peor caso al que
se enfrenta la rutina (equivalente, en tiempo de ejecuci on, a buscar un valor mayor que el
del ultimo elemento del vector). Podemos acotar superior e inferiormente cada medida
de tiempo con sendas funciones de n, como se muestra en la gura 2.2 (b). Una funci on
constante constituye una cota inferior ajustada y una lnea recta acota superiormente,
tambi en de forma ajustada, los tiempos m aximos para cada valor de n.
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
(a) (b)
Figura 2.2: (a) Medici on de tiempo de ejecuci on de SequentialSearcher.index of para vectores de tama no comprendido
entre 1 y 10. Para cada vector buscamos todos sus elementos, uno con cada ejecuci on. (b) En trazo discontinuo, cotas
superior e inferior.
Podemos repetir el experimento para el m etodo de b usqueda secuencial naf. La -
gura 2.3 muestra el resultado. Podemos destacar que, a diferencia de lo que ocurre con la
b usqueda secuencial, el tiempo para el mejor de los casos se acota superior e inferiormente
30 Apuntes de Algoritmia 28 de septiembre de 2009
por sendas lneas rectas (no horizontales).
Figura 2.3: Medici on de tiempo de ejecuci on de NaiveSequen-
tialSearcher.index of para vectores de tama no entre 1 y 10.
Las lneas discontinuas acotan superior e inferiormente los va-
lores de tiempo obtenidos al ejecutar el programa de b usqueda
sobre cada uno de los elementos del vector.
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
Si realizamos el mismo experimento con el m etodo de b usqueda binaria obtenemos
los resultados que se muestran en la gura 2.4. En este caso, la cota inferior sigue siendo
una constante, pero la cota superior m as ajustada es una funci on logartmica.
Figura 2.4: Medici on de tiempo de ejecuci on de BinarySear-
cher.index of para vectores de tama no entre 1 y 10. Para ca-
da tama no de vector se efect ua la b usqueda de cada uno de sus
elementos. Las curvas acotan superior e inferiormente los va-
lores obtenidos.
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
Comparaci on de perles de ejecuci on
La gura 2.5 permite comparar las cotas superior e inferior que hemos mostrado en
las gr acas de las guras 2.2 (b) y 2.4. A tenor de la gr aca, parece que SequentialSear-
cher.index of sea m as eciente que BinarySearcher.index of . Pero si atendemos a la ten-
dencia de la cota superior del coste, es posible que las dos curvas se crucen y que Binary-
Searcher.index of pase a ser m as eciente a partir de cierto valor de n. Comprob emos-
lo. La gura 2.6 muestra los resultados de la medici on de tiempos para SequentialSear-
cher.index of y BinarySearcher.index of . Se puede apreciar (gura 2.7) que para valores de
n superiores a 15, el m etodo BinarySearcher.index of presenta un comportamiento mejor
que SequentialSearcher.index of para el peor de los casos. Este comportamiento es tanto
mejor cuanto mayor es el valor de n.
Estamos comparando los algoritmos por el tiempo que necesitan para resolver el peor
caso que se les plantea para cada valor de n. Es este un buen criterio de comparaci on?
Si nuestro programa debe garantizar un tiempo de respuesta determinado, es obvio que
s: si acotamos el tiempo m aximo que necesita para resolver un problema de tama no n,
sabremos si un algoritmo es o no es adecuado. Pero no es el unico criterio que podemos
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 31
n
0 1 2 3 4 5 6 7 8 9 10
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0.00
0.50
1.00
1.50
2.00
2.50
3.00
Figura 2.5: Comparaci on entre cotas superior e inferior para
las ejecuciones de SequentialSearcher.index of (trazo discon-
tinuo) y BinarySearcher.index of (trazo continuo) para vec-
tores de talla comprendida entre 1 y 10.
n
0 10 20 30 40 50 60 70 80 90 100
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0
2
4
6
8
10
12
14
16
18
20
22
n
0 10 20 30 40 50 60 70 80 90 100
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0
2
4
6
8
10
12
14
16
18
20
22
(a) (b)
Figura 2.6: (a) Resultado de la medici on de tiempo de ejecuci on de SequentialSearcher.index of para vectores de ta-
ma nos comprendidos entre 1 y 100 y b usqueda de cada uno de sus elementos. (b)
Idem para BinarySearcher.index of .
n
0 10 20 30 40 50 60 70 80 90 100
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0
2
4
6
8
10
12
14
16
18
20
22
Figura 2.7: Comparaci on entre cotas superior e inferior para
las ejecuciones de SequentialSearcher.index of (trazo discon-
tinuo) y BinarySearcher.index of (trazo continuo) sobre vec-
tores de talla entre 1 y 100.
considerar. Como hemos visto, los m etodos NaiveSequentialSearcher.index of y Sequen-
tialSearcher.index of presentan id entico comportamiento para el peor de los casos. Pero
es obvio que resulta m as adecuado el m etodo SequentialSearcher.index of , pues, adem as,
presenta un mejor comportamiento en el mejor de los casos.
Hay una alegaci on que hacer a la comparaci on basada en el coste en el peor (y mejor)
de los casos, y si el peor caso para cada valor de n corresponde a una instancia muy
improbable? No sera mejor seleccionar el m etodo que se comporta mejor por t ermino
medio? En principio, el tiempo promedio se obtiene ejecutando el programa sobre dife-
32 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 2.8: En trazo grueso se muestra el tiempo promedio pa-
ra las ejecuciones de SequentialSearcher.index of (lnea dis-
continua) y BinarySearcher.index of (lnea continua), y en
trazo no, las respectivas cotas superior e inferior para vecto-
res de talla comprendida entre 1 y 100.
n
0 10 20 30 40 50 60 70 80 90 100
t
(
e
n
1
0
6
s
e
g
u
n
d
o
s
)
0
2
4
6
8
10
12
14
16
18
20
22
rentes instancias de igual talla y calculando la media del tiempo de ejecuci on. Pero hay
un problema: jada la talla, qu e instancias usamos para estimar la media? Diferentes
instancias proporcionar an diferentes valores del tiempo promedio para esa talla. Si, por
ejemplo, seleccionamos instancias en las que el n umero buscado aparece en las primeras
posiciones obtendremos un tiempo promedio menor que si escogemos instancias en las
que el n umero buscado aparece en las ultimas posiciones. Para efectuar una estimaci on
v alida debi eramos seleccionar instancias siguiendo una distribuci on de probabilidad si-
milar a la que se da en la explotaci on del algoritmo. En nuestro caso, supondremos que
hay id entica probabilidad de que la b usqueda del algoritmo secuencial se detenga en
cualquier punto del vector.
La gura 2.8 muestra, adem as de las cotas inferior y superior al tiempo de ejecuci on
de SequentialSearcher.index of y BinarySearcher.index of , el tiempo promedio para cada
valor de n. El coste temporal promedio de SequentialSearcher.index of es, bajo este supuesto
de equiprobabilidad, la semisuma del coste para el mejor y para el peor de los casos. No
ocurre lo mismo con la b usqueda binaria. M as adelante estudiaremos analticamente este
comportamiento.
El m etodo del desplegado permite formular una hip otesis sobre la expresi on
cerrada de una ecuaci on recursiva, pero no constituye una demostraci on de
que esta es soluci on. Podemos demostrar por inducci on que lo es, pero en aras de
la brevedad lo dejamos como ejercicio para el lector.
2.5. Expresi on del coste temporal de los algoritmos
con notaci on asint otica
Dada la denici on tan laxa del concepto de paso, un an alisis excesivamente detallado
puede no merecer la pena. Cuando decimos que el coste en el peor de los casos es 2n +4
o 3n +2, qu e signican exactamente sus diferentes constantes? Hay cierta arbitrariedad
en su valor, pues dependen de qu e criterio adoptamos al considerar qu e es, exactamente,
un paso. Podramos decir que el n umero de pasos es c
1
n + c
0
, para c
1
y c
0
constantes, y
estaramos proporcionando la misma informaci on esencial: que el n umero de pasos crece
en proporci on directa al valor de n.
La denominada notaci on asint otica es una forma de describir el comportamiento de
las funciones de coste temporal (o espacial) en funci on de la talla que resulta muy concisa
y, a la vez, simplica los an alisis de coste. El apartado B.13 introduce los elementos b asi-
cos de esta notaci on. Es importante que el lector tenga claros los conceptos presentados
en dicho apartado antes de proseguir.
2.5.1. Mejor y peor casos
En el apartado 2.4.1 concluimos que el n umero de pasos que comporta la ejecuci on de la
funci on NaiveSequentialSearcher.index of es f (n) = 2n + 4. Se trata de una funci on O(n)
y (n), es decir, (n). Decimos que NaiveSequentialSearcher.index of es un algoritmo de
coste temporal lineal (o simplemente lineal, si se sobreentiende que hablamos del coste
temporal). Cuando la cota inferior del mejor caso y la cota superior del peor caso coin-
ciden, usamos la notaci on zeta. As, decimos que NaiveSequentialSearcher.index of es
(n).
El algoritmo de b usqueda secuencial SequentialSearcher.index of presentaba un coste
temporal comprendido entre 4 pasos (mejor de los casos) y 3n + 2 (peor de los casos).
De la primera funci on podemos decir que es (1) y de la segunda, que es (n). No
obstante, la notaci on orden se usa generalmente para acotar superiormente el coste temporal
en el peor de los casos, y la notaci on omega para acotar inferiormente el coste temporal en el
40 Apuntes de Algoritmia 28 de septiembre de 2009
mejor de los casos. As, decimos que el n umero de pasos que comporta la ejecuci on de
SequentialSearcher.index of es (1) y O(n).
I
n
Pr(I)t(I).
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 43
Volvamos al problema de la b usqueda en un vector ordenado y supongamos que exis-
te la misma probabilidad de que el elemento buscado haga que SequentialSearcher.index of
se detenga en la i- esima iteraci on, para i entre 1 y n. El n umero promedio de pasos que
comporta la ejecuci on de SequentialSearcher.index of es
3
1
n +1
+
1in
(1 +3i)
1
n +1
=
3
n +1
+
n
n +1
+
3
n +1
1in
i
=
3 + n
n +1
+
3
n +1
n (n +1)
2
=
3
2
n +
3 + n
n +1
.
O sea, es una funci on (n) y supone la ejecuci on de, aproximadamente, la mitad de
pasos que en el peor de los casos.
Si la probabilidad de que busquemos uno u otro valor es diferente, el coste promedio
puede ser tambi en diferente. Supongamos que la probabilidad de que busquemos un
valor que hace que el algoritmo se detenga en la k- esima iteraci on es Pr(k) = 1/2
k
, para
k entre 1 y n, y que Pr(0) = 1/2
n
(n otese que
0kn
Pr(k) = 1). El n umero promedio de
pasos en funci on de n es, en ese caso,
3
1
2
n
+
1in
(1 +3i)
1
2
i
= 3
1
2
n
+
1in
1
2
i
+3
1in
i
2
i
3
1
2
n
+1 +3
1in
i
2
i
7.
El n umero medio de pasos est a acotado, bajo este supuesto, por una cantidad constante.
Como se puede comprobar, el an alisis del coste temporal promedio resulta general-
mente m as trabajoso que el del coste en el mejor o peor de los casos y es dependiente de
las asunciones que hagamos sobre la distribuci on de las instancias (y no siempre es f acil
averiguar la distribuci on de los datos reales en la explotaci on del algoritmo).
2.5.3. Coste amortizado
En algunos casos, la estimaci on de coste para el peor de los casos de una determinada
operaci on resulta excesivamente pesimista, pues es posible que las circunstancias que
implican un elevado coste sean raras y alcanzables unicamente tras efectuar ciertas ope-
raciones previas. Cuando esto ocurra, presentaremos an alisis de coste de otra naturaleza:
el denominado coste amortizado, que es el coste por operaci on efectuada cuando se lleva
a cabo una serie determinada de operaciones consecutivas.
No se debe confundir coste amortizado con el coste promedio: el coste amortizado es
el coste total de una serie de operaciones (en el peor de los casos) dividido por el n umero
de operaciones efectuadas, sin asumir ninguna distribuci on de probabilidad sobre las
instancias de cada talla.
Incremento de un contador binario
Consideremos un ejemplo: una rutina para incrementar un contador codicado en bina-
rio natural expresado como una secuencia de ceros y unos en un vector. Un vector como
44 Apuntes de Algoritmia 28 de septiembre de 2009
[0,0,0,1,0,0,1,1], por ejemplo, representa el n umero binario 10011. El n umero que re-
sulta de sumarle uno es 10100, que se representa con el vector [0,0,0,1,0,1,0,0]. Esta
rutina Python efect ua el incremento del contador (sin tener en cuenta posibles desborda-
mientos):
def increment(counter: "Sequence of (1 or 0)"):
i = len(counter)-1
while i >= 0 and counter[i] == 1:
counter[i] = 0
i -= 1
if i >= 0:
counter[i] = 1
Si el mayor n umero que podemos codicar es n, para n > 0, la talla del vector ser a 1 +
lg n. El coste en el peor de los casos es O(lg n): si el vector contiene todo unos, el bucle
lo recorre por completo y modica todos sus bits.
Qu e ocurre si efectuamos n incrementos consecutivos sobre un contador que empie-
za con valor cero? Parece l ogico pensar que el coste temporal total ser a O(n lg n). Sin em-
bargo, efectuar esas n operaciones requiere, en total, tiempo O(n) y no O(n lg n). C omo
es posible? En la tabla 2.2 se detalla el n umero de pasos que supone efectuar los primeros
16 incrementos de un contador binario que empieza en cero.
Tabla 2.2: Coste en pasos al efectuar cada uno de los n pri-
meros incremento de un contado binario inicializado en
cero.
Incremento Valor del contador Pasos
0
1 1 1
2 10 2
3 11 1
4 100 3
5 101 1
6 110 2
7 111 1
8 1000 4
9 1001 1
10 1010 2
11 1011 1
12 1100 3
13 1101 1
14 1110 2
15 1111 1
.
.
.
.
.
.
.
.
.
N otese que uno de cada dos incrementos s olo requiere la ejecuci on de un paso. De
los restantes incrementos, la mitad requieren ejecutar 2 pasos. Y de los restantes, s olo
la mitad requiere ejecutar un paso m as. Y as sucesivamente. Suponiendo que n + 1 es
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 45
potencia de 2, el coste de los n incrementos puede expresarse as:
1
n +1
2
+2
n +1
4
+3
n +1
8
+4
n +1
16
+ . . . =
1ilg n
i
n +1
2
i
< 2n,
o sea, O(n).
Si efectuar n incrementos requiere ejecutar a lo sumo 2n pasos, el coste promedio de
cada incremento es O(1): decimos que el coste amortizado de n incrementos sobre un
contador inicialmente nulo es constante. Usaremos un asterisco tras la notaci on orden
para expresar un coste amortizado. As, O(1)
1in
a
i
=
1in
c
i
+i
i1
=
(
1in
c
i
+n
0
.
Se dice que una funci on de potencial es v alida si
i
0
para todo i y
0
= 0. Para toda
funci on de potencial v alida, el coste real de ejecutar las n operaciones siempre es menor
que el coste amortizado total:
1in
c
i
=
(
1in
a
i
n
1in
a
i
.
Ve amos como aplicar esta idea al c alculo del coste amortizado del contador binario.
Denimos el potencial de un n umero como el n umero de bits que valen 1 en su codica-
ci on binaria. Cuando empezamos una secuencia de incrementos, partimos del n umero 0,
as que
0
= 0. Es obvio que el potencial siempre es mayor o igual que cero, por lo que
est a bien denido. Una operaci on de incremento cambiar a algunos bits de 1 a 0 y un bit
de 0 a 1. Sea #(1 0) el n umero de bits que pasan de 0 a 1. Podemos expresar el coste real
del incremento i- esimo con c
i
= #(1 0) + 1. La diferencia de potencial,
i
i 1 se
puede expresar con 1 #(1 0). As pues, el coste amortizado del i- esimo incremento
es
a
i
= c
i
+
i
i 1 = #(1 0) +1 +1 #(1 0) = 2.
As pues, el coste amortizado de cada incremento en una serie de incrementos es
constante.
46 Apuntes de Algoritmia 28 de septiembre de 2009
Redimensionamiento de un vector
Consideremos ahora un ejemplo diferente: un vector din amico que debe redimensionarse
para a nadir por el nal, uno a uno, m elementos.
Plante emonos qu e ocurre al efectuar una sola adici on en un vector con n elementos.
Es una operaci on que requiere reservar n +1 celdas de memoria (1 paso), copiar el valor
de las n celdas originales (n pasos), copiar al nal el nuevo valor (1 paso) y liberar la
memoria en la que residan los n valores originales (1 paso). Se trata, pues, de una ope-
raci on ejecutable en 2n + 3 pasos. Efectuar m adiciones requerir a un n umero de pasos
0i<m
2(n + i) +3, es decir, tiempo O(m
2
+ mn).
Es posible reducir el coste temporal a s olo O(m + n) si malgastamos algo de me-
moria. Cuando necesitemos que el vector almacene n elementos, reservaremos memoria
para una cantidad de celdas, N, mayor o igual que n. El valor N ser a la capacidad del
vector, frente a n, que es su tama no. Si nos piden a nadir una celda al vector y el tama no
es menor que la capacidad, la adici on se puede ejecutar en tiempo O(1). Si la capacidad
es igual al tama no, habr a que aumentar antes la capacidad del vector en un cantidad ja
o proporcional a la capacidad actual.
Si el aumento de capacidad se produce multiplicando por 2 la capacidad anterior,
estaremos efectuando una operaci on de coste temporal O(2N), pero las siguientes N
adiciones tendr an un coste temporal O(1). Si consideramos las N operaciones de adici on
consecutivas en su conjunto, su coste total ser a O(3N), que dividido por N arroja una
cantidad de tiempo constante por operaci on. Esta argumentaci on puede extenderse a la
adici on de m elementos a un vector de tama no n, con un coste temporal total O(n + m).
Si el tama no original, n, es por ejemplo 1, el coste de amortizado de cada una de las m
adiciones es O(1)
.
Puede parecer que la memoria desperdiciada es muy grande, pero nunca es m as del
doble que el tama no del vector, una cantidad proporcional a dicho tama no. Esta t ecnica
se conoce como doblado, pero no es necesario que la constante por la que multiplicamos
la capacidad sea igual a 2: cualquier constante mayor que 1 nos vale.
Supongamos que cada vez que rebasamos la capacidad de un vector se solicita me-
moria para cN nuevos elementos, siendo c > 1 y N la capacidad actual. Si efectuamos m
adiciones al vector cuando su capacidad y tama no es 1, se producir a una serie de reservas
de memoria que har an que la capacidad del vector siga la sucesi on c
0
, c, c
2
, . . . , c
k
, hasta
que c
k1
< m c
k
.
De las m operaciones de adici on, mk tendr an coste temporal O(1) y las restantes k
operaciones tendr an, en total, un coste temporal del orden de
0ik
c
i
=
c
k+1
1
c 1
.
Si consideramos (el orden de) la suma del coste de las mk operaciones de coste cons-
tante con el de las restantes k operaciones, tenemos
O
(
mk +
c
k+1
1
c 1
)
= O
(
c
k
k +
c
k+1
1
c 1
)
= O(c
k
).
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 47
El n umero total de operaciones, m, est a comprendido entre c
k1
y c
k
. El coste amorti-
zado de cada una de ellas es, pues, constante.
2.5.4. Una comparaci on de costes en t erminos asint oticos
Decimos que NaiveSequentialSearcher.index of y SequentialSearcher.index of son lineales,
y que BinarySearcher.index of es logartmico. Qu e implica, a efectos pr acticos, que el
coste temporal sea logartmico, lineal, exponencial, etc.? Resulta interesante tener cierta
intuici on sobre lo que supone clasicar un algoritmo por su coste temporal en el peor de
los casos como perteneciente a un orden determinado. Lo mejor ser a que comparemos
algunas gr acas de crecimiento. Empecemos por los crecimientos sublineales y lineal,
cuyas gr acas se muestran en la gura 2.9.
c
0
c
1
log n
c
2
n
c
3
n
n
t
Figura 2.9: Gr acas de crecimiento de funcio-
nes sublineales y lineal.
A la vista de la gura, reexionemos sobre el crecimiento de algunas funciones de
coste y las implicaciones que tienen al evaluar la eciencia de un algoritmo:
Un algoritmo de coste constante ejecuta un n umero de instrucciones acotado por
una constante independiente de la talla del problema.
Un algoritmo que soluciona un problema en tiempo constante es lo ideal: a la larga
es mejor que cualquier algoritmo de coste no constante.
El coste de un algoritmo logartmico crece muy lentamente conforme n aumenta.
Por ejemplo, si resolver un problema de talla n = 10 tarda 1 s, puede que tarde
2 s en resolver un problema 10 veces m as grande (n = 100) y, en tal caso, s olo
3 s en resolver uno 100 veces mayor (n = 1 000). Cada vez que el problema es 10
veces m as grande, se incrementa en 1 s el tiempo necesario (el factor exacto que
incrementa el tiempo en un microsegundo depende de constantes como la base del
logaritmo).
Un algoritmo cuyo coste es (
2n
(
n
e
)
n
, es decir, el coste temporal de la enumeraci on es
O(n
2n
(
n
e
)
n
).
Resulta prohibitivo utilizar un algoritmo que requiere tiempo exponencial cuando la
talla del problema es moderada o grande. Un problema que s olo se puede resolver en
tiempo exponencial es un problema intratable.
54 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 2.11:
Arboles de
decisi on asociados a (a)
el algoritmo de b usqueda
secuencial, y (b) el algo-
ritmo de b usqueda bina-
ria para un vector de 8
elementos.
x a[0]
x = a[0]
0 None
x a[1]
x = a[1]
1 None
x a[2]
x = a[2]
2 None
x a[3]
x = a[3]
3 None
x a[4]
x = a[4]
4 None
x a[5]
x = a[5]
5 None
x a[6]
x = a[6]
6 None
x a[7]
x = a[7]
7 None
None
(a)
x a[4]
x = a[4]
4 x a[2]
x = a[2]
2 x a[1]
x = a[1]
1 x a[0]
x = a[0]
0 None
None
None
x a[3]
x = a[3]
3 None
None
x a[6]
x = a[6]
6 x a[5]
x = a[5]
5 None
None
x a[7]
x = a[7]
7 None
None
(b)
2.7.2. Problemas presumiblemente difciles
Hay una serie de problemas que resultan de inter es y para los que hay unos resultados
curiosos. Nadie sabe resolverlos en tiempo polin omico y nadie ha demostrado que requie-
ran tiempo exponencial. Pero si nos dan una soluci on, podemos comprobar, en tiempo
polin omico, si es o no es v alida. Esto implica que s sabramos resolverlos en tiempo
polin omico en un ordenador no determinista, es decir, un ordenador capaz de llevar en
paralelo una cantidad de c alculos arbitrariamente grande: bastara con generar toda po-
sible soluci on en uno de los hilos de ejecuci on paralela y detenernos cuando uno de ellos
compruebe que ha alcanzado una soluci on. No es posible construir un computador ca-
paz de semejante proeza, pero tiene inter es considerar este tipo de dispositivos formales
desde un punto de vista te orico. La familia de los problemas con esta caracterstica se
conoce por NP, siglas del t ermino ingl es Non-deterministically Polynomial.
El estudio de la familia NP centra su inter es sobre cierto tipo de problemas: los de-
nominados problemas de decisi on. Se trata de problemas cuya soluci on es siempre s
o no. Dentro de la familia NP hay un subconjunto especial de problemas de decisi on:
la familia de los problemas NP-completos. Los problemas NP-completos son aquellos
problemas NP tan difciles (en un sentido formal) como cualquier otro problema de NP y
son particularmente interesantes porque si se consiguiera resolver uno de ellos en tiempo
polin omico, todos los problemas de NP se resolveran tambi en en tiempo polin omico.
Baste decir que cuando clasiquemos un problema como NP estaremos emitiendo un
juicio sobre la dicultad presumida del problema. Si lo clasicamos como NP-Completo,
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 55
nuestra presunci on de dicultad ser a m as rme.
A un hay otra familia de problemas de inter es atendiendo a su dicultad: los proble-
mas NP-Difciles, que son aquellos tan difciles, al menos, como los NP-completos, pero
de los que no se ha demostrado que sean NP.
2.7.3. Problemas irresolubles
Ciertos problemas ni siquiera son resolubles. Los problema de decisi on irresolubles re-
ciben el nombre de problemas indecidibles. El primer problema indecidible conocido
es el problema de la parada, fue planteado por Alan Turing y se puede formular del
siguiente modo: dado un programa, naliza su ejecuci on ante cualquier instancia?.
Se puede demostrar que ning un algoritmo puede resolver el problema por reduc-
ci on al absurdo. Supongamos que existe una funci on, halt, a la que se suministran dos
cadenas, una con un programa y otra con una instancia del problema que resuelve, y
devuelve cierto o falso en funci on de si dicho programa naliza ante esa instancia o
no. Consideremos esta funci on Python:
def trouble(s):
if halt(s, s) == False:
return True
while True:
pass
Si halt dice que un programa no naliza, trouble devuelve el valor True; en caso contrario,
entra en un bucle innito. Qu e ocurre si suministramos a trouble una cadena con c odigo
de trouble? Se detiene su ejecuci on? Consideremos las dos posibilidades:
Si se detiene es porque entonces halt devuelve False, lo que signica que no se de-
tiene.
Y si no se detiene, es porque halt devuelve True, lo que signica que s se detiene.
As pues, es imposible denir una funci on halt capaz de decir siempre si un programa
naliza o no.
La existencia de problemas indecidibles es un resultado negativo de gran importan-
cia, pues pone lmites a lo que se puede calcular con sistemas computadores (y si las
personas somos sistemas computadores, tambi en a lo que somos capaces de calcular!).
Captulo 3
ALGUNAS ESTRUCTURAS DE DATOS
Una estructura de datos es la organizaci on de una colecci on de datos que tiene por objeto
facilitar su acceso y/o modicaci on, as como expresar relaciones entre ellos. La estructu-
ra de datos se consulta y manipula mediante operaciones algortmicas. Dos estructuras
de datos pueden ofrecer un mismo conjunto de operaciones, es decir, implementar una
misma interfaz, pero hacerlo mediante algoritmos distintos y, en consecuencia, presentar
diferentes costes temporales (o espaciales). Al dise nar un algoritmo que necesita gestio-
nar una colecci on de datos, las operaciones que use efectivamente y su frecuencia de uso
resultan determinantes en la elecci on de una determinada estructura de datos de entre
todas las que presenten una misma interfaz, pues el coste computacional del algoritmo
se ver a afectado por dicha elecci on.
Python ofrece implementaciones nativas para ciertas colecciones de datos: listas, dic-
cionarios, conjuntos y tuplas. Con ellas y los tipos escalares (enteros, n umeros en coma
otante, etc.) construiremos otras estructuras de datos m as sosticadas.
El captulo no pretende ser exhaustivo en un tema tan amplio como las estructuras
de datos y se limita a presentar aquellas estructuras que usaremos en el resto del libro.
3.1. Clases abstractas b asicas para colecciones
Ya hemos visto que Python permite denir clases con m etodos abstractos y que pode-
mos usar estos como forma de especicar una interfaz. En el m odulo est andar collections,
Python ofrece una serie de clases abstractas con las que se especican algunas interfaces
(e incluyen la implementaci on de algunos m etodos denidos en t erminos de los m eto-
dos abstractos). La clase abstracta Container, por ejemplo, dene un m etodo sin cuerpo
con identicador contains que se puede invocar con la notaci on de llamada a m etodo
o con el operador binario in. Este m etodo recibe como argumento un elemento y debe
devolver un valor booleano que indique si la instancia sobre la que se invoca el m etodo
contiene o no a dicho elemento. Numerosas estructuras de datos (listas, conjuntos, etc.)
57
58 Apuntes de Algoritmia 28 de septiembre de 2009
ofrecen esta funcionalidad, por lo que decimos que implementan la interfaz Container. En
un algoritmo en el que se use una variable sobre la que s olo se efect uan operaciones de
pertenencia podremos utilizar cualquier estructura que implemente la interfaz Container.
Cu al escojamos concretamente depender a de factores relacionados con la eciencia (su
consumo de memoria o, especialmente, su coste temporal).
Estas son las clases abstractas m as b asicas predenidas en collections:
Container Permite que se inquiera por la pertenencia de un elemento a la colecci on. Las
clases que heredan Container deben implementar el m etodo contains (que puede
invocarse va el operador in):
contains (self , element): devuelve True si element est a incluido y False en caso
contrario.
Iterable Permite enumerar los elementos de la colecci on. Las clases que implementan
Iterable deben ofrecer un denici on de este m etodo (que puede invocarse en cual-
quier punto en el que se necesita una enumeraci on como, por ejemplo, en bucles
for):
iter (self ): enumera sus elementos.
Sized Permite conocer el n umero de elementos que contiene la colecci on. Las clases que
heredan de Sized deben implementar este m etodo (que puede invocarse mediante
la funci on len):
len (self ): devuelve el n umero de elementos contenidos.
Pongamos un ejemplo de clase que implementa las tres interfaces. La clase RangeSet
dene un rango de enteros consecutivos e implementa los m etodos de Container, Iterable
y Sized, adem as del m etodo repr para obtener una descripci on como cadena de la
estructura:
algoritmia/datastructures/misc.py
from ..collections import Container, Iterable, Sized
class RangeSet(Container, Iterable, Sized):
def init (self , min: "int", max: "int"):
self . min, self . max = min, max
def contains (self , v: "int") -> "bool":
return self . min <= v <= self . max
def iter (self ) -> "iterable<int>":
for i in range(self . min, self . max+1): yield i
def len (self ) -> "int":
return self . max - self . min + 1
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 59
def repr (self ) -> "str":
return {}({!r}, {!r}).format(self . class . name , self . min, self . max)
demos/datastructures/rangeset.py
from algoritmia.datastructures.misc import RangeSet
r = RangeSet(10, 20)
print("Talla:", len(r))
print("Contiene a 5?", 5 in r, "Y a 12?", 12 in r)
print("Elementos:", end=" ")
for e in r: print(e, end=" ")
Talla: 11
Contiene a 5? False Y a 12? True
Elementos: 10 11 12 13 14 15 16 17 18 19 20
Heredar de clases abstractas no s olo supone asumir el compromiso de implementar
ciertos m etodos; adem as, nos podemos beneciar por herencia de algunos m etodos pre-
denidos. El m odulo collections dene otras clases abstractas m as complejas, algunas de
las cuales denen m etodos que no tenemos por qu e implementar de nuevo.
3.2. Secuencias
Una secuencia es una colecci on de elementos dispuestos en orden, es decir, donde cada
elemento lleva asociado un ordinal con su posici on: su ndice. En el m odulo collections se
denen dos clases abstractas para modelar secuencias:
Sequence Hereda de Sized, Iterable y Container. Las clases no abstractas que especializan
a esta deben implementar el siguiente m etodo:
getitem (self , index): accede al elemento de ndice index (el primero tiene
ndice 0). Se puede acceder con el operador [], que debe ir precedido por
una referencia a la secuencia y encerrar entre los corchetes una expresi on que
devuelve un entero.
Toda clase que especializa a Sequence hereda sendas implementaciones de los si-
guientes m etodos:
reversed (self ): enumera los elementos de la secuencia del ultimo al primero.
index(self , value): devuelve el ndice del primer elemento cuyo valor coincide
con value, o lanza una excepci on ValueError si ninguno coincide.
count(self , value): devuelve el n umero de elementos con valor igual a value.
MutableSequence Una secuencia mutable ofrece, adem as de los m etodos de Sequence,
otros que permiten modicar la secuencia. Las clases no abstractas que especializan
a MutableSequence deben ofrecer implementaciones para estos m etodos:
60 Apuntes de Algoritmia 28 de septiembre de 2009
setitem (self , index, value): asigna value al elemento de ndice index. Puede
usar una sintaxis similar a la de getitem , pero el operador [] debe aparecer a
la izquierda de un operador de asignaci on a cuya derecha hay una expresi on.
delitem (self , index): suprime el elemento de ndice index. Se puede usar
con la palabra clave del seguida de una expresi on con el operador [].
insert(self , index, value): inserta value inmediatamente a continuaci on del ele-
mento de ndice index.
Los siguientes m etodos est an denidos en toda clase que hereda de MutableSequen-
ce:
append(self , value): a nade un elemento con valor value al nal de la secuencia.
reverse(self ): invierte el orden de los elementos de la secuencia.
extend(self , values): inserta la serie de valores values al nal de la secuencia.
pop(self , index=-1): devuelve y elimina el elemento de ndice index (por de-
fecto, el ultimo de la secuencia).
remove(self , value): elimina el elemento de valor value y, si no existe, lanza una
excepci on ValueError.
iadd (self , values): equivale a extend, pero se invoca con el operador +=.
Python ofrece implementaciones nativas para cadenas y tuplas, que son especializa-
ciones de Sequence, y para listas (vectores), que lo son de MutableSequence. En el ap endice
dedicado a presentar los elementos b asicos de Python se introducen las cadenas y las
tuplas, por lo que remitimos al lector a la secci on A.4.3 para repasar estos conceptos.
Ahora presentaremos con cierto detenimiento dos estructuras b asicas para imple-
mentar listas: la lista propiamente dicha (o vector) y la lista enlazada.
3.2.1. Vector
Un vector es un sucesi on de elementos contiguos en memoria que implementa los m eto-
dos de MutableSequence. En numerosos lenguajes de programaci on (entre los que se cuen-
ta Python) el primer elemento de los vectores tiene ndice 0. La direcci on en la que reside
un dato es d +i s, donde d es la direcci on de memoria en la que empieza el vector, i es el
ndice del elemento y s es el tama no de los elementos. Conocer la ubicaci on del elemento
de ndice i es, pues, inmediato. N otese que todos los elementos del vector deben ocupar
el mismo espacio para que el c alculo puede efectuarse correctamente.
Python permite que un vector tenga elementos de diferentes tipos porque almacena
en cada celda una referencia (b asicamente un puntero) a un valor, y todas las referen-
cias ocupan lo mismo. En Python, los vectores almacenan explcitamente su longitud,
es decir, el n umero de elementos que contienen. Ello permite, por ejemplo, efectuar e-
cientemente la comprobaci on de validez del ndice usado al acceder a un elemento. Los
ndices negativos permiten acceder a elementos del vector desde el nal (por ejemplo,
1 es el ndice del ultimo elemento y 2 el del pen ultimo).
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 61
En Python los vectores reciben el nombre de listas. Podemos crear vectores (o listas,
seg un la nomenclatura Python) encerrando sus elementos entre corchetes y separ andolos
con comas o a partir de iterables cualesquiera, con el constructor de listas list, o con la
sintaxis de listas comprensivas:
print([0, 2, 4, 8], list(2*i for i in range(4)), [2*i for i in range(4)])
[0, 2, 4, 8] [0, 2, 4, 6] [0, 2, 4, 6]
Las operaciones de acceso a elemento, asignaci on de valor a elemento y consulta a la
longitud se ejecutan en tiempo O(1). Las listas Python implementan adem as un amplio
repertorio de operaciones, algunas de las cuales se recogen en la tabla 3.1.
El operador de repetici on (*) permite reservar memoria para una lista o vector cuyo
contenido se jar a m as adelante:
a = [None] * 10
a[0] = 1
a[9] = 1
Las listas Python pueden redimensionarse para ajustar su tama no a las necesidades
conforme estas se presentan. Las listas gestionan internamente un vector con una canti-
dad de celdas que puede ser mayor que las realmente utilizadas para almacenar valores.
Se distingue, pues, entre longitud (n umero de celdas usadas) y capacidad (n umero de
celdas reservadas). Cuando se extiende la lista a nadiendo nuevos elementos se recurre a
las celdas no usadas del vector. Si todas ellas han sido consumidas, el espacio reservado
se redimensiona, es decir, se solicita un nuevo vector con el tama no deseado y se copia
el contenido del antiguo vector en el nuevo. Python gestiona internamente el redimen-
sionamiento con la t ecnica del doblado, es decir, cuando se a nade un elemento a una
lista cuya capacidad se ha agotado se reserva memoria para c veces la capacidad actual.
As asegura un coste temporal total O(n) al a nadir, uno a uno, n elementos a un vector
vaco. El coste temporal amortizado por adici on de un elemento es, pues, O(1) (v ease el
apartado 2.5.3).
Ciertos lenguajes, como Java o C#, distinguen entre vectores (en ingl es,
array) y listas. Los vectores presentan talla inmutable: una vez reserva-
da la memoria para un vector, esta no puede modicarse. En estos lenguajes, las
listas s pueden cambiar de talla. Python ofrece soporte para vectores con el m odu-
lo array, pero prescindiremos de el y usaremos los t erminos vector o lista para
referirnos a la misma estructura de datos.
El int erprete de Python es c odigo abierto escrito en C que puede consultarse
para conocer en detalle la implementaci on de cada una de las estructuras de datos
nativas. El c odigo fuente de los vectores Python, por ejemplo, se encuentra en los
cheros listobject.c (en el directorio Objects) y listobject.h (del directorio
Modules).
62 Apuntes de Algoritmia 28 de septiembre de 2009
Vector (tipo list de Python)
Operaci on Acci on Coste
Acceso
A[i] Indexaci on. Accede al elemento de ndice i. (1)
A[i:j] Corte. Proporciona el vector [A[i], A[i+1], ..., A[j-1]]. (j i)
len(A) Talla. Devuelve el n umero de elementos de A. (1)
min(A) Mnimo. Devuelve el elemento mnimo de A. (n)
max(A) M aximo. Devuelve el elemento m aximo de A. (n)
Asignaci on
A[i] = v Asignaci on al elemento de ndice i del valor v. (1)
A[i:j] = B Sustituci on de corte. Sustituye A[i:j] por B. O(n + m)
A.append(v) Adici on por la cola. A nade el elemento v a A. O(n)
Pertenencia
v in A Pertenencia. Indica si v es un elemento de A. O(n)
v not in A Pertenencia. Indica si v no es un elemento de A. O(n)
i = A.index(v) Obtenci on de ndice de elemento. Obtiene el ndice de la primera aparici on
del elemento v en A.
O(n)
Iteraci on
for v in A: v va tomando los valores almacenados en el vector. O(n)
Otras
C = A * i Repetici on. A concatenado consigo mismo i veces. (n i)
C = A + B Concatenaci on. Vector con los elementos de A seguidos de los de B. (n + m)
reversed(A) Inversi on. Devuelve un iterador sobre los elementos de A en orden inverso. O(n)
A.reverse() Inversi on in situ. O(n)
sorted(A) Ordenaci on. Devuelve un nuevo vector con los elementos de A en orden no
decreciente (por defecto). Admite tres par ametros opcionales con nombre:
key: funci on que se aplica a los elementos antes de su comparaci on;
cmp: funci on que ha de tener dos par ametros, x e y, y devolver valor
negativo si x debe ir delante de y, 0 si x e y son iguales, y valor positivo
si x debe ir detr as de y (permite as denir el criterio de ordenaci on);
reverse: valor booleano. Si vale cierto, invierte el resultado.
O(n lg n)
A.sort() Ordenaci on in situ. (Admite los par ametros opcionales de sorted). O(n lg n)
Tabla 3.1: Operaciones sobre vectores (que en su implementaci on Python reciben el nombre de listas) y sus costes
temporales respectivos. A y B son vectores de tallas n y m respectivamente, v es una variable de cualquier tipo e i es
un entero. (*) El coste temporal de n adiciones (m etodo append) o n extracciones del ultimo elemento (m etodo pop) de
un vector vaco es O(n), por lo que cada operaci on presenta un coste temporal amortizado O(1).
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 63
3.2.2. Lista enlazada
Una lista enlazada es una referencia a una secuencia de nodos, cada uno de los cua-
les alberga un valor y una referencia al siguiente nodo. Naturalmente, el ultimo nodo no
apunta a ning un otro. En una lista doblemente enlazada cada nodo alberga, adem as, una
referencia al nodo que le precede. Una lista con puntero a cabeza y cola (simple o doble-
mente enlazada) mantiene una referencia al primer elemento y otra al ultimo elemento.
La gura 3.1 muestra una representaci on gr aca de una lista doblemente enlazada con
puntero a cabeza y cola que almacena la secuencia 3, 8, 3, 2.
head
tail
L
3
prev next
8
prev next
3
prev next
2
prev next
Figura 3.1:
Lista doblemen-
te enlazada.
Las listas enlazadas permiten efectuar ecientemente ciertas operaciones de manipu-
laci on de la secuencia. Presentamos una implementaci on Python de la lista doblemente
enlazada con puntero a cabeza y cola. Empezamos por denir los elementos b asicos: los
nodos (que son instancias de una clase denida dentro de la clase principal) y el cons-
tructor, que inicializa la cabeza, la cola y un campo que almacena la longitud de la lista
(y evita recorrerla cada vez que se desea conocer el n umero de elementos que contiene):
algoritmia/datastructures/sequences.py
from ..collections import MutableSequence, namedtuple
class LinkedList(MutableSequence):
class Node(object):
slots = ("value", "prev", "next")
def init (self , value, prev, next):
self .value, self .prev, self .next = value, prev, next
def init (self , seq: "iterable<T>"=[]):
self . head = self .last = None
self . length = 0
for item in seq: self .append(item)
El constructor permite inicializar la lista con una serie de valores. Estos se a naden por
la cola de uno en uno con el m etodo append (y aunque se hereda de MutableSequence una
implementaci on de append, preferimos ofrecer una propia aqu):
algoritmia/datastructures/sequences.py
class LinkedList(MutableSequence):
...
def append(self , value: "T"):
node = LinkedList.Node(value, self .last, None)
if self .last == None:
self . head = self .last = node
else:
self .last.next = node
64 Apuntes de Algoritmia 28 de septiembre de 2009
self .last = node
self . length += 1
Las funciones que eliminan elementos de la lista se apoyan en un m etodo privado,
remove, que elimina un nodo cualquiera de la lista. (Aunque de los m etodos pop o remove
no se necesita implementaci on porque se hereda de MutableSequence, ofrecemos nuestras
propias implementaciones.)
algoritmia/datastructures/sequences.py
class LinkedList(MutableSequence):
...
def remove(self , node: "Node<T>"):
if node.prev: node.prev.next = node.next
if node.next: node.next.prev = node.prev
if self . head == node: self . head = node.next
if self .last == node: self .last = node.prev
def pop(self ) -> "T":
if self .last == None: raise IndexError(pop from an empty linked list)
value = self .last.value
self . remove(self .last)
self . length -= 1
return value
def remove(self , value: "T"):
if self . head != None:
node = self . head
while node.value != value and node.next: node = node.next
if node.value == value:
self . remove(node)
self . length -= 1
return
raise ValueError("Cannot remove {!r}".format(value))
El m etodo privado get node at permite implementar f acilmente los m etodos de acce-
so, modicaci on y borrado de un elemento por su ndice. Otro m etodo, insert, permite
a nadir elementos donde se desee. N otese que, por eciencia, get node at recorre la lista
desde el principio o desde el nal en funci on del ndice del elemento buscado. A un as,
acceder al elemento de ndice i es costoso: requiere tiempo que depende del valor de i.
algoritmia/datastructures/sequences.py
class LinkedList(MutableSequence):
...
def get node at(self , i: "int") -> "Node<T>":
if i > self . length // 2:
i -= self . length
elif i < -self . length // 2:
i += self . length
if i >= 0:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 65
node, j = self . head, 0
while node and j < i: node, j = node.next, j + 1
else:
i, node, j = -i, self .last, 1
while node and j < i: node, j = node.prev, j + 1
return node
def getitem (self , i: "int") -> "T":
node = self . get node at(i)
if node == None: raise IndexError(No item at position {!r}.format(i))
return node.value
def setitem (self , i: "int", value: "T") -> "T":
node = self . get node at(i)
if node == None: raise IndexError(No item at position {!r}.format(i))
node.value = value
return value
def delitem (self , i: "int"):
node = self . get node at(i)
if node == None: raise IndexError(No item at position {!r}.format(i))
self . remove(node)
self . length -= 1
def insert(self , i: "int", item: "T"):
if i == 0:
newnode = LinkedList.Node(item, None, self . head)
if self . head != None:
self . head.prev = newnode
self . head = newnode
else:
self . head = self .last = newnode
self . length += 1
elif i == self . length:
self .append(item)
else:
node = self . get node at(i-1)
if node == None: raise IndexError(Cannot insert at position {!r}.format(i))
newnode = LinkedList.Node(item, node, node.next)
if node.next: node.next.prev = newnode
node.next = newnode
if node == self .last: self .last = newnode
self . length += 1
Los restantes m etodos permiten borrar la lista, iterar sobre ella, preguntar por la per-
tenencia a ella de un valor, conocer la longitud y representarla como cadena:
66 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/datastructures/sequences.py
class LinkedList(MutableSequence):
...
def clear(self ):
self . head = self .last = None
self . length = 0
def iter (self ) -> "iterable<T>":
node = self . head
while node != None:
yield node.value
node = node.next
def contains (self , value: "T") -> "bool":
return any(value == v for v in self )
def len (self ) -> "int":
return self . length
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , list(self ))
El acceso al primer o ultimo elemento, la inserci on en primera o ultima posici on (por
la izquierda) o el borrado del primer o ultimo elementos son operaciones que requieren
tiempo constante.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12 Analiza razonadamente el coste temporal de cada una de las operaciones que hemos deni-
do en la clase LinkedList.
13 El m etodo reverse se hereda de MutableSequence. Esta es su implementaci on (seg un el m odulo
abcoll.py de la librera est andar):
def reverse(self ):
n = len(self )
for i in range(n//2):
self [i], self [n-i-1] = self [n-i-1], self [i]
Se trata de una rutina que requiere tiempo O(n
2
), donde n es el n umero de elementos de la
lista. Sabras decir por qu e? Podras ofrecer una implementaci on alternativa para listas doble-
mente enlazadas que se ejecute en tiempo O(n)?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3. Colas
En ocasiones necesitaremos colecciones de elementos a las que se accede de un modo
muy restringido. Las denominadas colas FIFO (por rst-in, rst-out) siguen la polti-
ca primero en entrar, primero en salir. Los elementos entran por un extremo de la cola
y salen por el otro (como en las colas de espera para adquirir entradas de cine). Las colas
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 67
LIFO (por last-in, rst-out) siguen la poltica ultimo en entrar, primero en salir. En
ellas los elementos entran y sale por el mismo extremo (como en una pila de vajilla a la
que podemos a nadir un plato dej andolo encima de los dem as y extraer unicamente el
plato que ocupa la cima).
Los vectores (listas Python) pueden servir de soporte para implementar colas LIFO
y colas FIFO. Pero a nadir o eliminar elementos por el principio requiere ejecutar O(n)
pasos, siendo n la longitud del vector. Otras estructuras de datos permiten realizar todas
las operaciones en tiempo constante.
Los dos tipos de cola implementar an esta interfaz:
algoritmia/datastructures/queues.py
class IQueue(Sized, Iterable):
@abstractmethod
def push(self , item: "T"): raise NotImplementedError
@abstractmethod
def pop(self ) -> "T": raise NotImplementedError
@abstractmethod
def top(self ) -> "T": raise NotImplementedError
3.3.1. Cola FIFO
Las colas FIFO (rst-in rst-out), o colas propiamente dichas, ofrecen un conjunto de ope-
raciones restringido al constructor y los m etodos push y pop, que en la literatura tambi en
se denominan enqueue y dequeue, respectivamente. Los elementos salen de la cola en
el mismo orden en el que ingresan, de ah el t ermino FIFO con el que se las conoce. Por
conveniencia, usaremos tambi en el m etodo len para conocer el n umero de elementos
de una cola y si esta est a vaca o no.
En la gura 3.2 mostramos una representaci on gr aca de una cola FIFO y el resultado
de efectuar algunas operaciones sobre ella.
Q
Q = FIFO()
Q
Q.push(1)
1
Q
Q.push(2)
1 2
(a) (b) (c)
Q
Q.push(3)
1 2 3
Q
v = Q.pop()
2 3
v 1
Q
v = Q.pop()
3
v 2
(d) (e) (f)
Figura 3.2: Traza de operaciones sobre
una cola FIFO. Los elementos entran
por la derecha y salen por la izquierda.
Nuestra implementaci on por defecto se basa en el uso de listas doblemente enlazadas
con puntero a cabeza y cola. En ellas podemos insertar por cualquiera de los dos extremos
y extraer el elemento que se encuentra en cualquiera de ellos:
68 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/datastructures/queues.py
class FIFO(IQueue):
def init (self , data: "iterable<T>"=[], **kw):
get factories(self , kw, sequenceFactory=lambda data: LinkedList(data));
self . seq = self .sequenceFactory(data)
def push(self , item: "T"):
self . seq.append(item)
def pop(self ) -> "T":
v = self . seq[0]
del self . seq[0]
return v
def top(self ) -> "T":
return self . seq[0]
def getitem (self , index: "int") -> "T":
return self . seq[index]
def len (self ) -> "int":
return len(self . seq)
def iter (self ) -> "iterable<T>":
for item in self . seq: yield item
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , list(self . seq))
0 1 2 3 4 5
0 False True False True False False
1 False False False False True False
2 False False False False True True
3 True True False False False False
4 False False False True False False
5 False False False False False True
AdjacencyMatrixDigraph
Operaci on Coste temporal
G = AdjacencyMatrixDigraph(V, E) O(V
2
)
(u,v) in G.E O(1)
for (u,v) in G.E: O(V
2
)
G.succs(u) O(V)
G.preds(v) O(V)
G.out degree(u) O(V)
G.in degree(v) O(V)
G.V.add(v) O(V)
G.V.remove(v) O(V)
G.E.add((u,v)) O(1)
G.E.remove((u,v)) O(1)
Coste espacial O(V
2
)
(a) (b)
Figura 3.7: (a) Representaci on de las aristas del grafo de la gura B.9 con una matriz de adyacencia. El valor True en la
celda (i, j) de la matriz signica que hay una arista uniendo los v ertices i y j. El valor False signica que no hay arista.
(b) Coste temporal y espacial de las operaciones sobre un grafo implementado con una matriz de adyacencia.
3.7.4. Digrafos con tabla de dispersi on en la correspondencia de
v ertices a conjuntos de sucesores. . .
Por defecto usamos un diccionario para asociar a cada v ertice su conjunto de sucesores.
Y tambi en por defecto implementamos los conjuntos de sucesores con tablas de disper-
si on. Pero podemos barajar otras posibilidades. La elecci on de las estructuras de datos
tendr a un impacto en el coste de las operaciones sobre los grafos. Consideremos diferen-
tes opciones para los conjuntos de sucesores.
. . . y conjuntos de sucesores basados en listas enlazadas
Podemos representar el conjunto de v ertices sucesores de un v ertice cualquiera con un
conjunto implementado con listas enlazadas. La gura 3.8 (a) muestra la representaci on
con listas de adyacencia del digrafo de la gura B.9 suponiendo un conjunto de v ertices
enteros y contiguos. (La gura 3.8 es una simplicaci on en tanto que la correspondencia
v ertice-sucesores es decir, el campo succs puede resultar m as compleja y basarse,
por ejemplo, en una tabla de dispersi on.) La denici on de esta clase es sencilla gracias al
modo en que hemos denido AdjacencyDigraph:
94 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/datastructures/graphs.py
from .sets import LinkedListSet
...
class LinkedListSetAdjacencyDigraph(AdjacencyDigraph):
def init (self , V=[], E=[], directed=True, **kw):
kw[setFactory] = lambda V: LinkedListSet()
super(LinkedListSetAdjacencyDigraph, self ). init (V, E, directed, **kw)
En la gura 3.8 se recoge el coste temporal esperado de las operaciones sobre Linked-
ListSetAdjacencyDigraph. N otese que al usar un mapeo implementado con tablas de dis-
persi on, el acceso a los sucesores de un v ertice es una operaci on con coste esperado O(1).
Pero no podemos garantizar tiempo constante para el peor de los casos.
0
1
2
3
4
5
1
sig
3
sig
4
sig
4
sig
5
sig
0
sig
1
sig
3
sig
5
sig
LinkedListSetAdjacencyDigraph
Operaci on Coste temporal
G = LinkedListSetAdjacencyDigraph(V, E) O(V +E)
(u,v) in G.E O(out degree(u))
for (u,v) in G.E: O(V +E)
G.succs(u) O(out degree(u))
G.preds(v) O(V +E)
G.out degree(u) O(out degree(u))
G.in degree(v) O(V +E)
G.V.add(v) O(1)
G.V.remove(v) O(V +E)
G.E.add edge((u,v)) O(out degree(v))
G.E.add unchecked((u,v)) O(1)
G.E.remove((u,v)) O(out degree(v))
Coste espacial O(V +E)
(a) (b)
Figura 3.8: (a) Representaci on esquem atica del grafo de la gura B.9 con listas enlazadas de adyacencia. (b) Coste
temporal y espacial de las operaciones sobre grafos con la implementaci on LinkedListSetAdjacencyDigraph suponiendo
que acceder al conjunto de sucesores de un v ertice es una operaci on con coste temporal O(1).
. . . y conjuntos de sucesores basados en listas
La gura 3.9 (a) muestra esquem aticamente un grafo en el que los conjuntos de adyacen-
cia se implementan con listas. Su codicaci on en Python es sencilla:
algoritmia/datastructures/graphs.py
from .sets import ListSet
...
class ListSetAdjacencyDigraph(AdjacencyDigraph):
def init (self , V=[], E=[], directed=True, **kw):
kw[setFactory] = lambda V: ListSet()
super(ListSetAdjacencyDigraph, self ). init (V, E, directed, **kw)
En la gura 3.9 (b) se recoge el coste temporal esperado y amortizado (cuando se
a nade aristas) de las operaciones sobre ListSetAdjacencyDigraph.
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 95
0
1
2
3
4
5
3
0
1
1
4
0
4
0
5
1
1
0
0
1
3
0
5
0
ListAdjacencyDigraph
Operaci on Coste temporal
G = ListSetAdjacencyDigraph(V, E) O(V +E)
(u,v) in G.E O(out degree(u))
for (u,v) in G.E: O(V +E)
G.succs(u) O(out degree(u))
G.preds(v) O(V +E)
G.out degree(u) O(1)
G.in degree(v) O(V +E)
G.V.add(v) O(1)
G.V.remove(v) O(V +E)
G.E.add((u,v)) O(out degree(v))
G.E.add unchecked((u,v)) O(1)
= {(v, u) (u, v) E}, es decir, usamos un nuevo mapeo que asocia a cada
v ertice sus predecesores:
algoritmia/datastructures/graphs.py
class InvAdjacencyDigraph(AdjacencyDigraph):
def init (self , V, E, directed=True, **kw):
super(InvAdjacencyDigraph, self ). init (V, E, directed, **kw)
if directed:
self . preds = self .mappingFactory(self .V)
for v in self . succs: self . preds[v] = self .setFactory(self .V)
for (u, v) in self .E: self . preds[v].add(u)
else:
self . preds = self . succs
def preds(self , v: "T") -> "iterable<T>":
return self . preds[v]
def in degree(self , v: "T") -> "int":
return len(self . preds[v])
def add vertex(self , v: "T"):
if v not in self . succs:
self . succs[v] = self .setFactory(self .V)
if self . directed: self . preds[v] = self .setFactory(self .V)
def remove vertex(self , v: "T"):
if v in self . succs:
for u in self . succs[v]:
self . preds[u].discard(v)
if self . directed:
for u in self . preds[v]:
self . succs[u].discard(v)
del self . preds[v]
del self . succs[v]
def add edge(self , edge: "(T, T)"):
self . succs[edge[0]].add(edge[1])
if self . directed: self . preds[edge[1]].add(edge[0])
else: self . succs[edge[1]].add(edge[0])
def add edge unchecked(self , edge: "(T, T)"):
if add_unckecked in self . succs. class . dict :
self . succs[edge[0]].add unchecked(edge[1])
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 97
if not self . directed: self . succs[1].add unchecked(edge[0])
else: self . succs[edge[1]].add unchecked(edge[0])
else:
self . add edge(edge)
def remove edge(self , edge: "(V, V)"):
self . succs[edge[0]].discard(edge[1])
if not self . directed: self . preds[edge[1]].discard(edge[0])
En este tipo de digrafos, el coste del acceso a los predecesores y el c alculo de su
n umero es igual al del acceso a los sucesores. Aunque el coste espacial sigue siendo
O(V + E), hemos de tener en cuenta que consume el doble de memoria que la re-
presentaci on basada en conjuntos de adyacencia sin inversa.
Los costes temporales y espacial de la estructura se muestran en la tabla 3.8.
InvAdjacencyDigraph
Operaci on Coste temporal
G = InvAdjacencyDigraph(V, E) O(V +E)
(u,v) in G.E O(1)
for (u,v) in G.E: O(V +E)
G.succs(u) O(out degree(u))
G.preds(v) O(in degree(v))
G.out degree(u) O(1)
G.in degree(v) O(1)
G.V.add(v) O(1)
G.V.remove(v) O(out degree(v) +in degree(v))
G.E.add edge((u,v)) O(1)
G.E.add unchecked((u,v)) O(1)
G.E.remove((u,v)) O(1)
Coste espacial O(V +E)
Tabla 3.8: Coste esperado de las operacio-
nes de InvAdjacencyDigraph asumiendo
las factoras de mapeo y conjuntos por
defecto.
3.7.6. Un estudio comparativo y algunas conclusiones
Hemos considerado numerosas implementaciones diferentes para los grafos. Dependien-
do de las circunstancias de uso una de ellas puede resultar preferible a las otras y no
podemos inclinarnos, sin m as, por una sola como ideal para toda situaci on imaginable.
Si nos preocupa el consumo de memoria y el grafo es disperso, por ejemplo, las ma-
trices de adyacencia no parecen una buena elecci on. No obstante, su gran eciencia (en el
peor de los casos) para determinar la pertenencia de una arista al grafo podra compensar
este problema si se usa en un algoritmo que efect ua esta operaci on numerosas veces.
La gesti on de las listas enlazadas comporta una mayor lentitud (en la pr actica) en
operaciones de recorrido de sucesores/predecesores y un mayor consumo de memoria
por la gesti on de punteros. No obstante, desde el punto de vista del peor de los casos la
adici on o eliminaci on de aristas es m as eciente en la pr actica que si usamos vectores:
basta un recorrido de una lista, la reserva de un nuevo nodo y el ajuste de un par de
98 Apuntes de Algoritmia 28 de septiembre de 2009
punteros, cuando los vectores pueden exigir la reserva y copiado de una gran zona de
memoria (por la t ecnica del doblado).
Las implementaciones de los grafos que mantienen explcitamente los conjuntos de
predecesores consumen el doble de memoria que aquellos que s olo almacenan los suce-
sores. En ciertos algoritmos s olo se recorren los v ertices en la direcci on de los sucesores,
por lo que no tiene sentido en tal caso el consumo de memoria adicional que supone el
almacenamiento de predecesores.
No obstante, y salvo que digamos lo contrario, usaremos en adelante la implementa-
ci on basada en conjuntos de adyacencia con inversa apoyadas en diccionario y conjuntos
con tablas de dispersi on. Suponen un compromiso razonable entre consumo de memoria
(es proporcional al n umero de v ertices y aristas) y velocidad de ejecuci on (ofrece tiem-
pos esperados constantes). Por comodidad, usaremos una clase con identicador Digraph
cuando usemos digrafos implementados con la clase InvAdjacencyDigraph. La clase Undi-
rectedGraph se usar a para los grafos no dirigidos:
algoritmia/datastructures/graphs.py
class Digraph(InvAdjacencyDigraph):
def init (self , V=[], E=[]):
super(Digraph, self ). init (V, E, directed=True)
def repr (self ) -> "str":
return self . class . name + (V={!r}, E={!r}).format(list(self .V), list(self .E))
class UndirectedGraph(AdjacencyDigraph):
def init (self , V=[], E=[]):
super(UndirectedGraph, self ). init (V, E, directed=False)
def repr (self ) -> "str":
return self . class . name + (V={!r}, E={!r}).format(list(self .V), list(self .E))
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23 Piensa en c omo se implementaran los m etodos de acceso convencionales en una nueva clase
para representar digrafos que almacenasen tanto una matriz de adyacencia como listas enlazadas
de adyacencia ordenadas. Con qu e coste podemos determinar la pertenencia de una arista a E y
obtener la relaci on de sucesores y predecesores de un v ertice cualquiera?
24 Sup on que hay un orden denido entre los v ertices de un digrafo. Rellena tablas de coste
temporal como las presentadas para una representaci on del grafo consistente en una tabla inde-
xada por los v ertices de arboles binarios de b usqueda bien balanceados, cada uno de los cuales
contiene los sucesores del v ertice correspondiente almacenados en un arbol de b usqueda bien
balanceado.
25 El cuadrado de un digrafo G = (V, E) se dene como G
2
= (V, E
) donde (u, w) E
si y
s olo si para alg un v V se cumple que (u, v) E y (v, w) E. La siguiente funci on recibe un
grafo G y devuelve el grafo G
2
:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 99
demos/datastructures/squaredgraph.py
from algoritmia.datastructures.graphs import Digraph
def squared graph(G):
Gsquared = Digraph(V=G.V)
for u in G.V:
for v in G.succs(u):
for w in G.succs(v):
Gsquared.E.add( (u,w) )
return Gsquared
if name == "__main__":
G = Digraph(E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
print(G:, G)
print(G al cuadrado:, squared graph(G))
G: Digraph(V=[0, 1, 2, 3, 4, 5], E=[(0, 1), (0, 3), (1, 4), (2, 4), (2, 5), (3
, 0), (3, 1), (4, 3), (5, 5)])
G al cuadrado: Digraph(V=[0, 1, 2, 3, 4, 5], E=[(0, 0), (0, 1), (0, 4), (1, 3)
, (2, 3), (2, 5), (3, 1), (3, 3), (3, 4), (4, 0), (4, 1), (5, 5)])
He aqu el cuadrado del grafo de la gura B.9:
0 1 2
3 4 5
Calcula el coste temporal de la funci on squared graph, justic andolo adecuadamente, en fun-
ci on de la implementaci on escogida para los grafos (sup on que los v ertices son enteros consecu-
tivos y que la correspondencia v ertice-sucesores se implementa con la clase IntKeyMapping): a)
Matriz de adyacencia. b) Vectores de adyacencia. c) Conjuntos de adyacencia con inversa imple-
mentados con tablas de dispersi on.
26 Las representaciones de grafos que hemos considerado mantienen en memoria, explcita-
mente, un conjunto de v ertices y otro de aristas (de diferentes formas y con diferentes prestacio-
nes). Sin embargo, algunos grafos poseen una estructura tal que resulta innecesario almacenar en
memoria ambos conjuntos. En su lugar, se pueden calcular al vuelo. Consideremos un grafo
G como el que mostramos a continuaci on, cuyo conjunto de v ertices es {0, 1, 2, . . . , n 1} para
alg un n positivo y en el que todo v ertice i, para 0 i < n 1, est a conectado con todos los
v ertices j que cumplen i < j < n. He aqu el caso particular en que n = 10:
0 1 2 3 4 5 6 7 8 9
Esta clase dene grafos como el descrito (sin considerar funciones de adici on/borrado de v ertices
y aristas):
100 Apuntes de Algoritmia 28 de septiembre de 2009
demos/datastructures/mygraph.py
class MyGraph:
def init (self , n):
self .n = n
def getattr (self , attr):
if attr == V:
return range(self .n)
elif attr == E:
return ((u,v) for u in range(self .n) for v in range(u+1,self .n))
def succs(self , u):
return range(u+1, self .n)
def preds(self , v):
return range(0, v)
def out degree(self , u):
return self .n - u - 1
def in degree(self , v):
return v
if name == "__main__":
G = MyGraph(5)
for v in G.V: print(v, end=" ")
print()
for (u, v) in G.E: print((u,v), end=" ")
print()
print(tuple(G.succs(1)))
print(G.out degree(1))
0 1 2 3 4
(0, 1) (0, 2) (0, 3) (0, 4) (1, 2) (1, 3) (1, 4) (2, 3) (2, 4) (3, 4)
(2, 3, 4)
3
Ya que no almacenamos los v ertices y aristas explcitamente, hemos a nadido un m etodo
getattr que da acceso a unos atributos virtuales V y E. Al acceder a V o E se crea un itera-
dor de v ertices o un iterador de aristas, respectivamente.
Qu e coste presenta cada uno de los m etodos de la clase MyGraph?
27 En el anterior ejercicio hemos visto como ciertos grafos estructurados pueden represen-
tarse con un consumo de memoria constante, generando la informaci on cuando se necesita. Hay
muchos otros tipos de grafos estructurados para los que no se precisa almacenar explcitamente
v ertices y aristas. Implementa y analiza el coste temporal de las operaciones b asicas de acceso
para los siguientes tipos grafos que pueden representarse con un coste espacial constante:
a) Digrafo mallado (o en malla o retcula) con m las y n columnas. He aqu un grafo como el
descrito con 4 las y 4 columnas:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 101
0, 0
0, 1
0, 2
0, 3
1, 0
1, 1
1, 2
1, 3
2, 0
2, 1
2, 2
2, 3
3, 0
3, 1
3, 2
3, 3
4, 0
4, 1
4, 2
4, 3
5, 0
5, 1
5, 2
5, 3
b) Grafos con n v ertices en el que cada v ertice i est a conectado a los v ertices i + 1, i + 2 e i + 3
(si estos valores son menores que n). He aqu un ejemplo de grafo como el descrito con 10
v ertices:
0 1 2 3 4 5 6 7 8 9
c) Grafos multietapa con n etapas y en los que la etapa i- esima contiene i v ertices y todo v ertice
de una etapa est a unido a todo v ertice de la siguiente. He aqu un grafo como el descrito con
4 etapas:
2, 2
2, 0
2, 1 1, 1
1, 0
3, 3
3, 2
0, 0
3, 1
3, 0
d) Digrafo multietapa con m etapas y n v ertices en cada etapa de modo que todo v ertice de una
etapa est a conectado con todo v ertice de la siguiente. He aqu un grafo como el descrito con 7
etapas y 4 v ertices por etapa:
1, 3
3, 0
2, 1
6, 2
5, 1
0, 3
4, 0
1, 2
3, 3 6, 3
5, 0
2, 2
4, 1 1, 1
3, 2
0, 0 6, 0
2, 3
4, 2
1, 0
5, 3
0, 1 6, 1 3, 1
2, 0
4, 3
5, 2 0, 2
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.7.7. Digrafo ponderado
Un digrafo ponderado es un digrafo G = (V, E) al que asociamos una funci on de las aris-
tas en los reales, d : E R, a la que denominamos funci on de ponderaci on. Es posible
102 Apuntes de Algoritmia 28 de septiembre de 2009
asociar m as de una funci on de ponderaci on a un mismo digrafo y obtener as diferentes
digrafos ponderados que comparten un mismo n ucleo. Pensemos, por ejemplo, en un
mapa de carreteras: podemos ponderar cada carretera con su longitud en kil ometros o
con el tiempo medio que requiere recorrerla. Para facilitar el trabajo con diferentes fun-
ciones de ponderaci on hemos optado por separar estas de la denici on del propio grafo.
Aquellos algoritmos que manipulen grafos ponderados recibir an el grafo y la funci on de
ponderaci on como datos separados. As, un algoritmo que calcule el camino m as corto
entre dos ciudades podr a devolver el camino que recorre menos kil ometros o el camino
que puede recorrerse en menor tiempo (pueden no coincidir) en funci on de si suminis-
tramos una funci on de ponderaci on u otra.
Esta codicaci on de las expresiones aritm eticas como arboles es muy similar
a la propia del lenguaje de programaci on Lisp. Por ejemplo, la expresi on 2
- 5 + 3 * 6 se codica en Lisp con esta lista: (+ (- 2 5) (* 3 6)).
116 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 3.15:
Arbol de sintaxis abstracta para la expresi on Python 2 - 5 + 3 * 6.
+
- *
5 3 6 2
De pasar de la expresi on a su arbol de sintaxis abstracta se encargan las primeras eta-
pas del int erprete o compilador del lenguaje. Las t ecnicas para efectuar esa traducci on
son bastante complejas y quedan fuera del objetivo de este texto. No obstante, adjunta-
mos el c odigo que permite analizar expresiones con par entesis y operadores de suma,
resta, producto y divisi on:
demos/datastructures/expressioneval.py
from algoritmia.datastructures.queues import LIFO
from algoritmia.datastructures.trees import ListOfListsTree
class ExpressionEvaluator:
def init (self , **kw):
get factories(self , kw, lifoFactory=lambda: LIFO())
def tokenize(self , expression: "str"):
i = 0
while i < len(expression):
lexeme = []
if 0 <= expression[i] <= 9:
while i < len(expression) and 0 <= expression[i] <= 9:
lexeme.append(expression[i])
i += 1
yield int(.join(lexeme))
elif expression[i] in +*-/():
yield expression[i]
i += 1
else:
i += 1
def parse(self , expression: "str"):
S = self .lifoFactory()
tree = []
op = {+: 0, -: 0, *: 1, /: 1}
for token in self .tokenize(expression):
if type(token) == type(0):
tree.append([token])
elif token in op:
while len(S) > 0 and S.top() in op and op[token] <= op[S.top()]:
tree[-2:] = [[S.pop(), tree[-2], tree[-1]]]
S.push(token)
elif token == (:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 117
S.push(()
elif token == ):
while S.top() != (:
tree[-2:] = [[S.pop(), tree[-2], tree[-1]]]
S.pop()
while len(S) > 0:
tree[-2:] = [[S.pop(), tree[-2], tree[-1]]]
return ListOfListsTree(tree[0])
La evaluaci on de la expresi on puede realizarse efectuando un recorrido del arbol en
postorden. Si aplicamos dicho recorrido al arbol del ejemplo y emitimos las races de los
arboles, observaremos esta secuencia de nodos: 2, 5, -, 3, 6, *, +. Es una secuencia intere-
sante, pues codica las mismas operaciones indicadas en la expresi on, pero en notaci on
polaca inversa. Esta notaci on se basa en el uso de una pila con resultados intermedios
en la que vamos apilando los n umeros que encontramos y operando con los valores api-
lados cuando encontramos una operaci on. La operaci on suma, por ejemplo, extrae los
dos elementos de la cima de la pila, los suma y deja el resultado en la pila.
La siguiente funci on eval ua expresiones aritm eticas mediante un recorrido en postor-
den:
demos/datastructures/expressioneval.py
from algoritmia.treetraversals import PostorderTreeTraverser
...
def evaluate(self , exp: "str") -> "int":
tree = self .parse(exp)
stack = self .lifoFactory()
visitor = lambda t: self .process root(t, stack=stack)
for dummy in PostorderTreeTraverser().traverse(tree, visitor): pass
return stack.pop()
def process root(self , tree, stack):
if isinstance(tree.root, str) and tree.root in "+-*/":
a, b = stack.pop(), stack.pop()
if tree.root == +: stack.push(b + a)
elif tree.root == -: stack.push(b - a)
elif tree.root == *: stack.push(b * a)
else: stack.push(b // a)
else:
stack.push(tree.root)
Y he aqu el resultado de una sencilla prueba:
demos/datastructures/expressioneval.py
if name == "__main__":
ee = ExpressionEvaluator()
exp = "2 - 5 + 3 * 6"
print({} -> {}.format(ee.parse(exp), ee.evaluate(exp)))
ListOfListsTree([+, [-, [2], [5]], [*, [3], [6]]]) -> 15
118 Apuntes de Algoritmia 28 de septiembre de 2009
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
29 Un lenguaje ensamblador permite ejecutar operaciones aritm eticas con una pila. Las instruc-
ciones para apilar y desapilar son PUSH valor y POP, respectivamente. Las siguientes instruccio-
nes toman dos elementos de la pila, les aplican una operaci on (la cima de la pila es el operando
derecho y el elemento que hay debajo es el operando izquierdo) y dejan el resultado en la pila:
ADD (suma), SUB (resta), MUL (producto) y DIV (divisi on).
Dise na una funci on que reciba una expresi on y muestre por pantalla las instrucciones de en-
samblador que permiten evaluarla en el computador. Por ejemplo, si le suministramos la cadena
"2 - 5 + 3 * 6" mostrar a por pantalla el siguiente texto:
PUSH 2
PUSH 5
SUB
PUSH 3
PUSH 6
MUL
SUM
30 Dise na una funci on que reciba un arbol de sintaxis abstracta correspondiente a una expresi on
y muestre por pantalla esa misma expresi on en notaci on inja (es decir, la notaci on convencional).
Al recibir el arbol ["+", ["-", [2], [5]], ["*", [3], [6]]] mostrar a el texto 2-5+3*6.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10. Cola de prioridad
Dado un conjunto de elementos entre los que existe una relaci on de orden total que inter-
pretamos como que unos elementos son m as prioritarios que otros, una cola de prioridad
Q es una colecci on de elementos de dicho conjunto con m etodos para insertar elementos,
acceder al elemento de mayor prioridad y extraer dicho elemento. Hay dos interpretacio-
nes posibles de la prioridad: que se considere un elemento m as prioritario cuanto menor
es o, por el contrario, cuanto mayor es.
Las operaciones que ofrece una cola de prioridad son:
Q.add(item): a nade el elemento item a Q.
Q.opt(): devuelve el elemento optimo de cuantos hay en Q (el mnimo o el m axi-
mo, seg un interpretemos la prioridad).
Q.extract opt(): devuelve y elimina de Q su elemento m as prioritario.
len(Q): devuelve el n umero de elementos almacenados en Q.
As pues, esta es la interfaz que implementaremos:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 119
algoritmia/datastructures/priorityqueues.py
class IPriorityQueue(Sized):
@abstractmethod
def add(self , item: "T"): raise NotImplementedError
@abstractmethod
def opt(self ) -> "T": pass
@abstractmethod
def extract opt(self ) -> "T": pass
Podemos implementar colas de prioridad con, por ejemplo, vectores o listas enla-
zadas. El coste computacional, en tal caso, es demasiado alto si lo comparamos con el
de otras estructuras (v ease la tabla 3.9). Hay una implementaci on alternativa, basada en
arboles binarios, que garantiza costes logartmicos para las operaciones de inserci on y
borrado y coste constante para el acceso al elemento de valor mnimo (o m aximo). Esta
estructura eciente recibe el nombre de montculo.
Cola de prioridad
con vector no ordenado
Operaci on Coste
Inserci on O(1)
Acceso al m as prioritario O(n)
Extracci on del m as prioritario O(n)
Cola de prioridad
con vector ordenado
Operaci on Coste
Inserci on O(n)
Acceso al m as prioritario O(1)
Extracci on del m as prioritario O(1)
(a) (b)
Cola de prioridad con lista
enlazada no ordenada
Operaci on Coste
Inserci on O(1)
Acceso al m as prioritario O(n)
Extracci on del m as prioritario O(n)
Cola de prioridad con lista
enlazada y ordenada
Operaci on Coste
Inserci on O(n)
Acceso al m as prioritario O(1)
Extracci on del m as prioritario O(1)
(c) (d)
Tabla 3.9: Coste de las operaciones propias de una cola de prioridad. Con n denotamos el n umero de elementos de la cola.
Las tablas muestran el coste seg un la implementemos (a) con un vector no ordenado, (b) con un vector ordenado, (c)
con una lista enlazada no ordenada y (d) con una lista ordenada. EL coste marcado con asterisco es un coste amortizado.
3.10.1. Montculos
Un mn-montculo (o min-heap, en ingl es) es un arbol binario completo por la izquierda,
etiquetado con objetos sobre los que hay denida una relaci on de orden total, que satis-
face la propiedad mn-montculo, a saber: la etiqueta de todo nodo es menor o igual que las
etiquetas de sus nodos hijo. El hecho de que el mn-montculo sea un arbol binario, esto es,
de aridad acotada, permite una implementaci on eciente si representamos dicho arbol
con un vector (v ease la secci on 3.9.4). La gura 3.16 muestra un mn-montculo cuyos
nodos est an etiquetados con n umeros naturales. La relaci on de orden entre etiquetas es
120 Apuntes de Algoritmia 28 de septiembre de 2009
la relaci on es menor o igual que. El m ax-montculo se dene de forma an aloga al mn-
montculo, pero en su caso la etiqueta de todo nodo es mayor o igual que la de cualquiera
de sus nodos hijo.
Nosotros implementaremos un montculo al que se suministra una funci on min o max
para indicar si se trata de un mn-montculo o un m ax-montculo, aunque los ejemplos e
ilustraciones asumiran que tratamos con mn-montculos.
N otese que los hijos del nodo que ocupa la posici on de ndice i tienen ndices
2i y 2i + 1. No deberan ser 2i + 1 y 2i + 2? Del mismo modo, el padre
del nodo de ndice i es i/2. No debera ser (i 1)/2? Con los arboles binarios
podemos recurrir a esta relaci on padre-hijos alternativa ubicando la raz del arbol
en la posici on de ndice 1. Esto supone desaprovechar una celda del vector (la de
ndice 0), pero permite alcanzar una ligera mejora en velocidad al evitar una adici on
cuando se visitan los hijos de un nodo y una sustracci on cuando se desea acceder
al padre de un nodo.
Consulta y extracci on del menor elemento
Los m etodos de consulta del mnimo y extracci on del mnimo son sencillos: el primero
se limita a consultar una posici on del vector heap y el segundo hace esto mismo, ubica en
la primera posici on el ultimo elemento del vector y efect ua una llamada a heapify sobre
la raz:
algoritmia/datastructures/priorityqueues.py
class Heap(IPriorityQueue, Iterable):
...
def opt(self ) -> "T":
if self . size == 0: raise IndexError(opt from an empty heap)
return self . heap[1]
def extract opt(self ) -> "T":
if self . size == 0: raise IndexError(extract opt from an empty heap)
m = self . heap[1]
if self . size > 1: self . heap[1], self . heap[self . size] = self . heap[self . size], None
124 Apuntes de Algoritmia 28 de septiembre de 2009
self . size -= 1
if self . size > 1: self . heapify(1)
return m
El c alculo de la longitud y el del mnimo elemento se ejecutan en tiempo O(1). El
m etodo extract min se ejecuta en tiempo O(lg n), pues efect ua una llamada a heapify sobre
la raz del arbol.
Inserci on
La inserci on de un elemento consiste en a nadirlo al nal de heap y hacerlo ascender hasta
su ubicaci on denitiva:
algoritmia/datastructures/priorityqueues.py
class Heap(IPriorityQueue, Iterable):
...
def add(self , item: "T"):
if self . size + 1 == len(self . heap): self . heap.append(None)
i = self . size = self . size + 1
self . heap[i] = item
self . bubble up(i)
def bubble up(self , i: "int"):
parent = i // 2
while i > 1 and self . opt(self . heap[i], self . heap[parent]) != self . heap[parent]:
self . heap[i], self . heap[parent] = self . heap[parent], self . heap[i]
i, parent = parent, parent // 2
Dado que se pueden producir intercambios hijo-padre de una hoja a la raz, el coste
temporal es O(lg n).
En la implementaci on que presentamos el coste O(lg n) es un coste amortizado: si
a nadimos un elemento que obliga a redimensionar el vector self .heap, el coste en el peor
de los casos ser a O(n), pero m operaciones como esa tendr an un coste total O(m + n +
mlg(m + n)); es decir, coste amortizado logartmico con la talla del heap. Si se desea evi-
tar este coste en el peor de los casos, ser a preciso dimensionar inicialmente el vector heap
con un n umero de elementos (vacos) tan grande que jam as se exceda la capacidad del
montculo. En muchos problemas esto resulta factible. En la siguiente secci on presenta-
mos un nuevo constructor que pala este problema permitiendo denir una capacidad
inicial sucientemente grande y, adem as, presenta una interesante optimizaci on.
Inicializaci on eciente del montculo
En ciertas aplicaciones partimos de un mn-montculo vaco en el que vamos insertando
una serie de valores, sin efectuar extracci on o consulta alguna hasta m as tarde. Si efectua-
mos n inserciones en un mn-montculo (inicialmente vaco) tendremos un coste tempo-
ral O(n lg n). Es posible efectuar esa misma operaci on en tiempo O(n) si se suministra el
vector con los valores ya introducidos (aunque desordenados) y se usa reiteradamente
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 125
la funci on heapify desde los padres de las hojas hacia la raz. Tras la llamada al padre
de (una o) dos hojas, el arbol que forman estos (dos o) tres nodos observa la propiedad
del mn-montculo. Y si tenemos dos mn-montculos con un padre com un e invocamos
heapify sobre este nodo padre, el resultado es un arbol que incluye a todos los nodos y
cumple la condici on mn-montculo. As, pues, invocar heapify desde los niveles menos
profundos hacia la raz hace que, nalmente, tengamos un arbol con todos los nodos y
tal que satisface la propiedad del mn-montculo.
Redenimos el constructor para que acepte un vector con los datos que forman parte
del mn-montculo desde el principio:
algoritmia/datastructures/priorityqueues.py
from itertools import chain, repeat
...
class Heap(IPriorityQueue, Iterable):
def init (self , opt: "min or max", data: "iterable<T>"=[], capacity=0):
self . opt = opt
self . size = len(data)
self . heap = list(chain((None,), data, repeat(None, max(0, capacity-self . size))))
for i in range(self . size//2, 0, -1): self . heapify(i)
Hemos dicho que el coste de esta inicializaci on especial es O(n), pero como hay O(n)
nodos con al menos un hijo y sobre todos ellos invocamos heapify, que es O(lg n), ca-
be esperar que el coste temporal total sea O(n lg n). Afortunadamente no es as. Cuan-
do heapify se ejecuta sobre un nodo de profundidad d, ejecutar a un n umero de pasos
O(lg(n) d), pues recorrer a en el peor de los casos todos los nodos entre el y el padre de
una hoja. Si el montculo es completo (en aras de las simplicidad del an alisis), el n umero
de nodos de profundidad d es 2
d
, es decir, s olo hay un nodo de profundidad 0, dos de
profundidad 1, cuatro de profundidad 2. . . lo que signica que el n umero de nodos de
profundidad d es mayor que el n umero de nodos de cualquier profundidad inferior. Y a
mayor profundidad, menor coste temporal. El coste temporal para el peor caso ser a pro-
porcional a este sumatorio:
0d<lg n
2
d
(lg(n) d).
Si n es potencia de dos, el sumatorio vale 2n lg(n) 2, as que el n umero de pasos
ejecutados en la construcci on del mn-montculo es O(n).
Otros m etodos
Enriquecemos la clase con un iterador que recorre sus elementos en un orden arbitrario,
una que permite conocer el n umero de elementos almacenados y otra que muestra el
contenido del Heap como cadena:
126 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/datastructures/priorityqueues.py
class Heap(IPriorityQueue, Iterable):
...
def iter (self ) -> "iterable<T>":
for i in range(1, self . size+1): yield self . heap[i]
def len (self ) -> "int":
return self . size
def repr (self ) -> "str":
b = min if self . opt == min else max
return {}({}, {!r}).format(self . class . name , b, self . heap[1:self . size+1])
= (u
, v
= (T e) e
elimina el ciclo,
conecta a todos los v ertices y es, por tanto, un arbol de recubrimiento. El peso D(T
) es
menor que el de D(T), pues d(u, v) < d(u
). As pues, T no es el MST.
Corolario 4.1 El algoritmo de Bar uvka calcula el MST.
Demostraci on. Con cada iteraci on del bucle interno, el algoritmo escoge la arista e que
conecta una regi on con otra, ambas disjuntas en el grafo que vamos formando. La arista
escogida es la de menor peso de entre todas las posibles, as que es una arista del MST.
En cada paso, pues, se a nade una arista del MST al grafo resultante.
An alisis
El coste computacional del algoritmo requiere cierto detenimiento. La primera pregunta
que hemos de hacernos es cu antas veces se ejecuta el bucle while? A lo sumo, lg V ve-
ces, ya que con cada iteraci on el n umero de componentes conexos se reduce, como mni-
mo, a la mitad: cada componente se funde con otro. Como inicialmente hay V compo-
nentes, hay un m aximo de O(log V) iteraciones. Cada iteraci on dispara una b usqueda
de componentes por primero en anchura que supone un coste O(V + E). Encontrar
la arista de coste mnimo para cada componente supone, en total, O(E) pasos. El resto
de operaciones suponen un coste que no supera O(E). As pues, el coste temporal del
algoritmo de Bar uvka es O(E lg V).
Sobre el inter es pr actico del MST
Encontrar el MST presenta inter es en el dise no de redes de transporte o comunicaciones:
permite interconectar todos los v ertices de un grafo con el menor coste posible (menor
n umero de kil ometros asfaltados en el caso de carreteras o menor n umero de kil ometros
de cable en el caso de redes para la transmisi on de datos o voz). Si tuvi esemos que dise nar
una red de comunicaciones por cable que permita comunicar cualquier par de ciudades
en el mapa de la pennsula ib erica y el cable tuviera que discurrir siempre junto a una
carretera, el arbol de recubrimiento mnimo que se muestra en la gura 4.10 nos indica
por d onde tirar cable para que la cantidad total empleada sea mnima.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 193
Figura 4.10:
Arbol de recubrimiento mnimo para el mapa de
la pennsula ib erica. Se puede apreciar a simple vista que la
suma de longitudes de sus aristas es menor que la de cual-
quiera de los arboles de recubrimiento de la gura 4.9.
4.4. Componentes fuertemente conexos
Recordemos que, en un digrafo, un componente fuertemente conexo es un conjunto de
v ertices tales que todos son mutuamente alcanzables dos a dos. N otese que si u es alcan-
zable desde v y v es alcanzable desde u, todo v ertice en un camino de u a v (o de v a u)
es alcanzable desde cualquier otro v ertice del camino.
Es posible detectar los componentes fuertemente conexos en un digrafo a partir de
un arbol de recubrimiento calculado por exploraci on por primero en profundidad. La -
gura 4.11 (a) muestra un digrafo y la gura 4.11 (b), uno de sus arboles de recubrimiento.
0
1
2
3
4 5
6 7
8
0
1
2
3
4
5
8
6
7
0
1
2
3
4
5
8
6
7
0
1
2
3
4
5
8
6
7
0
1
2
3
4
5
8
6
7
(a) (b) (c) (d)
Figura 4.11: (a) Un grafo dirigido con
cinco componentes fuertemente cone-
xos (agrupados con trazo discontinuo).
(b)
Arbol dirigido y con raz inducido
por un recorrido por primero en pro-
fundidad desde el v ertice 0. (c)
Arbol al
que se han eliminado las aristas que se-
paran los componentes fuertemente co-
nexos. (d) El arbol con las aristas del
grafo (en trazo discontinuo) que no for-
man parte de el.
Si u y v forman parte de un mismo componente fuertemente conexo, o bien u es un
descendiente de v o bien es un ascendiente suyo en el arbol de recubrimiento. Ello es
consecuencia de que ha de haber, necesariamente, un camino de u a v y otro de v a u. Es
decir, los v ertices de un mismo componente fuertemente conexo no pueden aparecer en
sub arboles separados. De ello se deriva el que podemos romper el arbol por ciertas
aristas sin temor a que los v ertices de un mismo componente queden aislados. Elimi-
nando las aristas apropiadas, podemos dejar un sub arbol por componente fuertemente
conexo. Pero, qu e aristas eliminamos? En el arbol de la gura 4.11 (c) hemos eliminado
194 Apuntes de Algoritmia 28 de septiembre de 2009
las aristas (2, 3), (3, 4), (4, 6) y (5, 8) para obtener los 5 componentes fuertemente conexos
del grafo. C omo calcularlos autom aticamente?
Antes de seguir hemos de denir un nuevo concepto: el de cabeza de un compo-
nente. La cabeza de un componente es su v ertice de menor profundidad en el arbol (el
m as pr oximo a la raz). Las cabezas de los componentes de nuestro ejemplo son los v erti-
ces 0, 3, 4, 6 y 8. Las aristas que queremos de eliminar siempre apuntan a cabezas de
componentes. As pues, parece que encontrar las cabezas permite determinar qu e aristas
eliminar: las que les relacionan con sus respectivos padres. C omo encontrarlas?
Consideremos las aristas del digrafo que no forman parte del arbol. (En la gu-
ra 4.11 (d) se han representado con trazo discontinuo.) Estas aristas pueden clasicarse
en aristas hacia atr as, aristas hacia adelante y aristas de cruce. Las aristas hacia
atr as son las que llegan a un ascendiente de su nodo de partida, las aristas hacia adelante
llegan a un descendiente de su nodo de partida y las aristas de cruce son todas las dem as.
En la gura, las aristas (2, 0), (5, 4) y (7, 6) son aristas hacia atr as y la arista (7, 8) es una
arista de cruce. Hay una propiedad interesante en las aristas de cruce: una arista de cruce
no puede apuntar hacia un nodo que no ha sido visitado ya porque, de no ser as, habra
sido seleccionada antes en la exploraci on por primero en profundidad y sera una arista
hacia adelante.
Consideremos qu e ocurre con los nodos del sub arbol de raz v para un v cualquiera:
a) Si de ninguno parte una arista hacia atr as o de cruce que apunten fuera del propio
sub arbol, entonces v es una cabeza.
b) Si de alguno de ellos parte una arista hacia atr as que llega a un antecesor de v, el nodo
v no puede ser una cabeza.
c) Y si de alguno parte una arista de cruce a un nodo de otra rama, qu e podemos con-
cluir? Si tenemos la precauci on de eliminar los v ertices y aristas de un componente
fuertemente conexo tan pronto acabamos de visitar su cabeza, la conclusi on es que v
no puede ser una cabeza. Dado un descendiente u de v, consideremos un arco de cru-
ce (u, w) y sea z la cabeza del componente de w. O bien z es un ascendiente de v o z y
v est an en ramas distintas. Si est an en ramas distintas, w y z ya habr an sido visitados
y eliminados (recordemos que los arcos de cruce siempre llegan a nodos ya visita-
dos) lo que impide que ahora estemos considerando esa arista de cruce. As pues,
z ha de ser un ascendiente de v y podemos concluir que v no es una cabeza, pues
u w z v u forman un ciclo.
En el arbol del ejemplo, sabemos que los nodos 0 y 8 son cabezas porque no tienen
descendientes con arcos que apunten fuera de sus sub arboles. Y los nodos 2, 5 y 7 no son
cabezas porque de ellos parten aristas hacia atr as. El nodo 1 no es una cabeza porque
de un descendiente suyo (el nodo 2) parte una arista hacia atr as a un ascendiente suyo
(el nodo 0). El nodo 3 no tiene descendientes con aristas de cruce fuera del arbol que el
encabeza ni aristas hacia atr as hacia un ascendiente suyo, por lo que es una cabeza. Lo
mismo ocurre con el nodo 4. Dado que la arista (7, 8) ha sido eliminada al detectar el
componente fuertemente conexo {8}, el nodo 6 no tiene aristas de cruce ni aristas hacia
atr as que partan de su unico descendiente.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 195
Basta, pues, con considerar arcos hacia atr as y de cruce para averiguar si cada nodo
es o no es cabeza. Sea d[v] el n umero de v ertices visitados antes de visitar el v ertice v.
Los arcos hacia atr as o de cruce siempre llegan a v ertices con valor d inferior al propio
del v ertice de partida. Sea l[v] el menor valor de d[v] posible para un v ertice alcanzable
por una arista hacia atr as o de cruce desde un nodo de un sub arbol de v (siempre que
el v ertice alcanzable no haya sido eliminado previamente). Un v ertice v es una cabeza
cuando l[v]==d[v]. El valor de d[v] es de c alculo trivial y el de l[v] se puede calcular
combinando los de los descendientes de v y los de arcos hacia atr as o de cruce:
algoritmia/graphalgorithms/connectedcomponents.py
from algoritmia.datastructures.queues import LIFO
...
class StrongConnectedComponentsFinder:
def init (self , **kw):
get factories(self , kw, setFactory=lambda V: set(),
lifoFactory=lambda V: LIFO(),
mappingFactory=lambda V: dict())
def strong connected components(self , G: "Digraph<T>") -> "iterable<iterable<T>>":
Q = self .lifoFactory(G.V)
d, l = self .mappingFactory(G.V), self .mappingFactory(G.V)
visited, dead = self .setFactory(G.V), self .setFactory(G.V)
comps = []
def strong components(u):
visited.add(u)
Q.push(u)
d[u] = len(d)
l[u] = d[u]
for v in G.succs(u):
if v not in dead:
if v not in visited:
strong components(v)
l[u] = min(l[u], l[v])
else:
l[u] = min(l[u], d[v])
if l[u] == d[u]:
comp = []
while True:
v = Q.pop()
comp.append(v)
dead.add(v)
if v == u: break
comps.append(comp)
strong components(list(G.V)[0])
return (tuple(comp) for comp in comps)
196 Apuntes de Algoritmia 28 de septiembre de 2009
demos/graphalgorithms/strongconncomps.py
from algoritmia.datastructures.graphs import Digraph
from algoritmia.graphalgorithms.connectedcomponents import StrongConnectedComponentsFinder
G = Digraph(E=[(0,1),(1,2),(1,3),(2,0),(2,3),(3,4),(3,6),\
(4,5),(4,6),(5,4),(5,8),(6,7),(7,6),(7,8)])
sccf = StrongConnectedComponentsFinder()
print([list(comp) for comp in sccf .strong connected components(G)])
[[8], [7, 6], [5, 4], [3], [2, 1, 0]]
El coste de este algoritmo es O(V +E).
0k<V
M
i,k
M
k,j
,
es decir, es True si y s olo si existe al menos un v ertice k tal que (i, k) y (k, j) son aristas.
As pues, M
2
, representa un nuevo digrafo, que mostramos en la gura 4.14 para el grafo
de ejemplo, en el que dos v ertices est an conectados si y s olo si hay un camino que los une
y est a formado por exactamente dos aristas.
0 1 2
3 4 5
Figura 4.14: Cuadrado del digrafo de la gura 4.13 (a).
Qu e es M M
2
? Es el grafo tal que (u, v) forman una arista si y s olo si hay alg un
camino que une u con v en el grafo original y tiene una o dos aristas.
La gura 4.15 muestra el digrafo descrito por M M
2
, donde M corresponde al di-
grafo de la gura 4.13 (a).
0 1 2
3 4 5
Figura 4.15: Digrafo descrito por M M
2
, donde M corresponde al digrafo de la gu-
ra 4.13 (a).
Queda claro que la o-l ogica de innitos t erminos M
+
= M M
2
M
3
es el cierre
transitivo de M. Pero, es necesario calcular una suma de innitos t erminos? En realidad
no: basta con calcular
1iV
M
i
. Esto es as porque cualquier camino con V +1 aristas
o m as repite alg un v ertice. As pues,
M
+
=
1iV
M
i
.
Podemos calcular M
+
en tiempo O(V
4
) mediante sumas y productos de matrices
l ogicas. Hay alguna forma de hacerlo mejor? Resulta factible realizar una suma y adici on
en una unica operaci on:
algoritmia/graphalgorithms/closure.py
class MatrixTransitiveClosureFinder(IMatrixTransitiveClosureFinder):
def init (self , **kw):
get factories(self , kw, matrixFactory=lambda it: [[cell for cell in row] for row in it])
def square and add(self , M: "square matrix<bool>") -> "square matrix<bool>":
R = self .matrixFactory(M)
202 Apuntes de Algoritmia 28 de septiembre de 2009
n = len(M)
for i in range(n):
for j in range(n):
for k in range(n):
R[i][j] = R[i][j] or (M[i][k] and M[k][j])
return R
El m etodo calcula R = M M
2
directamente y en tiempo O(V
3
) (y no supone me-
jora asint otica respecto de calcular M
2
y sumar M al resultado).
Qu e ocurre si invocamos square and addition sobre R = M M
2
? Obtenemos R R
2
.
Desarrollemos esa expresi on:
R R
2
= (M M
2
) ((M M
2
) (M M
2
))
= (M M
2
) (M
2
M
3
M
3
M
4
)
= M M
2
M
3
M
4
.
Si aplicamos ahora la misma operaci on al resultado, obtenemos
1i8
M
i
. Basta con
efectuar lg V 1 operaciones como esta para calcular M
+
, con lo que tenemos un
m etodo que se ejecuta en tiempo O(V
3
lg V):
algoritmia/graphalgorithms/closure.py
class MatrixTransitiveClosureFinder(IMatrixTransitiveClosureFinder):
...
def transitive closure(self , M: "square matrix<bool>") -> "square matrix<bool>":
R = M
i = 1
while i <= len(M):
R = self . square and add(R)
i *= 2
return R
Si sustituimos el c alculo directo del producto matricial por el m etodo de Winograd,
el coste temporal se reduce a O(V
2.38
lg V).
El algoritmo de Warshall sobre matrices booleanas
Warshall present o un algoritmo m as eciente para el c alculo de la clausura transitiva.
El algoritmo es parecido al que calcula el producto y suma de matrices, si bien altera el
orden de sus bucles:
algoritmia/graphalgorithms/closure.py
class WarshallMatrixTransitiveClosureFinder(IMatrixTransitiveClosureFinder):
def init (self , **kw):
get factories(self , kw, matrixFactory=lambda it: [[cell for cell in row] for row in it])
def transitive closure(self , M: "iterable<iterable<bool>>") -> "square matrix<bool>":
R = self .matrixFactory(M)
n = len(R)
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 203
for k in range(n):
for i in range(n):
for j in range(n):
R[i][j] = R[i][j] or (R[i][k] and R[k][j])
return R
N otese que la expresi on interior en los bucles trabaja unicamente con la propia matriz
R.
1in
M
i
, siendo M una matriz booleana de n n elementos.
Demostraci on. Podemos demostrarlo por inducci on sobre k, el ndice del bucle exterior, e
interpretando la matriz en t erminos del grafo que representa. Recordemos que los v erti-
ces forman el intervalo [0..n 1]. La hip otesis de inducci on es tras cada iteraci on del
bucle exterior, la celda R
ij
almacena el valor cierto si y s olo si hay un camino de i a j
que no incluye v ertices mayores que k.
Base de inducci on. Tras la primera iteraci on R
ij
vale cierto si ya lo vala originalmente,
es decir, si M
ij
era cierto, o si tanto M
i0
como M
0j
son cierto. En el primer caso, hay
una arista que conecta i con j. En el segundo caso, el camino que une i con j s olo pasa por
el v ertice 0.
Paso de inducci on. Si es cierto tras una iteraci on, lo es tambi en tras la siguiente? Si
R
ij
vale cierto ser a por una de esta dos razones:
Ya lo vala antes de ejecutar la iteraci on, en cuyo caso, por hip otesis de inducci on,
el enunciado es cierto.
Ha cambiado de valor en esta iteraci on, cosa que s olo puede ocurrir si, para alg un
par (i, j) se cumple que tanto R
ik
como R
kj
son cierto. Eso signica que hay un
camino de i a k y otro de k a j que s olo atraviesan v ertices k
tales que k
< k
(por hip otesis de inducci on). Al a nadir ahora el v ertice k, se sigue satisfaciendo el
invariante de bucle.
Al nalizar el proceso, la celda R
ij
indica si se puede ir de i a j pasando por cualquiera
de los v ertices del grafo. Si tomamos R como la matriz de adyacencia que describe un
nuevo grafo, este corresponde, por denici on, a la clausura transitiva del grafo descrito
con M.
El coste temporal del algoritmo es, evidentemente, (V
3
). Podemos mejorar el coste
para el mejor de sus casos:
204 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/graphalgorithms/closure.py
class WarshallMatrixTransitiveClosureFinder2(IMatrixTransitiveClosureFinder):
def init (self , **kw):
get factories(self , kw, matrixFactory=lambda it: [[cell for cell in row] for row in it])
def transitive closure(self , M: "square matrix<bool>") -> "square matrix<bool>":
R = self .matrixFactory(M)
n = len(R)
for k in range(n):
for i in range(n):
if R[i][k]:
for j in range(n):
if R[k][j]: R[i][j] = True
return R
demos/graphalgorithms/matrixclosure.py
from algoritmia.graphalgorithms.closure import *
M = [[ False, True, False, True, False, False],
[ False, False, False, False, True, False],
[ False, False, False, False, True, True],
[ True, True, False, False, False, False],
[ False, False, False, True, False, False],
[ False, False, False, False, False, True]]
for row in WarshallMatrixTransitiveClosureFinder2().transitive closure(M):
for value in row: print({!s:<5}.format(value), end= " ")
True True False True True False
True True False True True False
True True False True True True
True True False True True False
True True False True True False
False False False False False True
El coste de esta nueva versi on es (V
2
) y O(V
3
). Conviene considerar cu ando
el coste real se aproximar a a su cota inferior y cu ando a su cota superior. El punto cla-
ve est a en la densidad del grafo. N otese que, probablemente, el algoritmo presenta
un comportamiento pr oximo al mejor de los casos cuando el grafo es disperso, pues la
probabilidad de ejecutar el tercer bucle (el indexado con j) disminuye si aumenta la pro-
babilidad de que R
ik
valga falso.
El algoritmo de Warshall sobre digrafos
Presentamos una versi on que trabaja directamente con un grafo y produce un grafo a la
salida:
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 205
algoritmia/graphalgorithms/closure.py
from algoritmia.datastructures.graphs import Digraph
...
class DigraphTransitiveClosureFinder(IDigraphTransitiveClosureFinder):
def init (self , matrixTransitiveClosureFinder=None, **kw):
get factories(self , kw,
matrixTransitiveClosureFinderFactory=\
lambda G: WarshallMatrixTransitiveClosureFinder(),
digraphFactory=lambda V, E: Digraph(V, E))
def transitive closure(self , G: "Digraph<T>") -> "Digraph<T>":
V = tuple(G.V)
n = len(V)
R = self .matrixTransitiveClosureFinderFactory(G).transitive closure(
(((V[i], V[j]) in G.E) for j in range(n)) for i in range(n))
C = self .digraphFactory(G.V,
[(V[i], V[j]) for i in range(n) for j in range(n) if R[i][j]])
return C
demos/graphalgorithms/warshallclosure.py
from algoritmia.datastructures.graphs import Digraph
from algoritmia.graphalgorithms.closure import DigraphTransitiveClosureFinder
G = Digraph(E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
print(DigraphTransitiveClosureFinder().transitive closure(G))
G2 = Digraph(E=[(C, C++), (C, Java), (C, C#), (C, ObjC),
(C++, Java), (C++, C#), (Java, C#)])
print(DigraphTransitiveClosureFinder().transitive closure(G2))
Digraph(V=[0, 1, 2, 3, 4, 5], E=[(0, 0), (0, 1), (0, 3), (0, 4), (1, 0), (1, 1
), (1, 3), (1, 4), (2, 0), (2, 1), (2, 3), (2, 4), (2, 5), (3, 0), (3, 1), (3,
3), (3, 4), (4, 0), (4, 1), (4, 3), (4, 4), (5, 5)])
Digraph(V=[C#, C, Java, ObjC, C++], E=[(C, C#), (C, ObjC), (
C, Java), (C, C++), (Java, C#), (C++, C#), (C++, Java)])
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
69 Utiliza el algoritmo de Warshall para detectar los componentes conexos de un grafo no
dirigido.
70 Es posible calcular la clausura transitiva con una aproximaci on completamente diferen-
te. Podemos determinar los v ertices alcanzables desde un v ertice dado con una exploraci on por
primero en profundidad: los v ertices visitados al iniciar la exploraci on en un v ertice v son al-
canzables desde v. Con qu e coste espacial y temporal podemos calcular la clausura transitiva si
ejecutamos tantas exploraciones por primero en profundidad como resulte necesario?
71 Cu al es la clausura transitiva de estos grafos dirigidos?
a) Un grafo en el que un solo v ertice est a conectado a todos los dem as v ertices.
b) Un grafo en el que las aristas forman un ciclo euleriano.
c) Un grafo completo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
206 Apuntes de Algoritmia 28 de septiembre de 2009
4.6.2. M etodo basado en la detecci on de componentes
fuertemente conexos
Es posible calcular la clausura transitiva actuando en tres etapas. En la primera se calcula
el denominado grafo de condensaci on. Se trata de un grafo en el que cada v ertice re-
presenta a un componente fuertemente conexo del grafo original. La gura 4.16 muestra
el grafo de condensaci on para el grafo de la gura 4.11 (a). En la segunda fase se calcula
la clausura transitiva del grafo de condensaci on y en la tercera etapa se forma la clausura
del grafo original enlazando todo par de v ertices de un mismo componente fuertemente
conexo y todo par (u, v) donde u y v pertenecen a v ertices del grafo de condensaci on
directamente unidos en su clausura transitiva.
Figura 4.16: Grafo de condensaci on del grafo de la gura 4.11 (a). Cada v ertice re-
presenta un componente fuertemente conexo del grafo original: A es el conjunto de
v ertices {0, 1, 2}, B es {3}, C es {4, 5}, D es {6, 7} y E es {8}.
A B
C
D
E
Para calcular la clausura transitiva del grafo de condensaci on podemos utilizar cual-
quiera de los m etodos que ya conocemos. Si G
= (V
, E
) es el grafo de condensaci on
y usamos el algoritmo de Warshall, por ejemplo, este m etodo se ejecutar a en tiempo
O(V +E +V
3
). Si V
t
P
s
(t).
Dada la funci on de ponderaci on de aristas, d : E R, de entre todas las soluciones
factibles nos interesa la que minimiza la funci on de ponderaci on de un camino, D : V
+
implica que a + b a
+ b.
Podemos reescribir ahora el sumatorio interior:
D
s
(t) = mn
(u,t)E
((
mn
(v
1
,v
2
,...,v
n1
)P
s
(u)
D(v
1
, v
2
, . . . , v
n1
)
)
+ d(u, t)
)
.
Hemos llegado a una relaci on recursiva:
D
s
(t) = mn
(u,t)E
(D
s
(u) + d(u, t)) .
Hay que hacerse una pregunta: qu e ocurre con los v ertices t sin predecesores? Si un
v ertice no tiene predecesores y no es el inicial, es inalcanzable desde el inicial. Su distan-
cia desde el inicial es, pues, +. Si denimos mn() como + la ecuaci on produce el
resultado deseado.
Tomemos en consideraci on ahora el caso base de la recursi on, es decir, cuando t = s.
Se propone entonces el c alculo de la distancia de s a s y su valor es 0:
D
s
(t) = 0.
He aqu la ecuaci on recursiva que proporciona la distancia de s a un v ertice v cual-
quiera:
D
s
(v) =
0, si v = s;
mn
(u,v)E
(D
s
(u) + d(u, v)) , si no.
(4.3)
Recordemos, nalmente, que deseamos calcular mn
t
D
s
(t). (Bueno, en realidad lo que
queremos conocer es el camino m as corto de s a un v ertice cualquiera de , no s olo su
peso.)
222 Apuntes de Algoritmia 28 de septiembre de 2009
C alculo recursivo de la distancia
Estamos en condiciones de proponer un algoritmo recursivo que resuelva el problema
traduciendo la ecuaci on recursiva directamente a un programa Python:
algoritmia/graphalgorithms/dagshortestpaths.py
from algoritmia.utils import min, innity
...
class DAGShortestPaths(object):
...
def distances from one to some with recursion(self , G: "acyclic Digraph<T>",
d: "f: T, T -> R", s: "T", F: "iterable<T>") -> "R":
def Ds(v):
if v == s: return 0
return min((Ds(u)+d(u,v) for u in G.preds(v)), ifempty=innity)
return min(Ds(t) for t in F)
El algoritmo terminar a porque el grafo es acclico, lo que impide que se genere una
serie de llamadas recursivas innita. Pero se trata de un algoritmo muy costoso compu-
tacionalmente: cada llamada a la funci on supone la ejecuci on de O(V
V
) pasos en el
peor de los casos. Si el factor de ramaje est a acotado por una constante b > 1, el coste
temporal es O(b
V
).
Es un coste computacional prohibitivo. Podemos pensar que la recursi on lo hace
inevitable, pero no es as. La raz on de que el algoritmo sea tan costoso no es que ha-
ya llamadas recursivas, sino que estas se repiten. En la gura 4.25 se muestra un grafo
y el arbol de llamadas recursivas cuando queremos conocer el camino m as corto entre
dos de sus v ertices. Podemos presentar un algoritmo recursivo m as eciente si evitamos
efectuar llamadas repetidas.
Figura 4.25: (a) Grafo acclico y
ponderado con un v ertice inicial
y dos v ertices nales. (b)
Arbol de
llamadas al calcular la longitud
del camino m as corto con el algo-
ritmo recursivo.
0 1 2
3 4 5
3
1
5
2
4 3
1
1
1
Ds(5)
Ds(1)
Ds(0) Ds(3)
Ds(0)
Ds(2)
Ds(1)
Ds(0) Ds(3)
Ds(0)
Ds(4)
Ds(1)
Ds(0) Ds(3)
Ds(0)
Ds(2)
Ds(1)
Ds(0) Ds(3)
Ds(0)
(a) (b)
C alculo de la distancia con memorizaci on
Hay un t ecnica, conocida como memorizaci on, capaz de resolver el problema de la inde-
seable repetici on de c alculos. Consiste en una usar una tabla donde ir almacenando los
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 223
valores de Ds(v) conforme se van conociendo. Siempre que se desee conocer el valor de
Ds(v), se acceder a primero a la tabla para ver si ya fue calculado. Si es as se devolver a el
valor almacenado; si no, se calcular a y almacenar a en la tabla para usos futuros.
algoritmia/graphalgorithms/dagshortestpaths.py
class DAGShortestPaths(object):
...
def distances from one to some with memoization(self , G: "acyclic Digraph<T>",
d: "f: T, T -> R", s: "T", F: "iterable<T>") -> "R":
def Ds(v):
if v == s: return 0
for u in G.preds(v):
if u not in mem: mem[u] = Ds(u)
return min((mem[u] + d(u,v) for u in G.preds(v)), ifempty=innity)
mem = self .mappingFactory(G.V)
for t in F:
if t not in mem: mem[t] = Ds(t)
return min(mem[t] for t in F)
El coste temporal de este algoritmo es O(V +E) y el coste espacial es O(V).
C alculo iterativo de la distancia
Si durante la ejecuci on de la llamada Ds(v) se produce una llamada directa a Ds(u) (o
una consulta a mem[u], es porque (u, v) es una arista de G. Como G es acclico, el v ertice
u es anterior a v en cualquier orden topol ogico de los v ertices del grafo. Si calcul asemos el
valor de mem[v] para cada v en un orden topol ogico, siempre encontraramos que Ds(u),
para cualquier predecesor v, ya ha sido calculado antes y estara disponible en mem[u]
cuando se necesita. Y si memorizamos los valores calculados, no hace falta ejecutar una
llamada recursiva. Por tanto, la versi on iterativa ordena topol ogicamente los v ertices y
calcula las distancias m as cortas del origen a cada uno de ellos proces andolos en ese
orden:
algoritmia/graphalgorithms/dagshortestpaths.py
from .topsort import Topsorter
...
class DAGShortestPaths(object):
...
def distances from one to some(self , G: "acyclic Digraph<T>",
d: "f: T, T -> R", s: "T", F: "iterable<T>") -> "R":
mem = self .mappingFactory(G.V)
for v in G.V: mem[v] = innity
for v in self .topsorterFactory(G).topsorted(G):
if v == s: mem[v] = 0
else: mem[v] = min((mem[u] + d(u,v) for u in G.preds(v)), ifempty=innity)
return min(mem[t] for t in F)
224 Apuntes de Algoritmia 28 de septiembre de 2009
Probemos el programa. Calculemos con este algoritmo el camino m as corto entre el
v ertice 0 y los v ertices 2 o 5 de un grafo acclico y ponderado:
demos/graphalgorithms/dagdistance.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
from algoritmia.graphalgorithms.dagshortestpaths import DAGShortestPaths
d = WeightingFunction({(0,1): 3, (0,3): -1, (1,2): 5, (1,4): 2, (1,5): 4,
(2,4): 3, (2,5): 1, (3,1): 1, (4,5): 1})
G, s, F = Digraph(E=d.keys()), 0, [2, 5]
print(Distancia entre {} y {}: {}.format(
s, F, DAGShortestPaths().distances from one to some(G, d, s, F)))
Distancia entre 0 y [2, 5]: 3
La gura 4.26 muestra una traza del algoritmo.
Figura 4.26: Traza
del algoritmo ite-
rativo de c alculo
del coste del camino
m as corto de 0 a 2 o
5 en el grafo accli-
co ponderado de la
gura 4.25 (a). Los
v ertices se mues-
tran en un orden
topol ogico.
0 3 1 2 4 5
1 1 5 3 1
3 2
4
1
0 3 1 2 4 5
1 1 5 3 1
3 2
4
1
0
(a) (b)
3 1 2 4 5 0
1 1 5 3 1
3 2
4
1
0 0 1
1 2 4 5 0 3
3
1 1 5 3 1
2
4
1
0 1 mn(0 +3, 1 1)
(c) (d)
0 3 4 5 1 2
1 3 1
3 2
4
1
1 5
0 0 +5 0 1
0 3 5 1 2 4
1 1 5 1
3
4
1
3
2
0 5 mn(0 +2, 5 +3) 0 1
(e) (f)
0 3 1 2 4 5
1 1 5
3
3
2
1
4
1
0 5 2 mn(0 +4, 5 +1, 2 +1) 0 1
0 3 1 2 4 5
1 1 5
3
3
2
1
4
1
0 5 2 0 1 3
(g) (h)
C alculo del camino m as corto
La t ecnica de punteros hacia atr as permite recuperar el camino m as corto:
algoritmia/graphalgorithms/dagshortestpaths.py
from algoritmia.utils import argmin
...
class DAGShortestPaths(object):
...
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 225
def distances and paths from one to some(self , G: "acyclic Digraph<T>",
d: "f: T, T -> R", s: "T", F: "iterable<T>") -> "R":
mem, back = self .mappingFactory(G.V), self .mappingFactory(G.V)
for v in G.V: mem[v], back[v] = innity, None
for v in self .topsorterFactory(G).topsorted(G):
if v == s:
mem[v], back[v] = 0, v
else:
u = argmin(G.preds(v), lambda u: mem[u] + d(u,v), ifempty=v)
if u != v:
mem[v], back[v] = mem[u] + d(u, v), u
t = argmin(F, lambda v: mem[v])
return mem[t], backtrace(back, t)
demos/graphalgorithms/dagshortestpath.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
from algoritmia.graphalgorithms.dagshortestpaths import DAGShortestPaths
d = WeightingFunction({(0,1):3, (0,3):-1, (1,2):5, (1,4):2, (1,5):4,
(2,4): 3, (2,5):1, (3,1):1, (4,5):1})
G, s, F = Digraph(E=d.keys()), 0, [2, 5]
print(El camino mas corto entre {} y {} recorre distancia {} y es {}.format(
s, F, *DAGShortestPaths().distances and paths from one to some(G, d, s, F)))
El camino mas corto entre 0 y [2, 5] recorre distancia 3 y es [0, 3, 1, 4, 5]
La gura 4.27 muestra una traza del algoritmo en lo que afecta al diccionario de pun-
teros hacia atr as.
An alisis de complejidad computacional
El coste temporal de cualquiera de las versiones del m etodo presentado (recursiva con
memorizaci on o iterativas) es O(V + E) si utilizamos cualquier implementaci on de
los grafos excepto la matriz de adyacencia. En tal caso, el coste temporal se elevara a
(V
2
).
La ordenaci on topol ogica de los v ertices requiere tiempo O(V +E). Una vez orde-
nados, se recorren y para cada uno se consulta un valor por arco incidente. El coste total
es, pues, O(V +E). La recuperaci on del camino no afecta al coste temporal: recorrer
los punteros hacia atr as, formar la lista con los v ertices atravesados e invertirla al nal
requiere tiempo que podemos acotar superiormente con una funci on O(V).
Acerca del problema con varios v ertices iniciales y varios v ertices nales
El problema del c alculo del camino m as corto entre un v ertice cualquiera de un conjunto
V y un v ertice cualquiera de un conjunto V para un grafo acclico dirigido G =
(V, E) y ponderado tendr a aplicaci on en el captulo que dedicamos a la Programaci on
Din amica, por lo que vale la pena que nos detengamos brevemente a estudiarlo.
226 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 4.27: Punteros hacia atr as
en el c alculo del camino m as corto
entre un v ertice y un conjunto de
v ertices en un grafo acclico.
0 3 1 2 4 5
1 1 5 3 1
3 2
4
1
0
3 1 2 4 5 0
1 1 5 3 1
3 2
4
1
0 1
(a) (b)
1 2 4 5 0 3
3
1 1 5 3 1
2
4
1
0 1 0
0 3 4 5 1 2
1 3 1
3 2
4
1
1 5
0 5 0 1
(c) (d)
0 3 5 1 2 4
1 1 5 1
3
4
1
3
2
0 5 2 0 1
0 3 1 2 4 5
1 1 5
3
3
2
1
4
1
0 5 2 3 0 1
(e) (f)
0 3 1 2 4 5
1 1 5
3
3
2
1
4
1
0 5 2 0 1 3
(g)
Al considerar el problema con un solo v ertice de partida hemos derivado la ecuaci on
recursiva 4.3 gracias a una denici on, tambi en recursiva, del conjunto de caminos entre
los que buscamos el optimo. Tambi en podemos denir recursivamente el conjunto de
caminos que parte de cualquier v ertice de y naliza en cualquier v ertice de , pero
prestando especial atenci on al caso base. Los caminos que parten de un v ertice cualquiera
de y nalizan en un v ertice cualquiera de no necesariamente tienen cero aristas: es
posible que partan de un v ertice v y nalicen en otro w, ambos de , con una o m as
aristas cuya suma presente un valor negativo. No basta, pues, con suponer que el camino
m as corto que parte de un v ertice de y naliza en un v ertice de tiene cero aristas y
distancia cero. Hemos de considerar esa posibilidad, s, pero compar andola con el peso
de los caminos que pueden venir directamente de cualquier predecesor y tener origen en
un v ertice inicial cualquiera. As pues, la ecuaci on an aloga para nuestro problema a la
ecuaci on 4.3 es
D
(v) =
mn(0, mn
(u,v)E
(D
(v) = mn
({
0, si v ,
+, si v ,
, mn
(u,v)E
(D
() = mn
t
D
(t).
Y he aqu un algoritmo iterativo que resuelve esta ecuaci on:
algoritmia/graphalgorithms/dagshortestpaths.py
class DAGShortestPaths(object):
...
def distances and paths from some to some(self , G: "acyclic Digraph<T>",
d: "f: T, T -> R", I: "iterable<T>", F: "iterable<T>") -> "R":
mem, back = self .mappingFactory(G.V), self .mappingFactory(G.V)
for v in G.V: mem[v], back[v] = innity, None
for v in self .topsorterFactory(G).topsorted(G):
u = argmin(G.preds(v), lambda u: mem[u] + d(u, v), ifempty=v)
if v in I:
if u != v and mem[u] + d(u, v) < 0:
mem[v], back[v] = mem[u] + d(u, v), u
else:
mem[v], back[v] = 0, v
elif u != v:
mem[v], back[v] = mem[u] + d(u, v), u
t = argmin(F, lambda v: mem[v])
return mem[t], backtrace(back, t)
demos/graphalgorithms/dagsometosome.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
from algoritmia.graphalgorithms.dagshortestpaths import DAGShortestPaths
d = WeightingFunction({(0,1):3, (0,3):-1, (1,2):5, (1,4):2, (1,5):4,
(2,4): 3, (2,5):1, (3,1):1, (4,5):1})
G, I, F = Digraph(E=d.keys()), [0, 3], [2, 5]
print(El camino mas corto entre {} y {} recorre distancia {} y es {}.format(
I, F, *DAGShortestPaths().distances and paths from some to some(G, d, I, F)))
El camino mas corto entre [0, 3] y [2, 5] recorre distancia 3 y es [0, 3, 1, 4
, 5]
El coste temporal de este algoritmo es O(V +E) y el espacial es O(V).
4.8.2. Camino m as corto formado por k aristas
Nuestro objetivo ahora es resolver el problema del c alculo del camino m as corto en un
grafo ponderado y con ciclos, sin restricci on para la funci on de ponderaci on. Pero con-
228 Apuntes de Algoritmia 28 de septiembre de 2009
viene que empecemos por un problema diferente cuyo algoritmo resolutivo ser a instru-
mental para resolver el problema que nos hemos propuesto. Este nuevo problema es el
del c alculo del camino m as corto entre s y t formado por exactamente k aristas en un
digrafo ponderado cualquiera.
Dado un digrafo (no necesariamente acclico) G = (V, E) ponderado por una funci on
d : E R y dados dos v ertices s y t a los que denominamos v ertice inicial y v ertice nal,
respectivamente, deseamos encontrar un camino que parta de s, nalice en t, est e for-
mado por exactamente k aristas y tal que ning un otro camino entre dichos v ertices y
formado por k aristas presente menor distancia.
Si denimos el conjunto de caminos entre s y un v ertice v formados por i aristas as
P
s
(v, i) =
{(s)}, si i = 0 y v = s;
, si i = 0 y v = s;
{(v
1
, . . . , v
i+1
) v
1
= s; v
i+1
= v; (v
1
, . . . , v
i
) P
s
(u, i 1); (u, v) E}, si no;
El problema consiste en un buscar el camino de menor peso en P
s
(t, k), es decir, buscamos
arg mn
(v
1
,v
2
,...,v
k+1
)P
s
(t,k)
D((v
1
, v
2
, . . . , v
k+1
))
donde
D((v
1
, v
2
, . . . , v
n
)) =
1i<n
d(v
i
, v
i+1
),
Nuevamente, aunque deseamos obtener el camino optimo, resolveremos el problema
del c alculo del peso de dicho camino y obtendremos el camino como subproducto del
c alculo. As pues, desarrollaremos un algoritmo para calcular
D
s
(t, k) = mn
(v
1
,v
2
,...,v
k+1
)P
s
(t,k)
D((v
1
, v
2
, . . . , v
k+1
)).
Ecuaci on recursiva
Partimos de
D
s
(t, k) = mn
(v
1
,v
2
,...,v
k+1
)P
s
(t,k)
D
s
((v
1
, v
2
, . . . , v
k+1
))
= mn
(v
1
,v
2
,...,v
k+1
)P
s
(t,k)
(
1i<k+1
d(v
i
, v
i+1
)
)
.
Si sustituimos P
s
(t, k) en la denici on de D
s
(t, k) por su expresi on recursiva, tenemos:
D
s
(t, k) = mn
(u,t)E
(
mn
(v
1
,v
2
,...,v
k
)P
s
(u,k1)
((
1i<k
d(v
i
, v
i+1
)
)
+ d(u, t)
))
.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 229
Extraemos de la minimizaci on interior el ultimo t ermino del sumatorio:
D
s
(t, k) = mn
(u,t)E
((
mn
(v
1
,v
2
,...,v
k
)P
s
(u,k1)
(
1i<k
d(v
i
, v
i+1
)
))
+ d(u, t)
)
.
Y reescribimos el sumatorio interior, que no es m as que la distancia de un camino:
D
s
(t, k) = mn
(u,t)E
((
mn
(v
1
,v
2
,...,v
k
)P
s
(u,k1)
D((v
1
, v
2
, . . . , v
k
))
)
+ d(u, t)
)
.
Hemos llegado al t ermino general de una relaci on recursiva:
D
s
(t, k) = mn
(u,t)E
(D
s
(u, k 1) + d(u, t)) .
Como no hay nada especial en t, podramos haber deducido la recursi on para un v ertice
v cualquiera y un n umero de arista i cualquiera:
D
s
(v, i) = mn
(u,v)E
(D
s
(u, i 1) + d(u, v)) .
Hay un caso que podemos considerar base de la recursi on: cuando v = s e i = 0 se
propone la b usqueda de la distancia optima de s a s por un camino que no tiene arista
alguna. Dicha distancia es, por denici on, nula: D
s
(s, 0) = 0. Otro caso base tiene lugar
si v = s e i = 0, ya que todo camino v alido empieza en s: D
s
(v, 0) = +. He aqu la
ecuaci on completa:
D
s
(v, i) =
0, si v = s y i = 0;
+, si v = s y i = 0;
mn
(u,v)E
(D
s
(u, i 1) + d(u, v)) , en otro caso.
(4.5)
Recodemos que la soluci on del problema es D
s
(t, k).
Grafo multietapa asociado y algoritmo iterativo
Hay una interpretaci on interesante de esta ecuaci on recursiva que se ilustra en la gu-
ra 4.28. La distancia del camino m as corto con 3 aristas entre los v ertices 0 y 5 del grafo
de la gura 4.28 (a), por ejemplo, es la distancia del camino m as corto entre los v ertices
(0, 0) y (5, 3) del grafo de la gura 4.28 (b). Se trata de un grafo multietapa que se for-
ma a partir del primero disponiendo en cada una de sus k + 1 etapas una copia de los
v ertices del grafo original y, por cada arista entre dos v ertices u y v del grafo original,
una arista entre los v ertices correspondientes de dos etapas consecutivas.
Dado un grafo G = (V, E), un valor positivo k y una funci on de ponderaci on d, su
grafo multietapa asociado, G
= (V
, E
), es
V
= {(v, i) v V, 0 i k},
E
se dene as a partir de d:
d
: ((v
1
, 0), (v
2
, 1), . . . , (v
k+1
, k)). Y viceversa: todo camino en
G
:
algoritmia/graphalgorithms/kedgesshortestpaths.py
class KEdgesShortestPaths1:
def init (self , **kw):
get factories(self , kw, mappingFactory=lambda V, k: dict(),
columnMappingFactory=lambda V: dict())
def distance(self , G: "Digraph<T>", d: "f: T, T -> R", s: "T", t: "T", k: "int")->"R":
D = self .mappingFactory(G.V, k)
for v in G.V: D[v, 0] = innity
D[s, 0] = 0
for i in range(1, k+1):
for v in G.V:
D[v, i] = min((D[u, i-1] + d(u,v) for u in G.preds(v)), ifempty=innity)
return D[t, k]
Probemos el algoritmo con el grafo de la gura 4.28 (a).
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 231
demos/graphalgorithms/kedgesdistance.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
from algoritmia.graphalgorithms.kedgesshortestpaths import KEdgesShortestPaths
d = WeightingFunction({(0,1):3, (0,3):1, (1,0):-2, (1,1):2, (1,2): 2, (1,4):-2,
(1,5):3, (2,5):1, (3,1):1, (4,3):2, (4,5):2})
G = Digraph(E=d.keys())
print(Distancia de 0 a 5)
for i in range(6):
print( con {} aristas: {}.format(i, KEdgesShortestPaths().distance(G, d, 0, 5, i)))
Distancia de 0 a 5
con 0 aristas: inf
con 1 aristas: inf
con 2 aristas: 6
con 3 aristas: 3
con 4 aristas: 2
con 5 aristas: 4
El coste temporal del algoritmo es O(k(V + E)). El coste espacial es O(kV). El
coste espacial puede reducirse a s olo O(V) si unicamente estamos interesados en la
distancia del camino m as corto: s olo se precisa almacenar valores intermedios para dos
etapas (la actual y la anterior):
algoritmia/graphalgorithms/kedgesshortestpaths.py
class KEdgesShortestPaths:
def init (self , **kw):
get factories(self , kw, mappingFactory=lambda V, k: dict(),
columnMappingFactory=lambda V: dict())
def distance(self , G: "Digraph<T>", d: "f: T, T -> R", s: "T", t: "T", k: "int")->"R":
prev, curr = self .columnMappingFactory(G.V), self .columnMappingFactory(G.V)
for v in G.V: curr[v] = innity
curr[s] = 0
for i in range(1, k+1):
prev, curr = curr, prev
for v in G.V:
curr[v] = min((prev[u] + d(u,v) for u in G.preds(v)), ifempty=innity)
return curr[t]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
81 Dise na un algoritmo que recupere y devuelva el camino optimo con k aristas entre s y t.
82 Dise na un algoritmo que calcule el camino m as corto formado por k aristas entre cualquier
v ertice de un conjunto y cualquier v ertice de un conjunto .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.8.3. Camino m as corto en un digrafo ponderado cualquiera
Ya podemos resolver el problema m as general: el del c alculo del camino m as corto en
un grafo ponderado cualquiera. Bueno, casi, ya que impondremos una condici on: que
232 Apuntes de Algoritmia 28 de septiembre de 2009
el peso sumado de las aristas que forman cualquier ciclo no sea negativo. Si lo fuera el
problema estara mal denido y no habra camino optimo.
El problema que deseamos resolver se plantea as: dado un digrafo G = (V, E) pon-
derado por una funci on d : E R con ciclos no negativos y dados dos v ertices s y t, a los
que denominamos v ertice inicial y v ertice nal, respectivamente, encu entrese un camino
que parta de s y nalice en t tal que ning un otro camino entre dichos v ertices presente
menor distancia.
Reformulaci on como problema del camino m as corto formado por hasta
V 1 aristas
Ya sabemos calcular el camino m as corto con exactamente k aristas. Y sabemos que si hay
uno o m as caminos con peso optimo y k aristas, al menos uno de ellos no contiene un
ciclo: si contuviera un ciclo negativo, el problema estara mal formulado; y si contuviera
un ciclo con peso total nulo, el camino que obtenemos al eliminar el ciclo es de peso
optimo. As pues, ha de haber al menos un camino m as corto (o igual de corto) y formado
por, a lo sumo, V 1 aristas. Podramos calcular el camino m as corto con k aristas para
todo k entre 0 y V 1 y seleccionar a continuaci on el de menor peso, es decir, calcular
D
s
(t) = mn
0k<V
D
s
(t, k).
Pero hay un modo m as eciente: basta con calcular D
s
(t, k) y estudiar los valores almace-
nados en el diccionario D durante el c alculo. El grafo multietapa sobre el que calculamos
el (peso del) camino m as corto con V 1 aristas tiene una peculiaridad: contiene como
subgrafos a los propios del c alculo de los caminos m as cortos con cualquier n umero de
aristas inferior.
En la gura 4.29 (b) se muestra el grafo que hubi esemos construido implcitamente
al calcular el camino m as corto entre 0 y 5 con 5 aristas en el grafo de la gura 4.29 (a).
N otese que las dos primeras columnas, por ejemplo, junto a las aristas que conectan sus
v ertices, forman el grafo sobre el que calcular el camino m as corto de 0 a 5 con 1 arista;
y las cuatro primeras columnas con sus aristas, por poner otro ejemplo, forman el grafo
con el que calcular el camino m as corto de 0 a 5 con 3 aristas. En general, el camino m as
corto de (s, 0) a (t, i) es el camino m as corto con i aristas.
Si consideramos el peso asociado al camino m as corto que acaba en (t, i), para 0 i <
V, y seleccionamos el menor, tendremos el peso del camino m as corto de s a t con cual-
quier n umero de aristas en el grafo original. En el grafo de la gura, la distancia mnima
de entre las calculadas para cada uno de esos v ertices es mn
0i<6
D(5, i) = D(5, 4) = 2.
As pues, el camino m as corto tiene 4 aristas (se muestra en trazo negro en la gura).
El algoritmo dise nado se conoce por algoritmo de Bellman-Ford:
algoritmia/graphalgorithms/shortestpaths.py
class BellmanFordShortestPaths:
def init (self , **kw):
get factories(self , kw, mappingFactory=lambda V: dict())
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 233
0 1 2
3 4 5
3
1
2
2
2
2
3
1
1
2 2
0 1 2
3 4 5
0,0
1,0
2,0
3,0
4,0
5,0
0,1
1,1
2,1
3,1
4,1
5,1
0,2
1,2
2,2
3,2
4,2
5,2
0,3
1,3
2,3
3,3
4,3
5,3
0,4
1,4
2,4
3,4
4,4
5,4
0,5
1,5
2,5
3,5
4,5
5,5
3
1
2
2
2
2
3
1
1
2
2
3
1
2
2
2
2
3
1
1
2
2
3
1
2
2
2
2
3
1
1
2
2
3
1
2
2
2
2
3
1
1
2
2
3
1
2
2
2
2
3
1
1
2
2
(a) (b)
Figura 4.29: (a) Grafo con aristas negativas (pero sin ciclos negativos) y (b) grafo multietapa asociado sobre el que se
efect ua implcitamente la b usqueda del camino m as corto entre 0 y 5 con cualquier n umero de aristas. En trazo grueso
se muestra el camino optimo sobre el grafo original y su equivalente en el grafo multietapa asociado.
def distance(self , G: "Digraph<T>", d: "f: T, T -> R", s: "T", t: "T") -> "R":
D = self .mappingFactory(G.V)
for v in G.V: D[v, 0] = innity
D[s, 0] = 0
for i in range(1, len(G.V)):
for v in G.V:
D[v, i] = min((D[u, i-1] + d(u,v) for u in G.preds(v)), ifempty=innity)
return min(D[t, i] for i in range(len(G.V)))
Ponemos a prueba el programa con el grafo 4.29 (a):
demos/graphalgorithms/bellmanforddistance.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
from algoritmia.graphalgorithms.shortestpaths import BellmanFordShortestPaths
d = WeightingFunction({(0,1):3, (0,3):1, (1,0):1, (1,1):2, (1,2): 2, (1,4):-2,
(1,5):3, (2,5):1, (3,1):1, (4,3):2, (4,5):2})
bfsp = BellmanFordShortestPaths()
print(Distancia de 0 a 5:, bfsp.distance(Digraph(E=d.keys()), d, 0, 5))
Distancia de 0 a 5: 2
El camino m as corto
A la vista de que hemos reducido el c alculo del camino m as corto en un grafo con ciclos
no negativos al del camino m as corto en un grafo acclico (multietapa) obtenido a partir
del grafo original con ciclos, resulta inmediato aplicar la t ecnica de los punteros hacia
234 Apuntes de Algoritmia 28 de septiembre de 2009
atr as para recuperar el camino optimo. Eso s, hemos de tener en cuenta que los punteros
hacia atr as se expresan en t erminos del grafo multietapa, y no del original:
algoritmia/graphalgorithms/shortestpaths.py
class BellmanFordShortestPaths:
...
def shortest path(self , G: "Digraph<T>", d: "f: T, T -> R", s: "T", t: "T"
) -> "iterable<T>":
D = self .mappingFactory(G.V)
for v in G.V: D[v, 0] = innity
D[s, 0] = 0
back = self .mappingFactory(G.V)
back[s, 0] = (s, 0)
for i in range(1, len(G.V)):
for v in G.V:
u = argmin(G.preds(v), lambda u: D[u, i-1] + d(u,v), ifempty=None)
if u != None:
D[v, i] = D[u, i-1] + d(u,v)
back[v,i] = (u, i-1)
else:
D[v, i] = innity
k = argmin(range(len(G.V)), lambda i: D[t, i])
return [v for (v, i) in backtrace(back, (t, k))]
demos/graphalgorithms/bellmanfordpath.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
from algoritmia.graphalgorithms.shortestpaths import BellmanFordShortestPaths
d = WeightingFunction({(0,1):3, (0,3):1, (1,0):-2, (1,1):2, (1,2): 2, (1,4):-2,
(1,5):3, (2,5):1, (3,1):1, (4,3):2, (4,5):2})
G = Digraph(E=d.keys())
path = BellmanFordShortestPaths().shortest path(G, d, 0, 5)
print(Distancia de 0 a 5:, sum(d(path[i],path[i+1]) for i in range(len(path)-1)))
print(Camino:, path)
Distancia de 0 a 5: 2
Camino: [0, 3, 1, 4, 5]
Complejidad computacional
El coste temporal del algoritmo es O(VE). Recordemos que calcular el camino m as
corto con k aristas requera ejecutar O(kE) pasos, y nosotros hemos resuelto el problema
con el mismo esfuerzo computacional que requiere el c alculo del camino m as corto con
k = V 1 aristas.
El coste espacial es O(V
2
), ya que necesitamos almacenar un puntero hacia atr as por
cada uno de los V
2
v ertices del grafo multietapa.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 235
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83 Se puede reducir siempre la complejidad espacial si s olo interesa el valor de la distancia
mnima?
84 Si efectuamos el c alculo del camino m as corto con el algoritmo de Bellman-Ford en un grafo
no acclico y ponderado positivo, es necesario llegar hasta la etapa V? Puede detenerse antes el
algoritmo con la plena garanta de que se calcula correctamente el camino m as corto?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.8.4. Camino m as corto en un digrafo ponderado positivo
Cuando el digrafo es ponderado positivo, podemos seguir una estrategia resolutiva m as e-
ciente que el algoritmo de Bellman-Ford. Muchos grafos que modelan objetos del mundo
real son ponderados positivos, por lo que el algoritmo que presentamos en esta secci on
encuentra numerosas aplicaciones. Presentaremos el algoritmo en el marco de la reso-
luci on del problema del c alculo de los caminos m as cortos de un v ertice s a cualquier
v ertice del grafo. Hemos visto que los caminos de menor longitud de un v ertice s a cual-
quier otro forman un arbol de caminos con raz en s y este es un arbol de recubrimiento.
Podemos inspirarnos es este hecho para dise nar un algoritmo que, partiendo de un arbol
con un solo v ertice, s, a nada con cada iteraci on una arista y su v ertice destino. Lo impor-
tante ser a denir un criterio para la elecci on de una arista y no otra.
El algoritmo de Dijkstra: una primera versi on
El algoritmo que presentamos mantiene un diccionario D que asocia a cada v ertice v
del grafo un peso: el del camino de s a v m as corto conocido hasta el momento. En cada
iteraci on va extendiendo implcitamente el arbol de caminos m as cortos y lo hace selec-
cionando la arista que conecta un v ertice que ya forma parte del arbol con aquel ajeno
al arbol para el que la distancia a s conocida hasta el momento es menor. Los v ertices
que a un no forman parte del arbol pero que ya son alcanzables con una arista desde un
v ertice del arbol forman un conjunto al que denominamos frontera. A continuaci on
presentamos una implementaci on directa que genera una sucesi on de aristas con el arbol
de caminos m as cortos que tienen origen en s:
algoritmia/graphalgorithms/positivedigraphshortestpaths.py
class DijkstraShortestPaths:
def init (self , **kw):
get factories(self , kw, setFactory=lambda V: set(),
mappingFactory=lambda V: dict())
def backpointers from one to all(self , G: "positive Digraph<T>",
d: "f: T, T -> R", s: "T") -> "iterable<(T, T)>":
D = self .mappingFactory(G.V)
for v in G.V: D[v] = innity
D[s] = 0
yield (s, s)
for v in G.succs(s): D[v] = d(s,v)
added = self .setFactory(G.V)
236 Apuntes de Algoritmia 28 de septiembre de 2009
added.add(s)
in fringe from = dict((v, s) for v in G.succs(s))
while len(in fringe from) > 0:
(v, u) = argmin(in fringe from.items(), lambda edge: D[edge[0]])
del in fringe from[v]
added.add(v)
yield (v, u)
for w in G.succs(v):
if w not in added and D[v] + d(v,w) < D[w]:
D[w] = D[v] + d(v,w)
in fringe from[w] = v
El diccionario in fringe from almacena, para cada v ertice v de la frontera, el v ertice u
del arbol desde el que es alcanzable por una arista tal que D[u] + d(u, v) proporciona el
actual valor D[v].
Pongamos a prueba nuestra implementaci on sobre el grafo de la gura 4.30:
demos/graphalgorithms/dijkstraonetoall.py
from algoritmia.graphalgorithms.positivedigraphshortestpaths import DijkstraShortestPaths
from algoritmia.data.mallorca import Mallorca, km
for u, v in DijkstraShortestPaths().backpointers from one to all(Mallorca, km, Andratx):
print(({}, {}).format(u, v), end=", ")
(Andratx, Andratx), (Calvi`a, Andratx), (Palma de Mallorca, Calvi`a), (Marratx,
Palma de Mallorca), (Llucmajor, Palma de Mallorca), (Inca, Marratx), (Soller
, Andratx), (Campos del Port, Llucmajor), (Santany, Campos del Port), (Alcudi
a, Inca), (Manacor, Inca), (Pollenca, Alcudia), (Art`a, Manacor), (Capdepera, A
rt`a),
La gura 4.30 (b) muestra el arbol de caminos m as cortos.
El algoritmo que hemos presentado (y los renamientos que presentaremos m as ade-
lante) recibe el nombre de algoritmo de Dijkstra. (En realidad, se conoce por algoritmo
de Dijkstra la variante que permite conocer el camino m as corto de s a un v ertice de-
terminado, t. Ese resultado es un subproducto del algoritmo que hemos presentado.) Las
guras 4.31 y 4.32 muestran una traza del algoritmo para el grafo de Mallorca y la ciudad
de Andratx como punto de partida.
Correcci on
Teorema 4.3 El algoritmo de Dijsktra calcula el camino m as corto entre s y cualquier v ertice v.
Demostraci on. Por inducci on sobre la talla de added, el conjunto de los v ertices que forman
parte del arbol en cada instante. Comprobaremos que, para todo u added, D[u] es la
distancia del camino m as corto de s y u y que D[v], para todo v V added, es la
distancia del camino m as corto de s a v que, antes de llegar a v, unicamente visita v ertices
de added.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 237
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
(a) (b)
Figura 4.30: (a) Grafo ponderado que modela un mapa de carreteras entre algunas de las principales ciudades de la
isla de Mallorca. (El chero algoritmia.data.mallorca.py contiene la denici on del grafo en Python.) La zona
noroeste de la isla es muy accidentada, lo que hace que las carreteras no sigan trayectorias rectas, aunque la idealizaci on
de la representaci on gr aca sugiera lo contrario.) (b) En trazo grueso, aristas del arbol de caminos m as cortos con origen
en Andratx.
Base de inducci on. Cuando added = 1, su unico v ertice es s y D[s] vale 0, que es la
distancia del camino m as corto de s a s.
Hip otesis de inducci on. Supondremos que D[u] es la distancia del camino m as corto
de s a u para todo elemento u de added en un instante dado, es decir, para una talla
determinada de added.
Paso de inducci on. Sea v el v ertice de V added seleccionado con la funci on argmin.
Supongamos que D[v] no es la distancia del camino m as corto de s a v, es decir, que
existe un camino p m as corto. Dicho camino debe tener al menos un v ertice diferente de
v que no est e en added. Sea v
a v a nade una distancia que no puede ser negativa (no hay aristas
de peso negativo). Como el prejo de p que llega a v
. La
conclusi on es que el camino p no existe.
Si hubiera aristas de peso nulo, la unica posibilidad de que p existiera es que la por-
ci on que conecta v
] sera igual a D[v] y, por tanto, D[v] correspondera a la distancia del camino
m as corto entre s y v, como queramos demostrar.
N otese que una vez ingresa en added un v ertice v, su valor D[v] no se modica nunca
m as, as que mantiene la distancia mnima entre s y v.
El algoritmo naliza cuando todos los v ertices ingresan en added, as que D[v], para
238 Apuntes de Algoritmia 28 de septiembre de 2009
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
30
56
56
30
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
28
56
56
14
(a) (b)
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
28
56
42
48
56
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
28
54
56
42
48
12
56
(c) (d)
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
28
54
0
62
56
42
48
12
56
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
0
62
56
79
42
48
25
25
56
(e) (f)
Figura 4.31: Traza del algoritmo de Dijkstra sobre el grafo de Mallorca para el c alculo de los caminos m as cortos desde
Andratx a cualquier otra ciudad. Los v ertices de added y las aristas que se van devolviendo se muestran en trazo grueso.
Aunque el grafo del ejemplo es no dirigido, los arcos se almacenan con direcci on (pues se va de un punto a otro siguiendo
una direcci on determinada). En trazo discontinuo, la informaci on almacenada en el diccionario in fringe from: la clave
es el destino de la echa y su valor, el origen de la misma. En el interior de cada nodo se muestra el valor de D. La
gura (a) corresponde al instante previo a ejecutarse el bucle while. Cada una de las restantes guras reeja el estado
al nal de una iteraci on de dicho bucle.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 239
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
0
62
56
110
79
42
48
25
25
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
0
62
56
110
79
42
48
75
25
25
(g) (h)
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
0
62
56
110
79
42
48
75
25
25
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
115
0
62
56
89
79
42
48
75
36
25
(i) (j)
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
96
0
62
56
89
79
42
48
75
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
96
0
62
56
89
79
42
48
75
(k) (l)
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
104
96
0
62
56
89
79
42
48
75
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
14
79 28
54
104
96
0
62
56
89
79
42
48
75
(m) (n)
Figura 4.32: Continuaci on de la gura anterior.
240 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 4.33: El camino p no puede ser m as corto que el camino que conecta s
con v pasando unicamente por v ertices del conjunto added.
s
v
v
added
p
todo v V, es la distancia del camino m as corto de s a v. Si (v, u) es una de las aristas
devueltas, se observa D[v] = D[u] + d(u, v), as que el conjunto de aristas devueltas
representa efectivamente el conjunto de caminos m as cortos con origen en s.
An alisis y renamiento
Suponiendo que todos los v ertices son alcanzables desde s, el algoritmo ejecuta un total
de V 1 iteraciones, ya que el conjunto added se inicializa con un v ertice, en cada itera-
ci on ingresa uno nuevo en added y han de ingresar todos para que el algoritmo nalice.
En cada iteraci on se selecciona un v ertice de entre todos los que presenta la frontera. El
n umero de v ertices en la frontera es O(V). A continuaci on se visitan los sucesores del
v ertice seleccionado, que tambi en pueden ser hasta V. El coste del algoritmo es, pues,
O(V
2
).
Es posible modicar el coste temporal si utilizamos una estructura de datos adecuada
para representar la frontera. Efectuamos las siguientes operaciones sobre dicho conjunto:
Inserci on de un v ertice.
Extracci on del v ertice v con menor valor de D[v].
En principio, una cola de prioridad parece apropiada. Pero hemos de tener en cuenta
que el valor de D[v] para un v ertice de la frontera puede modicarse en el intervalo de
tiempo que transcurre entre su inserci on y extracci on. Por ello, un diccionario de priori-
dad es m as apropiado. Esta nueva versi on explota esta estructura de datos (y devuelve
tambi en el diccionario con la distancia m as corta de s a cualquier otro v ertice a la vez que
simplica la inicializaci on):
algoritmia/graphalgorithms/positivedigraphshortestpaths.py
class DijkstraWithPriorityDictShortestPaths(DijkstraShortestPaths):
def init (self , **kw):
get factories(self , kw,
priorityDictFactory=lambda V: MinPriorityDict(capacity=len(V)))
super(DijkstraWithPriorityDictShortestPaths, self ). init (**kw)
def backpointers from one to all(self , G: "positive Digraph<T>",
d: "f: T, T -> R", s: "T") -> "iterable<(T, T)>":
D = self .priorityDictFactory(G.V)
for v in G.V: D[v] = innity
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 241
D[s] = 0
back = self .mappingFactory(G.V)
back[s] = s
added = self .setFactory(G.V)
added.add(s)
while len(D) > 0:
(v, Dv) = D.extract opt item()
yield (v, back[v] if v in back else None)
added.add(v)
for w in G.succs(v):
if w not in added and Dv + d(v,w) < D[w]:
D[w] = Dv + d(v,w)
back[w] = v
demos/graphalgorithms/dijkstrabackpointers.py
from algoritmia.graphalgorithms.positivedigraphshortestpaths import *
from algoritmia.data.mallorca import Mallorca, km
sp =DijkstraWithPriorityDictShortestPaths()
for (u, v) in sp.backpointers from one to all(Mallorca, km, Andratx):
print(({}, {}).format(u, v), end = ", ")
(Andratx, Andratx), (Calvi`a, Andratx), (Palma de Mallorca, Calvi`a), (Marratx,
Palma de Mallorca), (Llucmajor, Palma de Mallorca), (Inca, Marratx), (Soller
, Andratx), (Campos del Port, Llucmajor), (Santany, Campos del Port), (Alcudi
a, Inca), (Manacor, Inca), (Pollenca, Alcudia), (Art`a, Manacor), (Capdepera, A
rt`a),
El algoritmo sigue efectuando V 1 iteraciones, pero las operaciones que realiza y
su coste respectivo es:
Se extrae el v ertice v con menor valor de D[v], pero esta operaci on ahora requiere
tiempo O(lg V). El coste global de estas extracciones es O(V lg V).
Se recorren los O(V) sucesores de V. Como cada v ertice s olo se extrae una vez de
la frontera y sus sucesores, por tanto, s olo se recorren una vez, el coste temporal
total de estos recorridos es O(E).
Si uno de los sucesores w ha de actualizar el valor de D[w], hemos de actualizar su
ubicaci on en el diccionario de prioridad con coste O(lg V). El coste global de estas
actualizaciones es O(E lg V).
As pues, el coste temporal global es O(E lg V).
N otese que si el grafo es denso, esta modicaci on del algoritmo es m as ineciente. En
grafos con E sensiblemente inferior a V
2
, la versi on del algoritmo que usa un diccio-
nario de prioridad con montculo resulta preferible.
Existe una implementaci on a un m as eciente que se basa en la implementaci on de
las colas de prioridad con montculos de Fibonacci (Fibonacci heaps). Su coste temporal es
O(E +V lg V).
242 Apuntes de Algoritmia 28 de septiembre de 2009
Camino m as corto entre un par de v ertices en un grafo ponderado positivo
Frecuentemente no necesitamos conocer todos los caminos m as cortos de s a cualquier
otro v ertice: s olo deseamos conocer el camino m as corto de s a un v ertice destino t. El
algoritmo de Dijkstra puede modicarse trivialmente para efectuar este c alculo: basta
con detener las iteraciones cuando el v ertice t ingresa en el arbol de recubrimiento:
algoritmia/graphalgorithms/positivedigraphshortestpaths.py
class DijkstraShortestPaths:
...
def shortest path(self , G: "positive Digraph<T>",
d: "f: T, T -> R", s: "T", t: "T") -> "iterable<T>":
back = self .mappingFactory(G.V)
for (v, u) in self .backpointers from one to all(G, d, s):
back[v] = u
if v == t: break
return backtrace(back, t)
demos/graphalgorithms/dijkstrashortestpath.py
from algoritmia.graphalgorithms.positivedigraphshortestpaths import *
from algoritmia.data.mallorca import Mallorca, km
sp = DijkstraWithPriorityDictShortestPaths()
path = sp.shortest path(Mallorca, km, Andratx, Manacor)
print(, .join(path)+.)
print(Distancia:, sum(km(path[i], path[i+1]) for i in range(len(path)-1)))
Andratx, Calvi`a, Palma de Mallorca, Marratx, Inca, Manacor.
Distancia: 79
El coste de este nuevo algoritmo para el peor de los casos es igual al del anterior: en
el peor caso, el v ertice destino ingresa en added tras haberlo hecho todos los dem as.
: E R
0
tal que
d
(u, v) + d
(v, w) d
(s, t)
added = self .setFactory(G.V)
while len(fringe) > 0:
v = fringe.extract opt()
added.add(v)
if v == t: break
for w in G.succs(v):
if w not in added and D[v] + d(v, w) < D[w]:
D[w] = D[v] + d(v,w)
fringe[w] = D[w] + d
(w, t)
back[w] = v
return backtrace(back, t)
Podemos probar con el grafo de Iberia usando la funci on km para ponderar las aristas
y como estimaci on optimista de la distancia entre dos v ertices:
demos/graphalgorithms/euclideangraph.py
from algoritmia.graphalgorithms.metricdigraphshortestpaths import MetricDigraphShortestPaths
from algoritmia.data.iberia import iberia, coords
from math import sqrt
def km(u, v, coords=coords):
246 Apuntes de Algoritmia 28 de septiembre de 2009
p, q = coords[u], coords[v]
return sqrt((p[0]-q[0])**2 + (p[1]-q[1])**2)
mdsp = MetricDigraphShortestPaths()
print(Camino {}.format(mdsp.shortest path(iberia, km, km, Ciudad Real, Soria)))
Camino [Ciudad Real, Toledo, Madrid, Venturada, Riaza, San Esteban
de Gormaz, Burgo de Osma, Soria]
En la gura 4.34 (b) se muestra c omo se ha reducido el c alculo al utilizar el algoritmo
con estimaci on de mejor compleci on del camino en el c alculo de la distancia m as corta
de Ciudad Real a Soria.
La demostraci on de correcci on del algoritmo se apoya en la del algoritmo de Dijkstra.
Cuando un v ertice v ingresa en added, el hecho de haberlo seleccionado a partir de una
puntuaci on que incluye la suma de d
V) para
grafos dispersos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
91 Deseamos encontrar el camino m as corto en un laberinto descrito mediante una matriz de
n m valores booleanos, la la y columna de la entrada y la la y columna de la salida. Una
casilla es un muro si su celda correspondiente en la matriz es el valor cierto, y es pasillo en caso
contrario.
He aqu un ejemplo de laberinto con entrada en (3, 0) y salida en (5, 6) en el que hemos
pintado de negro los muros.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 247
0 1 2 3 4 5 6
0
1
2
3
4
5
6
El camino m as corto es la secuencia de casillas (3, 0), (3, 1), (4, 1), (4, 2), (4, 3), (4, 4), (4, 5), (5, 5)
y (6, 5).
Te pedimos:
a) Que dise nes un algoritmo inspirado en el que hemos estudiado para que encuentre, en cual-
quier laberinto, el camino m as corto entre su entrada y salida, si este existe.
b) Que indiques el comportamiento del algoritmo cuando no existe un camino entre la entrada y
la salida.
c) Que lo modiques para que encuentre todos los caminos de longitud igual que la longitud del
m as corto.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.10. Caminos m as cortos entre todo par de v ertices
de un digrafo
Dado un digrafo G = (V, E) ponderado por una funci on d : E Ry sin ciclos negativos.
Deseamos calcular la distancia entre todo par de v ertices u y v.
Podemos hacerlo aplicando V veces un algoritmo de c alculo de la distancia del ca-
mino m as corto de un v ertice a todos los dem as. El algoritmo de Floyd-Warshall, que
mostramos a continuaci on, efect ua el c alculo directamente. Para expresar el algoritmo de
Floyd-Warshall hemos de suponer un orden arbitrario en el conjunto de v ertices. Supon-
gamos, pues, que V = {v
1
, v
2
, . . . , v
V
}.
Sea D(u, w, k) la distancia del camino m as corto de u a w cuyos v ertices intermedios
pertenecen al conjunto {v
1
, v
2
, . . . , v
k
}. Cuando k = 0, el conjunto de los v ertices interme-
dios es el conjunto vaco, as que el valor de D(u, w, 0) se puede deducir f acilmente: 0 si
u = w; d(u, w) si (u, w) es una arista del grafo; y + en otro caso. El valor de D(u, w, k),
para k > 0, se puede deducir de valores de la forma D(, , k 1): si el camino m as corto
de u a w no contiene al v ertice de v
k
, entonces su valor es D(u, w, k 1); y si lo contiene,
entonces es el camino m as corto de u a v
k
que pasa por v ertices de {v
1
, v
2
, . . . , v
k1
} unido
al camino m as corto de v
k
a w que pasa por v ertices de {v
1
, v
2
, . . . , v
k1
}. Esta ecuaci on
recursiva recoge esa idea:
D(u, v, k) =
0, si k = 0 y u = v;
d(u, v), si k = 0 y (u, v) E;
+ si k = 0, u = v y (u, v) / E;
mn
{
D(u, v, k 1),
D(u, w, k 1) + D(w, v, k 1)
}
, en otro caso.
248 Apuntes de Algoritmia 28 de septiembre de 2009
Los valores que deseamos conocer son D(u, v, V) para todo u, v V. Una implementa-
ci on recursiva inmediata de la ecuaci on conduce a c alculos repetidos y resulta, en conse-
cuencia, ineciente.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
92 Dado este digrafo ponderado en el que todas las aristas tienen peso unitario:
0 1
2
Dibuja el arbol de llamadas recursivas para calcular D(u, w, V) para todo u, v V.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
El orden de resoluci on de la recurrencia que garantiza que todo valor est a disponible
cuando se necesita es sencillo: el grafo de dependencias es un grafo multietapa en el que
todos los v ertices de la forma (u, w, k) con un mismo valor de k forman una etapa. Dentro
de cada etapa se pueden efectuar un recorrido en orden arbitrario. He aqu una primera
versi on de un algoritmo iterativo:
algoritmia/graphalgorithms/allshortestpaths.py
class AllShortestPathDraft:
def init (self , **kw):
get factories(self , kw, mappingFactory=lambda V: dict())
def distances all to all(self , G: "Digraph<T>", d: "f: T, T -> R"
) -> "mapping<(T, T), R>":
D = self .mappingFactory(G.V)
for u in G.V:
for w in G.V:
if u == w: D[u, w, None] = 0
elif w in G.succs(u): D[u, w, None] = d(u,w)
else: D[u, w, None] = innity
vk 1 = None
for vk in G.V:
for u in G.V:
for w in G.V:
D[u, w, vk] = min(D[u, w, vk 1], D[u, vk, vk 1] + D[vk, w, vk 1])
vk 1 = vk
distance = self .mappingFactory(G.V)
for u in G.V:
for w in G.V:
distance[u, w] = D[u, w, vk]
return distance
El coste temporal y espacial es (V
3
). Podemos reducir el coste espacial con este
renamiento:
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 249
algoritmia/graphalgorithms/allshortestpaths.py
class AllShortestPaths:
def init (self , **kw):
get factories(self , kw, mappingFactory=lambda V: dict())
def distances all to all(self , G: "Digraph<T>", d: "f: T, T -> R"
) -> "mapping<(T, T), R>":
D = self .mappingFactory(G.V)
for u in G.V:
for w in G.V:
if u == w: D[u,w] = 0
elif w in G.succs(u): D[u,w] = d(u,w)
else: D[u,w] = innity
for vk in G.V:
for u in G.V:
for w in G.V:
D[u,w] = min(D[u,w], D[u,vk] + D[vk,w])
return D
El coste espacial es ahora (V
2
).
Probemos el algoritmo sobre el grafo con el mapa de carreteras de la isla de Mallorca
para obtener una tabla de distancias mnimas entre cualquier par de ciudades de la isla.
demos/graphalgorithms/floydwarshall.py
from algoritmia.graphalgorithms.allshortestpaths import AllShortestPaths
from algoritmia.data.mallorca import Mallorca, km
mindist = AllShortestPaths().distances all to all(Mallorca, km)
print( , end="")
for u in Mallorca.V: print({:4}.format(u[:3]), end="")
print()
for u in Mallorca.V:
print({:4}.format(u[:3]), end="")
for v in Mallorca.V: print({:4}.format(mindist[u,v]), end="")
print()
Cal Man Pol Art Cap San And Cam Sol Pal Alc Mar Llu Inc
Cal 0 65 75 82 90 61 14 48 70 14 65 28 34 40
Man 65 0 60 17 25 27 79 40 114 51 50 37 54 25
Pol 75 60 0 46 54 87 89 95 54 61 10 47 81 35
Art 82 17 46 0 8 44 96 57 100 68 36 54 71 42
Cap 90 25 54 8 0 52 104 65 108 76 44 62 79 50
San 61 27 87 44 52 0 75 13 131 47 77 61 27 52
And 14 79 89 96 104 75 0 62 56 28 79 42 48 54
Cam 48 40 95 57 65 13 62 0 118 34 85 48 14 60
Sol 70 114 54 100 108 131 56 118 0 84 64 98 104 89
Pal 14 51 61 68 76 47 28 34 84 0 51 14 20 26
Alc 65 50 10 36 44 77 79 85 64 51 0 37 71 25
Mar 28 37 47 54 62 61 42 48 98 14 37 0 34 12
Llu 34 54 81 71 79 27 48 14 104 20 71 34 0 46
250 Apuntes de Algoritmia 28 de septiembre de 2009
Inc 40 25 35 42 50 52 54 60 89 26 25 12 46 0
Como puede apreciarse, el algoritmo de Floyd est a directamente inspirado en el al-
goritmo de Warshall para el c alculo de la clausura transitiva de un grafo (v ease la sec-
ci on 4.6). La correcci on del c alculo del algoritmo de Floyd es an aloga a la del algoritmo
de Warshall.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
93 Demuestra la validez de esta ecuaci on recursiva propuesta como soluci on del problema.
(Insprate en la demostraci on de validez del algoritmo de Warshall para el cierre transitivo de un
digrafo.)
94 La excentricidad de un v ertice es la mayor longitud (n umero de aristas) del camino m as corto
entre el y cualquier otro.
a) El radio de un grafo G es la excentricidad m as peque na de cualquiera de sus v ertices.
b) El di ametro de un grafo G es la excentricidad m as grande de cualquiera de sus v ertices.
c) El centro de un grafo G es el conjunto de v ertices de menor excentricidad.
d) El permetro de un grafo G es la longitud (n umero de aristas) de su ciclo m as corto.
Dise na funciones para el c alculo del radio, el di ametro, el centro y el permetro de un grafo G.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La t ecnica de punteros hacia atr as permite recuperar el camino optimo, pero en este
problema los punteros hacia atr as son un tanto peculiares y merecen especial atenci on.
La matriz back, indexada por pares (u, w), almacenar a el v ertice v
k
responsable de la
asignaci on D(u, w, k) = D(u, v
k
, k 1) + D(v
k
, w, k 1). La recuperaci on de cada camino
optimo requerir a recorrer adecuadamente esta estructura de punteros hacia atr as.
algoritmia/graphalgorithms/allshortestpaths.py
def shortest path backpointers all to all(self , G: "Digraph<T>", d: "f: T, T -> R"
) -> "mapping<T, (T or None)>":
D, back = self .mappingFactory(G.V), self .mappingFactory(G.V)
for u in G.V:
for w in G.V:
if u == w: D[u,w] = 0
elif w in G.succs(u): D[u,w] = d(u,w)
else: D[u,w] = innity
back[u,w] = None
for vk in G.V:
for u in G.V:
for w in G.V:
if D[u,vk] + D[vk,w] < D[u,w]:
D[u,w] = D[u,vk] + D[vk,w]
back[u,w] = vk
return back
def shortest paths(self , G: "Digraph<T>", back: "mapping<T, (T or None)>"):
def backtrace(u: "T", w: "T"):
vk = back[u, w]
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 251
return backtrace(u, vk) + [vk] + backtrace(vk, w) if vk != None else []
for (u,w) in back:
path = [u] + backtrace(u, w) + [w]
if all((path[i], path[i+1]) in G.E for i in range(len(path)-1)):
yield path
Mostramos s olo 20 de los caminos calculados por el algoritmo:
demos/graphalgorithms/floydwarshallpaths.py
from algoritmia.graphalgorithms.allshortestpaths import AllShortestPaths
from algoritmia.data.mallorca import Mallorca, km
back = AllShortestPaths().shortest path backpointers all to all(Mallorca, km)
for i, path in enumerate(sorted(AllShortestPaths().shortest paths(Mallorca, back))):
print(, .join(path))
if i == 20: break
print("...")
Alcudia, Art`a
Alcudia, Art`a, Capdepera
Alcudia, Inca
Alcudia, Inca, Manacor
Alcudia, Inca, Manacor, Santany
Alcudia, Inca, Marratx
Alcudia, Inca, Marratx, Palma de Mallorca
Alcudia, Inca, Marratx, Palma de Mallorca, Calvi`a
Alcudia, Inca, Marratx, Palma de Mallorca, Calvi`a, Andratx
Alcudia, Inca, Marratx, Palma de Mallorca, Llucmajor
Alcudia, Inca, Marratx, Palma de Mallorca, Llucmajor, Campos del Port
Alcudia, Pollenca
Alcudia, Pollenca, Soller
Andratx, Calvi`a
Andratx, Calvi`a, Palma de Mallorca
Andratx, Calvi`a, Palma de Mallorca, Llucmajor
Andratx, Calvi`a, Palma de Mallorca, Llucmajor, Campos del Port
Andratx, Calvi`a, Palma de Mallorca, Llucmajor, Campos del Port, Santany
Andratx, Calvi`a, Palma de Mallorca, Marratx
Andratx, Calvi`a, Palma de Mallorca, Marratx, Inca
Andratx, Calvi`a, Palma de Mallorca, Marratx, Inca, Alcudia
...
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
95 Si los pesos de los arcos son positivos, podemos calcular el camino m as corto entre todo par
de v ertices ejecutando V veces el algoritmo de Dijkstra. Qu e coste presenta este otro algoritmo?
Es m as o menos eciente que el algoritmo de Floyd-Warshall con grafos dispersos?
96 El algoritmo de Johnson utiliza la t ecnica expuesta en el ejercicio anterior, pero es de aplica-
ci on tambi en en grafos con pesos no negativos. Tras detectar que no hay ciclos de peso negativo
(en cuyo caso el problema est a mal formulado), ejecuta el algoritmo de Bellman-Ford sobre un
grafo modicado y, con el resultado obtenido, cambia el peso de todas las aristas, que pasan a
ser positivas. El nuevo peso de las aristas es tal que el camino m as corto entre un par de v ertices
252 Apuntes de Algoritmia 28 de septiembre de 2009
del grafo original lo es tambi en en el grafo modicado. Averigua los detalles del algoritmo de
Johnson consultando fuentes bibliogr acas y analiza el coste del algoritmo y los casos en los que
resulta preferible al algoritmo de Floyd-Warshall.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.11. Algunas consideraciones sobre los algoritmos
para el c alculo del camino m as corto
En diferentes captulos hemos estudiado diversos algoritmos con cometidos similares:
calcular el camino m as corto entre un v ertice s y otro t en un digrafo ponderado, o el
conjunto de caminos m as cortos entre un v ertice s y cualquier otro, o el camino m as corto
entre todo par de v ertices. Cada uno de los algoritmos estudiados presenta una serie de
restricciones que limitan su aplicaci on a determinados tipos de digrafo. Deteng amonos
para considerarlos.
Lo cierto es que los algoritmos que calculan el camino m as corto entre un par de
v ertices s y t son variantes sencillas de algoritmos que calculan el conjunto de caminos
m as cortos entre un v ertice s y todos los dem as que detienen el c alculo al conocer la
soluci on con destino en t. Los algoritmos que hemos estudiado son:
Si todas las aristas del digrafo presentan id entico peso, un recorrido por primero
en anchura calcula el conjunto de caminos m as en cortos en tiempo O(V +E).
Si el digrafo es acclico, sea cual sea la funci on de ponderaci on, el algoritmo presen-
tado en la secci on 4.8.1 efect ua el c alculo en tiempo O(V +E).
Si la funci on de ponderaci on es positiva y el grafo no es acclico, diferentes versio-
nes del algoritmo de Dijkstra efect uan el c alculo en tiempo:
O(V
2
) sin usar diccionario de prioridad (eciente en digrafos completos).
O(V +E lg V) usando un diccionario de prioridad basado en un heap.
O(E +V lg V) usando un diccionario de prioridad basado en un heap de
Fibonacci.
Si el grafo es m etrico, el coste en el peor de los casos coincide con el de Dijkstra,
pero hay una aceleraci on notable en la pr actica si se hace participar la distancia del
nal de cada camino parcial al destino.
Si la funci on de ponderaci on no es positiva pero el grafo no presenta ciclos negati-
vos, el algoritmo de Bellman-Ford resuelve el problema en tiempo O(VE).
El camino m as corto entre todo par de v ertices puede calcularse mediante el algorit-
mo de Floyd-Warshall en tiempo O(V
3
), pero tambi en ejecutando V veces el c alculo
del camino m as corto entre un v ertice y todos los dem as. Tiene inter es observar que
en digrafos con aristas de peso id entico o en digrafos acclicos, el coste temporal de es-
ta propuesta alternativa es O(VE). En el caso de grafos densos, este coste temporal
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 253
es comparable al de Floyd-Warshall, pero en grafos dispersos es preferible el algoritmo
alternativo. Cuando el grafo presenta una funci on de ponderaci on positiva, el coste tem-
poral de V ejecuciones del algoritmo de Dijkstra en su versi on con heaps con Fibonacci
es O(VE + V
2
lg V), que puede ser preferible al algoritmo de Floyd-Warshall en
grafos dispersos. Si el grafo tiene pesos negativo, el algoritmo de Johnson ofrece mejor
complejidad asint otica que el de Floyd-Warshall.
Captulo 5
DIVIDE Y VENCER
AS
La estrategia divide y vencer as (en ingl es, divide and conquer) consiste en dividir
un problema en una serie de subproblemas (problemas similares de menor talla), so-
lucionarlos independientemente y, nalmente, combinar sus respectivas soluciones para
formar con ellas la soluci on del problema original. La soluci on de los subproblemas pue-
de pasar por aplicar la misma estrategia recursivamente. La recursi on nalizar a cuando
el (sub)problema al que nos enfrentemos sea de talla tan reducida que pueda resolverse
por alg un procedimiento alternativo (y, frecuentemente, trivial).
Quiz a sera m as apropiado denominar a la estrategia divide, resuelve y combina,
pues son estos sus tres elementos fundamentales (aunque estudiaremos una variante
de la estrategia a la que llamamos reduce y vencer as en ingl es, decrease and con-
quer que quiz a convendra llamar reduce, resuelve y procesa).
La ordenaci on por fusi on fue inventada por John von Neumann en 1945,
quien propuso su implementaci on y uso en el EDVAC, uno de los primeros
ordenadores.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
99 Dise na una versi on del algoritmo de ordenaci on por fusi on que divida el vector en tres partes
iguales resolviendo recursivamente la ordenaci on de cada una de ellas. Para ello, deber as escribir
otro algoritmo de fusi on que act ue sobre tres vectores ordenados. Comp aralo con la versi on es-
tudiada. Qu e coste asint otico tiene? Implementa esta versi on y compara su tiempo de ejecuci on
con el de la versi on convencional. Es la nueva versi on m as r apida en la pr actica? Y si hablamos
de memoria, presenta un menor consumo asint otico de memoria? Y en la pr actica, consume
menos memoria?
100 El bucle principal de merge obliga a efectuar dos comparaciones por iteraci on. Hay una
variante del m etodo que ahorra una comparaci on: la fusi on con centinelas. Consiste en disponer,
al ordenar un vector de tama no n, de un vector de tama no n/2 +1 y otro de tama no n/2 +1.
En cada uno de ellos se copia una mitad del vector original, pero se a nade al nal de cada uno
un valor m as grande que ninguno de los elementos de dicho vector (un valor innito, por
as decirlo). El bucle itera hasta que se hayan copiado n elementos en el destino. (N otese que los
centinelas jam as se copiar an en el destino.)
Implementa el algoritmo de fusi on con centinelas para una versi on del algoritmo en el que el
valor innito se proporciona mediante un par ametro adicional.
101 Dise na una versi on del algoritmo de ordenaci on por fusi on que ordene una lista enlazada
en lugar de un vector. La funci on merge recibir a dos listas enlazadas ordenadas y devolver a una
nueva lista enlazada, tambi en ordenada. La lista original puede destruirse en el proceso. Estudia
el coste temporal de esta versi on y comp aralo con el de la versi on convencional.
262 Apuntes de Algoritmia 28 de septiembre de 2009
102 Es posible implementar iterativamente la ordenaci on por fusi on de una lista enlazada.
En lugar de proceder descendentemente (top-down), procesamos los datos ascendentemente
(bottom-up). Consideremos que la talla de la lista es potencia de 2 (no resulta difcil adaptar este
m etodo a cualquier talla). En una primera fase, se divide la lista en n listas con un elemento cada
una y se almacenan en una cola. En cada paso se toman las dos primeras listas de la cola y se
funden en una nueva, que pasa a ingresar en la cola. El procedimiento naliza cuando queda una
sola lista en la cola.
Implementa el m etodo y analiza el coste temporal y espacial de este algoritmo iterativo.
103 La ordenaci on por fusi on natural (natural mergesort) permite acelerar la ordenaci on
iterativa de una lista enlazada (ver el ejercicio anterior). En una primera fase, la lista original
se divide en fragmentos formados por elementos consecutivos ya ordenados. A continuaci on se
procede a ordenar siguiendo el m etodo iterativo.
Analiza el coste temporal en el mejor y peor de los casos de la ordenaci on por fusi on natural.
104 Podemos ordenar vectores siguiendo el mismo principio de funcionamiento iterativo que
hemos indicado para listas enlazadas. Basta con introducir en la cola los ndices que identican
cada fragmento pendiente de fusi on.
Implementa una versi on iterativa de la ordenaci on por fusi on sobre vectores.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.4. Un renamiento: ordenaci on in situ y reducci on del
consumo de memoria
La versi on presentada adolece de un problema: efect ua numerosas reservas y liberacio-
nes de memoria, pues solicita memoria para cada vector resultante de la divisi on de otro
y para los vectores en los que se almacenan resultados intermedios. Es posible ahorrar
memoria y copias de bloques de memoria si nos ayudamos de un unico vector auxiliar.
En la siguiente versi on no generamos explcitamente los vectores que resultan del pro-
ceso de divisi on, sino que los representamos implcitamente a partir de los ndices que
se nalan su principio y su nal. El resultado de mergesort no es, pues, un nuevo vector con
el contenido del original ordenado, sino una modicaci on del vector original, que pasa a
estar ordenado (ordenaci on in situ):
algoritmia/problems/sorting.py
class InPlaceMergeSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
self . mergesort(a, 0, len(a), [None]*len(a))
def merge(self , a: "sequence<T>", p: "int", q: "int", c: "sequence<T>"):
# Funde los subvectores v[p:(p + q)//2] y v[(p + q)//2:q].
i, j, k = p, (p+q)//2, p
while i < (p+q)//2 and j < q:
if a[i] < a[j]: c[k] = a[i]; i += 1
else: c[k] = a[j]; j += 1
k += 1
while i < (p+q)//2: c[k] = a[i]; i += 1; k += 1
while j < q: c[k] = a[j]; j += 1; k += 1
for k in range(p, q): a[k] = c[k]
28 de septiembre de 2009 Captulo 5. Divide y vencer as 263
def mergesort(self , a, p, q, c):
if q-p > 1:
self . mergesort(a, p, (p+q)//2, c)
self . mergesort(a, (p+q)//2, q, c)
self .merge(a, p, q, c)
demos/divideandconquer/inplacemergesort.py
from algoritmia.problems.sorting import InPlaceMergeSorter
a = [11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37]
InPlaceMergeSorter().sort(a)
print(Ordenado:, a)
Ordenado: [0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
El ndice del elemento que ocupa la posici on central del vector se puede
calcular mediante la f ormula (p + q)/2, pero esta es problem atica en len-
guajes como C o C++. Si el vector es de gran tama no, la suma p +q puede provocar
un error de desbordamiento que no detiene la ejecuci on del c alculo y produce un re-
sultado err oneo. El mismo c alculo se puede realizar de forma segura con la f ormula
alternativa p +(q p)/2.
La gura 5.5 muestra esquem aticamente qu e hace la nueva versi on de merge. Por
otra parte, la gura 5.6 muestra gr acamente una traza de la llamada a InPaceMergeSor-
ter.sort sobre el vector [55,22,44,66,11,33,22,66] y representa explcitamente, desple-
gando las llamadas a funci on, cada una de las activaciones de mergesort y merge.
a
p q p+q
2
ordenado ordenado
fusi on
c
p q
ordenado
copia
a
p q
ordenado
Figura 5.5: Gesti on de la memoria auxiliar
al llamar a merge(p, q). Se recibe un vec-
tor en el que los cortes a[p:(p+q)//2] y
a[(p+q)//2:q] est an ordenados. La funci on
usa un vector auxiliar c para almacenar tem-
poralmente una versi on ordenada de v[p:q].
El mismo vector c se usa en todas las lla-
madas recursivas a mergesort, consumiendo
as menos tiempo en la gesti on de memoria
din amica. Un ultimo paso copia c[p:q] en
a[p:q].
Los costes asint oticos temporal y espacial son id enticos a los de la anterior versi on de
la ordenaci on por fusi on, mergesort, aunque ahora la divisi on del vector original en dos
requiere tiempo constante.
0im
(
b
k
a
)
i
.
Si a < b
k
, el sumatorio es una serie geom etrica con raz on mayor que 1, con lo que
T(n) = a
m
(b
k
/a)
m+1
1
(b
k
/a) 1
= O(a
m
(b
k
/a)
m
) = O((b
k
)
m
) = O(n
k
).
Si a = b
k
, los elementos del sumatorio son todos iguales a 1, con lo que T(n) = O(a
m
m) =
O(a
log
b
n
log
b
n) = O(n
k
log
b
n). Y si a > b
k
, el sumatorio es una serie geom etrica con raz on
menor que 1, con lo que est a acotada superiormente por una constante y tenemos que
T(n) = O(a
m
) = O(a
log
b
n
) = O(n
log
b
a
).
Resolvamos un par de ecuaciones recursivas con el m etodo maestro para aprender a
aplicarlo. Empecemos con una recurrencia sencilla:
T(n) =
{
1, si n 1;
4T(n/2) + n, si n > 1.
El primer paso consiste en identicar en nuestra ecuaci on a, b y k, que en este caso son
a = 4, b = 2 y k = 1 (ya que n (n
1
)). Comparamos a = 4 con b
k
= 2
1
= 2 y
28 de septiembre de 2009 Captulo 5. Divide y vencer as 271
comprobamos que a > b
k
, as que T(n) O(n
log
2
4
) = O(n
2
). Un algoritmo cuyo coste
temporal se exprese como en la ecuaci on es un algoritmo cuadr atico.
Consideremos ahora la ecuaci on recursiva con la que expresamos el coste de la fun-
ci on in place mergesort:
T(n) =
{
(1), si n 1;
2T(n/2) +(n), si n > 1.
El valor de a y b es 2 (el vector de talla n se divide en 2 problemas de talla n/2). El coste
de cada uno de los elementos de la ordenaci on por fusi on resulta f acil de determinar: di-
vide parte el vector en dos mediante la asignaci on de valores a ndices, as que es (1).
Determinar si un vector es simple, es decir, si tiene talla 0 o 1, es una operaci on (1). La
soluci on trivial consiste en no hacer nada: un vector de un solo elemento siempre est a or-
denado. La fusi on mediante merge de dos vectores cuya suma de tallas es n requiere (n)
pasos. En consecuencia, k = 1. Dado que a = 2 = b
k
, tenemos T(n) O(n lg n).
Hay otro par de corolarios del teorema maestro (que presentamos sin demostraci on)
que pueden ser de gran ayuda a la hora de resolver ecuaciones recursivas divide y
vencer as:
Teorema 5.2 La soluci on a la ecuaci on T(n) = aT(n/b) +O(n
k
log
p
n), con a 1, b > 1 y
p 0, es:
si a < b
k
si a = b
k
si a > b
k
O(n
k
log
p
n) O(n
k
log
p+1
n) O(n
log
b
a
)
Teorema 5.3 La soluci on a la ecuaci on T(n) =
1ij
T(
i
n) + O(n), donde
1ij
i
< 1,
es T(n) O(n).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
109 Utiliza el m etodo maestro (con cualquiera de los teoremas presentados) para acotar asint oti-
camente la funci on T(n) denida recursivamente como sigue:
a) T(n) =
{
1, si n 1;
9T(n/3) + n, si n > 1.
b) T(n) =
{
1, si n 1;
T(2n/3) +1, si n > 1.
c) T(n) =
{
1, si n 1;
3T(n/4) +lg n, si n > 1.
d) T(n) =
{
1, si n 1;
5T(n/4) + n, si n > 1.
e) T(n) =
{
1, si n 1;
4T(n/2) + n
2
, si n > 1.
f) T(n) =
{
1, si n 1;
4T(n/2) + n
3
, si n > 1.
g) T(n) =
{
1, si n 1;
T(n/2) +1, si n > 1.
h) T(n) =
{
1, si n 1;
2T(n/2) +1, si n > 1.
272 Apuntes de Algoritmia 28 de septiembre de 2009
i) T(n) =
{
1, si n 1;
2T(n/2) + n lg n, si n > 1.
j) T(n) =
{
1, si n 1;
T(n/3) + T(n/4) + n, si n > 1.
110 Dibuja el arbol de recursi on para un algoritmo de ordenaci on inspirado en mergesort que
divida el vector en dos vectores: uno de talla 10 y otro de talla n 10. Deduce el coste temporal y
espacial de este algoritmo a partir de dicho arbol.
111 Demuestra la veracidad del teorema 5.1 en el caso particular de k = 1 sin recurrir al teorema
maestro (utiliza el desplegado y la demostraci on por inducci on).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.4. Complejidad espacial
El an alisis de la complejidad espacial se basa en el estudio del consumo de memoria
de una activaci on de la funci on recursiva y del m aximo n umero de llamadas recursivas
activas simult aneamente, es decir, de la profundidad del arbol de llamadas recursivas.
Cuando la recursi on encaja con el modelo del teorema maestro, la profundidad del arbol
es (log
b
n), pues la divisi on de un problema de talla n proporciona subproblemas de
talla igual o inferior a n/b, con b > 1. Si el espacio de pila que consume cada activa-
ci on de funci on es constante, el coste espacial es (log
b
n). En caso contrario habr a que
calcular el sumatorio
0ilog
b
n
s(n/b
i
),
donde s(n) es el espacio de pila consumido por la trama de activaci on de la funci on sobre
un problema de talla n.
30 14 2 0
2 3 10 12
15 16 22 23
25 30 38 45
0, si n = 1;
1, si n = 2;
C(n/2) + C(n/2) +2, si n > 2.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 285
Si n es potencia de 2, podemos expresar la ecuaci on recursiva as:
C(n) =
0, si n = 1;
1, si n = 2;
2C(n/2) +2, si n > 2.
Por desplegado llegamos a la expresi on
C(n) =
(
1k<lg n
2
k
)
+
n
2
= 2
lg n
2 +
n
2
= n 2 +
n
2
=
3n
2
2.
El n umero de comparaciones se reduce a un m aximo de 3n/2. Asint oticamente,
tanto el algoritmo que sigue una aproximaci on directa como el que sigue la estrategia
divide y vencer as son O(n). El benecio que nos reporta el segundo es de ndole pr acti-
ca, pues reduce signicativamente el n umero comparaciones para el peor caso (en torno
al 25 %). Interesa el esfuerzo y la mayor complejidad de la implementaci on cuando la
ganancia obtenida reduciendo el n umero de comparaciones puede verse compensada
por el sobrecoste de las llamadas recursivas? En ciertos problemas los objetos almace-
nados en el vector son grandes y/o efectuar comparaciones entre ellos puede resultar
extremadamente costoso. En tales supuestos, la reducci on del n umero de comparaciones
puede ser una gran ventaja.
n +1
n
(
n
n 1
T(n 2) +2c
2
)
+2c
2
n +1
n
(
n
n 1
(
n 1
n 2
T(n 3) +2c
2
)
+2c
2
)
+2c
2
n +1
n 2
T(n 3) +
0i<3
n +1
n +1 i
2c
2
n +1
n 2
T(n 3) + (n +1)2c
2
0i<3
1
n +1 i
n +1
n +1 k
T(n k) + (n +1)2c
2
0i<k
1
n +1 i
.
Se alcanza un caso base cuando k = n 1:
T(n)
n +1
2
T(1) + (n +1)2c
2
0i<n1
1
n +1 i
n +1
2
c
1
+ (n +1)2c
2
2in
1
i +1
.
El ultimo sumatorio es, salvo por una diferencia constante, el n- esimo n umero arm onico
(v ease la secci on B.9.7), as que es O(ln n). Podemos concluir que quicksort se ejecuta con
coste temporal promedio O(n lg n).
5.8.3. Aleatorizaci on de quicksort
Hemos visto que uno de los casos p esimos para quicksort consiste en que se le suminis-
tren vectores ya ordenados o casi ordenados (creciente o decrecientemente). Es probable
que estos casos se den en muchas aplicaciones pr acticas (por ejemplo, al ordenar un vec-
tor tras eliminar y a nadir un n umero relativamente peque no de celdas a un vector ya
ordenado), por lo que es frecuente usar una versi on aleatorizada del procedimiento de
partici on:
algoritmia/problems/sorting.py
from random import randrange
...
class RandomizedInPlaceQuickSorter(BasicInPlaceQuickSorter):
def partition(self , a: "sequence<T>", p: "int", r: "int") -> "int":
q = randrange(p, r)
a[r-1], a[q] = a[q], a[r-1]
return BasicInPlaceQuickSorter. partition(self , a, p, r)
28 de septiembre de 2009 Captulo 5. Divide y vencer as 297
Al escogerse un pivote aleatoriamente es muy improbable que escojamos siempre el
que nos perjudica, aun si nos proporcionan un vector ya ordenado. No hay garanta de
que el peor caso no se siga dando, pero al menos ya no hay una dependencia con el hecho
de que el vector que hemos de ordenar presente alguna caracterstica perniciosa, como
que est e ordenado o casi ordenado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
140 Hay alguna conguraci on de datos en el vector que vamos a ordenar que garantice que la
versi on aleatorizada de quicksort tenga un comportamiento cuadr atico?
141 Qu e coste tiene el algoritmo quicksort aleatorizado cuando todos los elementos del vector
son iguales? Est a en el caso mejor o peor?
142 He aqu un algoritmo para la b usqueda dicot omica aleatorizada:
algoritmia/problems/randomizedbinsearch.py
from random import randrange
from algoritmia.problems.searching import ISearcher
class RandomizedBinarySearcher(ISearcher):
def index of (self , a, v):
return self . index of (a, v, 0, len(a))
def index of (self , a, v, i, k):
if k-i == 1:
if v == a[i]: return i
elif k-i > 1:
j = randrange(i, k)
if v == a[j]: return j
elif v < a[j]: return self . index of (a, v, i, j)
else: return self . index of (a, v, j+1, k)
return None
(La llamada randrange(i, k) devuelve un n umero aleatorio entre i y k 1 con una distribuci on
uniforme.)
a) Qu e costes temporal y espacial presenta el algoritmo en el mejor de los casos? Y en el
peor de los casos?
b) Qu e coste temporal presenta el algoritmo en promedio?
c) Presentara alguna ventaja una versi on iterativa de ese m etodo?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.8.4. Otras t ecnicas de selecci on del pivote
La selecci on de un pivote mediante la generaci on de un ndice aleatorio no es la unica
forma de generar un pivote que resulte probablemente bueno. Si deseamos obtener el
mejor pivote posible, hemos de seleccionar la mediana del vector, es decir, aquel valor
tal que la mitad de los elementos del vector son menores que el y la otra mitad son
mayores o iguales que el. Su ndice en el vector ordenado es n/2. Ello conducira a un
298 Apuntes de Algoritmia 28 de septiembre de 2009
coste O(n lg n) para el algoritmo de ordenaci on. En el pr oximo apartado consideramos
un algoritmo para la selecci on del k- esimo menor elemento de un vector, util para obtener
la mediana y que se ejecuta en tiempo O(n); pero las constantes de proporcionalidad que
afectan a su funci on de coste temporal son tan altas que no resulta de inter es pr actico.
Un heurstico que da buenos resultados es escoger como pivote la mediana de tres
elementos arbitrarios del vector. Una vez escogido, se intercambia con el elemento a[r-1]
y se efect ua la partici on. De este modo se reduce la probabilidad de seleccionar como
pivote un elemento extremo, pues para que ello ocurra los tres elementos seleccionados
deberan ser, consistentemente, de los menores o mayores del conjunto. En cualquier
caso, la reducci on efectiva de tiempo de ejecuci on con este m etodo, que se conoce con el
nombre mediana de tres, es marginal.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
143 Implementa una versi on de quicksort que seleccione el pivote como mediana de tres ele-
mentos cualesquiera. (Prueba, por ejemplo, con los elementos que ocupan las posiciones primera,
central y ultima del subvector que se ordena en cada paso.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.8.5. Eliminaci on de recursi on por cola
Un serio inconveniente de la versi on de quicksort que hemos considerado es la profundi-
dad que pueden llegar a alcanzar las llamadas recursivas. Un vector de talla 10 000, por
ejemplo, puede requerir una pila de llamadas a funci on con capacidad para almacenar
10 000 tramas de activaci on. Es f acil, pues, producir un desbordamiento de la pila de lla-
madas a funci on. La t ecnica de eliminaci on de la recursi on por cola permite eliminar la
ultima de las dos llamadas recursivas, la que se efect ua sobre el segundo subvector:
algoritmia/problems/sorting.py
class BasicSemiIterativeInPlaceQuickSorter(RandomizedInPlaceQuickSorter):
def quicksort(self , a: "sequence<T> -> sequence<T>", p: "int", r: "int"):
while r - p > 1:
pivot index = self . partition(a, p, r)
self . quicksort(a, p, pivot index)
p = pivot index + 1
Sin embargo, no conseguimos paliar as el potencial desbordamiento de pila: es posi-
ble que el primer subvector sea de talla similar a la del vector original y, por tanto, que
las llamadas recursivas sigan alcanzando una profundidad pr oxima a la talla del vector.
Podemos solventar el problema si efectuamos recursi on sobre el menor de los vectores
que resultan de la partici on: como es indiferente el orden en que se ordena cada uno de
ellos, podemos considerar que la ultima llamada recursiva se efect ua sobre el menor de
ambos:
algoritmia/problems/sorting.py
class SemiIterativeInPlaceQuickSorter1(RandomizedInPlaceQuickSorter):
def quicksort(self , a: "sequence<T> -> sequence<T>", p: "int", r: "int"):
while r - p > 1:
pivot index = self . partition(a, p, r)
28 de septiembre de 2009 Captulo 5. Divide y vencer as 299
if r - pivot index < pivot index - p:
self . quicksort(a, pivot index+1, r)
r = pivot index
else:
self . quicksort(a, p, pivot index)
p = pivot index + 1
Un ultimo renamiento de inter es pr actico consiste en desplegar la funci on partition
en el cuerpo de quicksort:
algoritmia/problems/sorting.py
class SemiIterativeInPlaceQuickSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
self . quicksort(a, 0, len(a))
def quicksort(self , a: "sequence<T> -> sequence<T>", p: "int", r: "int"):
while r - p > 1:
pivot = a[r-1]
i = p - 1
for j in range(p, r-1):
if a[j] <= pivot:
i += 1
a[i], a[j] = a[j], a[i]
a[i+1], a[r-1] = a[r-1], a[i+1]
pivot index = i + 1
if r - pivot index < pivot index - p:
self . quicksort(a, pivot index+1, r)
r = pivot index
else:
self . quicksort(a, p, pivot index)
p = pivot index + 1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
144 Modica la ultima versi on de quicksort para que particione escogiendo un pivote al azar.
145 Los vectores de talla 2 pueden ordenarse directamente, sin necesidad de calcular un pi-
vote. Modica la versi on que has escrito como resultado del ejercicio anterior para que ordene
directamente los vectores de talla 2.
146 Considera, adem as, la ordenaci on directa de vectores de 3 elementos, que requiere efectuar
unicamente 2 o 3 comparaciones.
147 Implementa una versi on de quicksort aleatorizado que no efect ue recursi on cuando se al-
cance un tama no de vector igual o inferior a cierto umbral: para ordenar esos vectores utilizar a el
algoritmo de ordenaci on por inserci on. Determina empricamente un buen valor del umbral y
compara el tiempo de ejecuci on de la nueva versi on con el del algoritmo original.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.8.6. La eciencia de quicksort en la pr actica
C omo de r apido es quicksort en la pr actica? La tabla 5.1 compara el tiempo medio de
ejecuci on de mergesort y (la ultima versi on de) quicksort sobre un conjunto de instancias
300 Apuntes de Algoritmia 28 de septiembre de 2009
aleatorias (programas implementados en Python y ejecut andose en un Intel Core 2 Duo
4 a 2.26 GHz sobre Microsoft Windows). Se puede apreciar que, efectivamente, quicksort
requiere una cantidad de tiempo signicativamente menor que mergesort (en torno a un
60 % para vectores de talla moderadamente grande). Sin embargo, cuando quicksort se
enfrenta a alguno de sus peores casos (un vector ordenado, por ejemplo), no es competi-
tivo frente a mergesort (v ease la tabla 5.2). El comportamiento de quicksort es claramente
cuadr atico: duplicar la longitud del vector supone cuadruplicar el tiempo de ejecuci on.
La versi on aleatorizada de quicksort no presenta el mismo efecto sobre su tiempo de eje-
cuci on (tabla 5.3): el comportamiento es similar al que cabe esperar en el caso promedio,
y mejor que el de mergesort (comp arense con los de la tabla 5.2).
n 1000 2000 3000 4000 5000 10000
mergesort 9.56 20.90 32.94 45.35 57.32 124.91
quicksort 3.93 8.43 13.24 18.72 23.97 52.08
Tabla 5.1: Comparaci on entre mergesort y quicksort. Tiempo medio (en ms) sobre 10 instancias aleatorias.
n 1000 2000 3000 4000 5000 10000
mergesort 9.24 19.80 31.11 42.16 54.08 116.25
quicksort 117.09 466.23 1057.42 1 881.13 2 938.53 12 014.23
Tabla 5.2: Comparaci on entre mergesort y quicksort. Tiempo (en ms) para un vector en orden inverso.
n 1000 2000 3000 4000 5000 10000
quicksort aleatorizado 5.65 12.18 19.22 25.60 34.26 68.47
quicksort 117.21 468.18 1060.70 1869.47 3036.95 11884.96
Tabla 5.3: Comparaci on entre quicksort y quicksort aleatorizado. Tiempo (en ms) para un vector en orden inverso.
Se debe ser consciente de que la versi on aleatorizada sigue teniendo un coste cuadr ati-
co para el peor de los casos, s olo que ese caso o casos peores son altamente improbables.
v)
Aqu tienes una implementaci on (y una ejecuci on) de esta idea que construye un vector con
los puntos que forman una curva de movimiento browniano:
302 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/problems/brownian.py
from random import gauss
from math import sqrt
class BrownianMotionpointsGenerator1:
def points(self , a: "pair of floats", b: "pair of floats", v: "float"):
points = [a]
self . recursion(a, b, v, points)
points.append(b)
return points
def recursion(self , pa, pb, v, points):
((xa, ya), (xb, yb)) = pa, pb
if (xb-xa) <= 1:
return
else:
delta = gauss(0, sqrt(v))
(xc,yc) = ((xa+xb)/2.0, (ya+yb)/2.0 + delta)
self . recursion((xa,ya), (xc,yc), v/2, points)
points.append((xc, yc))
self . recursion((xc,yc), (xb,yb), v/2, points)
Esta otra versi on tambi en genera una lista de puntos, aunque con un procedimiento distinto:
algoritmia/problems/brownian.py
class BrownianMotionpointsGenerator:
def points(self , a: "pair of floats", b: "pair of floats", v: "float"):
return [a] + self . recursion(a, b, v) + [b]
def recursion(self , pa, pb, v):
((xa, ya), (xb, yb)) = pa, pb
if (xb-xa) <= 1:
return []
else:
delta = gauss(0, sqrt(v))
(xc,yc) = ((xa+xb)/2.0, (ya+yb)/2.0 + delta)
return self . recursion((xa,ya), (xc,yc), v/2) + [(xc,yc)] + \
self . recursion((xc,yc), (xb,yb), v/2)
Se pide:
a) Efect ua un an alisis de complejidad temporal y espacial de ambos m etodos.
b) Indica (razonadamente) si hay recursi on por cola en cada uno de los m etodos.
c) Elimina la recursi on por cola de los m etodos en los que la haya y efectuar los an alisis de
complejidad temporal y espacial de la versi on que resulta de eliminar la recursi on por cola.
Los an alisis de complejidad deben expresar el coste unicamente en funci on de
x
= x
b
x
a
,
y
= y
b
y
a
y/o v. El coste temporal de generar un n umero aleatorio con la funci on gauss puede
suponerse O(1).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28 de septiembre de 2009 Captulo 5. Divide y vencer as 303
5.9. Selecci on del k- esimo menor elemento
Dado un vector de n elementos, el problema de la selecci on (tambi en conocido como
select) consiste en buscar el elemento que ocupara la posici on de ndice k si el vector
estuviera ordenado de menor a mayor. El primer, segundo, etc. elementos en el vector or-
denado reciben el nombre de estadsticos de orden. N otese que resolver este problema
ecientemente supone disponer de un algoritmo eciente para el c alculo de la media-
na de un vector, pues la mediana es el k/2- esimo elemento (o el k/2- esimo, seg un
deniciones) del vector.
Implementaremos algoritmos en clases que se ci nen a esta interfaz:
algoritmia/problems/selecting.py
class ISelector(metaclass=ABCMeta):
@abstractmethod
def select(self , a: "sequence<T>", k: "int") -> "T": pass
Una soluci on trivial pasa por ordenar el vector en orden creciente y acceder a la k- esi-
ma celda. El coste vendr a determinado entonces por el algoritmo de ordenaci on. Usando
un algoritmo como mergesort, el coste temporal de este m etodo es O(n lg n).
Podemos solucionar el problema con menor coste temporal en la pr actica siguiendo
una aproximaci on divide y vencer as. Dado un vector a de talla mayor o igual que 1, la
soluci on hace uso del algoritmo de partici on presentado al estudiar el algoritmo quicksort.
El m etodo divide el vector original a[p:r] en los vectores a[p:q] y a[q+1:r], donde q es
el ndice del pivote, y dispone en a[p:q] todos los elementos menores o iguales que el
pivote y en a[q+1:r] todos los mayores. Entonces considera tres posibilidades:
Que el ndice k sea igual a q: en tal caso, el k- esimo menor elemento es a[q];
que sea menor que q: entonces, el k- esimo menor elemento est a en a[p:q];
y que sea mayor que q: entonces, el k- esimo menor elemento est a en a[q+1:r].
algoritmia/problems/selecting.py
from algoritmia.problems.sorting import BasicInPlaceQuickSorter
...
class QuickSelector(ISelector):
partition = BasicInPlaceQuickSorter. partition
def quickselect(self , a: "sequence<T>", k:"int", p: "int", r: "int") -> "T":
if r - p == 1:
return a[p]
else:
q = self . partition(a, p, r)
if k == q: return a[q]
elif k < q: return self . quickselect(a, k, p, q)
else: return self . quickselect(a, k, q+1, r)
304 Apuntes de Algoritmia 28 de septiembre de 2009
def select(self , a: "sequence<T>", k: "int") -> "T":
if not (0 <= k < len(a)): raise IndexError(repr(k))
return self . quickselect(a, k, 0, len(a))
demos/divideandconquer/quickselect.py
from algoritmia.problems.selecting import QuickSelector
v = [1, 6, 8, 56, 12, 9, 15, 3, 48, 0, 23, 40, 2, 31, 23, 7, 87, 18]
print(Ordenado :, sorted(v))
print(Con select:, [QuickSelector().select(v[:], i) for i in range(len(v))])
Ordenado : [0, 1, 2, 3, 6, 7, 8, 9, 12, 15, 18, 23, 23, 31, 40, 48, 56, 87]
Con select: [0, 1, 2, 3, 6, 7, 8, 9, 12, 15, 18, 23, 23, 31, 40, 48, 56, 87]
En el mejor de los casos, el algoritmo encuentra el elemento buscado en el primer
intento. La fase de partici on requiere tiempo lineal con n, as que el coste en el mejor de
los casos es (n). El coste en el peor de los casos es, como en quicksort, O(n
2
): puede
que no demos con el elemento buscado hasta haber considerado todos los dem as. Como
habremos seleccionado n elementos y para cada uno habremos efectuado una partici on,
el coste es cuadr atico con n.
Bajo ciertas condiciones de aleatoreidad (los n elementos son distintos y la proba-
bilidad de que el k- esimo menor elemento ocupe la posici on i del vector es igual para
cualquier valor de i), el coste esperado es O(n) (v ease Introduction to Algorithms, de
Cormen, Leiserson, Rivest y Stein).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
150 Realiza una traza del algoritmo select sabiendo que la llamada inicial a la funci on es se-
lect(a,8) y el vector es a = [22, 31, 19, 37, 25, 28, 3, 10, 35, 38, 44, 24, 13, 16, 8].
151 Haz una traza del algoritmo select al calcular el elemento mayor en [3, 7, 12, 22, 27, 29].
Qu e ocurre con el coste temporal cuando el vector est a ordenado de forma creciente (sin repe-
ticiones), y buscamos el mayor elemento del vector? Est a en el caso mejor o peor? Ocurre lo
mismo si el vector est a ordenado de forma decreciente (sin repeticiones) y buscamos el elemento
menor?
152 Escribe una versi on iterativa del algoritmo select eliminando la recursi on por cola.
153 Modica el algoritmo select de forma que la recursi on acabe cuando la talla del subvector
est e por debajo de un cierto lmite, orden andolo entonces y buscando el k- esimo menor elemento.
Implementa el algoritmo y determina experimentalmente un valor optimo para el umbral.
154 Hay una aproximaci on diferente al c alculo del k- esimo menor elemento: podemos construir
un min-heap con todos los elementos del vector y extraer sus k mejores elementos. Qu e coste
temporal presenta este m etodo alternativo?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Es posible encontrar el k- esimo menor elemento en tiempo O(n) para el peor de los
casos. La idea consiste en tratar de encontrar un buen pivote, uno que garantice que
al menos cierta fracci on de los elementos sea menor que el y que al menos cierta otra
fracci on sea mayor.
La mediana de las medianas permite seleccionar un pivote razonablemente bueno.
La t ecnica consiste en:
28 de septiembre de 2009 Captulo 5. Divide y vencer as 305
Dividir el vector de n elementos en n/d grupos con d elementos y un grupo con
los restantes n d n/d elementos.
Calcular la mediana de cada uno de los grupos, m
i
, para 1 i n/d. En el caso
del ultimo grupo, si hay un n umero par de elementos, de las dos posibles medianas
se escoge la mayor.
Calcular recursivamente la mediana del conjunto de medianas {m
1
, m
2
, . . . , m
n/d
}.
Usar esta mediana como pivote de la partici on, es decir, p ongase a la izquierda del
pivote todo elemento menor que el y a su derecha todo elemento mayor que el. Su-
pongamos que el pivote ocupa la posici on q del vector. Si k es igual a q, el pivote es
el k- esimo menor elemento; si k < q, buscamos recursivamente en la primera mitad
del vector; y si k > q, buscamos recursivamente en la segunda mitad del vector.
El siguiente algoritmo utiliza la mediana de las medianas con d = 5 para efectuar una
b usqueda recursiva del k- esimo menor elemento:
algoritmia/problems/selecting.py
class MedianOf5Selector(object):
def init (self , threshold: "int"=10):
self .threshold = threshold
def median of 5(self , a: "sequence<T>", i: "int") -> "T":
u, v, w, x = a[i], a[i+1], a[i+2], a[i+3]
if v > u:
u, v = v, u
if x > w:
w, x = x, w
if u < w:
u = a[i+4]
if v > u: u, v = v, u
else:
w = a[i+4]
if w > x: w, x = x, w
if u < w:
return v if v < w else w
else:
return x if x < u else u
def select(self , a: "sequence<T>", k: "int"):
if not (0 <= k < len(a)): raise IndexError(repr(k))
if len(a) <= self .threshold:
return sorted(a)[k]
else:
groups = [a[i:i+5] for i in range(0, len(a), 5) if len(a)-i >= 5]
m = [self . median of 5(a, i) for i in range(0, len(a), 5) if len(a)-i >= 5]
pivot = self .select(m, len(m)//2)
lessthan, equal = 0, 0
for v in a:
306 Apuntes de Algoritmia 28 de septiembre de 2009
if v < pivot: lessthan += 1
elif v == pivot: equal += 1
if k < lessthan:
return self .select([v for v in a if v < pivot], k)
elif k >= lessthan+equal:
return self .select([v for v in a if v > pivot], k-lessthan-equal)
else:
return pivot
demos/divideandconquer/medianof5select.py
from algoritmia.problems.selecting import MedianOf5Selector
v = [1, 6, 8, 56, 12, 9, 48, 0, 23, 40, 2, 31, 23, 7, 87, 18]
print(Ordenado :, sorted(v))
print(Con select:, [MedianOf5Selector().select(v[:], i) for i in range(len(v))])
Ordenado : [0, 1, 2, 6, 7, 8, 9, 12, 18, 23, 23, 31, 40, 48, 56, 87]
Con select: [0, 1, 2, 6, 7, 8, 9, 12, 18, 23, 23, 31, 40, 48, 56, 87]
Analicemos el coste temporal del algoritmo, que representaremos con T
d
(n) para ha-
cer explcita su dependencia del par ametro d. La divisi on del vector en n/d vectores,
la selecci on de sus respectivas medianas y la partici on (una vez conocemos el valor del
pivote) se efect uan en tiempo O(n).
La llamada recursiva que calcula la mediana de las medianas tiene lugar sobre un
vector de talla n/5, as que tiene un coste T
5
(n/5). Si se produce una nueva lla-
mada recursiva, esta tiene lugar bien sobre el subvector a[p:q], bien sobre el subvector
a[q+1:r], donde q es el ndice de la posici on que ocupa la mediana de las medianas en
el vector original. Los elementos del primer subvector son valores menores o iguales que
la mediana de las medianas y los del segundo, mayores o iguales. Qu e podemos decir
acerca de las tallas m aximas que pueden presentar estos vectores?
La gura 5.16 muestra una partici on de un vector de talla 39 en 7 grupos con 5 elemen-
tos y 1 grupo con 4 elementos. Cada columna (marcada con trazo discontinuo) representa
uno de esos grupos. Supongamos que los elementos de cada columna est an ordenados
por valor no decreciente de arriba a abajo. El elemento central es, por denici on, la me-
diana. En la gura se muestran las medianas de cada grupo con un crculo alrededor. La
mediana de las medianas ocupara la posici on central de entre todas ellas si estuvieran
ordenadas de menor a mayor. Supongamos que lo est an. Es obvio que puede haber has-
ta n/5 /2 medianas con valor mayor o igual que la mediana de las medianas. Cada
mediana es menor que 2 elementos de su grupo (excepto en el caso del ultimo grupo, que
puede tener menos elementos mayores que su mediana). Eso hace que podamos acotar el
n umero de elementos mayores que la mediana de las medianas: no puede haber m as de
3(n/5 /2 2) elementos mayores que ella. Un razonamiento similar nos lleva a que
ese es tambi en el mnimo n umero de elementos menores que la mediana de las medianas.
Podemos acotar esta cantidad as:
3
(
1
2
n
5
2
)
3n
10
6.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 307
1 2 3 4 5 6 7 8
Figura 5.16: Vector de 39 elementos (representados con puntos) dividido
en 7 grupos de 5 elementos y 1 de 4 elementos (cada grupo es una colum-
na). Los elementos rodeados con un crculo son las medianas de cada gru-
po. La mediana con fondo oscuro es la mediana de las medianas. Si supo-
nemos que las medianas est an ordenadas por valor no decreciente, la re-
gi on de fondo gris contiene valores que sabemos con seguridad que son
mayores que la mediana de las medianas.
En conclusi on: usar la mediana de las medianas como pivote pueda generar una partici on
en dos vectores de talla diferente, el mayor de los cuales tendr a 7n/10 + 6 elementos.
Como es posible que llamemos recursivamente a la funci on sobre este vector, el coste
temporal para el peor de los casos puede expresarse, para alguna constante n
0
, as:
T
5
(n)
{
(1), si n n
0
;
T
5
(n/5) + T
5
(7n/10 +6) +O(n), si n > n
0
Se puede comprobar que 7n/10 +6 < 7.99n/10 para n > 60, as que para esos valores
de n nos encontramos ante una ecuaci on recursiva abordable mediante el Teorema 5.3:
la ecuaci on puede expresarse como T(n) = T(
1
n) + T(
2
n) + O(n), donde
1
= 0.2,
2
= 0.799 y se observa
1
+
2
< 1. As pues, el coste temporal es O(n).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
155 Qu e coste temporal presenta quicksort en el peor de los casos si se selecciona el pivote con
la mediana de las medianas?
156 Implementa quicksort con la mediana de las medianas como t ecnica de elecci on del pivote
y compara su tiempo de ejecuci on con el de quicksort y quicksort aleatorizado.
157 Calcula experimentalmente umbrales de recursi on optimos para quicksort y quicksort con
selecci on del pivote con la mediana de las medianas. Compara ambos umbrales.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.10. El par de puntos m as pr oximos
Un campo interesante de la algortmica es la geometra computacional. Estudia los al-
goritmos que resuelven problemas geom etricos. Sus aplicaciones son numerossimas: vi-
si on articial, clasicaci on de datos, programaci on de videojuegos, realidad virtual, etc.
En este apartado y el siguiente planteamos y resolvemos mediante divide y vencer as un
par de problemas de geometra computacional: el c alculo del par de puntos m as pr oxi-
mos y el c alculo de la envolvente convexa de un conjunto de puntos.
El primer problema se formula as: dado un conjunto {z
0
, z
1
, . . . , z
n1
} de puntos en
el plano, deseamos conocer el par de puntos m as cercanos. Cada punto z
i
se describe con
un par de coordenadas, (x
i
, y
i
), y consideramos que la distancia entre dos puntos es la
distancia eucldea, es decir,
d(z
i
, z
j
) =
(x
i
x
j
)
2
+ (y
i
y
j
)
2
.
308 Apuntes de Algoritmia 28 de septiembre de 2009
Este problema se da, por ejemplo, en la detecci on autom atica de posibles co-
lisiones entre objetos en movimiento. En una pantalla que monitoriza el tr aco
a ereo resulta interesante detectar ecientemente el par de objetos m as pr oximos en-
tre s, ya que podran estar siguiendo trayectorias de colisi on y requerir intervenci on
urgente.
En el resto de la exposici on supondremos, adem as, que no hay dos puntos con id enti-
ca coordenada x (de haberlos, el algoritmo se complicara ligeramente). En la gura 5.17
se muestra un conjunto de 8 puntos cuyo par de puntos m as cercanos es (z
2
, z
3
).
Figura 5.17: Un conjunto de puntos en el plano.
z
1
z
2
z
3
z
0
z
4
z
5
z
6
z
7
Empezaremos por resolver un problema relacionado: el c alculo de la distancia a la que
se encuentran los puntos m as pr oximos. Una aproximaci on por fuerza bruta pasa por
calcular la distancia entre todo par de puntos y escoger la mnima:
algoritmia/problems/closestpair.py
from math import sqrt
def d(a: "(T, T)", b: "(T, T)") -> "R":
return sqrt((a[0]-b[0])**2 + (a[1]-b[1])**2)
class BruteForceClosestPointsFinder:
def distance between closest points(self , z: "sequence<(T, T)>") -> "R":
return min(d(z[i], z[j]) for i in range(len(z)) for j in range(i+1, len(z)))
def closest points(self , z: "sequence<(T, T)>") -> "((T, T), (T, T))":
return min((d(z[i], z[j]), (z[i], z[j])) \
for i in range(len(z)) for j in range(i+1, len(z)))[1]
Esta aproximaci on directa se ejecuta en tiempo O(n
2
), pues efect ua n(n 1)/2 c alcu-
los de distancia eucldea.
Pero podemos seguir una aproximaci on divide y vencer as y llegar a un algoritmo
m as eciente. Si dividimos el plano en dos regiones S
1
y S
2
con una lnea vertical (v ease
la gura 5.18), tendremos que considerar tres casos posibles:
que los dos puntos m as pr oximos est en en la regi on S1,
que est en en la regi on S2,
o que uno est e en S1 y otro en S2.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 309
z
1
z
2
z
3
z
0
z
4
z
5
z
6
z
7
S
1
S
2
Figura 5.18: El plano se divide en dos regiones, S
1
y S
2
, con igual
n umero de puntos.
Los dos primeros casos se pueden resolver recursivamente y el tercero comparando
todo punto de S
1
con todo punto de S
2
. Interesa que S
1
y S
2
contengan el mismo n umero
de puntos para que la partici on del conjunto est e equilibrada. Si los puntos est an orde-
nados por valor creciente de la abcisa, es sencillo encontrar el punto de corte optimo: la
mediana. Para no alterar el vector original, crearemos un vector con los ndices de los
puntos ordenados:
algoritmia/problems/closestpair.py
from algoritmia.utils import innity
...
class ClosestPointsFinder1:
def distance between closest points(self , z: "sequence<(T, T)>") -> "R":
x = [i for (v, i) in sorted((z[0], i) for i in range(len(z)))]
return self . minimum distance(z, 0, len(z), x)
def minimum distance(self , z: "sequence<(T, T)>", p: "int", r: "int",
x: "sequence<int>") -> "R":
if r - p < 2:
return innity
elif r - p == 2:
return d(z[x[p]], z[x[p+1]])
else:
q = (p + r) // 2
d1 = self . minimum distance(z, p, q, x)
d2 = self . minimum distance(z, q, r, x)
d3 = min(d(z[x[i]], z[x[j]]) for i in range(p, q) for j in range(q, r))
return min(d1, d2, d3)
Probemos el programa con los puntos de la gura 5.17:
demos/divideandconquer/closestpairdist.py
from algoritmia.problems.closestpair import *
z = [(0.5,3.5), (1,2), (3,2), (3.5,1.5), (4,3.5), (5,0.5), (5,3), (5.5,1.5)]
cpf = ClosestPointsFinder1()
bfcpf = BruteForceClosestPointsFinder()
print(Distancia entre puntos mas proximos:, cpf .distance between closest points(z))
print(
)
28 de septiembre de 2009 Captulo 5. Divide y vencer as 311
para q
S
1
S
2
R
p
S
1
S
2
R
(a) (b)
Figura 5.20: (a) Los puntos de S
2
que pueden
encontrarse a distancia inferior a d del pun-
to p han de encontrarse en la regi on R, un
rect angulo de 2 (gura de la izquierda).
(b) Si cada punto se compara s olo con los que
hay por encima de el, s olo los puntos de R
pue-
den estar a una distancia de p menor que .
Se nos presenta ahora una pregunta interesante: cu antos puntos ser an sometidos a
consideraci on hasta estar seguros de que estamos fuera de R
. Si un
punto q est a en R
de modo que disten entre s una cantidad menor o igual que es 4: uno en cada esquina
de R
(v ease la gura 5.21 (b)). Tambi en podemos acotar con una constante el n umero de
puntos cuya diferencia de altura con p es menor o igual que y cuya distancia no llega a
calcularse por estar en zona derecha.
q
S
1
S
2
R
p
S
1
S
2
R
(a) (b)
Figura 5.21: (a) La existencia del punto q en
R
.
Cuando hacemos el recorrido de puntos por sus ndices, i + 1, i + 2, etc., es seguro
que nunca pasaremos del punto i + c, donde c es una constante. As pues, comparar p
con todos los puntos de igual o superior altura requiere O(1) c alculos. Al haber hasta n
312 Apuntes de Algoritmia 28 de septiembre de 2009
puntos en la franja blanca, el coste temporal que requiere encontrar el par de puntos m as
cercanos con distancia menor o igual que (si los hay) es O(n).
algoritmia/problems/closestpair.py
class ClosestPointsFinder:
def distance between closest points(self , z: "sequence<(T, T)>") -> "R":
x = [i for (v,i) in sorted([(z[i][0], i) for i in range(len(z))])]
y = [i for (v,i) in sorted([(z[i][1], i) for i in range(len(z))])]
return self . minimum distance(z, x, y)
def minimum distance(self , z: "sequence<(T, T)>",
x: "sequence<int>", y: "sequence<int>") -> "R":
if len(x) <= 1:
return innity
elif len(x) == 2:
return d(z[x[0]], z[x[1]])
else:
splitter = (z[x[len(x)//2-1]][0] + z[x[len(x)//2]][0])/2
x1, x2 = [i for i in x if z[i][0] <= splitter], [i for i in x if z[i][0] > splitter]
y1, y2 = [i for i in y if z[i][0] <= splitter], [i for i in y if z[i][0] > splitter]
d1 = self . minimum distance(z, x1, y1)
d2 = self . minimum distance(z, x2, y2)
delta = min(d1, d2)
strip = [i for i in y if abs(z[i][0] - splitter) < delta]
for i in range(len(strip)):
for j in range(i+1, len(strip)):
if abs(z[strip[j]][1] - z[strip[i]][1]) > delta: break
delta = min(delta, d(z[strip[i]], z[strip[j]]))
return delta
demos/divideandconquer/closestpairdist2.py
from algoritmia.problems.closestpair import *
z = [(0.5,3.5), (1,2), (3,2), (3.5,1.5), (4,3.5), (5,0.5), (5,3), (5.5,1.5)]
cpf = ClosestPointsFinder()
bfcpf = BruteForceClosestPointsFinder()
print(Distancia entre puntos mas proximos:, cpf .distance between closest points(z))
print(
ab:
algoritmia/geometry.py
from collections import namedtuple
Point2D = namedtuple("Point2D", "x y")
def left(a: "Point2D", b: "Point2D", c: "Point2D") -> "bool":
return (a.y-b.y)*(c.x-b.x) - (a.x-b.x)*(c.y-b.y) > 0
def right(a: "Point2D", b: "Point2D", c: "Point2D") -> "bool":
return (a.y-b.y)*(c.x-b.x) - (a.x-b.x)*(c.y-b.y) < 0
Centr emonos en el subconjunto A, aunque el m etodo que vamos a aplicar es tam-
bi en v alido para la regi on B. Sea h el punto de A m as alejado del segmento pq. La gu-
ra 5.24 (b) muestra el tri angulo que forman los puntos p, q y h. El punto h forma parte de
la envolvente convexa y los puntos de A que se encuentran en el interior del tri angulo
no, pues son puntos interiores a esta. Es f acil decidir qu e puntos no est an en el interior
del tri angulo y, por tanto, son candidatos a formar parte de la envolvente convexa:
aquellos que est an a la izquierda del vector
hq.
Sea A
1
el primer conjunto y A
2
el segundo, como se ilustra en la gura 5.24 (c).
La envolvente convexa de A se puede obtener combinando la envolvente convexa de
{p, h} A
1
con la envolvente convexa de {h, q} A
2
. Y dicho c alculo puede efectuarse
aplicando recursivamente el mismo procedimiento: detecci on del punto m as alejado del
segmento que forman los dos puntos que ya sabemos que forman parte de la envolvente
convexa (v ease la gura 5.24 (d)); divisi on de A
1
y A
2
en nuevos subconjuntos de puntos
con posibilidad de formar parte de la envolvente convexa, etc.
316 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 5.24: (a) Conjunto de pun-
tos S y segmento que une los pun-
tos de menor y mayor abscisa, p
y q, respectivamente. El segmento
divide el conjunto S es dos regio-
nes: la de los puntos a la izquier-
da del vector
pq y la de los puntos
a su derecha. La envolvente conve-
xa se obtiene combinando apropia-
damente las envolventes convexas
de las regiones A y B. (b) Los pun-
tos de A (en negro) y B (en gris) se
consideran por separado. El punto
h es el punto de A m as alejado del
segmento pq: aqu el que forma un
tri angulo phq de mayor area. (c)
El tri angulo formado por p, h y q
divide el conjunto A en tres sub-
conjuntos: A
1
, el de los puntos a la
izquierda de
ph, A
2
, el de los pun-
tos a la izquierda de
hq y A
3
, el
de los puntos interiores al tri angu-
lo phq. Los puntos de A
3
no for-
man parte de la envolvente convexa
y se descartan (aparecen en gris).
(d) Se puede aplicar este razona-
miento recursivamente sobre las re-
giones A
1
y A
2
.
0
1
2
3
4
5
6
7
8
9
10 11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
p
q
A
B
0
1
2
3
4
5
6
7
8
9
10 11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
p
q
A
B
h
(a) (b)
p
q
A
3
h
0
1
2
3
4
5
6
7
8
9
10 11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
A
1
A
2
p
q
A
3
h
0
1
2
3
4
5
6
7
8
9
10 11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
A
1
A
2
h
1
h
2
(c) (d)
Los operadores *, << y + que tienen por operandos objetos de la clase Bi-
gIntBase disparan llamadas a los m etodos especiales mul , lshift y
add , respectivamente. N otese que en el caso de * la llamada es recursiva.
Analicemos el coste del algoritmo. Sea n el n umero de cifras de las cantidades multi-
plicadas. Dado que sumar dos cantidades de m cifras es O(m), tenemos:
T(n) =
{
(1), si n = 1;
4T(n/2) +(n), si n > 1.
Podemos identicar esta ecuaci on recursiva como un caso particular de las resolubles
por el m etodo maestro. El coste es O(n
lg 4
) = O(n
2
). No hemos logrado una mejora en el
coste temporal. Pero si ahora denimos
= wy, = xz, = (w + x) (y + z),
322 Apuntes de Algoritmia 28 de septiembre de 2009
tenemos:
u v = 10
2s
+10
s
( ) + .
He aqu una implementaci on de este nuevo m etodo:
algoritmia/problems/bigint.py
class BigInt(BigIntBase):
def init (self , digits: "sequence<int> or str or int"):
super(BigInt, self ). init (digits)
def sub (self , other: "BigInt") -> "BigInt":
n = max(len(self ), len(other))
result, carry = [], 0
for i in range(n):
s = self [i] - other[i] + carry
carry = s // 10
if s < 0: result.append(10 + s)
else: result.append(s % 10)
if carry: result.append(-carry)
return BigInt(list(reversed(result)))
def mul (self , other: "BigInt") -> "BigInt":
u, v = self , other
n = max(len(u), len(v))
if n == 1:
r = u[0] * v[0]
return BigInt([r]) if r < 10 else BigInt([r//10, r%10])
else:
s = n // 2
w, x = BigInt(u.digit[s:][::-1]), BigInt(u.digit[:s][::-1])
y, z = BigInt(v.digit[s:][::-1]), BigInt(v.digit[:s][::-1])
alpha, beta, gamma = w * y, x * z, (w+x) * (y+z)
return (alpha << (s<<1)) + ((gamma - beta - alpha) << s) + beta
demos/divideandconquer/bigint.py
from algoritmia.problems.bigint import BigInt
a, b = BigInt(1345), BigInt(2965)
print({} + {} = {} ({}).format(a, b, a+b, 1345+2965))
print({} * {} = {} ({}).format(a, b, a*b, 1345*2965))
1345 + 2965 = 4310 (4310)
1345 * 2965 = 3987925 (3987925)
Con este nuevo m etodo hemos reducido el n umero de productos a s olo 3 y la expre-
si on recursiva del coste temporal pasa a ser
T(n) =
{
(1), si n = 1;
3T(n/2) +(n), si n > 1.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 323
Aplicando el teorema maestro el tiempo es, pues, O(n
lg 3
) O(n
1.58
).
(
e f
g h
)
,
donde cada letra min uscula es una matriz de n/2 n/2 y
r = ae + bg, s = a f + bh,
t = ce + dg, u = c f + dh.
Hemos transformado un producto de dos matrices de talla n n en 8 productos y 4
sumas de matrices de talla n/2 n/2. El coste temporal puede expresarse as:
T(n) =
{
(1), si n = 1;
8T(n/2) +(n
2
), si n > 1.
El coste temporal es (n
3
), as que no parece obtenerse una reducci on de complejidad
asint otica.
324 Apuntes de Algoritmia 28 de septiembre de 2009
El algoritmo de Strassen es m as eciente y sigue una estrategia divide y vencer as.
En aras de la claridad, supondremos que n es potencia de 2. Strassen propuso una des-
composici on diferente que necesita unicamente 7 productos matriciales de matrices de
dimensi on n/2 n/2.
El algoritmo propone el c alculo de siete matrices de n/2 n/2:
P
1
= a f ah = a ( f h), P
2
= ah + bh = (a + b) h,
P
3
= ce + de = (c + d) e, P
4
= dg de = d (g e),
P
5
= ae + ah + de + dh = (a + d) (e + h), P
6
= bg + bh dg dh = (b d) (g + h),
P
7
= ae + a f ce c f = (a c) (e + f ).
Hay que calcular 7 productos matriciales y tenemos que:
r = P
5
+ P
4
P
2
+ P
6
, s = P
1
+ P
2
,
t = P
3
+ P
4
, u = P
5
+ P
1
P
3
P
7
.
He aqu una implementaci on directa del m etodo en una clase que modela las matrices
cuadradas:
algoritmia/problems/matrixprod.py
class SqMatrix:
def init (self , A: "square matrix"=None, n: "int"=0):
if A != None:
if any(len(A) != len(row) for row in A): raise Exception("Non square matrix")
self .n, self .A = len(A), A
else:
self .n, self .A = n, [[0] * n for i in range(n)]
def getitem (self , ij: "(int, int)"):
return self .A[ij[0]][ij[1]]
def setitem (self , ij: "(int, int)", value: "T"):
self .A[ij[0]][ij[1]] = value
return value
def add (self , other: "SqMatrix"):
if other.n != self .n: raise Exception("Different size matrices.")
r = SqMatrix([[self [i,j]+other[i,j] for j in range(self .n)] for i in range(self .n)])
return r
def sub (self , other: "SqMatrix"):
if other.n != self .n: raise Exception("Different size matrices.")
r = SqMatrix([[self [i,j]-other[i,j] for j in range(self .n)] for i in range(self .n)])
return r
def split(self ):
mid = self .n // 2
a = SqMatrix([[self [i,j] for j in range(mid)] for i in range(mid)])
28 de septiembre de 2009 Captulo 5. Divide y vencer as 325
b = SqMatrix([[self [i,j] for j in range(mid, self .n)] for i in range(mid)])
c = SqMatrix([[self [i,j] for j in range(mid)] for i in range(mid, self .n)])
d = SqMatrix([[self [i,j] for j in range(mid, self .n)] for i in range(mid, self .n)])
return a, b, c, d
def join(self , a: "SqMatrix", b: "SqMatrix", c: "SqMatrix", d: "SqMatrix"):
mid = self .n // 2
for i in range(mid):
for j in range(mid): self [i,j] = a[i,j]
for j in range(mid, self .n): self [i,j] = b[i,j-mid]
for i in range(mid, self .n):
for j in range(mid): self [i,j] = c[i-mid,j]
for j in range(mid, self .n): self [i,j] = d[i-mid,j-mid]
def mul (self , other: "SqMatrix"):
if other.n != self .n: raise Exception("Different size matrices.")
if self .n == 1: return SqMatrix([[self [0,0]*other[0,0]]])
a, b, c, d = self .split()
e, f , g, h = other.split()
P1 = a * (f - h)
P2 = (a + b) * h
P3 = (c + d) * e
P4 = d * (g - e)
P5 = (a + d) * (e + h)
P6 = (b - d) * (g + h)
P7 = (a - c) * (e + f )
r = P5 + P4 - P2 + P6
s = P1 + P2
t = P3 + P4
u = P5 + P1 - P3 - P7
result = SqMatrix(n=self .n)
result.join(r, s, t, u)
return result
demos/divideandconquer/strassen.py
from algoritmia.problems.matrixprod import SqMatrix
A = B = SqMatrix([[(i*4+j) for i in range(4)] for j in range(4)])
C = A * B
for i in range(C.n):
for j in range(C.n): print("{:6d}".format(C[i,j]), end=" ")
print()
56 152 248 344
62 174 286 398
68 196 324 452
74 218 362 506
326 Apuntes de Algoritmia 28 de septiembre de 2009
El coste temporal puede expresarse, pues, as:
T(n) =
{
c
1
, si n = 1;
7T(n/2) +(n
2
), si n > 1.
Por el teorema maestro tenemos, pues, un coste temporal O(n
lg 7
) O(n
2.81
).
El algoritmo de Strassen ilustra una complicadsima funci on de combinaci on de so-
luciones. Hay que tener en cuenta que este algoritmo s olo resulta m as eciente en la
pr actica cuando se trabaja con matrices enormes, ya que el sobrecoste asociado a la des-
composici on en matrices y combinaci on de resultados hace que sea m as lento que el
m etodo directo con matrices de tama no peque no o medio.
j=0
X
j
jk
N
, (5.3)
donde
N
es la N- esima raz principal de la unidad. (Recuerda que
n
N
= e
i2n/N
=
cos(
2
N
n) +i sin(
2
N
n), de ah que la anterior expresi on sea, efectivamente, una combina-
ci on de funciones sinusoidales.) Los valores complejos X
0
, X
1
, . . . , X
N1
de la ecuaci on 5.3
son los denominados coecientes de Fourier o la transformada discreta de Fourier (DFT,
del ingl es Discrete Fourier Transform) de la se nal x
0
, x
1
, . . . , x
N1
. Los coecientes de
Fourier proporcionan una descripci on de la se nal en t erminos de funciones elementales
(senos y cosenos). La gura 5.25 ayudar a a entender esta representaci on.
Seguro que los gr acos de DFT no te resultan tan extra nos: muchos equi-
pos de m usica muestran un an alisis frecuencial del sonido que reproducen
mediante unas barras luminosas que suben y bajan. Estas barras se corresponden,
b asicamente, con la magnitud de descriptores de Fourier de la se nal musical en re-
producci on. Son una animaci on de la primera mitad de las gr acas que aparecen a
mano derecha en la gura 5.25. Cuando suenan los bajos las barras de la izquierda
alcanzan valores elevados, y cuando suenan los agudos, lo hacen las de la derecha.
Estos valores se pueden calcular mediante esta f ormula:
X
k
=
N1
j=0
x
j
jk
N
(5.4)
para k entre 0 y N 1. Una implementaci on de este c alculo ejecutable en tiempo O(N
2
)
resulta sencilla:
algoritmia/problems/fft.py
from cmath import exp, pi
def dft(x):
wn = exp(complex(0,2*pi)/len(x))
return [sum(x[j] * wn**(-j*k) for j in range(len(x))) for k in range(len(x))]
328 Apuntes de Algoritmia 28 de septiembre de 2009
(a)
(b)
(c)
Figura 5.25: La transformada discreta de Fourier proporciona una descripci on frecuencial de la se nal discreta. En
cada una de las primeras guras se muestra a mano izquierda una se nal continua, en el centro una versi on discreta
de la misma y a mano derecha, una representaci on gr aca de su DFT (es una serie de n umeros complejos, pero s olo
mostramos su magnitud). (a) La se nal original es una funci on coseno multiplicada por 0.7. Se han tomado 64 muestras
y el periodo del coseno coincide con el periodo de muestreo. En su DFT puede observarse que todos los coecientes, salvo
dos, son nulos. Los dos no nulos se encuentran en los extremos izquierdo y derecho de la gr aca. Ello indica que la se nal
puede explicarse frecuencialmente con una unica funci on sinusoidal (en este caso un coseno) de baja frecuencia. (b)
Se nal coseno multiplicada por 0.3 y con frecuencia cuatro veces superior a la del coseno anterior. Nuevamente todos los
coecientes de la DFT, excepto dos, son nulos. Los que no son nulos se encuentran m as pr oximos a la zona central, lo
que indica una mayor frecuencia en la se nal original. (c) Se nal discreta obtenida al muestrear una se nal que es suma de
la dos anteriores. Su DFT es una superposici on de las DFT anteriores.
5.14.2. Un algoritmo divide y vencer as
La transformada r apida de Fourier (FFT, por el t ermino ingl es Fast Fourier Transform)
es una t ecnica de c alculo basada en divide y vencer as que calcula la DFT en tiempo
O(N lg N), en lugar de O(N
2
). El principio divide y vencer as en el que se apoya el
c alculo de la FFT consiste en considerar por separado los t erminos pares e impares de
la se nal discreta (supondremos que N es una potencia de 2). Es decir, en la siguiente
descomposici on del problema original:
X
k
=
N1
j=0
x
j
jk
N
=
N/21
j=0
x
2j
2jk
N
+
N/21
j=0
x
2j+1
(2j+1)k
N
. (5.5)
Como w
n
N
= e
i2n/N
tenemos:
w
2j
N
= e
i22j/N
= e
i2j/(N/2)
=
(
e
i2/(N/2)
)
j
=
j
N/2
,
w
(2j+1)
N
= e
i2(2j+1)/N
= e
i2/N
e
i22j/N
= e
i2/N
e
i2j/(N/2)
=
1
N
j
N/2
.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 329
Podemos reescribir (5.5) as:
X
k
=
N1
j=0
x
j
jk
N
=
N/21
j=0
x
2j
jk
N/2
+
k
N
N/21
j=0
x
2j+1
jk
N/2
.
Ya tenemos una ecuaci on recursiva: cada uno de los dos t erminos en la parte derecha
es el coeciente de una DFT sobre N/2 puntos. La DFT del primero corresponde a las
muestras con ndice impar y la del segundo, a las muestras con ndice par. Si notamos
con X
par
k
al coeciente de ndice k en la DFT de las muestras de ndice par, y con X
impar
k
al mismo coeciente para las muestras de ndice impar,
X
k
= X
par
k
+
k
N
X
impar
k
. (5.6)
Debe tenerse en cuenta que esta expresi on s olo es v alida para valores de k entre 0 y
N/2 1. Para valores de k entre N/2 y N 1, tenemos
X
k
= X
par
kN/2
+
(kN/2)
N
X
impar
kN/2
. (5.7)
Llegamos a un caso base cuando pretendemos calcular la DFT de una se nal formada
por una sola muestra. En tal caso, X
0
= x
0
0
N
= x
0
.
algoritmia/problems/fft.py
def fft(x):
wn = exp(complex(0,2*pi)/len(x))
if len(x) == 1:
return x
else:
even, odd = fft(x[::2]), fft(x[1::2])
return [even[k%(len(x)//2)] + wn**(-k)*odd[k%(len(x)//2)] for k in range(len(x))]
demos/divideandconquer/fft.py
from algoritmia.problems.fft import dft, fft
from math import sin, cos, pi
N= 16
x = [ complex(0.7*cos(2*pi*i/oat(N)) + 0.3*cos(2*pi*i*4/oat(N))) for i in range(N) ]
for a, b in zip(dft(x), fft(x)):
print({:+.1f}{:+.1f}j {:+.1f}{:+.1f}j.format(a.real, a.imag, b.real, b.imag))
-0.0+0.0j -0.0+0.0j
+5.6-0.0j +5.6-0.0j
+0.0+0.0j +0.0+0.0j
-0.0+0.0j -0.0-0.0j
+2.4-0.0j +2.4-0.0j
+0.0+0.0j +0.0-0.0j
+0.0+0.0j +0.0+0.0j
+0.0-0.0j +0.0-0.0j
+0.0+0.0j +0.0-0.0j
330 Apuntes de Algoritmia 28 de septiembre de 2009
+0.0+0.0j +0.0+0.0j
+0.0+0.0j +0.0-0.0j
+0.0+0.0j +0.0-0.0j
+2.4-0.0j +2.4+0.0j
-0.0+0.0j -0.0-0.0j
+0.0+0.0j +0.0-0.0j
+5.6-0.0j +5.6+0.0j
El algoritmo divide un problema de talla N de dos problemas de talla N/2 y combina
sus soluciones en tiempo proporcional a N. El coste es, por tanto, O(N lg N).
En la pr actica, esta versi on no es muy eciente por las reservas de memoria que com-
porta. Es posible transformar este algoritmo recursivo en otro iterativo con un consumo
de memoria m as conservador.
k
N
X
impar
kN/2
(5.8)
para k entre N/2 y N 1.
algoritmia/problems/fft.py
def bit reversed(s):
N = len(s)
rev = 0
bitrev= [0] * N
for i in range(N-1):
mask = N//2
bitrev[i] = rev
while rev >= mask:
rev -= mask
mask >>= 1
332 Apuntes de Algoritmia 28 de septiembre de 2009
rev += mask
bitrev[N-1] = N-1
return [ s[bitrev[k]] for k in range(N) ]
def it fft(s):
N = len(s)
A = bit reversed(s)
m = 2
while m <= N:
wn = exp(complex(0,2*pi)/m)
for j in range(0, N, m):
for k in range(m//2):
w = wn**(-k)
A[j+k], A[j+k+m/2] = A[j+k] + w * A[j+k+m/2], A[j+k] - w * A[j+k+m/2]
m <<= 1
return A
El coste temporal del algoritmo sigue siendo O(N lg N): se visitan todos los nodos
de un arbol con 2N 1 nodos y en los N 1 nodos interiores se efect ua un proceso que
requiere tiempo proporcional al n umero de hojas que son descendientes de el.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
164 Cuando la se nal de compone de valores reales, es posible calcular dos DFT por el precio
de una. Busca en la literatura la raz on e implementa esta versi on dos por uno de la DFT para
se nales reales.
165 La DFT encuentra aplicaci on en el producto eciente de polinomios. Estudia los fundamen-
tos de esta aplicaci on y analiza el coste temporal del m etodo resultante.
166 Los n miembros de un club deportivo se presentan a un campeonato. Queremos dise nar
un calendario que permita a todo participante jugar contra todos los dem as. Cada jugador debe
jugar exactamente un partido diario. Se nos pide dise nar un algoritmo que diga a cada jugador
el orden en el que deber a jugar contra los n 1 restantes. (Supondremos que n es una potencia
de 2.)
Pista: Al elaborar el calendario para 2
k
participantes, dividimos el conjunto en dos partes,
una con los 2
k1
primeros y otra con los restantes. Cada calendario se hace separadamente y se
completa el calendario haciendo que cada jugador de la primera mitad se combine con todos los
de la otra mitad.
167 Dise na un algoritmo con la t ecnica divide y vencer as para obtener la altura de un arbol
binario. Discute c ual es el coste de la operaci on seg un el n umero de nodos del arbol. Puedes
asumir que los arboles vienen dados como listas de listas Python.
Pista: la altura de un nodo ser a 1 m as el m aximo entre las alturas de los arboles que parten de
su hijo izquierdo y su hijo derecho.
168 Dado un conjunto S de reales y un real x, dise na un algoritmo con un coste temporal
(n log n) basado en el esquema divide y vencer as que diga si existen o no dos elementos
de S que sumen exactamente el valor x.
Pista: fjate en la ordenaci on que hace el algoritmo mergesort.
169 La mediana de un vector de n elementos en orden no decreciente es el elemento que ocupa
su posici on central. Dados dos vectores a y b de n elementos, ordenados de forma no decreciente,
dise na un algoritmo divide y vencer as que calcule la mediana de los 2n elementos de a y b en
tiempo O(lg n).
28 de septiembre de 2009 Captulo 5. Divide y vencer as 333
170 Hemos seguido la evoluci on del precio de venta de un piso en los ultimos n meses. Dis-
ponemos de los precios al nal de cada uno de los n meses en un vector. Nos gustara conocer
en qu e mes deberamos haber comprado el piso y en que mes deberamos haberlo vendido (re-
feridos a partir de sus ndices en el vector) para obtener el m aximo benecio posible. Dise na un
algoritmo divide y vencer as que solucione el problema en tiempo O(n).
171 Dado un vector a con n umeros enteros (positivos y negativos), queremos encontrar los
ndices i y k tales que hagan m aximo
ijk
a
i
. Soluciona el problema por divide y vencer as
con un algoritmo que requiera tiempo O(n lg n). Puedes dise nar un algoritmo que solucione el
problema en tiempo O(n)?
172 El elemento mayoritario de un vector a de n enteros es el elemento x que aparece en el
vector m as de n/2 veces. Dise na un algoritmo siguiendo la estrategia divide y vencer as para
comprobar si un vector a de n enteros contiene un elemento mayoritario. Sigue el siguiente princi-
pio de dise no: un elemento puede ser mayoritario si es mayoritario en la primera o en la segunda
mitad del vector.
Implementa y analiza el coste temporal de tu algoritmo.
173 Hay una estrategia que conduce a un algoritmo O(n) para determinar el elemento ma-
yoritario, si lo hay, de un vector a con n elementos. Se basa en crear n/2 pares al azar con los
elementos de a y descartar todos los pares cuyos elementos sean distintos. Si el a tiene un ele-
mento mayoritario, el conjunto de valores supervivientes contiene un elemento mayoritario y es
el mismo. Por qu e?
Implementa este algoritmo y analiza su coste computacional.
174 Dise na un algoritmo divide y vencer as para encontrar la moda de un vector, es decir,
aquel elemento del vector que se repite m as veces.
175 Los cuadr arboles (en ingl es, quad-trees) permiten comprimir ecientemente im agenes
con areas de color homog eneo. Los cuadr arboles siguen un principio de dise no divide y ven-
cer as. Estudia sus aspectos fundamentales y algunas de sus aplicaciones.
176 El problema de la gravitaci on con N cuerpos en 2 dimensiones consiste en simular el mo-
vimiento de N objetos dispuestos en el plano y sometidos al efecto de la atracci on gravitatoria. El
objeto k- esimo presenta una masa de w
k
kilogramos, est a ubicado en (x
k
, y
k
) y parte del reposo.
La simulaci on puede basarse en estimar la posici on y velocidad de cada cuerpo en cada ins-
tante considerando incrementos de tiempo muy peque nos. La estimaci on se basa en el c alculo de
la fuerza que ejercen sobre cada uno de los cuerpos los restantes N 1 cuerpos y aplicar la ace-
leraci on resultante a posici on y velocidad del cuerpo. Este c alculo requiere O(N
2
) tiempo y hace
inaplicable la simulaci on a sistemas con millones de objetos. El algoritmo de Barnes-Hut aplica el
principio de divide y vencer as para efectuar el c alculo en O(N lg N).
Implementa el algoritmo de Barnes-Hut y el algoritmo naf y compara experimentalmente el
tiempo de ejecuci on de ambos.
177 Una triangulaci on de Delaunay de un conjunto de puntos en el plano es una triangulaci on
en la que ning un punto est a en el circumcrculo de cualquiera de los tri angulos. Estudia e imple-
menta una t ecnica de c alculo de la triangulaci on de Delaunay basada en divide y vencer as y
que resuelve el problema con n puntos en tiempo O(n lg n).
178 El problema del circuito del caballero consiste en encontrar la serie de movimientos que
debe realizar un caballo de ajedrez para visitar exactamente una vez cada una de las casillas de
un tablero de n n partiendo y nalizando en una posici on dada.
Estudia e implementa la propuesta divide y vencer as de Ian Parberry.
179 Un problema de inter es pr actico es la ordenaci on de cheros (de registros) de tama no tan
enorme que no caben fsicamente en memoria. Es lo que conocemos por ordenaci on externa.
Estudia e implementa alguna estrategia de ordenaci on externa basada en divide y vencer as.
334 Apuntes de Algoritmia 28 de septiembre de 2009
180 En innidad de problema pr acticos se plantea la denominada b usqueda del vecino m as
pr oximo: dado un conjunto S R
m
de puntos y dado un punto p R
m
(no necesariamente de
S), encontrar el punto de S m as cercano a p. Nosotros vamos a ocuparnos de este problema para
puntos en el plano (m = 2). Una b usqueda exhaustiva (es decir, una que calcula la distancia de
p a cada uno de los puntos de S y escoge el que proporciona distancia mnima) requiere tiempo
O(n), siendo n el n umero de puntos en S. Cuando n es grande y se ha de encontrar el vecino m as
pr oximo para muchos puntos, el tiempo necesario puede resultar prohibitivo.
Hay muchos trabajos dedicados a este problema y un buen n umero de ellos trata con los
denominados kd- arboles. Un kd- arbol es una estructura de datos que se calcula una sola vez sobre
el conjunto S (y su construcci on puede requerir tiempo sensiblemente superior a O(n)) y que
facilita la b usqueda del vecino m as pr oximo.
En este ejercicio nos vamos a ocupar unicamente de la construcci on de los kd- arboles. M as
adelante nos ocuparemos de su explotaci on para la b usqueda eciente del vecino m as pr oximo.
En la gura (a) puedes ver un conjunto S formado por 32 puntos en el plano (m = 2). Un kd-
arbol divide recursivamente los puntos en pares de conjuntos disjuntos. Lo hace escogiendo un
eje (horizontal o vertical) y un valor sobre el eje escogido de acuerdo con un criterio de partici on.
El criterio de partici on est andar nos hace escoger el eje con mayor extensi on: el que presenta
mayor diferencia entre sus valores m aximo y mnimo para los puntos de S (y uno cualquiera en
caso de empate). En nuestro ejemplo, escogemos el eje horizontal. Como valor sobre dicho eje
se escoge la mediana, esto es, el valor que deja a su izquierda 16 puntos y a su derecha otros
16. El procedimiento sigue construyendo, recursivamente, dos nuevos kd- arboles: uno para los
16 puntos a la izquierda de la lnea de partici on y otro parta los 16 puntos a la derecha (v ease la
gura (b)).
. . .
(a) (b) (c) . . . (d)
La gura (c) muestra c omo sigue el proceso de selecci on/partici on en los kd-arboles izquierdo y
derecho. El proceso recursivo naliza cuando una regi on contiene un solo punto. La gura (d)
muestra el resultado nal.
Como estructura de datos, un kd- arbol es un arbol binario que representa las particiones efec-
tuadas. Sus nodos internos contienen la siguiente informaci on:
el eje seleccionado (en el ejemplo, el horizontal),
el valor de partici on, que es un n umero real (en el ejemplo, el valor que, en el eje horizontal,
parte el conjunto de puntos en dos conjuntos disjuntos),
el hijo izquierdo, que es un kd- arbol con el que se modelan los puntos a un lado del valor
de partici on en el eje seleccionado (en el caso de seleccionar el eje horizontal, representa
a los puntos que hay a la izquierda del valor de partici on y, en el otro caso, a los que se
encuentran bajo dicho valor),
y el hijo derecho, que es otro kd- arbol con el que se modelan los restantes puntos.
Cada nodo hoja contiene uno o ning un punto de S.
La construcci on de un kd- arbol es, obviamente, un proceso divide y vencer as. Te pedimos:
28 de septiembre de 2009 Captulo 5. Divide y vencer as 335
a) Que identiques los elementos del esquema divide y vencer as en el proceso de construc-
ci on de un kd- arbol.
b) Que analices el coste temporal y espacial de la construcci on de un kd- arbol para n puntos.
con cada uno de estos dos criterios de partici on:
1) el denominado criterio est andar: se escoge el eje de mayor extensi on y el valor de
partici on es la mediana, esto es, aqu el que parte el conjunto original en dos conjuntos
de puntos de igual tama no,
2) el denominado criterio del punto medio deslizante: parte la regi on actual en dos par-
tes de igual tama no por el eje de mayor tama no. Si hay dos ejes de igual tama no, escoge
aqu el en el que puntos presentan mayor dispersi on. Cabe la posibilidad de que el pun-
to de partici on no deje punto alguno en una de las dos nuevas regiones. En tal caso,
el punto de partici on cambia (se desliza) para que cada regi on contenga al menos
un punto. (De no ser por el posible deslizamiento, el resultado sera un cuadr arbol m-
dimensional.)
La gura (d) mostraba una representaci on gr aca del kd- arbol obtenido con el criterio est andar
y esta otra ilustra el que se obtiene con el criterio del punto medio deslizante:
181 Estamos acostumbrados a la estrategia divide y vencer as desde peque nos. El m etodo de
multiplicaci on de n umeros de varias cifras, por ejemplo, sigue una estrategia que podemos enfo-
car como aplicaci on del principio divide y vencer as. Cuando hemos de multiplicar un n umero
m por otro n, multiplicamos m por la ultima cifra de n (cosa que sabemos hacer r apidamente por
haber memorizado las tablas de multiplicar del 0 al 9) y sumamos al resultado el producto de m
por n/10 multiplicado por 10. Este programa Python ilustra la idea:
def occidental product(m, n):
if 0 <= n < 10: return n * m
return m * (n%10) + 10 * occidental product(m, n/10)
Los ni nos rusos tienen m as suerte que nosotros y no necesitan aprender las tablas de mul-
tiplicar del 3 al 9. El m etodo de multiplicaci on a la rusa para dos n umeros m y n consiste en lo
siguiente: si n es 0, el resultado es 0; si n es 1, el resultado es m; si n es mayor que 1 y par, el
resultado es el producto de 2m por n/2; y si n es mayor que 1 e impar, el resultado es la suma de
m al producto de 2m por n/2.
Se pide:
a) Una implementaci on recursiva del m etodo ruso.
b) Un an alisis de complejidad temporal y espacial de los algoritmos recursivos occidental y
ruso suponiendo que las operaciones b asicas (sumas, productos por n umeros de una cifra
y divisiones (o m odulos) por 2 o 10) tienen coste temporal constante.
c) Una discusi on acerca de si uno y otro m etodos presentan recursi on por cola.
336 Apuntes de Algoritmia 28 de septiembre de 2009
d) Un algoritmo iterativo que efect ue el mismo c alculo con el mismo coste temporal que el
algoritmo recursivo y un an alisis de la complejidad espacial del algoritmo iterativo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Ap endice A
PYTHON
En este ap endice resumimos aquellos aspectos del lenguaje de programaci on Python
(versi on 3.1) de los que hacemos uso, prestando especial atenci on a las caractersticas
que no encontramos en otros lenguajes imperativos u orientados a objetos. Aprovecha-
mos para presentar un m odulos, utils, con algunas utilidades de las que hacemos uso a
lo largo del texto.
Este ap endice no es un tutorial y s olo entra en detalles al considerar cuestiones que
diferencian a Python de otros lenguajes convencionales como C, C++, C# o Java. Para
aprender lo b asico sobre el lenguaje de programaci on recomendamos el tutorial de Guido
van Rossum que se incluye en la distribuci on del int erprete.
A.1. Formato
Los programas Python se escriben en cheros de texto con formato y se organizan en
lneas l ogicas.
Estas suelen corresponderse con lneas fsicas, aunque no siempre es as.
En ciertos casos, por ejemplo, el punto y coma permite unir dos lneas l ogicas en una
lnea fsica y la barra invertida permite juntar dos lneas fsicas para formar una lnea
l ogica. Si un par entesis est a abierto y naliza una lnea fsica, se entiende que la lnea
l ogica contin ua hasta el cierre del par entesis. Ilustremos lo dicho con un ejemplo, aunque
a un no hayamos explicado el signicado de cada sentencia. En este programa la primera
lnea fsica contiene dos lneas l ogicas (separadas por el punto y coma), las dos siguientes
lneas fsicas forman una unica lnea l ogica (la primera tiene un par entesis que a un no se
ha cerrado) y las dos ultimas otra (la pen ultima de ellas naliza en una barra invertida):
print(1); print(2)
print(
3)
print\
(4)
337
338 Apuntes de Algoritmia 28 de septiembre de 2009
Hay sentencias simples y compuestas. Las sentencias simples son parte de una sola
lnea l ogica (una expresi on, por ejemplo, no puede dividirse en dos o m as lneas l ogi-
cas). Las estructuras de control y las deniciones de funciones y clases son sentencias
compuestas y se componen de una o m as cl ausulas. Una cl ausula es una cabecera y una
suite. Cuando la suite no contiene otras cl ausulas puede expresarse en la misma lnea
l ogica que su cabecera. Las cl ausulas que aparecen en una suite presentan el mismo nivel
de indentaci on y sus respectivas suites presentan mayor indentaci on que las cabeceras
que les corresponden. Cada cabecera empieza con una palabra clave y naliza con dos
puntos. Es posible anidar cl ausulas utilizando niveles de indentaci on cada vez mayores.
He aqu algunos ejemplos de cl ausulas con sus respectiva suites:
if a == 0:
print(a)
if a == 1: print(a)
while a > 0:
print(a)
a = a - 1
while a > 0:
a = a - 1
if a % 2 == 0:
print(Par, a)
Una suite no puede estar vaca. Si se desea que no contenga acci on alguna puede
usarse la sentencia pass:
if a > b:
pass
else:
print(b)
Todos los caracteres desde la almohadilla (#) hasta el n de lnea fsica constituyen
un comentario y son ignorados.
print(0) # Un comentario que naliza con la lnea fsica.
Un programa Python es un chero con texto que se ajusta a la sintaxis del lenguaje
de programaci on Python. Las sentencias que no aparecen en una denici on de funci on o
clase se ejecutan cuando se ejecuta un chero de texto con c odigo Python, por lo que for-
man parte del programa. Un chero de texto que s olo contiene deniciones de funciones,
clases y variables (y quiz a c odigo de inicializaci on o de vericaci on) es un m odulo. Un
programa puede hacer uso de funciones, clases y variables denidas en el mismo chero
o en otros m odulos.
28 de septiembre de 2009 Ap endice A. Python 339
A.2. Salida por pantalla
La funci on print puede ir seguida de una o m as expresiones (separadas por comas) y
encerradas en un par de par entesis y muestra en pantalla el resultado de evaluarlas (se-
paradas por espacios):
print(1)
print(2, 3)
1
2 3
La funci on print acepta un par ametro especial con el que podemos evitar que im-
prima un salto de lnea al nal. El par ametro en cuesti on permite especicar el ultimo
car acter que se imprimir a:
print(1, end=)
print(2, 3)
12 3
La llamada print() imprime un salto de lnea.
A.3. Variables
Python es un lenguaje tipado din amicamente: las variables no tienen tipo, pero s los
valores que se asignan a variables y/o que participan en expresiones. Se puede asignar
un valor a una variable con el operador =. En este ejemplo se asigna el valor 2 a la
variable a y se muestra por pantalla el valor asignado:
a = 2
print(a)
2
Debe tenerse en cuenta que una variable no contiene el valor que se le asigna, sino
que mantiene una referencia al lugar de memoria en el que este reside. La gesti on de
memoria din amica es tarea del int erprete y no es necesario liberar explcitamente la me-
moria reservada: Python usa conteo de referencias y t ecnicas de detecci on de ciclos para
liberar la memoria no utilizada.
340 Apuntes de Algoritmia 28 de septiembre de 2009
A.4. Tipos b asicos
A.4.1. N umeros
Python ofrece un rico juego de n umeros organizados en una jerarqua. Todo n umero es
un dato de tipo Number.
Complejos Todo dato de tipo Complex es un dato de tipo Number. Podemos sumar,
restar, multiplicar y dividir complejos. Disponemos tambi en de un operador de expo-
nenciaci on, de la posibilidad de obtener la parte real, la parte imaginaria, el conjugado y
de operadores para comprobar si dos complejos son iguales (==) o diferentes (!=).
Un n umero complejo puro se representa con un n umero entero o en coma otante -
nalizado con la letra jota. Un complejo con parte real e imaginaria se obtiene, por ejemplo,
sumando un n umero (entero o en coma otante) a un n umero imaginario:
print((2 + 2j).real, (2 + 2j).imag, (2 + 2j).conjugate())
print((2 + 2j)/(2 - 4j))
2.0 2.0 (2-2j)
(-0.2+0.6j)
Reales Los n umeros de tipo Real se representan en coma otante (as que no todo n ume-
ro real en el sentido matem atico es representable) y son asimilables a los datos de tipo
double en C y sus literales se expresan del mismo modo que en dicho lenguaje.
Todo dato de tipo Real es tambi en un Complex, aunque su parte imaginaria es nula. Los
datos de tipo Real ofrecen un juego de operaciones m as rico que Complex. En particular
a naden operaciones de redondeo, de redondeo al alza (funci on ceil), de redondeo a la
baja (funci on oor), de obtenci on del resto m odulo N, divisi on con redondeo (operador
//) y comparaciones (operadores <, <=, > y >=).
La funci on oat permite convertir un entero o una cadena en un n umero en coma
otante.
Racionales El tipo Rational deriva del tipo Real. Sus instancias tienen un numerador
y un denominador (accesibles con .numerator y .denominator) y pueden convertirse a
n umeros en coma otante. El m odulo fractions ofrece el tipo Fraction, cuyas instancias
son datos de tipo Rational.
Enteros El tipo Integral deriva de Rational y a nade funciones de desplazamiento a iz-
quierda (<<), a derecha (>>) y operaciones y a nivel de bits (&) y o a nivel de bits
(|). Los enteros pueden formarse con un n umero de dgitos limitado unicamente por la
memoria disponible.
Los literales de enteros pueden codicarse en decimal, octal (prejo 0o o 0O), hexade-
cimal (prejo 0x o 0X) y binario (prejo 0b o 0B).
28 de septiembre de 2009 Ap endice A. Python 341
El operador / proporciona un oat a un cuando sus dos operandos sean enteros.
Para obtener un resultado entero puede recurrise al operador //.
A.4.2. Otros escalares
Booleanos Los valores l ogicos o booleanos son True (cierto) y False (falso), si bien
muchos valores especiales de otros tipos pueden jugar el papel de cierto o falso: el
entero 0 es falso y cualquier otro entero es cierto, una colecci on vaca es falso y cualquier
otra es cierto, etc.
None None es un valor especial que se usa para indicar la ausencia de valor o para
se nalar una situaci on excepcional.
A.4.3. Colecciones
Un colecci on es un multiconjunto de datos. Una secuencia es un multiconjunto ordena-
do de datos, cada uno de los cuales es accesible mediante un ndice. Un mapeo es una
correspondencia entre claves y valores. Un conjunto propiamente dicho es una colecci on
sin repeticiones (ni orden alguno). Python ofrece soporte directo para secuencias (listas
y tuplas), mapeos (diccionarios) y conjuntos propiamente dichos. Ofrece implementacio-
nes de otros tipos estructurados (deques y heaps, por ejemplo) a trav es de bibliotecas.
Listas (vectores)
Las listas Python equivalen a vectores din amicos (con informaci on de longitud) en len-
guajes de programaci on como C, o al tipo vector de la STL en C++. Podemos expresar
literales encerrando sus elementos entre un par de corchetes y separ andolos con comas.
El primer elemento de una lista tiene ndice 0. El operador de indexaci on accede a un
elemento y se expresa sint acticamente a nadiendo a la lista su ndice entre corchetes. Es
posible utilizar ndices negativos, en cuyo caso se empieza a contar desde el nal: 1 es
el ultimo elemento, 2 el pen ultimo, etc:
empty array = []
an array = [1, 2, 3, 5, 7, 11]
print(Los dos primeros elementos:, an array[0], an array[1])
print(
Estas suelen presentar como identicador el nombre del tipo o una abreviatura suya: int
para enteros, oat para n umeros en coma otante, complex para n umeros complejos, list
para listas, tuple para tuplas, str para cadenas y dict para diccionarios.
La funci on int obtiene la parte entera de un n umero en coma otante:
print(int(2.1), int(2.5), int(2.9), int(-2.1), int(-2.5), int(-2.9))
2 2 2 -2 -2 -2
La misma funci on puede aplicarse para interpretar como valor entero un n umero
codicado en una cadena:
print(int(2+1)+1)
22
La funci on oat proporciona un n umero en coma otante equivalente al entero o ca-
dena (si esta codica un valor num erico) que se le suministra como par ametro.
La funci on list puede construir una lista con los elementos de una secuencia (una
tupla, una cadena, etc.). La funci on tuple puede construir una tupla con los elementos de
348 Apuntes de Algoritmia 28 de septiembre de 2009
una secuencia. Se puede transformar una cadena en una lista de caracteres (cadenas de
longitud uno) con la funci on list:
print(list((1, 2, 3)), list(cadena))
print(tuple([1,2,3]), tuple(cadena))
[1, 2, 3] [c, a, d, e, n, a]
(1, 2, 3) (c, a, d, e, n, a)
La funci on str permite obtener una cadena a partir de cualquier tipo b asico:
print((str(2) + str(3.1)) * 2)
23.123.1
Se puede inicializar un diccionario con la funci on dict y una secuencia de pares clave-
valor:
print(dict([(a,1), (b, 2), (c, 3)]))
print(dict([(i, i**2) for i in range(10) if i % 2 == 0]))
{a: 1, c: 3, b: 2}
{0: 0, 8: 64, 2: 4, 4: 16, 6: 36}
A.5. Expresiones
A.5.1. Operadores
Al operar con escalares, los operadores Python son los mismos que encontramos en el
lenguaje C y siguen sus reglas de precedencia y asociatividad, con algunas excepciones:
Los operadores l ogicos se denotan con las palabras reservadas and, or y not.
Los operadores de comparaci on no siguen las reglas de precedencia convenciona-
les en otros lenguajes. Una expresi on como a < b <= c > d equivale a la expresi on
a < b and b <= c and c > d.
No hay operador de pre o postincremento ni operador de pre o postdecremento.
Se pueden efectuar asignaciones con operador para simplicar operaciones de incre-
mento (+=), decremento (-=), etc.:
a = 2
print(a, end= )
a += 2
print(a)
28 de septiembre de 2009 Ap endice A. Python 349
2 4
Python ofrece operadores que no tienen correlato en C o C++:
Operador de divisi on con redondeo, que se denota con //.
Operador de exponenciaci on, que se denota con ** y que es asociativo por la
derecha.
Operador de pertenencia. Se denota con la palabra clave in. Devuelve cierto si el
operando izquierdo est a incluido en el operando derecho (normalmente, un conte-
nedor). El operador not in devuelve cierto cuando in devuelve falso y vicever-
sa.
Desde la versi on 2.5, Python ofrece su propia versi on del operador condicional. La
expresi on que en C se notara con expr ? valor si : valor no se escribe as en Py-
thon: valor si if expr else valor no. He aqu un ejemplo de uso:
a = 1000
b = 1 if a > 0 else 2
c = 1 if b != 1 else 2
print(b, c)
1 2
A.5.2. Sobre la asignaci on
Cuando se asigna un objeto a una variable no se copia dicho objeto: se asigna a la variable
una referencia (un puntero) al objeto. Debe tenerse en cuenta, pues, que dos variables
pueden hacer referencia a un mismo objeto y que, en consecuencia, los cambios a uno
afectan al otro:
a = [1, 2, 3]
b = a
a[0] = 10
print(a, b)
[10, 2, 3] [10, 2, 3]
Cuando se desea apuntar a un objeto nuevo, este debe crearse explcitamente. El ope-
rador de corte, por ejemplo, proporciona un nuevo vector con parte de otro (o con todo
el):
350 Apuntes de Algoritmia 28 de septiembre de 2009
a = [1, 2, 3]
b = a[:]
a[0] = 10
print(a, b)
[10, 2, 3] [1, 2, 3]
A.6. Estructuras de control
Las estructuras de control permiten ejecutar bloques de c odigo condicionalmente o iterar
su ejecuci on mientras se observe cierta condici on.
A.6.1. De selecci on
Para supeditar la ejecuci on de un bloque a la satisfacci on de una condici on se usa la
sentencia if:
a = 102
if a % 2 == 0:
print({} es par.format(a))
102 es par
La sentencia if puede extenderse con varios elif (abreaviatura de else if ), cada uno
con su propia condici on, y opcionalmente un else nal. S olo se ejecuta uno de los blo-
ques: el de la primera condici on que se cumple o, si no se cumple ninguna, el asociado al
else (si lo hay):
a = 45
if a % 2 == 0:
print({} es par.format(a))
elif a % 3 == 0:
print({} no es par y es divisible por 3.format(a))
else:
print({} no es par ni divisible por 3.format(a))
45 no es par y es divisible por 3
A.6.2. De repetici on (o bucle)
La sentencia while permite iterar la ejecuci on de un bloque mientras se cumpla una con-
dici on:
28 de septiembre de 2009 Ap endice A. Python 351
a = 10
while a > 0:
print(a, end= )
a -= 1
10 9 8 7 6 5 4 3 2 1
La sentencia for-in permite recorrer una secuencia de valores. Una variable ndice
toma, ordenadamente, un valor de la secuencia con cada iteraci on:
for indice in 1, "dos", 3: print(indice, end= )
print()
for i in range(2, 6): print(i, end= )
print()
1 dos 3
2 3 4 5
Si se desea recorrer los elementos en orden inverso se puede recurrir a la funci on
reversed:
for i in reversed(range(2, 6)): print(i, end= )
print()
5 4 3 2
La sentencia break interrumpe la ejecuci on de un bucle (while o for-in) y la senten-
cia continue hace pasar inmediatamente a la siguiente iteraci on, sin ejecutar el resto del
bloque. Tanto break como continue afectan unicamente al bucle que las contiene direc-
tamente.
A.6.3. Excepciones
Al cargar un programa o m odulo, el int erprete efect ua comprobaciones l exicas y sint acti-
cas. S olo aquellos que superan esta etapa pasan a ser interpretados. En ejecuci on pueden
producirse errores sem anticos: operadores aplicados sobre operandos de tipos no compa-
tibles con la operaci on, divisiones por cero, intentos de acceso fuera de rango a elementos
de una lista, etc. Cuando se produce un error en tiempo de ejecuci on, el int erprete de Py-
thon lanza una excepci on y, si no es tratada, aborta la ejecuci on del programa:
a = 1/0
print(Fin)
Traceback (most recent call last):
File "programas\test_exc1.py", line 2, in <module>
a = 1/0
ZeroDivisionError: int division or modulo by zero
Las excepciones pueden tratarse con la estructura try-except:
352 Apuntes de Algoritmia 28 de septiembre de 2009
try:
a = 1/0
except:
print(Excepcion en el bloque entre try y except.)
print(Fin)
Excepcion en el bloque entre try y except.
Fin
Una excepci on es una instancia de cierta clase que hereda de la clase Exception. Hay
toda una jerarqua de excepciones, lo que permite que podamos capturar y tratar por
separado cada tipo de error detectado:
a = 1
array = []
try:
if a == 0:
print(1/0)
else:
print(array[10000])
except ZeroDivisionError:
print(Division por cero en el bloque entre try y except.)
except IndexError:
print(Error de indexacion en el bloque entre try y except.)
Error de indexacion en el bloque entre try y except.
Las excepciones pueden venir acompa nadas de un objeto que detalla el error:
a = 1
array = []
try:
if a == 0:
print(1/0)
else:
print(array[10000])
except ZeroDivisionError as e:
print(Division por cero en el bloque entre try y except: {}..format(e))
except IndexError as e:
print(Error de indexacion en el bloque entre try y except: {}..format(e))
Error de indexacion en el bloque entre try y except: list index out of range.
Si el c odigo donde se produce un error es parte de una funci on que est a siendo in-
vocada desde el programa principal o desde otra funci on, su ejecuci on se interrumpe a
menos que la lnea causante de la excepci on est e en un bloque try-except capaz de cap-
turar esa excepci on. Ocurre lo mismo con la funci on, si la hay, desde la que se produjo la
llamada. Las tramas de activaci on van desapil andose hasta que una funci on o el progra-
28 de septiembre de 2009 Ap endice A. Python 353
ma principal atrapen la excepci on. Si nadie lo hace, el int erprete la captura e interrumpe
la ejecuci on del programa.
Podemos generar nuestras propias excepciones con la sentencia raise:
raise IndexError
Traceback (most recent call last):
File "programas\test_exc5.py", line 2, in <module>
raise IndexError
IndexError
raise IndexError(Texto explicativo.)
File "programas\test_exc6.py", line 2
raise IndexError(Texto explicativo.)
IndentationError: unexpected indent
A.7. Funciones
A.7.1. Funciones con nombre
Las funciones se denen con la palabra def seguida del identicador de funci on, una lista
de par ametros encerrados entre par entesis y separados por comas y un bloque (inden-
tado) que describe su cuerpo. Para invocar una funci on basta con usar su identicador
y suministrar los argumentos separados por comas y encerrados en un par de par ente-
sis. El paso de par ametros se realiza por referencia a objeto. A efectos pr acticos podemos
considerar que los valores escalares se pasan por valor y el resto se pasa por referencia:
def myfunction(a, b):
a += 1
b.append(0)
x = 1
y = [2, 1]
myfunction(x, y)
print(x, y)
1 [2, 1, 0]
El valor devuelto por una funci on se indica con la sentencia return, que naliza la
activaci on de la funci on tan pronto se ejecuta:
354 Apuntes de Algoritmia 28 de septiembre de 2009
def contains(sequence, item):
for element in sequence:
if item == element:
return True
return False
print(contains([1,2,3,5,7,11,13], 5))
True
Los par ametros y el tipo de retorno se pueden marcar con informaci on que Python,
en principio, ignora. La informaci on asociada a un par ametro se separa de su identi-
cador con dos puntos y la que se asocia al valor devuelto por la funci on se separa del
par entesis que cierra su lista de par ametros con ->. Esta informaci on puede usarse para
hacer m as legible el c odigo proporcionando, por ejemplo, informaci on de tipo:
def contains(sequence: "list of ints", item: "int") -> "bool":
for element in sequence:
if item == element:
return True
return False
print(contains([1,2,3,5,7,11,13], 5))
True
La asociaci on entre argumentos en la invocaci on a una funci on y los par ametros de
la denici on es posicional. Existe una forma alternativa de establecer esta asociaci on:
haciendo alusi on al identicador de cada par ametro para asignarle un valor:
def contains(sequence, item):
for element in sequence:
if item == element: return True
return False
print(contains(item=3, sequence=[1,2,3,5,7,11,13]))
True
Se pueden denir par ametros con valor por defecto. En la denici on de la funci on el
identicador del par ametro se acompa na del smbolo = y una expresi on con el valor
por defecto. Si en la llamada no proporcionamos un valor al argumento, este toma el
valor por defecto:
def multiply(value, factor=2):
return value * factor
print(multiply(3, 3), multiply(3))
28 de septiembre de 2009 Ap endice A. Python 355
9 6
No es posible alternar par ametros con y sin valor por defecto en la denici on de una
funci on: todos los par ametros deben llevar valor por defecto a partir del punto en el que
aparece uno.
Al invocar o denir una funci on siempre debe proporcionarse, entre par entesis, la
relaci on de par ametros. Si no hay par ametros o si todos tienen valor por defecto y se
desea usar este, los par entesis no contendr an nada, pero deben aparecer igualmente.
Las funciones son objetos de primera clase a los que se puede acceder por su identi-
cador. Es posible, pues, suministrar un funci on como argumento en la llamada a otra o
almacenar funciones en variables, listas, diccionarios, etc.:
def summation(a, b, f ):
s = 0
for i in range(a, b+1): s += f (i)
return s
def square(x):
return x*x
def cube(x):
return x*x*x
functions = [square, cube]
for function in functions:
print(summation(1, 3, function))
14
36
Deben tenerse en cuenta las consecuencias de que el paso de par ametros a una fun-
ci on sea por referencia a objeto. Modicar el contenido de un objeto a trav es del par ame-
tro modica el objeto original (por ejemplo, si se trata de un vector, asignando valor a
un elemento del vector o mediante algunos de los m etodos que modican el contenido
del vector). Asignar un valor a un par ametro en el cuerpo de una funci on modica la
referencia, no el objeto apuntado:
def myfunction(a, b):
a = 2 * a # a apunta a un nuevo objeto.
b[0] = 10 # Se modica el objeto apuntado (que es el original).
b.append(0) # Se modica el objeto apuntado (que es el original).
b = [0, 1, 2] # b deja de apuntar al objeto original y apunta ahora a otro.
return b * a
x = 1
y = list(range(6))
print(x, y)
356 Apuntes de Algoritmia 28 de septiembre de 2009
print(myfunction(x, y))
print(x, y)
1 [0, 1, 2, 3, 4, 5]
[0, 1, 2, 0, 1, 2]
1 [10, 1, 2, 3, 4, 5, 0]
Las funciones pueden almacenarse en variables y usarse como par ametros al invocar
a otras funciones:
def f (a):
return a * 2
def g(h, x, y):
return h(x) + h(y)
print(g(f , 1, 2))
z = f
print(g(z, 1, 2))
6
6
Python permite denir funciones con un n umero de par ametros arbitrario. En la de-
nici on de la funci on hemos de usar un identicador precedido de un asterisco para in-
dicar que se desconoce el n umero de par ametros y que estos se asignar an a una variable
con dicho identicador como una tupla:
def fn(*args):
print(---)
if len(args) > 0:
print(Parametros suministrados:, len(args))
for arg in args: print( , arg)
else:
print(No hay parametros.)
fn(10)
fn(10,20)
fn()
fn( (10, 20) )
fn( *(10, 20) )
---
Parametros suministrados: 1
10
---
Parametros suministrados: 2
10
20
---
28 de septiembre de 2009 Ap endice A. Python 357
No hay parametros.
---
Parametros suministrados: 1
(10, 20)
---
Parametros suministrados: 2
10
20
El asterisco tambi en debe usarse si se suministra a la funci on una tupla con los argu-
mentos. N otese la diferencia entre las dos ultimas llamadas a fn: en la primera se sumi-
nistra un unico par ametro (una tupla con dos valores) y en el segundo, al usar el asterisco
delante de la tupla, se suministran dos argumentos almacenados en una tupla.
Es posible usar par ametros cuyo nombre se decide en el momento de la llamada.
Dichos par ametros se declaran con un identicador precedido por dos asteriscos y se
acceden en la funci on como un diccionario:
def fn(**kw):
print(---)
if len(kw) > 0:
print(Parametros suministrados:, len(kw))
for key in kw: print( %s -> %s. % (key, kw[key]))
else:
print(No hay parametros.)
fn(a=10)
fn(x=10,y=20)
fn()
---
Parametros suministrados: 1
a -> 10.
---
Parametros suministrados: 2
y -> 20.
x -> 10.
---
No hay parametros.
A.7.2. Algunas funciones predenidas
Python ofrece varias funciones predenidas. Entre ellas tenemos:
abs(value): valor absoluto. (Si value es un complejo, proporciona su m odulo.)
zip(a, b): dadas dos listas devuelve una sucesi on de pares. El par i- esimo est a for-
mado por el elemento i- esimo de a y el elemento i- esimo de b.
Se puede consultar la relaci on completa en la secci on 2 de la referencia de bibliotecas
de Python, que es parte de la documentaci on est andar.
358 Apuntes de Algoritmia 28 de septiembre de 2009
A.7.3. Funciones an onimas
En ocasiones hemos de suministrar funciones como argumentos en la llamada a otras
funciones y resulta tedioso denirlas previamente. Las funciones an onimas o -funciones
son funciones sencillas (b asicamente expresiones parametrizadas) que no se asocian a
identicador alguno. Para denirlas se usa la palabra reservada lambda con uno o m as
identicadores separados por comas (los par ametros), dos puntos y una expresi on:
def summation(a, b, f ):
s = 0
for i in range(a, b+1): s += f (i)
return s
print(summation(1, 3, lambda x: x*x))
14
Las -funciones resultan utiles para, por ejemplo, ordenar con diferentes criterios
mediante funciones predenidas. La funci on sorted, por ejemplo, ordena una secuencia
de elementos y permite indicar la clave de ordenaci on mediante el par ametro key, que es
una funci on que se aplica a cada tem antes de compararlo con otros:
names = [Juan, Ana, Luisa, Federico, Adriana]
print(sorted(names))
print(sorted(names, key=lambda x: len(x)))
print(sorted(names, key=lambda x: x[0]))
[Adriana, Ana, Federico, Juan, Luisa]
[Ana, Juan, Luisa, Adriana, Federico]
[Ana, Adriana, Federico, Juan, Luisa]
A.8. Clases
A.8.1. Denici on de clases e instanciaci on de objetos
Python es un lenguaje orientado a objetos y permite denir clases e instanciar objetos a
partir de dichas deniciones. Un bloque encabezado por una sentencia class es una de-
nici on de clase. Las funciones que se denen en el bloque son sus m etodos (o funciones
miembro, seg un la nomenclatura de C++).
Ciertos identicadores corresponden a m etodos especiales. El constructor, por ejem-
plo, tiene por identicador init .
28 de septiembre de 2009 Ap endice A. Python 359
class Person:
def init (self , name, age):
self .name = name
self .age = age
def last name(self ):
return self .name.split()[-1]
somebody = Person(Juan Nadie, 35)
print(somebody.name, somebody.age)
print(Apellido:, somebody.last name())
Juan Nadie 35
Apellido: Nadie
N otese que el primer par ametro de los m etodos es especial y suele usarse el identi-
cador self . Se trata de una referencia al propio objeto y proporciona un modo de acceso a
los atributos y m etodos del mismo. El par ametro self es asimilable al puntero this de C++.
En Python, self es, obligatoriamente, el primer par ametro de los m etodos convencionales
y a trav es suyo puede una instancia acceder a sus atributos y m etodos.
A.8.2. Algunos m etodos especiales
Ciertos m etodos cuyo identicador empieza y acaba con doble subrayado pueden ser
invocados de modo especial. He aqu los que usamos en el texto:
contains (self , item): Se usa para determinar la pertenencia de item al objeto. Se
puede invocar con el operador in.
len (self ): Proporciona la longitud del objeto (en el caso de una secuencia, por
ejemplo, el n umero de elementos que la forman). Se invoca con la funci on len.
getitem (self , key): Acceso al objeto con indexaci on para consulta. Se invoca con
el operador de indexaci on.
setitem (self , key, value): Acceso al objeto con indexaci on para asignaci on. Se
invoca con el operador de indexaci on sobre un objeto que aparece en la parte iz-
quierda de una asignaci on.
iter (self ): Iterador que recorre los elementos de la estructura (v ease el aparta-
do A.12).
getattr (self , id): Acceso al atributo id de un objeto para su consulta. Se invoca
con el operador de indexaci on.
setattr (self , id, value): Acceso al atributo id de un objeto para asignaci on de un
valor. Se invoca con el operador de indexaci on sobre un objeto que aparece en la
parte izquierda de una asignaci on.
360 Apuntes de Algoritmia 28 de septiembre de 2009
call (self , *args): Llamada al objeto. Permite usar el objeto como si se tratara de
una funci on.
str (self ): Representaci on del objeto como cadena (la que devuelve el m etodo).
El m etodo se invoca autom aticamente cuando se muestra el objeto por pantalla o
cuando se convierte el objeto a cadena con la funci on str.
repr (self ): Representaci on del objeto como cadena. Se diferencia del m etodo an-
terior en que str debe proporcionar una descripci on legible por humanos, mien-
tras que repr debe proporcionar una descripci on que el int erprete de Python
pueda analizar.
add (self , other): Operador de suma (+). Cuando est a denido para un objeto a,
el m etodo se invoca en una expresi on como a + b, y b sera el objeto accesible con
other.
sub (self , other): Operador de resta (-).
mul (self , other): Operador de multiplicaci on (*).
lshift (self , other): Operador de desplazamiento bit a bit a izquierdas (<<)..
gt (self , other): Operador de comparaci on mayor que (>).
lt (self , other): Operador de comparaci on menor que (<).
class MultiSet:
def init (self ):
self .content = {}
def add(self , item):
try:
self .content[item] += 1
except KeyError:
self .content[item] = 1
def len (self ):
total = 0
for key in self .content:
total += self .content[key]
return total
def contains (self , item):
return item in self .content
def getitem (self , item):
return self .content[item]
def setitem (self , item, value):
28 de septiembre de 2009 Ap endice A. Python 361
self .content[item] = value
A = MultiSet()
for i in 2, 3, 3, 3, 3, 4, 4, 6: A.add(i)
print(len(A))
print(3 in A, A[3])
A[3] = 2
print(A[3])
8
True 4
2
A.8.3. Herencia
Es posible denir una clase a partir de otra, heredando sus atributos y m etodos. Al denir
la clase basta con acompa nar al identicador de la clase con el de la clase (o clases) padre
encerrado entre par entesis. Python permite tomar por clase padre a tipos b asicos, como
las listas o los diccionarios:
class MyList(list):
def min and max(self ):
return min(self ), max(self )
a = MyList()
a.extend([5, 2, 10, 6])
print(a)
print(a.min and max())
[5, 2, 10, 6]
(2, 10)
La nueva clase puede redenir m etodos del padre.
Si se dene un constructor para la nueva clase y se desea que el constructor de la
clase padre se ejecute, se debe invocar explcitamente a trav es de la funci on super, que da
acceso a la clase de la que heredamos:
class A:
def init (self , n):
self .n = n
class B(A):
def init (self , n, m):
super(B, self ). init (n)
self .m = m
b = B(2,3)
print(b.n, b.m)
362 Apuntes de Algoritmia 28 de septiembre de 2009
2 3
Podemos determinar si un objeto es instancia de una clase, bien directamente, bien
por herencia, con la funci on predenida isinstance:
class A:
def init (self , n):
self .n = n
class B(A):
def init (self , n, m):
A. init (self , n)
self .m = m
b = B(2,3)
print(isinstance(b, A), isinstance(b, B), isinstance(b, list))
True True False
A.8.4. M etodos y campos privados y p ublicos
Python no distingue entre campos privados y p ublicos. Se ha adoptado el convenio de
que los identicadores de campos y m etodos que empiezan por el car acter de subrayado
no son p ublicos.
A.8.5. Creaci on de alias para m etodos
Es posible asociar m as de un identicador a un mismo m etodo. Basta para ello con asig-
nar, en el cuerpo de la clase, un identicador existente al identicador alias:
class A:
def init (self , n):
self .n = n
def my method(self , a):
return self .n * a
an alias = my method
a = A(2)
print(a.my method(4), a.an alias(4))
8 8
28 de septiembre de 2009 Ap endice A. Python 363
A.8.6. M etodos est aticos
Los m etodos cuya denici on est a precedida por una lnea @staticmethod (con el mismo
nivel de sangrado que la denici on) son m etodo est aticos o de clase, es decir, no se ejecu-
tan sobre una instancia de la clase. En su denici on no aparece el par ametro self y para
invocarlos se debe usar el nombre de la clase seguido por un punto y el identicador del
m etodo con su lista de argumentos.
class A:
@staticmethod
def my static method(a):
return "Hola, {}, soy estatico.".format(a)
print(A.my static method("tu"))
Hola, tu, soy estatico.
A.8.7. Clases con consumo de memoria ajustado
Cada instancia de una clase almacena sus atributos en un diccionario internamente, lo
que supone cierto sobrecoste de memoria. Se pueden denir clases cuyas instancias ci nen
m as sus necesidades al consumo real, aunque a costa de no permitir a nadir din ami-
camente campos a las instancias. Estas clases m as ecientes y restrictivas denen con
slots sus campos (mediante una cadena o tupla de cadenas). Una clase con la que re-
presentar puntos en dos dimensiones podra denirse as:
class Point:
slots = "x", "y"
def init (self , x, y):
self .x, self .y = x, y
def repr (self ):
return "(%s, %s)" % (self .x, self .y)
A.8.8. Propiedades
Los campos de un objeto pueden ser ledos directamente y se les puede asignar un va-
lor que se asocia a ellos tambi en directamente. En ocasiones conviene que la asignaci on
o acceso est en mediatizadas por alg un c alculo. Esto puede hacerse con m etodos, pero
los m etodos han de invocarse para ser ejecutados, lo que supone suministrar una lis-
ta (posiblemente vaca) de argumentos. Python ofrece una alternativa: las propiedades.
Una propiedad es un grupo de dos (en realidad tres, pero nosotros s olo consideramos
dos) m etodos que se ejecutan, respectivamente, cuando se accede a un identicador o se
asigna a ese identicador.
364 Apuntes de Algoritmia 28 de septiembre de 2009
class Days:
def init (self ):
self .days = 0
def get weeks(self ):
return self .days // 7
def set weeks(self , value):
self .days = 7 * value
return self .days
weeks = property( get weeks, set weeks)
d = Days()
d.days = 10
print(d.days)
d.weeks = 2
print(d.days)
print(d.weeks)
10
14
2
A.9. M odulos y paquetes
A.9.1. M odulos
Un m odulo es una colecci on de funciones, clases, variables, etc. que podemos utilizar
desde un programa. Hay una gran colecci on de m odulos disponible con la distribuci on
est andar de Python. El m odulo math, por ejemplo, ofrece implementaciones de las fun-
ciones matem aticas y dene algunas constantes con aproximaciones a y e.
La sentencia import seguida del nombre del m odulo (sin extensi on) da acceso a los
objetos denidos en el. Un objeto importado pasa a estar accesible desde el programa o
m odulo que lo importa, pero a trav es del identicador del m odulo, el operador punto y
el identicador del objeto en cuesti on:
import math
print(math.pi, math.sin(math.pi/3))
3.14159265359 0.866025403784
28 de septiembre de 2009 Ap endice A. Python 365
Se puede importar un solo objeto de un m odulo module con una sentencia de la forma
from module import object. En ese caso, se puede usar directamente el identicador del
objeto:
from math import sin
print(sin(0))
0.0
Se puede importar con una sola sentencia from-import m as de un objeto: basta con
separarlos con comas:
from math import sin, pi
print(sin(pi/2))
1.0
Hay una forma especial de la sentencia from-import para importar el contenido com-
pleto de un m odulo:
from math import *
print(sin(pi/4))
0.707106781187
Nosotros vamos a denir en esta secci on un m odulo con alguna utilidades para la
implementaci on de algoritmos. La norma IEEE que rige la codicaci on de los otantes
considera la representaci on del valor innito. Python no lo predene, as que nos resul-
tar a conveniente tenerla f acilmente accesible:
algoritmia/utils.py
innity = oat("+inf")
Los m odulos ofrecen espacios de nombres. Dos funciones, constantes o clases de-
nidas en espacios de nombres distintos pueden tener un mismo identicador. Es decir,
podemos denir un m odulo propio en que se ofrezca una denici on de sin diferente de
la que ofrece el m odulo math. Un programa podra utilizar ambas sin que los identica-
dores entren en conicto si se accede a una de ellas mencionando su espacio de nombres,
es decir, el m odulo en el que reside:
import math
def sin(x):
return x + 1
print(sin(0), math.sin(1))
366 Apuntes de Algoritmia 28 de septiembre de 2009
1 0.841470984808
A.9.2. Paquetes
Los paquetes permiten estructurar los espacios de nombres de los m odulos, que pasan a
organizarse en m odulos y subm odulos (sin lmite de profundidad). Los paquetes resul-
tan utiles cuando se dise nan libreras grandes. Un paquete se corresponde con un directo-
rio del sistema de cheros. Este directorio debe contener un chero llamado init.py (y
que, en principio, est a en blanco). Los cheros adicionales con extensi on .py son m odulos
del paquete, y los directorios que contengan cheros init.py denen subpaquetes.
Pongamos un ejemplo. Un directorio algs contiene un chero init.py y dos che-
ros fast.py y slow.py, cada uno de los cuales dene una funci on sin par ametros numbers.
Podemos ejecutar cada uno de ellos con algs.fast.numbers() y algs.slow.numbers(), res-
pectivamente.
Cuando se importan m odulos de un paquete, se pueden utilizar rutas relativas pa-
ra evitar posibles conictos. Si el paquete algoritmia tiene un m odulo collections en el
que se dene la clase LinkedListCollection, podemos acceder a ella desde otro m odulo
del mismo paquete con la sentencia from .collections import LinkedListCollection o con
from collections import LinkedListCollection. Y si deseamos acceder a la clase Iterable
del m odulo collections de la distribuci on est andar tendremos que ejecutar la sentencia
from ..collections import Iterable. Si en este ultimo caso no usamos los dos puntos an-
tes de collections, el int erprete buscar a en el m odulo del mismo paquete.
A.10. Interfaces (clases abstractas)
Python no ofrece soporte para interfaces, esto es, especicaciones de colecciones de m eto-
do y propiedades que debe implementar una clase. No obstante, permite obtener una
funcionalidad similar con las denominadas clases abstractas. Es necesario importar la
clase ABCMeta y las funciones abstractmethod, abstractproperty del m odulo abc. La clase en
la que denimos la interfaz se marca como que tiene por metaclase a ABCMeta. (No nos
extenderemos acerca de qu e es una metaclase y s olo las usaremos en el contexto de las in-
terfaces.) Los m etodos de la interfaz se marcan con @abstractmethod (y las propiedades con
@abstractproperty) en la lnea anterior a su denici on. Las clases que implementan la inter-
faz heredan de la clases que contiene su denici on. Dichas clases no se pueden instanciar
si algunos de los m etodos o propiedades marcados con @abstractmethod y @abstractproperty
no se han denido.
from abc import ABCMeta, abstractproperty, abstractmethod
class IEmployee(metaclass=ABCMeta):
@abstractmethod
def promote(self , level):
raise NotImplementedError
28 de septiembre de 2009 Ap endice A. Python 367
@abstractproperty
def name(self ): # readonly property
raise NotImplementedError
def get address(self ): raise NotImplementedError
def set address(self , value): raise NotImplementedError
abstractproperty( get address, set address)
A.11. Tuplas con campos con nombre
El m odulo collections ofrece la clase namedtuple, que permite denir tuplas a cuyos com-
ponentes podemos acceder tanto por ndice como por nombre, sin que ello suponga un
incremento del consumo de memoria por instancia.
Podemos denir, por ejemplo, una tupla Point cuyos dos campos corresponden a la
abcisa y ordenada de un punto en el plano, y a los que referimos con los campos x e y,
del siguiente modo:
from collections import namedtuple
Point = namedtuple("Point", "x y")
p = Point(1, 2)
print(p.x, p.y)
print(p[0], p[1])
1 2
1 2
Conviene recordar que las tuplas son inmutables.
A.12. Iteradores y generadores
Es frecuente que surja la necesidad de recorrer todos los elementos de una colecci on de
datos. Python permite denir iteradores, es decir, objetos que permiten efectuar estos
recorridos. C++ ofrece una funcionalidad equivalente a trav es de los iteradores para el
recorrido de estructuras de datos de la STL. Podemos iterar, por ejemplo, sobre los ele-
mentos de una lista o las claves de un diccionario:
368 Apuntes de Algoritmia 28 de septiembre de 2009
a list = range(1,5)
for value in a list: print(value, end= )
print()
a dict = {a: 1, b: 2}
for key in a dict: print(key, end= )
print()
1 2 3 4
a b
Decimos de un objeto sobre el que se puede iterar que es iterable. Adem as de listas
y diccionarios, son iterables las cadenas (se recorren car acter a car acter), los cheros (se
recorren lnea a lnea), los conjuntos (elemento a elemento en un orden arbitrario), etc.
La funci on range permite iterar sobre enteros en un rango determinado. La forma
general de range usa tres argumentos: el valor inicial, el lmite superior (que se excluye)
y el incremento entre elementos consecutivos. El constructor de listas acepta un iterable
para inicializar la lista:
an array = list(range(1, 100, 15))
print(an array)
[1, 16, 31, 46, 61, 76, 91]
Tambi en podemos obtener una tupla a partir de una lista con la funci on tuple:
print(tuple(range(6)))
(0, 1, 2, 3, 4, 5)
Si se invoca a range con dos argumentos, el incremento toma el valor uno. Si se invoca
con un s olo argumento, el valor inicial es 0:
print(list(range(6)), list(range(1,6)), list(range(1,6,2)), end= )
print(list(range(1,6,1)), list(range(6,1,-1)))
[0, 1, 2, 3, 4, 5] [1, 2, 3, 4, 5] [1, 3, 5] [1, 2, 3, 4, 5] [6, 5, 4, 3, 2]
En ocasiones necesitamos generar secuencias de valores llamando a una funci on o
haciendo referencia a un objeto. Es necesario almacenar informaci on de estado entre lla-
mada y llamada, lo que complica el dise no de las iteraciones. Python facilita esta labor
permitiendo denir generadores, esto es, funciones o m etodos que proporcionan una
serie de valores (sobre los que se puede iterar) almacenando autom aticamente el esta-
do. Podemos denir f acilmente un generador mediante una funci on que hace uso de la
sentencia yield. Cada ejecuci on de yield supone la devoluci on de un valor y el almace-
namiento del estado de la funci on para poder acceder a futuros valores:
28 de septiembre de 2009 Ap endice A. Python 369
def n squares(n):
for i in range(n):
yield i*i
for i in n squares(8): print(i, end= )
print()
0 1 4 9 16 25 36 49
class MyClass:
def init (self , a, b):
self .a = a
self .b = b
def iter (self ):
for i in range(self .a, self .b):
yield i
myobject = MyClass(1, 5)
for i in myobject: print(i, end= )
print()
for i in myobject: print(i, end= )
print()
1 2 3 4
1 2 3 4
Es posible denir expresiones que devuelven un iterador: las expresiones generatri-
ces, similares a las listas especicadas por comprensi on:
iterator = (n*n for n in range(10) if n % 2 == 0)
for value in iterator: print(value, end= )
print()
0 4 16 36 64
Los iteradores son objetos de la clase generator y es posible invocar sobre ellos una
funci on, next(), que genera el siguiente elemento en la serie que produce. Cuando la
serie se agota, next() dispara una excepci on de tipo StopIteration. Este ejemplo ilustra el
uso de next() y la detecci on del nal de la iteraci on por captura de la excepci on:
iterator = (n*n for n in range(10) if n % 2 == 0)
try:
while True:
print(next(iterator), end= )
except StopIteration:
print("Se acabo.")
370 Apuntes de Algoritmia 28 de septiembre de 2009
0 4 16 36 64 Se acabo.
Si no deseamos que se emita una excepci on al nalizar la iteraci on, hay un segundo
argumento de next con el valor que devuelve cuando se agota el iterador:
iterator = (n*n for n in range(10) if n % 2 == 0)
while True:
x = next(iterator, None)
if x == None: break
print(x, end= )
print("Se acabo.")
0 4 16 36 64 Se acabo.
A.12.1. Algunas funciones auxiliares para iterables
La funci on enumerate permite recorrer los elementos de una lista y conocer, para cada
uno, su ndice:
for index, item in enumerate([i**2 for i in range(10) if i % 2 == 0]):
print((index, item), end= )
print
(0, 0) (1, 4) (2, 16) (3, 36) (4, 64)
Podramos denir nuestra propia versi on de enumerate de este modo:
def myenumerate(iterable):
i = 0
for item in iterable:
yield i, item
i += 1
Python ofrece varias funciones predenidas sobre iterables. Entre ellas tenemos:
min(iterable): valor mnimo de una secuencia.
max(iterable): valor m aximo de una secuencia.
sum(iterable): suma de todos los valores de una secuencia.
sorted(iterable): devuelve una lista con los elementos del iterable ordenados.
reversed(iterable): recorre los elementos del iterable en orden inverso.
any(iterable): devuelve True si alguno de los elementos del iterable pueden inter-
pretarse como True.
28 de septiembre de 2009 Ap endice A. Python 371
all(iterable): devuelve True si todos los elementos del iterable puede interpretarse
como True.
iter(iterable): devuelve un iterador sobre los elementos de un objeto iterable.
next(iterable): devuelve el siguiente elemento en una iteraci on. Si la iteraci on se ha
agotado lanza la exception StopIteration. Si se invoca con next(iterable, value) no
lanza la excepci on cuando la iteraci on se agota; en su lugar devuelve value.
Podemos construir una lista con todos los valores y aplicar la funci on min:
print(min([x-x**3 for x in range(10)]))
-720
Si actuamos as, estamos reservando memoria para cada uno de los elementos y la
liberamos inmediatamente. Si reescribimos esta sentencia haciendo uso directo de expre-
siones generatrices, no se construye una lista en memoria:
print(min(x-x**3 for x in range(10)))
-720
La diferencia sint actica entre esta sentencia y la anterior es mnima: hemos eliminado
los corchetes que rodean al argumento. El consumo de memoria de la ultima versi on es
constante.
El uso de min y max resulta inc omodo cuando cabe la posibilidad de que se le sumi-
nistren iterables vacos, pues se produce una excepci on:
print(min([]))
Traceback (most recent call last):
File "programas\test_min_exception.py", line 2, in <module>
print(min([]))
ValueError: min() arg is an empty sequence
Nosotros adoptamos el convenio de que el menor elemento de un conjunto vaco es
el m aximo valor posible y de que el mayor elemento de un conjunto vaco es el mnimo
valor posible. En el siguiente m odulo redenimos las funciones min y max para que ad-
mitan un par ametro opcional (ifempty para min y max) con el valor que debe ser devuelto
cuando se suministra un iterable vaco:
algoritmia/utils.py
min = min
def min(*it: "iterable<T>", **kw: "ifempty with value") -> "T":
try:
return min(*it)
except ValueError:
372 Apuntes de Algoritmia 28 de septiembre de 2009
if ifempty in kw: return kw[ifempty]
raise ValueError
max = max
def max(*it: "iterable<T>", **kw: "ifempty with value") -> "T":
try:
return max(*it)
except ValueError:
if ifempty in kw: return kw[ifempty]
raise ValueError
from algoritmia.utils import min, max
print(min(10,20), min([10,20]), min(10,20,ifempty=100), min([],ifempty=100))
print(max(10,20), max([10,20]), max(10,20,ifempty=0), max([],ifempty=0))
10 10 10 100
20 20 20 0
Otras funciones relacionadas con la b usqueda del mnimo o m aximo de un iterable y
que nos convendr a tener accesibles son
argmin, que recibe un iterable y una funci on y devuelve el argumento de la funci on
que hace mnino el valor devuelto por la funci on;
y argmax, que recibe un iterable y una funci on y devuelve el argumento de la fun-
ci on que hace m aximo el valor devuelto por la funci on;
algoritmia/utils.py
def argmin(iterable: "iterable<T>", fn: "f: T -> S", ifempty: "T or None"=None) -> "T":
try:
return min( (fn(x), x) for x in iterable )[1]
except ValueError:
return ifempty
def argmax(iterable: "iterable<T>", fn: "f: T -> S", ifempty: "T or None"=None) -> "T":
try:
return max( (fn(x), x) for x in iterable )[1]
except ValueError:
return ifempty
from algoritmia.utils import argmin, argmax
print(En el rango de enteros [-3, 1], el valor de menor cuadrado es, end= )
print(argmin( range(-3, 2), lambda x: x**2))
print(y el de mayor cuadrado es, argmax( range(-3, 2), lambda x: x**2))
28 de septiembre de 2009 Ap endice A. Python 373
En el rango de enteros [-3, 1], el valor de menor cuadrado es 0
y el de mayor cuadrado es -3
Como no podemos invocar a la funci on len sobre generadores (ya que recorrer sus
elementos agota el generador), pero nos vendr a bien hacerlo en determinadas circuns-
tancias, denimos ahora una funci on de utilidad que cuenta los elementos de un genera-
dor:
algoritmia/utils.py
def count(iterable: "iterable<T>") -> "int":
return len(iterable) if hasattr(iterable, "__len__") else sum(1 for i in iterable)
Finalmente indicaremos que hay un m odulo con utilidades para la gesti on de itera-
dores: itertools. Haremos uso de algunas funciones denidas en el:
chain. Recibe dos o m as iteradores y proporciona un iterador que recorre todos los
elementos de su concatenaci on.
repeat. Recibe un valor y un entero n y proporciona una secuencia que lo repite n
veces.
from itertools import chain, repeat
for i in chain(range(0, 5), range(7, 11)):
print(i, end= )
print()
print(list(repeat(1, 5)))
0 1 2 3 4 7 8 9 10
[1, 1, 1, 1, 1]
A.13. Ficheros
Los cheros se abren con la funci on predenida open. El primer par ametro es la ruta del
chero y el segundo indica el modo de apertura. Se usa w para escritura, r para
lectura y a para adici on. El valor por defecto del segundo par ametro es r.
Por defecto se considera que los cheros son de texto. Son iterables y, si est an abiertos
en modo lectura, se recorren lnea a lnea con un bucle for-in. El m etodo write permite
escribir una cadena de texto arbitraria en un chero abierto para escritura. Un chero
abierto se cierra con el m etodo close.
374 Apuntes de Algoritmia 28 de septiembre de 2009
f = open(basura, w)
f .write(una lnea\ny otra\n)
f .write(una mas\n)
f .close()
for line in open(basura):
print(line, end=)
una lnea
y otra
una mas
A.14. Inyecci on de dependencias mediante factoras
Acabamos con un punto que pone en juego diferentes elementos del lenguaje de pro-
gramaci on Python y que explica un t ecnica que utilizamos intensivamente en el texto:
una forma ligera de inyecci on de dependencias. Un objeto puede depender de otros a
los que instancia o accede durante su construcci on o al ejecutar un m etodo. Los objetos
instanciados o accedidos pueden ser de una de entre varias clases que ofrecen un mismo
repertorio de m etodos y atributos (si se quiere, una misma interfaz). La inyecci on de de-
pendencias es la t ecnica o conjunto de t ecnicas que hacen posible inyectar las clases u
objetos que convenga.
Algunos algoritmos manejan estructuras de datos auxiliares de las que depende su
eciencia. Un algoritmo podra utilizar una colecci on de valores al realizar un c alculo,
pero el coste del c alculo se ver a afectado por la colecci on utilizada. Podramos pensar
en que un an alisis apropiado permitir a seleccionar la estructura de datos m as eciente y
que es esta y no otra la que hay que usar en toda circunstancia. Pero en ciertos casos no
hay una estructura id onea, sino una serie de estructuras de las que conviene seleccionar
una seg un el contexto de la ejecuci on.
Pongamos por caso un algoritmo, codicado como m etodo en una clase, que pro-
porciona un iterable con los n umeros primos comprendidos entre 2 y un valor n. El al-
goritmo, conocido como criba de Erat ostenes, prepara una colecci on con los n enteros y
elimina los que son m ultiplos de cualquier n umero entre 2 y
n. He aqu una codica-
ci on directa del algoritmo que utiliza una lista para mantener los supervivientes y una
peque na prueba:
from math import sqrt
class Primes:
def compute(self , n: "int") -> "iterable<int>":
ints = list(range(2, n+1))
for p in range(2, int(sqrt(n))+1):
for m in range(2, n // p + 1):
if m * p in ints:
ints.remove(m * p)
28 de septiembre de 2009 Ap endice A. Python 375
for k in ints:
yield k
primes = Primes()
for i in primes.compute(100):
print(i, end=" ")
print()
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
El algoritmo es ineciente en tanto que las consultas de pertenencia de un elemento
a la colecci on son muy costosas. Un algoritmo m as eciente en la pr actica utiliza un
conjunto (tipo set de Python) en lugar de un vector (tipo list de Python):
from math import sqrt
class Primes:
def compute(self , n: "int") -> "iterable<int>":
ints = set(range(2, n+1))
for p in range(2, int(sqrt(n))+1):
for m in range(2, n // p + 1):
if m * p in ints:
ints.remove(m * p)
for k in ints:
yield k
primes = Primes()
for i in primes.compute(100):
print(i, end=" ")
print()
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
Es el tipo set la mejor elecci on? No, el vector caracterstico, una estructura que
estudiamos en el texto, ofrece un mejor comportamiento. La inyecci on de dependencias
permite seleccionar la estructura de datos que deseamos utilizar en el momento de la
construcci on del objeto que calcula los primos. Para ello recurre a una factora, es decir,
una funci on que devuelve una instancia de la clase elegida a partir de algunos par ame-
tros. La factora puede tomar un valor por defecto, de modo que se disponga de una
estructura razonable cuando no se indica que se desea utilizar una en particular. Ve amos
c omo se puede hacer con nuestra clase:
from math import sqrt
class Primes:
def init (self , collectionFactory=lambda r: set(r)):
self .collectionFactory = collectionFactory
def compute(self , n: "int") -> "iterable<int>":
376 Apuntes de Algoritmia 28 de septiembre de 2009
ints = self .collectionFactory(range(2, n+1))
for p in range(2, int(sqrt(n))+1):
for m in range(2, n // p + 1):
if m * p in ints:
ints.remove(m * p)
for k in ints:
yield k
primes = Primes()
for i in primes.compute(100):
print(i, end=" ")
print()
2 3 5 7 11 13 17 19 23 29 31 37 41 43 47 53 59 61 67 71 73 79 83 89 97
El constructor de Primes recibe una funci on an onima que, a partir de un valor n, de-
vuelve un conjunto con los enteros entre 2 y n + 1. Podemos usar la clase como hasta el
momento y usar a la clase set. Pero tambi en podemos construir la instancia de Primes as:
primes = Primes(collectionFactory=lambda r: list(r))
for i in primes.compute(100):
print(i, end=" ")
print()
Hemos inyectado la clase list donde por defecto se usaba la clase set.
Esta t ecnica de inyecci on de dependencias es tremendamente util y se beneciar a de
una funci on que a nadimos al m odulo utils:
algoritmia/utils.py
def get factories(instance, given factories, **default factories):
for identier in default factories:
if identier not in given factories:
setattr(instance, identier, default factories[identier])
for identier in given factories:
if identier.endswith("Factory"):
if identier not in default factories:
raise KeyError("Unknown factory ({})".format(identier))
setattr(instance, identier, given factories[identier])
La funci on get factories recibe una instancia, un diccionario de par ametros opcionales
y un diccionario con las factoras esperadas. Al ejecutarse, a nade a la instancia un campo
por cada factora. Este campo tiene el valor por defecto o el que suministre el usuario en
el diccionario de par ametros opcionales.
La funci on permite simplicar el dise no de constructores con inyecci on de dependen-
cias. Se usara as en el ejemplo que estamos desarrollando:
28 de septiembre de 2009 Ap endice A. Python 377
algoritmia/problems/primes.py
from math import sqrt
from algoritmia.utils import get factories
class Primes:
def init (self , **kw):
get factories(self , kw, collectionFactory=lambda r: set(r))
def compute(self , n: "int") -> "iterable<int>":
ints = self .collectionFactory(range(2, n+1))
for p in range(2, int(sqrt(n))+1):
for m in range(2, n // p + 1):
if m * p in ints:
ints.remove(m * p)
for k in ints:
yield k
Una ventaja de usar get factories es el control de errores b asico que efect ua: si se sumi-
nistra un par ametro opcional acabado en el sujo Factory y no forma parte de los que se
declaran como par ametros opcionales de get factory, lanza una excepci on.
Ap endice B
CONCEPTOS MATEM
ATICOS
En el texto efectuamos numerosos an alisis en los que es necesario dominar ciertas nocio-
nes matem aticas b asicas. En este ap endice se repasan algunos de los conceptos funda-
mentales y se presentan algunos elementos de notaci on.
B.1. C alculo proposicional
Hay dos valores de verdad o valores booleanos: cierto y falso. Una proposici on es
una sentencia declarativa que es cierta o falsa. Por ejemplo, La algortmica es importan-
te es una proposici on. Si p y q son dos proposiciones, su conjunci on se denota con p q.
El valor de la conjunci on es cierto si y s olo si p y q son ciertas. La disyunci on se denota
con p q y su valor es falso si y s olo si p y q son falsas. La negaci on de una proposici on
p se denota con p y es cierta si p es falsa y viceversa. Si la verdad de p implica la ver-
dad de q, decimos que p implica q y lo denotamos con p q. Con p q denotamos
(p q) (q p).
Una proposici on abierta o predicado p(x) es una armaci on acerca de una variable
x que toma valores en un universo dado. Por ejemplo, el n umero x es primo es una
proposici on abierta y su valor de verdad o falsedad depende del valor de x. Con x, p(x)
indicamos que la proposici on abierta p(x) es cierta para todo valor posible de x. Con x,
p(x) indicamos que p(x) es cierta para al menos un valor de x. Con !x, p(x) indicamos
que existe un solo x tal que p(x) es cierta.
B.2. Conjuntos
Un conjunto es una colecci on de elementos no repetidos. Se denota encerrando estos
entre llaves. Por ejemplo, A = {2, 4, 6, 8} es el conjunto formado por los elementos 2, 4, 6
y 8. El conjunto vaco, {}, el que no tiene ning un elemento, se denota con .
379
380 Apuntes de Algoritmia 28 de septiembre de 2009
Denotamos con A la cardinalidad o n umero de elementos del conjunto A. Un con-
junto es nito si contiene un n umero nito de elementos, e innito en caso contrario.
Si un elemento a est a en un conjunto A decimos que pertenece a A y lo denotamos
con a A. Expresamos con b / A que el elemento b no est a en A.
Un conjunto A es subconjunto de otro, B, y se denota con A B, si todo elemento
de A pertenece a B. Si, adem as, A = B, decimos que es un subconjunto propio y lo
denotamos con A B.
La operaci on de uni on entre dos conjuntos A y B, que denotamos con A B, es el
conjunto de elementos que est an en A o B. La intersecci on A y B, A B, es el conjunto de
elementos que est an tanto en A como en B. Dos conjuntos son disjuntos si su intersecci on
est a vaca.
El conjunto de todos los subconjuntos de un conjunto A es el conjunto de las par-
tes de A y se denota con (A). La talla del conjunto de las partes de un conjunto con
n elementos es 2
n
. Por ejemplo, el conjunto de las partes de A = {a, b, c} es (A) =
{, {a}, {b}, {c}, {a, b}, {a, c}, {b, c}, {a, b, c}}, que tiene 2
3
= 8 elementos.
Una partici on de A es una serie de subconjuntos no vacos A
1
, A
2
, . . . , A
n
de A tal
que
A = A
1
A
2
A
n
, A
i
A
j
= , i = j, 1 i, j n. (B.1)
Podemos expresar los elementos de un conjunto enumer andolos, como al hacer A =
{2, 4, 6, 8}, o expresando una propiedad que estos satisfacen. Por ejemplo, con la notaci on
{x x es un entero par y 0 < x 8} representamos el mismo conjunto A. La barra
vertical se lee tal que.
Dado un conjunto U al que denominamos universo y un subconjunto suyo A, nota-
mos con
A el complemento de A, es decir, el conjunto de elementos que est an en U y no
en A:
A = {a U a / A}.
Algunos conjuntos fundamentales son:
El conjunto de los booleanos (valores l ogicos), B = {True, False}.
El conjunto de los n umeros naturales, N = {1, 2, 3, . . .}.
El conjunto de los n umeros enteros, Z = {0, 1, 2, 3, . . .}.
El conjunto de los n umeros enteros no negativos, Z
0
= {0, 1, 2, 3, . . .}.
El conjunto de los n umeros racionales, Q = {a/b a, b Z, b = 0}.
El conjunto de los n umeros reales, R, formado por el continuo de n umeros raciona-
les e irracionales (aquellos que no pueden expresarse como fracci on de dos enteros).
El conjunto de los n umeros reales positivos, R
>0
= {x R x > 0}.
El conjunto de los n umeros reales no negativos, R
0
= R
>0
{0}.
El conjunto de los n umeros complejos, C = {a + bi a, b R}, donde i =
1.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 381
, m
) (n < n
) (n = n
m m
).
b) (n, m) (n
, m
) (n n
).
Son ordenamientos totales?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.4. Funciones
Dados dos conjuntos A y B, una funci on f es una relaci on binaria en AB que satisface
que para todo elemento a A hay un unico b B tal que (a, b) f . El conjunto A es el
dominio de f y el conjunto B es su codominio. Solemos especicar dominio y codominio
con la notaci on f : A B y si (a, b) f escribimos f (a) = b y decimos que b es la
imagen de a bajo f . El rango de B es la imagen del dominio. Si f (a) = b decimos que a
es el argumento y b es el valor.
Una funci on f es mon otona creciente si a b implica que f (a) f (b). An alogamen-
te, es mon otona decreciente si a b implica que f (a) f (b). La funci on f es mon otona,
estrictamente creciente si a < b implica que f (a) < f (b) y es mon otona, estrictamente
decreciente si a < b implica que f (a) > f (b).
Una funci on f es una funci on inyectiva si y s olo si a, b A, a = b para los cuales
f (a) = f (b). Es biyectiva, si y s olo si, adem as, para todo b B, existe al menos un a A
para el que f (a) = b.
B.5. Estructuras algebraicas
Una operaci on es una acci on o procedimiento que produce un valor a partir de otros. Hay
dos tipos comunes de operaciones: las unarias, que act uan sobre un solo elemento (como
la negaci on de valores l ogicos, el cambio de signo de n umeros enteros, las funciones
trigonom etricas sobre n umeros reales), y las binarias (como las operaciones de suma,
resta, producto y divisi on de n umeros), que act uan sobre dos elementos. Una operaci on
384 Apuntes de Algoritmia 28 de septiembre de 2009
se dice cerrada si opera sobre elementos de un conjunto y produce elementos de ese
mismo conjunto.
Una estructura algebr aica es una tupla (A
1
, A
2
, . . . , A
n
), donde A
1
es un conjunto no
vaco y A
2
, . . . , A
n
son operaciones aplicables a sus elementos o elementos de A
1
, que
satisface determinados axiomas. Algunas estructuras algebraicas que aparecer an en el
texto son el monoide, el monoide conmutativo y el semianillo.
B.5.1. Monoide
Un monoide es una estructura algebraica (R, , 0), donde es un operador binario R
R R y 0 es un elemento identidad, con los siguientes axiomas:
Asociatividad: para todo a, b y c en R, se observa (a b) c = a (b c).
Elemento identidad: existe un elemento 0 tal que a 0 = 0 a = a para todo a en
R.
B.5.2. Monoide conmutativo
Un monoide (R, , 0) es conmutativo si es conmutativa, es decir, si a b = b a para
todo a, b en R.
En todo anillo conmutativo hay una relaci on de preorden denido as: a b si y
s olo si existe un c R tal que a c = b.
B.5.3. Semianillo
Dados un conjunto R, dos operaciones binarias, : R R R y : R R R, y dos
elementos de R, 0 y 1, la tupla (R, , , 0, 1) es un semianillo si satisface las siguientes
condiciones:
(R, ) es un monoide conmutativo con elemento identidad 0, es decir, cumple:
(a b) c = a (b c) para todo a, b y c R. [Asociatividad]
a b = b a para todo a, b R. [Conmutatividad]
0 a = a 0 = a para todo a R.
(R, ) es un monoide con elemento identidad 1:
(a b) c = a (b c) para todo a, b y c R.
1 a = a 1 = a para todo a R.
es distributiva sobre :
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 385
a (b c) = (a b) (a c), para todo a, b, c R.
(a b) c = (a c) (b c), para todo a, b, c R.
0 es absorbente para :
0 a = a 0 = 0.
Los semianillos que se muestran en la tabla B.1 aparecen en problemas abordables
por programaci on din amica y tienen nombre propio (la tabla no es una lista exhaustiva
de semianillos).
Semianillo R 0 1
Booleano {falso, cierto} falso cierto
Tropical (max-plus) R
0
{} m ax + 0
Tropical (min-plus) R
0
{+} mn + + 0
Conteo N + 0 1
Probabilstico [0, 1] + 0 1
Viterbi [0, 1] m ax 0 1
Difuso (fuzzy) [0, 1] m ax mn 0 1
Tabla B.1: Algunos semi-
anillos. (Hay dos versiones
del semianillo tropical.)
El semianillo tropical recibe su nombre del pas de origen del matem atico
Imre Simon: Brasil.
B.6. M etodos de demostraci on
B.6.1. Inducci on matem atica
La inducci on matem atica es un m etodo de demostraci on que permite probar la veracidad
de un predicado P, es decir, una funci on que devuelve cierto o falso, cuando depende
de un par ametro o par ametros que toman valores sobre un ordenamiento.
Inducci on d ebil
Sea P(n) alg un predicado acerca del natural n y supongamos que deseamos demostrar
que es cierto para todo valor de n. La demostraci on por inducci on consiste en:
Demostrar que P(1) es cierto.
Demostrar que si P(1), P(2), . . . , P(n) son ciertos, entonces P(n + 1) tambi en es
cierto.
386 Apuntes de Algoritmia 28 de septiembre de 2009
La demostraci on de que P(1) es cierto recibe el nombre de base de inducci on. La
suposici on de que P(1), P(2), . . . , P(n) son ciertos es la hip otesis de inducci on. La de-
mostraci on de que, dada la base de inducci on, P(n + 1) es cierto se denomina paso de
inducci on.
La denici on de la inducci on debe interpretarse en el marco de cada demostraci on
concreta. En ocasiones n est a denido sobre un conjunto diferente del de los naturales. Si,
por ejemplo, hemos de demostrar una propiedad sobre los n umeros impares, podemos
reinterpretarla considerando que P(1) es el caso base y que hemos de comprobar que si
P(1), P(3), . . . , P(n), para n impar, son ciertos, entonces P(n +2) tambi en lo es. Y a veces
tendremos que considerar casos base distintos de P(1), como P(0).
Inducci on (completa)
Hay un principio de inducci on m as b asico y equivalente al anterior:
Demostrar que P(1) es cierto (base de inducci on).
Demostrar que si P(n) es cierto (hip otesis de inducci on), entonces P(n +1) tambi en
es cierto (paso de inducci on).
Este principio se denomina de inducci on completa o simplemente de inducci on.
Tomemos por caso el predicado la suma de los n primeros n umeros impares es igual
a n
2
. Si hacemos una prueba vemos que parece cierto:
n suma de los n primeros impares n
2
1 1 = 1 1
2 1+3 = 4 4
3 1+3+5 = 9 9
4 1+3+5+7 = 16 16
5 1+3+5+7+9 = 25 25
Podemos demostrar por inducci on la validez del predicado P(n) para todos los naturales:
Base de inducci on. P(1) es cierto, es decir, la suma de los n primeros n umeros impares
es igual a n
2
si n es 1.
Demostraci on. El primer n umero impar es 1. El valor de 1
2
es igual a 1.
Hip otesis de inducci on. Si suponemos que P(1), P(2), . . . , P(n) es cierto (hip otesis de
inducci on). . .
Paso de inducci on. . . . entonces se cumple P(n +1).
Demostraci on. La suma de los n + 1 primeros n umeros naturales impares es igual a
la suma de los n primeros impares m as 2(n +1) 1, que es el impar (n +1)- esimo.
Como la suma de los n primeros impares es, por hip otesis de inducci on, igual a n
2
,
tenemos que la suma de los n +1 primeros n umeros impares es n
2
+2(n +1) 1 =
n
2
+2n +1 = (n +1)
2
.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 387
n
2
= n. (B.6)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
190 Calcula los valores de , , e y e.
191 Demuestra por inducci on que
n
2
=
{
n
2
, si n es par;
n1
2
, si n es impar;
y que
n
2
=
{
n
2
, si n es par;
n+1
2
, si n es impar;
para todo n 0.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.8. Potencias y logaritmos
Para todo real x > 0, m y n tenemos:
x
0
= 1, (B.7)
x
1
= x, (B.8)
x
1
= 1/x, (B.9)
(x
m
)
n
= x
mn
= (x
n
)
m
, (B.10)
x
m
x
n
= x
m+n
, (B.11)
x
m
/x
n
= x
mn
, (B.12)
n
x
m
= x
m/n
. (B.13)
El logaritmo de y en base b es un n umero x tal que y = b
x
y se nota log
b
y. El logaritmo
es la funci on inversa de la exponencial.
He aqu algunas propiedades utiles. Para todo x, y, a, b R se cumple:
log
b
1 = 0, (B.14)
log
b
a
x
= x log
b
a, si a > 0 y b = 1; (B.15)
log
b
b
x
= x, si b = 1; (B.16)
x
log
b
y
= y
log
b
x
(B.17)
log
b
(xy) = log
b
x +log
b
y, si x, y > 0 y b = 1; (B.18)
log
b
(x/y) = log
b
x log
b
y, si y = 0 y b = 1; (B.19)
log
b
(1/y) = log
b
y, si y = 0 y b = 1; (B.20)
log
b
a =
1
log
a
b
, si a, b = 1. (B.21)
390 Apuntes de Algoritmia 28 de septiembre de 2009
Podemos expresar el logaritmo en una base como funci on del logaritmo en cualquier
otra base:
log
c
x =
log
b
x
log
b
c
, b, c, x > 0 y c = 1. (B.22)
Notaremos con lg(n) el logaritmo binario de n, es decir, log
2
(n); con ln(n) el logaritmo
neperiano de n, o sea, log
e
(n); y con log(n) el logaritmo decimal o en base 10.
El logaritmo es una funci on que crece lentamente
log
b
x < x, x > 0 (B.23)
Por ejemplo, lg 1 = 0, lg 2 = 1, lg 1 024 = 10, lg 1 048 576 = 20, . . .
B.9. Sucesiones y sumatorios
B.9.1. Sucesiones
Una sucesi on es una funci on f : N R. El valor f (n) es el t ermino n- esimo. Es frecuente
utilizar una letra con subndice para referirse a un t ermino de una sucesi on. As, a
n
es el
n- esimo t ermino de la sucesi on que denotamos con {a
n
}.
i
= a
i+1
para tener una sucesi on ajustada a la denici on.
Una progresi on aritm etica de raz on o diferencia d es una sucesi on tal que, para todo
i > 1 se cumple a
i
a
i1
= d. Una progresi on geom etrica de raz on r es una sucesi on tal
que, para todo i > 1 se cumple a
i
/a
i1
= r.
B.9.2. Sumatorios
El sumatorio permite expresar de forma compacta la suma de t erminos de una sucesi on:
1in
a
i
= a
1
+ a
2
+ + a
n
. (B.24)
La variable i recibe el nombre de ndice.
He aqu algunas propiedades del sumatorio:
La ley distributiva para productos de sumas:
(
1in
a
i
)
(
1jm
b
j
)
=
1in
(
1jm
a
i
b
j
)
. (B.25)
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 391
Intercambio del orden de los sumatorios:
1in
1jm
a
ij
=
1jm
1in
a
ij
. (B.26)
Linealidad:
1in
(a
i
+ c b
i
) =
1in
a
i
+ c
1in
b
i
. (B.27)
B.9.3. Sumatorio de una progresi on aritm etica
El sumatorio de los n primeros elementos de una progresi on aritm etica {a
i
} es
1in
a
i
=
n
2
(a
1
+ a
n
). (B.28)
Se puede demostrar f acilmente si sumamos dos veces la serie de valores:
s = a
1
+ a
2
+ a
3
+ + a
n1
+ a
n
s = a
n
+ a
n1
+ a
n2
+ + a
2
+ a
1
(a
1
+ a
n
) +(a
2
+ a
n1
) +(a
3
+ a
n2
) + +(a
n1
+ a
2
) +(a
n
+ a
1
)
Cada t ermino entre par entesis suma a
1
+ a
n
y hay n t erminos. La suma es, por tanto,
2s = n(a
1
+ a
n
). Como hemos sumado dos veces la serie de valores es preciso dividir por
dos la suma total para obtener el resultado, s = n/2(a
1
+ a
n
).
1in
a
i
= a
1
r
n
1
r 1
. (B.29)
Se puede demostrar empleando la t ecnica del telescopio. Sea S el sumatorio. Tenemos
S = a
1
+ a
1
r + a
1
r
2
+ + a
1
r
n1
, (B.30)
rS = a
1
r + a
1
r
2
+ a
1
r
3
+ + a
1
r
n
, (B.31)
El valor de S rS es a
1
a
1
r + a
1
r a
1
r
2
+ a
1
r
2
a
1
r
3
+ + a
n1
r
ar
n
, o sea, S rS =
a
1
a
1
r
n
. Despejando S tenemos
S =
a
1
a
1
r
n
1 r
= a
1
1 r
n
1 r
= a
1
r
n
1
r 1
. (B.32)
392 Apuntes de Algoritmia 28 de septiembre de 2009
B.9.5. Algunos sumatorios de inter es
Estos otros sumatorios aparecer an en algunos an alisis:
1in
1 = n, (B.33)
1in
i =
n(n +1)
2
, (B.34)
1in
i
2
=
n(n +1)(2n +1)
6
, (B.35)
1in
i
3
=
n
2
(n +1)
2
4
, (B.36)
0in
2
i
= 2
n+1
1, (B.37)
0in
x
i
=
x
n+1
1
x 1
, x > 1, (B.38)
0in
x
i
1
1 x
, 0 < x < 1. (B.39)
0in
ix
i
1
(1 x)
2
, 0 < x < 1. (B.40)
5
((
1 +
5
2
)
n
(
1
5
2
)
n
)
. (B.42)
Como el t ermino
(
1
5
2
)
n
tiene valor absoluto menor que 1, se hace despreciable
conforme n crece. As pues, resulta f acil concluir que F(n) crece exponencialmente.
El valor
1+
5
2
es una constante importante: la secci on aurea. Se denota con
la letra y vale aproximadamente 1.618. La divisi on de un n umero de Fibo-
nacci por el anterior produce una aproximaci on a tanto mejor cuanto mayores son
los n umeros. Un rect angulo guarda la proporci on aurea si la divisi on del lado mayor
por el menor es .
Se dice que la proporci on aurea es muy agradable est eticamente y que ha sido
usada en numerosos cuadros y edicios cl asicos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
195 Demuestra por inducci on la ecuaci on (B.42).
196 Demuestra por inducci on que
0in
F(i) = F(n +2) 1.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.9.7. Los n umeros arm onicos
La secuencia de los n umeros arm onicos H(n), para n 0, se dene as:
H(n) =
1kn
1
k
(B.43)
394 Apuntes de Algoritmia 28 de septiembre de 2009
Podemos acotar el valor de H(n):
lg n +1
2
< H(n) lg n +1;
ln n < H(n) ln n +1.
B.10. Productorios y factorial
El productorio expresa de forma compacta el producto de una serie de t erminos:
1in
a
i
= a
1
a
2
a
n
. (B.44)
El factorial de un n umero entero positivo n se denota con n! y se dene como el
productorio de los n primeros n umeros naturales:
n! =
1in
i. (B.45)
El factorial de 0 es, por denici on, 1:
0! = 1. (B.46)
El valor de n! puede aproximarse as (aproximaci on de Stirling):
n!
2n
(
n
e
)
n
. (B.47)
B.11. Permutaciones
Una permutaci on de n elementos (distintos) es una secuencia ordenada de los mismos.
Por ejemplo, hay seis permutaciones de los elementos del conjunto {a, b, c}: abc, acb, bac,
bac, cab y cba.
El n umero de permutaciones de n elementos es n! y resulta f acil deducir por qu e: la
primera posici on puede ocuparla cualquiera de los n elementos; la segunda, cualquiera
de los n 1 restantes; la tercera, cualquiera de los n 2 que no han sido dispuestos; y
as sucesivamente hasta llegar a la ultima posici on, que s olo puede ser ocupada por el
elemento restante.
Una variaci on de n elementos tomados de k en k (o k-permutaci on) de n elementos
(distintos) es una secuencia ordenada de k de esos elementos. (Una permutaci on de n
elementos es una n-permutaci on.) Las 2-permutaciones de los elementos a, b y c son ab,
ac, ba, bc, ca y cb. El n umero de k-permutaciones de n elementos es
n (n 1) (n 2) (n k +1) =
n!
(n k)!
. (B.48)
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 395
Las combinaciones de n elementos tomados de k en k son las posibles elecciones de
k elementos de un conjunto de talla n (sin que importe el orden y sin repetir elementos).
Por ejemplo, las combinaciones de los elementos a, b y c tomados de dos en dos son ab,
ac y bc. El n umero de combinaciones de n elementos tomados de k en k se denota con (
n
k
),
se lee n sobre k y se calcula as:
(
n
k
)
=
n!
k!(n k)!
. (B.49)
Los n umeros combinatorios, que as se llaman, aparecen en el desarrollo de (x + y)
n
.
Teorema B.1 (Teorema binomial) Si n es un un entero positivo, entonces
(x + y)
n
=
0kn
(
n
k
)
x
k
y
nk
. (B.50)
Del teorema binomial se deduce, haciendo x = y = 1, que
2
n
=
0kn
(
n
k
)
=
(
n
0
)
+
(
n
1
)
+ +
(
n
n
)
. (B.51)
He aqu algunas relaciones interesantes entre n umeros combinatorios:
(
n
k
)
=
(
n
n k
)
, (B.52)
(
n
k
)
=
(
n 1
k
)
+
(
n 1
k 1
)
. (B.53)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
197 Demuestra por inducci on la ecuaci on (B.51).
198 Demuestra por inducci on que, para todo n 1 y todo k entre 1 y n, se cumple
(
n
k
)
n
k
.
199 Hay 8 invitados a cenar y 8 sillas alrededor de la mesa. De cu antas formas diferentes
pueden sentarse? Y si al nal s olo acuden 6, de cu antas formas diferentes pueden sentarse?
200 En el juego del p oker, cu antas manos diferentes pueden salir al repartir 5 cartas con una
baraja de 40 cartas?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.12. N umeros complejos
B.12.1. Notaci on: formas bin omica y polar
Un n umero complejo presenta la forma z = a + ib, donde i =
1. El valor a es la
parte real de z y el valor b es su parte imaginaria. Esta forma de representar un
396 Apuntes de Algoritmia 28 de septiembre de 2009
Figura B.1: Representaci on gr aca del complejo z = a + ib = r(cos + i sin ), donde
r =
a
2
+ b
2
y = tan
1
(b/a).
z
a
b
r
complejo como suma de una parte real y un factor imaginario recibe el nombre de forma
bin omica. Un n umero complejo z = a + ib se puede representar gr acamente como el
punto (a, b) en el plano real, como muestra la gura B.1.
Podemos expresar z = a + ib como r(cos + i sin ), donde r =
a
2
+ b
2
y =
tan
1
(b/a). Los valores r y reciben el nombre de magnitud y fase, respectivamente. La
f ormula de Euler nos dice que cos +i sin es e
i
, as que podemos expresar z como re
i
.
Esta representaci on de z recibe el nombre de forma polar.
Cabe notar que un n umero complejo en forma polar no presenta una representaci on
unica: si a nadimos 2k radianes a su fase, para cualquier k entero, obtenemos el mismo
n umero (v ease la gura B.2).
Se adopta el convenio de representar los complejos en su forma polar can onica, es
decir, usando una fase tal que 0 < 2.
Figura B.2: El n umero complejo re
es igual a re
+2
: a nadir a la fase 2k radianes, para k
entero, no afecta al valor del complejo.
z
r
+2
B.12.2. Producto y exponenciaci on de complejos en forma polar
Podemos expresar el producto de dos complejos en forma polar, z = re
i
y z
= r
e
i
, as:
z z
= r r
e
i(+
)
.
La forma polar de un complejo simplica notablemente el c alculo del producto. N otese
que el producto de dos n umeros complejos es un nuevo complejo cuya magnitud es r r
y cuya fase es +
.
Antes decamos que a nadir 2k radianes, con k entero, a la fase de un complejo no
modicaba su valor. El complejo e
2
= e
0
= 1, por lo que multiplicar re
por e
2
, es decir,
sumar 2 radianes a su fase, proporciona el mismo complejo.
La exponencial de un complejo en forma polar resulta, tambi en, sencilla de calcular.
Si z = re
i
, entonces
z
n
= r
n
e
n
.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 397
B.12.3. Races de la unidad
La ecuaci on
n
= 1 presenta n soluciones complejas can onicas: son las n races n- esimas
de la unidad.
Estas son de la forma e
i2k/n
, para k entre 0 y n 1. Podemos comprobar
f acilmente que el resultado de elevarlas a la n- esima potencia proporciona la unidad:
(e
i2k/n
)
n
= e
i2k
= e
i0
= 1.
Las races n- esimas de la unidad son puntos equidistribuidos a lo largo de una cir-
cunferencia de radio 1 centrada en el origen. la gura B.3 muestra las n races de n- esimas
de la unidad para n = 8. La raz n- esima
n
= e
i2/n
recibe el nombre de raz n- esima
principal. El conjunto de las n races puede expresarse con
0
n
,
1
n
,
2
n
, . . . ,
n1
n
.
e
20/8
e
21/8
e
22/8
e
23/8
e
24/8
e
25/8
e
26/8
e
27/8
Figura B.3: Las ocho races octavas de la unidad son puntos distribuidos alrededor de
la circunferencia de radio unidad. La r aiz octava principal aparece marcada con un
punto m as grueso.
B.13. Notaci on asint otica
El objetivo de la notaci on asint otica es agrupar las funciones de N en R
0
en una serie
de familias atendiendo a su crecimiento. Cada familia de funciones se caracteriza por
estar acotada superior y/o inferiormente, para valores de su argumento sucientemente
grandes, por una funci on afectada por cierta constante positiva.
B.13.1. Cota asint otica superior: notaci on orden
Dada una funci on g : N R
0
, denotamos con O(g(n)) al conjunto de funciones f (n)
tales que existen una constante positiva c y un entero n
0
que hacen que f (n) c g(n)
para n n
0
:
O(g(n)) ={f : N R
0
c > 0, n
0
> 0 : f (n) c g(n), n n
0
}.
La expresi on O(g(n)) se lee como orden de g(n) y f (n) O(g(n)) se lee tanto
diciendo f (n) pertenece a O(g(n)) como f (n) es O(g(n)) Si f (n) O(g(n)) decimos
que g(n) es una cota superior asint otica de f (n) y que f (n) es orden de g(n).
n)
Lineales O(n)
Superlineales
O(n log n)
Polin omicas
Cuadr aticas O(n
2
)
C ubicas O(n
3
)
Exponenciales
O(2
n
)
O(n
n
)
B.13.2. Cota asint otica inferior: notaci on omega
As como O() proporciona una cota superior asint otica para una funci on, () propor-
ciona una cota inferior asint otica. (g(n)) es el conjunto de funciones f (n) tales que
existen una constante positiva c y un entero n
0
que hacen que f (n) c g(n) para n n
0
:
(g(n)) ={f : N R
0
c > 0, n
0
> 0 : f (n) c g(n), n n
0
}.
De una funci on f (n) que pertenece a (g(n)) se dice que es (g(n)) y se lee es
omega de g(n). La gura B.5 ilustra la idea de que una funci on f (n) sea (g(n)).
Figura B.5: f (n) es (g(n)): a partir de cierto valor n
0
de n, f (n) siempre es mayor o igual que c veces g(n),
para alg un valor positivo de c.
n
c g(n)
f (n) (g(n))
n
0
Por ejemplo, la funci on f (n) = n + 1 es (n) porque n + 1 n para todo n 1 y
c = 1.
Al expresar la de una funci on debe usarse la cota m as ajustada posible, es decir, si
decimos f (n) = (g(n)), usamos la funci on g(n) m as grande posible.
He aqu algunas propiedades equivalentes a otras que ya hemos visto al estudiar la
notaci on orden:
Regla de la reexividad f (n) ( f (n)).
Regla de la constante (c f (n)) = ( f (n)) para c > 0.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 401
Regla de los m aximos ( f (n) + g(n)) = (m ax{f (n), g(n)}).
Regla de la transitividad Si f (n) (g(n)) y g(n) (h(n)), entonces f (n) (h(n)).
Regla del producto f
1
(n) (g
1
(n)) y f
2
(n) (g
2
(n)), entonces f
1
(n) f
2
(n)
(g
1
(n) g
2
(n)).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
204 Demuestra que todo polinomio de n de orden m, f (n) = a
m
n
m
+ + a
1
n
1
+ a
0
es (n
m
).
205 Determina la cota inferior asint otica de las funciones del ejercicio 203.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.13.3. Cota asint otica superior e inferior: notaci on zeta
Cuando una funci on f (n) es a la vez O(g(n)) y (g(n)), decimos que es (g(n)) (que se
lee zeta de g(n)).
(g(n)) =O(g(n)) (g(n)).
Una funci on f (n) que pertenece a (g(n)) est a asint oticamente acotada superior e
inferiormente, por sendas funciones proporcionales a g(n) a partir de un valor determi-
nado de n (v ease la gura B.6):
(g(n)) ={f : N R
0
c
1
, c
2
> 0, n
0
> 0 : c
1
g(n) f (n) c
2
g(n), n n
0
}.
En ocasiones usamos f (n) = (g(n)) para expresar f (n) (g(n)) y decimos que f es
zeta de g.
n
c
2
g(n)
c
1
g(n)
f (n) (g(n))
n
0
Figura B.6: f (n) es (g(n)) porque es O(g(n)) y
(g(n)).
Con se induce una relaci on de equivalencia sobre las funciones de N en R
0
. Ca-
da conjunto (g(n)) es una clase de equivalencia. Las clases de equivalencia reciben el
nombre de clases de complejidad.
Las reglas que observan tanto cotas asint oticas superiores como inferiores (regla de
la reexividad, de la constante, de los m aximos, de la transitividad y del producto) son
observadas tambi en por esta cota asint oticas que es superior e inferior a la vez. Adem as,
se observa en este caso esta otra regla:
Regla de la dualidad Si f (n) (g(n)), entonces g(n) ( f (n)).
402 Apuntes de Algoritmia 28 de septiembre de 2009
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
206 Demuestra que todo polinomio de n de orden m, f (n) = a
m
n
m
+ + a
1
n
1
+ a
0
es (n
m
).
207 Determina la zeta, si la hay, de las funciones del ejercicio 203.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.14. Recurrencias
Un objeto es recursivo si el mismo forma parte de su denici on. Una ecuaci on recur-
siva, recurrencia, relaci on de recurrencia o ecuaci on en diferencias es una ecuaci on o
desigualdad que describe una funci on en t erminos de su propio valor sobre otro(s) argu-
mento(s).
Un ejemplo de recurrencia es la ecuaci on que dene as los n umeros factoriales:
n! =
{
1, si n = 0 o n = 1;
n (n 1)!, si n > 1.
Las recurrencias plantean una relaci on entre un objeto e instancias m as sencillas del
mismo objeto. Incluyen, adem as, uno o m as casos base, casos triviales o de nal de re-
cursi on que denen el objeto no recursivamente para las instancias m as sencillas. En el
caso del factorial, por ejemplo, los casos base son n = 0 y n = 1.
Al analizar algoritmos aparecer an recurrencias con cierta frecuencia. Esta ecuaci on
aparece, por ejemplo, al analizar el coste temporal de algunos algoritmos Divide y Ven-
cer as:
T(n) =
{
(1), si n = 1;
T(n/2) + T(n/2) +(n), si n > 1.
El par ametro n representa la talla del problema y es un entero.
Es frecuente eliminar ciertos detalles al trabajar con recurrencias. En la ultima, por
ejemplo, resulta molesto el uso de los operadores y . Si suponemos que n es poten-
cia de 2, la anterior recurrencia pasa a reescribirse as:
T(n) =
{
(1), si n = 1;
2T(n/2) +(n), si n > 1.
Resolvemos una recurrencia cuando encontramos un t ermino general (o expresi on
cerrada) para el valor de T(n), es decir, sin recurrencia.
Algunas ecuaciones de recurrencia pueden manipularse con cierta independencia de
los casos base. Cuando as sea, indicaremos unicamente el caso general. Por ejemplo,
con T(n) = 2T(n 1) + n para n > 1 expresamos incompletamente una ecuaci on de
recurrencia.
B.14.1. El m etodo del desplegado
El m etodo del desplegado permite formular una conjetura acerca del t ermino general
de una recurrencia que podemos demostrar por inducci on. La idea consiste en sustituir
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 403
reiteradamente los t erminos de la forma T() por la parte derecha de su ecuaci on recur-
siva y deducir cu antas veces se precisa efectuar la sustituci on hasta llegar al caso base.
Veamos un ejemplo. Despleguemos la ecuaci on recursiva
T(n) =
{
2, si n = 1;
2T(n 1), si n > 1.
Empezamos con T(n):
T(n) = 2T(n 1).
Sustituimos ahora T(n 1) por 2T(n 2):
T(n) = 2T(n 1) = 2 2T(n 2).
Y as sucesivamente:
T(n) = 2T(n 1) = 2 2T(n 2) = 2 2 2T(n 3) = = 2
k
T(n k).
Llegaremos al caso base cuando n k = 1, es decir, cuando k = n 1:
T(n) = 2T(n1) = 2 2T(n2) = 2 2 2T(n3) = = 2
k
T(nk) = = 2
n1
T(1).
Sustituimos T(1) por 2 y llegamos a la conclusi on de que
T(n) = 2
n1
2 = 2
n
.
Debe tenerse en cuenta que el m etodo del desplegado no es un m etodo de demos-
traci on formal. Para poder armar que T(n) = 2
n
, hemos de recurrir, por ejemplo, a la
demostraci on por inducci on. El m etodo del desplegado es, unicamente, una forma de
conjeturar la soluci on de una ecuaci on recursiva.
Apliquemos el m etodo a la recurrencia que estudiamos en el ultimo apartado:
T(n) =
{
1, si n = 1;
2T(n/2) + n, si n > 1.
Empezamos con T(n):
T(n) = 2T(n/2) + n.
Resulta complicado manejar el t ermino n/2. En aras de la simplicidad supondremos
que n es par y, por tanto, n/2 = n/2. Mejor a un, supondremos que n es una potencia
de dos, es decir, que puede expresarse como 2
m
para alg un valor entero de m. De ese
modo, las sucesivas divisiones por dos siempre producir an resultados enteros. Con esta
asunci on tenemos:
T(n) = 2T(n/2) + n.
Seguimos con el desplegado:
T(n) = 2T(n/2) + n = 2 (2T(n/4) + n/2) + n = 2 (2 (2T(n/8) + n/4) + n/2) + n.
404 Apuntes de Algoritmia 28 de septiembre de 2009
Si reescribimos el desplegado as
T(n) = 2
3
T(n/2
3
) + n + n + n = 2
3
T(n/2
3
) +3n
resulta f acil generalizar el desplegado y expresar T(n) de este modo:
T(n) = 2
k
T(n/2
k
) + kn.
Qu e valor de k hace que n/2
k
= 1 y podamos aplicar el caso base de la recursi on? F acil:
k = lg n.
T(n) = 2
lg n
T(1) + n lg n = 2
lg n
1 + n lg n = n + n lg n.
La expresi on exacta de T(n) para valores de n que no son potencia de 2 resulta com-
pleja. Pero podemos armar que T(n) O(n lg n) si observamos que, al ser T(n) una
funci on estrictamente creciente, T(2
m
) < T(n) < T(2
m+1
) para todo 2
m
< n < 2
m+1
.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
208 Resuelve las siguientes recurrencias, donde c
1
y c
2
son constantes:
a) T(n) =
{
c
1
, si n = 0;
T(n 1) + c
2
, si n > 0.
b) T(n) =
{
c
1
, si n = 0;
T(n/2) + c
2
, si n > 0.
(Ten cuidado con el caso base en el apartado b), pues lg0 no est a denido. Puedes efectuar la
demostraci on considerando que la base de inducci on est a en n = 1, aunque la recurrencia tenga
su caso base en n = 0. El caso n = 0 se puede tratar por separado, al margen de la inducci on.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.14.2. La demostraci on por inducci on
Se propone adivinar la soluci on de la recurrencia y demostrar por inducci on la validez de
la soluci on propuesta. La experiencia (y, por ejemplo, el m etodo de desplegado) ayuda
enormemente a efectuar conjeturas razonables acerca de la soluci on.
Supongamos que deseamos acotar superiormente el valor de esta recurrencia:
T(n) =
{
1, si n = 1;
2T(n/2) + n, si n > 1.
Una conjetura es que T(n) O(n lg n), pues las divisiones del par ametro de la recu-
rrencia por una constante suelen conducir a costes logartmicos (cuya base es el de-
nominador). Si ahora demostramos por inducci on que T(n) cn lg n, tendremos que
T(n) O(n lg n).
La base de inducci on presenta una dicultad inesperada: no es cierto 1 c lg 1, pues
lg 1 = 0. Como estamos interesados en cotas asint oticas, podemos superar esta dicultad
cambiando de base de inducci on. Tomemos como base de inducci on el caso n = 2. Como
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 405
T(2) = 2T(1) + 2 = 4 y 4 c 2 lg2 para c 2, queda demostrada para n = 2. Fijemos,
pues, el valor de c a 2.
Ojo con el caso n = 3!: la ecuaci on recursiva relaciona T(3) con T(1), y no hemos
podido demostrar la validez del enunciado para n = 1. Debemos considerar, pues, que
n = 3 forma parte, tambi en, de la base de inducci on. Como T(3) = 2T(1) + 2 = 4, el
valor de c = 2 hace que se satisfaga T(3) = 4 2 3 lg3 = 9.50178.
Hemos de demostrar ahora que, para c = 2, se observa T(n) cn lg n si es cierto que
T(n
) cn
lg(n
) para todo n
Tambi en es frecuente usar la letra w, por weighth (que signica peso) para
la funci on de ponderaci on.
B.15.4. Caminos
Un camino en un grafo G = (V, E) es una secuencia de v ertices (v
1
, v
2
, . . . , v
n
) tal que
todo par de v ertices consecutivos est a unido por una arista de E, es decir, (v
i
, v
i+1
) (o
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 411
Calvi` a
Manacor
Pollen ca
Capdepera
Art` a
Andratx
Campos del Port
S oller
Palma de Mallorca
Llucmajor
Marratx
Inca
Santany
Alc udia
27
25
17
8
36
14
13
56
54
30
14
14
20
12
10
25
14
Figura B.13: Grafo ponderado que
modela un mapa de carreteras entre
algunas de las principales ciudades
de la isla de Mallorca.
{v
i
, v
i+1
}, si el grafo es no dirigido) pertenece a E para todo i entre 1 y n 1. El v ertice
v
1
es el v ertice de partida y v
n
es el v ertice de llegada. La longitud o talla de un camino
es el n umero de aristas que lo componen. El camino (v
1
, v
2
, . . . , v
n
) tiene longitud n 1.
Un caso particular de camino es el formado por un unico v ertice y tiene longitud 0.
En la gura B.14 se muestra un digrafo y algunas secuencias de v ertices que constitu-
yen caminos v alidos.
0
1
2
3
0
1
2
3
0
1
2
3
0
1
2
3
0
1
2
3
0
1
2
3
0
1
2
3
(0, 2, 3) (0, 1, 0, 2) (2, 3, 0, 1, 1)
Figura B.14: A la izquierda se muestra un
grafo y, a su derecha, tres caminos (en trazo
gris).
Denotaremos el conjunto de todos los caminos de un grafo entre un par de v ertices
s y t con P
G
(s, t):
P
G
(s, t) = {(v
1
, v
2
, . . . , v
n
) v
1
= s; v
n
= t; (v
i
, v
i+1
) E, 1 i < n}.
Cuando G se deduzca del contexto, usaremos la notaci on P(s, t).
Un camino trivial es un camino formado por un solo v ertice (y, por tanto, ninguna
arista). Un camino v
1
v
2
. . . v
n
es un camino cerrado o ciclo si v
1
= v
n
. En el grafo de
ejemplo de la gura B.14, los caminos (0, 1, 0), (0, 2, 3, 0) o (0, 2, 3, 0, 1, 0, 1, 0) son caminos
cerrados. Un camino contiene un ciclo si un segmento suyo es un ciclo. Una pista es un
camino que no repite aristas. Los tres caminos representados en la gura B.14 son pistas.
Un camino simple es una pista que no contiene v ertices repetidos. En la misma gura,
el primer camino es simple, pero no lo son los otros dos. Un ciclo simple es un camino
cerrado, simple (excepto por el primer y ultimo v ertice) y no trivial.
412 Apuntes de Algoritmia 28 de septiembre de 2009
Un camino que no es simple contiene al menos un bucle. Los dos ultimos caminos de
la gura tienen al menos un ciclo: el camino (0, 1, 0, 2) tiene el bucle (0, 1, 0) y el camino
(2, 3, 0, 1, 1) tiene el bucle (1, 1).
Dado que un camino est a compuesto por una sucesi on de aristas, en algunos sistemas
modelados con grafos ponderados tiene inter es el concepto de distancia o peso de un
camino. En principio, denimos la distancia o peso de un camino en un grafo G = (V, E)
ponderado por d : E R as:
1
D(v
1
, v
2
, . . . v
n
) =
1i<n
d(v
i
, v
i+1
).
En el grafo ponderado del mapa de carreteras de la isla de Mallorca de la gura B.13,
el camino
(Marratx, Inca, Manacor, Art` a)
tiene longitud 3 y una distancia (o peso) de 54 kil ometros.
La forma en que componemos las distancias asociadas a cada una de las aristas para
proporcionar la distancia de un camino no tiene por qu e ser la suma. Es posible denir
el peso de un camino como, por ejemplo, el producto de los pesos de sus aristas:
D(v
1
, v
2
, . . . v
n
) =
11<n
d(v
i
, v
i+1
).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
212 No siempre hemos de sumar pesos para obtener la distancia de un camino. Esta gura
ilustra un modelo de Markov para la predicci on del tiempo que har a cada da.
sol
nubes
lluvia
0.8
0.15
0.05
0.2
0.5
0.3
0.1
0.3
0.6
La arista (sol, lluvia) tiene peso 0.05, valor que interpretamos como la probabilidad de pasar de
un da soleado a uno lluvioso. Cada secuencia de v ertices es un camino al que asociamos una
probabilidad que no se calcula sumando las probabilidades (los pesos) de cada una de sus aristas,
sino multiplic andolos.
Supongamos que hoy hace sol, qu e es m as probable, la secuencia (sol, nubes, nubes, lluvia,
nubes) o la secuencia (sol, sol, nubes, sol, sol,sol)?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
No debe confundirse la longitud o talla de un camino con su distancia o peso. Longitud y distancia se
usan en este texto con sentidos diferentes: la longitud es el n umero de aristas de un camino y la distancia es
la suma de las distancias individuales asociadas a cada arista del camino.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 413
Un camino que visita todos los v ertices del grafo exactamente una vez es un camino
hamiltoniano. Un camino que visita todas las aristas de un grafo exactamente una vez
es un camino euleriano. Un ciclo hamiltoniano o euleriano es un camino hamiltoniano
o euleriano, respectivamente, cuyos v ertices primero y ultimo coinciden.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
213 La ciudad de K onigsberg tiene dos islas donde el ro Pregel se bifurca. Las islas y las dos
orillas del ro se interconectan con puentes tal y como muestra este grabado y su representaci on
esquem atica:
Modela el plano esquem atico como un grafo. Hay alguna forma de cruzar los siete puentes sin
cruzar ninguno m as de una vez?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
= (V
, E
V y E
E. Dado
un grafo G = (V, E) y un subconjunto V
es G
=
(V
, {(u, v) E u, v V
= (V
, E
E, que es
G
= ({u v : (u, v) E
)} {v u : (u, v) E
Arboles
Un arbol es un grafo no dirigido, que no contiene ciclos simples y en el que todo v ertice
es alcanzable desde cualquier otro. La gura B.21 muestra un arbol.
Figura B.21: Un arbol.
0 1 2
3 4 5
Podemos denir un grafo no dirigido G
= (V, E
como el conjunto
{{u, v}(u, v) E (v, u) E}. Decimos que G
(x
1
x
2
)
2
+ (y
1
y
2
)
2
.
0 1 2 3 4 5 6
0
1
2
3
4
5
6
Figura B.25: Un grafo eucldeo. Cada v ertice se muestra como un punto en el plano coor-
denado. El peso de cada arista es la distancia entre los puntos que une.
. Con
+
se denota el conjunto de todas las cadenas de talla 1 o superior,
es decir,
+
=
{}.
El operador de concatenaci on permite unir cadenas, respetando el orden de sus smbo-
los y de las cadenas, para formar otras nuevas. El operador se puede denotar con el ope-
rador injo u omitir. As, dado el alfabeto = {a, b}, la concatenaci on de las cadenas
ab aab puede denotarse, sencillamente, con abaab. Se suele adoptar el convenio de repre-
sentar los smbolos de alfabeto con las primera min usculas del alfabeto latino (a, b, c. . . ),
y las cadenas con las ultimas (u, v, w, x, y, z).
N otese que la cadena es el elemento neutro para la concatenaci on, es decir, x =
x = x.
Un lenguaje formal (o simplemente lenguaje) es un conjunto de cadenas formadas
con smbolos de un alfabeto. N otese que, dado un alfabeto, ,
+
y
son lenguajes.
Podemos extender la operaci on de concatenaci on a los lenguajes. Dados dos lenguajes
L
1
y L
2
, el lenguaje denido por su concatenaci on, L
1
L
2
, es el conjunto {x y x
L
1
, y L
2
}.
B.16.1. Gram aticas
Una gram atica generativa (o simplemente gram atica) es una cuadrupla G = (, N, S, P),
donde
es un alfabeto cuyos elementos reciben el nombre de smbolos terminales.
N es un alfabeto cuyos elementos se denominan smbolos no terminales. El con-
junto N no tiene elementos en com un con . Los no terminales suelen represen-
tarse, por convenio, con las primeras letras may usculas del alfabeto latino (A, B,
C. . . ).
S es un smbolo de N al que denominamos smbolo inicial
P es un subconjunto de pares de la forma ( N)
N ( N)
( N)
. Sus
elementos reciben el nombre de reglas o producciones. Las cadenas de ( N)
suelen denotarse, por convenio, con las primeras letras min usculas del alfabeto
griego (, , . . . ). Una producci on de la forma (A, ) suele representarse as:
A . El t ermino A es la parte izquierda y es la parte derecha.
Sea una producci on de P. El smbolo denota la relaci on binaria de de-
rivaci on. Con denotamos que la cadena es derivable directamente de
por aplicaci on de la producci on . Decimos que deriva , y lo denotamos con
n
= y
1
2
,
2
3
, . . . ,
n1
n
. La secuencia
1
,
2
, . . . ,
n
es una derivaci on
de en .
El lenguaje generado por una gram atica G es L(G) = {x
x}, es decir, el
conjunto de cadenas de terminales que deriva de S.
Las gram aticas se pueden clasicar, seg un la jerarqua de Chomsky, en cuatro familias
en funci on de la forma de sus producciones:
Tipo 0 o sin restricciones. Las producciones pueden tener cualquier forma.
Tipo 1 o sensibles al contexto. Las producciones son de la forma A .
Tipo 2 o incontextuales. Las producciones son de la forma A .
Tipo 3 o regulares. Las producciones son de la forma A aB o A a.
Toda gram atica de tipo i es una gram atica de tipo i 1. Los lenguajes generables
con gram aticas de tipo i son un subconjunto propio de las gram aticas generables con
gram aticas de tipo i 1.
Las gram aticas incontextuales y regulares son de gran inter es pr actico. Hay formas
normalizadas para las gram aticas incontextuales, es decir, dada una gram atica incontex-
tual G, es posible encontrar otra G
, saber si x L(G).
Este es el problema de la pertenencia. El problema del an alisis propone encontrar una
derivaci on de la cadena en la gram atica si esta existe. Resolver el problema del an alisis
es resolver el problema de la pertenencia.
Interesa, en particular, el problema del an alisis para gram aticas incontextuales (y re-
gulares, que son un caso particular de estas).
Conviene introducir un concepto adicional: el de arbol de an alisis. Un arbol de an ali-
sis de una cadena x para una gram atica incontextual es un arbol etiquetado tal que
Las hojas est an etiquetadas con smbolos de {} y, tomadas de izquierda a
derecha, forman la cadena x.
Los nodos internos se etiquetan con no terminales. Si un nodo interno est a etique-
tado con el no terminal A y sus hijos est an etiquetados con smbolos cuya conca-
tenaci on (de izquierda a derecha) es , entonces A es una producci on de la
gram atica.
La raz est a etiquetada con el smbolo S.
422 Apuntes de Algoritmia 28 de septiembre de 2009
Toda derivaci on de S en una cadena x
deseamos saber
si x L(G) y, de ser as, conocer un arbol de an alisis que la genere. N otese que decimos
un arbol de an alisis y no el arbol de an alisis. Una gram atica incontextual es ambigua
si una misma cadena puede derivarse de S con dos o m as arboles de an alisis diferentes.
Aut omata Finito de Estados
Un aut omata nito no determinista (AFND) es una quintupla A = (, Q, q
0
, E, F) donde
es un alfabeto de smbolo terminales;
Q es un conjunto nito de estados;
q
0
es un elemento de Q y se denomina estado inicial;
E : Q Q es un conjunto de transiciones entre estados;
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 423
y F es un subconjunto de F que recibe el nombre de conjunto de estados nales.
Los AFND pueden representarse mediante diagramas que representan los estados
con v ertices de un grafo y el conjunto de transiciones entre estados mediante aristas
etiquetadas con smbolos de . El estado inicial se se nala con una echa incidente y los
estados nales con un doble crculo. La gura B.27 muestra el diagrama correspondiente
al aut omata A denido con la quintupla (, Q, q
0
, E, F), donde
= {a, b},
Q = {0, 1, 2, 3, 4, 5},
q
0
= 0,
E = {(0, a, 1), (0, b, 2), (1, a, 1), (1, a, 2), (1, b, 3), (2, a, 4), (3, a, 5), (4, b, 2), (4, a, 3),
(4, b, 5), (5, a, 5)},
F = {4, 5}.
0
1
2
3
4
5
a
b
a
a
b
a
a
b
a
b
a
Figura B.27: Representaci on de un aut omata nito no determinista.
Una cadena x