Introdução ao JGraphT

Introdução ao JGraphT

1. Visão geral

Na maioria das vezes, quando estamos implementando algoritmos baseados em gráficos, também precisamos implementar algumas funções utilitárias.

JGraphT é uma biblioteca de classes Java de código aberto que não apenas nos fornece vários tipos de gráficos, mas também muitos algoritmos úteis para resolver os problemas de gráfico mais freqüentemente encontrados.

Neste artigo, veremos como criar diferentes tipos de gráficos e como é conveniente usar os utilitários fornecidos.

2. Dependência do Maven

Vamos começar adicionando a dependência ao nosso projeto Maven:


    org.jgrapht
    jgrapht-core
    1.0.1

A versão mais recente pode ser encontrada emMaven Central.

3. Criação de gráficos

O JGraphT suporta vários tipos de gráficos.

3.1. Gráficos simples

Para começar, vamos criar um gráfico simples com um vértice do tipoString:

Graph g
  = new SimpleGraph<>(DefaultEdge.class);
g.addVertex(“v1”);
g.addVertex(“v2”);
g.addEdge(v1, v2);

3.2. Directed/Undirected Graphs

Também nos permite criar gráficos direcionados / não direcionados.

Em nosso exemplo, criaremos um gráfico direcionado e o usaremos para demonstrar outras funções e algoritmos de utilidade:

image

DirectedGraph directedGraph
  = new DefaultDirectedGraph<>(DefaultEdge.class);
directedGraph.addVertex("v1");
directedGraph.addVertex("v2");
directedGraph.addVertex("v3");
directedGraph.addEdge("v1", "v2");
// Add remaining vertices and edges

3.3. Gráficos Completos

Da mesma forma, também podemos gerar um gráfico completo:

image

public void createCompleteGraph() {
    completeGraph = new SimpleWeightedGraph<>(DefaultEdge.class);
    CompleteGraphGenerator completeGenerator
      = new CompleteGraphGenerator<>(size);
    VertexFactory vFactory = new VertexFactory() {
        private int id = 0;
        public String createVertex() {
            return "v" + id++;
        }
    };
    completeGenerator.generateGraph(completeGraph, vFactory, null);
}

3.4. Multi-gráficos

image

Além dos gráficos simples, a API também fornece multigrafos (gráficos com vários caminhos entre dois vértices).

Além disso, podemos ter arestas ponderadas / não ponderadas ou definidas pelo usuário em qualquer gráfico.

Vamos criar um multigrafo com bordas ponderadas:

public void createMultiGraphWithWeightedEdges() {
    multiGraph = new Multigraph<>(DefaultWeightedEdge.class);
    multiGraph.addVertex("v1");
    multiGraph.addVertex("v2");
    DefaultWeightedEdge edge1 = multiGraph.addEdge("v1", "v2");
    multiGraph.setEdgeWeight(edge1, 5);

    DefaultWeightedEdge edge2 = multiGraph.addEdge("v1", "v2");
    multiGraph.setEdgeWeight(edge2, 3);
}

Além disso, podemos ter gráficos não modificáveis ​​(somente leitura) e escutáveis ​​(permite que ouvintes externos controlem modificações), bem como subgráficos. Além disso, sempre podemos criar todas as composições desses gráficos.

Mais detalhes da API podem ser encontradoshere.

4. Algoritmos API

Agora que temos objetos gráficos completos, vamos dar uma olhada em alguns problemas comuns e suas soluções.

4.1. Iteração de gráfico

Podemos percorrer o gráfico usando vários iteradores, comoBreadthFirstIterator,DepthFirstIterator,ClosestFirstIterator,RandomWalkIterator de acordo com o requisito. Precisamos simplesmente criar uma instância dos respectivos iteradores passando objetos de gráfico:

DepthFirstIterator depthFirstIterator
  = new DepthFirstIterator<>(directedGraph);
BreadthFirstIterator breadthFirstIterator
  = new BreadthFirstIterator<>(directedGraph);

Depois de obter os objetos iteradores, podemos realizar a iteração usando os métodoshasNext()enext().

4.2. Encontrando o caminho mais curto

Ele fornece implementações de vários algoritmos, como Dijkstra, Bellman-Ford, Astar e FloydWarshall no pacoteorg.jgrapht.alg.shortestpath.

Vamos encontrar o caminho mais curto usando o algoritmo de Dijkstra:

@Test
public void whenGetDijkstraShortestPath_thenGetNotNullPath() {
    DijkstraShortestPath dijkstraShortestPath
      = new DijkstraShortestPath(directedGraph);
    List shortestPath = dijkstraShortestPath
      .getPath("v1","v4").getVertexList();

    assertNotNull(shortestPath);
}

Da mesma forma, para obter o caminho mais curto usando o algoritmo Bellman-Ford:

@Test
public void
  whenGetBellmanFordShortestPath_thenGetNotNullPath() {
    BellmanFordShortestPath bellmanFordShortestPath
      = new BellmanFordShortestPath(directedGraph);
    List shortestPath = bellmanFordShortestPath
      .getPath("v1", "v4")
      .getVertexList();

    assertNotNull(shortestPath);
}

4.3. Encontrando subgráficos fortemente conectados

Antes de começarmos a implementação, vamos ver brevemente o que significam subgráficos fortemente conectados. A subgraph is said to be strongly connected only if there is a path between each pair of its vertices.

No nosso exemplo, \ {v1, v2, v3, v4} pode ser considerado um subgrafo fortemente conectado se pudermos atravessar para qualquer vértice, independentemente do que seja o vértice atual.

Podemos listar quatro subgráficos para o gráfico direcionado mostrado na imagem acima: {v9}, {v8}, \ {v5, v6, v7}, \ {v1, v2, v3, v4}

Implementação para listar todos os subgráficos fortemente conectados:

@Test
public void
  whenGetStronglyConnectedSubgraphs_thenPathExists() {

    StrongConnectivityAlgorithm scAlg
      = new KosarajuStrongConnectivityInspector<>(directedGraph);
    List> stronglyConnectedSubgraphs
      = scAlg.stronglyConnectedSubgraphs();
    List stronglyConnectedVertices
      = new ArrayList<>(stronglyConnectedSubgraphs.get(3)
      .vertexSet());

    String randomVertex1 = stronglyConnectedVertices.get(0);
    String randomVertex2 = stronglyConnectedVertices.get(3);
    AllDirectedPaths allDirectedPaths
      = new AllDirectedPaths<>(directedGraph);

    List> possiblePathList
      = allDirectedPaths.getAllPaths(
        randomVertex1, randomVertex2, false,
          stronglyConnectedVertices.size());

    assertTrue(possiblePathList.size() > 0);
}

4.4. Circuito Euleriano

Um Circuito Euleriano em um gráficoG é um circuito que inclui todos os vértices e arestas deG. Um gráfico que possui é um gráfico euleriano.

Vamos dar uma olhada no gráfico:

image

public void createGraphWithEulerianCircuit() {
    SimpleWeightedGraph simpleGraph
      = new SimpleWeightedGraph<>(DefaultEdge.class);
    IntStream.range(1,5)
      .forEach(i-> simpleGraph.addVertex("v" + i));
    IntStream.range(1,5)
      .forEach(i-> {
        int endVertexNo = (i + 1) > 5 ? 1 : i + 1;
        simpleGraph.addEdge("v" + i,"v" + endVertexNo);
    });
}

Agora, podemos testar se um gráfico contém Circuito Euleriano usando a API:

@Test
public void givenGraph_whenCheckEluerianCycle_thenGetResult() {
    HierholzerEulerianCycle eulerianCycle
      = new HierholzerEulerianCycle<>();

    assertTrue(eulerianCycle.isEulerian(simpleGraph));
}
@Test
public void whenGetEulerianCycle_thenGetGraphPath() {
    HierholzerEulerianCycle eulerianCycle
      = new HierholzerEulerianCycle<>();
    GraphPath path = eulerianCycle.getEulerianCycle(simpleGraph);

    assertTrue(path.getEdgeList()
      .containsAll(simpleGraph.edgeSet()));
}

4.5. Circuito Hamiltoniano

UmGraphPath que visita cada vértice exatamente uma vez é conhecido como Caminho Hamiltoniano.

Um ciclo hamiltoniano (ou circuito hamiltoniano) é um caminho hamiltoniano em que há uma aresta (no gráfico) do último vértice ao primeiro vértice do caminho.

Podemos encontrar o Ciclo Hamiltoniano ideal para um gráfico completo com o métodoHamiltonianCycle.getApproximateOptimalForCompleteGraph().

Esse método retornará um tour mínimo aproximado de um vendedor ambulante (ciclo Hamiltoniano). A solução ideal é NP-completa, portanto, essa é uma aproximação decente que é executada em tempo polinomial:

public void
  whenGetHamiltonianCyclePath_thenGetVerticeSequence() {
    List verticeList = HamiltonianCycle
      .getApproximateOptimalForCompleteGraph(completeGraph);

    assertEquals(verticeList.size(), completeGraph.vertexSet().size());
}

4.6. Detector de Ciclo

Também podemos verificar se há algum ciclo no gráfico. Atualmente,CycleDetector suporta apenas gráficos direcionados:

@Test
public void whenCheckCycles_thenDetectCycles() {
    CycleDetector cycleDetector
      = new CycleDetector(directedGraph);

    assertTrue(cycleDetector.detectCycles());
    Set cycleVertices = cycleDetector.findCycles();

    assertTrue(cycleVertices.size() > 0);
}

5. Visualização de gráfico

JGraphT allows us to generate visualizations of graphs and save them as images, primeiro vamos adicionar a dependência de extensãojgrapht-ext do Maven Central:


    org.jgrapht
    jgrapht-ext
    1.0.1

A seguir, vamos criar um gráfico direcionado simples com 3 vértices e 3 arestas:

@Before
public void createGraph() {

    File imgFile = new File("src/test/resources/graph.png");
    imgFile.createNewFile();

    DefaultDirectedGraph g =
      new DefaultDirectedGraph(DefaultEdge.class);

    String x1 = "x1";
    String x2 = "x2";
    String x3 = "x3";

    g.addVertex(x1);
    g.addVertex(x2);
    g.addVertex(x3);

    g.addEdge(x1, x2);
    g.addEdge(x2, x3);
    g.addEdge(x3, x1);
}

Agora podemos visualizar este gráfico:

@Test
public void givenAdaptedGraph_whenWriteBufferedImage_thenFileShouldExist() throws IOException {

    JGraphXAdapter graphAdapter =
      new JGraphXAdapter(g);
    mxIGraphLayout layout = new mxCircleLayout(graphAdapter);
    layout.execute(graphAdapter.getDefaultParent());

    BufferedImage image =
      mxCellRenderer.createBufferedImage(graphAdapter, null, 2, Color.WHITE, true, null);
    File imgFile = new File("src/test/resources/graph.png");
    ImageIO.write(image, "PNG", imgFile);

    assertTrue(imgFile.exists());
}

Aqui criamos umJGraphXAdapter que recebe nosso gráfico como um argumento do construtor e aplicamos ummxCircleLayout a ele. Isso estabelece a visualização de maneira circular.

Além disso, usamos ummxCellRenderer para criar umBufferedImagee, em seguida, gravamos a visualização em um arquivo png.

Podemos ver a imagem final em um navegador ou em nosso renderizador favorito:

image

Podemos encontrar mais detalhes emofficial documentation.

6. Conclusão

O JGraphT fornece quase todos os tipos de gráficos e variedade de algoritmos de gráficos. Abordamos como usar algumas APIs populares. No entanto, você sempre pode explorar a biblioteca noofficial page.

A implementação de todos esses exemplos e trechos de código pode ser encontradaover on Github.