Vous êtes sur la page 1sur 216

Apuntes de Algoritmia

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.

La palabra algoritmo proviene, efectivamente, del vocablo latino algobaris-


mus, que a su vez procede del nombre de la ciudad de Kiva, en Uzbekist an.
Abu Jafar Mohammed ibn M us a Al-Khow arizm fue un matem atico persa del siglo
IX que escribi o tratados de aritm etica y algebra. Ibn M us a Al-Khow arizm signica
hijo de Musa, de la ciudad de Khow arizm, que es el nombre antiguo de Kiva.
Y en el problema del cambio con el menor n umero de monedas, una soluci on a la
instancia con Q = 9 y nuestro sistema monetario es: 2 monedas de 2 euros y un billete
de 5 euros. Al tratarse de un problema de optimizaci on, decimos que esa es la soluci on
optima para esta instancia del problema. La funci on objetivo proporciona el n umero de
piezas, que en el caso de la soluci on optima es 3. Ese valor recibe el nombre de valor
optimo para esa instancia del problema.
Los problemas que abordaremos en este texto suelen proponer la b usqueda de un
elemento (o el valor de una funci on aplicada a dicho elemento) en un determinado con-
junto de posibles soluciones (en el caso de los problemas de optimizaci on, dicho elemento
ofrece un valor m aximo o mnimo en dicho conjunto): el espacio de b usqueda. Una estra-
tegia de resoluci on sencilla podra consistir en, para cada instancia, recorrer el espacio de
8 Apuntes de Algoritmia 28 de septiembre de 2009
b usqueda completamente hasta encontrar la soluci on buscada. Pero esta aproximaci on es
inviable para la mayora de problemas interesantes: el tama no del espacio de b usqueda
suele ser enorme (o incluso innito).
En ocasiones, el espacio de b usqueda no presenta una gran talla, pero el problema
sigue resultando de difcil resoluci on. La raz on es que el espacio de b usqueda se dene
imponiendo una serie de restricciones a un conjunto de elementos. En los problemas de
optimizaci on, por ejemplo, es frecuente que el conjunto X se dena imponiendo restric-
ciones a un conjunto X

tal que X X

. El conjunto X

recibe el nombre de conjunto


de soluciones (aunque puede incluir entre sus elementos algunos que no son solucio-
nes aceptables del problema) y X se denomina conjunto de soluciones factibles, esto es,
soluciones que satisfacen las restricciones impuestas.
1.2.2. Algoritmo
Suponemos que el lector ya sabe programar y conoce al menos dos o tres lenguajes de
programaci on en los que es capaz de expresar diferentes programas con los que resolver
(instancias de) un mismo problema. Aunque estos programas sean distintos compartir an
los mismos principios de funcionamiento, las mismas secuencias de acciones, es decir,
el mismo algoritmo. Podemos decir, informalmente, que el algoritmo es el m etodo que,
independientemente del lenguaje de programaci on, permite resolver un problema y que
un programa es una codicaci on del algoritmo en un lenguaje de programaci on concreto.
Los valores de los par ametros que denen una instancia concreta constituyen la entrada
del algoritmo (o del programa), y la soluci on respectiva, tambi en codicada adecuada-
mente, es su salida.
Demos una denici on m as formal. Un algoritmo es una secuencia nita de reglas
para la resoluci on de cualquier instancia de un problema determinado que, al interpre-
tarse en el contexto de cada una de las instancias, proporciona una secuencia nita de
operaciones que conducen a la soluci on. Todo algoritmo presenta estas caractersticas:
Es nito. Siempre naliza tras ejecutar un n umero nito de acciones.
Est a bien denido. Cada una de sus acciones est a denida con precisi on y sin ambi-
g uedad.
Produce una o m as salidas a partir de cero o m as entradas. Todo algoritmo tiene cero o
m as entradas, es decir, datos conocidos antes de empezar su ejecuci on. El resul-
tado o salida del algoritmo consiste en uno o m as datos (la soluci on).
Es efectivo. Cada operaci on del algoritmo es lo sucientemente elemental como para
que podamos ejecutarla de modo exacto en un tiempo nito.
No todo procedimiento es, pues, un algoritmo. Una receta de cocina, por ejemplo, es
un m etodo para elaborar un plato, pero normalmente incumple la condici on de estar bien
denido: los productos, cantidades y tiempos de cocci on pueden darse sin demasiada
precisi on o incluso con cierta ambig uedad.
28 de septiembre de 2009 Captulo 1. Introducci on 9
Hemos de indicar, nalmente, que tampoco se considera algoritmo a todo progra-
ma de ordenador: un programa que no naliza nunca no es un algoritmo, sino lo que
denominamos un procedimiento computacional.
1.3. El ciclo de desarrollo
Como hemos dicho, el resultado de codicar o implementar un algoritmo en un lenguaje
de programaci on determinado es un programa. Podemos considerar, grosso modo, que el
proceso de dise no de una soluci on algortmica se divide en tres etapas: especicaci on ri-
gurosa del problema, dise no de un algoritmo y codicaci on del mismo en un programa.
Pero podemos renar este proceso y considerar algunas etapas m as:
1. Especicaci on. Se proporciona un enunciado formal del problema que indica con
precisi on qu e salida se espera obtener para cada entrada.
2. Dise no. Aplicaci on de t ecnicas de construcci on de algoritmos.
3. Vericaci on. Demostraci on de que el algoritmo resuelve realmente el problema es-
pecicado, es decir, proporciona determinada salida a partir de su entrada.
4. An alisis. Estimaci on de recursos necesarios para la resoluci on de instancias del
problema. Nos importan dos recursos: tiempo y espacio. Un algoritmo es m as e-
ciente cuanto menor tiempo de ejecuci on precise y cuanta menor cantidad de me-
moria ocupen los datos temporales que manipula para alcanzar la soluci on. El
an alisis es independiente del computador y del lenguaje de programaci on.
5. Codicaci on. Obtenci on de un programa correcto a partir del algoritmo. Imple-
mentar el algoritmo es codicarlo en un lenguaje de programaci on determinado.
6. Evaluaci on. Obtenci on de un perl del programa a partir de la medici on del con-
sumo real de recursos.
El ciclo de desarrollo no es un proceso lineal en el que se pasa de una etapa a la
siguiente. Frecuentemente se itera sobre una o m as etapas hasta llegar a un programa
satisfactorio, como muestra la gura 1.1.
Por ejemplo, al vericar la correcci on puede que comprobemos que el algoritmo s olo
es v alido para un subconjunto de las entradas o que proporciona una respuesta aproxi-
mada. Si no podemos contentarnos con ello, hemos de modicar el algoritmo (efectuar
renamientos) o redise narlo. Lo mismo ocurre si detectamos un consumo de recursos
excesivo en el an alisis. La codicaci on del algoritmo en un lenguaje de programaci on
conduce, frecuentemente, a nuevos renamientos, pues la idiosincrasia de cada lenguaje
de programaci on suele obligar a elegir entre varias formas posibles de expresar aspec-
tos concretos del algoritmo para obtener una implementaci on eciente. La ultima etapa
10 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 1.1: Ciclo de desarrollo de un programa. El proceso puede iterar sobre
una o m as etapas.
Especicaci on
Dise no del algoritmo
Vericaci on
An alisis
Codicaci on
Evaluaci on
permite contrastar la eciencia de nuestra implementaci on y, mediante renamientos su-
cesivos, obtener reducciones signicativas del tiempo de ejecuci on o del consumo de
memoria.
De este ciclo prestaremos especial atenci on a las cuatro primeras etapas, que consti-
tuyen el campo de inter es fundamental de la algoritmia.
1.3.1. Especicaci on
Para abordar un problema hemos de ser capaces de expresar adecuadamente todo lo re-
lacionado con el. No es estrictamente necesario recurrir a la notaci on matem atica, pero
resulta de gran de ayuda: es una notaci on clara, concisa y sin ambig uedades. En ocasio-
nes, adem as, puede sugerir las estructuras de datos con las que podemos representar los
datos del problema y la soluci on.
Consideremos el problema de la entrega de una cantidad de dinero con el menor
n umero de monedas y billetes. Un sistema monetario se puede describir con una tupla (o
vector) de tantos elementos como tipos distinto de moneda o billete haya. Denominemos
N a dicha cantidad. La tupla (v
1
, v
2
, . . . , v
N
), donde cada v
i
Nes un n umero entero que
indica el valor de cada una de las monedas o billetes, describe un sistema monetario. Una
instancia del problema (entrada del algoritmo) consistir a en una tupla como la descrita y
un entero positivo Q, que es la cantidad de dinero que queremos entregar.
Una soluci on de una instancia del problema puede expresarse con una tupla de ta-
ma no N, digamos (a
1
, a
2
, . . . , a
N
), donde a
i
es el n umero de monedas (o billetes) de valor
v
i
que entregamos. N otese que no toda N-tupla expresa una soluci on factible: debe ob-
servarse la restricci on de que la suma de todos sus elementos sea exactamente igual a Q.
Y la soluci on que buscamos es aquella cuya suma de valores es mnima.
La especicaci on del problema puede expresarse, pues, as:
Entrada: Q Ny V = (v
1
, v
2
, . . . , v
N
) N
N
, donde v
i
= v
j
para todo i = j.
28 de septiembre de 2009 Captulo 1. Introducci on 11
Salida: ( a
1
, a
2
, . . . , a
N
) = arg opt
(a
1
,a
2
,...,a
N
)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.

Hay muchos libros que tratan sobre resoluci on de problemas. Adem as de


How to solve it, de G. P olya, podemos recomendar The art and craft of
problem solving, de P. Zeitz (que constituye, adem as, una buena sntesis de mu-
chas de las nociones matem aticas que aparecen en este texto), How to Solve It:
Modern Heuristics, de Z. Michalewicz y B. B. Fogel, o Problem-Solving Strate-
gies, de A. Engel (aunque presenta un claro sesgo matem atico por estar orientado
a la preparaci on de estudiantes para las olimpiadas matem aticas internacionales).
Para cada uno de estos pasos, P olya propone una serie de sugerencias que pueden
ser de ayuda cuando nos encontramos atascados. As, por ejemplo, para el primero de
los puntos (Comprende el problema) sugiere una lectura cuidadosa del enunciado y
un esfuerzo por asegurase de que se entiende con claridad y de que no se escapa deta-
lle alguno. Qu e datos de entrada denen una instancia del problema? Qu e salida ha
de proporcionar el algoritmo? Qu e restricciones debe satisfacer la salida? Suele ser de
ayuda dibujar un diagrama o esquema e identicar en el los datos de entrada y la salida. Es
necesario, adem as, introducir una notaci on apropiada que nos permita expresar los diferen-
tes elementos del problema sin ambig uedades, con claridad y concisi on.
El segundo de los puntos (Traza un plan) propone identicar etapas que permitan
avanzar hacia la resoluci on del problema. En ocasiones, dise naremos algoritmos siguien-
do un proceso claramente dividido en etapas. En programaci on din amica, por ejemplo,
actuaremos siguiendo estos pasos: 1) modelar la soluci on con una ecuaci on recursiva; 2)
representar la ecuaci on con un grafo de dependencias; 3) encontrar un orden topol ogico
en el grafo; 4) resolver la ecuaci on recursiva con un recorrido de los nodos del grafo en
dicho orden.
En la tercera etapa (Ejecuta el plan) debe vericarse que cada paso del plan hace lo
correcto. En la cuarta y ultima etapa (Reexiona sobre el trabajo realizado) se propone
una revisi on autocrtica para detectar inconsistencias, ambig uedades e incorrecciones.
28 de septiembre de 2009 Captulo 1. Introducci on 13
El punto en el que es m as frecuente quedar bloqueado es el segundo. Por d onde
empezar a abordar la resoluci on del problema? P olya y otros autores proponen una serie
de t ecnicas que pueden resultar de ayuda:
Fija objetivos parciales: divide el problema en subproblemas. Puede que los subproblemas
sean m as sencillos y la combinaci on de sus soluciones conduzca a la soluci on del
problema original.
Intenta reconocer algo familiar. Busca relaciones entre el problema propuesto y lo que
ya sabes. Hay problemas parecidos que ya sabes resolver?
Busca alg un patr on en el problema. Si alg un elemento se repite o presenta una regula-
ridad, puede que sea la clave para la resoluci on del problema.
Usa analogas. Hay alg un problema similar pero de m as sencilla resoluci on? Puede
que te d e pistas. Si el problema propuesto es general, considera casos m as concre-
tos. Cuantos m as problemas resuelvas, m as f acil te resultar a encontrar analogas.
Introduce algo extra. Puede que introducir un nuevo elemento, como informaci on
extra que puede derivarse de los datos de entrada, facilite encontrar una relaci on
m as clara entre la entrada y la salida.
Separa en casos. Puede que el problema se pueda separar en una serie de casos y
que resolver el problema pase por clasicarlo en el caso adecuado y aplicar una
estrategia particular.
Trabaja hacia atr as (o resuelve el pen ultimo paso). A veces conviene imaginar que el
problema ha sido resuelto y, a partir de la salida que debera obtenerse, ir hacia
atr as, hacia los datos de entrada. Sup on que el problema ya est a casi resuelto y que
s olo falta por efectuar el ultimo paso, la ultima decisi on. Puede que encuentres una
clave que conduzca a un m etodo de resoluci on recursivo.
Rompe las reglas. Puede que si te saltas una regla resuelvas una variante m as sencilla
del problema y que esta soluci on te inspire al tratar de resolver el problema general.
Fjate en los casos extremos. Es posible que la soluci on resulte trivial en casos extremos
(cero, innito, lista vaca, grafo completo. . . ) y que este hecho ayude al dise nar un
m etodo resolutivo para casos normales.
Ens uciate las manos. Haz algo. Resuelve instancias concretas. Experimenta.
Es evidente que seguir el proceso propuesto por P olya no es garanta de que se vaya a
dar con una soluci on, pero puede ser de ayuda para organizar el ataque a un problema.
En cualquier caso el lector debe ser plenamente consciente de que s olo se aprende a resolver
problemas resolviendo problemas. En este curso resolveremos unos cuantos.
14 Apuntes de Algoritmia 28 de septiembre de 2009

No es sensato reinventar la rueda constantemente. Muchos de los problemas


que estudiaremos o que pueden plantearse durante el ejercicio profesional ya
han sido abordados y resueltos. Saber buscar soluciones algortmicas en la literatura
es una habilidad fundamental para todo programador y que puede ahorrar muchos
esfuerzos innecesarios.
1.3.3. Vericaci on
Una vez se ha dise nado un algoritmo debe vericarse que este resuelve efectivamente
el problema para toda posible instancia. Esta demostraci on formal puede resultar extre-
madamente ad hoc, pero es frecuente que la inducci on resulte exitosa, ya que muchos
algoritmos son recursivos o se basan en la iteraci on para recorrer ordenadamente una
serie de valores.
En ciertos casos el algoritmo ser a una instanciaci on de un algoritmo gen erico para
el que ya existe una demostraci on de correcci on bajo la suposici on de que se observan
ciertas condiciones. Bastar a entonces con comprobar que nuestro modelado del problema
satisface los requisitos para que el algoritmo instanciado sea correcto.

Este ser a el caso de
los algoritmos que dise nemos a partir de esquemas algortmicos.
1.3.4. An alisis
Al analizar un algoritmo pretendemos estimar los recursos computacionales que este
consume. Hay dos recursos fundamentales: el tiempo de ejecuci on y la memoria necesa-
ria. Nos referimos a ellos con los t erminos tiempo y espacio.
Los datos que asignamos a los par ametros de una instancia, debidamente codica-
dos para ser interpretables por un algoritmo o programa, constituyen su entrada, y la
soluci on respectiva, tambi en codicada adecuadamente, es su salida. Podemos agrupar
las instancias en funci on de su talla, es decir, de la cantidad de memoria necesaria para
especicar la entrada. La talla de una instancia guarda cierta relaci on con la dicultad
esperada en la resoluci on de dicha instancia.
Cabe pensar en estimar el consumo de recursos en funci on de una implementaci on
particular del algoritmo: podemos medir el tiempo de ejecuci on y calcular el espacio de
memoria necesario para su ejecuci on. Pero resulta poco factible proporcionar datos so-
bre consumo de recursos de los programas que sean a la vez precisos y generalmente
v alidos, pues dependen de innidad de factores: ordenador sobre el que se ejecuta el
programa, compilador o int erprete utilizados, gesti on de la memoria en el sistema ope-
rativo, longitud de palabra de la memoria o espacio reservado para cada tipo de datos,
etc.
Un objetivo m as accesible es la estimaci on del consumo de recursos a partir del al-
goritmo si nos limitamos a presentar la tendencia asint otica del coste con la talla de la
instancia del problema, es decir, su evoluci on con los par ametros que determinan la di-
cultad de una instancia del problema. (En ocasiones esta tendencia asint otica no de-
pender a s olo de la talla de la instancia, sino tambi en de par ametros relacionados con
la propia salida.) En muchos casos, estas descripciones proporcionan informaci on su-
28 de septiembre de 2009 Captulo 1. Introducci on 15
ciente para comparar y elegir un algoritmo de entre los muchos que pueden solucionar
un problema. Elegir el mejor algoritmo, el m as eciente, puede resultar tremendamente
util y econ omico en el desempe no de cualquier actividad profesional relacionada con la
construcci on de software.
Veremos que algunos problemas se resisten al dise no de algoritmos ecientes y es-
tudiaremos muy brevemente una clasicaci on de problemas que atiende a su dicultad.
Saber que un problema es intrnsecamente difcil nos puede ahorrar muchos esfuerzos
in utiles si pasamos a proponernos objetivos realistas sobre el dise no de un algoritmo que
lo aborde. Cuando un problema se resista al dise no de un algoritmo eciente, puede que
nos contentemos con obtener una soluci on aproximada. Presentaremos tambi en, pues,
algunas estrategias para el dise no de soluciones aproximadas.
1.3.5. Codicaci on
Al codicar un algoritmo lo expresamos en un lenguaje de programaci on concreto. Exis-
ten lenguajes de programaci on imperativos, declarativos, orientados a objetos, funciona-
les. . . Los lenguajes de programaci on modernos suelen seguir una aproximaci on mixta
e integran diferentes paradigmas de programaci on. Nosotros trabajaremos con un len-
guaje de programaci on orientado a objetos, Python, aunque permite expresar algoritmos
como funciones (no m etodos de una clase) y permite recurrir a recursos propios de la
programaci on funcional.
Naturalmente, la elecci on de un lenguaje de programaci on tiene un efecto inmediato
en la velocidad de ejecuci on o consumo de memoria de la implementaci on de un algorit-
mo. Usualmente, los lenguajes muy expresivos se ven lastrados por una menor velocidad
de ejecuci on (y mayor consumo de memoria) que los m as pr oximos a la m aquina sobre
la que se ejecutan los programas. Pero hay un factor a tener muy en cuenta: la mayor o
menor competencia del programador con el lenguaje elegido.

La expresi on de un algoritmo en un lenguaje natural, como el castellano,


se presta a la imprecisi on y la ambig uedad. Los lenguajes de programaci on
se dise nan precisamente con objeto de evitar ambos problemas. (Bueno, no todos:
algunos de los denominados lenguajes de programaci on esot ericos se dise nan
rompiendo reglas que se asumen en el dise no de un buen lenguaje.)
El proceso de codicaci on es (muy) sensible a la comisi on de fallos. Hoy es frecuente
que la codicaci on incluya el dise no y ejecuci on de pruebas unitarias. Se trata de cons-
truir programas que ejecuten nuestra implementaci on del algoritmo con entradas para
las que conocemos la salida. Las pruebas unitarias tratan de comprobar el comporta-
miento de la implementaci on frente a entradas convencionales y entradas con valores
extremos y/o inapropiadas. Las pruebas unitarias son especialmente utiles cuando los
algoritmos implementados deben integrarse en un programa complejo.
16 Apuntes de Algoritmia 28 de septiembre de 2009

Las pruebas unitarias forman parte de la cultura moderna de desarrollo de


software. Para cualquiera de los lenguajes de programaci on de uso corriente
existen varias libreras para implementar pruebas unitarias y entornos que automa-
tizan su uso. Si se trabaja en el desarrollo en equipo de sistemas complejos con
integraci on continua, las pruebas unitarias son imprescindibles para asegurar que
los cambios efectuados en el c odigo existente o en el de creaci on reciente no afec-
tan a los programas o libreras que hacen uso de el directa o indirectamente.
1.3.6. Evaluaci on
Una vez el algoritmo ha sido implementado, puede que convenga estudiar el perl de eje-
cuci on del programa. Podemos medir tiempos directamente o hacer uso de herramientas
especializadas del entorno de programaci on. Los perladores (del ingl es prolers)
permiten averiguar el tiempo de ejecuci on o el n umero de veces que se ejecuta una deter-
minada funci on o lnea de programa. La obtenci on de un perl de ejecuci on es determi-
nante a la hora de decidir qu e fragmentos de un programa merecen un esfuerzo adicional
de optimizaci on, es decir, de mejora del c odigo.
1.4. Dise no de algoritmos basado en esquemas
algortmicos
En buena parte del texto seguiremos una aproximaci on al dise no de algoritmo conocida
como dise no basado en esquemas. Existen familias de problemas que pueden abordar-
se con estrategias similares y t ecnicas o principios de dise no generales que encuentran
aplicaci on en numerosos problemas. Centraremos el foco de atenci on principal en ciertas
estrategias de dise no de algoritmos (divide y vencer as, b usqueda con vuelta atr as, estra-
tegia voraz, programaci on din amica y ramicaci on y poda). Son estrategias basadas en
abstracciones muy potentes que permiten agrupar en una misma familia de problemas
lo que, en principio, son o parecen ser problemas muy diferentes.
Una vez se conocen estas estrategias se puede seguir este proceso:
Clasicaci on del problema como miembro de una familia. Por regla general, cada fa-
milia de problemas se caracteriza por las propiedades de su conjunto de datos de
entrada y/o de la soluci on. Este paso consiste, pues, en detectar la familia del pro-
blema y demostrar el cumplimiento de dichas propiedades. En ocasiones conviene
abstraer el problema para dotarlo de mayor generalidad y facilitar as su clasica-
ci on en un cat alogo de familias de problemas.
Selecci on de un esquema algortmico. Un esquema algortmico es un algoritmo abs-
tracto que especica una estrategia resolutiva para problemas de una familia de-
terminada. El esquema suele especicarse como una plantilla con funciones no de-
nidas sobre las que se debe garantizar el cumplimiento de ciertas propiedades.
28 de septiembre de 2009 Captulo 1. Introducci on 17
Instanciaci on del esquema en un algoritmo concreto. El proceso de instanciaci on con-
siste en interpretar el esquema algortmico, es decir, denir aquellos elementos de
la plantilla de los que s olo se nos indicaba qu e propiedades deben cumplir. El re-
sultado puede ser un algoritmo en bruto mejorable mediante renamientos su-
cesivos.
Vericaci on. Hemos de demostrar que el algoritmo desarrollado responde efectivamen-
te el problema propuesto. En el caso del dise no basado en esquemas, esta demos-
traci on puede basarse en un formalizaci on adecuada y la comprobaci on de que se
cumplen determinadas restricciones en los datos de entrada.
An alisis y renamientos sucesivos del algoritmo. Los esquemas pueden proporcionar
algoritmos que son inecientes por no explotar a fondo peculiaridades de un pro-
blema concreto. Una an alisis de coste puede conducir a detectar problemas de e-
ciencia y apuntar ideas que permitan un renamiento (mejora) del algoritmo.
Adem as de seguir este procedimiento de dise no basado en el uso de esquemas al-
gortmicos, estudiaremos algunos de los algoritmos m as ecientes que se conocen para
resolver ciertos problemas. Estos algoritmos son interesantes por su aplicaci on en innu-
merables campos pr acticos, pero tambi en como ilustraci on de t ecnicas particulares utili-
zables en el dise no de soluciones algortmicas para otros problemas.
Captulo 2
AN

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.

Es tan importante mejorar la eciencia cuando los computadores son m as


y m as r apidos? La ley de Moore conjetura que la capacidad de integraci on
de circuitos se duplica cada 18 meses, lo que aumenta su potencia. Cabe pensar
que un algoritmo que hoy es inutilizable por excesivamente lento alg un da ser a util
gracias a los avances tecnol ogicos en el dise no y construcci on de los ordenadores.
Sin embargo, las limitaciones fsicas impiden aumentar la potencia indenidamente y
hay problemas para los que los algoritmos m as ecientes conocidos tienen un coste
computacional tal que siempre ser an inecientes para instancias de talla moderada.
Un consejo antes de seguir: el lector har a bien en repasar algunos de los conceptos
matem aticos que se apuntan en el ap endice B. En particular, los que se indican en los
apartados B.7, B.8, B.9, B.10, B.13 y B.14. Tambi en es muy recomendable repasar los co-
nocimientos de Python, lenguaje de programaci on del que se ofrece un resumen muy
conciso en el ap endice A.
2.1. Medici on de tiempo de ejecuci on
Vamos a plantearnos, como primer objetivo, aprender a medir tiempos de ejecuci on de
programas. Consideremos un problema concreto: la b usqueda de un valor en un vector
ordenado de enteros. Se trata de una variante del problema de la b usqueda de una pala-
bra en un diccionario. Nuestro problema se enuncia as: dado un vector con n n umeros
enteros positivos, diferentes y ordenados de menor a mayor, encu entrese la posici on de
un elemento de valor x y, si no lo hay, indquese. Estudiaremos tres algoritmos diferen-
tes:
1. Una t ecnica de b usqueda secuencial que resulta evidentemente ineciente: recorrer
el vector completamente y, al encontrar el valor x, memorizar su ndice para de-
volverlo al nal del recorrido del vector. Denominaremos a esta t ecnica b usqueda
secuencial naf.
2. Un renamiento de la t ecnica anterior: recorrer el vector hasta encontrar el valor
x, momento en el que se devuelve su ndice, o hasta llegar a un valor que permi-
ta concluir que el valor buscado no est a presente, momento en el que detenemos
la b usqueda y avisamos de este hecho. Esta t ecnica se denominar a b usqueda se-
cuencial, sin m as.
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 21
3. Yuna t ecnica iterativa muy diferente que considera en cada instante la b usqueda en
un subvector del vector original (y que inicialmente es el propio vector original).
Con cada iteraci on se compara x con el elemento central de un subvector y
si coinciden, naliza la b usqueda y devuelve su ndice;
si x es menor, aplica el mismo procedimiento a la mitad izquierda;
y si x es mayor, a la mitad derecha.
Si en alg un momento se pretende efectuar la b usqueda en un subvector vaco, el
m etodo naliza concluyendo que el elemento buscado no se encuentra en el vector.
Llamaremos a este m etodo b usqueda binaria.

Evidentemente, el primer m etodo es poco razonable: la b usqueda debera


nalizar tan pronto encontramos el elemento buscado. No obstante, se trata
de un error frecuente en programadores primerizos.
Implementemos los tres algoritmos con el lenguaje de programaci on Python. Cons-
truiremos tres clases diferentes que implementan una misma interfaz. Python no ofre-
ce interfaces, pero podemos obtener la misma funcionalidad con clases que presentan
m etodos abstractos. Toda clase que hereda de otra con un m etodo abstracto s olo puede
instanciar objetos si ofrece una implementaci on de dicho m etodo. La clase ISearcher, que
denimos a continuaci on, obliga a denir un m etodo index of que recibe una secuencia y
un valor y que devuelve un entero (el ndice de la secuencia en la que aparece el valor) o
None si el valor no aparece en la secuencia:
algoritmia/problems/searching.py
from abc import ABCMeta, abstractmethod
...
class ISearcher(metaclass=ABCMeta):
@abstractmethod
def index of (self , a: "sequence<T>", x: "T") -> "int or None": pass

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.

La funci on clock presenta un comportamiento diferente en Unix y en Microsoft


Windows. En el primero devuelve un valor que depende de la funci on C del
mismo nombre y suele ser una medida de tiempo invertido por la CPU en el proceso
sobre el que efectuamos la medici on. En Microsoft Windows devuelve el tiempo real
transcurrido desde el inicio del proceso, pero dicho tiempo puede verse afectado por
la dedicaci on de la CPU a otras actividades. La funci on en Unix tiene un inconve-
niente: el reloj suele presentar una resoluci on baja (del orden de las cent esimas de
segundo).
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 23
Si efectuamos dos medidas de tiempo, una antes de efectuar el c alculo y otra inme-
diatamente despu es, el tiempo transcurrido ser a la diferencia entre las dos medidas:
from time import clock
t1 = clock()
acciones
t2 = clock()
t = t2 - t1
Las funciones dise nadas necesitan de vectores en los que buscar y valores buscados
concretos para su ejecuci on. Asumamos, por el momento, un vector de tama no jo (por
ejemplo, n = 10) cuyos elementos son enteros no repetidos en un rango determinado (por
ejemplo, en el rango [0..5n 1]) y que buscamos un elemento cualquiera de los presentes
en el vector:
experiments/searching/timing1.py
from algoritmia.searching import NaiveSequentialSearcher
from time import clock
from random import seed, randrange, sample
# Generaci on de un vector aleatorio.
seed(0) # Semilla del generador de n umeros aleatorios.
n = 100 # Talla del vector.
a = sorted(sample(range(5*n), n)) # Vector ordenado de n valores aleatorios sin
# repetici on en el rango [0..5n 1].
x = a[randrange(n)] # Selecci on de un elemento al azar.
searcher = NaiveSequentialSearcher()
# Ejecuci on con medici on de tiempo.
t1 = clock()
index = searcher.index of (a, x)
t2 = clock()
t = t2 - t1
print(Tiempo transcurrido: {:.8f} segundos.format(t))

El m odulo random ofrece funciones para la generaci on de n umeros alea-


torios. La semilla se inicializa con seed. La funci on sample selecciona una
muestra aleatoria sin reemplazamiento de una poblaci on descrita mediante una se-
cuencia o iterable; por ejemplo, sample([1,2,3],2) devuelve, al azar, una de es-
tas listas: [1,2], [2,1], [2,3], [3,2], [1,3] o [3,1]. La llamada randrange(m)
genera un n umero entero aleatorio en el rango [0..m1] con una distribuci on uni-
forme. La funci on sorted devuelve una lista ordenada de menor a mayor con los
mismos valores que contiene la secuencia que se proporciona como argumento.

Este es el resultado de su ejecuci on con el int erprete de Python 3.1 en un ordenador


con procesador Intel Core 2 Duo a 2.26 GHz, 4 Gb de memoria RAM y sistema operativo
Microsoft Windows Vista Business SP1:
24 Apuntes de Algoritmia 28 de septiembre de 2009
Tiempo transcurrido: 0.00001446 segundos
Si repetimos el experimento obtenemos un resultado diferente:
Tiempo transcurrido: 0.00001411 segundos
Y una nueva ejecuci on muestra un resultado tambi en diferente:
Tiempo transcurrido: 0.00001467 segundos
Ejecuci on repetida un n umero jo de veces
Podemos proporcionar el tiempo medio invertido en ejecutar la llamada si repetimos la
ejecuci on del programa un n umero suciente de veces:
experiments/searching/timing2.py
from algoritmia.searching import NaiveSequentialSearcher
from time import clock
from random import seed, randrange, sample
# Generaci on de un vector aleatorio.
seed(0)
n = 100
a = sorted(sample(range(5*n), n))
x = a[randrange(n)]
searcher = NaiveSequentialSearcher()
# Ejecuci on repetida con medici on de tiempo.
r = 100000
t1 = clock()
for i in range(r):
index = searcher.index of (a, x)
t2 = clock()
for i in range(r):
index = 0
t3 = clock()
t = ((t2 - t1) - (t3 - t2)) / r
print(Tiempo medio por ejecucion: {:.8f} segundos.format(t))
N otese que el c alculo del tiempo transcurrido se ha complicado un poco. El objetivo
del bucle entre los instantes t2 y t3 es poder descontar el tiempo que supone la ejecuci on
del bucle que repite el c alculo. He aqu el resultado:
Tiempo transcurrido: 0.00001009 segundos
C omo es posible? En promedio parece tardar 10.09 millon esimas de segundo, cuan-
do la medici on del tiempo de una ejecuci on individual no baja de las 14 millon esimas.
Hemos de tener en cuenta que la ejecuci on de un programa Python se efect ua sobre una
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 25
representaci on intermedia del c odigo y que esta representaci on intermedia es interpreta-
da. La primera ejecuci on de una rutina puede verse afectada por sobrecostes imputables
al proceso de interpretaci on. (Por otra parte, la funci on clock puede verse afectada por
cambios de contexto en un Microsoft Windows, pues mide tiempo de ejecuci on real del
sistema, no s olo el tiempo de CPU dedicado a nuestro proceso.)
Hemos ejecutado 100000 repeticiones de la b usqueda para obtener un promedio. Co-
mo el vector sobre el que buscamos es peque no, el tiempo medido est a en el orden de
las millon esimas de segundo y el experimento no supone una gran inversi on de tiempo.
Pero, y si dese aramos medir tiempos de ejecuci on para vectores mayores, con tiempos
de ejecuci on individuales diez, cien o mil veces mayores? El tiempo de ejecuci on del
experimento podra hacerlo impracticable.
Ejecuci on repetida hasta superar una cantidad de tiempo
Una t ecnica que permite solucionar este problema consiste en repetir la ejecuci on de la
rutina durante al menos una cantidad de tiempo preestablecida. Si la rutina efect ua el
c alculo muy r apidamente, tendr a que repetirse un gran n umero de veces para conse-
guir que transcurra el tiempo necesario. Si, por contra, la rutina se ejecuta en un lapso
de tiempo grande, una sola ejecuci on ser a suciente para tener una medici on razonable-
mente precisa. Este programa expresa esta idea en Python:
experiments/searching/timing3.py
from algoritmia.searching import NaiveSequentialSearcher
from time import clock
from random import seed, randrange, sample
tmin = 2 # Tiempo mnimo de ejecuci on: dos segundos.
seed(0)
n = 100
a = sorted(sample(range(5*n), n))
x = a[randrange(n)]
searcher = NaiveSequentialSearcher()
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
26 Apuntes de Algoritmia 28 de septiembre de 2009
t = ((t2 - t1) - (t4 - t3)) / r
print(Tiempo medio por ejecucion: {:.8f} segundos.format(t))
El tiempo registrado en las variables t3 y t4 tiene por objeto estimar el tiempo que
requiere la ejecuci on del c odigo extra que hemos a nadido para aplicar la t ecnica de me-
dici on, pues ejecuta el mismo n umero de operaciones a nadidas a causa de la medici on
de tiempo (un bucle que se itera el mismo n umero de veces y en el que cada iteraci on su-
pone efectuar una comparaci on, una resta, una asignaci on, una llamada a clock con otra
asignaci on y un incremento). El resultado en este caso es el siguiente:
Tiempo transcurrido: 0.00001084 segundos
Pasemos a ocuparnos del estudio de la evoluci on del tiempo de ejecuci on con la talla
de las instancias del problema. Pero, antes, deteng amonos a considerar qu e entendemos
exactamente por talla de una instancia.
2.2. Talla de una instancia
Podemos denir informalmente la talla de una instancia del problema como la cantidad
de memoria necesaria para describirla. As pues, la talla requiere la denici on de una
codicaci on, es decir, una correspondencia entre los datos y cadenas de smbolos de un
determinado alfabeto. La ocupaci on espacial guarda relaci on con el n umero de dichos
smbolos.
Un n umero entero y positivo n puede codicarse, por ejemplo, con un alfabeto una-
rio, como {1}, formando una cadena con n repeticiones del mismo smbolo. En el alfabe-
to binario {0, 1}, el mismo n umero podra codicarse en binario natural. El n umero de
smbolos necesarios para codicar n con la primera codicaci on es s
1
(n) = n. La segun-
da codicaci on s olo requiere s
2
(n) = 1 +lg n smbolos. La diferencia en la cantidad de
memoria necesaria si usamos una u otra codicaci on es enorme, como se puede ver al
comparar las columnas 2 y 3 en la tabla 2.1. Si hay una gran diferencia entre el n umero
de smbolos necesarios para codicar un entero positivo con un alfabeto unario (base 1)
y para codicarlo en binario natural (base 2), no ser a mejor a un usar un sistema de nu-
meraci on en una base mayor? El n umero de smbolos necesarios para expresar un entero
positivo n en base 10 es s
10
(n) = 1 +

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 tiempo promedio no siempre coincide con la semisuma del tiempo en el


mejor y en el peor de los casos.
Podemos observar que tambi en en tiempo promedio resulta m as eciente el m eto-
do BinarySearcher.index of cuando n es sucientemente grande: el tiempo promedio de
SequentialSearcher.index of se puede ajustar bien con una lnea recta mientras que el de
BinarySearcher.index of se describe mejor con una funci on logartmica.
Cabe destacar que hemos supuesto que la probabilidad de que busquemos cualquiera
de los elementos del vector es id entica. En una aplicaci on pr actica, no obstante, puede
que no sea el caso y, por tanto, que el coste promedio manieste un comportamiento
diferente.
Del estudio realizado podemos extraer algunas conclusiones:
La complejidad temporal relaciona el tiempo necesario para resolver una instancia
del problema con la talla del problema.
El tiempo de ejecuci on no es funci on de la talla en el sentido de que no siempre
existe un unico valor de tiempo asociado a la resoluci on de cualquier instancia
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 33
con una talla determinada (ni siquiera descontando los errores de precisi on en la
medida). Hay un rango de valores posibles para el tiempo de ejecuci on dada una
talla. El rango viene jado por el tiempo de ejecuci on en el mejor y peor de los
casos.
Cabe esperar que el tiempo de ejecuci on crezca con la talla del problema. No nece-
sariamente ocurre as si comparamos el tiempo de ejecuci on de instancias particu-
lares, pues algunas resultan m as favorables que otras. Por regla general observa-
mos esta tendencia si consideramos el tiempo de ejecuci on en el peor de los casos o
en promedio.
El tiempo de ejecuci on promedio puede utilizarse como criterio de comparaci on
entre algoritmos, pero en tal caso hemos de prestar atenci on a la distribuci on de
probabilidad de las instancias para cada valor estudiado de la talla.
Un programa puede resultar m as eciente que otro para instancias de talla pe-
que na, pero dejar de serlo cuando nos enfrentamos a instancias de talla sucien-
temente grande. Los ordenadores suelen resultar ecaces cuando abordamos pro-
blemas de gran talla, por lo que resulta relevante centrar el estudio en el compor-
tamiento del tiempo de ejecuci on en valores sucientemente grandes de la talla del
problema.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2 Te mostramos a continuaci on cuatro procedimientos para ordenar, de menor a mayor, los
elementos de un vector: ordenaci on por selecci on, por el m etodo de la burbuja, por inserci on y
por la t ecnica quicksort. Cada m etodo se implementa en una clase que implementa la interfaz
IInPlaceSorter, que podemos traducir por ordenador in situ. Un m etodo de ordenaci on in situ
es aquel que modica la propia secuencia de valores que se le suministra como entrada.
Obt en perles de ejecuci on en funci on del tama no del vector que se suministra como par ame-
tro.
(Ten en cuenta que el vector que suministras a la funci on se modica y queda ordenado,
por lo que si efect uas dos llamadas sobre el mismo vector, sin m as, la segunda llamada propone
ordenar un vector que ya est a ordenado. Si quieres ordenar un vector (desordenado o no) y medir
el tiempo de ejecuci on, aseg urate de que pasas el mismo contenido (desordenado o no) a todas
las funciones.)
En un estudio comparativo de estos algoritmos, puedes decantarte por uno de ellos como
m as eciente? Por qu e? Hay un mejor o peor caso para cada uno de ellos?
algoritmia/problems/sorting.py
from abc import ABCMeta, abstractmethod
...
class IInPlaceSorter(metaclass=ABCMeta):
@abstractmethod
def sort(self , a: "sequence<T> -> sorted sequence<T>"): pass
class InPlaceSelectionSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
for i in range(len(a)-1):
k = i
34 Apuntes de Algoritmia 28 de septiembre de 2009
for j in range(i+1, len(a)):
if a[j] < a[k]: k = j
a[i], a[k] = a[k], a[i]
class InPlaceBubbleSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
for i in range(len(a)):
for j in range(len(a)-1-i):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
class InPlaceInsertionSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
for i in range(1, len(a)):
x = a[i]
j = i-1
while j >= 0 and x < a[j]:
a[j+1] = a[j]
j -= 1
a[j+1] = x
class InPlaceQuickSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
self . quicksort(a, 0, len(a))
def quicksort(self , a: "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
(No te preocupes si no entiendes el ultimo m etodo. Lo estudiaremos m as adelante, en el
captulo dedicado a la t ecnica Divide y Vencer as.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 35
2.4. Conteo de pasos
Las t ecnicas de medici on de tiempo presentan un gran inconveniente: deben efectuar-
se a partir de una implementaci on concreta del algoritmo. Por otra parte, las medidas
obtenidas son imprecisas. Vamos a plantear un m etodo que sacrica el detalle para dar
una visi on m as basta del coste temporal, pero que puede resultar suciente para efectuar
comparaciones. Una ventaja fundamental del m etodo que proponemos ahora es que no
requiere que dispongamos de una implementaci on concreta del algoritmo. Pretendemos
alcanzar as independencia del sistema computador y del lenguaje de programaci on. Nos
basaremos en el conteo de pasos. Un paso es una instrucci on o conjunto de instrucciones
cuyo tiempo de ejecuci on est a acotado por un valor constante. Consideremos estas dos
asignaciones en el lenguaje de programaci on Python:
a = 1
b = a * 10 + 1
Ciertamente la segunda ha de tardar m as tiempo en ejecutarse que la primera: com-
porta una multiplicaci on, una suma y una asignaci on, cuando la primera es una simple
asignaci on; pero ambas sentencias se ejecutan en tiempo constante, as que cada una de
ellas se considera un paso.
N otese que el concepto de paso es independiente del lenguaje de programaci on. Cada
una de las dos sentencias del ejemplo se computa como un paso, y seguiran contando
como pasos individuales si estuvieran escritas en un lenguaje de programaci on diferente.
2.4.1. Conteo de pasos en algoritmos iterativos
Estudiemos ahora este fragmento de programa:
s = 0
for i in range(n):
s += i
La primera lnea se considera un paso, evidentemente. La segunda lnea es especial: su
ejecuci on se repite n veces, as que el tiempo de ejecuci on depende del valor de n. No
podemos acotar su tiempo de ejecuci on por una constante: siempre habr a un valor su-
cientemente grande de n que haga que el tiempo de ejecuci on sea superior a cualquier
valor constante jado de antemano. No podemos considerar que esta lnea cuente, pues,
como un solo paso, sino como n. La tercera lnea, por s sola, es un paso, pero se encuen-
tra en un bucle que se ejecuta n veces. Hemos de contarla, pues, como n pasos. El n umero
de pasos que requiere la ejecuci on de las tres lneas es 1 + n + n = 2n +1.
Veamos un fragmento de programa algo m as complejo:
s = 0
for i in range(n):
for j in range(n):
s += i
36 Apuntes de Algoritmia 28 de septiembre de 2009
La tercera lnea comporta la ejecuci on de n iteraciones de un bucle, pero el bucle com-
pleto se ejecuta n veces (est a dentro de otro bucle), as que su ejecuci on aporta un n umero
total de n
2
pasos. La lnea 4 aporta otros n
2
pasos. El n umero de pasos que supone la eje-
cuci on de este fragmento de programa es 1 + n + n
2
+ n
2
= 2n
2
+ n +1.
Este otro fragmento requiere algo de calma al efectuar el conteo:
s = 0
for i in range(n):
for j in range(3):
s += i
Al contener un bucle dentro de otro, se puede pensar que el n umero de pasos ser a un
polinomio de grado 2 con n, pero no es as. El bucle indexado con j se ejecuta tres veces.
El n umero de pasos que supone la ejecuci on de las lneas 3 y 4 es, pues, 6. Y el n umero
de pasos que resulta de ejecutar el fragmento de programa es 1 + n +3n +3n = 7n +1.
Los bucles for-in suelen conducir a un conteo sencillo de pasos. Los bucles while,
m as exibles que los for-in, suelen requerir un estudio de la evoluci on de alguna variable
contador (si la hay) o de las circunstancias que permiten violar la condici on del bucle.
Consideremos, por ejemplo, este bucle:
s = 0
i = 1
while i <= n:
s += i
i *= 2
La variable i no se incrementa con cada iteraci on en una cantidad constante, sino que
duplica su valor. Mientras este sea menor o igual que n, el bucle efectuar a una nueva
iteraci on. Los valores que ir a tomando i son 1, 2, 4, 8, 16. . . 2
k
, donde k es el mayor entero
tal que 2
k
n. Tomando logaritmos tenemos lg2
k
lg n, es decir, k lg n. As pues, el
n umero de pasos guarda relaci on en este caso con el logaritmo en base 2 de n y es igual
a 5 +lg n para todo entero n > 0.
Hay cierta arbitrariedad en la denici on del concepto de paso. Estas lneas, por ejem-
plo, han sido computadas antes como dos pasos:
a = 1
b = a * 10 + 1
Pero, de acuerdo con la denici on, tambi en pueden computarse como un solo paso: el
tiempo de ejecuci on de ambas lneas puede acotarse por una constante (por ejemplo, una
que sea el doble de la constante que la usada cuando cada lnea se comput o como un
paso). No hay problema en ello. El conteo de pasos no pretende estimar con exactitud el
tiempo de ejecuci on.
Contemos el n umero de pasos del m etodo de b usqueda secuencial naf (m etodo in-
dex of de la clase NaiveSequentialSearcher del programa searching.py, p agina 22). La pri-
mera de las lneas es una cabecera de m etodo. Por s sola no parece comportar coste
alguno. Podemos considerar, eso s, que cuando se llame al m etodo se ejecutar a un pa-
so, pues las acciones propias de la llamada a requieren un tiempo total constante: crear
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 37
un registro de activaci on en la pila de llamadas a funci on, apilar la direcci on del vec-
tor a y del valor x, reservar espacio para variables locales, guardar en pila la direcci on
de memoria desde la que se efect ua la llamada y saltar a una nueva direcci on de me-
moria. La asignaci on de la segunda lnea es un paso. El bucle efect ua tantas iteraciones
como tama no tiene el vector a. Si denotamos dicho tama no con n, el n umero de pasos
que comporta el bucle en s es n. La comparaci on del valor de a[i] con el valor de x se
efect ua n veces, as que la lnea del if aporta a nuestra cuenta otros n pasos. La siguiente
lnea s olo se ejecuta una vez (estamos suponiendo que los n elementos del vector son
diferentes), as que s olo aporta un paso, aunque est e dentro de un bucle. Finalmente, la
devoluci on del ndice cuenta como un solo paso, aunque supone la ejecuci on de algunas
acciones relativamente complejas (desapilar el registro de activaci on y saltar a la direc-
ci on desde la que se efectu o la llamada a la funci on). El n umero total de pasos es, pues,
1 +1 + n + n +1 +1 = 2n +4.
M etodos como NaiveSequentialSearcher.index of permiten encontrar una relaci on uni-
ca entre la talla del vector, n, y el n umero de pasos ejecutados. No siempre es as. Consi-
deremos el otro m etodo de b usqueda secuencial: SequentialSearcher.index of (tambi en en
el m odulo searching.py, p agina 22). El n umero de pasos depende ahora de la talla del
vector y del valor buscado. Podemos, eso s, acotar superior e inferiormente el n umero
de pasos: en el mejor caso se ejecutan las lneas de la llamada a m etodo y las tres siguien-
tes; y en el peor caso se ejecutan las lneas de la llamada y el segundo return una vez y
n veces las lneas del bucle for, el if y el elif. As pues, el n umero de pasos es siempre
mayor o igual que 4 y menor o igual que 3n + 2. Como se ve, en ese caso no es posible
hablar de una funci on que exprese el coste temporal en funci on de n y hemos de pasar a
considerar el n umero de pasos en el mejor y en el peor de los casos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3 Calcula el n umero de pasos que comporta la ejecuci on de las tres primeras funciones presen-
tadas en el ejercicio 2 en funci on del tama no del vector.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Analicemos el n umero de pasos con el que BinarySearcher.index of resuelve el pro-
blema de la b usqueda. Su implementaci on en Python se muestra en la p agina 22. Nue-
vamente el n umero de pasos depende del valor buscado, y no s olo de n. Con el n de
simplicar el estudio supondremos que n es potencia de 2. Si el elemento buscado ocu-
pa la posici on n/2 del vector, se ejecutan 6 pasos. Qu e ocurre en otro caso? Depende:
si el valor del elemento en la posici on n/2 es mayor que x, se busca en un vector con
n/2 1 elementos; si es menor, se busca en un vector con n/2 elementos. Consideremos
el peor de los casos: que nos toque buscar en el vector con n/2 elementos. Aplicando un
razonamiento similar y considerando tambi en el peor caso, tendremos que continuar la
b usqueda en un vector de n/4 elementos. Y as hasta llegar a considerar un vector con
un solo elemento. Esto ocurrir a tras repetir el proceso 1 + lg n veces. Cada una de las
iteraciones supone la ejecuci on de 5 pasos. A estos hay que sumar el de la llamada a la
funci on y el de la inicializaci on de las variables left y right. El n umero de pasos ejecuta-
dos en el peor de los casos es, pues, 5 lg n + 7. Y en el mejor de los casos? En el mejor
de los casos el elemento buscado ocupa la posici on central y, como hemos dicho antes, se
38 Apuntes de Algoritmia 28 de septiembre de 2009
ejecutar an 6 pasos.
2.4.2. Conteo de pasos en algoritmos recursivos
Los algoritmos que hemos visto son de naturaleza iterativa, es decir, se basan en la repe-
tici on de un c alculo controlado por un bucle (for o while). Tambi en consideraremos en
el texto algoritmos recursivos, esto es, algoritmos que expresamos con funciones que se
llaman a s mismas. El procedimiento de b usqueda binaria puede expresarse recursiva-
mente:
algoritmia/problems/searching.py
class RecursiveBinarySearcher(ISearcher):
def index of (self , a: "sequence<T>", x: "T") -> "int or None":
return self . binary search(a, x, 0, len(a))
def binary search(self , a: "sequence<T>", x: "T",
left: "int", right: "int") -> "int or None":
if left < right:
i = (right + left) // 2
if a[i] == x:
return i
elif a[i] > x:
return self . binary search(a, x, left, i)
else:
return self . binary search(a, x, i+1, right)
else:
return None
Centr emonos en el m etodo binary search. Cuando n (la talla de a) vale cero, el al-
goritmo requiere la ejecuci on de 3 pasos. Para valores de n mayores, nuestro algoritmo
presenta un mejor y un peor caso. El n umero de pasos de un algoritmo recursivo puede
expresarse, en primera instancia, mediante una ecuaci on recursiva. Supongamos de nue-
vo que n es potencia de 2 y mayor que cero. El mejor caso requiere la ejecuci on de 5 pasos.
El peor caso obliga a ejecutar un n umero de pasos que podemos expresar mediante esta
ecuaci on recursiva:
f (n) =
{
8, si n = 1;
6 + f (n/2), si n > 1.
El t ermino inferior de la ecuaci on es el denominado caso general (es una expresi on
recursiva) y el superior es su caso base (cuando naliza la recursi on).
Las ecuaciones recursivas admiten, en muchos casos, una expresi on cerrada o t ermino
general. Decimos que la expresi on cerrada es la soluci on de la ecuaci on recursiva. En la
secci on B.14 de los ap endices se resumen algunas t ecnicas para la resoluci on de ecuacio-
nes recursivas. Una de las t ecnicas com unmente utilizadas es el desplegado, y consiste
en sustituir reiteradamente la parte izquierda de la ecuaci on por su parte derecha con
objeto de identicar un patr on:
f (n) = 6 + f (n/2) = 6 +6 + f (n/4) = 6 +6 +6 + f (n/8) = = 6k + f (n/2
k
).
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 39
El desplegado naliza cuando n/2
k
= 1, es decir, cuando k = lg n, pues entonces llega-
mos al caso base de la ecuaci on recursiva:
f (n) = 6k + f (n/2
k
) = 6 lg n + f (1) = 6 lg n +8.
El n umero de pasos de la versi on recursiva de la b usqueda binaria es, pues, compa-
rable al de la versi on iterativa.

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).

N otese que estamos extendiendo el uso de la notaci on asint otica al coste


temporal en s, cuando este no siempre es una funci on, es decir, a una misma
talla corresponden diferentes instancias y cada una de ellas se resuelve con un
n umero de pasos que puede ser diferente.
Siguiendo este proceder, podemos decir que el m etodo BinarySearcher.index of es
(1) y O(lg n). Recordemos que con ello decimos que su coste temporal es constante
en el mejor de los casos y que crece logartmicamente con n en el peor de los casos. Por
regla general resulta m as informativo el coste en el peor de los casos y muchas veces, al
describir el coste temporal de un algoritmo, se omite su coste temporal en el mejor de los
casos. Es corriente que, en aras de la simplicidad, s olo se diga de BinarySearcher.index of
que se ejecuta en tiempo O(lg n).
Podramos sintetizar la informaci on considerada m as relevante de los tres algoritmos
estudiados as:
NaiveSequentialSearcher.index of (n) O(n) (n)
SequentialSearcher.index of (1) O(n)
BinarySearcher.index of (1) O(lg n)
La expresi on de costes mediante el orden de su cota superior no s olo sintetiza la
informaci on esencial: resulta, adem as, util a la hora de simplicar los c alculos necesarios
para efectuar el coste. Anotamos el aporte al coste global de cada una de las lneas de
NaiveSequentialSearcher.index of :
class NaiveSequentialSearcher(ISearcher):
def index of (self , a, x): # (1) por la llamada.
index = None # Asignaci on (1)
for i in range(len(a)): # Bucle que se ejecuta (n) veces.
if x == a[i]: # Comparaci on (1), repetida (n) veces.
index = i # Asignaci on (1), ejecutada una sola vez.
return index # (1) por la devoluci on del valor.
No se cuentan los pasos individuales, sino la contribuci on al coste asint otico global,
que es independiente de factores constantes. El coste global es (1) + (1) + (n) +
(n) +(1) +(1), que es (n).

Usamos expresiones como O(h


1
(n)) + O(h
2
(n)) para denotar el orden
de la suma de las funciones f y g, que es O(h
1
(n)) + O(h
2
(n)) =
O(m ax{h
1
(n), h
2
(n)}). N otese que la ultima expresi on no es una ecuaci on, pues
en tal caso llegaramos al absurdo de deducir de O(n) + O(n) = O(n) que
O(n) = 0.
Analicemos el coste de SequentialSearcher.index of :
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 41
class SequentialSearcher(ISearcher):
def index of (self , a, x): # (1).
for i in range(len(a)): # Bucle que se ejecuta hasta n veces: O(n) pasos.
if x == a[i]: # Comparaci on (1), que se ejecuta O(n) veces.
return i # (1), pero cero o una vez: O(1).
elif x < a[i]: # Comparaci on (1), que se ejecuta O(n) veces.
return None # (1), pero cero veces o una vez: O(1).
return None # (1), pero cero veces o una vez: O(1).
El coste temporal global es (1) + O(n) + O(n) (1) + O(1) + O(n) (1) + O(1) +
O(1), o sea, O(n).
Y, nalmente, analicemos el coste temporal de BinarySearcher.index of :
class BinarySearcher(ISearcher):
def index of (self , a, x): # (1)
left, right = 0, len(a) # (1)
while left < right: # O(lg n) veces.
i = (right + left) // 2 # (1), ejecutado O(lg n) veces.
if x == a[i]: # (1), ejecutado O(lg n) veces.
return i # (1), ejecutado a lo sumo una vez: O(1).
elif x < a[i]: # (1), ejecutado O(lg n) veces.
right = i # (1), ejecutado O(lg n) veces.
else:
left = i + 1 # (1), ejecutado O(lg n) veces.
return None # (1), a lo sumo una vez: O(1).
Operando de modo similar a como ya hemos hecho obtenemos un coste temporal O(lg n).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4 Analiza el coste temporal asint otico de estos algoritmos:
algoritmia/problems/sorting.py
class IInPlaceSorter(metaclass=ABCMeta):
@abstractmethod
def sort(self , a: "sequence<T> -> sorted sequence<T>"): pass
class InPlaceSelectionSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
for i in range(len(a)-1):
k = i
for j in range(i+1, len(a)):
if a[j] < a[k]: k = j
a[i], a[k] = a[k], a[i]
class InPlaceBubbleSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
for i in range(len(a)):
for j in range(len(a)-1-i):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
42 Apuntes de Algoritmia 28 de septiembre de 2009
class InPlaceInsertionSorter(IInPlaceSorter):
def sort(self , a: "sequence<T> -> sorted sequence<T>"):
for i in range(1, len(a)):
x = a[i]
j = i-1
while j >= 0 and x < a[j]:
a[j+1] = a[j]
j -= 1
a[j+1] = x
5 La siguiente funci on calcula el producto de dos matrices, A, de dimensi on p q, y B, de
dimensi on q r:
algoritmia/problems/matrixprod.py
def matrix product0(A: "matrix", B: "matrix") -> "matrix":
p, q, r = len(A), len(A[0]), len(B[0])
C = [[0] * r for i in range(p)]
for i in range(p):
for j in range(r):
for k in range(q):
C[i][j] += A[i][k]*B[k][j]
return C
Acota asint oticamente, superior e inferiormente, el coste temporal del algoritmo.
6 Esta otra rutina efect ua el mismo c alculo del ejercicio anterior:
algoritmia/problems/matrixprod.py
def matrix product(A: "matrix", B: "matrix") -> "matrix":
p, q, r = len(A), len(A[0]), len(B[0])
return [[sum(A[i][k]*B[k][j] for k in range(q)) for j in range(r)] for i in range(p)]
Acota asint oticamente, superior e inferiormente, el coste temporal del algoritmo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5.2. Coste promedio
El coste en el mejor y peor de los casos ofrece una informaci on descriptiva del compor-
tamiento del algoritmo para valores crecientes de la talla de las instancias y situaciones
extremas. Podemos ofrecer una descripci on m as rica si presentamos, adem as, infor-
maci on sobre su comportamiento esperado. El coste temporal promedio es el tiempo de
ejecuci on promediado para todas las entradas posibles de una talla dada asumiendo una
distribuci on de probabilidad para las instancias de igual talla.
Si
n
es el conjunto de instancias de talla n y denotamos con t(I) el n umero de pasos
necesarios para resolver la instancia I
n
y con Pr(I) denotamos la probabilidad de
que ocurra la instancia I, el coste temporal promedio en funci on de n es

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)

indicar a coste amortizado constante.


El m etodo de la funci on de potencial
Hay un m etodo de an alisis de coste amortizado que establece una analoga con la energa
potencial en fsica. El trabajo que se hace como inversi on para futuras operaciones se
anota como potencial que puede gastarse en operaciones futuras. El coste amortizado
de la i- esima operaci on sobre una estructura de datos se dene como
a
i
= c
i
+
i
i 1,
donde
k
es el potencial tras ejecutar la k- esima operaci on y c
i
es el coste real de ejecu-
tar la i- esima operaci on. El coste total amortizado de una serie de n operaciones es

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 (

n) crece a un ritmo superior que otro que es (log n),


pero no llega a presentar un crecimiento lineal. Cuando la talla se multiplica por 4,
el coste se multiplica por 2.
Analicemos ahora los crecimientos lineal y superlineales que se muestran gr aca-
mente en la gura 2.10.
48 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 2.10: Gr acas de crecimiento de fun-
ciones lineal y superlineales.
c
3
n
n
t
c
4
n log n
c
5
n
2
c
6
n
3
c
7
2
n
c
8
n
n
Un algoritmo (n log n) presenta un crecimiento del coste ligeramente superior al
de un algoritmo lineal. Por ejemplo, si tardamos 10s en resolver un problema de
talla 1 000, tardaremos 22 s, poco m as del doble, en resolver un problema de talla
2 000.
Un algoritmo cuadr atico empieza a dejar de ser util para tallas medias o grandes,
pues pasar a tratar con un problema el doble de grande requiere cuatro veces m as
tiempo.
Un algoritmo c ubico s olo es util para problemas peque nos: duplicar el tama no del
problema hace que se tarde ocho veces m as tiempo.
Un algoritmo exponencial raramente es util. Si resolver un problema de talla 10
requiere una cantidad de tiempo determinada con un algoritmo (2
n
), tratar con
uno de talla 20 requiere unas 1 000 veces m as tiempo!
La tabla 2.3 tambi en ayuda a tomar conciencia de las tasas de crecimiento. Suponga-
mos que las instancias de un problema de talla n = 1 se resuelven con varios algoritmos
constantes, lineales, etc. en 1 s. En la tabla se muestra el tiempo aproximado que cuesta
resolver problemas con cada uno de ellos. En la tabla 2.4 se muestran tiempos con tallas
de problema mayores para todos los costes de la tabla 2.3, excepto el exponencial.
Complejidad temporal n = 1 n = 5 n = 10 n = 50 n = 100
Constante 1 s 1.0 s 1 s 1 s 1 s
Logartmico 1 s 1.7 s 2 s 2.7 s 3 s
Lineal 1 s 5.0 s 10 s 50 s 100 s
n log n 1 s 8.5 s 11 s 86 s 201 s
Cuadr atico 1 s 25.0 s 100 s 2.5 ms 10 ms
C ubico 1 s 125.0 s 1 ms 125 ms 1 s
Exponencial (2
n
) 1 s 32.0 s 1 ms 35.75 a nos 40 10
6
eones
Tabla 2.3: Tiempo de ejecuci on en funci on de n para algoritmos con diferentes costes.
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 49

El ultimo n umero de la tabla 2.3 es b arbaro: un e on son mil millones de a nos


y los cientcos estiman actualmente que la edad del universo es de entre
13 y 14 eones!
Complejidad temporal n = 1 000 n = 10 000 n = 100 000
Constante 1 s 1 s 1 s
Logartmico 4 s 5 s 6 s
Lineal 1 ms 10 ms 100 ms
n log n 4 ms 50 ms 600 ms
Cuadr atico 1 s 1 minuto y 40 s 2 horas y 46 minutos
C ubico 16.5 minutos 11.5 das casi 32 a nos
Tabla 2.4: Tiempo de ejecuci on en funci on de n para algoritmos con diferentes costes.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7 Si la ejecuci on de un programa requiere 1 s de tiempo para resolver una instancia de talla
100, cu anto tiempo requerir a resolver uno de talla 1 000 en cada uno de estos casos?
a) El algoritmo es lineal.
b) El algoritmo es O(n lg n), siendo n la talla de la instancia.
c) El algoritmo es cuadr atico.
d) El algoritmo es c ubico.
e) El algoritmo es O(2
n
).
8 Si resolver una instancia de talla 100 con determinado algoritmo requiere 1 , cu al es la
mayor talla que podemos resolver en un minuto en cada uno de los casos enumerados en el
ejercicio anterior?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

El an alisis de coste temporal asint otico reduce al coste a su t ermino domi-


nante. Esta simplicaci on permite expresar de forma muy sint etica el coste y
ofrece una medida de eciencia que puede resultar suciente para establecer una
primera comparaci on entre algoritmos. Es f acil que el coste asint otico de un algorit-
mo venga dominado por la sentencia m as anidada en ciertos bucles, por ejemplo. En
tal caso, no es necesario efectuar un conteo de pasos lnea a lnea (o instrucci on a
instrucci on) como el que hemos presentado: basta con estimar el coste de ejecuci on
del bloque en cuesti on. En ciertos algoritmos, una operaci on u operaciones puede
ocupar ese bloque crtico y basta con contar el n umero de veces que se ejecuta.
As, no es raro encontrar an alisis de complejidad para algoritmos de ordenaci on en
los que se expresa el coste en n umero de comparaciones e intercambios de celdas
de un vector.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9 Una cadena a es subcadena de otra b si la secuencia de caracteres a aparece en b. Por ejemplo,
la cadena mato es subcadena de onomatopeya, pero toma no lo es. Esta funci on Python
recibe dos cadenas, a y b, y nos dice si a es una subcadena de b:
50 Apuntes de Algoritmia 28 de septiembre de 2009
def is a substring(a: "str", b: "str") -> "bool":
for i in range(len(b)-len(a)+1):
j = 0
while j < len(a):
if a[j] != b[i+j]:
break
j += 1
if j == len(a):
return True
return False
Supondremos que la longitud de b es mayor o igual que la longitud de a. Acota superior e
inferiormente su coste temporal en funci on de n y m, las longitudes de a y b, respectivamente.
10 Una empresa de seguridad ha instalado un sistema de reconocimiento autom atico de placas
de matrcula en la puerta de una f abrica. El primer da registr o la entrada de n vehculos y alma-
cen o en un vector a sus matrculas. El segundo da volvi o a registrar la entrada de n vehculos y
almacen o las matrculas en un segundo vector b. El jefe de seguridad quiso saber si los dos das
entraron exactamente los mismos vehculos. Pens o que hacer la comprobaci on manualmente su-
pondra una inversi on excesiva de tiempo, as que encarg o el dise no de una funci on Python para
resolver el problema. Esta es la soluci on que ide o:
def same content(a: "Sequence", b: "Sequence") -> "bool":
for i in range(len(a)):
found = False
for j in range(len(b)):
if a[i] == b[j]:
found = True
break
if not found:
return False
return True
Se pide:
a) Un an alisis del coste temporal asint otico para el mejor y el peor de los casos. Expresa el coste
en funci on de n, el tama no de los vectores a y b.
b) El m etodo sort ordena de menor a mayor el contenido de un vector (por ejemplo, si a es un
vector, a.sort() lo ordena) en tiempo O(n lg n) y (n). Sabiendo esto, puedes dise nar un
algoritmo m as eciente? Si es as, descrbelo y analiza su coste temporal asint otico para el
mejor y el peor de los casos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.5.5. Una ultima consideraci on: conteo de operaciones muy
relevantes
Si adoptamos la evoluci on asint otica del n umero de pasos como criterio para establecer
la eciencia de un algoritmo y efectuar comparaciones (un tanto bastas) con otros algorit-
mos, estamos simplicando enormemente el an alisis y caracterizando su comportamien-
to con una familia de funciones (su orden, omega, etc.) que describe el comportamiento
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 51
en el mejor caso, en el peor caso, en caso promedio, etc. Con este tipo de an alisis, poco
importa si un algoritmo ejecuta 2n +5 o 20n +1 pasos: su coste asint otico es equivalente
(aunque puede que en la pr actica haya diferencias apreciables en el tiempo de ejecuci on,
claro est a).
En esta lnea de simplicaci on de los an alisis hay un paso adicional que en ocasio-
nes es posible adoptar: el conteo, unicamente, del n umero de veces que se ejecutar a una
instrucci on determinada (en el peor caso, mejor caso o caso promedio) siempre que po-
damos asegurar que dicho n umero es, asint oticamente, equivalente al n umero total de
pasos que ejecuta el algoritmo.
Tomemos por ejemplo el algoritmo InPlaceBubbleSorter.sort (ejercicio 2, p agina 33) y la
operaci on de comparaci on menor que. Esta operaci on se ejecuta con cada iteraci on de
los bucles anidados. Contar el n umero de veces que se ejecuta equivale, asint oticamente,
a contar el n umero total de pasos del algoritmo.
En el an alisis de algoritmos, pues, estudiaremos el coste temporal recurriendo al con-
teo de algunas operaciones muy relevantes para el mismo: comparaciones, sumas, pro-
ductos, etc.
2.6. Coste espacial
Los programas de ordenador consumen dos recursos fundamentales: tiempo y espacio.
El coste espacial de un algoritmo es la cantidad de memoria que necesita para resolver
instancias de un problema en funci on de la talla. La memoria que consume un algoritmo,
al implementarse con un programa y ejecutarse sobre un ordenador, puede dividirse en
diferentes categoras:
Una parte de tama no jo:
El espacio necesario para el c odigo del programa.
El espacio necesario para almacenar variables escalares, de tama no jo o cons-
tantes.
Y una parte de tama no variable:
El espacio necesario para almacenar estructuras de datos complejas (vectores,
matrices, etc), que puede depender de la talla del problema.
El espacio necesario para la pila de llamadas a funci on. Las variables locales
suelen residir en dicha pila, aunque ello s olo resulta realmente necesario en
el caso de los programas recursivos. El n umero de tramas de activaci on en la
pila depende de la talla del problema en el caso de los programas recursivos.
Dado que nos basta con efectuar an alisis de coste espacial asint oticos, nos centrare-
mos en la estimaci on de la parte variable, es decir, la cantidad de memoria que depende
de la talla del problema. No tendremos en cuenta el espacio necesario para especicar la instan-
cia del problema, s olo el que ocupan las variables que hemos de utilizar para resolverla. Por otra
52 Apuntes de Algoritmia 28 de septiembre de 2009
parte, nos limitaremos a contar el n umero de celdas b asicas de memoria necesarias, es
decir, cada variable escalar o elemento simple de vector contar a como una sola celda, sin
tener en cuenta el n umero mnimo de bits con el que podramos representar los valores
almacenados en ellas.
Consideremos el m etodo NaiveSequentialSearcher.index of : usamos dos variables au-
xiliares, i e index y la funci on no llama a ninguna otra (ni a s misma), as que el coste
espacial es constante o, en notaci on asint otica (1). Lo mismo ocurre con SequentialSear-
cher.index of y BinarySearcher.index of : el coste espacial es (1).
Analicemos RecursiveBinarySearcher.index of (p agina 38), que es un m etodo recursi-
vo. El n umero de variables escalares locales utilizadas es constante, pero el m etodo es
recursivo y cada activaci on de la funci on comporta la reserva de memoria en la pila de
llamadas a funci on. Cada una de las activaciones reserva tanto espacio como requieran
las variables locales (m as una cantidad de memoria de tama no jo). El n umero de lla-
madas simult aneas en pila es menor o igual que 1 +lg n, as que el espacio es (1) y
O(lg n).
Cabe hacer una observaci on importante acerca del coste espacial: siempre est a acota-
do superiormente por el coste temporal. Si un algoritmo necesita usar cierta cantidad de
celdas de memoria, tendr a que visitar cada una de las celdas de memoria al menos una
vez (de lo contrario, no las necesitara).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
11 La ordenaci on por conteo es una t ecnica de ordenaci on r apida para vectores de n umeros
enteros (comprendidos entre 0 y un valor superior k 1). Esta funci on Python es una implemen-
taci on del algoritmo:
algoritmia/problems/sorting.py
class CountingSorter(ISorter):
def sorted(self , a: "sequence<int>") -> "sorted iterable<int>":
if len(a) == 0: return []
b = [0] * len(a)
k = max(a) + 1
c = [0] * k
for v in a: c[v] += 1
for i in range(1, k): c[i] += c[i-1]
for j in range(len(a)-1,-1,-1):
b[c[a[j]]-1] = a[j]
c[a[j]] -= 1
return b
Se pide un an alisis de complejidad temporal y espacial. Indica (razonadamente) el coste tem-
poral de cada una de las lneas de la funci on y de d onde proviene cada uno de los t erminos de la
expresi on asint otica del coste espacial.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28 de septiembre de 2009 Captulo 2. An alisis de algoritmos 53
2.7. Complejidad de problemas
Hasta el momento hemos considerado unicamente el coste temporal de algoritmos con-
cretos. Un mismo problema, como el de b usqueda en un vector ordenado, puede resol-
verse en tiempo O(n) u O(lg n). Es O(lg n) la menor cota superior al coste temporal con
el que podemos resolver ese problema? Obs ervese que formulamos la pregunta del cos-
te en relaci on al problema, no a un algoritmo. El campo de estudio que se ocupa de la
dicultad intrnseca de los problemas recibe el nombre de complejidad de problemas.
Resulta interesante saber si hay lmites al coste temporal con el que podemos resolver un
problema determinado, pues permite saber cuando hemos dise nado un algoritmo opti-
mo, es decir, cuando tenemos la garanta de que no se puede dise nar un algoritmo m as
eciente.
Tomemos por caso el problema de la b usqueda en un vector ordenado. Un resultado
importante nos asegura que ning un algoritmo basado en la comparaci on de los elemen-
tos con el valor buscado puede resolver el problema en tiempo con una cota superior
asint otica inferior a O(lg n). De acuerdo con ello, el m etodo binary search (o su versi on
recursiva) es un algoritmo optimo. La demostraci on se basa en el estudio de los arboles de
decisi on para b usqueda.
Un arbol de decisi on para este problema (con un vector de tama no n) es un arbol bi-
nario cuyas hojas est an etiquetadas con n umeros entre 0 y n 1 (los ndices del vector) o
None y representa las comparaciones que efect ua el algoritmo. La gura 2.11 muestra los
arboles de decisi on asociados a los algoritmos SequentialSearcher.index of y BinarySear-
cher.index of para vectores de 8 elementos.
El n umero de comparaciones efectuadas al buscar el valor que ocupa una determi-
nada posici on es la longitud del camino entre la raz y el nodo etiquetado con el ndice
de dicha posici on. La profundidad del arbol es la mayor cantidad de comparaciones que
debe efectuar el algoritmo. Como cada nodo tiene a lo sumo dos hijos, el n umero de no-
dos a distancia de la raz menor o igual que k es 1 + 2 + + 2
k1
. Por su estructura, el
arbol de decisi on para el algoritmo SequentialSearcher.index of tiene profundidad 2 + n.
El que corresponde al algoritmo de b usqueda binaria presenta profundidad 3 + 2 lg n.
Este ultimo arbol es optimo en el sentido de que no es posible encontrar otro de altura
inferior que resuelva el mismo problema.
2.7.1. Problemas difciles
La complejidad de ciertos problemas es exponencial. Tomemos por caso la enumeraci on
de todas la permutaciones de los elementos de un vector de tama no n. Hay n! per-
mutaciones y mostrarlas (o almacenarlas) requiere tiempo n n!. La aproximaci on de
Stirling indica que n!

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)

A.extend(B) Adici on por la cola. A nade B a A por la cola. O(n + m)


A.insert(i, v) Inserci on del elemento v entre A[i] y A[i+1]. O(n)
Borrado
del A[i] Borrado del elemento en posici on i. Elimina A[i]. O(n)
A.remove(v) Borrado del primer elemento con un valor dado v. O(n)
v = A.pop() Extracci on y devoluci on del ultimo elemento. 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))

Hacemos uso, por primera vez, de factoras para inyectar dependencias en


una clase. En el apartado A.14 se presenta esta t ecnica y se muestra la
implementaci on del m etodo get factories.
Las listas enlazadas garantizan que cada una de las operaciones presente un coste
temporal O(1), pero lo hacen a costa de cierta sobrecarga de memoria: cada elemento
apilado consume el espacio de dos punteros y el propio valor (adem as de la informa-
ci on propia de toda instancia de una clase). Este sobrecoste puede ser indeseable. Por
eso, el constructor de la clase FIFO admite una factora para la estructura de datos de
soporte. Por defecto es una lista enlazada, pero nada impide que recurramos a una lista
convencional:
demos/datastructures/fifo.py
from algoritmia.datastructures.queues import FIFO
dtFifo = FIFO([0, 1])
listBasedFifo = FIFO([0, 1], sequenceFactory=lambda data: list(data))
for i in range(2, 6):
listBasedFifo.push(i)
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 69
dtFifo.push(i)
while len(listBasedFifo) > 0:
print(dtFifo.pop(), listBasedFifo.pop(), end=" : ")
0 0 : 1 1 : 2 2 : 3 3 : 4 4 : 5 5 :
Si usamos una lista (vector), el coste de apilar pasa a ser O(n) en el peor de los ca-
sos, pero O(1) si consideramos el coste amortizado (ya que las listas usan la t ecnica del
doblado al a nadir/eliminar elementos). El coste de desapilar se encarece notablemente:
pasa a ser O(n). La tabla 3.2 resume los costes de las operaciones.
Operaci on Coste
FIFO con lista enlazada FIFO con vector
Q.push(v) O(1) O(1)

Q.pop(v) O(1) O(n)


Q.top(v) O(1) O(1)
Q[i] O(n) O(1)
len(Q) O(1) O(1)
for item in Q: O(n) O(n)
Tabla 3.2: Coste de la cola FIFO. (El asterisco
se nala un coste amortizado.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
14 Dise na una cola FIFO en la que los datos se almacenan en una lista Python. Analiza el coste
para el peor de los casos y el coste amortizado de las operaciones push y pop.
15 Dise na una cola FIFO con capacidad m axima para n elementos usando una lista de tama no
jo y de modo que las operaciones push y pop se ejecuten en tiempo constante. (La estructura de
soporte es lo que denominamos un buffer circular.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.3.2. Pila o cola LIFO
Las pilas o colas LIFO (last-in, rst-out) tienen tambi en m etodos push y pop, pero los ele-
mentos ingresan en la pila y son extrados de ella por el mismo extremo. Las pilas suelen
ofrecer la posibilidad de consultar el valor del primer elemento (el ultimo en entrar y
que ser a primero en salir si no se a nade ning un otro), al que se denomina cima, con un
m etodo top. Por conveniencia, usaremos tambi en la funci on len para conocer el n umero
de elementos de una pila y poder determinar si esta est a vaca o no.
En la gura 3.3 mostramos una representaci on gr aca de una pila y el resultado de
efectuar algunas operaciones sobre ella. Tanto pilas como colas FIFO son estructuras muy
utiles y haremos uso frecuente de ellas.
Nuestra implementaci on tambi en se apoya en las listas doblemente enlazadas:
algoritmia/datastructures/queues.py
class LIFO(IQueue):
def init (self , data: "Sequence of values"=[], **kw):
get factories(self , kw, sequenceFactory=lambda data: LinkedList(data))
self . seq = self .sequenceFactory(reversed(data))
70 Apuntes de Algoritmia 28 de septiembre de 2009
S = Stack()
S.push(1)
1
S.push(2)
1
2
S.push(3)
1
2
3
v = S.pop()
1
2
v 3
v = S.top()
1
2
v 2
v = S.pop()
1 v 2
(a) (b) (c) (d) (e) (f) (g)
Figura 3.3: Traza de operaciones sobre una pila. Los elementos entran y salen por el extremo superior.
def push(self , item: "T"):
self . seq.insert(0, 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[len(self . seq)-1 - index]
def len (self ) -> "int":
return len(self . seq)
def iter (self ) -> "iterable<T>":
for item in reversed(self . seq): yield item
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , list(reversed(self . seq)))
La tabla 3.3 resume los costes de las colas LIFO en funci on de la estructura de soporte.
Tabla 3.3: Coste de la cola LIFO. (El asterisco
se nala un coste amortizado.)
Operaci on Coste
LIFO con lista enlazada LIFO con vector
Q.push(v) O(1) O(1)

Q.pop(v) O(1) O(1)

Q.top(v) O(1) O(1)


Q[i] O(n) O(1)
len(Q) O(1) O(1)
for item in Q: O(n) O(n)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
16 Dise na una pila con capacidad para n elementos usando un vector y de modo que las ope-
raciones push, pop y top se ejecuten en tiempo constante.
17 En el m odulo collections de la librera est andar de Python encontramos una estructura de da-
tos que implementa una cola con dos extremos en el que las operaciones de adici on de elementos
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 71
(por el principio y el nal) y de extracci on (por cualquiera de los dos extremos) presenta coste
temporal O(1): la deque, del ingl es double-ended queue, es decir, cola con dos extremos.
La implementaci on de la cola con dos extremos se puede basar en listas doblemente enlazadas
con punteros a cabeza y cola. Implementa tu propia clase Deque asegurando coste temporal O(1)
para las operaciones de inserci on y extracci on de elementos por cualquiera de los dos extremos.

La implementaci on del deque en el int erprete de Python es un tanto sosti-


cada: una lista doblemente enlazada de vectores de elementos de talla ja.
Con ello se reduce el n umero de reservas y liberaciones de memoria, aunque a costa
de malgastar una cantidad acotada de memoria. Cabe decir, por otra parte, que
el perl de las colas deque no necesariamente incluye la posibilidad de un acceso
directo a sus elementos, pero que Python s lo ofrece en su implementaci on. Ello
puede resultar util para, por ejemplo, mostrar su contenido o recorrer los valores que
almacena con un iterador. Por cierto, deque se lee en ingl es igual que deck, que
signica baraja (de cartas).
18 Implementa una deque con capacidad limitada a n elementos (sin recurrir al tipo deque de
Python) que preserve el coste constante de cada una de las operaciones. Utiliza un vector de talla
n como soporte y suministra el par ametro n al constructor.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.4. Colecciones
Una colecci on es una agrupaci on de tems no necesariamente ordenados y en el que pue-
de que haya repeticiones. Corresponde al concepto matem atico de multiconjunto. A una
colecci on podemos a nadirle tems, eliminarle tems concretos, pedirle la cardinalidad, so-
licitarle que indique si contiene al menos una instancia de un tem determinado, pedirle
una enumeraci on de sus tems y vaciarla (eliminar todos sus tems).

Este es el perl de
las colecciones:
algoritmia/datastructures/collections.py
from ..collections import Container, Iterable, Sized
from abc import abstractmethod
...
class ICollection(Container, Iterable, Sized):
@abstractmethod
def add(self , item: "T"): pass
@abstractmethod
def remove(self , item: "T"): pass
@abstractmethod
def clear(self ): pass
Cuando implementemos una colecci on utilizaremos el m etodo add para a nadir un
elemento, el m etodo remove para eliminar un elemento y clear para eliminar todos los ele-
mentos. Hay otros m etodos que han de implementarse por formar parte de las clases abs-
tractas Sized, Iterable y Container: la funci on len proporciona la cardinalidad, el operador
72 Apuntes de Algoritmia 28 de septiembre de 2009
in (m etodo contains ) para saber si un tem pertenece a la colecci on, y el m etodo iter
para recorrer todos los tems (con un bucle for, por ejemplo). Las colecciones pueden
implementarse de muchos modos: mediante listas enlazadas, vectores, arboles, tablas de
dispersi on, etc. Mostramos una implementaci on que se apoya en secuencias (vectores o
listas enlazadas):
algoritmia/datastructures/collections.py
from .sequences import LinkedList
...
class Collection(ICollection):
def init (self , data: "iterable<T>"=[], **kw):
get factories(self , kw, sequenceFactory=lambda data: list(data))
self . seq = self .sequenceFactory(data)
self . size = len(self . seq)
if not hasattr(self . seq, clear): # No todas las secuencias soportan el m etodo clear.
def clear():
self . seq = self .sequenceFactory(())
self . size = 0
self .clear = clear
def add(self , item: "T"):
self . seq.append(item)
self . size += 1
def remove(self , item: "T"):
self . seq.remove(item)
self . size -= 1
def contains (self , item: "T") -> "bool":
return item in self . seq
def len (self ) -> "int":
return self . size
def clear(self ):
self . seq.clear()
self . size = 0
def iter (self ) -> "iterable<T>":
return self . seq. iter ()
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , list(self ))
class LinkedListCollection(Collection):
def init (self , data: "iterable<T>"=[]):
super(LinkedListCollection, self ). init (data, sequenceFactory=LinkedList)
Por defecto, el soporte de la colecci on es una lista. Con esa estructura, add es una ope-
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 73
raci on O(n) en el peor de los casos, donde n es la talla de la colecci on. De todos modos,
hemos de tener en cuenta que su coste amortizado de efectuar n adiciones es O(1). Esto
es as porque las listas usan la t ecnica del doblado para a nadir nuevos elementos. Todas
las dem as operaciones de la colecci on son O(n), excepto la que proporciona la talla de la
colecci on, que es O(1).
Si en lugar de una lista usamos una lista enlazada (como hacemos en LinkedListCollec-
tion), tenemos el mismo coste para todas las operaciones excepto add, que pasa a ser O(1)
en el peor de los casos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
19 Queremos implementar una colecci on de enteros pertenecientes al rango [0..m 1]. La es-
tructura debe garantizar la mayor eciencia posible en las siguientes operaciones:
a) A nadir un elemento.
b) Eliminar un elemento.
c) Saber si contiene un entero determinado.
d) Borrar todos los elementos.
e) Conocer el n umero total de elementos en la colecci on.
f) Devolver la suma de todos los valores almacenados.
Debes indicar c omo implementaras esta estructura de datos y c omo y con qu e coste temporal
puedes ejecutar cada una de las operaciones indicadas si nos imponen una restricci on: no po-
demos gastar m as que O(m) espacio. Ten en cuenta que puede haber valores repetidos en la
colecci on y, por tanto, el n umero de elementos almacenados podra ser mucho mayor que m.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.5. Vectores asociativos, mapeos o diccionarios
Un vector asociativo, mapeo o diccionario es un conjunto de claves y una colecci on de
valores donde cada clave est a asociada a uno de los valores. Puede verse, tambi en, co-
mo un conjunto de pares clave-valor en el que no hay dos elementos con id entica clave.
Conceptualmente, un vector es un caso particular de vector asociativo: pone en corres-
pondencia un rango de enteros con valores.
El m odulo collections ofrece un repertorio de clases abstractas para denir mapeos:
Mapping Un mapeo hereda de Container, Iterable y Sized. Puede verse como una colecci on
de pares clave-valor donde no hay dos claves iguales. Cada par clave-valor recibe
el nombre de tem. Al iterar un mapeo se enumeran sus claves y al preguntar si
un elemento est a en el mapeo, la pregunta se reere a si pertenece al conjunto de
claves. Toda clase no abstracta que hereda de Mapping debe denir el m etodo:
getitem (self , key): devuelve el valor asociado a la clave key.
Una clase que hereda de Mapping cuenta con los siguientes m etodos ya implemen-
tados:
74 Apuntes de Algoritmia 28 de septiembre de 2009
get(self , key, default=None): devuelve el valor asociado a la clave key si la cla-
ve key forma parte del conjunto de claves; si no, devuelve el valor default.
contains (self , key): dice si key pertenece o no al conjunto de claves.
iterkeys(self ): enumera las claves.
itervalues(self ): enumera los valores.
iteritems(self ): enumera los tems.
keys(self ): devuelve una lista con las claves.
values(self ): devuelve una lista con los valores.
items(self ): devuelve una lista con los tems.
eq (self , other): dice si self y other son iguales.
ne (self , other): dice si self y other son diferentes.
MutableMapping Un mapeo mutable hereda de Mapping y obliga a denir los siguientes
m etodos:
setitem (self , key, value): asocia el valor value a la clave key.
detitem (self , key): borra el tem con clave key.
Una clase que hereda de MutableMapping cuenta con los siguientes m etodos ya im-
plementados:
pop(self , key, default=default value): devuelve el valor asociado a key y elimina
el tem de clave key; si no existe dicha clave, lanza una excepci on o devuelve
default si este argumento opcional se ha proporcionado.
popitem(self ): extrae y devuelve un tem cualquiera.
clear(self ): elimina todos los tems.
update(self , other=(), **kw): a nade los tems que se suministran como tuplas
en other o como argumentos con nombre en kw.
setdefault(self , key, default=None): si key es una clave devuelve su valor aso-
ciado y si no, el valor default.
Podemos implementar vectores asociativos con diferentes estructuras de datos, entre
las que podemos citar las listas con salto, los arboles binarios de b usqueda autobalan-
ceados y las tablas de dispersi on. Nosotros estudiaremos esta ultima implementaci on.
3.5.1. Tabla de dispersion
Una tabla de dispersi on consiste en un vector A de talla m y una funci on de dispersi on
h : K [0..m1], siendo K el conjunto de claves. La funci on de dispersi on asocia a cada
clave un valor entero que usamos como ndice en el vector A. T engase en cuenta que
las claves puede ser cadenas o, en general, objetos de cualquier tipo. La funci on h suele
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 75
calcular un valor num erico a partir de la representaci on binaria de dichos objetos y, por
ejemplo, obtener el valor m odulo m del entero resultante. En principio, cuando guarda-
mos en la tabla un par clave-valor (k, v), almacenamos el valor v en la celda A[h(k)]. La
gura 3.4 ilustra esta idea.
Mara
Ana
Mar 21
0
1
18
2
19
3
4
h A
Figura 3.4: Tabla de dispersi on que asocia enteros (edades) a cadenas
(nombres). La funci on h asigna un ndice del vector A a cada cadena.
La funci on h puede proporcionar el mismo valor para dos claves distintas: es lo que
denominamos una colisi on. Hay varias formas de superar este problema:
Podemos almacenar en cada celda del vector A una secuencia con los pares clave-
valor asignados a ella, como se ilustra en la gura 3.5. Es lo que se conoce como
resoluci on de colisiones por encadenamiento. N otese que es necesario almacenar
en cada nodo de la lista un par clave-valor: cada vez que, por ejemplo, se inserta
un par clave-valor, hemos de comprobar si ya presente en la lista alg un par con la
misma clave; si es as, se sustituye su valor por el nuevo, y si no, se a nade el nuevo
par clave-valor.
Juan
Mara
Ana
Mar
0
1
2
3
4
h A
(Ana,21)
next
(Juan,20)
next
(Mar,18)
next
(Mara,19)
next
Figura 3.5: Tabla de dispersi on con resoluci on de colisiones por encadenamiento. Cada entrada de la tabla almacena
un puntero a una serie de pares clave-valor. La funci on de dispersi on asigna el mismo ndice a las cadenas Mar y
Juan, por lo que la lista asociada a dicho ndice tiene dos nodos.
Podemos almacenar cada par clave-valor que colisiona con otro par en una cel-
da distinta del vector. Esta t ecnica se conoce por direccionamiento abierto. Hay
varias posibilidades a la hora de decidir cu al es la siguiente celda. Se puede, por
ejemplo y como muestra la gura 3.6, asignar a la siguiente celda libre o aplicar
una nueva funci on de dispersi on (repitiendo el proceso mientras haya conictos).
Tambi en es necesario almacenar pares clave-valor por razones an alogas a las con-
sideradas con la t ecnica de resoluci on de colisiones por encadenamiento.
76 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 3.6: Tabla de dispersi on con direccionamiento abierto.
La funci on de dispersi on asigna el ndice 2 a la cadena Juan,
lo que produce una colisi on con la cadena Mar. Trata de re-
solver el conicto almacenando el par (Juan, 20) en la si-
guiente celda, que tambi en est a ocupada. El conicto se resuel-
ve almacenando el par en la celda de ndice 4.
Juan
Mara
Ana
Mar (Ana, 21)
0
1
(Mar, 18)
2
(Mara, 19)
3
(Juan, 20)
4
h A
El n umero de colisiones incide negativamente en las prestaciones de la tabla de dis-
persi on. Consideremos, por ejemplo, la resoluci on de colisiones por encadenamiento. Un
mayor n umero de colisiones implica una mayor longitud de algunas de las listas enlaza-
das y, en consecuencia, mayor tiempo esperado (tiempo en promedio) para recorrerlas.
Tomemos un caso extremo: que cualquier par clave-valor corresponda a la misma cel-
da. La tabla de dispersi on degenera entonces en una simple lista. El coste temporal que
presentan las operaciones de inserci on, b usqueda y borrado cuando hay n elementos en
el vector asociativo en este supuesto patol ogico es O(n). Ocurre un problema an alogo si
usamos direccionamiento abierto.
El n umero de colisiones depende de dos factores:
De la calidad de la funci on de dispersi on. Se debe usar funciones de dispersi on
que distribuyan uniformemente las claves entre las celdas de la tabla.
Del factor de carga, = n/m, donde n es el n umero de elementos en la tabla y m es
la capacidad del vector A.
Si se establece una buena poltica de resoluci on de colisiones, las tablas de dispersi on
permiten implementar en tiempo promedio constante ciertas operaciones como el acceso,
la inserci on de elementos y su eliminaci on.
Vectores asociativos con tablas de dispersi on en Python: diccionarios
En la terminologa de Python, los vectores asociativos reciben el nombre de diccionarios.
Python ofrece una implementaci on directa basada en tablas de dispersi on con direccio-
namiento abierto e impone una restricci on a las claves: que sean objetos inmutables (va-
lores num ericos, cadenas o tuplas). Como redimensiona con la t ecnica del doblado, el
coste amortizado del crecimiento del vector achacable a cada inserci on es constante.
La tabla 3.4 recoge algunas de las operaciones que podemos efectuar con diccionarios.
Los costes presentados en la tabla son generalmente costes esperados, no costes en el peor
de los casos.
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 77
Vector asociativo (tipo dict de Python)
Operaci on Acci on Coste
Constructor
A = dict(seq) Construye un diccionario a partir de una secuencia de pares clave-valor. (n)
Acceso
A[k] Indexaci on. Accede al elemento de clave k. O(1)
len(A) Talla. Devuelve el n umero de claves de A. O(1)
A.items() Lista de componentes. Devuelve una lista de tuplas (clave, valor). (n)
A.keys() Lista de claves. Devuelve una lista con las claves del diccionario. (n)
A.values() Lista de valores. Proporciona una lista con todos los valores de A. (n)
A.get(k) Indexaci on. Accede al elemento de clave k. O(1)
A.get(k, v) Indexaci on. Accede al elemento con clave k. Si no existe, devuelve el valor v. O(1)
A.setdefault(k, v) Asignaci on con valor por defecto. Devuelve A[k] si existe. Si no, devuelve v
y lo asigna a A[k].
O(1)
Asignaci on
A[k] = v Asignaci on a celda. Asigna al elemento con clave k el valor v. O(1)
A.update(B) Actualizaci on. Hace A[k] = B[k] para toda clave k de B. (m)
Borrado
del A[k] Borrado de elemento de clave k. Elimina la clave k y su valor asociado. O(1)
A.clear() Borrado. Elimina todas las claves (y valores asociados) de A. (n)
A.popitem() Extracci on aleatoria. Devuelve un par (clave, valor) arbitrario y lo elimina de
la tabla.
O(1)
Pertenencia
k in A Pertenencia. Indica si k es una clave de A. O(1)
k not in A Pertenencia. Indica si k no es una clave de A. O(1)
Iteraci on
for k in A: Iteraci on. Recorre las claves de A, que van asignando valor a k. O(n)
Tabla 3.4: Operaciones sobre diccionarios Python. A es una tabla con n elementos y B es una tabla con m elementos. El
ndice o clave k es un valor inmutable cualquiera y v es un valor cualquiera. Los costes de la tercera columna son costes
esperados, no costes en el peor de los casos.

El int erprete de Python procura que el factor de carga en un vector asociativo


sea menor que 2/3. Cuando se sobrepasa, redimensiona la tabla y reasigna
las claves a nuevas celdas del vector. Cada colisi on en el vector interno se resuel-
ve mediante el c alculo de una nueva funci on de dispersi on. Se puede consultar el
procedimiento exacto leyendo la documentaci on incluida en comentarios del chero
dictobject.c, que se encuentra en el directorio Objects del c odigo fuente del
int erprete de Python.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
20 Usa un diccionario como soporte para una implementaci on de una colecci on (multiconjun-
to). Cada clave corresponder a a un tem y su valor asociado, un entero, indicar a el n umero de
veces que dicho tem pertenece a la colecci on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
78 Apuntes de Algoritmia 28 de septiembre de 2009
3.5.2. Vectores asociativos con claves enteras en un rango
Cuando las claves son un subconjunto de enteros comprendidos entre 0 y n 1, para
un valor dado de n, podemos ofrecer una implementaci on de los vectores asociativos
muy eciente, capaz de garantizar tiempos de acceso O(1) para el peor de los casos.
Basta con usar dos vectores de talla n, uno que indica si hay un valor asociado a una
clave determinada (un ndice del vector) y otro que, cuando la respuesta es armativa,
contiene el valor (en su celda con id entico ndice). Conviene que dispongamos de un
objeto que nos permita gestionar estos dos vectores del mismo modo que gestionamos un
diccionario. Hay, no obstante, un problema de ndole pr actica: para garantizar eciencia
en las operaciones conviene que nos declaren la capacidad del diccionario, esto es, el
mayor n umero de tems que puede albergar, antes de a nadir claves.
algoritmia/datastructures/mappings.py
from ..collections import MutableMapping, Mapping, Sequence
...
class IntKeyMapping(MutableMapping):
def init (self , data: "iterable<(int, T)> or mapping<(int, T)>"=[],
capacity: "int"=0):
if isinstance(data, Mapping): data = tuple(data.items())
elif not isinstance(data, Sequence): data = tuple(data)
if data: capacity = max(capacity, max(k for (k, v) in data)+1)
self . has key, self . value = [False] * capacity, [None] * capacity
for (k,v) in data: self . has key[k], self . value[k] = True, v
def getcapacity(self ) -> "int":
return len(self . has key)
def setcapacity(self , capacity: "int"):
if capacity < len(self . has key):
self . has key, self . value = self . has key[:capacity], self . value[:capacity]
elif capacity > len(self . has key):
newcells = capacity - len(self . has key)
self . has key.extend([False] * newcells)
self . value.extend([None] * newcells)
capacity = property(getcapacity, setcapacity)
def getitem (self , key: "int") -> "T":
if not ((0 <= key < len(self . value)) and self . has key[key]): raise KeyError(key)
return self . value[key]
def setitem (self , key: "int", value: "T") -> "T":
if not (0 <= key < len(self . value)): raise KeyError(key)
self . has key[key], self . value[key] = True, value
return value
def delitem (self , key: "int"):
if not (0 <= key < len(self . value)) or not self . has key[key]: raise KeyError(key)
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 79
self . has key[key], self . value[key] = False, None
def contains (self , key: "int") -> "bool":
return (0 <= key < len(self . value)) and self . has key[key]
def iter (self ) -> "iterable<int>":
for i in range(len(self . has key)):
if self . has key[i]: yield i
def len (self ) -> "int":
return sum(1 for k in self )
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , list(self .items()))
Dado que distinguimos entre pares clave-valor en el diccionario y capacidad del mis-
mo, hemos a nadido una propiedad que permite consultar y modicar la capacidad.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21 Analiza el coste de cada una de las operaciones denidas en la clase IntKeyMapping. Expresa
el coste en funci on de la capacidad del vector asociativo y del n umero de elementos que contiene.
22 El coste de ciertas operaciones de IntKeyMapping depende de la capacidad del diccionario,
y no del n umero de elementos que contiene. Podemos reducir estos costes (a costa del de otras
operaciones) si los elementos del conjunto forman una lista doblemente enlazada. C omo modi-
caras la estructura de acuerdo con esta idea para que ninguna operaci on viera negativamente
afectado su coste asint otico? Qu e coste presentaran entonces las operaciones de IntKeyMapping?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
En las implementaciones de los algoritmos que estudiaremos usaremos, en muchas
ocasiones, diccionarios Python. No obstante, en muchos casos sera posible utilizar ins-
tancias de la clase IntKeyMapping en su lugar si codic asemos adecuadamente los datos
del problema, es decir, si represent asemos algunos los objetos que participan como claves
de un diccionario mediante enteros. Esta codicaci on nos permitira garantizar un buen
comportamiento en el peor de los casos, pero mermara sensiblemente la legibilidad de
los algoritmos. Por ello, en muchos an alisis asumiremos que esta codicaci on es posible
y que se usa la estructura m as apropiada en t erminos de eciencia.
3.6. Conjuntos
Un conjunto es una colecci on de datos no repetidos. Deniremos para las clases de tipo
conjunto las mismas operaciones que presenta una colecci on, pero a nadiremos otras
como la intersecci on o la uni on. Existen numerosas implementaciones posibles para los
conjuntos. Nosotros estudiaremos cinco, aunque s olo presentamos c odigo de dos: una
basada en vectores y otra basada en los denominados vectores caractersticos (y que s olo
resulta de aplicaci on para conjuntos de enteros).
Las clases abstractas Set y MutableSet del m odulo collections jan los m etodos que
deben implementar los conjuntos inmutables y mutables, respectivamente:
80 Apuntes de Algoritmia 28 de septiembre de 2009
Set Un conjunto es una colecci on que implementa Container, Iterable y Sized y en la que
cada elemento s olo puede ocurrir una vez (no hay elementos repetidos). Se corres-
ponde con el concepto matem atico de conjunto. Las clases que especializan a Set
reciben una implementaci on de los siguientes m etodos:
le (self , other): dice si self es un subconjunto de other (se invoca con el ope-
rador <=).
lt (self , other): dice si self es un subconjunto propio de other (se invoca con
el operador <).
ge (self , other): dice si other es un subconjunto de self (se invoca con el ope-
rador >=).
gt (self , other): dice si other es un subconjunto propio de self (se invoca con
el operador >).
eq (self , other): dice si self y other son iguales (se invoca con el operador ==).
ne (self , other): dice si self y other son distintos (se invoca con el operador
!=).
isdisjoint(self , other): dice si self y other son disjuntos.
and (self , other): devuelve un conjunto (del mismo tipo que self ) con la in-
tersecci on de self y other (se invoca con el operador &).
or (self , other): devuelve un conjunto (del mismo tipo que self ) con la uni on
de self y other (se invoca con el operador |).
sub (self , other): devuelve un conjunto (del mismo tipo que self ) con los
elementos de self que no est an en other (se invoca con el operador -).
xor (self , other): devuelve un conjunto (del mismo tipo que self ) con los
elementos de self y other que no est an en la intersecci on de ambos conjuntos
(se invoca con el operador ).
MutableSet Un conjunto mutable es un Set que dene ciertos m etodos para editar su
contenido. Los clases que especializan a MutableSet deben implementar estos m eto-
dos:
add(self , value): a nade value y devuelve True si no formaba parte del conjunto
y False en caso contrario.
discard(self , value): elimina value y devuelve True si ya formaba parte del con-
junto y False en caso contrario.
insert(self , index, value): inserta value inmediatamente a continuaci on del ele-
mento de ndice index.
Las clases que especializan a Set reciben una implementaci on de los siguientes
m etodos:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 81
remove(self , value): elimina value si est a en el conjunto; si no, lanza una excep-
ci on KeyError.
pop(self ): extrae y devuelved un elemento cualquiera del conjunto.
clear(self ): elimina todos los elementos del conjunto.
ior (self , values): a nade todos los elementos de values (se invoca con el ope-
rador |=).
iand (self , other set): elimina todos los elementos que no est an en other set
(se invoca con el operador &=).
ixor (self , values): elimina todos los elementos que est an en values y self y
a nade los que est an en values, pero no en self (se invoca con el operador =).
isub (self , values): elimina todos los elementos de values (se invoca con el
operador -=).
Presentamos ahora algunas implementaciones de conjuntos apoyados en diferentes
estructuras ya estudiadas.
3.6.1. Mediante vectores
Podemos usar vectores para implementar conjuntos. Deberemos tener la precauci on de
comprobar si un elemento ya existe antes de a nadirlo, pues hemos de evitar la existencia
de elementos repetidos. Esta comprobaci on hace costosa la inserci on de elementos: la
comprobaci on supone efectuar O(n) comparaciones:
algoritmia/datastructures/sets.py
from .. collections import MutableSet
from algoritmia.datastructures.sequences import LinkedList
...
class Set(MutableSet):
def init (self , it: "iterable<T>"=[], **kw):
get factories(self , kw, collectionFactory=Collection)
self . collection = self .collectionFactory()
for item in it:
if item not in self . collection:
self . collection.add(item)
if not hasattr(self . collection, clear):
def clear(): self . collection = self .collectionFactory(())
self .clear = clear
def add(self , item: "T"):
if item not in self . collection: self . collection.add(item)
def add unchecked(self , item: "T"):
self . collection.add(item)
def remove(self , item: "T"):
82 Apuntes de Algoritmia 28 de septiembre de 2009
if item not in self . collection: raise KeyError(item)
self . collection.remove(item)
def discard(self , item: "T"):
self . collection.remove(item)
def contains (self , item: "T") -> "bool":
return item in self . collection
def len (self ) -> "int":
return len(self . collection)
def clear(self ):
self . collection.clear()
def iter (self ) -> "iterable<T>":
for item in self . collection: yield item
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , self . collection)
class ListSet(Set):
def init (self , it: "Iterable<T>"=[]):
super(ListSet, self ). init (it)
Hemos a nadido una operaci on especial, add unchecked que obliga a cierta precau-
ci on en su uso. El m etodo en cuesti on a nade un elemento al conjunto sin comprobar si
ya pertenece al mismo. S olo debe usarse cuando se sabe de antemano que el elemento no
formaba parte del conjunto. Esto ocurre, por ejemplo, cuando seleccionamos elementos
de otro conjunto con cierto criterio y los vamos a nadiendo a uno nuevo. Si hici esemos
una comprobaci on de pertenencia durante la inserci on, incurriramos en un coste tempo-
ral importante que resulta innecesario.
3.6.2. Mediante listas enlazadas
Podemos recurrir a las listas enlazadas para implementar conjuntos, pero tendremos el
mismo problema que al usar vectores: el coste de operaciones como a nadir o eliminar un
tem son O(n).
algoritmia/datastructures/sets.py
class LinkedListSet(Set):
def init (self , it: "Iterable<T>"=[]):
super(LinkedListSet, self ). init (it, collectionFactory=LinkedListCollection)
3.6.3. Mediante tablas de dispersi on
Python ofrece una implementaci on de conjuntos basada en tablas de dispersi on mediante
el tipo nativo set. Los elementos del conjunto son las claves en la tabla de dispersi on (sus
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 83
valores asociados son irrelevantes). En la tabla 3.5 se relacionan las operaciones sobre
conjuntos que ofrece el tipo set de Python. Hay que advertir nuevamente que los tiempos
de la tabla son, generalmente, tiempos esperados, y no tiempos en el peor de los casos (la
implementaci on se basa en diccionarios).
Conjunto (tipo set de Python)
Operaci on Acci on Coste
Constructor
A = set(seq) Construye un conjunto con los elementos de un iterable. O(n)
Acceso
len(A) Talla. Devuelve el n umero de elementos en A. O(1)
Asignaci on
A.add(v) A nadir. A nade el elemento v al conjunto A. O(1)
A.update(s) A nadir. A nade todos los elementos de s al conjunto A. O(p)
Borrado
A.remove(v) Eliminar. Elimina el elemento v del conjunto A. El elemento debe pertenecer a
A.
O(1)
A.discard(v) Eliminar. Elimina el elemento v del conjunto A. O(1)
A.clear() Borrado. Elimina todos los elementos de A. O(n)
v = A.pop() Selecci on (con extracci on) aleatoria. Devuelve un elemento arbitrario y lo eli-
mina del conjunto.
O(1)
Pertenencia/Inclusi on
v in A Pertenencia. Indica si v es un elemento de A. O(1)
v not in A Pertenencia. Indica si v no es un elemento de A. O(1)
A <= B Inclusi on. Devuelve cierto si A es un subconjunto de B. O(n)
Otros
C = A | B Uni on. Devuelve los elementos en la uni on de A y B. O(n + m)
A |= B Uni on in situ. A pasa a ser el conjunto resultante de unir A y B. O(m)
C = A & B Intersecci on. Devuelve los elementos en la intersecci on de A y B. O(n)
A &= B Intersection in situ. A pasa a ser el conjunto resultante de intersectar A y B. O(n)
C = A - B Diferencia. Devuelve el conjunto resultante de eliminar de A los elementos de
B.
O(n)
A -= B Diferencia in situ. A pasa a ser el conjunto resultante de eliminar de A los ele-
mentos de B.
O(n)
Iteraci on
for k in A: Iteraci on. Recorre los elementos de A, que van asignando valor a k. O(n)
Tabla 3.5: Operaciones sobre conjuntos implementados con el tipo set de Python. A es un conjunto con n elementos, B
es un conjunto con m elementos, C es un conjunto, s es un iterable con p elementos y v es un valor cualquiera. El coste
de cada una de las operaciones es un coste esperado, no un coste en el peor de los casos.
84 Apuntes de Algoritmia 28 de septiembre de 2009
3.6.4. Mediante arboles equilibrados
Hay una alternativa interesante para la implementaci on de conjuntos: los arboles equili-
brados que garantizan tiempos O(lg n) para la comprobaci on de pertenencia, la inserci on
y la extracci on de elementos. Los arboles AVL, los arboles 2-3-4, los arboles rojinegros o
los arboles rojinegros escorados a la izquierda, por ejemplo, presentan este comporta-
miento. No obstante, no los estudiaremos en este captulo. El lector interesado puede
consultar otras fuentes bibliogr acas para obtener m as informaci on.
3.6.5. Mediante vectores caractersticos
Un vector caracterstico permite representar f acilmente y de forma eciente un conjunto
cuyos elementos son enteros de un rango de valores consecutivos. Es un simple vector
de valores booleanos cuyos ndices (un rango de enteros consecutivos) forman un super-
conjunto del conjunto representado.
Tomemos por caso un conjunto cuyos elementos son valores naturales comprendidos
entre 0 y 9. Un vector C de 10 valores booleanos es suciente para representar dicho
conjunto: si C[i] vale True, el valor i pertenece al conjunto, y si vale False, no.
Resulta sencillo implementar las operaciones b asicas propias de los conjuntos con
vectores caractersticos. Un contador de elementos puede abaratar la consulta de la car-
dinalidad del conjunto o la determinaci on de si es o no vaco:
algoritmia/datastructures/sets.py
from ..collections import Sequence
...
class IntSet(MutableSet):
def init (self , it: "Iterable<int>"=[], capacity: "int"=0):
if not isinstance(it, Sequence): it = tuple(it)
if it: capacity = max(capacity, max(it)+1)
self . contains = [False] * capacity
for item in it: self . contains[item] = True
def getcapacity(self ) -> "int":
return len(self . contains)
def setcapacity(self , capacity: "int"):
if capacity < len(self . contains):
self . contains = self . contains[:capacity]
elif capacity > len(self . contains):
self . contains.extend([False] * (capacity - len(self . contains)))
capacity = property(getcapacity, setcapacity)
def add(self , item: "int"):
self . contains[item] = True
add unchecked = add
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 85
def remove(self , item: "int"):
if not item in self : raise KeyError(item)
self . contains[item] = False
def discard(self , item: "int"):
if item in self : self . contains[item] = False
def contains (self , item: "int") -> bool:
return (0 <= item < len(self . contains)) and self . contains[item]
def len (self ) -> "int":
return sum(1 for item in self . contains if item)
def clear(self ):
for i in range(len(self . contains)): self . contains[i] = False
def iter (self ) -> "Iterable<int>":
for item in range(len(self . contains)):
if self . contains[item]: yield item
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , list(self ))
La complejidad computacional del vector caracterstico no s olo depende del n ume-
ro de elementos del conjunto; tambi en depende del tama no m aximo del conjunto. La
tabla 3.6 muestra el coste de algunas operaciones sobre conjuntos (usamos la misma
nomenclatura para las operaciones que se usa en el lenguaje Python). Este tipo de im-
plementaci on es muy eciente para seg un qu e operaciones, pues algunas dependen del
rango del conjunto.
Vector caracterstico
Operaci on Coste
len(A) O(N)
A.add(v) O(1)
A.remove(v) O(1)
A.clear() O(N)
v in A O(1)
for item in A: O(N)
Tabla 3.6: Coste de algunas operaciones (usando la nota-
ci on del tipo set) sobre un conjunto implementado con un
vector caracterstico. A es un conjunto de n elementos en
el rango [0..N 1].
3.7. Digrafos y grafos no dirigidos
Un grafo dirigido no es m as que un conjunto nito de elementos, a los que denominamos
v ertices, relacionados entre s. Dos v ertices relacionados forman una arista, y la arista
est a dirigida si la relaci on no es conmutativa. Los grafos dirigidos reciben el nombre de
digrafos. Cuando hablemos de grafos en los que la relaci on entre v ertices es conmutati-
va, utilizaremos la expresi on grafos no dirigidos. Es importante notar que todo grafo
86 Apuntes de Algoritmia 28 de septiembre de 2009
no dirigido es un caso particular de digrafo. (Antes de continuar, es conveniente que
el lector repase las deniciones sobre digrafos presentadas en el ap endice de conceptos
matem aticos.)
Los digrafos permiten modelar innidad de sistemas del mundo real en los que di-
ferentes elementos de un conjunto est an relacionados entre s: ciudades conectadas por
carreteras, proyectos divididos en tareas que dependen unas de otras, relaciones familia-
res en diagramas geneal ogicos, aeropuertos conectados por vuelos directos, etc.
Asumiremos que cualquier implementaci on de un digrafo ofrece el perl que se des-
cribe en esta interaz:
algoritmia/datastructures/graphs.py
from abc import abstractmethod, abstractproperty, ABCMeta
class IDigraph(metaclass=ABCMeta):
@abstractproperty
def V(self ) -> "IVertexSet": pass
@abstractproperty
def E(self ) -> "IEdgeSet": pass
@abstractproperty
def is directed(self ) -> "bool": pass
@abstractmethod
def succs(self , u: "T") -> "iterable<T>": pass
@abstractmethod
def preds(self , v: "T") -> "iterable<T>": pass
@abstractmethod
def out degree(self , u: "T") -> "int": pass
@abstractmethod
def in degree(self , v: "T") -> "int": pass
def degree(self , v: "T") -> "int": return self .out degree(v)
class IVertexSet(Container, Iterable, Sized): pass
class IEdgeSet(Container, Iterable, Sized): pass
Los objetos de una clase que implemente IDigraph ofrecen dos propiedades especia-
les: una conjunto de v ertices (IVertexSet) y un conjunto de aristas (IEdgeSet). Estas propie-
dades no tiene por qu e ser conjuntos independientes, cada uno con su propia copia de
v ertices y aristas. Basta con que se comporten como colecciones de v ertices y aristas, res-
pectivamente, y se puede recurrir a ellas para recorrer sus elementos, efectuar consultas
de pertenencia con el operador in o determinar su cardinalidad.
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 87
Otro atributo permite saber si el digrafo es dirigido o si, por contra, es un grafo no
dirigido. Tanto succs como preds reciben como argumento un v ertice y proporcionan una
iteraci on sobre v ertices: los sucesores y los predecesores, respectivamente, de dicho v erti-
ce. Los m etodos out degree e in degree proporcionan los grados de salida y entrada de un
v ertice.
N otese que hemos denido un alias para el m etodo out degree, que tambi en puede
invocarse con el identicador degree. Puede resultar conveniente usar este alias cuando
objeto es un grafo no dirigido.
La interfaz IDigraph asume que un digrafo, una vez construido, no puede modicarse.
La siguiente interfaz a nade la posibilidad de a nadir v ertices y aristas a un digrafo:
algoritmia/datastructures/graphs.py
class IEditableDigraph(IDigraph):
@abstractproperty
def V(self ) -> "IEditableVertexSet": pass
@abstractproperty
def E(self ) -> "IIEditableEdgeSet": pass
class IEditableVertexSet(Container, Iterable, Sized):
@abstractmethod
def add(self , v: "T"): pass
@abstractmethod
def remove(self , v: "T"): pass
class IEditableEdgeSet(Container, Iterable, Sized):
@abstractmethod
def add(self , u: "T or (T, T)", v: "vertex or None"): pass
@abstractmethod
def remove(self , u: "T or (T, T)", v: "T or None"): pass
3.7.1. Sobre una implementaci on naf de los digrafos
Un digrafo G se dene como un simple par de conjuntos: el de v ertices, V, y el de aristas,
E. Una implementaci on naf podra pasar por denir una clase que mantiene dos conjun-
tos (con cualquiera de las estructuras de datos para conjuntos que hemos presentado).
Sin embargo, esta implementaci on de los digrafos resulta inapropiada cuando tenemos
en cuenta el patr on tpico de uso en muchos algoritmos. Es frecuente, por ejemplo, que
deseemos enumerar los sucesores de un v ertice v. Si E es un mero conjunto de aristas,
enumerar los sucesores pasa por recorrer todos los elementos de E para detectar y emitir
aquellas aristas que parten de v. Es mejor considerar una estructura alternativa para el
conjunto de aristas.
88 Apuntes de Algoritmia 28 de septiembre de 2009
3.7.2. Digrafos representados con mapeo a conjuntos de
adyacencia
El conjunto de aristas puede estructurarse como un mapeo que pone en correspondencia
a cada v ertice con el conjunto de sus sucesores. Si optamos por esta representaci on para
E, no es necesario almacenar explcitamente V, pues este conjunto est a formado por las
claves del mapeo. Esa es una de las razones por las que el atributo V de IDigraph sea una
vista del conjunto de v ertices, y no un conjunto propiamente dicho.
Podemos denir una clase gen erica, AdjacencyDigraph, para los digrafos que imple-
mentan el conjunto de v ertices como un mapeo a conjuntos de adyacencia. Cuando se
instancie, se suministrar an como par ametros dos factoras: una que construye el mapeo
de los v ertices a los conjuntos de v ertices sucesores y otra que construye dichos conjun-
tos. Por defecto, el mapeo ser a una instancia de la clase dict y los conjuntos ser an ins-
tancias de la clase set. Con esta elecci on de estructuras de datos ofrecemos unas buenas
prestaciones para el caso promedio, pero no para el peor de los casos. Si el conjunto de
v ertices es un subconjunto de rango de enteros, se puede recurrir a estructuras de datos
m as ecientes, como el IntKeyMapping o el IntSet.
Presentemos ahora el constructor de la clase AdjacencyDigraph:
algoritmia/datastructures/graphs.py
class AdjacencyDigraph(IEditableDigraph):
def init (self , V: "iterable<T>"=[],
E: "iterable<(T, T)> or mapping<T, iterable<T>>"=[],
directed: "bool"=True, **kw):
get factories(self , kw, mappintFactory=lambda V: dict(),
setFactory=lambda V: set(),
mappingRedimFactory=lambda aMapping, maxkey: aMapping,
setRedimFactory=lambda aSet, maxkey: aSet)
if isinstance(E, Mapping): E = tuple((u, v) for u in E for v in E[u])
elif not isinstance(E, Sequence): E = tuple(E)
if not isinstance(V, Sequence): V = tuple(V)
if not V: V = set(chain((u for (u,v) in E), (v for (u, v) in E)))
self . directed = directed
self . succs = self .mappingFactory(V)
for v in V: self . succs[v] = self .setFactory(V)
for (u,v) in E: self . succs[u].add(v)
if not directed:
for (u,v) in E: self . succs[v].add(u)
self . vertexSet = AdjacencyDigraph.VertexSet(self )
self . edgeSet = AdjacencyDigraph.EdgeSet(self )
El primer par ametro del constructor es una iteraci on con los v ertices. Hemos de decir
que este par ametro es opcional: si se especica el conjunto de aristas (pares de v ertices),
el conjunto de v ertices es la uni on de todos los v ertices que se referencian en ellos. El
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 89
segundo par ametro es la especicaci on de las aristas. Hay dos posibilidades: que se su-
ministre una iteraci on de pares de v ertices o que se proporcione un mapeo que asocia a
cada v ertice una iteraci on de v ertices (sus sucesores). Las primeras acciones del construc-
tor consisten en representar estos par ametros de un forma estandarizada: E es una tupla
de pares de v ertices y V es un iterable con los v ertices.
El tercer par ametro es un booleano que nos indica si el digrafo lo es propiamente,
es decir, si est a dirigido, o si no lo es. (Recordemos que un grafo no dirigido es un caso
particular de digrafo.) El constructor almacena el valor del par ametro en un campo.
Los siguientes par ametros, que son opcionales, son factoras. El primero permite cons-
truir un mapeo de v ertices a conjuntos de v ertices y por defecto es una simple tabla de
dispersi on. El segundo facilita la construcci on de conjuntos de v ertices. El constructor
usa estas factoras para construir una representaci on del conjunto de aristas que se alma-
cena en el campo succs. N otese que si el grafo no es dirigido cada arista ingresa en el
grafo dos veces (una por cada sentido de la arista si se entiende como dirigida).
Los dos siguientes par ametros son funciones que nos permiten redimensionar el grafo
(si es necesario) cuando se a naden nuevos v ertices. En constructor se limita a memori-
zarlos.
El constructor acaba asignando valor a dos campos: vertexSet y edgeSet. Estos cam-
pos reciben sendas instancias de las clases internas VertexSet y EdgeSet, que proporcionan
formas elegantes de acceder a los conjuntos V y E del digrafo. Las instancias son accesible
como propiedades con los identicadores V y E:
algoritmia/datastructures/graphs.py
class AdjacencyDigraph(IEditableDigraph):
...
V = property(lambda self : self . vertexSet)
E = property(lambda self : self . edgeSet)
La propiedad is directed permite saber si el grafo es dirigido o no:
algoritmia/datastructures/graphs.py
class AdjacencyDigraph(IEditableDigraph):
...
is directed = property(lambda self : self . directed)
Gracias a la clase VertexSet el usuario de un digrafo G puede enumerar sus v ertices
con un bucle for sobre G.V, o conocer el n umero de v ertices con len(G.V). Tambi en
puede a nadir un v ertice v con G.V.add(v). La clase EdgeSet ofrece una funcionalidad
similar para G.E en lo tocante a las aristas. Esta es la implementaci on de estas dos clases:
algoritmia/datastructures/graphs.py
class AdjacencyDigraph(IEditableDigraph):
...
class VertexSet(IEditableVertexSet):
def init (self , G: "AdjacencyDigraph"):
self .G = G
90 Apuntes de Algoritmia 28 de septiembre de 2009
def contains (self , v: "T") -> "bool":
return v in self .G. succs
def iter (self ) -> "iterable<T>":
return (v for v in self .G. succs.keys())
def len (self ) -> "int":
return len(self .G. succs)
def add(self , v: "T"):
self .G. add vertex(v)
def remove(self , v: "T"):
self .G. remove vertex(v)
def repr (self ) -> "str":
return repr(list(self ))
class EdgeSet(IEditableEdgeSet):
def init (self , G: "AdjacencyDigraph"):
self .G = G
def contains (self , e: "(T, T)") -> "bool":
return e[0] in self .G. succs and e[1] in self .G. succs[e[0]]
def iter (self ) -> "iterable<(T, T)>":
if self .G.is directed:
for u in self .G. succs:
for v in self .G. succs[u]: yield (u, v)
else:
prev = self .G.setFactory(self .G.V)
for u in self .G. succs:
prev.add(u)
for v in self .G. succs[u]:
if v not in prev: yield(u, v)
def len (self ) -> "int":
return sum(len(self .G. succs[u]) for u in self .G. succs)
def add(self , u: "vertex or (T, T)", v: "T or None"=None):
if v == None: (u,v) = u
self .G. add edge((u, v))
def add unchecked(self , u: "T or (T, T)", v: "T or None"=None):
if v == None: (u,v) = u
self .G. add edge unchecked((u, v))
def remove(self , u: "T or (T, T)", v: "T or None"=None):
if v == None: (u,v) = u
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 91
self .G. remove edge((u, v))
def repr (self ) -> "str":
return repr(list(self ))
La clase se completa con algunos m etodos de la interfaz IDigraph que a un no hemos
denido y con otros auxiliares para VertexSet y EdgeSet:
algoritmia/datastructures/graphs.py
class AdjacencyDigraph(IEditableDigraph):
...
def succs(self , u: "T") -> "iterable<T>":
return self . succs[u]
def preds(self , v: "T") -> "iterable<T>":
return (u for u in self . succs if v in self . succs[u])
def out degree(self , u: "T") -> "int":
return len(self . succs[u])
def in degree(self , v: "T") -> "int":
return sum(1 for u in self .preds(v))
def add vertex(self , v: "T"):
if v not in self . succs:
self .mappingRedimFactory(self . succs, v)
self . succs[v] = self .setFactory(self .V)
for u in self . succs:
self .setRedimFactory(self . succs[u], v)
def remove vertex(self , v: "T"):
if v in self . succs:
del self . succs[v]
for u in self . succs:
if v in self . succs[u]: self . succs[u].remove(v)
def add edge(self , edge: "(T, T)"):
if edge[0] not in self . succs: self . add vertex(edge[0])
if edge[1] not in self . succs: self . add vertex(edge[0])
self . succs[edge[0]].add(edge[1])
if not self .is directed: self . succs[edge[1]].add(edge[0])
def add edge unchecked(self , edge: "(T, T)"):
if hasattr(self . succs[edge[0]], add_unckecked):
self . succs[edge[0]].add unchecked(edge[1])
if not self .directed: self . succs[edge[1]].add unchecked(edge[0])
else:
self . add edge(edge)
def remove edge(self , edge: "(T, T)"):
92 Apuntes de Algoritmia 28 de septiembre de 2009
self . succs[edge[0]].remove(edge[1])
if not self .is directed: self . succs[edge[1]].remove(edge[0])
def repr (self ) -> "str":
return {}(V={!r}, E={!r}, directed={!r}).format(self . class . name , \
list(self .V), list(self .E), self .is directed)
Naturalmente, el coste de cada una de las operaciones de la clase AdjacencyDigraph (y
de VertexSet y EdgeSet) depender a de las estructuras elegidas para el mapeo y los conjun-
tos de adyacencia, que se proporcionan mediante las factoras de mapeo y de conjuntos.
En los siguientes apartados consideraremos algunos casos particulares de especial rele-
vancia.
3.7.3. Digrafos implementados con matriz de adyacencia
Cuando el conjunto de v ertices es un subconjunto de los enteros entre 0 y n 1, para un
valor de n dado, podemos implementar el mapeo de v ertices a conjuntos de adyacencia
con una estructura que garantiza tiempos de acceso constantes: IntSet. Al mismo tiem-
po, podemos implementar los conjuntos de adyacencia con vectores caractersticos, esto
es, por medio de IntSet. En tal caso, determinar la pertenencia de una arista al grafo o
a nadir/eliminar aristas, pasan a ser operaciones ejecutables en tiempo constante. A cam-
bio, hay un consumo de memoria O(n
2
). Esta representaci on se conoce por matriz de
adyacencia, pues puede verse como una matriz de valores booleanos en la que el valor
True indica una relaci on de adyacencia (una arista) desde el v ertice que corresponde al
ndice de la la al v ertice que corresponde al ndice de la columna, y el valor False indica
su ausencia (gura 3.7 (a)):
algoritmia/datastructures/graphs.py
from .mappings import IntKeyMapping
from .sets import IntSet
...
class AdjacencyMatrixDigraph(AdjacencyDigraph):
def init (self , V=[], E=[], directed=True):
capacity = max(V)+1
super(AdjacencyMatrixDigraph, self ). init (V, E, directed,
mappingFactory=lambda V: IntKeyMapping(capacity=capacity),
setFactory=lambda V: IntSet(capacity=capacity),
mappingRedimFactory=lambda mapping, v: mapping.setcapacity(v+1),
setRedimFactory=lambda set, v: set.setcapacity(v+1))
def add vertex(self , v: "T"):
if v not in self . succs:
if self . succs.capacity <= v: self . succs.capacity = v + 1
self . succs[v] = self .setFactory(self .V)
if self . succs[0].capacity <= v:
for u in self . succs:
self . succs[u].capacity = v + 1
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 93
La adici on de un v ertice es una operaci on especialmente costosa, pues obliga a redi-
mensionar la matriz de adyacencia. Esto hace de las matrices de adyacencia representa-
ciones poco apropiadas para grafos cuyos conjuntos de v ertices varan durante la ejecu-
ci on de un algoritmo. La gura 3.7 (b) recoge los costes asociados a cada operaci on para
el peor de los casos.

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)

G.remove edge((u,v)) O(out degree(v))


Coste espacial O(V +E)
(a) (b)
Figura 3.9: (a) Repre-
sentaci on del digrafo de
la gura B.9 con vec-
tores de adyacencia. (b)
Coste temporal y es-
pacial de las operacio-
nes sobre grafos con la
implementaci on ListSe-
tAdjacencyDigraph, su-
poniendo que acceder a
la lista de sucesores de
un v ertice es un opera-
ci on con coste temporal
O(1).
. . . y conjuntos de sucesores basados en tablas de dispersi on
Podemos considerar, adem as, una implementaci on de los conjuntos de adyacencia basa-
da en tablas de dispersi on:
algoritmia/datastructures/graphs.py
class SetAdjacencyDigraph(AdjacencyDigraph):
def init (self , V=[], E=[], directed=True, **kw):
kw[setFactory] = lambda V: set()
super(SetAdjacencyDigraph, self ). init (V, E, directed, **kw)
En la tabla 3.7 se recoge el coste temporal esperado de las operaciones sobre SetAdja-
cencyDigraph.
SetAdjacencyDigraph
Operaci on Coste temporal (esperado)
G = SetAdjacencyDigraph(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(V)
G.out degree(u) O(1)
G.in degree(v) O(V)
G.V.add(v) O(1)
G.V.remove(v) O(V)
G.E.add((u,v)) O(1)
G.E.remove((u,v)) O(1)
Coste espacial O(V +E)
Tabla 3.7: Coste temporal esperado y espacial
de las operaciones sobre grafos en su imple-
mentaci on SetAdjacencyDigraph.
96 Apuntes de Algoritmia 28 de septiembre de 2009
3.7.5. Digrafos con conjuntos de adyacencia con inversa
Las operaciones que implican trabajar con los predecesores de un v ertice son relativa-
mente costosas en todas las implementaciones de digrafos estudiadas hasta el momento.
Podemos reducir su complejidad temporal a costa de un aumento de la ocupaci on es-
pacial si, adem as de almacenar explcitamente el conjunto de aristas E, almacenamos su
inversa E

= {(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.

La longitud de una carretera y el tiempo necesario para recorrerla pueden


ser bien distintos. T engase en cuenta que la segunda no es s olo funci on de
la longitud de la carretera, pues depende adem as de la calidad del rme, del n umero
de carriles, de la uidez del tr aco, etc.
Pongamos un ejemplo de digrafos: los grafos eucldeos, que son digrafos (en particu-
lar, grafos no dirigidos) cuyos v ertices son puntos en un espacio eucldeo y en los que la
ponderaci on de una arista se dene como la distancia eucldea entre los puntos que une.
El grafo eucldeo de la gura B.25 se puede codicar as:
demos/datastructures/weightedgraph.py
from algoritmia.datastructures.graphs import UndirectedGraph
from math import sqrt
G = UndirectedGraph(E= [((0,6),(2,2)), ((0,6),(2,6)), ((2,2),(4,4)), ((2,2),(4,0)),
((2,2),(6,4)), ((2,6),(4,4)), ((4,0),(6,4))])
def d(u, v):
return sqrt( (u[0]-v[0])**2 + (u[1]-v[1])**2 )
for (u, v) in G.E:
print({{{}, {}}}: {:4.2f}.format(u, v, d(u,v)))
{(6, 4), (4, 0)}: 4.47
{(6, 4), (2, 2)}: 4.47
{(2, 6), (0, 6)}: 2.00
{(2, 6), (4, 4)}: 2.83
{(4, 4), (2, 2)}: 2.83
{(0, 6), (2, 2)}: 4.47
{(2, 2), (4, 0)}: 2.83
En muchos grafos la ponderaci on de las aristas no puede deducirse a partir de atribu-
tos de los v ertices conectados. La funci on de ponderaci on puede ser una tabla arbitraria
que relaciona aristas con valores. Un diccionario puede poner en correspondencia aristas
con valores arbitrarios y ofrece acceso con un coste temporal esperado O(1). Python per-
mite denir un diccionario a cuyos elementos se accede como si de una funci on se tratara
(para mantener la compatibilidad en el acceso con verdaderas funciones de pondera-
ci on). Basta para ello con denir un m etodo con identicador call :
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 103
algoritmia/datastructures/graphs.py
class WeightingFunction(Mapping, Callable):
def init (self , data: "sequence<((T, T), Weight)> or mapping<(V, V), Weight>" =[],
symmetrical: "bool"=False, **kw):
get factories(self , kw, mappingFactory=lambda keyvalues: dict(keyvalues))
self . mapping = self .mappingFactory(data)
self .symmetrical = symmetrical
if symmetrical:
for (u, v) in self . mapping.keys():
if (v, u) in self . mapping:
if self . mapping[u, v] != self . mapping[v, u]:
raise ValueError("{!r} is different from {!r}".format((u,v), (v,u)))
if v != u: del self . mapping[v, u]
def getitem (self , key: "T x T") -> "weight":
return self . mapping[key]
def iter (self ) -> "iterable<(T, T)>":
return iter(self . mapping)
def len (self ) -> "int":
return len(self . mapping)
def call (self , u: "T or (T, T)", v: "T or None"=None) -> "float":
if v == None: u, v = u
if (u,v) in self . mapping: return self . mapping[u,v]
elif self .symmetrical: return self . mapping[v,u]
raise KeyError(repr((u,v)))
demos/datastructures/weightingfunction.py
from algoritmia.datastructures.graphs import Digraph, WeightingFunction
d = WeightingFunction({(0,1):4, (0,3):4, (1,4):1, (2,4):0, (2,5):2, (3,0):1,
(3,1):4, (4,3):1, (5,5):2})
G = Digraph(E=d.keys())
for (u, v) in G.E: print(({}, {}): {}..format(u, v, d(u,v)), end=" ")
(0, 1): 4. (0, 3): 4. (1, 4): 1. (2, 4): 0. (2, 5): 2. (3, 0): 1. (3, 1): 4. (
4, 3): 1. (5, 5): 2.
3.8.

Arbol
Conviene que el lector repase en este punto la denici on y conceptos sobre arboles que
se presentan en el apartado B.15.9.
Algunos de los algoritmos que estudiaremos (como los de Kruskal y Prim) propor-
cionan como resultado un arbol. Dado que un arbol es un caso particular de grafo no
104 Apuntes de Algoritmia 28 de septiembre de 2009
dirigido (uno en el que no hay ciclos), podemos utilizar para su representaci on cualquie-
ra de las implementaciones para grafos no dirigidos presentadas en el apartado anterior.
Por economa, algunos algoritmos que devuelven un arbol lo representan con un simple
conjunto o lista de aristas.
3.9.

Arbol con raz
Los arboles con raz son arboles en los que uno de los nodos, al que se denomina raz,
se distingue. Este nodo induce una relaci on de direcci on entre los v ertices que une una
arista: las aristas se entienden siempre dirigidas del v ertice m as cercano a la raz hacia el
m as lejano.
Vamos a implementar arboles con raz mediante la especializaci on de esta interfaz:
algoritmia/datastructures/trees.py
from abc import ABCMeta, abstractproperty, abstractmethod
class IRootedTree(metaclass=ABCMeta):
@abstractproperty
def root(self ) -> "T": pass
@abstractmethod
def succs(self , v: "T") -> "iterable<T>": pass
@abstractmethod
def preds(self , v: "T") -> "empty iterable<T> or iterable<T> with a single item":
pass
@abstractmethod
def in degree(self , v: "T") -> "0 or 1": pass
@abstractmethod
def out degree(self , v: "T") -> "int": pass
@abstractmethod
def subtrees(self ) -> "iterable<IRootedTree<T>>": pass
@abstractmethod
def tree(self , v: "T") -> "IRootedTree<T>": pass
Dado que los arboles con raz son casos particulares de digrafos (con la distinci on
de la raz como informaci on adicional), hemos escogido una nomenclatura para algu-
nos m etodos que deriva de la propia de los digrafos: succs, preds, in degree y out degree.
La propiedad root proporciona el nodo raz del arbol. El m etodo subtrees enumera los
sub arboles directos (los que tienen por raz a un sucesor de la raz) y el m etodo tree de-
vuelve el sub arbol que tiene por raz al nodo que se le especica.
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 105
3.9.1. Implementaci on basada en grafos
Podemos representar un arbol con raz con un digrafo y una referencia a su raz:
algoritmia/datastructures/trees.py
class DigraphTree(IRootedTree):
def init (self , G: "IDigraph", root: "node"):
self . G, self . root = G, root
root = property(lambda self : self . root)
def succs(self , v: "T") -> "iterable<T>":
return self . G.succs(v)
def preds(self , v: "T") -> "empty iterable<T> or iterable<T> with a single item":
if v != self . root:
for u in self . G.preds(v):
yield u
def in degree(self , v: "T") -> "0 or 1":
return 1 if v != self . root else 0
def out degree(self , v: "T") -> "int":
return self . G.out degree(v)
def subtrees(self ) -> "iterable<DigraphTree<T>>":
for v in self . G.succs(self . root):
yield DigraphTree(self . G, v)
def tree(self , v: "int") -> "DigraphTree<T>":
return DigraphTree(self . G, v)
def repr (self ) -> "str":
return "{}({!r}, {!r})".format(self . class . name ,
self . G, self . root)
El coste de las operaciones vendr a determinada por la estructura con la que se ha
implementado el par ametro G del constructor.
3.9.2. Implementaci on basada en lista de listas
Las listas de listas permiten implementar c omodamente arboles dirigidos con raz (orde-
nados o no) en lenguajes de programaci on que dan soporte sint actico a las listas, como
Python o Lisp. El arbol se representa con una lista cuyo primer elemento siempre es un
nodo y cuyos siguientes elementos son los sub arboles asociados a sus hijos. Esta clase
permite envolver una lista de listas y ofrecer as parte del perl de las clase Tree.
106 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/datastructures/trees.py
class ListOfListsTree(IRootedTree):
def init (self , list of lists: "list<list<T>>"):
self . lol = list of lists
root = property(lambda self : self . lol[0])
def succs(self , v: "T") -> "iterable<T>":
lol = self . search(v, self . lol)
if lol != None:
for i in range(1, len(lol)):
yield lol[i][0]
def preds(self , v: "T") -> "empty iterable<T> or iterable<T> with a single item":
if v != self . lol[0]:
p = self . search parent(v, self . lol)
if p != None:
yield p
def in degree(self , v: "T") -> "0 or 1":
return 1 if v != self . lol[0] else 0
def out degree(self , v: "T") -> "int":
lol = self . search(v, self . lol)
return len(lol) - 1
def subtrees(self ) -> "iterable<ListOfListsTree<T>>":
for i in range(1, len(self . lol)):
yield ListOfListsTree(self . lol[i])
def tree(self , v: "T") -> "ListOfListsTree<T>":
return ListOfListsTree(self . search(v, self . lol))
def search(self , v: "T", lol: "list<list<T>> or None"):
if len(lol) > 0 and v == lol[0]:
return lol
for i in range(1, len(lol)):
found = self . search(v, lol[i])
if found != None:
return found
return None
def search parent(self , v: "T", lol: "list<list<T>>") -> "T or None":
for i in range(1, len(lol)):
if lol[i][0] == v:
return lol[0]
for i in range(1, len(lol)):
found = self . search parent(v, lol[i])
if found != None:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 107
return found
return None
def repr (self ) -> "str":
return "{}({!r})".format(self . class . name , self . lol)
He aqu un ejemplo de construcci on del arbol de la gura B.22 como lista de listas:
tree = ListOfListsTree([1, [2, [3], [4], [5, [6]]], [7, [8, [9]]]])
3.9.3. Implementaci on con referencias a padres
En ciertas aplicaciones resulta natural representar arboles en los que cada nodo apunta
a su padre. Podemos recurrir a un diccionario (o a un vector si los ndices son valores
enteros) de referencias al nodo padre indexado por los propios nodos. Si las etiquetas
son todas diferentes, las propias etiquetas pueden utilizarse como ndices y valores del
diccionario. El unico nodo que no tiene padre es el nodo raz. El diccionario puede dejarse
sin entrada asociada a la raz o asociar a la raz un valor especial, como None o una
referencia al propio nodo:
algoritmia/datastructures/trees.py
class ParentReferenceTree(IRootedTree):
def init (self , parent references: "mapping<T,T>", root: "T"=None, **kw):
get factories(self , kw, mappingFactory=lambda pr: dict(pr))
self . parent = self .mappingFactory(parent references)
if root == None and len(self . parent) > 0:
x = next(iter(self . parent.keys()))
while x in self . parent and x != self . parent[x]: x = self . parent[x]
self . root = x
else:
self . root = root
root = property(lambda self : self . root)
def succs(self , v: "T") -> "iterable<T>":
for w in self . parent:
if self . parent[w] == v:
yield w
def preds(self , v: "T") -> "empty iterable<T> or iterable<T> with a single item":
if v != self . root:
yield self . parent[v]
def in degree(self , v: "T") -> "0 or 1":
return 1 if v != self . root else 0
def out degree(self , v: "T") -> "int":
return sum(1 for w in self .succs(v))
108 Apuntes de Algoritmia 28 de septiembre de 2009
def subtrees(self ) -> "iterable<ParentReferenceTree<T>>":
for v in self .succs(self . root):
yield ParentReferenceTree(None, v, mappingFactory=lambda pr: self . parent)
def tree(self , v: "T") -> "ParentReferenceTree<T>":
return ParentReferenceTree(None, v, mappingFactory=lambda pr: self . parent)
def repr (self ) -> "str":
return "{}({!r}, {!r})".format(self . class . name ,
list(self . parent.items()), self . root)
Un arbol como el de la gura B.22 se puede codicar as con un diccionario Python:
tree = ParentReferenceTree({1: 1, 2: 1, 3: 2, 4: 2, 5: 2, 6: 5, 7: 1, 8: 7, 9: 8})
La gura 3.10 muestra, para este mismo ejemplo, la estructura de referencias imple-
mentada sobre el diccionario parent. Esta implementaci on s olo permite acceder ecien-
temente a los nodos antecesores de un nodo dado, cosa que en algunos problemas es
suciente. En este mismo captulo presentamos una aplicaci on de esta representaci on de
los arboles dirigidos con raz (MFSET, secci on 3.14.3). En el siguiente captulo haremos un
uso intensivo de esta idea representando los llamados arboles de caminos m as cortos
con punteros a padres.
Figura 3.10: (a)

Arbol con raz con referencias (en
trazo discontinuo) al nodo padre de cada nodo. (b)
Representaci on del arbol mediante un vector en el
que cada celda contiene el ndice del nodo padre.
1
2
3 4 5
6
7
8
9
1
2
3 4 5
6
7
8
9
1 1 2 7 8 2 2 5 1
1 2 3 4 5 6 7 8 9
(a) (b)
La representaci on de arboles con referencia al padre se puede utilizar para implemen-
tar inmediatamente otra estructura de datos: un bosque de arboles. Todos los nodos se
incluyen en un unico diccionario (o vector) en el que el valor de un nodo apunta a otro o
a s mismo. Cada nodo que se apunte a s mismo ser a la raz de un arbol.
3.9.4. Implementaci on vectorial de arboles con raz y aridad
acotada
Los arboles con raz y aridad acotada, es decir, cuyos nodos internos tienen un n umero
de hijos igual o inferior a cierto valor a, pueden implementarse con vectores. Ilustraremos
la exposici on estudiando unicamente el caso de los arboles binarios, es decir, aquellos
cuyos nodos presentan aridad inferior o igual a 2. Dejamos al lector el desarrollo del
estudio correspondiente a arboles con nodos de aridad acotada mayor.
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 109
Hay una relaci on entre profundidad y n umero de nodos en un arbol de aridad aco-
tada por a: el n umero de nodos es menor o igual que (a
d+1
1)/(a 1), siendo d la
profundidad del arbol.
La gura 3.11 (a) muestra un arbol binario bajo cuyos nodos se han dispuesto ciertos
valores escogidos siguiendo unas reglas precisas:
el nodo raz tiene valor 0,
si un nodo interno tiene valor i, su hijo izquierdo tiene valor 2i +1 y su hijo derecho
tiene valor 2i +2.
a
b
c
d e
f
g
h i
0
1
3
7 8
4
2
5 6
a b c d e f g h i
0 1 2 3 4 5 6 7 8
hijos
padre
raz
(a) (b)
Figura 3.11: (a)

Arbol binario con
raz. El n umero bajo cada nodo se ha
asignado seg un se indica en el texto.
(b) Representaci on vectorial del arbol.
Los n umeros sobre los nodos son ndi-
ces en el vector. Cada celda del vec-
tor contiene la etiqueta de un nodo. La
raz es el primer nodo y tiene ndice
0. El padre del nodo de ndice 3 tie-
ne ndice (3 1)/2 = 1 y sus dos
hijos tienen ndices 2 3 + 1 = 7
y 2 3 +2 = 8.
Estas reglas asignan un n umero unico a cada nodo. Podemos usar esos valores como
ndices en un vector. Se obtiene, pues, una representaci on vectorial del arbol (v ease la
gura 3.11 (b)):
La raz ocupa la posici on 0 del vector.
El hijo izquierdo del nodo interno de ndice i es el de ndice 2i +1.
El hijo derecho del nodo interno de ndice i es el de ndice 2i +2.
El padre del nodo de ndice i, para i = 0, ocupa la posici on (i 1)/2.
Los nodos de profundidad d ocupan las posiciones de ndices 2
d
1 a mn(2
d+1

2, n 1), donde n es el n umero de nodos del arbol.


He aqu una implementaci on de arboles de aridad acotada:
algoritmia/datastructures/trees.py
class BoundedArityTree(IRootedTree):
def init (self , arity: "int"=0, seq: "iterable<T>"=[],
bounded arity tree: "BoundedArityTree<T>"=None, root index: "int"=0):
if bounded arity tree != None:
self . arity = bounded arity tree. arity
self . list = bounded arity tree. list
else:
self . arity = arity
110 Apuntes de Algoritmia 28 de septiembre de 2009
self . list = list(seq)
self . root index = root index
root = property(lambda self : self . list[self . root index])
def succs(self , v: "T") -> "iterable<T>":
for i in range(self . root index, len(self . list)):
if v == self . list[i]:
rst child = self . arity * i + 1
for i in range(rst child, min(len(self . list), rst child + self . arity)):
if self . list[i] != None:
yield self . list[i]
break
def preds(self , v: "T") -> "empty iterable<T> or iterable<T> with a single item":
if v != self . list[self . root index]:
for i in range(self . root index, len(self . list)):
if v == self . list[i]:
yield self . list[(i-1) // self . arity]
def in degree(self , v: "T") -> "0 or 1":
return 1 if v != self . list[self . root index] else 0
def out degree(self , v: "T") -> "int":
return sum(1 for w in self .succs(v))
def subtrees(self ) -> "iterable<BoundedArityTree<T>>":
rst child = self . arity * self . root index + 1
for i in range(rst child, min(len(self . list), rst child + self . arity)):
if self . list[i] != None:
yield BoundedArityTree(bounded arity tree=self , root index=i)
def tree(self , v: "T") -> "BoundedArityTree<T>":
i = self . list.index(v)
if i >= self . root index:
return BoundedArityTree(bounded arity tree=self , root index=i)
def repr (self ) -> "str":
return "{}({}, {!r}, root_index={!r})".format(
self . class . name , self . arity, self . list, self . root index)
Con esta indexaci on es posible representar el arbol binario de la gura 3.11 (a) con un
simple vector (v ease la gura 3.11 (b)):
tree = BoundedArityTree(2, [a, b, g, c, f, h, i, d, e])
El arbol de la gura 3.11 es especial: es un arbol binario completo por la izquierda.
Un arbol binario es completo por la izquierda si sus n nodos, numerados de acuerdo con
las reglas especicadas, forman el intervalo [0..n 1]. Gr acamente se puede determinar
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 111
que es completo porque todas las hojas se encuentran en uno o dos niveles de profundi-
dad y
el pen ultimo nivel (si hay m as de un nivel) est a completo, es decir, tiene todos los
nodos;
y en el ultimo nivel est an todos los nodos desde el extremo izquierdo hasta un
punto a partir del cual no hay nodo alguno.
Para contrastar, en la gura 3.12 se muestra un arbol binario que no es completo. Su
representaci on obliga a disponer celdas sin informaci on que podemos marcar con None.
a
b
c
d e
f
g
h i
j
0
1
3
7 8
4
2
5 6
13
a b g c f h i d e j
0 1 2 3 4 5 6 7 8 9 10 11 12 13
(a) (b)
Figura 3.12: (a)

Arbol
binario etiquetado e in-
completo. (b) Repre-
sentaci on vectorial del
arbol binario.
Hay un par propiedades a las que recurriremos en ocasiones:
Un arbol binario completo por la izquierda de profundidad d tiene un n umero de
nodos n tal que 2
d
n < 2
d+1
.
Un arbol binario completo por la izquierda con n nodos tiene profundidad lg(n).
3.9.5. Recorrido de arboles con raz
Una operaci on sobre arboles con m ultiples aplicaciones es el recorrido de sus nodos o
sub arboles en ciertos ordenes. Este recorrido suele tener por objeto efectuar alg un pro-
ceso sobre cada uno de los nodos o sub arboles y devolver el resultado de dicho proceso.
El resultado del recorrido es una iteraci on de valores: los que resultan de aplicar una
funci on visitor a cada uno de los sub arboles visitados.
Las clases que permitan recorrer arboles con raz implementar an esta interfaz:
algoritmia/treetraversals.py
from abc import ABCMeta, abstractmethod
from algoritmia.utils import get factories
class ITreeTraverser(metaclass=ABCMeta):
@abstractmethod
def traverse(self , tree: "IRootedTree<T>",
visitor: "f: IRootedTree<T> -> S") -> "iterable<S>": pass
Estudiaremos ahora tres recorridos distintos para arboles con raz:
112 Apuntes de Algoritmia 28 de septiembre de 2009
Recorrido por niveles o por primero en anchura.
Recorrido por primero en profundidad, del que hay dos variantes:
recorrido en preorden
recorridoen postorden.
Recorrido por niveles de un arbol con raz
El recorrido por niveles procesa todos los sub arboles del arbol en orden de profundidad
creciente. Primero procesa la raz; luego, todos sus hijos; luego, todos los nietos (primero
los hijos del primer hijo, despu es los del segundo, y as sucesivamente); luego, todos los
bisnietos (primero los del primer nieto, despu es los del segundo, etc.); y as sucesivamen-
te. Este recorrido se ilustra en la gura 3.13.
Figura 3.13: (a) Un arbol con raz. (b) Recorrido por ni-
veles del arbol.
0
1 2
3 4
5 6 7 8
9 10
0
1 2
3 4
5 6 7 8
9 10
(a) (b)
El algoritmo se apoya en el uso de una cola FIFO:
algoritmia/treetraversals.py
from algoritmia.datastructures.queues import FIFO
...
class LevelOrderTreeTraverser(ITreeTraverser):
def init (self , **kw):
get factories(self , kw, foFactory=lambda: FIFO())
def traverse(self , tree: "IRootedTree<T>",
visitor: "f: IRootedTree<T> -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda subtree: subtree.root)
Q = self .foFactory()
Q.push(tree)
yield visitor(tree)
while len(Q) > 0:
t = Q.pop()
for child in t.subtrees():
Q.push(child)
yield visitor(child)
La funci on que hemos dise nado proporciona una secuencia de valores: la que resulta
de ir aplicando, por niveles, la funci on visitor a cada uno de los sub arboles de un arbol.
Por defecto visitor devuelve la raz del sub arbol procesado:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 113
from algoritmia.treetraversals import LevelOrderTreeTraverser
from algoritmia.datastructures.trees import ListOfListsTree
tree = ListOfListsTree([0, [1, [3, [5], [6, [9], [10]], [7]], [4, [8]]], [2]])
print(list(LevelOrderTreeTraverser().traverse(tree)))
[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Suponiendo que visitor tenga coste temporal O(1), el recorrido en un arbol con n no-
dos supondr a la ejecuci on de (n) pasos. El an alisis resulta enga noso si se razona del
siguiente modo: el n umero de iteraciones del bucle while depende del n umero de eje-
cuciones de la sentencia yield, que es (n) (una por nodo); pero dentro del bucle while
hay un bucle for que se ejecuta una vez por cada hijo de la raz del arbol que se explora
actualmente, as que si el factor de ramaje del arbol (n umero m aximo de hijos) es r, el
coste temporal del algoritmo completo es O(nr). Pero no es as. El bucle se ejecuta una
vez por cada nodo y cada vez se ejecuta una extracci on de Q; como no podemos extraer
m as elementos que los que hemos introducido, es obvio que la sentencia interior del bu-
cle for no puede ejecutarse m as de n veces. As pues, el coste temporal del algoritmo es
(n).
El coste espacial es O(n): un arbol con s olo dos niveles, el de la raz y el de sus hijos,
forzar a el ingreso de n 1 elementos en Q antes de efectuar la segunda extracci on.
Recorridos por primero en profundidad de un arbol con raz: preorden y
postorden
El recorrido por primero en profundidad consiste en visitar la raz y, a continuaci on,
recorrer por primero el profundidad cada uno de los sub arboles que tienen por raz a
sus hijos. Hay dos versiones del recorrido en funci on de si se procesa un nodo antes
de procesar a sus hijos (recorrido en preorden) o despu es (recorrido en postorden). La
gura 3.14 muestra en trazo discontinuo los dos recorridos y con tri angulos indica el
instante en el que se aplica el proceso en cada caso.
0
1 2
3 4
5 6 7 8
9 10
0
1 2
3 4
5 6 7 8
9 10
0
1 2
3 4
5 6 7 8
9 10
(a) (b) (c)
Figura 3.14: (a) Recorrido
por primero en profundidad
de un arbol con raz. En
funci on del instante de pro-
ceso de cada sub arbol (indi-
cado con los tri angulos so-
bre sus respectivas races),
el recorrido es (b) en preor-
den o (c) en postorden.
La codicaci on del recorrido en preorden es sencilla si nos ayudamos de una pila. La
pila se inicializa con el arbol completo y en cada iteraci on se extrae el arbol de la cima de
la pila, se visita este y apilan sus sub arboles en orden inverso. El proceso naliza cuando
la pila se agota:
114 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/treetraversals.py
class PreorderTreeTraverser(ITreeTraverser):
def init (self , **kw):
get factories(self , kw, lifoFactory=lambda: LIFO())
def traverse(self , tree: "IRootedTree<T>",
visitor: "f: IRootedTree<T> -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda subtree: subtree.root)
Q = self .lifoFactory()
Q.push(tree)
while len(Q) > 0:
t = Q.pop()
yield visitor(t)
for st in reversed(tuple(t.subtrees())):
Q.push(st)
El recorrido en postorden es un poco m as complicado, pues la pila debe contener dos
tipos de datos distintos: arboles que a un han de ser sometidos al recorrido en postorden
y arboles que ya est an listos para visitar. La pila se inicializa con el arbol completo y en
cada iteraci on se extrae el arbol de la pila, se comprueba si est a listo para visitar y, en tal
caso, se visita; en caso contrario, se apilan sus sub arboles en orden inverso.
algoritmia/treetraversals.py
class PostorderTreeTraverser(ITreeTraverser):
def init (self , **kw):
get factories(self , kw, lifoFactory=lambda: LIFO())
def traverse(self , tree: "IRootedTree<T>",
visitor: "f: IRootedTree<T> -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda subtree: subtree.root)
Q = self .lifoFactory()
Q.push(tree)
while len(Q) > 0:
t = Q.pop()
if isinstance(t, ReadyToVisitTree):
yield visitor(t.tree)
else:
Q.push( ReadyToVisitTree(t))
for st in reversed(tuple(t.subtrees())):
Q.push(st)
ReadyToVisitTree = namedtuple("_ReadyToVisitTree", "tree")
Aqu tenemos un ejemplo de uso de estos recorridos:
from algoritmia.treetraversals import *
from algoritmia.datastructures.trees import ListOfListsTree
tree = ListOfListsTree([0, [1, [3, [5], [6, [9], [10]], [7]], [4, [8]]], [2]])
print(Preorden:, list(PreorderTreeTraverser().traverse(tree)))
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 115
print(Postorden:, list(PostorderTreeTraverser().traverse(tree)))
Preorden: [0, 1, 3, 5, 6, 9, 10, 7, 4, 8, 2]
Postorden: [5, 9, 10, 6, 7, 3, 8, 4, 1, 2, 0]
El coste temporal de ambas funciones es O(n).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28 Hay un tercer tipo de recorrido por primero en profundidad, pero s olo para arboles binarios:
el recorrido en inorden. Consiste en recorrer primero el sub arbol izquierdo, procesar a continua-
ci on la raz y, nalmente, recorrer el sub arbol derecho:
algoritmia/treetraversals.py
class InorderTreeTraverser(object):
def init (self , **kw):
get factories(self , kw, lifoFactory=lambda: LIFO())
def traverse(self , tree: "IRootedTree<t>",
visitor: "f: IRootedTree<T> -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda subtree: subtree.root)
Q = self .lifoFactory()
Q.push(tree)
while len(Q) > 0:
t = Q.pop()
if isinstance(t, ReadyToVisitTree):
yield visitor(t.tree)
else:
st= tuple(t.subtrees())
if len(st) == 2: Q.push(st[1])
Q.push( ReadyToVisitTree(t))
if len(st) == 2: Q.push(st[0])
...
Haz una traza del m etodo para un arbol binario completo con 7 nodos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Una aplicaci on: evaluaci on de expresiones aritm eticas
El recorrido en postorden encuentra aplicaci on en la evaluaci on de expresiones aritm eti-
cas por parte de los int erpretes para lenguajes de programaci on. Una expresi on aritm etica
puede representarse mediante un arbol. La expresi on Python 2 - 5 + 3 * 6, por ejem-
plo, con el orden de precedencia convencional, se puede representar con la lista de listas
["+", ["-", [2], [5]], ["*", [3], [6]]], es decir, con un arbol como el de la gu-
ra 3.15. Este tipo de arboles reciben la denominaci on de arboles de sintaxis abstracta, ya
que hacen abstracci on de los detalles sint acticos del lenguaje y expresan las relaciones
entre sus palabras.

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.

No debemos confundir el t ermino montculo (heap) en el sentido en que lo


usamos aqu por el de montculo de memoria (memory heap), es decir, la
zona de memoria de la que se sirven las peticiones de memoria din amica.
Figura 3.16: Un mn-montculo. Las etiquetas son n umeros naturales. La
etiqueta de cualquier nodo es menor o igual que la etiqueta de sus nodos
hijos.
5
12
18
32 18
22
76
46
46 67

Hemos de advertir que estamos permitiendo que en los arboles aparezcan


nodos con etiquetas id enticas. En principio esto resulta imposible si consi-
deramos que dichas etiquetas son los v ertices de un grafo, pues deben formar un
conjunto (es decir, no hay lugar para elementos repetidos). No obstante, y dado que
es natural en muchas aplicaciones que en un montculo ingresen valores id enticos,
asumiremos que hay una funci on de etiquetado que asocia a cada v ertice un valor
determinado. Es este el valor que consideramos almacenado en el montculo.
Una ventaja de los montculos es la eciencia con la que es posible a nadir un elemento
y mantener la propiedad del montculo en todos sus nodos. La gura 3.17 muestra, paso
a paso, el proceso de adici on de un elemento a un mn-montculo. En la gura 3.17 (a)
tenemos el mn-montculo original. Al insertar un elemento de valor 6, empezamos por
a nadirlo a la primera posici on libre en el arbol. El mn-montculo pasa al estado de la
gura 3.17 (b). El nodo a nadido hace que se viole la propiedad mn-montculo entre el y
su padre. La soluci on es sencilla: se intercambian ambos, con lo que pasamos al estado
descrito en la gura 3.17 (c). Surge un nuevo conicto entre el nodo insertado y su padre,
por lo que se procede a un nuevo intercambio. El nuevo estado (gura 3.17 (d)) respeta
la condici on del mn-montculo, as que hemos acabado.
El algoritmo de inserci on es, pues, muy sencillo: a nadir al nal e intercambiar el nodo
con su padre mientras se viole la propiedad mn-montculo. El coste es O(lg n) si hay n
nodos, pues la profundidad de un arbol binario completo est a acotada superiormente
por lg n.
El mn-montculo es un arbol binario completo y, por tanto, puede representarse de
forma compacta mediante un vector. Consultar el elemento de menor puntuaci on en un
mn-montculo supone acceder al elemento de ndice 0 en un vector y es, por tanto, una
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 121
5
12
18
32 18
22
76
46
46 67
5
12
18
32 18
22
76 6
46
46 67 22
6
(a) (b)
5
12
18
32 18
6
76 22
46
46 67
12
6
5
6
18
32 18
12
76 22
46
46 67
(c) (d)
Figura 3.17: Adici on de
un elemento a un mn-
montculo.
operaci on O(1). La extracci on de dicho elemento resulta algo m as complicada. La gu-
ra 3.18 muestra la extracci on del elemento de menor puntuaci on paso a paso. Inicial-
mente se copia el elemento que ocupa la ultima posici on del vector en la primera (gu-
ra 3.18 (a)). En el ejemplo, ello supone violar la condici on mn-montculo en la cima. Para
solucionar este problema, se selecciona el hijo con menor puntuaci on y se intercambia la
cima con el (gura 3.18 (b)). Nuevamente hay una violaci on de la propiedad, as que hay
un nuevo intercambio con el hijo de menor puntuaci on (gura 3.18 (c)). En el ejemplo no
hay nuevas violaciones de la propiedad, as que el proceso se detiene y se llega al estado
de la gura 3.18 (d), que es un mn-montculo bien formado. El n umero de intercam-
bios que se pueden efectuar est a acotado tambi en por la profundidad del arbol, as que
extraer el elemento de la raz es una operaci on O(lg n).
5
6
18
32 18
12
76 22
46
46 67
5
22
22
6
18
32 18
12
76
46
46 67
22
6
(a) (b)
6
22
18
32 18
12
76
46
46 67
22
12
6
12
18
32 18
22
76
46
46 67
(c) (d)
Figura 3.18: Eliminaci on
del elemento de menor
puntuaci on en un mn-
montculo.
122 Apuntes de Algoritmia 28 de septiembre de 2009
Un constructor sencillo
El atributo heap es el vector con el que representamos el arbol binario. Aunque antes
hemos dise nado una clase para representar arboles con aridad acotada, no recurriremos
a ella por razones de eciencia y exibilidad. Inicialmente el arbol est a vaco:
class Heap(IPriorityQueue, Iterable):
def init (self , opt: "min or max"):
self . heap = [None]
self . size = 0
self . best = opt
M as adelante modicaremos este constructor para facilitar la inicializaci on de la es-
tructura con un conjunto de valores.
El m etodo heapify
Al eliminar el elemento de menor puntuaci on en un mn-montculo subimos a la raz el
ultimo elemento del mn-montculo y efectuamos un proceso de intercambios padre-hijo
mientras detectemos una violaci on de la propiedad de mn-montculo. Esta operaci on,
generalizada para empezar en un nodo cualquiera y no s olo en la raz, resulta interesante
porque se puede utilizar en dos contextos distintos: la extracci on del elemento de menor
puntuaci on y la creaci on eciente de un mn-montculo cuando partimos de una lista
con n elementos que deben ingresar en el. La operaci on en cuesti on recibe el nombre de
heapify y al invocarla se proporciona (el ndice de) el nodo donde empezamos la secuencia
de intercambios y que puede estar violando la propiedad del mn-montculo:
algoritmia/datastructures/priorityqueues.py
class Heap(IPriorityQueue, Iterable):
...
def heapify(self , i: "int"):
while True:
l, r = 2*i, 2*i+1
if l <= self . size and self . opt(self . heap[l], self . heap[i]) != self . heap[i]:
best = l
else:
best = i
if r <= self . size and self . opt(self . heap[r], self . heap[best]) != self . heap[best]:
best = r
if best == i: break
self . heap[i], self . heap[best] = self . heap[best], self . heap[i]
i = best
El m etodo es una versi on iterativa de este procedimiento recursivo que posiblemente
resulte m as sencillo de entender:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 123
def recursive heapify(self , i: "int"):
l, r = 2*i, 2*i+1
if l <= self .size and self .best(self .heap[l], self .heap[i]) != self .heap[i]:
best = l
else:
best = i
if r <= self .size and self .best(self .heap[r], self .heap[best]) != self .heap[best]:
best = r
if best == i: return
self .heap[i], self .heap[best] = self .heap[best], self .heap[i]
self .recursive heapify(best)

La versi on iterativa resulta directamente de suprimir la recursi on por cola


que exhibe el m etodo recursive heapify. M as adelante, al hablar de la estra-
tegia divide y vencer as daremos m as detalles acerca de la recursi on por cola y
la transformaci on recursivo-iterativa que nos permite suprimirla.
Dado que un mn-montculo con n elementos es un arbol con O(lg n) niveles y el
m etodo heapify realiza, en el peor de los casos, intercambios a lo largo de un camino de la
raz al padre de una hoja, se ejecuta en tiempo O(lg n).

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])

Python ofrece colas de prioridad implementadas con mn-montculos. Para


m as informaci on, cons ultese el manual del m odulo heapq. No usamos esa
implementaci on porque hemos enriquecido la nuestra con operaciones adicionales.
MinHeap y MaxHeap
Y acabamos con un par de clases que especializan la clase Heap para colas de prioridad
en las que el elemento m as prioritario es el de menor valor (MinHeap) y otras en las que
el elemento m as prioritario es el de mayor valor (MaxHeap):
algoritmia/datastructures/priorityqueues.py
class MinHeap(Heap):
def init (self , data: "iterable<T>"=[], capacity: "int"=0):
super(MinHeap, self ). init (min, data, capacity)
class MaxHeap(Heap):
def init (self , data: "iterable<T>"=[], capacity: "int"=0):
super(MaxHeap, self ). init (max, data, capacity)
Coste temporal
La tabla 3.10 resume el coste de las operaciones b asicas sobre montculos, as como del
constructor.
Tabla 3.10: Coste para el peor de los casos de las operaciones propias de
una cola de prioridad implementada con un montculo. A es un vector con
hasta n elementos, Q es una cola de prioridad con n elementos e item es
un valor cualquiera (comparable con el resto de los que forman el Heap).
El coste de la inserci on es para el peor de los casos si el mn-montculo no
ha de redimensionarse; en caso contrario, es un coste amortizado.
Heap
Operaci on Coste
Q = Heap(A, n) O(n)
Q.add(item) O(lg n)
v = Q.opt() O(1)
v = Q.extract opt() O(lg n)
len(Q) O(1)
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 127
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
31 A nade un m etodo a la clase Heap llamado delete que recibe un ndice y elimina el elemento
que ocupa esa posici on en el vector sobre el que trabaja el montculo. La cola de prioridad debe
mantener la propiedad montculo tras la ejecuci on del m etodo y este debe ejecutarse en tiempo
O(lg n), siendo n la talla del montculo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.10.2. Una aplicaci on: el m etodo de ordenaci on heapsort
Podemos formar un mn-montculo con los elementos de una secuencia de n elementos
en tiempo O(n). Extraer a continuaci on los elementos en orden de valor creciente supo-
ne ejecutar n extracciones del mn-montculo, cada una de las cuales supone un coste
O(lg n). El coste de este algoritmo de ordenaci on es, pues, O(n lg n):
algoritmia/problems/sorting.py
from algoritmia.datastructures.priorityqueues import MinHeap
...
def sorted(self , seq: "sequence<T>") -> "sorted iterable<T>":
Q = MinHeap(seq)
return (Q.extract opt() for i in range(len(Q)))
from algoritmia.problems.sorting import HeapSorter
print(list(HeapSorter().sorted([1, 7, 2, 0, 9, 12, 3, 4, 20, 5])))
[0, 1, 2, 3, 4, 5, 7, 9, 12, 20]
Se trata de un algoritmo m as eciente que otros ya considerados, como la ordenaci on
por inserci on, por selecci on o el m etodo de la burbuja, todos O(n
2
). Tan s olo podemos
poner un reparo: se requiere un espacio adicional O(n) para el mn-montculo. Hay, no
obstante, una forma relativamente sencilla de evitar este espacio extra: usar el propio
vector que se desea ordenar ascendentemente para implementar en el un m ax-montculo.
La idea consiste en crear un m ax-montculo con los n elementos y extraer el de mayor
valor. El m ax-montculo pasa a contener n 1 elementos y la ultima celda del vector
queda libre. Podemos, pues, almacenar ah el elemento extrado. Si procedemos as con
la extracci on y almacenamiento en la nueva posici on libre de los n elementos, habremos
ordenado el vector. Este procedimiento se conoce por heapsort.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
32 Implementa un m etodo que reciba un vector y modique su contenido de acuerdo con el
procedimiento descrito para que quede ordenado ascendentemente. El coste espacial de la fun-
ci on debe ser constante, es decir, debes efectuar la ordenaci on in-situ.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.11. Cola de prioridad con dos extremos
Las colas de prioridad ofrecen acceso y extracci on eciente del elemento optimo (esto
es, el elemento mnimo o m aximo, pero no ambos). En ocasiones necesitamos acceder y
128 Apuntes de Algoritmia 28 de septiembre de 2009
extraer los elementos optimo y p esimo ecientemente, es decir, queremos disponer de
una estructura con esta interfaz:
algoritmia/datastructures/doubleendedpriorityqueues.py
class IDoubleEndedPriorityQueue(IPriorityQueue):
@abstractmethod
def worst(self ): pass
@abstractmethod
def extract worst(self ): pass
Hay muchas estructuras de datos inspiradas en el montculo que ofrecen esta funcio-
nalidad. Podemo citar entre ellas los deaps (por double-ended heaps), los min-max-heaps y los
montculo de intervalos (interval heaps). Nosotros estudiaremos unicamente los montcu-
los de intervalos. No es la estructura m as sencilla, pero s la que mejores prestaciones
ofrece en la pr actica.
3.11.1. Montculo de intervalos
Un montculo de intervalos es un arbol binario completo en el que cada nodo puede
contener un elemento a o un intervalo [a, b], donde a b. Todos los nodos contienen un
intervalo a excepci on del ultimo, que puede contener un intervalo o un elemento. Todos
los nodos con hijos cumplen la propiedad del intervalo. Si [a, b] es el intervalo de un
nodo y [c, d] es el de uno de sus hijos, entonces se ha de cumplir que a c d b. Si
el nodo hijo contiene un elemento c en lugar de un intervalo, entonces se ha de cumplir
a c b. Una consecuencia de la observancia de esta propiedad es que el intervalo
o valor en cualquier nodo descendiente de uno dado esta contenido en el intervalo de
dicho nodo. La gura 3.19 (a) muestra un montculo de intervalos.
Figura 3.19: (a) Un montcu-
lo de intervalos. Bajo cada no-
do se muestran los ndices de
las celdas del vector en las que
se almacenan los valores. (b)
Representaci on vectorial del
montculo de intervalos.
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[15, 44]
38
[46, 71]
[50, 52] [51, 59]
0 1
2 3
6 7
14 15 16 17
8 9
18
4 5
10 11 12 13
(a)
2 75 4 60 46 71 12 25 15 44 50 52 51 59 12 18 16 20 38
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
(b)
Una consecuencia inmediata de que el intervalo [a, b] del nodo raz incluya a todos
los elementos o intervalos de sus descendientes es que a y b son, respectivamente, los
elementos menor y mayor del montculo. Conocer el valor mnimo o m aximo es, pues,
una operaci on O(1). Otra observaci on interesante es que si nos quedamos con el extremo
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 129
inferior del intervalo de cada nodo (o el valor del ultimo nodo si no contiene un interva-
lo), tenemos un mn-montculo; y si hacemos lo mismo con el extremo superior tenemos
un m ax-montculo. Podemos inspirarnos en los montculos para dise nar operaciones de
adici on de nuevos elementos y de extracci on del mnimo o m aximo, aunque deberemos
tener en cuenta las interacciones entre ambos montculos, pues a veces se transeren da-
tos de uno a otro, como veremos m as adelante.
Nosotros implementaremos los montculos de intervalos con un arbol binario basado
en un vector (v ease la gura 3.19 (b)). Como cada nodo tiene (hasta) un par de valores
asignados, ocuparemos celdas contiguas del vector para almacenar pares. Hemos de te-
ner en cuenta que los ndices de las celdas bastan para conocer las relaciones padre-hijo
entre nodos y los valores que est an en un mismo nodo:
Un ndice par hace referencia al extremo inferior de un intervalo o, cuando se nala
la ultima celda del vector ocupada, al valor del ultimo nodo del arbol. Un ndice
impar siempre hace referencia al extremo superior de un intervalo.
Si numeramos los nodos empezando en cero, por niveles y de izquierda a derecha
en cada nivel, el nodo de ndice i corresponde a las celdas de ndice 2i y 2i +1 en el
vector. El elemento 2i +1 puede no existir cuando i se nala al ultimo nodo.
La celda de ndice par i en el vector corresponde a un nodo cuyos hijos ocupan (si
existen) las celdas 2(i +1) y 2(i +1) +1 (hijo izquierdo) y 2(i +1) +2 y 2(i +1) +3
(hijo derecho). Si el ndice i es impar, su hijo izquierdo ocupa las posiciones 2i y
2i +1 y su hijo derecho, las posiciones 2i +2 y 2i +3.
La celda de ndice i pertenece a un nodo que tiene por padre (si existe) al nodo que
ocupa las celdas 2 (i/2 1)/2 y 2 (i/2 1)/2 +1.
Dicho esto, podemos presentar el constructor como un m etodo que prepara el vector
que representa al arbol, que se asegura de que cada celda tiene un par de valores correc-
tamente ordenados (m etodo swap), y que llama a m etodos an alogos a heapify para el
montculo de mnimos y de m aximos:
algoritmia/datastructures/doubleendedpriorityqueues.py
class IntervalHeap(IDoubleEndedPriorityQueue):
def init (self , data: "iterable<T>"=[], capacity: "int"=0):
capacity = max(capacity, len(data))
self . heap = data + [None] * (capacity-len(data))
self . size = len(data)
for v in range(0, self . size, 2):
if v+1 < self . size: self . swap(v)
last parent = self . parent(self . size-1)
for v in range(last parent, -1, -2):
self . heapify min(v)
self . heapify max(v)
def swap(self , i: "int") -> "bool":
130 Apuntes de Algoritmia 28 de septiembre de 2009
if self . heap[i] > self . heap[i+1]:
self . heap[i], self . heap[i+1] = self . heap[i+1], self . heap[i]
return True
return False
def parent(self , i: "int") -> "int":
return ((i // 2 - 1) // 2) * 2
Los m etodos an alogos a heapify han sido adaptados para gestionar que los valores
que pudieran estar desordenados, se ordenen:
algoritmia/datastructures/doubleendedpriorityqueues.py
def heapify min(self , i: "int"):
smallest = i
while True:
for j in self . children(i):
if self . heap[j] < self . heap[smallest]: smallest = j
if smallest == i: break
self . heap[smallest], self . heap[i] = self . heap[i], self . heap[smallest]
i = smallest
if i+1 < self . size:
self . swap(i)
def heapify max(self , i: "int"):
largest = i+1
while True:
for j in self . children(i):
if j + 1 < self . size:
if self . heap[j+1] > self . heap[largest]: largest = j+1
else:
if self . heap[j] > self . heap[largest]: largest = j
if largest == i+1: break
self . heap[largest], self . heap[i+1] = self . heap[i+1], self . heap[largest]
i = largest // 2 * 2
if i+1 < self . size:
self . swap(i)
def children(self , i: "int") -> "iterable<int>":
j = 2*(i+1)
if j < self . size: yield j
j += 2
if j < self . size: yield j
Insertar un elemento es relativamente sencillo, aunque considera una serie de casos
que requieren un tratamiento especco. Un caso trivial es la adici on de un elemento a un
montculo de intervalos vaco: se inserta el elemento en la posici on 0 del vector y listos. En
otro caso, se a nade el elemento a la primera posici on libre del vector. Si hemos creado un
nodo con intervalo donde haba un nodo con un valor simple, es necesario ver si hemos
de reordenarlos para que denan un intervalo bien formado. Si se han reordenado, es
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 131
posible que el mn-montculo subyacente viole la condici on del mn-montculo, por lo
que hemos de ver si el nuevo valor asciende hacia la raz. En caso contrario, es el m ax-
montculo el que puede necesitar que el valor ascienda. Si el elemento v a nadido forma
un nodo con un solo valor hemos de comprobar si se viola la condici on del montculo
de intervalos con respecto al nodo padre. Sea [a, b] el intervalo del padre. Si se viola la
condici on es porque v < a o v > b. En tal caso, intercambia v por a o por b para que
se restablezca la observaci on de la propiedad entre los dos nodos, pero se dispara una
llamada a bubble up min o a bubble up max seg un se haya intercambiado v con a o con
b. Las funciones bubble up min y bubble up max se encargan de corregir la violaci on de
la propiedad del mn-montculo o del m ax-montculo haciendo que el valor conictivo
ascienda en el arbol:
algoritmia/datastructures/doubleendedpriorityqueues.py
def add(self , v: "T"):
if self . size == len(self . heap):
self . heap.append(None)
if self . size == 0:
self . heap[0] = v
self . size += 1
else:
self . heap[self . size] = v
self . size += 1
if self . size % 2 == 0:
i = self . size - 2
if self . swap(i):
self . bubble up min(i)
else:
self . bubble up max(i)
else:
i = self . size - 1
parent = self . parent(i)
if parent >= 0:
if self . heap[parent] <= v <= self . heap[parent+1]: return
if v > self . heap[parent+1]:
self . heap[parent+1], self . heap[i] = self . heap[i], self . heap[parent+1]
self . bubble up max(parent)
elif v < self . heap[parent]:
self . heap[parent], self . heap[i] = self . heap[i], self . heap[parent]
self . bubble up min(parent)
def bubble up max(self , i: "int"):
while True:
parent = self . parent(i)
if parent < 0: return
if self . heap[parent+1] >= self . heap[i+1]: return
self . heap[parent+1], self . heap[i+1] = self . heap[i+1], self . heap[parent+1]
i = parent
132 Apuntes de Algoritmia 28 de septiembre de 2009
def bubble up min(self , i: "int"):
while True:
parent = self . parent(i)
if parent < 0: return
if self . heap[parent] <= self . heap[i]: return
self . heap[parent], self . heap[i] = self . heap[i], self . heap[parent]
i = parent
Las guras 3.20 y 3.21 ilustran, paso a paso, el proceso de inserci on de valores en el
montculo de intervalos.
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[15, 44]
38
[46, 71]
[50, 52] [51, 59]
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[15, 44]
[38, 10]
[46, 71]
[50, 52] [51, 59]
(a) (b)
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[15, 44]
[10, 38]
[46, 71]
[50, 52] [51, 59]
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[10, 44]
[15, 38]
[46, 71]
[50, 52] [51, 59]
(c) (d)
Figura 3.20: Inserci on de un elemento en el montculo de intervalos de la gura anterior. El elemento, de valor 10,
ingresa en el ultimo nodo y se intercambia con el valor que ya estaba ocupando ese nodo. A continuaci on, asciende por
el mn-montculo formado con los extremos inferiores de los intervalos.
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[10, 44]
[15, 38] 70
[46, 71]
[50, 52] [51, 59]
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[10, 70]
[15, 38] 44
[46, 71]
[50, 52] [51, 59]
(a) (b)
[2, 75]
[4, 70]
[12, 25]
[12, 18] [16, 20]
[10, 60]
[15, 38] 44
[46, 71]
[50, 52] [51, 59]
(c) (d)
Figura 3.21: Inserci on de un elemento en un montculo de intervalos. El elemento, de valor 70, ingresa en un nuevo
nodo y asciende por el max-montculo formado con los extremos superiores de los intervalos.
Acceder al mnimo o al m aximo es trivial:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 133
algoritmia/datastructures/doubleendedpriorityqueues.py
def min(self ) -> "T":
if self . size == 0: raise IndexError("Empty Interval Heap")
return self . heap[0]
def max(self ) -> "T":
if self . size == 0: raise IndexError("Empty Interval Heap")
if self . size == 1: return self . heap[0]
return self . heap[1]
Y extraer el mnimo o m aximo son operaciones similares a las equivalentes en mn-
montculos y m ax-montculos:
algoritmia/datastructures/doubleendedpriorityqueues.py
def extract min(self ) -> "T":
if self . size == 0: raise IndexError("Empty Interval Heap")
retval = self . heap[0]
if self . size <= 2:
if self . size == 1:
self . heap[0] = None
else:
self . heap[0], self . heap[1] = self . heap[1], None
self . size -= 1
return retval
self . heap[0], self . heap[self . size-1] = self . heap[self . size-1], None
self . size -= 1
self . heapify min(0)
return retval
def extract max(self ) -> "T":
if self . size == 0: raise IndexError("Empty Interval Heap")
if self . size == 1:
retval = self . heap[0]
self . heap[0] = None
self . size -= 1
return retval
retval = self . heap[1]
if self . size == 2:
self . heap[1] = None
self . size -= 1
return retval
self . heap[1], self . heap[self . size-1] = self . heap[self . size-1], None
self . size -= 1
self . heapify max(0)
return retval
Las guras 3.22 y 3.23 ilustran paso a paso el proceso de extracci on del mnimo y el
m aximo.
A nadimos nalmente algunas funciones auxiliares:
134 Apuntes de Algoritmia 28 de septiembre de 2009
[2, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[10, 44]
[15, 38] 44
[46, 71]
[50, 52] [51, 59]
[44, 75]
[4, 60]
[12, 25]
[12, 18] [16, 20]
[10, 70]
[15, 38]
[46, 71]
[50, 52] [51, 59]
(a) (b)
[4, 75]
[44, 60]
[12, 25]
[12, 18] [16, 20]
[10, 70]
[15, 38]
[46, 71]
[50, 52] [51, 59]
[4, 75]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[44, 70]
[15, 38]
[46, 71]
[50, 52] [51, 59]
(c) (d)
[4, 75]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
[44, 38]
[46, 71]
[50, 52] [51, 59]
[4, 75]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
[38, 44]
[46, 71]
[50, 52] [51, 59]
(e) (f)
Figura 3.22: Extracci on del mnimo de un montculo de intervalos. Tras extraer el valor 2, el ultimo elemento pasa a
ocupar su posici on y su valor desciende por el mn-montculo hasta llegar al ultimo nodo, que acaba intercambiando
sus dos valores.
[4, 75]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
[38, 44]
[46, 71]
[50, 52] [51, 59]
[4, 44]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
38
[46, 71]
[50, 52] [51, 59]
(a) (b)
[4, 71]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
38
[46, 44]
[50, 52] [51, 59]
[4, 71]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
38
[44, 46]
[50, 52] [51, 59]
(c) (d)
[4, 71]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
38
[44, 59]
[50, 52] [51, 46]
[4, 71]
[10, 60]
[12, 25]
[12, 18] [16, 20]
[15, 70]
38
[44, 59]
[50, 52] [46, 51]
(e) (f)
Figura 3.23: Extracci on del m aximo del montculo de intervalos de la anterior gura.
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 135
algoritmia/datastructures/doubleendedpriorityqueues.py
def len (self ) -> "int":
return self . size
def iter (self ) -> "iterable<T>":
for i in range(self . size):
yield self . heap[i]
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , self . heap[:self . size])
Los costes computacionales de esta estructura de datos se muestran en la tabla 3.11.
IntervalHeap
Operaci on Coste
Q = IntervalHeap(A, n) O(n)
Q.add(item) O(lg n)
v = Q.min() O(1)
v = Q.extract min() O(lg n)
v = Q.max() O(1)
v = Q.extract max() O(lg n)
len(Q) O(1)
Tabla 3.11: Coste para el peor de los casos de las operaciones propias
de una cola de prioridad de dos extremos implementada con un heap
de intervalos.
Acabamos deniendo un par de clases que denen funciones para consultar y extraer
el optimo y el p esimo, que ser an el mnimo y el m aximo o el m aximo y el mnimo seg un
la clase empleada:
algoritmia/datastructures/doubleendedpriorityqueues.py
class MinMaxHeap(IntervalHeap, IDoubleEndedPriorityQueue):
opt = IntervalHeap.min
extract opt = IntervalHeap.extract min
worst = IntervalHeap.max
extract worst = IntervalHeap.extract max
class MaxMinHeap(IntervalHeap, IDoubleEndedPriorityQueue):
opt = IntervalHeap.max
extract opt = IntervalHeap.extract max
worst = IntervalHeap.min
extract worst = IntervalHeap.extract min
3.11.2. Una aplicaci on: rango de los k mejores elementos de un
serie potencialmente innita
Imaginemos que nos proporcionan una secuencia potencialmente innita de datos que
se van proporcionando de uno en uno. Queremos poder conocer en todo instante entre
136 Apuntes de Algoritmia 28 de septiembre de 2009
qu e valores se encuentran los k mejores elementos de la serie (en este caso los k menores)
vistos hasta el momento.
El montculo de intervalos permite resolver el problema ecientemente. Iremos in-
troduciendo en el montculo de intervalos los elementos conforme se presenten. Cuando
quiera que el montculo contenga k + 1 elementos, extraeremos el m aximo. El mnimo y
el m aximo de los (hasta) k valores se conoce siempre en tiempo O(1). Tener conocimiento
del rango de los (hasta) k mejores valores, cuando se han visto n elementos, presenta un
coste temporal O(n lg k).
Si adem as quisi eramos conocer esos k elementos, podemos hacerlo enumerando el
contenido del montculo de intervalos, aunque en tal caso no se obtendran ordenados.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
33 Implementa el m etodo descrito para una serie de n umeros enteros que el usuario ir a intro-
duciendo por teclado. Cada vez que el usuario introduzca un espacio en lugar de un n umero, el
programa mostrar a por pantalla los 10 n umeros m as peque nos vistos hasta el momento.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.12. Diccionario de prioridad
Ciertos algoritmos, como el de Dijkstra (secci on 4.8.4) y el de Kruskal (secci on ??), usan
conjuntos de pares clave-puntuaci on que deben garantizar eciencia en estas operacio-
nes:
construcci on de la estructura a partir de un conjunto de pares clave-puntuaci on,
asignaci on y modicaci on de puntuaci on asociada a una clave,
acceso al par clave-puntuaci on de puntuaci on optima,
extracci on del par clave-puntuaci on de puntuaci on p esima.
Esta es la interfaz, que hereda m etodos de MutableMapping:
algoritmia/datastructures/prioritydicts.py
class IPriorityDict(MutableMapping):
@abstractmethod
def opt(self ) -> "K": pass
@abstractmethod
def opt item(self ) -> "(K, T)": pass
@abstractmethod
def opt value(self ) -> "T": pass
@abstractmethod
def extract opt(self ) -> "K": pass
@abstractmethod
def extract opt item(self ) -> "(K, T)": pass
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 137
Se necesita, pues, una estructura hbrida entre una cola de prioridad y un diccionario,
estructura que denominaremos diccionario de prioridad, pues pone en relaci on claves
con valores que indican prioridad.
3.12.1. Diccionario de prioridad basado en montculo
La idea fundamental consiste en mantener un montculo de pares puntuaci on-clave (que
obedecer a, pues, la propiedad del montculo para la puntuaci on) y un diccionario cuyas
claves sean las claves del diccionario de prioridad y cuyos valores sean los ndices en el
montculo de cada par clave-puntuaci on. La gura 3.24 ilustra esta idea. La unica dicul-
tad de la implementaci on estriba en mantener el diccionario interno actualizado cuando
se produce un movimiento entre nodos del montculo. Para modicar la puntuaci on aso-
ciada a un valor hemos de considerar si esta crece o decrece para iniciar una serie de
intercambios con uno de sus hijos o con su padre, respectivamente.
(17, Toni)
(18, Mar)
(20, Luis) (23, Ana)
(21, Mara)
1
2
4 5
3
1
0
0
1
2
4
3
4
3
5
6
2
7
Mara
Ana
Luis
Toni
Mar
Figura 3.24: Un diccionario de prioridad de nombres y edades. Est a indexado por nombres de personas y se considera
m as prioritaria a la persona de menor edad. El mn-montculo est a ordenado por edades (el menor valor en la cima) y
un diccionario establece una correspondencia entre nombres y posici on de los nodos del mn-montculo.
He aqu una implementaci on del diccionario de prioridad que gestiona un vector (un
montculo) con pares valor-clave y un mapeo que asocia claves a ndices: las posiciones
de cada elemento en el vector. Las operaciones de inserci on y extracci on se inspiran en
las vistas para montculos, pero tienen la precauci on de mantener la coherencia entre
el mapeo de claves-ndice y el vector. Las operaciones que no tienen equivalente en el
montculo son las de consulta y asignaci on del valor asociado a una clave. La consulta es
trivial, pero la asignaci on debe tener en cuenta que el objeto modicado puede tener que
descender (va heapify) o ascender (va un m etodo bubble up) en el montculo.
algoritmia/datastructures/prioritydicts.py
class PriorityDict(IPriorityDict):
def init (self , opt: "min or max", data: "iterable<(K, T)>"=[],
capacity: "int"=0, **kw):
get factories(self , kw, mappingFactory=dict,
mappingRedimFactory=lambda mapping, maxkey: mapping)
self . opt = opt
138 Apuntes de Algoritmia 28 de septiembre de 2009
self . index = self .mappingFactory()
if isinstance(data, Mapping): data = data.items()
elif not isinstance(data, Sequence): data = tuple(data)
for (i, (key, )) in enumerate(data): self . index[key] = i+1
self . size = len(data)
self . heap = list(chain((None,), ((v, k) for (k, v) in data),
repeat(None, max(0, capacity-self . size))))
for i in range(self . size//2, 0, -1): self . heapify(i)
def heapify(self , i: "int"):
while True:
l, r = 2*i, 2*i+1
if l <= self . size and self . opt(self . heap[l], self . heap[i]) != self . heap[i]:
best = l
else:
best = i
if r <= self . size and self . opt(self . heap[r], self . heap[best]) != self . heap[best]:
best = r
if best == i: break
self . index[self . heap[i][1]], self . index[self . heap[best][1]] = best, i
self . heap[i], self . heap[best] = self . heap[best], self . heap[i]
i = best
def bubble up(self , i: "int"):
p = i // 2
while i > 1 and self . opt(self . heap[i], self . heap[p]) != self . heap[p]:
self . index[self . heap[i][1]], self . index[self . heap[p][1]] = p, i
self . heap[i], self . heap[p] = self . heap[p], self . heap[i]
i, p = p, p // 2
def opt(self ) -> "K":
if self . size == 0: raise IndexError(opt from an empty priority dict)
return self . heap[1][1]
def opt item(self ) -> "(K, T)":
if self . size == 0: raise IndexError(opt from an empty priority dict)
return (self . heap[1][1], self . heap[1][0])
def opt value(self ) -> "T":
if self . size == 0: raise IndexError(opt from an empty priority dict)
return self . heap[1][0]
def extract opt(self ) -> "K":
return self .extract opt item()[0]
def extract opt item(self ) -> "(K, T)":
m = self .opt item()
if self . size > 1:
self . heap[1], self . index[self . heap[self . size][1]] = self . heap[self . size], 1
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 139
self . heap[self . size] = None
self . size -= 1
if self . size > 1: self . heapify(1)
del self . index[m[0]]
return m
def contains (self , key: "K") -> bool:
return key in self . index
def getitem (self , key: "K") -> "T":
return self . heap[self . index[key]][0]
def setitem (self , key: "K", score: "T") -> "T":
if key in self . index:
i = self . index[key]
if score == self . heap[i][0]: return score
if self . opt(score, self . heap[i][0]) != score:
self . heap[i] = (score, key)
self . heapify(i)
return score
else:
self .mappingRedimFactory(self . index, key)
if self . size + 1 >= len(self . heap): self . heap.append(None)
self . index[key] = i = self . size = self . size + 1
self . heap[i] = (score, key)
self . bubble up(i)
return score
def delitem (self , key: "K"):
if key in self . index:
i = self . index[key]
self . heap[i], self . index[self . heap[self . size][1]] = self . heap[self . size], i
self . size -= 1
self . heapify(i)
if i > 1 and self . heap[i] < self . heap[i//2]:
p = i // 2
while i > 1 and self . heap[i] < self . heap[p]:
self . index[self . heap[i][1]], self . index[self . heap[p][1]] = p, i
self . heap[i], self . heap[p] = self . heap[p], self . heap[i]
i, p = p, p//2
else:
self . heapify(i)
del self . index[key]
def iter (self ) -> "iterable<K>":
for key in self . index: yield key
def len (self ) -> "int":
return self . size
140 Apuntes de Algoritmia 28 de septiembre de 2009
def repr (self ) -> "str":
b = min if self . opt == min else max
return {}({}, {!r}).format(self . class . name , b, [(k, self [k]) for k in self ])
class MinPriorityDict(PriorityDict):
def init (self , data: "iterable<(K, T)>"=[], capacity: "int"=0, **kw):
super(MinPriorityDict, self ). init (min, data, capacity, **kw)
class MaxPriorityDict(PriorityDict):
def init (self , data: "iterable<(K, T)>"=[], capacity: "int"=0, **kw):
super(MaxPriorityDict, self ). init (max, data, capacity, **kw)
En la tabla 3.12 se muestran las operaciones soportadas por el diccionario de priori-
dad y su coste.
Tabla 3.12: Coste de las operaciones propias de un diccio-
nario de prioridad implementado con una hibridaci on de
montculo y diccionario. Hay que se nalar que los costes
suponen que las operaciones de acceso y modicaci on en
un diccionario son O(1). (D es un diccionario con hasta
n elementos, Q es un diccionario de prioridad con n pa-
res clave-valor, key es un objeto inmutable y v es un va-
lor cualquiera.)
Diccionario de prioridad
Operaci on Coste
Q = PriorityDict(D) O(n)
key = Q.opt() O(1)
v = Q.opt value() O(1)
(key, v) = Q.opt item() O(1)
key = Q.extract opt() O(lg n)
(key, v) = Q.extract opt item() O(lg n)
key in Q O(1)
Q[key] = v O(lg n)
v = Q[key] O(1)
del Q[key] O(lg n)
for key in Q: O(n)
len(Q) O(1)
N otese que hay una diferencia entre el montculo y el diccionario de prioridad en
el modo con el que insertamos un elemento en la estructura (en este caso, un par clave-
valor). En el primero usamos el m etodo add y en el segundo, asignamos el valor a Q[key].
Esta misma asignaci on sirve, cuando la clave ya est a en el diccionario, para modicar su
valor asociado.
Una aplicaci on: c alculo de esca nos con la ley de Hondt
Con la ley electoral vigente en Espa na, el reparto de esca nos de cada circunscripci on elec-
toral se efect ua mediante la denominada Ley de Hondt. Supongamos que corresponden
m esca nos a una circunscripci on electoral en la que se presentan n partidos polticos a
los que identicaremos con los n umeros enteros entre 1 y n. Sea v
i
el n umero de votos
obtenidos por el partido poltico i. La Ley de Hondt asigna un esca no a cada uno de los
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 141
partidos que obtiene uno de los m valores m as altos de v
i
/j, para 1 i n y 1 j m
(supondremos que no hay empates).
He aqu un ejemplo: en unas elecciones se reparten 4 esca nos y concurren los partidos
PA, PB, PC, PD y PE, que han obtenido respectivamente 60, 100, 40, 10 y 5 votos. La siguiente
tabla muestra la divisi on del n umero de votos por todos los enteros entre 1 y 4:
PA PB PC PD PE
j = 1
60
/
1
= 60.00
100
/
1
= 100.0
40
/
1
= 40.00
10
/
1
= 10.00
5
/
1
= 5.00
j = 2
60
/
2
= 30.00
100
/
2
= 50.00
40
/
2
= 20.00
10
/
2
= 5.00
5
/
2
= 2.50
j = 3
60
/
3
= 20.00
100
/
3
= 33.00
40
/
3
= 13.33
10
/
3
= 3.33
5
/
3
= 1.66
j = 4
60
/
4
= 15.00
100
/
4
= 25.00
40
/
4
= 10.00
10
/
4
= 2.00
5
/
4
= 1.25
La ley de Hondt asigna el primer esca no a PB, el segundo a PA, el tercero a PB y el
cuarto a PC, pues los 4 valores m as altos son 100.00 (100/1), 60.00 (60/1), 50.00 (100/2) y
40.00 (40/1), que se muestran en la tabla con negrita.
Vamos a dise nar una funci on que recibe un diccionario con los votos obtenidos por
cada uno de los n partidos y el n umero de esca nos en juego, m. La funci on devolver a una
lista con el partido al que se asigna cada uno de los esca nos (por el orden con el que se
obtienen). Una implementaci on directa requerir a espacio O(nm) y tiempo O(nmlg nm),
pues habr a de almacenar explcitamente la tabla y ordenar decrecientemente sus mn en-
tradas. Podemos hacerlo mejor si usamos un diccionario de prioridad (en esta caso, ba-
sado en un max-montculo):
demos/datastructures/dhondtlaw.py
from algoritmia.datastructures.prioritydicts import MaxPriorityDict
def dHondt law(votes, m):
Q = MaxPriorityDict(((party, (votes[party], 1)) for party in votes))
result = []
for i in range(m):
(party, (rest, j)) = Q.opt item()
result.append(party)
Q[party] = (votes[party]/(j+1), j+1)
return result
if name == "__main__":
print(dHondt law({PA: 60, PB: 100, PC: 40, PD: 10, PE: 5}, 4))
[PB, PA, PB, PC]
El coste espacial es O(m) y el temporal O(n +mlg n): la inicializaci on del diccionario
de prioridad se ejecuta en O(n) pasos y luego efectuamos m modicaciones de valores
asociados a claves en el diccionario de prioridad, que contiene n elementos.
142 Apuntes de Algoritmia 28 de septiembre de 2009
3.12.2. Diccionario de prioridad basado en montculo de
Fibonacci
Un montculo de Fibonacci es un diccionario de prioridad que presenta un coste amorti-
zado para ciertas operaciones que lo hacen ideal como estructura de datos en determina-
dos algoritmos si atendemos al coste computacional de dichos algoritmos para el peor de los casos.
El algoritmo de Dijkstra, por ejemplo, se benecia del uso de un montculo de Fibonacci.
No obstante, es una estructura de datos que en la pr actica puede ofrecer un rendimiento
peor el diccionario de prioridad basado en montculo, pues hace uso de listas doblemente
enlazadas y la gesti on de sus punteros es pesada.
Las operaciones de adici on de un elemento, acceso al elemento optimo y mejora del
valor asociado a un elemento presentan coste amortizado O(1). La operaci on de elimina-
ci on de un elemento cualquiera (el optimo entre ellos) tiene coste amortizado O(lg n).
El montculo de Fibonacci pueden verse como evoluci on de otra estructuras de datos
m as sencilla, el montculo binomial, por lo que empezaremos por introducir esta ultimo.
Montculos binomiales
Un montculo binomial es un conjunto de arboles binomiales. Un arbol binomial es un
arbol con una estructura peculiar. El arbol binomial de orden 0 es un arbol con un solo
nodo. El arbol binomial de orden k tiene por raz un nodo cuyos hijos son las races
de k arboles: uno de orden k 1, otro de orden k 2, y as hasta uno de orden 0. Un
arbol binomial de orden k tiene 2
k
hijos y profundidad k. La gura 3.25 muestra arboles
binomiales de orden 0, 1, 2 y 3.
Figura 3.25:

Arboles binomiales de orden 0, 1, 2 y 3. Un arbol binomial
de orden k tiene k hijos, que son las races de arboles binomiales de orde-
nes k 1, k 2,. . . , 1 y 0.
Un montculo binomial tiene a lo sumo un arbol binomial de cada orden. El montcu-
lo tiene el orden de su arbol binomial de mayor orden. Los arboles binomiales de un
montculo binomial est an etiquetado con valores y satisfacen la propiedad del montcu-
lo. Si hablamos de un mn-montculo, esto signica que la clave de todo nodo es menor
o igual que la de cualquiera de sus hijos.
Hay una operaci on muy importante en el montculo binomial: la fusi on de dos montcu-
los binomiales, esto es, la obtenci on de un nuevo montculo (aunque destruyendo los
originales) con la agregaci on de los nodos de los dos originales. Antes de comentarla con
detalle, hablemos de la fusi on de dos arboles binomiales del mismo orden preservando
la propiedad del montculo. Dos arboles binomiales de orden k pueden fundirse en un
solo arbol binomial de orden k + 1 a nadiendo uno de ellos a la raz del otro. Si se escoge
adecuadamente qu e arbol ser a hijo del otro a partir de los valores asociados a las races
de los dos arboles, se puede preservar la propiedad del montculo. Esta operaci on puede
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 143
efectuarse en tiempo O(1). La gura 3.26 muestra la fusi on de dos arboles binomiales de
orden 2 preservando la propiedad del mn-montculo.
5
12 7
8
2
3 10
15
2
3 10
15
5
12 7
8
+
Figura 3.26: Fusi on de dos arboles binomiales de orden 2 preservando
la propiedad del mn-montculo. El resultado es un arbol binomial de
orden 3.
Podemos saber cu antos arboles binomiales y de qu e orden son estos a partir del
n umero n de valores almacenados en un montculo binomial. Si codicamos el n umero
en binario, tendremos un uno por cada arbol binomial y la posici on de ese uno determi-
na el orden del arbol correspondiente. As, un montculo binomial con 17 nodos tendr a 1
arbol binomial de orden 0 y uno de orden 4, pues 17 se codica en binario con 10001. Es
evidente, a partir de este resultado, que un arbol binomial con n nodos est a formado por
a lo sumo lg n +1 arboles binomiales.
Esta codicaci on binaria y la adici on con acarreo ayuda a entender lo que ocurre
cuando fundimos dos montculos binomiales. Cuando fundimos dos montculos bino-
miales con n y m nodos, respectivamente, obtenemos un montculo binomial con n + m
nodos. Si en la codicaci on binaria de n hay un 1 en la posici on de orden 0 pero no la
hay en la codicaci on de m, el arbol binomial de orden 0 del primer montculo se copia
sin m as. Si hubiera un 1 en ambas codicaciones, el resultado sera un arbol binomial de
orden 1 y ninguno de orden 0. Pero si uno de los dos montculos originales ya tuviera
un arbol de orden 1, el resultante de la fusi on de los dos de orden 0 tendra que fundir-
se con este. La creaci on de arboles binomiales se propaga como el bit de acarreo en la
adici on binaria. El n umero de operaciones de fusi on de arboles binomiales est a acotado
por O(lg n + m) y cada fusi on, como ya hemos visto, es O(1). As pues, la fusi on de dos
montculos binomiales comporta un coste temporal logartmico con el n umero de nodos.
La operaci on de fusi on es importante porque permite implementar f acilmente las
operaciones de inserci on y extracci on del optimo. La inserci on no es m as que la fusi on
del montculo original con otro que s olo contiene al elemento que deseamos insertar.
La extracci on del mnimo pasa por su localizaci on (ser a la raz de uno de los arboles
binomiales), la creaci on de un nuevo montculo binomial con sus sub arboles directos y
la fusi on del montculo original sin el arbol de la raz con el nuevo montculo. Ambas
operaciones son, evidentemente, O(lg n).
Finalmente, la consulta del elemento optimo puede efectuarse en tiempo O(lg n), u
O(1) si se tiene la precauci on de mantener una referencia a la raz del arbol optimo y se
mantiene actualizada con cada operaci on susceptible de cambiar el optimo.
Hay una operaci on interesante porque es la que hace atractivos los montculos de
Fibonacci: la mejora del valor de un nodo (entendemos por mejora su decremento en
un mn-montculo y su incremento en un m ax-montculo). Si se tiene acceso directo al
elemento, basta con hacer intercambios entre padre y nodo mientras se viole la propiedad
del montculo entre ambos.
144 Apuntes de Algoritmia 28 de septiembre de 2009
Montculo de Fibonacci como estructura perezosa
Podemos ver los montculos de Fibonacci como versiones perezosas de los montculos
binomiales y proporcionan buenos coste amortizados.
Un montculo de Fibonacci es un conjunto de arboles. Los nodos de los arboles man-
tienen referencias al padre y a sus hermanos izquierdo y derecho (formando con ellos
una lista circular doblemente enlazada), as como una clave y un valor, su n umero de
hijos y un valor booleano que indica si est a marcado o no (ya veremos que signica
esto):
algoritmia/datastructures/prioritydicts.py
class FibonacciHeap(IPriorityDict):
class Node:
def init (self , key: "K", value: "T"):
self .parent = self .child = None
self .left = self .right = self
self .key = key
self .value = value
self .degree = 0
self .mark = False
def init (self , opt: "min or max"=min, data: "iterable<(K, T)>"=[], **kw):
get factories(self , kw, mappingFactory=lambda: dict())
self . size = 0
self . minroot = None
self . map = self .mappingFactory()
self . opt = lambda a, b: None if a == None or b == None else opt(a, b)
for key, value in data:
self .add(key, value)
def getitem (self , key: "K") -> "T":
return self . map[key].value
El conjunto de arboles relaja su condici on de que no haya dos arboles con el mismo
orden y s olo al ejecutar cierta operaci on aplica un proceso de consolidaci on que asegura
que haya a lo sumo uno de cada orden. En todo instante, eso s, se observa que todo nodo
tiene a lo sumo O(lg n) hijos.
El an alisis del coste amortizado de los montculos de Fibonacci se basa en la t ecnica
del potencial. El coste real de cada operaci on se complementa con el aumento o descenso
de un cierto valor de potencial. La idea es que las operaciones costosas decrementan el
valor del potencial y las r apidas lo aumentan. El coste amortizado es el coste real menos
el aumento del potencial. La funci on de potencial para los montculos de Fibonacci es
t +2m.
El conjunto de arboles que forman un montculo de Fibonacci se gestiona con una lista
circular doblemente enlazada a la que entramos desde el arbol que tiene la raz optima.
Acceder al elemento de valor optimo a trav es de esta referencia es, pues, una operaci on
O(1).
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 145
algoritmia/datastructures/prioritydicts.py
class FibonacciHeap(IPriorityDict):
...
def opt item(self ) -> "(K, T)":
return self . minroot.key, self . minroot.value
def opt(self ) -> "K":
return self . minroot.key
def opt value(self ) -> "T":
return self . minroot.value
Los montculos de Fibonacci tambi en soportan la operaci on de fusi on, pero dado el
car acter perezoso de la estructura, es una operaci on muy sencilla: la concatenaci on de las
dos listas de arboles y la actualizaci on de la referencia al optimo. Es pues, una operaci on
O(1). Insertar un nuevo nodo es fusionar el montculo original con uno que s olo tiene
un nodo. La funci on de potencial se incrementa en uno al ejecutar una inserci on, ya que
aparece un nuevo arbol en la estructura. El coste amortizado de la operaci on es O(1):
algoritmia/datastructures/prioritydicts.py
class FibonacciHeap(IPriorityDict):
...
def add(self , key: "K", value: "T"):
node = self . map[key] = FibonacciHeap.Node(key, value)
if self . minroot == None:
self . minroot = node
else:
node.left, node.right = self . minroot, self . minroot.right
self . minroot.right = node.right.left = node
if self . opt(node.value, self . minroot.value) != self . minroot.value:
self . minroot = node
self . size += 1
Extraer el optimo es un proceso complejo, que requiere la ejecuci on de varias fases:
1. Se elimina el arbol que contiene el optimo y se funde el montculo resultante con el
que forman los sub arboles directos del nodo que contena al optimo. Si el n umero
de hijos era k, la funci on de potencial aumentar a en k 1. El coste amortizado de es-
ta etapa es O(k), es decir, O(lg n), ya que el n umero de nodos siempre est a acotado
por O(lg n).
2. Si intent aramos actualizar ahora la referencia al nodo que contiene el valor optimo,
tendramos que recorrer toda la lista de races y esta puede haber degenerado hasta
el punto de tener n elementos. Se aprovecha, pues, el esfuerzo para fundir arboles
con el mismo grado. El proceso de fusi on de dos arboles con igual n umero de hijos
pasa por hacer a la raz de uno de ellos nodo hijo de la raz del otro, preservando la
propiedad del montculo. El proceso se reitera hasta que cada una de la races tiene
un n umero de hijos diferente del de las dem as races. El proceso de fusi on puede
146 Apuntes de Algoritmia 28 de septiembre de 2009
efectuarse en tiempo O(lg n + m), donde m es el n umero de races al inicio de esta
fase. Como al nal tendremos O(lg n) races, el potencial decrecer a en mO(lg n).
El coste amortizado de esta etapa es O(lg n).
3. Se actualiza la referencia al nodo que contiene el valor optimo. Esta operaci on es
O(lg n) y no afecta a la funci on de potencial.
algoritmia/datastructures/prioritydicts.py
class FibonacciHeap(IPriorityDict):
...
def extract opt(self ) -> "K":
opt = self .opt()
self . remove opt()
return opt
def extract opt item(self ) -> "(K, T)":
item = (self . minroot.key, self . minroot.value)
self . remove opt()
return item
def remove opt(self ):
z = self . minroot
del self . map[z.key]
if z != None:
nchildren = z.degree
x= z.child
while nchildren > 0:
t = x.right
x.left.right, x.right.left = x.right, x.left
x.left, x.right = self . minroot, self . minroot.right
self . minroot.right = x.right.left = x
x.parent = None
x = t
nchildren -= 1
z.left.right, z.right.left = z.right, z.left
if z == z.right:
self . minroot = None
else:
self . minroot = z.right
self . consolidate()
self . size -= 1
philog = log((1 + sqrt(5)) / 2)
def consolidate(self ):
a = [None] * int(log(self . size)/FibonacciHeap. philog)
n roots = self . count roots()
x = self . minroot
while n roots > 0:
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 147
d = x.degree
next = x.right
while a[d] != None:
y = a[d]
if self . opt(x.value, y.value) != x.value: x, y = y, x
self . link(y, x)
a[d] = None
d += 1
a[d] = x
x = next
n roots -= 1
self . minroot = None
for x in a:
if x != None:
if self . minroot != None:
x.left.right, x.right.left = x.right, x.left
x.left, x.right = self . minroot, self . minroot.right
self . minroot.right = x
x.right.left = x
if self . opt(x.value, self . minroot.value) != self . minroot.value:
self . minroot = x
else:
self . minroot = x
def count roots(self ) -> "int":
n roots = 0
x = self . minroot
if x != None:
n roots += 1
x = x.right
while x != self . minroot:
n roots += 1
x = x.right
return n roots
def link(self , y: "Node<K, T>", x: "Node<K, T>"):
y.left.right, y.right.left = y.right, y.left
y.parent = x
if x.child == None:
x.child = y.right = y.left = y
else:
y.left, y.right = x.child, x.child.right
x.child.right = y.right.left = y
x.degree += 1
y.mark = False
La operaci on m as interesante de los montculos de Fibonacci es la mejora del valor
asociado a un nodo. Cuando la mejora viola la propiedad del montculo entre el nodo en
148 Apuntes de Algoritmia 28 de septiembre de 2009
cuesti on y su padre, el nodo (con sus descendientes) se corta del arbol y se a nade a la
lista de arboles. Si el nodo padre no es raz, se marca. Si ya estaba marcado, tambi en
se corta y se marca a su padre. El proceso puede repetirse hasta alcanzar la raz y crear
O(k) nuevos arboles. Todos sus nodos raz, excepto aquel que inici o la cascada de cortes,
estaban marcados y dejan de estarlo al convertirse en races. As pues, el potencial decrece
en al menos la cantidad k 2 y el tiempo invertido en la operaci on es O(k), por lo que el
tiempo amortizado es O(1).
algoritmia/datastructures/prioritydicts.py
class FibonacciHeap(IPriorityDict):
...
def setitem (self , key: "K", value: "T") -> "T":
if key not in self . map:
self .add(key, value)
else:
self . improve value(key, value)
def improve value(self , key: "K", value: "T"):
node = self . map[key]
if self . opt(value, node.value) != value:
raise ValueError(("{} at {} does not improve {}").format(value, key, node.value))
node.value = value
parent = node.parent
if parent != None and self . opt(node.value, parent.value) != parent.value:
self . cut(node, parent)
self . cascading cut(parent)
if self . opt(node.value, self . minroot.value) != self . minroot.value:
self . minroot = node
def cut(self , x: "Node<K, T>", y: "Node<K, T>"):
x.left.right, x.right.left = x.right, x.left
y.degree -= 1
if y.child == x: y.child = x.right
if y.degree == 0: y.child = None
x.left, x.right = self . minroot, self . minroot.right
self . minroot.right = x
x.right.left = x
x.parent = None
x.mark = False
def cascading cut(self , node: "Node<K, T>"):
parent = node.parent
if parent != None:
if not node.mark:
node.mark = True
else:
self . cut(node, parent)
self . cascading cut(parent)
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 149
El borrado de un elemento cualquiera se puede implementar asignando a su nodo un
valor mejor que el del optimo para que pase a ser el optimo y extraerlo entonces con la
operaci on que ya hemos presentado antes.
algoritmia/datastructures/prioritydicts.py
class FibonacciHeap(IPriorityDict):
...
def delitem (self , key: "K"):
self . improve value(key, None)
self . remove opt()
Acabamos presentando algunos m etodos para consultar el n umero de nodos, iterar
sobre sus elementos y representar como cadena la estructura:
algoritmia/datastructures/prioritydicts.py
from math import log, sqrt
...
class FibonacciHeap(IPriorityDict):
...
def len (self ) -> "int":
return self . size
def iter (self ) -> "iterable<K>":
for key in self . map: yield key
def repr (self ) -> "str":
b = min if self . opt == min else max
return {}({}, {!r}).format(self . class . name ,b,[(k,self [k]) for k in list(self )])
La implementaci on de los montculos de Fibonacci es bastante compleja porque el
acceso eciente a la lista de arboles y a los hermanos de un nodo requiere la implementa-
ci on de listas circulares doblemente enlazadas. La gesti on de estas listas, unida a la de los
punteros de acceso a padre e hijos, tiene un impacto negativo en el tiempo de ejecuci on
de las operaciones. Por ello, los montculos de Fibonacci tienen mayor inter es te orico que
pr actico.

Hay estructuras de datos m as ecientes para efectuar las operaciones que


ofrece el montculo de Fibonacci, los pairing heaps o los soft heaps.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
34 Una academia ofrece k asignaturas (entre obligatorias y optativas) a n alumnos. (Codica-
mos las asignaturas con enteros entre 0 y k 1 y los alumnos con enteros entre 0 y n 1). Cada
alumno est a matriculado de todas las obligatorias y de algunas optativas. (En cada optativa el
n umero de alumnos matriculados es muy inferior a n.) Cada asignatura eval ua a sus estudiantes
en varios instantes de tiempo, con lo que modica su puntuaci on muy a menudo. Por ejemplo,
un estudiante puede tener hoy un 8 en cierta asignatura y un 9.5 o un 5.2 al da siguiente. En cada
instante, la nota media de un estudiante es la media de las notas obtenidas en todas las asignatu-
ras de las que est a matriculado. Nos interesa saber en todo momento qui en es el estudiante que
tiene mejor nota media y qui en el que tiene peor nota media.
150 Apuntes de Algoritmia 28 de septiembre de 2009
Has de dise nar una estructura de datos (directa o resultante de combinar dos o m as) que
soporte las siguientes operaciones: (1) Inicializaci on en tiempo O(nk). (2) Actualizaci on de las
notas de una asignatura optativa con m estudiantes en tiempo O(mlog n). (3) Actualizaci on de
las notas de una asignatura obligatoria en tiempo O(n). (4) Consulta del estudiante con mejor
nota media en tiempo O(1). (5) Consulta del estudiante con peor nota media en tiempo O(1).
(6) Acceso a la nota de un estudiante determinado en una asignatura concreta en tiempo O(1).
Describe la estructura o estructuras de datos y justica razonadamente que se observan los costes
exigidos a cada operaci on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.13. Diccionarios de prioridad con dos extremos
Del mismo modo que nos hemos basado en los montculos para dise nar los montculos de
intervalos, podemos dise nar un diccionario de prioridad con acceso y extracci on eciente
del menor y mayor elementos:
algoritmia/datastructures/doubleendedprioritydicts.py
from abc import abstractmethod
from .prioritydicts import IPriorityDict
class IDoubleEndedPriorityDict(IPriorityDict): # K: key type, T: value type
@abstractmethod
def worst(self ) -> "K": pass
@abstractmethod
def worst value(self ) -> "T": pass
@abstractmethod
def worst item(self ) -> "(K, T)": pass
@abstractmethod
def extract worst(self ) -> "K": pass
@abstractmethod
def extract worst item(self ) -> "(K, V)": pass
Una implementaci on natural toma como punto de partida el diccionario de prioridad.
La idea b asica es extender su montculo subyacente para convertirlo en un montculo de
intervalos. Una tabla permite mantener la correspondencia entre claves y valores. Mos-
tramos la implementaci on sin mayor comentario:
algoritmia/datastructures/doubleendedprioritydicts.py
from collections import Mapping, Sequence
...
class MinMaxPriorityDict(IDoubleEndedPriorityDict):
def init (self , data: "iterable<(K, V)> or mapping<K, T>"=[],
capacity: "int"=0, **kw):
get factories(self , kw, mappingFactory=lambda capacity: dict(),
mappingRedimFactory=lambda mapping, maxkey: mapping)
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 151
if isinstance(data, Mapping): data = data.items()
elif not isinstance(data, Sequence): data = tuple(data)
self . size = len(data)
capacity = max(capacity, self . size)
self . heap = [(v,k) for (k,v) in data] + [None] * (capacity-self . size)
self . index = self .mappingFactory(capacity)
for i in range(self . size): self . index[self . heap[i][1]] = i
for v in range(0, self . size, 2):
if v+1 < self . size: self . swap(v)
last parent = self . parent(self . size-1)
for v in range(last parent, -1, -2):
self . heapify min(v)
self . heapify max(v)
def children(self , i: "int") -> "iterable<int>":
j = 2*(i+1)
if j < self . size: yield j
j += 2
if j < self . size: yield j
def parent(self , i: "int") -> "int":
return ((i // 2 - 1) // 2) * 2
def swap(self , i: int) -> bool:
if self . heap[i][0] > self . heap[i+1][0]:
self . heap[i], self . heap[i+1] = self . heap[i+1], self . heap[i]
self . index[self . heap[i][1]] = i
self . index[self . heap[i+1][1]] = i+1
return True
return False
def getitem (self , key: "K") -> "T":
if key not in self . index: raise KeyError(key)
return self . heap[self . index[key]][0]
def delitem (self , key: "K"):
if key not in self . index: raise KeyError(key)
(newvalue, newkey) = self . heap[self . size-1]
self . heap[self . size-1] = None
self . size -= 1
i = self . index[key]
del self . index[key]
if i != self . size:
self . heap[i] = (self . heap[i][0], newkey)
self . index[newkey] = i
self [newkey] = newvalue
152 Apuntes de Algoritmia 28 de septiembre de 2009
def setitem (self , key: "K", v: "T") -> "T":
if key in self . index:
i = self . index[key]
ii = i if i % 2 == 0 else i-1
oldvalue = self . heap[i][0]
self . heap[i] = (v, key)
if ii+1 < self . size:
if self . swap(ii):
oldvalue = self . heap[i][0]
if oldvalue < v:
self . bubble up max(ii)
self . heapify min(ii)
elif oldvalue > v:
self . bubble up min(ii)
self . heapify max(ii)
else:
if oldvalue < v:
if ii == i: self . heapify min(ii)
else: self . bubble up max(ii)
elif oldvalue > v:
if ii == i: self . bubble up min(ii)
else: self . heapify max(ii)
else:
if v < oldvalue:
self . bubble up min(ii)
elif v > oldvalue:
parent = self . parent(ii)
if parent >= 0:
if self . heap[parent+1][0] < v:
self . heap[parent+1], self . heap[ii] = \
self . heap[ii], self . heap[parent+1]
self . index[self . heap[parent+1][1]] = parent+1
self . index[self . heap[ii][1]] = ii
self . bubble up max(parent)
else:
self . index = self .mappingRedimFactory(self . index, key)
if self . size == len(self . heap):
self . heap.append(None)
if self . size == 0:
self . heap[0] = (v, key)
self . index[key] = 0
self . size += 1
else:
self . heap[self . size] = (v, key)
self . index[key] = self . size
self . size += 1
if self . size % 2 == 0:
ii = self . size - 2
if self . swap(ii):
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 153
self . bubble up min(ii)
else:
self . bubble up max(ii)
else:
ii = self . size - 1
parent = self . parent(ii)
if parent >= 0:
if self . heap[parent][0] <= v <= self . heap[parent+1][0]: return v
if v > self . heap[parent+1][0]:
self . heap[parent+1], self . heap[ii] = self . heap[ii], self . heap[parent+1]
self . index[self . heap[parent+1][1]] = parent+1
self . index[self . heap[ii][1]] = ii
self . bubble up max(parent)
elif v < self . heap[parent][0]:
self . heap[parent], self . heap[ii] = self . heap[ii], self . heap[parent]
self . index[self . heap[parent][1]] = parent
self . index[self . heap[ii][1]] = ii
self . bubble up min(parent)
return v
def min(self ) -> "K":
if self . size == 0: raise IndexError("Empty Interval Heap")
return self . heap[0][1]
def min value(self ) -> "T":
if self . size == 0: raise IndexError("Empty Interval Heap")
return self . heap[0][0]
def min item(self ) -> "(K, T)":
if self . size == 0: raise IndexError("Empty Interval Heap")
return (self . heap[0][1], self . heap[0][0])
def max(self ) -> "K":
if self . size == 0: raise IndexError("Empty Interval Heap")
if self . size == 1: return self . heap[0][1]
return self . heap[1][1]
def max value(self ) -> "T":
if self . size == 0: raise IndexError("Empty Interval Heap")
if self . size == 1: return self . heap[0][0]
return self . heap[1][0]
def max item(self ) -> "(K, T)":
if self . size == 0: raise IndexError("Empty Interval Heap")
if self . size == 1: return (self . heap[0][1], self . heap[0][0])
return (self . heap[1][1], self . heap[1][0])
def extract min(self ) -> "K":
return self .extract min item()[0]
154 Apuntes de Algoritmia 28 de septiembre de 2009
def extract min value(self ) -> "T":
return self .extract min item()[1]
def extract min item(self ) -> "(K, T)":
if self . size == 0: raise IndexError("Empty Interval Heap")
retval = (self . heap[0][1], self . heap[0][0])
if self . size <= 2:
if self . size == 1:
del self . index[self . heap[0][1]]
self . heap[0] = None
else:
del self . index[self . heap[0][1]]
self . heap[0], self . heap[1] = self . heap[1], None
self . index[self . heap[0][1]] = 0
self . size -= 1
return retval
del self . index[self . heap[0][1]]
self . heap[0], self . heap[self . size-1] = self . heap[self . size-1], None
self . index[self . heap[0][1]] = 0
self . size -= 1
self . heapify min(0)
return retval
def extract max(self ) -> "K":
return self .extract max item()[0]
def extract max value(self ) -> "T":
return self .extract max item()[1]
def extract max item(self ) ->" (K, T)":
if self . size == 0: raise Exception("Empty Interval Heap")
if self . size == 1:
retval = (self . heap[0][1], self . heap[0][0])
del self . index[self . heap[0][1]]
self . heap[0] = None
self . size -= 1
return retval
retval = (self . heap[1][1], self . heap[1][0])
if self . size == 2:
del self . index[self . heap[1][1]]
self . heap[1] = None
self . size -= 1
return retval
del self . index[self . heap[1][1]]
self . heap[1], self . heap[self . size-1] = self . heap[self . size-1], None
self . index[self . heap[1][1]] = 1
self . size -= 1
self . heapify max(0)
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 155
return retval
def heapify min(self , i: "int"):
smallest = i
while True:
for j in self . children(i):
if self . heap[j][0] < self . heap[smallest][0]: smallest = j
if smallest == i: break
self . heap[smallest], self . heap[i] = self . heap[i], self . heap[smallest]
self . index[self . heap[smallest][1]] = smallest
self . index[self . heap[i][1]] = i
i = smallest
if i+1 < self . size:
self . swap(i)
def heapify max(self , i: "int"):
largest = i+1
while True:
for j in self . children(i):
if j + 1 < self . size:
if self . heap[j+1][0] > self . heap[largest][0]: largest = j+1
else:
if self . heap[j][0] > self . heap[largest][0]: largest = j
if largest == i+1: break
self . heap[largest], self . heap[i+1] = self . heap[i+1], self . heap[largest]
self . index[self . heap[largest][1]] = largest
self . index[self . heap[i+1][1]] = i+1
i = largest // 2 * 2
if i+1 < self . size:
self . swap(i)
def bubble up max(self , i: "int"):
while True:
parent = self . parent(i)
if parent < 0: return
if self . heap[parent+1][0] >= self . heap[i+1][0]: return
self . heap[parent+1], self . heap[i+1] = self . heap[i+1], self . heap[parent+1]
self . index[self . heap[parent+1][1]] = parent+1
self . index[self . heap[i+1][1]] = i+1
i = parent
def bubble up min(self , i: "int"):
while True:
parent = self . parent(i)
if parent < 0: return
if self . heap[parent][0] <= self . heap[i][0]: return
self . heap[parent], self . heap[i] = self . heap[i], self . heap[parent]
self . index[self . heap[parent][1]] = parent
self . index[self . heap[i][1]] = i
156 Apuntes de Algoritmia 28 de septiembre de 2009
i = parent
def contains (self , key: "K") -> "bool":
return key in self . index
def iter (self ) -> "iterable<K>":
for key in self . index: yield key
def len (self ) -> "int":
return self . size
def repr (self ) -> "str":
return {}({!r}).format(self . class . name ,
[(k,v) for (v,k) in self . heap[:self . size]])
opt = min
opt value = min value
opt item = min item
extract opt = extract min
extract opt item = extract min item
worst = max
worst value = max value
worst item = max item
extract worst = extract max
extract worst item = extract max item
Los costes temporales para el peor de los casos son equivalentes a los del dicciona-
rio de prioridad, coincidiendo los de extracci on y consulta de elementos m as y menos
prioritarios.
Conviene, nalmente, disponer de un diccionario de prioridad con dos extremos en
el que el elemento optimo sea el de mayor valor y el p esimo sea el de menor valor:
algoritmia/datastructures/doubleendedprioritydicts.py
class MaxMinPriorityDict(MinMaxPriorityDict):
opt = MinMaxPriorityDict.max
opt value = MinMaxPriorityDict.max value
opt item = MinMaxPriorityDict.max item
extract opt = MinMaxPriorityDict.extract max
extract opt item = MinMaxPriorityDict.extract max item
worst = MinMaxPriorityDict.min
worst value = MinMaxPriorityDict.min value
worst item = MinMaxPriorityDict.min item
extract worst = MinMaxPriorityDict.extract min
extract worst item = MinMaxPriorityDict.extract min item
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 157
3.14. Conjuntos disjuntos (particiones): MFSET
Una relaci on de equivalencia (o sea, una relaci on reexiva, sim etrica y transitiva) induce
una partici on de los elementos de un conjunto en una serie de clases de equivalencia
(v ease la secci on B.3). Cada elemento forma parte de un unico conjunto al que tambi en
pertenecen todos los elementos equivalentes a el.
Dado un conjunto nito de elementos y una relaci on de equivalencia, nos propo-
nemos encontrar y representar sus clases de equivalencia ecientemente. Dise naremos
una estructura de datos que permita dar de alta un elemento (operaci on add), preguntar
por la clase de equivalencia a la que pertenece un elemento (operaci on nd) y unir dos
clases (dos subconjuntos disjuntos) cuando se descubra que dos de sus elementos son
equivalentes (operaci on merge). Las estructuras de datos que implementan estas dos ope-
raciones se denominan MFSET, por el ingl es merge-nd sets, es decir, conjuntos de
fusi on-b usqueda. Nosotros a nadiremos un m etodo que permite conocer las particiones
existentes en todo momento. Este es el perl de toda implementaci on de un MFSET:
algoritmia/datastructures/mergefindsets.py
from abc import abstractmethod, ABCMeta
class IMergeFindSet(metaclass=ABCMeta):
@abstractmethod
def add(self , x: "T"): pass
@abstractmethod
def nd(self , x: "T") -> "T": pass
@abstractmethod
def merge(self , x: "T", y: "T"): pass
@abstractmethod
def partition(self ) -> "iterable<iterable<T>>": pass
...
Los MFSET encuentran numerosas aplicaciones. Entre ellas podemos citar la deter-
minaci on de equivalencia entre aut omatas nitos, el c alculo de los componentes conexos
de un grafo no dirigido (secci on 4.2), la obtenci on del arbol de recubrimiento de mnimo
peso en un grafo no dirigido (algoritmo de Kruskal, secci on ??), o el c alculo de regiones
conexas de color homog eneo en tratamiento de im agenes (que se resuelve en esta misma
secci on).
En aras de facilitar la comprensi on de la estructura de datos, presentaremos sus ideas
fundamentales al hilo de una de sus aplicaciones: la detecci on de componentes conexos
en una imagen.
158 Apuntes de Algoritmia 28 de septiembre de 2009
3.14.1. Una aplicaci on: componentes conexos en una imagen
Un campo de aplicaci on en el que se plantea este problema es el c alculo de las regiones
conexas de un cierto color en una imagen (que supondremos binaria por simplicidad).
La imagen de la gura 3.27, por ejemplo, es un mapa de bits con dos regiones conexas
de color gris. Queremos dise nar un programa que determine el conjunto de pxeles que
compone cada una de las regiones. Podemos ir barriendo de izquierda a derecha cada
una de las las (que exploramos, por ejemplo, de arriba abajo) y clasicando cada pxel
gris de acuerdo con el siguiente m etodo:
si tanto el pxel vecino por el norte como el pxel vecino por el oeste son blancos, el
pxel considerado pertenece a una nueva regi on;
si el pxel vecino por el norte es gris y el que hay al oeste no, el pxel que estamos
considerando pertenece a la misma regi on que el vecino por el norte;
si el pxel vecino por el oeste es gris y el pxel vecino por el norte no, el pxel que
estamos considerando pertenece a la misma regi on que el vecino por el oeste;
si tanto el pxel vecino por el norte como el pxel vecino por el oeste son grises, fun-
dimos sus respectivas regiones en una sola regi on y decidimos que el pxel considerado
pertenece a esta.
Figura 3.27: Un mapa de bits con dos regiones conexas de color gris.
Una traza con la imagen de la gura 3.27 ayudar a a entender el funcionamiento del
m etodo. Empezamos considerando la primera la de la imagen. Como todos sus pxeles
son blancos, no hay nada que hacer. El primer pxel gris est a en la segunda la. Como sus
pxeles vecinos por el norte y el oeste son blancos, creamos una nueva regi on. Llamamos
A a esa nueva regi on y etiquetamos el pxel con esa letra (gura 3.28 (a)). El siguien-
te pxel (gura 3.28 (b)) copia la etiqueta de su vecino por el oeste. El tercer pxel gris
pertenece a una nueva regi on, a la que llamamos B (gura 3.28 (c)). La gura 3.28 (d)
muestra el resultado de etiquetar todos los pxeles de la la 2. La tercera la no es pro-
blem atica: cada pxel gris se clasica como perteneciente a la regi on A o a la regi on
B (gura 3.28 (e)). La gura 3.28 (f) es interesante: el pxel que aparece marcado como
C se ha clasicado como perteneciente a una nueva regi on, aunque en realidad perte-
nece a la regi on A. El m etodo propuesto comete, pues, un aparente error. Si seguimos
aplicando el m etodo a las siguientes las, llegaremos a una nueva situaci on interesante:
la descrita por la gura 3.28 (g). El pxel que vamos a clasicar tiene por vecinos al nor-
te y al oeste a dos pxeles grises pertenecientes a regiones aparentemente diferentes. El
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 159
m etodo indica que debemos fundir ambas y crear una unica regi on a la que se adscribe el
pxel que estamos estudiando. Hemos optado por fundir la regi on C con la regi on A
y denominar a la regi on resultante A (gura 3.28 (h)). El error que pareca haber come-
tido el m etodo queda as corregido. El resultado nal se muestra en la gura 3.28 (i): el
algoritmo ha encontrado dos regiones y cada pxel gris ha sido clasicado correctamente
como miembro de una u otra.
A A A A A B
(a) (b) (c)
A A B B B B B B B B B B B A A
A A B
B B B B B B B B B B B
B B B B
A A
A A A
A A
B
B C
B B B B B B B B B B B
B B B B
(d) (e) (f)
A A
A A
A A
A A
A A
A A
A A A A A A A
B
B
B
B
C C
C C
C C
C C
B B B B B B B B B B B
B B B B
B B B B
B B B B
B B B B
A A
A A
A A
A A
A A
A A
A A A A A A A A
B
B
B
B
A A
A A
A A
A A
B B B B B B B B B B B
B B B B
B B B B
B B B B
B B B B
A A
A A
A A
A A
A A
A A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
A
B
B
B
B
A A
A A
A A
A A
B B B B B B B B B B B
B B B B
B B B B
B B B B
B B B B
(g) (h) (i)
Figura 3.28: Traza del c alculo de regiones conexas de color gris en la imagen.
La operaci on costosa de este m etodo es, evidentemente, la fusi on de dos regiones.
Una soluci on naf consiste en recorrer todos los pxeles de una de ellas para sustituir su
etiqueta por la etiqueta de la otra regi on. Es posible que ciertas im agenes, maliciosamente
dise nadas, obliguen a realizar numerosas fusiones, as que el coste de la fusi on puede te-
ner un impacto muy negativo en el coste global del m etodo.
3.14.2. Estructura de datos
La estructura de datos MFSET aborda precisamente este problema y lo resuelve con una
notable eciencia computacional. Ahora que hemos visto una aplicaci on, volvamos al
terreno de la formalizaci on y planteemos el problema en abstracto. Cuando lo hayamos
resuelto, presentaremos su aplicaci on al problema de las regiones conexas de un mismo
color.
Queremos implementar una estructura que particiona los elementos de un conjunto
nito S en una serie de subconjuntos disjuntos, {S
1
, S
2
, . . . , S
k
}, es decir, S =

1ik
S
i
160 Apuntes de Algoritmia 28 de septiembre de 2009
y S
i
S
j
= para todo i = j. Cada conjunto se identicar a mediante una etiqueta.
Usaremos como etiqueta de un conjunto a un elemento arbitrario del mismo. Diremos
que dicho elemento es el representante del conjunto.
Cada pxel pertenecer a a un conjunto de la partici on. Los pxeles se ir an dando de
alta en un conjunto a medida que se recorra la imagen. Cuando se descubra una relaci on
de vecindad entre dos pxeles, los conjuntos de las particiones a las que pertenecen se
fundir an. Al nalizar el recorrido de la imagen se podr a averiguar si dos pxeles perte-
necen o no a una misma regi on conexa inquiriendo por sus representantes: s olo si ambos
coinciden estar an los dos pxeles en la misma regi on.
3.14.3. Bosque de conjuntos disjuntos
Un bosque es un conjunto de arboles. Una estructura adecuada para representar un MF-
SET es el denominado bosque de conjuntos disjuntos. La raz de cada arbol es el represen-
tante del conjunto formado por los nodos del arbol. La gura 3.29 (a) muestra un mapa
de bits con tres regiones conexas y la gura 3.29 (b), un bosque de conjuntos disjuntos en
el que cada uno de los arboles se corresponde con una de las regiones.
Figura 3.29: (a) Imagen con tres regio-
nes conexas y (b) un bosque de conjun-
tos disjuntos para representar las regio-
nes conexas de la imagen. Cada arbol es
una regi on conexa y sus nodos son las
coordenadas (la y columna) de sus pxe-
les. El primer arbol representa el conjun-
to {(0, 0), (1, 0), (1, 2), (0, 2), (2, 0), (2, 1),
(2, 2)}, el segundo arbol representa el con-
junto {(0, 4), (0, 6), (0, 5)} y el tercer arbol
representa el conjunto {(2, 4), (2, 5)}.
2 2
1 1
0 0
0 1 2 3 4 5 6
0, 0
1, 0 1, 2 0, 2
2, 0 2, 2
2, 1
0, 4
0, 6 0, 5
2, 4
2, 5
(a) (b)
Implementar la estructura de datos no resulta complicado. B asicamente consiste en
un bosque representado con punteros al padre (v ease el apartado 3.9.3). Los nodos raz
de cada arbol no tienen padre, lo que se representa haciendo que esos nodos se apunten
a s mismos. La gura 3.30 muestra las referencias de cada nodo.
Crear un nuevo conjunto consiste en crear un nuevo arbol que se a nade al bosque.
Ese arbol s olo tiene un nodo que se apunta a s mismo. Encontrar el representante de un
nodo es sencillo: basta con recorrer sus ancestros hasta llegar al nodo raz de su arbol
y devolver el elemento de dicho nodo. Y fundir dos subconjuntos a los que pertenecen
dos nodos x e y tambi en resulta sencillo: es suciente con que la raz del arbol al que
pertenece x apunte a la raz del arbol al que pertenece el nodo y:
algoritmia/datastructures/mergefindsets.py
class NaiveMFset(IMergeFindSet):
def init (self , sets: "iterable<iterable<T>>"=[], **kw):
get factories(self , kw, mappingFactory=lambda nodes: dict())
if not isinstance(sets, Sequence): sets = tuple(sets)
all = []
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 161
for s in sets: all.extend(s)
self . parent = self .mappingFactory(all)
rst = None
for s in sets:
for item in s:
if rst == None: rst = item
self . parent[item] = rst
rst = None
def add(self , x: "T"):
self . parent[x] = x
def nd(self , x: "T") -> "T":
while x != self . parent[x]: x = self . parent[x]
return x
def merge(self , x: "T", y: "T"):
u = self .nd(x)
v = self .nd(y)
self . parent[u] = v
def partition(self ) -> "iterable<iterable<T>>":
aux = self .mappingFactory(self . parent.keys())
for key in self . parent:
aux.setdefault(self .nd(key), []).append(key)
return tuple(aux.values())
def repr (self ) -> "str":
return {}({!r}).format(self . class . name , self .partition())
Probemos nuestra implementaci on con la imagen de la gura 3.29 (a):
demos/datastructures/pixels.py
from algoritmia.datastructures.mergendsets import NaiveMFset
bitmap = [[1, 0, 1, 0, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 0],
[1, 1, 1, 0, 1, 1, 0]]
n, m = len(bitmap), len(bitmap[0])
S = NaiveMFset(((i,j),) for i in range(n) for j in range(m) if bitmap[i][j])
for i in range(len(bitmap)):
for j in range(len(bitmap[i])):
if bitmap[i][j] == 1:
if i > 0 and bitmap[i-1][j] == 1: S.merge( (i,j), (i-1,j) )
if j > 0 and bitmap[i][j-1] == 1: S.merge( (i,j), (i,j-1) )
print(pixel parent\n---------------)
for pixel in sorted(S. parent): print(pixel, , S. parent[pixel])
print(Conjuntos:, S.partition())
162 Apuntes de Algoritmia 28 de septiembre de 2009
pixel parent
---------------
(0, 0) (0, 0)
(0, 2) (0, 0)
(0, 4) (0, 4)
(0, 5) (0, 4)
(0, 6) (0, 4)
(1, 0) (0, 0)
(1, 2) (0, 2)
(2, 0) (0, 0)
(2, 1) (0, 0)
(2, 2) (0, 2)
(2, 4) (2, 4)
(2, 5) (2, 4)
Conjuntos: ([(1, 2), (0, 0), (2, 0), (2, 2), (2, 1), (1, 0), (0, 2)], [(2, 5),
(2, 4)], [(0, 6), (0, 5), (0, 4)])
Figura 3.30: Bosque de conjuntos disjuntos hallado por
el programa de prueba. En cada arbol se muestran las
referencias al padre. N otese que el bosque es diferente del
de la gura 3.29, aunque equivalente.
2, 2
0, 0
0, 2 1, 0 2, 0 2, 1
1, 2
0, 4
0, 5 0, 6
2, 4
2, 5
Qu e coste presentan las operaciones de nuestra estructura de datos?
S.add(x) se limita a ajustar una referencia para que apunte a x en tiempo O(1).
S.nd(x) recorre la lista de antecesores de x hasta llegar a la raz del arbol y su
coste temporal es proporcional a la profundidad de x en el arbol que lo contiene.
S.merge(x, y) recorre las listas de antecesores de x e y hasta llegar a las respectivas
races y, entonces, modica una referencia. El coste temporal es proporcional a la
suma de profundidades de x e y en sus respectivos arboles.
El punto clave del an alisis es la profundidad de los arboles. Estudiemos el compor-
tamiento de este factor en el peor de los casos. Si unimos un arbol de profundidad a a
un arbol de profundidad b, el arbol resultante es de profundidad a + 1 o b + 1, seg un el
orden en que se suministren sus respectivos nodos a S.merge. En el peor de los casos, el
resultado de fundir n arboles de profundidad unitaria es un arbol de profundidad n y
es, por tanto, una operaci on con un coste total O(n). As pues, realizar n operaciones de
creaci on y un total de m operaciones de uni on o b usqueda con arboles que originalmente
eran de profundidad unitaria supondr a un coste O(mn).
3.14.4. Heursticos para la reducci on del coste
Si el problema de la estructura presentada es que la profundidad de los arboles puede
ser igual al n umero de nodos que lo integran, podemos controlar esta de alg un mo-
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 163
do? Veamos por qu e los arboles pueden degenerar hasta convertirse en meras listas. La
gura 3.31 muestra el resultado de unir un arbol de profundidad 2 a un arbol de profun-
didad 0 y el resultado de unir un arbol de profundidad 0 a un arbol de profundidad 2 (el
orden importa). En el primer caso obtenemos un arbol de profundidad 3, y en el segundo,
un arbol de profundidad 2.
a
b
c
d
a
b
c
d
a
b
c
d
a
b
c
d
(a) (b)
Figura 3.31: Fusi on de arboles. (a) Si se
une un arbol de profundidad 2 a un arbol
de profundidad 0, el resultado es un arbol
de profundidad 3. (b) Si se une un arbol
de profundidad 0 a un arbol de profundi-
dad 2, el arbol resultante tiene profundi-
dad 2.
Una primera idea consiste, pues, en efectuar la uni on teniendo en cuenta la profundi-
dad de los dos arboles implicados y hacer que la raz del de menor profundidad apunte
a la raz del otro. Calcular la profundidad de cada arbol antes de efectuar la fusi on de
ambos es costoso. Optaremos por asociar a cada nodo un valor, al que denominaremos
rango, que acota superiormente su altura en el arbol. La uni on consistir a en la determi-
naci on del rango de las races de los arboles implicados y el cambio de la referencia de la
raz con menor rango para que apunte a la raz con mayor rango (en caso de empate, da
igual qui en apunta a qui en). Este heurstico se denomina uni on por rango:
algoritmia/datastructures/mergefindsets.py
class RankUnionMFset(NaiveMFset):
def init (self , sets: "iterable<iterable<T>>"=[], **kw):
get factories(self , kw, mappingFactory=lambda nodes: dict())
if not isinstance(sets, Sequence): sets = tuple(sets)
all = []
for s in sets: all.extend(s)
self . parent = self .mappingFactory(all)
self . rank = self .mappingFactory(all)
rst = None
for s in sets:
for item in s:
if rst == None: rst = item
self . parent[item] = rst
self . rank[item] = self . rank.get(item, 0) + 1
rst = None
def add(self , x: "T"):
self . parent[x] = x
self . rank[x] = 1
164 Apuntes de Algoritmia 28 de septiembre de 2009
def merge(self , x: "T", y: "T"):
u = self .nd(x)
v = self .nd(y)
if self . rank[u] < self . rank[v]:
self . parent[u] = v
elif self . rank[u] > self . rank[v]:
self . parent[v] = u
else:
self . parent[v] = u
self . rank[u] += 1
Se puede demostrar que la profundidad de un arbol de n nodos construido con la
t ecnica de uni on por rango no puede exceder de lg n +1. El c alculo de nd en un arbol
de n nodos pasa a ser O(lg n) y la fusi on de un arbol de profundidad n
1
y un arbol de
profundidad n
2
es una operaci on de coste O(lg(m ax{n
1
, n
2
})). Efectuar m operaciones de
b usqueda o uni on sobre n arboles de profundidad originariamente unitaria es O(mlg n).
Un segundo heurstico que mejora tambi en la eciencia de nuestra estructura de da-
tos es el denominado compresi on de caminos. La idea consiste en hacer del c alculo de
nd(x) una operaci on de dos pasos:
un primer paso en el que se recorren todos los nodos desde x hasta la raz,
y un segundo paso en el que asignamos al padre de todos los nodos recorridos
dicha raz (compresi on del camino).
La gura 3.32 muestra un arbol antes de efectuar una operaci on de b usqueda de la
raz y despu es de efectuar la compresi on del camino desde uno de los nodos.
Figura 3.32:

Arbol original (izquierda) y despu es de efectuar una
llamada a nd, con compresi on de caminos, sobre el nodo d.
a
b
c
d
e
f
a
e
d
c
b
f
algoritmia/datastructures/mergefindsets.py
class PathCompressionMFset(NaiveMFset):
def init (self , sets: "iterable<iterable<T>>"=[], ** kw):
super(PathCompressionMFset, self ). init (sets, **kw)
def nd(self , x: "T"):
r = x
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 165
while r != self . parent[r]: r = self . parent[r]
while x != self . parent[x]: self . parent[x], x = r, self . parent[x]
return r
Tanto la uni on por rango como la compresi on de caminos mejoran, individualmente,
la complejidad computacional de las operaciones de b usqueda y uni on. Combinadas,
proporcionan una mejora a un mayor:
algoritmia/datastructures/mergefindsets.py
class MFset(RankUnionMFset, PathCompressionMFset):
def init (self , sets: "iterable<iterable<T>>"=[], **kw):
super(MFset, self ). init (sets, **kw)
def nd(self , x: "T") -> "T":
return PathCompressionMFset.nd(self , x)
Al ejecutar nuestra nueva implementaci on con la imagen de la gura 3.29 (a) obtene-
mos:
demos/datastructures/pixels2.py
from algoritmia.datastructures.mergendsets import MFset
bitmap = [[1, 0, 1, 0, 1, 1, 1],
[1, 0, 1, 0, 0, 0, 0],
[1, 1, 1, 0, 1, 1, 0]]
n, m = len(bitmap), len(bitmap[0])
S = MFset(((i,j),) for i in range(n) for j in range(m) if bitmap[i][j])
for i in range(len(bitmap)):
for j in range(len(bitmap[i])):
if bitmap[i][j] == 1:
if i > 0 and bitmap[i-1][j] == 1: S.merge( (i,j), (i-1,j) )
if j > 0 and bitmap[i][j-1] == 1: S.merge( (i,j), (i,j-1) )
print(pixel parent\n---------------)
for pixel in sorted(S. parent): print(pixel, , S. parent[pixel])
print(Conjuntos:, S.partition())
pixel parent
---------------
(0, 0) (1, 0)
(0, 2) (1, 2)
(0, 4) (0, 5)
(0, 5) (0, 5)
(0, 6) (0, 5)
(1, 0) (1, 2)
(1, 2) (1, 2)
(2, 0) (1, 0)
(2, 1) (1, 0)
(2, 2) (1, 2)
(2, 4) (2, 5)
166 Apuntes de Algoritmia 28 de septiembre de 2009
(2, 5) (2, 5)
Conjuntos: ([(1, 2), (0, 0), (2, 0), (2, 2), (2, 1), (1, 0), (0, 2)], [(2, 5),
(2, 4)], [(0, 6), (0, 5), (0, 4)])
La gura 3.33 muestra el conjunto de arboles construido al usar uni on por rango y
compresi on de caminos.
Figura 3.33: Bosque de conjuntos disjuntos correspondiente a la
imagen de la gura 3.27 hallado al usar uni on por rango y com-
presi on de caminos.
1, 2
0, 2 1, 0 2, 2
0, 0 2, 0 2, 1
0, 5
0, 4 0, 6
2, 5
2, 4
Pero la gura 3.33 no reeja el estado ultimo del MFSET. En la llamada a S.partition()
del programa pixels2.py se modica el MFSET a consecuencia de nuevas llamadas al
m etodo nd, que aplanan a un m as los arboles! Cada consulta realizada puede contri-
buir a reducir a un m as la altura del arbol en la que se encuentra el elemento buscado. La
gura 3.34 muestra el verdadero estado nal.
Figura 3.34: Bosque de conjuntos disjuntos tras
inquirir por el representante de cada uno de los
elementos del bosque de la gura 3.33.
1, 2
0, 2 1, 0 2, 2 0, 0 2, 0 2, 1
0, 5
0, 4 0, 6
2, 5
2, 4
El efecto de cualquier consulta hace que el nodo consultado y todos su ancestros
apunten a la raz, con lo que es posible que la profundidad del arbol disminuya. El
MFSET con compresi on de caminos es una estructura que mejora con el uso!
Que complejidad computacional presenta el MFSET con los heursticos de uni on por
rango y compresi on de caminos? No presentaremos aqu un an alisis detallado. Efectuar
m operaciones con n arboles de profundidad original unitaria es O(m(n)), donde es
la funci on inversa de cierta funci on A:
(n) = mn{k A(k, 1) n}.
La funci on A se dene as:
A(k, j) =
{
j +1, si k = 0;
A
(j+1)
(k 1, j), si k > 0;
donde
A
i
(k, j) =
{
j, si i = 0;
A(k, A
(i1)
(k, j)), si i > 0;
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 167

El lector encontrar a una discusi on detallada sobre el coste de las operacio-


nes en un MFSET en Introduction to Algorithms, de T. H. Cormen, C. Lei-
serson, R. Rivest y C. Stein.
Para los primeros valores de k, A(k, 1) es:
A(0, 1) = 2, A(1, 1) = 3, A(2, 1) = 7, A(3, 1) = 2047, A(4, 1) = 16
512
.
El valor de A(4, 1) es enorme (cercano a un uno seguido de 617 ceros). Cualquier
valor de n menor o igual que 16
512
hace, pues, que (n) sea menor o igual que 4. A
efectos pr acticos, podemos suponer que (n) es una constante.

El n umero de atomos estimado para el universo observable es de unos 10


80
,
as que suponer que (n) es una constante no parece descabellado.
As pues, en la pr actica, efectuar m operaciones cualesquiera con n arboles original-
mente unitarios se considera O(m), es decir, cada operaci on supone un coste amortizado
constante. Mostramos esta informaci on en la tabla 3.13.
MFSET
Operaci on Coste temporal
(amortizado)
S = MFSET() O(1)
S.add(x) O(1)
S.merge(x, y) O(1)
y = S.nd(x) O(1)
Tabla 3.13: Coste temporal amortizado de las operaciones sobre un MFSET.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35 Haz sendas trazas del algoritmo de c alculo de componentes conexas con las siguientes im age-
nes. Usa un MFSET para implementar los conjuntos de pxeles.
2 2
1 1
0 0
0 1 2 3 4 5 6
8
7
6
5
4
3
2
1
0
0 1 2 3 4 5 6 7 8 9 10
36 Escribe un programa de c alculo de componentes conexas en una imagen binaria que con-
sidere que cada pxel tiene 8 vecinos (en lugar de los cuatro que hemos considerado hasta el
momento).
37 Hemos implementado el MFSET con el heurstico de uni on por rango, que trata de llevar
cuenta de la profundidad de cada arbol y hacer que, al fundir dos arboles, el de menor altura sea
hijo de la raz del de mayor altura. Otro heurstico consiste en llevar cuenta del n umero de nodos
de cada arbol y, al fundir dos arboles, hacer que el que contiene menos nodos sea hijo de la raz
del que contiene m as. Implementa esta nueva versi on y compara empricamente la eciencia de
ambos heursticos.
38 Considera que la imagen presenta pxeles de 256 niveles de gris diferentes. Un pxel blanco
tiene valor 255 y un pxel negro tiene valor 0. Dise na un programa que, dado un umbral u (un
valor entre 0 y 255) detecte las regiones conexas de tonos de gris con valor menor o igual que u.
168 Apuntes de Algoritmia 28 de septiembre de 2009
39 Las personas que iniciaron la colonizaci on de una isla provienen todas de diferentes familias.
Conforme transcurre el tiempo, los habitantes de la isla se casan entre s y tienen descendencia.
En los juzgados de la isla se registran todas la bodas y nacimientos.
Hay una relaci on de parentesco natural entre padres, hijos y hermanos. Cuando dos personas
se casan, establecen un vnculo de parentesco poltico y pasan a formar parte de una misma
familia. La nueva relaci on de parentesco hace que todos los integrantes de las respectivas familias
de los c onyuges pasen a considerarse miembros de una sola familia. O sea, dos personas son
familia si existe una cadena de personas que son familia natural o poltica dos a dos.
Queremos saber en todo instante cu antos grupos familiares independientes hay. No nos im-
porta saber qu e personas pertenecen a qu e grupo: s olo nos importa saber cu antos grupos fami-
liares hay. La consulta en s debe efectuarse en tiempo constante.
40 Cierto juego de tablero para k jugadores se desarrolla en una matriz de n m hex agonos so-
bre la que se disponen chas hexagonales. La siguiente gura muestra un instante en una partida
sobre un tablero de 6 20 hex agonos con 3 jugadores (cuyas respectivas chas aparecen marcadas
con las letras A, B y C).
0
1
2
3
4
5
A B C C A A C A C B A
A C B B C A B C C B B B C B A C A
A B C B C C C A B C
A C B C C C B C A A C B A C
C B C C C B C A C C C
A C A C C B B C C A B A C
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
En cada turno, cada jugador dispone una cha propia en un hex agono libre siguiendo ciertas reg-
las que ahora no vienen al caso. Se dice que dos chas est an directamente conectadas si tienen un
lado com un y conectadas si es posible encontrar un camino que s olo pasa por pares de chas
conectadas directamente. Una regi on de chas conectadas es un conjunto de chas tal que cual-
quier par de ellas est an conectadas. En un momento dado, el juego naliza y se declara vencedor
al jugador con la regi on de chas conectadas de mayor tama no. En el ejemplo de la gura, vence
el jugador C, pues tiene una regi on con 8 chas (fjate en la regi on que incluye al hex agono de la
la 4 y columna 4), cuando las regiones conectadas m as grandes de los jugadores A y B tienen 3
y 5 chas, respectivamente.
Con qu e estructura o estructuras de datos y de qu e modo puedes determinar ecientemente, en
cualquier instante, qui en es el ganador? Qu e coste computacional presenta el m etodo expresado
en tu respuesta? Indica en qu e estado est an las estructuras de datos al inicio de una partida,
qu e ocurre tras cada jugada y c omo se puede decidir r apidamente qui en va ganando en cada
instante.
41 Queremos gestionar una agenda con n pares persona/n umero-de-tel efono. Una misma per-
sona puede tener m as de un n umero de tel efono y un mismo n umero de tel efono puede estar
asociado a m as de una persona. Tenemos un ejemplo en la siguiente lista de pares (donde usa-
mos n umeros de tel efono bajos en aras de la claridad): (Juan, 5), (Dani, 3), (Ana, 7), (Marcos, 5),
(Luis, 7), (Luis, 6), (Mara, 5), (Mara, 7), (Juana, 8), (Juana, 9), (Edma, 8), (Dani, 1) y (Julia, 9).
Qu e estructura o combinaci on de estructuras de datos utilizaras para conseguir efectuar
estas operaciones con las restricciones de complejidad computacional que se indican?
a) Dado un n umero de tel efono, mostrar el nombre de las p personas asociadas a el en tiempo
O(p +lg q), donde q es la cantidad de n umeros de tel efono distintos en la agenda.
b) Dado un nombre con m letras, mostrar los t n umeros de tel efono asociados a dicha persona
en tiempo O(m + t).
28 de septiembre de 2009 Captulo 3. Algunas estructuras de datos 169
Debes describir la estructura o combinaci on de estructuras de datos que garantizan los costes
indicados y describir brevemente el modo en que realizaras las operaciones. Ilustra tu respuesta
con representaciones esquem aticas para la lista de pares del ejemplo. Debes, adem as,
c) indicar un m etodo para la construcci on de esa estructura o combinaci on de estructuras
de datos a partir de un vector con una lista de pares persona/n umero-de-tel efono que no
presenta orden alguno,
d) e indicar el coste temporal de dicha construcci on.
Estamos interesados en encontrar unidades familiares en nuestra lista de personas y n ume-
ros de tel efono. Una unidad familiar es un conjunto de personas tales que cada una de ellas
comparte un n umero de tel efono con al menos otra persona de la unidad familiar. En la agenda
del ejemplo hay tres unidades familiares:
Juan, Marcos, Mara, Luis y Ana (Juan, Marcos y Mara comparten el n umero 5 y Luis,
Mara y Ana el 7)
Juana, Edma y Julia (Juana y Edma comparten el n umero 8 y Juana y Julia comparten el
n umero 9)
Dani (Dani no comparte n umero con nadie)
Se pide la estructura de datos que permite averiguar el n umero de unidades familiares, el
m etodo con el que es posible conocerlas y el coste espacial y temporal que supone el empleo de
dicho m etodo.
42 Deseamos becar a los m mejores estudiantes de una universidad en la que hay M estudiantes
matriculados y en la que se imparten n titulaciones. Para evitar agravios comparativos hemos de
becar a, al menos, k estudiantes de cada titulaci on (donde m kn). El procedimiento acordado
consiste en becar a los k estudiantes de cada titulaci on que presentan mejor expediente y, si m >
kn, becar a los restantes m kn estudiantes que, independientemente de la titulaci on cursada,
presentan mejor expediente. Selecciona las estructuras de datos adecuadas y dise na un m etodo
que solucione el problema en tiempo O(M + mlg M).
43 En un campamento con n ni nos se ha formado un n umero desconocido de clanes. Cuando
un ni no desea ingresar en un clan, debe solicitarlo a un miembro cualquiera del clan. Si este
accede, el ni no es sometido a un ritual inici atico e ingresa en el clan. Cada clan tiene un cabecilla
que es el fundador del mismo y que, naturalmente, no solicit o a nadie el ingreso en el clan. Un
clan est a formado por al menos dos personas. Deseamos acabar con los clanes del campamento,
pero lo primero es conocer con detalle la situaci on actual. A cada ni no se le pregunta si forma
parte de un clan (puede responder s o no) y, en caso de respuesta armativa, a qu e persona
solicit o su ingreso. Deseamos obtener un listado de clanes y el nombre del cabecilla de cada clan.
Indica la estructura o estructuras de datos con que implementar ecientemente esta operaci on de
clasicaci on en clanes y c omo generar el listado de clanes con sus respectivos miembros y nombre
del lder. Describe con precisi on el algoritmo o algoritmos que uses.
44 A los pacientes que llegan a urgencias en un hospital se les asigna un n umero entero de
0 a n atendiendo a la gravedad de su afecci on (n es la m axima gravedad y 0 es la mnima). Se
pens o en usar un m ax-montculo para implementar la lista de espera priorizada por gravedad,
pero se observ o una anomala: cuando a dos o m as pacientes se les asignaba una misma gravedad,
podan salir del m ax-montculo en cualquier orden. Es imperativo que, a igual gravedad, salgan
en el mismo orden en que entraron, es decir, priorizados por el valor de la hora de entrada (que
se conoce). Si tenemos en cuenta esta restricci on, c omo podemos garantizar que el ingreso y
la extracci on de un paciente de una lista con m pacientes sean operaciones de coste temporal
O(lg m)?
170 Apuntes de Algoritmia 28 de septiembre de 2009
45 Gestionamos una agencia que facilita la creaci on de clubes de amigos entre usuarios de In-
ternet. Nuestros abonados rellenan un formulario con sus preferencias en una serie de campos
(aci on por el cine, por los libros, por la m usica. . . ) y con el determinamos hasta qu e punto dos
personas son compatibles. Cuanticamos este grado de compatibilidad para todo par de perso-
nas con un n umero real entre 0 y 100 (ojo! el n umero puede tener decimales). Inicialmente cada
persona pertenece a un club del que s olo ella es miembro. Cuando encontramos dos personas con
un grado de compatibilidad mayor que cierto valor , hacemos que formen parte de un unico
club. El nuevo club contiene a todos los miembros de los clubes a los que pertenecen las personas
compatibles (aunque ello suponga que, posiblemente, haya personas en el mismo club con una
compatibilidad baja) y los clubes originales desparacen. Nuestros ingresos y gastos dependen
del n umero de usuarios y del n umero de clubes, as que recurrimos a una tabla f en cuya celda
f [n, m] est a almacenado el benecio que obtenemos con n usuarios y m clubes. Como dado un
n umero de usuarios n, hay un n umero de clubes (posiblemente) diferente para cada valor de ,
nos gustara saber qu e valor de conviene escoger para maximizar nuestro benecio.
Dise na una estructura o conjunto de estructuras de datos que nos permitan calcular eciente-
mente el valor optimo de . Un programa utilizar a la estructura del siguiente modo: (1) Dar a de
alta en el sistema a los n usuarios con sus respectivos formularios cumplimentados. (2) Calcu-
lar a el grado de compatibilidad entre todo par de personas (y suponemos que calcular el grado
de compatibilidad entre dos personas es una operaci on O(1)). (3) Ir a probando valores decrecien-
tes de , de 100 a 0, calcular a para cada valor de el n umero de clubes de amigos, m, y con dicho
valor acceder a al benecio f [n, m]. (4) Mostrar a por pantalla el valor de que ofreci o mayor valor
de f [n, m].
El programa debe ejecutarse en tiempo O(n
2
log n), lo que depende de la estructura o estruc-
turas de datos que uses. Haz una propuesta y justica que garantiza ese coste temporal.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Captulo 4
ALGORITMOS SOBRE GRAFOS
Muchos problemas se pueden modelar en t erminos de b usqueda de ciertas estructuras en
grafos ponderados o no. Cuando abordemos determinados problemas puede que recu-
rramos instrumentalmente a algoritmos que solucionan algunos problemas fundamen-
tales: recorrer los v ertices siguiendo cierto criterio en el que juegan un papel importante
las aristas, ordenar topol ogicamente los v ertices de un digrafo acclico, hallar un arbol
de recubrimiento, conocer el camino m as corto entre dos v ertices, o entre un conjunto de
v ertices y otro, etc. En este captulo formulamos algunos de estos problemas b asicos y
presentamos algoritmos que los solucionan ecientemente.
4.1. Exploraci on de grafos
Es frecuente tener que recorrer los v ertices de un grafo en cierto orden para aplicar alg un
proceso sobre ellos. Los primeros algoritmos sobre grafos que vamos a estudiar solucio-
nan el problema de recorrer o explorar todos los v ertices de un grafo de acuerdo con
ciertos criterios. No parecen tener gran inter es por s mismos, pero son instrumenta-
les para el dise no de otros algoritmos. Los usaremos, por ejemplo, en otros apartados
de esta secci on, pero tambi en al estudiar las estrategias de b usqueda con retroceso y
ramicaci on y acotaci on.
Los algoritmos de recorrido son generalizaciones de los que ya hemos presentado
sobre arboles con raz (apartado 3.9.5). Estos son el recorrido por primero en anchura y
el recorrido por primero en profundidad, este ultimo con dos variantes: en preorden y en
postorden.
Centraremos el discurso en los grafos no dirigidos y consideraremos m as adelante
el caso de los digrafos en general. Los algoritmos de recorrido de grafos se presentar an
como clases que implementan esta interfaz:
171
172 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/graphalgorithms/traversals.py
class IDigraphTraverser(metaclass=ABCMeta):
@abstractmethod
def traverse(self , G: "Digraph<T>", s: "T", visitor: "f: T, T -> S") -> "iterable<S>":
pass
@abstractmethod
def full traverse(self , G: "Digraph<T>", visitor: "f: T, T -> S") -> "iterable<S>":
pass
El m etodo traverse recorrer a todos los v ertices desde uno que se suministra como
par ametro (y que denotamos con s por start vertex o v ertice inicial).
Recordemos que un conjunto de v ertices mutuamente alcanzables recibe el nombre
de componente conexo. La alcanzabilidad en un grafo no dirigido dene una relaci on de
equivalencia que particiona el conjunto de v ertices en componentes conexos. Un grafo
no dirigido es conexo si s olo presenta un componente conexo. Si el grafo no es cone-
xo, s olo se visitar an los v ertices del componente conexo al que pertenece ese v ertice. El
m etodo full traverse visitar a con seguridad todos los v ertices del grafo, aunque este no
sea conexo. N otese que full traverse no requiere que se indique ning un v ertice de partida.
En ocasiones resultar a de inter es recorrer conocer desde qu e v ertice se alcanza cada uno
de los v ertices en el recorrido. Podemos dise nar un m etodo exible, capaz de enumerar
v ertices o aristas a voluntad, si usamos como par ametro una funci on visitadora que se
invocar a sobre aristas (pares de v ertices) y devolver a un valor, que puede ser la misma
arista o su v ertice de llegada.
Es decir, cuando se visita un v ertice v directamente desde otro u se invocar a a la fun-
ci on visitadora suministrando como argumento la arista (u, v). Como el v ertice de parti-
da v del recorrido de cualquier zona conexa no se alcanza desde otro v ertice, para ellos
suministraremos como argumento el par (v, v). En un grafo conexo, s olo se visitar a un
par como ese: el del v ertice en el que iniciamos el recorrido. En un grafo no conexo,
full traverse producir a una visita a un par como ese por cada componente conexo.
N otese que (v, v) no es una arista del grafo no dirigido, as que hablaremos de pseudo-
aristas cuando nos reramos a los pares de v ertices que se usan como argumentos de la
funci on visitadora.
4.1.1. Exploraci on de grafos por primero en anchura
La exploraci on por primero en anchura propone el recorrido de los v ertices de un grafo
en el que, cuando visitamos un v ertice v, se visitar an a inmediatamente a continuaci on
todos sus sucesores no visitados. Como no hay un orden establecido entre los sucesores
de un v ertice, no podremos hablar de un recorrido unico por primero en anchura: cuando
toque seleccionar los sucesores de un v ertice, estos se recorrer an en un orden arbitrario,
de ah que no haya un unico recorrido por primero en anchura.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 173
Exploraci on por primero anchura de los v ertices de un componente conexo
El algoritmo de recorrido se ayuda de una cola FIFOy de un conjunto de v ertices al que se
van a nadiendo los ya visitados. El conjunto de visitados es necesario porque, a diferencia
del caso de los arboles, ahora es posible llegar a un mismo v ertice desde otro por m as de
un camino y tendremos que marcar de alg un modo los que ya han sido visitados para
evitar repetir visitas. He aqu el algoritmo:
1. El v ertice inicial se inserta en una cola FIFO, se anota como visitado y se aplica la
funci on visitor a la pseudo-arista (s, s).
2. Se repite el siguiente procedimiento hasta que la cola est e vaca: Se extrae el primer
v ertice u de la cola y se consideran todos sus sucesores. Si un sucesor v no haba sido
visitado a un, se marca como visitado, se a nade a la cola FIFO y se aplica visitor sobre
la arista (u, v).
Nuestra implementaci on usa una funci on visitor por defecto que produce el v ertice
destino cuando se visita una arista:
algoritmia/graphalgorithms/traversals.py
from algoritmia.datastructures.queues import FIFO
from algoritmia.datastructures.graphs import Digraph
...
class BreadthFirstTraverser(IDigraphTraverser):
def init (self , **kw):
get factories(self , kw, foFactory=lambda G: FIFO(),
setFactory=lambda G: set())
def traverse(self , G: "Digraph<T>", s: "T",
visitor: "f: T, T-> S"=None)->"iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
return self . traverse(G, s, visitor, visited)
def traverse(self , G, s, visitor, visited) -> "iterable<S>":
visited.add(s)
Q = self .foFactory(G.V)
Q.push(s)
yield visitor(s, s)
while len(Q) > 0:
u = Q.pop()
for v in G.succs(u):
if v not in visited:
Q.push(v)
visited.add(v)
yield visitor(u, v)
174 Apuntes de Algoritmia 28 de septiembre de 2009
demos/graphalgorithms/bfs.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.traversals import BreadthFirstTraverser
G = UndirectedGraph(E=[(0,1), (0,3), (1,4), (2,5), (3,1), (4,3)])
print(list(BreadthFirstTraverser().traverse(G, 0)))
[0, 1, 3, 4]
En la gura 4.1 se muestra una traza paso a paso de BreadthFirstTraverser.traverse
sobre el grafo con el que hemos probado el algoritmo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46 Haz una traza de BreadthFirstTraverser.traverse al invocarse sobre el v ertice 0 de estos grafos.
Indica el resulta de la enumeraci on con una funci on visitor denida como lambda u,v: (u, v).
0 1 2
3 4 5
0 1 2
3 4 5
6 7 8
(a) (b)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Exploraci on por primero anchura de todos los v ertices
Hemos visto que el algoritmo s olo recorre los v ertices alcanzables desde el v ertice inicial.
Los v ertices 2 y 5 del grafo de la gura 4.1 no se visitan si iniciamos el recorrido en los
v ertices 0, 1, 3 o 4, pues no son alcanzables desde ninguno de ellos.
Un algoritmo que recorre todos los v ertices procede del siguiente modo: lanza un
recorrido de componente conexo por primero en anchura sobre un v ertice cualquiera y
marca como visitados a todos los v ertices de su componente; a continuaci on, prueba con
cada uno de los dem as v ertices y, cada vez que encuentra uno no visitado (o sea, uno que
no pertenece a un componente ya explorado), lanza una nueva b usqueda por primero en
anchura desde ese v ertice y a nade los v ertices visitados al conjunto:
algoritmia/graphalgorithms/traversals.py
class BreadthFirstTraverser(IDigraphTraverser):
...
def full traverse(self , G, visitor=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
for u in G.V:
if u not in visited:
for v in self . traverse(G, u, visitor, visited): yield v
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 175
0 1 2
3 4 5
Q 0
visitor(0, 0)
0 1 2
3 4 5
0
(a) Grafo sobre el que proponemos una exploraci on
por primero en anchura desde el v ertice 0.
(b) Se almacena en visited el v ertice 0, se apila en Q
y se visita la pseudo-arista (0, 0), que con la funci on
visitor por defecto proporciona el v ertice 0.
Q 1 3
visitor(0, 1)
visitor(0, 3)
0 1 2
3 4 5
0 1
3
Q 3 4
visitor(1, 4)
0 1 2
3 4 5
0 1
3 4
(c) Se extrae de Q el v ertice 0 y sus sucesores se
exploran. Primero, el v ertice 1 (aunque el orden de
visita es arbitrario). Como a un no est a en visited,
se a nade a Q y a visited y se emite el resultado de
llamar a visitor(0,1). A continuaci on se explora el
v ertice 3 y emite el resultado de visitor(0,3).
(d) Se extrae de Q el v ertice 1 y se recorren sus su-
cesores. El v ertice 3 (el orden de visita es arbitrario)
ya ha ingresado en visited, as que no se ejecuta ac-
ci on alguna sobre el. El v ertice 4 no forma parte de
visited y, por tanto, se a nade a Q y se emite el resul-
tado de llamar a visitor(1,4).
Q 4
0 1 2
3 4 5
0 1
3 4
Q
0 1 2
3 4 5
0 1
3 4
(e) Se extrae de Q el v ertice 3. Sus sucesores, los
v ertices 0, 1 y 4 ya est an en visited, por lo que no
disparan la ejecuci on de nuevas acciones.
(f) Se extrae de Q el v ertice 4. Sus sucesores, los
v ertices 1 y 3 ya est an en visited, por lo que no dis-
paran la ejecuci on de nuevas acciones. La cola Q
est a vaca, por lo que el algoritmo termina.
Figura 4.1: Traza de la exploraci on por primero en anchura sobre un grafo, empezando en un v ertice determinado: el
v ertice 0. Las subguras (a) y (b) muestran el estado del algoritmo antes de iniciar la ejecuci on del bucle. Cada una de
las siguientes subgura muestra el estado tras la ejecuci on de una iteraci on del bucle while. Con la funci on de visita
por defecto, se enumeran los v ertices del componente conexo que incluye a 0 en este orden: 0, 1, 3 y 4.
demos/graphalgorithms/bfsfull.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.traversals import BreadthFirstTraverser
G = UndirectedGraph(E=[(0,1), (0,3), (1,4), (2,5), (3,1), (4,3)])
print(list(BreadthFirstTraverser().full traverse(G)))
[0, 1, 3, 4, 2, 5]
El m etodo BreadthFirstTraverser.full traverse recorre los componentes conexos en un
176 Apuntes de Algoritmia 28 de septiembre de 2009
orden arbitrario y dentro de cada componente puede empezar por uno cualquiera de sus
v ertices. Supongamos que empieza por el v ertice 0. Si hacemos una traza paso a paso
llegaremos a la misma situaci on descrita en el ultimo paso de la gura 4.1, pues la pila
estar a vaca. Pero cada vez que vaca la pila, el algoritmo prosigue con su recorrido de
v ertices hasta encontrar uno no visitado y lanza una nueva exploraci on desde el.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47 Haz una traza del m etodo BreadthFirstTraverser.full traverse sobre el grafo no dirigido de la
gura. Destaca las aristas que se procesan durante la exploraci on.
0
1
2
3
4
5
6
7
8
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
An alisis de complejidad
Conviene se nalar que hemos dise nado algoritmos gen ericos en el sentido de que permi-
ten especicar la estructura de datos auxiliar con la que se lleva cuenta de los v ertices ya
visitados (mediante la factora setFactory). Si el grafo est a formado por v ertices identica-
dos con n umeros enteros en un rango [0..n], podemos usar un vector caracterstico para
ofrecer costes O(1) en el peor de los casos cuando se a nade un v ertice o se inquiere por
la pertenencia de un v ertice al conjunto (y no s olo en promedio, como en el caso de los
conjuntos implementados con tablas de dispersi on). He aqu un ejemplo de uso de IntSet
como conjunto de visitados al recorrer un grafo que tambi en usa estructuras de datos
optimizadas para v ertices enteros:
demos/graphalgorithms/bfsints.py
from algoritmia.datastructures.graphs import AdjacencyDigraph
from algoritmia.datastructures.sets import IntSet
from algoritmia.datastructures.mappings import IntKeyMapping
from algoritmia.graphalgorithms.traversals import BreadthFirstTraverser
G = AdjacencyDigraph(E=[(0,1), (0,3), (1,4), (2,5), (3,1), (4,3)],
mappingFactory=lambda V: IntKeyMapping(capacity=max(V)+1),
setFactory=lambda V: IntSet(capacity=max(V)+1))
bft = BreadthFirstTraverser(setFactory=lambda V: IntSet(capacity=max(V)+1))
print(list(bft.traverse(G, 0)))
[0, 1, 3, 4]
En los an alisis de costes, por tanto, se supondr a siempre que se escogen las estructuras
de datos m as apropiadas para un codicaci on eciente de los datos. En el caso de los
grafos, por ejemplo, se asumir a en los an alisis que los v ertices se identican con enteros
en un rango y que se pueden usar vectores caractersticos para implementar conjuntos o
que los diccionarios se pueden implementar con vectores.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 177
Si asumimos una representaci on de los grafos con conjuntos de adyacencia basa-
dos en listas (enlazadas o no) o tablas de dispersi on, el coste temporal del recorrido es
O(V + E): cada v ertice ingresa en la pila y se extrae de ella una sola vez, y para ca-
da v ertice extrado se recorren todas las aristas que parten de el. El espacio necesario es
O(V), ya que la pila Q puede llegar a almacenar V 1 v ertices.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
48 El coste temporal del recorrido es O(V
2
) si el grafo se representa con una matriz de adya-
cencia. Por qu e? Se ve afectado tambi en el coste espacial?
49 Escribe una funci on que determine si un grafo no dirigido es conexo mediante una explora-
ci on en anchura de sus v ertices. Cu al es el coste temporal del algoritmo?
50 El recorrido por primero en anchura permite detectar si un grafo dirigido presenta ciclos.
Implementa una funci on detecta ciclos que devuelva True si el grafo contiene ciclos y False en caso
contrario. Qu e complejidad computacional tiene?
51 El caballo efect ua un curioso movimiento en el juego de ajedrez. En la siguiente gura se
marcan todos los escaques alcanzables con un solo movimiento desde el escaque D4, que son E6,
F5, F3, E2, C2, B3, B5 y C6.
A B C D E F G H
1
2
3
4
5
6
7
8
Dise na un programa que permita responder a las siguientes preguntas mostrando el conjunto de
casillas alcanzables con cero o m as movimientos desde una casilla determinada:
a) Qu e casillas podemos alcanzar con un caballo blanco que parte de D1?
b) Puede un caballo alcanzar cualquier casilla del tablero desde cualquier otra?
c) Podemos alcanzar cualquier casilla de un tablero de 2 100 con un caballo ubicado inicial-
mente en el escaque A1? Y en un tablero de 3 100?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
El caso de los digrafos
En un digrafo G = (V, E), los v ertices alcanzables desde un v ertice v no necesariamente
permiten alcanzar, a su vez, a v. Por tanto, la relaci on de alcanzabilidad no particiona a V
en componentes conexos. Con todo, podemos estar interesados en efectuar exploraciones
de los v ertices desde uno dado usando el m etodo propuesto, pero teniendo en cuenta
que s olo hay garanta de visitar todos los v ertices con el m etodo traverse si el grafo es
fuertemente conexo, es decir, si todo par de v ertices es mutuamente alcanzable.
4.1.2. Exploraci on de grafos por primero en profundidad
La exploraci on por primero en profundidad puede verse como un proceso recursivo que
empieza en un v ertice desde el que se dispara un recorrido por primero en profundidad
178 Apuntes de Algoritmia 28 de septiembre de 2009
para cada uno de los sucesores no visitados. Hay dos variantes: exploraci on en preorden
y exploraci on en postorden, dependiendo de si la llamada a visitor sobre un v ertice (en
realidad sobre una arista o pseudo-arista) se efect ua antes de visitar a sus sucesores o
despu es.
El procedimiento es de naturaleza recursiva. Al visitar un v ertice tambi en ahora pro-
cesaremos la arista desde la que dicho v ertice ha sido visitado (con la excepci on de la
visita al v ertice s, para el que procesamos la pseudo-arista (s, s)):
1. Si llegamos a v desde u marcamos a v como visitado.
2. Procesamos la arista (u, v) si el recorrido es en preorden.
3. Aplicamos el mismo m etodo sobre todo v ertice sucesor de v no visitado previamente.
4. Y procesamos la arista (u, v) si el recorrido es en postorden.
Al igual que ocurra con el recorrido por primero en anchura, el que no haya un
orden denido entre los sucesores de cada v ertice hace que no haya un unico recorrido
en preorden o postorden.
La gura 4.2 ilustra un recorrido de v ertices en preorden.
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
2
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
2 3
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
1
2 3
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3
(a) (b) (c) (d) (e)
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
7
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
7 8
(f) (g) (h) (i) (j)
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
0 1
2 3 4
5 6 7 8
(k) (l) (m) (n) ( n)
Figura 4.2: Traza de la exploraci on por primero en profundidad en preorden sobre un grafo empezando en el v ertice 2.
Los v ertices se recorren en el orden 2, 3, 1, 0, 4, 7, 8, 6 y 5. Los v ertices se van sombreando conforme son visitados por
el algoritmo. Las aristas en trazo grueso representan la recursi on en el proceso de exploraci on: cada v ertice a lo largo
del camino en trazo grueso corresponde a una llamada al procedimiento de c alculo. Al alcanzar un nuevo v ertice, se
procesa la ultima arista destacada en trazo negro.
Una implementaci on recursiva de los recorridos en preorden y en postorden
Resulta inmediato codicar estos recorridos recursivamente, tanto para el recorrido de
los v ertices en un componente conexo como para los de todos los componentes conexos:
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 179
algoritmia/graphalgorithms/traversals.py
class RecursivePreorderTraverser(IDigraphTraverser):
def init (self , **kw):
get factories(self , kw, foFactory=lambda G: FIFO(),
setFactory=lambda G: set())
def traverse(self , G: "Digraph<T>", u: "T",
visitor: "f: T, T -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
fo = self .foFactory(G.V)
self . recursive traversal(G, u, u, visitor, visited, fo)
return fo
def recursive traversal(self , G: "Digraph<T>", u: "T", v: "T",
visitor: "f: T, T -> S", visited: "set<T>", fo: "FIFO<T>") -> "iterable<S>":
visited.add(v)
fo.push(visitor(u, v))
for w in G.succs(v):
if w not in visited:
self . recursive traversal(G, v, w, visitor, visited, fo)
def full traverse(self , G: "Digraph<T>",
visitor: "f: T, T -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
fo = self .foFactory(G.V)
for u in G.V:
if u not in visited:
self . recursive traversal(G, u, u, visitor, visited, fo)
while len(fo) > 0:
yield fo.pop()
class RecursivePostorderTraverser(IDigraphTraverser):
def init (self , **kw):
get factories(self , kw, foFactory=lambda V: FIFO(),
setFactory=lambda V: set())
def traverse(self , G: "Digraph<T>", u: "T", visitor: "f: T, T -> S"=None,
visited: "set<T>"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
if visited == None: visited = self .setFactory(G.V)
fo = self .foFactory(G.V)
self . recursive traversal(G, u, u, visitor, visited, fo)
return fo
def recursive traversal(self , G: "Digraph<T>", u: "T", v: "T",
visitor: "f: T, T -> S", visited: "set<T>", fo: "FIFO<T>") -> "iterable<S>":
visited.add(v)
180 Apuntes de Algoritmia 28 de septiembre de 2009
for w in G.succs(v):
if w not in visited: self . recursive traversal(G, v, w, visitor, visited, fo)
fo.push(visitor(u, v))
def full traverse(self , G: "Digraph<T>",
visitor: "f: T, T -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
for u in G.V:
if u not in visited:
for v in self .traverse(G, u, visitor, visited): yield v
Los recorridos recursivos presentan una desventaja: la enumeraci on de los resultados
de las visitas se retrasa hasta el nal de la exploraci on de cada componente conexo. Si se
deseara emitir dichos resultados tan pronto es posible conocerlos, el algoritmo incurrira
en una ineciencia importante, pues cada uno de ellos debera ascender por una sere
de llamadas recursivas recursive traversal hasta conseguir ser emitido en la llamada a tra-
versal o full traversal. La soluci on adoptada para evitar este problema es ir almacenando
los resultados en una cola FIFO tan pronto se producen para emitirlos todos seguidos
desde traversal o full traversal.
Hay una raz on para que el m etodo recursivo recursive traversal de los dos recorridos
no devuelvan directamente el resultado del procesado de las aristas: si lo hicieran el
coste temporal sera O(V
2
), pues los objetos devueltos va yield desde una invocaci on
recursiva a recursive traversal deberan ascender en la pila de llamadas a funci on de
alg un modo hasta la llamada a traversal o full traversal. Como la pila puede llegar a tener
profundidad V, cada objeto (y hay V) tendra que ser devuelve hasta ese n umero de
veces. La pila enumeration sirve de almac en temporal en el que los v ertices ingresan (y se
extraen) por orden de enumeraci on. As pues, el coste temporal de nuestra aproximaci on
recursiva es O(V +E), y el espacial es O(V).
Hemos de matizar nuestra armaci on de que el coste temporal es O(V +E). Esto
es cierto si el grafo se representa con conjuntos de adyacencia soportados con listas: cada
v ertice del grafo es visitado una sola vez y para cada v ertice visitado se recorren todas
sus aristas de salida. Si, por contra, el grafo se implementa con una matriz de adyacencia,
el coste temporal asciende a O(V
2
).
Una implementaci on iterativa del recorrido en preorden
Podemos transformar el algoritmo recursivo de recorrido en preorden en otro iterativo
equivalente con ayuda de un pila de aristas:
algoritmia/graphalgorithms/traversals.py
from algoritmia.datastructures.queues import LIFO
from itertools import dropwhile
...
class PreorderTraverser(IDigraphTraverser):
def init (self , **kw):
get factories(self , kw, lifoFactory=lambda V: LIFO(),
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 181
setFactory=lambda V: set())
def traverse(self , G: "Digraph<T>", u: "T",
visitor: "f: T, T -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
stack = self .lifoFactory(G.V)
return self . traverse(G, u, visitor, stack, visited)
def traverse(self , G: "Digraph<T>", u: "T", visitor: "f: T, T -> S",
stack: "lifo<T>", visited: "set<T>") -> "iterable<S>":
stack.push((u, u))
while len(stack) > 0:
(u, v) = stack.pop()
if v not in visited:
visited.add(v)
yield visitor(u, v)
for w in G.succs(v):
if w not in visited:
stack.push((v, w))
def full traverse(self , G: "Digraph<T>",
visitor: "f: T, T -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
stack = self .lifoFactory(G.V)
for v in G.V:
if v not in visited:
for w in self . traverse(G, v, visitor, stack, visited):
yield w
El coste temporal de esta implementaci on O(V +E), id entico al de la versi on recur-
siva. El coste espacial, sin embargo, es O(E), ya que se van apilando aristas. Podemos
reducir el coste espacial en una versi on iterativa si adoptamos la aproximaci on que se
sigue en la del recorrido en postorden que presentamos a continuaci on.
Una implementaci on iterativa del recorrido en postorden
El recorrido en postorden tambi en puede efectuarse iterativamente con ayuda de una
pila de aristas:
algoritmia/graphalgorithms/traversals.py
class PostorderTraverser(IDigraphTraverser):
def init (self , **kw):
get factories(self , kw, lifoFactory=lambda V: LIFO(),
setFactory=lambda V: set())
def traverse(self , G, u, visitor=None):
visitor = visitor or (lambda u, v: v)
182 Apuntes de Algoritmia 28 de septiembre de 2009
visited = self .setFactory(G.V)
return self . traverse(G, u, visitor, visited)
def traverse(self , G, u, visitor=lambda u, v: v, visited: "Set of V"=None):
def traverse(self , G, u, visitor=lambda u, v: v, visited: "Set of V"=None):
visited.add(u)
S = self .lifoFactory(G.V)
S.push(((u, u), (v for v in G.succs(u))))
while len(S) > 0:
((u, v), succs) = S.pop()
succs = dropwhile(lambda w: w in visited, succs)
w = next(succs, None)
if w != None:
S.push(((u, v), succs))
S.push(((v, w), (x for x in G.succs(w))))
visited.add(w)
else:
yield visitor(u, v)
def full traverse(self , G, visitor=None):
visitor = visitor or (lambda u, v: v)
visited = self .setFactory(G.V)
S = self .lifoFactory(G.V)
for v in G.V:
if v not in visited:
for w in self . traverse(G, v, visitor, visited):
yield w
El coste temporal sigue siendo O(V +E). El coste espacial es O(V) porque en la
pila se almacena a lo sumo un elemento por v ertice. Cada uno de los elementos apilados
es un tanto complejo (un par formado por una arista y un iterador), pero los iteradores
son estructuras que consumen espacio constante.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52 Haz una traza de los algoritmos de exploraci on por primero en profundidad sobre este grafo
no dirigido:
1 2 3 4
5 6 7
53 Muestra ordenadamente las v ertices visitados al recorrer en preorden y postorden los grafos
del ejercicio 46.
54 Un tipo particular de laberinto consiste en una matriz que puede contener muro o paso
abierto entre dos celdas vecinas. Todas las celdas de los bordes tienen muro en su lado o lados
exteriores. Hay dos excepciones: las casillas de entrada y salida no tienen muro exterior. Desde
cualquier casilla se puede acceder a cualquier otra a trav es de un camino y todos los muros est an
conectados entre s de tal modo que si a nadimos uno m as las casillas dejan de ser mutuamente
alcanzables. Se dice que el laberinto est a bien formado si entre dos celdas cualesquiera s olo
hay un camino que no pasa dos veces por una misma celda. Un rompecabezas cl asico consiste en
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 183
encontrar el camino que va de la entrada a la salida sin visitar dos veces una misma celda. He
aqu un laberinto como el descrito construido sobre una matriz de 40 20 celdas:
Dise na un programa que construya laberintos siguiendo una estrategia basada en la explo-
raci on por primero en profundidad. Te apuntamos la idea clave. Empieza por poner 4 muros a
todas las celdas de la matriz. Inicia entonces un recorrido por primero en profundidad desde una
casilla cualquiera y de modo que, en cada instante, escoja al azar la siguiente casilla que ha de vi-
sitar de entre sus (hasta) 4 vecinos. Una casilla s olo es elegible si no ha sido visitada previamente.
Ve destruyendo los muros que separan celdas conforme pasas de una celda a otra. Al nal, rompe
los muros exteriores de dos casillas para que el laberinto tenga una entrada y una salida.
Para representar el laberinto puedes usar una matriz en la que cada celda indica si hay pared
al norte, al sur, al este y/o al oeste. Una cadena puede indicar qu e muros est an presentes (n
para norte, s para sur, w para oeste, y e para este). He aqu un peque no laberinto y su
representaci on:
[[wn, nes, wns, ne],
[ws, n, ne, w],
[ns, e, wes, we],
[wns, s, ns, es]]
Aunque, la verdad, puede resultar m as eciente codicar con s olo cuatro bits la existencia
de paredes. El valor 15, que en binario es 1111, representara una casilla con pared en las cuatro
direcciones. Recuerda que puedes activar y desactivar bits con operaciones como & (y-l ogica bit a
bit) y | (o-l ogica bit a bit). Y a un m as eciente resulta codicar en cada casilla la posible existencia
de s olo dos muros: el muro este y el sur. Es posible porque, salvo en la casillas de los bordes oeste
y norte, la existencia de un muro oeste se deduce de la existencia de un muro este en la casilla
de su izquierda, y la existencia de un muro norte se deduce de la existencia de un muro sur en la
casilla de encima.
55 Dise na una funci on que reciba un laberinto (descrito con la codicaci on anterior) al que se
han a nadido n muros al azar, y nos diga si la entrada y la salida est an conectadas.
56 Dise na un algoritmo que construya laberintos bien formados, pero ahora sobre una malla
hexagonal. He aqu un ejemplo de este nuevo tipo de laberinto:
184 Apuntes de Algoritmia 28 de septiembre de 2009
(Pista: El soporte del laberinto sigue siendo una matriz convencional. La principal diferencia
estriba en las relaciones de vecindad entre casillas.)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2. Componentes conexos en grafos no dirigidos
Ciertos algoritmos para la resoluci on de otros problemas s olo funcionan correctamente
si el grafo es conexo, as que un test de conectividad puede constituir un paso previo
necesario para garantizar la existencia o validez de una soluci on.
Dise naremos diferentes clases para la detecci on de componentes conexos y determi-
naci on de conectividad en grafos no dirigidos que implementar an esta interfaz:
algoritmia/graphalgorithms/connectedcomponents.py
class IConnectedComponentsFinder(metaclass=ABCMeta):
@abstractmethod
def connected components(self ,
G: "undirected Digraph<T>") -> "iterable<iterable<T>>": pass
def is connected(self , G: "undirected Digraph<T>") -> "bool":
return count(self .connected components(G)) == 1
(N otese que se ofrece una implementaci on por defecto para is connected.)
Detecci on basada en el recorrido de grafos
Los algoritmos de recorrido de grafos no dirigidos permiten conocer los componentes
conexos. Si generamos las aristas en lugar de los v ertices, cada pseudo-arista de la forma
(u, u) marca el inicio de un componente conexo, lo que hace inmediato implementar un
buscador de componentes conexos basado en el recorrido de grafos:
algoritmia/graphalgorithms/connectedcomponents.py
from algoritmia.graphalgorithms.traversals import BreadthFirstTraverser
from algoritmia.utils import count
...
class GraphTraversalConnectedComponentsFinder(IConnectedComponentsFinder):
def init (self , **kw):
get factories(self , kw, digraphTraverserFactory=lambda G: BreadthFirstTraverser())
def connected components(self , G: "undirected Digraph<T>"\
) -> "iterable<iterable<T>>":
traverser = self .digraphTraverserFactory(G)
component = []
rst = True
for (u, v) in traverser.full traverse(G, visitor=lambda u, v: (u, v)):
if u == v and not rst:
yield component
component = []
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 185
component.append(v)
rst = False
yield component
Probemos el algoritmo con el grafo de la gura 4.3:
0
1
2
3
4
5
6
7
8
Figura 4.3: Un grafo no dirigido con tres componentes conexos.
demos/graphalgorithms/conncomps.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.connectedcomponents import *
G=UndirectedGraph(E=[(0,1), (0,2), (1,2), (3,4),
(5,6), (5,7), (5,8), (6,7), (6,8), (7,8)])
print([c for c in GraphTraversalConnectedComponentsFinder().connected components(G)])
[[0, 1, 2], [3, 4], [5, 8, 6, 7]]
El coste temporal del algoritmo es O(V + E), que es el coste del recorrido por
primero en profundidad.
Detecci on basada en conjuntos de uni on-b usqueda
Ya resolvimos el problema de la detecci on de los componentes conexos en el captulo
anterior sobre un grafo no dirigido particular cuando buscamos regiones de un mismo
color en una imagen. La imagen puede representarse con un grafo no dirigido si consi-
deramos que cada pixel del color que nos interesa es un v ertice y est a conectado a los
v ertices del mismo color que son vecinos suyos. Como vimos entonces, la estructura de
datos MFSET ayuda a resolver ecientemente el problema. El siguiente algoritmo adapta
a grafos no dirigidos en general la soluci on que dise namos entonces para im agenes:
algoritmia/graphalgorithms/connectedcomponents.py
from algoritmia.datastructures.mergendsets import MFset
...
class SetMergingConnectedComponentsFinder(IConnectedComponentsFinder):
def init (self , **kw):
get factories(self , kw, mfsetFactory=lambda V: MFset((v,) for v in V))
def connected components(self , G: "undirected Digraph<T>"\
) -> "iterable<iterable<T>>":
mfset = self .mfsetFactory(G.V)
for (u, v) in G.E: mfset.merge(u, v)
return mfset.partition()
186 Apuntes de Algoritmia 28 de septiembre de 2009
En una primera fase, el algoritmo construye un bosque de conjuntos disjuntos en el
que cada v ertice forma parte de un arbol distinto (es decir, forma parte de un conjun-
to unitario). A continuaci on, une todo par de conjuntos unidos por una arista. El coste
temporal es O(V +E) y el coste espacial es O(V).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
57 Podemos generar laberintos mediante uniendo componentes conexos con conjuntos de uni on-
b usqueda. En una matriz selecciona un muro cualquiera que separa dos casillas. Detecta si las
casillas a ambos lados del muro pertenecen a componentes conexos diferentes. Si es as, elimina
el muro. Un MFSET permite controlar ecientemente a qu e componente conexo pertenece cada
casilla, as como unir dos componentes conexos cuando se elimina un muro. Cuando no se pue-
den eliminar m as muros, tenemos un laberinto que conecta entre s a cualquier par de casillas.
Implementa un algoritmo que genere laberintos mediante esta t ecnica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.

Arboles de recubrimiento
Un arbol no dirigido es un grafo no dirigido que observa ciertas propiedades. Un arbol
de recubrimiento para un grafo no dirigido G = (V, E) es un arbol cuyos v ertices coinci-
den con V y cuyo conjunto de aristas es un subconjunto de E formado por V 1 aristas
y tal que todo par de nodos est a unido por un unico camino que no repite aristas. En la
gura 4.4 se muestra un grafo no dirigido y dos posibles arboles de recubrimiento.
Figura 4.4: (a) Un grafo no
dirigido. (b)

Arbol de recu-
brimiento sobre el grafo (aris-
tas en trazo grueso). (c) Otro
arbol de recubrimiento.
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
(a) (b) (c)
Implementaremos clases para el c alculo del arbol de recubrimiento que se adaptan a
esta interfaz:
algoritmia/graphalgorithms/spanning.py
class ISpanningTreeFinder(metaclass=ABCMeta):
@abstractmethod
def spanning tree(self , G: "undirected Digraph<T>", s: "T") -> "iterable<(T, T)>":
pass
Tambi en podemos hablar de arbol de recubrimiento con raz en el caso de los digrafos.
Dado un v ertice v, un arbol de recubrimiento que lo tenga por raz es un arbol (dirigido)
con el conjunto de v ertices alcanzables desde v y tal que un unico camino una a v con
cada uno de dichos v ertices. En la gura 4.5 (b), por ejemplo, se muestra un arbol de re-
cubrimiento con raz para el grafo de la gura 4.5 (a) obtenido a partir de una exploraci on
del grafo por primero en profundidad.
Si el grafo no dirigido no es conexo, no hay arbol de recubrimiento. Cuando el grafo
no es conexo, no obstante, podemos calcular un arbol de recubrimiento por cada subcon-
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 187
0
1
2
3
4 5
6 7
8 0
1
2
3
4 5
6 7
8 0
1
2
3
4 5
6 7
8
(a) (b)
Figura 4.5: (a) Un di-
grafo. (b) En trazo
grueso, aristas de un
arbol de recubrimiento
con raz (en el v ertice
0) para el grafo.
junto de v ertices mutuamente alcanzables. Todos ellos forman lo que se denomina un
bosque de recubrimiento. La gura 4.6 muestra un grafo no dirigido y no conexo y uno
de sus bosques de recubrimiento.
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
0
1
2
3
4
5
6
7
8
9
(a) (b)
Figura 4.6: (a) Un grafo no dirigido y no conexo. (b) Un
bosque de recubrimiento sobre el grafo (aristas en trazo
grueso).
Las clases que implementen m etodos de b usqueda de bosques de recubrimiento se
ce nir an a esta interfaz ISpanningForestFinder:
algoritmia/graphalgorithms/spanning.py
class ISpanningForestFinder(metaclass=ABCMeta):
@abstractmethod
def spanning forest(self , G: "undirected IDigraph<T>"\
) -> "iterable<iterable<(T, T)>>": pass
En los grafos no dirigidos y ponderados tiene aplicaci on pr actica el c alculo de un
arbol de recubrimiento mnimo o MST(acr onimo del ingl es MinimumSpanning Tree).
Dado G = (V, E) no dirigido y ponderado por una funci on d : E R
0
, un MST es aquel
arbol de recubrimiento T E cuya suma de pesos
D(T) =

(u,v)T
d(u, v)
es menor o igual que la de cualquier otro arbol de recubrimiento.

N otese, pues, que sera m as apropiado referirnos al arbol de recubrimiento


con coste mnimo, pero en la literatura se le conoce como arbol de recubri-
miento mnimo.
La gura 4.7 (a) muestra un grafo no dirigido y ponderado. La gura 4.7 (b) muestra,
en trazo grueso, un MST cuyo coste es de 45.
Y esta es la interfaz para clases que implementan algoritmos de c alculo del arbol de
recubrimiento mnimo:
188 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 4.7: (a) Grafo no dirigido y ponderado. (b)

Arbol
de recubrimiento mnimo (MST) sobre el grafo (aristas
en trazo continuo).
0
1
2
3
4
5
6
7
8
9
0
15
2
3
13
11
4
5
8
12
9
16
10
17
1
6
14
7
0
1
2
3
4
5
6
7
8
9
0
15
2
3
13
11
4
5
8
12
9
16
10
17
1
6
14
7
0
1
2
3
4
5
6
7
8
9
(a) (b)
algoritmia/graphalgorithms/minimumspanning.py
class IMinimumSpanningTreeFinder(metaclass=ABCMeta):
@abstractmethod
def minimum spanning tree(self , G: "undirected Digraph<T>",
d: "f: T, T -> R", u: "T") -> "iterable<(T, T)>": pass
class IMinimumSpanningForestFinder(metaclass=ABCMeta):
@abstractmethod
def minimum spanning forest(self , G: "undirected Digraph<T>",
d: "f: T, T -> R") -> "iterable<(T, T)>": pass
4.3.1.

Arbol de recubrimiento a partir del recorrido del grafo
Podemos obtener un arbol de recubrimiento mediante un recorrido del grafo. El procedi-
miento de obtenci on de las aristas que forman el arbol es extremadamente sencillo: cada
vez que se decide ir de un v ertice u a otro v ertice no visitado v, se a nade la arista (u, v)
al arbol. Bastar a con utilizar una funci on visitor que devuelva la arista en lugar de su
v ertice destino. (Recordemos que hay una pseudo-arista especial que no debe emitirse:
la que tiene por v ertice inicial y nal el v ertice en el que se inicia la exploraci on.)
algoritmia/graphalgorithms/spanning.py
class GraphTraversalSpanningTreeFinder(ISpanningTreeFinder):
def init (self , **kw):
get factories(self , kw, graphTraverserFactory=lambda G: BreadthFirstTraverser())
def spanning tree(self , G, s):
traverser = self .graphTraverserFactory(G)
for (u, v) in traverser.traverse(G, s, visitor=lambda u, v: (u, v)):
if u != v: yield (u, v)
demos/graphalgorithms/bfsspanning.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.spanning import GraphTraversalSpanningTreeFinder
G = UndirectedGraph(E=[(0,1), (0,3), (1,4), (2,5), (3,1), (4,3)])
print(list(GraphTraversalSpanningTreeFinder().spanning tree(G, 0)))
[(0, 1), (0, 3), (1, 4)]
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 189
El coste temporal de este algoritmo es O(V +E) y el coste espacial es O(V).
La gura 4.8 muestra una representaci on simplicada de un mapa de carreteras de
la pennsula ib erica. La gura 4.9 muestra dos arboles de recubrimiento del mapa de
carreteras iniciando su construcci on desde diferentes ciudades.
Evora
A Coru na
Aara
Abrantes
Adanero
Aguilar de Campoo
Alacant
Albacete
Alboc`asser
Albufeira
Alcala de Henares
Alcance
Alcantarilla
Alcaraz
Alca nices
Alca niz
Alcolea del Pinar
Alcoy
Alemra
Alfaro
Algeciras
Almadrones
Almaden Almansa
Almaz an
Almonte
Almu necar
Alzira
Amposta
And ujar
Ansiao
Antequera
Aranda de Duero
Aranjuez
Arenas de San Pedro Arganda del Rey
Armi non
Arz ua
Arevalo
Astorga
Aveiro
Aviles
Ayamonte
Ayora
Baamonde
Badajoz
Baena
Bailen
Barbastro
Barcelona
Barreiros
Basauri
Baza
Becerrea
Beja
Belmonte
Benabarre
Benavente
Benicarl o
Benic` assim
Benidorm
Betanzos
Bicorp
Bilbao
Bolta na
Borriol
Braga
Braganca
Burgo de Osma
Burgos
Bejar
Calatayud
Caldas da Rainha
Callosa del Segura
Campillos
Campomanes
Cangas de Ons Carballo
Carboneras
Carregado
Cartagena
Casa de Juan Gil
Casas Iba nez
Cascais
Caspe
Castelldefels
Castell o de la Plana
Castelo Branco
Castro Urdiales
Caudete
Ca nete
Cerceda
Cervera
Chantada
Chiclana de la Frontera
Chiva
Cieza
Cinctorres
Cistierna
Ciudad Real
Cocentaina
Coimbra
Collado Villalba
Coruche
Covilha
Cudillero
Cuenca
Cullera
C aceres
C adiz
C ordoba
Daimiel
Daroca
Donostia
Dos Hermanas
Durango
Durcal
El Barco de Avila
El Burgo del Ebro
Elda
Elvas
Elx
Espiel
Espinho
Estepa
Estepona
Estremoz
Faro
Feira
Ferrol
Figueira da Foz
Figueres
Fraga
Fuengirola
Gaia
Gandia
Gibrale on
Gij on
Gimileo
Girona
Grado
Granada
Grao de Sagunt
Graus
Gr andula
Guadalajara
Guadiaro
Guarda
Guardamar del Segura
Guntn
Helln
Herrera del Duque
Honrubia
Huelva
Huesca
Hjar
Ibi
Igualada
Izurzun
Jabugo
Jaca
Jaen
Jerez de la Frontera
LAlcora
LHospitalet de lInfant
La Albuera
La Ba neza
La Espina
La Jonquera
La Lnea de la Concepcion
La Magdalena
La Roda
La Seu dUrgell
La Union
Lagos
Laln
Laredo
Leiria
Lepe
Lerma
Les Borges Blanques
Le on
Linares
Liria
Lisboa
Llanes
Lleida
Llerena
Llivia
Logro no
Loja
Lorca
Los Alcazares
Los Gallardos
Losa del Obispo
Luarca
Lucena
Lugo
Macedo de Cavaleiros
Madrid
Madridejos
Manresa
Mansilla de la Mulas
Manzanares
Maqueda
Marbella
Marn
Matar o
Mazag on
Medina del Campo
Medinaceli
Mieres
Miranda del Ebro
Mogadouro
Molina de Aragon
Mombuey
Monforte de Lemos
Monreal del Campo
Montalban
Montemor-o-Novo
Montijo
Montoro
Monz on
Mon ovar
Morella
Motril
Murcia
Murca
M alaga
Merida
Navalmoral de la Mata
Navia
Novelda
Nueno
Nules
O Barco
Oca na
Odemira
Oitura
Oliva
Olot
Onda
Ontinyent
Osorno
Ourense
Ourique
Oviedo
Padul
Palencia
Pamplona
Peniche
Penyscola
Pe nafiel
Pe naranda de Bracamonte
Piedrabuena
Pinoso
Plasenncia
Pola de Siero
Ponferrada
Pont de Suert
Ponte de Lima
Ponte do Sor
Pontedeume
Pontevedra
Portalegre
Portbou
Portman
Porto
Potes
Puerto Lumbreras
Puerto Real
Puertollano
Puigcerd` a
Quintana del Puente
Quintanilha
Reinosa
Requena
Reus
Riaza
Ribadavia
Ribadeo
Ribadesella
Ribeira de Pena
Rinc on de la Victorio
Ripoll
Ronda
Ruidera
Sabi nanigo
Saced on
Sagunt
Salamanca
San Ciprian
San Esteban de Gormaz
San Fernando
San Rafael
Sant Joan dAlacant
Santa Pola
Santander
Santarem
Santiago de Compostela
Santo Domingo de la Calzada
Segorbe
Segovia
Serpa
Setubal
Sevilla
Silla
Sines
Sintra
Sitges
Solares
Soria
Sueca
Tafalla
Talavera de la Reina
Taranc on
Tarazona
Tarifa
Tarragona
Tavira
Teruel
Toledo
Tordesillas Toro
Torreblanca
Torrelavega
Torremolinos
Torres Novas
Torres Vedras
Torrevieja
Totana
Trujillo
Tuj
Unquera
Utiel
Valdepe nas
Valencia de Alcantara
Valladolid
Valongo
Valverde del Camino
Val`encia
Vegadeo
Venta El Alto
Venturada
Vera
Vern
Viana do Castelo
Vic
Vielha
Vigo
Vila Flor
Vila Nova de Foz Coa
Vila Real
Vila-real
Vilafranca del Cid
Vilafranca del Penedes
Vilagarca de Arousa
Vilareal de Santo Antonio
Villal on de Campos
Villarrobledo
Villena
Vinar os
Viseu
Vitoria
Velez-Rubio
Xert
Xinzo de Limia
X`ativa
Yecla
Zafra
Zamora
Zaragoza
Zarauz
Zuera Agreda
Avila
Ecija
Ubeda
Figura 4.8: Mapa de ca-
rreteras de la pennsula
ib erica (cuyos datos est an
disponibles en el m odulo
algoritmia.data.iberia).
(a) (b)
Figura 4.9: Dos arboles de recubrimiento en el mapa de la pennsula ib erica. (a) Generado tomando como punto de
partida Madrid. (b) Generado tomando como punto de partida Castell o de la Plana.
190 Apuntes de Algoritmia 28 de septiembre de 2009
Bosque de recubrimiento
Si deseamos obtener un bosque de recubrimiento, recurriremos a cualquiera de las clases
que efect uan un recorrido completo del grafo. Devolveremos una secuencia de aristas
por cada arbol del bosque:
algoritmia/graphalgorithms/spanning.py
class GraphTraversalSpanningForestFinder(ISpanningForestFinder):
def init (self , **kw):
get factories(self , kw, graphTraverserFactory=lambda G: BreadthFirstTraverser())
def spanning forest(self , G):
traverser = self .graphTraverserFactory(G)
tree = []
for (u, v) in traverser.full traverse(G, visitor=lambda u, v: (u,v)):
if u == v:
if len(tree) > 0:
yield tree
tree = []
else:
tree.append((u, v))
if len(tree) > 0:
yield tree
demos/graphalgorithms/bfsforest.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.spanning import GraphTraversalSpanningForestFinder
G = UndirectedGraph(E=[(0,1), (0,3), (1,4), (2,5), (3,1), (4,3)])
print([list(comp) for comp in GraphTraversalSpanningForestFinder().spanning forest(G)])
[[(0, 1), (0, 3), (1, 4)], [(2, 5)]]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
58 Qu e coste presenta el algoritmo de c alculo del bosque de recubrimiento?
59 Laberintos como el que se propone generar en el ejercicio 54 son arboles de recubrimiento
para el grafo formado con un v ertice por celda de la matriz y una arista entre cada par de vecinos
conectados. Implementa una generador de laberintos de ese estilo obteniendo un arbol de recu-
brimiento para el grafo que subyace a la matriz. Esta vez debes considerar que hay un arco entre
cada par de casillas vecinas con un muro de separaci on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.3.2.

Arbol de recubrimiento de coste mnimo
El algoritmo de Bar uvka calcula el MST de un grafo no dirigido G = (V, E) y ponde-
rado por d : E R mediante un proceso de fusi on de subconjuntos del MST, que son
subgrafos de G.
Dado un subgrafo, denominemos arista saliente a toda arista del grafo no dirigido
original con un v ertice en el subgrafo y otro en un v ertice de un subgrafo distinto. El
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 191
proceso arranca considerando que cada v ertice, por s solo, es un subgrafo de G. Se inicia
entonces un proceso iterativo que naliza cuando el MST contiene n 1 aristas o cuando
no hay aristas salientes en ninguno de los subgrafos. En cada iteraci on se fusiona cada
subgrafo con aquel al que apunta su arista saliente de menor peso. N otese que en una
misma iteraci on se pueden ejecutar varias fusiones.
algoritmia/graphalgorithms/minimumspanning.py
from .connectedcomponents import GraphTraversalConnectedComponentsFinder
...
class BaruvkasMinimumSpanningForestFinder(IMinimumSpanningForestFinder):
def init (self , **kw):
get factories(self , kw,
connectedComponentsFinderFactory=\
lambda G: GraphTraversalConnectedComponentsFinder(),
graphFactory=lambda V: UndirectedGraph(V, E=[]))
def minimum spanning forest(self , G: "undirected Digraph<T>",
d: "f: T, T -> R") -> "iterable<(T, T)>":
G2 = self .graphFactory(G.V)
ccf = self .connectedComponentsFinderFactory(G)
while True:
C = [list(c) for c in ccf .connected components(G2)]
lbl = dict((v, i) for i in range(len(C)) for v in C[i])
merged = [False]*len(C)
for i in range(len(C)):
if not merged[i]:
e = argmin(((u,v) for u in C[i] for v in G.succs(u) if lbl[u] != lbl[v]),
lambda edge: d(edge))
if e!=None:
G2.E.add unchecked(e)
yield e
merged[lbl[e[1]]] = True
for v in C[lbl[e[1]]]:
lbl[v] = lbl[e[0]]
if not any(merged): break
demos/graphalgorithms/baruvka.py
from algoritmia.datastructures.graphs import UndirectedGraph, WeightingFunction
from algoritmia.graphalgorithms.minimumspanning import *
baruvka = BaruvkasMinimumSpanningForestFinder()
d = WeightingFunction({(0,1): 0, (0,2): 15, (0,3): 2, (1,3): 3, (1,4): 13, (2,3): 11,
(2,5): 4, (3,4): 5, (3,5): 8, (3,6): 12, (4,7): 9, (5,6): 16,
(5,8):10, (6,7): 17, (6,8): 1, (6,9): 6, (7,9): 14, (8,9): 7},
symmetrical=True)
MST = list(baruvka.minimum spanning forest(UndirectedGraph(E=d.keys()), d))
print(MST: {} con peso {}..format(MST, sum(d(u,v) for (u,v) in MST)))
MST: [(0, 1), (2, 5), (3, 0), (4, 3), (6, 8), (7, 4), (9, 6), (3, 5), (8, 5)]
192 Apuntes de Algoritmia 28 de septiembre de 2009
con peso 45.
Lema 4.1 Sea G = (V, E) un grafo no dirigido y ponderado por una funci on d : E R, sea X
un subconjunto de V y sea e = (u, v) la arista de menor peso que conecta X con V X. La arista
e es parte del MST.
Demostraci on. Supongamos que T es un arbol de recubrimiento que no contiene a e. Va-
mos a demostrar que T no es el MST. En consecuencia, e deber a formar parte del MST. La
arista e es un par (u, v) donde u X y v V X. Como T es un arbol de recubrimien-
to, ya conectaba X con V X. Sea e

= (u

, v

) la arista que efectuaba esta conexi on. Si


a nadimos a T la arista e, se producir a un ciclo. El arbol T

= (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).

R.E. Tarjan es el autor de este algoritmo. Lo public o bajo el ttulo de Depth


rst search and linear graph algorihms en el SIAM Journal of Computing, en
1972.
4.5. Ordenaci on topol ogica de un digrafo acclico
Los digrafos acclicos presentan una propiedad interesante: es posible encontrar un or-
den lineal de sus v ertices de modo que, para toda arista (u, v), el v ertice u preceda al
v ertice v. Este orden recibe el nombre de orden topol ogico. Un algoritmo de ordenaci on
topol ogica encuentra un orden como el descrito en un digrafo acclico.
Consideremos el digrafo acclico que mostramos en la gura 4.12 (a). La representa-
ci on de la gura 4.12 (b) dispone los v ertices de izquierda a derecha en un orden que
satisface la propiedad enunciada: las aristas siempre parten de un v ertice m as a la iz-
quierda que el v ertice de llegada. No tiene por qu e haber un unico ordenamiento que
satisfaga esta propiedad. El ordenamiento de la gura 4.12 (c), por ejemplo, tambi en la
observa.
Figura 4.12: Tres representacio-
nes de un digrafo que muestra
la relaci on su dise no se ins-
pir o en para algunos lenguajes
de programaci on. (a) Represen-
taci on arbitraria. (b) Representa-
ci on que muestra los v ertices or-
denados topol ogicamente de iz-
quierda a derecha. (c) Represen-
taci on similar a la anterior, pero
con un orden topol ogico distinto.
C
C++
Java
ObjC
C#
(a)
C C++ Java ObjC C#
(b)
C C++ Java ObjC C#
(c)
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 197
El ordenamiento topol ogico encuentra aplicaci on, por ejemplo, en la planicaci on de
actividades. Si los v ertices son actividades que se van a realizar y las aristas determinan
relaciones de precedencia entre ellas, el ordenamiento topol ogico describe un orden de
ejecuci on de tareas que no viola ninguna de las relaciones de precedencia. De hecho,
resulta de ayuda pensar en t erminos de actividades y relaciones de precedencia a la hora
de dise nar una estrategia de ordenaci on topol ogica. La idea consiste en asignar a cada
tarea v un instante de nalizaci on que indica el instante de tiempo en que podemos
ejecutarla porque ya han sido ejecutadas las que deben precederla. Dicho instante es el
ndice que ocupara la tarea en un vector ordenado topol ogicamente, es decir, un valor
num erico entre 0 y V 1 . Si hay una relaci on (u, v), la tarea v debe ejecutarse despu es
de completar la tarea u, as que le corresponde un instante de nalizaci on superior.
Si efectuamos un recorrido por primero en profundidad en postorden, el v ertice u se
visitar a despu es que el v ertice v. As pues, el orden topol ogico es un postorden invertido:
podemos asignar a cada v ertice su instante de nalizaci on cuando es visitado.
algoritmia/graphalgorithms/topsort.py
from algoritmia.graphalgorithms.traversals import PostorderTraverser
from algoritmia.datastructures.queues import LIFO
from algoritmia.utils import get factories
from abc import ABCMeta, abstractmethod
class ITopsorter(metaclass=ABCMeta):
@abstractmethod
def topsorted(self , G: "Digraph<T>") -> "iterable<T>": pass
class Topsorter(ITopsorter):
def init (self , **kw):
get factories(self , kw, postorderTraverserFactory=lambda G: PostorderTraverser())
def topsorted(self , G):
traverser = self .postorderTraverserFactory(G)
return reversed(list(traverser.full traverse(G)))
demos/graphalgorithms/topsort.py
from algoritmia.datastructures.graphs import Digraph
from algoritmia.graphalgorithms.topsort import Topsorter
G = Digraph(E=[(C, C++), (C, Java), (C, C#), (C, ObjC),
(C++, Java), (C++, C#), (Java, C#)])
print(list(Topsorter().topsorted(G)))
[C, C++, Java, ObjC, C#]
Correcci on
Teorema 4.1 La funci on topsort devuelve el conjunto de v ertices de un grafo acclico ordenado
topol ogicamente.
198 Apuntes de Algoritmia 28 de septiembre de 2009
Demostraci on. Asignemos a cada v ertice sundice en la enumeraci on topol ogica. Basta con
demostrar que si (u, v) E, la posici on de u es anterior a la posici on de v. El recorrido
completo en postorden puede iniciar una exploraci on en el v ertice v sin haber visitado u.
En tal caso, todo v ertice alcanzable desde v ocupar a una posici on de ndice mayor que
el de u. Si, por contra, la exploraci on ya ha pasado por u, visitar a v antes que u y, en
consecuencia, le asignar a un ndice mayor que el de u.
N otese que topsort s olo funciona correctamente si el grafo es acclico. Puede resultar
necesario, pues, efectuar un preproceso que nos indique si el grafo presenta ciclos. Este
preproceso puede basarse en un recorrido en profundidad de los v ertices, tal como se
indica en el ejercicio 50.
An alisis de complejidad
La complejidad temporal de la funci on topsort es O(V + E), pues no es m as que la
inversi on del resultado de una exploraci on por primero en profundidad del grafo com-
pleto.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
60 Haz una traza del algoritmo de ordenaci on topol ogica sobre este grafo:
1
2
7
6
3
8
10
4
9
11 5
A continuaci on, dibuja el grafo con los v ertices dispuestos a lo largo de un eje horizontal respe-
tando el orden topol ogico.
61 Cu antas ordenaciones topol ogicas diferentes admite un grafo con n v ertices y ninguna
arista?
62 Es posible calcular la clausura transitiva de un grafo dirigido acclico m as ecientemente a
partir de su ordenaci on topol ogica. C omo? Con qu e coste?
63 Qu e ocurre si suministramos un grafo no conexo al algoritmo de ordenaci on topol ogica?
Si el algoritmo desarrollado no visita todos los v ertices, modifcalo para que lo haga.
64 Al planicar un proyecto hay tareas que dependen de la terminaci on de otras. Nos suminis-
tran un chero en el que cada lnea contiene una tarea (descrita por caracteres y guiones) y, tras
dos puntos, una secuencia de tareas que deben ejecutarse antes. Por ejemplo:
alquilar-perforadora: estudio-mercado-perforadoras
obtener-licencia-empresa: constituir-sociedad aportar-capital
permiso-de-obras: obtener-licencia-empresa aprobar-proyecto
aportar-capital: ahorrar
perforar: permiso-de-obras alquilar-perforadora contratar
explotar-pozo: contratar-plantilla-explotacion perforar
La tarea permiso-de-obras requiere nalizar antes las tareas obtener-licencia-empresa y
aprobar-proyecto.
Dise na un programa que lea el chero de dependencias y muestre por pantalla un orden de
ejecuci on de tareas que permita que toda tarea se ejecute s olo cuando se han ejecutado aquellas
de las que depende.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 199
65 Demuestra que todo grafo acclico tiene al menos un sumidero (v ertice con grado de salida
nulo) y al menos una fuente (v ertice con grado de entrada nulo).
66 Un algoritmo alternativo de ordenaci on topol ogica consiste en lo siguiente: en un bucle,
se busca un v ertice con grado de entrada nulo, se extrae y se ubica en la siguiente posici on del
vector de v ertices ordenados; el v ertice y todas las aristas que parten de el se eliminan de G antes
de efectuar una nueva iteraci on del bucle. El bucle naliza cuando el grafo queda sin v ertice
alguno.
Implementa el algoritmo y analiza su complejidad computacional.
67 Deseamos implementar una versi on simplicada del programa make a la que llamaremos
minimake. El programa minimake lee un chero en el que cada lnea tiene varios campos separa-
dos por el caracter :: el nombre de un chero f
0
, el nombre de uno o m as cheros f
1
, f
2
, . . . y un
comando que permite generar el chero f
0
a partir de f
1
, f
2
, . . .
Por ejemplo, una lnea como esta
prog.exe:prog.c:prog.h:libreria.o:gcc prog.c libreria.o -o prog.exe
da una receta para generar un chero prog.exe que depende de otros tres: prog.c, prog.h ( este
se incluye en programa.c a trav es de una directiva #include) y libreria.o: compilar el che-
ro prog.c con el compilador gcc y la opci on -o prog.exe y enlazar el chero resultante con
libreria.o. N otese que libreria.o puede tener sus propias dependencias y acci on que permite
generarlo.
Las reglas no se aplican siempre: s olo se debe ejecutar la regla asociada a un chero f
0
si no
existe o si la fecha de modicaci on de cualquiera de los cheros de los que depende es posterior
a la de f
0
.
Se pide una implementaci on de minimake. El programa leer a una especicaci on que sigue
el formato descrito y construir a un grafo que represente todas la dependencias. A continuaci on,
comprobar a si se trata de un grafo acclico. En caso de que no lo sea, avisar a al usuario del proble-
ma y se detendr a. Si se super o la comprobaci on, el programa obtendr a la fecha de modicaci on
de todos los cheros y decidir a cu ales debe generar. Finalmente, ejecutar a una sola vez cada una
de las ordenes que resulta estrictamente necesario ejecutar.
68 Un arbol dirigido es un grafo acclico. Si numeramos sus v ertices con el orden con el que son
procesados siguiendo estrategias de exploraci on 1) en preorden, 2) en postorden, 3) por primero
en anchura, qu e obtenemos?, a) un orden topol ogico, b) un orden topol ogico inverso, c) ninguno
de los anteriores.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.6. Clausura transitiva de un digrafo
La relaci on de alcanzabilidad en un grafo no dirigido es una relaci on de equivalencia
que particiona el conjunto de v ertices en componentes conexos. No ocurre lo mismo en
un digrafo, pues un v ertice v puede ser alcanzable desde otro u y, sin embargo, u puede
no ser alcanzable desde v.
Recordemos que un digrafo G = (V, E) describe una relaci on binaria sobre su con-
junto de v ertices: dos v ertices est an relacionados si existe un arco que los une. Tomando
como base esta relaci on podemos denir otra, , as:
si (u, v) E, entonces u v;
200 Apuntes de Algoritmia 28 de septiembre de 2009
si u v y v w, entonces u w.
Esta relaci on recibe el nombre de cierre transitivo o clausura transitiva de la relaci on
original. Si dos v ertices satisfacen u v, entonces hay un camino de u a v y decimos que
v es alcanzable desde u.
N otese que esta relaci on binaria describe un nuevo grafo dirigido en el que dos v erti-
ces est an conectados si hay camino del de partida al de llegada en el grafo original. Su
representaci on mediante una matriz de adyacencia permite responder a la pregunta es
v alcanzable desde u?, para cualesquiera u y v, en tiempo O(1). La gura 4.13 muestra
un digrafo y su cierre transitivo.
Figura 4.13: (a) Un digrafo y (b) su clausura o cierre
transitivo.
0 1 2
3 4 5
0 1 2
3 4 5
(a) (b)
Dado que el cierre transitivo de un grafo es an alogo al cierre transitivo de una matriz
booleana, empezaremos por presentar esta ultima operaci on. Cuando hayamos presen-
tando el algoritmo de Warshall sobre matrices booleanas, lo extenderemos a los digrafos.
4.6.1. Cierre transitivo de una matriz booleana
Implementaremos clases que se ci nen a esta interfaz:
algoritmia/graphalgorithms/closure.py
class IMatrixTransitiveClosureFinder(metaclass=ABCMeta):
@abstractmethod
def transitive closure(self , M: "square matrix<booL>") -> "square matrix<bool>":
raise NotImplementedError
C omo calcular ecientemente el cierre transitivo? Conviene pensar en t erminos de
la representaci on de los grafos dirigidos mediante matrices de adyacencia y presentar
antes algunas operaciones matriciales. Para simplicar la exposici on, asumiremos que el
conjunto de v ertices es [0..V 1]. Podemos denir operaciones entre matrices boolea-
nas cuadradas equiparables a la suma y al producto de matrices de reales sustituyendo
sumas y productos de reales por o-l ogicas e y-l ogicas de valores l ogicos, respectiva-
mente.
La suma de dos matrices booleanas de n n elementos requiere efectuar O(n
2
) ope-
raciones y el producto, O(n
3
). Pero es posible calcular el producto m as ecientemente.
Por ejemplo, el m etodo de Strassen, que estudiaremos m as adelante (al estudiar divide
y vencer as), reduce el coste temporal del producto matricial a O(n
2.81
). Y el m etodo de
Winograd lo reduce a O(n
2.38
).
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 201
Sea M una matriz booleana indexada por los v ertices del grafo y tal que M
ij
es cierto
si y s olo si (i, j) E. Nos interesa considerar qu e signica el producto de M por s misma,
es decir, qu e signica M
2
. Un ejemplo nos ayudar a. Sea G el digrafo de la gura 4.13 (a)
y M la matriz booleana que lo representa. M
2
= M M describe lo que denominamos el
cuadrado del digrafo G. El valor de M
2
es
M
2
i,j
=

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.

El trabajo de Stephen Warshall se titula A Theorem on Boolean Matrices y


fue publicado en el Journal of the ACM, volumen 9 n umero 1, p aginas 11-12
en enero de 1962.
Teorema 4.2 El m etodo WarshallMatrixTransitiveClosureFinder.transitive closure obtiene el re-
sultado de evaluar la expresi on

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

V, cabe esperar una notable aceleraci on del c alculo. Pero


hay una idea a un mejor que explota la aciclicidad del grafo de condensaci on: podemos
recorrer los v ertices en orden topol ogico inverso y calcular el conjunto de nodos alcanza-
bles desde uno como uni on de los alcanzables desde sus sucesores.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
72 Implementa un algoritmo para el c alculo de la clausura transitiva basado en la detecci on de
componentes fuertemente conexos.
73 La operaci on inversa de la clausura transitiva recibe el nombre de reducci on transitiva y es
un grafo tal que u y v est an unidos por un camino si y s olo si (u, v) es una arista del grafo original.
Tiene inter es calcular la reducci on transitiva de menor n umero de aristas (tambi en conocida por
digrafo equivalente mnimo). C omo puede calcularse?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.7. Camino con menor n umero de aristas
La gura 4.17 (a) muestra un grafo no dirigido sobre el que presentamos una traza del
algoritmo de exploraci on en anchura. Hemos dispuesto los v ertices en circunferencias
conc entricas alrededor del v ertice en el que vamos a iniciar una exploraci on. Analice-
mos los momentos claves de la exploraci on por primero en anchura que se muestran
en la gura 4.17. Inicialmente se visita el v ertice central (gura 4.17 (a)). Como todos
sus v ertices adyacentes se consideran a continuaci on, ser an los siguientes v ertices en ser
visitados. Todos ellos est an separados del v ertice inicial por una sola arista (v ease la -
gura 4.17 (b)). A continuaci on se ir an explorando todos los v ertices adyacentes a estos,
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 207
siempre que no hayan sido visitados ya, es decir, acabaremos visitando en este momento
todos los v ertices separados del inicial por dos aristas (v ease la gura 4.17 (c)). A conti-
nuaci on, todos los v ertices adyacentes a los que est an a dos aristas se visitar an si no se
han visitado ya, con lo que habremos considerado todos los v ertices que se encuentran a
tres aristas del v ertice inicial (v ease la gura 4.17 (d)). As pues, los v ertices se visitan en
un orden creciente de distancia (en n umero de aristas) al inicial.
(a) (b) (c) (d)
Figura 4.17: Traza de la exploraci on por primero en anchura sobre el grafo de la izquierda a partir del v ertice central. (a)
Iniciamos un recorrido en anchura en el v ertice central, (b) se visitan primero los v ertices alcanzables desde el (primera
circunferencia); a continuaci on, (c) los alcanzables desde los v ertices marcados en la anterior gura; y, nalmente, (d)
los v ertices exteriores.
El hecho de que el recorrido en anchura visite los v ertices por n umero creciente de
aristas del camino de menor longitud que lo conecta al v ertice de partida no es una pe-
culiaridad de este grafo en particular: se observa en cualquier grafo. Recordemos que el
recorrido en anchura, tal cual lo hemos denido, puede emitir pares de v ertices en lugar
de v ertices. Cada par de la forma (v, u) indica que la visita al v ertice v se realiz o desde el
v ertice u. En la gura 4.18 se muestran, como aristas dirigidas, los pares de v ertices que
se emiten al realizar la exploraci on por primero en anchura del grafo de la gura 4.17 (a)
partiendo del v ertice central.
Figura 4.18:

Arbol de punteros hacia atr as con el mejor camino de cualquier v ertice al
v ertice central.
As pues, un solo recorrido por primero en anchura permite calcular los caminos con
menor n umero de aristas que van de un v ertice a cualquier otro en un digrafo.
Empezaremos por resolver estos dos problemas y acabaremos este apartado estudian-
do otros dos:
Los caminos de menor longitud que parten de cualquier v ertice de un conjunto de
v ertices y llegan a cada uno de los dem as.
208 Apuntes de Algoritmia 28 de septiembre de 2009
Y el camino de menor longitud entre un v ertice de un conjunto y un v ertice de otro
conjunto.
4.7.1. El camino de menor longitud entre dos v ertices
Podemos dise nar f acilmente un algoritmo que, dado un grafo G = (V, E), dirigido o no,
calcule el menor n umero de aristas que se deben recorrer para ir de un v ertice s a otro t.
La longitud del camino con menor n umero de aristas entre dos v ertices
Podemos calcular el menor n umero de aristas con el que podemos ir de un v ertice s a
otro t con un simple recorrido por primero en anchura:
algoritmia/graphalgorithms/shortestlengthpaths.py
from .traversals import BreadthFirstTraverser
...
class BreadthFirstShortestPaths:
def init (self , **kw):
get factories(self , kw,
mappingFactory=lambda V: dict(),
breadthFirstTraverserFactory=lambda G: BreadthFirstTraverser())
def distance(self , G: "Digraph<T>", s: "T", t: "T"):
length = self .mappingFactory(G.V)
length[s] = 0
bft = self .breadthFirstTraverserFactory(G)
for (u, v) in bft.traverse(G, s, lambda u, v: (u, v)):
if u != v: length[v] = length[u] + 1
if v == t: return length[v]
return None
Hemos inicializado el diccionario length con el n umero de arcos necesarios para ir de
s a s, que es cero. El primer v ertice devuelto por la b usqueda por primero en anchura
es el inicial, cuyo valor inicial en length es nulo. Todos los v ertices sucesores del inicial
pasar an a tener valor 1 en length, pues todos ellos toman el valor length[s], que es 0, y le
suman 1. A continuaci on, los sucesores no visitados de los v ertices v con length[v] a 1 se
visitan. Como sabemos que son visitados desde cada v, su valor es length es length[v]+1.
Y as sucesivamente.
Probemos el programa con el grafo de la gura 4.19 (a):
demos/graphalgorithms/bfslength.py
from algoritmia.datastructures.graphs import Digraph
from algoritmia.graphalgorithms.shortestlengthpaths import BreadthFirstShortestPaths
G = Digraph(E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
bfsp = BreadthFirstShortestPaths()
for (s,t) in (0,3), (0,4), (2,0):
print(Aristas entre {} y {}: {}.format(s, t, bfsp.distance(G, s, t)))
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 209
Aristas entre 0 y 3: 1
Aristas entre 0 y 4: 2
Aristas entre 2 y 0: 3
La gura 4.19 (b) muestra el valor length para cada v ertice al calcular el camino de
menor longitud que parte del v ertice 2 y llega al v ertice 0.
0 1 2
3 4 5
(a)
0 1 2
3 4 5
3 3 0
2 1 1
0 1 2
3 4 5
0 1 2
3 4 5
(b) (c)
Figura 4.19: (a) Un digrafo para el que calculamos el camino
de menor longitud. Hemos se nalado el v ertice inicial con
fondo gris y el v ertice nal con un doble crculo. (b) Junto
a cada v ertice se muestra el valor de length al calcular el
camino de menor longitud entre el v ertice 2 y el v ertice 0. (c)
Con trazo grueso, punteros hacia atr as en el mismo c alculo.
El coste temporal del algoritmo es O(V +E), pues se ha dise nado sobre el recorrido
por primero en anchura con un tiempo de procesamiento constante por v ertice visitado.
El camino de menor longitud entre dos v ertices
Supongamos ahora que deseamos conocer el camino de mnima longitud, y no su lon-
gitud. Podemos realizar un recorrido por primero en anchura y utilizar una t ecnica in-
teresante: el recorrido de punteros hacia atr as. Si un v ertice v es visitado desde otro u,
usaremos un puntero de v a u que nos permita rastrear la serie de v ertices que, desde
el inicial, han generado la visita a v. La gura 4.19 (c) muestra los punteros hacia atr as
para cada v ertice al calcular el camino de menor longitud del v ertice 2 al v ertice 0. El ca-
mino de menor longitud de s a t se puede recuperar siguiendo la ruta de punteros hacia
atr as desde t a s e invirtiendo la secuencia de v ertices encontrados:
algoritmia/graphalgorithms/shortestlengthpaths.py
class BreadthFirstShortestPaths:
...
def shortest path(self , G: "Digraph<T>", s: "T", t: "T"):
backpointer = self .mappingFactory(G.V)
bft = self .breadthFirstTraverserFactory(G)
for (u, v) in bft.traverse(G, s, lambda u, v: (u, v)):
backpointer[v] = u
if v == t: break
return backtrace(backpointer, t)
N otese que hemos hecho uso de una funci on backtrace para efectuar el recorrido del
nodo a la raz y, una vez invertida la secuencia de v ertices, proporcionar el camino. Como
esta funci on se utilizar a desde diferentes m odulos, se ha implementado en un m odulo
de utilidades:
210 Apuntes de Algoritmia 28 de septiembre de 2009
algoritmia/graphalgorithms/pathutils.py
def backtrace(backpointer: "mapping<T, (T or None)>", t: "T"):
if t not in backpointer: return None
path = [t]
v = t
while v in backpointer and v != backpointer[v]:
v = backpointer[v]
path.append(v)
if v not in backpointer: return None
path.reverse()
return path
Podemos probar el nuevo m etodo:
demos/graphalgorithms/bfsshortestpath.py
from algoritmia.datastructures.graphs import Digraph
from algoritmia.graphalgorithms.shortestlengthpaths import BreadthFirstShortestPaths
G = Digraph(E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
bfsp = BreadthFirstShortestPaths()
for (s,t) in (0,3), (0,4), (2,0):
print(Camino mnimo entre {} y {}: {}.format(s, t, bfsp.shortest path(G, s, t)))
Camino mnimo entre 0 y 3: [0, 3]
Camino mnimo entre 0 y 4: [0, 1, 4]
Camino mnimo entre 2 y 0: [2, 4, 3, 0]
El coste temporal del algoritmo, O(V +E), no se ve afectado por el hecho de recu-
perar el camino, ya que el bucle de recuperaci on del camino se ejecuta en tiempo O(V).
M as adelante estudiaremos algoritmos para el problema del camino m as corto en un
digrafo ponderado (no el camino de menor longitud, sino el de menor suma de pesos o
distancias asociadas a las aristas). Uno de ellos, que exige que la funci on de ponderaci on
sea positiva, se inspira en la exploraci on por primero en anchura, pero usa una cola de
prioridad en lugar de una cola FIFO: es el denominado algoritmo de Dijkstra.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
74 Haz sendas trazas de c alculo del camino de menor longitud entre dos v ertices sobre el grafo
que mostramos a continuaci on, a) una que calcule el camino de menor longitud entre el v ertice
central y el de la esquina superior derecha y b) otra que calcule el camino de menor longitud entre
el v ertice inferior izquierdo y el de la esquina superior derecha.
(a) (b)
75 Dise na un m etodo que calcule el n umero de aristas de los caminos de menor longitud de un
v ertice s a cualquier otro v ertice del grafo.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 211
76 Dado un laberinto descrito en Python como se indica en el ejercicio 54 (aunque no necesa-
riamente bien formado) queremos dise nar:
a) Una funci on que nos devuelva la longitud del camino de menor longitud entre las casillas de
entrada y salida (si lo hay).
b) Una funci on que nos devuelva la secuencia de v ertices que corresponde al camino de menor
longitud entre las casillas de entrada y salida.
Debes analizar la complejidad computacional de cada uno de los algoritmos dise nados.

El problema del camino m as corto en un laberinto se solucion o por vez pri-


mera en el artculo The shortest path through a maze, International Sym-
posium on the Theory of Switching (1959), Harvard University Press.
77 Disponemos de un vector con los m lexemas del castellano (voces del diccionario, formas fe-
meninas y plurales, verbos conjugados, etc.). La m axima longitud de un lexema es de n caracteres.
Dos lexemas est an conectados si es posible ir de uno a otro a trav es de una secuencia de palabras
que se diferencian, una de la siguiente, en un solo car acter. Por ejemplo, Roma y reto est an
conectados: roma romo remo reto. (N otese que todos los lexemas que aparecen
en un camino de conexi on entre dos lexemas est an conectados dos a dos). Disponemos de una
rutina que, en tiempo O(n), compara dos lexemas para saber si se diferencian en un solo car acter.
Construye una estructura de datos que permita dar respuesta eciente a estas cuestiones:
a) Calcular la conexi on m as corta entre dos lexemas dados, si la hay.
b) Proporcionar todos los lexemas conectados con uno dado.
c) Conocer el n umero de grupos de lexemas conectados dos a dos.
Cu anto espacio y tiempo se requiere para construir la estructura de datos? C omo efectuaras
cada uno de los c alculos? Con que costes temporal y espacial?
78 Sea G un grafo no dirigido. La excentricidad de un v ertice de G es la mayor longitud (n umero
de aristas) del camino de menor longitud entre el y cualquier otro v ertice (tambi en en n umero de
aristas). El radio de G es la excentricidad m as peque na de cualquiera de sus v ertices y el di ametro
de un grafo G es la excentricidad m as grande de cualquiera de sus v ertices.
El permetro de un digrafo G es la longitud (n umero de aristas) de su ciclo m as corto.
Dise na y describe algoritmos que calculen el radio y el di ametro de un grafo no dirigido y el
permetro de un digrafo. El coste temporal debe ser inferior o igual a O(V
3
).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
212 Apuntes de Algoritmia 28 de septiembre de 2009

Paul Erd os (19131996) es uno de los matem aticos m as importantes del


siglo XX. Entre los matem aticos hay un juego: la adjudicaci on de n umeros
de Erd os. Paul Erd os tiene n umero de Erd os 0; cualquier co-autor de un trabajo
matem atico que incluya la rma de Erd os tiene n umero de Erd os 1; cualquier de
los rmantes de un trabajo en el que particip o un autor con n umero de Erd os 2 (y
no tiene n umero de Erd os 0 o 1), tiene n umero de Erd os 3; y as sucesivamente. Si
creamos un grafo cuyos v ertices son autores y cuyas aristas representan la relaci on
ser co-autor de una publicaci on cientca, el n umero de Erd os de un autor no es
m as que la longitud del camino m as corto entre Erd os y dicho autor. (Adem as de
por sus logros, Erd os es famoso por su exc entrico car acter. El lector disfrutar a las
muchsimas an ecdotas que adornan la biografa de este curioso personaje.)
Hay un juego similar en el ambito de la cinematografa: los grados de separa-
ci on de Kevin Bacon. Los actores que han participado en una pelcula en la que
intervino Kevin Bacon tienen 1 grado de separaci on de Kevin Bacon; los que no lo
han hecho, pero han tenido un papel en una pelcula en la que haba un actor con
grado de separaci on 1, tienen grado de separaci on 2; y as sucesivamente. Un juego
derivado de este propone encontrar el camino m as corto (serie de pelculas) entre
un actor cualquiera y Kevin Bacon.
4.7.2. Los caminos de menor longitud de un v ertice a cualquier
otro
Los algoritmos que obtienen el camino de menor longitud entre dos v ertices s y t detienen
su ejecuci on cuando alcanzamos t. Qu e pasara si no se detuvieran hasta haber visitado
todos los v ertices? El diccionario length (o el diccionario backpointer) contendra, para
cada v ertice v, el menor n umero de aristas (o una codicaci on con punteros hacia atr as
de un camino con el menor n umero de aristas) con el que es posible ir de s a v. Es decir,
tendremos el camino de menor longitud de un v ertice a todos los dem as por el coste (en
el peor caso) del c alculo de uno s olo.
Es interesante que observemos la estructura del diccionario backpointer. Cada v ertice
apunta a un v ertice predecesor, as que podemos entender backpointer como un conjunto
de aristas o como un arbol de recubrimiento codicado con punteros al padre. En la
gura 4.20 se muestra un grafo y el conjunto de aristas seleccionadas (con orientaci on de
clave a valor en el diccionario backpointer) al calcular el camino de menor longitud de 0 a
todos los dem as v ertices.
Figura 4.20: (a) Un grafo no dirigido sobre el que se propone encontrar
los caminos de menor longitud de 0 a cualquier v ertice. (b) En trazo grue-
so, (posible) conjunto de aristas representadas por backpointer cuando
calculamos el camino de menor longitud desde el v ertice 0 a todos los
dem as.
0 1 2
3 4 5
6 7 8
0 1 2
3 4 5
6 7 8
0 1 2
3 4 5
6 7 8
(a) (b)
En esta funci on no almacenamos los punteros hacia atr as en un diccionario, sino que
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 213
los enumeramos conforme se calculan:
algoritmia/graphalgorithms/shortestlengthpaths.py
def shortest path backpointers from one to all(self , G: "Digraph<T>", s: "T") \
-> "iterable<(T, T)>":
bft = self .breadthFirstTraverserFactory(G)
return ((v, u) for (u, v) in bft.traverse(G, s, lambda u, v: (u, v)))
Podemos usar la salida del algoritmo para construir un mapeo y suministrar este a
backtrace:
demos/graphalgorithms/bfsonetoall.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.shortestlengthpaths import BreadthFirstShortestPaths
from algoritmia.graphalgorithms.pathutils import backtrace
G = UndirectedGraph(E=[(0,1), (0,3), (1,2), (1,3), (1,4), (1,5), (2,5), (3,6), (4,6),
(4,7), (4,8), (5,8)])
tree = dict(BreadthFirstShortestPaths().shortest path backpointers from one to all(G, 0))
print(Camino mas corto de 0)
for v in range(9): print( a {}: {}.format(v, backtrace(tree, v)))
Camino mas corto de 0
a 0: [0]
a 1: [0, 1]
a 2: [0, 1, 2]
a 3: [0, 3]
a 4: [0, 1, 4]
a 5: [0, 1, 5]
a 6: [0, 3, 6]
a 7: [0, 1, 4, 7]
a 8: [0, 1, 4, 8]
4.7.3. Los caminos de menor longitud desde cualquier v ertice
de un conjunto a todos los dem as
Hay una generalizaci on interesante del problema que acabamos de resolver, es decir,
un problema del que este es un caso particular: dado un grafo G = (V, E), encontrar
los caminos con menor n umero de aristas que parten de un v ertice cualquiera de un
conjunto I V y nalizan en cualquier v ertice de V. No es difcil resolverlo a partir de
lo que sabemos, aunque tendremos que presentar una nueva versi on del algoritmo de
exploraci on por primero en anchura para que este considere varios puntos de partida:
algoritmia/graphalgorithms/traversals.py
class BreadthFirstTraverser(IDigraphTraverser):
...
def traverse from some(self , G: "Digraph<T>", I: "iterable<T>",
visitor: "f: T, T -> S"=None) -> "iterable<S>":
visitor = visitor or (lambda u, v: v)
214 Apuntes de Algoritmia 28 de septiembre de 2009
Q = self .foFactory(G.V)
for v in I: Q.push(v)
visited = self .setFactory(G.V)
for v in I:
visited.add(v)
yield visitor(v, v)
while len(Q) > 0:
u = Q.pop()
for v in G.succs(u):
if v not in visited:
Q.push(v)
visited.add(v)
yield visitor(u, v)
Hemos inicializado la cola FIFO Q con todos los v ertices iniciales. Nos aseguramos
as de que salgan de Q todos ellos antes que ning un otro. En las primeras iteraciones se
visitar a, pues, a todos los v ertices que est en a distancia 1 de cualquiera de ellos. Estos
preparar an la visita de los que se encuentran a distancia 2, y as sucesivamente. Ya po-
demos presentar el algoritmo de c alculo de camino de menor longitud entre cualquier
v ertice de I y cualquier v ertice de F:
algoritmia/graphalgorithms/shortestlengthpaths.py
class BreadthFirstShortestPaths:
...
def shortest path backpointers from some to all(self , G: "Digraph<T>",
I: "iterable<T>") -> "iterable<(T, T)>":
bft = self .breadthFirstTraverserFactory(G)
return ((v, u) for (u, v) in bft.traverse from some(G, I, lambda u, v: (u, v)))
demos/graphalgorithms/bfsfromsome.py
from algoritmia.datastructures.graphs import UndirectedGraph
from algoritmia.graphalgorithms.shortestlengthpaths import BreadthFirstShortestPaths, backtrace
G = UndirectedGraph(E={0: [1,3], 1:[2,3,4,5], 2:[5], 3:[6], 4:[6,7,8], 5:[8]})
tree = dict(BreadthFirstShortestPaths().shortest path backpointers from some to all(G, [0, 2]))
print(Camino mas corto de 0 o 2)
for v in G.V: print( a {}: {}.format(v, backtrace(tree, v)))
Camino mas corto de 0 o 2
a 0: [0]
a 1: [0, 1]
a 2: [2]
a 3: [0, 3]
a 4: [0, 1, 4]
a 5: [2, 5]
a 6: [0, 3, 6]
a 7: [0, 1, 4, 7]
a 8: [2, 5, 8]
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 215
0 1 2
3 4 5
6 7 8
0 1 2
3 4 5
6 7 8
Figura 4.21: Punteros hacia atr as al calcular los caminos de menor longitud de los v ertices 0 o 2
a cualquier v ertice. Los punteros hacia atr as describen un bosque en el que la raz de cada arbol
es un v ertice del que parte al menos un camino optimo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
79 Calcula el coste temporal y espacial del algoritmo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.7.4. El camino de menor longitud entre dos conjuntos de
v ertices
Y hay un ultimo problema que merece consideraci on: el c alculo del camino de menor
longitud que parte de un v ertice cualquiera de un conjunto y llega a un v ertice cualquiera
de otro conjunto. Lo formulamos as: dado un grafo G = (V, E), encontrar el camino con
menor n umero de aristas que parte de un v ertice cualquiera de un conjunto I V y
termina en un v ertice cualquiera de un conjunto F V. Basta con dise nar un algoritmo
basado en la exploraci on por primero en anchura y ver cual de los caminos que naliza
en un v ertice de F presenta menor longitud:
algoritmia/graphalgorithms/shortestlengthpaths.py
from algoritmia.utils import argmin
...
class BreadthFirstShortestPaths:
...
def shortest path from some to some(self , G: "Digraph<T>", I: "iterable<T>",
F: "iterable<T>") -> "iterable<(T, T)>":
length, backpointer = self .mappingFactory(G.V), self .mappingFactory(G.V)
for v in I:
length[v] = 0
backpointer[v] = v
bft = self .breadthFirstTraverserFactory(G)
bft.visitor = lambda u, v: (v, u)
for (u, v) in bft.traverse from some(G, I, lambda u, v: (u, v)):
if v != u:
length[v] = length[u] + 1
backpointer[v] = u
return backtrace(backpointer, argmin(F, lambda v: length[v]))
demos/graphalgorithms/bfssometosome.py
from algoritmia.datastructures.graphs import Digraph
from algoritmia.graphalgorithms.shortestlengthpaths import BreadthFirstShortestPaths, backtrace
G = Digraph(E=[(0,1), (0,3), (1,2), (1,3), (1,4), (1,5), (2,5), (3,6), (4,6),
216 Apuntes de Algoritmia 28 de septiembre de 2009
(4,7), (4,8), (5,8)])
print(Camino mas corto de 0 o 2 a 7 u 8:, end="")
print(BreadthFirstShortestPaths().shortest path from some to some(G, [0, 2], [7, 8]))
Camino mas corto de 0 o 2 a 7 u 8:[2, 5, 8]
Figura 4.22: Punteros hacia atr as al calcular el camino de menor longitud de {0, 2} a {7, 8}.
El camino con menor n umero de aristas que llega a7 y parte de cualquier v ertice inicial es
[0, 1, 4, 7], y el de menor n umero de aristas que llega a 8 desde cualquier v ertice inicial
es [2, 5, 8]. Entre los dos, el m as corto es el segundo.
0 1 2
3 4 5
6 7 8
0 1 2
3 4 5
6 7 8
0 1 2
3 4 5
6 7 8
El coste temporal sigue siendo O(V +E).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
80 Se proporciona un laberinto especicado mediante una matriz L de n las y m columnas. En
la celda L[i][j] (para 0 i < n y 0 j < m) se almacena una lista con las coordenadas de todas
las casillas con las que est a conectada la celda de coordenadas (i, j). Si, por ejemplo, la casilla
(5, 3) s olo tiene conexi on con el norte y el sur, la celda L[5][3] almacena la lista [(4, 3), (6, 3)].
La entrada del laberinto, que no necesariamente est a bien formado, se indica con el par de
enteros (e
0
, e
1
) y la salida con el par (s
0
, s
1
). El laberinto contiene un botn en la casilla (b
0
, b
1
).
(Naturalmente, las coordenadas de la entrada, la salida y el botn son distintas entre s y corres-
ponden a celdas v alidas de la matriz.)
a) Te gustara conocer el camino de menor longitud de la entrada a la salida que permite recoger
el botn. Qu e algoritmo te permite hacerlo tan ecientemente como sea posible? Los datos de
entrada para tu algoritmo son: los valores n y m, la matriz L y las coordenadas (e
0
, e
1
), (s
0
, s
1
)
y (b
0
, b
1
). Explica primero c omo calcular la longitud del camino de menor longitud entre la
entrada y la salida y tal que pasa por el botn. A continuaci on, indica c omo obtendras las
casillas que forman el camino en cuesti on (y no s olo su longitud).
b) El amo del laberinto va a poner un botn, pero no dir a d onde hasta m as adelante. Cuando lo
haga, dar a muy poco tiempo para que le digas c omo entrar y salir del laberinto por el camino
de menor longitud que permite recoger el botn. De hecho, has de responder a la pregunta
Qu e longitud tiene el camino de menor longitud entre la entrada y la salida que recoge el
botn de la casilla (b
0
, b
1
)? en tiempo O(1). Y has de responder a la pregunta Qu e casillas
recorre ese camino? en tiempo O(k) donde k es la longitud del camino en cuesti on. El objetivo
es preprocesar el laberinto y preparar la informaci on necesaria para responder a estas dos
preguntas con el coste temporal indicado.
O sea, inicialmente se proporcionan los valores n y m, la matriz L y las coordenadas (e
0
, e
1
)
y (s
0
, s
1
). C omo preprocesaras el laberinto para responder en el tiempo indicado a las dos
preguntas tan pronto se sepa d onde est a el botn? Qu e informaci on necesitas almacenar para
poder responder ecientemente a estas dos preguntas? C omo responderas a ambas pregun-
tas con los costes indicados?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 217
4.8. Caminos m as cortos en digrafos ponderados
Hemos aprendido a resolver algunos problemas sobre caminos de menor longitud, esto
es, con menor n umero de aristas. Nos ocuparemos ahora de un generalizaci on de es-
tos problemas en los que la longitud de un camino se dene como la suma de pesos
asociados a las aristas. La ponderaci on de una arista puede representar una distancia,
un coste, una duraci on, etc., por lo que los problemas que resolveremos presentan gran
n umero de aplicaciones pr acticas. Esta posibilidad de que la ponderaci on tenga dife-
rentes sentidos seg un el problema modelado har a que usemos indistintamente t erminos
como camino de menor peso, camino m as corto o camino de menor longitud. Y
llamaremos distancia entre dos v ertices a la longitud del camino m as corto entre am-
bos.
Dado un digrafo G = (V, E) cuyos arcos est an ponderados por una funci on d : E
R, abordaremos cuatro problemas:
1. Problema del camino m as corto entre dos v ertices: Dados un v ertice s, al que deno-
minamos v ertice inicial o fuente, y un v ertice t, al que denominamos v ertice nal o
destino, queremos conocer el camino m as corto de s a t.
2. Problema de los caminos m as cortos de un v ertice a todos los dem as: Dado un
v ertice s al que denominamos v ertice inicial o fuente, queremos encontrar para
todo v ertice v el camino m as corto de s a v.
3. Problema de los caminos m as cortos de un conjunto de v ertices a todos los dem as:
Dado un conjunto de v ertices V, al que denominamos de v ertices iniciales,
queremos encontrar, para todo v ertice v, el camino m as corto de un v ertice cual-
quiera de a v.
4. Problema del camino m as corto entre dos conjuntos de v ertices: Dados un conjunto
de v ertices V, al que denominamos de v ertices iniciales, y un conjunto V
de v ertices nales, queremos encontrar el camino m as corto que parte de un v ertice
cualquiera de y naliza en otro cualquiera de .
En cualquiera de estos cuatro problemas puede darse el caso de que haya dos o m as
caminos entre un par de v ertices con la misma distancia y que esta sea la menor posible.
En tal caso, nuestros algoritmos devolver an uno cualquiera de ellos (pero no todos) como
camino m as corto.
Dejamos para otra secci on un problema relacionado con estos: el del c alculo de la
distancia entre todo par de v ertices.
Los caminos m as cortos de s a cualquier v ertice que vamos a calcular presentan
subestructura optima, una propiedad que podemos enunciar como El prejo del ca-
mino m as corto de s a t que acaba en un v ertice v es un camino m as corto de s a v. Es
natural que sea as, ya que si hubiera un camino m as corto de s a v podramos ir de s a
t por un camino m as corto que el que conocemos: el que formaramos sustituyendo el
prejo citado por el mejor camino de s a v. Cabe notar que hablamos de un camino m as
218 Apuntes de Algoritmia 28 de septiembre de 2009
corto de s a v y no de el camino m as corto de s a v ya que puede haber dos o m as
caminos de s a v con igual peso y ser este el peso optimo.
Es interesante observar que en un digrafo ponderado estrictamente positivo un ca-
mino m as corto de s a cualquier v ertice v no puede contener ciclos: recorrer el ciclo su-
pondr a recorrer una distancia para ir de un v ertice a s mismo y siempre ser a mejor no
recorrerla. Si hay aristas con peso nulo, un camino m as corto de s a un v ertice puede tener
un ciclo, pero el mismo camino sin ese ciclo es tambi en un camino m as corto. Es obvio
decir que en el caso de grafos dirigidos acclicos, sea o no positiva la funci on de ponde-
raci on, ning un camino puede tener ciclos. Estas dos propiedades (subestructura optima
y ausencia de ciclos) hacen que podamos construir un arbol de recubrimiento ( optimo
en cierto sentido) con los caminos m as cortos de s a cualquier v ertice alcanzable desde
s. Un arbol de recubrimiento con estas caractersticas recibir a el nombre de arbol de ca-
minos m as cortos con origen o raz en s. En la gura 4.23 (a) se muestran este arbol de
recubrimiento de caminos m as cortos para el mapa de carreteras de la pennsula ib erica
tomando como ciudad de partida Madrid y en 4.23 (b) el que se obtiene con Castell on de
la Plana como v ertice inicial.
(a) (b)
Figura 4.23: (a)

Arbol de recubrimiento con los caminos m as cortos de Madrid a todas las dem as ciudades del mapa
ib erico. (b)

Idem para Castell on de la Plana como punto de partida.
En el caso de que haya m as de un v ertice inicial, obtendremos un bosque de caminos
m as cortos. En la gura 4.24 se muestra uno sobre el mapa de la pennsula ib erica.
Qu e ocurre si hay pesos negativos? En principio, podra darse el caso de que las aris-
tas que forman un ciclo tuvieran una suma total de pesos negativa. Pero si tal ciclo fuera
alcanzable desde el/un v ertice inicial no tendra sentido hablar del problema del camino
m as corto: el camino m as corto tendra innitas aristas y su peso sera . Asumi-
remos, pues, que cualquiera de los problemas se plantea sobre grafos sin ciclos de peso
negativo. Veremos que en grafos ponderados con pesos posiblemente negativos (aunque
sin ciclos de peso total negativo) tambi en podemos construir un arbol de caminos m as
cortos, pero sobre un grafo diferente.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 219
Figura 4.24: Bosque de recubrimiento con los caminos m as
cortos de Madrid o Castell o de la Plana a todas las dem as
ciudades del mapa ib erico.
Estudiaremos diferentes algoritmos para el c alculo del camino m as corto en un gra-
fo ponderado. Algunos son m as ecientes que otros, pero s olo son aplicables cuando
se observan ciertas restricciones. En cada uno de los siguientes apartados estudiaremos
algoritmos aplicables a grafos con diferentes restricciones:
grafos ponderados acclicos,
grafos ponderados positivos,
grafos con ponderaci on arbitraria (pero sin ciclos negativos).
No resolveremos los cuatro problemas para cada uno de los tres tipos de grafo. Pero
resulta sencillo hacerlo como ejercicio y as lo proponemos cuando convenga.
En otra secci on estudiaremos el problema del c alculo del camino m as corto en un tipo
particular de grafo con gran inter es pr actico: los denominados grafos m etricos.
4.8.1. Camino m as corto en un digrafo ponderado acclico
Para los grafos ponderados acclicos resolveremos primero el problema del c alculo del
camino m as corto entre un v ertice s y cualquier v ertice de un conjunto . Cuando resol-
vamos el problema del camino m as corto en un grafo cualquiera entre dos v ertices, s y t,
recurriremos al algoritmo que desarrollamos como soluci on de este problema.
Finalizaremos el apartado dedicado a grafos acclicos calculando el camino m as corto
que parte de cualquier v ertice de un conjunto y llega a cualquier v ertice de un conjunto
. El algoritmo que desarrollemos se tomar a como punto de partida para el dise no de
otro que resultar a util en programaci on din amica.
Derivaci on de una ecuaci on recursiva para el c alculo de la distancia de un
v ertice a cualquier otro
Empecemos por plantear formalmente el problema: dado un grafo acclico G = (V, E)
ponderado por una funci on d : E R y dados un v ertice s V y un conjunto de v ertices
220 Apuntes de Algoritmia 28 de septiembre de 2009
V, a los que denominamos respectivamente v ertice inicial o fuente y conjunto de
v ertices nales u objetivo, encu entrese un camino que parta de s y nalice en cualquier
v ertice de con el menor coste posible.
Cualquier camino que merezca ser examinado como posible soluci on del problema
podr a expresarse como una secuencia de v ertices, (v
1
, v
2
, . . . , v
n
) V
+
tal que v
1
= s,
v
n
y (v
i
, v
i+1
) E para todo i entre 1 y n 1. Si denotamos con P
s
(t) al conjunto de
caminos que parten de s y nalizan en un v ertice t, es decir,
P
s
(t) = {(v
1
, v
2
, . . . , v
n
) v
1
= s; v
n
= t; (v
i
, v
i+1
) E, 1 i < n},
tenemos que buscamos un camino de este conjunto:
P
s
() =

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
+

R, que es nuestra funci on objetivo y est a denida as:


D((v
1
, v
2
, . . . , v
n
)) =

1i<n
d(v
i
, v
i+1
).
Es decir, deseamos conocer
arg mn
(v
1
,v
2
,...,v
n
)P
s
()
D((v
1
, v
2
, . . . , v
n
)). (4.1)
Aunque deseamos conocer el camino m as corto, y no (s olo) el peso del camino m as
corto, vamos a concentrarnos brevemente en este problema asociado, que se formula
como el c alculo de
D
s
() = mn
(v
1
,v
2
,...,v
n
)P
s
()
D(v
1
, v
2
, . . . , v
n
). (4.2)
Veremos m as tarde que el camino m as corto puede obtenerse como subproducto de su
c alculo.
Denotemos con D
s
(t) la distancia del camino m as corto entre el v ertice s y un v ertice
t cualquiera:
D
s
(t) = mn
(v
1
,v
2
,...,v
n
)P
s
(t)
D(v
1
, v
2
, . . . , v
n
) = mn
(v
1
,v
2
,...,v
n
)P
s
(t)
(

1i<n
d(v
i
, v
i+1
)
)
.
Buscamos, pues, conocer
D
s
() = mn
t
D
s
(t).
El conjunto de caminos P
s
(t) puede denirse recursivamente a partir de P
s
(u), para
todo u predecesor de t:
P
s
(t) = {(v
1
, v
2
, . . . , v
n
) v
1
= s; v
n
= t; (v
1
, v
2
, . . . , v
n1
) P
s
(u), (u, t) E}.
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 221
El caso base de la recurrencia es P
s
(s) = {(s)}, pues el camino m as corto de s a s en un
grafo sin ciclos es un camino sin aristas y formado por un s olo v ertice: s. La denici on
recursiva de P
s
(t) puede, pues, escribirse as:
P
s
(t) =
{
{(s)}, si s = t;
{(v
1
, . . . , v
n
) v
1
= s; v
n
= t; (v
1
, . . . , v
n1
) P
s
(u), (u, t) E}, si no.
En D
s
(t) podemos sustituir P
s
(t) por su expresi on recursiva y considerar en primer
lugar la minimizaci on que afecta a la ultima arista que puede formar parte de un camino
que naliza en t:
D
s
(t) = mn
(u,t)E
(
mn
(v
1
,v
2
,...,v
n1
)P
s
(u)
((

1i<n1
d(v
i
, v
i+1
)
)
+ d(u, t)
))
.
Y, ahora, extraer de la minimizaci on interior el ultimo t ermino del sumatorio:
D
s
(t) = mn
(u,t)E
((
mn
(v
1
,v
2
,...,v
n1
)P
s
(u)
(

1i<n1
d(v
i
, v
i+1
)
))
+ d(u, t)
)
.
Este ultimo paso puede darse porque d(u, t) no depende de la minimizaci on interior
(s olo de la exterior) y porque mn
aR
(a + b) = (mn
aR
a) + b para todo b real (en esta
igualdad la minimizaci on interior juega el papel de a y d(u, t) el de b). Que mn
aR
(a +
b) = (mn
aR
a) + b se deduce f acilmente del hecho de a a

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

(u) + d(u, v))), si v ;


mn
(u,v)E
(D

(u) + d(u, v)) , en otro caso;


28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 227
que podemos expresar de forma m as compacta con
D

(v) = mn
({
0, si v ,
+, si v ,
, mn
(u,v)E
(D

(u) + d(u, v))


)
. (4.4)
Y deseamos conocer
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

= {((u, i 1), (v, i)) (u, v) E, 0 < i k};


230 Apuntes de Algoritmia 28 de septiembre de 2009
y su funci on de ponderaci on asociada d

se dene as a partir de d:
d

((u, i 1), (v, i)) = d(u, v).


N otese que todo camino (v
1
, v
2
, . . . , v
k+1
) en G con k aristas tiene un camino equivalente
(con el mismo peso) en G

: ((v
1
, 0), (v
2
, 1), . . . , (v
k+1
, k)). Y viceversa: todo camino en
G

tiene un camino equivalente en G y con el mismo peso. As pues, resulta evidente


que calcular la distancia del camino m as corto en G

entre (s, 0) y (t, k) es un problema


equivalente a calcular la distancia del camino entre s y t de k aristas m as corto en G.
Figura 4.28: (a) Grafo ponderado
(no acclico). En trazo grueso se
muestran las aristas del camino
(0, 1, 4, 5), que es el m as corto. (b)
Grafo multietapa asociado al ante-
rior sobre el que se efect ua la b usque-
da del camino m as corto entre 0 y
5 con 3 aristas. En trazo grueso se
muestra el camino equivalente en es-
te grafo al que se destac o en el grafo
original de la izquierda.
0 1 2
3 4 5
3
1
5
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
3
1
5
2
2
2
3
1
1
2
2
3
1
5
2
2
2
3
1
1
2
2
3
1
5
2
2
2
3
1
1
2
2
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
(a) (b)
No es necesario construir explcitamente G

para calcular su camino m as corto entre


(s, 0) y (t, k). He aqu una funci on iterativa implementada en Python que trabaja implci-
tamente con 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

el primer v ertice de p que no est a en added. La gura 4.33


ilustra esta situaci on.
Supongamos que no hay aristas de peso nulo. Por fuerza, para que el camino presente
una distancia menor que D[v], la distancia de s a v

ha de ser menor que D[v], ya que el


tramo de p que va de 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

est a formado s olo por v ertices de


added, esto equivale a suponer que D[v

] < D[v]. Pero eso es una contradicci on: de ser


as, en la iteraci on actual no habramos seleccionado el v ertice v, sino el v ertice 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

con v se formara con una secuencia de aristas de peso nulo. En tal


caso, D[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.

Edsger W. Dijkstra (19302002) es uno de los pioneros de la algortmica.


El conocido como algoritmo de Dijkstra se public o en el artculo A note
on two problems in connection with graphs, Numerical Mathematics, n um. 1, pp.
269271, 1959.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85 Un digrafo ponderado en el que todos los arcos pesan 1 es un digrafo ponderado positivo.
Podemos usar, pues, el algoritmo de Dijkstra para calcular el camino m as corto entre un par de
v ertices. Es lo mejor que podemos hacer?
86 Haz una traza del algoritmo de Dijkstra para los siguientes grafos, tomando como v ertice
origen el 1.
1 2
3 4
5
50
30
100
10
5
50
20
10
1 2 3
4 5 6
10
15 7
10
8
12
9
1 2
3
4 5
6
3
6
10
0
4
2
2
1
1
2
1
2 3
4 5
10
5
3
2 4 6
2
3
9
(a) (b) (c) (d)
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 243
87 Muestra un ejemplo sencillo de un grafo dirigido con pesos negativos en las aristas para el
que el algoritmo de Dijkstra produce una soluci on err onea.
88 Deseamos encontrar un camino de coste mnimo entre la esquina inferior izquierda y la
esquina superior derecha de una matriz en la que el valor de la celda de ndices (i, j) representa
la altura del terreno en esas coordenadas. Desde una casilla podemos desplazarnos unicamente
hacia el norte y hacia el este. Buscamos:
a) el camino de menor altura total (suma de alturas de cada casilla visitada);
b) el camino de altura media mnima.
Dise na un algoritmo que resuelva estos problemas y analiza su complejidad computacional en
funci on n y m, los n umeros de las y columnas de la matriz.
89 Si en lugar de la suma de pesos de aristas consideramos el producto de sus pesos como peso
de un camino, qu e cambios es preciso introducir en el algoritmo de Dijkstra? Es suciente con
exigir que el peso de las aristas sea positivo para poder aplicar el algoritmo de Dijkstra?
90 Dado un grafo ponderado positivo, deseamos calcular el camino m as corto entre un v ertice
cualquiera de un conjunto y todos los dem as v ertices. Qu e modicaciones habra que hacer al
algoritmo de Dijkstra? Justica tu respuesta y analiza la complejidad computacional del algoritmo
resultante.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.9. El camino m as corto en un digrafo ponderado
eucldeo
Un grafo eucldeo es un grafo dirigido G = (V, E) y ponderado cuyos v ertices son puntos
de R
n
(por simplicidad supondremos que n = 2) y en el que la funci on que proporciona
el peso de las aristas es la distancia eucldea entre los v ertices que une.
La distancia eucldea d
e
: E R
0
presenta dos propiedades interesantes que ex-
plotaremos en el dise no de un algoritmo eciente: a) no proporciona valores negativos y
b) satisface la desigualdad triangular, es decir, se observa d
e
(u, w) d
e
(u, v) + d
e
(v, w)
para todo u, v y w en R
n
. La primera propiedad hace que podamos aplicar el algoritmo
de Dijkstra, pero la segunda conducir a a una variante que se traduce en una aceleraci on
notable del c alculo, aunque sin ofrecer una mejora asint otica para el peor de los casos.
El algoritmo que presentamos no requiere que el grafo sea eucldeo. M as adelante indi-
caremos las propiedades que debe observar la funci on para que el nuevo algoritmo sea
aplicable.
Durante la ejecuci on del algoritmo de Dijkstra se itera un procedimiento consistente
en extraer un v ertice de un diccionario de prioridad e insertar/actualizar informaci on
relativa a algunos de sus sucesores en el diccionario. El v ertice extrado, v, es siempre
uno para el que el algoritmo puede garantizar que conoce el camino m as corto desde el
origen hasta el. (Los punteros hacia atr as permiten recuperar el camino m as corto de s a
cualquier v ertice v al nalizar el c alculo.) En la gura 4.34 (a) ilustra un aspecto intere-
sante del algoritmo de Dijkstra al calcular el camino m as corto de Ciudad Real a Soria
sobre el grafo eucldeo del mapa de la pennsula ib erica. Hemos marcado con un crculo
244 Apuntes de Algoritmia 28 de septiembre de 2009
y un punto grueso los v ertices para los que algoritmo de Dijkstra lleg o a conocer el ca-
mino m as corto desde Ciudad Real y s olo con un crculo aquellos que llegaron a ingresar
en la frontera. Se puede observar que se han visitado pr acticamente todos los v ertices
alrededor de Ciudad Real. Es as porque se ha llegado a calcular la distancia que separa
Ciudad Real de todas aquellas ciudades que se encuentran a la misma o inferior distancia
de Soria.
Ciudad Real
Soria
Ciudad Real
Soria
(a) (b)
Figura 4.34: (a) Ciudades visitadas al ejecutar el algoritmo de Dijkstra para calcular el camino m as corto de Ciudad
Real a Soria. Las ciudades marcadas con un punto grueso y rodeadas con un crculo son v ertices que forman parte del
arbol de caminos m as cortos al acabar la ejecuci on. Las ciudades rodeadas con un crculo han sido visitadas durante el
c alculo. (b) Resultado al usar el algoritmo implementado en MetricDigraphShortestPaths.
Podemos considerar que Dijkstra va explorando los v ertices que encuentra en un
crculo centrado en s y cuyo radio va creciendo hasta incluir a t.
Modicaremos el algoritmo de Dijkstra para evitar visitar tantas ciudades y focalizar
la b usqueda en los v ertices que hay efectivamente entre el de partida y el de llegada.
Nuestra va de ataque al problema pasa por modicar el valor que asociamos a cada
v ertice en el diccionario de prioridad. En lugar de corresponder a la distancia del camino
m as corto de s a v conocido hasta el momento, contendr a una estimaci on de la distancia
del camino m as corto de s a t pasando por v. Nuestra estimaci on se podr a desglosar en
tres partes:
la distancia de s a u, para el predecesor u de v por el que pasa el camino m as corto
(conocido hasta el momento) de s a v,
el peso de la arista (u, v),
y una estimaci on optimista del camino m as corto de v a t, que corresponder a a la
distancia eucldea entre v y t.
N otese que esta estimaci on de la distancia del camino m as corto de s a t pasando por
v es optimista, pues cualquier camino de v a t tendr a un distancia igual o superior. No
28 de septiembre de 2009 Captulo 4. Algoritmos sobre grafos 245
es necesario que dicha estimaci on optimista sea la distancia eucldea. Podemos imponer
unas condiciones m as laxas: si nuestro grafo esta ponderado por una funci on d : E
R
0
, deberemos suministrar a la rutina de c alculo otra funci on d

: E R
0
tal que
d

(u, v) 0 para todo u, v de V;


d

(u, v) + d

(v, w) d

(u, w) para todo u, v y w de V;


d

(u, v) d(u, v) para todo (u, v) de E.


N otese que la distancia eucldea satisface estas tres condiciones.
Veamos el algoritmo:
algoritmia/graphalgorithms/metricdigraphshortestpaths.py
class MetricDigraphShortestPaths(object):
def init (self , **kw):
get factories(self , kw, mappingFactory=lambda V: dict(),
setFactory=lambda V: set(),
priorityDictFactory=lambda D, V: MinPriorityDict(D, capacity=len(V)))
def shortest path(self , G: "metric Digraph<T>", d: "f: T, T -> R",
d

: "f: T, T -> R", s: "T", t: "T"):


D = self .mappingFactory(G.V)
for v in G.V: D[v] = innity
D[s] = 0
back = self .mappingFactory(G.V)
back[s] = s
fringe = self .priorityDictFactory(D.items(), G.V)
fringe[s] = 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, t) no es problem atico, pues esa misma cantidad


se ha sumado a todo posible camino de s a v. Cuando t ingresa en el added sabemos que
no puede haber un camino m as corto de s a t que el que contiene el arbol de caminos
m as cortos, ya que dicho camino consistira en un prejo que sigue una rama del arbol
(partiendo de s), una arista a alg un v ertice v que no incluida en el arbol y un camino de v
a t cuya distancia es mayor o igual que d

(v, t). Pero sabemos que la distancia del tramo


que va de s a v m as d

(v, t) no puede ser menor que la distancia de s a t por el camino


que representa el arbol.
Volveremos a considerar este problema m as adelante, cuando estudiemos la estrate-
gia conocida por Ramicaci on y Acotaci on. All lo estudiaremos a la luz de un marco
general.
4.9.1. Sobre la complejidad computacional
El coste en el peor de los casos sigue siendo el propio del algoritmo de Dijkstra. En la
pr actica, sin embargo, cabe esperar un comportamiento mejor. Sin entrar en detalles, bas-
te apuntar que el algoritmo explora, aproximadamente, los v ertices que se encuentran en
el lugar de los puntos cuya distancia al origen sumada a la distancia al destino es igual a
la longitud del camino m as corto de s a t. Este lugar corresponde a una elipse. En casos
tpicos, el coste temporal del algoritmo es O(V) para grafos densos y O(

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 expresi on divide y vencer as tiene origen en la estrategia seguida por


Napole on en Austerlitz en 1805. El ej ercito formado por austriacos y rusos
superaba en 15 000 soldados a las tropas francesas. Napole on se anticip o al ataque
con una ofensiva al centro de las tropas enemigas que las dividi o en dos bandos a
los que pudo vencer por separado.
Un ejemplo paradigm atico de algoritmo que sigue una estrategia divide y vencer as
es la ordenaci on por fusi on. Empezaremos nuestra exposici on, pues, presentando este
algoritmo. A continuaci on introduciremos el esquema algortmico divide y vencer as y
nos detendremos a reexionar acerca de las condiciones que debe satisfacer cada elemen-
to del esquema para conducir a algoritmos ecientes. El denominado teorema maestro
nos permitir a dar soluci on a muchas de las ecuaciones recursivas con las que se expresa
el coste temporal de los algoritmos basados en divide y vencer as.
Algunos algoritmos reduce y vencer as pueden transformarse de forma sencilla en
algoritmos iterativos equivalentes: se trata de los que presentan la denominada recursi on
por cola. Transformar la recursi on en iteraci on proporciona un ahorro de espacio (al eli-
255
256 Apuntes de Algoritmia 28 de septiembre de 2009
minar la necesidad de gestionar una pila de llamadas a funci on) sin que el consumo de
tiempo se vea afectado. Estudiaremos la t ecnica de eliminaci on de la recursi on por cola.
5.1. Ordenaci on por fusi on
Planteemos formalmente el problema de la ordenaci on, que ya es bien conocido por el
lector: dado un vector a de talla n entre cuyos elementos existe una relaci on de orden
total es menor o igual que, que denotamos con , deseamos ordenar el vector en
orden no decreciente, es decir, a[i] a[i+1] para todo valor entero de i entre 0 y n 1.
5.1.1. Una primera soluci on basada en divide y vencer as
La t ecnica de ordenaci on por fusi on (tambi en conocida como ordenaci on por mezcla o,
por su nombre en ingl es, mergesort) divide el problema de ordenar un vector de talla
n en dos subproblemas que deben resolverse por separado: ordenar un vector con los
n/2 primeros elementos y ordenar otro vector con los restantes n/2 elementos. Una
vez ordenados ambos vectores, se funden en uno solo completamente ordenado.
El procedimiento es de naturaleza recursiva: ordenar por fusi on un vector supone
ordenar dos vectores m as peque nos tambi en por fusi on. El caso base de la recursi on
considera los vectores de talla unitaria y su ordenaci on es trivial: no hay que hacer nada
pues todo vector de talla unitaria est a ordenado. La gura 5.1 ilustra el principio b asico
de funcionamiento de la ordenaci on por fusi on y la gura 5.2 muestra un proceso de
ordenaci on por fusi on completo.
Figura 5.1: Principio de
funcionamiento de la or-
denaci on por fusi on. Las
echas de trazo disconti-
nuo indican otras divisio-
nes del problema en sub-
problemas y combinaciones
de resultados.
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
4
8
11
9
18
10
29
11
30
12
37
13
43
14
75
15
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
dividir el problema (de talla 16)
en dos problemas (de talla 8),
resolver independientemente cada problema
y combinar ambas soluciones.
La operaci on de divisi on de un vector en otros dos es extremadamente sencilla: basta
con cortar el vector en dos de talla tan parecida como resulte posible. La operaci on de
combinaci on de resultados o fusi on es f acil de entender si se piensa en c omo se mezclan
dos mitades ordenadas de una baraja para tener un mazo ordenado: vamos escogiendo
28 de septiembre de 2009 Captulo 5. Divide y vencer as 257
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
11
0
21
1
1
2
3
3
0
4
98
5
12
6
82
7
29
8
30
9
11
10
18
11
4
12
43
13
37
14
75
15
1
0
3
1
11
2
21
3
0
4
12
5
82
6
98
7
11
8
18
9
29
10
30
11
4
12
37
13
43
14
75
15
0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
4
8
11
9
18
10
29
11
30
12
37
13
43
14
75
15
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
D
i
v
i
d
i
r
C
o
m
b
i
n
a
r
Figura 5.2: Traza com-
pleta del algoritmo de or-
denaci on por fusi on.
la primera carta de una u otra mitad seg un convenga. Cuando una de las mitades de
la baraja se agota, a nadimos al mazo todas las cartas de la otra mitad, que, recordemos,
est an ordenadas. La gura 5.3 muestra gr acamente el proceso de fundir dos vectores
ordenados, a y b, en un vector destino c.
Implementaremos el procedimiento de fusi on en una funci on, merge, que recibe dos
vectores, a y b, y devuelve un nuevo vector, c. La funci on se denir a en el seno de otra
funci on, mergersort, a la que auxilia y que efect ua la ordenaci on partiendo el vector en
dos, ordenando cada uno de los nuevos vectores y fundiendo los vectores ordenados con
merge:
algoritmia/problems/sorting.py
class MergeSorter(ISorter):
def sorted(self , a: "sequence<T>") -> "sorted iterable<T>":
if len(a) <= 1:
return a
else:
return self .merge(self .sorted(a[:len(a)//2]), self .sorted(a[len(a)//2:]))
def merge(self , a: "sorted sequence<T>",
b: "sorted sequence<T>") -> "sorted sequence<T>":
c = [None] * (len(a)+len(b))
i, j, k = 0, 0, 0
while i < len(a) and j < len(b):
258 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 5.3: Traza de merge para dos
vectores ordenados, a y b. El resul-
tado se almacena en el vector destino
c. En cada paso se selecciona el ele-
mento de menor valor de entre los dos
apuntados, respectivamente, por los
ndices i y j y se deposita en la cel-
da de ndice k en c. Cuando uno de
los ndices i o j desborda su rango
de valores permitidos, se copia el res-
to de a o b al nal de c. En la gura
esto ocurre con el ndice i.
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b
0 1 2 3 4 5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0 1 2 3 4 5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1 2 3 4 5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1
3
2 3 4 5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1
3
2
11
3 4 5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1
3
2
11
3
12
4 5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1
3
2
11
3
12
4
21
5 6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1
3
2
11
3
12
4
21
5
82
6 7
c
i j k
1
0
3
1
11
2
21
3
a 0
0
12
1
82
2
98
3
b 0
0
1
1
3
2
11
3
12
4
21
5
82
6
98
7
c
i j k
if a[i] < b[j]: c[k] = a[i]; i += 1
else: c[k] = b[j]; j += 1
k += 1
while i < len(a): c[k] = a[i]; i += 1; k += 1
while j < len(b): c[k] = b[j]; j += 1; k += 1
return c
La rutina merge consta de tres bucles while. El ndice i recorre el vector a y el ndice j
recorre el vector b. El primer bucle while compara, con cada iteraci on, el elemento menor
entre a[i] y b[j]. Si se selecciona a[i], se incrementa i, y en caso contrario, se incremen-
ta j. El elemento seleccionado se almacena en c[k] y k, que empieza con el valor 0, se
incrementa. El primer bucle while naliza cuando i o j alcanzan la talla de a o b, res-
pectivamente. De los bucles while segundo y tercero, s olo uno llega a ejecutar alguna
iteraci on. Se ejecuta el segundo si el ndice j se sali o del vector b. Su cometido es copiar
el contenido de a[i:] al nal de c. El tercer bucle while itera si el ndice que se sali o de
28 de septiembre de 2009 Captulo 5. Divide y vencer as 259
rango es i, y tiene por objeto copiar b[j:] al nal de c.
Podemos probar nuestra codicaci on del algoritmo:
demos/divideandconquer/mergesort.py
from algoritmia.problems.sorting import MergeSorter
print(MergeSorter().sorted([11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37]))
[0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
97 Realiza una traza completa del algoritmo de ordenaci on por fusi on para estos dos vectores:
a) [10, 3, 50, 3, 1, 45, 1]; b) [12, 8, 21, 19, 26, 2, 29, 11, 5, 14, 26, 34, 21, 20, 30].
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.1.2. Correcci on del algoritmo
Resulta f acil demostrar por inducci on que MergeSorter.sorted devuelve los elementos del
vector ordenados si demostramos antes que merge funde dos vectores ordenados de ta-
llas n y m en un vector ordenado de talla n + m. Al llamar a merge(a, b), los ndices i y j
recorren los elementos de a y b de izquierda a derecha. Con cada incremento de uno de
los ndices, el elemento a[i] o b[j] se a nade a c. Como cada elemento se a nade a c una
sola vez, al nal c contiene un copia de todos los elementos de a y b. Es evidente que los
elementos de c est an ordenados, pues en cada instante se selecciona el menor de entre to-
dos los elementos de a y b, que es el menor entre a[i] y b[j]. As pues, MergeSorter.merge
proporciona un vector ordenado con todos los elementos de dos vectores ordenados.
Demostremos por inducci on sobre la talla del vector que el m etodo ordena su conte-
nido. La base de inducci on considera vectores de talla 0 o 1: MergeSorter.sorted devuelve
su contenido intacto, es decir, los valores ordenados. Ahora hemos de demostrar que si
toda secuencia de n

< n elementos devuelta por MergeSorter.sorted est a ordenada, los n


elementos devueltos por MergeSorter().sorted(v) est an ordenados. La prueba es trivial:
el vector a, de talla n, se divide en a[:n/2] y a[n/2:], ambos de talla inferior a n, y am-
bos se procesan con MergeSorter.sorted. Por hip otesis de inducci on, este m etodo devuelve
copias ordenadas de a[:n/2] y a[n/2:]. Estas copias ordenadas se procesan con Merge-
Sorter.merge, que devuelve un vector ordenado con los mismos elementos que contena
a.
5.1.3. Complejidad computacional de la ordenaci on por fusi on
Hagamos primero el an alisis de complejidad temporal. Se trata de un algoritmo recur-
sivo, as que podemos expresar el n umero de pasos invertido por el algoritmo sobre un
vector de n elementos con una ecuaci on recursiva:
T(n) =
{
c
1
, si n 1;
T(n/2) + T(n/2) + c
2
n + c
3
, si n > 1.
260 Apuntes de Algoritmia 28 de septiembre de 2009
El t ermino c
2
n recoge el coste temporal de dos acciones: la creaci on de dos vectores
de tallas n/2 y n/2 a partir del original y la fusi on de dos vectores ordenados de
esas mismas tallas, que son ambas operaciones de tiempo (n). La constante c
3
recoge
el tiempo dedicado a las acciones que, con cada llamada a funci on, se ejecutan en tiempo
constante.
Si suponemos que n es una potencia de 2, la ecuaci on se simplica notablemente:
T(n) =
{
c
1
, si n = 1;
2T(n/2) + c
2
n + c
3
, si n > 1.
Despleguemos la ecuaci on:
T(n) = 2T(n/2) + c
2
n + c
3
= 2(2T(n/4) + c
2
n/2 + c
3
) + c
2
n + c
3
= 2
2
T(n/2
2
) +2c
2
n +2c
3
+ c
3
= 2
2
(2T(n/2
3
) + c
2
n/2
2
+ c
3
) +2c
2
n +2c
3
+ c
3
= 2
3
T(n/2
3
) +3c
2
n +2
2
c
3
+2
1
c
3
+2
0
c
3
= . . .
= 2
k
T(n/2
k
) + kc
2
n + c
3
0i<k
2
i
= 2
k
T(n/2
k
) + kc
2
n + c
3
(2
k
1).
En k = lg n tenemos T(n/2
k
) = T(1):
T(n) = 2
k
T(n/2
k
) + kc
2
n + c
3
(2
k
1)
= nT(1) + c
2
n lg n + c
3
(n 1)
= c
1
n + c
2
n lg n + c
3
(n 1).
O sea, T(n) (n lg n).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
98 Recuerda que el desplegado no es suciente para demostrar que T(n) puede expresarse sin
recursi on de ese modo. Demuestra por inducci on la veracidad de la conjetura, es decir, que
T(n) = c
1
n + c
2
n lg n + c
3
(n 1).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Se puede ilustrar el proceso de divisi on con un arbol de recursi on. Un arbol de recur-
si on es un arbol etiquetado y con raz cuyos nodos representan llamadas a funci on. En la
gura 5.4 se muestra un arbol de recursi on cuyos nodos est an etiquetados con la talla de
los problemas que tratan de resolver las respectivas llamadas a MergeSorter.sorted. Sobre
este arbol resulta sencillo efectuar una estimaci on del coste temporal.
El coste espacial de MergeSorter.sorted es (n). En el instante en que mayor memoria
se consume hay 1 +lg n tramas de activaci on en la pila de llamadas a funci on. Esta canti-
dad coincide con la profundidad del arbol. De las 1 +lg n tramas, una requiere un vector
28 de septiembre de 2009 Captulo 5. Divide y vencer as 261
1 1 1 1 1 1 1 1
n/4 n/4 n/4 n/4
n/2 n/2
n n
2n/2
4n/4
n
n + n lg n
nivel 0
nivel 1
nivel 2
nivel lg n
1
+
l
g
n
n
i
v
e
l
e
s
Figura 5.4:

Arbol de recursi on de una llamada a Mergesorter.sorted sobre un vector de talla n, siendo n potencia de 2.
Hay 1 + lg n niveles. En el nivel k, para k > 0, se efect uan 2
k
divisiones y fusiones de vectores de talla n/2
k
. Cada
divisi on y fusi on se realiza en tiempo O(n/2
k
), as que todas las divisiones y fusiones de ese nivel suponen un coste
temporal total O(n). La columna de la derecha recoge estos costes que, sumados, suponen la ejecuci on de O(n lg n)
pasos.
de talla n; otra, un vector de talla n/2; otra, uno de talla n/4, etc. El resultado de sumar
las tallas de estos vectores arroja un consumo de memoria proporcional a n.

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.

Un inconveniente de esta versi on de la ordenaci on por fusi on es que ne-


cesita un vector auxiliar del mismo tama no que el vector original. Requiere,
por tanto, consumir memoria adicional que crece con n, la talla del vector. Existen
versiones de este mismo algoritmo que realizan las fusiones en tiempo lineal sin ne-
cesidad de vectores auxiliares. No obstante, las constantes de proporcionalidad del
coste temporal pueden hacer preferible el que consume m as memoria.
264 Apuntes de Algoritmia 28 de septiembre de 2009
55
0
22
1
44
2
66
3
11
4
33
5
22
6
66
7
InPlaceMergeSorter.sort(a)
55 22 44 66 11 33 22 66
. mergesort(a,0,8, c)
11 33 22 66 55 22 44 66
. mergesort(a,0,4, c)
44 66 11 33 22 66 55 22
. mergesort(a,0,2, c)
22 44 66 11 33 22 66 55
. mergesort(a,0,1, c)
55 44 66 11 33 22 66 22
. mergesort(a,1,2, c)
22 55 44 66 11 33 22 66 22 55 22 55
.merge(a,0,2, c)
22 55 11 33 22 66 44 66
. mergesort(a,2,4, c)
22 55 66 11 33 22 66 44
. mergesort(a,2,3, c)
22 55 44 11 33 22 66 66
. mergesort(a,3,4, c)
22 55 11 33 22 66 44 66
44 66
.merge(a,2,4, c)
11 33 22 66 22 44 55 66 22 44 55 66
.merge(a,0,4, c)
22 44 55 66 11 33 22 66
. mergesort(a,4,8, c)
22 44 55 66 22 66 11 33
. mergesort(a,4,6, c)
22 44 55 66 33 22 66 11
. mergesort(a,4,5, c)
22 44 55 66 11 22 66 33
. mergesort(a,5,6, c)
22 44 55 66 22 66 11 33 11 33
.merge(a,4,6, c)
22 44 55 66 11 33 22 66
. mergesort(a,6,8, c)
22 44 55 66 11 33 66 22
. mergesort(a,6,7, c)
22 44 55 66 11 33 22 66
. mergesort(a,7,8, c)
22 44 55 66 11 33 22 66 22 66
.merge(a,6,8, c)
22 44 55 66 11 22 33 66 11 22 33 66
.merge(a,4,8, c)
11 22 22 33 44 55 66 66 11 22 22 33 44 55 66 66
.merge(a,0,8, c)
c a
Figura 5.6: Traza de llamada al m etodo InPlaceMergeSorter.sort sobre el vector [55,22,44,66,11,33,22,66]. El
par ametro c es un vector auxiliar. Una vez hecha la fusi on, el contenido del subvector ordenado se copia en la porci on
correspondiente de a (en negrita).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
105 Analiza el coste temporal y espacial de in place mergesort.
106 Es posible utilizar s olo un vector auxiliar de tama no n/2 cuando se desea fundir dos
vectores ordenados de tama nos n/2 y n/2 que forman un vector de tama no n. En ese unico
vector se copia la primera mitad del vector de talla n. Los ndices i y j apuntan al primer elemento
del vector peque no y al primer elemento no copiado en el vector original. Se procede enton-
ces siguiendo el m etodo convencional. Una ventaja de esta aproximaci on, adem as del ahorro de
memoria, es que inicialmente no se mueven en memoria m as que la mitad de los elementos y, si
llega el caso en que se agota el primer vector, los elementos del segundo ya est an en su destino
28 de septiembre de 2009 Captulo 5. Divide y vencer as 265
nal.
Implementa esta versi on del algoritmo.
107 La rutina InPlaceMergeSorter.sort puede evitar movimientos innecesarios de bloques de me-
moria ( ultima lnea del m etodo) si efect ua llamadas alternativas sobre el vector original y el vector
auxiliar. Implementa una versi on del m etodo que explote esta idea.
108 La variante bit onica de la fusi on (funci on merge) copia en el vector auxiliar los fragmen-
tos que se van a ordenar, pero uno en orden creciente y el otro en orden decreciente (de ah el
calicativo de bit onica). A continuaci on, sit ua sendos punteros (ndices), i y j, apuntando
respectivamente al inicio y nal de la regi on copiada. La fusi on procede como siempre: copia en
el destino el menor de los elementos apuntados, s olo que en este caso, ha de decrementar j. Esto
permite simplicar el bucle principal (cuya terminaci on ahora la determina el cruce de i y j, es
decir, el bucle aborta su ejecuci on cuando i > j) y elimina los dos bucles adicionales.
Implementa la variante bit onica de la ordenaci on por fusi on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2. La estrategia divide y vencer as
Un esquema algortmico es una descripci on del m etodo que seguimos cuando solucio-
namos una familia de problemas. En el se describe el (meta)algoritmo y las condiciones
que deben observar los datos de entrada y/o las funciones en que se apoya el m etodo
resolutivo. Todos los algoritmos que clasicamos como divide y vencer as siguen una
misma estrategia: ante un problema difcil producen problemas m as sencillos cuyas
soluciones, adecuadamente procesadas, permiten obtener la soluci on del problema ori-
ginal. Ello conduce a un procedimiento de naturaleza recursiva que se apoya en varias
funciones o rutinas con cometidos muy claros: determinar si un problema es difcil o sen-
cillo, resolver directamente un problema sencillo, generar subproblemas m as sencillos a
partir de un problema difcil, y combinar sus soluciones.
5.2.1. El esquema algortmico
La estrategia divide y vencer as puede modelarse con el siguiente esquema algortmico:
algoritmia/schemes/divideandconquer.py
class IDivideAndConquerProblem(metaclass=ABCMeta):
@abstractmethod
def is simple(self ) -> "bool":
"""Devuelve cierto sii es un problema que admite resoluci on directa."""
raise NotImplementedError
@abstractmethod
def trivial solution(self ) -> "Solution":
"""Resuelve directamente el problema si es simple."""
raise NotImplementedError
@abstractmethod
def divide(self ) -> "Iterable of DivideAndConquerProblem":
"""Devuelve los problemas que resultan de dividir a este."""
266 Apuntes de Algoritmia 28 de septiembre de 2009
raise NotImplementedError
@abstractmethod
def combine(self , solutions: "Iterable of Solutions") -> "Solution":
"""Recibe un iterable con soluciones de subproblemas y las combina
para formar con ellas su propia soluci on."""
raise NotImplementedError
class DivideAndConquerSolver:
def solve(self , problem: "IDivideAndConquerProblem") -> "Solution":
if problem.is simple():
return problem.trivial solution()
else:
return problem.combine(self .solve(p) for p in problem.divide())
Hasta el momento hemos implementado la ordenaci on por fusi on con funciones que
reciben un vector y proporcionan una copia con sus elementos ordenados. Para trabajar
con el esquema hemos de cambiar el punto de vista: conviene pensar en t erminos de ins-
tancias del problema capaces de determinar si son simples (y en tal caso proporcionar
directamente una soluci on), dividirse en nuevas instancias y combinarse con las solucio-
nes de dichas instancias para formar una nueva soluci on. Las instancias de un problema
son, pues, instancias de una clase que implementa a IDivideAndConquerProblem. La pri-
mera versi on de mergesort que hemos dado puede implementarse de este modo:
algoritmia/problems/sorting.py
from algoritmia.schemes.divideandconquer import IDivideAndConquerProblem
...
class MergesortProblem(IDivideAndConquerProblem):
def init (self , a: "sequence<T>"):
self .a = a
def is simple(self ) -> "bool":
return len(self .a) <= 1
def trivial solution(self ) -> "sorted sequence<T>":
return self .a
def divide(self ) -> "iterable<MergesortProblem>":
yield MergesortProblem(self .a[:len(self .a)//2])
yield MergesortProblem(self .a[len(self .a)//2:])
def combine(self , s: "iterable<sorted sequence<T>>") -> "sorted sequence<T>":
a, b = tuple(s)
c = [None] * (len(a)+len(b))
i, j, k = 0, 0, 0
while i < len(a) and j < len(b):
if a[i] < b[j]: c[k] = a[i]; i += 1
else: c[k] = b[j]; j += 1
k += 1
28 de septiembre de 2009 Captulo 5. Divide y vencer as 267
while i < len(a): c[k] = a[i]; i += 1; k += 1
while j < len(b): c[k] = b[j]; j += 1; k += 1
return c
demos/divideandconquer/mergesortfromscheme.py
from algoritmia.problems.sorting import MergesortProblem
from algoritmia.schemes.divideandconquer import DivideAndConquerSolver
problem = MergesortProblem([11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37])
print(DivideAndConquerSolver().solve(problem))
[0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
Modelemos ahora con el esquema la segunda versi on del algoritmo, la que realiza
una ordenaci on in situ:
algoritmia/problems/sorting.py
class InPlaceMergesortProblem(IDivideAndConquerProblem):
def init (self , a: "sequence<T>", p: "int"=0, q: "int or None"=None,
c: "sequence<T> or None"=None):
self .a = a
if q == None: q, c = len(a), a[:]
self .p, self .q = p, q
self .c = c
def is simple(self ) -> "bool":
return self .q - self .p <= 1
def trivial solution(self ) -> "InPlaceMergesortProblem":
return self
def divide(self ) -> "iterable<InPlaceMergesortProblem>":
middle = (self .p + self .q) // 2
yield InPlaceMergesortProblem(self .a, self .p, middle, self .c)
yield InPlaceMergesortProblem(self .a, middle, self .q, self .c)
def combine(self , s: "iterable<InPlaceMergesortProblem>"
) -> "InPlaceMergesortProblem":
a, b = tuple(s)
i, j, k = a.p, b.p, a.p
while i < a.q and j < b.q:
if self .a[i] < self .a[j]: self .c[k] = self .a[i]; i += 1
else: self .c[k] = self .a[j]; j += 1
k += 1
while i < a.q: self .c[k] = self .a[i]; i += 1; k += 1
while j < b.q: self .c[k] = self .a[j]; j += 1; k += 1
for k in range(a.p, b.q): self .a[k] = self .c[k]
return self
268 Apuntes de Algoritmia 28 de septiembre de 2009
demos/divideandconquer/inplacemergesortfromscheme.py
from algoritmia.problems.sorting import InPlaceMergesortProblem
from algoritmia.schemes.divideandconquer import DivideAndConquerSolver
v = [11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37]
DivideAndConquerSolver().solve(InPlaceMergesortProblem(v))
print(v)
[0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
En este problema concreto, dividir resulta ser una operaci on sencilla y combinar es
relativamente compleja. No siempre ser a as. Otro algoritmo de ordenaci on que sigue la
estrategia divide y vencer as y que estudiaremos m as adelante, quicksort, presenta un
mecanismo de divisi on complicado y una combinaci on de resultados trivial.
5.2.2. Correcci on
La correcci on de los algoritmos divide y vencer as se demuestra, generalmente, por in-
ducci on. La base de inducci on se apoya en demostrar que la soluci on trivial resuelve
efectivamente los problemas simples. El paso de inducci on consiste, b asicamente, en de-
mostrar que divide parte el problema en subproblemas de un modo tal que combine pueda
proporcionar una soluci on correcta combinando las soluciones de los subproblemas, que
se suponen correctas.

Ese es el procedimiento que seguimos, por ejemplo, al demostrar
la correcci on del algoritmo de ordenaci on por fusi on en la secci on 5.1.2.
5.2.3. Complejidad temporal
El estudio de la complejidad temporal de un algoritmo que instancia el esquema presen-
tado depende, evidentemente, de factores como el coste de cada una de sus funciones y
el tama no y n umero de los (sub)problemas en que dividimos cada (sub)problema.
Si hemos de resolver un problema de talla n invertiremos un tiempo D(n) en divi-
dirlo en una serie de subproblemas. En principio, cada uno de los subproblemas puede
presentar una talla cualquiera (aunque, eso s, menor que n). Pongamos que la divisi on
de un problema de talla n produce a problemas de tallas respectivas n
1
, n
2
, . . . , n
a
. Una
vez se hayan solucionado estos, deberemos combinar sus soluciones. Denotemos con
C(n
1
, n
2
, . . . , n
a
) el tiempo necesario para efectuar esta operaci on. El coste del algoritmo
en funci on de n, T(n), presenta un t ermino general de esta forma:
T(n) =
(

1ia
T(n
i
)
)
+ D(n) + C(n
1
, n
2
, . . . , n
a
).
Tendremos que resolver la ecuaci on recursiva para efectuar un an alisis de compleji-
dad, cosa que no siempre resultar a sencillo. Si s olo estamos interesados en el coste tem-
poral asint otico, podemos simplicar el problema tratando de resolver ecuaciones recur-
sivas m as sencillas. Por ejemplo, como cabe esperar que T() sea una funci on mon otona
28 de septiembre de 2009 Captulo 5. Divide y vencer as 269
creciente, solemos acotar superiormente el t ermino general con
T(n) aT(m ax
1ia
n
i
) + D(n) + C(n
1
, n
2
, . . . , n
a
).
Podemos hacer algunas asunciones acerca de las diferentes funciones del esquema
para obtener una ecuaci on recursiva m as sencilla. En problemas reales es frecuente ob-
servar que las funciones auxiliares satisfacen estas propiedades:
is simple decide que un problema es simple si su talla es menor o igual que 1 y lo
hace en tiempo constante;
trivial solution resuelve un problema trivial en tiempo constante;
divide divide un problema de talla n en a subproblemas, todos de talla n/b para
b > 1, con un coste temporal D(n);
y combine combina las a soluciones a los subproblemas de talla n/b en tiempo C(n).
Si se satisfacen estas condiciones, la relaci on de recurrencia para el coste temporal se
puede expresar as:
T(n) =
{
c, si n 1;
aT(n/b) + D(n) + C(n), si n > 1.
(5.1)
El m etodo maestro
El teorema o m etodo maestro ofrece una forma directa de acotar asint oticamente mu-
chas de las ecuaciones recursivas que se dan en algoritmos divide y vencer as, como la
descrita en (5.1).

En aras de la simplicidad, nosotros trabajaremos con una versi on simplicada


del teorema maestro. El lector interesado en conocer el teorema maestro ori-
ginal (y su demostraci on), puede consultarlo en la segunda edici on de Introduction
to Algorithms, de Cormen, Leiserson, Rivest y Stein.
Teorema 5.1 La soluci on de la ecuaci on recursiva T(n) = aT(n/b) +(n
k
), donde a 1 y
b > 1, es
si a < b
k
si a = b
k
si a > b
k
O(n
k
) O(n
k
log n) O(n
log
b
a
)
Demostraci on. Supondremos que n = b
m
para alg un m entero, es decir, supondremos
que n es potencia entera de b. Tenemos entonces que (n
k
) = ((b
k
)
m
). A efectos del
an alisis asint otico, supondremos que la funci on ((b
k
)
m
) es, precisamente, (b
k
)
m
. No
hemos especicado un caso base para la recursi on, pero supondremos que este se alcanza
cuando n = 1 y que T(1) = 1. Podemos reescribir la ecuaci on recursiva as:
T(b
m
) = aT(b
m1
) + (b
k
)
m
.
270 Apuntes de Algoritmia 28 de septiembre de 2009
Dividiendo ambas partes de la igualdad por un mismo valor tenemos
T(b
m
)
a
m
=
aT(b
m1
)
a
m
+
(b
k
)
m
a
m
=
T(b
m1
)
a
m1
+
(
b
k
a
)
m
.
Y ahora desplegamos:
T(b
m
)
a
m
=
T(b
m1
)
a
m1
+
(
b
k
a
)
m
=
T(b
m2
)
a
m2
+
(
b
k
a
)
m1
+
(
b
k
a
)
m
=
T(b
m3
)
a
m3
+
(
b
k
a
)
m2
+
(
b
k
a
)
m1
+
(
b
k
a
)
m
= . . .
=
T(b
mj
)
a
mj
+

mj<im
(
b
k
a
)
i
Llegamos al caso base cuando b
mj
= 1, es decir, cuando j = m:
T(b
m
)
a
m
= T(b
0
) +

0<im
(
b
k
a
)
i
= 1 +

0<im
(
b
k
a
)
i
=

0im
(
b
k
a
)
i
.
Finalmente,
T(b
m
) = a
m

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.

Muchos algoritmos divide y vencer as hacen un uso eciente de la memoria


del ordenador. La memoria est a organizada en una jerarqua en la que cada
etapa ofrece mayor capacidad de almacenamiento y menor velocidad. La memoria
cach e, por ejemplo, es la m as r apida (suele estar integrada en el propio chip del
microprocesador), pero ofrece una capacidad en el orden de los megaoctetos. La
memoria RAM es decenas de veces m as lenta, pero permite almacenar Gigaocte-
tos de informaci on. La diferencia entre usar principalmente memoria cach e o RAM
puede ser dram atica. Dado que muchos algoritmos de divide y vencer as particio-
nan los datos del problema en bloques de memoria contigua y sin relaci on entre s,
suelen permitir un uso m as eciente de la cach e. Los algoritmos que hacen este uso
eciente de la cach e sin dedicar acciones explcitamente a la gesti on de la cach e se
conocen como algoritmos ignorantes de la cach e (en ingl es, cache-oblivious al-
gorithms).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
112 Expresa recursivamente el coste espacial y aplica el teorema maestro para dar una expre-
si on cerrada del coste espacial de mergesort e in place mergesort.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.2.5. Algunas consideraciones sobre la eciencia
Vale la pena entender el signicado de a y b en la expresi on recursiva del coste compu-
tacional. El factor a es el n umero de subproblemas en que se divide un problema y b es
28 de septiembre de 2009 Captulo 5. Divide y vencer as 273
la proporci on entre la talla del problema original y la de los subproblemas. Si a es mayor
que b, hay alg un solapamiento entre subproblemas, lo que implica un cierto grado de
repetici on de c alculos. Una clave para obtener un buen rendimiento es evitar los solapa-
mientos, con lo que a no ser a mayor que b. Otro elemento que tiene un impacto notable
sobre la eciencia es el balanceo en la talla de los subproblemas que se obtienen a par-
tir de un problema: conviene que sean de tallas similares, ya que as la profundidad del
arbol de recursi on es menor. La clave de la eciencia de la ordenaci on por fusi on radica,
precisamente, en que divide cada vector en vectores de tallas similares y sin solapamiento,
es decir, sin elementos comunes (a b).
En un orden pr actico de cosas, un inconveniente de los algoritmos divide y ven-
cer as es el tiempo de ejecuci on que conlleva efectuar las diferentes llamadas recursivas:
reserva de espacio en pila, copia de par ametros en pila, inicializaci on de variables loca-
les, etc. Este coste es importante si consideramos los problemas de talla peque na, pues las
llamadas recursivas pueden suponer tanto o m as tiempo de ejecuci on que el del mismo
c alculo indicado en su cuerpo. Una forma de paliar este inconveniente es dejar de efec-
tuar llamadas recursivas cuando la talla del problema sea menor o igual que cierto valor
n
0
, es decir, considerar que ciertos problemas difciles son en realidad simples y que
pueden resolverse de forma directa. El valor n
0
recibe el nombre de umbral de recursi on,
pues ja el punto a partir del cual se considera conveniente resolver un problema direc-
tamente, sin recursi on. Las versiones con umbral de recursi on encajan perfectamente en
los esquemas divide y vencer as presentados: basta con interpretar adecuadamente los
m etodos is simple y trivial solution.
En el caso de la ordenaci on por fusi on podemos efectuar una ordenaci on directa cuan-
do la talla del vector sea realmente peque na. Ordenar un vector de tres elementos, por
ejemplo, puede efectuarse directamente con unas pocas comparaciones, ahorrando as la
realizaci on de tres llamadas recursivas y dos llamadas a merge. O podemos usar un um-
bral de recursi on mayor y apoyarnos en un m etodo de ordenaci on que resulte r apido
para vectores de talla peque na, como la ordenaci on por inserci on:
algoritmia/problems/sorting.py
class ThresholdedInPlaceMergeSorter(InPlaceMergeSorter):
def init (self , threshold: "int"):
self .threshold = threshold
def mergesort(self , v: "sequence<T>", p: "int", q: "int", c: "sequence<T>"):
if q-p > self .threshold:
self . mergesort(v, p, (p+q)//2, c)
self . mergesort(v, (p+q)//2, q, c)
self .merge(v, p, q, c)
elif q-p <= self .threshold:
for i in range(p, q): # Insertion Sort
x = v[i]
j = i-1
while j >= 0 and x < v[j]:
v[j+1] = v[j]
j -= 1
274 Apuntes de Algoritmia 28 de septiembre de 2009
v[j+1] = x
demos/divideandconquer/thresholdedmergesort.py
from algoritmia.problems.sorting import ThresholdedInPlaceMergeSorter
a = [11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37]
ThresholdedInPlaceMergeSorter(5).sort(a)
print(Ordenado:, a)
Ordenado: [0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
Un umbral de recursi on a un mayor eliminar a m as llamadas recursivas, pero a costa,
probablemente, de complicar (a un m as) el c odigo si seguimos la misma aproximaci on
directa del ejemplo. Una alternativa consiste en ordenar los vectores de talla inferior
o igual a cierto valor con un algoritmo de ordenaci on como el de inserci on o selecci on.
Se trata, es cierto, de algoritmos cuadr aticos, pero que se ejecutan sobre vectores de talla
acotada por una constante y, en la pr actica, pueden proporcionar una aceleraci on notable
de la velocidad por ahorrar gran n umero de llamadas recursivas. N otese, en cualquier
caso, que la mejora depende de un par ametro, el umbral de recursi on, cuyo valor optimo
debe determinarse experimentalmente.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
113 Determina experimentalmente el valor de threshold que proporciona la mayor ganancia de
velocidad en promedio para vectores desordenados cuando se usa la ordenaci on por inserci on
para vectores de talla peque na.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Los algoritmos basados en el esquema divide y vencer as suelen ser f acil-


mente paralelizables en sistemas multiprocesador con memoria compartida.
Cada una de las llamadas recursivas puede, en principio, asignarse a un procesador
distinto y resolver su parte del problema en paralelo.
5.2.6. Algunas consideraciones sobre el esquema y su uso en el
dise no de un algoritmo
El esquema algortmico proporciona un modelo para desarrollar un m etodo resolutivo
para un problema, pero requiere un esfuerzo de interpretaci on y adaptaci on a su ambito
particular que no siempre resulta sencillo. Raramente una instanciaci on inmediata del
esquema conducir a a una soluci on eciente. Suele necesitarse efectuar una serie de re-
namientos que vayan explotando las caractersticas concretas del problema particular
para simplicar el algoritmo resultante. En ciertos casos se ha de ser muy cuidadoso en
la selecci on de estructuras de datos que ayuden a alcanzar una mayor eciencia compu-
tacional.
El esquema es un apoyo a la hora de dise nar un algoritmo porque ofrece un marco
conceptual, una gua de dise no. Si intentamos abordar un problema mediante la estra-
tegia divide y vencer as, por ejemplo, el esquema nos indica que hemos de pensar en
un m etodo de divisi on de una instancia del problema en otras de menor talla, as como
28 de septiembre de 2009 Captulo 5. Divide y vencer as 275
en un m etodo que combine las soluciones de estas instancias para formar la soluci on del
problema general. El desarrollo que hemos efectuado para llegar al algoritmo en su ver-
si on nal partiendo del esquema no siempre es necesario. Muchos de los renamientos
suelen derivarse de observaciones de f acil constataci on y es frecuente proporcionar direc-
tamente el algoritmo nal. Eso s, entender que este es, efectivamente, una instanciaci on
del esquema puede resultar algo m as difcil cuando no se presentan los renamientos
sucesivos y se muestra s olo el algoritmo nal.
5.3. Un caso especial: reduce y vencer as
Se considera que ciertos algoritmos siguen la estrategia divide y vencer as pese a no
presentar funci on de combinaci on de soluciones. En lugar de la combinaci on de resul-
tados, se selecciona un problema concreto, de menor talla que el original, y se aplica
recursi on s olo sobre el. El siguiente esquema recoge esa idea:
algoritmia/schemes/decreaseandconquer.py
class DecreaseAndConquerProblem(metaclass=ABCMeta):
@abstractmethod
def is simple(self ):
"""Devuelve cierto sii es un problema que admite resoluci on directa."""
raise NotImplementedError
@abstractmethod
def trivial solution(self ):
"""Resuelve directamente el problema si es simple."""
raise NotImplementedError
@abstractmethod
def decrease(self ):
"""Selecciona un subproblema cuya soluci on conduce a la de este problema."""
raise NotImplementedError
def process(self , s):
"""Recibe la soluci on de un subproblema suyo para obtener su propia soluci on."""
return s
class DecreaseAndConquerSolver:
def solve(self , problem):
if problem.is simple():
return problem.trivial solution()
else:
return problem.process(self .solve(problem.decrease()))
En el esquema, el m etodo divide ha sido reemplazado por decrease, que devuelve uno
s olo de los subproblemas a los que puede dar lugar la divisi on del problema original.
Adem as, combine ha sido sustituido por process. N otese que en el esquema s olo se resuel-
276 Apuntes de Algoritmia 28 de septiembre de 2009
ve recursivamente el problema seleccionado por decrease, por lo que parece apropiado
denominar a este esquema reduce y vencer as.
Un ejemplo paradigm atico del esquema reduce y vencer as es la b usqueda binaria
o dicot omica, que presentamos en el siguiente apartado. Tambi en el c alculo eciente de
una potencia se puede abordar con este esquema.
5.3.1. Coste computacional
La ecuaci on recursiva con la que se expresa el coste temporal ser a, en este caso, de la
forma
T(n) =
{
c, si n 1;
T(n/b) + D(n) + C(n), si n > 1.
(5.2)
La funci on D(n) recoge el coste de la divisi on del problema original y de la selecci on
de uno de los subproblemas resultantes y C(n) es el coste del m etodo process. Se trata,
evidentemente, de un caso particular de la ecuaci on recursiva (5.1) y podemos abordar
su resoluci on mediante el teorema maestro (en este caso el par ametro a vale 1).
En cuanto al coste espacial, cabe indicar que vendr a determinado por la memoria
ocupada en las tramas de activaci on en la pila de llamadas a funci on. El n umero de
dichas tramas es del orden de log
b
n.
5.3.2. Eliminaci on de la recursi on por cola
En ocasiones podemos reducir signicativamente la complejidad espacial de un algo-
ritmo recursivo si lo transformamos en otro iterativo equivalente, ya que se elimina la
necesidad de la gesti on de tramas de activaci on propias de la recursi on. Esto resulta
particularmente f acil cuando la ultima acci on del caso general en la recursi on es, precisa-
mente, una llamada recursiva. Este tipo de recursi on se denomina recursi on por cola.
El m etodo solve de DecreaseAndConquer presenta recursi on por cola en un caso par-
ticular: cuando el m etodo process se limita a devolver la soluci on del subproblema se-
leccionado (que es lo que hace la implementaci on por defecto del m etodo en cuesti on).
Esto ocurre, por ejemplo, al instanciar el esquema para efectuar b usqueda binaria, como
veremos en la siguiente secci on. Cuando tal cosa sucede, podemos reescribir el m etodo
solve as:
algoritmia/schemes/decreaseandconquer.py
class TailRecursiveDecreaseAndConquerSolver:
def solve(self , problem):
if problem.is simple():
return problem.trivial solution()
else:
return self .solve(problem.decrease())
N otese que ya no hay referencia alguna a process y que la ultima acci on del m eto-
do solve o bien naliza la recursi on o bien es una llamada recursiva al m etodo solve.
Qu e c alculo acaba efectuando una recursi on como esta?
28 de septiembre de 2009 Captulo 5. Divide y vencer as 277
Vamos paso a paso: selecciona un subproblema del problema original (reduce el
original) y considera si es de soluci on trivial. Si no lo es, repite el proceso de reducci on
sobre el subproblema, generando uno de menor talla. Cuando llega, nalmente, a un
subproblema trivial, lo resuelve directamente y devuelve su soluci on. Este valor lo recibe
la ultima invocaci on de solve, que se limita a pasarlo a la pen ultima invocaci on de solve,
que hace lo propio y lo pasa a la antepen ultima. . . y as hasta llegar a la primera invoca-
ci on de solve, que acaba por devolver dicho valor como soluci on al problema original. En
denitiva, la soluci on del problema trivial es tambi en la soluci on del problema original.
El m etodo puede expresarse como un proceso de dos etapas:
b usqueda, por reducci on reiterada, del subproblema trivial cuya soluci on es tam-
bi en soluci on del original,
y devoluci on de dicha soluci on.
Este procedimiento iterativo efect ua ese mismo c alculo:
algoritmia/schemes/decreaseandconquer.py
class IterativeDecreaseAndConquerSolver:
def solve(self , problem):
while not problem.is simple():
problem = problem.decrease()
return problem.trivial solution()
La versi on iterativa no supone una mejora en el tiempo de ejecuci on (en t erminos
asint oticos), pero es frecuente que s suponga una mejora en el espacio necesario debido
a que no se efect uan llamadas recursivas, con lo que no son necesarios sus registros de
activaci on en la pila de llamadas a funci on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
114 Podemos aplicar el m etodo de eliminaci on de recursi on por cola a mergesort? Por qu e?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.4. B usqueda binaria o dicot omica
El problema de la b usqueda de un elemento en un vector ordenado puede enunciarse as:
dado un vector a con n elementos ordenados de menor a mayor y un valor v, deseamos
saber si alg un elemento tiene valor v y, si es as, conocer su ndice.
Cuando nos proporcionan un vector en el que efectuar la b usqueda podemos dividir
este en tres fragmentos: uno con los (n 1)/2 primeros elementos, otro con el elemen-
to central (el que tiene ndice n/2) y otro con los ultimos n/2 elementos. Para selec-
cionar cu al de los tres problemas conviene seleccionar procedemos del siguiente modo:
comparamos el valor buscado con el elemento central, si el valor buscado es menor que
dicho elemento, escogemos la mitad inicial del vector, si es mayor, la mitad nal y, en
otro caso, el vector formado unicamente por el elemento central.
Podemos expresar esta idea mediante una instanciaci on del esquema. Usaremos el
valor especial None para indicar que v no se encuentra en el vector y, en caso contrario,
278 Apuntes de Algoritmia 28 de septiembre de 2009
un entero para representar el ndice que ocupa v en el vector. Una instancia del problema
vendr a especicada con una cuadrupla (a, i, k, v) donde:
a es el vector en el que efectuamos la b usqueda,
i y k son ndices que indican que la b usqueda se centra en el corte a[i:k],
y v es el valor buscado.
algoritmia/problems/searching.py
from algoritmia.schemes.decreaseandconquer import DecreaseAndConquerProblem
...
class BinarySearchProblem(DecreaseAndConquerProblem):
def init (self , a: "sequence<T>", v: "T", i: "int"=0, k: "int or None"=None):
if k == None: k = len(a)
self .a, self .v, self .i, self .k = a, v, i, k
def is simple(self ) -> "bool":
return self .k - self .i <= 1
def trivial solution(self ) -> "int or None":
if self .i == self .k or self .v != self .a[self .i]: return None
return self .i
def decrease(self ) -> "BinarySearchProblem":
j = (self .i + self .k) // 2
if self .v < self .a[j]: return BinarySearchProblem(self .a, self .v, self .i, j)
elif self .v == self .a[j]: return BinarySearchProblem(self .a, self .v, j, j+1)
else: return BinarySearchProblem(self .a, self .v, j+1, self .k)
demos/divideandconquer/binsearchfromscheme.py
from algoritmia.schemes.decreaseandconquer import DecreaseAndConquerSolver
from algoritmia.problems.searching import BinarySearchProblem
a = [2, 3, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
for v in 1, 10, 30, 100:
print(Valor {} en {}..format(
v, DecreaseAndConquerSolver().solve(BinarySearchProblem(a, v))))
Valor 1 en None.
Valor 10 en None.
Valor 30 en 10.
Valor 100 en None.
La funci on de divisi on del problema de talla n proporciona un unico problema de
talla, a lo sumo, n/2. El coste del algoritmo es, por tanto, O(lg n). El coste espacial es,
tambi en, O(lg n): el arbol de llamadas recursivas presenta, a lo sumo, 1 +lg n niveles.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 279
No hemos proporcionado una implementaci on para el m etodo process de BinarySear-
chProblem porque heredamos la que proporciona el esquema y se limita a devolver la
soluci on que recibe como argumento.
Instanciar el esquema literalmente resulta un tanto engorroso y diculta, por su rigi-
dez, la introducci on de renamientos en el algoritmo. Esta otra versi on, que no sigue el
esquema al pie de la letra efect ua el mismo c alculo e introduce algunas mejoras de ndole
pr actica:
class RecursiveBinarySearcher(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 = (i+k)/2
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
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
115 La b usqueda ternaria se basa en la divisi on del vector en cinco fragmentos en lugar de en
tres: uno con (aproximadamente) el primer tercio del vector, otro con el elemento que le sigue,
otro con el (casi) tercio central, otro con el elemento que le sigue y otro con el resto de elemen-
tos. Implementa un algoritmo de b usqueda ternaria a partir del esquema y, a continuaci on, una
versi on que siga los principios de dise no expresados en la funci on binary search. Es mejor la
b usqueda ternaria que la binaria?
116 Qu e coste presentara, en el peor caso, una versi on de la b usqueda binaria que dividiera
un vector de talla n en un vector de talla 2n/3 y otro con los restantes elementos?
117 Sea M una matriz cuadrada de enteros de n n. Se observa M[i][j] < M[i][j+1] para toda
la i, 0 i < n, y M[i][n-1] < M[i+1][0] para cualquier par de las consecutivas. La siguiente
matriz cumple esta condici on:

30 14 2 0
2 3 10 12
15 16 22 23
25 30 38 45

Dado un elemento x de la matriz, escribe un algoritmo reduce y vencer as que devuelva, en


tiempo O(lg n), el n umero de la y el n umero de columna en las que se encuentra este valor.
118 Sea a un vector de enteros diferentes (negativos y/o positivos) dispuestos en orden cre-
ciente. Dise na un algoritmo reduce y vencer as que encuentre un i tal que 0 i < n y a[i] = i
siempre que este i exista. El algoritmo debe tener un coste O(lg n).
119 Dada una secuencia innita y ordenada en la que s olo sus n primeros t erminos son distintos
de None averigua, en tiempo O(lg n), si contiene un determinado valor x. (Es posible preguntar
por el valor del elemento i- esimo y obtener una respuesta en tiempo O(1).)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
280 Apuntes de Algoritmia 28 de septiembre de 2009
5.4.1. Una versi on con umbral de recursi on
Cuando la talla del vector es menor que cierto valor, podemos efectuar una b usqueda
lineal y evitar as realizar algunas llamadas recursivas:
algoritmia/problems/searching.py
class ThresholdedBinarySearcher(ISearcher):
def init (self , threshold: "int"):
self .threshold = threshold
def index of (self , a: "sequence<T>", v: "T") -> "int or None":
return self . index of (a, v, 0, len(a))
def index of (self , a: "sequence<T>", v: "T", i: "int", k: "int") -> "int or None":
if k-i <= self .threshold:
for j in range(i, k):
if v == a[j]: return j
return None
else:
j = (i + k) // 2
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)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
120 Determina experimentalmente un valor optimo para el umbral de recursi on cuando usa-
mos un m etodo de b usqueda secuencial si el vector no es lo bastante grande.
121 Compara experimentalmente el tiempo de ejecuci on del m etodo iterativo con el mejor um-
bral de recursi on encontrado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.4.2. Una versi on iterativa de la b usqueda dicot omica
El algoritmo de b usqueda dicot omica presenta recursi on por cola, as que podemos pro-
ceder a su eliminaci on y obtener la soluci on iterativamente:
demos/divideandconquer/iterativebinsearch.py
from algoritmia.schemes.decreaseandconquer import IterativeDecreaseAndConquerSolver
from algoritmia.problems.searching import BinarySearchProblem
a = [2, 3, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
for v in 1, 10, 30, 100:
problem = BinarySearchProblem(a, v)
print(Valor {} en {}..format(v, IterativeDecreaseAndConquerSolver().solve(problem)))
Valor 1 en None.
Valor 10 en None.
Valor 30 en 10.
Valor 100 en None.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 281
El coste temporal de este algoritmo iterativo es O(lg n), como en la versi on recursiva,
pero el coste espacial es O(1).
Resulta ilustrativo el algoritmo resultante de aplicar la transformaci on recursivo-
iterativa que elimina la recursi on por cola en la versi on que no se deriva directamente
del esquema:
algoritmia/problems/searching.py
class IterativeBinarySearcher(ISearcher):
def index of (self , a: "sequence<T>", v: "T") -> "int or None":
i, k = 0, len(a)
while k-i > 1 and a[(i+k)//2] != v:
j = (i+k) // 2
if v < a[j]: k = j
if v > a[j]: i = j+1
return (i+k)//2 if k-i >= 1 and a[(i+k)//2] == v else None
5.5. Potencia entera de un n umero
Podemos calcular el valor de a
n
, para a real y n entero positivo, efectuando n 1 multi-
plicaciones mediante un algoritmo trivial. Este algoritmo es O(n).
Divide y vencer as permite reducir notablemente el coste bas andose en que a
n
=
a
n/2
a
n/2
. He aqu una primera versi on que explota esta propiedad:
algoritmia/problems/power.py
def power1(a, n):
if n == 0:
return 1
elif n == 1:
return a
elif n % 2 == 0:
return power1(a, n//2) * power1(a, n//2)
else:
return power1(a, n//2) * power1(a, n//2+1)
demos/divideandconquer/power.py
from algoritmia.problems.power import power1
print(2**11 =, power1(2, 11))
2**11 = 2048
Podemos expresar el coste en funci on del valor del exponente n con esta relaci on
recursiva (que ilustramos con un arbol de recursi on en la gura 5.7):
T(n) =
{
c
1
, si n = 0 o n = 1;
T(n/2) + T(n/2) + c
2
, si n > 1.
282 Apuntes de Algoritmia 28 de septiembre de 2009
El coste temporal de este algoritmo es (n), igual que el del algoritmo trivial! No hemos
ganado nada.
Figura 5.7:

Arbol
de recursi on de una
llamada a power
(primera versi on
del algoritmo) pa-
ra un valor de n
que es potencia de
2. Hay 1 + lg n ni-
veles y en el ni-
vel k se efect uan 2
k
operaciones.
1 1 1 1 1 1 1 1
n/4 n/4 n/4 n/4
n/2 n/2
n
1 = 2
0
2 = 2
1
4 = 2
2
n = 2
lg n
2n 1
1
+
l
g
n
n
i
v
e
l
e
s
Es f acil optimizar el algoritmo para aquellos casos en que n es par: se est an efectuan-
do dos llamadas a power(a, n/2), cuando una es suciente si sustituimos la expresi on
power(n/2)*power(n/2) por power(n/2)**2. En el caso de los valores impares de n no
hay llamadas repetidas, pero es f acil dise nar una versi on que efect ua una sola llamada
recursiva si tenemos en cuenta que, para n impar, a
n
= a (a
n/2
)
2
:
algoritmia/problems/power.py
def power(a, n):
if n == 0:
return 1
elif n == 1:
return a
elif n % 2 == 0:
return power(a, n//2)**2
else:
return a * power(a, n//2)**2
El coste temporal puede expresarse ahora con esta recursi on:
T(n) =
{
c
0
, si n 1;
T(n/2) + c
1
, si n > 1.
El coste temporal es, por tanto, (lg n). La gura 5.8 muestra c omo el arbol de llamadas
recursivas se ha simplicado notablemente en esta nueva versi on del algoritmo.

La idea de no repetir llamadas recursivas porque producir an resultados


id enticos es uno de los principios fundamentales de otra importante t ecni-
ca de dise no de algoritmos: la programaci on din amica.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
122 Identica los elementos del esquema reduce y vencer as en la funci on power.
123 Qu e coste espacial presenta power?
28 de septiembre de 2009 Captulo 5. Divide y vencer as 283
1
n/4
n/2
n 1
1
1
1
1 +lg n
1
+
l
g
n
n
i
v
e
l
e
s
Figura 5.8:

Arbol de recursi on de una llamada a power para un valor de n que es
potencia de 2. Hay 1 + lg n niveles y en cada nivel se ejecuta un solo paso (sin
tener en cuenta, claro est a, la correspondiente llamada recursiva).
124 Dise na una versi on de power que admita valores enteros negativos de n.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.6. Mnimo y m aximo de un vector
Deseamos buscar el mnimo y el m aximo de un vector no ordenado de n enteros. Una
soluci on inmediata para el problema sera:
algoritmia/problems/minmax.py
class DirectMinMaxFinder:
def min max(self , a):
minimum = maximum = a[0]
for i in range(1,len(a)):
if a[i] < minimum: minimum = a[i]
elif a[i] > maximum: maximum = a[i]
return (minimum, maximum)
El n umero de comparaciones efectuadas (entre elementos del vector) est a compren-
dido entre n 1 y 2n 2. Podemos reducir el n umero de comparaciones si seguimos
una estrategia divide y vencer as. Podemos partir el vector en dos fragmentos de talla
similar y buscar por separado los elementos menor y mayor de cada fragmento. El me-
nor elemento del vector es el menor de entre los menores de cada fragmento, y el mayor
elemento del vector es el mayor de entre los mayores de cada fragmento:
algoritmia/problems/minmax.py
class MinMaxFinder:
def min max(self , a):
return self . min max(a, 0, len(a))
def min max(self , a, i, k):
if k - i == 1:
return (a[i], a[i])
elif k - i == 2:
return (a[i], a[i+1]) if a[i+1] > a[i] else (a[i+1], a[i])
else:
284 Apuntes de Algoritmia 28 de septiembre de 2009
j = (i + k) // 2
(min1, max1) = self . min max(a, i, j)
(min2, max2) = self . min max(a, j, k)
return (min1 if min1 < min2 else min2, max1 if max1 > max2 else max2)
demos/divideandconquer/minmax.py
from algoritmia.problems.minmax import MinMaxFinder
a = [10, 8, 7, 5, 4, 3, 5, 9, 20]
print(Mnimo y maximo de {}: {}.format(a, MinMaxFinder().min max(a)))
Mnimo y maximo de [10, 8, 7, 5, 4, 3, 5, 9, 20]: (3, 20)
Cu antas comparaciones efect ua el nuevo algoritmo? La gura 5.9 ayuda a realizar el
an alisis. Si consideramos que n es potencia de 2, tenemos lg n niveles en el arbol de recur-
si on. En los lg n 1 primeros niveles se efect uan dos comparaciones por cada llamada
recursiva. Cuando los vectores tienen talla 2 (que corresponde al nivel m as bajo en el
arbol de llamadas recursivas en este caso particular), una sola comparaci on es suciente
para determinar los valores mnimo y m aximo. El ultimo nivel aporta, pues, la mitad de
comparaciones de las que le correspondera en principio: una sola comparaci on permite
elegir a la vez m aximo y mnimo. Si la talla del vector no fuera potencia de 2 podra lle-
garse a solicitar recursivamente la b usqueda en vectores de talla unitaria, pero en ellos
no es necesario efectuar comparaci on alguna.
Figura 5.9:

Arbol
de recursi on de
una llamada a
min max (prime-
ra versi on del al-
goritmo) para un
valor de n que es
potencia de 2.
2 2 2 2 2 2 2 2
n/4 n/4 n/4 n/4
n/2 n/2
n
Comparaciones
2 = 2
1
4 = 2
2
8 = 2
3
n/2 = 2
lg n
/2
n 2 + n/2
l
g
n
n
i
v
e
l
e
s
El n umero de comparaciones necesarias para un vector de talla n, que expresaremos
con C(n), es:
C(n) =

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.

La reducci on del n umero de cierto tipo de operaciones puede resultar in-


teresante al dise nar circuitera que implemente un procedimiento de c alculo,
pues reduce el n umero de componentes que deben integrarse.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
125 Qu e n umero de comparaciones es necesario para obtener unicamente el mnimo aplican-
do una estrategia similar a la de la funci on min max? Resulta conveniente el nuevo algoritmo
frente al c alculo del mnimo mediante un recorrido secuencial de los elementos del vector?
126 Los algoritmos presentados efect uan numerosas comparaciones de igualdad para decidir
si el tama no del vector (la diferencia entre k e i) es tan peque no que el problema se puede resolver
de forma directa. Muchas veces falla la comparaci on de igualdad de la lnea 3 para, a continua-
ci on, fallar tambi en la de la lnea 5. Puedes modicar el orden de las comparaciones de igualdad
o sustituirlas por otro tipo de comparaciones para reducir el n umero total de operaciones?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.7. Mnimo de un vector convexo
Una funci on f es convexa si es decreciente hasta cierto valor y creciente a partir de el.
Un vector a es convexo si existe un ndice j tal que a[i-1] a[i] para todo i j y
a[k] a[k+1] para todo k j. La gura 5.10 muestra gr acamente un vector convexo.
Evidentemente, el elemento a[j] es el menor elemento del vector. Nuestro prop osito es
dise nar un algoritmo eciente para la b usqueda del menor valor de un vector convexo.
286 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 5.10: Representaci on gr aca del vector convexo
[9.5, 8, 6, 5, 4.5, 4, 3.5, 3.25, 5.75, 6.5, 8.25].
1 3 5 7 9
0
2
4
6
8
10
0 2 4 6 8 10
Una aproximaci on directa, no basada en divide y vencer as, consiste en recorrer
ntegramente el vector a y buscar el primer elemento a[j] tal que a[j] < a[j+1], pues es
el mnimo. El coste temporal de esta b usqueda es (1) y O(n).
Seguir la estrategia reduce y vencer as nos permite dise nar una soluci on m as e-
ciente. La idea consiste en estudiar la tendencia del vector en su zona central. Si es cre-
ciente, se explora recursivamente la primera mitad del vector; si es decreciente, s olo se
efect ua recursi on sobre la segunda mitad. La gura 5.11 muestra una traza del algoritmo
apuntado sobre el vector de la gura 5.10.
Figura 5.11: B usqueda del mnimo
en un vector convexo. En cada ins-
tante se considera el par de puntos
centrales y se estudia la tendencia
(ascendente o descendente) de es-
tos puntos. La zona gris no puede
contener el mnimo en ning un caso.
El algoritmo naliza cuando que-
dan uno o dos puntos por explorar.
1 3 5 7 9
0
2
4
6
8
10
0 2 4 6 8 10 1 3 5 7 9
0
2
4
6
8
10
0 2 4 6 8 10
1 3 5 7 9
0
2
4
6
8
10
0 2 4 6 8 10 1 3 5 7 9
0
2
4
6
8
10
0 2 4 6 8 10
He aqu una primera versi on del programa:
algoritmia/problems/convexmin.py
class ConvexMin1:
def min(self , a: "convex sequence<T>") -> "T":
if len(a) == 1:
return a[0]
elif len(a) == 2:
return min(a[0], a[1])
else:
28 de septiembre de 2009 Captulo 5. Divide y vencer as 287
j = len(a) // 2
if a[j-1] < a[j]:
return self .min(a[:j])
else:
return self .min(a[j:])
demos/divideandconquer/convexmin.py
from algoritmia.problems.convexmin import ConvexMin1
for v in [9.5, 8, 6, 5, 4.5, 3.5, 3.25, 5.75, 6.5, 8.25], [10, 4, 3, 0], [0, 2, 10, 60]:
print(Mnimo en {}: {}.format(v, ConvexMin1().min(v)))
Mnimo en [9.5, 8, 6, 5, 4.5, 3.5, 3.25, 5.75, 6.5, 8.25]: 3.25
Mnimo en [10, 4, 3, 0]: 0
Mnimo en [0, 2, 10, 60]: 0
Esta otra versi on, aunque m as complicada, ahorra la reserva de memoria propia de la
divisi on del vector en fragmentos. Las variables i y k mantienen los lmites entre los que
cabe encontrar el mnimo (regi on blanca en la gura 5.11):
algoritmia/problems/convexmin.py
class ConvexMin:
def min(self , a: "convex sequence<T>") -> "T":
return self . min(a, 0, len(a))
def min(self , a: "convex sequence<T>", i: "int", k: "int") -> "T":
if k - i == 1:
return a[i]
elif k - i == 2:
return min(a[i], a[i+1])
else:
j = (i + k) // 2
if a[j-1] < a[j]:
return self . min(a, i, j)
else:
return self . min(a, j, k)
El coste temporal del algoritmo es (lg n). El an alisis es sencillo si consideramos la
similitud del proceso con el de b usqueda binaria.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
127 Qu e coste espacial presenta el m etodo ConvexMin.min?
128 Puedes modicar el algoritmo para que su tiempo de ejecuci on sea (1) y O(lg n) a nadien-
do una condici on a la estructura if-else m as interior de ConvexMin.min?
129 La t ecnica presentada para vectores puede extenderse a la b usqueda del mnimo de una
funci on f : R R en un intervalo [a, b] en el que f es convexa. Dise na un algoritmo que encuen-
tre dicho mnimo siguiendo una estrategia similar a la de la b usqueda del mnimo de un vector
convexo. (No est a claro qu e es la talla en un problema planteado sobre un dominio continuo:
depende de la precisi on con que deseemos expresar el resultado.)
288 Apuntes de Algoritmia 28 de septiembre de 2009
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Este algoritmo permite eliminar la recursi on por cola para mejorar su coste espacial:
algoritmia/problems/convexmin.py
class IterativeConvexMin:
def min(self , a: "convex sequence of values"):
i, k = 0, len(a)
while k-i > 2:
j = (i + k) // 2
if a[j-1] < a[j]:
k = j # No hace falta modicar el valor de i.
else:
i = j # No hace falta modicar el valor de k.
return a[i] if k-i == 1 else min(a[i], a[i+1])
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
130 Compara el coste espacial de IterativeConvexMin.min con el de ConvexMin.min.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.8. El algoritmo de ordenaci on quicksort
Quicksort (que se puede traducir como ordenaci on r apida) es uno de los algoritmos de
ordenaci on m as r apidos y utilizados. Sigue una aproximaci on divide y vencer as pero,
curiosamente, no divide una instancia del problema en dos suproblemas de tallas simila-
res. Su complejidad temporal para el peor de los casos es, como veremos, cuadr atica con
la talla del vector. Pese a ello, su comportamiento es tan bueno en la pr actica que es el
algoritmo escogido habitualmente para ordenar vectores.

Este algoritmo fue ideado por C. A. R. Hoare y publicado en el artculo titu-


lado Quicksort (Computer Journal, 5 (1), pp. 1015, 1962). Lo invent o por
un golpe de suerte. En los a nos 60, la ordenaci on era uno de los problemas a los
que se destinaban m as recursos de computaci on. Un m etodo de ordenaci on r apido
supona, pues, un enorme ahorro de recursos y, en consecuencia, econ omico. Hoa-
re dijo en 1996: He sido muy afortunado. Qu e estupenda forma de empezar una
carrera en la inform atica: descubriendo un nuevo algoritmo de ordenaci on!
La idea en que se basa quicksort es sencilla: se considera un elemento del vector al que
denominamos pivote y se crean dos vectores, uno con todos los elementos de valor menor
que el y otro con todos los elementos de valor mayor o igual (sin incluir al pivote). Esta
operaci on se denomina partici on. El algoritmo se aplica recursivamente sobre cada
uno de los nuevos vectores. El resultado de la primera llamada se concatena al pivote y
al resultado de la segunda llamada. De ese modo se obtiene un vector completamente
ordenado. El proceso se muestra gr acamente en al gura 5.12.
Aqu tienes una primera implementaci on que reeja directamente esta idea y que
toma como pivote al primer elemento del vector:
28 de septiembre de 2009 Captulo 5. Divide y vencer as 289
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
3
0
1
1
0
2
4
3
21
0
98
1
12
2
82
3
29
4
30
5
11
6
18
7
43
8
75
9
37
10
11
0
0
1
1
3
2
4
3
11
0
12
1
18
2
21
3
29
4
30
5
37
6
43
7
75
8
82
9
98
10
11
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
Dividir el problema separando menores
que el pivote y mayores o iguales que el pivote,
resolver independientemente cada problema
y combinar ambas soluciones.
Figura 5.12: Principio de funcionamiento del algoritmo quicksort.
algoritmia/problems/sorting.py
class BasicQuickSorter(ISorter):
def sorted(self , a: "sequence<T>") -> "sorted iterable<T>":
if len(a) <= 1:
return a
else:
pivot = a[0]
left = [x for x in a if x < pivot]
right = [x for x in a[1:] if x >= pivot]
return self .sorted(left) + [pivot] + self .sorted(right)
demos/divideandconquer/quicksort.py
from algoritmia.problems.sorting import BasicQuickSorter
a = [11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37]
print(Ordenado:, BasicQuickSorter().sorted(a))
Ordenado: [0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
Aunque en este algoritmo se escoge siempre como pivote al primer elemento del
vector, esto no tiene porqu e ser as. De hecho, hay razones para escoger otros pivotes
(posiblemente) m as convenientes y m as adelante consideraremos otros elementos como
pivote.
En la gura 5.13 se muestra el proceso completo seguido al ordenar el vector del
ejemplo con BasicQuickSorter.sorted.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
131 Realiza una traza de BasicQuickSorter.sorted sobre estos vectores:
a) [21, 27, 17, 5, 20, 8, 7, 4, 19, 2, 6, 29]
290 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 5.13: Traza
de ejecuci on del al-
goritmo de ordena-
ci on BasicQuick-
Sorter.sorted.
11
0
21
1
3
2
1
3
98
4
0
5
12
6
82
7
29
8
30
9
11
10
18
11
43
12
4
13
75
14
37
15
3
0
1
1
0
2
4
3
11 21
0
98
1
12
2
82
3
29
4
30
5
11
6
18
7
43
8
75
9
37
10
1
0
0
1
3 4
0
12
0
11
1
18
2
21 98
0
82
1
29
2
30
3
43
4
75
5
37
6
0
0
1 11
0
12 18
0
82
0
29
1
30
2
43
3
75
4
37
5
98
29
0
30
1
43
2
75
3
37
4
82
29 30
0
43
1
75
2
37
3
30 43
0
75
1
37
2
37
0
43 75
0
D
i
v
i
d
i
r
C
o
m
b
i
n
a
r
37
0
43
1
75
2
30
0
37
1
43
2
75
3
29
0
30
1
37
2
43
3
75
4
29
0
30
1
37
2
43
3
75
4
82
5
29
0
30
1
37
2
43
3
75
4
82
5
98
6
11
0
12
1
18
2
0
0
1
1
11
0
12
1
18
2
21
3
29
4
30
5
37
6
43
7
75
8
82
9
98
10
0
0
1
1
3
2
4
3
0
0
1
1
3
2
4
3
11
4
11
5
12
6
18
7
21
8
29
9
30
10
37
11
43
12
75
13
82
14
98
15
28 de septiembre de 2009 Captulo 5. Divide y vencer as 291
b) [23, 43, 11, 5, 9, 22, 15, 17, 5, 32, 29, 30, 12, 23, 0]
Debes indicar el valor de pivot, left y right tras cada proceso de partici on.
132 Realiza una traza de BasicQuickSorter.sorted sobre [45, 40, 38, 33, 31, 22, 10, 5, 2, 1].
Qu e ocurre con el coste temporal cuando el vector est a ordenado, como en este caso, de forma
decreciente? Est a en el caso mejor o peor?
133 Realiza una traza de BasicQuickSorter.sorted sobre [1, 2, 5, 10, 22, 31, 33, 38, 40, 45].
Qu e ocurre con el coste temporal cuando el vector est a ordenado, como en este caso, de forma
creciente? Est a en el caso mejor o peor?
134 Realiza una traza de BasicQuickSorter.sorted sobre [1, 1, 1, 1, 1, 1, 1, 1, 1]. Qu e ocu-
rre con el coste temporal cuando el vector s olo contiene ejemplares del mismo valor? Est a en el
caso mejor o peor?
135 Dise na una versi on de BasicQuickSorter.sorted que acepte, como par ametro auxiliar, una
funci on de comparaci on que al recibir dos valores a y b devuelva 1 si a < b, 0 si a = b y 1 si
a > b. Utiliza la funci on para:
a) Ordenar decrecientemente un vector usando la funci on de comparaci on apropiada.
b) Ordenar por valores crecientes de edad un vector de tuplas de la forma (nombre, edad).
Por ejemplo, el vector [(Juan, 20), (Ana,21), (Luis,19), (Mar,19)], una vez
ordenado, es [(Luis,19), (Mar,19), (Juan, 20), (Ana,21)].
136 Un algoritmo de ordenaci on es estable si los valores id enticos desde el punto de vista de la
comparaci on ocupan la misma posici on relativa al nalizar la ordenaci on. Consideremos un ejem-
plo como el del ultimo apartado del ejercicio anterior. En el vector ordenado (Luis, 19) apare-
ce delante de (Mar,19) tanto en el vector original como en el vector ya ordenado. Pero es evi-
dente que tan ordenado est a el vector [(Luis,19), (Mar,19), (Juan, 20), (Ana,21)]
como el vector [(Mar,19), (Luis,19), (Juan, 20), (Ana,21)]. Un algoritmo de or-
denaci on estable siempre produce como resultado el primero de estos vectores. Es quicksort un
algoritmo de ordenaci on estable?
137 Es la versi on de mergesort que hemos presentado un algoritmo de ordenaci on estable? Si
no lo es, puedes modicarla para que lo sea?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Los algoritmos de ordenaci on estables tienen aplicaci on cuando se pretende


ordenar con criterios que afectan a varios campos. Si deseamos, por ejemplo,
ordenar una lista de registros que describen a personas por edad y, en caso de
empate, por salario, podemos usar un algoritmo de ordenaci on estable que haga
dos pasadas: la primera ordenar a por salario y la segunda, por edad.
5.8.1. Ordenaci on in situ
En lugar de efectuar la partici on del vector original creando dos nuevos vectores (left
y right en la versi on presentada), que resulta oneroso en consumo de memoria, vamos
a hacerla in situ. El m etodo partition del siguiente programa efect ua la partici on sobre
a[p:r]. El resultado de la funci on es el ndice del pivote una vez hecha la partici on:
292 Apuntes de Algoritmia 28 de septiembre de 2009
demos/divideandconquer/inplacequicksort.py
from algoritmia.problems.sorting import InPlaceQuickSorter
a = [11, 21, 3, 1, 98, 0, 12, 82, 29, 30, 11, 18, 43, 4, 75, 37]
InPlaceQuickSorter().sort(a)
print(Ordenado:, a)
Ordenado: [0, 1, 3, 4, 11, 11, 12, 18, 21, 29, 30, 37, 43, 75, 82, 98]
La rutina de partici on evita la creaci on de nuevos vectores gracias a haber marcado
la regi on de inter es en el vector original a con dos ndices, p y r. El m etodo partition
selecciona como pivote al ultimo elemento de la regi on, que tiene ndice a[r-1], y se
almacena en una variable pivot. A continuaci on usa dos ndices, i y j que inicializa con
los valores p 1 y p, respectivamente. En todo momento se satisfacen estas propiedades
(invariantes de bucle):
a[0], a[1], . . . , a[i] son valores menores o iguales que pivot,
a[i+1], a[i+2], . . . , a[j] son valores mayores que pivot.
Con cada iteraci on se considera un elemento a[j], para j tomando valores crecientes, y se
compara con el pivote pivot. Si el elemento es menor o igual que el pivote, se intercambia
con a[i] y se incrementa i, manteniendo as las dos invariantes de bucle. El proceso nali-
za cuando j alcanza el valor r 2 y es entonces cuando se efect ua un ultimo intercambio:
el pivote se intercambia con el elemento de la posici on i + 1. La gura 5.14 muestra la
ejecuci on paso a paso de partition sobre un vector.
Es interesante apreciar c omo, tras el proceso de partici on, el pivote ya ocupa su posi-
ci on denitiva en el vector ordenado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
138 Qu e ndice devuelve el algoritmo de partici on cuando los elementos del vector tienen
el mismo valor? Y cuando el elemento escogido como pivote es el segundo menor del vector?
Compru ebalo con el vector [7, 31, 36, 5, 22, 15, 10]. Qu e talla tienen los (sub)vectores que
se generan en esta partici on?
139 Realiza una traza de quicksort sobre estos vectores con el nuevo m etodo de partici on:
a) [21, 27, 17, 5, 20, 8, 7, 4, 19, 2, 6, 29]
b) [23, 43, 11, 5, 9, 22, 15, 17, 5, 32, 29, 30, 12, 23, 0]
Debes indicar el valor de pivot, left y right tras cada proceso de partici on.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.8.2. Complejidad computacional
La rutina de partici on requiere tiempo O(n) cuando divide un vector de talla n en sendos
vectores de tallas respectivas n i 1 e i, como se deduce f acilmente de un an alisis de la
28 de septiembre de 2009 Captulo 5. Divide y vencer as 293
1
0
3
1
46
2
2
3
14
4
0
5
9
6
5
7
1
0
3
1
46
2
2
3
14
4
0
5
9
6
5
7
1
0
3
1
46
2
2
3
14
4
0
5
9
6
5
7
1
0
3
1
46
2
2
3
14
4
0
5
9
6
5
7
1
0
3
1
2
2
46
3
14
4
0
5
9
6
5
7
1
0
3
1
2
2
46
3
14
4
0
5
9
6
5
7
1
0
3
1
2
2
0
3
14
4
46
5
9
6
5
7
1
0
3
1
2
2
0
3
14
4
46
5
9
6
5
7
1
0
3
1
2
2
0
3
5
4
46
5
9
6
14
7
p r
i j i j i j
i j i j i j
i j i j i
Figura 5.14: Traza del procedimiento de partici on (de izquierda a derecha y de arriba a abajo). Se muestra el estado del
fragmento de vector que est a siendo particionado antes de iniciar el bucle y tras cada una de sus iteraciones. El pivote
aparece siempre con fondo gris. La ultima imagen muestra el estado del vector tras el intercambio del pivote por el
elemento a[i+1], con lo que pasa a estar en su posici on denitiva en el vector.
funci on partition. La relaci on de recurrencia para el coste temporal es de la forma
T(n) =
{
c
1
, si n 1;
T(i) + T(n i 1) + c
2
n + c
3
, si n > 1;
donde i puede tomar cualquier valor entre 0 y n 1.
Peor caso: particiones desequilibradas
Hay varias situaciones que podemos clasicar como peor caso para el algoritmo quick-
sort: el vector est a ordenado crecientemente, est a ordenado decrecientemente o s olo con-
tiene valores repetidos. En tales casos, partition siempre considera que el pivote es el
ultimo elemento (o el primero), dando lugar a dos nuevos (sub)problemas de ordenaci on
de tallas muy diferentes: uno sin ning un elemento y otro con todos los elementos del
vector original excepto el pivote. El coste temporal para este caso peor puede expresarse
as:
T(n) =
{
c
1
, si n 1;
T(0) + T(n 1) + c
2
n + c
3
, si n > 1;
donde c
2
n es el coste asociado a la partici on. O sea, un coste temporal O(n
2
). El arbol de
llamadas de la gura 5.12 muestra gr acamente esta situaci on.
Mejor caso: particiones equilibradas
El mejor de los casos es aquel que hace que el pivote siempre se encuentre en la posici on
central del vector. En tal caso, el vector se divide en dos fragmentos de igual tama no (o
294 Apuntes de Algoritmia 28 de septiembre de 2009
Figura 5.15: Tama no de
los problemas en el arbol
de llamadas de quicksort
para el peor de los ca-
sos. El nivel k requie-
re O(n k) pasos para
efectuar la partici on.
n
0 n 1
0 n 2
0 n 3
1
n
n 1
n 2
n 3
1
n(n +1)/2
n
n
i
v
e
l
e
s
que se diferencian en una unidad). El coste puede expresarse as:
T(n)
{
c
1
, si n = 0 o n = 1;
T(n/2) + T(n/2 1) + c
2
n + c
3
, si n > 1.
El coste temporal es (n lg n).
Caso promedio
Supongamos, sin p erdida de generalidad, que todos los elementos del vector son diferen-
tes. Si escogemos como pivote el elemento que acabar a ocupando la posici on de ndice i,
generamos dos nuevos problemas: uno de talla i y otro de talla n i 1. La probabilidad
de que el elemento que escogemos como pivote sea el que acabe ocupando una de las n
posibles posiciones es de 1/n. As pues, el coste promedio puede escribirse as:
T(n) =
1
n
(T(0) + T(n 1) + c
2
n)
+
1
n
(T(1) + T(n 2) + c
2
n)
+. . .
+
1
n
(T(n 1) + T(0) + c
2
n) .
28 de septiembre de 2009 Captulo 5. Divide y vencer as 295
O sea,
T(n) =
1
n

0i<n
(T(i) + T(n i 1) + c
2
n)
=
(
1
n

0i<n
T(i)
)
+
(
1
n

0i<n
T(n i 1)
)
+
(
1
n

0i<n
c
2
n
)
=
(
1
n

0i<n
T(i)
)
+
(
1
n

0i<n
T(n i 1)
)
+ c
2
n
=
(
1
n

0i<n
T(i)
)
+
(
1
n

0i<n
T(i)
)
+ c
2
n
=
(
2
n

0i<n
T(i)
)
+ c
2
n.
Podemos resolver la ecuaci on recursiva si multiplicamos ambas partes por n y restamos
la misma f ormula para n 1:
nT(n)(n 1)T(n 1)
= n
((
2
n

0i<n
T(i)
)
+ c
2
n
)
(n 1)
((
2
n 1

0i<n1
T(i)
)
+ c
2
(n 1)
)
= 2

0i<n
T(i) + c
2
n
2
2

0i<n1
T(i) c
2
(n 1)
2
= 2
(

0i<n
T(i)

0i<n1
T(i)
)
+ c
2
n
2
c
2
(n 1)
2
= 2T(n 1) +2c
2
n c
2
= 2T(n 1) + c
2
(2n 1).
Llegamos as a este t ermino general:
T(n) =
n +1
n
T(n 1) + c
2
2n 1
n
.
Como (2n 1)/n es menor que 2,
T(n)
n +1
n
T(n 1) +2c
2
.
296 Apuntes de Algoritmia 28 de septiembre de 2009
Despleguemos:
T(n)
n +1
n
T(n 1) +2c
2

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.

Recientemente se ha propuesto un algoritmo de ordenaci on hbrido llama-


do Introsort (por instrospective sort) que se comporta en la pr actica como
Quicksort pero tiene un coste temporal O(n lg n) para el peor de los casos. El al-
goritmo consiste, b asicamente, en Quicksort modicado para detectar cu ando su
comportamiento conduce a un coste cuadr atico (porque las particiones son dese-
quilibradas). Al detectar esta situaci on sobre un fragmento del vector cambia de
m etodo de ordenaci on y utiliza Heapsort, que es O(n lg n). El algoritmo Introsort es
el candidato a m etodo de ordenaci on est andar en la librera STL de C++.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 301
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
148 Disponemos de un conjunto T de n tornillos de tama nos diferentes y un conjunto R de
n tuercas de tama nos tambi en diferentes. Cada tornillo encaja exactamente en una tuerca. Los
tornillos y las tuercas vienen descritos en sendos vectores de n elementos donde el elemento
almacenado en la celda de ndice i es el di ametro de la i- esima tuerca o tornillo. Por problemas
t ecnicos nos resulta imposible comparar el tama no de una tuerca con el de otra tuerca o el tama no
de un tornillo con el de otro tornillo. Podemos, eso s, determinar en tiempo constante si una
tuerca se ajusta a un tornillo determinado, si es demasiado grande o si es demasiado peque na.
Dise na un algoritmo divide y vencer as que nos indique qu e tornillos encajan en qu e tuercas.
Estudia a continuaci on la complejidad temporal del algoritmo.
149 El denominado movimiento browniano es un tipo de movimiento aleatorio que se ob-
serva en fen omenos como la dispersi on de tinta en agua, el precio de las acciones de la bolsa, la
forma rugosa de las monta nas, etc.
Generar curvas que simulen el movimiento browniano tiene aplicaci on en fsica, gr acos
por computador, etc. Hay un m etodo de simulaci on del movimiento browniano conocido co-
mo m etodo de desplazamiento del punto medio. A partir de dos puntos en 2 dimensiones, a y
b, y una varianza v, se determina el punto medio entre los dos puntos y se perturba a nadien-
do un valor aleatorio a su ordenada, produciendo un punto c que forma parte de la curva de
movimiento browniano. El valor aleatorio que se suma a la ordenada se genera a partir de una
distribuci on gaussiana con media en 0 y la varianza que nos suministran. El procedimiento se
aplica recursivamente, por una parte, a los puntos a y c con varianza v/2 y, por otra parte, a los
puntos c y b con varianza v/2. La recursi on naliza cuando la diferencia de abcisas es menor o
igual que 1.
Aqu tienes un par de pasos en la aplicaci on del m etodo. En la gura de la izquierda se mues-
tran los puntos que nos suministran, a y b, el punto medio entre ambos y el punto resultante de
desplazarlo por un valor aleatorio, c. Si naliz asemos aqu, la curva de movimiento browniano
correspondera a la lnea discontinua. En la gura de la derecha se muestra el resultado de aplicar
el proceso recursivamente a las dos lneas que forman la curva de movimiento browniano en la
gura de la izquierda.
a
b
c
= gauss(0,

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(

Idem por fuerza bruta:, bfcpf .distance between closest points(z))


310 Apuntes de Algoritmia 28 de septiembre de 2009
Distancia entre puntos mas proximos: 0.707106781187

Idem por fuerza bruta: 0.707106781187


El coste temporal del algoritmo es
T(n) =
{
c, si n 1;
2T(n/2) +O(n
2
), si n > 1.
El coste temporal es, pues, O(n
2
), con lo que no hemos ganado nada respecto de la ver-
si on basada en fuerza bruta. Pero hay una idea que permite reducir el tiempo de eje-
cuci on. Sea d
1
la menor distancia entre dos puntos situados a la izquierda de la lnea
divisoria, sea d
2
la menor distancia entre dos puntos a su derecha y sea = mn{d
1
, d
2
}.
Cuando consideramos el c alculo de la distancia entre puntos en lados opuestos de la lnea
divisoria, podemos descartar cualquier punto que se encuentre a m as de unidades de
distancia de la lnea divisoria. As pues, s olo interesa buscar puntos ubicados en la banda
blanca de la gura 5.19.
Figura 5.19: Si la menor distancia entre dos puntos del lado izquierdo
d
1
y la menor distancia entre dos puntos del lado derecho es d
2
no tie-
ne sentido comparar pares de puntos de regiones que, ya s olo en el eje
horizontal, distan de la lnea de separaci on m as de = mn{d
1
, d
2
}
unidades.
z
0
z
1
z
2
z
3
z
4
z
5
z
6
z
7
S
1
S
2
d
1
d
2

En cualquier caso, parece que seguimos sin haber ganado nada en t erminos asint oti-
cos para el peor caso: la zona blanca puede contener cerca de n puntos, con casi la mitad
a la izquierda de la lnea central y el resto a la derecha. Si esto ocurre, recorrer todos los
puntos de la franja blanca y calcular su distancia con respecto a todos los puntos en el
lado contrario de la lnea divisora requiere O(n
2
) c alculos de distancia.
Centr emonos en qu e ocurre con cada uno de los puntos que cae a un lado concreto
de la lnea divisoria con todos los que hay al otro lado de dicha lnea. Supongamos, sin
p erdida de generalidad, que estamos considerando un punto p que cae a mano izquierda
de la lnea. De todos los puntos que hay a mano derecha, s olo interesan aquellos cuya
diferencia de ordenadas (en valor absoluto) con respecto de p sea menor que . La regi on
en la que pueden caer dichos puntos se muestra en la gura 5.20 (a) etiquetada con la
letra R.
Adoptemos el criterio de que al calcular d(p, q), el punto q presenta mayor altura que
el punto p. Como la distancia eucldea es una funci on conmutativa, la distancia d(p, q

)
28 de septiembre de 2009 Captulo 5. Divide y vencer as 311
para q

a inferior altura que p se calcular a como d(q

, p). De este modo la regi on de inter es


para p, R, se reduce a la que se muestra etiquetada con R

en la gura 5.20 (b).


Se plantea ahora la siguiente pregunta: C omo determinar ecientemente qu e puntos
a la derecha de la lnea divisoria caen en R

? Todo sera m as sencillo si los puntos se


presentaran ordenados por altura creciente. Si el punto p presenta ndice i en el vector
ordenado, bastar a con considerar los puntos i + 1, i + 2, etc. hasta llegar a uno cuya
diferencia de altura con respecto a p sea mayor o igual que .
p

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

? Veamos cu antos puntos


puede haber en R

. Sabemos que ning un par de puntos a la derecha de la lnea divisoria


dista menos de unidades. Eso es v alido tambi en para los pares de puntos de R

. Si un
punto q est a en R

, es seguro que no hay m as puntos en el interior de la circunferencia


de radio centrada en q. La gura 5.21 (a) muestra un punto q y la zona en la que no
puede haber otros puntos. La mayor cantidad de puntos que podemos disponer 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

hace que no pueda haber puntos en el inte-


rior del crculo de radio centrado en q: no hay
dos puntos a la derecha de la lnea con distan-
cia menor que . (b) En R

puede haber hasta


4 puntos separados por una distancia menor o
igual que : dichos puntos ocuparan las 4 es-
quinas de 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(

Idem por fuerza bruta:, bfcpf .distance between closest points(z))


Distancia entre puntos mas proximos: 0.707106781187

Idem por fuerza bruta: 0.707106781187


El coste temporal de la rutina recursiva es
T(n) =
{
c, si n 1;
2T(n/2) +O(n), si n > 1,
as que T(n) O(n lg n). A este coste hemos de a nadir el de dos ordenaciones (por abcisa
y por ordenada), pero como estas son operaciones O(n lg n), no afectan al coste global del
algoritmo.
28 de septiembre de 2009 Captulo 5. Divide y vencer as 313
Nos queda por averiguar qu e par de puntos es m as pr oximo (s olo sabemos a qu e dis-
tancia se encuentran dichos puntos):
algoritmia/problems/closestpair.py
def closest points(self , z: "sequence<(T, T)>") -> "((T, T), (T, T))":
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 . closest pair(z, x, y)[:2]
def closest pair(self , z: "sequence<(T, T)>",
x: "sequence<int>", y: "sequence<int>") -> "((T, T), (T, T))":
if len(x) <= 1:
return (None, None, innity)
elif len(x) == 2:
return (z[x[0]], z[x[1]], 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]
(p1, q1, d1) = self . closest pair(z, x1, y1)
(p2, q2, d2) = self . closest pair(z, x2, y2)
(p, q, delta) = (p1, q1, d1) if d1 < d2 else (p2, q2, 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
dij = d(z[strip[i]], z[strip[j]])
if dij < delta: p, q, delta = z[strip[i]], z[strip[j]], dij
return (p, q, delta)
demos/divideandconquer/closestpair.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(Par de puntos mas proximos:, cpf .closest points(z))
print(Por fuerza bruta:, bfcpf .closest points(z))
Par de puntos mas proximos: ((3, 2), (3.5, 1.5))
Por fuerza bruta: ((3, 2), (3.5, 1.5))
314 Apuntes de Algoritmia 28 de septiembre de 2009
5.11. La envolvente convexa de un conjunto de
puntos en el plano
Una regi on del plano C es un conjunto de puntos y es convexa si para todo par de puntos
(x
p
, y
p
) y (x
q
, y
q
) de C, los puntos del segmento de lnea que los une est an comprendidos
en el conjunto.
Un polgono puede describirse con una secuencia de puntos (sus v ertices) ordenada
en sentido antihorario (u horario) y describe un conjunto de puntos: su interior (incluyen-
do los puntos de sus aristas). Un polgono es convexo si todo par de puntos contenidos
en el pueden unirse con un segmento de lnea cuyos puntos tambi en est an contenidos en
el conjunto. La gura 5.22 muestra un polgono convexo y otro que no lo es.
Figura 5.22: (a) Polgono convexo. (b) Polgono no convexo: el segmento
entre p y q no est a incluido en el interior del polgono.
p
q
(a) (b)
Equivalentemente, un polgono es convexo si el angulo exterior entre todo par de aris-
tas contiguas es mayor o igual que 180 grados. Y, tambi en equivalentemente, un polgono
es convexo si al recorrer sus v ertices en sentido antihorario s olo presenta giros a la iz-
quierda. La supercie de un polgono convexo es una regi on convexa.
La envolvente convexa de un conjunto de puntos S es la regi on convexa de puntos
C m as peque na que comprende a S. La envolvente convexa de S queda descrita con un
polgono convexo cuyos v ertices son puntos de S. La gura 5.23 muestra un conjunto de
puntos y su envolvente convexa. Los puntos extremos de S (los que presentan mayores
y menores valores de alguna de sus coordenadas) son puntos del polgono que dene la
envolvente convexa, pero puede que no sean los unicos.
El problema que deseamos resolver se plantea as: dado un conjunto de n puntos
en el plano, S = {(x
0
, y
0
), (x
1
, y
1
), . . . , (x
n1
, y
n1
)}, deseamos conocer su envolvente
convexa, que es un subconjunto ordenado (una secuencia) de puntos de S.
5.11.1. El algoritmo QuickHull
Nos centramos ahora en el c alculo de la envolvente convexa mediante un m etodo de-
nominado QuickHull. Supondremos que no hay dos puntos con la misma abscisa. Los
puntos p y q que tienen menor y mayor valor de la abscisa forman parte de la envol-
vente convexa (del mismo modo que los dos con la menor y mayor ordenada, aunque
no los consideramos ahora). Consideremos ahora dos subconjuntos de puntos: A, el con-
junto de puntos que est an a la izquierda de

pq y B, el de los puntos a la derecha de

pq
28 de septiembre de 2009 Captulo 5. Divide y vencer as 315
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
50
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
50
(a) (b)
Figura 5.23: (a) Conjun-
to de puntos S y (b) su en-
volvente convexa.
(ve ase la gura 5.24 (a)). Las siguientes rutinas permiten determinar si un punto c est a a
la izquierda o a la derecha de un vector

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

ph y aquellos que est an a la izquierda de

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)

El m etodo QuickHull recibe este nombre porque parece inspirado en quick-


sort y presenta un comportamiento similar: O(n
2
) en el peor de los casos,
pero mucho m as eciente en promedio. Del mismo modo que QuickHull se inspira en
quicksort, existe un m etodo que calcula la envolvente convexa en tiempo O(n lg n)
siguiendo una estrategia divide y vencer as inspirada en mergesort.
Ya tenemos la recursi on divide y vencer as. Se debe tener la precauci on, eso s, de
evitar la repetici on de puntos a la hora de combinar las envolventes convexas, pues el
punto h forma parte de ambas. Nos falta determinar el m etodo por el que podemos cal-
cular el punto h m as alejado del segmento pq. El punto m as alejado es el que forma un
tri angulo phq de mayor area. El area de un tri angulo determinado por tres puntos a, b
y c se puede calcular as:
algoritmia/geometry.py
def triangle area(a: "Point2D", b: "Point2D", c: "Point2D") -> "bool":
return abs((a.y-b.y)*(c.x-b.x) - (a.x-b.x)*(c.y-b.y)) / 2
Detectemos y solucionemos el caso base de la recursi on. Cuando se solicita el c alculo
de la envolvente convexa de dos puntos el resultado es trivial: los dos puntos.
El siguiente m etodo calcula la envolvente convexa siguiendo el procedimiento descri-
to. El resultado es una lista con los puntos de la envolvente en sentido horario. El hecho
28 de septiembre de 2009 Captulo 5. Divide y vencer as 317
de que haya dos llamadas diferentes a la funci on recursiva se debe a que calculamos la
envolvente de A y la de B por separado, pues lo dicho para A debe interpretarse apro-
piadamente para B (por ejemplo, los puntos supervivientes no est an a la izquierda del
segmento que se considera en cada caso, sino a su derecha):
algoritmia/problems/convexhull.py
from algoritmia.utils import argmax
from algoritmia.geometry import left, right, triangle area
...
class QuickHullFinder:
def quickhull(self , S: "sequence<(T, T)>") -> "iterable<int>":
if len(S) <= 2: return list(S)
p, q = min(S), max(S)
qhull = self . quickhull(p, [z for z in S if z != p and z != q and left(p, q, z)], q)[1:] + \
self . quickhull(q, [z for z in S if z != p and z != q and right(p, q, z)], p)[1:]
return qhull
def quickhull(self , p, A, q):
if len(A) == 0: return [p, q]
h = argmax(A, lambda z: triangle area(p,q,z))
return self . quickhull(p, [z for z in A if left(p, h, z)],h) + \
self . quickhull(h, [z for z in A if left(h, q, z)],q)[1:]
Probamos el programa con el conjunto de puntos de la gura 5.23 (a). La salida por
pantalla del programa son los ndices de los puntos que forman la envolvente convexa,
en sentido horario y empezando por el de m as a la izquierda. Se puede comprobar que
coincide con el que se muestra en la gura 5.23 (b):
demos/divideandconquer/quickhull.py
from algoritmia.geometry import Point2D
from algoritmia.problems.convexhull import QuickHullFinder
from random import seed, shufe
# Creaci on de vector con 50 puntos aleatorios (no repetidos) con 0 x, y < 1000.
seed(10)
x, y = list(range(1000)), list(range(1000))
shufe(x); shufe(y) # shufe baraja el contenido de la lista.
pts = [Point2D(x[i],y[i]) for i in range(50)]
qh = QuickHullFinder().quickhull(pts)
for point in qh: print(pts.index(point)+1, end=" ")
15 22 2 42 6 44 21 46 41 10 39
318 Apuntes de Algoritmia 28 de septiembre de 2009

La envolvente convexa se conoce por dos nombres m as en espa nol: cierre


convexo y recubrimiento convexo. Imagina que los puntos son clavos en un
tablero. La envolvente convexa es la forma que adquirira una goma el astica que
abarque todos los clavos.
Hay una aplicaci on pr actica de la envolvente convexa en el terreno de los vi-
deojuegos (y de las simulaciones fsicas): la detecci on de colisiones. En una ver-
si on simplicada consiste en detectar si un punto se encuentra dentro de una gura
poligonal (ha impactado la bala en un enemigo?). C omo determinar si los pun-
tos 1, 2 y 3 est an en el interior del polgono (que es no convexo) de la gura (a)?
1
2
3
1
2
3
1
2
3
1
2
3
(a) (b) (c) (d)
Decidir si un punto est a en el interior del polgono es computacionalmente costoso.
Cabe esperar que la mayora de las veces la respuesta sea no, as que tiene in-
ter es disponer de algoritmos que proporcionen una respuesta r apida para este caso
particular. Podemos efectuar una primera prueba sobre la caja de acotaci on axial-
mente alineada (o AABB, por el t ermino ingl es axially aligned bounding box) del
polgono (v ease la gura (b)). La caja se describe con la menor y mayor abscisa
y la menor y mayor ordenada. Un m aximo de cuatro sencillas comparaciones per-
miten determinar que el punto 2 no est a en el interior del polgono. Los puntos 1 y
3 de la gura superan esta primera prueba. Una comprobaci on m as ajustada con-
siste en detectar si se encuentran en el interior de la envolvente convexa (v ease la
gura (c)). Consideremos cada arista del polgono que dene la envolvente convexa
recorriendo sus aristas en sentido horario. Podemos advertir que todos los puntos
de la regi on caen a mano derecha de cada una de las aristas (gura (d)). Determinar
a qu e lado de un vector cae un punto es un c alculo sencillo. El punto 1 puede des-
cartarse respondiendo a, a lo sumo, 6 preguntas: no est a en la envolvente convexa
de los v ertices del polgono, as que no puede estar en el polgono. S olo el punto 3
debe someterse a procedimientos de c alculo m as costosos para saber si est a o no
dentro del polgono. Hay otros procedimientos para detectar si un punto est a o no
en un polgono y que pasan por estudiar la paridad del n umero de intersecciones de
un segmento vertical que parte del punto con las aristas del polgono.
5.11.2. Complejidad computacional
La funci on quickhull divide el conjunto de n puntos en dos conjuntos, A y B, que no inclu-
yen los puntos p y q. Lo peor que puede ocurrir es que uno de ellos tenga n 2 puntos y
el otro ninguno; lo mejor, que uno contenga n/2 2 puntos y el otro n/2 2.
En cada paso recursivo, la funci on quickhull divide un conjunto con n puntos en tres
conjuntos: dos de ellos contienen candidatos a formar parte de la envolvente convexa y
otro, implcito, contiene los puntos que se sabe con certeza que son interiores a la en-
volvente y, por tanto, descartables. La recursi on tiene lugar sobre cada uno de los dos
28 de septiembre de 2009 Captulo 5. Divide y vencer as 319
primeros.
Lo mejor que puede ocurrir es que se descarten todos los puntos y los dos conjuntos
sobre los que se recurre est en vacos. En tal caso, se producen nuevas llamadas recursivas,
pero su ejecuci on requiere tiempo constante. El coste en el mejor de los casos es, pues,
lineal.
Lo peor que puede ocurrir es que no se descarte ning un punto y que, adem as, se
produzcan llamadas recursivas mal balanceadas: una sobre un conjunto vaco y la otra
sobre n 2 puntos (todos los puntos a excepci on de p y q). En el peor caso tenemos, pues,
una recurrencia cuyo t ermino general es de la forma T(n) = T(n 2) + f (n). El t ermino
f (n) es O(n), pues ese es el orden del coste temporal con el que podemos encontrar el
punto m as alejado de los otros dos, asignar los puntos en los nuevos subconjuntos y
combinar el resultado de calcular la envolvente convexa de cada conjunto. El algoritmo
es, pues, cuadr atico en el peor de los casos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
158 Puedes dise nar algunos casos concretos en los que el algoritmo QuickHull se ejecuta en
tiempo proporcional al n umero de puntos?
159 Un peor caso para el algoritmo QuickHull se da cuando los puntos de S se disponen en una
circunferencia. Haz una traza para ese caso.
160 En aras de la simplicidad expositiva hemos supuesto que no hay dos puntos con la misma
abscisa. C omo afecta al algoritmo el que los pueda haber?
161 Hay un algoritmo divide y vencer as para el c alculo de la envolvente convexa con un coste
temporal O(n lg n). B uscalo en la bibliografa, est udialo e implem entalo en Python.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.12. Producto de enteros grandes
Podemos codicar un entero positivo con n o menos cifras en un vector de talla n alma-
cenando una cifra en cada celda del vector (y poniendo ceros en las casillas que no tienen
cifra asignada). Dados dos vectores de talla n que codican de este modo sendos enteros
positivos u y v, se desea calcular en un nuevo vector el resultado de multiplicar u y v, u v
(que es un n umero de hasta 2n cifras). Por ejemplo, el producto de los enteros 1 345 (co-
dicado con [1, 3, 4, 5]) y 2 965 (codicado con [2, 9, 6, 5]) es 3 987 925 (codicado
con [3, 9, 8, 7, 9, 2, 5]).
Un algoritmo directo puede efectuar el c alculo en tiempo O(n
2
). Divide y vencer as
proporciona un algoritmo asint oticamente m as eciente. Estudiemos una primera apro-
ximaci on:
Dividimos u y v en otros dos n umeros (de igual talla):
u = 10
s
w + x, v = 10
s
y + z,
donde s = n/2. Los valores x y z se corresponden, respectivamente, con los
dgitos menos signicativos de u y v (x = u m od 10
s
y z = v m od 10
s
), y los valores
w e y se corresponden, respectivamente, con los dgitos m as signicativos de u y v
(x = u 10
s
y z = v 10
s
),
320 Apuntes de Algoritmia 28 de septiembre de 2009
Ejemplo: u = 1 345 = 13 10
2
+45 y v = 2 965 = 29 10
2
+65.
Y realizamos el producto con esta descomposici on:
u v = 10
2s
wy +10
s
(wz + xy) + xz
Ejemplo: u v = (13 29) 10
4
+(13 65 +29 45) 10
2
+45 65 = 377 10
4
+(845 +
1 305) 10
2
+2 925 = 3 770 000 +215 000 +2 925 = 3 987 925
Nuestro objetivo es reducir el n umero de productos entre dgitos, pues el producto es una
operaci on relativamente costosa. Si tenemos en cuenta que los productos por potencias
de 10 son meros desplazamientos, podemos considerar que el algoritmo ha reducido la
multiplicaci on de 2 n umeros de n cifras a 4 multiplicaciones de n umeros de n/2 cifras,
2 desplazamientos y 3 sumas. Para valores grandes de n, el coste del algoritmo viene
determinado por las operaciones de multiplicaci on. He aqu una implementaci on que
dene una clase para nuestros enteros con hasta n cifras y en el que hemos sobrecargado
los operadores pertinentes:
algoritmia/problems/bigint.py
class BigIntBase:
def init (self , digits: "sequence<int> or str or int"):
if type(digits) == list:
self .digit = digits[::-1]
elif type(digits) == str:
self .digit = [int(digit) for digit in reversed(digits)]
elif type(digits) == int:
self .digit = []
while digits > 0:
self .digit.append(digits%10)
digits //= 10
if self .digit == []: self .digit = [0]
def repr (self ):
if len(self .digit) == 0: return 0
for i in range(len(self .digit)-1,-1,-1):
if self .digit[i] != 0: break
return .join(str(digit) for digit in reversed(self .digit[:i+1]))
def len (self ):
i = 0
for i in range(len(self .digit)-1,-1,-1):
if self .digit[i] != 0: break
return i+1
def getitem (self , i: "int") -> "int":
if not (0 <= i < len(self .digit)): return 0
return self .digit[i]
def add (self , other: "BigInt") -> "BigInt":
28 de septiembre de 2009 Captulo 5. Divide y vencer as 321
n = max(len(self ), len(other))
result, carry = [], 0
for i in range(n):
s = self [i] + other[i] + carry
carry = s // 10
result.append(s % 10)
if carry: result.append(carry)
return self . class (list(reversed(result)))
def lshift (self , n: "Bigint") -> "BigInt":
return self . class (self .digit[::-1]+[0]*n)
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 BigIntBase([r]) if r < 10 else BigIntBase([r//10, r%10])
else:
s = n // 2
w, x = BigIntBase(u.digit[s:][::-1]), BigIntBase(u.digit[:s][::-1])
y, z = BigIntBase(v.digit[s:][::-1]), BigIntBase(v.digit[:s][::-1])
return ((w*y) << (s<<1)) + ((w*z+x*y) << s) + x*z
demos/divideandconquer/bigintbase.py
from algoritmia.problems.bigint import BigIntBase
a, b = BigIntBase(1345), BigIntBase(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)

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
).

El algoritmo se debe a A. Karatsuba e Y. Ofman, quienes lo publicaron en


Multiplication of multidigit numbers on automata, Dokl. Akad. Nauk SSSR,
145, pp. 293294, 1962. Aunque el algoritmo es asint oticamente m as eciente, no
es aconsejable su uso en la pr actica a menos que se desee operar con n umeros
realmente grandes (hablamos de centenares de dgitos).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
162 Funciona el algoritmo de multiplicaci on si los n umeros est an codicados en hexadecimal?
Si es as, implementa un programa que, aplicando la t ecnica de divide y vencer as, efect ue dicho
c alculo con n umeros hexadecimales codicados con cadenas.
163 Reformula el problema y su soluci on para n umeros codicados en base 2. Al implementar
el programa ten en cuenta que el producto de n umeros de 32 bits (o 64, seg un la longitud de
palabra de tu m aquina) es m as r apido si se efect ua directamente que mediante el m etodo divide
y vencer as. Ten en cuenta este hecho a la hora de jar un umbral de recursi on adecuado.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5.13. Producto de matrices cuadradas
El producto de dos matrices A y B de talla n n requiere tiempo O(n
3
) siguiendo la
aproximaci on tradicional:
algoritmia/problems/matrixprod.py
def matrix product(A: "matrix", B: "matrix") -> "matrix":
p, q, r = len(A), len(A[0]), len(B[0])
return [[sum(A[i][k]*B[k][j] for k in range(q)) for j in range(r)] for i in range(p)]
Si n es potencia de 2, podemos dividir cada matriz cuadrada de n n en cuatro ma-
trices cuadradas de talla n/2 n/2 y reescribir C = AB con
(
r s
t u
)
=
(
a b
c d
)

(
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.

El algoritmo de Strassen se public o originalmente en el artculo Gaussian


elimination is not optimal, Numerische Mathematik, 13, pp. 354356, 1969,
de V. Strassen. Desde entonces se han ido inventando algoritmos con constantes
progresivamente m as peque nas. Hasta el momento, el m as r apido se debe a Wino-
grad y permite efectuar el producto de matrices cuadradas en tiempo O(n
2.376
). Las
cotas asint oticas temporales establecidas por los algoritmos de producto de matri-
ces cuadradas reducen el coste de otros algoritmos que pueden expresar algunos
de sus pasos en t erminos de estos mismos productos. Los m etodos basados en
operaciones matriciales para el c alculo de la clausura positiva de un digrafo son
un ejemplo, s olo que operando sobre el semianillo formado por los booleanos y la
conjunci on y disyunci on l ogicas.
5.14. La transformada r apida de Fourier
La transformada r apida de Fourier es un algoritmo paradigm atico de la estrategia divide
y vencer as y, posiblemente, uno de los m as utilizados en innidad de campos con apli-
caci on pr actica: telecomunicaciones, an alisis del habla, visi on articial, compresi on de
datos con p erdidas, etc. Para entender el objeto de la transformada r apida de Fourier
es preciso que introduzcamos brevemente algunos conceptos b asicos sobre teora de la
se nal y que el lector est e familiarizado con los n umeros complejos (y la notaci on que
presentamos en la secci on B.12 del ap endice B).
5.14.1. Algunos conceptos previos
Una se nal es la variaci on de alguna magnitud fsica (corriente el ectrica, presi on en el
aire, etc) en el tiempo, es decir, es una funci on f (t) que proporciona un valor para cada
instante t. Nos interesa especialmente considerar magnitudes que podemos expresar con
n umeros complejos, es decir, f : R C.
Una se nal es peri odica cuando se repite cada cierto tiempo, es decir, f (t) = f (t +
kT), para alg un valor T R y para todo k Z (n otese que k puede tomar valores
negativos). T recibe el nombre de periodo y f = 1/T el de frecuencia. El periodo se expresa
en segundos y la frecuencia en ciclos por segundo o Herzios (Hz).
28 de septiembre de 2009 Captulo 5. Divide y vencer as 327
Una se nal es discreta cuando se considera la variaci on de la magnitud en instantes
discretos de tiempo. Estos instantes est an separados entre s una cantidad de tiempo
constante . Representaremos una se nal discreta como una secuencia de valores com-
plejos . . . , x
2
, x
1
, x
0
, x
1
, x
2
, . . .. Las se nales discretas provienen habitualmente de la
medici on de una se nal en una serie de instantes, es decir, x
n
= f (n) para todo n Z.
Decimos entonces que x se ha obtenido por muestreo de f . La frecuencia de muestro es
1/ Hz. Una se nal discreta es peri odica si se repite cada N instantes de tiempo, es decir,
x
n
= x
n+kN
, para todo n y k Z. Dado que la se nal se repite, basta con conocer los
valores x
0
, x
1
, . . . , x
N1
para tener una representaci on completa de la se nal.
Toda se nal peri odica puede descomponerse en una combinaci on de funciones sinu-
soidales (senos y cosenos, que son funciones peri odicas) de diferente frecuencia. Pode-
mos expresar cada x
k
, para k entre 0 y N 1, como
x
k
=
1
N
N1

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.

El tratamiento digital de se nales es una disciplina dedicada al an alisis y ma-


nipulaci on de se nales sobre computadores digitales. Sus aplicaciones son
innumerables: comunicaciones electr onicas, an alisis y reconocimiento autom atico
del habla, visi on articial, compresi on de audio y vdeo, retoque fotogr aco, etc. La
transformada r apida de Fourier es una t ecnica para el an alisis frecuencial de las
se nales. Fue ideada por Cooley y Tukey en 1965, si bien sus precedentes se remon-
tan a 1805, a no en el que Gauss utiliz o esta t ecnica para interpolar la trayectoria
de Pallas y Juno. La transformada r apida de Fourier permiti o efectuar un enorme
avance en el tratamiento de se nales.
5.14.3. Una versi on in situ iterativa
Al efectuar la divisi on de un vector para el c alculo de la DFT por divide y vencer as, se
separan los elementos que ocupan posici on par y los que ocupan posici on impar. Con
cada uno de ellos se forma un nuevo vector para el que se calcular una nueva DFT. La
gura 5.26 muestra el proceso recursivo como un arbol cuyos nodos est an etiquetados
con los vectores para los que se va a calcular una DFT.
Figura 5.26: Descomposici on
recursiva en el c alculo de
la FFT para un vector de 8
componentes.
[x
0
, x
1
, x
2
, x
3
, x
4
, x
5
, x
6
, x
7
]
[x
0
, x
2
, x
4
, x
6
]
[x
0
, x
4
]
[x
0
] [x
4
]
[x
2
, x
6
]
[x
2
] [x
6
]
[x
1
, x
3
, x
5
, x
7
]
[x
1
, x
5
]
[x
1
] [x
5
]
[x
3
, x
7
]
[x
3
] [x
7
]
En la gura 5.27 se puede ver qu e ocurre con los ndices de los elementos del vector
original que pasan a formar parte de cada uno de los vectores que decoran el arbol. Se
puede apreciar que las hojas presentan ndices cuyo valor codicado en binario e inver-
tido es la secuencia de n umeros [0..N 1]. Hay una explicaci on sencilla: cada divisi on
28 de septiembre de 2009 Captulo 5. Divide y vencer as 331
separa elementos en posici on par de elementos en posici on impar. La primera de estas di-
visiones pone a mano izquierda los elementos pares, cuyo bit menos signicativo (BMS)
es 0, y a mano derecha los pares, cuyo BMS es 1. Esta divisi on hace que el BMS de los
ndices pase a ser el m as signicativo. El siguiente nivel de divisi on hace que el segundo
BMS pase a ser el segundo bit m as signicativo. Y as sucesivamente.
La versi on iterativa de la FFT trata de resolver el problema evitando la recursi on y
recorriendo el arbol por niveles desde el pen ultimo nivel hasta la raz. Para ello, empieza
por ordenar la valores de la se nal de acuerdo con la inversi on de los bits de sus ndices.
Calcula entonces la DFT de los nodos del pen ultimo nivel (que tiene 2 componentes)
combinando las DFT de sus nodos hijo (que son hojas y tienen 1 componente cada una).
Procede entonces a calcular la DFT de los nodos del antepen ultimo nivel (que tiene 4
componentes) combinando las DFT de sus dos nodos hijo (que son del pen ultimo nivel
y tienen 2 componentes cada uno). Y as sucesivamente hasta llegar a la raz.
000 001 010 011 100 101 110 111
000 010 100 110
000 100
000 100
010 110
010 110
001 011 101 111
001 101
001 101
011 111
011 111
Figura 5.27:

Indices (en binario)
de los elementos del vector en la
descomposici on recursiva para el
c alculo de la FFT.
Podemos efectuar las operaciones in situ si tenemos la precauci on de calcular si-
mult aneamente X
k
y X
kN/2
para los diferentes valores de k (y la interpretaci on de N
que corresponde a cada una de las DFT calculadas). Y ahorrar, adem as, unas pocas ope-
raciones si tenemos en cuenta que
(kN/2)
N
=
k
N
, los que permite reescribir la ecua-
ci on (5.7) como
X
k
= X
par
kN/2

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(

Ultimos dos elementos:, an array[-2], an array[-1])


Los dos primeros elementos: 1 2

Ultimos dos elementos: 7 11


El posible concatenar listas con el operador suma:
342 Apuntes de Algoritmia 28 de septiembre de 2009
an array = [1, 2, 3, 5, 7, 11]
print(an array + [13, 17])
[1, 2, 3, 5, 7, 11, 13, 17]
O extender una lista con el m etodo extend (que modica la lista sobre la que se invo-
ca):
an array = [1, 2, 3, 5, 7, 11]
an array.extend([13, 17])
print(an array)
[1, 2, 3, 5, 7, 11, 13, 17]
El m etodo append extiende una lista elemento a elemento:
an array = [1, 2, 3, 5, 7, 11]
an array.append(13)
an array.append(17)
print(an array)
[1, 2, 3, 5, 7, 11, 13, 17]
El redimensionamiento puede resultar relativamente costoso en tiempo de ejecuci on,
pues en ocasiones comporta la reserva de nueva memoria y la liberaci on de la usada hasta
el momento. Cuando se conoce el tama no m aximo de la lista puede resultar conveniente
efectuar una reserva previa de toda la memoria que ha de ocupar. Podemos hacerlo con
el operador de repetici on, que se representa con un asterisco:
an array = [0] * 10
an array[5] = 3
print(an array)
[0, 0, 0, 0, 0, 3, 0, 0, 0, 0]
Una forma elegante de construir listas en Python es mediante la denominada com-
prensi on de listas, que imita la notaci on matem atica de descripci on de conjuntos. El
conjunto A = {i
2
0 i < 10}, por ejemplo, puede expresarse con este vector:
print([ i**2 for i in [0, 1, 2, 3, 4, 5] ])
[0, 1, 4, 9, 16, 25]
La funci on range(n) proporciona la secuencia de enteros entre 0 y n 1, as que re-
sulta c omoda al denir listas por comprensi on:
28 de septiembre de 2009 Ap endice A. Python 343
print([ i**2 for i in range(6) ])
[0, 1, 4, 9, 16, 25]
Se pueden utilizar condiciones complejas a la hora de decir qu e elementos forman
parte de una lista. Por ejemplo, el conjunto A = {i
2
i es par, 0 i < 10} puede expre-
sarse con este vector:
print([ i**2 for i in range(10) if i % 2 == 0])
[0, 4, 16, 36, 64]
El operador de corte permite seleccionar una serie de elementos contiguos o equies-
paciados en la lista. El rango de ndices de los elementos seleccionados se indica entre
corchetes y separados por dos puntos. El corte a[i:j] para un vector a es el vector forma-
do con los elementos a[i], a[i+1], . . . , a[j-1]. N otese que se excluye el elemento a[j]. Si se
usan tres valores en un corte, el tercero indica el incremento con el que se consideran los
elementos. El corte a[i:j:k] es el vector formado con los elementos a[i], a[i+k], a[i+2*k]
. . . , a[i+n*k], donde n es el mayor entero tal que i + nk < j:
an array = [0, 2, 4, 6, 8, 10, 12]
print(an array, an array[2:4], an array[2:8:2], an array[1:-1], an array[6:0:-2])
[0, 2, 4, 6, 8, 10, 12] [4, 6] [4, 8, 12] [2, 4, 6, 8, 10] [12, 8, 4]
Algunos elementos del corte pueden omitirse y tomar valor por defecto. El primer
ndice vale 0 si no se explicita y la omisi on del segundo hace que sea igual a la longitud
de la lista:
an array = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
print(an array[:6], an array[6:], an array[:])
[0, 1, 2, 3, 4, 5] [6, 7, 8, 9] [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
En la tabla 3.1 se recogen algunos operadores y los m etodos de uso com un con listas
Python (acompa nados de sus respectivos costes temporales).
Tuplas
Python ofrece, adem as, la posibilidad de trabajar con tuplas, que son listas inmutables.
Sint acticamente, los literales de tupla se expresan como los de las listas, pero con par ente-
sis en lugar de corchetes. S olo hay un problema: la tupla de un solo elemento se puede
confundir con una expresi on entre par entesis, as que requiere a nadir una coma tras su
unico elemento para romper la ambig uedad:
344 Apuntes de Algoritmia 28 de septiembre de 2009
a tuple = (1, 2, 3, 5, 7, 11)
empty tuple = ()
unitary tuple = (1,)
Una ultima observaci on: salvo para la tupla vaca, los par entesis son opcionales:
a tuple = 1, 2, 3, 5, 7, 11
empty tuple = ()
unitary tuple = 1,
Podemos usar los operadores (indexaci on, corte, etc.) y m etodos de las listas que no
modican su contenido. Podemos, por ejemplo, concatenar dos tuplas (se genera una
nueva tupla resultante de unir ambas), pero no podemos usar el m etodo extend (modi-
cara el contenido de la tupla, que es inmutable).
Se pueden efectuar asignaciones m ultiples o intercambios del valor de dos variables
mediante expresiones idiom aticas de Python que hacen uso de tuplas:
a, b = 1, 2
a, b = b, a
print(a, b)
2 1
Cadenas
Las cadenas Python pueden encerrarse entre comillas simples o dobles. En principio,
sigue las reglas de C para expresar caracteres especiales, es decir, usa la barra invertida
como secuencia de escape:
print(una\ncadena)
a = "otra\ncadena"
print(a)
una
cadena
otra
cadena
Una cadena que se abre con triples comillas (simples o dobles) puede ocupar varias
lneas:
print(una
cadena que ocupa
varias lneas)
una
cadena que ocupa
varias lneas
28 de septiembre de 2009 Ap endice A. Python 345
No hay un tipo de datos car acter: un car acter es una cadena de longitud unitaria.
Podemos acceder a un car acter con el operador de indexaci on y a una subcadena de
cualquier longitud con el operador de corte.
Las cadenas son inmutables. Si se desea modicar el contenido de una cadena se
debe generar otra a partir de la primera.
Es posible concatenar cadenas con el operador + y repetir una cadena con el opera-
dor *:
print(cadena + cadena, cadena * 2)
cadenacadena cadenacadena
Las cadenas disponen de un m etodo format pata interpolar en la cadena valores de
expresiones que se suministran como argumentos. Cada marca {} se sustituye por un
argumento:
a = 3
print(Valor: {} {}.format(a, a+10))
Valor: 3 13
Se puede alterar el orden de interpolaci on encerrando un n umero entre las llaves (0
para el primer argumento, 1 para el segundo, etc.):
a = 3.2
print(Dos valores: {1} y {0}.format(a, 4))
Dos valores: 4 y 3.2
Se pueden poner marcas que controlan el aspecto de la cadena, como el n umero de-
cimales en un n umero real:
a = 3.212
print({0:.1f} {0:.4f} {0}.format(a, 4))
3.2 3.2120 3.212
El lector interesado puede consultar los detalles acerca del control del formato en el
manual de Python.
Si deseamos obtener una lista de palabras podemos usar el m etodo split:
print(una frase corta.split())
[una, frase, corta]
El m etodo split considera que el separador es una secuencia de blancos, pero permite
especicar un separador alternativo:
346 Apuntes de Algoritmia 28 de septiembre de 2009
print(una:frase::corta.split(:))
[una, frase, , corta]
El m etodo strip elimina los blancos iniciales y nales de una cadena, y lstrip y rstrip,
s olo los iniciales y nales, respectivamente:
print(| + una cadena .strip() + |)
|una cadena|
Diccionarios
Los diccionarios permiten asociar valores con claves. Una clave es cualquier objeto inmu-
table (lo son los escalares, las tuplas y las cadenas, pero no las listas). Se puede acceder al
valor asociado a una clave con el operador de indexaci on. En Python, los diccionarios se
implementan mediante tablas de dispersi on.
Los literales de diccionario son una sucesi on de pares (tuplas) clave-valor separados
por comas y encerrados entre llaves. Los componentes de cada par clave-valor se separan
entre s con dos puntos:
a dict = {a: 1, b: 2, c: 3}
empty dict = {}
print(Valor asociado a la clave "a" del diccionario:, a dict[a])
Valor asociado a la clave "a" del diccionario: 1
En la tabla 3.4 se recogen algunos operadores y m etodos de uso com un con dicciona-
rios (acompa nados de sus respectivos costes temporales).
Conjuntos
Python ofrece una implementaci on de conjuntos basada en tablas de dispersi on. La fun-
ci on set construye un conjunto a partir de una secuencia:
a = set([1, 3, 4])
print(a)
{1, 3, 4}
Tambi en hay un literal de conjunto: la sucesi on de elementos separados por comas y
encerrada entre llaves:
28 de septiembre de 2009 Ap endice A. Python 347
a = {1, 3, 4}
print(a)
{1, 3, 4}
El literal {} no representa al conjunto vaco, sino al diccionario vaco. El conjunto
vaci o debe representarse con set().
Los conjuntos soportan operaciones de uni on, intersecci on, pertenencia, etc.:
a = {1, 3, 4}
b = {2, 3, 5}
print(a.union(b), a.intersection(b), 3 in a)
{1, 2, 3, 4, 5} {3} True
En la tabla 3.5 se muestran algunos operadores y m etodos para conjuntos Python.
Los datos de tipo set son mutables y, por tanto, no pueden usarse como claves en un
diccionario. Existe un tipo especial para representar una conjunto inmutable: frozenset.

En ingl es, frozen signica congelado, as que un frozenset es un


conjunto congelado, es decir, un conjunto que ya no puede alterarse.
A.4.4. Coerci on
Hay reglas de promoci on autom atica de tipos que permiten operar entre, por ejemplo,
un entero y un otante. El resultado es siempre del tipo m as general.
Es posible convertir datos de un tipo en otro mediante ciertas funciones predenidas.

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

No hay un claro consenso acerca de si Nincluye o no al valor 0. En algunos


textos se incluye y en otros no. Por otra parte, conjuntos como R
>0
suelen
denotarse con R
+
, pero nosotros reservamos el superndice + para representar
clausuras positivas.
Un multiconjunto es una colecci on de elementos posiblemente repetidos. Una k-
tupla es un multiconjunto ordenado, es decir, en el que importa el orden de los elementos,
de tama no k. Notaremos con (x
1
, x
2
, . . . , x
k
) a la k-tupla formada por los elementos x
1
, x
2
,
. . . , x
k
en ese orden. El valor k es la aridad de la tupla. Una tupla de aridad 2 es un par,
una de aridad 3 es una terna y una de aridad 4 es una cuadrupla.
Dados dos conjuntos A y B, su producto cartesiano es
A B = {(a, b) a A, b B}. (B.2)
El plano real, por ejemplo, se dene como R R. Cada par (x, y) dene un punto o
coordenada del plano.
Un intervalo es un conjunto de n umeros reales comprendidos entre otros dos. Un
intervalo abierto excluye los valores de los lmites: ]a, b[= {x R a < x < b}. Un
intervalo cerrado los incluye: [a, b] = {x R a x b}. Un intervalo semi-abierto
incluye a uno pero no al otro: [a, b[= {x R a x < b}; ]a, b] = {x R a < x b}.
Un intervalo entero es un conjunto de n umeros enteros entre otros dos: [i..j] = {k
Z i k j}.

La notaci on de los intervalos cerrados colisiona con la propia de las listas


Python. Por ejemplo, en Python, [2,3] es la lista formada por el valor 2
seguido del valor 3. El contexto debera ser suciente para saber si hablamos de
intervalos cerrados o de listas Python.
B.3. Relaciones binarias
Una relaci on binaria R sobre dos conjuntos A y B es un subconjunto del producto car-
tesiano A B. Si el par (a, b) R, escribimos a Rb y si (a, b) / R, escribimos a Rb. Si
decimos que R es una relaci on binaria sobre un conjunto A queremos decir que es un
subconjunto de A A.
Un ejemplo de relaci on binaria sobre los naturales es la relaci on es menor que.
Dicha relaci on es el conjunto {(a, b) a, b Ny a < b}.
Una relaci on binaria R A A es:
Reexiva si a R a para todo a A.
Irreexiva si a R a para todo a A.
Sim etrica si a Rb implica b R a para todo a, b A.
Asim etrica si a Rb implica b R a para todo a, b A.
Antisim etrica si a Rb y b R a implica a = b, para todo a, b A.
382 Apuntes de Algoritmia 28 de septiembre de 2009
Transitiva si a Rb y b Rc implica a Rc para todo a, b, c A.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
182 Indica cu ales de las anteriores propiedades satisfacen las siguientes relaciones binarias so-
bre los n umeros naturales:
a) es igual que.
b) es distinto de.
c) es menor que.
d) es mayor que.
e) es menor o igual que.
f) es mayor o igual que.
g) R = {(a, b) a, b Ny a = b 1}.
h) R = {(a, b) a, b Ny a m od b = 5}.
(Nota: m od es el operador m odulo, es decir, el resto de la divisi on entera.)
183 Determina las propiedades que satisfacen las relaciones binarias y sobre el domi-
nio de los conjuntos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Una relaci on binaria reexiva, sim etrica y transitiva es una relaci on de equivalencia.
Si R es una relaci on de equivalencia sobre A, denominamos clase de equivalencia de
a A al subconjunto [a] = {b A a Rb}. Una relaci on de equivalencia sobre A
particiona al conjunto A en un subconjunto por clase de equivalencia.
Por ejemplo, imagina un conjunto con diferentes polgonos de 3, 4, 5 y 6 lados y
colores rojo, verde o azul. La relaci on de equivalencia son del mismo color particiona
el conjunto en tres clases de equivalencia, una por cada color. Los elementos de cada
clase pueden tener diferente n umero de lados, pero no distinto color. Otra relaci on de
equivalencia es tiene el mismo n umero de lados y divide el conjunto original en 4
clases de equivalencia, una por n umero de lados. Los elementos de cada clase tienen el
mismo n umero de lados, pero es posible que tengan colores distintos.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
184 Sea R la relaci on de equivalencia congruencia m odulo 5 sobre los n umeros naturales, es
decir, a Rb si y s olo si a m od 5 = b m od 5.
a) Demuestra que es una relaci on de equivalencia.
b) Cu antas clases de equivalencia induce la relaci on R en el conjunto de los naturales?
c) Cu antos elementos hay en cada clase?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Una relaci on binaria reexiva y transitiva es un preorden o cuasiorden. Una relaci on
binaria reexiva, antisim etrica y transitiva es un orden (parcial). Una relaci on binaria
irreexiva, asim etrica y transitiva es un orden estricto. Por ejemplo, la comparaci on entre
enteros < es un orden estricto, pero no lo es la comparaci on .
Solemos usar el smbolo para denotar una relaci on de orden. Un ordenamiento es
un par (A, ) donde A es un conjunto y un orden. Si a, b A y a b decimos que a
es menor que b en el orden o que a precede a b.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 383
Un elemento minimal de un ordenamiento (A, ) es un elemento de A que no es
precedido por ning un otro elemento de A. Un elemento mnimo es un elemento de A
que precede a cualquier otro elemento A. (Se dene de forma an aloga elemento maximal
y m aximo.)
Un ordenamiento en el que todo par de elementos est a relacionado, es decir, a b o
b a para todo a, b A, es un orden total o lineal. En un orden total s olo puede haber
un elemento minimal (maximal) que es tambi en elemento mnimo (m aximo). En el caso
de (N, <), por ejemplo, el 0 es el unico elemento minimal o mnimo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
185 De todas las relaciones del ejercicio 182, indica cu ales corresponden a ordenamientos par-
ciales, estrictos o totales.
186 Denimos sendas relaciones en el conjunto NNcomo sigue:
a) (n, m) (n

, 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

Otra forma de vericar este predicado es con esta gura:


El cuadrado de trazo negro que contiene n
2
baldosas se forma a nadiendo
2n +1 baldosas al que tiene (n 1)
2
.
Los principios de inducci on d ebil y completa son equivalentes. La diferencia entre
ellos estriba en la naturaleza de la hip otesis de inducci on. En la inducci on fuerte se asu-
me que el predicado es cierto para todos los valores menores que un n + 1 arbitrario,
mientras que en la d ebil se asume unicamente que es cierto para el valor anterior a n +1.

El principio de inducci on completa funciona con efecto domin o. Es como


si dispusieras una la de chas de domin o innita por uno de sus extremos
separadas de tal modo que al caer una empuje a la siguiente. Al tumbar la primera
cha, la segunda se ver a empujada y, a su vez, empujar a a la tercera. No importa lo
lejos del origen que est e cualquier cha: m as tarde o m as temprano ser a empujada
por la anterior. . . y empujar a a la siguiente. Obs ervese que todo depende del caso
base: si no cae primero la primera cha, no caer a ninguna.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
187 Demuestra por inducci on que, para todo n > 0, se cumple
2
0
+2
1
+2
2
+ . . . +2
n
= 2
n+1
1.
188 Demuestra por inducci on que, para todo n entero tal que n > 4, se cumple n
2
< 2
n
.
189 El principio de inducci on debe manejarse con cautela. D onde se comete un error en esta
demostraci on de la proposici on todos los caballos son del mismo color?
Base de inducci on: Todos los conjuntos formados por un solo caballo cumplen que todos
sus integrantes son del mismo color.
Hip otesis de inducci on: Supongamos que todos los conjuntos con n caballos s olo contienen
caballos del mismo color.
Paso de inducci on: Dividamos un conjunto A con n + 1 caballos en dos subconjuntos, A
1
y A
2
, de modo que A
1
A
2
= A, A
1
A
2
= , A
1
n y A
2
n. Por hip otesis de
inducci on todos los caballos de A
1
son del mismo color y todos los caballos de A
2
son del
mismo color. Como hay al menos un caballo que est a en A
1
y A
2
, todos los caballos de
ambos conjuntos son del mismo color.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

El ejercicio de los caballos del mismo color y su falsa demostraci on fueron


propuestos por Gy orgy P olya.
388 Apuntes de Algoritmia 28 de septiembre de 2009
B.6.2. Demostraci on por contradicci on
En la demostraci on por contradicci on o reducci on al absurdo se asume que el predicado
que se desea probar es falso y se demuestra que esta asunci on implica que alguna pro-
piedad conocida es falsa y que, por tanto, la asunci on original de que el predicado era
falso es err onea.
Un ejemplo cl asico es la demostraci on de que existen innitos n umeros primos.
Para demostrarlo, asumiremos que esto es falso y que, por tanto, existe un n umero primo
p
k
que es el mayor de todos. Sean p
1
, p
2
, p
3
, . . . , p
k
todos los n umeros primos mayores
que uno y en orden creciente, y consideremos el n umero n
n = (p
1
p
2
p
3
p
k
) +1 .
Claramente, n es mayor que p
k
, as que, por la asunci on de que p
k
es el mayor n umero
primo, n no es primo. Sin embargo, ninguno de los n umeros p
1
, p
2
, p
3
, . . . , p
k
divide a n
exactamente, porque siempre habr a un resto igual a 1. Hemos llegado a una contradic-
ci on, porque todo n umero es, o bien primo, o bien un producto de primos! Por lo tanto,
la asunci on original de que p
k
era el n umero primo mayor es falsa, lo que implica que el
predicado es verdadero: hay innitos n umeros primos.
B.6.3. Demostraci on por contraejemplo
Una forma de demostrar que algo es falso es dando un contraejemplo. Si, por ejemplo,
nos piden demostrar que el cuadrado de un n umero es siempre par basta con calcular
el cuadrado de 5, que es 25, para constatar que la propiedad es falsa. Es suciente con
que no sea cierta en un caso particular (un contraejemplo) para demostrar su falsedad.
B.7. Las funciones enteras techo y suelo
La funci on suelo o parte entera devuelve el entero m as pr oximo menor o igual que el
argumento. El valor de tierra de x se denota con x:
x = m ax{n Z : n x}. (B.3)
La funci on techo devuelve el entero m as pr oximo mayor o igual que el argumento. El
valor de techo de x se denota con x:
x = mn{n Z : n x}. (B.4)
De la denici on es evidente que se cumple
x 1 < x x x < x +1, (B.5)
o que
x +1 = x +1,
x +1 = x +1.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 389
Si n es un entero,

n
2

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
}.

Aunque denimos las sucesiones como funciones de N en R, presentare-


mos sucesiones que son funciones de Z
0
en R, o sea, sucesiones cuyo
primer t ermino es a
0
. No hay problema: basta con desplazar la sucesi on haciendo
a

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
).

Este m etodo consistente en sumar la serie de valores con una versi on


invertida de ella misma se atribuye al matem atico Gauss. Se dice que,
siendo Gauss ni no, su profesor castig o a la clase haci endoles sumar los 100 pri-
meros n umeros. Gauss acab o la tarea casi al instante al descubrir este ingenioso
procedimiento.
B.9.4. Sumatorio de una progresi on geom etrica
Por otra parte, la suma de los n primeros t erminos de una progresi on geom etrica con
raz on r = 1 es

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)

Se puede ver gr acamente que


1in
i =
n(n+1)
2
con esta gura:
El n umero de cuadrados es
1in
i. Si duplicamos la gura e invertimos una
de las copias, tenemos este rect angulo:
El area es de n(n + 1) cuadrados. Como hay el doble que en el sumatorio, el
n umero de cuadrados de la primera gura es n(n +1)/2.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
192 Demuestra por inducci on que la suma de los n primeros t erminos de una progresi on aritm eti-
ca {a
i
} es
n
2
(a
1
+ a
n
).
193 Demuestra por inducci on que la suma de los n primeros t erminos de una progresi on geom etri-
ca {a
i
} de raz on r es a
1
r
n
1
r1
.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 393
194 Demuestra por inducci on las igualdades (B.33B.38) y las desigualdades (B.39) y (B.40).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.9.6. Los n umeros de Fibonacci
La secuencia 0, 1, 1, 2, 3, 5, 8, 13, 21, 34, . . . , aparece con frecuencia. La secuencia se conoce
por el nombre de n umeros de Fibonacci o secuencia de Fibonacci y se genera as:
F(0) = 0; F(1) = 1; F(n) = F(n 1) + F(n 2), n 2. (B.41)
(N otese que la denici on de F(n) hace referencia a F(n1) y F(n2): es una recurrencia.
Estudiaremos las recurrencias m as adelante.)
Hay una forma cerrada (f ormula de De Moivre) para los n umeros de Fibonacci:
F(n) =
1

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).

En ingl es, la expresi on O( f (n)) se lee big-oh of f (n), es decir, o


may uscula de f (n).
La gura B.4 ilustra la idea de que una funci on f (n) sea orden de otra, g(n): existe un
valor c tal que inicialmente f (n) puede ser mayor o menor que c veces g(n), pero a partir
de cierto valor n
0
, f (n) es menor o igual que c g(n).
398 Apuntes de Algoritmia 28 de septiembre de 2009
Figura B.4: f (n) es O(g(n)) porque, para n mayor o
igual que un n
0
dado y para un c positivo determinado,
f (n) c g(n).
n
c g(n)
f (n) O(g(n))
n
0
Unos ejemplos ayudar an a entender el concepto de orden:
La funci on f (n) = n +1, por ejemplo, pertenece a O(n), pues siempre hay un valor
n
0
y un valor c para los que n + 1 c n si n n
0
. Consideremos, por ejemplo,
n
0
= 1 y c = 2: n + 1 es menor o igual que 2n para todo valor de n mayor o igual
que 1.
La funci on f (n) = 5n + 12 tambi en es O(n). Se observa 5n + 12 6n para todo n
mayor o igual que 12.
La funci on f (n) = 4n
2
+2n +1 es O(n
2
). El valor de f (n) es menor o igual que 5n
2
para todo n mayor o igual que 3.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
201 Son ciertas las siguientes armaciones?
a) 3n +2 O(n).
b) 100n +6 O(n).
c) 10n
2
+4n +2 O(n
2
).
d) 10n
2
+4n +2 O(n
3
).
e) 6 2
n
+ n
2
O(2
n
).
f) 6 2
n
+ n
2
O(n
100
).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Si una funci on f (n) pertenece a O(n), por ejemplo, tambi en pertenece a O(n
2
), ya que
O(n) O(n
2
). De hecho, hay una relaci on de inclusi on entre las diferentes familias de
funciones:
O(1) O(lg n) O(

n) O(n) O(n lg n) O(n


2
) O(n
3
) O(2
n
) O(n
n
).
(Evidentemente, esta relaci on no es exhaustiva.) Para evitar ambig uedades se debe in-
dicar el orden de una funci on con su cota m as ajustada: cuando expresamos el orden
O(g(n)) de una funci on f (n), usamos la funci on g(n) que m as se ajusta a f (n). As, de
f (n) = 5n no decimos que es O(n
2
) (aunque, evidentemente, lo es), sino que es O(n).

No te obsesiones con la base de los logaritmos al usar la notaci on orden:


recuerda que los logaritmos con base diferente son proporcionales, ya que
log
b
x = log
b
a log
a
x si a, b > 0. Como log
b
a es una constante positiva para a > 1
y b > 0, tenemos que log
a
x es O(log
b
x) (y viceversa).
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 399
La manipulaci on de cotas es fundamental para la estimaci on de costes temporales y
espaciales de los algoritmos. Pasamos a presentar una serie de resultados que nos permi-
tir an clasicar r apidamente una funci on de Nen R
0
en el orden al que pertenece:
Regla de la reexividad f (n) O( f (n)).
Regla de la constante O(c f (n)) = O( f (n)) para c > 0.
Regla de los m aximos O( f (n) + g(n)) = O(m ax{f (n), g(n)})
Regla de la transitividad Si f (n) O(g(n)) y g(n) O(h(n)), entonces f (n) O(h(n)).
Regla del producto Si f
1
(n) O(g
1
(n)) y f
2
(n) O(g
2
(n)), entonces f
1
(n) f
2
(n)
O(g
1
(n) g
2
(n)).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
202 Demuestra que todo polinomio de n de orden m, f (n) = a
m
n
m
+ + a
1
n
1
+ a
0
es O(n
m
).
203 Determina el orden de estas funciones:
1) f (n) = 4.
2) f (n) = 0.1.
3) f (n) = 108374.
4) f (n) = n.
5) f (n) = 10n.
6) f (n) = 0.028764n.
7) f (n) = n.
8) f (n) = n +3.
9) f (n) = 2n +1093.
10) f (n) = n
2
.
11) f (n) = 4n
2
.
12) f (n) = n
2
+ n.
13) f (n) = 8n
2
+ n.
14) f (n) = 0.1n
2
+2n.
15) f (n) = 20n
3
n.
16) f (n) = n
6
+10n
3
.
17) f (n) = n
100
+ n
99
.
18) f (n) = 2
n
.
19) f (n) = 2
n
+ n
100
.
20) f (n) = 3
n
+2
n
.
21) f (n) = lg n.
22) f (n) = log n.
23) f (n) = n +lg n.
24) f (n) = n log n.
25) f (n) = n
2
+ n log n.
26) f (n) = n log n
2
.
27) f (n) = n
2
lg n.
28) f (n) = n
2
+ n
2
ln n.
29) f (n) = ln n
2
.
30) f (n) = n lg n
3
.
31) f (n) =
{
3n, si n par;
8n
2
, si no.
32) f (n) =
{
3n, si n par;
8n, si no.
33) f (n) =
{
n
2
, si n < 10;
3n, si n 10.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Las funciones que pertenecen a cada familia de funciones tienen adjetivos que las
identican, como se muestra en la tabla B.2. As, decimos que el coste temporal de un
algoritmo es lineal cuando su tiempo de ejecuci on o n umero de pasos es O(n) y c ubico
cuando es O(n
3
). De hecho, abusando del lenguaje decimos que el algoritmo es de tiempo
lineal o c ubico, respectivamente.
400 Apuntes de Algoritmia 28 de septiembre de 2009
Tabla B.2: Adjetivos con los que
se calica a las funciones perte-
necientes a algunos ordenes.
Sublineales
Constantes O(1)
Logartmicas O(log n)
Polilogartmicas O(log
k
n)
O(

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

< n (hip otesis de inducci on). En particular, T(n/2)


2 n/2 lg(n/2).
T(n) = 2T(n/2) + n
4 n/2 lg(n/2) + n
2n lg(n/2) + n
= 2n(lg(n) lg(2)) + n
= 2n lg(n) 2n + n
2n lg(n).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
209 Demuestra por inducci on que
T(n) =
{
1, si n = 1;
2T(n/2) +1, si n > 1;
es O(n).
210 Demuestra por inducci on que
T(n) =
{
1, si n = 1;
T(n 1) +1, si n > 1;
es O(n).
211 Demuestra por inducci on que
T(n) =
{
1, si n = 1;
T(n 1) + n, si n > 1;
es O(n
2
).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
B.14.3. Los n umeros de Catalan
Los n umeros de Catalan se denen como
C(n) =
1
n +1
(
2n
n
)
=
(2n)!
(n +1)!n!
, (B.54)
406 Apuntes de Algoritmia 28 de septiembre de 2009
o recursivamente como
C(n) =
{
1, si n = 1;
2(2n1)
n+1
C(n 1), si n > 1.
(B.55)
Los primeros n umeros de Catalan son 1, 2, 5, 14, 42, 132, 429, 1430, 4862, 16796, . . .
Los n umeros de Catalan aparecen en numerosos problemas, por ejemplo:
C(n 1) es el n umero de diferentes formas de parentizar binariamente una cadena
de n letras. Por ejemplo, la cadena abcd puede parentizarse binariamente de estas
formas: ((ab)c)d, (a(bc))d, (ab)(cd), a((bc)d), a(b(cd)).
C(n) es el n umero de arboles binarios con n nodos internos. (V ease la gura B.7.)
C(n) es el n umero de formas de dividir un (n + 2)-gono en n tri angulos. (V ease la
gura B.8.)
Figura B.7:

Arboles binarios con 3 nodos
internos.

Figura B.8: Divisiones de un pent agono en tres


tri angulos.
B.15. Digrafos y grafos (no dirigidos)
B.15.1. Grafos dirigidos o digrafos
Un grafo dirigido o digrafo G es una tupla (V, E) en la que V es un conjunto de v ertices
o nodos y E V V es un conjunto de pares ordenados llamados aristas o arcos. N otese
que E es una relaci on binaria sobre V.

Usamos las letras V y E para referirnos a los conjuntos de v ertices y aristas,


respectivamente, por los t erminos ingleses vertex y edge. Los t erminos
vienen de considerar que los polgonos y poliedros describen grafos. Considera un
cubo con sus v ertices numerados como este:
1
2
3
4
5
6
7
8
Hay aristas conectando, por ejemplo, los v ertices 1 y 2 o 4 y 6.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 407
He aqu un ejemplo de grafo:
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)}.
Podemos representar un digrafo con un diagrama en el que cada v ertice es un crculo
(o una caja) y cada arista es una echa que une dos v ertices. La gura B.9 muestra una
representaci on del grafo anterior. En ella se aprecia que hay dos aristas uniendo los v erti-
ces 0 y 3: la arista (0, 3) y la arista (3, 0). Son aristas distintas, pues el orden con el que
aparecen los v ertices es diferente (las aristas son pares ordenados).
0 1 2
3 4 5
Figura B.9: Digrafo.
El primero de los v ertices de una arista (u, v) es el v ertice de partida u origen de la
arista, y el segundo, el v ertice de llegada o destino. Decimos que la arista sale, parte o
emerge del primero y entra, llega o incide en el segundo. En el digrafo del ejemplo, los
v ertices 2 y 5 est an unidos por la arista (2, 5). El v ertice 2 es el v ertice de partida de la
arista (2, 5), y el v ertice 5 es el de llegada.
Dado un v ertice u, los v ertices v tales que (u, v) E se denominan sucesores de u o
v ertices adyacentes a u. Y dado un v ertice v, los v ertices u tales que (u, v) E se deno-
minan predecesores de v. Un v ertice sin sucesores es un sumidero y un v ertice sin pre-
decesores es una fuente. El grado de salida de un v ertice v es el n umero de sucesores que
tiene, y el grado de entrada, el n umero de predecesores. Denotaremos con out degree(u)
al grado de salida de un v ertice u y con in degree(u) a su grado de entrada. En el grafo
del ejemplo, el conjunto de sucesores del v ertice 2 es {4, 5} y su conjunto de predecesores
es el conjunto vaco. El grado de salida del v ertice 2 es, pues, 2, y el de entrada es 0.
El grado de entrada (o de salida) de un digrafo G = (V, E) es el mayor grado de
entrada (o salida) de sus v ertices:
in degree(G) = m ax
vV
in degree(v), out degree(G) = m ax
uV
out degree(u).
El grafo del ejemplo anterior tiene grados de entrada y salida id enticos; ambos valen 2.
El conjunto de v ertices de un grafo no tiene por qu e ser un subconjunto de los n ume-
ros naturales. He aqu un grafo cuyos v ertices son lenguajes de programaci on.
V = {C, C++, Java, C#, ObjC},
E = {(C, C++), (C, Java), (C++, Java), (C, C#), (Java, C#), (C++, C#), (C, ObjC)}.
Las aristas de este grafo expresan la relaci on fue tomado como base para: C fue toma-
do como base para C++, C fue tomado como base para Java, . . . La gura B.10 muestra
este grafo.
408 Apuntes de Algoritmia 28 de septiembre de 2009
Figura B.10: Grafo de lenguajes de programaci on. Cada
arista une dos lenguajes e indica que un lenguaje (el v erti-
ce de partida) fue tomado como base para el dise no de otro
(el v ertice de llegada).
C#
C
Java
ObjC
C++
B.15.2. Grafos no dirigidos
Un grafo no dirigido G es un par (V, E) donde V es un conjunto de v ertices y E es un
conjunto de pares no ordenados de v ertices distintos, es decir, E {{u, v} : u = v; u, v
V}. El conjunto E es, pues, una relaci on binaria sim etrica sobre V.

Observa la notaci on de las aristas en un grafo no dirigido: {u, v} frente a


(u, v), que es como denotamos las aristas de un digrafo. Las llaves indican
que {u, v} es un conjunto de dos elementos y, por tanto, su orden no importa: {u, v}
es equivalente a {v, u}.
Consideremos, por ejemplo, un grafo en el que los v ertices son personas y las aristas
expresan la relaci on es hermano de: si u es hermano de v, entonces v es hermano de u.
Se trata, pues, de un grafo no dirigido.
He aqu otro ejemplo de grafo no dirigido:
V = {0, 1, 2, 3, 4, 5}, E = {{0, 1}, {0, 3}, {1, 3}, {1, 4}, {2, 4}, {2, 5}, {3, 4}}.
En un digrafo representamos gr acamente las aristas con echas. En un grafo no dirigido
las aristas son simples lneas (ver gura B.11).
Figura B.11: Grafo no dirigido.
0 1 2
3 4 5
En un grafo no dirigido, dos v ertices unidos por una arista son v ertices adyacentes
y existe entre ellos, indistintamente, una relaci on de sucesi on y precedencia. En el grafo
de la gura B.11, los v ertices 2 y 5 son adyacentes y el conjunto de v ertices adyacentes al
v ertice 4 es {1, 2, 3}. No tiene sentido hablar de grado de entrada o salida de un v ertice
v, pero s de su grado, sin m as, que es la talla del conjunto de v ertices adyacentes a el y
que denotamos con degree(v). El grado de un grafo G = (V, E) es el grado del v ertice con
m as v ertices adyacentes:
degree(G) = m ax
uV
degree(u).
El grado del grafo de la gura B.11 es 3.
Un grafo no dirigido puede considerarse un caso particular de digrafo en el que
(u, v) E implica (v, u) E. Cuando en lo sucesivo nos reramos a un grafo, sin adjeti-
vos, entenderemos que se trata de un grafo dirigido.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 409
B.15.3. Grafos etiquetados y grafos ponderados
Un grafo etiquetado es un grafo G = (V, E) y una funci on f : E L que asocia a cada
arista una etiqueta, es decir, un elemento de un conjunto dado L.
Consideremos, por ejemplo, el digrafo y etiquetado G descrito por los siguientes con-
juntos V y E:
V = { Pepe, Mara, Ana, Mar},
E = {(Pepe, Mara), (Pepe, Ana), (Pepe, Mar), (Mara, Pepe), (Mara, Ana),
(Ana, Pepe), (Ana, Mara), (Ana, Mar), (Mar, Pepe), (Mar, Ana)},
y el conjunto de etiquetas
L = {esposo de, padre de, ex-esposo de, esposa de, madrastra de, hija de, hijastra de,
hija de, ex-esposa de, madre de}.
El grafo G y la funci on de etiquetado f : E L denida as:
f (Pepe, Mara) = esposo de, f (Pepe, Ana) = padre de,
f (Pepe, Mar) = ex-esposo de, f (Mara, Pepe) = esposa de,
f (Mara, Ana) = madrastra de, f (Ana, Pepe) = hija de,
f (Ana, Mara) = hijastra de, f (Ana, Mar) = hija de,
f (Mar, Pepe) = ex-esposa de, f (Mar, Ana) = madre de.
describe relaciones familiares entre cuatro personas. Cada etiqueta indica la relaci on en-
tre las dos personas que enlaza (y la direcci on de la arista importa). La gura B.12 muestra
una representaci on gr aca de G etiquetado con f .
Pepe
Mara
Ana
Mar
esposo de
padre de
ex-esposo de esposa de
madrastra de
hija de
hijastra de hija de
ex-esposa de
madre de
Figura B.12: Digrafo etiquetado. Los
v ertices son personas y las aristas est an
etiquetadas con vnculos familiares.
En ciertos problemas hemos de asociar un valor num erico a cada una de las aristas.
Un grafo que representa, por ejemplo, ciudades conectadas por carreteras necesitar a aso-
ciar a cada carretera (arista) su longitud en kil ometros si estamos interesados en efectuar
c alculos de distancias entre pares de ciudades.
Un grafo ponderado es un caso particular de grafo etiquetado. Se describe con un
grafo G = (V, E) y una funci on d : E R que asigna un peso num erico a cada arista y
a la que denominamos funci on de ponderaci on. Notaremos un grafo ponderado por d
con G = (V, E, d).
410 Apuntes de Algoritmia 28 de septiembre de 2009
El grafo no dirigido denido como sigue modela un mapa de carreteras entre algunas
de las principales ciudades de la isla de Mallorca.
V = {Alc udia, Andratx, Art` a, Calvi` a, Campos del Port, Capdepera, Inca,
Llucmajor, Manacor, Marratx, Palma de Mallorca, Pollen ca, Santany, S oller},
E = {{Alc udia, Art` a}, {Alc udia, Inca}, {Alc udia, Pollen ca}, {Andratx, Calvi` a},
{Andratx, Palma de Mallorca}, {Andratx, S oller}, {Art` a, Capdepera},
{Art` a, Manacor}, {Calvi` a, Palma de Mallorca}, {Campos del Port, Llucmajor},
{Campos del Port, Santany}, {Inca, Manacor}, {Inca, Marratx},
{Llucmajor, Palma de Mallorca}, {Manacor, Santany},
{Marratx, Palma de Mallorca}, {Pollen ca, S oller}}.
La siguiente funci on de ponderaci on asocia a cada conexi on entre dos ciudades la dis-
tancia kilom etrica de la carretera que las une.
d(Alc udia, Art` a) = 36, d(Alc udia, Inca) = 25,
d(Alc udia, Pollen ca) = 10, d(Andratx, Calvi` a) = 14,
d(Andratx, Palma de Mallorca) = 30, d(Andratx, S oller) = 56,
d(Art` a, Capdepera) = 8, d(Art` a, Manacor) = 17,
d(Calvi` a, Palma de Mallorca) = 14, d(Campos del Port, Llucmajor) = 14,
d(Campos del Port, Santany) = 13, d(Inca, Manacor) = 25,
d(Inca, Marratx) = 12, d(Llucmajor, Palma de Mallorca) = 20,
d(Manacor, Santany) = 27, d(Marratx, Palma de Mallorca) = 14,
d(Pollen ca, S oller) = 54.
La gura B.13 muestra una representaci on gr aca de G. Los pesos aparecen cerca de
las respectivas aristas.
Un grafo es ponderado positivo si los pesos son valores positivos o nulos, es decir,
si d es una funci on d : E R
0
. Un mapa de carreteras, por ejemplo, puede modelarse
con un grafo ponderado positivo.
Como encontraremos con frecuencia sistemas que, al modelarse con un grafo ponde-
rado, asignan a cada arista una distancia, es frecuente llamar distancia de una arista a
su peso. De hecho, normalmente usamos la letra d para la funci on de ponderaci on porque
recuerda el t ermino distancia.

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?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

El problema de los puentes de K onigsberg fue propuesto por Euler en 1736


y de el reciben nombre los caminos eulerianos.
B.15.5. Subgrafos
Un grafo G

= (V

, E

) es un subgrafo de un grafo G = (V, E) si V

V y E

E. Dado
un grafo G = (V, E) y un subconjunto V

de V, el subgrafo de G inducido por V

es G

=
(V

, {(u, v) E u, v V

)}). La gura B.15 muestra (a) un grafo, (b) un subgrafo suyo


y (c) el subgrafo inducido por un subconjunto de v ertices. Del mismo modo podemos
denir el subgrafo G

= (V

, E

) inducido por un conjunto de aristas E

E, que es
G

= ({u v : (u, v) E

)} {v u : (u, v) E

)). La gura B.15 (d) muestra un


subgrafo del grafo de la gura B.15 (a) inducido por un subconjunto de aristas.
0 1 2
3 4 5
0 1 2
3 4
(a) (b)
0 1
3 4
0 1
3 4 5
(c) (d)
Figura B.15: (a) Digrafo G. (b) Un subgrafo de
G. (c) El subgrafo de G inducido por los v ertices
{0, 1, 3, 4}. (d) El subgrafo de G inducido por las aristas
{(0, 3), (1, 4), (3, 1), (5, 5)}.
414 Apuntes de Algoritmia 28 de septiembre de 2009
Un subgrafo completo, es decir, con todos sus v ertices directamente conectados a
todos los dem as v ertices es un clique.
B.15.6. Conectividad
Un v ertice w es alcanzable desde un v ertice u si existe un camino v
1
v
2
. . . v
n
tal que v
1
= u
y v
n
= w. Un grafo no dirigido es conexo si cualquier par de v ertices est a unido por un
camino, es decir, si todo v ertice es alcanzable desde cualquier otro. Los componentes
conexos de un grafo no dirigido son los conjuntos de v ertices alcanzables dos a dos.
En la gura B.16 se muestra un ejemplo de grafo no dirigido y no conexo con dos
componentes conexos.
Figura B.16: Grafo no dirigido y no conexo. El grafo contiene dos componentes conexos,
{0, 1, 3, 4} y {2, 5}, que mostramos rodeadas por sendas lneas discontinuas.
0 1 2
3 4 5
Un digrafo es d ebilmente conexo si entre todo par de nodos u y v hay un camino
que une u con v o v con u. Un digrafo es fuertemente conexo si todo par de v ertices es
mutuamente alcanzable. Los componentes fuertemente conexos de un digrafo son los
conjuntos de v ertices mutuamente alcanzables dos a dos. La relaci on de alcanzabilidad
mutua es una relaci on de equivalencia, as que particiona el conjunto de v ertices. Cada
una de las particiones es un componente fuertemente conexo.
En la gura B.17 (a), aparece un digrafo que no es fuertemente conexo: no es posible,
por ejemplo, alcanzar el v ertice 4 desde el v ertice 5. El de la gura B.17 (b) s lo es.
Figura B.17: (a) Digrafo d ebilmente conexo. (b) Digrafo fuer-
temente conexo.
0 1 2
3 4 5
0 1 2
3 4 5
(a) (b)
B.15.7. Densidad
Un grafo G = (V, E) es denso si casi todo par de v ertices est a unido por una arista, es
decir, si el n umero de aristas, E, es pr oximo a V
2
en el caso de los grafos dirigidos y
a V (V 1)/2 en el caso de los no dirigidos. Un grafo es completo si todo v ertice
est a unido al resto de v ertices. Un grafo es disperso cuando E es mucho menor que
V
2
.
En la gura B.18 se muestra, a mano izquierda, un grafo completo no dirigido y, a
mano derecha, un grafo completo dirigido.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 415
(a) (b)
Figura B.18: (a) Grafo no dirigido y
completo. (b) Digrafo completo.
B.15.8. Multigrafos
Un multigrafo es un grafo en el que el conjunto de aristas es un multiconjunto, es decir,
una colecci on en la que puede haber elementos repetidos. En un multigrafo puede existir,
pues, m as de una arista entre dos v ertices. La gura B.19 muestra un multigrafo.
0 1 2
3 4 5
Figura B.19: Multigrafo.
B.15.9. Algunos tipos especiales de grafo
Ciertos grafos presentan una estructura peculiar y nos interesa estudiarlos porque surgen
de forma natural al modelar ciertos problemas y/o permiten obtener versiones especia-
lizadas (m as ecientes) de ciertos algoritmos.
Digrafos acclicos
Un digrafo es acclico si no contiene ciclos. Los digrafos acclicos se conocen en la li-
teratura con el t ermino dag. El grafo de los lenguajes de programaci on (gura B.10,
p agina 408), por ejemplo, es un grafo acclico.
Determinar si un grafo es o no acclico presenta inter es, ya que ciertos algoritmos s olo
funcionan sobre grafos acclicos. Por ejemplo, cuando deseemos resolver el problema
del camino m as corto en un grafo, deberemos seleccionar un algoritmo de un cat alogo
y el m as eciente de ellos s olo es aplicable si el grafo es acclico.
Grafos multietapa
Un grafo G = (V, E) es multietapa si es posible particionar su conjunto de v ertices V en
k conjuntos, es decir V = V
1
V
2
V
k
, donde V
i
V
j
= para todo i = j, y todas las
aristas (u, v) E que tienen origen en una etapa i tienen destino en la etapa i +1.
416 Apuntes de Algoritmia 28 de septiembre de 2009
La gura B.20 muestra una representaci on gr aca del grafo multietapa G = (V, E)
donde:
V = {0, 1, 2, 10, 11, 20, 21, 22, 23, 24, 30, 40, 41, 42},
E = {(0, 10), (0, 11), (1, 10), (2, 10), (2, 11), (10, 20), (10, 22), (10, 23), (10, 24),
(11, 20), (11, 21), (11, 22), (20, 30), (21, 30), (23, 30), (30, 40), (30, 41), (30, 42)}.
El grafo consta de 5 etapas:
V
1
= {0, 1, 2}, V
2
= {10, 11}, V
3
= {20, 21, 22, 23, 24},
V
4
= {30}, V
5
= {40, 41, 42}.
Figura B.20: Grafo multietapa. Los v ertices se han representado
de forma que cada columna corresponde a una etapa.
0
1
2
10
11
20
21
22
23
24
30 40
41
42

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

) a partir de uno dirigido G =


(V, E) eliminando la direcci on de cada arista, es decir, deniendo E

como el conjunto
{{u, v}(u, v) E (v, u) E}. Decimos que G

es el grafo no dirigido que subyace a G.


Un arbol dirigido es un digrafo cuyo grafo subyacente es un arbol. Un arbol (dirigido)
con raiz es un arbol dirigido con un v ertice distinguido al que llamamos nodo raz y tal
que todo v ertice es alcanzable desde el.
En un arbol (dirigido) con raz cada v ertice es una hoja o un nodo interno. Las hojas
presentan un grado de salida nulo y los nodos internos presentan un grado de salida
igual o superior a 1. En una arista (u, v) decimos que u es el nodo padre de u y v es un
nodo hijo de u. Los nodos hoja no tienen hijos y el nodo raz es el unico nodo que no
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 417
tiene padre. Todos los hijos de un mismo nodo son nodos hermanos. La aridad de un
nodo es su grado de salida y la aridad de un arbol dirigido con raz es la aridad del nodo
con mayor n umero de hijos. Extendiendo la terminologa de relaciones familiares, todos
los nodos alcanzables desde uno determinado mediante la relaci on es nodo hijo de son
sus descendientes, y todos los alcanzables mediante la relaci on es nodo padre de son
sus ascendientes. Todo nodo de un arbol dirigido con raz, excepto el nodo raz, es un
descendiente del nodo raz.
N otese que el mero hecho de determinar que un nodo cualquiera en un arbol (no
dirigido) es un nodo raz permite inducir una relaci on padre-hijo entre todo par de nodos
adyacentes. As pues, el adjetivo dirigido es un tanto redundante cuando hablamos de
un arbol dirigido con raz y, en aras de la brevedad, lo evitaremos en lo sucesivo.
El nodo raz tiene profundidad 0. Si un nodo tiene profundidad n, sus nodos hijo
tienen profundidad n + 1. La profundidad de un arbol con raz es la profundidad de su
hoja m as profunda y coincide con el n umero de aristas del camino m as largo entre la
raz y una hoja. Toda hoja tiene altura 0. La altura de un nodo interior es uno m as la
mayor altura de cualquiera de sus hijos. N otese que la altura del nodo raz es igual a la
profundidad de su hoja m as profunda. La altura de un arbol con raz es la altura de su
raz y coincide con la profundidad del arbol.
Un sub arbol es el arbol con raz formado por un nodo (que es la raz del sub arbol)
y todos sus descendientes preservando la relaci on es hijo de denida entre ellos. Los
arboles dirigidos con raz son estructuras recursivas: un arbol con raz es un nodo raz y
cero o m as arboles con raz (sus sub arboles).
Decimos que un arbol con raz es ordenado si se asume un orden total entre los hijos
de cada nodo.
Un arbol etiquetado es un arbol y una funci on que relaciona cada v ertice con un
elemento de cierto conjunto de smbolos (etiquetas).
Los arboles dirigidos con raz se representan gr acamente con diagramas en los que
cada nodo es un crculo o caja y cada nodo padre est a unido a sus nodos hijo por una
lnea recta. Los nodos se distribuyen en niveles de arriba a abajo. El nodo raz est a en
el nivel m as alto y los nodos hijo de un nodo ocupan un nivel inferior. Las aristas no
suelen mostrarse con echas, sino con lneas, pues la representaci on por niveles ya lleva
implcito el sentido de las aristas. En los arboles ordenados tiene sentido hablar de los
nodos hijos en funci on del lugar que ocupan en la representaci on gr aca, pues aparecen
ordenados de izquierda a derecha. La gura B.22 muestra la representaci on gr aca de un
arbol con raz e ilustra muchos de los t erminos introducidos.
Un digrafo es un arbol si todos los v ertices excepto uno tienen un solo predecesor.
El v ertice que no tiene predecesor alguno se denomina raz del arbol. La gura B.23 (a)
muestra un grafo no dirigido arb oreo. La gura B.23 (b) muestra el mismo grafo, pero de
forma que hace m as patente su estructura de arbol.
Un grafo no dirigido es un arbol si es conexo y acclico. Una propiedad interesante
de este tipo de grafos es que hay exactamente un camino sin ciclos entre cualquier par
de v ertices. La gura B.24 (a) muestra un grafo no dirigido arb oreo y la gura B.24 (b)
muestra el mismo grafo, pero haciendo maniesta su estructura de arbol. N otese que no
es la unica visi on posible del grafo como arbol: cualquier nodo puede ocupar la posici on
418 Apuntes de Algoritmia 28 de septiembre de 2009
Figura B.22: Un arbol con raz de profundidad 3. Las
aristas no se representan con echas porque su direcci on
siempre es de padre a hijo, es decir, de arriba a abajo. El
arbol se considera ordenado si est a denida la posici on de
cada hijo con respecto a sus hermanos.
1
2
3 4 5
6
7
8
9
Nodo raz
Nodo padre
Nodo hijo
Hoja
Nodos hermanos Sub arbol
0
1
2
3
p
r
o
f
u
n
d
i
d
a
d
Figura B.23: (a) Digrafo con estructura de arbol. (b)
Otra representaci on del mismo grafo en el que se ad-
vierte mejor que es, efectivamente, un arbol.
0
10
11
12
20
21
22
0
10 11 12
20 21 22
(a) (b)
de la raz.
Figura B.24: (a) Grafo no dirigido con estructura de
arbol. (b) Otra representaci on del mismo grafo en el
que se advierte mejor que es, efectivamente, un arbol.
0
10
11
12
20
21
22
0
10 11 12
20 21 22
(a) (b)
Una colecci on de arboles es un bosque.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
214 Es posible a nadir una arista a un arbol de forma que el grafo resultante sea tambi en un
arbol?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Grafos eucldeos
Un grafo ponderado (dirigido o no dirigido) es eucldeo si cada v ertice lleva asociado un
punto en un espacio R
n
y la funci on de ponderaci on asigna a cada arista (u, v) el valor
de la distancia eucldea entre los puntos asociados a u y v.
Podemos modelar el mapa de la gura B.13 con un grafo eucldeo si cada v ertice man-
tiene las coordenadas geogr acas de las ciudades respectivas y cada arista se pondera con
la distancia que las separa. Ojo!: entendemos por distancia la longitud del segmento de
lnea recta que las une, no la distancia real que recorre la correspondiente carretera: esta
ultima depende de los desniveles del terreno y de lo sinuoso del camino.
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 419
La gura B.25 muestra gr acamente el grafo eucldeo no dirigido G = (V, E), donde
V = {(0, 6), (2, 2), (2, 6), (4, 0), (4, 4), (6, 4)},
E = {{(0, 6), (2, 2)}, {(2, 2), (4, 4)}, {(2, 2), (6, 4)}, {(2, 2), (4, 0)},
{(2, 6), (0, 6)}, {(2, 6), (4, 4)}, {(4, 0), (6, 4)}}.
N otese que cada v ertice es un punto en R
2
. La funci on de ponderaci on eucldea es
d((x
1
, y
1
), (x
2
, y
2
)) =

(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.

Si necesitas dibujar digrafos o grafos no dirigidos, puedes hacerlo con la ayu-


da de cualquier programa de dibujo. No obstante, hay una herramienta que
automatiza el proceso: se le suministra una descripci on del grafo en un chero de
texto y proporciona un chero postscript (o en otro formato) con un diagrama que lo
representa. El programa se llama dot, fue desarrollado por investigadores de AT&T
y forma parte del paquete graphviz. Muchas distribuciones Linux lo incorporan. Si
no ocurre con la tuya, busca graphviz en Google.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . EJERCICIOS . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
215 El plan de estudios de un M aster en Patafsica es un conjunto de asignaturas organizadas
en tres grupos Ciencias Inexactas, Inactividades Literarias y El Calendario Patafsico. Cada
uno tiene cuatro asignaturas llamadas como el grupo correspondiente y seguidas de los n umeros
1, 2, 3 y 4. La asignatura n de un grupo es incompatible con la asignatura n 1 del mismo grupo,
para 1 < n 4. Por ejemplo, no se puede cursar Ciencias Inexactas 3 hasta haber superado
Ciencias Inexactas 2. Existen dos incompatibilidades adicionales: no se puede cursar Ciencias
Inexactas 2 si no se super o Inactividades Literarias 1 ni se puede cursar El Calendario Pa-
tafsico 4 si no se super o Inactividades Literarias 4. Se pide:
a) Modelar con un grafo las asignaturas relacionadas por ha de haberse superado para cur-
sar.
b) Determinar si se trata de un grafo cclico o acclico.
c) Obtener el subgrafo inducido por las asignaturas de los dos primeros grupos.
d) Si las asignaturas son anuales, cu antos a nos son necesarios, al menos, para obtener el
M aster en Patafsica? Qu e camino o caminos de asignaturas obliga a ello?
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

La patafsica fue inventada por Alfred Jarry y es la ciencia de las soluciones


imaginarias. Fue popular en los 60. La letra de Maxwells Silver Hammer,
una canci on de los Beatles, empieza as Joan was quizzical, studied pataphysical
science in the home late nights, all alone with a test-tube. Ohh-oh-oh-oh. . . .
420 Apuntes de Algoritmia 28 de septiembre de 2009
B.16. Teora de lenguajes formales
Un alfabeto es un conjunto nito de smbolos. Una cadena es una sucesi on de cero
o m as smbolos del alfabeto. La cadena de longitud cero se denomina cadena vaca y se
denota con . El conjunto de todas las cadenas que se pueden formar con un alfabeto se
denota con

. 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

, si existe una secuencia


1
,
2
, . . . ,
n
de cadenas de (N )
+
tal que =
1
,
28 de septiembre de 2009 Ap endice B. Conceptos matem aticos 421

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

tal que L(G) = L(G

) y cuyas producciones siguen un


determinado patr on. Una forma normal interesante es la Forma Normal de Chomsky. En
ella, las producciones s olo pueden ser de la forma A a o A BC. La unica excepci on
es la producci on S , que puede formar parte de P aunque no se ci na a ninguno de
los dos patrones.
B.16.2. An alisis
Un problema de inter es es, dada una gram atica G y una cadena x

, 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

puede representarse mediante un arbol de


an alisis.
El problema del an alisis para las gram aticas regulares (tipo 3) puede resolverse me-
diante un dispositivo denominado Aut omata Finito de Estados. Si la gram atica es incon-
textual (tipo 2), la denominada m aquina de pila puede efectuar el an alisis.
Hay algoritmos especializados para resolver el problema del an alisis en gram aticas
incontextuales en formas especiales y para subclases de las gram aticas incontextuales.
Ilustremos los diferentes conceptos con un ejemplo. Sea G la gram atica (, N, S, P)
donde = {a, b}, N = {S, A, B, C} y P est a formado por las siguientes producciones:
S AB, S AC, C SB, A a, B b.
(Dicho sea de paso, es una gram atica en Forma Normal de Chomsky.)
La derivaci on directa ASB AABB forma parte de la derivaci on
S AC ASB AABB aABB aAbB aabB aabb.
Esta derivaci on puede representarse con el arbol de an alisis de la gura B.26.
Figura B.26:

Arbol de an alisis para la cadena aabb con la gram atica descrita en el
texto.
S
A
a
C
S
A
a
B
b
B
b
La cadena aabb es derivable desde S, as que forma parte del lenguaje L(G).
Podemos reformular, pues, el problema as: Dada 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

es aceptada por un aut omata A si hay una secuencia de transicio-


nes (q
0
, x
1
, q
1
), (q
1
, x
2
, q
2
), . . . , (q
n1
, x
n
, q
n
) tal que q
0
es el estado inicial y q
n
es un estado
nal cualquiera. El lenguaje aceptado por un aut omata A es el conjunto de cadenas que
acepta y se denota con L(A).
Para toda gram atica regular G hay un AFND A(G) que acepta exactamente las cade-
nas del lenguaje L(G). Para construir el aut omata basta con crear un estado por cada no
terminal y un estado adicional, que es el estado nal. El estado inicial es el estado aso-
ciado al smbolo (no terminal) inicial. Por cada producci on de la forma A aB se crea
una transici on de la forma (A, a, B) y por cada producci on de la forma A a se crea una
transici on (A, a, Z), donde Z es el estado nal.
Dada la correspondencia entre producciones de la gram atica G y transiciones del
aut omata A(G), la secuencia de transiciones que permite aceptar una cadena en A(G)
puede traducirse inmediatamente en una derivaci on en G.

Vous aimerez peut-être aussi