Большинство из нас написали несколько программ, которые имеют дело с алгоритмами теории графов, такими как нахождение кратчайшего пути между двумя вершинами, нахождение минимального остовного дерева для данного графа и так далее. В каждом из этих алгоритмов программный способ представления графа состоит в том, чтобы использовать либо матрицу смежности, либо список смежности . Оба они не очень интуитивно понятный способ определения ввода графика. Например, матрица смежности может привести к ошибкам, если записи не сделаны в правильных столбцах и строках. Более того, во время выполнения вы не очень уверены, какая строка / столбец представляет какое ребро, и все становится еще сложнее, когда дело доходит до ввода для графа с большим количеством вершин.
Во время инженерных исследований я реализовал довольно много графовых алгоритмов на Java, и во всех них у меня были вложенные циклы for для получения входных данных матрицы смежности. Недавно, когда я читал книгу DSL Мартина Фаулера, я натолкнулся на идею создания DSL для обеспечения ввода графа, то есть DSL, который позволил бы пользователю указывать вершины, ребра и их веса. Я выбрал графические алгоритмы, которые я реализовал, и просто удалил вход матрицы смежности и вместо этого использовал созданный мной DSL. Алгоритм работал как шарм.
В этой статье я покажу действительный синтаксис для DSL, взяв различные графические входы и показывая для них DSL. Затем я покажу вам созданную мной библиотеку, которая состоит из семантической модели графика, синтаксического анализатора и лексера DSL и простого API-интерфейса разработчика, который заполняет семантическую модель из сценария DSL. Парсер и лексер были сгенерированы с использованием ANTLR, и, следовательно, эта библиотека требует, чтобы ANTLR Jar был доступен в classpath. И, наконец, я покажу, как можно использовать этот DSL для нахождения минимального остовного дерева с помощью алгоритма Крускала.
Синтаксис DSL и некоторые примеры
DSL для приведенного ниже графика (g1):
График G1
Graph { A1 -> B2 (12.3) B2 -> C3(0.98) C3->D4 (2) D4 ->E5 (12.45) }
Обратите внимание, что между элементами в DSL выше есть разные пробелы. Это просто для того, чтобы показать различные способы написания DSL.
DSL для приведенного ниже графика (g2) будет:
График G2
Graph{ A1 -> B2 (12.3) B2 -> C3 (0.98) C3 -> D4 (2) E5 }
Обратите внимание, что между ‘Graph’ и ‘{‘ нет пробела. Это просто для того, чтобы показать разные способы написания.
DSL для приведенного ниже графика (g3) будет:
Graph { A -> B (12.3) B -> C (0.98) C -> D (2) D -> E (12.45) }
Теперь, чтобы показать некоторые из недействительных сценариев DSL:
Graph { 1A -> B (12.3) B -> C (0.98) }
Выше недопустимо, потому что имя вершины начинается с цифры.
Graph { }
Вышеуказанное недопустимо, потому что Граф ожидает, что будет определена хотя бы одна вершина Но он может иметь ноль или более ребер.
Библиотека для ввода графиков на основе DSL
Я использовал ANTLR, который выполняет всю задачу по созданию лексера и парсера для грамматики, которую я определил для моего DSL. Таким образом, мне не нужно беспокоиться о создании парсера или беспокоиться о создании токенов из входного скрипта DSL.
Классы parser и lexer вместе с классами семантической модели упакованы в jar, и этот jar вместе с ANTLR jar необходимо включить, чтобы использовать запись DSL для ввода в граф.
Структуру банки DSL можно увидеть на скриншоте ниже:
Классы в пакете графа соответствуют семантической модели, то есть это универсальные классы и могут использоваться независимо от того, использует ли кто-то DSL или нет. Классы в graph.dsl соответствуют сгенерированным ANTLR java-классам для лексера и анализатора.
Грамматика, используемая ANTLR для лексического анализа и анализа:
grammar Graph; graph: GRAPH_START (edge|vertex)+ GRAPH_END; edge: (vertex) TO (vertex) weight; vertex: ID; weight: '(' NUM ')'; GRAPH_START : 'Graph'([ ])*'{'; GRAPH_END : '}'; WEIGHT_START: '('; WEIGHT_END: ')'; TO: '->'; ID: ^[a-zA-Z][a-zA-Z0-9]*; NUM: [0-9]*[.]?[0-9]+; WS: [ \t\r\n]+ -> skip;
Приведенная выше грамматика имеет возможности для улучшения, но в качестве первой попытки я постаралась удержать ее на этом уровне.
Загрузите банку DSL отсюда (GraphDSL.jar).
Загрузите банку ANTLR здесь (antlr-4.1-complete.jar).
Примечание. Этот DSL разработан с использованием ANTLR версии 4.
Алгоритм Крускала для нахождения минимального остовного дерева
График, который используется для проверки реализации этого алгоритма:
и DSL для того же:
Graph { A -> B (7) B -> C (8) A -> D (5) B -> D (9) D -> E (15) D -> F (6) E -> F (8) E -> C (5) B -> E (7) E -> G (9) F -> G (11) }
Давайте посмотрим на реализацию:
package kruskalsalgo; import java.util.ArrayList; import java.util.Collections; import java.util.Scanner; import graph.Edge; import graph.Graph; import graph.Vertex; import graph.GraphBuilder; import java.io.IOException; import java.util.Comparator; public class KruskalsAlgorithm { public static void main(String[] args) throws IOException { //Load the graph data from the DSL Graph g = new GraphBuilder().buildGraph("graph.gr"); ArrayList<Set> forest = new ArrayList<Set>(); ArrayList<Edge> finalEdgeSet = new ArrayList<Edge>(); //Creating disjoint set of vertices which represents the initial forest for (Vertex v : g.getVertices()) { Set newSet = new Set(); newSet.getVertexList().add(v); forest.add(newSet); //Creating Disjoint Sets } //sort the edges in the graph based on their weight Collections.sort(g.getEdges(), new Comparator<Edge>(){ public int compare(Edge o1, Edge o2) { return o1.getWeight().compareTo(o2.getWeight()); } }); for (Edge edge : g.getEdges()) { //Find in which set the vertices in the edges belong int rep1 = Set.findRep(edge.getFromVertex(), forest); int rep2 = Set.findRep(edge.getToVertex(), forest); //If in different sets then merge them into one set and pick the edge. if (rep1 != rep2) { finalEdgeSet.add(edge); Set.Union(rep1, rep2, forest); } } System.out.println("The Minimum Spanning tree is"); for (Edge edge : finalEdgeSet) { System.out.println("Vertex: " + edge.getFromVertex().getLabel() + " to Vertex: " + edge.getToVertex().getLabel()); } System.out.println(""); }//End of Main } class Set { private ArrayList<Vertex> vertexList; private int representative; static int count; public Set() { vertexList = new ArrayList<Vertex>(); this.representative = ++(Set.count); } //Find the set identifier in which the given vertex belongs to. public static int findRep(Vertex vertex, ArrayList<Set> forest) { int rep = 0; for (Set set : forest) { for (Vertex v : set.getVertexList()) { if (v.getLabel().equals(vertex.getLabel())) { return set.getRepresentative(); } } } return rep; } //Find the set given the step identifier. public static Set findSet(int rep, ArrayList<Set> forest) { Set resultSet = null; for (Set set : forest) { if (set.getRepresentative() == rep) { return set; } } return resultSet; } //Merge the set into another and remove it from the main set. public static void Union(int rep1, int rep2, ArrayList<Set> forest) { Set set1 = Set.findSet(rep1, forest); Set set2 = Set.findSet(rep2, forest); for (Vertex v : set2.getVertexList()) { set1.getVertexList().add(v); } forest.remove(set2); } public ArrayList<Vertex> getVertexList() { return vertexList; } public int getRepresentative() { return representative; } }
Приведенный выше код загружает данные графика из dsl-graph.gr. Сценарии DSL должны быть помещены в пакет ресурсов, чтобы библиотека DSL могла найти его.
Выход для вышеуказанного кода:
The Minimum Spanning tree is Vertex: A to Vertex: D Vertex: E to Vertex: C Vertex: D to Vertex: F Vertex: A to Vertex: B Vertex: B to Vertex: E Vertex: E to Vertex: G
и показывая то же самое схематично