Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Ingeniería
Tema 5 Informática
Grafos Estructuras
de Datos II
2018/19
Grafos
Objetivos
Estudiar la especificación del TAD Grafo
Presentar diferentes alternativas de implementación
Conocer los algoritmos más importantes de manipulación de grafos
Contenidos
6.1 Conceptos
6.2 Especificación algebraica
6.3 Implementación
6.4 Recorridos sobre grafos
6.5 Ordenación topológica
6.6 Caminos mínimos sobre grafos: Algoritmos de Dijkstra y Floyd
6.7 Árbol de expansión de coste mínimo: Algoritmos de Prim y Kruskal
Estructuras de Datos II Tema 5 Grado en Ingeniería Informática
+ 3
Grafos
Duración
6 clases
Bibliografía
Data Structures and Algorithms in C++
Autores: Michael T. Goodrich, Roberto Tamassia, David M. Mount
Editorial: John Wiley & Sons, Inc.
Año: 2004
Págs. 575 – 656
Estructuras de datos y métodos algorítmicos
Autores: N. Martí, Y. Ortega, J.A. Verdejo
Editorial: Prentice-Hall, 2004
Págs. 231 – 276, 381 – 395
Fundamentos de Estructuras de Datos. Soluciones en Ada, Java y C++
Autores: Zenón J. Hernández Figueroa y otros
Editorial : Thomson, 2005
Págs. 415 – 455
4
+ 5
Conceptos
Conceptos
Un ejemplo de grafo podría ser la red de metro de Madrid, donde los vértices
representan las estaciones y las aristas la línea que une dos estaciones:
Conceptos
Otro ejemplo de aplicación de los grafos es la construcción de modelos donde se
estudian las relaciones de precedencia que existen entre las tareas que se
necesitan para completar un trabajo (grafos PERT)
Conceptos
Conceptos
Algunas definiciones sobre grafos:
Un ciclo es simple si son distintos cada vértice y cada arista del ciclo, excepto
el primer y último vértices
Se dice que el vértice x es antecesor del vértice y si existe una arista que tenga
por origen a x y por destino a y, es decir, si la arista (x, y) ∈ A
Conceptos
Número de nodos:
v2 v3
Número de aristas:
V=
A=
Camino =
Ciclo =
Grado (v3) =
v1 v4 Adyacentes (v3) =
Conceptos
Número de nodos:
Número de aristas:
v2 V=
A=
Camino =
Ciclo =
GradoEnt (v2) =
GradoSal(v2)=
v1 v3 Adyacentes (v3) =
Antecesores (v3) =
Antecesores (v2) =
Conceptos
Un grafo no dirigido G es conexo si existe un camino entre cualquier par de
nodos que forman el grafo
v2 v4
v2 v3 v4
v3
v5
v5
v1
v1
v2 v3
v5
13
+ 14
Especificación algebraica
Puesto que el grafo es un conjunto de vértices y aristas, la signatura del TAD grafo deberá
contener, al menos, las operaciones de añadir vértices y aristas a un grafo
Definiremos un TAD para los grafos dirigidos, con las operaciones básicas para su
construcción, así como las operaciones de cálculo de vértices adyacentes a uno dado y
las de pertenencia de arista y de vértice
espec grafosDirigidos
usa booleanos, conjunto
parámetro formal
género vértice
operaciones
_ == _: vértice vértice booleano
_ ≠ _: vértice vértice booleano Gen (grafo) =
fpf
renombrar conjunto<vértice> por conjVértices
conjunto<(vértice, vértice)> por conjAristas Mod (grafo) =
género grafo
operaciones
gVacío: grafo Obs (grafo) =
+vértice: grafo vértice grafo
parcial +arista: grafo vértice vértice grafo
_ ∈ _ : vértice grafo booleano
( _, _ ) ∈ _ : vértice vértice grafo booleano
esVacío: grafo booleano
adyacentes: grafo vértice conjVértices
-vértice: grafo vértice grafo
-arista: grafo vértice vértice grafo
vértices: grafo conjVértice
aristas: grafo conjAristas
Especificación algebraica
dominios de definición g: grafo; v1,v2: vértice
+arista (g,v1,v2) está definido sólo si v1∈g ∧ v2∈g
ecuaciones g: grafo; v,v1,v2,v3,v4: vértice
+vértice (+vértice (g, v1), v2) =
v ∈ gVacío =
v1 ∈ +vértice (g, v2) =
v1 ∈ +arista (g, v2, v3) =
Especificación algebraica
(v1, v2) ∈ gVacío =
(v1, v2) ∈ +vértice (g, v3) =
(v1, v2) ∈ +arista (g, v3, v4) =
esVacío (gVacío) =
esVacío (+vértice (g, v)) =
esVacío (+arista (g, v1, v2)) =
adyacentes (gVacío, v) =
adyacentes (+vértice (g, v1), v2) =
adyacentes (+arista (g, v1, v2), v3) =
Especificación algebraica
-vértice (gVacío, v) =
-vértice (+vértice (g, v1), v2) =
Especificación algebraica
vértices (gVacío) =
vértices (+vértice (g, v)) =
vértices (+arista (g, v1, v2)) =
aristas (gVacío) =
aristas (+vértice (g, v)) =
aristas (+arista (g, v1, v2)) =
20
+ 21
Implementación
private:
T obj;
};
Implementación
Una Arista es un objeto con tres atributos: dos vértices y opcionalmente una etiqueta
Implementación
Implementación
Existen básicamente 3 formas de implementar los grafos: una estática y dos dinámicas. La
elección de cada una de ellas dependerá de las operaciones que se vayan a aplicar sobre
los vértices y las aristas
Matriz de Adyacencia
Sea G = (V, A) un grafo de n nodos, donde suponemos que los nodos V = {v1, v2,... , vn}
están ordenados y podemos representarlos por sus ordinales {1, 2,... , n}
Implementación
vértices de tipo entero
Esta implementación sólo se puede usar si los vértices son de tipo entero
Esta representación es útil para aquellos problemas donde se necesite saber si existe una
arista entre dos vértices dados, ya que es de orden constante O(1)
El principal inconveniente es que se necesita un espacio de O(n2) aunque el grafo tenga muy
pocas aristas
Implementación
vértices de tipo T
Si los vértices no fueran valores enteros podría utilizarse un Diccionario para almacenar los
pares <vértice, índice del vértice>
Tipo T Entero
Los Diccionarios son tipos asociativos que permiten almacenar pares (clave, valor). En el
caso de los grafos, la clave sería el vértice y el valor su índice
Existen implementaciones de los tipos diccionarios que permiten obtener el valor asociado a
una clave en un tiempo O(1)
Implementación
vértices de tipo T
private:
enum { N = …};
Arista<T>* m[N][N];
int nv;
Diccionario<T, int> dVertices;
}
Implementación
Ejemplo de Grafo Dirigido y no Etiquetado
<CA, HU>
CA CO HU SE
<HU, CA>
Córdoba <SE, HU>
Cádiz
<SE, CA> <SE, CO>
- Para un grafo con aristas etiquetadas, la matriz se declararía de la forma: Arista<T, U>* m[N][N];
- Donde U es el parámetro de tipo para la etiqueta de las aristas
2
<CA, HU, 4>
CA CO HU SE
Sevilla CA • • <CA, SE, 2>
Huelva
CO • • • •
3 m=
5 HU • • •
<HU, SE, 2>
4 2 SE •
Implementación
Implementación
Listas de Adyacencia
Esta representación consiste en n listas, de forma que la lista i-ésima contiene los
vértices adyacentes al vértice i
G
A C
1 B
2 A C D
B
3
4
D E 5 B C
G
A D
1 B D
2 A E
E
3 E
A E
4
B C B C D
5
Implementación
Implementación
Por tanto, para un caso más general, podemos utilizar, en lugar de una tabla, una
lista enlazada para almacenar la información de los vértices
Implementación
Sevilla
Huelva
Objeto de tipo Adyacencia,
formado por un vértice y una
lista de vértices
Córdoba
Cádiz
Huelva Sevilla
Cádiz Huelva
Córdoba
Implementación
Implementación
Grafo Etiquetado con Pesos
60
Huelva Sevilla 60
Huelva
Cádiz
150
Representación mediante
Listas de Adyacencia Córdoba
Implementación
Se utilizan para grafos dirigidos, si lo que queremos saber es, de forma eficiente, los
antecesores de un determinado nodo
Se utiliza otra tabla de listas cuya lista i-ésima contiene los vértices antecesores al vértice i.
Dicha lista se conoce con el nombre de lista de antecesores
Una lista múltiple de adyacencia es una estructura de datos que une ambas tablas en una
única estructura
Hay un nodo por cada arista del grafo
Cada nodo guarda la información de dos vértices (origen y destino de la arista) y dos
punteros
el primero apunta al nodo que almacena la siguiente arista que tiene el mismo vértice destino
el segundo apunta al nodo que almacena la siguiente arista que tiene el mismo vértice origen
Implementación
Implementación
A C origen destino
mismo
origen
B (sigAdy)
mismo
destino
D E
(sigAnt)
1 2 3 4 5
DESTINO listas de
antecesores
ORIGEN A B
1
2 B A B C B D
3
4
5 E B E C Las posiciones de ORIGEN y DESTINO son
los valores asociados por el diccionario a
listas de
adyacencia cada vertice (i.e.: <A, 1>, <B, 2>, etc.)
Implementación
Implementación
Eficiencia de las operaciones
Operación Matriz Lista Simple(1) Lista Múltiple
n: número de vértices Grafo
m: número de aristas
esVacio
gE: grado máximo de entrada
gS: grado máximo de salida estaVertice
estaArista
vertices
aristas
adyacentes
antecesores
insertarVertice
insertarArista
eliminarVertice
(1) Versión en la que se usa una tabla para
almacenar la información de los vértices
eliminarArista
Estructuras de Datos II Tema 5 Grado en Ingeniería Informática
+
6.4 Recorridos sobre Grafos
43
+ 44
en anchura
en profundidad
ambos necesitan una estructura auxiliar para
almacenar los vértices visitados ya que deben
evitarse las visitas reiteradas
Este método utiliza una cola como estructura auxiliar para mantener los vértices que se
vayan a procesar posteriormente
A
D
H
T C
R
Este método utiliza una pila como estructura auxiliar para mantener los vértices que se
vayan a procesar posteriormente
Fondo D Cima
B
A
D
H
T C
R
51
+ 52
Ordenación topológica
¿Qué es?: Es la ordenación lineal de todos los nodos de un Grafo según la distribución
y sentido de sus aristas
Aplicación:
Representación de las fases de un proyecto mediante un grafo dirigido acíclico:
Los vértices representan tareas y las aristas relaciones temporales entre ellas
Ordenación topológica: Lista de tareas en un orden lógico que puedan realizarse
Ordenación topológica
Ordenación topológica
Lista<Vertice<T> > ordTopologica (const Grafo& G)
var Lista<Vertice<T> > L; T v, w;
Conjunto<T> ceroPred
Conjunto<Vertice<T> > vert, suc
Diccionario <T, int> numPred
int np;
fvar
inicio
vert = G.vertices()
para todo v en vert hacer
numPred.asignar(v.getObj(), 0); ceroPred.poner(v)
fpara
para todo v en vert hacer
suc = G.adyacentes(v)
para todo w en suc hacer
numPred.observar(w, np)
numPred.asignar(w, np + 1)
ceroPred.eliminar(w)
fpara
fpara
…
Ordenación topológica
Ordenación topológica
Ejemplo: ordTopologica(G)
A
D
H L
T C
R
A B C D H R T
numPred
57
+ 58
Uno de los problemas que se plantea con frecuencia en los grafos es determinar el
camino de menor coste entre un par de vértices. Este problema se plantea,
generalmente, para grafos dirigidos y valorados (con factor de peso en las aristas)
Suponemos que cada arista tiene asociada un coste cij no negativo, es decir,
El coste de un camino C = v1, v2, ..., vk es la suma de los costes de las aristas de C
k −1
coste(C ) = ∑ c i , i +1
i =1
El algoritmo tiene como precondición que el grafo G=(V, A) tiene que ser dirigido,
valorado y con factores de peso positivos
El algoritmo comienza inicializando el conjunto de vértices por visitar con todos los
vértices del grafo y termina cuando no quedan vértices por visitar. En cada
iteración, escoge el vértice por visitar con menor coste desde v usando un camino
que pase sólo por vértices ya incluidos en el conjunto de vértices tratados
Para mantener, en cada paso, la distancia mínima desde el origen podemos usar
una tabla llamada D (distancia), que mantiene la distancia desde el origen a cada
vértice del grafo
En cada iteración, una vez seleccionado de entre los vértices por visitar el vértice w
más próximo a v, se actualiza la tabla D para aquellos vértices u (no visitados)
adyacentes de w y que cumplen que D[w] + coste(G, w, u) < D [u]
Para indicar que no existe camino desde el vértice v1 hasta el vértice v2 usaremos
la indeterminación ∞
3 ∞ 1 2
3 2 1 2
3 2 1 2
2 5 2 5 2 5
∞ ∞ 3 ∞ 3 7
4 v4 4 v4 4 v4
v5 v5 v5
v1 v2 v1 5 v2 v1 v2
5 5
0 4 0 4 0 4
2 2 2 2 2 2
v3 v3 v3
2 1 2 2 1 2 2 1 2
3 3 3
2 5 2 5 2 5
3 7 3 5 3 5
v5 4 v4 4 v4 v5 4 v4
v5
1
2
3
4
Sea G un grafo dirigido y valorado. Suponemos que los vértices están numerados de 1 a n.
En este caso, utilizamos una matriz con los pesos de las aristas, de tal forma que cada
elemento de la matriz representa el peso cij asociado a la arista (vi , vj). Utilizaremos cij = ∞
para representar que no existe arista entre los vértices vi y vj
La idea principal consiste en encontrar una matriz D de n x n elementos, de tal forma que
cada elemento Dij sea el coste mínimo de los caminos entre vi y vj
Para calcular el camino de coste mínimo entre los vértices i y j podemos considerar dos
posibilidades: no pasar por el vértice k, en cuyo caso tendremos que calcular el mejor
camino con el resto de los vértices (Dk-1[i, j]), o bien pasar por el vértice k, en cuyo caso
tendremos que obtener caminos que vayan de i a k y de k a j
D1[i, j] = min (D0[i, j], D0[i, 1] + D0[1, j]). Menor de los costes entre el anterior camino desde i hasta j y
la suma de los costes de caminos desde i hasta 1 y 1 hasta j
D2[i, j] = min (D1[i, j], D1[i, 2] + D1[2, j]). Menor de los costes entre el anterior camino desde i hasta j y
la suma de los costes de caminos desde i hasta 2 y 2 hasta j
Dn[i, j] = min (Dn-1[i, j], Dn-1[i, n] + Dn-1[n, j]). Menor de los costes entre el anterior camino desde i
hasta j y la suma de los costes de caminos desde i hasta n y n hasta j
Como en el algoritmo de Dijkstra, para cada vértice sería conveniente almacenar el índice
del último vértice que ha conseguido que el camino sea mínimo desde vi hasta vj. Para ello,
se utiliza una matriz de vértices según el siguiente criterio:
• A(vi, vj) = 0, si no hay camino de vi a vj ,o hay una arista entre los vértices
• A(vi, vj) = vk, si vj es accesible desde vi a través de vk en el camino mínimo entre ambos vértices
Estructuras de Datos II Tema 5 Grado en Ingeniería Informática
+ 66
68
+ 69
Un problema típico relacionado con los grafos consiste en diseñar una red de
transporte (carreteras, ferrocarril, ...) que conecte una serie de poblaciones
construyendo el menor número posible de kilómetros de vía
Se trata de encontrar un subgrafo que contenga todos los vértices, que siga siendo
conexo y que el coste total de sus aristas sea mínimo
v1 v1
6 5
1 1
v2 5 5 v4 v2 5 v4
v3 v3
3 6 4 3 4
2 2
6
v5 v6 v5 v6
2 v2 2 2 v2 2
3 v4 v4
v1 1 v1 1
4
v3 v3
5
3 6 3
4 4
v5 v6 v5 v6
10 14
v1 v4 v6
9
3 7
7 v7 v8
v3
5 4 2
6
v2 v5
5
Para añadir las aristas examinamos el conjunto A en orden creciente según su coste:
Si la arista conecta dos vértices que pertenecen al mismo árbol, no se añade para evitar
la aparición de ciclos en el árbol de expansión
Cuando todos los vértices de T estén en una única componente conexa, habremos encontrado
el árbol de expansión buscado
Algoritmo de Kruskal
Conjunto<Arista> Kruskal (const Grafo& G)
var
Lista <Arista> A; Conjunto <Arista> T; Arista ai; V w;
Diccionario<V, int> B; int i, n; int arbol1, arbol2;
fvar
inicio
n = G.vertices().cardinalidad()
A = secuencia de las aristas de G ordenada crecientemente según su coste
i=0
para todo w en G.vertices() hacer
B.asignar(w, i); i++
fpara
i=1
mientras T.cardinalidad( ) < n - 1 hacer
ai = A.observa[i]
B.observar(ai.getOrigen( ), arbol1); B.observar(ai.getDestino( ), arbol2);
si arbol1 ≠ arbol2 entonces
T.poner(ai)
fusionar (B, ai.getOrigen( ), ai.getDestino( ))
fsi
i++
fmientras
devuelve T
fin
10 14
v1 v4 v6
9
3 7
7
v7 v8
v3
4
5 2
6
v2 v5
5
v1 v2 v3 v4 v5 v6 v7 v8
B 1 21 312 4
2
1 5 4
6
2
1 17