Table of Contents

3.1 Basic Definitions and Applications

Summary

Undirected graphs indicate symmetric relationships while directed graphs depict asymmetric relationships. An undirected graph is connected if, if for every pair of node u and v, there is a path from u to v. Graphs can be used to depict a number of things such as transportation networks, connection networks, dependency networks, information networks, and social networks. A directed graph is strongly connected if, for every two nodes u and v, there is a path from u to v and a path from v to u. One of the fundamental operations in a graph is that of traversing a sequence of nodes connected by edges, also known as paths. An undirected graph is a tree if it is connected and it does not contain a cycle. Rooted trees are crucial objects in computer science because they encode the notion of a hierarchy. If a graph is connected and does not contain a cycle, then it has n-1 edges. If a graph is connected and has n-1 edges, then it does not contain a cycle. If a graph does not contain a cycle and has n-1 edges, then it is connected.

Notes

Graph G is a way of encoding pairwise relationships among a set of objects: the consists of a collection V of nodes and a collection E of edges. Edges in a (undirected) graph indicate a symmetric relationship between their ends. Asymmetric relationships are defined in a directed graph, in which the roles of the two given elements are not interchangeable; there is a tail node (edge leaves) and a head node (edge enters). A node in a graph is also called a vertex.

Examples of Graphs

Paths and Connectivity

Trees

Questions
Additional Information

In class, we did not go into any particular depth on directed graphs. So, after reading the section multiple times, I feel like really understand directed graphs now and how they differ from undirected graphs. Asymmetric relationships are defined in a directed graph. The roles of the two given elements are not interchangeable; there is a tail node (edge leaves) and a head node (edge enters). (u,v) does not equal (v, u). In the instance of (u, v) u is the tail and v is the head. Moreover, a directed graph is considered strongly connected it there is an edge that connect u to v and an edge that connects v to u. Finally, paths have to respect the direction of the edges. In other words, if there is an edge from u to v, the path cannot move in the direction v to u.

This section was a 9 out of 10 in readability because it was mostly an introduction section, which included a bunch of definitions. Thus, there was not a whole lot for me to have to wrap my brain around. Moreover, the definitions and properties of different graphs were explained very explicitly and clearly, which made comprehension easy. Also, the different examples of graphs really helped me understand the essence of graphs.

3.2 Graph Connectivity and Traversal

Summary

There are two ways of identifying node-to-node connectivity: breadth-first search and depth-first search. Breadth-first search explores nodes from s out in all possible directions and adds nodes to the resulting BFS tree layer by layer. Depth-first search starts at node s and you follow one edge out to another node and an edge from that node outward and so on. When you hit a dead end you backtrack to a node that has an edge to a node that is not yet explored. The resulting BFS tree is wider, shallower, and bushier than the DFS search tree, which is longer and spindly. Moreover, the resulting theorem holds true, for any two nodes s and t in a graph, their connected components are either identical or disjointed.

Notes

Breadth-First Search

Exploring a Connected Component

Exploring a Connected Component Algorithm

 R will consist of nodes to which s has a path
 Initially R = {s}
 While there is an edge (u, v) where u is in R and v is not
    Add v to R
 Endwhile

Depth-First Search

DFS Algorithm

 DFS(u):
    Mark u as “Explored” and add u to R
    For each edge (u, v) incident to u:
       If v is not marked “Explored” then:
          Recursively invoke DFS(u)
       Endif
    Endfor

The Set of All Connected Components

Additional Information

Initially, I was confused by the following claim: for any two nodes s and t in a graph, their connected components are either identical or disjointed. But, after thinking it over, it becomes blatantly obvious. If there is a path from s to t, then their connected components must be identical because it does not matter what node you start your BFS or DFS search at in a given connected component, it will always yield the same components within their respective trees. And, conversely, if there is not a path between two nodes, then there can be no nodes that their BFS or DFS trees have in common because then they would be connected components. Therefore, their components must be disjointed.

On a scale of 1 to 10, this section was an 8 in terms of readability. The differences between BFS and DFS were explained very well, and the two given algorithms were laid out very explicitly. The only reason this section was not a 10 was because, as usual, it takes me a couple of reads to fully understand the theorems (and what they mean in normal person language) and their proofs.

3.3 Implementing Graph Traversal

Summary

There are two ways to represent graphs for algorithm implementation: an adjacency matrix and an adjacency list. Overall, for our case, the adjacency list is a more efficient choice for algorithm implementation – including BFS and DFS algorithms. To implement BFS, we will use an adjacency list, maintain an array that depicts which nodes have been discovered, and lists to represent the layers. The BFS algorithm that results from this implementation runs in O(m + n) time. BFS can also be implemented using a queue, but that version does not improve upon the runtime for the implementation we already have. DFS is recursive, and it maintains the nodes waiting to be processed in a stack. We will also maintain an array that depicts which nodes have been explored. The resulting runtime from this implementation of DFS is O(n + m). Moreover, BFS and DFS algorithms never see nodes that are disjointed from the starting node.

Notes

Representing Graphs

Queues and Stacks

Implementing Breadth-First Search

BFS Implementation

 BFS(s):
    Set Discovered[s] = true and Discovered[v] = false for all other v
    Initialize L[0] to consist of the single element s
    Set the layer counter i = 0
    Set the current BFS tree T = Ø
    While L[i] is empty:
       Initialize an empty list L[i+1] 
       For each node u in the set L[i]
          Consider each edge (u, v) incident to u
          If Discovered[v] = false:
             Set Discovered[v] = true
             Add edge (u, v) to the tree T
             Add v to the list L[i+1]
          Endif
       Endfor
       Increment the layer counter i by one
   Endwhile

Implementing Depth-First Search

DFS Implementation

 DFS(s):
    Initialize S to be a stack with one element s
    While S is not empty:
       Take a node u from S
       If Explored[u] = false then
          Set Explored[u] true
          For each edge (u, v) incident to u
             Add v to the stack S
          Endfor
       Endif
    Endwhile

Finding a Set of All Connected Components

Additional Information

The difference between a node being “explored” and a node being “discovered” was pretty confusing to me, especially because in class no clear distinction between the two was made. When a node is discovered, it is the first time the node is seen. An edge found after a previous node was explored allowed us to see, and ultimately, discover this new node. Dissimilarly, when a node is explored, it is when al incident edges to that given node are scanned, and potentially yield new nodes for the connectivity algorithm to explore.

On a scale of 1 to 10, this section was a 7 in terms of readability. I was kind of confused on why the section was called 3.3 Implementing Graph Traversal Using Queues and Stacks when the BFS was not initially explained using a queue. The queue version of the implementation felt kind of like an after thought, especially because the queue versus the regular list implementation did not really make any improvements in terms of runtime. Maybe they did this to explicitly show the difference between BFS and DFS, but nonetheless it seemed like an afterthought. Besides that, the rest of the section was extremely well written and explained very clearly, especially the DFS implementation with stacks.

3.4 Bipartite Graphs: An Application of BFS

Summary

A bipartite graph is one where the nodes can be broken up into two sets in a way that every edge has one end in one set and the other end in the other. If a graph is bipartite, then it cannot contain an odd cycle. The presence of an odd cycle is the only obstacle to a graph being bipartite. The algorithm we will use to test whether a graph is bipartite is basically laid over BFS with the added complexity that the odd layers will be colored blue and the even layers will be colored red. Therefore, the total runtime for the algorithm is O(n + m). If there is no edge in the graph that joins two nodes of the same layer, the graph is bipartite. If there is an edge that joins two nodes in the same layer, the graph is not bipartite.

Notes

The Problem

Designing the Algorithm

Analyzing the Algorithm

Additional Information

The concept of an odd cycle was initially kind of confusing to me when it was explained in class. However, now to me it is clear that an odd cycle is a cycle that has an odd number of nodes in it and, therefore, has an odd number of edges. This would make a graph unable to be bipartite because starting at any point in the cycle, the first node you color and the last node that you color will have to be the same color. Therefore, the odd cycle within a graph keeps the graph from being defined as bipartite.

On a scale of 1 to 10, this section was a 9 in terms of readability. I was a little nervous about how easy it would be to comprehend this section because the section relied heavily on the concept of visualizing the binary coloring of theoretical nodes. However, the authors did a fantastic job of using the colors to clearly explain how to check if a graph is bipartite.

3.5 Connectivity in Directed Graphs

Summary

In directed graphs, the edge (u, v) represents an edge that goes from u to v; edges have a direction. To represent directed graphs, we will still use adjacency lists, but, now, there are two lists that are associated with each node. One list represents the nodes that the given node has edges to (outgoing edges); the other list represents the nodes that the given node has edges from (incoming edges). Directed BFS and directed DFS still run in O(n + m) runtime; the only thing that changes is the fact that the algorithms have to follow the rules of the directional edges. A strongly connected graph if, for every two nodes u and v, there is a path from u to v and a path from v to u. If u can be reached from v and if v can be reached from u, then the two nodes are mutually reachable. An algorithm checking for strong connectivity merely requires BFS over graph G and graph G with its edges reversed. If both searches yield the same results, the component is strongly connected. This algorithm runs in O(m + n) runtime.

Notes

The Graph Search Algorithms

Strong Connectivity

Questions
Additional Information

One thing that I was struggling to understand at first was how you can get set of nodes with paths to s instead of getting a set of nodes that s has paths to. This was difficult at first for me to grasp because with getting a set of nodes that s has a path to, it is just performing BFS and DFS with the additional condition of edge directions. However, after reading the section a second time, I was able to understand how you can get a set of nodes with paths to s. You merely need to reverse the direction of all of the edges in the original graph G and put the in a new graph, and traverse that one with BFS or DFS from s. I'm still a little confused on how exactly you can reverse the directions of the original graph. Hopefully, that will be explained to us in class.

On a scale of 1 to 10, this section was a 10 in terms of readability. This section was merely an extrapolation on what we learned in section 3.1 with directed graph connectivity and 3.2 and 3.3 with BFS and DFS. The authors did a fantastic job of explaining how directed graphs really aren't that different than undirected graphs; they just have the added condition of an edge having direction. This explanation went along well with their explanations of directed BFS, DFS, and strong connectivity.

3.6 Directed Acyclic Graphs and Topological Ordering

Summary

A directed graph is acyclic if it has no cycles. A topological ordering of a graph is an ordering of its nodes as v1, v2,… so that i<j for every edge (vi, vj). In other words, all of the edges are pointing forward. All DAGs have topological orderings. The first node in the topological ordering of an directed acyclic graph would need to have no incoming edges. We can implement an algorithm for finding the topological ordering of a DAG in O(m + n) time by keeping track of the active nodes, keeping track of the number of incoming edges for a given node and maintaining a set that has all of the active nodes in the graph with no incoming edges from other active nodes. The active nodes with no active incoming edges are the ones that will be deleted and added to the topological ordering next because there are no tasks that have precedence over that given node.

Notes

The Problem

Designing and Analyzing the Algorithm

To compute topological ordering of G:

 Find a node v with no incoming edges and order it first
 Delete v from G
 Recursively compute a topological ordering of G-{v} 
    and append this order after v
Additional Information

At first, I was confused by the concept of implementing the topological order algorithm to make it have the tighter bound of O(m + n). But after reading that portion of the section again, it became a little more clear to me. What the authors are saying is that in order to get a tighter bound on the runtime, we need to be more efficient in finding the nodes that we are trying to delete. To do this we need to keep track of the number of incoming active edges a given node has and the set of all active nodes in graph G that has no incoming edges of active nodes. This would make the algorithm run more efficiently because the next node that you should delete to add to the topological ordering is the node that has no incoming active edges. This is because at that point, there are no process that have precedence and need to be performed before the node with no incoming edges. The active incoming edges symbolize the fact that there are processes that must be performed before that given node can be popped off.

On a scale of 1 to 10, this section is a 8 in terms of readability. The only reason that this section is not a 10 is because it took me a couple of reads to fully understand topological ordering and the runtime of the algorithm, which is used to compute it.