5

The other question on this only answered how to detect a cycle, not also output it. So, I'd like to write an algorithm run BFS or DFS in O(V + E) time (V=vertices, E=edges), on an undirected graph, and output the cycle if it has one.

What I know so far is how BFS/DFS works and that you can detect a cycle using BFS if you visit a node that already has been marked as visited.

eltigre
  • 223
  • 4
  • 10
  • possible duplicate of [How can I find the actual path found by BFS?](http://stackoverflow.com/questions/9590299/how-can-i-find-the-actual-path-found-by-bfs) – Paul Jan 30 '15 at 20:47
  • 1
    I was looking for pseudo code of an algorithm. That question doesn't really have that. Also, I'm not sure if that solution provides a run time of O(V + E) – eltigre Jan 30 '15 at 20:54

2 Answers2

5

To detect and output a cycle with DFS, just mark each vertex as you get to it; if any child of the current vertex is marked, you know you have a cycle involving that child. Furthermore you know that that child vertex is the first vertex belonging to this particular cycle that was encountered by the DFS, and that every move in the DFS since it first encountered that vertex (i.e. every recursive call since then that hasn't yet returned) has visited another vertex in the cycle. The only information you need to pass back up the call stack is this child vertex, or a special value indicating that no cycle was found. You can pass this back as a return value:

dfs(v, p) {
    marked[v] = true
    For each neighbour u of v:
        If u != p:   # I.e. we ignore the edge from our parent p
            If marked[u]:
                Append v to cycleVertices
                Return u   # Cycle!
            Else:
                result = dfs(u, v)
                If result == FINISHED:
                    # Some descendant found a cycle; now we're just exiting
                    Return FINISHED
                Else if result != NOCYCLE:
                    # We are in a cycle whose "top" vertex is result.
                    Append v to cycleVertices
                    If result == v:
                        return FINISHED   # This was the "top" cycle vertex
                    Else:
                        return result     # Pass back up

    marked[v] = false    # Not necessary, but (oddly?) not harmful either ;)
    Return NOCYCLE
}

After calling dfs(r, nil) for some vertex r (and any non-vertex value nil), cycleVertices will be populated with a cycle if one was found.

[EDIT: As pointed out by Juan Lopes, unmarking vertices is not necessary, and is possibly confusing; but, interestingly, doesn't affect the time complexity for undirected graphs.]

Community
  • 1
  • 1
j_random_hacker
  • 50,331
  • 10
  • 105
  • 169
  • The algorithm you wrote out in your text is what I came up with since I posted the question. However, is your method in O(V + E) time? – eltigre Jan 30 '15 at 22:46
  • Yes, because each vertex will be visited only once (despite the unmarking). Suppose to the contrary that some vertex v is visited more than once, and suppose further that among all such vertices, v's first visit is the earliest. Call the DFS root r, the path from r to v's first visit P1, the path from r to v's second visit P2, and the last vertex before P1 and P2 diverged u. Call the segment of P2 from u to v Q. But then P1 followed by the reversal of Q gives a cycle (having top vertex u), which would have been found by DFS before P2 could be tried... – j_random_hacker Jan 30 '15 at 23:09
  • ... , because DFS explores all paths leaving v before exploring the next path leaving u (which is the earliest that P2 could be explored). This is a contradiction, so no such v can exist. – j_random_hacker Jan 30 '15 at 23:10
  • @JuanLopes: Actually because I just finished reading a description of how to find cycles with DFS in a *directed* graph, where this is necessary ;) But for fun I decided to see whether it will work for an undirected graph too, and as you can see from my comment, it does. – j_random_hacker Jan 31 '15 at 00:02
  • 3
    I find it particularly confusing, as it make the DFS seem like a backtracking. And for directed graphs it does increase the algorithm complexity. Usually to find cycles in directed graphs I use the WHITE, GREY, BLACK states. You mark the vertex GREY when entering it and BLACK when exiting. There is a cycle is when you visit a GREY vertex twice. – Juan Lopes Jan 31 '15 at 00:07
  • @JuanLopes: DFS and backtracking are basically synonyms. And it doesn't increase the complexity, as the proof in my first comment shows. – j_random_hacker Jan 31 '15 at 00:09
  • 1
    DFS and backtracking are **not** synonyms. In DFS you never visit the same vertex twice, as in backtracking you are interested in all vertex combinations. And yes, it does increase the complexity. Consider [this directed graph](http://imgur.com/xajQL8h). Can you see how nodes D, E, and F will be visited twice? – Juan Lopes Jan 31 '15 at 00:14
  • @JuanLopes: Here we're only concerned with undirected graphs. If the edges on your graph are made undirected, then D, E and F will indeed be visited only once by my algorithm, since the cycle ABDCA (or ACDBA) will be discovered before they can be visited a second time. – j_random_hacker Jan 31 '15 at 00:20
  • 1
    That's why I said that it increases the complexity **for directed graphs**. And for undirected graphs the unmarking is actually redundant. – Juan Lopes Jan 31 '15 at 00:21
  • Ah, so you did. As for your assertion that the terms "backtracking" and "DFS" are different: it seems sensible to make a distinction, but "backtracking" doesn't appear in the index of CLRS 2nd Ed. Is it actually a standard term? – j_random_hacker Jan 31 '15 at 00:26
1

If you're using DFS, you can do it recursively by printing out the name of the node depending on whether the visited node is already visited:

define function DFSVisit(node, cycle):
    if node.visited is true:
        push node.name to cycle
        return true
    else 
        set node.visited to true

    for each child of node:
        if DFSVisit(child, cycle) is true:
            set foundCycle to true
            break out of for loop

    if foundCycle is true:
        if (cycle.length <= 1 or cycle[first] != cycle[last]):
            push node.name to cycle
        return true
    else 
        return false

By the end of it, cycle will contain a cycle that was found in the graph, otherwise it will be empty. Also note that the order that the cycle is shown will be dependant on how you 'push' to the cycle (push to back will print backwards, push to front will print 'in order')

Edit: Many thanks to @j_random_hacker for helping me debug my pseudo-code!

bajuwa
  • 330
  • 1
  • 9
  • A couple of bugs: (1) You never set `node.visited` to true! (2) Even if you did, this will output not just the cycle but all nodes from the DFS root down to the first node that participates in the cycle (IOW a "lassoo" that contains the cycle). – j_random_hacker Jan 30 '15 at 22:43
  • For (1) that was a typo, I was accidentally setting it to false instead of true. For (2) as I had already mentioned this was only to show how the printing would work, not the actual DFS since the question stated they already had that part – bajuwa Jan 30 '15 at 22:50
  • (1) OK, but please edit to fix that then; (2) That still doesn't make sense to me. How can you say you are showing "how the printing would *work*" if your code will print too many nodes? – j_random_hacker Jan 30 '15 at 23:16
  • Fixed the typos, sorry for the delay as I was mucking with my internet settings and didn't notice I disconnected before the save went through. As for the rest of your comment: if anything, my old code didn't print enough. It would only print the first node it encountered that was visited, and not the rest of the path. With the updated code it will print them all now. Even with that aside, I'm not sure how the algo could print 'too many nodes'? It exits all of the recursive calls once a cycle is found, regardless of the size of the cycle (which isn't even a requirement?) – bajuwa Jan 30 '15 at 23:31
  • Suppose the graph consists of the edges AB, BC, CD, DE and EC, and DFS starts at A. Your code will print CEDCBA, even though the cycle doesn't include A or B. – j_random_hacker Jan 30 '15 at 23:36
  • Oh I see what you mean, thanks! I think I can still edit the code to remove that issue – bajuwa Jan 30 '15 at 23:39
  • If you fix it so that when the cycle is first detected the vertex is pushed onto `cycle` instead of printed, I'll +2. – j_random_hacker Jan 31 '15 at 00:05
  • Bah, forgot about the first one x) Thanks a bunch for all the debugging help! – bajuwa Jan 31 '15 at 00:07