Introduction à JGraphT

Introduction à JGraphT

1. Vue d'ensemble

La plupart du temps, lorsque nous implémentons des algorithmes basés sur des graphiques, nous devons également implémenter certaines fonctions utilitaires.

JGraphT est une bibliothèque de classes Java open source qui nous fournit non seulement divers types de graphiques, mais également de nombreux algorithmes utiles pour résoudre les problèmes de graphes les plus fréquemment rencontrés.

Dans cet article, nous verrons comment créer différents types de graphiques et comment il est pratique d’utiliser les utilitaires fournis.

2. Dépendance Maven

Commençons par ajouter la dépendance à notre projet Maven:


    org.jgrapht
    jgrapht-core
    1.0.1

La dernière version peut être trouvée dans leMaven Central.

3. Créer des graphiques

JGraphT supporte différents types de graphes.

3.1. Graphiques simples

Pour commencer, créons un graphique simple avec un sommet de typeString:

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

3.2. Directed/Undirected Graphs

Cela nous permet également de créer des graphes dirigés / non dirigés.

Dans notre exemple, nous allons créer un graphe dirigé et l’utiliser pour illustrer d’autres algorithmes et fonctions d’utilité:

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. Graphiques complets

De même, nous pouvons également générer un graphe complet:

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

image

Outre les graphes simples, l’API nous fournit également des multigraphes (graphes avec des chemins multiples entre deux sommets).

En outre, nous pouvons avoir des arêtes pondérées / non pondérées ou définies par l'utilisateur dans n'importe quel graphique.

Créons un multigraphe avec des arêtes pondérées:

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

En plus de cela, nous pouvons avoir des graphiques ainsi que des sous-graphiques non modifiables (en lecture seule) et écoutables (permettant aux auditeurs externes de suivre les modifications). De plus, nous pouvons toujours créer toutes les compositions de ces graphiques.

Des détails supplémentaires sur l'API peuvent être trouvéshere.

4. Algorithmes d'API

Maintenant que nous avons des objets graphiques complets, examinons quelques problèmes courants et leurs solutions.

4.1. Itération de graphe

Nous pouvons parcourir le graphique en utilisant divers itérateurs tels queBreadthFirstIterator,DepthFirstIterator,ClosestFirstIterator,RandomWalkIterator selon l'exigence. Nous devons simplement créer une instance d'itérateurs respectifs en passant des objets de graphe:

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

Une fois que nous obtenons les objets itérateur, nous pouvons effectuer l'itération en utilisant les méthodeshasNext() etnext().

4.2. Trouver le chemin le plus court

Il fournit des implémentations de divers algorithmes tels que Dijkstra, Bellman-Ford, Astar et FloydWarshall dans le packageorg.jgrapht.alg.shortestpath.

Trouvons le chemin le plus court à l’aide de l’algorithme de Dijkstra:

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

    assertNotNull(shortestPath);
}

De même, pour obtenir le chemin le plus court en utilisant l'algorithme Bellman-Ford:

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

    assertNotNull(shortestPath);
}

4.3. Recherche de sous-graphes fortement connectés

Avant de commencer, voyons brièvement ce que signifient les sous-graphes fortement connectés. A subgraph is said to be strongly connected only if there is a path between each pair of its vertices.

Dans notre exemple, \ {v1, v2, v3, v4} peut être considéré comme un sous-graphe fortement connecté si nous pouvons passer à n’importe quel sommet indépendamment de la nature du sommet actuel.

Nous pouvons lister quatre de ces sous-graphes pour le graphe orienté montré dans l'image ci-dessus: {v9}, {v8}, \ {v5, v6, v7}, \ {v1, v2, v3, v4}

Implémentation pour lister tous les sous-graphes fortement connectés:

@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. Circuit eulérien

Un circuit eulérien dans un grapheG est un circuit qui comprend tous les sommets et arêtes deG. Un graphe qui l'a est un graphe eulérien.

Jetons un œil au graphique:

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

Nous pouvons maintenant vérifier si un graphique contient un circuit eulérien à l'aide de l'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. Circuit hamiltonien

UnGraphPath qui visite chaque sommet exactement une fois est appelé chemin hamiltonien.

Un cycle hamiltonien (ou circuit hamiltonien) est un chemin hamiltonien tel qu'il existe une arête (dans le graphique) du dernier sommet au premier sommet du chemin.

Nous pouvons trouver le cycle hamiltonien optimal pour un graphe complet avec la méthodeHamiltonianCycle.getApproximateOptimalForCompleteGraph().

Cette méthode renverra un circuit approximatif de vendeur voyageur (cycle hamiltonien). La solution optimale est NP-complete. Il s’agit donc d’une approximation décente fonctionnant en temps polynomial:

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

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

4.6. Détecteur de cycle

Nous pouvons également vérifier s'il y a des cycles dans le graphique. Actuellement,CycleDetector ne prend en charge que les graphiques orientés:

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

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

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

5. Visualisation graphique

JGraphT allows us to generate visualizations of graphs and save them as images, ajoutons d'abord la dépendance d'extensionjgrapht-ext depuis Maven Central:


    org.jgrapht
    jgrapht-ext
    1.0.1

Ensuite, créons un graphique orienté simple avec 3 sommets et 3 arêtes:

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

Nous pouvons maintenant visualiser ce graphique:

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

Ici, nous avons créé unJGraphXAdapter qui reçoit notre graphe en tant qu'argument constructeur et nous lui avons appliqué unmxCircleLayout . Ceci établit la visualisation de manière circulaire.

De plus, nous utilisons unmxCellRenderer pour créer unBufferedImage et ensuite écrire la visualisation dans un fichier png.

Nous pouvons voir l'image finale dans un navigateur ou notre moteur de rendu préféré:

image

Nous pouvons trouver plus de détails dans lesofficial documentation.

6. Conclusion

JGraphT fournit presque tous les types de graphiques et divers algorithmes de graphes. Nous avons expliqué comment utiliser quelques API populaires. Cependant, vous pouvez toujours explorer la bibliothèque sur lesofficial page.

L'implémentation de tous ces exemples et extraits de code peut être trouvéeover on Github.