Algoritmo de caminho mais curto em Java

Algoritmo de caminho mais curto em Java

1. Visão geral

A ênfase neste artigo é o problema do caminho mais curto (SPP), sendo um dos problemas teóricos fundamentais conhecidos na teoria dos grafos e como o algoritmo de Dijkstra pode ser usado para resolvê-lo.

O objetivo básico do algoritmo é determinar o caminho mais curto entre um nó inicial e o restante do gráfico.

2. Problema de caminho mais curto com Dijkstra

Dado um gráfico ponderado positivamente e um nó inicial (A), Dijkstra determina o caminho e a distância mais curtos da origem para todos os destinos no gráfico:

image

A idéia central do algoritmo Dijkstra é eliminar continuamente caminhos mais longos entre o nó inicial e todos os destinos possíveis.

Para acompanhar o processo, precisamos ter dois conjuntos distintos de nós, resolvidos e não configurados.

Nós resolvidos são aqueles com uma distância mínima conhecida da fonte. O conjunto de nós não resolvidos reúne nós que podemos alcançar a partir da fonte, mas não sabemos a distância mínima do nó inicial.

Aqui está uma lista de etapas a seguir para resolver o SPP com Dijkstra:

  • Defina a distância parastartNode para zero.

  • Defina todas as outras distâncias para um valor infinito.

  • AdicionamosstartNode ao conjunto de nós não resolvidos.

  • Enquanto o conjunto de nós não resolvidos não estiver vazio, nós:

    • Escolha um nó de avaliação no conjunto de nós não configurados; o nó de avaliação deve ser aquele com a menor distância da origem.

    • Calcule novas distâncias para direcionar vizinhos, mantendo a menor distância em cada avaliação.

    • Adicione vizinhos que ainda não foram resolvidos ao conjunto de nós não configurados.

Essas etapas podem ser agregadas em dois estágios, Inicialização e Avaliação. Vamos ver como isso se aplica ao nosso gráfico de amostra:

2.1. Inicialização

Antes de começarmos a explorar todos os caminhos no gráfico, precisamos primeiro inicializar todos os nós com uma distância infinita e um predecessor desconhecido, exceto a fonte.

Como parte do processo de inicialização, precisamos atribuir o valor 0 ao nó A (sabemos que a distância do nó A ao nó A é 0, obviamente)

Portanto, cada nó no restante do gráfico será diferenciado com um predecessor e uma distância:

image

Para concluir o processo de inicialização, precisamos adicionar o nó A aos nós não configurados e configurá-lo para ser escolhido primeiro na etapa de avaliação. Lembre-se de que o conjunto de nós resolvidos ainda está vazio.

2.2. Avaliação

Agora que inicializamos nosso gráfico, escolhemos o nó com a menor distância do conjunto não configurado e avaliamos todos os nós adjacentes que não estão nos nós estabelecidos:

image

A ideia é adicionar o peso da aresta à distância do nó de avaliação e, em seguida, compará-lo com a distância do destino. e.g. para o nó B, 0 + 10 é menor que INFINITY, portanto, a nova distância para o nó B é 10 e o novo predecessor é A, o mesmo se aplica ao nó C.

O nó A é então movido dos nós não configurados configurados para os nós estabelecidos.

Os nós B e C são adicionados aos nós não configurados porque podem ser alcançados, mas precisam ser avaliados.

Agora que temos dois nós no conjunto não configurado, escolhemos aquele com a menor distância (nó B) e reiteramos até resolvermos todos os nós no gráfico:

image

Aqui está uma tabela que resume as iterações que foram realizadas durante as etapas de avaliação:

Iteração

Não resolvido

Resolvido

EvaluationNode

A

B

C

D

E

F

1

A

A

0

A-10

A-15

X-∞

X-∞

X-∞

2

B, C

A

B

0

A-10

A-15

B-22

X-∞

B-25

3

C, F, D

A, B

C

0

A-10

A-15

B-22

C-25

B-25

4

D, E, F

A, B, C

D

0

A-10

A-15

B-22

D-24

D-23

5

E, F

A, B, C, D

F

0

A-10

A-15

B-22

D-24

D-23

6

E

A, B, C, D, F

E

0

A-10

A-15

B-22

D-24

D-23

Final

ALL

NONE

0

A-10

A-15

B-22

D-24

D-23

 

A notação B-22, por exemplo, significa que o nó B é o antecessor imediato, com uma distância total de 22 do nó A.

Finalmente, podemos calcular os caminhos mais curtos do nó A são os seguintes:

  • Nó B: A -> B (distância total = 10)

  • Nó C: A -> C (distância total = 15)

  • Nó D: A -> B -> D (distância total = 22)

  • Nó E: A -> B -> D -> E (distância total = 24)

  • Nó F: A -> B -> D -> F (distância total = 23)

3. Implementação Java

Nesta implementação simples, representaremos um gráfico como um conjunto de nós:

public class Graph {

    private Set nodes = new HashSet<>();

    public void addNode(Node nodeA) {
        nodes.add(nodeA);
    }

    // getters and setters
}

Um nó pode ser descrito comname, aLinkedList em referência ashortestPath, adistance da fonte e uma lista de adjacências chamadaadjacentNodes:

public class Node {

    private String name;

    private List shortestPath = new LinkedList<>();

    private Integer distance = Integer.MAX_VALUE;

    Map adjacentNodes = new HashMap<>();

    public void addDestination(Node destination, int distance) {
        adjacentNodes.put(destination, distance);
    }

    public Node(String name) {
        this.name = name;
    }

    // getters and setters
}

O atributoadjacentNodes é usado para associar vizinhos imediatos ao comprimento da borda. Esta é uma implementação simplificada de uma lista de adjacência, mais adequada ao algoritmo Dijkstra do que à matriz de adjacência.

Já o atributoshortestPath é uma lista de nós que descreve o caminho mais curto calculado a partir do nó inicial.

Por padrão, todas as distâncias de nó são inicializadas comInteger.MAX_VALUE para simular uma distância infinita, conforme descrito na etapa de inicialização.

Agora, vamos implementar o algoritmo Dijkstra:

public static Graph calculateShortestPathFromSource(Graph graph, Node source) {
    source.setDistance(0);

    Set settledNodes = new HashSet<>();
    Set unsettledNodes = new HashSet<>();

    unsettledNodes.add(source);

    while (unsettledNodes.size() != 0) {
        Node currentNode = getLowestDistanceNode(unsettledNodes);
        unsettledNodes.remove(currentNode);
        for (Entry < Node, Integer> adjacencyPair:
          currentNode.getAdjacentNodes().entrySet()) {
            Node adjacentNode = adjacencyPair.getKey();
            Integer edgeWeight = adjacencyPair.getValue();
            if (!settledNodes.contains(adjacentNode)) {
                calculateMinimumDistance(adjacentNode, edgeWeight, currentNode);
                unsettledNodes.add(adjacentNode);
            }
        }
        settledNodes.add(currentNode);
    }
    return graph;
}

O métodogetLowestDistanceNode() retorna o nó com a menor distância dos nós não definidos definidos, enquanto o métodocalculateMinimumDistance() compara a distância real com a recém-calculada enquanto segue o caminho recém-explorado:

private static Node getLowestDistanceNode(Set < Node > unsettledNodes) {
    Node lowestDistanceNode = null;
    int lowestDistance = Integer.MAX_VALUE;
    for (Node node: unsettledNodes) {
        int nodeDistance = node.getDistance();
        if (nodeDistance < lowestDistance) {
            lowestDistance = nodeDistance;
            lowestDistanceNode = node;
        }
    }
    return lowestDistanceNode;
}
private static void CalculateMinimumDistance(Node evaluationNode,
  Integer edgeWeigh, Node sourceNode) {
    Integer sourceDistance = sourceNode.getDistance();
    if (sourceDistance + edgeWeigh < evaluationNode.getDistance()) {
        evaluationNode.setDistance(sourceDistance + edgeWeigh);
        LinkedList shortestPath = new LinkedList<>(sourceNode.getShortestPath());
        shortestPath.add(sourceNode);
        evaluationNode.setShortestPath(shortestPath);
    }
}

Agora que todas as peças necessárias estão no lugar, vamos aplicar o algoritmo de Dijkstra no gráfico de amostra que é o assunto do artigo:

Node nodeA = new Node("A");
Node nodeB = new Node("B");
Node nodeC = new Node("C");
Node nodeD = new Node("D");
Node nodeE = new Node("E");
Node nodeF = new Node("F");

nodeA.addDestination(nodeB, 10);
nodeA.addDestination(nodeC, 15);

nodeB.addDestination(nodeD, 12);
nodeB.addDestination(nodeF, 15);

nodeC.addDestination(nodeE, 10);

nodeD.addDestination(nodeE, 2);
nodeD.addDestination(nodeF, 1);

nodeF.addDestination(nodeE, 5);

Graph graph = new Graph();

graph.addNode(nodeA);
graph.addNode(nodeB);
graph.addNode(nodeC);
graph.addNode(nodeD);
graph.addNode(nodeE);
graph.addNode(nodeF);

graph = Dijkstra.calculateShortestPathFromSource(graph, nodeA);

Após o cálculo, os atributosshortestPathedistance são definidos para cada nó do gráfico, podemos iterar por meio deles para verificar se os resultados correspondem exatamente ao que foi encontrado na seção anterior.

4. Conclusão

Neste artigo, vimos como o algoritmo Dijkstra resolve o SPP e como implementá-lo em Java.

A implementação deste projeto simples pode ser encontrada nos seguintesGitHub project link.