School Work y grafos">
Grafos
Grafos
Grafos
Grafos
Grafos: introducción y
definiciones
Los grafos permiten modelar infinidad de sistemas del mundo real en los que diferentes
elementos de un conjunto están relacionados entre sı́: ciudades conectadas por carre-
teras, proyectos divididos en tareas que dependen unas de otras, relaciones familiares
en diagramas genealógicos, aeropuertos conectados por vuelos directos, etc. En muchos
de estos sistemas surge la necesidad de resolver ciertos problemas que, en esencia, son
instancias de problemas prototı́picos que podemos formular en el terreno abstracto de
los grafos. Tiene interés, por tanto, encontrar algoritmos eficientes para la resolución de
estos problemas.
Un par de ejemplos ayudará a entender el planteamiento:
Este capı́tulo y los dos siguientes se dedican a presentar algunos conceptos relacio-
nados con grafos, cuestiones relativas a su implementación y algunos de los algoritmos
resolutivos para ciertos problemas fundamentales:
3 5
El primero de los vértices de una arista (u, v) es el vértice de partida u origen
de la arista, y el segundo, el vértice de llegada o destino. Decimos que la arista sale,
2 8
parte o emerge del primero y entra, llega o incide en el segundo. En el grafo del ejemplo,
los vértices 2 y 5 están unidos por la arista (2, 5). El vértice 2 es el vértice de partida
1 7
de la arista (2, 5), y el vértice 5 es el de llegada.
Hay aristas conectando, por Dado un vértice u, los vértices v tales que (u, v) ∈ E se denominan sucesores de u
ejemplo, los vértices 1 y 2 o
4 y 6. o vértices adyacentes a u. Y dado un vértice v, los vértices u tales que (u, v) ∈ E se
denominan predecesores de v. Un vértice sin sucesores es un sumidero y un vértice
sin predecesores es una fuente. El grado de salida de un vértice v es el número de
Si necesitas dibujar grafos, sucesores que tiene, y el grado de entrada, el número de predecesores. Denotaremos
puedes hacerlo con la ayuda de con out degree(u) al grado de salida de un vértice u y con in degree(u) a su grado de
cualquier programa de dibujo.
No obstante, hay una entrada. En el grafo del ejemplo, el conjunto de sucesores del vértice 2 es {4, 5} y su
herramienta que automatiza el conjunto de predecesores es el conjunto vacı́o. El grado de salida del vértice 2 es, pues,
proceso: se le suministra una
descripción del grafo en un 2, y el de entrada es 0.
fichero de texto y proporciona El grado de entrada (o de salida) de un grafo G = (V, E) es el mayor grado de
un fichero postscript (o en otro
formato) con un diagrama que entrada (o salida) de sus vértices:
lo representa. El programa se
llama dot, fue desarrollado por
investigadores de AT&T y forma
in degree(G) = max in degree(v),
v∈V
parte del paquete graphviz.
Muchas distribuciones Linux lo out degree(G) = max out degree(u).
incorporan. Si no ocurre con la u∈V
tuya, busca graphviz en Google.
El grafo del ejemplo anterior tiene grados de entrada y salida idénticos; ambos valen 2.
Sucesor: Successor. El conjunto de vértices de un grafo no tiene por qué ser un subconjunto de los
números naturales. He aquı́ un grafo cuyos vértices son lenguajes de programación.
Vértice adyacente: Adjacent
vertex.
V = {C , C ++, Java, C #, Objective C },
Predecesor: Predecessor. E = {(C , C ++), (C , Java), (C ++, Java), (C , C #), (Java, C #), (C ++, C #),
Sumidero: Sink. (C , Objective C )}.
Fuente: Source.
152 Apuntes de Algorı́tmica
Grado de salida: Out-degree.
Las aristas de este grafo expresan la relación ((fue tomado como base para)): ((C fue
tomado como base para C ++)), ((C fue tomado como base para Java)), . . . La figura 7.2
muestra este grafo.
C#
Java
C++ Objective C
Figura 7.2: Grafo de lenguajes de programación. Cada arista une dos lenguajes e indica
que un lenguaje (el vértice de partida) fue tomado como base para el diseño de otro (el
vértice de llegada).
E = {{0, 1}, {0, 3}, {1, 3}, {1, 4}, {2, 4}, {2, 5}, {3, 4}}.
0 1 2
3 4 5
En un grafo no dirigido, dos vértices unidos por una arista son vértices adyacentes
y existe entre ellos, indistintamente, una relación de sucesión y precedencia. En el grafo
de la figura 7.3, los vértices 2 y 5 son adyacentes y el conjunto de vértices adyacentes al
vértice 4 es {1, 2, 3}. No tiene sentido hablar de grado de entrada o salida de un vértice
v, pero sı́ de su grado, sin más, que es la talla del conjunto de vértices adyacentes a él Grado: Degree.
y que denotamos con degree(v). El grado de un grafo G = (V, E) es el grado del vértice
con más vértices adyacentes:
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}.
describe relaciones familiares entre cuatro personas. Cada etiqueta indica la relación
entre las dos personas que enlaza (y la dirección de la arista importa). La figura 7.4
muestra una representación gráfica de G etiquetado con f .
ex-esposa de esposo de
Figura 7.4: Grafo dirigido y etiquetado. Los vértices son personas y las aristas están
etiquetadas con vı́nculos familiares.
En ciertos problemas hemos de asociar un valor numérico a cada una de las aristas.
Un grafo que representa, por ejemplo, ciudades conectadas por carreteras necesitará aso-
ciar a cada carretera (arista) su longitud en kilómetros si estamos interesados en efectuar
cálculos de distancias entre pares de ciudades.
Grafo ponderado: Weighted Un grafo ponderado es un caso particular de grafo etiquetado. Se describe con un
graph.
grafo G = (V, E) y una función d : E → R que asigna un peso numérico a cada arista y
a la que denominamos función de ponderación. Notaremos un grafo ponderado por
Función de ponderación:
Weighting function. d con G = (V, E, d).
El grafo no dirigido definido como sigue modela un mapa de carreteras entre algunas
de las principales ciudades de la isla de Mallorca.
La siguiente función de ponderación asocia a cada conexión entre dos ciudades la dis-
La figura 7.5 muestra una representación gráfica de G. Los pesos aparecen cerca de
las respectivas aristas.
Pollença
10
54
Alcúdia
25 36
Sóller
Inca 8 Capdepera
12
56 Artà
25 17
Marratxı́
14
Calvià14
14 Palma de Mallorca
Andratx Manacor
30 20
Llucmajor
14 27
Campos del Port
13
Santanyı́
Figura 7.5: Grafo no dirigido y ponderado. El grafo modela un mapa de carreteras entre
las principales ciudades de la isla de Mallorca. Cada arista es una conexión por carretera
entre ciudades y el peso de la arista es su distancia en kilómetros. La zona noroeste de
la isla es muy accidentada, lo que hace que las carreteras no sigan trayectorias rectas,
aunque la idealización de la representación gráfica sugiera lo contrario.
Un grafo es ponderado positivo si los pesos son valores positivos o nulos, es decir,
si d es una función 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ón de ponderación También es frecuente usar la
letra w, por ((weighth)) (que
porque recuerda el término ((distancia)). significa peso) para la función
de ponderación.
7.4. Caminos
Un camino en un grafo G = (V, E) es una secuencia de vértices (v1 , v2 , . . . , vn ) tal que Camino: Path.
todo par de vértices consecutivos está unido por una arista de E, es decir, (vi , vi+1 ) (o
{vi , vi+1 }, si el grafo es no dirigido) pertenece a E para todo i entre 1 y n − 1. El vértice
1 3 1 3 1 3 1 3
0 2 0 2 0 2 0 2
(0, 2, 3) (0, 1, 0, 2) (2, 3, 0, 1, 1)
Figura 7.6: A la izquierda se muestra un grafo y, a su derecha, tres caminos (en trazo
gris).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 105 No siempre hemos de sumar pesos para obtener la ((distancia)) de un camino.
Esta figura ilustra un modelo de Markov para la predicción del tiempo que hará cada
dı́a.
1 No debe confundirse la longitud o talla de un camino con su distancia o peso. Longitud y distancia
se usan en este texto aquı́ con sentidos diferentes: la longitud es el número de aristas de un camino y
la distancia es la suma de las distancias individuales asociadas a cada arista del camino.
0.5
nubes
0.15 0.3
0.2 0.3
0.05
sol lluvia
0.1
0.8 0.6
La arista (sol, lluvia) tiene peso 0.05, valor que interpretamos como la probabilidad de
pasar de un dı́a soleado a uno lluvioso. Cada secuencia de vértices 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ándolos.
Supongamos que hoy hace sol, ¿qué es más probable, la secuencia (sol, nubes, nubes,
lluvia, nubes) o la secuencia (sol, sol, nubes, sol, sol,sol)?
......................................................................................
Un camino que visita todos los vértices del grafo exactamente una vez es un camino
hamiltoniano. Un camino que visita todas las aristas de un grafo exactamente una Camino hamiltoniano:
Hamiltonian path.
vez es un camino euleriano.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Camino euleriano: Euler path.
· 106 La ciudad de Königsberg tiene dos islas donde el rı́o Pregel se bifurca. Siete
puentes interconectan las islas y las dos orillas del rı́o tal y como muestra esta figura: Este problema fue propuesto por
Euler en 1736 y de él reciben
nombre los caminos eulerianos.
¿Hay alguna forma de cruzar los siete puentes sin cruzar ninguno más de una vez?
......................................................................................
7.6. Subgrafos
Un grafo G0 = (V 0 , E 0 ) es un subgrafo de un grafo G = (V, E) si V 0 ⊆ V y E 0 ⊆ E. Subgrafo: Subgraph.
Dado un grafo G = (V, E) y un subconjunto V 0 de V , el subgrafo de G inducido
por V 0 es G0 = (V 0 , {(u, v) ∈ E | u, v ∈ V 0 )}). La figura 7.7 muestra (a) un grafo, (b)
un subgrafo suyo y (c) el subgrafo inducido por un subconjunto de vértices. Del mismo
modo podemos definir el subgrafo G0 = (V 0 , E 0 ) inducido por un conjunto de aristas
E 0 ⊆ E, que es G0 = ({u | ∃v : (u, v) ∈ E 0 )} ∪ {v | ∃u : (u, v) ∈ E 0 )). La figura 7.7 (d)
muestra un subgrafo del grafo de la figura 7.7 (a) inducido por un subconjunto de aristas
Un subgrafo completo, es decir, con todos sus vértices conectados a todos los demás
vértices es un clique.
0 1 2 0 1 2 0 1 0 1
3 4 5 3 4 3 4 3 4 5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
La patafı́sica fue inventada por · 107 El plan de estudios de un Máster en Patafı́sica es un conjunto de asignatu-
Alfred Jarry y es la ciencia de
las soluciones imaginarias. Fue
ras organizadas en tres grupos ((Ciencias Inexactas)), ((Inactividades Literarias)) y ((El
popular en los 60. La letra de Calendario Patafı́sico)). Cada uno tiene cuatro asignaturas llamadas como el grupo co-
Maxwell’s Silver Hammer, una
canción de los Beatles, empieza
rrespondiente y seguidas de los números 1, 2, 3 y 4. La asignatura n de un grupo es
ası́ ((Joan was quizzical, studied incompatible con la asignatura n − 1 del mismo grupo, para 1 < n ≤ 4. Por ejemplo, no
pataphysical science in the
home late nights, all alone with
se puede cursar ((Ciencias Inexactas 3)) hasta haber superado ((Ciencias Inexactas 2)).
a test-tube. Ohh-oh-oh-oh. . . )). Existen dos incompatibilidades adicionales: no se puede cursar ((Ciencias Inexactas 2)) si
no se superó ((Inactividades Literarias 1)) ni se puede cursar ((El Calendario Patafı́sico 4))
si no se superó ((Inactividades Literarias 4)). Se pide:
a) Modelar con un grafo las asignaturas relacionadas por ((ha de haberse superado para
cursar)).
c) Obtener el subgrafo inducido por las asignaturas de los dos primeros grupos.
d) Si las asignaturas son anuales, ¿cuántos años son necesarios, al menos, para obtener
el Máster en Patafı́sica? ¿Qué ((camino)) o ((caminos)) de asignaturas obliga a ello?
......................................................................................
7.7. Conectividad
Alcanzable: Reachable. Un vértice v es alcanzable desde un vértice u si existe un camino de u a v. Un grafo
no dirigido es conexo si cualquier par de vértices está unido por un camino, es decir,
Conexo: Connected.
si todo vértice es alcanzable desde cualquier otro. Las componentes conexas de un
Componentes conexas:
grafo no dirigido son los conjuntos de vértices alcanzables dos a dos.
Connected component. En la figura 7.8 se muestra un ejemplo de grafo no dirigido y no conexo con dos
componentes conexas.
0 1 2
3 4 5
Figura 7.8: Grafo no dirigido y no conexo. El grafo contiene dos componentes conexas,
{0, 1, 3, 4} y {2, 5}, que mostramos rodeadas por sendas lı́neas discontinuas.
Débilmente conexo: Weakly Un grafo dirigido es débilmente conexo si entre todo par de nodos u y v hay un
connected.
camino que une u con v o v con u. Un grafo dirigido es fuertemente conexo si todo
Fuertemente conexo:
par de vértices es mutuamente alcanzable. Las componentes fuertemente conexas
Strongly connected. de un grafo dirigido son los conjuntos de vértices mutuamente alcanzables dos a dos.
En la figura 7.9 (a), aparece un grafo dirigido que no es fuertemente conexo: no es
posible, por ejemplo, alcanzar el vértice 4 desde el vértice 5. El de la figura 7.9 (b) sı́ lo
es.
0 1 2 0 1 2
3 4 5 3 4 5
(a) (b)
Figura 7.9: (a) Grafo dirigido débilmente conexo. (b) Grafo dirigido fuertemente co-
nexo.
7.8. Densidad
Un grafo es denso si casi todo par de vértices está unido por una arista, es decir, Denso: Dense.
si el número de aristas, |E|, es próximo 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értice
está unido al resto de vértices. Un grafo es disperso cuando |E| es mucho menor que Disperso: Sparse.
|V |2 .
En la figura 7.10 se muestra, a mano izquierda, un grafo completo no dirigido y, a
mano derecha, un grafo completo dirigido.
(a) (b)
Figura 7.10: (a) Grafo no dirigido y completo. (b) Grafo dirigido y completo.
7.9. Multigrafos
Un multigrafo es un grafo en el que el conjunto de aristas es un multiconjunto, es Multigraf: Multigraph.
decir, una colección en la que puede haber puede elementos repetidos. En un multigrafo
puede existir, pues, más de una arista entre dos vértices. La figura 7.11 muestra un
multigrafo.
0 1 2
3 4 5
24
23
2 22 42
1 11 21 41
0 10 20 30 40
Figura 7.12: Grafo multietapa. Los vértices se han representado de forma que cada
columna corresponde a una etapa.
7.10.2. Árboles
Ya hemos estudiado los árboles en el contexto de las estructuras de datos. Un árbol es
un caso particular de grafo ya que, a fin de cuentas, es un conjunto de nodos (vértices)
vinculados entre sı́ por relaciones padre-hijo (aristas). Si en cada par de vértices rela-
cionados tenemos en cuenta quién es padre y quién hijo, el grafo es dirigido; en caso
contrario, no dirigido.
Un grafo dirigido es un árbol si todos los vértices excepto uno tienen un solo pre-
decesor. El vértice que no tiene predecesor alguno se denomina raı́z del árbol. La fi-
gura 7.13 (a) muestra un grafo dirigido arbóreo. La figura 7.13 (b) muestra el mismo
grafo, pero de forma que hace más patente su estructura de árbol.
Un grafo no dirigido es un árbol si es conexo y acı́clico. Una propiedad interesante
de este tipo de grafos es que hay exactamente un camino sin ciclos entre cualquier par
de vértices. La figura 7.14 (a) muestra un grafo dirigido arbóreo y la figura 7.14 (b)
muestra el mismo grafo, pero haciendo manifiesta su estructura de árbol.
Una colección de árboles es un bosque. Un grafo no dirigido es un bosque si es
acı́clico.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 108 ¿Es posible añadir una arista a un árbol de forma que el grafo resultante sea
también un árbol?
......................................................................................
21 10 0
11 0 20 10 11 12
22 12 20 21 22
(a) (b)
Figura 7.13: (a) Grafo dirigido con estructura de árbol. (b) Otra representación del
mismo árbol en el que se advierte mejor que es, efectivamente, un árbol.
21 10 0
11 0 20 10 11 12
22 12 20 21 22
(a) (b)
Figura 7.14: (a) Grafo no dirigido con estructura de árbol. (b) Otra representación
del mismo árbol en el que se advierte mejor que es, efectivamente, un árbol.
V = {(0, 6), (2, 2), (2, 6), (4, 0), (4, 4), (6, 4)},
E = {{(0, 6), (2, 2)}, {(0, 6), (2, 4)}, {(2, 2), (6, 0)}, {(2, 2), (4, 4)}, {(2, 2), (6, 4)},
{(2, 2), (4, 0)}, {(2, 6), (0, 6)}, {(2, 6), (4, 4)}, {(4, 0), (2, 2)}, {(4, 0), (6, 4)},
{(4, 4), (2, 6)}, {(4, 4), (2, 2)}, {(6, 4), (2, 2)}, {(6, 4), (4, 0)}}.
Nótese
p que cada vértice es un punto en R2 . La función de ponderación es d((x1 , y1 ), (x2 , y2 )) =
(x1 − x2 )2 + (y1 − y2 )2 .
0
0 1 2 3 4 5 6
Figura 7.15: Un grafo euclı́deo. Cada vértice se muestra como un punto en el plano
coordenado. El peso de cada arista es la distancia entre los puntos que une.
Implementación de grafos
8.1. Grafos
Es este apartado vamos a presentar diferentes implementaciones de los grafos dirigidos.
Cada una de ellas propone una forma diferente de organizar la información del conjunto
de aristas E, ofreciendo ası́ diferentes soluciones de compromiso entre el coste espacial y
el coste temporal de cada una de las operaciones básicas de acceso: fundamentalmente,
la determinación de la pertenencia de una arista a E y la obtención de la lista de vértices
sucesores y predecesores de un vértice.
Consideraremos las siguientes implementaciones de E:
Matriz de adyacencia.
Listas de adyacencia.
Vectores de adyacencia.
Vectores de adyacencia ordenados.
Conjuntos de adyacencia.
Conjuntos de adyacencia con inversa.
Al construir un grafo indicaremos si éste es o no es dirigido. Una vez presentadas
las implementaciones haremos un breve estudio comparativo.
0 1 2 3 4 5
0 1 2 0
False True False True False False
1T
False False False False True False
2
False False False False True True
3 4 5
3
True True False False False False
4
False False False True False False
5 False False False False False True
(a) (b)
Figura 8.1: (a) Grafo dirigido. (b) Representación de su conjunto de aristas con una
matriz de adyacencia. El valor True en la celda (i, j) de la matriz significa que hay una
arista uniendo los vértices i y j. El valor False significa que no hay arista.
12 print
13 print ’Sucesores de 0:’, G.succs(0)
14 print ’Predecesores de 0:’, G.preds(0)
?
15 print ’ Es 0 un vértice?:’, 0 in G.V
?
16 print ’ Es 8 un vértice?:’, 8 in G.V
?
17 print ’ Es (0,1) una arista?:’, (0,1) in G.E
?
18 print ’ Es (0,0) una arista?:’, (0,0) in G.E
Vértices: [0, 1, 2, 3, 4, 5]
Aristas: (0, 1) (0, 3) (1, 4) (2, 4) (2, 5) (3, 0) (3, 1) (4, 3) (5, 5)
Sucesores de 0: [1, 3]
Predecesores de 0: [3]
?
Es 0 un vértice?: True
?
Es 8 un vértice?: False
?
Es (0,1) una arista?: True
?
Es (0,0) una arista?: False
0 1 2
3 4 5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 109 Diseña una función que reciba un valor entero positivo n y devuelva un grafo
no dirigido completo cuyo conjunto de vértices es V = {0, 1, . . . , n − 1}. El conjunto de
aristas debe representarse con una matriz de adyacencia.
· 110 Al instanciar un objeto de la clase AdjacencyMatrixGraph se construye inter-
namente una matriz de dimensión |V | × |V | que es simétrica y tiene diagonal principal
nula. Modifica la clase para que represente un grafo no dirigido almacenando únicamen-
te la región triangular superior de la matriz (sin la diagonal principal). Al representar
el grafo de la figura 8.2 se almacenará ası́ su matriz de adyacencia:
[[True, False, True, False, False ],
[ False, True, True, False ],
[ False, True, True ],
[ True, False ],
[ False ]]
......................................................................................
21 class AdjacencyMatrixGraph:
22 def __init__ (self , V =[], E=[], directed =True):
23 self .directed = directed
24 self .V = V
25 if directed : self .E = AdjacencyMatrix (V , E)
26 else: self .E = AdjacencyMatrix (V , E + [(v,u) for (u,v) in E])
27
test adjacency matrix graph 2.py test adjacency matrix graph 2.py
1 from adjacency_matrix_graph_ 2 import AdjacencyMatrixGraph
2 from show_graph import show_graph
3
Resulta farragoso tener que trabajar explı́citamente con la tabla index cada vez que
se desea acceder al ı́ndice de un vértice. Python ofrece una solución más elegante: usar
directamente un diccionario de diccionarios para mantener la matriz E.
C#
Java
C++ Objective C
16 class AdjacencyMatrixGraph:
17 def __init__ (self , V =[], E=[], directed =True):
18 self .directed = directed
19 self .V = V
20 if directed : self .E = AdjacencyMatrix (V , E)
21 else: self .E = AdjacencyMatrix (V , E + [(v,u) for (u,v) in E])
22
test adjacency matrix graph a.py test adjacency matrix graph a.py
1 from adjacency_matrix_graph import AdjacencyMatrixGraph
2 from show_graph import show_graph
3
El conjunto de aristas no tiene por qué ser una lista. Podemos suministrar, por
ejemplo, un conjunto:
test adjacency matrix graph b.py test adjacency matrix graph b.py
1 from adjacency_matrix_graph import AdjacencyMatrixGraph
2 from show_graph import show_graph
3 from sets import Set
4
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 111 Diseña una función que reciba una lista de vértices y devuelva un grafo no
dirigido completo. El grafo debe representarse con una matriz de adyacencia.
......................................................................................
0 sig sig
1 3
1 sig
4
2 sig sig
4 5
3 sig sig
0 1
4 sig
0 1 2 3
5 sig
3 4 5 5
(a) (b)
Figura 8.4: (a) Grafo dirigido. (b) Representación del grafo con listas (enlazadas) de
adyacencia.
3 class AdjacencyLists:
4 def __init__ (self , V , E):
5 self .lists = [None] * len(V )
6 for v in V : self .lists[v] = LinkedList()
7 for (u, v) in E: self .lists[u].insert_head (v)
8
40 i += 1
41 return preds
42
43 class AdjacencyListsGraph:
44 def __init__ (self , V =[], E=[], directed =True):
45 self .directed = directed
46 self .V = V
47 if directed : self .E = AdjacencyLists(V , E)
48 else: self .E = AdjacencyLists(V , E + [(v,u) for (u,v) in E])
49
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 112 Diseña una clase para la implementación de grafos con listas enlazadas de
adyacencia que permita trabajar con conjuntos arbitrarios de vértices. Analiza la com-
plejidad computacional de cada uno de sus métodos.
......................................................................................
0 1
0
1 3
0
1
4
0 1
2
4 5
0 1
3
0 1
0
4
3
0 1 2 0
5
5
3 4 5
(a) (b)
Figura 8.5: (a) Grafo dirigido. (b) Representación del grafo con vectores de adyacencia.
16 class AdjacencyArraysGraph:
17 def __init__ (self , V =[], E=[], directed =True):
18 self .directed = directed
19 self .V = V
20 if directed : self .E = AdjacencyArrays(V , E)
21 else: self .E = AdjacencyArrays(V , E + [(v,u) for (u,v) in E])
22
test adjacency arrays graph a.py test adjacency arrays graph a.py
1 from adjacency_arrays_graph import AdjacencyArraysGraph
2 from show_graph import show_graph
3
46 class SortedAdjacencyArraysGraph:
47 def __init__ (self , V =[], E=[], directed =True):
48 self .directed = directed
49 self .V = V
50 if directed : self .E = SortedAdjacencyArrays(V , E)
51 else: self .E = SortedAdjacencyArrays(V , E + [(v,u) for (u,v) in E])
52
test adjacency arrays graph b.py test adjacency arrays graph b.py
1 from adjacency_arrays_graph import SortedAdjacencyArraysGraph
2 from show_graph import show_graph
3
3 class AdjacencySets:
4 def __init__ (self , V , E):
5 self .sets = {}
6 for u in V : self .sets[u] = Set([ v for (w,v) in E if u == w ])
7
14 class AdjacencySetsGraph:
15 def __init__ (self , V =[], E=[], directed =True):
16 self .directed = directed
17 self .V = V
18 if directed : self .E = AdjacencySets(V , E)
19 else: self .E = AdjacencySets(V , E + [(v,u) for (u,v) in E])
20
3 class InvAdjacencySetsGraph:
4 def __init__ (self , V =[], E=[], directed =True):
5 self .directed = directed
6 self .V = V
7 if directed :
8 self .E = AdjacencySets(V , E)
9 self .EInv = AdjacencySets(V , [(v,u) for (u,v) in E])
10 else:
11 self .E = AdjacencySets(V , E + [(v,u) for (u,v) in E])
12
test inv adjacency sets graph.py test inv adjacency sets graph.py
1 from inv_adjacency_sets_graph import InvAdjacencySetsGraph
2 from show_graph import show_graph
3
necesario para construir un grafo con cada implementación a partir de un conjunto con
sus vértices y un conjunto que especifica sus aristas. Nótese que mantener ordenados
los vectores de adyacencia acelera la determinación de pertenencia de una arista si el
número de sucesores es elevado. Otra opción que hemos de considerar es si deseamos
almacenar vectores de adyacencia inversa, esto es, una representación explı́cita de los
predecesores de cada vértice, pues duplica el consumo de memoria y sólo resulta con-
veniente si nuestro algoritmo accede efectivamente a los predecesores de un vértice.
Espacio
Matriz de adyacencia Θ(|V |2 )
Listas de adyacencia Θ(|V | + |E|)
Vectores de adyacencia Θ(|V | + |E|)
Vectores de adyacencia ordenados Θ(|V | + |E|)
Conjuntos de adyacencia Θ(|V | + |E|)
Conjuntos de adyacencia con inversa Θ(|V | + |E|)
G.succs(u) G.preds(v)
Matriz de ady. Θ(|V |) Θ(|V |)
Listas de ady. Θ(1) Θ(|E|)
Vectores de ady. Θ(1) Θ(|E|)
Vectores de ady. ordenados Θ(1) Θ(|V | lg |V |)
Conjuntos de ady. Θ(1) Θ(|E|)
Conjuntos de ady. con inversa Θ(1) Θ(1)
Tabla 8.3: Coste temporal del acceso a sucesores y predecesores. El tiempo de acceso
a sucesores/predecesores considera únicamente el acceso a la lista, vector o conjunto
de sucesores/predecesores, y no su recorrido. (Los tiempos de las implementaciones con
conjuntos son tiempos esperados.)
Salvo que digamos lo contrario, usaremos en adelante los conjuntos de adyacencia con
inversa. Suponen un compromiso razonable entre consumo de memoria (es proporcional
al número de aristas) y velocidad de ejecución (ofrece tiempos esperados constantes).
En los algoritmos del siguiente capı́tulo usaremos una clase Graph que no es sino la
clase InvAdjacencySetsGraph:
graph.py graph.py
1 from inv_adjacency_sets_graph import InvAdjacencySetsGraph
2
3 Graph = InvAdjacencySetsGraph
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 115 Rellena tablas similares para las operaciones de adición/eliminación de vértices
y aristas.
· 116 Implementa una nueva clase para representar grafos que almacene tanto una
matriz de adyacencia como listas de adyacencia (para sucesores y predecesores) desorde-
nadas. ¿Con qué coste podemos efectuar cada una de las operaciones? ¿Qué complejidad
espacial presenta la nueva estructura de datos?
· 117 Piensa en cómo se implementarı́an los métodos de acceso convencionales en
una nueva clase para representar grafos que almacenase tanto una matriz de adyacencia
como listas de adyacencia (para sucesores y predecesores) ordenadas. ¿Con qué coste
podemos determinar la pertenencia de una arista a E y obtener la relación de sucesores
y predecesores de un vértice cualquiera?
......................................................................................
3 class AdjacencySets:
4 def __init__ (self , V , E):
5 self .sets = {}
6 for u in V : self .sets[u] = Set([ v for (w,v) in E if u == w ])
7
20 class InvAdjacencySetsGraph:
21 def __init__ (self , V =[], E=[], directed =True):
22 self .directed = directed
23 self .V = V
24 if directed :
25 self .E = AdjacencySets(V , E)
26 self .EInv = AdjacencySets(V , [(v,u) for (u,v) in E])
27 else:
28 self .E = AdjacencySets(V , E + [(v,u) for (u,v) in E])
29
test adjacency sets graph b.py test adjacency sets graph b.py
1 from inv_adjacency_sets_graph import InvAdjacencySetsGraph
2 from show_graph import show_graph
3
10 G.add_vertex (’Python’)
11 G.add_edge( (’C++’, ’Python’) )
12 G.remove_edge( (’C’, ’Objective C’) )
13 G.remove_vertex (’Objective C’)
14 print
15 show_graph(G)
------------+-----------------------------------+-----------------------------
C |C# Java C++ |
C++ |C# Python Java |C
Java |C# |C C++
C# | |C Java C++
Python | |C++
C# Python C#
Java Java
C C
(a) (b)
Figura 8.6: (a) Grafo de lenguajes de programación original. (b) Grafo obtenido a
partir del anterior mediante la adición y supresión de algunos vértices y aristas.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 118 Implementa las operaciones de adición y supresión de vértices y aristas en las
diferentes implementaciones de grafos mostradas en la sección 8.1. Analiza sus respec-
tivos costes temporales.
· 119 El cuadrado de un grafo dirigido G = (V, E) se define como G2 = (V, E 0 )
donde (u, w) ∈ E 0 si y sólo si para algún v ∈ V se cumple que (u, v) ∈ E y (v, w) ∈ E.
La siguiente función recibe un grafo G y devuelve el grafo G2 . Ambos grafos están
implementados con matrices de adyacencia.
3 def squared_graph(G):
4 Gsquared = Graph(V =[v for v in G.V ])
5 for u in G.V :
6 for v in G.succs(u):
7 for w in G.succs(v):
8 Gsquared.add_edge( (u,w) )
9 return Gsquared
5 G = Graph(V =range(6),
6 E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
7 G2 = squared_graph(G)
8
12 print
13 print ’Grafo G al cuadrado:’
14 show_graph(G2)
Grafo G:
Vértice |Sucesores |Predecesores
------------+-----------------------------------+-----------------------------
0 |1 3 |3
1 |4 |0 3
2 |4 5 |
3 |0 1 |0 4
4 |3 |1 2
5 |5 |2 5
Grafo G al cuadrado:
Vértice |Sucesores |Predecesores
------------+-----------------------------------+-----------------------------
0 |0 1 4 |0 4
1 |3 |0 3 4
2 |3 5 |
3 |1 3 4 |1 2 3
4 |0 1 |0 3
5 |5 |2 5
a) Matriz de adyacencia.
b) Vectores de adyacencia.
· 121 Dados dos grafos G1 = (V, E1 ) y G2 = (V, E2 ) con el mismo conjunto de vérti-
ces, se define la suma de dos grafos como un grafo G0 que tiene una arista (u, v) si
(u, v) ∈ E1 o (u, v) ∈ E2 . Diseña una función que reciba dos grafos con el mismo con-
junto de vértices y devuelva la suma de los dos. Calcula el coste temporal del algoritmo
en función de la implementación escogida para los grafos:
a) Matriz de adyacencia.
b) Vectores de adyacencia.
a) Matriz de adyacencia.
b) Vectores de adyacencia.
Unión: Join. · 123 Dados dos grafos G1 = (V1 , E1 ) y G2 = (V2 , E2 ) se define la union de G1 y
G2 como el grafo G0 = (V1 ∪ V2 , E1 ∪ E2 ). Diseña una función que calcule la unión de
dos grafos. Calcula el coste temporal del algoritmo en función de la implementación
escogida para los grafos:
a) Matriz de adyacencia.
b) Vectores de adyacencia.
0 1 2 1 2
6
3 4 5 4 5
Diseña una función que reciba un grafo G y una arista y devuelva el grafo resultante
de contraer dicha arista en G. Calcula el coste temporal del algoritmo en función de la
implementación escogida para los grafos:
a) Matriz de adyacencia.
b) Vectores de adyacencia.
c) Conjuntos de adyacencia con inversa.
· 126 Dados dos grafos G1 = (V, E1 ) y G2 = (V, E2 ), se desea obtener un nuevo grafo
G = (V, E) que sea la diferencia simétrica de G1 y G2 , tal que el conjunto de aristas E
contenga las aristas de E1 que no estén en E2 , junto con las de E2 que no estén en E1 .
Escribe funciones que construyan el grafo resultante G, considerando que los grafos se
implementan como:
a) Matriz de adyacencia.
b) Listas enlazadas de adyacencia.
c) Vectores de adyacencia.
d) Conjuntos de adyacencia con inversa.
Analiza el coste espacial y temporal de cada función.
· 127 Se dice que un grafo G = (V, E) es bipartido si: Bipartido: Bipartite.
b) Vectores de adyacencia.
0 1 2 3 4 5 6 7 8 9
Figura 8.7: Un grafo dirigido con estructura que puede representarse sin necesidad de
explicitar V y E.
1 class MyGraph:
2 def __getattr__ (self , attr ):
3 if attr == ’V’: return range(10)
4 elif attr == ’E’: return [(u,v) for u in range(10) for v in range(u+1,10)]
5
1 class MyGeneralGraph:
2 def __init__ (self , n):
3 self .n = n
4
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 129 ¿Qué coste presenta cada uno de los métodos de la clase MyGeneralGraph?
......................................................................................
Otro tipo de grafo estructurado de tal forma que no se precisa almacenar explı́ci-
tamente sus vértices y aristas es el denominado grafo mallado, en malla o retı́cula. La Malla: Grid.
figura 8.8 muestra un caso particular de grafo mallado.
0, 4 1, 4 2, 4 3, 4 4, 4
0, 3 1, 3 2, 3 3, 3 4, 3
0, 2 1, 2 2, 2 3, 2 4, 2
0, 1 1, 1 2, 1 3, 1 4, 1
0, 0 1, 0 2, 0 3, 0 4, 0
mygrid.py mygrid.py
1 from show_graph import show_graph
2
3 class MyGrid :
4 def __getattr__ (self , attr ):
5 if attr == ’V’:
6 return [ (i,j) for i in range(5) for j in range(5) ]
7 elif attr == ’E’:
8 return [ ((i,j), (i+1,j+1)) for i in range(4) for j in range(4) ] + \
9 [ ((i,j), (i+1,j)) for i in range(4) for j in range(5) ] + \
10 [ ((i,j), (i,j+1)) for i in range(5) for j in range(4) ]
11
28 G = MyGrid ()
29 show_graph(G)
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 130 Diseña una clase que permita definir grafos mallados como el anterior dados el
número de filas y el número de columnas del grafo.
· 131 Diseña una clase que permita definir grafos con esta estructura:
0 1 2 3 4 5 6 7 8 9
· 132 Diseña una clase que, dado un número de columnas, permita definir grafos
multietapa con esta estructura:
3, 3
2, 2 3, 2
1, 1 2, 1 3, 1
0, 0 1, 0 2, 0 3, 0
· 133 Diseña una clase que, dado un número de columnas, permita definir grafos
multietapa con esta estructura:
0, 3 1, 3 2, 3 3, 3 4, 3 5, 3 6, 3
0, 2 1, 2 2, 2 3, 2 4, 2 5, 2 6, 2
0, 1 1, 1 2, 1 3, 1 4, 1 5, 1 6, 1
0, 0 1, 0 2, 0 3, 0 4, 0 5, 0 6, 0
......................................................................................
3 G = Graph(V =range(6),
4 E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
5
25 for u in G.V :
26 print ’Aristas que parten de %s y sus pesos:’ % u,
27 for v in G.succs(u):
28 print ’( %s, %s): %s. ’ % (u, v, d(u,v)),
29 print
4
0 1 2
4 0
1 4 1 2
1
3 4 5 2
3 G = Graph(V =range(6),
4 E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
5
Vértices: [0, 1, 2, 3, 4, 5]
Aristas que parten de 0 y sus pesos: (0,1): 4. (0,3): 4.
Aristas que parten de 1 y sus pesos: (1,4): 1.
Aristas que parten de 2 y sus pesos: (2,4): 0. (2,5): 2.
Aristas que parten de 3 y sus pesos: (3,0): 1. (3,1): 4.
Aristas que parten de 4 y sus pesos: (4,3): 1.
Aristas que parten de 5 y sus pesos: (5,5): 2.
Nótese que hemos codificado con None la ausencia de peso entre un par de vértices.
Una alternativa hubiera sido codificar el ((peso de las aristas sin peso)), valga la expresión,
con un valor ((infinito)) (o un valor suficientemente grande) o un valor negativo (si los
pesos válidos son positivos), pero no con el valor cero, pues hay aristas cuyo peso es,
precisamente, cero.
En lugar de usar una matriz podemos codificar la asociación entre aristas y pesos
con un diccionario. De este modo nos ahorramos el problema de codificar ((pesos de
aristas sin peso)).
4
0 1 2
1 0
4 3 2
1
3 4 5
3 G = Graph(V =range(6),
4 E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3), (5,5)])
5
Vértices: [0, 1, 2, 3, 4, 5]
Aristas que parten de 0 y sus pesos: (0, 1): 4. (0, 3): 4.
Aristas que parten de 1 y sus pesos: (1, 4): 1.
Aristas que parten de 2 y sus pesos: (2, 4): 0. (2, 5): 2.
Aristas que parten de 3 y sus pesos: (3, 0): 1. (3, 1): 4.
Aristas que parten de 4 y sus pesos: (4, 3): 1.
Aristas que parten de 5 y sus pesos: (5, 5): 2.
3 G = Graph(V =range(6),
4 E=[(0,1), (0,3), (1,4), (2,4), (2,5), (3,0), (3,1), (4,3)],
5 directed =False)
6
Vértices: [0, 1, 2, 3, 4, 5]
Aristas que parten de 0 y sus pesos: (0,1): 4. (0,3): 4.
Aristas que parten de 1 y sus pesos: (1,0): 4. (1,3): 1. (1,4): 3.
Obsérvese cómo hemos codificado la función d para que no sea necesario almacenar
en la tabla el peso de (v, u) si ya se ha almacenado el peso de (u, v).
Los métodos de codificación de la función de ponderación son independientes del
modo en que se codifica el propio grafo: no importa si usamos matrices o listas de
adyacencia. Consideremos un ejemplo más: un grafo ponderado (el de la figura 7.5)
implementado con listas de adyacencia.
mallorca.py mallorca.py
1 from graph import Graph
2
36 def d (u,v):
37 if (u,v) in km: return km[u,v]
38 else: return km[v,u]
Vértices: [(0, 6), (2, 2), (2, 6), (4, 4), (4, 0), (6, 4)]
Aristas que parten de (0, 6) y sus distancias:
((0, 6),(2, 6)): 2.00.
((0, 6),(2, 2)): 4.47.
Aristas que parten de (2, 2) y sus distancias:
((2, 2),(0, 6)): 4.47.
((2, 2),(4, 4)): 2.83.
((2, 2),(6, 4)): 4.47.
((2, 2),(4, 0)): 2.83.
Aristas que parten de (2, 6) y sus distancias:
((2, 6),(4, 4)): 2.83.
((2, 6),(0, 6)): 2.00.
Aristas que parten de (4, 4) y sus distancias:
((4, 4),(2, 6)): 2.83.
((4, 4),(2, 2)): 2.83.
Aristas que parten de (4, 0) y sus distancias:
((4, 0),(6, 4)): 4.47.
((4, 0),(2, 2)): 2.83.
Aristas que parten de (6, 4) y sus distancias:
((6, 4),(4, 0)): 4.47.
((6, 4),(2, 2)): 4.47.
iberia. Cada lı́nea del fichero contiene tres campos separados: ciudad de origen, ciudad
de destino y distancia (euclı́dea) que los separa.
iberia iberia
1 A Coru~na:Pontedeume:20.904
2 Abrantes:Castelo Branco:67.303
3 Adanero:San Rafael:39.312
4 Aguilar de Campoó:Burgos:56.790
5 Aguilar de Campoó:Reinosa:22.853
6 Alacant:Santa Pola:9.234
7 Albacete:Almansa:57.257
8 Albacete:La Roda:27.547
9 Albacete:Ruidera:77.504
10 Albocàsser:Xert:18.468
11 Albocàsser:L’Alcora:23.946
12 Albufeira:Ourique:53.544
13 Alcala de Henarés:Guadalajara:31.348
.
.
.
590 Ávila:Pe~
naranda de Bracamonte:48.620
591 Ávila:Adanero:35.523
592 Ávila:Arévalo:54.040
593 Écija:Lucena:62.371
594 Évora:Montemor-o-Novo:24.167
595 Évora:Beja:55.480
596 Évora:Gr^
andula:59.931
597 Úbeda:Linares:25.871
Ferrol
Ribadeo Navia
Barreiros
LuarcaCudillero
Avilés
Gijón
Vegadeo
A Coruña La Espina Ribadesella Santander
Pontedeume Grado Llanes Laredo Urdiales
Carballo Betanzos Pola de Siero
Oviedo Unquera Solares
Torrelavega Castro
Cangas de Onı́s Bilbao
Basauri Zarauz
Donostia
Mieres
Baamonde Durango
Potes
Campomanes
Santiago de Compostela
Arzúa Lugo Reinosa
Guntı́n
La Magdalena Cistierna Cerceda Izurzun
Lalı́n Becerrea Aguilar de Campoó Vitoria
Armiñón Pamplona Vielha
Vilagarcı́a de ArousaChantada León
Miranda del Ebro Jaca
Pontevedra Monforte de Lemos Ponferrada Astorga Sabiñánigo Boltaña
Mansilla de la Mulas Gimileo Llivia Portbou
Marı́n Tafalla Pont de Suert Puigcerdà La Jonquera
O Barco Osorno Logroño
Ourense Burgos
Santo Domingo de la Calzada Nueno La Seu d’Urgell Figueres
Vigo Ribadavia La Bañeza Graus
Benabarre Ripoll
Tuj Olot
Xinzo de Limia Alfaro
Huesca
Mombuey Villalón de Campos
Quintana del Puente Barbastro
Palencia Lerma Vic Girona
Verı́n Benavente
Ponte de Lima Monzón Manresa
Viana do Castelo Bragança Zuera
Quintanilha Ágreda
Tarazona
Braga Alcañices Soria Lleida Cervera
Macedo de Cavaleiros Valladolid Aranda de Duero Oitura Les Borges Igualada
Blanques
Ribeira de Pena Peñafiel Zaragoza Fraga Mataró
Murça Zamora Toro Tordesillas San Esteban
BurgodedeGormaz
Osma El Burgo del Ebro Vilafranca del Penedés
Mogadouro Almazán Barcelona
Vila Real
Porto
Valongo Vila Flor Calatayud Castelldefels
Gaia Medina del Campo Riaza Sitges
Espinho Medinaceli Hı́jar Caspe Reus
Tarragona
Vila Nova de Foz Côa
Feira Arévalo Daroca Alcañiz L’Hospitalet de l’Infant
Segovia Alcolea del Pinar
Montalbán
Salamanca Adanero Almadrones Molina de Aragón
Aveiro Peñaranda de Bracamonte Monreal del Campo
Viseu Guadalajara
Venturada Amposta
Guarda San Rafael Morella
Collado Villalba CinctorresXert
Ávila
Coimbra Béjar Alcala de Henarés
Sacedón Vinarós
Covilha Madrid Vilafranca Benicarló
delPenyı́scola
Cid
Figueira da Foz Teruel Albocàsser
El Barco de Ávila Torreblanca
Arenas de San Pedro Arganda del Rey
L’Alcora
Borriol
Benicàssim
Ansiao Plasenncia Maqueda Cuenca Castelló de la Plana
Aranjuez Tarancón Cañete Onda
Vila-real
Leiria Castelo Branco Talavera de la Reina Ocaña Nules
Navalmoral de la Mata Segorbe
Toledo
Losa del Obispo
Liria Sagunt
Torres Novas
Abrantes Utiel Grao de Sagunt
Caldas
Penicheda Rainha Honrubia Caudete
Cáceres Trujillo Belmonte Requena Chiva València
Santarem Valencia de Alcántara Madridejos
Portalegre Silla
Ponte do Sôr
Casas Ibáñez
Torres Vedras Villarrobledo Alcance
Carregado Herrera del Duque Sueca
La Roda Bicorp AlziraCullera
Coruche Casa de Juan Gil
Sintra Piedrabuena Daimiel Ayora
Elvas Ciudad Real Manzanares Albacete
Lisboa Estremoz Mérida Ruidera Xàtiva Gandia
Cascais
Montijo Badajoz
Montemor-o-Novo Almansa Ontinyent Oliva
Almadén
’Evora Puertollano Valdepeñas Cocentaina
Setubal La Albuera Villena Ibi
Alcoy Benidorm
Alcaraz Yecla
Hellı́n Elda
Zafra Monóvar
Grândula Pinoso
Sant Joan d’Alacant
Novelda
Llerena Alacant
Elx
Espiel Cieza
Sines Santa Pola
Beja Callosa del Segura
Serpa Linares Guardamar del Segura
Montoro Bailén
Andújar Úbeda
Córdoba Torrevieja
Alcantarilla
Jabugo Murcia
Ourique
Odemira Totana
Venta El Alto Jaén Los Alcázares
Vélez-Rubio
Lorca
Valverde del Camino Écija La Unión
Puerto LumbrerasCartagena
Portman
Baena
Gibraleón Sevilla Lucena Baza
Lagos Albufeira
Vilareal deAyamonte
Santo Antonio
Tavira Lepe Huelva Dos Hermanas Vera
Almonte Estepa
Mazagón Loja Granada Los Gallardos
Faro
Campillos
Antequera Padul Carboneras
Durcal
Alemrı́a
Jerez de la Frontera Ronda Málaga Motril
Almuñecar Aara
Rincón de la Victorio
Torremolinos
Puerto Real
Cádiz Fuengirola
Marbella
San Fernando Estepona
Chiclana de la Frontera
Guadiaro
La Lı́nea de la Concepción
Algeciras
Tarifa
Diseñemos una rutina capaz de leer el grafo en cuestión como grafo no dirigido
y ponderado en el que la distancia entre dos ciudades conectadas por carretera es la
distancia euclı́dea. La función de lectura del grafo devuelve dos objetos: el grafo en sı́ y
la función de ponderación.
load graph.py load graph.py
1 from graph import Graph
2 from math import sqrt
3 from sets import Set
4
5 def load_weighted_graph(filename):
6 G = Graph(V =Set(), E=[], directed =False)
7 dist = {}
8 for line in file(filename):
9 cityA, cityB , aux = line.strip().split(’:’)
10 G.add_vertex ( cityA )
11 G.add_vertex ( cityB )
12 G.add_edge((cityA, cityB ))
13 dist[cityA, cityB ] = dist[cityB , cityA] = float(aux )
14
18 return G, d
3 G, d = load_weighted_graph(’iberia’)
4 print ’Ciudades conectadas a Castelló de la Plana’,
5 print ’y distancia euclı́dea (aproximada):’
6 for v in G.succs(’Castelló de la Plana’):
7 print ’ %s: %.3f’ % (v, d(’Castelló de la Plana’, v))
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 134 Diseña una función que reciba un grafo y almacene en un fichero la información
relativa a sus aristas como pares de vértices separados por el carácter ((:)).
· 135 Diseña una función que reciba un grafo ponderado (un grafo y una función
de ponderación de las aristas) y almacene en un fichero la información relativa a sus
aristas y pesos respectivos como tripletes ((origen:destino:peso)). Almacena el mapa
de carreteras de Mallorca (véase la figura 7.5) en ese formato.
......................................................................................
En este capı́tulo presentamos algunos algoritmos sobre grafos. Nos limitamos a estudiar
algunos de los considerados más relevantes o que resultarán instrumentales en otros
capı́tulos.
Componentes conexas.
Camino más corto entre un par de vértices de un grafo o entre un vértice y todos
los demás:
• En grafos acı́clicos.
• En grafos ponderados positivos (algoritmo de Dijkstra).
• En grafos sin bucles (algoritmo de Bellman-Ford).
3 def level_order_traversal (G, s): # G debe ser un árbol (codificado como un grafo).
4 Q = FIFO()
5 Q.append (s)
6 while not Q.is_empty():
7 u = Q.pop()
8 print u, # Procesa el vértice u.
9 for v in G.succs(u):
10 Q.append (v)
1 2
3 4
5 6 7 8
9 10
1 2
3 4
5 6 7 8
9 10
0 1 2 3 4 5 6 7 8 9 10
Nótese que el orden de visita coincide con la numeración utilizada en los vértices,
que era, precisamente, una numeración por niveles (asignamos números sucesivos de
arriba a abajo y de izquierda a derecha).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 136 Diseña una versión de la función de recorrido por niveles que explore un árbol
representado mediante una lista de listas. El árbol de la figura 9.1 se suministrará a la
función codificado ası́: [0, [1, [3, [5], [6, [9], [10]], [7]], [4, [8]]], [2]].
......................................................................................
15 recursive_depth_first_traversal (G, v)
0 1 3 5 6 9 10 7 4 8 2
Es posible diseñar una versión iterativa usando una cola LIFO que, en cierto sentido,
simula la pila de llamadas a función:
1 2
3 4
5 6 7 8
9 10
Figura 9.3: Recorrido por primero en profundidad del árbol de la figura 9.1.
0 1 3 5 6 9 10 7 4 8 2
Hemos dicho antes que hay tres tipos diferentes de recorrido por primero en profun-
didad: dos, los recorridos en preorden y postorden, son de aplicación a cualquier tipo
de árbol y el tercero, recorrido en inorden, sólo es aplicable a los árboles binarios. Sólo
se diferencian en el instante en que efectúan el procesado de los nodos. Presentamos
algoritmos para los tres tipos de recorrido, pero sólo en su versión recursiva. (Nótese
que recursive_depth_first_traversal es el recorrido en preorden).
Recorrido en preorden: 0 1 3 5 6 9 10 7 4 8 2
Recorrido en postorden: 5 9 10 6 7 3 8 4 1 2 0
0 0
1 2 1 2
3 4 3 4
5 6 7 8 5 6 7 8
9 10 9 10
(a) (b)
Figura 9.4: Recorrido en (a) preorden y (b) postorden del árbol de la figura 9.1.
Siguiendo las lı́neas de trazo discontinuo se puede ver el orden de procesado de los
vértices: los triángulos marcan los instantes en que se procesan los respectivos vértices.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 137 Haz una traza de in_order_traversal (véase el programa tree traversal.py)
para un árbol binario completo con 7 vértices.
- *
5 3 5
En esta figura se muestra el efecto del recorrido sobre el árbol del ejemplo:
10
+
15
−5
- *
5 5
3
5 3 5
Esta codificación de las La lista de listas con la que representaremos un árbol como el de la figura es
expresiones aritméticas es muy
similar a la propia del lenguaje
["+", ["-", [5]], ["*", [3], [5]]]. El resultado de evaluar la expresión que repre-
de programación Lisp. La senta ese árbol es el valor 10.
expresión ((-5 + 3 * 5)) se
codifica en Lisp con esta lista:
Diseña una función que reciba un árbol codificado como lista de listas y devuelva el
(((+ (- 5) (* 3 5)))). resultado de evaluar la expresión que representa.
· 140 Disponemos de un lenguaje ensamblador para un ordenador que efectúa ope-
raciones aritméticas con una pila. Las instrucciones para apilar y desapilar son PUSH
valor y POP, respectivamente. Las siguientes instrucciones toman dos elementos de la
pila, les aplican una operación (la cima de la pila es el operando derecho y el elemento
que hay debajo de la pila es el operando izquierdo) y dejan el resultado en la pila: ADD
(suma), SUB (resta), MUL y (producto) DIV (división). Una operación adicional, CHSGN,
cambia el signo del elemento de la cima de la pila.
Diseña una función que reciba un árbol que codifica una expresión aritmética y
muestre por pantalla las instrucciones de ensamblador que permiten evaluarla en el
computador.
Por ejemplo, si le suministramos este árbol, ["+", ["-", [5]], ["*", [3], [5]]],
mostrará por pantalla el siguiente texto:
PUSH 5
CHSGN
PUSH 3
PUSH 5
MUL
SUM
......................................................................................
1. Se marca como visitado el vértice que nos suministran y se inserta en una cola FIFO.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 141 Haz una traza de breadth_first_search al invocarse sobre el vértice 0 de este
grafo:
0 1 2
3 4 5
bfs1.py bfs1.py
1 from fifo import FIFO
2 from sets import Set
3
10 action(u)
11 for v in G.succs(u):
12 if v not in visited :
13 visited.add (v)
14 Q.append (v)
La función action tiene un solo parámetro: el vértice que se procesa. Podemos poner
a prueba nuestra función con este sencillo programa:
8 def show_node(u):
9 print "Visitando el vértice", u
10
11 breadth_first_search(G, 0, show_node)
0 1 2
3 4 5
Visitando el vértice 0
Visitando el vértice 1
Visitando el vértice 3
Visitando el vértice 4
0 1 2 0 1 2
Q Q 0
3 4 5 3 4 5
1. Inicialmente no hay ningún vértice visita- 2. Empezamos visitando el vértice 0. En la
do y la cola Q está vacı́a. cola Q ingresa el vértice 0.
0 1 2 0 1 2
Q Q 1 3
3 4 5 3 4 5
3. Se saca de la cola su primer (y único) 4. Se insertan en Q los sucesores de u, es
elemento: el vértice 0, que se procesa y al- decir, los vértices 1 y 3, que son los sucesores
macena en una variable u. del vértice 0 (se visitan).
0 1 2 0 1 2
Q 3 Q 3 4
3 4 5 3 4 5
5. A continuación, se extrae el primer ele- 6. Se añade a la cola el vértice 4, que es el
mento de la cola (el vértice 1) y se procesa. único sucesor del vértice 1.
0 1 2 0 1 2
Q 4 Q 4
3 4 5 3 4 5
7. Extraemos de Q y procesamos el vértice 8. Los sucesores del vértice 3 (los vértices 0
3. y 1) no ingresan en Q porque ya han sido
visitados.
0 1 2 0 1 2
Q Q
3 4 5 3 4 5
9. Se extrae ahora el vértice 4 y se procesa. 10. El vértice 3, sucesor del vértice 4, no in-
gresa en Q porque ya ha sido visitado. El al-
goritmo termina porque la cola Q está vacı́a.
Figura 9.6: Traza de la exploración por primero en anchura sobre el grafo de la figu-
ra 9.5 empezando en el vértice 0.
¿Es posible alcanzar cualquier casilla del tablero desde cualquier otra con un
caballo?
¿Qué casillas podemos alcanzar con un peón blanco que parte del escaque D1?
Figura 9.7: Un grafo no dirigido en el que los vértices se han dispuesto en circunfe-
rencias concéntricas según la longitud que los separa del vértice central.
cola habrán ingresado entonces todos los vértices adyacentes a estos vértices, es decir,
habrán ingresado todos los vértices separados del central por dos aristas (figura 9.8 (c)).
(a) (b)
(c) (d)
Figura 9.8: Traza de la exploración por primero en anchura sobre el grafo de la figu-
ra 9.7. (a) Iniciamos un recorrido en anchura en el vértice central, (b) se visitan primero
los vértices alcanzables desde él (primera circunferencia); a continuación, (c) los alcan-
zables desde los vértices marcados en la anterior figura; y, finalmente, (d) los vértices
exteriores.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 143 Haz una traza del algoritmo de recorrido por primero en anchura en el grafo
que mostramos a continuación. Empieza por el vértice central.
......................................................................................
bfs.py bfs.py
1 from fifo import FIFO
2 from sets import Set
3
8 def show_node(u):
9 print "Visitando el vértice", u
10
14 print
15 print "Resultado de full breadth first search"
16 full_breadth_first_search(G, show_node)
Resultado de full_breadth_first_search
Visitando el vértice 0
Visitando el vértice 1
Visitando el vértice 3
Visitando el vértice 4
Visitando el vértice 2
Visitando el vértice 5
0 1 2 0 1 2
Q Q 5
3 4 5 3 4 5
11. Supongamos que se selecciona aho- 12. Ingresa en Q el vértice 5, pero no el vérti-
ra el vértice 2. Al llamar a la función ce 4.
breadth_first_search(G, 2, visited ) se visita
y procesa el vértice 2.
0 1 2 0 1 2
Q Q
3 4 5 3 4 5
13. El siguiente paso consiste, pues, en ex- 14. El único sucesor del vértice 5 es el mismo
traer de Q el vértice 5 y procesarlo. vértice 5, pero no ingresa en Q porque ya ha
sido visitado.
Figura 9.9: Traza de la exploración de todos los vértices por primero en anchura sobre
el grafo de la figura 9.5. Esta figura muestra los pasos que se dan después de los que se
ilustran en la figura 9.6.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 144 Haz una traza del algoritmo full_breadth_first_search sobre este grafo no diri-
gido:
0 2 4 6 8
1 3 5 7
......................................................................................
Análisis de complejidad
El coste del algoritmo depende de la representación del grafo. Si el grafo tiene |V |
vértices y |E| aristas y se representa mediante listas de adyacencia, el coste temporal
es O(|V | + |E|): se visitan todos los vértices una sola vez y para cada uno de ellos se
recorren todas las aristas que parten de él. Si el grafo se representa mediante una matriz
de adyacencia, el coste temporal pasa a ser O(|V |2 ), pues el cálculo de los sucesores
requiere tiempo O(|V |). El espacio necesario es O(|V |), pues la pila Q puede llegar a
almacenar |V | − 1 vértices.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 145 Escribe una función que determine si un grafo no dirigido es conexo mediante
una exploración en anchura de sus vértices. ¿Cuál es el coste temporal del algoritmo?
· 146 El recorrido por primero en profundidad permite detectar si un grafo presenta
ciclos.Implementa una función detecta_ciclos que devuelva True si el grafo contiene La detección de ciclos es un
problema de gran interés
ciclos y False en caso contrario. ¿Qué complejidad computacional tiene? práctico para, por ejemplo,
...................................................................................... detectar situaciones de bloqueo
mutuo (deadlock ), es decir,
situaciones en las que una tarea
espera a la finalización de otra
que, directa o indirectamente
Una aplicación del recorrido por primero en anchura: cálculo del número de depende de la primera.
aristas que separan a dos vértices
Hemos visto que la exploración por primero en anchura recorre los vértices por nive-
les. Tras visitar el primer vértice, visita a todos los que se encuentran a una arista
de distancia del primero. Sólo cuando los ha visitado todos, visita todos los vértices
alcanzables desde el primero con dos aristas. Y ası́ sucesivamente. Podemos aprovechar
esta propiedad para diseñar un algoritmo que calcule el menor número de aristas que
se deben recorrer para ir de un vértice s a un vértice t:
test bfs shortest path length.py test bfs shortest path length.py
1 from graph import Graph
2 from bfs_shortest_path_length import bfs_shortest_path_length
3
Más adelante estudiaremos un algoritmo para el problema del camino más corto
en un grafo (no el de menor número de aristas, sino el de menor suma de pesos o
distancias) que se inspira en la exploración por primero en anchura, pero que usa una
cola de prioridad en lugar de una cola FIFO: el algoritmo de Dijkstra.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 147 Diseña un programa que calcule el menor número de ciudades que hemos
de travesar para viajar entre ciudades cualesquiera del mapa de la penı́nsula ibérica
descrito en el fichero iberia.
· 148 Podemos representar un laberinto sobre un tablero matricial de n × m casillas
en el que cada casilla es pared, pasillo, entrada o salida y en el que la única entrada y
la única salida se encuentran en los bordes del tablero. He aquı́ una ilustración de un
laberinto que cumple estas condiciones:
Diseña:
a) Una función que, dada una descripción de un laberinto como lista de cadenas de-
vuelva un grafo G = (V, E) que lo represente en el que el vértice 0 corresponde a
la casilla de entrada y el vértice |V | − 1 corresponda a la casilla de salida. Cada
carácter de las cadenas codificará el tipo de la casilla correspondiente (por ejemplo,
’x’ es pared, ’ ’ es pasillo, ’e’ es entrada y ’s’ es salida). Por ejemplo, esta matriz
modela el laberinto del ejemplo:
[’xxxxxxx’,
’x x x s’,
’x xxx x’,
’e x x x’,
’x x x’,
’xxxxxxx’]
c) Una función que nos devuelva la longitud del camino más corto entre el vértice de
El problema del camino más entrada y el de salida.
corto en un laberinto se
solucionó en el artı́culo ((The
shortest path through a maze)), 208 Apuntes de Algorı́tmica
International Symposium on the
Theory of Switching (1959),
Harvard University Press.
c 2003 A. Marzal, M.J. Castro y P. Aibar 9 Algoritmos sobre grafos
d) Una función que nos devuelva la secuencia de vértices que corresponde al camino
más corto entre la entrada y la salida.
Al igual que ocurrı́a con el recorrido por primero en anchura, no hay un único
recorrido por primero en profundidad, pues no hay un orden definido entre los sucesores
de un vértice.
dfs.py dfs.py
1 from sets import Set
2
Esta primera versión del recorrido por primero en profundidad es una generaliza-
ción del recorrido en preorden de un árbol: el vértice se procesa tan pronto se invoca el
método depth_first_search sobre él, antes de iniciar la visita de sus sucesores. Una ver-
sión simétrica generaliza el recorrido en postorden procesando cada vértice únicamente
cuando todos sus sucesores han sido procesados:
dfs.py dfs.py
10 def depth_first_search_post(G, u, action, visited = Set()):
11 visited.add (u)
12 for v in G.succs(u):
13 if v not in visited :
14 depth_first_search(G, v, action, visited )
15 action(u)
16
17 depth_first_search_post = depth_first_search
5 6 7 8 5 6 7 8 5 6 7 8
2 3 4 2 3 4 2 3 4
0 1 0 1 0 1
5 6 7 8 5 6 7 8 5 6 7 8
2 3 4 2 3 4 2 3 4
0 1 0 1 0 1
5 6 7 8 5 6 7 8 5 6 7 8
2 3 4 2 3 4 2 3 4
0 1 0 1 0 1
5 6 7 8 5 6 7 8 5 6 7 8
2 3 4 2 3 4 2 3 4
0 1 0 1 0 1
5 6 7 8 5 6 7 8 5 6 7 8
2 3 4 2 3 4 2 3 4
0 1 0 1 0 1
· 150 Haz una traza del algoritmo de exploración por primero en profundidad sobre
este grafo no dirigido:
2 1 3 4
5 6 7
· 151 Diseña un programa que construya laberintos siguiendo una estrategia basada
en la exploración por primero en profundidad. Codificaremos el laberinto de modo
diferente al propuesto en el ejercicio 148. Usaremos una matriz en la que cada celda
indica si hay pared al norte, al sur, al este y/o al oeste. He aquı́ un laberinto de este
tipo:
Si codificamos cada celda de la matriz con una cadena que indica qué muros están
presentes (’n’ para norte, ’s’ para sur, ’w’ para oeste, y ’e’ para este ’e’), el laberinto
del ejemplo se describirá con esta matriz:
[[’wn’, ’nes’, ’wns’, ’ne’],
[’ws’, ’n’, ’ne’, ’w’],
[’ns’, ’e’, ’wes’, ’we’],
[’wns’, ’s’, ’ns’, ’es’]]
Inicialmente, todas las celdas de la matriz tienen cuatro muros. Inicia un recorrido Resulta más eficiente codificar
con cuatro bits la existencia de
aleatorio del laberinto (escoge las direcciones inexploradas al azar) por primero en paredes. El valor 15, que en
profundidad desde una de las casillas exteriores. Ve destruyendo los muros que separan binario es 1111, representarı́a
una casilla con pared en las
celdas conforme avanza la exploración. Al final, rompe los muros exteriores de dos cuatro direcciones. Recuerda
casillas para que el laberinto tenga una entrada y una salida. que puedes activar y desactivar
bits con operaciones como & y |.
He aquı́ un grafo generado por el procedimiento descrito sobre una matriz de 40 × 20 Y aún más eficiente resulta
celdas: codificar en cada casilla la
posible existencia de sólo 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.
......................................................................................
8 def show_node(u):
9 print "Visitando el vértice", u
10
Análisis de complejidad
El algoritmo de exploración por primero en profundidad es O(|V | + |E|) si el grafo
se representa con listas o conjuntos de adyacencia y O(|V |2 ) si se representa con una
matriz de adyacencia: cada vértice del grafo es visitado una sola vez y para cada vértice
visitado se recorren todas sus aristas de salida. Su complejidad espacial es O(|V |): la
pila de llamadas recursivas puede llegar a tener altura |V |.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 152 Haz una traza del algoritmo de exploración por primero en profundidad sobre
el grafo de la figura 9.5:
a) Empezando en el vértice 0.
b) Empezando en el vértice 2.
· 153 Muestra ordenadamente los vértices visitados al explorar por primero en pro-
fundidad en el grafo de la figura 9.7:
C#
Java
C ++ Objective C
(a)
C C ++ Java Objective C C#
(b)
C C ++ Java C# Objective C
(c)
Figura 9.11: Tres representaciones del grafo de los lenguajes de programación. (a)
Representación arbitraria. (b) Representación que muestra los vértices ordenados to-
pológicamente de izquierda a derecha. (c) Representación similar a la anterior, pero con
un orden topológico distinto.
Si hay una relación (u, v), la tarea v debe ejecutarse después de completar la tarea
u, ası́ que le corresponde un ((instante de finalización)) superior. Esto se puede conseguir
si efectuamos un recorrido por primero en profundidad ejecutando una acción en pos-
torden: la asignación del ((instante de finalización)). Eso sı́, hemos de asignar ((instantes
de finalización)) en orden decreciente. Este programa ilustra la idea:
topsort.py topsort.py
1 from sets import Set
2
12 def topsort(G):
13 global _counter
14 visited = Set()
15 f = [None] * len(G.V )
16 _counter = len(G.V )
17 for v in G.V :
18 if v not in visited :
19 topsort_from(G, v, visited , f )
20 return f
8 for u in topsort(G):
9 print u
C
C++
Java
Objective C
C#
Demostración. Cada vértice se visita una sola vez y recibe, al finalizar la visita, un
valor numérico determinado por un contador, ası́ que cada vértice v recibe un valor
numérico f [v] diferente. Basta con demostrar que si (u, v) ∈ E, f [u] < f [v]. Si se
invoca topsort_from sobre u, recibirá un valor f [u] menor que f [v], pues la visita a v
finaliza antes que la visita a u. El único problema es, pues, que se llame a topsort_from
sobre v antes de que se produzca la llamada a la misma función sobre u. En tal caso, v
también habrá completado su visita antes de que se efectúe la visita a u y tendrá, por
tanto, un valor del ((instante de finalización)) mayor.
2 3 4
1 6 10 11 5
7 8 9
A continuación, dibuja el grafo con los vértices dispuestos a lo largo de un eje horizontal
respetando el orden topológico.
· 156 ¿Cuántas ordenaciones topológicas diferentes admite un grafo con n vértices
y ninguna arista?
· 157 ¿Qué ocurre si suministramos un grafo no conexo al algoritmo de ordenación
topológica? Si el algoritmo desarrollado no visita todos los vértices, modifı́calo para que
lo haga.
· 158 Al planificar un proyecto hay tareas que dependen de la terminación de otras.
Nos suministran un fichero en el que cada lı́nea contiene una tarea (descrita por ca-
racteres y guiones) y una secuencia de tareas que deben ejecutarse antes. Por ejemplo:
ejemplo tareas.py
1 alguilar-perforadora estudio-mercado-perforadoras
4 def connected_components(G):
5 sets = MFset()
6 for u in G.V :
7 sets.new (u)
8 for u in G.V :
9 for v in G.succs(u):
10 sets.merge(u, v)
11
12 conncomps = {}
13 for u in G.V :
14 v = sets.find (u)
15 if v in conncomps:
16 conncomps[v].add (u)
17 else:
18 conncomps[v] = Set([u])
19 return conncomps.values()
0 2 4 6 8
1 3 5 7
7 print connected_components(G)
Ciertos algoritmos sólo funcionan correctamente si el grafo es conexo, ası́ que el test
de conectividad puede constituir un paso previo necesario.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 163 ¿Qué complejidad computacional presenta el algoritmo connected_components?
(Exprésalo en función de |V | y/o |E|.)
· 164 Diseña un programa que determine si un grafo no dirigido es o no conexo.
· 165 Diseña una función que reciba un laberinto como los descritos en el ejercicio 151
y nos diga si la entrada y la salida están conectadas. Utiliza al algoritmo de cálculo de
las componente conexas.
......................................................................................
1 def boolean_matrix_transitive_closure(M ):
2 R = []
3 for i in range(len(M )):
4 R.append ([False] * len(M ))
5
Demostración. Podemos demostrarlo por inducción sobre k, el ı́ndice del bucle exterior
e interpretando la matriz en términos de la matriz de adyacencia para un grafo cuyos
vértices forman un rango de enteros consecutivos y empezado en cero. La hipótesis de
inducción es ((la iteración k-ésima pone a True la celda M [i][j] si y sólo si hay un
camino de i a j que no incluye vértices mayores que k)).
Base de inducción Tras la primera iteración M [i][j] vale True si valı́a True origi-
nalmente o si tanto M [i][0] como M [0][j] valı́an True.
He aquı́ una versión del algoritmo formulada en términos de grafos que devuelve una
matriz de adyacencia indexada por vértices a partir de la cual es inmediato construir el
grafo inducido por la clausura transitiva de G:
warshall.py
1 def warshall (G):
2 D = {}
3 for u in G.V :
4 for v in G.V :
5 D[u,v] = u == v or v in G.succs(u)
6
7 for v in G.V :
8 for u in G.V :
9 for w in G.V :
10 D[u,w] = D[u,w] or (D[u,v] and D[v,w])
11
12 return D
warshall.py warshall.py
1 def warshall (G):
2 D = {}
3 for u in G.V :
4 for v in G.V :
5 D[u,v] = u == v or v in G.succs(u)
6
7 for v in G.V :
8 for u in G.V :
9 if D[u,v]:
10 for w in G.V :
11 D[u,w] = D[u,w] or (D[u,v] and D[v,w])
12
13 return D
a) Un grafo en el que un vértice está unido por sendas aristas a todos los demás vértices.
c) Un grafo completo.
· 169 Es posible calcular la clausura positiva de un grafo dirigido acı́clico más efi-
cientemente a partir de su ordenación topológica. ¿Cómo? ¿Con qué coste?
· 170 La operación ((inversa)) recibe el nombre de reducción transitiva (transitive
reduction) y es un grafo tal que u y v están unidos por un camino si y sólo si (u, v)
es una arista del grafo original. Tiene interés calcular la reducción transitiva de menor
número de aristas (también conocida por ((digrafo equivalente mı́nimo))). ¿Cómo puede
calcularse?
......................................................................................
2 5 2 5 2 5
0 8 0 8 0 8
3 6 3 6 3 6
1 9 1 9 1 9
4 7 4 7 4 7
Figura 9.13: (a) Grafo no dirigido. (b) Árbol de recubrimiento sobre el grafo (aristas
en trazo grueso). (c) Otro árbol de recubrimiento.
Nótese que es posible transitar de cualquier vértice a cualquier otro visitando úni-
camente aristas del árbol de recubrimiento.
Un árbol de recubrimiento mı́nimo o MST de un grafo no dirigido y ponderado Árbol de recubrimiento
mı́nimo: Minimum spanning
G = (V, E, d) es aquel árbol de recubrimiento T ⊆ E cuya suma de pesos tree (MST).
X
D(T ) = d(u, v)
(u,v)∈T
es menor o igual que la de cualquier otro árbol de recubrimiento. Nótese, pues, que serı́a más
apropiado referirnos al árbol de
recubrimiento con coste
Apuntes de Algorı́tmica 219 mı́nimo, pero en la literatura se
le conoce como ((árbol de
recubrimiento mı́nimo)).
9.5 El problema del árbol de recubrimiento de coste mı́nimo 2003/12/09-12:57
4 4
15 2 5 10 15 2 5 10
8 8
0 2 11 16 1 8 0 2 11 16 1 8
12 12
0 3 3 6 6 7 0 3 3 6 6 7
1 13 5 17 14 9 1 13 5 17 14 9
9 9
4 7 4 7
(a) (b)
Figura 9.14: (a) Grafo no dirigido y ponderado. (b) Árbol de recubrimiento mı́nimo
(MST) sobre el grafo (aristas en trazo continuo).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 171 Dado un grafo G no dirigido y conexo, modifica el algoritmo de recorrido por
primero en profundidad para que devuelva las aristas que formarı́an parte de un árbol
de recubrimiento para el grafo G.
· 172 Dado un grafo no dirigido, ponderado y conexo, ¿devuelve un MST el algoritmo
que has diseñado como solución al ejercicio 171? Razona tu respuesta.
......................................................................................
4 4 4
15 2 5 10 15 2 5 10 15 2 5 10
8 8 8
0 2 11 16 1 8 0 2 11 16 1 8 0 2 11 16 1 8
12 12 12
0 3 3 6 6 7 0 3 3 6 6 7 0 3 3 6 6 7
1 13 5 17 14 9 1 13 5 17 14 9 1 13 5 17 14 9
9 9 9
4 7 4 7 4 7
4 4 4
15 2 5 10 15 2 5 10 15 2 5 10
8 8 8
0 2 11 16 1 8 0 2 11 16 1 8 0 2 11 16 1 8
12 12 12
0 3 3 6 6 7 0 3 3 6 6 7 0 3 3 6 6 7
1 13 5 17 14 9 1 13 5 17 14 9 1 13 5 17 14 9
9 9 9
4 7 4 7 4 7
4 4 4
15 2 5 10 15 2 5 10 15 2 5 10
8 8 8
0 2 11 16 1 8 0 2 11 16 1 8 0 2 11 16 1 8
12 12 12
0 3 3 6 6 7 0 3 3 6 6 7 0 3 3 6 6 7
1 13 5 17 14 9 1 13 5 17 14 9 1 13 5 17 14 9
9 9 9
4 7 4 7 4 7
4 4 4
15 2 5 10 15 2 5 10 15 2 5 10
8 8 8
0 2 11 16 1 8 0 2 11 16 1 8 0 2 11 16 1 8
12 12 12
0 3 3 6 6 7 0 3 3 6 6 7 0 3 3 6 6 7
1 13 5 17 14 9 1 13 5 17 14 9 1 13 5 17 14 9
9 9 9
4 7 4 7 4 7
12 forest = MFset()
13 for u in G.V : forest.new (u)
14
15 i=0
16 for (weight, (u,v)) in E: # Recorremos las aristas.
17 if forest.find (u) != forest.find (v): # si u y v están en conjuntos diferentes. . .
18 T [i] = (u,v)
19 i += 1
20 forest.merge(u, v) # . . . unimos sus respectivos conjuntos en uno solo.
21 if i == len(G.V )-1: break
22 return T
8 dist = {(0,1): 0, (0,2): 15, (0,3): 2, (1,3): 3, (1,4): 13, (2,3): 11, (2,5): 4,
9 (3,4): 5, (3,5): 8, (3,6): 12, (4,7): 9, (5,6): 16, (5,8):10, (6,7): 17,
10 (6,8): 1, (6,9): 6, (7,9): 14, (8,9): 7}
11 def d (u,v):
12 if (u,v) in dist: return dist[u,v]
13 return dist[v,u]
14
15 E = Kruskal (G, d)
16 print ’MST:’, E
17 print ’Peso del MST:’, sum ([ d(u,v) for (u,v) in E])
MST: [(0, 1), (6, 8), (0, 3), (2, 5), (3, 4), (6, 9), (3, 5), (4, 7), (5, 8)]
Peso del MST: 45
Figura 9.16: Árbol de recubrimiento mı́nimo para las ciudades de la penı́nsula ibérica.
Si hubiésemos admitido que pudiera haber más de un MST, hubiésemos tenido que
considerar la posibilidad de que D(T 0 ) = D(T ), lo que implicarı́a d(u, v) = d(u0 , v 0 ). En
tal caso, tanto T como T 0 serı́an MST y hubiera dado igual escoger e o e0 , pues ambos
forman parte de un MST.
Demostración. Con cada iteración, el algoritmo escoge la arista e que conecta dos re-
giones disjuntas del grafo y lo hace seleccionando la arista de menor peso de cuantas no
ha considerado todavı́a.
Análisis de complejidad
La complejidad temporal es O(|V | + |E| lg |E|). Ordenar el vector de aristas por orden
creciente de peso es O(|E| lg |E|). El algoritmo efectúa un máximo de |E| iteraciones
y un máximo de |E| consultas de pertenencia y de |V | fusiones de conjuntos sobre el
MFSet. El coste temporal de estas operaciones se considera globalmente O(|E|). El algoritmo de Prim, que
estudiaremos más adelante,
resuelve este problema en
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . tiempo O(|V |2 ), ası́ que es
· 173 Realiza una traza del algoritmo de Kruskal para los grafos de las siguientes preferible al de Kruskal si el
grafo es denso.
figuras:
1
6 3
8 7
1 2 3 4
2 3 2 5 4 2 9
0 4
4 0
1 11 6 14 5
4 2 8 7 6 10
2 1 2
3 4 7 8 9
· 175 Supongamos que hemos obtenido un MST para un grafo G no dirigido, conexo
y ponderado. Si a continuación se añadiera un nuevo vértice y la arista o aristas que
lo conectan al resto de los vértices, ¿de qué forma se podrı́a actualizar rápidamente el
MST, sin necesidad de ejecutar nuevamente el algoritmo?
· 176 El algoritmo de Kruskal puede utilizarse para generar laberintos como los
descritos en el ejercicio 151. El procedimiento a seguir es éste: Dispón muros sólo en el
perı́metro exterior del laberinto. Pondera cada enlace entre dos celdas vecinas con peso
nulo y calcula el MST (todos los árboles de recubrimiento serán MSTs). Recorre los arcos
en cualquier orden (todos pesan lo mismo) y obtén una arborescencia de recubrimiento.
Pon muros de separación entre todo par de vértices contiguos que no estén unidos en
el MST. Finalmente, elimina dos muros exteriores para crear la entrada y la salida del
laberinto. Implementa este método de generación y representa gráficamente el laberinto
resultante.
· 177 El laberinto puede construirse sobre una malla hexagonal. He aquı́ un ejemplo:
a) Diseña un programa que indique qué pares de ciudades hemos de conectar entre
sı́ suponiendo que es posible comunicar dos ciudades con una cantidad de cable igual
a la distancia que las separa en lı́nea recta, haya o no carretera que las una.
b) Diseña un programa que indique qué pares de ciudades hemos de conectar entre
sı́ suponiendo que es posible comunicar dos ciudades con una cantidad de cable igual
a la distancia que las separa en lı́nea recta, pero sólo si hay carretera entre ellas.
D̂ = min D(v1 , v2 , . . . , vn ).
(v1 ,v2 ,...,vn )∈P (s,t)
Muchos de los problemas que estudiaremos se formulan finalmente como una ins-
tancia del problema del camino más corto, del problema de la distancia mı́nima o una
variante de estos. Será frecuente en temas posteriores, pues, plantear la búsqueda de un
camino óptimo entre dos vértices.
En las representaciones gráficas de los problemas de búsqueda del camino más corto
en un grafo, destacaremos los vértices inicial y final con, respectivamente, una flecha de
entrada y una de salida. Si deseamos calcular el camino más corto entre los vértices 0 y
5 del grafo de la figura 9.17 (a), representaremos el grafo como se muestra en la figura
figura 9.17 (b).
3 2 3 2
0 1 2 0 1 2
1 2 3 1 2 3
1 2 1 1 2 1
2 2
3 4 5 3 4 5
(a) (b)
Figura 9.17: (a) Grafo dirigido y ponderado. (b) El mismo grafo, pero con los vértices
inicial (s = 0) y final (t = 5) destacados.
No hay problema en considerar más de un vértice final. El camino más corto entre
s y uno cualquiera de dos vértices finales, t1 y t2 , por ejemplo, es el camino más corto
de entre el camino más corto de s a t1 y el camino más corto de s a t2 .
Vamos a proponer algoritmos diferentes en función de ciertas propiedades que deben
cumplir los grafos. La única restricción que impondremos a todos los grafos es natural:
el grafo no podrá tener ciclos cuya suma de pesos sea negativa. Si los pesos de un ciclo
son negativos, no hay ((camino más corto)): siempre es posible mejorar el peso de un
camino añadiendo el ciclo de peso negativo. El problema estarı́a, pues, mal definido.
Hemos llegado a una definición recursiva del conjunto de caminos entre dos vértices,
pues expresa P (s, t) en función de P (s, u), para todo u predecesor de t. Ello conduce a
un algoritmo, también recursivo, que genera todos los caminos entre s y t:
Tomemos por caso el grafo de la figura 9.18 y la búsqueda del camino más corto
entre sus vértices (0, 0) y (2, 2).
4 5 6 7
1 2 3 4 5
0 1 2 3 4 5
Figura 9.18: Un ejemplo de grafo ponderado. Cada vértice se une a los dos siguientes
test brute force shortest path.py test brute force shortest path.py
1 from graph import Graph
2 from brute_force_shortest_path import brute_force_paths, brute_force_shortest_path,\
3 path_distance
4
5 n=6
6 G = Graph(V =range(n),
7 E=[(u,u+1) for u in range(n-1)]+[(u,u+2) for u in range(n-2)])
8
9 def d (u,v):
10 if v-u == 1: return v
11 return v+2
12
[0, 2, 3, 4, 5] 16
[0, 1, 2, 3, 4, 5] 15
Camino más corto: [0, 1, 3, 5]
D̂ = min D(v1 , v2 , . . . , vn ),
(v1 ,v2 ,...,vn )∈P (s,t)
No hay nada especial en t, ası́ que podrı́amos haber deducido la recursión para un
vértice v cualquiera:
D(s, v) = min (D(s, u) + d(u, v)) .
(u,v)∈E
D(s, s) = 0.
D̂ = D(s, t).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 180 Hemos dicho antes que podemos ponderar un camino mediante el producto de
los pesos de sus aristas, y no sólo mediante la suma. ¿Es posible efectuar una derivación
como la que acabamos de presentar si sustituimos sumatorios por productorios?
......................................................................................
Un algoritmo recursivo
Podemos ((traducir)) la ecuación recursiva a un programa Python y obtener ası́ la im-
plementación de un algoritmo recursivo.
shortest path.py shortest path.py
1 def recursive_dag_shortest_distance(G, d, s, v, infinity= 3.4e+38):
2 if v == s:
3 return 0
4 elif len(G.preds(v)) == 0:
5 return infinity
6 else:
7 return min([recursive_dag_shortest_distance(G,d,s,u,infinity) + d(u,v) \
8 for u in G.preds(v)])
Un algoritmo iterativo
Si al calcular D(s, v) se produce una ((llamada)) recursiva a D(s, u), es porque (u, v)
es una arista. O sea, u es un vértice anterior a v en cualquier orden topológico de los
vértices del grafo (recordemos que es un grafo acı́clico). Podemos diseñar una versión
iterativa basada en un recorrido de los vértices del grafo en orden topológico:
13 D = {}
14 for v in topsort(G):
15 if v == s:
16 D[s,v] = 0
17 elif len(G.preds(v)) == 0:
18 D[s,v] = infinity
19 else:
20 D[s,v] = min( [ D[s,u] + d(u,v) for u in G.preds(v) ] )
21 if v == t: break
22
23 return D[s, t]
2
3 4 5
10 def d (u,v):
11 return dist[u,v]
12
2
2
3 2 1
3 2 1
1 1 2 3 2
1 1 2 3 2 0 3 1 2 4 5
0 3 1 2 4 5
0
(a) (b)
2 2
3 2 1 3 2 1
1 1 2 3 2 1 1 2 3 2
0 3 1 2 4 5 0 3 1 2 4 5
0 0+1 0 1 mı́n(0 + 3, 1 + 1)
(c) (d)
2 2
3 2 1 3 2 1
1 1 2 3 2 1 1 2 3 2
0 3 1 2 4 5 0 3 1 2 4 5
0 1 2 2+2 0 1 2 4 mı́n(2 + 2, 4 + 3)
(e) (f)
2
3 2 1
1 1 2 3 2
0 3 1 2 4 5
0 1 2 4 4 mı́n(2 + 2, 4 + 1, 4 + 2)
(g)
2
3 2 1
1 1 2 3 2
0 3 1 2 4 5
0 1 2 4 4 4
(h)
Figura 9.20: Traza del algoritmo de cálculo del camino más corto en el grafo acı́clico
ponderado de la figura 9.19.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 181 Queremos calcular la distancia del camino más corto en grafos ponderados
estructurados como éste:
0 1 2 3 4 5 6 7 8 9
Nótese que cada vértice tiene únicamente dos (o menos) aristas: una a cada uno de los
dos vértices siguientes. El algoritmo presentado requiere espacio Θ(|V |) para resolver
el problema. ¿Puedes diseñar un algoritmo inspirado en éste que reduzca el espacio
necesario a Θ(1)?
......................................................................................
Cálculo del camino más corto: la técnica ((seguir la pista hacia atrás))
Hemos aprendido a calcular el peso del camino más corto, pero no sabemos cómo calcular
el camino. En realidad es sencillo si aplicamos una técnica que utilizaremos muchas veces
Seguir la pista hacia atrás: de ahora en adelante: la técnica que llamamos ((seguir la pista hacia atrás)). Consiste en
Backtracing.
recordar, para cada vértice v, de qué vértice u ((viene)) la solución óptima hasta dicho
punto. Recordar significa escribir en un vector indexado por v el valor u. Esta primera
versión implementa esta idea de un modo un tanto farragoso:
20 path = [t]
21 while backp[path[-1]] != None:
22 path.append ( backp[path[-1]] )
23 path.reverse()
24
El vector backp, cuyo nombre es prefijo del término inglés ((backpointer)) (por ((puntero
hacia atrás))), almacena la información necesaria para recuperar el camino. Es posible
recuperar el camino si se sigue la sucesión de punteros hacia atrás desde el nodo t.
Hagamos una traza paso a paso centrándonos en backp, como ilustra la figura 9.21.
Tras ordenar topológicamente los vértices del grafo, empezamos con el vértice 0. Como
es el vértice inicial, su puntero atrás, backp[s] se inicializa a None (figura 9.21 (a)).
Sólo hay un predecesor del vértice 3, el vértice 0, ası́ que backp[3] apunta a él (figu-
ra 9.21 (b)). Pasamos al vértice 1. Se puede llegar a este vértice desde dos vértices. El
camino óptimo viene del vértice 3, ası́ que backp[1] apunta al vértice 3 (figura 9.21 (c)).
Pasamos al vértice 2. Sólo se puede llegar desde el vértice 1 (figura 9.21 (d)). Pasamos
al vértice 4. El camino óptimo viene del vértice 1 (figura 9.21 (e)). Ahora calculamos
D[0,5] y backp[5]. Este último apunta al vértice 1 (figura 9.21 (f)). El camino ópti-
mo del vértice 0 al vértice 5 se destaca ahora en trazo más grueso (figura 9.21 (h)).
Recorriendo hacia atrás los punteros de backp desde el vértice 5 y hasta llegar al valor
None vamos formando el camino de distancia mı́nima. El recorrido obtiene el camino
en orden inverso: [5, 1, 3, 0]. Es necesario, pues, invertir el resultado para obtener el
camino óptimo: [0, 3, 1, 5].
Podemos codificar el procedimiento anterior ası́:
shortest path.py shortest path.py
25 def dag_shortest_path(G, d, s, t, infinity=3.4e+38):
26 # Devuelve la distancia del camino mı́nimo y la lista de vértices que lo forman.
27 sorted = topsort(G)
28 D = {}
29 backp = {}
30
31 for v in sorted :
32 if v == s:
33 D[v], backp[v] = 0, None
34 elif G.preds(v):
35 D[v], backp[v] = min( [ (D[u] + d(u,v), u) for u in G.preds(v) ] )
36 else:
37 D[v], backp[v] = infinity, None
38 if v == t: break
2 2
3 2 1 3 2 1
1 1 2 3 2 1 1 2 3 2
0 3 1 2 4 5 0 3 1 2 4 5
0 0 0+1
(a) (b)
2 2
3 2 1 3 2 1
1 1 2 3 2 1 1 2 3 2
0 3 1 2 4 5 0 3 1 2 4 5
0 1 2 0 1 2 4
(c) (d)
2 2
3 2 1 3 2 1
1 1 2 3 2 1 1 2 3 2
0 3 1 2 4 5 0 3 1 2 4 5
0 1 2 4 4 0 1 2 4 4 4
(e) (f)
2
3 2 1
1 1 2 3 2
0 3 1 2 4 5
0 1 2 4 4 4
(g)
Figura 9.21: Traza del cálculo del camino más corto en el grafo acı́clico de la figura 9.19.
En la zona inferior del grafo se muestran los punteros hacia atrás (backp) a partir de
los cuales se recupera el camino de distancia mı́nima.
39
40 path = [t]
41 while backp[path[-1]] != None:
42 path.append ( backp[path[-1]] )
43 path.reverse()
44
El principio de optimalidad
Hemos dicho que si D(s, v), el coste del camino más corto de s a v, puede expresarse
como D(s, u) + d(u, v) es porque se forma concatenando el camino más corto de s a u
con el arco (u, v). Podemos decir, pues, que el camino más corto entre s y v se forma
añadiendo un arco al camino más corto entre s y otro vértice u.
Esta es una propiedad interesante y se la conoce como ((Principio de optimali-
Principio de optimalidad: dad)). Podrı́amos reformularla ası́: ((los prefijos de una solución óptima son, a su vez,
Optimality principle.
óptimos)). Haremos uso frecuente del principio de optimalidad cuando estudiemos Pro-
gramación Dinámica.
donde P (s, t, k) es el conjunto de todos los caminos que unen s y t con k. Podemos
definir este conjunto recursivamente:
P (s, t, k) = {(v1 , v2 , . . . , vk+1 ) | v1 = s; vk+1 = t; (v1 , v2 , . . . , vk ) ∈ P (s, u, k−1); (u, t) ∈ E}.
El caso base es P (s, s, 0) = {s} y P (s, v, 0) = ∅ si v 6= s.
Sustituimos ahora P (s, t, k) en la definición de D(s, t, k) por su expresión recursiva:
X
D(s, t, k) = min min d(vi , vi+1 ) + d(u, t) .
(u,t)∈E (v1 ,v2 ,...,vk )∈P (s,u,k−1)
1≤i<k−1
D(s, v, 0) = +∞.
D̂ = D(s, t, k).
V 0 = {(v, i) | v ∈ V, 0 ≤ i ≤ k},
E 0 = {((u, i − 1), (v, i)) | (u, v) ∈ E, 0 < i ≤ k};
Nótese que todo camino (v1 , v2 , . . . , vk+1 ) en G con k aristas tiene un camino equivalente
(con el mismo peso) en G0 : ((v1 , 0), (v2 , 1), . . . , (vk+1 , k)). Y viceversa: todo camino en
G0 tiene un camino equivalente en G. Ası́ pues, resulta evidente que calcular la distancia
del camino más corto en G0 entre (s, 0) y (t, k) es un problema equivalente a calcular
la distancia del camino entre s y t de k aristas más corto en G.
2 2 2
21 21 21
2 2 2
3,0 3,1 3,2 3,3
2 −2 −2 −2
3
1 1 1
2 2,0 2,1 2,2 2,3
0 1
1 2 2 2 2
1 1 1
2 2 2
1 3 1,0 1,1 1,2 1,3
1 −2 1
3 3 3
1 1 1
2 2 0,0 0,1 0,2 0,3
3 4 5
(a) (b)
Figura 9.22: (a) Grafo ponderado con ciclos. En trazo grueso se muestran las aristas
del camino (0, 3, 1, 5). (b) Grafo multietapa asociado al anterior sobre el que se efectúa
la búsqueda del camino más corto entre 0 y 5 con 3 aristas. En trazo grueso se muestra
el camino equivalente en este grafo al que se destacó en el grafo de la izquierda.
13 return D[s, t, k]
10 def d (u,v):
11 return dist[u,v]
12
13 for i in range(6):
14 print ’Distancia de 0 a 5 con %d aristas:’ %i, shortest_distance_k (G, d, 0, 5, i)
13 return D[t, k]
La distancia del camino más corto entre dos vértices (con cualquier número
de aristas)
El camino más corto entre s y t no puede tener más de |V | − 1 aristas. Si tuviera más,
contendrı́a un ciclo. Como no admitimos ciclos negativos, siempre serı́a posible mejorar
el peso del camino eliminado ese hipotético ciclo. Ası́ pues,
Es decir, el peso camino más corto entre s y t es el menor de entre los pesos de los
camino más cortos de s a t con k aristas, para k inferior a |V |.
No es necesario efectuar |V | llamada a la función shortest_distance_k . Podemos
calcular el camino más corto con |V | − 1 aristas y obtendremos, como subproducto
el valor de D(s, t, k) para todo k < |V | − 1. En la figura 9.23 se muestra el grafo
que hubiésemos construido implı́citamente al calcular el camino más corto entre 0 y
5 con 5 aristas en el grafo de la figura 9.22 (a). En ese grafo, hemos considerado que
todos los vértices de la forma (t, i), para 0 ≤ i < 5. La distancia mı́nima de entre
las calculadas para cada uno de esos vértices es D(0, 6). Se trata de un grafo acı́clico,
ası́ que el algoritmo desarrollado en el apartado anterior encuentra aplicación en este
nuevo problema.
2 2 2 2 2
21 21 21 21 21
2 2 2 2 2
3,0 3,1 3,2 3,3 3,4 3,5
−2 −2 −2 −2 −2
1 1 1 1 1
2,0 2,1 2,2 2,3 2,4 2,5
2 2 2 2 2
1 1 1 1 1
2 2 2 2 2
1,0 1,1 1,2 1,3 1,4 1,5
3 3 3 3 3
1 1 1 1 1
0,0 0,1 0,2 0,3 0,4 0,5
Figura 9.23: Grafo multietapa asociado al grafo de la figura 9.22 (a) sobre el que se
efectúa implı́citamente la búsqueda del camino más corto entre 0 y 5 con cualquier
número de aristas.
10 def d (u,v):
11 return dist[u,v]
12
Distancia de 0 a 5: 2
33 for v in G.V :
34 D[v, 0], backp[s, 0] = infinity, None
35 D[s, 0], backp[s, 0] = 0, None
36
10 def d (u,v):
11 return dist[u,v]
12
Distancia de 0 a 5: 2
Camino de distancia mı́nima de 0 a 5: [0, 3, 1, 4, 5]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 183 Aplica el algoritmo de Bellman-Ford al mapa de la penı́nsula ibérica diseñando
una función que devuelva una lista con las ciudades que deben visitarse al ir por el
camino más corto de una ciudad a otra.
......................................................................................
Complejidad computacional
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 184 Aplica el algoritmo de Bellman-Ford al mapa de la penı́nsula ibérica diseñando
una función que devuelva la distancia más camino más corta entre dos ciudades (por
carretera, se entiende).
......................................................................................
Si los grafos presentan ciclos pero todas sus aristas tienen pesos positivos, es posible re-
solver el problema de calcular el camino más corto entre s y t con una menor complejidad
computacional. El algoritmo de Dijkstra resuelve el problema en tiempo O(|E| lg |V |). Edsger W. Dijkstra (1930–2002)
es uno de los pioneros de la
Empezaremos, no obstante, por presentar un versión ineficiente (cuadrática con |V |) informática. El conocido como
cuya idea fundamental se ilustra en el siguiente ejemplo. ((algoritmo de Dijkstra)) se
publicó en el artı́culo ((A note
on two problems in connection
Apuntes de Algorı́tmica 239 with graphs)), Numerical
Mathematics, núm. 1, pp.
269–271, 1959.
9.6 El problema del camino más corto 2003/12/09-12:57
Un ejemplo
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞
S
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞ ∞
S “
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ 14 ∞ ∞ ∞ ∞ ∞ ∞ 30 ∞ ∞ 56
S “
No estamos seguros de saber cuál es el camino más corto desde Andratx hasta cada
una de esas ciudades, ası́ que no la añadimos a S.
Consideramos ahora la ciudad con menor valor de D tal que no pertenece a S:
Calvià. Según la tabla D, su distancia a Andratx es de 14 kilómetros. Ahora estamos
completamente seguros de que esa es la menor distancia con la que podemos ir de una
Andratx a Calvià. Cualquier otra forma de llegar a Calvià se formará sumando una
cantidad positiva a una distancia igual o mayor, pues todos los valores almacenados en
D para vértices de V − S son mayores. Éste es el estado actual:
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ 14 ∞ ∞ ∞ ∞ ∞ ∞ 30 ∞ ∞ 56
S “ “
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ 14 ∞ ∞ ∞ ∞ ∞ ∞ 28 ∞ ∞ 56
S “ “
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ 14 ∞ ∞ ∞ 48 ∞ 42 28 ∞ ∞ 56
S “ “ “
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ 14 ∞ ∞ 54 48 ∞ 42 28 ∞ ∞ 56
S “ “ “ “
Y ahora a Llucmajor, que permite alcanza Campos del Port a través de un camino
de 62 kilómetros:
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D ∞ 0 ∞ 14 62 ∞ 54 48 ∞ 42 28 ∞ ∞ 56
S “ “ “ “ “
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 ∞ 14 62 ∞ 54 48 79 42 28 ∞ ∞ 56
S “ “ “ “ “ “
Ya sabemos llegar de algún modo a Manacor: a través de Inca; pero aún no estamos
seguros de que éste sea el camino más corto. Seleccionamos ahora a la ciudad de Sóller,
que nos da acecso a Pollença:
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 ∞ 14 62 ∞ 54 48 79 42 28 110 ∞ 56
S “ “ “ “ “ “ “
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 ∞ 14 62 ∞ 54 48 79 42 28 110 75 56
S “ “ “ “ “ “ “ “
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 ∞ 14 62 ∞ 54 48 79 42 28 110 75 56
S “ “ “ “ “ “ “ “ “
Ahora consideramos Alcúdia, que da acceso por ve primera a Artà, pero que también
permite mejorar nuestra estimación del camino más corto entre Andratx y Pollença:
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 115 14 62 ∞ 54 48 79 42 28 89 75 56
S “ “ “ “ “ “ “ “ “ “
Ahora extraemos Manacor y, por fin, estamos seguros de que no hay camino de
Andratx a ella con menos de 79 kilómetros: cualquier otra forma de llegar a Manacor
obligará a sumar una cantidad positiva (nula, en el mejor de los casos) al este valor.
Saber que Manacor edstá accesible desde Andratx con un camino de 79 kilómetros nos
permite actualizar nuestra estimación de la distancia del camino más corto de Andratx
a Artà:
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 96 14 62 ∞ 54 48 79 42 28 89 75 56
S “ “ “ “ “ “ “ “ “ “ “
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 96 14 62 ∞ 54 48 79 42 28 89 75 56
S “ “ “ “ “ “ “ “ “ “ “ “
Capdepera
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 96 14 62 104 54 48 79 42 28 89 75 56
S “ “ “ “ “ “ “ “ “ “ “ “ “
Llucmajor
Palma de
Marratxı́
Manacor
Mallorca
Santanyı́
Andratx
Pollença
del Port
Campos
Alcúdia
Calvià
Sóller
Artà
Inca
D 79 0 96 14 62 104 54 48 79 42 28 89 75 56
S “ “ “ “ “ “ “ “ “ “ “ “ “ “
17 for v in G.succs(u):
18 if v not in S:
19 D[v] = min(D[v], D[u] + d(u,v))
20
21 return D[t]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 185 Haz una traza del algoritmo de Dijkstra para los siguientes grafos, tomando
como vértice origen el 1.
50 10
1 2 3 2 1
10
30 20 12 10
5 100 5 8 7 15
10
50 9
4 3 4 5 6
(a) (b)
3 3
1 2 2 3
10 4 10
2 6 9
6 1 3 1 3 2 6 4
1 2 5
2 2
5 4 5 4
(c) (d)
......................................................................................
Paso de inducción. Supongamos que D[v] es la distancia del camino más corto de
s a v para todo elemento de S en un instante dado, es decir, para una talla
determinada de S. Sea u el vértice de V − S seleccionado en las lı́nea 10–14 del
algoritmo. Nuestro objetivo es demostrar que D[u] es la distancia del camino
más corto de s a u. Supongamos que no lo es, es decir, que existe un camino p
más corto. Dicho camino debe tener al menos un vértice diferente de u que no
esté en S. Sea w el primer vértice de p que no está en S.
w u
p S
11
21 return D[t]
Análisis de complejidad
El coste espacial es idéntico al de la versión anterior, esto es, Θ(|V |). El coste temporal
es ahora O(|E| lg |V |): extraemos |V | vértices de Q y cada extracción necesita ejecutar
O(lg |V |) pasos; por otra parte, cada arista del grafo puede generar una llamada a
Q.decrease, operación que también tiene un coste O(lg |V |).
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 189 Supóngase que se modifica la condición del bucle ((while)) del algoritmo de
Dijkstra: while not Q.is_empty() por while len(Q) > 1. Esta modificación provocarı́a
que el bucle se repitiera |V | − 1 veces en lugar de |V | veces. ¿Puede afirmarse que el
algoritmo modificado resolverı́a igualmente el problema? ¿Por qué?
· 190 ¿Cómo podrı́a comprobarse si un grafo dirigido es fuertemente conexo utili-
zando el algoritmo de Dijkstra? Comenta qué modificaciones serı́an necesarias realizar
sobre el algoritmo.
· 191 ¿Qué coste temporal tendrı́a el algoritmo de Dijkstra si el grafo estuviera
representado mediante una matriz de adyacencia? Razona la respuesta adecuadamente.
· 192 Es posible diseñar una versión más eficiente del algoritmo de Dijkstra, capaz
de resolver el problema del camino más corto en O(|V | lg |V |) pasos. Averigua cómo.
......................................................................................
Un refinamiento
El algoritmo de Dijkstra, tal cual lo hemos presentado, espera a alcanzar todos los
vértices para finalizar. Es posible, no obstante, finalizar antes: tan pronto ingresa t en
el conjunto S:
dijkstra.py dijkstra.py
1 from heap import IndexedMinHeap
2 from sets import Set
3
22 return D[t]
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 193 Analiza la complejidad computacional de la última versión del algoritmo.
......................................................................................
dijkstra.py dijkstra.py
24 def dijkstra_shortest_path(G, d, s, t, infinity=3.4e+38):
25 S = Set()
26 D = {}
27 backp = {}
28 for v in G.V : D[v], backp[v] = infinity, None
29 D[s] = 0
30 backp[s] = None
31
45 v=t
46 path = [v]
47 while backp[v] != None:
48 v = backp[v]
49 path.append (v)
50 path.reverse()
51 return D[t], path
Algunas observaciones
Hemos dicho que el algoritmo de Dijkstra guarda relación con la exploración por primero
en anchura de un grafo. Si la función de ponderación asignara peso 1 a todas las aristas
y la cola de prioridad respetara el orden de ingreso para las extracciones (primero en
entrar, primero en salir), tendrı́amos un algoritmo equivalente (aunque más costoso) a
la exploración por primero en anchura.
El algoritmo de Dijkstra también guarda relación con otro que ya hemos estudiado
detenidamente: el algoritmo de Kruskal. Ambos siguen una estrategia de exploración
por ((primero según prioridad)). En el algoritmo de Kruskal se seleccionan aristas con
un criterio de prioridad que podemos calificar de estático: su peso. En el algoritmo de
Dijkstra se seleccionan vértices con una priorización dinámica, que se va actualizando
conforme la ejecución del algoritmo evoluciona.
El algoritmo de Dijkstra puede utilizarse como base para la resolución de otros
problemas, como proponen los siguientes ejercicios.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 194 Dado un grafo dirigido G = (V, E) ponderado con pesos no negativos en las
aristas, modifica el algoritmo de Dijkstra para que devuelva como resultado los costes
de los caminos mı́nimos entre cualquier par de vértices del grafo. ¿Cuál serı́a el coste
temporal del algoritmo?
· 195 Es posible que haya dos o más caminos entre dos vértices con el mismo peso.
Explica cómo modificar el algoritmo de Dijkstra para que si hay más de un camino de
coste mı́nimo entre el origen s y t, se escoja el camino con el menor número de aristas.
· 196 Modifica el algoritmo de Dijkstra para que contabilice, para cada vértice v ∈ V ,
el número de caminos de distancia mı́nima que existen desde el origen a v.
......................................................................................
3 2
0 1 2 0 1 2
1 2 3
1 2 1
2
3 4 5 3 4 5
(a) (b)
Figura 9.24: (a) Grafo acı́clico y ponderado. (b) Estructura de punteros hacia atrás.
2 4 5
Este tipo de árbol recibe el nombre de árbol de caminos más cortos, pues con Árbol de caminos más
cortos: Shortest paths tree.
él resulta sencillo calcular el camino más corto de s a cualquier vértice v de V : basta
con recorrer los punteros hacia atrás desde v hasta s e invertir la secuencia de vértices
visitados.
Nótese que los árboles de caminos más cortos son árboles de recubrimiento del grafo.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 197 El MST es el árbol de recubrimiento cuya suma de pesos de aristas es mı́nima.
¿Es, además, el árbol de caminos más cortos para algún vértice?
......................................................................................
Presentamos ahora versiones de los tres algoritmos de cálculo del camino más cor-
to modificados para devolver el árbol de caminos más cortos con origen en un vértice
cualquiera s. La complejidad computacional de cada uno de ellos es la propia del algo-
ritmo en el que se basa: O(|E|) para grafos acı́clicos, O(|E||V |) para grafos con ciclos
no negativos y O(|E| lg |V |) para el algoritmo de Dijkstra.
58 return backp
10 def d (u,v):
11 return dist[u,v]
12
13 tree = dag_shortest_paths_tree(G, d, 0)
14 for v in range(6):
15 print ’Camino más corto de 0 a %d:’ % v, path_from_paths_tree(tree, v)
58 for v in G.V :
59 D[v, 0], backp[s, 0] = infinity, None
60 D[s, 0], backp[s, 0] = 0, None
61
69 stage = {}
70 for v in G.V :
71 stage[v] = 0
72 mindist = D[v,0]
73 for i in range(len(G.V )):
74 if [D[v, i] <mindist:
75 stage[v] = i
76 mindist = D[v,i]
77
88 return path
dijkstra.py dijkstra.py
53 def dijkstra_shortest_paths_tree(G, d, s, infinity=3.4e+38):
54 S = Set()
55 D = {}
56 backp = {}
57 for v in G.V : D[v], backp[v] = infinity, None
58 D[s] = 0
59 backp[s] = None
60
72 return backp
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .problemas. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
· 198 Calcula el árbol de caminos más cortos que parten de Castellón de la Plana
y llegan a cualquier de las ciudades de la penı́nsula ibérica (ver ejercicio 178) si sólo
permitimos desplazamientos hacia la izquierda. Representa gráficamente el árbol de
caminos más cortos sobre el propio mapa, sin modificar la ubicación geográfica de las
ciudades.
......................................................................................
floyd.py floyd.py
1 def floyd (G, d, infinity=3.4e+38):
2 D = {}
3 for u in G.V :
4 for v in G.V :
5 if u == v:
6 D[u,v] = 0
7 elif v in G.succs(u):
8 D[u,v] = d(u,v)
9 else:
10 D[u,v] = infinity
11
12 for v in G.V :
13 for u in G.V :
14 for w in G.V :
15 D[u,w] = min(D[u,w], D[u,v] + D[v,w])
16
17 return D
7 print ’ ’,
8 for u in Mallorca.V :
9 print ’ %4s’ % u[:4],
10 print
11
12 for u in Mallorca.V :
13 print ’ %4s’ % u[:4],
14 for v in Mallorca.V :
15 print ’ %4d’ % mindist[u,v],
16 print
Alcú Andr Artà Calv Camp Capd Inca Lluc Mana Marr Palm Poll Sant Sóll
Alcú 0 79 36 65 85 44 25 71 50 37 51 10 77 64
Andr 79 0 96 14 62 104 54 48 79 42 28 89 75 56
Artà 36 96 0 82 57 8 42 71 17 54 68 46 44 100
Calv 65 14 82 0 48 90 40 34 65 28 14 75 61 70
Camp 85 62 57 48 0 65 60 14 40 48 34 95 13 118
Capd 44 104 8 90 65 0 50 79 25 62 76 54 52 108
Inca 25 54 42 40 60 50 0 46 25 12 26 35 52 89
Lluc 71 48 71 34 14 79 46 0 54 34 20 81 27 104
Mana 50 79 17 65 40 25 25 54 0 37 51 60 27 114
Marr 37 42 54 28 48 62 12 34 37 0 14 47 61 98
Palm 51 28 68 14 34 76 26 20 51 14 0 61 47 84
Poll 10 89 46 75 95 54 35 81 60 47 61 0 87 54
Sant 77 75 44 61 13 52 52 27 27 61 47 87 0 131
Sóll 64 56 100 70 118 108 89 104 114 98 84 54 131 0
4 2
1 3 3
0 2 0 1
(a) (b)
Figura 9.26: (a) Grafo planar. (b) Disposición de sus vértices que hace patente que es
planar.
(a) (b)
se conoce en la literatura como problema del viajante de comercio. Problema del viajante de
comercio: Traveling salesman
Aparte de su evidente interés en la organización de rutas de reparto o comercio, este problem.
problema surge en la organización de rutas de montaje en sistemas de fabricación con
robots u operarios que trabajan en serie o en la determinación de los movimientos del
cabezal de un plotter que permite realizar un dibujo en el menor tiempo posible.
Es un problema para el que no se conoce algoritmo capaz de resolverlo en tiempo
polinómico. Se recurre muchas veces a aproximaciones que producen caminos hamilto-
nianos de coste próximo al de menor coste.
Un problema relacionado es el que demanda el cálculo de un ciclo hamiltoniano.
Un problema relacionado demanda el cálculo de un ciclo o camino que pase por cada
arista al menos una vez. El problema surge, por ejemplo, al asignar rutas a agentes
(personas, camiones, etc.) de reparto de paqueterı́a y correo o al determinar la ruta de
los camiones de basura
El problema conocido como problema del cartero chino propone el cálculo del Problema del cartero chino:
Chinese postman problem.
ciclo con menor número de aristas que visita todas y cada una de las aristas al menos
una vez.
Es el problema del cartero chino
porque fue planteado por un
autor chino: M. Kwan.
9.9.6. Problemas de conectividad
Se desea conocer el menor número de aristas que, eliminadas, convierten un grafo conexo
en dos componentes conexas disjuntas. Existe una formulación alternativa referida al
número de vértices.
Este tipo de problemas tienen por objeto determinar la fiabilidad de una red de
comunicaciones: ¿cuántos nodos o enlaces de la red han de caer para que sea imposible
comunicarse entre dos puntos de la misma?