Tsp

Share Embed Donate


Short Description

Download Tsp...

Description

Mario Rossainz López

Branch & Bound y El Agente Viajero Viajero

BRANCH & BOUND Y EL PROBLEMA DEL AGENTE VIAJERO

1.- Introducción: Uno de los problemas más interesantes que se han tratado de resolver con diversas técnicas algorítmicas y heurísticas planteadas es El problema del Agente Viajero (the Traveling Salesman Problem)  por ser un problema de complejidad NP-Completo. El problema del agente viajero dice lo siguiente: Se conocen las distancias entre un cierto número de ciudades. Un agente viajero debe, a partir de una de ellas, visitar una ciudad exactamente una vez y regresar al punto de partida habiendo recorrido en total la menor distancia posible. Formalmente el problema puede ser enunciado como sigue: dado un grafo g conexo y  ponderado y dado uno de sus vértices v0 , encontrar el ciclo Hamiltoniano de costo mínimo que comienza y termina en v0 [GUE99]. En la literatura e investigación podemos encontrar distintas propuestas de solución a este problema utilizando diversos paradigmas dentro del análisis y diseño de algoritmos. El paradigma de los algoritmos Ávidos, la programación dinámica, la vuelta a atrás y la ramificación y poda son técnicas algorítmicas que han sido utilizadas para encontrar  algoritmos de solución al problema en cuestión, sin embargo debido a la complejidad de éste, hasta hoy no se ha encontrado un algoritmo que resuelva totalmente un problema  NP-Completo. Es por eso que sigue siendo de interés el estudio de este tipo de problemas y dentro de la investigación de los CPANS que he estado realizando desde hace más de un año a la fecha, se ha planteado la posibilidad de tener una implementación paralela del problema del Agente Viajero utilizando como técnica algorítmica de solución la de Ramificación y Poda (Branch and Bound) por ser la técnica de estudio en mi investigación vista como una Composición Paralela de Alto Nivel.

2.- La técnica de Ramificación Ramifi cación y Poda: Es una técnica de diseño algorítmica cuyo nombre en español proviene de  Branch and   Bound  en el idioma Ingles y se aplica normalmente en la solución de problemas de optimización donde la complejidad computacional es grande.  Ramificación y Poda es una variante de la técnica de Vuelta Atrás, siendo similar a ésta última en que se realiza una enumeración parcial del espacio de soluciones del problema basándose en la generación de un árbol de expansión. Sin embargo la diferencia radica en que en  Ramificación y Poda existe la posibilidad de generar nodos siguiendo distintas estrategias y en Vuelta Atrás no [GUE99]. El diseño de Ramificación y Poda puede seguir un recor rido de su árbol de expansión en anchura (estrategia LIFO), en profundidad (estrategia FIFO), o utilizando el cálculo de funciones de costos para seleccionar el nodo que en principio parezca más prometedor a analizar (estrategia del mínimo costo o LC).

1

Mario Rossainz López

Branch & Bound y El Agente Viajero Viajero

Además de estas estrategias Ramificación y Poda utiliza cotas para hacer el podado de las ramas del árbol de expansión que conduzcan a la solución óptima. Para ello se calcula en cada nodo una cota del posible valor de aquellas soluciones alcanzables desde ése nodo. Si la cota muestra que cualquiera de estas soluciones tiene que ser  necesariamente peor que la mejor solución hallada hasta ese momento, no se necesita seguir explorando por esa rama del árbol, lo que permite llevar a cabo el proceso de  poda. Dentro de esta técnica, para determinar en cada momento que nodo será ramificado y dependiendo de la estrategia de búsqueda utilizada, se requerirá almacenar todos los nodos que no hayan sido podados, es decir, aquellos con posibilidad de ser ramificados (nodos vivos), en alguna estructura de datos que podamos podamos recorrer. Se utilizará una PILA de nodos generados que todavía no han sido examinados si la estrategia de búsqueda seleccionada es la de búsqueda en profundidad (LIFO). Pero si la estrategia seleccionada ha sido la de búsqueda en amplitud (FIFO) , entonces la estructura de datos a utilizar será una COLA. Por el contrario, si la estrategia elegida es la del mínimo costo (LC), la estructura necesaria de uso será un  Montículo (HEAP) para  poder almacenar los nodos ordenados por su costo. La estrategia del mínimo costo utiliza una función de costos que decide en cada momento qué nodo debe explorarse, con la esperanza de alcanzar lo más rápidamente posible una solución más económica que la mejor encontrada hasta ese momento.

2.1.-El Algoritmo de Ramificación y Poda: En un algoritmo de ramificación y poda se llevan a cabo básicamente tres etapas: • • •

La etapa de Selección La etapa de Ramificación La etapa de Poda

 La etapa de Selección: Se encarga de extraer un nodo de entre el conjunto de los nodos que no han sido podados y que tienen la posibilidad de ser ramificados. La forma de elección depende directamente de la estrategia de búsqueda que se decida utilizar en el algoritmo.  La etapa de Ramificación: Se construyen los posibles nodos hijos del nodo seleccionado en el paso anterior, formando f ormando el árbol de expansión.  La etapa de Poda: Se eliminan algunos de los nodos creados en la etapa anterior, aquellos cuyo costo parcial sea mayor que la mejor cota mínima calculada en ese momento. La contribución de éste algoritmo es la disminución en lo posible del espacio de  búsqueda y por tanto la atenuación de la complejidad en la exploración del árbol de  posibilidades de la solución óptima. Aquellos nodos no podados pasan a formar parte del conjunto de nodos con posibilidad  de ser ramificados, y se comienza de nuevo el proceso de selección. El algoritmo finaliza una vez encontrada la solución del problema o bien cuando se agota el conjunto de nodos con posibilidad de ser ramificados.

2

Mario Rossainz López

Branch & Bound y El Agente Viajero

Para cada nodo del árbol de expansión se dispondrá de una función de costo que estime el valor óptimo de la solución si se continuara por esa rama o camino. De esta forma, si la cota que se obtiene para un nodo, es peor que una solución ya obtenida por otra rama, se poda esa rama, pues no es interesante seguir por ella. Las funciones de costo son funciones crecientes respecto a la profundidad del árbol. Con todo esto se afirma que el valor de la técnica de  Ramificación y Poda esta en la  posibilidad de tener varias formas de exploración del árbol y al mismo tiempo el acotamiento de la búsqueda de solución, lo cual se traduce en eficiencia!!! [GUE99]. Sin embargo la dificultad del algoritmo radica en encontrar una buena función de costo  para el problema a resolver, donde “ buena” es en el sentido de que garantice la poda y que su cálculo no sea muy costoso. La ventaja añadida a los algoritmos de ramificación y poda es la de posibilitar su implementación y ejecución de forma paralela. Puesto que se tiene un conjunto de nodos vivos, es decir, con posibilidad de ser ramificados, sobre los que se efectúan las tres etapas antes mencionadas del algoritmo, nada impide tener más de un proceso trabajando sobre este conjunto de nodos, extrayéndolos, expandiéndolos y llevando a cabo la poda. Los CPANs en principio podrían proporcionar los algoritmos paralelizables necesarios  para resolver problemas como el del Agente Viajero con la técnica de Ramificación y Acotación. Sobre todo porque el abordar este tipo de problemas de forma paralela, se hace necesario si lo que se quiere además de obtener la solución es la de resolver el  problema en tiempos razonables ya que la complejidad de dicho problema es intrínseca. Esto último requiere de ciertos requerimientos que respecto a otras técnicas algorítmicas resultan caros, por ejemplo, aquellos que tienen que ver con la memoria. Se necesita por  tanto, que cada objeto nodo sea autónomo, es decir, que contenga toda la información necesaria para que siendo objeto activo (esto es, teniendo capacidad de ejecución en si mismo) realice los procesos de ramificación y poda para la reconstrucción de la solución encontrada hasta ese momento.

3.- Paralelización de la técnica de Ramificación y Poda usando el estándar POSIX Threads Existen varios esquemas para la paralelización de los algoritmos de Ramificación y Poda dependiendo de las características de las diferentes arquitecturas paralelas que se tengan [CAP]. Pero en resumen los esquemas se clasifican en dos grandes grupos: 1.- Esquemas Basados en Memoria Compartida 2.- Esquemas Basados en Sistemas Distribuidos La implementación paralela que aquí se muestra se basa en el primer tipo de esquemas, suponiendo una lista de nodos activos donde la comunicación entre ellos se hace a través de variables que utilizan una memoria común. Uno de los mayores problemas de la técnica de Ramificación y Poda es la implementación de los algoritmos que la diseñan. Sin embargo, el uso del paradigma de

3

Mario Rossainz López

Branch & Bound y El Agente Viajero

la orientación a objetos hace menos difícil esta tarea gracias al uso de sus propiedades como son la herencia y la modularidad. La implementación concurrente de Ramificación y Poda consta de los siguientes módulos principales: 1.- Se dispondrá del módulo que contiene el programa principal donde se construye el  problema del agente viajero y se resuelve de forma concurrente. En este modulo se creará la estructura de datos compartida por todos los nodos del árbol de expansión. Cada nodo que sea un nodo vivo, es decir, próximo a ser expandido será obtenido de dicha estructura y convertido a un hilo para ser lanzado en paralelo con el resto de nodos almacenados en la estructura. El proceso se repetirá mientras la estructura no este vacía o bien se haya llegado a la solución óptima. 2.- El módulo hilo.h es un archivo de encabezado que contiene la clase abstracta CHilo con un método virtual puro  fnHilo que se corresponderá con el hilo de ejecución. De esta manera cuando se quiera utilizar la clase CHilo se tendrá que escribir una clase derivada de ella y redefinir el método fnHilo. El cuerpo de éste método será el código que ejecutará el hilo [CEB03]. 3.- El módulo  Nodos.h contiene la clase  Nodos que hereda de CHilo e implementa por  tanto el método virtual puro  fnHilo. Cualquier instancia de esta clase será un hilo que contendrá el esquema de funcionamiento general de los algoritmos que diseñan la técnica de  Ramificación y Poda , en este caso para resolver el problema del agente viajero. 4.- El módulo  Nodo.h contiene la clase Nodo que describe e implementa las estructuras de datos que conforman a los nodos que serán los hilos de ejecución. 5.- Finalmente la estructura de datos que se maneja para almacenar los nodos (hilos) que se van generando, es un montículo, pues la estrategia de búsqueda seleccionada en esta implementación ha sido la del mínimo costo (LC), aunque como ya se mencionó se  podría utilizar una pila o una cola como estructura de datos según se siga una estrategia LIFO o FIFO respectivamente.

3.1.- La Clase CHilo: La clase CHilo proporciona métodos para iniciar un hilo, para que otro hilo pueda esperar a que éste finalice, tiene un constructor que permite asignar un nombre a un hilo y establecer los atributos con los que éste se creará, un método para acceder al nombre del hilo, uno más para obtener el identificador de éste, un método para asignación de atributos y un método para modificar el nombre del hilo.

4

Mario Rossainz López

Branch & Bound y El Agente Viajero

class CHilo { private: string m_nombreHilo; pthread_t m_idHilo; pthread_attr_t *m_attr; static void *ejec_hilo(void *arg); protected: virtual void *fnHilo() = 0;

// // // //

nombre del hilo identificador del hilo atributos del hilo invoca a fnHilo

// hilo de ejecución

public: CHilo(string nom = "", pthread_attr_t *attr = NULL); virtual ~CHilo(); void iniciar(); // inicia el hilo void *esperar_finalizacion(); // espera a que el hilo finalice string obtener_nombre() const; // devuelve m_nombreHilo void asignar_nombre(string); // asigna el nombre del hilo pthread_t obtener_id() const; // devuelve m_idHilo void asignar_atributos(pthread_attr_t *attr); // atributos };

La definición de la clase CHilo define los siguientes métodos [CEB03]:

 Método CHilo ~CHilo Ejec_hilo

fnHilo

Iniciar esperar_finalizacion

obtener_nombre asignar_nombre obtener_id asignar_atributos

Propósito Es el constructor de la clase. Inicia los datos nombre y atributos del hilo. Si los atributos no se especifican, se utilizan los definidos por omisión Es el destructor de la clase. Finaliza el hilo sólo en caso de que no lo haya hecho por sí mismo Es un método estático y privado. Corresponde a la función que tiene que ejecutar el hilo. Esta función recibe como parámetro la dirección ( this) del objeto que encapsula al hilo, con el fin de poder invocar a la función virtual fnHilo Es un método virtual puro que hace que la clase CHilo sea abstracta. Esto hace necesario derivar una clase concreta de esta redefiniendo éste método, donde se escribirá el código que ejecutará el hilo. Inicia la ejecución del hilo representado por el objeto CHilo que reciba este mensaje. Esto es, se lanza el hilo. Este método permite, al hilo que lo invoca, esperar a que el hilo que recibe el mensaje finalice su ejecución. El valor devuelto hace referencia al valor devuelto por el hilo, o en su defecto al estado del mismo. Es el método que devuelve el nombre del hilo Es el método que asigna un nombre al hilo Es el método que devuelve el identificador del hilo Es el método utilizado para asignar los atrib utos al hilo

5

Mario Rossainz López

Branch & Bound y El Agente Viajero

El código que corresponde a cada uno de los métodos descritos es el siguiente: // Constructor CHilo::CHilo(string nom, pthread_attr_t *attr) : m_nombreHilo(nom), m_idHilo(0), m_attr(attr) { } // Destructor CHilo::~CHilo() { if (m_idHilo) // si el hilo existe, finalizarlo { int cod = pthread_cancel(m_idHilo); //terminar el hilo m_idHilo if (cod) throw(string("error: pthread_cancel")); m_idHilo = 0; } } // Crear el hilo que ejecutará el método static ejec_hilo, // el cual recibe como argumento this. void CHilo::iniciar() { if (m_idHilo) return; int cod = pthread_create(&m_idHilo, m_attr, ejec_hilo, this); if (cod) throw(string("error: pthread_create")); } // El método ejec_hilo invoca a fnHilo, método virtual puro // que el usuario debe redefinir para escribir el código que // ejecutará el hilo. void *CHilo::ejec_hilo(void *arg) { CHilo *pObj = reinterpret_cast(arg); // this return pObj->fnHilo(); } // Método que esperará a que m_idHilo finalice void *CHilo::esperar_finalizacion() { void *vr; int cod = pthread_join(m_idHilo, &vr); if (cod) throw(string("error: pthread_join")); m_idHilo = 0; return vr; } string CHilo::obtener_nombre() const { return m_nombreHilo; } void CHilo::asignar_nombre(string nom) { m_nombreHilo = nom; } pthread_t CHilo::obtener_id() const { return m_idHilo; } void CHilo::asignar_atributos(pthread_attr_t *attr) { m_attr = attr; }

6

Mario Rossainz López

Branch & Bound y El Agente Viajero

3.2.- El Programa Principal: Es el módulo que define el problema del agente viajero y lo resuelve aplicando la técnica de Ramificación y Poda en su versión concurrente. Se tiene una función llamada nodoInicial( ) que definirá el nodo raíz del árbol de expansión. Nodo* nodoInicial() { Nodo *n; int i,j; mat_ady m; for(i=0;ireducir(); n->s[0]=0; for(i=1;is[i]=-1; n->k=0; return n; }

Este nodo inicial contendrá la matriz de costos inicial que representa el grafo que define el problema y se almacenará inicialmente en la estructura montículo con un costo mínimo inicial. Mientras el montículo no este vacío se irán sacando los nodos vivos (es decir, aquellos que no hayan sido podados y que estén próximos a expandirse) del montículo y se lanzarán en paralelo para ir generando los correspondientes subárboles de expansión. Cada uno de los nodos de los subárboles de expansión generados que cumplan con la regla del costo mínimo se almacenarán en la estructura compartida, es decir, en el montículo. El uso correcto de dicha estructura por los nodos hilos esta garantizada por la implementación de la sincronización de éstos a través del uso de candados y variables de condición en base al modelo productor-consumidor.

7

Mario Rossainz López

Branch & Bound y El Agente Viajero

int main() { int i,cont=0; Nodos *n[5000]; Nodo * aux; Nodo *raiz=nodoInicial(); estructura.insert(mmid::value_type(raiz->coste,raiz)); while(!estructura.empty()) { pthread_mutex_lock(&candado1); while((bandera)&&(!producer_done)) pthread_cond_wait(&var_cond,&candado1); if((bandera)&& producer_done) { pthread_mutex_unlock(&candado1); break; } bandera=true; pthread_mutex_unlock(&candado1); pthread_mutex_lock(&candado3); iter=estructura.begin(); pthread_mutex_unlock(&candado3); pthread_mutex_lock(&candado2); bandera2=false; pthread_cond_signal(&var_cond2); pthread_mutex_unlock(&candado2); aux=(iter->second); numanalizados++; aux->imprime_datos_nodo("Nodo"); n[cont]=new Nodos(aux); n[cont]->iniciar(); cont++; } void *sol; for(i=0;iesperar_finalizacion(); aux=static_cast(sol); aux->imprime_datos_nodo("Solucion"); cout
View more...

Comments

Copyright ©2017 KUPDF Inc.
SUPPORT KUPDF