Analisis De Grafos

  • Uploaded by: Abel ORian
  • 0
  • 0
  • May 2020
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Analisis De Grafos as PDF for free.

More details

  • Words: 5,987
  • Pages: 20
Estrategia Greedy El método codicioso consiste en tomar sucesivamente, las decisiones de modo que cada decisión individual sea la mejor de acuerdo con algún criterio limitado "a corto plazo" cuya evaluación no sea demasiado costosa. Una vez tomada una decisión, no se podrá revertir, ni siquiera si más adelante se hace obvio que no fue una buena decisión. Por esta razón, los métodos codiciosos no necesariamente hallan la solución óptima en muchos problemas. No obstante, en el caso de los problemas que se estudiaran en este capítulo, es posible demostrar que la estrategia codiciosa apropiada produce soluciones óptimas. En general la estrategia Greedy trabaja en fases. En cada fase, se realiza una decisión que aparentemente es la mejor, sin considerar o lamentar futuras consecuencias. Generalmente, esta idea es pensada como escoger algún óptimo local. Si se resume la estrategia como "toma lo que puedas obtener, ahora" podrá entender el porque del nombre de esta clase de algoritmos. Cuando el algoritmo termina tendremos la esperanza que el óptimo local es igual al óptimo global. Si este es el caso, entonces el algoritmo es correcto, de otra manera el algoritmo a generado una solución suboptimal. Si la "mejor" respuesta no es requerida entonces los algoritmos greedy son a menudo usados para generar respuestas aproximadas, en vez de usar algoritmos más complicados que requieren de otra estrategia para generar "la" respuesta correcta. En general la estrategia Greedy trabaja en etapas "Top-Down" realizando una elección greedy después de la otra. En resumen, la estrategia Greedy consiste de dos partes: 1. Subestructura Optimal 2. Partir con elección de óptimos locales y continuar haciendo elecciones localmente optimales, hasta que la solución es encontrada. Problemas Asociados a esta técnica : Arbol Expandido Minimal ( Minimum Spanning Trees) “Dado un grafo no-dirigido y conexo G = , donde cada arco tiene asociado una "longitud" o "peso", se desea un subconjunto T, de arcos E, tal que el grafo restante sea conexo si solamente los arcos en T son usados, y la suma de los pesos de los arcos es el más pequeño posible. Tal subconjunto debe formar un subgrafo que es un árbol, al cual se le llama Minimum Spanning Tree.” En el proceso de cambio de monedas, por ejemplo $15.530 puede expresarse en $15.530 = 1 * $10.000 + 1 * $ 5.000 + 1 * $ 500 + 3 * ($10). En total se requieren 5 elementos. Al hacer esto estamos seguro de que el número de billetes y monedas es el mínimo. (Problema de Planificación de Tareas) Dado los trabajos t 1, t 2, ... t n, con tiempos t’1, t‘2, ... t’n y un solo procesador. Cuál es la mejor forma de planificar esos trabajos a fin de minimizar el tiempo medio de terminación? (Código de Huffman) en la compresión de archivos. Un código de longitud fija de un alfabeto A= { a1 , a2 , ..., am } con m-letras es una expresión en donde cada letra ai = ci,1 ci,2 ...ci,n , en donde i= 1, ...m, y donde cada ci,j es un elemento de algún conjunto de símbolos. El conjunto C = { ci,1 ci,2 ...ci,n , tal que, i= 1, ...m, es llamado código de longitud fija de longitud n, y cada ci,1 ci,2 ...ci,,n es llamado código. Veamos con cierto detalle, como funciona esta estrategia en la resolución del problema de comprimir archivos.

Nociones basicas de los grafos

Los grafos pueden representarse estructuras de datos distintas. Los algoritmos que se aplican sobre ellos adoptan tiempos distintos dependiendo de la forma de representación elegida. En particular, los tiempos de ejecución variarán en función del número de vértices y el de aristas, por lo que la utilización de una representación u otra dependerá en gran medida de si el grafo es denso (cuando tiene muchas aristas) o disperso (muy pocas aristas). Sea G = (V, E) un grafo simple con n=|V|, m=|E|. Definiremos la densidad del grafo como:

Notar que 0 < d < 1, donde d=0 si todos los vértices son aislados y d=1 si el grafo es completo. Si d es cercano a cero se dice que el grafo es disperso y si d es cercano a 1 se dice que el grafo es denso. Las representaciones más utilizadas de representación de los grafos son: a. Representación por matriz de adyacencia b. Representación por listas de adyacencias La matriz de adyacencia es la forma más común de representación y la más directa. Consiste en una tabla de tamaño nxn, en que la que aij tendrá como valor 1 si existe una arista del vértice i al vértice j. En caso contrario, el valor será 0. Si el grafo es no dirigido hay que asegurarse de que se marca con un 1 tanto la entrada aij como la entrada aji, puesto que se puede recorrer en ambos sentidos. Como se puede apreciar, la matriz de adyacencia siempre ocupa un espacio de n2, es decir, depende solamente del número de nodos y no del de aristas, por lo que será útil para representar grafos densos. Una lista de adyacencia consiste de una lista de los vértices del grafo y para cada vértice de una lista de sus vértices vecinos. Ejemplos:

Directed acyclic graph (DAG) Es un grafo dirigido con siglos no dirigidos, que es, para algún vértice v, no hay caminos dirigidos en que v sea el inicio y final. Orden topológico: ▪Orden topológico de un DAG G=(V,E) es un orden lineal de todos los vértices tal que si G contiene el arco (u,v), entonces u aparece antes que v en el orden. ▪Cuando se tienen muchas actividades que dependen parcialmente unas de otras, este orden permite definir un orden de ejecución sin conflictos. ▪Gráficamente se trata de poner todos los nodos en una línea de manera que sólo haya arcos hacia delante. Algoritmo: Topological_Orden(G) Llamar a DFS(G) para calcular el tiempo de término f[v] para cada vértice. Insertar cada nodo en una lista enlazada según su orden de término. Retornar la lista enlazada. Costo: O(V + E),

Búsqueda en profundidad Una Búsqueda en profundidad (en inglés DFS o Depth First Search) es un algoritmo que permite recorrer todos los nodos de un grafo o árbol de manera ordenada, pero no uniforme. Su funcionamiento consiste en ir expandiendo todos y cada uno de los nodos que va localizando, de forma recurrente, en un camino concreto. Cuando ya no quedan más nodos que visitar en dicho camino, regresa (Backtracking), de modo que repite el mismo proceso con cada uno de los hermanos del nodo ya procesado. Costo: O( | V | + | E | ) = O(bd) DFS(grafo G) PARA CADA vertice u ∈ V[G] HACER estado[u] ← NO_VISITADO padre[u] ← NULO tiempo ← 0 PARA CADA vertice u ∈ V[G] HACER SI estado[u] = NO_VISITADO ENTONCES DFS_Visitar(u) DFS-Visitar(nodo u) estado[u] ← VISITADO tiempo ← tiempo + 1 d[u] ← tiempo PARA CADA v ∈ Vecinos[u] HACER SI estado[v] = NO_VISITADO ENTONCES padre[v] ← u DFS_Visitar(v) estado[u] ← TERMINADO tiempo ← tiempo + 1 f[u] ← tiempo

Clasificación de las aristas o ejes y parentización

Otra propiedad interesante de depth-first search es que puede servir para clasificar las aristas del grafo G=(V,E). Esa clasificación de ejes puede ser usada para recoger importante información acerca del grafo. Por ejemplo, un grafo dirigido es acíclico si y solo si no tiene aristas de tipo “back”. Se pueden definir 4 tipos de aristas: •Tree edges: Son aristas del bosque generado por DFS. •Back edges: Son aquellas aristas que conectan un vértice u con un vértice v ancestro en el depth-first tree. •Forward edges: Son aquellas aristas que conectan un vértice u con un vértice v descendiente en el depth-first tree. •Cross edges: Son todos las demás aristas. Ellos pueden ir desde vértices en el mismo depthfirst tree, a lo largo de un vértice que no es ancestro de otro, o pueden ir de vértices de diferentes depth-first trees.

a) Resultado de DFS. b) Lema de parentización. c)Tipos de aristas en el DFS.

Búsqueda en anchura Es un algoritmo para recorrer o buscar elementos en un grafo (usado frecuentemente sobre árboles). Intuitivamente, se comienza en la raíz (eligiendo algún nodo como elemento raíz en el caso de un grafo) y se exploran todos los vecinos de este nodo. A continuación para cada uno de los vecinos se exploran sus respectivos vecinos adyacentes, y así hasta que se recorra todo el árbol. Formalmente, BFS es un algoritmo de búsqueda sin información, que expande y examina todos los nodos de un árbol sistemáticamente para buscar una solución. El algoritmo no usa ninguna estrategia heurística. Costo: O( | V | + | E | ) = O(bd) Procedimiento: •Dado un vértice fuente s, Breadth-first search sistemáticamente explora los vértices de G para “descubrir” todos los vértices alcanzables desde s. •Calcula la distancia (menor número de vértices) desde s a todos los vértices alcanzables. •Después produce un árbol BF con raíz en s y que contiene a todos los vértices alcanzables. •El camino desde s a cada vértice en este recorrido contiene el mínimo número de vértices. Es el camino más corto medido en número de vértices. •Su nombre se debe a que expande uniformemente la frontera entre lo descubierto y lo no descubierto. Llega a los nodos de distancia k, sólo tras haber llegado a todos los nodos a distancia k-1. Algoritmo: BFS(grafo G, nodo_fuente s) { // recorremos todos los vértices del grafo inicializándolos a NO_VISITADO, // distancia INFINITA y padre de cada nodo NULL for u ∈ V[G] do { estado[u] = NO_VISITADO; distancia[u] = INFINITO; /* distancia infinita si el nodo no es alcanzable */ padre[u] = NULL; } estado[s] = VISITADO; distancia[s] = 0; Encolar(Q, s); while !vacia(Q) do { // extraemos el nodo u de la cola Q y exploramos todos sus nodos adyacentes u = extraer(Q); for v ∈ adyacencia[u] do { if estado[v] == NO_VISITADO then { estado[v] = VISITADO; distancia[v] = distancia[u] + 1; padre[v] = u;

Encolar(Q, v); } }

}

}

Aplicaciones: •Buscar todos los componentes conexos en un grafo. •Buscar todos los nodos con un componente conexo •Buscar en camino mas corto entre dos vértices u y v (con y sin peso).

Árbol expandido minimal Un árbol expandido minimal (Minimal spaning tree) de un grafo es un subgrafo que forma un árbol, el cual pasa por todos los vértices del grafo y tiene un costo mínimo, etiquetado en cada una de las aristas. Cada grafo tiene su árbol recubridor mínimo.

Figura: Árbol espandido minimal. Algoritmos que encuentran el árbol mínimo: •Algoritmo de Prim •Algoritmo de Kruskal

Algoritmo de Prim El algoritmo de Prim es un algoritmo de la teoría de los grafos para encontrar un árbol recubridor mínimo en un grafo conexo, no dirigido y cuyas aristas están etiquetadas. En otras palabras, el algoritmo encuentra un subconjunto de aristas que forman un árbol con todos los vértices, donde el peso total de todas las aristas en el árbol es el mínimo posible. Si el grafo no es conexo, entonces el algoritmo encontrará el árbol recubridor mínimo para uno de los componentes conexos que forman dicho grafo no conexo. Costo: Usando Fibonacci heap: O(E + V log V) Algoritmo: *Estructura de datos auxiliar: Q = Estructura de datos Cola de prioridad (se puede implementar con un heap) PRIM (Grafo G, nodo_fuente s) // inicializamos todos los nodos del grafo. La distancia la ponemos a infinito // y el padre de cada nodo a NULL for each u ∈ V[G] do distancia[u] = INFINITO padre[u] = NULL distancia[s]=0 Encolar(cola, V[G]) //encolamos todos los nodos del grafo while cola != 0 do // OJO: Se extrae el nodo que tiene distancia mínima y se conserva la condición de Cola de prioridad u = extraer_minimo(cola) for v ∈ adyacencia[u] do if ((v ∈ cola) && (distancia[v] > peso(u, v)) do padre[v] = u distancia[v] = peso(u, v) actualizar(cola,v);

Algoritmo de Kruskal Funciona de la siguiente manera:

• se crea un bosque B (un conjunto de árboles), donde cada vértice del grafo es un árbol separado • se crea un conjunto C que contenga a todas las aristas del grafo • mientras C es novacío • eliminar una arista de peso mínimo de C • si esa arista conecta dos árboles diferentes se añade al bosque, combinando los dos árboles en un solo árbol • en caso contrario, se desecha la arista Al acabar el algoritmo, el bosque tiene una sola componente, la cual forma un árbol de expansión mínimo del grafo.

Pseudocodigo: 1 function Kruskal(G) 2 for each vertex v in G do 3 Define an elementary cluster C(v) ← {v}. 4 Initialize a priority queue Q to contain all edges in G, using the weights as keys. 5 Define a tree T ← Ø //T will ultimately contain the edges of the MST 6 // n es el número total de vértices 7 while T has fewer than n-1 edges do 8 // edge u,v is the minimum weighted route from/to v 9 (u,v) ← Q.removeMin() 10 // previene ciclos en T. suma u,v solo si T no contiene una arista que una u y v. 11 // Nótese que el cluster contiene más de un vértice si una arista une un par de 12 // vértices que han sido añadidos al árbol. 13 Let C(v) be the cluster containing v, and let C(u) be the cluster containing u. 14 if C(v) ≠ C(u) then 15 Add edge (v,u) to T. 16 Merge C(v) and C(u) into one cluster, that is, union C(v) and C(u). 17 return tree T

Costo: O(E Log V) m el número de aristas del grafo y n el número de vértices, el algoritmo de Kruskal muestra una complejidad O(m log m) o, equivalentemente, O(m log n), cuando se ejecuta sobre estructuras de datos simples. Los tiempos de ejecución son equivalentes porque: • m es a lo sumo n2 y log n2 = 2logn es O(log n). • ignorando los vértices aislados, los cuales forman su propia componente del árbol de expansión mínimo, n ≤ 2m, así que log n es O(log m). Se puede conseguir esta complejidad de la siguiente manera: primero se ordenan las aristas por su peso usando una ordenación por comparación (comparison sort) con una complejidad del orden de O(m log m); esto permite que el paso "eliminar una arista de peso mínimo de C" se ejecute en tiempo constante. Lo siguiente es usar una estructura de datos sobre conjuntos disjuntos (disjointset data structure) para controlar qué vértices están en qué componentes. Es necesario hacer orden de O(m) operaciones ya que por cada arista hay dos operaciones de búsqueda y posiblemente una unión de conjuntos. Incluso una estructura de datos sobre conjuntos disjuntos simple con uniones por rangos puede ejecutar las operaciones mencionadas en O(m log n). Por tanto, la complejidad

total es del orden de O(m log m) = O(m log n).

All pairs shortest path Buscar “Todos los pares de caminos más cortos” consiste en buscar la menor distancia entre cada par de nodos en un, posiblemente, grafo dirigido. Se conocen varios métodos para lograrlo, la siguiente lista muestra algunos métodos comunes: ◦Floyd-Warshall: es un elegante, y rápido algoritmo, de costo O(n³). Asume la ausencia de ciclos de peso negativo, aunque permite pesos negativos en las aristas. ◦Johnson's algoritmo: Es difícil de implementar, pero con mejor performance para grafo con pocas aristas. ◦Algoritmos para “single source path” pueden ser usados repetidamente, aunque hacer esto puede ser peor en costo o más difícil de optimizar.

Single-source shortest path Es el problema de encontrar un camino entre dos vértices (o nodos) tal que la suma de los pesos de sus aristas sea mínimo. Un ejemplo es buscar la vía más rápida desde una locación a otra en una carretera; en este caso, los vértices representan localidades y las aristas segmentos de carretera y los pesos, el tiempo que se requiere para viajar ese segmento. El problema es llamado aveces single-pair shortest path problem, para distinguirlo de las siguientes generalizaciones: ◦Single-source shortest path problem: Encontrar camino más corto desde un vértice origen 'v' a todos los demás en el grafo. ◦Single-destination shortest path problem: Encontrar camino más corto desde todos los vértices en el grafo hacia un vértice singular 'v' ◦All-pairs shortest path problem: Encontrar caminos mas cortos entre cada par de vértices v, v' en el grafo. Los algoritmos mas importantes que resuelven estos problemas son: ◦Dijkstra: Resuelve los problemas de “single-pair”, “single source” y “single-destination” shortest path problems. ◦Bellman-Ford: Resuelve el problema de “single source” si las aristas pueden tener pesos negativos. ◦Floyd-Warshall: Resuelve “All pairs shortest path”. ◦Johnson's : Resuelve “All pairs shortest path”, y puede ser mas rápido que Floyd-Warshall en grafos con pocos aristas. Usos: Mapas, Redes.

Dijkstra Resuelve “single-source path problem” para un grafo con pesos no negativos, produciendo un “árbol de caminos cortos”. Para un nodo raíz dado en el grafo, el algoritmo encuentra el camino con menor peso entre ese vértice y cada uno de los demás. Esto puede ser usado para encontrar el costo menor del camino de

vértices dados como origen y otro como destino,parando la ejecución una vez que esa camino a sido determinado. Costo: O( | E | + | V | log | V | ) con Fibonacci Heap. Algoritmo: Teniendo un grafo dirigido ponderado de N nodos no aislados, sea x el nodo inicial, un vector D de tamaño N guardará al final del algoritmo las distancias desde x al resto de los nodos. 1. Inicializar todas las distancias en D con un valor infinito relativo ya que son desconocidas al principio, exceptuando la de x que se debe colocar en 0 debido a que la distancia de x a x sería 0. 2. Sea a = x (tomamos a como nodo actual). 3. Recorremos todos los nodos adyacentes de a menos los nodos marcados, llamaremos a estos vi. 4. Si la distancia desde x hasta vi guardada en D es mayor que la distancia desde x hasta a sumada a la distancia desde a hasta vi; esta se substituye con la segunda nombrada, esto es: si (Di > Da + d(a,vi)) entonces Di = Da + d(a,vi) 5. Marcamos como completo el nodo a. 6. Tomamos como próximo nodo actual el de menor valor en D (puede hacerse almacenando los valores en una cola de prioridad) y volvemos al paso 3 mientras existan nodos no marcados. Una vez terminado al algoritmo, D estará completamente lleno. dijkstra (Grafo G, nodo_fuente s) // inicializamos todos los nodos del grafo. La distancia de cada nodo es infinita // y los padres son NULL for u ∈ V[G] do distancia[u] = INFINITO padre[u] = NULL distancia[s] = 0 //encolamos el nodo_fuente s Encolar (cola, grafo) mientras cola no es vacía do // OJO: Se extrae el nodo que tiene distancia mínima y se conserva la condición // de Cola de prioridad u = extraer_mínimo(cola) for v ∈ adyacencia[u] do if u= source break if distancia[v] > distancia[u] + peso (u, v) do distancia[v] = distancia[u] + peso (u, v) padre[v] = u

Bellman-Ford El algoritmo de Bellman-Ford, genera el camino más corto en un Grafo dirigido ponderado (en el que el peso de alguna de las aristas puede ser negativo). El algoritmo de Dijkstra resuelve este mismo problema en un tiempo menor, pero requiere que los pesos de las aristas no sean negativos.

Por lo que el Algoritmo Bellman-Ford normalmente se utiliza cuando hay aristas con peso negativo. Algoritmo: Existen dos versiones: • Versión no optimizada para grafos con ciclos negativos, cuyo coste de tiempo es O(VE) • Versión optimizada para grafos con aristas de peso negativo, pero en el grafo no existen ciclos de coste negativo, cuyo coste de tiempo, es también O(VE). BellmanFord(Grafo G, nodo_origen s) // inicializamos el grafo. Ponemos distancias a INFINITO menos el nodo origen que // tiene distancia 0 for v ∈ V[G] do distancia[v]=INFINITO predecesor[v]=NIL distancia[s]=0 // relajamos cada arista del grafo tantas veces como número de nodos -1 haya en el grafo for i=1 to |V[G]-1| do for (u,v) ∈ E[G] do if distancia[v]>distancia[u] + peso(u,v) then distancia[v] = distancia[u] + peso (u,v) predecesor[v] = u // comprobamos si hay ciclos negativo for (u,v) ∈ E[G] do if distancia[v] > distancia[u] + peso(u,v) then print ("Hay ciclo negativo") return FALSE return TRUE BellmanFord_Optimizado(Grafo G, nodo_origen s) // inicializamos el grafo. Ponemos distancias a INFINITO menos el nodo origen que // tiene distancia 0. Para ello lo hacemos recorriéndonos todos los vértices del grafo for v ∈ V[G] do distancia[v]=INFINITO padre[v]=NIL distancia[s]=0 encolar(s, Q) en_cola[s]=TRUE mientras Q!=0 then u = extraer(Q) en_cola[u]=FALSE // relajamos las aristas for v ∈ ady[u] do if distancia[v]>distancia[u] + peso(u,v) then distancia[v] = distancia[u] + peso (u,v) padre[v] = u if en_cola[v]==FALSE then encolar(v, Q) en_cola[v]=TRUE

Algoritmo de Johnson Es una forma de encontrar el camino más corto entre todos los pares de vértices de un grafo dirigido

disperso. Permite que las aristas tengan pesos negativos, si bien no permite ciclos de peso negativo. Funciona utilizando el algoritmo de Bellman-Ford para hacer una transformación en el grafo inicial que elimina todas las aristas de peso negativo, permitiendo por tanto usar el algoritmo de Dijkstra en el grafo transformado. La complejidad temporal de este algoritmo, usando montículos de Fibonacci en la implementación del algoritmo de Dijkstra, es de O(V^2log V + VE): el algoritmo usa un tiempo de O(VE) para la fase Bellman-Ford del algoritmo, y O(V log V + E) para cada una de las V instancias realizadas del algoritmo de Dijkstra. Entonces, cuando el grafo es disperso el tiempo total del algoritmo puede ser menor que el algoritmo de Floyd-Warshall, que resuelve el mismo problema en un tiempo de O(V^3). El algoritmo de Johnson consiste en los siguientes pasos: 1. Primero se añade un nuevo nodo q al grafo, conectado a cada uno de los nodos del grafo por una arista de peso cero. 2. En segundo lugar, se utiliza el algoritmo de Bellman-Ford, empezando por el nuevo vértice q, se determina para cada vértice v el peso mínimo h(v) del camino de q a v. Si en este paso se detecta un ciclo negativo, el algoritmo concluye. 3. Seguidamente, a las aristas del grafo original se les cambia el peso usando los valores calculados por el algoritmo de Bellman-Ford: una arista de u a v con tamaño w(u,v), da el nuevo tamaño w(u,v) + h(u) – h(v) 4. Por último, para cada nodo s se usa el algoritmo de Dijkstra para determinar el camino más corto entre s y los otros nodos, usando el grafo con pesos modificados. En el grafo con pesos modificados, todos los caminos entre un par de nodos s y t tienen la misma cantidad h(s) – h(t) añadida a cada uno de ellos, así que un camino que sea el más corto en el grafo original también es el camino más corto en el grafo modificado y viceversa. Sin embargo, debido al modo en el que los valores h(v) son computados, todos los pesos modificados de las aristas son no negativos, asegurando entonces la optimalidad de los caminos encontrados por el algoritmo de Dijkstra. Las distancias en el grafo original pueden ser calculadas a partir de las distancias calculadas por el algoritmo de Dijkstra en el grafo modificado invirtiendo la transformación realizada en el grafo. Tipos Arista = REGISTRO o : NATURAL d : NATURAL peso : INT sig : NATURAL FIN LAristas = PUNTERO A Arista TGrafo = ARRAY [1..N] DE LAristas THv = ARRAY [1..N] DE ENTERO TVector = ARRAY [1..N] DE ENTERO TMatriz = ARRAY [1..N] DE TVector //suponemos ig>1 PROC Johnson (↓grafo: TGrafo; ↓ig: NATURAL; ↑ distancias: TMatriz ; ↑ previos: TMatriz) VARIABLES i : NATURAL p : LAristas min_caminos : THv aux_dist, aux_prev : TVector INICIO grafo[ig] ← nueva_arista(ig,1,0,NULO)

FIN

inc(ig) p ← grafo[ig] PARA i ← 2 HASTA ig-2 HACER p^.sig ← nueva_arista(ig,i,0,NULO) p ← p^.sig FIN BellmanFord(grafo,ig, min_caminos) PARA i ← 1 HASTA ig-1 HACER p ← grafo[i] MIENTRAS (p != NULO) HACER p^.peso ← p^.peso + min_caminos[p^.o] - min_caminos[p^.d] p ← p^.sig FIN FIN PARA i ← 1 HASTA ig-2 HACER Dijkstra(grafo,i, aux_dist,aux_prev) // devuelve los caminos mínimos desde el último nodo // a todos los demás previos[i] ← aux_prev; CalcularDistancias(grafo, previos, aux_dist,distancias); // este algoritmo realiza la transformación inversa a la // que habíamos hecho antes sobre los pesos, para obtener // las distancias reales. FIN

Las etapas del algoritmo de Johnson están descritas en la siguiente ilustración:

En la imagen de la izquierda tiene dos aristas negativas y no contiene ciclos negativos. En la segunda imagen se muestra el nuevo vértice q con peso 0 hacia todos los nodos. En la tercera imagen, se muestra el árbol de caminos mínimos calculado con el algoritmo de Bellman-Ford con q como vértice inicial, y los valores h(v) calculados para cada otro nodo como la longitud del camino más corto entre q y ese nodo. A modo de ilustración, en dicha imagen solo aparecen los caminos que se tomarían para determinar cada valor h(v). Nótese que todos estos valores h(v) no son positivos, porque q tiene una arista de unión con cada nodo de peso 0, y por tanto el camino más corto no puede ser mayor que ese valor. En la parte derecha se muestra el grafo modificado, hecho reemplazando el peso de cada arista w(u,v) por w(u,v) + h(u) – h(v). En este grafo modificado, todos los pesos de las aristas son positivos y el camino más corto entre dos nodos cualesquiera usa la misma secuencia de aristas que en el camino más corto entre los mismos dos nodos en el grafo original. El algoritmo concluye aplicando el algoritmo de Dijkstra para cada uno de los cuatro nodos originales en el grafo modificado (cuarta imagen).

Floyd-Warshall Es un algoritmo de análisis sobre grafos para encontrar el camino mínimo en grafos dirigidos ponderados. El algoritmo encuentra el camino entre todos los pares de vértices (All-pairs shortest path) en una única ejecución. El algoritmo de Floyd-Warshall es un ejemplo de programación dinámica.

Algoritmo: 1 /* Suponemos que la función pesoArista devuelve el coste del camino que va de i a j 2 (infinito si no existe). 3 También suponemos que n es el número de vértices y pesoArista(i,i) = 0 4 */ 5 6 int camino[][]; 7 /* Una matriz bidimensional. En cada paso del algoritmo, camino[i][j] es el camino mínimo 8 de i hasta j usando valores intermedios de (1..k-1). Cada camino[i] 9 [j] es inicializado a pesoArista(i,j) 10 */ 11 12 procedimiento FloydWarshall () 13 para k: = 0 hasta n − 1 15 para i: = 0 hasta n -1 16 para j: 0 hasta n -1 17 d[i][j] = mín ( d[i][j], d[i][k] + d[k][j]);

Respecto a ciclos negativos: Para que haya coherencia numérica, Floyd-Warshall supone que no hay ciclos negativos (de hecho, entre cualquier pareja de vértices que forme parte de un ciclo negativo, el camino mínimo no está bien definido porque el camino puede ser infinitamente pequeño). No obstante, si hay ciclos negativos, Floyd-Warshall puede ser usado para detectarlos. Si ejecutamos el algoritmo una vez más, algunos caminos pueden decrementarse pero no garantiza que, entre todos los vértices, caminos entre los cuales puedan ser infinitamente pequeños, el camino se reduzca. Si los números de la diagonal de la matriz de caminos son negativos , es condición necesaria y suficiente para que este vértice pertenezca a un ciclo negativo. Ejecución: Hallar el camino mínimo desde el vértice 3 hasta 4 en el grafo con la siguiente matriz de distancias:

Aplicamos el algoritmo de Floyd-Warshall, y para ello en cada iteración fijamos un vértice intermedio. 1ª Iteración: nodo intermedio = 1 La matriz es simétrica, por lo que solamente hará falta calcular el triangulo superior de las distancias. d23 = min(d23, d21 + d13) = 8 d24 = min(d24, d21 + d14) = 4 d25 = min(d25, d21 + d15) = 9 d26 = min(d26, d21 + d16) = d34 = min(d34, d31 + d14) = 6 d35 = min(d35, d31 + d15) = 7 d36 = min(d36, d31 + d16) = 1 d45 = min(d45, d41 + d15) = d46 = min(d46, d41 + d16) = 4 d56 = min(d56, d51 + d16) = La matriz de distancia después de esta iteración es:

2ª Iteración: nodo intermedio = 2 d13 = min(d13, d12 + d23) = 5 d14 = min(d14, d12 + d24) = 1 d15 = min(d15, d12 + d25) = 12 d16 = min(d16, d12 + d26) =

d34 = min(d34, d32 + d24) = 6 d35 = min(d35, d32 + d25) = 7 d36 = min(d36, d32 + d26) = 1 d45 = min(d45, d42 + d25) = 13 d46 = min(d46, d42 + d26) = 4 d56 = min(d56, d52 + d26) = La matriz de distancia después de esta iteración es:

3ª Iteración: nodo intermedio = 3 d12 = min(d12, d13 + d32) = 3 d14 = min(d14, d13 + d34) = 1 d15 = min(d15, d13 + d35) = 12 d16 = min(d16, d13 + d36) = 6 d24 = min(d24, d23 + d34) = 4 d25 = min(d25, d23 + d35) = 9 d26 = min(d26, d23 + d36) = 9 d45 = min(d45, d43 + d35) = 13 d46 = min(d46, d43 + d36) = 4 d56 = min(d56, d53 + d36) = 8 La matriz de distancia después de esta iteración es:

4ª Iteración: nodo intermedio = 4

d12 = min(d12, d14 + d42) = 3 d13 = min(d13, d14 + d43) = 5 d15 = min(d15, d14 + d45) = 12 d16 = min(d16, d14 + d46) = 5 d23 = min(d23, d24 + d43) = 8 d25 = min(d25, d24 + d45) = 9 d26 = min(d26, d24 + d46) = 8 d35 = min(d35, d34 + d45) = 7 d36 = min(d36, d34 + d46) = 1 d56 = min(d56, d54 + d46) = 8 La matriz de distancia después de esta iteración es:

5ª Iteración: nodo intermedio = 5 d12 = min(d12, d15 + d52) = 3 d13 = min(d13, d15 + d53) = 5 d14 = min(d14, d15 + d54) = 1 d16 = min(d16, d15 + d56) = 5 d23 = min(d23, d25 + d53) = 8 d24 = min(d24, d25 + d54) = 4 d26 = min(d26, d25 + d56) = 8 d34 = min(d34, d35 + d54) = 6 d36 = min(d36, d35 + d56) = 1 d46 = min(d46, d45 + d56) = 4 La matriz de distancia después de esta iteración es:

6ª Iteración: nodo intermedio = 6 d12 = min(d12, d16 + d62) = 3 d13 = min(d13, d16 + d63) = 5 d14 = min(d14, d16 + d64) = 1 d15 = min(d15, d16 + d65) = 12 d23 = min(d23, d26 + d63) = 8 d24 = min(d24, d26 + d64) = 4 d25 = min(d25, d26 + d65) = 9 d34 = min(d34, d36 + d64) = 5 d35 = min(d35, d36 + d65) = 7 d45 = min(d45, d46 + d65) = 12 La matriz de distancia después de esta iteración es:

Ya se han hecho todas las iteraciones posibles. Por tanto, el camino mínimo entre 2 vértices cualesquiera del grafo será el obtenido en la matriz final. En este caso, el camino mínimo entre 3 y 4 vale 5.

Heaps Es una estructura de Árbol con información perteneciente a un conjunto ordenado. Los montículos tienen la característica de que cada nodo padre tiene un valor mayor que el de todos sus nodos hijos. Un árbol cumple la condición de montículo si satisface dicha condición y además es un árbol binario completo.Un árbol binario es completo cuando todos los niveles están llenos, con la excepción del último que puede quedar exento de dicho cumplimiento. Ésta es la única restricción en los montículos. Ella implica que el mayor elemento (o el menor, dependiendo de la relación de orden escogida) está siempre en el nodo raíz. Debido a esto, los montículos se utilizan para implementar colas de prioridad, por la razón de que en una cola siempre se consulta el elemento de mayor valor, y esto conlleva la ventaja de que en los montículos dicho elemento está en la raíz. Otra ventaja que poseen los montículos es que su implementación usando

arrays es muy eficaz, por la sencillez de su codificación y liberación de memoria, ya que no hace falta utilizar punteros.No sólo existen montículos ordenados con el elemento de la raíz mayor que el de sus hijos, sino también en caso contrario que la raíz sea menor que sus progenitores. Todo depende de la ordenación con la que nos interese programar el montículo, que debe ser parámetro de los algoritmos de construcción y de manipulación de dicho montículo. La eficiencia de las operaciones en los montículos es crucial en diversos algoritmos de recorrido de grafos y de ordenamiento (Heapsort).

Heap de Fibonacci * ver documento anexo. Un Heap de Fibonacci es una colección de árboles que satisfacen la propiedad de Min-Heap, es decir, la clave de un hijo es siempre mayor o igual que la de su padre. Esto implica que la clave mínima está siempre en la raíz. Comparado con los Heaps binomiales, la estructura de un heap de Fibonacci es más flexible. Los árboles no tienen una forma predefinida y en un caso extremo el heap puede tener cada elemento en un árbol separado o en un único árbol de profundidad n. Esta flexibilidad permite que algunas operaciones puedan ser ejecutadas de una manera ‘perezosa’, posponiendo el trabajo para operaciones posteriores. Por ejemplo, la unión de dos Heaps se hace simplemente concatenando las dos listas de árboles, y la operación Decrementar clave a veces corta un nodo de su padre y forma un nuevo árbol. Sin embargo, se debe introducir algún orden para conseguir el tiempo de ejecución deseado. En concreto, el grado de los nodos(el número de hijos) se tiene que mantener bajo: cada nodo tiene un grado máximo de O(logn) y la talla de un subárbol cuya raíz tiene grado k es por lo menos Fk+2 , donde Fk es un número de Fibonacci (por eso el nombre que se les da a este tipo de heap. Esto se consigue con la regla de que podemos cortar como mucho un hijo de cada nodo no raíz. Cuando es cortado un segundo hijo, el nodo también necesita ser cortado de su padre y se convierte en la raíz de un nuevo árbol. El número de árboles se decrementa en la operación Borrar mínimo, donde los árboles están unidos entre sí. Como resultado de esta estructura, algunas operaciones pueden llevar mucho tiempo mientras que otras se hacen muy deprisa. En el análisis del coste de ejecución amortizado pretendemos que las operaciones muy rápidas tarden un poco más de lo que tardan. Este tiempo extra se resta después al tiempo de ejecución de operaciones más lentas. La cantidad de tiempo ahorrada para un uso posterior es medida por una función potencial. Esta función es:

Object 1

Donde t es el número de árboles en el Heap de Fibonacci, y m es el número de nodos marcados. Un nodo está marcado si al menos uno de sus hijos se cortó desde que el nodo se fue hecho hijo de otro nodo (todas las raíces están desmarcadas). Además, la raíz de cada árbol en un Heap tiene una unidad de tiempo almacenada. Esta unidad de tiempo puede ser usada más tarde para unir este árbol a otro con coste amortizado 0. Cada nodo marcado también tiene dos unidades de tiempo almacenadas. Una puede ser usada para cortar el nodo de su padre. Si esto sucede, el nodo se convierte en una raíz y la segunda unidad de tiempo se mantendrá almacenada como en cualquier otra raíz.

Related Documents

Analisis De Grafos
May 2020 10
Grafos
June 2020 16
Grafos
June 2020 9
Teoria De Grafos
June 2020 10
Apostila-grafos
May 2020 13
Grafos Definiciones
October 2019 18

More Documents from ""

April 2020 2
Analisis De Grafos
May 2020 10
Fibonacci Heap
May 2020 5
Document
August 2019 47
December 2019 34