DSA Chapter 5.1 2024
DSA Chapter 5.1 2024
Example Graphs
Graph Terminology
• Direction - Edges can be directed (one-way) or undirected (bi-directional). For example, a
social network friendship is undirected (Facebook), but a follower relationship is directed
(Twitter).
Directed vs Undirected
• Weight - Edges can have an associated numerical value called a weight. For example,
representing the time it takes to travel between two cities. For undirected, the weight is
optional and if it has, it would be the same for both directions. For directed graphs, the
weights can be unequal. Weights can be negative or non-negative, but negative weights
appear less frequently than non-negative ones.
• Connected – A graph where every vertex can reach any other vertex either directly with an
edge between them, or indirectly by having a path through other vertices.
A can reach D and C directly; A can reach B by traversing through C. Hence, A can reach all
vertices. Similar with B, C, and D.
• Cycle – A cycle is at least 3 distinct vertices that have a path that starts and ends at the same
vertex without revisiting any other vertices. The minimum cycle would form a triangle.
A->B->D->A is a cycle
• Cyclic – A graph with at least one cycle.
• Acyclic – A graph that contains no cycles. A tree is a connected acyclic graph.
• Big O – In graphs, Big O is commonly stated in terms of vertices (V) and edges (E). For example,
𝑂(𝑉 ∗ 𝐸) means that we would visit every vertex, and for every vertex, we will check every edge.
•
Graph Representations
• Adjacency List: For each vertex in the graph, there is a list of its adjacent vertices. The
representation consists of a list of lists, where each inner list corresponds to the neighbors
of a vertex. This is the most common representation you will see.
• Adjacency Matrix: A 2D array of V x V vertices where each cell represents an edge between
the vertices (i, j). If a cell contains 0, there is no edge. Contains either an edge weight or 1 if
an edge exists. You will see this less common than the list because it takes a lot of memory
and setup to do; its running time would be 𝑂(𝑉 2 ).
• Edge List: A list of all edges, where each edge is represented as a tuple of (source,
destination). Edge lists are generally less common than adjacency lists and matrices but can
be useful especially for graphs that have a large number of vertices but only a relatively small
number of edges.
• There are other graph representations, but we won’t discuss it in this course because they
are less common and usually have more niche use cases.
Let’s look at this example graph:
For an adjacency list, it would look like this: For an adjacency matrix, it would look like this:
{ { 0 1 2 3 <- columns
0: {{2, 5}}, <- adj. vertex is 2 w/ weight of 5 0 { 0, 0, 5, 0 },
1: {{0, 9}, {2, 11}}, 1 { 9, 0, 11, 0 },
2: {}, 2 { 0, 0, 0, 0 },
3: {{1, 4}, {2, 3} 3 { 0, 4, 3, 0 }
} }^
rows
Note that the above representations are just pseudocode
Let’s break it down:
For the list, we start with the list of adjacent vertices of 0, then we list the ones for 1, 2, and
then 3.
For the matrix, we create a 2d array, with indices 0 to 3 for the rows and 0 to 3 for the
columns.
Let’s look at vertex 0:
The vertex adjacent to it is 2, and the weight at the edge between them is 5.
• In an adjacency list, we usually list it in this order: the adjacent vertex, then the weight
associated.
• In an adjacency matrix, the rows and columns would represent each vertex. 0 is
connected to 2, so we put the weight of 5 to the row 0, column 2. 0 is not connected to
anything else, not even to itself, so the other values are 0.
Let’s look at vertex 1:
One vertex adjacent to it is 0, and the weight at the edge between them is 9. Another vertex
adjacent to it is 2. We add 0 and 2 to the list. The order for which adjacent vertex will be listed
first could vary depending on implementation or specific problem, but for simplicity, we will
start listing from the lower valued vertex.
For the matrix, 1 is connected to 0, so we enter the weight 9 to the row 1, column 0. 1 is also
connected to 2, so we put 11 to row 1, column 2. The other values are 0.
For vertex 2, it’s not connected to anything (it can’t traverse to any other vertex), so we just
put an empty list. We should still list the adjacency list for every vertex even if they’re empty.
For the matrix, it’s not connected to anything, so everything is 0.
Try checking the list and matrix for vertex 3 to confirm your understanding.
Undirected Graph:
Directed Graph:
When there are no connected edges, we use ∞ so that we can distinguish it from when the
weight is 0.
Another Example of Adjacency List
Undirected Graph:
vertex list
0 1,2
1 0,2,3
2 0,1,3
3 0,1,2
Directed Graph:
vertex list
0 1,2
1 3
2 1,3
3 None
Graph Traversal
The stack will look like this if we pop off vertices when there are no more unvisited adjacent
vertices:
We have 0 in the stack. 0 is connected to 2 so we’ll push 2 to the stack. 2 has no connections
so we will pop it off the stack. We’ll push in 3 to the stack. 3 has two connections, 2 and 1. We
already visited 2 so we don’t push it, then we push 1 to the stack.
1 is connected to 0 but we have already visited 0 so we don’t push it to the stack. 1 has no
other unvisited connections so we pop it off the stack. Now 3 has no other connections
unvisited so we pop it off the stack. 0 has no more unvisited connections so we pop it off the
stack. We have successfully visited all the vertices. The output is 2, 1, 3, 0.
Note that this is only one way of using the stack. You could instead pop off the vertices from the
stack right after they’re visited. In that case, the output would be 0, 2, 3, 1.
Now we’ll do BFS, starting from 0. In BFS, we dequeue a vertex first before enqueueing its
adjacent vertices, if there are.
We have 0 in the queue. We’ll dequeue it. 0 is connected to 2 and 3, so we’ll add them to the
queue in that order. We will dequeue 2. 2 has no connections so we don’t enqueue anything
after 3.
We will dequeue 3. 3 is connected to 2 and 1, but 2 was already visited so we only enqueue 1.
We will dequeue 1. 1 has no more connections and we have successfully visited all the
vertices
Let’s try to implement BFS on an adjacency matrix:
#include <iostream>
#include <vector>
#include <queue>
// Mark as seen, set the previous vertex, and add to the queue
seen[i] = true;
prev[i] = curr;
q.push(i);
}
}
// If the target vertex wasn't reached, return an empty path
if (prev[targetVertex] == -1) {
return {};
}
// Build the path from target to start by following the previous vertice
s
int curr{ targetVertex };
std::vector<int> out;
do {
out.push_back(curr);
curr = prev[curr];
} while (curr != -1);
return out;
}
int main() {
// Define a graph as an adjacency matrix
AdjacencyMatrix graph =
{
{0, 1, 1, 0, 0},
{1, 0, 1, 1, 0},
{1, 1, 0, 0, 0},
{0, 1, 0, 0, 1},
{0, 0, 0, 1, 0}
};
int startVertex{ 0 };
int targetVertex{ 4 };
// Print the BFS path from the start vertex to the target vertex
std::cout << "BFS from vertex " << startVertex << " to vertex " << targe
tVertex << ": ";
std::vector<int> path{ bfs(graph, startVertex , targetVertex) };
if (path.empty()) {
std::cout << "No path found." << '\n';
}
else {
for (int i{ 0 }; i < path.size(); ++i) {
std::cout << path[i] << " ";
}
std::cout << '\n';
}
return 0;
}
In the example graph:
// Recursive function to traverse through the graph. It keeps track of the vert
ices visited and the path taken.
bool walk(WeightedAdjacencyList& graph, int curr, int target, std::vector<bool>
& seen, std::vector<int>& path) {
// If the current vertex has been visited before, return false.
if (seen[curr]) {
return false;
}
// If there is no path to the target, remove the current vertex from the pa
th and return false.
path.pop_back();
return false;
}
int main()
{
// Create a weighted graph
WeightedAdjacencyList graph = {
{{1, 2}, {2, 3}}, // edges from vertex 0
{{2, 2}}, // edges from vertex 1
{{3, 2},{4, 2}}, // edges from vertex 2
{{2, 4}}, // edges from vertex 3
{} // edges from vertex 4
};
return 0;
}
The output is
Path: 0 1 2 4
Here is how the algorithm works:
1. We start from the vertex 0. We mark it as visited and add it to the path.
6. We explore its neighbors. The first neighbor is 3, but it does not allow us to go to 4, so we
take the next, 4.
If you just look at the graph, you might think 0 -> 2 -> 4 is a valid path. That's correct. But it doesn't
mean the code is wrong, it's just that DFS chose a different path.
Why would we see different paths with DFS than what we might visually expect in an unweighted
graph?
1. The order in which the edges are stored in the WeightedAdjacencyList can influence the
order of exploration. If the edge (0, 1) comes before (0, 2) in the list for node 0, DFS will
explore the path through node 1 first.
2. DFS prioritizes going deep. It won't consider other options at the same level until it has fully
explored one branch. So, if it finds a path to the target through one branch, it won't find
other branches that might be shorter.
DFS doesn't guarantee finding the shortest path, and the order of exploration is affected by the
structure of the adjacency list and the recursive nature of the algorithm.