graphs i
TRANSCRIPT
-
7/30/2019 Graphs i
1/33
-
7/30/2019 Graphs i
2/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 2
Edges have no direction.
If an edge connects vertices 1 and 2, either convention can be used:
No duplication: only one of(1, 2) or(2, 1) is allowed inE.
Full duplication: both (1, 2) and (2, 1) should be inE.
Example: directed graph
Edges have direction (shown by arrows).
The edge (3, 6) is not the same as the edge (6, 3) (both exist above).
Depicting a graph:
The picture with circles (vertices) and lines (edges) is only a depiction
=> a graph is purely a mathematical abstraction.
Vertex labels:
Can use letters, numbers or anything else.
Convention: use integers starting from 0.
=> useful in programming, e.g. degree[i] = degree of vertex i.
Edges can be drawn "straight" or "curved".
The geometry of drawing has no particular meaning:
-
7/30/2019 Graphs i
3/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 3
Graph conventions:
What's allowed (but unusual) in graphs:
Self-loops (occasionally used).
Multiple edges between a pair of vertices (rare).
Disconnected pieces (frequent in some applications).
Example:
What's not (conventionally) allowed:
Mixing undirected and directed edges.
Re-using labels in vertices.
Bidirectional arrows.
Most common:
No multiple edges.
No self-loops.
Other terms used:
Vertices: nodes, terminals, endpoints.
Edges: links, arcs.
In-Class Exercise 7.1: If we disallow multiple edges and self-loops, what is the maximum number of edges in an undirected graph with n
vertices? What is this number in order-notation?
Definitions:
Degrees:
Undirected graph: the degree of a vertex is the number of edges incident to it.
Directed graph: the out-degree is the number of (directed) edges leading out, and the in-degree is the number of (directed) edges
terminating at the vertex.
-
7/30/2019 Graphs i
4/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 4
Neighbors:
Two vertices are neighbors (or are adjacent) if there's an edge between them.
Two edges are neighbors (or are adjacent) if they share a vertex as an endpoint.
Paths:
Undirected: a sequence of vertices in which successive vertices are adjacent.
Directed: a sequence of vertices in which every pair of successive vertices has this property: there's a directed edge from the first to
the second.
A simple path does not repeat any vertices (and therefore edges) in the sequence.
A cycle is a simple path with the same vertex as the first and last vertex in the sequence.
Connectivity:
Undirected: Two vertices are connected if there is a path that includes them.
Directed: Two vertices are strongly-connected if there is a (directed) path from one to the other.
Components:
A subgraph is a subset of vertices together with the edges from the original graph that connects vertices in the subset.
Undirected: A connected component is a subgraph in which every pair of vertices is connected.
Directed: A strongly-connected component is a subgraph in which every pair of vertices is strongly-connected.
A maximal component is a connected component that is not a proper subset of another connected component.
Digraph: another name for a directed graph.
Example:
More definitions:
Euler tour: A cycle that traverses all edges exactly once (but may repeat vertices).
-
7/30/2019 Graphs i
5/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 5
Known result: Euler tour exists if and only if all vertices have even degree.
Hamiltonian tour: A cycle that traverses all vertices exactly once.
Known result: testing existence of a Hamiltonian tour is (very) difficult.
Euler path: A path that traverses all edges exactly once.
Hamiltonian path: A path that traverses all vertices exactly once.
Trees:
A tree is a connected graph with no cycles.
A spanning tree of a graph is a connected subgraph that is a tree.
Weighted graphs:
Sometimes, we include a "weight" (number) with each edge.
Weight can signify length (for a geometric application) or "importance".
Example:
-
7/30/2019 Graphs i
6/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 6
Why are graphs important?
History:
Maze-searching.
Euler's crossing problem: the Konigsberg bridges
How to cross each bridge just once and return to starting point?
Applications:
Fundamental mathematical construct to represent "connectivity".
Appears in thousands of problems.
Source of many classic problems: traveling salesman, routing, spanning trees.
Many "graph-structured" applications: networks, transportation-systems, electronic circuits, molecules.
Source of theory:
Many important algorithms.
Key to understanding algorithm design and analysis.
Simple to describe, yet perplexing:
Euler tour: easy problem.
Hamiltonian tour: hard problem.
The field of graph theory:
Large area of mathematics:
Analysis of general graphs.
Analysis of special types of graphs.
Many classic problems
e.g., the four-color theorem.
Optimization problems based on graphs,
e.g., shortest-paths.
Graph algorithms: an area in computer science.
Rich source of algorithms, theory, insight.
Useful algorithms used in many applications
(e.g., in a compiler).
-
7/30/2019 Graphs i
7/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 7
In this course:
Fundamental algorithms for exploring a graph: breadth-first and depth-first.
Finding shortest paths and minimum spanning tree.
Convention about undirected vs. directed:
When not specified, assume undirected.
Unless otherwise mentioned, an algorithm or definition about undirected graphs usually can be modified to apply to directed graphs.
(although, directed graphs are usually more complicated).
In-Class Exercise 7.2: Suppose di is the degree of vertex i in a connected undirected graph with n vertices and m edges. LetD = d1 + ... +
dn. What is the relation betweenD and m?
Graph Data Structures
First, an idea that doesn't work:
We have already represented trees (like binary trees) with node instances and pointers between instances.
Idea: use a node instance for each vertex, and a pointer from one vertex to another if an edge exists between them.
In-Class Exercise 7.3: Draw an example graph and the corresponding pointer-based data structure. Why doesn't it work?
The two fundamental data structures:
Adjacency matrix.
Key idea: use a 2D matrix.
Row i has "neighbor" information about vertex i.
Undirected: adjMatrix[i][j] = 1 if and only if there's an edge between vertices i andj.
adjMatrix[i][j] = 0 otherwise.Directed: adjMatrix[i][j] = 1 if and only if there's an edge from i toj.
adjMatrix[i][j] = 0 otherwise.
Example: undirected
0 11 0 0 0 0 0
1 0 1 0 0 0 0 0
11 0 1 0 1 0 0
0 0 1 0 1 0 1 0
0 0 0 1 0 0 1 0
0 0 1 0 0 0 11
0 0 0 111 0 0
0 0 0 0 0 1 0 0
-
7/30/2019 Graphs i
8/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 8
Note: adjMatrix[i][j] == adjMatrix[j][i] (convention for undirected graphs).
Example: directed
0 1 0 0 0 0 0 0
0 0 1 0 0 0 0 0
1 0 0 1 0 1 0 0
0 0 0 0 1 0 1 0
0 0 0 0 0 0 1 0
0 0 0 0 0 0 0 1
0 0 0 1 0 0 0 0
0 0 0 0 0 0 0 0
Adjacency list.
Key idea: use an array of vertex-lists.
Each vertex list is a list of neighbors.
Example: undirected
Example: directed
-
7/30/2019 Graphs i
9/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 9
Convention: in each list, keep vertices in order of insertion
=> add to rear of list
Both representations allow complete construction of the graph.
Advantages of matrix:
Simple to program.
Some matrix operations (multiplication) are useful in some applications (connectivity).
Efficient for dense (lots of edges) graphs.
Advantages of adjacency list:
Less storage for sparse (few edges) graphs.
Easy to store additional information in the data structure.
(e.g., vertex degree, edge weight)
In-Class Exercise 7.4: Suppose a graph has Vvertices andEedges. In order-notation (in terms ofVandE), what is the size of the adjacency
matrix representation? What is the size of the adjacency list representation?
In-Class Exercise 7.5: Write a Java class to implement the adjacency list representation. Do not write all the details, but merely what you need
for initializing the data structure.
Graph ADT:
ADT = Abstract Data Type.
What operations should a graph ADT support?Insertion of vertices, edges.
Is the graph connected?
What are the connected components?
Does the graph have a cycle?
Example: consider this interface for an undirected graph ADT:
public interface UndirectedGraphSearchAlgorithm {
// Call this once to set up data structures.
public void initialize (int numVertices, boolean isWeighted);
-
7/30/2019 Graphs i
10/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 10
// Call this repeatedly with each edge to be inserted:
public void insertUndirectedEdge (int startVertex, int endVertex, double weight);
// Return the number of connected components. If "1", then the whole
// graph is connected.
public int numConnectedComponents ();
// Return the component to which each vertex belongs.
public int[] componentLabels ();
// Is there a cycle in the graph?
public boolean existsCycle();
}
In-Class Exercise 7.6: How would you use this ADT to tell whether a graph is a connected tree?
Next, let's look at some sample code using the matrix representation:
Convention for weighted graphs: use weight=0 to represent the lack of an edge.
public class UndirectedGraphSearch implements UndirectedGraphSearchAlgorithm {
int numVertices; // Number of vertices, given as input.
int numEdges; // We keep track of the number of edges.
boolean isWeighted; // Is this a weighted graph?
double [][] adjMatrix; // The matrix. Note: we use "double" to store
// "double" weights, if the graph is weighted.
public void initialize (int numVertices, boolean isWeighted)
{
// Store:
this.numVertices = numVertices;
this.isWeighted = isWeighted;
// Create the adjacency matrix.
adjMatrix = new double [numVertices][];
for (int i=0; i < numVertices; i++) {
adjMatrix[i] = new double [numVertices];
for (int j=0; j < numVertices; j++)
adjMatrix[i][j] = 0;
numEdges = 0;
}
// Insert a given input edge.
public void insertUndirectedEdge (int startVertex, int endVertex, double weight)
{
// Unweighted graph: use weight 1.0.
if (! isWeighted) {
weight = 1.0;
}
// Insert in both places:
adjMatrix[startVertex][endVertex] = weight;
-
7/30/2019 Graphs i
11/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 1
adjMatrix[endVertex][startVertex] = weight;
numEdges ++;
}
The same with an adjacency list:
public class UndirectedGraphSearch implements UndirectedGraphSearchAlgorithm {
int numVertices; // Number of vertices, given as input.
int numEdges; // We keep track of the number of edges.
boolean isWeighted; // Is this a weighted graph?
LinkedList[] adjList; // The list (of lists): one list for each vertex.
// We are using java.util.LinkedList as our linked list.
// This will store instances of GraphEdge.
// We will test new insertions to see if they exist. Because LinkedList performs
// ".equals()" testing for containment, we need an instance (and only one) of GraphEdge
// for such testing.
GraphEdge testEdge = new GraphEdge (-1, -1, 0);
public void initialize (int numVertices, boolean isWeighted)
{
// Store:
this.numVertices = numVertices;
this.isWeighted = isWeighted;
// Adjacency-list representation
adjList = new LinkedList [numVertices];
for (int i=0; i < numVertices; i++)
adjList[i] = new LinkedList ();
// Note: we are using java.util.LinkedList.
numEdges = 0;
}
// Insert a given input edge.
public void insertUndirectedEdge (int startVertex, int endVertex, double weight)
{
if (! isWeighted) {
weight = 1.0;
}
// Adj-list representation: see if the edge is already there.
testEdge.startVertex = startVertex;
testEdge.endVertex = endVertex;
// Exploit the methods in java.util.LinkedList.
if (adjList[startVertex].contains (testEdge)) {
// ... report error ...
return;
}
// It's undirected, so add to both vertex lists.
GraphEdge e = new GraphEdge (startVertex, endVertex, 1.0);
adjList[startVertex].addLast (e);
// We wouldn't have this for directed graphs:
GraphEdge e2 = new GraphEdge (endVertex, startVertex, 1.0);
adjList[endVertex].addLast (e2);
numEdges ++;
}
-
7/30/2019 Graphs i
12/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 12
Breadth-First Search
About graph search:
"Searching" here means "exploring" a particular graph.
Searching will help reveal properties of the graph
e.g., is the graph connected?
Usually, the input is: vertex set and edges (in no particular order).
Key ideas in breadth-first search: (undirected)
Mark all vertices as "unvisited".
Initialize a queue (to empty).
Find an unvisited vertex and apply breadth-first search to it.
Add the vertex's neighbors to the queue.
Repeat: extract a vertex from the queue, and add its "unvisited" neighbors to the queue.
Example:
Initially, place vertex 0 in the queue.
Dequeue 0
=> mark it as visited, and add its unvisited neighbors to queue:
Dequeue 1
=> mark it as visited, and add its unvisited neighbors to queue:
-
7/30/2019 Graphs i
13/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 13
Dequeue 2
=> mark it as visited, and add its unvisited neighbors to queue:
Dequeue 2
=> it's already visited, so ignore.
Continuing ...
Breadth-first search tree, and visit order:
-
7/30/2019 Graphs i
14/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 14
Exploring an edge: examining an unvisited neighbor.
If an unvisited neighbor gets on the queue for the first time, the edge is called a "tree edge".
Putting the tree edges and all vertices together results in: the breadth-first search tree.
For a particular graph and its implementation, the tree produced is unique.
However, starting from another vertex will result in another tree, that may be just as useful.
In-Class Exercise 7.7: What is the visit-order and breadth-first search tree for this graph:
Searching an unconnected graph:
The connected components are explored in order:
Example:
The tree, and visit order:
-
7/30/2019 Graphs i
15/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 15
Implementation:
There are two varieties:
Allow a vertex to have two states: "unvisited" and "visited"
An "unvisited" vertex can get placed in the queue multiple times.
It's possible to dequeue a "visited" vertex.
When dequeueing, check whether vertex was visited.Allow three states: "unvisited", "in-queue", and "visited":
When placing a neighbor in the queue for the first time, mark it as "in-queue".
Place only "unvisited" neighbors in queue.
=> Queue has only unique, unvisited vertices.
Extra space required.
We'll use the former approach, with an adjacency matrix:
Pseudocode:
Algorithm: breadthFirstMatrix (adjMatrix, n)
Input: A graph's adjacency matrix, number of vertices n.
// Visit order will start with "0", so initialize to -1.
1. for i=0 to n-1
2. visitOrder[i] = -1
3. endfor
// A counter for the order:
4. visitCount = -1
// Standard queue data structure.
5. Create queue;
// Look for an unvisited vertex and explore its tree.
// We need this because the graph may have multiple components.
6. for i=0 to n-17. if visitOrder[i] < 0
// We call this "iterative" because other searches are recursive.
8. breadthFirstMatrixIterative (i)
9. endif
10. endfor
Algorithm: breadthFirstMatrixIterative (u)
Input: vertex u, adjMatrix is assumed to be global.
// Queue needs to be reset for each tree.
1. Clear queue;
-
7/30/2019 Graphs i
16/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 16
// Place root of tree on the queue.
2. queue.addToRear (u);
// Continue processing vertices until no more can be added.
3. while queue not empty
// Remove a vertex.
4. v = remove item at front of queue;
// If it hasn't been visited ...
5. if visitOrder[v] < 0
// Visit the vertex.
6. visitCount = visitCount + 1
7. visitOrder[v] = visitCount // Look for neighbors to visit.
8. for i=0 to n-1
9. if adjMatrix[v][i] = 1 andi != v // Check self-loop: i != v
10. queue.addToRear (i)
11. endif
12. endfor
13. endif
14. endwhile
In-Class Exercise 7.8: Does the above code identify components? If so, where? If not, how can you modify the code to identify components?
A sample Java implementation: (source file)
import java.util.*;
public class UndirectedBreadthFirstMatrix {
int numVertices; // Number of vertices, given as input.
int numEdges; // We keep track of the number of edges.
boolean isWeighted; // Is this a weighted graph?
boolean useMatrix = true; // Adjacency-matrix or list?
double [][] adjMatrix; // The matrix. Note: we use "double" to store
// "double" weights, if the graph is weighted.
int[] visitOrder; // visitOrder[i] = the i-th vertex to be visited in order.
int visitCount; // We will track visits with this counter.
LinkedList queue; // The queue for breadth-first, a java.util.LinkedList instance.
public void initialize (int numVertices, boolean isWeighted)
{
// ... We've seen this before ...
}
public void insertUndirectedEdge (int startVertex, int endVertex, double weight)
{
// ...}
//-------------------------------------------------------------------------------
// BREADTH-FIRST SEARCH
// Initialize visit information before search.
void initSearch ()
{
// IMPORTANT: all initializations will use "-1". We will test
// equality with "-1", so it's important not to change this cavalierly.
http://www.seas.gwu.edu/~simhaweb/alg/lectures/module7/examples/UndirectedBreadthFirstMatrix.java -
7/30/2019 Graphs i
17/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 17
visitCount = -1;
for (int i=0; i < numVertices; i++) {
visitOrder[i] = -1;
}
}
// Matrix implementation of breadth-first search.
void breadthFirstMatrix ()
{
// 1. Initialize visit variables.
initSearch ();
// 2. Create a queue.
queue = new LinkedList();
// 3. Find an unvisited vertex and apply breadth-first search to it.
// Note: if the graph is connected, the call with i=0 will result
// in visiting all vertices. Nonetheless, we don't know this
// in advance, so we need to march through all the vertices.
for (int i=0; i < numVertices; i++) {
if (visitOrder[i] == -1) {
// We call it "iterative" because depthFirst search is "recursive".
breadthFirstMatrixIterative (i);
}
}
}
// Apply breadthfirst search to a particular vertex (root of a tree)
void breadthFirstMatrixIterative (int u)
{
// 1. A fresh queue is used for a new breadth-first search.
queue.clear();
// 2. Start with the first vertex: add to REAR of queue using java.util.LinkedList.addLast().
// Because LinkedList stores objects, we "objectify" the vertex. A more efficient
// implementation would use a separate queue of int's.
queue.addLast (new Integer(u));
// 3. As long as the queue has vertices to process...
while (! queue.isEmpty()) {
// 3.1 Dequeue and extract vertex using java.util.LinkedList.removeFirst().
Integer V = (Integer) queue.removeFirst();
int v = V.intValue();
// 3.2 If the vertex has been visited, skip.
if (visitOrder[v] != -1)
continue;
// 3.3 Otherwise, set its visit order.
visitCount++;
visitOrder[v] = visitCount;
// 3.4 Next, place its neighbors on the queue.
for (int i=0; i < numVertices; i++) {
// 3.4.1 First, check whether vertex i is a neighbor.
if ( (adjMatrix[v][i] > 0) && (i != v) ) {
// 3.4.1.1 If i hasn't been visited, place in queue.
if (visitOrder[i] == -1) {
queue.addLast (new Integer(i));
}
}
} // end-for
} // end-while
-
7/30/2019 Graphs i
18/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 18
}
} // end-class
Analysis: adjacency matrix
Assume Vvertices andEedges.
Each vertex is processed Vtimes (worst-case) in starting a tree.
=> O(V).
Searching for a neighbor: O(V) (scan through matrix).
=> all scans take O(V2).
Each queue operation is O(1).
Each edge is processed once:
=> O(E) queue operations and O(E) vertex manipulations.
Total: O(V2 + E) = O(V2).
Analysis: adjacency list
Searching for a neighbor: O(# neighbors)
=> total neighbor searches is O(E) (Why?)
Other operations are the same
Total: O(V + E) = O(E).
In-Class Exercise 7.9: Which is better: adjacency matrix or adjacency list?
About O(V + E):
Note that O(V + E) = O(E)
=> it is written as O(V + E) just for emphasis.
O(V + E) is optimal:
Every vertex and every edge must be examined.
=> O(V + E)
=> not possible to do better than O(V + E)
BFS (with adjacency list) is an example of an optimal algorithm.
Directed graphs:
BFS in directed graphs is similar, except that a vertex's "neighbor" is one that's reachable by an edge going out from the vertex.
=> search code is identical (but insertion code is different).
Applications:
Connectivity:
Breadth-first search identifies connected components.
However, depth-first search is preferred (required for directed graphs).
-
7/30/2019 Graphs i
19/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 19
Shortest paths:
A path between two vertices in the tree is the shortest path in the graph.
Optimization algorithms:
Various problems result in "graph search space".
BFS together with "exploration rules" is often used to search for solutions (e.g., branch-and-bound exploration).
Note: BFS works on a weighted graph by ignoring the weights and only using connectivity information (i.e., is there an edge or not?).
Depth-First Search on Undirected Graphs
Key ideas:
Mark all vertices as "unvisited".
Visit first vertex.
Recursively visit its "unvisited" neighbors.
Example:
Start with vertex 0 and mark it visited.
Visit the first neighbor 1, mark it visited.
Explore 1's first unvisited neighbor, 2.
-
7/30/2019 Graphs i
20/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 20
Continuing until all vertices are visited ...
Vertices are marked in order of visit.
An edge to an unvisited neighbor that gets visited next is in the depth-first search tree.
In-Class Exercise 7.10: What is the visit-order and depth-first search tree for this graph:
Then, apply breadth-first search, but using a stack instead of a queue. Show the stack contents.
Implementation:
Easier than breadth-first search.
Pseudocode: adjacency matrix
Algorithm: depthFirstMatrix (adjMatrix, n)
Input: A graph's adjacency matrix, number of vertices n.
// Visit order will start with "0", so initialize to -1.
1. for i=0 to n-1
2. visitOrder[i] = -1
3. endfor
// A counter for the order:
4. visitCount = -1
-
7/30/2019 Graphs i
21/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 2
// Look for an unvisited vertex and explore its tree.
// We need this because the graph may have multiple components.
5. for i=0 to n-1
6. if visitOrder[i] < 0
7. depthFirstMatrixRecursive (i)
8. endif
9. endfor
Algorithm: depthFirstMatrixRecursive (v)
Input: vertex v, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Look for first unvisited neighbor.
3. for i=0 to n-1
4. if adjMatrix[v][i] > 0 andi != v
5. if visitOrder[i] < 0
// If unvisited visit recursively.
6. depthFirstMatrixRecursive (i)
7. endif
8. endif
9. endfor
Completion order:
In breadth-first search, once a vertex is processed, it is never processed again.
In depth-first, we also encounter a vertex after returning from the recursive call.
=> we can record a completion order.
To record the completion order, simply increment the completion-counterafter the recursion:
Algorithm: depthFirstMatrixRecursive (v)
Input: vertex v, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Look for first unvisited neighbor.
3. for i=0 to n-1
4. if adjMatrix[v][i] > 0 andi != v // Check self-loop.
5. if visitOrder[i] < 0
// If unvisited visit recursively.
6. depthFirstMatrixRecursive (i)
7. endif
8. endif
9. endfor
// After returning from recursion, set completion order:
-
7/30/2019 Graphs i
22/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 22
10. completionCount = completionCount + 1
11. completionOrder[v] = completionCount
Note: completionCount and completionOrder need to be initialized (not shown) in depthFirstMatrix.
Sample Java code: (source file)
public class UndirectedDepthFirstMatrix {
int numVertices; // Number of vertices, given as input.
int numEdges; // We keep track of the number of edges.
boolean isWeighted; // Is this a weighted graph?
boolean useMatrix = true; // Adjacency-matrix or list?
double [][] adjMatrix; // The matrix. Note: we use "double" to store
// "double" weights, if the graph is weighted.
int[] visitOrder; // visitOrder[i] = the i-th vertex to be visited in order.
int visitCount; // We will track visits with this counter.
int[] completionOrder; // completionOrder[i] = the i-th vertex that completed.
int completionCount; // For tracking.
public void initialize (int numVertices, boolean isWeighted)
{
// ...
}
public void insertUndirectedEdge (int startVertex, int endVertex, double weight)
{
// ...
}
//-------------------------------------------------------------------------------
// DEPTH-FIRST SEARCH
// Initialize visit information before search.
void initSearch ()
{ // IMPORTANT: all initializations will use "-1". We will test
// equality with "-1", so it's important not to change this cavalierly.
visitCount = -1;
completionCount = -1;
for (int i=0; i < numVertices; i++) {
visitOrder[i] = -1;
completionOrder[i] = -1;
}
}
// Matrix implementation of depth-first search.
void depthFirstMatrix ()
{
// 1. Initialize visit variables (same initialization for breadth-first search).
initSearch ();
// 2. Find an unvisited vertex and apply depth-first search to it.
// Note: if the graph is connected, the call with i=0 will result
// in visiting all vertices. Nonetheless, we don't know this
// in advance, so we need to march through all the vertices.
for (int i=0; i < numVertices; i++) {
if (visitOrder[i] < 0) {
depthFirstMatrixRecursive (i);
}
}
}
http://www.seas.gwu.edu/~simhaweb/alg/lectures/module7/UndirectedDepthFirstMatrix.java -
7/30/2019 Graphs i
23/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 23
// Recursive visiting of vertices starting from a vertex.
void depthFirstMatrixRecursive (int v)
{
// 1. First, visit the given vertex. Note: visitCount is a global.
visitCount++;
visitOrder[v] = visitCount;
// 2. Now find unvisited children and visit them recursively.
for (int i=0; i < numVertices; i++) {
// 2.1 Check whether vertex i is a neighbor and avoid self-loops.
if ( (adjMatrix[v][i] > 0) && (i != v) ) {if (visitOrder[i] == -1) {
// i is an unvisited neighbor.
depthFirstMatrixRecursive (i);
}
}
}
// 3. After returning from recursion(s), set the post-order or "completion" order number.
completionCount++;
completionOrder[v] = completionCount;
}
} // end-class
"Back" and "Down" edges:
Consider the previous example:
Back edge
When vertex 2 probes neighbor 0, 0 has already been visited.
Vertex 0 is higher up in the tree (ancestor of 2)
=> 0 has a lower visitOrder
=> visitOrder[0] < visitOrder[2].
The exploration from 2 to 0 is called a back edge.
Down edge:
When vertex 3 probes neighbor 6, 6 has already been visited.
Vertex 6 is lower down in the tree (descendant of 3)
visitOrder[6] > visitOrder[3]
The exploration from 3 to 6 is called a down edge.
-
7/30/2019 Graphs i
24/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 24
Let's modify the code to detect back and down edges:
Algorithm: depthFirstMatrixRecursive (u, v)
Input: the vertex u from which this is being called, vertex v
to be explored, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Look for first unvisited neighbor.
3. for i=0 to n-14. if adjMatrix[v][i] > 0 andi != v // Check self-loop.
5. if visitOrder[i] < 0
// If unvisited visit recursively.
6. depthFirstMatrixRecursive (i)
7. else if (i != u) // Avoid trivial case.
8. if visitOrder[i] < visitOrder[v]
// Found a back edge.
9. numBackEdges = numBackEdges + 1
10. else
// Found a down edge.
11. numDownEdges = numDownEdges + 1
12. endif
13. endif // visitOrder < 0
14. endif // adjMatrix[v][i] > 0
15. endfor
// After returning from recursion, set completion order:
16. completionCount = completionCount + 1
17. completionOrder[v] = completionCount
Why identify back and down edges?
A graph with no back edges revealed by DFS is an acyclic graph.
Down edges are useful in identifying so-called articulation edges.
Identifying connected components:
Depth-first search is most often used for identifying connected components in an undirected graph.
Key ideas:
Every time depth-first is re-started (in depthFirstMatrix)
a new component has been found.
create a new component label.
In the recursion (depthFirstMatrixRecursive), simply identify each vertex with the current component label.
Let's re-write (part of) the pseudocode to identify components:
Algorithm: depthFirstMatrix (adjMatrix, n)
Input: A graph's adjacency matrix, number of vertices n.
// Visit order will start with "0", so initialize to -1.
1. for i=0 to n-1
2. visitOrder[i] = -1
3. endfor
// A counter for the order:
4. visitCount = -1
5. completionCount = -1
6. currentComponentLabel = -1
// Look for an unvisited vertex and explore its tree.
-
7/30/2019 Graphs i
25/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 25
// We need this because the graph may have multiple components.
7. for i=0 to n-1
8. if visitOrder[i] < 0
9. currentComponentLabel = currentComponentLabel + 1
10. depthFirstMatrixRecursive (i)
11. endif
12. endfor
Algorithm: depthFirstMatrixRecursive (v)
Input: vertex v, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Mark the component label
3. componentLabel[v] = currentComponentLabel
// Look for first unvisited neighbor...
4. // ... etc (same as earlier version)
Sample Java code: (source file).
Analysis: adjacency matrix
Same as breadth-first search: O(V2)
Why?
O(1) work for processing each vertex (except for identifying neighbors).
O(V) work for identifying neighbors.
=> O(V2) overall.
Analysis: adjacency list
Similar analysis (to breadth-first search) gives: O(V + E).
DFS with adjacency list is optimal.
Applications:
Connectivity: identifying connected components.
=> which earlier-stated problem would this solve?
Cycle existence.
Others: finding articulation edges, vertices, "bipartiteness".
Identifying equivalence classes
Depth-First Search in Directed Graphs
Key ideas:
A straightforward depth-first search is similar to the undirected version
=> only explore edges going outward from a vertex in a directed graph.
In addition to "back" and "down" edges, it is useful to identify "cross" edges.
http://www.seas.gwu.edu/~simhaweb/alg/lectures/module7/examples/UndirectedDepthFirstMatrix2.java -
7/30/2019 Graphs i
26/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 26
Example:
Consider: (slightly different from previous example)
Applying DFS gives:
Let's modify the pseudocode to detect cross edges as well:
Key ideas:
What used to be a back edge in the undirected version is now either a back edge or cross edge.
Back or down edge: when the vertex on the other side is "visited" but not yet "completed".
Cross edge: when the vertex on the other side is "completed".
Pseudocode:
Algorithm: depthFirstMatrixRecursive (u, v)
Input: the vertex u from which this is being called, vertex v
to be explored, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Look for first unvisited neighbor.
3. for i=0 to n-1
4. if adjMatrix[v][i] > 0 andi != v // Check self-loop.
5. if visitOrder[i] < 0
// If unvisited visit recursively.
6. depthFirstMatrixRecursive (i)
7. else if (i != u) // Avoid trivial case.
8. if completionOrder[i] < 0 // Not finished processing i yet
-
7/30/2019 Graphs i
27/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 27
// Found a back edge.
9. numBackEdges = numBackEdges + 1
10. else if visitOrder[i] > visitOrder[v]
// Found a down edge.
11. numDownEdges = numDownEdges + 1
12. else
// Found a cross edge
13. numCrossEdges = numCrossEdges + 1
14. endif
15. endif // visitOrder < 0
16. endif // adjMatrix[v][i] > 0
17. endfor
// After returning from recursion, set completion order:
18. completionCount = completionCount + 1
19. completionOrder[v] = completionCount
Analysis: (same as undirected case)
Adjacency matrix: O(V2).
Adjacency list: O(V+E).
Strongly-Connected Components in Directed Graphs
Finding components:
Recall, in a directed graph a strongly-connected component is a set of vertices, along with edges associated with those vertices, such that
there is a path from every vertex in that set to every other in that set.
Example: vertices 0,1,2 form a strongly-connected component.
In-Class Exercise 7.11: Describe in pseudocode a simple algorithm to find strongly-connected components that uses the above DFS algorithm
for directed graphs. Hint: vertices i andj are in the same strongly-connected component ifi is reachable fromj andj is reachable from i. How
long does your algorithm take?
We will look at two different algorithms for finding strongly-connected components:
Kosaraju's Algorithm: this applies DFS twice but in an unusual way.
Tarjan's Algorithm: computes the components directly but is harder to understand.
-
7/30/2019 Graphs i
28/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 28
Kosaraju's algorithm:
First, we need to understand the reverse of a directed graph.
Suppose G = (V,E) is a graph.
G' = (V,E') is the reverse ofG ifE'=Ewith the direction of edges reversed.
Next, recall completion-order
later completion order => closer to root
The algorithm has two phases:
First phase: find completion order on reverse graph using standard DFS.
Second phase: use a modified DFS on original graph in which completion order is used as a priority.
Let's describe the pseudocode first:
Algorithm: stronglyConnectedComponents (adjMatrix, n)
Input: A graph's adjacency matrix, number of vertices n.
1. G' = reverse (G)
2. completionOrder = depthFirstSearch (G')
3. sortOrder = sort vertices in reverse order of completion // Last to first.
4. componentLabels = modifiedDepthFirstSearch (G, sortOrder)
5. return componentLabels
Algorithm: modifiedDepthFirstSearch (adjMatrix, sortOrder)
Input: A graph's adjacency matrix, a sort (priority) order of vertices
1. for i=0 to n-1
2. visitOrder[i] = -1
3. endfor
4. visitCount = -1
5. currentComponentLabel = -1
// Look for an unvisited vertex and explore its tree.
6. for i=0 to n-1
7. v = sortOrder[i] // In order of completion of reverse search.
8. if visitOrder[v] < 0
9. currentComponentLabel = currentComponentLabel + 1
10. modifiedDepthFirstMatrixRecursive (v)
11. endif12. endfor
13. return componentLabels
Algorithm: modifiedDepthFirstMatrixRecursive (v)
// Mark vertex v as visited and record component label.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
3. componentLabel[v] = currentComponentLabel
// Look for first unvisited neighbor.
4. for i=0 to n-1
u = sortOrder[i] // In order of completion of reverse search.
-
7/30/2019 Graphs i
29/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 29
5. if adjMatrix[v][u] > 0 andu != v
6. if visitOrder[u] < 0
7. depthFirstMatrixRecursive (u)
8. endif
9. endif
10. endfor
Why does this work? The proof is in two parts.
Consider two vertices u and v.
Part (1): Ifu and v are mutually reachable, does the algorithm report that they are in the same component?
Suppose u is visited before v in the second-phase.
Then, DFS will certainly visit v.
=> The algorithm reports both in the same component.
The same holds ifv is visited before u.
Part (2): If the algorithm reports that u and v are in the same component, is that in fact always true?
Because u and v are in the same reported component, they are part of a single tree
=> Let w be the root of this tree.
Then w was visited before u and v.
=> w has a later phase-1 completion time than u and v.
(Store this fact for now).
Next, observe that we reached u from w in the second phase
=> there's a directed path from w to u in G.
=> there's a directed path from u to w in G'.
Suppose there is no path from w to u in G' (the reverse).
=> Then, the path from u to w in G' would result in u having a later completion time
-
7/30/2019 Graphs i
30/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 30
=> Contradiction.
Thus, there is a path from w to u in G'.
=> There is a path in G from u to w,
Which means, there are paths in G from w to u and back.
The same argument shows that there is path from w to v and back.
=> u and v are mutually reachable in G.
Tarjan's Algorithm:
Some intuition:
Consider what happens when we complete processing "3" in
How can we identify the strong component "3, 4, 6"?
First, there is no backedge that goes from "4, 6" to any node "above" 3.
Second, both "4" and "6" were visited in the descent from "3".
But nodes "above" 3 will have a lower visitOrder.
=> keep track of lowest visitOrder reachable by a back edge from a "potential component".
How to mark vertices in a "component"?
Build a stack.
Place vertices on the stack as you recurse down.
When coming back up the recursion, if you've identified a component, all vertices in the component are going to be at the top
of the stack.
Example above: "3, 4, 6" will be on the stack when returning to "3".
Another example: "0, 1, 2" will be at the top when returning to "0".
(It won't be removed along the way back from "3" to "0").
Pseudocode:
Algorithm: depthFirstMatrixRecursive (u, v)
Input: the vertex u from which this is being called, vertex v
to be explored, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Current lowest reachable:
3. lowestReachable[v] = visitOrder[v];
// Important: minLowestReachable is a local variable so that a
// fresh version is used in each recursive call.
4. minLowestReachable = lowestReachable[v];
// Initialize stackTop to 0 outside. stackTop points to next available.
5. stack[stackTop] = v;
6. stackTop ++;
-
7/30/2019 Graphs i
31/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 3
// Look for first unvisited neighbor.
7. for i=0 to n-1
8. if adjMatrix[v][i] > 0 andi != v // Check self-loop.
9. if visitOrder[i] < 0
// If unvisited visit recursively.
10. depthFirstMatrixRecursive (i)
11. else if (i != u) // Avoid trivial case.
// Identify back, down and cross edges ... (not shown)
12. endif // visitOrder < 0
13. if lowestReachable[i] < minLowestReachable
14. minLowestReachable = lowestReachable[i]15. endif
16. endif // adjMatrix[v][i] > 0
17. endfor
// We reach here after all neighbors have been explored.
// Check whether a strong component has been found.
18. if minLowestReachable < lowestReachable[v]
// This is not a component, but we need to update the
// lowestReachable for this vertex since its ancestors will
// use this value upon returning.
19. lowestReachable[v] = minLowestReachable
20. return
21. endif
// We've found a component.
22. do
// Get the stack top.
23. stackTop = stackTop - 1
24. u = stack[stackTop];
// We start currentStrongComponentLabel from "0".
25. strongComponentLabels[u] = currentStrongComponentLabel;
// Set this to a high value so that it does not affect
// further comparisons up the DFS tree.
26. lowestReachable[u] = numVertices;
27. while stack[stackTop] != v
28. currentStrongComponentLabel = currentStrongComponentLabel + 1
// After returning from recursion, set completion order... (not shown)
Directed Acyclic Graphs (DAG's)
What are they?
A DAG is a directed graph without a cycle
=> no path can cause you to revisit a vertex.
Example:
-
7/30/2019 Graphs i
32/33
3/2/13 Algorithms
www.seas.gwu.edu/~simhaweb/alg/lectures/module7/module7.html 32
Example application: task scheduling
A large activity consists of many related tasks.
Some tasks need to occur before others.
=> typically a precedence relation among tasks.
Represent task precedence using a DAG.
Sequential scheduling:
Suppose the tasks represent programs that must be executed on a sequential processor.
Goal: find an execution sequence that does not violate precedence constraints.
Example:3 0 1 2 5 4 6 7
Note: 4 0 1 2 5 3 6 7 violates precedence requirements.
Topological sort:
A topological sortof a DAG is a sequence of vertices that:
does not violate precedence;
contains all the vertices.
A simple (the "classic") algorithm:
Find a vertex that has no predecessors (zero in-degree)
(there must be one, or it's not a DAG).
Add this to the sequence.
Remove it from the graph.
In removing, adjust the in-degree of every neighbor of the removed vertex.
Repeat.
In-Class Exercise 7.12: Apply the classic algorithm to the above example.
Using depth-first search:
Key observation: the vertex whose completion occurs first in depth-first search can be placed last in the sequence.
-
7/30/2019 Graphs i
33/33
3/2/13 Algorithms
Why? It has no successors (descendants in the DFS tree).
Steps:
Examine completionOrder in depth-first search.
Place first vertex to complete at end of sequence.
Remove it from the graph.
Place next vertex to complete in next-to-last in sequence.
... and so on ...
Note: after DFS "completes" a vertex, the vertex is never seen again
=> no need to remove it from graph!
Example:
Pseudocode:
We only need to record topological sort order every time a completion occurs.
Partial pseudocode:
Algorithm: depthFirstMatrixRecursive (u, v)
Input: the vertex u from which this is being called, vertex v
to be explored, adjMatrix is assumed to be global.
// Mark vertex v as visited.
1. visitCount = visitCount + 1
2. visitOrder[v] = visitCount
// Look for first unvisited neighbor and recurse
3. for i=0 to n-1
// ... (same as before, not shown) ...
4. endfor
// After returning from recursion, set completion order:
5 . completionCount = completionCount + 1
6. completionOrder[v] = completionCount
// Set topological sort order.
// i i i i i