Análisis de Algoritmos Profesor: M.C. Cuauhtemoc Gomez Suarez Tarea 06: Para calificación de examen. Sección: 503 Manuel Alejandro Salazar Mejı́a. Matrı́cula: 0300704C 11 de enero de 2011 1 Tarea: a) Programar la solución para el problema de las rutas más cortas de todos los pares. * Utilizar el algoritmo de Floyd-Warshall * Utilizar matrices de adyacencia para la representación de los grafos El algoritmo de Floyd-Warshall Utilizaremos una formulación diferente de programación dinámica para resolver el problema de todos los pares de rutas más cortas en un grafo dirigido G = (V, E). El algoritmo resultante, conocido como el algoritmo de Floyd-Warshall, se ejecuta en tiempo de Θ(V 3 ). Como antes, vertices con pesos negativos pueden estar presentes, pero asumimos que no hay ciclos con peso negativo. El algoritmo de Floyd-Warshall considera vertices intermedios en una ruta mas corta, y se basa en la siguiente observación. De acuerdo con nuestro supuesto de que los vertices de G son V = {1, 2, ..., n} , consideremos un subconjunto {1, 2, ..., k} de vertices para algun k. Para cualquier par de vertices i, j V , tenga en cuenta todas las rutas i a j cuyos vertices intermedios están formados por {1, 2, ..., k}, y sea p una ruta con el mı́nimo peso de entre ellos. Por lo que explota una relación entre la ruta p y rutas mas cortas de i a j con todos los vertices intermedios en el conjunto {1, 2, ..., k − 1}. La relacion depende de si k es un vertice intermedio de la ruta p. - Si k no es un vertice intermedio de la ruta p, entonces todos los vertices intermedios de la ruta p están en el conjunto {1, 2, ..., k − 1}. p1 - Si k es un vertice intermedio de la ruta p, entonces separamos p en i p2 k j, asi vemos que p1 es una ruta mas corta de i a k con todos sus vertices intermedios en {1, 2, ..., k − 1}. Del mismo modo, p2 es unaruta mas corta desde el vertice k a el vertice j con todos los vertices intermedios en el conjunto {1, 2, ..., k − 1}. Entonces encontrando una solución recursiva para el problema de todos los pares de rutas más cortas definimos: (k) dij como el peso de la ruta mas corta desde el vertice i al vertice j, para los cuales todos los vertices intermedios están en el conjunto {1, 2, ..., k}. 2 y (k) dij = wij (k−1) (k−1) (k−1) min(dij , dik + dkj ) k=0 k≥1 ahora para saber como se altera la matriz de predecesores, podemos dar una (k) formulación recursiva de Πij . Cuando k = 0, una ruta mas corta de i hasta j no tiene vertices intermedios en absoluto. Por lo tanto, N IL if i = j or wij = ∞ (0) Πij = i if i 6= j and wij < ∞ Para k ≥ 1, si tomamos la ruta i ( (k−1) Πij (k) Πij = (k−1) Πkj k j: (k−1) (k−1) (k−1) if dij ≤ dik + dkj , (k−1) (k−1) (k−1) > dik + dkj . if dij Algoritmo de Floyd-Warshall. pseudocodigo FLOYD-WARSHALL(W ) 1: n ← rows[W ] 2: D(0) ← W 3: para k ← 1 hasta n hacer 4: para i ← 1 hasta n hacer 5: para j ← 1 hasta n hacer (k−1) (k−1) (k−1) (k) 6: dij ← min(dij , dik + dkj ) 7: fin para 8: fin para 9: fin para 10: regresa D(n) 11: regresa Π(n) 3 Se propone el siguiente codigo en C++, floyd warshall.cpp: #include< iostream > #include< climits > #include< cstdlib > #include< cstdio > //definiciones para el algoritmo #define INF 10000000 #define NIL -1 using namespace::std; //matrices de pesos y de padres int W[5][5]; int Padre[5][5]; //inicializar la matriz de adyacencia y de padres void inicializar(){ for(int i = 0; i < 5; i + +) for(int j = 0; j < 5; j + +){ Padre[i][j] = NIL; if(i == j) W[i][j] = 0; else W[i][j] = INF; } } //insertar una arista validando i = j void inserta arista(int i, int j, int w){ if(i == j) W[i][j] = 0; else{ W[i][j] = w; Padre[i][j] = i+1; } } //validacion para suma con infinito int suma(int x, int y){ if( x == INF —— y == INF) return INF; else return x + y; } algoritmo que calcula las rutas mas cortas void floyd warshall(){ //ciclo principal de floyd warshall for(int k = 0; k < 5; k + +) for(int i = 0; i < 5; i + +) for(int j = 0; j < 5; j + +) if( W[i][j] > suma(W[i][k], W[k][j]) ){ W[i][j] = suma(W[i][k], W[k][j]); Padre[i][j] = Padre[k][j]; } printf(“W =\n”); for(int i = 0; i < 5; i + +){ for(int j = 0; j < 5; j + +) printf(“ %d ”, W[i][j]); printf(“\n”); } printf(“P =\n”); for(int i = 0; i < 5; i + +){ for(int j = 0; j < 5; j + +) printf(“ %d ”, Padre[i][j]); printf(“\n”); } } 4 int main(){ int narist; int a, b, c; printf(“ingresa el numero de aristas\n”); scanf(“ %d”, &narist); //inicializar la matriz de adyacencias y la matriz de predecesores inicializar(); //leer las aristas printf(“ingresa la arista en el orden: vertice1 vertice2 peso\n”); while(narist){ //leer arista (a,b) con capacidad c scanf(“ %d %d %d”, &a, &b, &c); inserta arista(a, b, c); narist–; } floyd warshall(); return 0; } dicho codigo se ha implementado para la resolución del siguiente grafo: Figura 1: Grafo a resolver con el algoritmo de Floyd-Warshall. 5 en la figura 2 podemos ver una corrida de la forma en que trabaja el algoritmo de Floyd-Warshall: Figura 2: corrida del algoritmo de Floyd-Warshall sobre el grafo de la figura 1. 6 ahora ejecutando el archivo floyd warshall.cpp: Figura 3: ejecución del programa floyd warshall.cpp. como se puede observar el resultado es el mismo W es la matriz resultante y P es la matriz de predecesores con la que se puede reconstruir la ruta mas corta obtenida pero eso esta fuera de mi alcance. 7 b) Programar la solución para el problema del flujo máximo * Utilizar el método de Ford-Fulkerson El algoritmo de Ford-Fulkerson Para comprender mejor este algoritmo es necesario definir algunos conceptos. Primero decimos que un grafo que representa flujos es un grafo dirigido y ponderado, donde el peso de las aristas representa una capacidad máxima de transportar un flujo. El flujo residual es el flujo disponible en una determinada arista una vez que se ha enviado flujo por ella (en ningún caso el flujo neto residual debe ser mayor a la capacidad de dicha arista ni menor que cero). El flujo residual lo calculamos como la capacidad menos flujo actual, donde flujo actual es el flujo que ya se ha ocupado en alguna iteración del algoritmo. Un camino de flujo residual es aquel camino de la fuente al sumidero donde todas las aristas en el camino tienen un flujo residual mayor a cero. El algoritmo comienza por hacer que el flujo actual en todas las aristas del grafo sea igual a cero, en consecuencia el flujo residual será igual a la capacidad de las mismas. El siguiente paso es encontrar un camino de la fuente al sumidero donde todas las aristas incluidas en el camino tengan una capacidad residual mayor a cero. La cantidad máxima de flujo que puede enviarse al sumidero por dicho camino corresponde como es lógico al valor de la capacidad residual mı́nima en dicho camino. A esta cantidad se le denomina incremento en el flujo, debido a que se suma al flujo actual en todas las aristas en el camino encontrado. La consecuencia inmediata es que el flujo residual se verá modificado y la arista con la menor capacidad estará transportando el flujo máximo (su flujo residual se convertirá en cero) y por lo tanto no deberá ser considerada en la siguiente iteración del algoritmo. Este proceso se repite siempre que pueda encontrarse un nuevo camino de flujo residual (un camino donde todas las aristas tengan un flujo residual mayor a cero). Al final el flujo máximo que puede enviarse de la fuente al sumidero corresponde a la suma de todos los incrementos calculados con cada nuevo camino encontrado. El algoritmo de Ford-Fulkerson depende fuertemente del método que se use para encontrar los caminos de flujo residual y estos a su vez dependen de la forma en la que se represente el grafo. Por un lado, la representación de matrices hace muy rápido el encontrar el valor de los flujos y las capacidades de cada arista pero hace lento el encontrar los nodos adyacentes y por lo tanto la búsqueda de caminos. Por otro lado, las listas de adyacencias hacen muy rápido el encontrar los nodos adyacentes pero hacen lento el encontrar el valor de los flujos y capacidades. 8 En cada iteración del método de Ford-Fulkerson, encontramos algunas rutas p que aumentan e incrementan el flujo f en cada vertice de p por la capacidad residual cf (p). La consecuencia de la aplicación del método calcula el caudal máximo en un grafo G = (V, E), poniendo al dı́a el flujo f [u, v] entre cada par u, v de vertices que estan conectados por una vertice. Si u y v no estan conectados por una arista en cualquier dirección, se supone implı́citamente que f [u, v] = 0. La capacidad de c(u, v) se supone que se administra junto con el grafo, y c(u, v) = 0 si (u, v) E. Algoritmo de Ford-Fulkerson. pseudocodigo FORD-FULKERSON(G,s,t) 1: para cadavertice(u, v)E[G] hacer 2: f [u, v] ← 0 3: f [v, u] ← 0 4: mientras existaunarutapdeshastatenlaredresidualGf hacer 5: cf (p) ← min{cf (u, v) : (u, v)estaenp} 6: para cadavertice(u, v)enp hacer 7: f [u, v] ← f [u, v] + cf (p) 8: f [v, u] ← −f [u, v] 9: fin para 10: fin mientras 9 Se propone el siguiente codigo en C++, ford fulkerson.cpp: #include < stdio.h > #include < list > //definiciones para el algoritmo #define MAXVERT 100 #define NULO -1 #define INFINITO 100000000 using namespace::std; //definicion de una estructura para almacenar los flujos actuales y capacidades typedef struct{ int flujo; int capacidad; }FLUJOS; //el grafo se almacena como una matriz FLUJOS grafo[MAXVERT][MAXVERT]; int nvert, padre[MAXVERT]; //valores iniciales de los flujos antes de insertar aristas void inicia grafo(){ for(int i = 0; i < nvert; i + +) for(int j = 0; j < nvert; j + +) grafo[i][j].capacidad = 0; } //se considera que puede haber mas de una arista entre cada para de vertices void inserta arista(int origen, int destino, int capacidad){ grafo[origen][destino].capacidad += capacidad; } //busqueda de caminos residuales, devuelve verdadero al encontrar un camino int BFS(int fuente, int sumidero){ int visitado[MAXVERT], u, v, residual; list< int > cola; //inicializar la busqueda for(u = 0; u < nvert; u + +){ padre[u] = NULO; visitado[u] = 0; } cola.clear(); //hacer la busqueda visitado[fuente] = 1; cola.push back(fuente); //ciclo principal de la busqueda por anchura while(!cola.empty()){ //saca nodo de la cola u = cola.front(); cola.pop front(); for(v = 0; v < nvert; v + +){ //elige aristas con flujo residual mayor a cero en el recorrido residual = grafo[u][v].capacidad - grafo[u][v].flujo; if(!visitado[v] && ( residual > 0)){ cola.push back(v);//mete nodo a la cola padre[v] = u;//guarda a su padre visitado[u] = 1;//lo marca como visitado } } }//devolver estado del camino al sumidero al terminar el recorrido return visitado[sumidero]; } //algoritmo de ford-fulkerson int ford fulkerson(int fuente, int sumidero){ int flujomax, incremento, residual, u; //los flujos a cero antes de iniciar el algoritmo 10 for(int i = 0; i < nvert; i + +) for(int j = 0; j < nvert; j + +) grafo[i][j].flujo = 0; flujomax = 0; //mientras existan caminos de flujo residual while(BFS(fuente, sumidero)){ //busca el flujo minimo en el camino de f a s incremento = INFINITO;//inicializa incremento a infinito //busca el flujo residual minimo en el camino de fuente a sumidero for(u = sumidero; padre[u] != NULO; u = padre[u]){ residual = grafo[padre[u]][u].capacidad- grafo[padre[u]][u].flujo; incremento = min( incremento, residual); } //actualiza los valores de flujo, flujo maximo y residual en el camino for(u = sumidero; padre[u] != NULO; u = padre[u]){ //actualiza los valores en el sentido de fuente a sumidero grafo[padre[u]][u].flujo += incremento; //hace lo contrario en el sentido de sumidero a fuente grafo[u][padre[u]].flujo -= incremento; } // muestra la ruta for (u=sumidero; padre[u]!=(-1); u=padre[u]) printf(“ %d< −”,u); printf(“ %d añade %d de flujo adicional\n”, fuente,incremento); flujomax += incremento; }//al salir del ciclo ya no quedan rutas de incremento de flujo se devuelve el ciclo maximo return flujomax; } int main(){ int narist; int a, b, c; int fuente, sumidero; int flujo; //leer parametros del grafo printf(“numero de vertice y numero de aristas\n”); scanf(“ %d %d”, &nvert, &narist); //inicializar el grafo inicia grafo(); //leer las aristas printf(“ingresa la arista en el orde v1 v2 peso\n”); while(narist){ //leer arista (a,b) con capacidad c scanf(“ %d %d %d”, &a, &b, &c); inserta arista(a, b, c); narist–; } for(int i = 0; i < nvert; i + +) for(int j = 0; j < nvert; j + +) printf(“grafo[ %d][ %d] = %d\n”, i, j, grafo[i][j].capacidad); //leer la consulta printf(“introduce el vertice fuente y el sumidero del grafo\n”); scanf(“ %d %d”, &fuente, &sumidero); flujo = ford fulkerson(fuente, sumidero); printf( el flujo maximo entre %d y %d es %d\n”, fuente, sumidero, flujo); printf(“El flujo entre los vertices quedo asi\n”); for(int i = 0; i < nvert; i + +) for(int j = 0; j < nvert; j + +) if( (i != j) && (grafo[i][j].flujo != 0) ) printf(“( %d, %d) = %d\n”, i, j, grafo[i][j].flujo); return 0; } 11 aqui vemos una corrida de como deberia funcionar el algoritmo de FordFulkerson: Figura 4: corrida del algoritmo Ford-Fulkerson. como se puede apreciar el resultado del flujo maximo obtenido es en el inciso d) con un valor de 14. ahora haciendo la corrida del programa ford fulkenson.cpp se tiene: 12 Figura 5: corrida del programa ford fulkerson.cpp. como podemos ver el flujo maximo que nos regresa entre el vertice fuente 1(vertice s en el grafo) y el vertice sumidero 4(vertice t en el grafo) es 14. Conclusión: Como podemos ver el algoritmo de floyd-warshall efectivamente regresa la ruta más corta de un grafo. Y en cambio el algoritmo de ford-fulkerson nos regresa el flujo maximo que se puede transportar desde un origen o fuente hasta un consumidor o sumidero, los dos creo que tienen muchas aplicaciones que ya se han mencionado en clase, recuerdo que el de floyd se puede utilizar en planeacion de vuelos y cosas por el estilo, y el de ford en cambio se puede utilizar para determinar si se cumplen las leyes de Kirchoff donde la suma de los flujos entrantes a un vertice, debe de ser igual a la suma de los flujos saliendo del vertice. Referencias: * Thomas H. Cormen, Charles E. Leiserson, Introduction to Algorithms, Second Edition 13