Grafos: algunas definiciones Un grafo dirigido G es un par (V, E), donde V es un conjunto finito de nodos (o vértices) y E es una relación binaria sobre V . Un grafo no dirigido G es un par (V, E), donde V es un conjunto finito de nodos (o vértices) y E es una conjunto de pares no ordenados de vértices. Un camino de largo k desde un vértice u a un vértice u0 es una secuencia de vértices hv0, v1, . . . , vk i tal que u = v0 y u0 = vn, y (vi, vi+1) ∈ E, para i = 1, 2, . . . , k. Un nodo u es alcanzable desde un nodo v, si existe un camino entre v y u. Un ciclo es un camino hv0, . . . , vk i, donde v0 = vk . Un grafo no dirigido es conexo si cada par de vértices están conectados por un camino. Un grafo dirigido es fuertemente conexo si hay un camino entre cada par de vértices. Jorge Baier Aranda, PUC 41 A veces se asocian costos a los arcos a través de una función w : E → N. Jorge Baier Aranda, PUC 42 Representando Grafos Existen dos maneras estándar de representar grafos; a través de listas de adyacencia y matrices de adyacencia. Listas de Adyacencia Una lista de adyacencia en un grafo G = (V, E) consiste en un arreglo que contiene una lista para cada elemento de V . En la lista del nodo v (v ∈ V ) se encuentran todos los nodos u tales que (v, u) ∈ V . Ventaja: Representación “compacta”. Desventaja: No es O(1) saber si hay un nodo entre un par de nodos. Jorge Baier Aranda, PUC 43 Matrices de Adyacencia En una matriz de adyacencia suponemos que los nodos están numerados 1, . . . , |V |. El grafo se representa por una matriz A tal que ( 1 si (i, j) ∈ E A[i, j] = 0 en otro caso Ventaja: O(1) saber si hay un nodo entre un par de nodos. Desventaja: Representación puede ser poco “compacta”. Ambas representaciones pueden ser trivialmente extendidas si se necesita agregar costos a los arcos. Jorge Baier Aranda, PUC 44 Búsqueda en grafos Muchos problemas pueden ser planteados como problemas de búsqueda en grafos. En un problema de búsqueda interesa encontrar cómo es posible llegar desde un nodo fuente s, hasta un nodo destino que cumpla con una propiedad P . Búsqueda en Profundidad (BFS) La búsqueda es un tipo de búsqueda sistemática que “descubre” primero los nodos que se encuentran más cercanos a la fuente. De esta manera, en una primera fase, encuentra todos los nodos que se encuentran a distancia 1 de la fuente, luego los que se encuentran a distancia k, y ası́ sucesivamente. Durante la búsqueda, BFS evita “visitar” un nodo por segunda vez. Por esta razón, en el proceso de búsqueda los nodos pueden tener 3 colores: Jorge Baier Aranda, PUC 45 • Blanco: El nodo aún no ha sido visitado. • Gris: El nodo es sucesor de un nodo visitado y pronto será visitado. • Negro: El nodo ha sido visitado. La siguiente versión de BFS calcula la distancia desde la fuente (s) hasta todos los nodos del grafo. Además, calcula el predecesor de cada nodo u, π[u], que está sobre el camino más corto de s a u. El conjunto de nodos adyacentes a v se obtiene con la operación Adj[v]. En d[u] queda almacenada el costo desde s al nodo u. Jorge Baier Aranda, PUC 46 BFS(G, s) 1 for each u ∈ V [G] − {s} 2 do color[u] ← blanco 3 d[u] ← ∞ 4 π[u] ← nil 5 color[s] ← gris 6 d[s] ← 0 7 π[s] ← nil 8 Q ← {s} 9 while Q 6= ∅ 10 do u ← head[Q] 11 for each v ∈ Adj[u] 12 do if color[v] = blanco 13 then color[v] ← gris 14 d[v] ← d[u] + 1 15 π[v] ← u 16 Enqueue(Q, v) 17 Dequeue(Q) 18 color[u] ← negro Jorge Baier Aranda, PUC 47 Búsqueda en Profundidad (DFS) Como su nombre lo indica, es una búsqueda que avanza en profundidad. El comportamiento final es parecido a hacer una especie de backtracking. Igual que en el caso anterior, mientras el algoritmo ejecuta, cambia el color de los nodos por blanco, gris o negro. En la siguiente versión de DFS, se calcula un arreglo d, tal que d[u] contiene el número de nodos que se han visitado antes de visitar al nodo u. Además, en π[u] queda almacenado el nodo que fue visitado antes que u. Jorge Baier Aranda, PUC 48 DFS(G) 1 for each u ∈ V [G] 2 do color[u] ← blanco 3 π[u] ← nil 4 tiempo ← 0 5 for each u ∈ V [G] 6 do if color[u] = blanco 7 then DFS-Visit(u) DFS-Visit(u) 1 color[u] ← gris 2 d[u] ← tiempo ← tiempo + 1 3 for each v ∈ Adj[u] 4 do if color[v] = blanco 5 then π[v] ← u 6 DFS-Visit(v) 7 color[u] ← negro Jorge Baier Aranda, PUC 49 Camino más corto entre dos nodos Consiste en encontrar el camino de menor costo entre un nodo fuente s y un nodo destino e. Si todos los arcos tienen asociado el mismo costo, entonces este problema se puede resolver fácilmente haciendo una búsqueda BFS. En otro caso es necesario un algoritmo levemente distinto. El algoritmo de Dijkstra resuelve este problema para cuando las aristas tienen costos no negativos. Este algoritmo mantiene, en todo momento, un conjunto S de nodos cuyo costo mı́nimo desde la fuente ya ha sido calculado. Para todo elemento u ∈ V , se almacena en d[u] el costo entre s y u estimada hasta el momento. Observación: Si u ∈ S, entonces d[u] contiene el costo mı́nimo entre s y u. Jorge Baier Aranda, PUC 50 En todo momento, el algoritmo mantiene una cola de prioridades, en donde almacena los nodos del grafo que aún no están en S. El ı́ndice de la cola está dato por el valor de d para estos nodos. Además, el algoritmo almacena en π[u] al antecesor de u en el camino de menor costo hasta u desde s. Jorge Baier Aranda, PUC 51 El siguiente es el algoritmo: Dijkstra(G, w, s) 1 for each v ∈ V [G] 2 do d[v] ← ∞ 3 π[v] ← nil 4 d[s] ← 0 5 S←∅ 6 Q ← V [G] 7 while Q 6= ∅ 8 do u ← Extract-Min(Q) 9 S ← S ∪ {u} 10 for v ∈ Adj[u] 11 do if d[v] > d[u] + w(u, v) 12 then d[v] ← d[u] + w(u, v) 13 π[v] ← u Nota 1: La función Extract-Min(Q) extra el nodo de Q que tiene menor distancia estimada desde s. Nota 2: Este algoritmo sólo funciona cuando las aristas tienen costos positivos. En caso de haber costos negativos, es posible usar el algoritmo de Bellman-Ford (no lo veremos). Jorge Baier Aranda, PUC 52 Caminos más cortos entre todo par de nodos Este problema consiste en encontrar el costo del camino más corto entre todo par de nodos. El algoritmo de Floyd-Warshall, ataca este problema con la técnica de programación dinámica. Supongamos que tenemos los nodos de un grafo G = (V, E) numerados entre 1 y |V |. Sea dkij la distancia mı́nima entre el nodo i y el nodo j, con todos los nodos intermedios en el conjunto {1, . . . , k}. Podemos escribir una expresión recursiva para dkij : ( wij si k = 0 dkij = k−1 k−1 mı́n{dk−1 , d + d ij ik kj } en otro caso Jorge Baier Aranda, PUC 53 |V | A nosotros nos interesará calcular dij , y es posible hacerlo de manera bottom-up: Floyd-Warshall(W ) 1 n ← rows[W ] 2 for i ← 1 to n 3 do for j ← 1 to n 4 do d0ij = wij 5 for k ← 1 to n 6 do for i ← 1 to n 7 do for j ← 1 to n k−1 k−1 8 do dkij ← mı́n{dk−1 ij , dik + dkj } Jorge Baier Aranda, PUC 54 Tareas Usando matrices de adyacencia (matrices de costo), programar: 1. Algoritmo BFS en C y C++, extendiéndolo para que se detenga cuando ha encontrado un nodo que cumple alguna propiedad. 2. Algoritmo de Dijkstra en C y C++. 3. Algoritmo de Bellman-Ford, en C y C++. 4. Algoritmo de Floyd-Warshall en C, extendiéndolo para que retorne una matriz de predecesores. Todos los algoritmos deben estar precisa y concisamente documentados. Debe quedar claro, el input, el output (de existir) y qué hace el algoritmo. Jorge Baier Aranda, PUC 55