«1. Обзор

Большую часть времени, когда мы реализуем алгоритмы на основе графов, нам также необходимо реализовать некоторые служебные функции.

JGraphT — это библиотека классов Java с открытым исходным кодом, которая не только предоставляет нам различные типы графиков, но и множество полезных алгоритмов для решения наиболее часто встречающихся проблем с графами.

В этой статье мы увидим, как создавать различные типы графиков и насколько удобно пользоваться предоставленными утилитами.

2. Зависимость Maven

Давайте начнем с добавления зависимости в наш проект Maven:

<dependency>
    <groupId>org.jgrapht</groupId>
    <artifactId>jgrapht-core</artifactId>
    <version>1.0.1</version>
</dependency>

Последнюю версию можно найти на Maven Central.

3. Создание графиков

JGraphT поддерживает различные типы графиков.

3.1. Простые графы

Для начала создадим простой граф с вершиной типа String:

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

3.2. Ориентированные/неориентированные графы

Это также позволяет нам создавать ориентированные/неориентированные графы.

В нашем примере мы создадим ориентированный граф и будем использовать его для демонстрации других полезных функций и алгоритмов:

DirectedGraph<String, DefaultEdge> 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. Полные графы

Точно так же мы можем сгенерировать полный граф:

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

3.4. Мультиграфы

Помимо простых графов API также предоставляет нам мультиграфы (графы с несколькими путями между двумя вершинами).

Кроме того, у нас могут быть взвешенные/невзвешенные или определяемые пользователем ребра в любом графе.

Давайте создадим мультиграф со взвешенными ребрами:

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

В дополнение к этому у нас могут быть неизменяемые (только для чтения) и прослушиваемые (позволяет внешним слушателям отслеживать модификации) графы, а также подграфы. Также мы всегда можем создать все композиции этих графиков.

Дополнительные сведения об API можно найти здесь.

4. Алгоритмы API

Теперь, когда у нас есть полноценные графовые объекты, давайте рассмотрим некоторые распространенные проблемы и их решения.

4.1. Итерация графа

Мы можем перемещаться по графу, используя различные итераторы, такие как BreadthFirstIterator, DepthFirstIterator, ClosestFirstIterator, RandomWalkIterator в соответствии с требованиями. Нам просто нужно создать экземпляр соответствующих итераторов, передав объекты графа:

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

Получив объекты итератора, мы можем выполнить итерацию, используя методы hasNext() и next().

4.2. Поиск кратчайшего пути

Он предоставляет реализации различных алгоритмов, таких как Dijkstra, Bellman-Ford, Astar и FloydWarshall, в пакете org.jgrapht.alg.shortestpath.

Найдем кратчайший путь, используя алгоритм Дейкстры:

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

Аналогично, чтобы найти кратчайший путь, используя алгоритм Беллмана-Форда:

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

4.3. Поиск сильно связанных подграфов

Прежде чем мы перейдем к реализации, давайте кратко рассмотрим, что означают сильно связанные подграфы. Подграф называется сильно связным, только если существует путь между каждой парой его вершин.

В нашем примере {v1,v2,v3,v4} можно считать сильно связным подграфом, если мы можем перейти к любой вершине, независимо от того, какая вершина является текущей.

Мы можем перечислить четыре таких подграфа для ориентированного графа, показанного на изображении выше: {v9},{v8},{v5,v6,v7},{v1,v2,v3,v4}

Реализация для списка вывести все сильносвязные подграфы:

@Test
public void 
  whenGetStronglyConnectedSubgraphs_thenPathExists() {

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

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

    List<GraphPath<String, DefaultEdge>> possiblePathList 
      = allDirectedPaths.getAllPaths(
        randomVertex1, randomVertex2, false,
          stronglyConnectedVertices.size());
 
    assertTrue(possiblePathList.size() > 0);
}

4.4. Эйлерова цепь

Эйлерова цепь в графе G — это цепь, включающая все вершины и ребра графа G. Граф, в котором она есть, является эйлеровым графом.

Давайте посмотрим на граф:

public void createGraphWithEulerianCircuit() {
    SimpleWeightedGraph<String, DefaultEdge> 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);
    });
}

Теперь мы можем проверить, содержит ли граф Эйлеров цикл, используя 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. Гамильтонова цепь

Путь GraphPath, который посещает каждую вершину ровно один раз, называется гамильтоновым путем.

Гамильтонов цикл (или гамильтонова схема) — это гамильтонов путь, такой, что существует ребро (в графе) от последней вершины до первой вершины пути.

Мы можем найти оптимальный гамильтонов цикл для полного графа с помощью метода HamiltonianCycle.getApproximateOptimalForCompleteGraph().

Этот метод вернет приблизительный минимальный тур коммивояжера (гамильтоновский цикл). Оптимальное решение является NP-полным, так что это хорошее приближение, работающее за полиномиальное время:

public void 
  whenGetHamiltonianCyclePath_thenGetVerticeSequence() {
    List<String> verticeList = HamiltonianCycle
      .getApproximateOptimalForCompleteGraph(completeGraph);
 
    assertEquals(verticeList.size(), completeGraph.vertexSet().size());
}

«

«4.6. Детектор циклов

@Test
public void whenCheckCycles_thenDetectCycles() {
    CycleDetector<String, DefaultEdge> cycleDetector 
      = new CycleDetector<String, DefaultEdge>(directedGraph);
 
    assertTrue(cycleDetector.detectCycles());
    Set<String> cycleVertices = cycleDetector.findCycles();
 
    assertTrue(cycleVertices.size() > 0);
}

Мы также можем проверить, есть ли циклы в графе. В настоящее время CycleDetector поддерживает только ориентированные графы:

5. Визуализация графов

<dependency>
    <groupId>org.jgrapht</groupId>
    <artifactId>jgrapht-ext</artifactId>
    <version>1.0.1</version>
</dependency>

JGraphT позволяет нам создавать визуализации графов и сохранять их в виде изображений, сначала давайте добавим зависимость расширения jgrapht-ext от Maven Central: ~~ ~

@Before
public void createGraph() {

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

    DefaultDirectedGraph<String, DefaultEdge> g = 
      new DefaultDirectedGraph<String, DefaultEdge>(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);
}

Далее давайте создадим простой ориентированный граф с 3 вершинами и 3 ребрами:

@Test
public void givenAdaptedGraph_whenWriteBufferedImage_thenFileShouldExist() throws IOException {

    JGraphXAdapter<String, DefaultEdge> graphAdapter = 
      new JGraphXAdapter<String, DefaultEdge>(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());
}

Теперь мы можем визуализировать этот граф:

Здесь мы создали JGraphXAdapter, который получает наш граф как конструктора, и мы применили к нему mxCircleLayout. Это выстраивает визуализацию по кругу.

Кроме того, мы используем mxCellRenderer для создания BufferedImage, а затем записываем визуализацию в файл png.

Готовое изображение мы можем увидеть в браузере или в нашем любимом рендерере:

Подробности можно найти в официальной документации.

6. Заключение

JGraphT предоставляет почти все типы графов и различные графовые алгоритмы. Мы рассмотрели, как использовать несколько популярных API. Однако вы всегда можете ознакомиться с библиотекой на официальной странице.