Busca
Busca em IA: planejamento e resolução de problemas
Em IA, busca é a família de algoritmos que resolve problemas por exploração sistemática de alternativas — possíveis sequências de ações, configurações ou hipóteses — até que um objetivo seja alcançado. Muitas tarefas de IA podem ser formuladas como:
- Você tem um estado inicial
- Você pode aplicar ações que geram estados sucessores
- Você quer alcançar um estado objetivo (ou maximizar uma recompensa / minimizar um custo)
- Você precisa fazer isso de forma eficiente apesar de um número potencialmente enorme de possibilidades
A busca é fundamental no planejamento clássico em IA, na navegação de robôs, em jogos, em escalonamento, e também aparece em sistemas modernos de aprendizado de máquina (machine learning) (por exemplo, decodificação de sequências com busca em feixe, planejamento com heurísticas aprendidas, busca em árvore no aprendizado por reforço).
Este artigo foca em algoritmos de busca em grafos — especialmente BFS, DFS e A* — e no papel das heurísticas.
Modelando problemas como busca
Formulação em espaço de estados
Um problema de busca normalmente é definido por:
- Representação de estado
s(por exemplo, posição do robô em uma grade, uma configuração do cubo de Rubik) - Estado inicial
s0 - Ações / função de transição:
Succ(s)retorna sucessores (e opcionalmente custos das ações) - Teste de objetivo:
Goal(s)é verdadeiro sesé uma solução - Custo do caminho: soma dos custos por passo, frequentemente escrito como
g(s)para o custo do início atés
O trabalho do algoritmo de busca é encontrar um caminho (uma sequência de ações) de s0 até qualquer estado objetivo, idealmente minimizando o custo total.
Árvores vs grafos (e por que isso importa)
Mesmo que a exploração conceitual forme uma árvore, muitos problemas são naturalmente grafos:
- Você pode chegar ao mesmo estado por múltiplos caminhos
- Existem ciclos (por exemplo, mover para a esquerda e depois para a direita retorna você à mesma célula)
Na prática, a maioria das implementações sérias de busca são buscas em grafo que rastreiam estados visitados para evitar loops infinitos e trabalho redundante.
É aqui que estruturas de dados centrais — filas, pilhas, conjuntos hash, filas de prioridade — importam; veja Algoritmos e Estruturas de Dados.
Busca em Largura (BFS)
Ideia
BFS explora o espaço de busca nível por nível: primeiro todos os estados na profundidade 1, depois profundidade 2, e assim por diante. Ela usa uma fila FIFO para a fronteira.
Propriedades
- Completa: sim, se o fator de ramificação for finito e existir uma solução.
- Ótima: sim, quando todos os custos de passo são iguais (ou quando se minimiza o número de passos).
- Complexidade de tempo: (O(b^d)), onde
bé o fator de ramificação edé a profundidade do objetivo mais raso. - Complexidade de espaço: (O(b^d)) (BFS pode consumir muita memória).
Esses limites exponenciais se conectam diretamente a Complexidade: até mesmo uma busca “simples” pode se tornar intratável à medida que b e d crescem.
Exemplo prático: caminho mais curto em uma grade não ponderada
Se cada movimento custa 1, BFS encontra o caminho com o menor número de passos.
from collections import deque
def bfs(start, is_goal, neighbors):
q = deque([start])
parent = {start: None} # for path reconstruction
while q:
s = q.popleft()
if is_goal(s):
return reconstruct_path(parent, s)
for n in neighbors(s):
if n not in parent: # visited check
parent[n] = s
q.append(n)
return None
def reconstruct_path(parent, goal):
path = []
cur = goal
while cur is not None:
path.append(cur)
cur = parent[cur]
return list(reversed(path))
BFS é amplamente usada em:
- caminhos mais curtos em grafos não ponderados
- encontrar componentes conectados / alcançabilidade
- planejamento base quando os custos são uniformes
Busca em Profundidade (DFS)
Ideia
DFS explora o mais longe possível ao longo de um ramo antes de fazer retrocesso. Ela usa uma pilha (frequentemente via recursão).
Propriedades
- Completa: não necessariamente (pode ficar presa em caminhos de profundidade infinita sem verificação de ciclos ou limites de profundidade).
- Ótima: não (pode encontrar primeiro uma solução muito profunda/cara).
- Complexidade de tempo: (O(b^m)), onde
mé a profundidade máxima de busca. - Complexidade de espaço: (O(b \cdot m)), frequentemente muito menor que a BFS.
DFS é atraente quando a memória é limitada, mas deve ser usada com cuidado.
Limites de profundidade e DFS com Aprofundamento Iterativo (IDDFS)
Uma correção comum é a DFS com limite de profundidade, que para na profundidade L. Para recuperar a completude semelhante à BFS (para custos unitários) com memória semelhante à DFS, use aprofundamento iterativo:
- Execute DFS com
L=0,1,2,...até encontrar uma solução.
IDDFS:
- é completa (sob suposições padrão)
- é ótima para problemas de custo unitário (como a BFS)
- usa muito menos memória do que a BFS
DFS/IDDFS aparecem frequentemente na resolução de quebra-cabeças e em busca combinatória onde a memória é um gargalo.
Busca de Custo Uniforme (UCS) e algoritmo de Dijkstra
BFS só é ótima quando cada passo custa o mesmo. Quando ações têm diferentes custos não negativos, você quer Busca de Custo Uniforme (UCS), que é essencialmente o algoritmo de Dijkstra aplicado a partir do estado inicial.
- A fronteira é ordenada pelo custo do caminho até agora
g(n) - Sempre expande o nó atualmente mais barato
Propriedades (com custos de passo não negativos):
- Completa: sim (dado fator de ramificação finito e custo mínimo de passo positivo)
- Ótima: sim
UCS é a versão “sem heurística” do A*.
Heurísticas: guiando a busca
Uma heurística é uma função:
h(n)= custo estimado do nó/estadonaté um objetivo
Heurísticas são a chave para escalar a busca: elas direcionam a exploração para estados promissores.
Admissibilidade e consistência
Duas propriedades importantes:
Heurística admissível (admissible heuristic): nunca superestima o custo real restante
[ h(n) \le h^(n) ] onde (h^(n)) é o verdadeiro custo ótimo restante (cost-to-go).Heurística consistente (monotone heuristic): obedece a uma desigualdade triangular ao longo das arestas
[ h(n) \le c(n,n') + h(n') ] para todo sucessor (n') de (n).
Consistência implica admissibilidade (sob suposições típicas) e torna o A* mais fácil de implementar corretamente: uma vez que um nó é expandido, seu melhor custo de caminho é final.
Exemplos comuns de heurísticas (navegação em grade)
Para uma grade com 4 vizinhos e custos unitários:
- Distância de Manhattan (Manhattan distance):
h = abs(x - gx) + abs(y - gy)
admissível e consistente.
Para uma grade com 8 vizinhos com custo diagonal √2:
- Distância octil (Octile distance) é frequentemente usada.
Para espaço contínuo com custos euclidianos:
- Distância euclidiana (Euclidean distance) é admissível (se obstáculos só puderem aumentar o custo real).
Construindo heurísticas na prática
Padrões comuns de projeto:
- Relaxamentos (relaxations): resolver uma versão mais fácil do problema (por exemplo, ignorar obstáculos) e usar seu custo ótimo como
h. - Abstrações (abstractions): agrupar estados em estados abstratos e pré-computar distâncias (bases de padrões em quebra-cabeças).
- Marcos e subobjetivos (landmarks and subgoals): estimar a distância via restrições intermediárias obrigatórias.
- Combinar heurísticas admissíveis (combine admissible heuristics):
max(h1, h2)é admissível se cada uma for admissível (frequentemente mais forte do que qualquer uma isoladamente).- A soma só é segura quando os componentes são garantidos como partes de custo disjuntas.
A força da heurística é crucial: quanto mais próximo h(n) estiver do custo real restante (sem superestimar), menos nós o A* tipicamente expande.
A\*: busca ótima com heurísticas
Ideia
A* classifica nós na fronteira por:
[ f(n) = g(n) + h(n) ]
g(n): custo exato do início aténh(n): custo estimado denaté o objetivo
Ele expande primeiro o nó com menor f(n).
Garantias
Com custos de passo não negativos:
- Se
hé admissível, A* é ótimo (busca em árvore; para busca em grafo, consistência é tipicamente exigida para a implementação mais simples com “conjunto fechado”). - Se
hé consistente, a busca em grafo do A* é ótima e evita lógica complicada de reexpansão de nós.
Detalhes práticos de implementação
Uma busca em grafo típica com A* usa:
- Conjunto aberto (open set) (fronteira): uma fila de prioridade por
f - Conjunto fechado (closed set): um conjunto de estados visitados/expandidos
g_score[state]: melhor custo encontrado até agoracame_from[state]: ponteiro de retorno para reconstrução de caminho
import heapq
import math
def astar(start, is_goal, neighbors, h):
open_heap = []
heapq.heappush(open_heap, (h(start), 0, start)) # (f, g, state)
came_from = {start: None}
g_score = {start: 0}
closed = set()
while open_heap:
f, g, s = heapq.heappop(open_heap)
if s in closed:
continue
if is_goal(s):
return reconstruct_path(came_from, s)
closed.add(s)
for (n, cost) in neighbors(s): # cost >= 0
tentative_g = g_score[s] + cost
if n in closed:
continue
if tentative_g < g_score.get(n, math.inf):
came_from[n] = s
g_score[n] = tentative_g
heapq.heappush(open_heap, (tentative_g + h(n), tentative_g, n))
return None
Notas:
- Muitas bibliotecas de fila de prioridade não oferecem suporte fácil a decrease-key; inserir duplicatas e ignorar entradas desatualizadas é comum.
- Com heurísticas inconsistentes, você pode precisar permitir reabrir nós (remover de
closedquando um caminho melhor é encontrado), ou mudar para uma formulação mais cuidadosa.
Exemplo: A\* em uma grade
Suponha que você queira o caminho mais curto para um robô em uma grade com obstáculos:
- Estado:
(x, y) - Ações: mover N/S/L/O com custo 1
- Heurística: distância de Manhattan até o objetivo
A* vai priorizar fortemente células “mais próximas” do objetivo, mas desviará de obstáculos quando g aumentar.
É por isso que o A* é uma escolha padrão em jogos, navegação robótica e planejamento de rotas.
Comparando BFS, DFS e A\*
Principais trade-offs:
BFS
- Ótima para: caminhos mais curtos com custo unitário, espaços de estado pequenos a médios
- Fraqueza: explosão de memória
DFS / IDDFS
- Ótima para: busca com pouca memória, encontrar alguma solução rapidamente em espaços profundos
- Fraqueza: não é ótima em custo; pode se perder sem limites
A*
- Ótima para: caminhos ótimos com heurísticas informativas
- Fraqueza: ainda pode ser exponencial; pesada em memória em espaços grandes
Quando os custos variam, compare:
- UCS: ótima, mas explora amplamente sem orientação
- A*: ótima e guiada por
h
Variantes e extensões que você verá na prática
Busca Gulosa Best-First (Greedy Best-First Search)
Usa apenas f(n) = h(n). Muitas vezes é rápida, mas:
- não é ótima
- pode ser incompleta em alguns espaços infinitos
- pode ficar presa explorando regiões “aparentemente próximas”, mas bloqueadas
A\* Ponderado (Weighted A\*) (busca com suboptimalidade limitada)
Usa:
[ f(n) = g(n) + w \cdot h(n), \quad w > 1 ]
Isso direciona a busca de forma mais agressiva em direção à heurística, frequentemente reduzindo expansões de maneira dramática, ao custo de soluções potencialmente subótimas. Muitos sistemas reais preferem esse trade-off.
IDA\* (A\* com Aprofundamento Iterativo) (Iterative Deepening A\*)
Uma variante do A* eficiente em memória:
- realiza aprofundamento iterativo sobre um limiar de custo
f - usa memória semelhante à DFS
- frequentemente usada em quebra-cabeças com espaços de estado enormes (por exemplo, quebra-cabeças de peças deslizantes)
Busca bidirecional
Busca a partir do início e do objetivo e encontra um ponto de encontro no meio.
- Funciona melhor quando o objetivo é bem definido e predecessores podem ser gerados.
- Pode reduzir a complexidade de (b^d) para aproximadamente (b^{d/2}) em casos ideais.
Busca em feixe (beam search) (aproximada)
Mantém apenas os melhores k nós em cada profundidade (ou por pontuação). Comum na geração de sequências (por exemplo, tradução automática) e em outras tarefas de grande ramificação. A busca em feixe não garante encontrar a solução ótima, mas costuma ser eficaz na prática; veja também seu uso junto com Arquitetura Transformer.
Busca em planejamento clássico
No planejamento clássico (por exemplo, planejamento no estilo STRIPS), estados codificam fatos sobre o mundo, e ações têm pré-condições e efeitos. Sistemas de planejamento frequentemente usam:
- Busca progressiva (forward search): do estado inicial em direção aos objetivos (comum com A*)
- Busca regressiva (regression): das condições de objetivo de volta em direção ao início
- Heurísticas derivadas de problemas de planejamento relaxados (relaxed planning problems) (ignoram efeitos de deleção), produzindo estimativas poderosas usadas em planejadores como o FF
Heurísticas de planejamento são essencialmente funções h(n) informadas pelo domínio. Elas podem fazer a diferença entre explorar bilhões de estados e encontrar um plano rapidamente.
A busca também se conecta a Aprendizado por Reforço: AR resolve problemas de decisão sequencial via funções de valor/políticas aprendidas, enquanto a busca/planejamento clássicos frequentemente assumem um modelo conhecido. Métodos híbridos (AR baseado em modelo, planejamento com heurísticas aprendidas) combinam ambos.
Considerações práticas de engenharia
Escolhendo representações
O desempenho frequentemente é dominado por:
- quão compacto é o seu estado
- quão rápido você consegue gerar sucessores
- quão eficiente é o hashing/igualdade para verificações de visitados
Para problemas grandes, uma codificação cuidadosa do estado (bitsets, empacotamento em inteiros) pode gerar grandes ganhos de velocidade.
Detecção de duplicatas e “conjuntos fechados”
A busca em grafo tipicamente precisa de:
- um conjunto de visitados/fechado para impedir ciclos
- uma tabela de melhor custo (
g_score) para evitar perder tempo com caminhos piores
Mas isso consome memória. Se a memória for restrita, considere IDDFS/IDA*, abstração ou busca limitada.
Quando heurísticas são difíceis (ou aprendidas)
Sistemas modernos podem usar heurísticas aprendidas (por exemplo, uma rede neural prevendo a distância restante). Elas podem ser eficazes, mas podem violar admissibilidade. Estratégias comuns:
- Aceitar suboptimalidade (usar A* Ponderado / busca em feixe)
- Calibrar ou limitar a heurística (difícil em geral)
- Usar o modelo aprendido para propor candidatos, mas verificar com busca clássica
Essa é uma forma de como busca “simbólica” e Redes Neurais interagem na IA contemporânea.
Escala e paralelismo
Busca em grande escala pode ser paralelizada (dividindo a fronteira, conjuntos fechados distribuídos), mas sincronização e detecção de duplicatas tornam-se complexas. É aqui que ideias de Fundamentos de Sistemas Distribuídos começam a importar.
Aplicações
Algoritmos de busca e heurísticas impulsionam muitos sistemas de IA e de software:
- Robótica: planejamento de caminho (modelos de grade/mundo), planejamento de tarefas (sequências de ações)
- Jogos: navegação (A*), estratégia (busca em árvore; veja Busca em Árvore de Monte Carlo)
- Compiladores e síntese de programas: explorar transformações ou programas candidatos
- Escalonamento e roteamento: caminhos mais curtos, planejamento guiado por restrições
- Decodificação em PLN: busca em feixe para selecionar sequências de saída prováveis
Resumo: o que lembrar
- BFS: caminho mais curto em grafos não ponderados; simples, mas pesada em memória.
- DFS: pouca memória; não é ótima; use limites de profundidade para garantir terminação.
- UCS (Dijkstra): ótima para custos não negativos variáveis; sem orientação por heurística.
- A*: ótima quando
hé admissível (e tipicamente consistente para busca em grafo); desempenho depende fortemente da qualidade da heurística. - Heurísticas são centrais: elas codificam a estrutura do problema para reduzir a busca drasticamente.
- Sistemas reais frequentemente trocam optimalidade por velocidade com A* Ponderado, busca em feixe ou heurísticas específicas do domínio.
A busca é um dos exemplos mais claros de como a IA combina teoria (garantias, complexidade, optimalidade) com engenharia prática (representações, estruturas de dados, heurísticas) para resolver problemas reais de forma eficiente.