Введение в JGraphT
1. обзор
В большинстве случаев, когда мы реализуем алгоритмы на основе графов, нам также необходимо реализовать некоторые служебные функции.
JGraphT - это библиотека классов Java с открытым исходным кодом, которая не только предоставляет нам различные типы графиков, но и множество полезных алгоритмов для решения наиболее часто встречающихся проблем с графами.
В этой статье мы увидим, как создавать различные типы графиков и насколько удобно использовать предоставленные утилиты.
2. Maven Dependency
Начнем с добавления зависимости в наш проект Maven:
org.jgrapht
jgrapht-core
1.0.1
Последнюю версию можно найти наMaven Central.
3. Создание графиков
JGraphT поддерживает различные типы графиков.
3.1. Простые графики
Для начала создадим простой граф с вершиной типаString:
Graph g
= new SimpleGraph<>(DefaultEdge.class);
g.addVertex(“v1”);
g.addVertex(“v2”);
g.addEdge(v1, v2);
3.2. Directed/Undirected Graphs
Это также позволяет нам создавать ориентированные / ненаправленные графы.
В нашем примере мы создадим ориентированный граф и используем его для демонстрации других служебных функций и алгоритмов:
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. Полные графики
Точно так же мы можем также сгенерировать полный график:
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. Мульти-графы
Помимо простых графиков, 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 можно найти вhere.
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 shortestPath = dijkstraShortestPath
.getPath("v1","v4").getVertexList();
assertNotNull(shortestPath);
}
Аналогично, чтобы получить кратчайший путь, используя алгоритм Беллмана-Форда:
@Test
public void
whenGetBellmanFordShortestPath_thenGetNotNullPath() {
BellmanFordShortestPath bellmanFordShortestPath
= new BellmanFordShortestPath(directedGraph);
List shortestPath = bellmanFordShortestPath
.getPath("v1", "v4")
.getVertexList();
assertNotNull(shortestPath);
}
4.3. Поиск сильносвязных подграфов
Прежде чем мы перейдем к реализации, давайте кратко рассмотрим, что означают сильно связанные подграфы. A subgraph is said to be strongly connected only if there is a path between each pair of its vertices.с
В нашем примере \ {v1, v2, v3, v4} можно считать сильно связным подграфом, если мы можем перейти к любой вершине, независимо от того, какой является текущая вершина.
Мы можем перечислить четыре таких подграфа для ориентированного графа, показанного на изображении выше: {v9}, {v8}, \ {v5, v6, v7}, \ {v1, v2, v3, v4}
Реализация перечислить все сильно связанные подграфы:
@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. Схема Эйлера
Эйлерова схема в графеG - это схема, которая включает в себя все вершины и ребраG. Граф, который имеет это, является эйлеровым графом.
Посмотрим на график:
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);
});
}
Теперь мы можем проверить, содержит ли граф Eulerian Circuit, используя 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 verticeList = HamiltonianCycle
.getApproximateOptimalForCompleteGraph(completeGraph);
assertEquals(verticeList.size(), completeGraph.vertexSet().size());
}
4.6. Детектор цикла
Мы также можем проверить, есть ли циклы в графе. В настоящее времяCycleDetector поддерживает только ориентированные графы:
@Test
public void whenCheckCycles_thenDetectCycles() {
CycleDetector cycleDetector
= new CycleDetector(directedGraph);
assertTrue(cycleDetector.detectCycles());
Set cycleVertices = cycleDetector.findCycles();
assertTrue(cycleVertices.size() > 0);
}
5. Визуализация графика
JGraphT allows us to generate visualizations of graphs and save them as images, сначала давайте добавим зависимость расширенияjgrapht-ext из Maven Central:
org.jgrapht
jgrapht-ext
1.0.1
Затем давайте создадим простой ориентированный граф с 3 вершинами и 3 ребрами:
@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);
}
Теперь мы можем визуализировать этот график:
@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());
}
Здесь мы создалиJGraphXAdapter, который получает наш график в качестве аргумента конструктора, и мы применили к немуmxCircleLayout . Это раскладывает визуализацию по кругу.
Кроме того, мы используемmxCellRenderer для созданияBufferedImage, а затем записываем визуализацию в файл png.
Мы можем увидеть финальное изображение в браузере или в нашем любимом рендерере:
Мы можем найти более подробную информацию вofficial documentation.
6. Заключение
JGraphT предоставляет практически все типы графов и различные графические алгоритмы. Мы рассмотрели, как использовать несколько популярных API. Однако вы всегда можете изучить библиотеку наofficial page.
Реализацию всех этих примеров и фрагментов кода можно найти вover on Github.