Está en la página 1de 11

Algoritmos de Teoria de grafos M. C.

Justino Ramiréz Ortegón

ALGORITMOS DE
TEORIA DE GRAFOS

M. C. JUSTINO RAMIRÉZ ORTEGÓN

1 de 11
GRAFOS
Sea V el conjunto de vértices y A el conjunto de aristas de un grafo. En nuestro caso no vamos a
permitir más que una arista por cada par de vértices, de esta forma AVxV. A estará formado por pares
(v1, v2) donde v1 y v2 son vértices de V. Si el orden de los pares importa, el grafo es dirigido, si el orden no
importa, el grafo es no dirigido.
Cada arista puede tener asociado un coste, en este caso se dice que el grafo es valorado. En el grafo
podemos tener vértices y costes de distinto tipo, por tanto vamos a tener dos parámetros.

GRAFOS
Parametros
Tipos
vertices: conjunto finito
valores:
operaciones
:valores
_+_:valores valores  valores
Ecuaciones
x
+x=
Operaciones
gVacio:grafo
ponarista: grafo vertice vertice valores  grafo
quitaarista: grafo vertice vertice  grafo
coste_arista: grafo vertice vertice  valores
hay_arista: grafo vertice vertice  booleano
sucesores: grafo vertice  CONJUNTO(vértices)
predecesores: grafo vertice  CONJUNTO(vértices)
ecuaciones
u1=u2  v1=v2  ponarista(ponarista(g, u1, v1, c1), u2, v2, c2) = ponarista(g, u2, v2, c2)
u1u2  v1v2 
ponarista(ponarista(g, u1, v1, c1), u2, v2, c2) = ponarista(ponarista(g, u2, v2, c2), u1, v1, c1)
quitaarista(gvacio, u, v) = gvacio
u1=u2  v1=v2  quitaarista(ponarista(g, u1, v1, c1), u2, v2) = quitaarista(g, u2, v2)
u1u2  v1v2 
quitaarista(ponarista(g, u1, v1, c1), u2, v2) = ponarista(quitaarista(g, u2, v2), u1, v1, c1)
coste_arista(gvacio, u, v) = 
u1=u2  v1=v2  coste_arista(ponarista(g, u1, v1, c1), u2, v2) = c1
u1u2  v1v2  coste_arista(ponarista(g, u1, v1, c1), u2, v2) = coste_arista(g, u2, v2)
hayarista(g, u, v) = ¬(coste_arista(g, u, v) = 
sucesores(gvacio, u) = cjto_vacio
u1=u2  sucesores(ponarista(g, u1, v1, c1), u2) = añadir(v1, sucesores(g, u2))
u1u2  sucesores(ponarista(g, u1, v1, c1), u2) = sucesores(g, u2)
predecesores(gvacio, v) = cjto_vacio
v1=v2  predecesores(ponarista(g, u1, v1, c1), v2) = añadir(u1, predecesores(g, v2))
v1v2  predecesores(ponarista(g, u1, v1, c1), v2) = predecesores(g, v2)

Implementación.
En la práctica la especificación anterior sirve de poco para la impleentación. Tenemos dos maneras de
implementar las aristas de un grafo. Si consideramos que los nodos de un grafo se puede representar por
un conjunto de la forma V={1, 2, 3,..., n}, las aristas las podemos representar de los modos siguiente:
a) Matriz de costes, o matriz de adyacencia.
Se utiliza una matriz M de n filas y n columnas donde cada entrada M(i, j) es el coste de la
arista que parte de i hasta j. Si no hay arista entre i y j entonces M(i, j) = .
De esta forma, el acceso a cada arista tiene coste constante, (1).
Para buscar los vértices adyacentes, tanto sucesores o predecesores, consiste en recorrer una
fila o una columna, por tanto tendrá coste lineal, (n).
Si el grafo es denso, habrá muchas aristas, entonces la matriz anterior tendrá pocos infinitos y
es una buena representación.
Algoritmos de Teoria de grafos M. C. Justino Ramiréz Ortegón

b) Lista de adyacencia.
Utilizamos un vector g[1..n] en los que cada g(i) es una lista de los sucesores de i.
Para buscar los sucesores de i tan solo hay que recorrer la lista g(i), coste lineal, (n).
Para buscar una arista también basta con recorrer la lista, coste lineal, (n).

CAMINOS DE COSTE MÍNIMO.


Partimos de un grafo dirigido dado por su matriz de adyacencia.
G(i, j) = c Si hay una arista de i a j con coste c y ij.
G(i, j) =  Si no hay tal arista y ij.
G(i, i) = 0.

Tenemos que calcular el camino con coste mínimo para ir de i a j. Si la solución es  entonces no hay
camino posible entre i y j.

En primer lugar veamos la idea en la que se basa el algoritmo.

Vamos a denotar como R(i, j) = {i, x1, x2, ..., xh, j} a un camino que nos lleva de i a j.
Si el camino mínimo que lleva de i a j pasa por k y es R(i, j) = R(i, k) ++ R(k, j) entonces los caminos
R(i, k) y R(k, j), que llevan de i a k y de k a j también son mínimos. De no ser así sería porque hay dos
caminos R’(i, k) y R’(k, j) que nos lleva de i a k y de k a j cuyo coste es menor que los originales.
Entonces el coste de R(i, j) es mayor que la suma de ambos costes y por tanto mayor, R(i, j) no podría ser
el camino de coste mínimo.

Además es fácil comprobar que el camino mínimo no puede contener ciclos, de ser así los podríamos
eliminar obteniendo un camino con coste menor.

Consideramos Ck(i, j)  coste del camino mínimo que puede pasar por los nodos 1, 2,..., k. La solución
del problema será C(i, j) = Cn(i, j).
Si el camino mínimo Ck(i, j) no pasa por k  Ck(i, j) = Ck-1(i, j).
Si el camino mínimo Ck(i, j) pasa por k  Ck(i, j) = Ck-1(i, k) + Ck-1(k, j).
Por tanto, para el coste del camino mínimo que lleva de i a j que puede pasar por los nodos 1, 2, ..., k
es: Ck(i, j) = min{ Ck-1(i, j), Ck-1(i, k) + Ck-1(k, j)}.

El caso básico es no pasar por ningún nodo, por tanto C0(i, j) debe ser el coste de la arista entre i y j o
 si no existe esta arista, en cualquier caso C0(i, j) = G(i, j).

Observemos que el coste en espacio para almacenar esta información es (n3), se trata de información
que depende de 3 elementos, el vértice origen, el destino y el vértice más alto por el que se puede pasar.
Sin embargo este coste se puede optimizar teniendo en cuenta como se calculan las entradas de las
matrices:
C0(i, i) = G(i, i) = 0
Ck(i, i) = min{ Ck-1(i, i), Ck-1(i, k) + Ck-1(k, j)}=0
Por tanto para los elementos de la diagonal principal siempre valen 0. No cambian.

Ck(i, k) = min{ Ck-1(i, k), Ck-1(i, k) + Ck-1(k, k)} = min{ Ck-1(i, k), Ck-1(i, k)} = Ck-1(i, k)
Este tipo de elementos tampoco cambian.

Ck(k, j) = min{ Ck-1(k, j), Ck-1(k, k) + Ck-1(k, j)} = min{ Ck-1(k, j), Ck-1(k, j)} = Ck-1(k, j)
Este tipo de elementos tampoco cambian.

El resto de elementos es
Ck(i, j) = min{ Ck-1(i, j), Ck-1(i, k) + Ck-1(k, j)}.
Ck(r, s) = min{ Ck-1(r, s), Ck-1(r, k) + Ck-1(k, s)}.

Lo que vamos a hacer es tener una sola matriz C y sobre ella ir calculando sucesivamente las Ck,
vamos a comprobar que se obtiene el resultado requerido, la operación que se realiza es la siguiente:
C=G
Para k = 1 hasta n hacer
Para i=1 hasta n hacer
Para j=1 hasta n hacer

3 de 11
C(i, j) = min{ C (i, j), C(i, k) + C(k, j)};
Fpara;
Fpara;
fpara

Falta comprobar que si antes de la ejecución del bucle para i se verifica que la asignación C = C k-1, tras
la ejecución del bucle se verifica que C = Ck.
 Para i = j
C(i, i) = min{C (i, i), C(i, i) + C(i, i)} = C(i, i), No varía
Efectivamente Ck(i, i) = 0 y se mantiene constante para todo k.
 Para j = k; ik
Ck(i, k) = min{Ck-1(i, k), Ck-1(i, k) + C k-1(k, k)} = min{ Ck-1(i, k), Ck-1(i, k)}= Ck-1(i, k) = C(i, k)
 Para i = k; jk
Ck(k, j) = min{Ck-1(k, j), Ck-1(k, k) + C k-1(k, j)} = min{Ck-1(k, j), C k-1(k, j)}= C k-1(k, j) = C(k, j)
 Para i, j, k distintos
Ck(i, j) = min{Ck-1(i, j), Ck-1(i, k) + C k-1(k, j)} = min{Ck-1(i, j), C(i, k) + C(k, j)}
El problema se presenta aquí, puede ocurrir que al cambiar el valor de C(i, j) para actualizarlo a
Ck(i, j) si necesitamos posteriormente Ck-1(i, j) no disponemos de él. Pero esto no ocurre nunca,
porque para calcular Ck(i, j) necesitamos los valores de Ck-1(i, j), Ck-1(i, k), Ck-1(k, j). Los dos
últimos no varían y el primero es el que vamos a sustituir, pero solo lo utilizamos ahora y al
calcular el resto de las entradas no lo necesitamos.

Por tanto podemos sustituir todos los elementos de la matriz conservando toda la información que
necesitamos.

Basado en esta idea podemos ver el algoritmo de Floyd.

Algoritmo de Floyd.
Proc floyd(G[1..n, 1..n], C[1..n, 1..n] , P[1..n, 1..n]);
C:=G; P:=[0];
Para k=1 hasta n hacer
Para i=1 hasta n hacer
Para j=1 hasta n hacer
Si C(i, k)+C(k, j) < C(i, j) entonces
C(i, j) = C(i, k) + C(k, j)
P(i, j) = k
Fsi;
Fpara;
Fpara;
Fpara;
Fproc;

Lo único que puede quedar un poco oscuro es el objetivo de la matriz P. Esta matriz es la que se
utiliza para conocer el camino concreto. Para hacerlo se actúa así, si queremos conocer el camino mínimo
entre i y j, calculamos las matrices C y P por el algoritmo anterior, a continuación tomamos P(i, j) = k.
Esto significa que el camino mínimo que lleva de i a j pasa por k. Necesitamos conocer el camino mínimo
de i a k y de k a j, pero como ya hemos calculado las matrices C y P, consultamos P(i, k) = r y P(k, j) =s.
A continuación consultamos P(i, r), P(r, k), P(k, s) y P(s, j) ... etc.
Ojo, si C(i, j) = , no hay camino de i a j.

Este algoritmo tiene un coste (n3) en tiempo y (2n2) = (n2) en espacio.

Algoritmo de Dijkstra.
Este algoritmo se utiliza para calcular el camino mínimo con origen fijo. Como antes, consideramos
que el conjunto de vértices es V={1, 2,..., n} y por comodidad consideramos que el origen del camino es
1.
El algoritmo de Dijkstra sigue la estrategia devoradora.
Consideramos el conjunto de candidatos que son los vértices a los que deseamos llegar, inicialmente
este conjunto es {2,..., n}.

La estrategia que vamos a seguir es elegir el nodo más cercano a los elegidos. Vamos a tener un vector
D[1..n] donde D[1]=0 y D[i] = “distancia a los elegidos” para i=2,3, ..., n.
Algoritmos de Teoria de grafos M. C. Justino Ramiréz Ortegón

El candidato elegido es aquel v para el cual D[v] es mínimo cuando v no está elegido. A continuación
se deben actualizar las distancias de los vértices no elegidos.
Proc dijkstra(G[1..n, 1..n], D[1..n], P[1..n]);
D[1]:=0; P:=[1];
C:={2, 3, ..., n}; Esta es la inicialización
Para i:=2 hasta n
D[i]:=G[1, i];
Fpara;
Repetir n-2 veces
V:= vértice en C con D[v] mínimo;
C:=C – {v};
Para cada wC hacer Cada vez añadimos un vértice a 1
los elegidos. 10 50
Si D[v] + G[v, w] < D[w] entonces
D[w] := D[v] + G[v, w];
P[w] := v 5 100 30 2
Fsi;
Fpara; 20
5
Frepetir;
Fproc; 4 3
50

Vamos a ver el funcionamiento del algoritmo con el seguiente ejemplo:


C D P 1
Paso 0 {2, 3, 4, 5} [0, 50, 30, 100, 10] [1, 1, 1, 1, 1] 10
Paso 1 {2, 3, 4} [0, 50, 30, 20, 10] [1, 1, 1, 5, 1]
5 30 2
Paso 2 {2, 3} [0, 40, 30, 20, 10] [1, 4, 1, 5, 1]
Paso 3 {2} [0, 35, 30, 20, 10] [1, 3, 1, 5, 1]
5

Tenemos que demostrar la corrección del algoritmo. 4 3


Para ello vamos a denotar por S={1, 2, 3, ..., n}-C, el conjunto de vértices no elegidos.
Teorema.
1)  iS, i1 se tiene que D[i] es el coste mínimo de ir desde 1 a i.
2)  iS, (iC) se tiene que D[i] es el coste del camino ESPECIAL1 mínimo que solo pasa por nodos de
S.
Demostración.
Vamos a demostrar ambos puntos por inducción en el número de pasadas por el bucle.
Caso base, 0 pasadas.
En la inicialización C = {2, 3, ..., n}  S = {1}
D[i] = G[1, i] i
1) se verifica trivialmente porque S={1} y D[1] = G[1,1] = 0.
2) Es cierto porque para todo iC, D[i] = G[1, i]. Como en S solo está el 1, los caminos
especiales solo pueden pasar por el 1 y por i, por tanto solo hay un camino mínimo especial
y es la arista (1, i).

Supongamos por hipótesis de inducción que son ciertos 1) y 2). Vamos a ver que pasa al ejecutar otra
vez el bucle.
Tomamos vC tal que D[v]= min {D[w] con vC}
C = C –{v}
S = S  {v}
Para cada w
1) Sea iS.
Si iv entonces por H.I. D[i] es el coste mínimo de ir desde 1 a i.
Si i=v entonces por H.I. D[v] es el coste mínimo de ir desde 1 a i por nodos de S.
Tenemos que ver que al añadir v a S y quitarlo de C, D[v] es el camino mínimo. Supongamos
que no fuese así, esto sería por que hay un camino mínimo que no es especial y que lleva de 1 a
i. Como el camino no es especial, hay un nodo x por el que pasa el camino y que no es de S y
todos los anteriores nodos por los que se pasa son de S.
El coste de este camino se puede calcular así d(1, v) = d(1, x) + d(x, v), donde el camino de 1 a x
si es especial. Se tiene que D[v]>d(1, v) = d(1, x) + d(x, v)  d(1,x) = D[x] por H.I., pero xS 
xC  D[v]>D[x]D[v] porque en S se van introduciendo los elementos con D[v] mínimo.
Absurdo.

1
Un camino ESPECIAL de i a j con iS y jS es un camino que lleva de i a j pero pasando solo por nodos
de S
5 de 11
2) Sea iC.
Al añadir un nuevo elemento a S pueden ocurrir dos cosas, que el camino especial para i siga
siendo el mismo, porque al añadir v a S solo aparezcan caminos especiales para i con coste
mayor, o que el camino especial cambie, en cuyo caso este nuevo camino especial debe pasar por
v, de no ser así, ese camino especial ya habría sido elegido en algún paso anterior del bucle.
a) si el camino especial mínimo no cambia, se da que D[i] = min{D[i], D[v]+G[v, i]} = D[i] y
por hipótesis de inducción este el coste del camino especial mínimo que solo pasa por nodos
de S.
b) si el camino especial mínimo cambia, tenemos dos posibilidades, que v sea el último nodo
justo antes de i o que no lo sea
i) si v es el último nodo antes de i entonces D[i] = min{D[i], D[v]+G[v, i]} = D[v]+G[v, i]
ii) si v no es el último nodo antes de i entonces sea x el último nodo antes de i, se tiene que
d(1, i) = d(1, v)+d(v, x)+G[x, i], pero entonces
Si x es el último nodo antes de i que está en S entonces D[x] es el camino mínimo que
lleva de 1 a x, y x ya estaba en S antes de v  D[i] ya es el coste de un camino especial
mínimo.

Eficiencia.
Implementación del conjunto de candidatos.
Dado que se manejan mínimos y se toman ordenadamente, podemos distinguir 2 posibilidades:
- Implementarlo mediante una matiz de adyacencia y un vector de booleanos, C[1..n] donde
C[i]= True significa que i es uno de los vértices escogidos.
El coste de inicialización es (n).
El cálculo del mínimo tiene coste O(n) y la actualización de los D[i], O(n). Como esta tarea
hay que repetirla n-2 veces el coste del bucle es (n2).
El coste total del algoritmo es (n2).
- Implementarlo con un montículo de mínimos según D, con una lista de adyacencia.
Coste de la inicialización, con un montículo (n).
Elección del mínimo y eliminación O(log n), repetido n-2 veces  (n log n)
Actualización de D(i), mediante flotar, O(log n), repetir por cada arista a  (a log n)
El coste total del bucle será ((n+a) log n)
Suponemos que na, cada vértice tiene al menos una arista, si el grafo es denso, a se aproxima
a n2 y el coste es aproximadamente (n2 log n), pero si el grafo es disperso, a se aproxima a n
y el coste será (n log n).

Vamos a ver una comparativa entre los algoritmos de Dijkstra y Floyd. Ojo, Dijkstra solo calcual la
distancia desde un vértice al resto, mientras que Floyd calcula todas las distancias entre todos los vértices.
Para poderlos comparar debemos calcular todas las distancias mediante Dijkstra mediante llamadas
sucesivas al algorítmo
Dijkstra Floyd
Denso (n ) n llamadas  (n )
2 3
(n3)
Disperso (n log n) n llamadas (n2 log n)

ALGORITMOS PARA EL CÁLCULO DE ÁRBOLES DE RECUBRIMIENTO DE COSTE


MÍNIMO.
Para el cálculo de árboles de recubrimiento de coste mínimo vamos a ver dos algorítmos el de Prim y
el de Kruskal.
Consideramos G = (V, A) un grafo no dirigido conexo. Es esencial que el grafo sea conexo, de no
serlo no puede haber un árbol de recubrimiento de coste mínimo. En lo sucesivo el árbol de recubrimiento
de coste mínimo lo abreviaremos por ARCM.
Un árbol es un grafo conexo y sin ciclos.

Proposición.
Un grafo conexo y sin ciclos, un árbol, con n vértices tiene n-1 aristas.
Demostración.
Por inducción en el número de vértices.
|V|=1. Como en V solo hay un vértice no podemos añadir ninguna arista, porque en caso de añadirlo
siempre se formarán ciclos. El número de aristas es 0 = 1-1
Algoritmos de Teoria de grafos M. C. Justino Ramiréz Ortegón

Supongamos que si |V| = n-1 y G =(V, A) es un grafo conexo sin ciclos  |A| = (n-1)-1, para n1
Sea V un conjunto de vértices tal que |V| = n>1 y G = (V, A) es un grafo conexo sin ciclos, sea xV,
tal que x tiene una sola arista e en A. Construimos V’ = V – {x}, A’ = A – {e}. Tenemos que G’=(V’, A’)
es un grafo conexo, sin ciclos y |V’| = n-1
 Sean v, w dos vértices de G’. Entonces v y w son dos vértices de G, como G es conexo existe un
camino en G que lleva de v a w. Tenemos que ver que este camino no utiliza la arista a.
Efectivamente, porque la arista e tiene como un de sus extremos x y de x no sale ninguna otra
arista, esto significa que cualquier camino que utilice la arista e o bien parte de x o bien llega a x.
Como ni v ni w pueden ser el vértice x, porque xG’, el camino que lleva de v a w no puede
utilizar la arista e, entonces todas las aristas que utiliza este camino están en G’ = G-{e}  existe
un camino que lleva de v a w en G’.
 En G’ no puede haber ciclos porque no los había en G.

Por hipótesis de inducción, como |V’| = n-1 y es grafo conexo y sin ciclos |A’| = n-2  |A| = n-1.
El único detalle es demostrar que en G siempre existe un vértice del que parte una única arista, si |V|
>1 y G es grafo conexo y sin ciclos. Supongamos que todos los vértices de V tienen al menos dos aristas
distintas. Como el grafo es conexo, podemos construir un camino que pasa por todos los vértices de G,
por la definición de grafo conexo. Sea el camino {x1, x2, ..., xn-1, xn} xiV todos distintos, como en xn hay
al menos dos aristas distintas debe haber otra arista (xn, y) con yxn-1. Podemos construir el camino {x1,
x2, ..., xn-1, xn, y}, pero yV  existe un k{1,..., n-2} tal que y = xk. Entonces tenemos el camino {x1, x2,
..., xk-1, xk, xk+1,...,xn-1, xn, xk}  existe un ciclo {xk, xk+1,...,xn-1, xn, xk}. Absurdo.

Cada grafo se divide en todas sus componentes conexas. Dado G = (V, A), (V’, T’) es una componente
conexa de G si se verifican tres cosas:
1) V’V
2) T’A
3) (V’, A’) es arbol conexo.

DEFINICIÓN.
Un BOSQUE DE RECUBRIMIENTO B de G = (V, T) es un conjunto B = {(V1, T1), (V2, T2), ..., (Vk, Tk)}
k k
tal que cada (Vi, Ti) es una componente conexa de G , V  Vi y T  Ti .
i 1 i 1
Dado un bosque de recubrimiento, se dice que es PROMETEDOR si se puede completar de forma que
lleve a un árbol de recubrimiento de coste mínimo.

Teorema.
Sea (V, B) un bosque de recubrimiento, (V, B) = (V1, T1)(V2, T2)  ... (Vk, Tk), donde (Vi, Ti) son
árboles de recubrimiento para i=1,2,...,k. Sea i0{1, 2, ..., k}. Sea eB, e = (u,v) que sale de  Vi 0

, Ti 0 ,
u  Vi 0 y v  Vi 0 
 
por tanto  o .
u  V y v  V 
 i0 i0 
Si e tiene coste mínimo  (V, B{e}) es prometedor.
Demostración.
 e no crea ciclos en (V, B{e}).
u  Vi 0 y v  Vj0 
Como e sale de  Vi 0

, Ti 0 , entonces existe j0{1, 2, ..., k} tal que i0j0 y

 o


u  V y v  V 
 j0 i0 
Supongamos que e crea un ciclo en (V, B{e}) entonces en este ciclo interviene e, que parte cuyos
extremos están en componentes conexas distintas, entonces tiene que haber algún camino que una las
dos componentes conexas y que no utiliza la arista e. Entonces las dos componentes conexas forman
una sola.
 Vamos a ver que (V, B) está contenido en (V, T) un ARCM.
Si eT entonces B{e}T  (V, B{e}) es prometedor
7 de 11
Si eT entonces como T tiene n-1 aristas  T{e} tiene n aristas  no es un árbol  hay un ciclo
que incluye a esa arista  tiene que existir otra arista d que salga de Vi 0 (o Vj0 ) que forma parte
del mismo ciclo. Además coste(e)coste(d), porque es la arista de coste mínimo que sale de
Vi 0 (o Vj0 ) . Construimos T’ = (T{e}) – {d}. En (V, T’) ya no hay ciclos, es un árbol además
tenemos que el coste de T’ es c(T’) = c(T{e}) – {d}) = c(T) + c(e) – c(d)  c(T), pero T es ARCM 
c(T)c(T’)  c(T) = c(T’)  T’ = (T{e}) – {d} es ARCM.
Falta comprobar que B{e} T’ = (T{e}) – {d}
- e(T{e}) – {d}
- e’B  e’ d porque d sale de Vi 0 (o Vj0 )  no puede estar en B porque todas las
aristas de B quedan dentro de la misma componente conexa. Por tanto e’T–
{d}(T{e}) – {d}

Algoritmo general.
El esquema básico que siguen los algoritmos para el cálculo de ARCM, es el siguiente:
(V, B) = (V, )
mientras ncc(B)>1 {ncc(B) es el número de componentes conexas de B}
(V, B) = (V1, T1)  (V2, T2)  ...  (Vk, Tk)
escoger i: 1ik
e = arista de coste mínimo que sale de (Vi, Ti)
Unir Vi y Vj con e
fmientras
A partir de aquí los algoritmos concretos introducen detalles sobre como hacer las tareas que aquí
quedan de un modo abstracto.

Vamos a comprobar que el caso base es prometedor. Como el grafo es conexo, el (V, T) ARCM existe.
T  (V, )(V, T)  (V, ) es prometedor.
Habiendo comprobado el caso base y utilizando el teorema anterior tenemos demostrada la corrección
del algoritmo anterior y con ello la de todos los algoritmos derivados de él, en particular los dos que
vamos a estudiar el de Prim y el de Kruskal.

Algoritmo de Prim.
Transformamos el “escoger i: 1ik” en tomar i=1. De esta forma tenemos un árbol que va a ir
creciendo, al que le vamos a ir añadiendo nodos. El resto de las componentes conexas son vértices
aislados.
Se este modo podemos dividir los nodos en alegidos y sueltos. La elección de la arista es equivalente
ahora a la elección del nodo suelto más cercano a los elegidos. Como vemos este algoritmo es muy
similar al de Dijkstra.
Func prim_abstracto(G = (V, A)) dev T:cjto de aristas;
T := ; Observese que cada vuelta del bucle añade uno y solo uno de
B := {1}; los vértices no elegidos, como queremos que al final haya n
Mientras |B|  n hacer nodos elegidos debemos ejecutar el bucle n-1 veces. Se
e = (u, v) tal que uB, vB y coste(e) sea mínimo puede sustituir el bucle mientras por un repetir n-1 veces ...
T = T {e}
B = B {v}
fmientras
ffunc

Vamos a detallar un poco más este algoritmo.


Func prim_concreto(G[1..n, 1..n]) dev T:cjto;
T := ;
Para i:=2 hasta n hacer
Mascercano(i):=1 {el nodo “elegido” más cercano es el 1}
Dist_min(i):=G(i, 1) {distancia al nodo elegido más cercano}
Fpara;
repetir n-1 veces hacer
Min:=;
Para j:=2 hasta n hacer {Vamos a buscar el nodo más cercano a los elegidos}
Si dist_min(j)0  dist_min(j)min entonces {Si dist_min(j)<0, el nodo ya esta elegido}
Min:=dist_min(j);
k:=j;
Fsi;
Fpara;
T = T {(k, mascercano(k))}; {añadimos el nodo a los elegidos}
Algoritmos de Teoria de grafos M. C. Justino Ramiréz Ortegón

dist_min(k):= -1 {utilizamos dist_min(k) = -1 para indicar que nodo está elegido}


para j:=2 hasta n hacer {Actualizamos las distancias mínimas a los elegidos}
si G(k, j) < dist_min(j) entonces {Si la distancia del nodo no elegido al recientemente elegido es
menor que la distancia mínima hasta ahora}
dist_min(j) := G(k, j); {Actualizamos los datos}
mascercano(j):=k;
fsi;
fpara;
frepetir
ffunc

El coste de este algoritmo en tiempo es (n2).


El coste de este algoritmo en espacio adicional es (n).

Si utilizamos montículos, de aristas, produce un coste (a log n) donde a es el número de aristas.

Algoritmo de Kruskal.
El algoritmo de Kruskal parte también del algoritmo general, pero a diferencia del de Prim que hace
crecer un árbol y las demás componentes conexas son unitarias, en Kruskal escogemos la arista de coste
mínimo de todas las existentes, no solo las que salen de una componente concreta.

Examinemos como trabaja el algoritmo de Kruskal. Se escoge una arista e=(u, v) con coste mínimo.
Deben de saber a que componente conexa pertenece u y a que componente pertenece v, comp(u) y
comp(v) respectivamente.
 Si comp(u)comp(v) podemos unir ambas componentes en una sola utilizando la arista e.
 Si comp(u)=comp(v) no debemos añadir ninguna arista porque como u y v están en la misma
componente conexa, al añadir una nueva arista se formaría un ciclo.

Dado que se debe escoger la arista de coste mínimo, conviene ordenar las aristas según su coste.
Vamos a ver el algorítmo de Kruskal:
Func kruskal(({1,2,...,n}, A)) dev T:cjto de aristas
Ordenar(A);
T:=
Inicializar ECC con n conjuntos unitarios; {ECC es la estructura de componentes conexas}
Mientras |T|<n-1 hacer
(u, v) := arista de coste mínimo;
A := A – {(u, v)};
Si buscar(u)buscar(v) entonces
T:=T{(u, v)}
unir(u, v);
Fsi;
Fmientras;
Ffunc;

Podemos ver que la estructura de datos que vamos a manejar para almacenar las componentes conexas
debe tener al menos estás dos operaciones:
- buscar(u) que devuelve la componente conexa a la que pertenece un vértice
- unir(u, v) que debe unir las componentes conexas a las que pertenecen u y v.

Si definimos la relación R tal que uRv  u y v están en la misma componente conexa, tenemos que la
relación R es de equivalencia. Así tenemos que las dos operaciones anteriores equivalen a calcular la clase
a la que pertenece un vértice y a la unión de clases respectivamente. Por esta razón vamos a utilizar una
estructura que, según el autor, se denomina Partición, o conjuntos disjuntos o relación de equivalencia.
Quedan bastantes detalles a decidir para la implementación, como por ejemplo cual va a ser la
representación de cada clase. Dado que cada elemento está en una y solo en una de las clases, podemos
tomar como representante cualquiera de los elementos de la clase. Para unificar criterios vamos a utilizar
como representante de la clase al menos elemento de la clase.
Hemos de tener en cuenta a la hora de establecer la implementación que la utilización de esa
estructura optimice el algoritmo con el que lo vamos a utilizar.

Una primera aproximación a la implementación sería representar a las clases de equivalencia como un
vector Cjto[1..n], de forma que Cjto[i] = j significa que el elemento i pertenece a la clase de equivalencia
representada por j. Con esta implementación, las operaciones requeridas quedan del modo siguiente:
Fun buscar(u) dev v v:=cjto(u)

9 de 11
Ffunc; Proc unir(u, v);
Complejidad (1) i:=min(u,v);
j:=max(u,v);
para k=1 hasta n hacer
si cjto(k)=j entonces
cjto(k):=i;
fpara;
Fproc;
Complejidad (n)

Si se realiza n secuencias de operaciones buscar y unir, tal y como ocurre en el algoritmo de Kruskall,
el coste será (n2).

El coste se debe principalmente al coste de la operación unir. Si mejoramos la eficiencia de la unión,


sin empeorar demasiado la búsqueda, podemos mejorar la secuencia.
Para mejorarlo vamos a utilizar la siguiente representación:

si i  j, i y j pertenecen a la misma componente conexa


Cjtoi  j 
si i  j, i es la cabeza de una componente conexa
Esta representación construye un árbol en el que están todos los vértices que forman parte de la misma
componente conexa. De esta forma para unir dos componentes conexas tan solo hay que variar la cabeza
de una de ellas para que apunte a la otra.
Fun buscar2(u) dev v Proc unir2(u, v);
v:=u; Si u<v entonces
mientras cjto[v]v hacer Cjto(v):=u
v:=cjto(v); Else
fmientras; Cjto(u):=v
Ffunc; Fsi;
Complejidad (altura(árbol)) Fproc;
En el caso peor el árbol será degenerado y su Complejidad (n)
altura es n. En este caso el coste sigue siendo
cuadrático.

Para mejorarlo podemos unir las componentes conexas no


teniendo en cuenta su valor si no su tamaño, de esta forma, si
añadimos a un árbol otro de distinta altura, la altura resultante
será el máximo de las dos. Si unimos dos árboles de la misma s
altura, la altura del árbol resultante solo aumenta en una
t s+1t
unidad. s<t
Para poder implementar esta estrategia vamos a
añadir un vector que lleve el tamaño del árbol para
poderlos comparar de forma eficiente.
Proc unir3(u, v);
Si tam(u)<tam(v) entonces
Cjto(u):=v; t t
Si no
t+1
Si tam(u) = tam(v) entonces
Cjto(u):=v;
tam(u) := tam(u)+1
Fsi;
Fproc;
Complejidad (1)
El coste de buscar tiene complejidad (altura(árbol)), pero ahora la altura del árbol es siempre menor
o igual que el log n. El coste de la secuencia de operaciones es (n log n).
Ahora el algoritmo de Kruskall tiene el siguiente coste
Ordenar: (a log a) = (a log n)
Inicialización: (n)
Bucle principal (n log n)
Como na entonces el coste del algoritmo es (a log n).
Algoritmos de Teoria de grafos M. C. Justino Ramiréz Ortegón

11 de 11

También podría gustarte