CS 2223 May 03 2021
Daily Exercise:
Classical selection: Beethoven Symphony No 5 in C minor (1804-1808)
Musical Selection:
Ironic, Alanis Morisette (1996)
Visual Selection:
The
Persistence of Memory, Salvador Dali (1931)
Live Selection:
Hound Dog, Elvis,
1956
1 Directed Graphs and Associated Algorithms
When you point your finger at someone, there are three pointing back at you.
Anonymous
1.1 Homework 4
Open discussion on HW4, due TUESDAY tomorrow at 6PM.
I notice that at least one student encountered problems using my algs.days.day22.DirectedCycle and algs.days.day22.DirectedDFS classes, which I reduced for lecture purpose. Please use the Sedgewick ones of the same name.
1.2 Directed Graph structure
Any discussion of graphs always start with undirected graphs. Over the next few lectures we are going to expand the domain to introduce some standard extensions.
Today we will discuss directed graphs or digraph for short. A digraph is a set of vertices and a collection of directed edges. Each directed edge connects an ordered pair of vertices in a specific direction.
The first difference between a graph and a digraph is that a digraph may have up to two different edges between the same pair of vertices. Specifically, the edge (v, w) is different from (w, v).
Many of the concepts introduced in the past few lectures apply here. Note that the Digraph implementation has only one line changed, namely, within the addEdge method. This method only updates a single Bag in the storage.
1.3 DFS over a Digraph
Naturally it makes sense to consider searching digraphs in the same manner as performed over graphs. This is shown on page 571.
With directed graphs, there are more complex traversals through a graph, and we are no longer simply interested in whether the entire graph is "connected" between any two vertices. Rather, we are interested in specific reachability from individual vertices.
Note that it is not necessary for you to create a new class, DirectedDFS, to use for the directed search. Indeed, you can place the recursive dfs method in any class, and then make the marked arrays local fields of that class.
/** Compute DFS from source vertex. */ public DirectedDFS(Digraph G, int s) { marked = new boolean[G.V()]; dfs(G, s); } /** Compute DFS from collection of sources. */ public DirectedDFS(Digraph G, Iterable<Integer> sources) { marked = new boolean[G.V()]; for (int v : sources) { if (!marked[v]) dfs(G, v); } } void dfs(Digraph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]) dfs(G, w); } }
As you can see, the code is nearly identical. The only change is that you only visit neighbors in the direction of an edge.
One of the fundamental questions that arise with directed graphs is to identify cycles in a graph. Recall that a cycle is (p. 567), "a directed path with at least one edge whose first and last vertices are the same."
We can convert the mechanics of Depth First Search into a cycle detector:
1.4 Directed Acyclic Graphs (DAGs)
When executing DFS, you recall that you don’t extend the depth first search to marked vertices. Well, can you use this observation to detect cycles? Consider the following graph.
And using the recursive DFS above, you would visit 0 to 1 to 2 and then to 3. How can you detect when there is a cycle? You need some more bookkeeping.
See handout.
Specifically, in the above graph, using dfs visit from vertex 0 would recursively invoke dfs(1) and then dfs(2) and finally dfs(3). The function stack would look list this:
dfs(0) ; check 1 dfs(1) ; check 2 [then 3] dfs(2) ; check 3 dfs(3) ; no outgoing neighbors
At this point, the recursion begins to unwind, all the way back to:
dfs(0) ; check 1 dfs(1) ; [done with 2] now checking 3
When visiting 3 (a previously marked vertex) there is a cycle. We need a way to determine this.
The solution is to maintain an extra array inStack which determines which vertices are "actively being pursued in the depth first recursion from the original source vertex." If you revisit a previously marked vertex that is still under active investigation, then you have found a cycle. This code is shown below:
void dfs(Digraph G, int v) { onStack[v] = true; marked[v] = true; for (int w : G.adj(v)) { // terminate once a cycle has been found. HAS to be inside // the for loop, because unwinding recursive calls will // return and we must stop this for loop immediately. if (hasCycle() != null) { return; } if (!marked[w]) { edgeTo[w] = v; dfs(G, w); } else { // might be a cycle if w is still on stack. // Construct cycle on demand if (onStack[w]) { cycle = new Stack<Integer>(); for (int x = v; x != w; x = edgeTo[x]) { cycle.push(x); } cycle.push(w); cycle.push(v); } } } onStack[v] = false; // done }
We can test out this class using sample graphs. Does the following graph have a cycle anywhere?
% java algs.days.day22.DirectedCycle day22-dag.txt
Let’s review this problem using a handout.
1.5 Topological Sort
We now can detect when a directed graph is acyclic; often called DAGs, these are commonly used to represent any number of problems. Here is one problem, known as Topological Sort.
Given a digraph, produce a linear ordering of its vertices such that for every directed edge uv (from vertex u to vertex v), u comes before v in the ordering.
We can use Depth First Search to determine this ordering. The key is to reflect on the final step in dfs which marks onStack[v] as false. This line executes when the recursion is ’unwinding’ back to an earlier vertex to try to attempt a different search direction.
It is exactly at this point that you know vertex v is a dead-end of sorts. That is, it is impossible to reach other vertices in the graph through this direction. Well, isn’t this exactly what we want to determine when it comes for topological sorting?
The only trouble is that we are unwinding and visiting vertices in reverse order from how they should appear in a toplogical ordering.
For example, if there are three vertices – A, B and C, with an edge from (A,B) to signal A must complete before B, and an edge from B to C to signal B must complete before C, then we know C must be the LAST in the topological ordering. When issuing dfs(A) it terminates at C and during the unwinding of the recursion, the onStack[v]=false statement executes for C, B and then A.
void dfs(Digraph G, int v) { marked[v] = true; for (int w : G.adj(v)) { if (!marked[w]) { edgeTo[w] = v; dfs(G, w); } } // amazingly, store vertex in order by reverse postorder. reversePost.push(v); }
The final processing step is to ensure that the entire graph is visited. It may be the case that not every vertex is reachable from vertex 0, because of the nature of the directed edges.
Thus in the constructor to DirectedDFS search, we iterate over all unmarked vertices in the graph. The first dfs search is launched from vertex 0; thereafter, any unmarked vertex becomes the point at which a new dfs search is initiated. This continued until all vertices in the graph are marked.
public DirectedDFS(Digraph G) { marked = new boolean[G.V()]; edgeTo = new int[G.V()]; reversePost = new Stack<Integer>(); for (int v = 0; v < G.V(); v++) { if (!marked[v]) { dfs(G, v); } } }
This completes the discussion of Topological sort.
% java algs.days.day22.Topological day22-dag.txt 0 3 2 4 5 6 1 8 7
1.6 What if there is no special source
Throughout we’ve always assumed there is a special designated source vertex from which the search begins. If this is not the case, then you can still pursue a searching strategy to explore the graph and reveal its structure.
What happens is a for loop will iterate over all possible vertices, and invoke the recursive dfs call on each unmarked vertex.
public DirectedDFS(Digraph G) { marked = new boolean[G.V()]; edgeTo = new int[G.V()]; reversePost = new Stack<Integer>(); for (int v = 0; v < G.V(); v++) { if (!marked[v]) { dfs(G, v); } } }
1.7 Daily Exercise
Is it possible to convert DirectedCycle into a non-recursive solution? Yes, but the code is more complicated. Think about it, and I will post in tomorrow’s code base.
1.8 Interview Challenge
Each Friday I will post a sample interview challenge. Since Friday was project presentation day, I was unable to make this available. Here it is.
During most technical interviews, you will likely be asked to solve a logical problem so the company can see how you think on your feet, and how you defend your answer.
You have ordered pancakes for breakfast at a restaurant. There are three
pancakes stacked one on top of the other, so only the top side of the top
pancake is visible.
The manager apologies, and explains that there was a cooking problem. One
of the pancakes is perfectly fine, one is burnt on one side, and one is
burnt on both sides.
As you look down, you notice the topmost pancake is burnt. What is the
probability that the other side of the topmost pancake is also burnt?
1.9 Daily Question
If you have any trouble accessing this question, please let me know immediately on Discord.
1.10 Version : 2021/05/04
(c) 2021, George T. Heineman