Análisis de Algoritmos

advertisement
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
Descargar