Einführung in JGraphT

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:

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. Vollständige Grafiken

In ähnlicher Weise können wir auch ein vollständiges Diagramm erstellen:

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-Graphen

image

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:

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);
    });
}

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:

image

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.