• Nenhum resultado encontrado

Busca heurística limitada pela memória

Condições para otimalidade: admissibilidade e consistência

3.5.3 Busca heurística limitada pela memória

A maneira mais simples para reduzir os requisitos de memória para A* é adaptar a ideia de aprofundamento iterativo para o contexto de busca heurística, resultando no algoritmo de

aprofundamento iterativo A* (IDA* – Iterative Deepening A*). A principal diferença entre IDA* e

o aprofundamento iterativo padrão é que o corte utilizado é o f-custo (g + h) em vez da profundidade; em cada iteração, o valor de corte é o menor f-custo de qualquer nó que excedeu o corte na iteração anterior. O IDA* é prático para muitos problemas com custos de passo unitário e evita a sobrecarga associada à manutenção de uma fila ordenada de nós. Infelizmente, ele sofre das mesmas dificuldades dos custos de valor real que a versão iterativa da busca de custo uniforme descrita no Exercício 3.17. Esta seção examina brevemente outros dois algoritmos de memória limitada, chamados RBFS e MA*.

A busca recursiva de melhor escolha (RBFS – Recursive Best First Search) é um algoritmo recursivo simples que tenta imitar a operação de busca padrão pela melhor escolha, mas usando apenas um espaço linear de memória. O algoritmo é mostrado na Figura 3.26. Sua estrutura é semelhante ao de uma busca em profundidade recursiva, mas, em vez de continuar indefinidamente seguindo o caminho atual, ele utiliza a variável f_limite para acompanhar o f-valor do melhor caminho alternativo disponível de qualquer ancestral do nó atual. Se o nó atual exceder esse limite, a recursão reverte para o caminho alternativo. Com a reversão da recursão, o RBFS substitui o f- valor de cada nó ao longo do caminho por um valor de backup — o melhor f-valor de seus filhos. Dessa forma, a RBFS lembra o f-valor das melhores folhas da subárvore esquecida e pode, portanto, decidir se vale a pena reexpandir a subárvore algum tempo mais tarde. A Figura 3.27 mostra como a RBFS atinge Bucareste.

função BUSCA-RECURSIVA-PELA-MELHOR(problema) retorna uma solução ou falha

retorna RBFS(problema, FAZ-NÓ(problema. ESTADO-INICIAL), ∞)

função RBFS (problema, nó, f_limite) retorna uma solução ou falha e um limite novo f_custo

se problema. TESTE-OBJETIVO(nó.ESTADO) então retorne SOLUÇÃO (nó) sucessores ← [ ]

para cada ação em problema. AÇÕES(nó.ESTADO) fazer adicionar NÓ-FILHO(problema, nó, ação) em sucessores se sucessores estiver vazio então retornar falha), ∞

para cada s em sucessores fazer / * atualizar f com o valor da busca anterior, se houver */ s.f ← max(s.g + s.h, nó.f))

repita

melhor ← valor f mais baixo do nó em sucessores se melhor.f > f_limite então retornar falha, melhor.f alternativa ← segundo valor f mais baixo entre sucessores

resultado, melhor.f ← RBFS(problema, melhor, min(f_limite, alternativa) se resultado ≠ falha então retornar resultado

Figura 3.27 Etapas de uma busca RBFS para a rota mais curta para Bucareste. O valor do f_limite

para cada chamada recursiva é mostrado no topo de cada nó atual, e cada nó é rotulado com seu

f-custo. (a) O caminho via Rimnicu Vilcea é seguido até que a melhor folha atual (Pitesti) tenha um

valor que é pior do que o melhor caminho alternativo (Fagaras). (b) A recursão reverte e o melhor valor da folha da subárvore esquecida (417) é copiado para Rimnicu Vilcea, então Fagaras é expandida, revelando um melhor valor da folha de 450. (c) A recursão reverte e o melhor valor da folha da subárvore esquecida (450) é copiado para Fagaras, então Rimnicu Vilcea é expandida. Dessa vez, devido ao melhor caminho alternativo (através de Timisoara), custa pelo menos 447, e a expansão continua para Bucareste.

A RBFS é um pouco mais eficiente do que a IDA*, mas ainda sofre pela geração excessiva de um mesmo nó. No exemplo da Figura 3.27, a RBFS segue o caminho via Rimnicu Vilcea, depois “muda de ideia” e tenta Fagaras, e depois muda de ideia novamente. Essas mudanças de ideia ocorrem porque, cada vez que o melhor caminho atual é estendido, seu f-valor possivelmente cresce — h geralmente é menos otimista para nós mais perto do objetivo. Quando isso acontece, o segundo melhor caminho pode se tornar o melhor caminho; assim, a busca tem que recuar para segui-lo. Cada mudança de ideia corresponde a uma iteração da IDA* e pode exigir muitas reexpansões de nós esquecidos para recriar o melhor caminho e estendê-lo com mais um nó.

Como a busca em árvore A*, a RBFS é um algoritmo ótimo se a função heurística h(n) for admissível. Sua complexidade de espaço é linear com relação à profundidade da solução ótima, mas a sua complexidade de tempo é bastante difícil de caracterizar: ela depende tanto da precisão da função heurística como do quão frequente o melhor caminho se altera à medida que os nós são expandidos.

As buscas IDA* e RBFS sofrem por usarem pouca memória. Entre iterações, a IDA* retém apenas um número único: o limite atual do f-custo. A RBFS retém mais informações na memória, mas utiliza apenas espaço linear: mesmo se mais memória estiver disponível, a RBFS não teria como fazer uso dela. Por esquecerem muito do que fizeram, ambos os algoritmos podem acabar reexpandindo os mesmos estados muitas vezes. Além disso, eles sofrem o crescimento potencialmente exponencial em complexidade associado com caminhos redundantes em grafos (veja a Seção 3.3).

Parece sensato, portanto, usar toda a memória disponível. Dois algoritmos que fazem isso são o

MA* (A* de memória limitada) e o SMA* (MA* simplificado). O SMA* é bem mais simples, de

modo que iremos descrevê-lo. O SMA* procede exatamente como o A*, expandindo a melhor folha até que a memória esteja cheia. Nesse ponto, não poderá adicionar um novo nó à árvore de busca sem suprimir um antigo. O SMA* sempre suprime o pior nó folha — o que tem o maior f_valor. Como o RBFS, o SMA*, em seguida, faz o backup do valor do nó esquecido em seu pai. Dessa forma, o ancestral de uma subárvore esquecida conhece a qualidade do melhor caminho daquela subárvore. Com essa informação, o SMA* regenera a subárvore somente quando todos os outros caminhos foram mostrados como piores do que o caminho que ele esqueceu. Outra maneira de dizer isso é que, se todos os descendentes de um nó n forem esquecidos, não saberemos para onde ir a partir de n, mas ainda teremos uma ideia de como vale a pena ir a algum lugar de n.

O algoritmo completo é muito complicado para reproduzir aqui,10 mas há uma sutileza que vale a

pena mencionar. Dissemos que o SMA* expande a melhor folha e exclui a pior folha. E, se todos os nós folha tiverem o mesmo f_valor? Para evitar a seleção do mesmo nó para exclusão e expansão, o SMA* expande a melhor folha mais nova e exclui a pior folha mais antiga. Estas coincidem quando há apenas uma folha, mas nesse caso a árvore de busca atual deve ser um único caminho da raiz até a folha que preenche toda a memória. Se a folha não for um nó objetivo, mesmo que esteja em um

caminho de solução ótima, essa solução não será alcançável com a memória disponível. Desta

forma, o nó poderá ser descartado exatamente como se não tivesse sucessores.

O SMA* estará completo se houver qualquer solução acessível, isto é, se d, a profundidade do nó objetivo mais raso, for menor que o tamanho da memória (expressa em nós). Será ótimo se qualquer solução ótima for alcançada; caso contrário, ele devolverá a melhor solução alcançável. Em termos práticos, o SMA* é uma escolha bastante robusta para encontrar soluções ótimas, especialmente quando o espaço de estados é um grafo, os custos de passo não são uniformes e a geração do nó é cara em comparação com a sobrecarga de manutenção da borda e do conjunto explorado.

Para problemas muito difíceis, no entanto, muitas vezes o SMA* é forçado a alternar constantemente entre muitos caminhos candidatos à solução, da qual pode caber na memória apenas um pequeno subconjunto (isso se assemelha ao problema de degradação em sistemas de paginação de disco). Então, o tempo extra que é necessário para a regeneração repetida dos mesmos nós significa que os problemas que poderiam ser praticamente solúveis com A*, dada a memória ilimitada, tornam-se intratáveis por SMA*. Isso significa dizer que as limitações de memória podem

se tornar um problema intratável do ponto de vista de tempo computacional. Embora nenhuma

teoria atual explique o equilíbrio entre tempo e memória, esse parece ser um problema inevitável. A única saída é abandonar a exigência de otimalidade.