• Nenhum resultado encontrado

Resolução de problemas por meio de busca

3.4 ESTRATÉGIAS DE BUSCA SEM INFORMAÇÃO

3.4.3 Busca em profundidade

A busca em profundidade (DFS – Depth-first search) sempre expande o nó mais profundo na borda atual da árvore de busca. O progresso da busca é ilustrado na Figura 3.16. A busca prossegue imediatamente até o nível mais profundo da árvore de busca, onde os nós não têm sucessores. À

medida que esses nós são expandidos, eles são retirados da borda e, então, a busca “retorna” ao nó seguinte mais profundo que ainda tem sucessores inexplorados.

Figura 3.16 Busca em profundidade em uma árvore binária. A região inexplorada é mostrada em

cinza-claro. Os nós explorados sem descendentes na borda são removidos da memória. Os nós na profundidade 3 não têm sucessores e M é o único nó objetivo.

O algoritmo de busca em profundidade é uma instância do algoritmo de busca em grafo da Figura 3.7; enquanto a busca em largura utiliza uma fila FIFO, a busca em profundidade utiliza uma fila LIFO. Uma fila LIFO significa que o nó gerado mais recentemente é escolhido para expansão. Deverá ser o nó mais profundo não expandido porque é mais profundo do que seu pai, que, por sua vez, era o nó não expandido mais profundo quando foi selecionado.

Como alternativa para a implementação do estilo da BUSCA EM GRAFOS, é comum implementar primeiro a busca em profundidade com uma função recursiva que chama a si mesma para cada um dos seus filhos por vez (a Figura 3.17 apresenta um algoritmo em profundidade recursivo incluindo um limite de profundidade).

função BUSCA-EM-PROFUNDIDADE-LIMITADA(problema, limite) retorna uma solução ou

falha/corte

retornar BPL-RECURSIVA (CRIAR-NÓ(problema, ESTADO-INICIAL), problema, limite) função BPL-RECURSIVA(nó, problema, limite) retorna uma solução ou falha/corte

se problema. TESTAR-OBJETIVO (nó.ESTADO) então, retorna SOLUÇÃO (nó) se não se limite = 0 então retorna corte

senão

corte_ocorreu? ← falso para cada ação no problema.AÇÕES(nó.ESTADO) faça filho ← NÓ-FILHO (problema, nó, ação)

resultado ← BPL-RECURSIVA (criança, problema limite − 1) se resultado = corte então corte_ocorreu? ← verdadeiro

senão se resultado ≠ falha então retorna resultado se corte_ocorreu? então retorna corte senão retorna falha

Figura 3.17 Uma implementação recursiva da busca em árvore de profundidade limitada.

As propriedades da busca em profundidade dependem fortemente da versão utilizada da busca ser a busca em grafos ou a busca em árvore. A versão da busca em grafos, que evita estados repetidos e caminhos redundantes, é completa em espaços de estados finitos porque acabará por expandir cada nó. A versão da busca em árvore, por outro lado, não é completa — por exemplo, na Figura 3.6, o algoritmo seguirá o laço Arad-Sibiu-Arad-Sibiu para sempre. A busca em profundidade em árvore pode ser modificada, sem qualquer custo extra de memória, para verificar novos estados com relação aos nós do caminho da raiz até o nó atual; isso evita laços em espaço de estados finitos, mas não evita a proliferação de caminhos redundantes. Em espaço de estados infinitos, ambas as versões falham se um caminho não objetivo infinito for encontrado. Por exemplo, no problema 4 de Knuth, a busca em profundidade ficaria aplicando o operador fatorial para sempre.

Por motivos semelhantes, ambas as versões são não ótimas. Por exemplo, na Figura 3.16, a busca em profundidade vai explorar a subárvore esquerda toda, mesmo sendo o nó C um nó objetivo. Se o nó J também fosse um nó objetivo, a busca em profundidade iria devolvê-lo como solução em vez de

C, que seria uma solução melhor; então, a busca em profundidade não é ótima.

A complexidade temporal da busca em profundidade em grafo é limitada pelo tamanho do espaço de estados (que certamente pode ser infinito). A busca em profundidade em árvore, por outro lado, poderá gerar todos os nós O(bm) na árvore de busca, onde m é a profundidade máxima de qualquer

nó; isso pode ser muito maior do que o tamanho do espaço de estados. Observe que m em si pode ser muito maior do que d (profundidade da solução mais rasa) e é infinito se a árvore for ilimitada.

Até agora, a busca em profundidade parece não apresentar uma vantagem clara sobre a busca em largura, então por que incluí-la? O motivo é a complexidade espacial. Para uma busca em grafos, não há vantagem, mas uma busca em profundidade em árvore precisa armazenar apenas um único caminho da raiz até um nó folha, juntamente com os nós irmãos remanescentes não expandidos para cada nó no caminho. Uma vez que um nó é expandido, ele pode ser removido da memória, tão logo todos os seus descendentes tenham sido completamente explorados (veja a Figura 3.16). Para um espaço de estados com fator de ramificação b e profundidade máxima m, a busca em profundidade exige o armazenamento de apenas O(bm) nós. Usando as mesmas suposições da Figura 3.13 e supondo que nós na mesma profundidade do nó objetivo não têm sucessores, verificamos que a busca em profundidade exigiria 156 kilobytes, em vez de 10 exabytes na profundidade d = 16, um espaço sete trilhões de vezes menor. Isso levou à adoção da busca em profundidade em árvore como o carro-chefe básico de muitas áreas da IA, incluindo a satisfação de restrição (Capítulo 6), a satisfatibilidade proposicional (Capítulo 7) e a programação lógica (Capítulo 9). Para o restante

desta seção, nos concentraremos principalmente na versão de busca em árvore da busca em profundidade.

Uma variante da busca em profundidade chamada busca com retrocesso utiliza ainda menos memória. (veja o Capítulo 6 para mais detalhes). No retrocesso, apenas um sucessor é gerado de cada vez, em lugar de todos os sucessores; cada nó parcialmente expandido memoriza o sucessor que deve gerar em seguida. Desse modo, é necessária apenas a memória O(m), em vez de O(bm). A busca com retrocesso permite ainda outro truque de economia de memória (e de economia de tempo): a ideia de gerar um sucessor pela modificação direta da descrição do estado atual, em vez de copiá- lo primeiro. Isso reduz os requisitos de memória a apenas uma descrição de estado e a O(m) ações. Para que isso funcione, devemos ser capazes de desfazer cada modificação quando voltarmos para gerar o próximo sucessor. No caso de problemas com grandes descrições de estados, como a montagem robótica, essas técnicas são críticas para o sucesso.