Universidade do Algarve Faculdade de Ciências e Tecnologia Departamento de Engª Electrónica e Informática Inteligência Artificial (2005/2006 2º Semestre) Trabalho 1: Caminho mais curto entre duas cidades Discentes: Ricardo Seromenho a23842 Rui Dias a24254
Introdução O nosso programa tem por objectivo resolver o problema do caminho mais curto entre duas localidades (ou simplesmente achar um caminho). O programa cham se mapa e começa por aceitar dois parâmtros quando é chamado. A localização de dois ficheiros.csv, o primeiro contendo um mapa de distâncias e um segundo contendo uma tabela das coordenadas das localidades. Para resolver o problema são utilizadas duas estratégias: a estratégia de procura cega, para a quel escolhemos a procura em largura primeiro e a estratégia de procura heurística em que escolhemos o algoritmo da procura A* e o Algoritmo de pesquisa IDA*. Assim sendo o utilizador do nosso programa pode escolher de um destes três algoritmos para encontrar o caminho de uma localidade a outra. Sendo que os da estratégia heurística darão o caminho mais curto e o da estratégia de procura cega dará um caminho, mas sem certezas de ser o mais curto. Na estratégia de procura heurística a escolha de qual cidade vai ser analisada depende de uma função f(n) = g(n) + h(n) em que a função g(n) é a distância real de uma cidade a outra e a função h(n) é a distância em linha recta da cidade em questão até à cidade de destino. Esta função h(n) tem de ser admissível para que os algoritmos funcionem bem. Para uma função heurística ser admissivel, então, neste caso, a distância em linha recta de uma cidade a outra tem de ser menor ou igual que a distância real. O que se verifica para todo o caso. Sendo que a nossa heurística é admissível, teremos então a certeza que será encontrado o caminho mais curto. O output do programa são vários dados sobre o que o algoritmo fez, tais como: os nós gerados, os nós expandidos, a penetrância, o comprimento da solução encontrada, o tempo de execução do algoritmo, e o principal de tudo, o caminho da cidade de origem à cidade de destino e a sua distância.
Desenvolvimento O programa foi desenvolvido em JAVA, devido a ser uma linguagem orientada a objectos, sendo por isso no nosso ver, mais simples de resolver o problema. Ao evocar o programa, este deve ser evocado com dois argumentos, o primeiro a localização do ficheiro que contém a tabela das distâncias e o segundo que contém os nomes e as corrdenadas das cidades. Estes ficheiros devem estar em formato csv, como descrito a seguir: (Um ficheiro destes faz se facilmente em excel e depois guradar como tipo.csv) Coordenadas e nomes das cidades 3 colunas, em que a primeira coluna é o nome da cidade e as duas seguintes a localização em x e em y. Cada linha corresponderá a uma cidade, podendo por isso o número de linhas variar. Tabela das distâncias Número de linhas e colunas igual pois a cada linha/coluna corresponde uma cidade, a ordem das cidades neste ficheiro deve ser a mesma oredem do ficheiro anterior. O trabalho foi dividido pelos dois. Então um fez o algoritmo de procura em largura primeiro e o A* e o outro o IDA*. Sendo que depois integrámos um no outro. O programa está organizado em 6 classes, que passamos então a explicá las: P r i n c i p a l Esta classe tem apenas o efeito de servir de ponto de encontro de todas as outras classes. Apenas tem um método, que é o método main do nosso programa. O nosso main é muito simples: Mostramos as opções de escolha ao utilizador. N o d e Aproveitar coisas que já estão feitas, foi o que nós fizemos em duas ou três ocasiões. Na página web do livro Artificial Intelligence: A Modern Approach estava lá esta classe, mas fizemos lhe algumas alterações. No decorrer do algortimos A* e largura primeiro, estes vão gerando uma árvore. Cada nó dessa árvore será um objecto desta classe. Em que cada objecto desta classe será capaz de guardar: public int cidade a cidade correspondente a este nó; public Node parent um nó que corresponde ao pai deste nó; public int depth a profundidade a que o nó se encontra; public double fn guarda o valor da função f(n) = g(n) + h(n) public double gn guarda a distância desde o nó de origem até ao próprio.
M a p a O programa recebe como parâmetros de entrada a localização de dois ficheiros: o ficheiro que tem a tabela das distâncias das cidades e outro que tem os nomes e as corrdenadas das cidades. Esta classe foi feita com o intuito de gurdar essa informação exclusivamente em um objecto. Assim sendo, cada objecto desta classe terá: public int [][]distancia matriz que guarda a distância entre cidades; public Point []localizacao matriz de Pontos que guarda as coordenadas das cidades; public String []nomes matriz que guarda os nomes das cidades; public double []h matriz que guarda a heurística para cada cidade. Nas matrizes acima cada lugar ocupado na matriz corresponde a uma cidade. Por exemplo se a posição 17 da matrxiz nomes tem a string Faro, então em todas as outras matrizes a posição 17 corresponderá a Faro. Métodos da classe: public void init_distancia(string caminho) Este método já estava feito por outra pessoa e foi encontrado na net em http://ostermiller.org/utils/csv.html. Para utilizar este método é necessário adicionar um package, que está disponivel na mesma página, ao projecto. Este método foi alterado para que percorra o ficheiro csv e coloca a informação encontrada na matriz das distâncias. Se encontrar a um valor mete o na matriz, se fôr um traço, coloca um 1. public void init_localizacao(string caminho) Praticamente o mesmo que o método anterior mas é para retirar a informação do ficheiro das coordenadas. Depois preenche a matriz localizaçao e a matriz dos nomes ao mesmo tempo. public void calc_h(point d) O que este método faz é iniciar a matriz das heurísticas e preenchê la com a distância em linha recta desde a cidade x, até à cidade destino que é o parâmetro de entrada deste método. public void showh(),public void showdistancia(),public void showlocalizacao() métodos que foram utilizados para testes. Estes métodos mostram simplesmente a informação contida nas matrizes h, distancias e localizacao respectivamente. P r o b l e m a Os algoritmos de procura em largura primeiro e A* estão implementados nesta classe. Um objecto desta classe tem:
Vector l_nos Fila a ser utilizada pelos algoritmos, para gurdar os nós que vão sendo gerados; public int nos_gerados Numero de nós gerados; public int nos_expandidos Número de nós expandidos; public int L Comprimento da solução (número de arcos desde a origem ao destino; public int distancia Distância da origem ao destino; public double penetrancia Razão entre o número de nós expandidos e o comprimento da solução; Node no No que vai sendo expandido na execução dos algoritmos; Métodos da classe: public void init_lnos(int s, Mapa m) Inicia a fila dos nós. Tem como entrada a cidade de origem e um objecto mapa devidamente iniciado com os dados dos ficheiros. Coloca um primeiro nó na fila; esse nó é o nó da cidade de origem. public void printpath(mapa m,int d, int source) Recebe o mapa que têm as distâncias e os nomes das cidades, recebe a cidade de destino e a cidade de origem. O que este método faz é simples: analisa o nó do objecto e vai ver o caminho. Como? A primeira cidade do nó é o destino, que é adicionada ao Vector caminho. Depois é um ciclo que iguala o nó ao pai desse nó e mete a cidade no Vector caminho. Esse ciclo pára quando o nó fôr igual à cidade de origem. Isto para que depois se possa imprimir o caminho pela ordem correcta. Depois tem um ciclo que imprime o Vector caminho. E por fim imprime as informaçãoes adicionais que estão descritas na introdução. public boolean pesquisafila(node ver) Recebe um nó ver, e depois verifica se esse nó está ou não na fila. Se estiver retorna true, caso contrário retorna falso. public boolean Largura(int s, int d, Mapa m) Método que executa o algoritmo de largura primeiro. Recebe a cidades origem e destino e também um objecto Mapa devidamente inicializado. O algoritmo começa por invocar o método init_lnos. Depois entra num ciclo que só pára, ou quando a fila estiver vazia ou quando o nó que vai ser expandido fôr o nó da cidade de destino. A cada iteraccção desse ciclo o nó da cabeça da fila l_nos é removido e atribuido ao no a ser analisado. Depois começa um ciclo for, que vai expandir esse nó e colocar os nós gerados na fila. Este método retorna true caso o algoritmo tenha encontrado o caminho e false caso contrário. public boolean AStar(int s, int d, Mapa m) Parecido ao método anterior, mas a cada nó geradoé atribuido o fn. Então só depois de analisado o fn é que se coloca na fila. Pois o nó é colocado na fila que vai estando o todo o seu tempo de vida ordenada. Do método anterior para este é a diferença é a colocação do nó na fila. Também este método retorna true caso tenho encontrado um caminho e false caso contrário
IDA Nesta classe e implementado o algoritmo do IDA* que determina o caminho mais curto entre dois nós. Variáveis globais da classe: int NMAX Numero de cidades do mapa; public double []no_actual no_actual e usado para saber qual dos nos esta a ser usado actualmente 0 numero do no actual, 1 pai do no actual, 2 heuristica do no public int estado_inicial Nó inicial public int estado_final Nó final public int [][]matriz Ligações dos nos do grafo com a distancia em km public int [][]coord Matriz com as coordenadas de cada nó public double [][]nos_expandidos Fila dos nos expandidos public int n_nos Numero de nos actualmente na fila public double [][]matriz_caminho Matriz do caminho percorrido pelo algoritmo public int n_nos_gerados numero de nos gerados public int n_nos_expandidos numero de nos expandidos public double f_limite limite maximo do f(h) public double []heuristica Matriz com os valores da heuristica para cada nó. Métodos utilizados na Classe IDA: public void init_matriz(double []h, int [][]dist, Point []local) Inicia as matrizes e os arrays para a execução do programa. Da entrada o h que tem os valores da heuristica, o valor dist que é a distancia real entre os nos e a variável local contem os valores para colocar na matriz coord. public int ida_ast ( ) inicia o algoritmo do IDA mas só termina quando encontra uma solução e inicia o f_limite com o valor do h do nó inicial. Retorna o valor do estado_final. public int ida ( ) É o algoritmo em si calcula o caminho mais perto entre dois nós. Retorna o valor do estado final se sucesso e 1 em caso de falha. public double mínimo( ) calcula o mínimo entre os nos que se encontram na fila e não foram expandidos e devolve o menor valor encontrado. public void expand(double pai ) Expande os nós de um determinado nó e coloca os na fila de espera. Entra um valor double que é o no a ser expandido. Não permite que um no seja gerado se este for igual ao seu avô. public void remove ( ) retira um no da lista coloca o como no actual e faz um shift a esquerda aos nos seguintes da lista.
public double gedene(int estado_actual, int pai) determina o g de um no e retorna o seu valor; public void print_caminho(mapa map) Imprime o caminho entre dois nos. Percorre a matriz_caminho e os nos que se encontram repetidos são analisados e retirado o que tem maior valor de heuristica. Entra a variável map da classe Mapa que troca os números usados nos grafos por letras. S t o p W a t c h Esta classe foi retirada da Internet e não foi alterada. E utilizada para determinar o tempo de execução do programa. Só utilizamos as funções start() stop() e timestring(), a função start() inicia a contagem do tempo, a função stop para a contagem do tempo e a função timestring() retorna o tempo decorrido.
Conclusão Comparações de vários outputs dos vários algoritmos para o mesmo problema, da cidade A para a cidade N, para o mapa da romenia que está n o site do livro Artificial Intelligence: A Modern Approach. Largura primeiro: A -> S -> F -> B -> U -> V -> I -> N Nós expandidos: 58 Nós gerados: 70 Penetrância: 10.0 Distancia: 856 L: 7 Tempo de execução: 0.0 mseconds A* A -> S -> R -> P -> B -> U -> V -> I -> N Nós expandidos: 44 Nós gerados: 121 Penetrância: 15.0 Distancia: 824 L: 8 Tempo de execução: 0.0 mseconds IDA* A -> S -> R -> P -> B -> U -> V -> I -> N Nós expandidos: 16 Nós gerados: 1668 L: 8 Penetrancia : 208 Tempo de execução: 16.0 mseconds Numa primeira análise podemos verificar logo que o IDA* é o mais lento de todos. Depois podemos confirmar também que o procura largura primeiro não encontra o caminho curto. Simplesmente encontra um caminho. O IDA é mais complexo de implementar que os outros dois algoritmos que implementámos.