查找有向图中的所有循环,包括后边

Find all Cycles in the Directed Graph including back edges

给定下面的 ,找到从顶点 1 回到 1 的所有可能路径,包括后边。

结果:

        [1,2,3,2,1]
        [1,2,1]
        [1,2,3,1]

我试过用DFS只能得到一个周期[1,2,3,2,1]。我不知道如何跟踪后缘。

代码:

private static void dfsNow(Node vertex, HashMap<String, List<Node>> adjList,
                           HashSet<String> visited, HashSet<String> candidates,
                           ArrayList<String> res) {

    candidates.add(vertex.v);
    res.add(vertex.v);

    for(Node adj :  adjList.get(vertex.v)) {

        if(candidates.contains(adj.v)) {
           
            res.add(adj.v);
            System.out.println(vertex.v+":"+adj.v);
        } else {
            //adj.setParent(vertex); // build the trace back
            dfsNow( adj, adjList,visited,candidates,res);
        }
    }
    candidates.remove(vertex.v);
    visited.add(vertex.v);
}

你离答案很近了。

为了检测图中的环,您需要使用深度优先搜索算法。没错。

但是您的解决方案中有几处需要改进:

  • 对于此任务,您不需要 。因为您可以仅使用 顶点 来建模 有向图 在处理加权图(即每条边都与一个值相关联)并且需要解决例如使用 Dijkstra 或 Bellman–Ford 的最短路径问题时很有用算法。那么你真的需要边来模拟顶点之间的连接。但是在这个任务中,边是冗余的,每个顶点可以指向其他顶点
  • 邻接表 不能是单独的数据结构。相反,每个 顶点 都必须知道它的 邻居 。 IE。每个 顶点 必须包含对 相邻顶点.
  • 集合的引用
  • 由于可以包含多个循环,结果需要用嵌套集合表示。喜欢列表列表 List<List<Vertex>>.
  • contains()List 上进行的检查非常昂贵(O(n) 在最坏的情况下), 并且不是能够关联元素的列表。 Map 是一个更好的选择,因为 map 允许在恒定时间内检索值并将多个键与相同的值相关联(即当一组顶点点与单个顶点连接时的模型案例)。

请注意,循环 [1,2,3,2,1] 无效,因为有 多个顶点 访问了两次 。它是两个循环 1-22-3 的组合。如果您需要发现图形中的所有循环,则必须一次只检测一个循环,否则尝试恢复循环路径可能会失败,因为引用彼此指向一个。只是因为在您的解决方案中只能跟踪一条路径,所以您没有 运行 遇到第二个循环 2->3->2 的麻烦。 IE。循环中的所有顶点(除了一个)必须是唯一的,这将允许维护多条路径。

在下面的实现中,我特意提供了一种能够检测包含特定顶点的循环的方法,以及一种获取所有循环的方法仅由唯一顶点组成.这样您就可以通过更改它们来玩耍,并确保 map 永远不会包含循环的最后一段。 IE。只有条目 2->3 会被添加到映射中,但条目 3->2 不会,否则你会创建一个无限循环。

下图的实现仅使用 顶点。深度优先搜索算法实现迭代(虽然递归实现更容易,但递归在Java中有局限性,尤其是在处理大型数据集和迭代方法时性能更高)。

public class CyclicGraph {
    private Map<Integer, Vertex> vertexById = new HashMap<>();
    
    public void addVertex(int vertexId, int... neighbours) {
//        vertexById.putIfAbsent(vertexId, new Vertex(vertexId));
//        Vertex current = vertexById.get(vertexId); // does the same as the line below with one important distinction - new Vertex will be instantiated by `computeIfAbsent` only entry is not present
        Vertex current = vertexById.computeIfAbsent(vertexId, Vertex::new); // adding a vertex
        
        for (int next : neighbours) {
            Vertex neighbour = vertexById.computeIfAbsent(next, Vertex::new); // retrieving neighbour
            current.addNeighbour(neighbour);                                  // adding neighbour
        }
    }
    
    public List<List<Vertex>> getCyclesContainingVertex(int targetId) {
        Vertex target = vertexById.get(targetId);
        
        if (vertexById.get(targetId) == null) {
            return Collections.emptyList();
        }
        List<List<Vertex>> cycles = new ArrayList<>();
        
        Deque<Vertex> stack = new ArrayDeque<>();
        Set<Vertex> seen = new HashSet<>();
        Map<Vertex, Vertex> paths = new HashMap<>();
        
        stack.add(target);
        while (!stack.isEmpty()) {
            Vertex current = stack.pop();
            seen.add(current);
            for (Vertex neighbour : current.getNeighbours()) {
                if (seen.contains(neighbour) && neighbour.equals(target)) { // the cycle was found
                    // build cycle, don't add vertex to the stack and don't add new entry to the paths (it can prevent other cycles containing neighbour vertex from being detected)
                    cycles.add(buildCycle(paths, neighbour, current));
                } else if (!seen.contains(neighbour)) {
                    stack.add(neighbour);
                    paths.put(neighbour, current);
                    seen.add(neighbour);
                }
            }
        }
        return cycles;
    }
    
    public List<List<Vertex>> getAllCycles() {
        List<List<Vertex>> cycles = new ArrayList<>();
        Deque<Vertex> stack = new ArrayDeque<>();
        Set<Vertex> seen = new HashSet<>();
        
        for (Vertex next : vertexById.values()) {
            if (seen.contains(next)) continue;
            
            Map<Vertex, Vertex> paths = new HashMap<>();
            stack.add(next);
            while (!stack.isEmpty()) {
                Vertex current = stack.pop();
                seen.add(current);
                for (Vertex neighbour : current.getNeighbours()) {
                    if (seen.contains(neighbour)) { // the cycle was found
                        // build cycle, don't add vertex to the stack and don't add new entry to the paths (it can prevent other cycles containing neighbour vertex from being detected)
                        cycles.add(buildCycle(paths, neighbour, current));
                    } else {
                        stack.add(neighbour);
                        paths.put(neighbour, current);
                        seen.add(neighbour);
                    }
                }
            }
        }
        return cycles;
    }
    
    private List<Vertex> buildCycle(Map<Vertex, Vertex> paths, Vertex start, Vertex end) {
        Deque<Vertex> cycle = new ArrayDeque<>();
        Vertex current = end;
        while (current != null && !current.equals(start)) {
            cycle.addFirst(current);
            current = paths.get(current);
        }
        cycle.addFirst(start);
        return new ArrayList<>(cycle);
    }
    
    public void clear() {
        this.vertexById.clear();
    }
    
    private class Vertex {
        private int id;
        private Set<Vertex> neighbours = new HashSet<>();
        
        public Vertex(int id) {
            this.id = id;
        }
        
        public boolean addNeighbour(Vertex vert) {
            return neighbours.add(vert);
        }
        
        // getters, hashCode/equeals, toString()
    }
}

演示中使用的更详细的图表示例以及问题中提供的图表。

main() - 演示

public static void main(String[] args) {
    CyclicGraph graph = new CyclicGraph();
    
    System.out.println("Graph described in the question (only cycles that include vertex 1):");
    graph.addVertex(1, 2);
    graph.addVertex(2, 1, 3);
    graph.addVertex(3, 1, 2);

    for (List<Vertex> cycle : graph.getCyclesContainingVertex(1)) {
        System.out.println(cycle);
    }
    
    graph.clear();
    System.out.println("\nMore elaborate Graph (all cycles):");
    
    graph.addVertex(1, 2);
    graph.addVertex(2, 1, 5);
    graph.addVertex(3, 1, 2);
    graph.addVertex(4, 1, 3);
    graph.addVertex(5, 3, 4);
    
    for (List<Vertex> cycle : graph.getAllCycles()) {
        System.out.println(cycle);
    }
}

输出

Graph described in the question (cycles that lead to 1):
[Vertex{ 1 }, Vertex{ 2 }] // 1 is reachable from 2
[Vertex{ 1 }, Vertex{ 2 }, Vertex{ 3 }] // 1 is reachable from 3

More elaborate Graph (all cycles):
[Vertex{ 1 }, Vertex{ 2 }] // 1 is reachable from 2
[Vertex{ 2 }, Vertex{ 5 }, Vertex{ 3 }] // 2 is reachable from 3
[Vertex{ 1 }, Vertex{ 2 }, Vertex{ 5 }, Vertex{ 3 }] // 1 is reachable from 3
[Vertex{ 3 }, Vertex{ 1 }, Vertex{ 2 }, Vertex{ 5 }, Vertex{ 4 }] // 3 is reachable from 4
[Vertex{ 1 }, Vertex{ 2 }, Vertex{ 5 }, Vertex{ 4 }] // 1 is reachable from 4