Einführung in JGraphT
1. Überblick
Wenn wir graphbasierte Algorithmen implementieren, müssen wir meistens auch einige Dienstprogrammfunktionen implementieren.
JGraphT ist eine Open-Source-Java-Klassenbibliothek, die uns nicht nur verschiedene Arten von Diagrammen bietet, sondern auch viele nützliche Algorithmen zur Lösung der am häufigsten auftretenden Diagrammprobleme.
In diesem Artikel erfahren Sie, wie Sie verschiedene Arten von Diagrammen erstellen und wie einfach es ist, die bereitgestellten Dienstprogramme zu verwenden.
2. Maven-Abhängigkeit
Beginnen wir mit dem Hinzufügen der Abhängigkeit zu unserem Maven-Projekt:
org.jgrapht
jgrapht-core
1.0.1
Die neueste Version finden Sie unterMaven Central.
3. Diagramme erstellen
JGraphT unterstützt verschiedene Arten von Diagrammen.
3.1. Einfache Grafiken
Erstellen wir zunächst ein einfaches Diagramm mit einem Scheitelpunkt vom TypString:
Graph g
= new SimpleGraph<>(DefaultEdge.class);
g.addVertex(“v1”);
g.addVertex(“v2”);
g.addEdge(v1, v2);
3.2. Directed/Undirected Graphs
Es erlaubt uns auch, gerichtete / ungerichtete Graphen zu erstellen.
In unserem Beispiel erstellen wir ein gerichtetes Diagramm und verwenden es, um andere Dienstprogrammfunktionen und -algorithmen zu demonstrieren:
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. Vollständige Grafiken
In ähnlicher Weise können wir auch ein vollständiges Diagramm erstellen:
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-Graphen
Im Gegensatz zu einfachen Diagrammen bietet API auch Multigraphen (Diagramme mit mehreren Pfaden zwischen zwei Scheitelpunkten).
Außerdem können wir in jedem Diagramm gewichtete / ungewichtete oder benutzerdefinierte Kanten haben.
Erstellen wir einen Multigraph mit gewichteten Kanten:
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);
}
Darüber hinaus können sowohl Diagramme als auch Untergraphen nicht modifizierbar (schreibgeschützt) und hörbar (externe Listener können Änderungen verfolgen) sein. Außerdem können wir jederzeit alle Kompositionen dieser Diagramme erstellen.
Weitere API-Details finden Sie inhere.
4. API-Algorithmen
Nachdem wir nun vollständige Fledge-Graph-Objekte haben, schauen wir uns einige häufig auftretende Probleme und deren Lösungen an.
4.1. Graph Iteration
Wir können den Graphen mit verschiedenen Iteratoren wieBreadthFirstIterator,DepthFirstIterator,ClosestFirstIterator,RandomWalkIterator gemäß der Anforderung durchlaufen. Wir müssen lediglich eine Instanz der entsprechenden Iteratoren erstellen, indem wir Diagrammobjekte übergeben:
DepthFirstIterator depthFirstIterator
= new DepthFirstIterator<>(directedGraph);
BreadthFirstIterator breadthFirstIterator
= new BreadthFirstIterator<>(directedGraph);
Sobald wir die Iteratorobjekte erhalten haben, können wir die Iteration mit den MethodenhasNext() undnext() durchführen.
4.2. Den kürzesten Weg finden
Es bietet Implementierungen verschiedener Algorithmen wie Dijkstra, Bellman-Ford, Astar und FloydWarshall im Paketorg.jgrapht.alg.shortestpath.
Finden wir den kürzesten Weg mit dem Dijkstra-Algorithmus:
@Test
public void whenGetDijkstraShortestPath_thenGetNotNullPath() {
DijkstraShortestPath dijkstraShortestPath
= new DijkstraShortestPath(directedGraph);
List shortestPath = dijkstraShortestPath
.getPath("v1","v4").getVertexList();
assertNotNull(shortestPath);
}
So ermitteln Sie mit dem Bellman-Ford-Algorithmus den kürzesten Weg:
@Test
public void
whenGetBellmanFordShortestPath_thenGetNotNullPath() {
BellmanFordShortestPath bellmanFordShortestPath
= new BellmanFordShortestPath(directedGraph);
List shortestPath = bellmanFordShortestPath
.getPath("v1", "v4")
.getVertexList();
assertNotNull(shortestPath);
}
4.3. Suche nach stark verbundenen Untergraphen
Bevor wir mit der Implementierung beginnen, wollen wir uns kurz ansehen, was stark verbundene Untergraphen bedeuten. A subgraph is said to be strongly connected only if there is a path between each pair of its vertices.
In unserem Beispiel kann \ {v1, v2, v3, v4} als stark verbundener Untergraph betrachtet werden, wenn wir zu einem beliebigen Scheitelpunkt übergehen können, unabhängig davon, um welchen aktuellen Scheitelpunkt es sich handelt.
Wir können vier solcher Untergraphen für den im obigen Bild gezeigten gerichteten Graphen auflisten: {v9}, {v8}, \ {v5, v6, v7}, \ {v1, v2, v3, v4}
Implementierung, um alle stark verbundenen Untergraphen aufzulisten:
@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. Eulersche Schaltung
Eine Eulersche Schaltung in einem GraphenG ist eine Schaltung, die alle Eckpunkte und Kanten vonG enthält. Ein Graph, der es hat, ist ein Eulerscher Graph.
Schauen wir uns die Grafik an:
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);
});
}
Jetzt können wir mit der API testen, ob ein Diagramm Eulerian Circuit enthält:
@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. Hamiltonian Circuit
EinGraphPath, das jeden Scheitelpunkt genau einmal besucht, wird als Hamilton-Pfad bezeichnet.
Ein Hamilton-Zyklus (oder eine Hamilton-Schaltung) ist ein Hamilton-Pfad, so dass es eine Kante (in der Grafik) vom letzten Scheitelpunkt zum ersten Scheitelpunkt des Pfades gibt.
Wir können den optimalen Hamilton-Zyklus für einen vollständigen Graphen mit derHamiltonianCycle.getApproximateOptimalForCompleteGraph()-Methode finden.
Diese Methode gibt eine ungefähre minimale Handlungsreisetour (Hamilton-Zyklus) zurück. Die optimale Lösung ist NP-vollständig. Dies ist also eine gute Näherung, die in Polynomzeit ausgeführt wird:
public void
whenGetHamiltonianCyclePath_thenGetVerticeSequence() {
List verticeList = HamiltonianCycle
.getApproximateOptimalForCompleteGraph(completeGraph);
assertEquals(verticeList.size(), completeGraph.vertexSet().size());
}
4.6. Zyklusdetektor
Wir können auch prüfen, ob das Diagramm Zyklen enthält. Derzeit unterstütztCycleDetector nur gerichtete Diagramme:
@Test
public void whenCheckCycles_thenDetectCycles() {
CycleDetector cycleDetector
= new CycleDetector(directedGraph);
assertTrue(cycleDetector.detectCycles());
Set cycleVertices = cycleDetector.findCycles();
assertTrue(cycleVertices.size() > 0);
}
5. Diagrammvisualisierung
JGraphT allows us to generate visualizations of graphs and save them as images, fügen wir zuerst die Erweiterungsabhängigkeit vonjgrapht-extvon Maven Central hinzu:
org.jgrapht
jgrapht-ext
1.0.1
Als Nächstes erstellen wir ein einfaches gerichtetes Diagramm mit 3 Eckpunkten und 3 Kanten:
@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);
}
Wir können jetzt diesen Graphen visualisieren:
@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());
}
Hier haben wir einJGraphXAdapter erstellt, das unseren Graphen als Konstruktorargument erhält, und wir haben einmxCircleLayout darauf angewendet. Dadurch wird die Visualisierung kreisförmig angelegt.
Außerdem verwenden wirmxCellRenderer, umBufferedImage zu erstellen, und schreiben dann die Visualisierung in eine PNG-Datei.
Wir können das endgültige Bild in einem Browser oder unserem Lieblingsrenderer sehen:
Weitere Details finden Sie inofficial documentation.
6. Fazit
JGraphT bietet nahezu alle Arten von Grafiken und eine Vielzahl von Grafikalgorithmen. Wir haben uns mit der Verwendung weniger gängiger APIs befasst. Sie können die Bibliothek jedoch immer auf denofficial page durchsuchen.
Die Implementierung all dieser Beispiele und Codefragmente finden Sie inover on Github.