Я пытаюсь использовать DFS и BFS, чтобы найти все простые пути длиной до заданного k, начиная с заданной вершины ориентированного графа. Никакие циклы не допускаются.
Мой код выглядит следующим образом, и я проверил, что оба алгоритма дают правильные результаты.
Я определяю класс графа и помещаю в этот класс код BFS и DFS. В нужном мне графе около 190 000 вершин и 2 500 000 ребер. Средняя исходящая степень каждой вершины равна 105:
public class AdjGraph {
private int V; // sum of vertices
private int E; // sum of edges
private List<Vertex> vertexList;
private Map<Vertex, List<Edge>> vertexAdj;
public AdjGraph() {
this.V = 0;
this.E = 0;
this.vertexList = new ArrayList<>();
this.vertexAdj = new HashMap<>();
}
public void addVertex(Vertex v) {
this.vertexList.add(v);
this.vertexAdj.put(v, new ArrayList<>());
this.V++;
}
public void addEdge(Edge e) {
Vertex startVertex = e.getStartVertex();
List<Edge> edgeList = this.vertexAdj.get(startVertex);
if (!edgeList.contains(e)) {
edgeList.add(e);
this.vertexAdj.put(startVertex, edgeList);
}
}
public List<Vertex> getVertexList() {
return vertexList;
}
// Version of DFS
public Map<Integer, List<List<Vertex>>> findAllPathsUpToLengthByDFS(Vertex start, int k) {
boolean[] visited = new boolean[vertexList.size()];
List<Vertex> path = new ArrayList<>();
Map<Integer, List<List<Vertex>>> allPaths = new HashMap<>();
findAllPathsUpToLengthByDFSUtil(start, k, visited, path, allPaths);
return allPaths;
}
// Used by the function findAllPathsUpToLengthByDFS(...)
private void findAllPathsUpToLengthByDFSUtil(Vertex u, int k, boolean[] visited, List<Vertex> path, Map<Integer, List<List<Vertex>>> allPaths) {
// Mark the current node as visited and store in path
visited[vertexList.indexOf(u)] = true;
path.add(u);
// If current path length exceeds k, backtrack
if (path.size() - 1 > k) {
visited[vertexList.indexOf(u)] = false;
path.remove(path.size() - 1);
return;
}
// If current path length is within k, add it to allPaths
int pathLength = path.size() - 1;
if (pathLength <= k) {
allPaths.computeIfAbsent(pathLength, x -> new ArrayList<>()).add(new ArrayList<>(path));
}
// Recur for all the vertices adjacent to this vertex
for (Edge edge : vertexAdj.get(u)) {
Vertex v = edge.getEndVertex();
if (!visited[vertexList.indexOf(v)]) {
findAllPathsUpToLengthByDFSUtil(v, k, visited, path, allPaths);
}
}
// Remove current vertex from path and mark it as unvisited
path.remove(path.size() - 1);
visited[vertexList.indexOf(u)] = false;
}
// Version of BFS
public Map<Integer, List<List<Vertex>>> findAllPathsUpToLengthByBFS(Vertex start, int k) {
Map<Integer, List<List<Vertex>>> allPaths = new HashMap<>();
Queue<List<Vertex>> queue = new LinkedList<>();
// Initialize the queue with the start vertex
List<Vertex> startPath = new ArrayList<>();
startPath.add(start);
queue.add(startPath);
while (!queue.isEmpty()) {
List<Vertex> path = queue.poll();
Vertex last = path.get(path.size() - 1);
// Add the path to allPaths if its length is within k
int pathLength = path.size() - 1;
if (pathLength <= k) {
allPaths.computeIfAbsent(pathLength, x -> new ArrayList<>()).add(new ArrayList<>(path));
}
// If the path length is already k, no need to explore further
if (pathLength < k) {
// Explore the neighbors of the last vertex in the path
for (Edge edge : vertexAdj.get(last)) {
Vertex next = edge.getEndVertex();
if (!path.contains(next)) { // Avoid cycles
List<Vertex> newPath = new ArrayList<>(path);
newPath.add(next);
queue.add(newPath);
}
}
}
}
return allPaths;
}
}
Класс вершины:
public class Vertex {
// The origin names are not id1 and id2.
// I change them for simplicity.
private String id1;
private String id2;
public Vertex() {
}
public Vertex(String id1, String id2) {
this.id1 = id1;
this.id2 = id2;
}
// Getters and Setters
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Vertex other = (Vertex) obj;
return id1.equals(other.id1) && id2.equals(other.id2);
}
@Override
public int hashCode() {
return Objects.hash(id1, id2);
}
}
Класс края:
public class Edge {
private Vertex startVertex;
private Vertex endVertex;
public Edge(Vertex startVertex, Vertex endVertex) {
this.startVertex = startVertex;
this.endVertex = endVertex;
}
// Getters for startVertex and endVertex
public Vertex getStartVertex() {
return startVertex;
}
public Vertex getEndVertex() {
return endVertex;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null || getClass() != obj.getClass()) {
return false;
}
Edge other = (Edge) obj;
return Objects.equals(startVertex, other.startVertex) &&
Objects.equals(endVertex, other.endVertex);
}
@Override
public int hashCode() {
return Objects.hash(startVertex, endVertex);
}
}
Вот пример. Объем данных в этом примере относительно невелик, поэтому разница во времени выполнения неочевидна. Когда график большой, разница очевидна. На моем графике BFS работает 2 мс, а DFS — 15 с при k = 1:
public static void main(String[] args) {
AdjGraph graph = new AdjGraph();
for (int i = 0; i < 1000; i++) {
Vertex v = new Vertex(String.valueOf(i), String.valueOf(i));
graph.addVertex(v);
}
List<Vertex> vertexList = graph.getVertexList();
for (int i = 0; i < vertexList.size(); i++) {
for (int j = 0; j < vertexList.size(); j++) {
if (j != i) {
graph.addEdge(new Edge(vertexList.get(i), vertexList.get(j)));
}
}
}
Vertex start = vertexList.get(0); // Start node
int k = 1; // Maximum path length
Long t1 = System.currentTimeMillis();
Map<Integer, List<List<Vertex>>> allPaths = graph.findAllPathsUpToLengthByBFS(start, k);
// Map<Integer, List<List<Vertex>>> allPaths = graph.findAllPathsUpToLengthByBFS(start, k);
Long t2 = System.currentTimeMillis();
System.out.println("Running time: " + (t2 - t1) + "ms");
}
Удивительно, но BFS намного быстрее, чем DFS. Кто-нибудь знает, почему? Может быть, это потому, что в DFS слишком много повторяющихся вычислений? Как я могу их оптимизировать?
Спасибо!
@trincot Я добавлю их прямо сейчас. Они немного сложны.
Хорошо, чем проще вы это сделаете, тем лучше. Если разницу во времени можно продемонстрировать с помощью графа с 20 узлами вместо 1000, то это предпочтительнее. Если у вас есть методы, которые не используются, не включайте их. Просто постарайтесь быть минималистичными, не потеряв при этом работоспособности и демонстрации проблемы.
Спасибо за завершение занятий. Теперь нам нужен (минимальный) основной код драйвера, который демонстрирует, что один быстрее другого. Кроме того, в вашем коде есть неопределенные переменные, например tripId
, routeId
,
@trincot Я использую файл для создания большого графика. Я попытаюсь напрямую создать пример. Это может занять у меня некоторое время.
Я понимаю, но вы могли бы создать более крупный график с некоторой симметрией, например, A связан с B и C, B с C и D, C с D и E и т. д., пока у вас не будет достаточно узлов, которые будут иметь значение для времени. Это можно создать с помощью некоторого цикла.
Что такое tripId
и routeId
?
Извини. Это id1 и id2. Я их уже исправил.
@trincot Я добавил пример, и он работает успешно. Можно продемонстрировать разницу во времени работы BFS и DFS.
Хорошо, просто getVertexList
не определен, а main
должен иметь тип void
.
@trincot Моя вина. Сейчас я их исправил.
Основная причина заключается в том, что в вашей реализации DFS вы все еще ищете соседей, даже если уже достигли максимальной длины пути. Другими словами, ваш базовый вариант выхода находится на один уровень глубже, чем вы могли бы сделать, а это означает, что выполняется множество итераций без какой-либо возможности добавить новый путь.
Эту проблему можно решить, изменив эту часть:
for (Edge edge : vertexAdj.get(u)) {
Vertex v = edge.getEndVertex();
if (!visited[vertexList.indexOf(v)]) {
findAllPathsUpToLengthByDFSUtil(v, k, visited, path, allPaths);
}
}
к этому:
if (pathLength < k) { // *********
for (Edge edge : vertexAdj.get(u)) {
Vertex v = edge.getEndVertex();
if (!visited[vertexList.indexOf(v)]) {
findAllPathsUpToLengthByDFSUtil(v, k, visited, path, allPaths);
}
}
}
Обратите внимание, что именно этот шаблон вы использовали в своей реализации BFS. И с этим изменением вы можете удалить другую обработку базового случая:
// If current path length exceeds k, backtrack
if (path.size() - 1 > k) {
visited[vertexList.indexOf(u)] = false;
path.remove(path.size() - 1);
return;
}
Однако есть еще кое-что, что вы можете сделать для улучшения реализации DFS, хотя это будет иметь менее драматический эффект:
visited[vertexList.indexOf(v)]
— относительно медленная операция при наличии большого количества вершин. Было бы более эффективно использовать хэш-набор вместо массива.path
упорядоченный хэш-набор (LinkedHashSet
), чтобы можно было отбросить visited
и использовать path.contains
для этой цели.Вот версия реализации DFS, в которой реализованы эти изменения. Я назвал функции буквой «2» в конце, чтобы вы могли включить их в свой проект и сравнить производительность с исходными функциями:
// Improved Version of DFS (DFS2)
public Map<Integer, List<List<Vertex>>> findAllPathsUpToLengthByDFS2(Vertex start, int k) {
// Use an insertion-ordered set so you don't need a separate visited array
Set<Vertex> path = new LinkedHashSet<>();
Map<Integer, List<List<Vertex>>> allPaths = new HashMap<>();
findAllPathsUpToLengthByDFSUtil2(start, k, path, allPaths);
return allPaths;
}
private void findAllPathsUpToLengthByDFSUtil2(Vertex u, int k, Set<Vertex> path, Map<Integer, List<List<Vertex>>> allPaths) {
path.add(u);
int pathLength = path.size() - 1;
allPaths.computeIfAbsent(pathLength, x -> new ArrayList<>()).add(new ArrayList<>(path));
if (pathLength < k) { // This is where you should stop recursion
for (Edge edge : vertexAdj.get(u)) {
Vertex v = edge.getEndVertex();
if (!path.contains(v)) {
findAllPathsUpToLengthByDFSUtil2(v, k, path, allPaths);
}
}
}
path.remove(u);
}
Хотя DFS требует дополнительных затрат на стек вызовов, с точки зрения памяти преимущества DFS перед BFS начинают проявляться, когда k
становится больше. Например, я попробовал ваш main
код, но с графом всего в 25 узлов (вместо 1000), но с k
, равным 5. Тогда мы видим, что BFS оказывается медленнее, чем DFS. В основном это связано с тем, что в памяти хранится множество частичных путей, в то время как DFS по своей природе использует только один частичный путь.
Большое спасибо! Ваше объяснение невероятно ясное и полезное. Я искренне ценю время и усилия, которые вы вложили, чтобы помочь мне. Я тщательно оценю, как эти изменения повлияют на производительность во время выполнения :)
Пожалуйста! ;-)
Эй, у меня есть еще один вопрос. Код алгоритма поиска с возвратом обычно включает в себя оценку того, удовлетворено ли условие завершения в первую очередь. Если оно удовлетворено, то возвращайтесь. Однако функция findAllPathsUpToLengthByDFSUtil2 не имеет этой части и по-прежнему успешно выполняется. Почему?
Это похоже на else
утверждения if
. Вместо этого оператора переноса if
мы могли бы использовать оператор if
с противоположным условием, и внутри его тела сделать { path.remove(u); return; }
, а под этим блоком if
у вас будет развернутый цикл for
для рекурсивного случая. Все сводится к одному и тому же. Иногда вы видите явный базовый вариант, иногда он неявный, поскольку опущен else
.
Понятно! Большое спасибо! Алгоритм возврата действительно для меня немного сложен, лол.
Можете ли вы добавить код, с помощью которого вы измерили, что один быстрее другого? Не могли бы вы также добавить недостающие определения классов (
Vertex
,Edge
) и методы, чтобы мы могли скомпилировать этот код и воспроизвести тайминги?