Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Caminos Mínimos

Descargar como pdf o txt
Descargar como pdf o txt
Está en la página 1de 10

Caminos

mínimos

Algoritmo y
Estructura
de Datos II
Caminos mínimos en grafos

Problema de los caminos mínimos

Uno de los problemas más comunes en la teoría de grafos consiste en encontrar el camino
mínimo entre 2 vértices dados. Por ejemplo, si tenemos un grafo representando aeropuertos y
rutas de vuelo de una o varias compañías aéreas, podríamos usarlo para consultar cuál es la ruta
más económica para ir desde Buenos Aires a Hong Kong. O quizás deseamos encontrar la ruta
más corta, con menos aeropuertos intermedios, o con menos horas de vuelo. En cualquier caso,
debemos almacenar para cada tramo los pesos correspondientes a cada criterio (costo del billete,
distancia, tiempo de vuelo). Nótese que cada tramo es una arista del grafo.

La resolución al problema de los caminos mínimos tiene diferentes algoritmos de resolución y


diferentes complejidades, dependiendo de las características del grafo. La resolución es bastante
simple en el caso de los grafos no ponderados. Si las aristas tienen pesos positivos, su resolución
es algo más compleja. Para el caso más general, cuando las aristas tienen pesos positivos o
negativos, la resolución es aún más compleja. A continuación exploramos cada uno de estos
casos.

Problema del camino mínimo sin pesos

El problema del camino mínimo sin pesos con un único origen consiste en encontrar el camino
más corto, medido por el número de aristas, desde el vértice de origen a cualquier otro vértice del
grafo. Es importante resaltar que este problema se suele asociar a los grafos no ponderados, ya
que en este caso la longitud del camino entre dos vértices está dada por el número de aristas que
componen dicho camino.

-1-
V0 V1

V2 V3 V4

V5 V6

Figura 1. Grafo no ponderado

En el grafo de la Figura 1, asumimos que el vértice de origen O es V2. El algoritmo que resuelve el
problema de los caminos mínimos sin pesos, comienza encontrando el camino de longitud 0 al
nodo de origen. Por definición, el camino más corto desde O a V2 es un camino de longitud 0.
Ahora miramos los vértices a distancia 1 de O (V0 y V5), a continuación buscamos los vértices a
distancia 2 y así sucesivamente. Esta estrategia se llama búsqueda en anchura, o Breadth First
Search (BFS). En la publicación "Caminos mínimos sin pesos" podrás ver cómo funciona esta
estrategia para encontrar los caminos mínimos desde el vértice de origen a cada uno de los
vértices del grafo.
Nota que habrán algunos casos en el que existan vértices no alcanzables desde el vértice de
origen. En este caso, diremos que la distancia a dichos vértices es infinita. Por ejemplo, en la
Figura 1, si el vértice de origen es V4, entonces el vértice V6 se encuentra a distancia 1, el vértice
V5 se encuentra a distancia 2, y el resto de vértices están a distancia infinita, ya que no es posible
construir un camino entre V4 y dichos vértices.

Dv Dv+1
v w

Figura 2. Principio de funcionamiento

-2-
El principio de funcionamiento de dicho algoritmo está ejemplificado en la Figura 2. Nótese que
desde el vértice de origen O vamos construyendo un camino de distancia Dv hasta un nodo v.
Para todo nodo w adyacente a v, hay un camino de distancia Dv+1 desde O a w.

El algoritmo de búsqueda empieza asumiendo que Dw=, para todo vértice w diferente a O.
Luego el grafo se explora en anchura, y cada vez que se llega a un vértice w cuya distancia es 
se reduce su Dw, que pasa a valer Dv+1, donde Dv es el nodo que estamos explorando
actualmente. El alumno debiera notar que el hecho que el algoritmo finalice no garantiza que
todos los vértice del grafo finalizan con valores Dw diferentes a  Sólo aquellos vértices
alcanzables desde el vértice de origen tendrán un valor de Dw diferente a . Recordemos que un
vértice w es alcanzable desde un vértice v si existe un camino que lleva desde v a w.

Analicemos ahora qué estructuras de datos necesitamos para implementar el algoritmo descripto
anteriormente. El alumno debiera notar que existen dos acciones básicas a realizar:

- Necesitamos buscar repetidamente el vértice a procesar.

- Necesitamos comprobar rápidamente los vértices adyacentes al vértice actual.

Omitamos por un momento el primer problema, y enfoquemos nuestra atención en el segundo


problema, qué es más simple de resolver. Esto es así, ya que hemos visto que los grafos se
representan en general usando una lista o una matriz de adyacencia. En cualquier caso, los
vértices adyacentes se encuentran iterando sobre dicha estructura. En el caso de las listas de
adyacencia, simplemente accederemos a cada uno de los nodos de la lista enlazada para dicho
vértice. En el caso de las matrices de adyacencia, tendremos que recorrer toda la fila
correspondiente al vértice siendo procesado, para detectar aquellos vértices que son adyacentes
al actual (es decir, si el vértice siendo procesado es v, quiero detectar todos los vértices w tal que
m[v][w]=1). Obviamente, para realizar esta operación, es más eficiente emplear una lista de
adyacencia para implementar el grafo, ya que la matriz obliga a recorrer posiciones que no
necesariamente pertenecen a la lista de adyacencia del vértice siendo procesado. Nota además
que si usamos una lista de adyacencia, cada arista se procesará a lo sumo una vez, por lo que el
costo total de las iteraciones sobre las listas de adyacencia será de O(|E|).

Por ejemplo, si nos remitimos al grafo de la figura 1, observaremos que si el vértice de origen
fuese V2, entonces todas las aristas del grafo serían procesadas exactamente una vez, ya que
todos los vértices del grafo son alcanzables desde dicho vértice de origen. Por otra parte, si el

-3-
vértice de origen fuese V4, solo las aristas que conectan a los vértices V5 y V6 serían procesadas
una vez, ya que el resto de vértices no son alcanzables desde dicho vértice de origen.

Volvamos ahora al primer problema, que intenta encontrar rápidamente cuál es el siguiente vértice
a procesar. Si empleamos un vector conteniendo listas de adyacencia para cada vértice del grafo,
explorar el vector que la implementa buscando un vértice en particular tiene un costo medio de
O(|V|), que es el tamaño de dicho vector. Pero esta operación se hace O(|V|) veces, ya que para
cada vértice siendo procesado debemos decidir cual es el siguiente vértice a procesar. Esta
búsqueda secuencial sobre el vector es demasiada costosa. Para resolver este problema, notamos
que en lugar de considerar todos los vértices del grafo como potenciales candidatos a
ser el próximo vértice a procesar, podemos considerar solamente a aquellos vértices w que han
visto rebajado su Dw desde . Para poder procesar exclusivamente dichos vértices y no otros,
necesitamos almacenarlos en una estructura auxiliar. Debido a que deseamos procesar los
vértices en el orden en que su distancia deja de ser , parece apropiado usar una cola como
estructura auxiliar.

Usando una cola, el algoritmo comienza tomando el vértice de origen y poniéndolo en una cola
vacía. A continuación, tomamos el primer vértice de la cola, recorremos su lista de adyacencia, y
los ponemos en la cola. Todos estos vértices estarán a distancia Dv+1 respecto del vértice actual
v. El algoritmo continúa que la cola queda vacía. Nota que el uso de una cola posibilita procesar
primero todos aquellos vértices que están a distancia 1 del vértice de origen, luego aquellos
vértices que están a distancia 2, a continuación los vértices que están a distancia 3, etc. Recuerda
que cada vez que estamos procesando los vértices adyacentes al vértice actual, sólo
introduciremos en la cola los vértices w que ven decrementada su Dw desde . El uso de la cola
permite que el costo para elegir el siguiente vértice a procesar sea constante (O(1)), por lo que el
costo total de esta operación sobre todos los vértices del grafo será de O(|V|).

Con este análisis ya podemos saber cuál es el costo total del algoritmo, basado en las 2
operaciones descriptas anteriormente. La elección del siguiente vértice a procesar cuesta en total,
O(|V|), mientras que descubrir los vértices adyacentes al vértice siendo procesado tiene un costo
total de O(|E|). En otras palabras, el costo total del algoritmo, desde que comienza hasta que
procesa todos los vértices es de O(|V|+|E|).

-4-
Problema de los caminos mínimos con pesos positivos

El problema de los caminos mínimos con pesos positivos y origen único consiste en encontrar el
camino más corto desde el vértice origen O al resto de vértices del grafo, donde el peso o costo
de cada arista del grafo es un valor no negativo. El alumno debe recordar que en el caso de los
grafos ponderados, la longitud de un camino con pesos es la suma del costo de las aristas del
camino. Al restringir el problema a grafos con aristas de peso positivo, garantizamos que cualquier
camino del grafo tiene un costo positivo. Esto permite usar un algoritmo sencillo y eficiente para
encontrar todos los caminos mínimos desde un vértice de origen, que fue propuesto por el
científico holandés Edsger Dijkstra. Dijkstra es uno de los mayores contribuidores a las Ciencias
de la Computación. Entre sus contribuciones a la informática está el problema del camino más
corto, también conocido como el algoritmo de Dijkstra, la Notación polaca inversa y el relacionado
algoritmo shunting yard, el algoritmo del banquero y la construcción del semáforo para coordinar
múltiples procesadores y programas. Otro concepto debido a Dijkstra, en el campo de la
computación distribuida, es el de la auto-estabilización, una vía alternativa para garantizar la
confiabilidad del sistema.

Figura 3. Edsger Wybe Dijkstra

Fuente: http://es.wikipedia.org/wiki/Archivo:Edsger_Wybe_Dijkstra.jpg

El algoritmo de Dijkstra para la resolución del problema de los caminos mínimos con pesos
positivos es muy similar al algoritmo que resuelve el problema de los caminos mínimos sin pesos.
Este algoritmo debe resolver dos problemas principales:

-5-
- ¿Cómo ajustamos el valor de Dw de cada vértice w del grafo?

- ¿Cómo buscamos el siguiente vértice v a ser procesado?

En el primer problema, debemos notar que a diferencia del problema del camino mínimo sin
pesos, cada vértice w puede ser visitado varias veces, y su Dw se puede actualizar múltiples
veces. Tomemos el ejemplo de la Figura 4. Como vemos en este grafo, el vértice u ya ha sido
procesado, Du tiene un valor de 2, y al procesar este vértice hemos decrementado Dw a 8, ya que
existe una arista de peso 6 que conduce a w desde u. En el algoritmo de los caminos mínimos sin
pesos, el vértice w iría a la cola, y Dw no volvería a ser modificado. Sin embargo, cuando existen
pesos, podría ocurrir que al procesar otro vértice como v, podamos encontrar un camino de menor
peso que conduzca hasta w. En el gráfico de la Figura 4, observamos que existe un camino de
costo 3 que lleva desde el origen hasta v, y una arista de costo 2 que lleva desde v hasta w, por lo
que podríamos encontrar un camino de menor costo que lleva desde el vértice de origen hasta w,
pasando por v. En este ejemplo, ese camino tendría costo 5. Basados en este ejemplo, podemos
resolver el problema de saber cómo ajustar el valor de Dw de cada vértice del grafo, haciendo:

Dw=Dv+cv,w

si Dv+cv,w es menor que el valor actual de Dw, para cada vértice w adyacente al vértice v siendo
procesado actualmente.

3
8
v 2 w
0
O
6

u 2

Figura 4. Actualización de Dw

-6-
El segundo problema plantea la elección del siguiente vértice a ser procesado. En el caso del
algoritmo que resuelve el problema de los caminos mínimos sin pesos, usamos una cola como
estructura auxiliar para determinar el orden en que los vértices debían ser procesados. En este
caso, debemos prestar atención a un teorema que dice que debemos posar nuestra atención en
vértices no visitados que minimicen el valor Dv para cada vértice v del grafo, con el propósito de
garantizar que el algoritmo produce correctamente los caminos mínimos. En otras palabras, el
siguiente vértice a procesar en cada paso del algoritmo, debiera ser aquel vértice v aun no
procesado cuyo Dv sea el menor entre todos los vértices no procesados. Dicho esto, podemos
comprobar rápidamente que una cola no es una estructura adecuada para este propósito. Para
ver esto, supongamos, usando la Figura 4, que luego de procesar u ponemos a w en la cola
existente. Actualmente estamos procesando todos los vértices de distancia 2 desde el origen. El
orden en que los insertamos en la cola podría estar relacionado con el costo de las aristas de la
lista de adyacencia de u. Supongamos que existe una arista de costo 5 que lleva desde u a un
vértice z. Entonces z se insertaría antes que w en dicha cola. Sin embargo, al procesar v,
cambiaríamos el valor de Dw de 8 a 5. Esto significaría que ahora debiéramos procesar w antes
que z. Sin embargo, z fue insertado antes que w en la cola, y por lo tanto será procesado antes.
Con esto vemos que una cola no es apropiada, sin embargo, una cola de prioridad ordenada por
Dw sí sería de utilidad, y cumpliría exactamente con el propósito deseado. En el ejemplo que
veníamos siguiendo, al procesar el vértice v insertaríamos en la cola de prioridad el par (w,5).
Nota que en la estructura existiría ya una instancia (w,8). El hecho que existan dos instancias
simplemente indicaría que existen al menos 2 caminos que llevan desde el vértice de origen al
vértice w, uno de costo 5 y el otro de costo 8. Eventualmente, sacaríamos de la cola de prioridad
el par (w,5), procederíamos a procesar el vértice w, y dejaríamos 5 como valor final de Dw.
Eventualmente, cuando sacásemos de la cola de prioridad el par (w,8), simplemente lo
descartaríamos ya que el vértice w figuraría en nuestra lista de vértices procesados.

Notemos que en la elección del nuevo vértice v a ser procesado, eliminamos repetidamente el
menor elemento de la cola de prioridad hasta sacar un vértice no visitado. Ya que la cola de
prioridad contendrá a lo sumo tantos elementos como aristas hayan en el grafo, realizaremos |E|
inserciones y eliminaciones, donde cada una de estas operaciones cuesta O(log N), siendo
N=O(|E|). Por lo tanto, el costo total de la elección del nuevo vértice será de O(|E| log |E|), y ese
es a su vez el costo total del algoritmo, ya que al igual que en el caso del algoritmo que encuentra
todos los caminos mínimos sin pesos, cuesta O(|E|) el determinar todos los vértices adyacentes a
cada vértice siendo procesado. Por lo tanto, el costo total estaría dado por O(|E| log |E| + |E|), que

-7-
es lo mismo que O(|E| log |E|), por propiedades de la notación O.

En la publicación "Caminos mínimos con pesos positivos" observamos un ejemplo de cómo el


algoritmo de Dijkstra se comporta para un grafo de ejemplo.

Problema de los caminos mínimos con pesos

El problema de los caminos mínimos con pesos y origen único consiste en encontrar el camino
más corto desde el vértice origen O al resto de vértices del grafo, donde el peso de cada arista del
grafo puede ser un valor positivo, negativo, o cero. Claramente, este caso es el mas general de
todos los problemas que resuelven el problema de los caminos mínimos. Si restringimos el
problema a grafos conteniendo aristas de peso positivo, estamos frente al problema resuelto en el
punto anterior. Si restringimos el problema aún más, y consideramos solamente grafos que
contengan aristas de peso 1, entonces estaremos frente al problema resuelto en la sección
Problema del camino mínimo sin pesos".
En este punto, el alumno debiera notar que el hecho de que existan aristas de peso negativo hace
que el algoritmo de Dijkstra no funcione correctamente, en aquellos casos donde existan en el
grafo ciclos cuyo costo sea negativo. Esto es simple de ver, ya que de existir tal ciclo, uno podría
dar vueltas indefinidamente para alcanzar un camino a otro vértice de menor costo. Para ver esto
más claramente, observemos el grafo de la Figura 5, donde vemos que el ciclo V 1, V3, V4, V1 tiene
un costo negativo de -5. En este caso, supongamos que deseamos encontrar el camino mínimo
que lleva desde V3 a V4. El camino directo entre ambos vértices tiene costo 2. Sin embargo,
observamos que existe un camino más corto V3, V4 , V1 , V3 , V4 con costo -3. Pero claramente,
podemos dar vueltas arbitrariamente al ciclo, es decir, construir un camino V3, V4 , (V1 , V3 , V4)*,
donde (V1 , V3 , V4)* implica que se dan varias vueltas al ciclo V1 , V3 , V4, V1, y hacer que el costo
final del camino sea menor, por lo que concluimos que el camino más corto que lleva del vértice
V3 al vértice V4 está indefinido.

Este problema no se restringe a vértices en el ciclo. Por ejemplo, si deseamos encontrar el camino
más corto que lleva desde el vértice V2 al vértice V5, concluiríamos de igual manera de que dicho
camino es indefinido, ya que existen maneras de entrar y salir del ciclo. Por ejemplo, uno podría
elegir el siguiente camino V2, V0, (V3, V4 , V1)*, V6 , V5, donde (V3 , V4 , V1)* implica que se dan
varias vueltas al ciclo V3 , V4, V1, V3.

-8-
2
V0 V1
4
1 3 -10

V2 2 V3 2 V4

8 4
5 6

1
V5 V6

Figura 5. Problemas con ciclos de costo negativo

Por otra parte, es importante destacar que las aristas de costo negativo no representan un
problema en aquellos grafos en los que no existen ciclos negativos. En dichos grafos, el algoritmo
de Dijkstra funciona correctamente.

El algoritmo que resuelve el problema general de los caminos mínimos, es una combinación de los
algoritmos vistos anteriormente para resolver los caminos mínimos sin pesos y con pesos
positivos. Este algoritmo sufre un incremento drástico en su tiempo de ejecución, ya que cada
vértice v puede ser procesado varias veces durante la ejecución del algoritmo, haciendo que su
valor de Dv pueda ser modificado varias veces también. En este algoritmo, utilizamos nuevamente
una cola, en lugar de una cola de prioridad, pero insertando Dv+cv,w como medida de la distancia.
Debemos tener en cuenta que cuando visitemos un vértice v por i-ésima vez el valor de Dv es la
longitud del camino más corto formado por, a lo sumo, i aristas. Lo interesante de este algoritmo
es que permite detectar ciclos de costo negativo (en cuyo caso, como ya dijimos, las distancias a
aquellos vértices que son alcanzables a través de un ciclo negativo estarán indefinidas). En
particular, si no hay ciclos de costo negativo, decimos que un vértice puede salir de la cola a lo
sumo |V| veces. Si sale más veces, podemos detener el algoritmo, y anunciar que hemos
detectado un ciclo de costo negativo. El hecho de tener que contemplar que un vértice pueda ser
procesado hasta |V| veces, determina que el costo del algoritmo sea O(|E||V|), ya que tendremos
que realizar |V| veces una operación de costo O(|E|) (el descubrir los vértices adyacentes al
vértice siendo procesado). En la publicación "Caminos mínimos con pesos" observamos como se
ejecuta este algoritmo sobre un grafo ponderado con aristas negativas y positivas.

-9-

También podría gustarte