Escolar Documentos
Profissional Documentos
Cultura Documentos
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.
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.
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.
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
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 en camino mas corto entre dos vértices u y v (con y sin peso).
Algoritmo de Kruskal
Pseudocodigo:
1 function Kruskal(G)
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 (disjoint-
set 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).
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:
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.
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.
Algoritmo:
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
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)
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
FIN
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]);
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.
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