Implementação de Algoritmos em Python

DFS - Viagem de IA (Busca em Profundidade)

A busca em profundidade (Depth-First Search) explora o grafo seguindo um caminho até o final, e quando não há mais opções, volta e tenta outro caminho.

def dfs_viagem(graph, inicio, destino):
    # Usando uma pilha para armazenar os nós que precisam ser visitados
    pilha = [inicio]

    # Dicionário para armazenar os nós visitados e o nó anterior
    visitados = {inicio: None}

    while pilha:
        atual = pilha.pop()

        if atual == destino:
            # Quando encontrar o destino, reconstrua o caminho
            return reconstruir_caminho(atual, visitados)

        for vizinho in graph[atual]:
            if vizinho not in visitados:
                visitados[vizinho] = atual
                pilha.append(vizinho)

    # Se não encontrar o destino
    return None

def reconstruir_caminho(destino, visitados):
    caminho = []
    atual = destino

    while atual is not None:
        caminho.append(atual)
        atual = visitados[atual]

    caminho.reverse()  # Inverter o caminho para que ele vá do início ao destino
    return caminho

A Viagem de Sabrina:

  • Exploração: A busca em profundidade é como uma jornada pela rede de IA, explorando cada vez mais a fundo.
  • Transporte: A pilha é o "transporte" de Sabrina, armazenando os nós visitados.
  • Mapa: A variável visitados serve para rastrear as paradas e evitar que Sabrina se perca.
  • Objetivo: O algoritmo explora todos os nós possíveis, sem se preocupar com o caminho mais curto.
  • Retorno: A reconstrução do caminho permite traçar a rota completa da viagem, da origem ao destino.

Reconstruindo a Jornada:

  • Caminho de Volta: Quando o destino é alcançado, Sabrina utiliza as informações da pilha para reconstruir o caminho.
  • Inversão: O caminho é invertido para mostrar a sequência correta da viagem.

Quando usar DFS:

  • Exploração Completa: Ideal para explorar todas as possibilidades em um grafo.
  • Problemas Específicos: Útil em problemas onde a exploração completa é mais importante que a otimização.

Algoritmo de Busca com Custo Uniforme (UCS)

Algoritmo de Busca com Custo Uniforme (Uniform Cost Search - UCS). Este algoritmo é uma variação da busca em largura, onde, ao invés de explorar todos os nós com a mesma prioridade, ele explora os nós com o menor custo acumulado até aquele ponto. A ideia principal do UCS é explorar caminhos que custam menos para alcançar o objetivo, sem considerar a heurística, apenas o custo real acumulado desde o nó inicial.

Como funciona:

  • Expansão: UCS sempre escolhe o nó com o menor custo acumulado para expandir.
  • Fila de Prioridade: Organiza os nós a serem explorados com base no custo total.
  • Revisão: Atualiza os caminhos se encontrar um caminho com custo menor.

Detalhamento das Funções:

  • uniform_cost_search(graph, start, goal):
    • Recebe o grafo, o nó inicial e o objetivo.
    • Utiliza uma fila de prioridade.
    • A fila garante que o nó com menor custo seja explorado primeiro.
    • Atualiza os custos à medida que explora novos nós.
    • Reconstrói o caminho utilizando um dicionário de predecessores.
import heapq

def uniform_cost_search(graph, start, goal):
    """
    Algoritmo de Busca com Custo Uniforme (UCS).
    A busca explora os nós com o menor custo acumulado até o momento.

    graph: Grafo representado como um dicionário onde as chaves são os nós e os valores
           são outros dicionários que representam os vizinhos e os custos de viagem.
    start: Nó inicial da busca.
    goal: Nó objetivo da busca.

    Retorna: O caminho encontrado entre o nó inicial e o objetivo.
    """

    # Fila de prioridade (min-heap) para explorar nós com o menor custo acumulado.
    open_list = []
    heapq.heappush(open_list, (0, start))  # (custo acumulado, nó)

    # Dicionário para armazenar os custos acumulados para cada nó.
    g_scores = {start: 0}

    # Dicionário para armazenar o nó predecessor de cada nó para reconstrução do caminho.
    came_from = {}

    while open_list:
        # Pega o nó com o menor custo acumulado da fila de prioridade.
        current_cost, current_node = heapq.heappop(open_list)

        # Se o nó objetivo for encontrado, reconstrua o caminho.
        if current_node == goal:
            path = []
            while current_node in came_from:
                path.append(current_node)
                current_node = came_from[current_node]
            path.append(start)  # Adiciona o nó inicial ao caminho.
            return path[::-1]  # Retorna o caminho na ordem correta.

        # Expande os vizinhos do nó atual.
        for neighbor, travel_cost in graph[current_node].items():
            tentative_cost = current_cost + travel_cost  # Calcula o custo até o vizinho.

            # Se o vizinho não foi explorado ou encontramos um caminho mais barato até ele.
            if neighbor not in g_scores or tentative_cost < g_scores[neighbor]:
                g_scores[neighbor] = tentative_cost
                heapq.heappush(open_list, (tentative_cost, neighbor))
                came_from[neighbor] = current_node  # Registra o nó predecessor.

    # Se o nó objetivo não for encontrado, retorna None.
    return None


# Exemplo de uso
graph = {
    'A': {'B': 1, 'C': 4},
    'B': {'A': 1, 'C': 2, 'D': 5},
    'C': {'A': 4, 'B': 2, 'D': 1},
    'D': {'B': 5, 'C': 1}
}

start = 'A'
goal = 'D'

path = uniform_cost_search(graph, start, goal)
print("Caminho encontrado:", path)

Explicação do Código:

  • Fila de Prioridade: Utilizada para escolher o próximo nó a ser explorado com base no menor custo acumulado.
  • Dicionários:
    • g_scores: Armazena o custo para alcançar cada nó.
    • came_from: Guarda o nó anterior para reconstruir o caminho.
  • Expansão de Nós:
    • Calcula o custo para cada vizinho.
    • Atualiza o custo e adiciona à fila se o novo caminho for mais barato.
  • Reconstrução: Reconstroi o caminho a partir do objetivo até o início.
  • Fila de Prioridade: Garante que o caminho encontrado seja o de menor custo.

Complexidade:

  • Tempo: O(E log V)
  • Espaço: O(V + E)

Considerações:

  • UCS é eficiente para grafos com custos variáveis.
  • Não utiliza heurísticas, podendo ser mais lento em grafos grandes.