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:
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:
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
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:
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:
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.