Conferencia 3. TDA Grafos
Conferencia 3. TDA Grafos
Conferencia 3. TDA Grafos
Los grafos resultan entidades matemáticas muy útiles para representar relaciones entre objetos.
En muchos problemas de la computación y otras disciplinas, a menudo es necesario representar
relaciones arbitrarias entre objetos para los que los grafos son modelos naturales de tales
relaciones.
Algunas aplicaciones de los Grafos.
- Un mapa de rutas de ferrocarriles. Estamos interesados en las respuestas a preguntas como:
¿Cuál es la forma más rápida o barata para viajar desde la ciudad A hasta la ciudad B? Para
responder dicha pregunta se requiere información sobre las interconexiones (rutas) y los
objetos (ciudades).
- En un circuito eléctrico, las interconexiones entre los objetos (transistores, resistencias,
capacitores, etc.) son importantes. Los grafos pueden ser útiles para responder preguntas tales
como: ¿Está todo conectado?, ¿El circuito funciona?
- El problema de “Calendarización de tareas”, los objetos son las tareas a ser desarrolladas y las
interconexiones indican cuáles trabajos deben ser hechos antes que otros. Preguntas tales
como: ¿Cuándo debe ser desarrollada cada tarea? pueden responderse con la ayuda de un
grafo.
Definiciones.
1
Estructuras de Datos y Algoritmos II curso 2019-2020
En lo adelante, utilizaremos los términos vértice y arista para evitar confusiones con los nodos del
TDA Lista.
En un grafo no dirigido son iguales las aristas (V1, V2) y (V2, V1) y se representan por {V1, V2}.
Ejemplo:
1
1
2
2
Ejemplos:
A
Camino: ABCA
C
Longitud: 3
Ciclo Simple: ABA
B
Ejemplos:
- Un árbol es un Grafo sin ciclos.
- Un bosque es un grupo de árboles desconectados.
En la mayoría de los grafos hay, como mucho, una arista entre un vértice v y otro w (esto incluye
el caso en que hay una arista en cada dirección). En consecuencia, A V 2. Cuando la mayoría
de las aristas están presentes, tenemos A = ( V 2 ), y el grafo se denomina denso (pues tiene
gran número de aristas).
2
Estructuras de Datos y Algoritmos II curso 2019-2020
En la mayoría de las aplicaciones los vértices están conectados a otros pocos vértices del grafo, en
ese caso se dice que el grafo es relativamente disperso, teniéndose A = ( V ). Por tanto, es
particularmente importante que los algoritmos sean eficientes para grafos dispersos.
Nos interesa aquí trabajar solo dos representaciones de las estudiadas: la Matriz de adyacencia y la
lista de adyacencia.
V V V
2 3 4
V V
5 6
Matriz de Adyacencia.
Una forma sencilla de representar un grafo es utilizar un arreglo bidimensional, denominado
matriz de adyacencia. Para cada arista (v, w), a[v][w] representa el costo de la arista; las aristas
inexistentes se pueden representar mediante un INFINITO “lógico”.
La inicialización del grafo puede requerir que la matriz de adyacencia completa se inicialice en
INFINITO y, después, para cada arista se actualice la entrada apropiada de la matriz. Esto tiene un
costo de tiempo y espacio de O( V 2 ), que resulta razonable para grafos densos, pero es
inaceptable para grafos dispersos.
En el ejemplo:
Observe que la matriz de adyacencia tiene (0) como valor INFINITO para indicar que
6x6 elementos y se ha utilizado el valor cero no hay arista.
3
Estructuras de Datos y Algoritmos II curso 2019-2020
v0 v1 v2 v3 v4 v5 v6
v 0 0 1 0 1 0 0 0
v1 0 0 0 1 1 0 0
v 2 1 0 0 0 0 1 0
v3 0 0 1 0 1 1 1
v 4 0 0 0 0 0 0 1
v5 0 0 0 0 0 0 0
v 6 0 0 0 0 0 1 0
Listas de Adyacencia.
Para grafos dispersos, una representación mejor es la suministrada por las listas de adyacencia.
Para cada vértice, se mantiene una lista enlazada de todos sus vértices adyacentes.
En el ejemplo:
Observe que, el número total de nodos en las listas es exactamente igual al número de aristas. En
consecuencia, se utiliza un espacio O( A ) para almacenar los nodos. Ya que tenemos V listas,
se necesita un espacio adicional O( V ).
Si asumimos que todo vértice está en alguna arista, el número de aristas es, al menos, V 2,
por lo que se puede descartar todo el término O( V ) cuando está presente un término O( A ).
Es decir, O(A + V ) = O(max { A , V }) = O( A ). Por consiguiente, decimos que el
espacio necesario es O( A ), o lineal con respecto al tamaño del grafo.
Las listas de adyacencia pueden construirse en tiempo lineal a partir de la relación de aristas.
Partimos de todas las listas vacías y cuando nos encontramos con una arista (v, w, cvw), añadimos a
la lista de adyacencia de v una entrada formada por w y el costo. La inserción puede realizarse en
cualquier punto de la lista, pero resultaría especialmente conveniente hacerlo al principio, para así
garantizar tiempo constante. De esta forma, cada arista puede insertarse en tiempo constante; por
lo que la estructura de listas de adyacencias puede construirse en tiempo lineal.
Observe que cuando insertamos una arista no comprobamos si ya está presente. Esto no puede
hacerse en tiempo constante, y la comprobación acabaría con la cota lineal en tiempo de la
construcción. En cualquier caso, esto no tiene gran importancia, pues la mayoría de los algoritmos
que funcionan con grafos seguirán funcionando con multigrafos en los que hay dos o más aristas
de diferentes costos conectando un par de vértices.
/*
* TDA Grafo versión 1.0
4
Estructuras de Datos y Algoritmos II curso 2019-2020
*
*/
public interface Grafo {
//operaciones de creación/modificacion del grafo
int insVertice(String nombre);
void insArista(String origen, String destino, float peso);
void insArista(String origen, String destino);
void elimVertice(String nombre);
void elimArista(String origen, String destino);
A partir de esa interfaz podemos por el momento definir dos clases para implementar el TDA
Grafo: GrafoMatrizAdyacencia y GrafoListaAdyacencia que son las dos
representaciones fundamentales con las que vamos a trabajar en el curso.
NOTA: Al final de cada clase se colocará la versión actualizada de la interface Grafo, lo cual se
hará a partir de los nuevos algoritmos que se estudien en la clase.
ES IMPORTANTE que cada estudiante domine la interfaz asociada al TGA Grafo.
Recorridos en grafos
Existen dos formas de recorrer un grafo: búsqueda en amplitud (BFS)(Breadth-first search) y
búsqueda en profundidad (DFS) (depth-first search).
BÚSQUEDA EN AMPLITUD
Utilizaremos la representación de lista de adyacencias de un grafo conexo.
La búsqueda en amplitud se puede utilizar para hallar la distancia entre algún vértice inicial y los
restantes vértices del grafo. Recuerde que la distancia entre cualquier par de vértices es la longitud
del camino más corto entre ese par de vértices, por tanto, esta distancia es el número mínimo de
aristas que hay que recorrer para pasar desde el vértice inicial hasta el vértice concreto que se esté
examinando. Comenzando en el vértice s, esta distancia se calcula examinando todas las aristas
incidentes en el vértice s, y pasando después a un vértice adyacente w, repitiéndose entonces todo
el proceso. El recorrido continúa hasta que sea hayan examinado todos los vértices del grafo.
Para cada vértice, hay que calcular la distancia desde el vértice inicial, y esa distancia se le asocia
al vértice en cuestión. Se enumeran todos los vértices adyacentes al vértice estudiado antes de
seguir adelante con la búsqueda. Esto asegura que todos los vértices sean examinados al menos
una vez (si el grafo es conexo).
En una búsqueda en amplitud, cada vértice se visita o es procesado en algún sentido, dependiendo
de la aplicación concreta. La búsqueda comienza en un vértice concreto del grafo (vértice origen).
A continuación, la búsqueda se extiende a todos los vértices del grafo que estén más próximo al
vértice inicial antes de visitar ningún otro. Estos vértices están relacionados de alguna forma con el
vértice especificado, y forman parte de un grupo que depende de la aplicación concreta.
Inicialmente, se visitan todos aquellos vértices que sean adyacentes al vértice inicial. A
continuación, se visitan todos los vértices que estén en una distancia de 2 del vértice inicial. Este
proceso se repite hasta que hayan visitado todos los vértices posibles. Sin embargo, este enfoque
de búsqueda puede dar lugar a problemas. Los vértices ya visitados no deben visitarse de nuevo.
5
Estructuras de Datos y Algoritmos II curso 2019-2020
Se puede evitar esta situación marcando aquellos vértices que se hayan visitado. Si un vértice ya
ha sido marcado con anterioridad (esto es, si ya ha sido visitado o alcanzado), nunca vuelve a ser
visitado. Es necesario tener un campo booleano adicional llamado visitado para llevar la cuenta de
los vértices ya visitados, además es necesario tener un campo llamado D para almacenar la
información de distancia.
Una estructura de cola en que el primero en entrar sea el primero en salir resulta cómoda para
llevar la cuenta de aquellos vértices cuyos vecinos quizá no hayan sido visitados. Esta estructura
de cola es adecuada, por cuanto los vértices se visitan por el mismo orden en que aparecen en una
lista de adyacencia. Por tanto, un vértice que preceda a otro en una lista de adyacencia se colocará
en la cola antes que el otro, esto es, empleando una estrategia del tipo primero en llegar, primero
en ser servido.
Dado un vértice inicial s, el algoritmo general para enfoque BFS es como sigue:
1. Se marcan todos los vértices del grafo como no visitados (o alcanzados)
2. Se marca y se visita s.
3. Se pone a s en la cola.
4. Mientras la cola no esté vacía:
4a. Se quita el primero de la cola, y se toma como vértice actual.
4b. Para cada vecino w del vértice actual:
4c. Si ese vecino no está marcado,
entonces
Se visita y se marca el vecino w.
Se actualiza el valor D(s,w).
Se pone el vecino w en la cola.
5. Si queda algún vértice sin marcar,
entonces Asignarlo a s y volver al paso 2.
sino Terminar el algoritmo (Fin)
Ejemplo:
V2 V1 v2 v3 v4 v5
1 2 V6 v2 V1 v3 v6
V3 v3 V1 v2 v4 v7 v8 V1
V1 0 1 2 V7 v4 V1 v3 v5 V8
v5 V1 v4 v9
1 2 V8 V6 v2 V2 V3 V4 V5
V4
V7 v3
V6 V7 V8 V9
V5 1 2 V9 V8 v3 v4
V9 v5
Aplicando la estrategia BFS que se acaba de describir al grafo que se ha colocado encima, se
obtiene el recorrido que se indica, suponiéndose que v1 sea la posición inicial y que a todas las
aristas se les ha asignado un valor 1.
Una búsqueda en amplitud en la figura anterior con las listas de adyacencias ordenadas que se
muestran y comenzando en el vértice v1 dan lugar a la siguiente sucesión de vértices visitados v1,
v2, v3, v4, v5, v6, v7, v8, v9.
El vértice inicial v1 se visita en primer lugar. A continuación, se consideran los vértices
adyacentes a v1. Todo vértice adyacente se encuentra a una distancia 1 de v1. Dado que todos los
6
Estructuras de Datos y Algoritmos II curso 2019-2020
vértices de la lista de adyacencia para v1 están ordenados según se muestren en la figura anterior,
los vértices adyacentes a v1 se visitan por el orden siguiente v2, v3, v4.
A continuación, se pasa a los vértices que se encuentran a una distancia 2 de v1. El recorrido de
estos vértices se ve facilitado por haber almacenado todos los vértices adyacentes a v1 en una
estructura de cola en que el primero en entrar es el primero en salir. Dado que v2 fue el primer
vértice adyacente a v1 que se visitó (y se colocó en la cola), exploraremos los vértices adyacentes
a v2 que no hayan sido visitados. El único de estos vértices es v6. El proceso se repite para v3, el
segundo vértice adyacente a v1, lo cual da lugar a visitas a los vértices v7 y v8. A continuación se
exploran los vértices no visitados de v4. No hay ninguno. Por último, el último vértice no visitado
adyacente a v5 es v9. Esta última visita contempla la búsqueda en amplitud (recorrido) del grafo.
La estrategia BFS da lugar al recorrido que indican las flechas.
Comentarios
Obsérvese que todo vértice visitado tiene asociado un valor verdadero en su marca.
BÚSQUEDA EN PROFUNDIDAD
Se puede utilizar una búsqueda en profundidad (DFS) en un grafo arbitrario para realizar el
recorrido de un grafo general. A medida que es encuentra cada nuevo vértice, se marca el camino
para evitar volver a visitarlo de nuevo. Recuerde que también se utilizaba este marcado en la
búsqueda en amplitud. La estrategia DFS es como sigue: se toma un vértice s como comienzo, y se
marca. A continuación, se toma y se marca un vértice no marcado adyacente a s, y ese vértice pasa
ser el nuevo vértice de partida, dejando posiblemente por el momento al vértice inicial original con
vértices no explorados. La búsqueda continúa por el grafo hasta que el camino en curso finalice:
no hay adyacentes al vértice actual, o bien todos los vértices adyacentes estén marcados. A
continuación, la búsqueda vuelve al último vértice que todavía tenga vértices adyacentes sin
marcar, y continúa marcando todos los vértices de forma recursiva hasta que ya no queden vértices
sin marcar.
Tal como se hacía en el caso de la búsqueda en amplitud, también es preciso marcar los vértices en
la búsqueda en profundidad. Si no se hiciera esto, los grafos que contuvieran ciclos darían lugar a
bucles infinitos. Tal como sucedía en el caso de BFS, el marcado de vértice evita que éstos
vuelvan a ser visitados. En comparación con una estrategia BFS, que opera en amplitud, esta
estrategia de búsqueda va lo más lejos posible, o a tanta profundidad como sea posible, con
respecto al vértice original.
Un algoritmo de búsqueda en profundidad consta de una única rutina principal que invoca a un
procedimiento recursivo de la manera siguiente:
Principal
1. Se marcan todos los vértices del grafo como no visitados.
2. Se invoca a DFS(s) para algún vértice inicial s.
3. Si queda algún vértice sin marcar,
entonces Asignarlo a s y volver al paso 2.
sino Terminar el algoritmo (Fin)
Procedimiento DFS(v)
1. Se marca y visita v.
7
Estructuras de Datos y Algoritmos II curso 2019-2020
Ejemplo.
En la figura que sigue aparece una búsqueda en profundidad del grafo de dado anteriormente. Al
comenzar en el vértice v1 se obtiene la siguiente sucesión de vértices visitados v1, v2, v3, v4, v5,
v9, v8, v7, v6.
V2
2 9 V6
a
V3
V1 1 3 8 V7
a
4 7 V8
a
V4
V5 5 6 V9
a
Se dará a continuación un seguimiento del resultado. El vértice inicial, v1, se marca y se visita. El
primer vértice de la lista de adyacencia para v1 es v2. Se marca y visita este vértice. Ahora el
primer vértice de la lista de adyacencia para v2 es v1 y este vértice ya se ha visitado.
Consiguientemente, el próximo vértice adyacente que hay que marcar y visitar es v3. Los dos
primeros vértices de la lista de adyacencia para v3 (esto es, v1 y v2) ya se han visitado. Por tanto,
v4 es el próximo vértice que se debe visitar y marcar en el proceso de búsqueda. Una vez más,
dado que los dos primeros vértices de la lista de adyacencia correspondiente a v4 ya han sido
visitados, el próximo vértice que hay que marcar y visitar es v5. En la lista de adyacencia
correspondiente a v5, el próximo vértice que hay que visitar es v9. Por tanto, se marca y se visita
v9. Llegados aquí, no quedan nuevos vértices adyacentes a v9 por visitar. A continuación, se
marca y se visita v8. Como ya no quedan vértices sin marcar que sean adyacentes a v4, se vuelve
al vértice v3, lo cual da lugar a que se marque y se visite v7. Dado que v8 ya sido marcado y
visitado, se produce un retorno a v2, lo cual da lugar que se marque y se visite el vértice v6.
Finalmente, se produce un retorno a v1 y finaliza el recorrido. El recorrido del grafo se muestra en
el mismo grafo, donde las flechas muestran un seguimiento de los vértices visitados y el número
de cada vértice denota el número de secuencia en que ha sido visitado. Por ejemplo, los vértices v1
y v6 han sido el primero y el último de los vértices visitados, respectivamente.
Dado que los vértices adyacentes se necesitan durante el recorrido, la representación más eficiente
vuelve a ser una lista de adyacencia.
CONCLUSIONES
RESUMEN
/*
* Interface TDA Grafo, versión 1.1 (final para esta Conferencia 1)
*
*/
8
Estructuras de Datos y Algoritmos II curso 2019-2020
//recorridos
Lista recorridoAmplitud(String VerticeOrigen);
Lista recorridoProfundidad(String VerticeOrig);