Documentos de Académico
Documentos de Profesional
Documentos de Cultura
Un grafo es un objeto matemtico que se utiliza para representar circuitos, redes, etc. Los grafos
son muy utilizados en computacin, ya que permiten resolver problemas muy complejos.
Imaginemos que disponemos de una serie de ciudades y de carreteras que las unen. De cada
ciudad saldrn varias carreteras, por lo que para ir de una ciudad a otra se podrn tomar diversos
caminos. Cada carretera tendr un coste asociado (por ejemplo, la longitud de la misma). Gracias a
la representacin por grafos podremos elegir el camino ms corto que conecta dos ciudades,
determinar si es posible llegar de una ciudad a otra, si desde cualquier ciudad existe un camino
que llegue a cualquier otra, etc.
El estudio de grafos es una rama de la algoritmia muy importante. Estudiaremos primero sus
rasgos generales y sus recorridos fundamentales, para tener una buena base que permita
comprender los algoritmos que se pueden aplicar.
Glosario
Un grafo consta de vrtices (o nodos) y aristas. Los vrtices son objetos que contienen informacin
y las aristas son conexiones entre vrtices. Para representarlos, se suelen utilizar puntos para los
vrtices y lneas para las conexiones, aunque hay que recordar siempre que la definicin de un
grafo no depende de su representacin.
Un camino entre dos vrtices es una lista de vrtices en la que dos elementos sucesivos estn
conectados por una arista del grafo. As, el camino AJLOE es un camino que comienza en el vrtice
A y pasa por los vrtices J,L y O (en ese orden) y al final va del O al E. El grafo ser conexo si existe
un camino desde cualquier nodo del grafo hasta cualquier otro. Si no es conexo constar de
varias componentes conexas.
Un camino simple es un camino desde un nodo a otro en el que ningn nodo se repite (no se pasa
dos veces). Si el camino simple tiene como primer y ltimo elemento al mismo nodo se
denomina ciclo. Cuando el grafo no tiene ciclos tenemos un rbol (ver rboles). Varios rboles
independientes forman un bosque. Un rbol de expansin de un grafo es una reduccin del grafo
en el que solo entran a formar parte el nmero mnimo de aristas que forman un rbol y conectan
a todos los nodos.
Segn el nmero de aristas que contiene, un grafo es completo si cuenta con todas las aristas
posibles (es decir, todos los nodos estn conectados con todos), disperso si tiene relativamente
pocas aristas y denso si le faltan pocas para ser completo.
Las aristas son la mayor parte de las veces bidireccionales, es decir, si una arista conecta dos nodos
A y B se puede recorrer tanto en sentido hacia B como en sentido hacia A: estos son
llamados grafos no dirigidos. Sin embargo, en ocasiones tenemos que las uniones son
unidireccionales. Estas uniones se suelen dibujar con una flecha y definen un grafo dirigido.
Cuando las aristas llevan un coste asociado (un entero al que se denomina peso) el grafo
es ponderado. Una red es un grafo dirigido y ponderado.
Representacin de grafos
Una caracterstica especial en los grafos es que podemos representarlos utilizando dos estructuras
de datos distintas. En los algoritmos que se aplican sobre ellos veremos que adoptarn tiempos
distintos dependiendo de la forma de representacin elegida. En particular, los tiempos de
ejecucin variarn en funcin del nmero de vrtices y el de aristas, por lo que la utilizacin de
una representacin u otra depender en gran medida de si el grafo es denso o disperso.
Para nombrar los nodos utilizaremos letras maysculas, aunque en el cdigo deberemos hacer
corresponder cada nodo con un entero entre 1 y V (nmero de vrtices) de cara a la manipulacin
de los mismos.
int V,A;
int a[maxV][maxV];
void inicializar()
int i,x,y,p;
char v1,v2;
// Leer V y A
memset(a,0,sizeof(a));
scanf("%c %c %d\n",&v1,&v2,&p);
x=v1-'A'; y=v2-'A';
a[x][y]=p; a[y][x]=p;
}
}
En esta implementacin se ha supuesto que los vrtices se nombran con una letra mayscula y no
hay errores en la entrada. Evidentemente, cada problema tendr una forma de entrada distinta y
la inicializacin ser conveniente adaptarla a cada situacin. En todo caso, esta operacin es
sencilla si el nmero de nodos es pequeo. Si, por el contrario, la entrada fuese muy grande se
pueden almacenar los nombres de nodos en un rbol binario de bsqueda o utilizar una tabla de
dispersin, asignando un entero a cada nodo, que ser el utilizado en la matriz de adyacencia.
Como se puede apreciar, la matriz de adyacencia siempre ocupa un espacio de V*V, es decir,
depende solamente del nmero de nodos y no del de aristas, por lo que ser til para representar
grafos densos.
Otra forma de representar un grafo es por medio de listas que definen las aristas que conectan los
nodos. Lo que se hace es definir una lista enlazada para cada nodo, que contendr los nodos a los
cuales es posible acceder. Es decir, un nodo A tendr una lista enlazada asociada en la que
aparecer un elemento con una referencia al nodo B si A y B tienen una arista que los une.
Obviamente, si el grafo es no dirigido, en la lista enlazada de B aparecer la correspondiente
referencia al nodo A.
Las listas de adyacencia sern estructuras que contendrn un valor entero (el nmero que
identifica al nodo destino), as como otro entero que indica el coste en el caso de que el grafo sea
ponderado. En el ejemplo se ha utilizado un nodo z ficticio en la cola (ver listas, apartado
cabeceras y centinelas).
struct nodo
int v;
int p;
nodo *sig;
};
void inicializar()
{
int i,x,y,peso;
char v1,v2;
z->sig=z;
a[i]=z;
scanf("%c %c %d\n",&v1,&v2,&peso);
x=v1-'A'; y=v2-'A';
En este caso el espacio ocupado es O(V + A), muy distinto del necesario en la matriz de adyacencia,
que era de O(V2). La representacin por listas de adyacencia, por tanto, ser ms adecuada para
grafos dispersos.
Hay que tener en cuenta un aspecto importante y es que la implementacin con listas enlazadas
determina fuertemente el tratamiento del grafo posterior. Como se puede ver en el cdigo, los
nodos se van aadiendo a las listas segn se leen las aristas, por lo que nos encontramos que un
mismo grafo con un orden distinto de las aristas en la entrada producir listas de adyacencia
diferentes y por ello el orden en que los nodos se procesen variar. Una consecuencia de esto es
que si un problema tiene varias soluciones la primera que se encuentre depender de la entrada
dada. Podra presentarse el caso de tener varias soluciones y tener que mostrarlas siguiendo un
determinado orden. Ante una situacin as podra ser muy conveniente modificar la forma de
meter los nodos en la lista (por ejemplo, hacerlo al final y no al principio, o incluso insertarlo en
una posicin adecuada), de manera que el algoritmo mismo diera las soluciones ya ordenadas.
Exploracin de grafos
A la hora de explorar un grafo, nos encontramos con dos mtodos distintos. Ambos conducen al
mismo destino (la exploracin de todos los vrtices o hasta que se encuentra uno determinado), si
bien el orden en que stos son "visitados" decide radicalmente el tiempo de ejecucin de un
algoritmo, como se ver posteriormente.
En primer lugar, una forma sencilla de recorrer los vrtices es mediante una funcin recursiva, lo
que se denomina bsqueda en profundidad. La sustitucin de la recursin (cuya base es la
estructura de datos pila) por una cola nos proporciona el segundo mtodo de bsqueda o
recorrido, la bsqueda en amplitud o anchura.
Suponiendo que el orden en que estn almacenados los nodos en la estructura de datos
correspondiente es A-B-C-D-E-F... (el orden alfabtico), tenemos que el orden que seguira el
recorrido en profundidad sera el siguiente:
A-B-E-I-F-C-G-J-K-H-D
A-B-C-D-E-G-H-I-J-K-F
Es decir, en el primer caso se exploran primero los verdes y luego los marrones, pasando primero
por los de mayor intensidad de color. En el segundo caso se exploran primero los verdes, despus
los rojos, los naranjas y, por ltimo, el rosa.
En estos ejemplos hay que tener en cuenta que es fundamental el orden en que los nodos estn
almacenados en las estructuras de datos. Si, por ejemplo, el nodo D estuviera antes que el C, en la
bsqueda en profundidad se tomara primero la rama del D (con lo que el ltimo en visitarse sera
el C), y en la bsqueda en anchura se explorara antes el H que el G.
Bsqueda en profundidad
Se implementa de forma recursiva, aunque tambin puede realizarse con una pila. Se utiliza un
array val para almacenar el orden en que fueron explorados los vrtices. Para ello se incrementa
una variable global id (inicializada a 0) cada vez que se visita un nuevo vrtice y se almacena id en
la entrada del array val correspondiente al vrtice que se est explorando.
int id=0;
int val[V];
void buscar()
int k;
val[k]=0;
if (val[k]==0) visitar(k);
int t;
val[k]=++id;
if (val[t->v]==0) visitar(t->v);
El resultado es que el array val contendr en su i-sima entrada el orden en el que el vrtice i-
simo fue explorado. Es decir, si tenemos un grafo con cuatro nodos y fueron explorados en el
orden 3-1-2-4, el array val quedar como sigue:
val[3]=1; // etc.
val[4]=4;
Una modificacin que puede resultar especialmente til es la creacin de un array "inverso" al
array val que contenga los datos anteriores "al revs". Esto es, un array en el que la entrada i-
sima contiene el vrtice que se explor en i-simo lugar. Basta modificar la lnea
val[k]=++id;
sustituyndola por
val[++id]=k;
Para el orden de exploracin de ejemplo anterior los valores seran los siguientes:
val[1]=3;
val[2]=1;
val[3]=2;
val[4]=4;
encolar(&cola,k);
while (!vacia(cola))
desencolar(&cola,&k);
val[k]=++id;
if (val[t->v]==0)
encolar(&cola,t->v);
val[t->v]=-1;
Definiciones Bsicas
Ejemplos:
V = {v1,v2,v3,v4,v5} V = {v1,v2,v3,v4}
E = { {v1,v2}, {v1,v3}, {v1,v5}, {v2,v3}, {v3,v4}, E = { (v1,v2), (v2,v2), (v2,v3), (v3,v1), (v3,v4),
{v4,v5} } (v4,v3) }
Adems de esto, los grafos pueden ser extendidos mediante la adicin de rtulos (labels) a los
arcos. Estos rtulos pueden representar costos, longitudes, distancias, pesos, etc.
Matriz de adyacencia
Un grafo se puede representar mediante una matriz A tal que A[i,j]=1 si hay un arco que
conecta vi con vj, y 0 si no. La matriz de adyacencia de un grafo no dirigido es simtrica.
Una matriz de adyacencia permite determinar si dos vrtices estn conectados o no en tiempo
constante, pero requieren O(n2) bits de memoria. Esto puede ser demasiado para muchos grafos
que aparecen en aplicaciones reales, en donde |E|<<n2. Otro problema es que se requiere
tiempo O(n) para encontrar la lista de vecinos de un vrtice dado.
Listas de adyacencia
Esta representacin consiste en almacenar, para cada nodo, la lista de los nodos adyacentes a l.
Para el segundo ejemplo anterior,
v1: v2
v2: v2, v3
v3: v1, v4
v4: v3
Esto utiliza espacio O(|E|) y permite acceso eficiente a los vecinos, pero no hay acceso aleatorio a
los arcos.
Un grafo es conexo si desde cualquier vrtice existe un camino hasta cualquier otro vrtice del
grafo.
En muchas aplicaciones es necesario visitar todos los vrtices del grafo a partir de un nodo dado.
Algunas aplicaciones son:
Encontrar ciclos
A medida que recorremos el grafo, iremos numerando correlativamente los nodos encontrados
(1,2,...). Suponemos que todos estos nmeros son cero inicialmente, y utilizamos un contador
global n, tambin inicializado en cero.
El siguiente algoritmo en seudo-cdigo muestra cmo se puede hacer este tipo de recorrido
recursivamente:
++n;
DFN[v]=n;
Para hacer un recorrido en profundidad a partir del nodo v, utilizamos el siguiente programa
principal:
n=0;
for(todo w)
DFN[w]=0;
DFS(v);
Si hubiera ms de una componente conexa, esto no llegara a todos los nodos. Para esto podemos
hacer:
n=0;
for(todo w)
DFN[w]=0;
++ncc;
DFS(v);
n=0;
for(todo w)
DFN[w]=0;
while(!pila.estaVacia())
v=pila.desapilar();
++n;
DFN[v]=n;
pila.apilar(w);
La implementacin es similar a la de DFS, pero se utiliza una cola en lugar de una pila.
El resultado es que los nodos se visitan en orden creciente en relacin a su distancia al nodo
origen.
rboles cobertores
Todo recorrido de un grafo conexo genera un rbol cobertor, consistente del conjunto de los arcos
utilizados para llegar por primera vez a cada nodo.
Para un grafo dado pueden existir muchos rboles cobertores. Si introducimos un concepto de
"peso" (o "costo") sobre los arcos, es interesante tratar de encontrar un rbol cobertor que tenga
costo mnimo.
En esta seccin veremos dos algoritmos para encontrar un rbol cobertor mnimo para un grafo no
dirigido dado, conexo y con costos asociados a los arcos. El costo de un rbol es la suma de los
costos de sus arcos.
Algoritmo de Kruskal
Este es un algoritmo del tipo "avaro" ("greedy"). Comienza inicialmente con un grafo que contiene
slo los nodos del grafo original, sin arcos. Luego, en cada iteracin, se agrega al grafo el arco ms
barato que no introduzca un ciclo. El proceso termina cuando el grafo est completamente
conectado.
En este caso, afortunadamente, se puede demostrar que el mtodo "avaro" logra siempre
encontrar el ptimo global, por lo cual un rbol cobertor encontrado por esta va est garantizado
que es un rbol cobertor mnimo.
Una forma de ver este algoritmo es diciendo que al principio cada nodo constituye su propia
componente conexa, aislado de todos los dems nodos. Durante el proceso de construccin del
rbol, se agrega un arco slo si sus dos extremos se encuentran en componentes conexas
distintas, y luego de agregarlo esas dos componentes conexas se fusionan en una sola.
Para la operatoria con componentes conexas supondremos que cada componente conexa se
identifica mediante un representante cannico (el "lider" del conjunto), y que se dispone de las
siguientes operaciones:
if(Find(v)!=Find(w))
Agregar e al rbol;
Union(Find(v), Find(w));
}
El tiempo que demora este algoritmo est dominado por lo que demora la ordenacin de los
arcos. Si |V|=n y |E|=m, el tiempo es O(m log m) ms lo que demora
realizar moperaciones Find ms n operaciones Union.
Es posible implementar Union-Find de modo que las operaciones Union demoran tiempo
constante, y las operaciones Find un tiempo casi constante. Ms precisamente, el costo
amortizado de un Find est acotado por log* n, donde log* n es una funcin definida como el
nmero de veces que es necesario tomar el logaritmo de un nmero para que el resultado sea
menor que 1.
Por lo tanto, el costo total es O(m log m) o, lo que es lo mismo, O(m log n) (por qu?).
Ejemplo:
Lema
Sea V' subconjunto propio de V, y sea e={v,w} un arco de costo mnimo tal que v est
en V' y w est en V-V'. Entonces existe un rbol cobertor mnimo que incluye a e.
Este lema permite muchas estrategias distintas para escoger los arcos del rbol. Veamos por
ejemplo la siguiente:
Algoritmo de Prim
Comenzamos con el arco ms barato, y marcamos sus dos extremos como "alcanzables". Luego, a
cada paso, intentamos extender nuestro conjunto de nodos alcanzables agregando el arco ms
barato que tenga uno de sus extremos dentro del conjunto alcanzable y el otro fuera de l.
Agregar e al rbol;
Agregar e al rbol;
Para implementar este algoritmo eficientemente, podemos mantener una tabla donde, para cada
nodo de V-A, almacenamos el costo del arco ms barato que lo conecta al conjunto A. Estos costos
pueden cambiar en cada iteracin.
Si se organiza la tabla como una cola de prioridad, el tiempo total es O(m log n). Si se deja la tabla
desordenada y se busca linealmente en cada iteracin, el costo es O(n2). Esto ltimo es mejor que
lo anterior si el grafo es denso.
En este problema los rtulos de los arcos se interpretan como distancias. La distancia (o largo) de
un camino es la suma de los largos de los arcos que lo componen.
La idea del algoritmo es mantener un conjunto A de nodos "alcanzables" desde el nodo origen e ir
extendiendo este conjunto en cada iteracin.
Los nodos alcanzables son aquellos para los cuales ya se ha encontrado su camino ptimo desde el
nodo origen. Para esos nodos su distancia ptima al origen es conocida. Inicialmente A={s}.
Para los nodos que no estn en A se puede conocer el camino ptimo desde s que pasa slo por
nodos de A. Esto es, caminos en que todos los nodos intermedios son nodos de A. Llamemos a
esto su camino ptimo tentativo.
En cada iteracin, el algoritmo encuentra el nodo que no est en A y cuyo camino ptimo
tentativo tiene largo mnimo. Este nodo se agrega a A y su camino ptimo tentativo se convierte
en su camino ptimo. Luego se actualizan los caminos ptimos tentativos para los dems nodos.
El algoritmo es el siguiente:
A={s};
D[s]=0;
while(A!=V)
Agregar v a A;
D[w]=min(D[w],D[v]+cost(v,w));
Implementaciones:
Usando una cola de prioridad para la tabla D el tiempo es O(m log n).
Ejemplo:
Para aplicar este algoritmo, los nodos se numeran arbitrariamente 1,2,...n. Al comenzar la
iteracin k-sima se supone que una matriz D[i,j] contiene la distancia mnima entre i y jmedida a
travs de caminos que pasen slo por nodos intermedios de numeracin <k.
Estas distancias se comparan con las que se obtendran si se pasara una vez por el nodo k, y si de
esta manera se obtendra un camino ms corto entonces se prefiere este nuevo camino, de lo
contrario nos quedamos con el nodo antiguo.
Al terminar esta iteracin, las distancias calculadas ahora incluyen la posibilidad de pasar por
nodos intermedios de numeracin <=k, con lo cual estamos listos para ir a la iteracin siguiente.
Para inicializar la matriz de distancias, se utilizan las distancias obtenidas a travs de un arco
directo entre los pares de nodos (o infinito si no existe tal arco). La distancia inicial entre un nodo y
s mismo es cero.
for(1<=i,j<=n)
for(1<=i<=n)
D[i,i]=0;
for(k=1,...,n)
for(1<=i,j<=n)
D[i,j]=min(D[i,j], D[i,k]+D[k,j]);
Dada la matriz de adyacencia de un grafo (con D[i,j]=1 si hay un arco entre i y j, y 0 si no), resulta
til calcular la matriz de conectividad, en que el casillero respectivo contiene un 1 si y slo si existe
un camino (de largo >=0) entre i y j. Esta matriz se llama tambin la cerradura transitiva de la
matriz original.
Luego sustituir cada valor infinito por 0 y cada valor no infinito por 1.
Mejor todava, podramos modificar el algoritmo de Floyd para que vaya calculando con ceros y
unos directamente, usando las correspondencias siguientes:
for(1<=i,j<=n)
D[i,j]=adyacente(i,j); // 1 si existe, 0 si no
for(1<=i<=n)
D[i,i]=1;
for(k=1,...,n)
for(1<=i,j<=n)
Si lo que queremos es obtener el camino ms corto entre todas las pareja posibles, lo primero que
se nos puede ocurrir es utilizar el algoritmo de Dijkstra para cada vrtice. Sin embargo, podemos
encontrar algoritmos ms eficientes para este caso.
Este algoritmo se basa en lo siguiente: supongamos que tenemos la forma de llegar del
vrtice i al j de forma ptima contando con k-1 puntos intermedios. Ahora queremos encontrar la
distancia ms corta agregando un k-simo punto intermedio. Existen dos casos: podemos ir
de i a k y despus a j, o mantener la distancia de i a j que ya tenamos. En ambos estamos
considerando los k -1 puntos como intermedios. Al hacer que k tome el valor de todos los vrtices,
obtenemos la distancia ms corta entre todas las parejas.
Visto como formula, donde dk(i,j) es la distancia mnima para ir de i a j con k puntos, y p(i, j) es el
peso de la arista de i a j, tenemos que:
Si adems de la distancia, queremos encontrar la trayectoria, podemos hacer algo similar a lo que
hicimos con Dijkstra en el problema anterior. La funcin para imprimir esta trayectoria sera la
misma. Tambin podemos encontrar las aristas maxi-min y mini-max utilizando un enfoque
parecido al de Dijkstra.
Problema ejemplo
Risk
Problema
Risk es un juego de mesa en el cual varios jugadores intentan conquistar el mundo. El tablero
consiste en un mapa del mundo dividido en pases hipotticos. Durante el turno de un jugador, el
ejrcito de una nacin puede atacar a un pas con el que comparta frontera. Despus de
conquistar el pas, el ejrcito puede ocuparlo.
Es comn que un jugador empiece una serie de conquistas con el objetivo de transferir parte de su
ejrcito de un pas a otro. Normalmente, es conveniente escoger una ruta en la cual la cantidad de
pases que se tienen que conquistar sea mnima. Dada la descripcin de un tablero con 20 pases,
cada uno con 1 a 19 conexiones a otros pases, tu trabajo es escribir un programa que tome un
pas inicial y uno final, y calcule la cantidad mnima de pases que se tienen que conquistar para
llegar al destino. Por ejemplo, si el pas inicial y el final son vecinos, el programa deber regresar
uno.
La entrada de tu programa consistir en una serie de configuraciones para los pases. Cada caso
consistir en la descripcin de un tablero en las primeras 19 lneas. La representacin evita repetir
fronteras, por lo que slo incluye la frontera de un pas i con uno j cuando i < j. Por lo tanto, la i-
sima lnea, donde i es menor a 20, contiene un entero x que indica cuantos de los pases con
numeracin ms alta comparten frontera con el pas i, seguido con x enteros distintos mayores
a i pero menores a 20, que describen las fronteras entre el pas i y el j. La lnea 20 de cada caso
contiene un entero (1 n 100) ndicando la cantidad de pases que siguen a continuacin. Las
siguientes n lneas contienen exactamente dos enteros (1 a, b 20; a b) indicando los pases
iniciales y finales en una conquista.
Puede haber varios casos en la entrada; tu programa deber de ser capaz de leerlos y procesarlos
hasta llegar al fin de archivo. Existir por lo menos una ruta de un pas a otro en cualquier
configuracin dada.
Salida
Para cada caso en la entrada, tu programa deber imprimir el siguiente mensaje Test Set #t,
donde t es el nmero del caso actual, empezando en 1. Las siguientes nt lneas tendrn el
resultado para el caso correspondiente esto es, la cantidad mnima de pases a conquistar. Para
cada prueba en el caso, se deber empezar escribiendo el nmero del pas a seguido por la cadena
to , el nmero del pas b, la cadena : y terminando un entero que indique la cantidad mnima
de movimientos requeridos para ir del pas a al b en la configuracin dada. Despus de todas
pruebas en un caso, tu programa deber imprimir una lnea en blanco.
Zhejiang: 1221
Solucin: Como el peso de cada arista es unitario, podemos utilizar una bsqueda en amplitud por
cada distancia que nos pidan. Sin embargo, resulta ms fcil implementar Floyd, y como la
cantidad de vrtices son muy pocos la diferencia en tiempo no es significativa.
Cdigo
En las primeras tres lneas declaramos las variables que vamos a utilizar. Para los ciclos
usamos i y j, para contar en que prueba vamos c, t para leer con que vrtice es adyacente el
vrtice actual, n, x, a, y b las utilizamos de acuerdo a la descripcin del problema. La matriz de
adyacencia la guardamos en m.
En el procedimiento Floyd (lneas 5 a 13), calculamos la distancia ms corta entre todos los pares
de vrtices. En cada iteracin del ciclo k, utilizamos al vrtice k como punto intermedio para ir
de i a j. Si utilizndolo como punto intermedio logramos llegar ms rpido, actualizamos la
distancia (lnea 12). Una vez que utilizamos a todos los vrtices como intermedio, obtenemos las
distancias ms cortas para cada par de vrtices.
En seguida tenemos la parte principal del cdigo. En la lnea 16 inicializamos el nmero de casos a
cero. Mientras no encontremos el fin de archivo, procesamos las entradas. Inicializamos la matriz
a infinito, excepto por la diagonales, que van a cero (lneas 19 y 21). Algo importante es que el
valor de infinito debe ser ms grande que cualquier distancia que podamos obtener pero no
demasiado grande, ya que podemos causar un desbordamiento en la lnea 11 y 12. Tambin
debemos recordar que la funcin fillchar asigna el valor a cada byte, no a cada casilla; por lo que
en cada casilla tenemos el valor de 0x01010101 = 16843009, y no 1 como pudiera parecer.
Entre las lneas 22 y 31 leemos la conexin entre pases. Como los caminos pueden ser transitados
en ambas direcciones, asignamos dos valores a la matriz de adyacencia (lnea 28). Despus
utilizamos Floyd para calcular las distancias mnimas (lnea 32). Por ltimo, escribimos cual caso
estamos procesando (lnea 34), leemos las parejas de vrtices, y para cada una de ellas,
imprimimos la distancia mnima (lneas 38 y 39).
Caso ejemplo
3234
13
14
15
5
Entrada:
14
25
32
43
51
m[]
0 1 1 1
1 0 1
1 1 0 1
1 1 0 1
Desarrollo: 1 0
k= 1 k= 2 k= 3 k= 4 k= 5
0 1 1 1 0 1 1 1 0 1 1 1 0 1 1 1 2 0 1 1 1 2
1 0 1 2 1 0 1 2 1 0 1 2 1 0 1 2 3 1 0 1 2 3
1 1 0 1 1 1 0 1 1 1 0 1 1 1 0 1 2 1 1 0 1 2
1 2 1 0 1 1 2 1 0 1 1 2 1 0 1 1 2 1 0 1 1 2 1 0 1
1 0 1 0 1 0 2 3 2 1 0 2 3 2 1 0
Test Set #1
1 to 4: 1
2 to 5: 3
Salida:
3 to 2: 1
4 to 3: 1
5 to 1: 2
Nota: Slo utilizamos 5 vrtices para poder mostrar todo el proceso. El signo de infinito tiene un
valor de 16843009 durante la ejecucin.
Informacin Adicional
Es claro que el tiempo de ejecucin de Floyd es de O(V3). Para grficas dispersas existe un
algoritmo conocido como el algoritmo de Johnson, el cual tiene un tiempo de ejecucin de
O(V2lgV + VE).
El algoritmo de Floyd tambin puede ser utilizado para encontrar la cerradura transitiva, en cuyo
caso se le conoce como el algoritmo de Warshall.. Lo que busca la cerradura transitiva es saber si
se puede alcanzar un vrtice j a partir del vrtice i. Utilizando Floyd, si el camino ms corto
de i a j es menor a infinito es porque si existe un camino por el que podemos llegar. Sin embargo,
podemos hacer algunos cambios para eficientar los clculos.
Aqu m sigue siendo la matriz de adyacencia pero en lugar de enteros utilizamos booleanos
(aunque tambin funciona con enteros). El vrtice j lo podemos alcanzar desde i si ya podamos
llegar desde i hasta j (parte izquierda de la operacin), o si podemos llegar desde i a j pasando a
travs de k (parte derecha).
Cdigo en C
2.2 Problema Camino Ms Corto Abril 9, 2010
Los problemas conocidos como problemas del camino mnimo o camino ms corto, tratan como su
nombre indica de hallar la ruta mnima o ms corta entre dos puntos. Este mnimo puede ser la
distancia entre los puntos origen y destino o bien el tiempo transcurrido para trasladarse desde un
punto a otro. Se aplica mucho para problemas de redes de comunicaciones.
Este tipo de problemas pueden ser resueltos por el mtodo del Simplex, sin embargo existen otros
mtodos ms eficientes como por ejemplo el algoritmo de Dijkstra o el de Bellman-Ford.
Este algoritmo calcula el camino mnimo de un nodo a a otro nodo z en particular, a la vez que
calcula los caminos mnimos desde el nodo inicial a dado hasta cada uno de los otros nodos del
grafo.
Consiste en encontrar la ruta ms corta entre dos nodos dados de un grafo dirigido y valuado (con
capacidades). Veremos dos algoritmos, por un lado el algoritmo de Dijkstra, que encuentra el
camino ms corto entre el nodo origen y cada uno de los otros nodos de la red, y por otro lado el
algoritmo de Floyd, que encuentra el camino ms corto entre cualquier par de nodos de la red.
Algoritmo de DIJKSTRA
Conjunto C : Conjunto de vrtices candidatos. Inicialmente contiene todos los nodos menos el
nodo origen.
Conjunto S : Conjunto de vrtices seleccionados, es decir, aquellos para los que ya conocemos su
camino mnimo desde el nodo origen. Inicialmente contiene el nodo origen.
Vector D : Almacenar la longitud del camino ms corto desde el origen a cada nodo. Tendr
tantas posiciones como nodos tenga el grafo. El coste de ir del nodo origen a s mismo lo
estimaremos como cero.
Vector P : Almacenar el nodo predecesor a cada nodo en el camino ms corto desde el origen
hasta l. Tendr tantas posiciones como nodos tenga el grafo. La posicin del nodo predecesor al
origen estableceremos que sea cero para indicar que es el nodo origen.
Llamaremos al nodo origen o, y el coste de ir del nodo i al nodo j lo denotaremos como COSTEij .
Seleccionamos el nodo que sea destino de la arista con menor valor que salga del nodo o,
llammoslo u. Introducimos el nodo u en S y lo sacamos de C. Almacenamos en la posicin u del
vector D el valor COSTEou y en la posicin u del vector P el valor del nodo predecesor, es decir, o.
Seleccionamos el siguiente nodo al que podamos llegar con menor coste, bien directamente
desde o, bien a travs del otro nodo seleccionado u. Llamamos al nuevo nodo seleccionado v.
Introducimos el nodo v en S y lo sacamos de C. Introducimos en la posicin v del vector D el coste
de llegar al nodo v, si es directamente desde o ser COSTEov, si es a travs
de u ser D[u]+COSTEuv. Por ltimo, en la posicin v del vector P introducimos el valor del nodo
predecesor, ya sea o o u.
Repetiremos este proceso hasta que todos los nodos hayan sido seleccionados, es decir, hasta que
el conjunto C est vaco, o lo que es lo mismo, hasta que en el conjunto S se encuentren todos los
nodos del grafo. En ese momento en el vector D tendremos almacenado el coste mnimo para
llegar desde el nodo origen a cualquier nodo del grafo, y podremos obtener el camino ms corto
mediante el vector P.
Ejemplo: Aplicar el algoritmo de Dijkstra sobre el siguiente grafo siendo el nodo origen el 1:
Algoritmo DE FLOYD
El algoritmo de Floyd es ms general que el de Dijkstra, ya que determina la ruta ms corta entre
dos nodos cualquiera de la red.
El algoritmo representa una red de n nodos como una matriz cuadrada de orden n, la llamaremos
matriz C. De esta forma, el valor Cijrepresenta el coste de ir desde el nodo i al nodo j, inicialmente
en caso de no existir un arco entre ambos, el valor Cij ser infinito.
Definiremos otra matriz D, tambin cuadrada de orden n, cuyos elementos van a ser los nodos
predecesores en el camino hacia el nodo origen, es decir, el valor Dij representar el nodo
predecesor a j en el camino mnimo desde i hasta j. Inicialmente se comienza con caminos de
longitud 1, por lo que Dij = i.
Las diagonales de ambas matrices representan el coste y el nodo predecesor para ir de un nodo a
si mismo, por lo que no sirven para nada, estarn bloqueadas.
Los pasos a dar en la aplicacin del algoritmo de Floyd son los siguientes:
Se selecciona la fila y la columna k de la matriz C y entonces, para i y j, con ik, jk e ij, hacemos:
Si k n, aumentamos k en una unidad y repetimos el paso anterior, en caso contrario paramos las
iteraciones.
La matriz final C contiene los costes ptimos para ir de un vrtice a otro, mientras que la
matriz D contiene los penltimos vrtices de los caminos ptimos que unen dos vrtices, lo cual
permite reconstruir cualquier camino ptimo para ir de un vrtice a otro.
Ejemplo: Aplicar el algoritmo de Floyd sobre el siguiente grafo para obtener las rutas ms cortas
entre cada dos nodos. El arco (3, 5) es direccional, de manera que no est permitido ningn trfico
del nodo 5 al nodo 3 . Todos los dems arcos permiten el trfico en ambas direcciones.
j=3 (jk, ji): C[2,1]+C[1,3] = 3+10 = 13 < C[2,3] = , por tanto hacemos:
C[2,3] = 13 y D[2,3] = 1.
j=4 (jk, ji): C[2,1]+C[1,4] = 3+ = , no hay que cambiar nada, no podemos llegar de 2 a 4 a
travs de 1.
j=5 (jk, ji): C[2,1]+C[1,5] = 3+ = , no hay que cambiar nada, no podemos llegar de 2 a 5 a
travs de 1.
j=2 (jk, ji): C[3,1]+C[1,2] = 10+3 = 13 < C[3,2] = , por tanto hacemos:
C[3,2] = 13 y D[3,2] = 1.
j=4 (jk, ji): C[3,1]+C[1,4] = 10+ = , no hay que cambiar nada, no podemos llegar de 3 a 4 a
travs de 1.
j=5 (jk, ji): C[3,1]+C[1,5] = 10+ = , no hay que cambiar nada, no podemos llegar de 3 a 5 a
travs de 1.
j=2 (jk, ji): C[4,1]+C[1,2] = +3 = , no hay que cambiar nada, no podemos llegar de 4 a 2 a
travs de 1.
j=3 (jk, ji): C[4,1]+C[1,3] = +10 = , no hay que cambiar nada, no podemos llegar de 4 a 3 a
travs de 1.
j=5 (jk, ji): C[4,1]+C[1,5] = + = , no hay que cambiar nada, no podemos llegar de 4 a 5 a
travs de 1.
Tomamos i=5 (i k), en este caso ocurre como en el paso anterior, como C[5,1]=, entonces no
habr ningn cambio, no puede haber ningn camino desde 5 a travs de 1.
Tomamos k=2:
Tomamos i=1:
j=3: C[1,2]+C[2,3] = 3+13 = 16 > C[1,3] = 10 , por tanto no hay que cambiar nada, el camino es
mayor que el existente.
C[1,4] = 8 y D[1,4] = 2 .
Tomamos i=3:
j=1: C[3,2]+C[2,1] = 13+3 = 16 > C[3,1] = 10, no hay que cambiar nada.
Tomamos i=4:
C[4,1] = 8 y D[4,1] = 2.
Tomamos i=1:
C[1,5] = 25 y D[1,5] = 3 .
Tomamos i=2:
C[2,5] = 28 y D[2,5] = 3.
Tomamos i=4:
Tomamos i=1:
j=3: C[1,4]+C[4,3] = 8+6 = 14 > C[1,3] = 10, no hay que cambiar nada.
C[1,5] = 12 y D[1,5] = 4.
Tomamos i=2:
C[2,3] = 11 y D[2,3] = 4.
C[2,5] = 9 y D[2,5] = 4.
Tomamos i=3:
j=1: C[3,4]+C[4,1] = 6+8 = 14 > C[3,1] = 10, no hay que cambiar nada.
C[3,2] = 11 y D[3,2] = 4.
C[3,5] = 10 y D[3,5] = 4.
Tomamos i=5:
C[5,1] = 12 y D[5,1] = 2 .
C[5,3] = 10 y D[5,3] = 4.
Tomamos k=5:
Tomamos i=1:
j=3: C[1,5]+C[5,3] = 12+10 = 22 > C[1,3] = 10, no hay que cambiar nada.
Tomamos i=2:
j=3: C[2,5]+C[5,3] = 9+10 = 19 > C[2,3] = 11, no hay que cambiar nada.
Tomamos i=3:
j=1: C[3,5]+C[5,1] = 10+12 = 22 > C[3,1] = 10, no hay que cambiar nada.
j=2: C[3,5]+C[5,2] = 10+9 = 19 > C[3,2] = 11, no hay que cambiar nada.
Tomamos i=4:
Las matrices finales C y D contienen toda la informacin necesaria para determinar la ruta ms
corta entre dos nodos cualquiera de la red. Por ejemplo, la distancia ms corta del nodo 1 al
nodo 5 es C[1,5] = 12.
Para determinar la ruta asociada del camino mnimo entre el nodo 1 y el nodo 5 haremos lo
siguiente:
Antes de explicar directamente el algoritmo de Kruskal, comenzar dando conceptos sobre que es
un rbol de expansin mnima para entender mejor el problema.
rbol de Expansin
Dado un grafo conexo, no dirigido G. Un rbol de expansin es un rbol compuesto por todos los
vrtices y algunas (posiblemente todas) de las aristas de G. Al ser creado un rbol no existirn
ciclos, adems debe existir una ruta entre cada par de vrtices.
Un grafo puede tener muchos arboles de expansin, veamos un ejemplo con el siguiente grafo:
En la imagen anterior se puede observar que el grafo dado posee 3 arboles de expansin, dichos
arboles cumplen con las propiedades antes mencionadas como son unir todos los vrtices usando
algunas aristas.
Dado un grafo conexo, no dirigido y con pesos en las aristas, un rbol de expansin mnima es un
rbol compuesto por todos los vrtices y cuya suma de sus aristas es la de menor peso. Al ejemplo
anterior le agregamos pesos a sus aristas y obtenemos los arboles de expansiones siguientes:
De la imagen anterior el rbol de expansin mnima seria el primer rbol de expansin cuyo peso
total es 6.
El problema de hallar el rbol de Expansin Mnima (MST) puede ser resuelto con varios
algoritmos, los mas conocidos con Prim y Kruskal ambos usan tcnicas voraces (greedy).
Algoritmo de Kruskal
Para poder comprender el algoritmo de kruskal ser necesario revisar primer el tutorial de Union-
Find.
Como trabaja:
Primeramente ordenaremos las aristas del grafo por su peso de menor a mayor. Mediante la
tcnica greedy Kruskal intentara unir cada arista siempre y cuando no se forme un ciclo, ello se
Algoritmo en Pseudocdigo
1 mtodo Kruskal(Grafo):
1 struct Edge{
5 Edge(){}
Ahora el siguiente paso es ordenar las aristas del grafo en orden ascendente:
Para el ordenamiento podemos usar las libreras predefinidas de Java y C++ como estamos
ordenando estructuras necesitamos un comparador, en este caso estamos ordenando por peso
por lo tanto dentro de la estructura antes definida agregamos:
1 struct Edge{
7 }
Lo siguiente ser recorrer todas las aristas ya ordenadas y verificar si sus vrtices estn o no en la
misma componente.
La primera arista a verificar es la que une a los vrtices 8 y 7, verificamos si estn en la misma
componente, para ello hacemos Find(8) , Find(7):
Observamos la tabla de Union-Find y vemos que Find(3) != Find(9). Entonces es posible realizar la
unin de ambas componentes:
Continuamos con la siguiente arista:
En la imagen
podemos observar que ambos vrtices no estn en la misma componente, por tanto realizamos la
Union( 6 , 7 ):
Continuamos con la siguiente arista, los vrtices 1 y 2 no estn en la misma componente conexa:
Realizamos la Union(1,2):
En este caso si
observamos la imagen los vrtices 7 y 9 estn en la misma componente conexa; asimismo en la
tabla de Union-Find el elemento raz del vrtice 7 es el mismo que el del vrtice 9 por ello
afirmamos que estn el a misma componente conexa, por lo tanto no habr que realizar la unin
de ambos vrtices. Con esto evitamos tener ciclos en el rbol de expansin mnima.
Al observar la
imagen los vrtices 3 y 4 no estn en la misma componente conexa por lo tanto realizamos la
Union(3,4) en el grafo:
Continuamos con la siguiente arista:
Los vrtices 8
y 9 estn en la misma componente conexa por lo tanto no realizamos Unin de vrtices.
Continuemos con la siguiente arista:
Los vrtices 1
y 8 estn diferentes componentes. Realizamos la Union(1,8) en el grafo:
Continuamos con la siguiente arista:
Los vrtices 2
y 3 estn en la misma componente conexa por lo tanto no realizamos Union de componentes.
Continuamos con la siguiente arista:
El peso total del rbol de expansin mnima para el grafo mostrado es 39.
En cdigo simplemente es iterar sobre el arreglo de aristas ingresado y ordenado obteniendo sus
respectivos datos. Para verificar si estn o no en la misma componente usamos el mtodo
sameComponent explicado en el tutorial de Union-Find:
1 for( int i = 0 ; i < E ; ++i ){ //Recorremos las aristas ya ordenadas por peso
10 }
11 }
Verificacin de MST
Para que sea un MST vlido el nmero de aristas debe ser igual al nmero de vrtices 1. Esto se
cumple debido a que el MST debe poseer todos los vrtices del grafo ingresado y adems no
deben existir ciclos. Si vemos el ejemplo antes explicado tenemos en el MST:
Nmero de Aristas = 8
Nmero de Vrtices = 9
Cumple con lo dicho -> 9 1 = 8 por tanto tenemos un MST vlido. Veamos otro ejemplo teniendo
como grafo ingresado lo siguiente:
Como podemos observar el grafo ingresado posee 2 componentes conexas, al aplicar kruskal
obtendremos los siguientes MST:
En la imagen podemos observar el MST luego de aplicar kruskal, sin embargo no es un MST vlido
porque no tiene 1 componente conexa que posea todos los vrtices, comprobemos:
Nmero de Aristas = 7
Nmero de Vrtices = 9
No cumple lo dicho inicialmente 9 1 != 7 por lo tanto tenemos un MST invalido. En cdigo basta
con un if:
1 //Si el MST encontrado no posee todos los vrtices mostramos mensaje de error
2 //Para saber si contiene o no todos los vrtices basta con que el numero
4 if( V - 1 != numAristas ){
5 puts("No existe MST valido para el grafo ingresado, el grafo debe ser conexo.");
6 return;
7 }