El Algoritmo de Dijkstra o de Caminos mínimos
El algoritmo de Dijkstra (o de caminos mínimos) es un
algoritmo voraz para la determinación del camino más corto de un grafo.
Puede parecer una definición un tanto complicada con lo de
voraz y grafo, pero vamos a explicarlo paso a paso:
¿Qué es un grafo?
Un grafo es una colección de nodos o vértices unidos por
líneas o aristas. Se usa para representar relaciones binarias entre los
elementos.
Voy a saltarme las nociones matemáticas, dando nociones
simples para entender el problema en cuestión. Puedes encontrar más información
en aquí.
Imagina que los nodos son ciudades y que cada una de ellas
tiene un aeropuerto. Se puede ir de una ciudad a otra si la aerolínea ha creado
una ruta. Esa ruta tiene un coste, bien sea kilómetros o coste total de ir en
avión a esa ciudad.
Pues bien, esos datos se pueden simplificar con un grafo dirigido
ponderado, como el que se indica a continuación:
En el grafo, un nodo seria la ciudad, cada una de las
aristas las diferentes rutas disponibles y el valor de la arista el coste de ir
de una ciudad a otra.
El algoritmo de Dijkstra se basa en estas estructuras para
la resolución del problema del camino más corto de un nodo a otro. En el caso de
no estar ponderadas las aristas, se suele dar el valor de 1.
¿Qué es un algoritmo voraz?
El esquema voraz (greedy algorithms) se aplica a problemas
de optimización en los que la solución se puede construir paso a paso sin
necesidad de reconsiderar decisiones ya tomadas. Genéricamente el problema que
se puede resolver con este tipo de esquema es: encontrar un conjunto de
candidatos que constituya una solución y que optimice una función objetivo.
Este esquema se utiliza principalmente en problemas de planificación de tareas
y en problemas que se pueden modelar con grafos, en los que hay que realizar una
búsqueda, cálculo de recorridos u optimización de pesos, entre otras tareas.
Los problemas que se pueden resolver con este esquema
constan de n candidatos y se trata de encontrar una solución basada en hallar
un subconjunto de esos candidatos, o una secuencia ordenada de los mismos, de
manera que se optimice (maximice o minimice) una función objetivo. En este
esquema se trabaja por etapas, considerando la elección de un candidato en cada
etapa. Habrá que seleccionar en cada una el candidato más prometedor de los aún
disponibles y decidir si se incluye o no en la solución.
Si quieres profundizar más sobre los algoritmos voraces,
dispones de más información aquí.
¿Qué es el algoritmo de Dijkstra?
El algoritmo de Dijkstra, también llamado algoritmo de
caminos mínimos, es un algoritmo para la determinación del camino más corto
dado un vértice origen al resto de los vértices en un grafo con pesos en cada
arista. Su nombre se refiere a Edsger Dijkstra,
quien lo describió por primera vez en 1959.
La idea subyacente en este algoritmo consiste en ir
explorando todos los caminos más cortos que parten del vértice origen y que
llevan a todos los demás vértices; cuando se obtiene el camino más corto desde
el vértice origen, al resto de vértices que componen el grafo, el algoritmo se
detiene. El algoritmo es una especialización de la búsqueda de costo uniforme,
y como tal, no funciona en grafos con aristas de coste negativo (al elegir
siempre el nodo con distancia menor, pueden quedar excluidos de la búsqueda
nodos que en próximas iteraciones bajarían el costo general del camino al pasar
por una arista con costo negativo).
Este algoritmo utiliza dos conjuntos de nodos, S y C. S contiene los nodos
ya seleccionados y cuya distancia mínima al origen ya se conoce y C contiene los demás nodos, C = N \ S, aquellos cuya distancia
mínima al origen no se conoce todavía. Al inicio del algoritmo S sólo contiene el nodo origen y cuando
finaliza el algoritmo contiene todos los nodos del grafo y además se conocen
las longitudes mínimas desde el origen a cada uno de ellos. La función de
selección elegirá en cada paso el nodo de C
cuya distancia al origen sea mínima.
El algoritmo de Dijkstra utiliza la noción de camino especial. Un camino desde el nodo
origen hasta otro nodo es especial si todos los nodos intermedios del camino
pertenecen a S, es decir, se conoce
el camino mínimo desde el origen a cada uno de ellos. Hará falta un array, especial[], que en cada paso del
algoritmo contendrá la longitud del camino especial más corto (si el nodo está
en S), o el camino más corto
conocido (si el nodo está en C) que
va desde el origen hasta cada nodo del grafo. Cuando se va a añadir un nodo a S, el camino especial más corto hasta
ese nodo es también el más corto de todos los caminos posibles hasta él. Cuando
finaliza el algoritmo todos los nodos están en S y por lo tanto todos los caminos desde el origen son caminos
especiales.
Las longitudes o distancias mínimas que se esperan calcular
con el algoritmo estarán almacenadas en el array especial[].
Se supone que los nodos están numerados de $1$ a $n$, por lo
que N = {1, 2, ... , n},
y que el nodo 1 es el nodo origen.
También se supone que la función Distancia()
devolverá la distancia o coste entre los dos nodos que son sus argumentos. Si
existe una arista entre dichos nodos devolverá la etiqueta o peso asociado, en
caso de que no exista una arista devolverá un valor representativo o
suficientemente grande, por ejemplo ∞.
tipo VectorNat = matriz[O..n] de natural
fun Dijkstra (G = <N,A>: grafo):
VectorNat, VectorNat
var
especial, predecesor: VectorNat
C:
conjunto de nodos
fvar
C={2,3,...,n}
para i = 2 hasta n hacer
especial[i] = Distancia(1 ,i)
predecesor[i] = 1
fpara
mientras C contenga más de 1 nodo hacer
v = nodo en C que minimiza especial[v]
C = C\{v}
para cada w en C hacer
si especial[w] > especial[v] +
Distancia(v,w) entonces
especial[w] = especial[v] + Distancia(v,w)
predesecor[w] = v
fsi
fpara
fmientras
dev especial[],predecesor[]
ffun
Ejemplo de ejecución:
Si quieres profundizar más sobre los algoritmos voraces,
dispones de más información aquí,
El ejemplo del robot desplazándose por un circuito
Sea un robot R que
dispone de una batería de N unidades
de energía y se encuentra en un circuito por el que puede desplazarse. El
objetivo es que R se desplace desde
el punto en el que se encuentra hasta el punto de salida S del circuito, contando con que en el camino se puede encontrar
con obstáculos, O, que son
infranqueables. El paso por una casilla franqueable supone un gasto de energía
igual al valor que indica la casilla, que deberá ser mayor que 0. Se busca un
algoritmo que permita al robot llegar al punto S gastando el mínimo de energía.
El circuito se puede representar mediante una matriz de
dimensiones n x m en la que desde
cada elemento se puede acceder a un elemento adyacente con el consumo de
energía que indique la casilla. En el apartado 3.8 del texto base se puede ver
un ejemplo detallado de un circuito concreto. El esquema que se utilizará para
su resolución será el indicado en el texto base para este problema: esquema voraz, en particular la
solución basada en el algoritmo de Dijkstra.
Aplicación del algoritmo Dijkstra
El tablero o circuito se puede modelar como un grafo en el
que cada casilla es un nodo y el contenido de la casilla el coste energético de
acceder a dicho nodo por alguna de sus aristas incidentes. El grafo resultante
es dirigido y por lo tanto la matriz no es simétrica.
Para un tablero de 5 x 5, los nodos se han numerado del 1 al
25 empezando por la posición (1,1) del tablero, siguiendo por la (1,2), (1,3),...hasta
la (5,5). Dada una posición (i,j) del tablero, le corresponde el número de nodo
(i - 1) x 5 + j. En la posición del tablero en el que está el robot R, el coste
de acceder a ella desde un nodo que no sea un obstáculo es 0, ya que no se
indica otro posible coste, lo mismo ocurre con la posición S.
Con esta representación, el problema se reduce a encontrar
un camino de coste mínimo desde la posición en la que se encuentra el robot, R,
hasta la posición R. Este problema se puede solucionar utilizando el algoritmo
de Dijkstra para calcular la longitud del camino mínimo que va desde el origen
hasta el resto de los nodos del grafo. En este caso, el algoritmo Dijkstra se
detendrá una vez que el camino hasta el nodo S ya se haya calculado, obteniendo
así la ruta de coste mínimo en el camino de R a S.
El algoritmo de Dijkstra adaptado a este problema es el
siguiente:
tipo VectorNat = matriz[O..n] de natural
fun Dijkstra (G = <N,A>: grafo, R:
natural, S: natural): VectorNat, VectorNat
var
especial, predecesor: VectorNat
C:
conjunto de nodos
fvar
C={1,2,3,...,n} excepto R
para i = 1 hasta n y i ≠ R hacer
especial[i] = Distancia(R ,i)
predecesor[i] = R
fpara
mientras C contenga al nodo S hacer
v = nodo en C que minimiza especial[v]
C = C\{v}
si v ≠ S entonces
para cada w en C hacer
si especial[w] > especial[v] + Distancia(v,w) entonces
especial[w] = especial[v] + Distancia(v,w)
predesecor[w] = v
fsi
fpara
fsi
fmientras
dev especial[],predecesor[]
ffun
Para finalizar
Este ejemplo se ha desarrollado en Java para un trabajo de la asignatura de Programación y Estructuras De Datos Avanzadas del grado en Ingeniería Informática de la UNED.
Os adjunto el proyecto realizado en Java con la solución del
problema junto con la memoria que he entregado en su día en el siguiente enlace.
¡Nos vemos Más allá del 0 y del 1!
Atentamente:
Carlos Caride Santeiro
Comentarios
Publicar un comentario