Senhores(as), espero que estejam bem!
Notei algo interessante no código: no início criamos grafos todos interconectados, ou seja, todo nó se conecta a todos os nós vizinhos, correto? Porém, na resposta do exercício que o professor passou — que no caso seria de A até C — o algoritmo escolheu o melhor caminho sendo A → B → C.
A cada nó visitado, ele adiciona 1 no G e faz a soma com o número negativo de H para obter o f. Até aqui tudo bem. Entretanto, o melhor caminho não seria esse, já que A se liga diretamente a C. Analisando o código, descobri a causa: é a má configuração da heurística. Como a heurística é negativa, as contas na fórmula não assumem o caminho mais rápido. Vou demonstrar como isso acontece no exemplo.
- Situação inicial
Início: Produto A (0.9)
Objetivo: Produto C (0.7)
Heurística = -conversao_probabilidade
Ou seja:
h(A) = -0.9
h(B) = -0.8
h(C) = -0.7
h(D) = -0.6
- Primeira expansão (partindo de A)
Fila inicial: [( -0.9, g=0, A )]
Expande A → vizinhos = B, C, D.
Cálculo de f = g + 1 + h:
Para B: f = 0 + 1 + (-0.8) = 0.2
Para C: f = 0 + 1 + (-0.7) = 0.3
Para D: f = 0 + 1 + (-0.6) = 0.4
Fila: [(0.2, g=1, B), (0.3, g=1, C), (0.4, g=1, D)]
Observação: Como podem ver, B vai para o começo da fila como nó a ser expandido. Porém, A já havia encontrado C. Então, por que não parou aí? Isso acontece porque o A* escolhe sempre o nó com o menor f. Por apenas 0.1, C não foi escolhido.
- Segunda expansão (a partir de B)
Expande B → vizinhos = A, C, D (mas A já foi visitado).
Para C via B: f = g(=1) + 1 + h(C)(-0.7) = 1.3
Para D via B: f = 1 + 1 + (-0.6) = 1.4
Fila: [(0.3, g=1, C), (0.4, g=1, D), (1.3, g=2, C), (1.4, g=2, D)]
- Terceira expansão
Agora o próximo menor f é o Produto C (0.3).
Esse é o objetivo, então o algoritmo para.
- Recuperando o caminho
O dicionário de caminhos guarda:
B veio de A
C veio de B
Então o caminho reconstruído é: A → B → C
Analogia com o carrinho de sorvete
Imagine o seguinte:
Início (A)
|
|--- Ponte B (200m) ---> [B]
| |
| |--- Ponte extra (1000m) ---> [Objetivo C]
|
|--- Ponte C (300m) -------------------------------> [Objetivo C]
Você tem duas opções de ponte para chegar até o carrinho de sorvete (C).
Ponte B: mais curta (200m), mas que leva a um desvio de 1000m depois.
Ponte C: mais longa no início (300m), mas vai direto ao objetivo.
O algoritmo, por causa da heurística incorreta, escolhe expandir primeiro a Ponte B e acaba chegando ao caminho mais longo.
Por que isso acontece?
Isso ocorre por um fator principal: a definição incorreta da heurística. Assim, o algoritmo encontra um caminho válido, mas não o ótimo.
No A*, para garantir que o primeiro caminho encontrado seja o ótimo, a heurística precisa ser:
Admissível: nunca superestimar o custo real até o objetivo.
Consistente: obedecer à desigualdade triangular dos custos.
No caso, como usamos:
return -produto.conversao_probabilidade
isso não representa um custo até o objetivo, mas apenas uma ordenação de preferência.
Resultado: a heurística não é admissível nem consistente, então o A* pode encontrar caminhos não ótimos (como A → B → C em vez de A → C).
Como resolver?
Se a ideia é encontrar o caminho mais curto em número de passos (arestas do grafo):
def heuristica(produto):
return 0
Isso transforma o A* em um Dijkstra, garantindo sempre o caminho mais curto.
Se quiser usar algo parecido com “distância real”:
Por exemplo, se fossem produtos em categorias, a heurística poderia ser baseada na distância entre categorias.
def heuristica(produto, objetivo):
# Heurística baseada na categoria
if produto.categoria == objetivo.categoria:
return 0
else:
return 1
E se fosse para continuar com o exemplo do professor porém de maneira corrigida seria alterando essa parte:
caminho = []
produto = objetivo
while produto in caminhos:
caminho.insert(0, produto)
produto = caminhos[produto]
return caminho
Não deu para colocar a solução completa, coloco no comentário abaixo.