Unit 3
Unit 3
Adjacency lists can readily be adapted to represent weighted graphs, that is, graphs for which
each edge has an associated weight, typically given by a weight function w : E R. For
example, let G = (V, E) be a weighted graph with weight function w. The weight w(u,v) of the
edge (u,v) E is simply stored with vertex v in u's adjacency list. The adjacency-list
representation is quite robust in that it can be modified to support many other graph variants.
For the adjacency-matrix representation of a graph G = (V, E), we assume that the vertices are
numbered 1, 2, . . . , |V| in some arbitrary manner. The adjacency-matrix representation of a
graph G then consists of a |V| |V| matrix A = (aij) such that
Breadth-first search
Breadth-first search is one of the simplest algorithms for searching a graph and the archetype
for many important graph algorithms. Dijkstra's single-source shortest-paths algorithm
(Chapter 25) and Prim's minimum-spanning-tree algorithm (Section 24.2) use ideas similar
to those in breadth-first search.
BFS(G,s)
1 for each vertex u V[G] - {s}
2 do color[u] WHITE
3 d[u]
4 [u] NIL
5 color[s] GRAY
6 d[s] 0
7 [s] NIL
8 Q {s}
9 while Q
10 do u head[Q]
11 for each v Adj[u]
12 do if color[v] = WHITE
13 then color[v] GRAY
14 d[v] d[u] + 1
15 [v] u
16 ENQUEUE(Q,v)
17 DEQUEUE(Q)
18 color[u] BLACK
The procedure BFS works as follows. Lines 1-4 paint every vertex white, set d [u]
to be infinity for every vertex u, and set the parent of every vertex to be NIL. Line 5
paints the source vertex s gray, since it is considered to be discovered when the
procedure begins. Line 6 initializes d[s] to 0, and line 7 sets the predecessor of the
source to be NIL. Line 8 initializes Q to the queue containing just the vertex s;
thereafter, Q always contains the set of gray vertices.
The main loop of the program is contained in lines 9-18. The loop iterates as long
as there remain gray vertices, which are discovered vertices that have not yet had
their adjacency lists fully examined. Line 10 determines the gray vertex u at the
head of the queue Q. The for loop of lines 11-16 considers each vertex v in the
adjacency list of u. If v is white, then it has not yet been discovered, and the
algorithm discovers it by executing lines 13-16. It is first grayed, and its
distance d[v] is set to d[u] + 1. Then, u is recorded as its parent. Finally, it is placed
at the tail of the queue Q. When all the vertices on u's adjacency list have been
examined, u is removed from Q and blackened in lines 17-18.
Analysis
Before proving all the various properties of breadth-first search, we take on the somewhat
easier job of analyzing its running time on an input graph G = (V,E). After initialization, no
vertex is ever whitened, and thus the test in line 12 ensures that each vertex is enqueued at most
once, and hence dequeued at most once. The operations of enqueuing and dequeuing take O(1)
time, so the total time devoted to queue operations is O(V). Because the adjacency list of each
vertex is scanned only when the vertex is dequeued, the adjacency list of each vertex is scanned
at most once. Since the sum of the lengths of all the adjacency lists is (E), at most O(E) time
is spent in total scanning adjacency lists. The overhead for initialization is O(V), and thus the
total running time of BFS is O(V + E). Thus, breadth-first search runs in time linear in the size
of the adjacency- list representation of G.
Depth-first search
As in breadth-first search, whenever a vertex v is discovered during a scan of the adjacency list
of an already discovered vertex u, depth-first search records this event by setting v's
predecessor field [v] to u. Unlike breadth-first search, whose predecessor subgraph forms a
tree, the predecessor subgraph produced by a depth-first search may be composed of several
trees, because the search may be repeated from multiple sources. The predecessor subgraph of
a depth-first search is therefore defined slightly differently from that of a breadth-first search.
DFS(G)
1 for each vertex u V[G]
2 do color[u] WHITE
3 [u] NIL
4 time 0
5 for each vertex u V[G]
6 do if color[u] = WHITE
7 then DFS-VISIT(u)
Figure 23.4 illustrates the progress of DFS on the graph shown in Figure 23.2.
Procedure DFS works as follows. Lines 1-3 paint all vertices white and initialize their fields
to NIL. Line 4 resets the global time counter. Lines 5-7 check each vertex in V in turn and,
when a white vertex is found, visit it using DFS-VISIT. Every time DFS-VISIT(u) is called in
line 7, vertex u becomes the root of a new tree in the depth-first forest. When DFS returns,
every vertex u has been assigned a discovery time d[u] and a finishing time â[u].
In each call DFS-VISIT(u), vertex u is initially white. Line 1 paints u gray, and line 2 records
the discovery time d[u] by incrementing and saving the global variable time. Lines 3-6 examine
each vertex v adjacent to u and recursively visit v if it is white. As each vertex v Adj[u] is
considered in line 3, we say that edge (u, v) is explored by the depth-first search. Finally, after
every edge leaving u has been explored, lines 7-8 paint u black and record the finishing time
in â[u].
Topological sort
A topological sort of a dag G = (V, E) is a linear ordering of all its vertices such
that if G contains an edge (u, v), then u appears before v in the ordering. (If
the graph is not acyclic, then no linear ordering is possible.) A topological sort
of a graph can be viewed as an ordering of its vertices along a horizontal line
so that all directed edges go from left to right. Topological sorting is thus
different from the usual kind of "sorting".
STRONGLY-CONNECTED-COMPONENTS(G)
1 call DFS(G) to compute finishing times f[u] for each vertex u
2 compute GT
3 call DFS(GT), but in the main loop of DFS, consider the vertices
in order of decreasing f[u] (as computed in line 1)
4 output the vertices of each tree in the depth-first forest of step 3 as a
separate strongly connected component
There are two famous algorithms for finding the Minimum Spanning Tree:
Kruskal’s Algorithm
Kruskal’s Algorithm builds the spanning tree by adding edges one by one into a growing spanning tree.
Kruskal's algorithm follows greedy approach as in each iteration it finds an edge which has least weight and
add it to the growing spanning tree.
Algorithm Steps:
Sort the graph edges with respect to their weights.
Start adding edges to the MST from the edge with the smallest weight until the edge of the largest weight.
Only add edges which doesn't form a cycle , edges which connect only disconnected components.
So now the question is how to check if 2 vertices are connected or not ?
This could be done using DFS which starts from the first vertex, then check if the second vertex is visited or
not. But DFS will make time complexity large as it has an order of �(�+�) where � is the number of
vertices, � is the number of edges. So the best solution is "Disjoint Sets":
Disjoint sets are sets whose intersection is the empty set so it means that they don't have any element in
common.
Consider following example:
In Kruskal’s algorithm, at each iteration we will select the edge with the lowest weight. So, we will start
with the lowest weighted edge first i.e., the edges with weight 1. After that we will select the second lowest
weighted edge i.e., edge with weight 2. Notice these two edges are totally disjoint. Now, the next edge will
be the third lowest weighted edge i.e., edge with weight 3, which connects the two disjoint pieces of the
graph. Now, we are not allowed to pick the edge with weight 4, that will create a cycle and we can’t have
any cycles. So we will select the fifth lowest weighted edge i.e., edge with weight 5. Now the other two
edges will create cycles so we will ignore them. In the end, we end up with a minimum spanning tree with
total cost 11 ( = 1 + 2 + 3 + 5).
Prim’s Algorithm
Prim’s Algorithm also use Greedy approach to find the minimum spanning tree. In Prim’s
Algorithm we grow the spanning tree from a starting position. Unlike an edge in Kruskal's,
we add vertex to the growing spanning tree in Prim's.
Algorithm Steps:
Maintain two disjoint sets of vertices. One containing vertices that are in the growing
spanning tree and other that are not in the growing spanning tree.
Select the cheapest vertex that is connected to the growing spanning tree and is not in
the growing spanning tree and add it into the growing spanning tree. This can be done
using Priority Queues. Insert the vertices, that are connected to growing spanning tree,
into the Priority Queue.
Check for cycles. To do that, mark the nodes which have been already selected and
insert only those nodes in the Priority Queue that are not marked.
Time Complexity:
The time complexity of the Prim’s Algorithm is because each edge is inserted in the
priority queue only once and insertion in priority queue take logarithmic time.
The single-source shortest path problem, in which we have to find shortest paths from a source
vertex v to all other vertices in the graph.
The single-destination shortest path problem, in which we have to find shortest paths from all
vertices in the directed graph to a single destination vertex v. This can be reduced to the single-source
shortest path problem by reversing the arcs in the directed graph.
The all-pairs shortest path problem, in which we have to find shortest paths between every pair of
vertices v, v' in the graph.
Bellman-Ford algorithm finds the distance in bottom up manner. At first it finds those
distances which have only one edge in the path. After that increase the path length to find all
possible solutions.
Input − The cost matrix of the graph:
06∞7∞
∞ 0 5 8 -4
∞ -2 0 ∞ ∞
∞ ∞ -3 0 9
2∞7∞0
Output − Source Vertex: 2Vert: 0 1 2 3 4Dist: -4 -2 0 3 -6Pred: 4 2 -1 0 1The graph has no
negative edge cycle
Algorithm
bellmanFord(dist, pred, source)
Input − Distance list, predecessor list and the source vertex.
Output − True, when a negative cycle is found.
Begin
iCount := 1
maxEdge := n * (n - 1) / 2 //n is number of vertices
for all vertices v of the graph, do
dist[v] := ∞
pred[v] := ϕ
done
dist[source] := 0
eCount := number of edges present in the graph
create edge list named edgeList
while iCount < n, do
for i := 0 to eCount, do
if dist[edgeList[i].v] > dist[edgeList[i].u] + (cost[u,v] for edge i)
dist[edgeList[i].v] > dist[edgeList[i].u] + (cost[u,v] for edge i)
pred[edgeList[i].v] := edgeList[i].u
done
done
iCount := iCount + 1
for all vertices i in the graph, do
if dist[edgeList[i].v] > dist[edgeList[i].u] + (cost[u,v] for edge i), then
return true
done
return false
End
Single-Source Shortest paths in Directed Acyclic Graphs
Dijkstra’s Algorithm – Single Source Shortest Path Algorithm
Dijkstra’s Algorithm is also known as Single Source Shortest Path (SSSP) problem. It is used
to find the shortest path from source node to destination node in graph.
The graph is widely accepted data structure to represent distance map. The distance between
cities effectively represented using graph.
Dijkstra proposed an efficient way to find the single source shortest path from the
weighted graph. For a given source vertex s, the algorithm finds the shortest path to
every other vertex v in the graph.
Assumption : Weight of all edges is non-negative.
Steps of the Dijkstra’s algorithm are explained here:
1. Initializes the distance of source vertex to zero and remaining all other vertices to
infinity.
2. Set source node to current node and put remaining all nodes in the list of unvisited
vertex list. Compute the tentative distance of all immediate neighbour vertex of the current
node.
3. If the newly computed value is smaller than the old value, then update it.
For example, C is the current node, whose distance from source S is dist (S, C) = 5.
d(S, N) = 11
d(S, N) = 7
d(S, C) + d(C, N) < d(S, N) ⇒ Relax
d(S, C) + d(C, N) > d(S, N) ⇒ Don’t
edge (S, N)
update d(S, N)
Update d(S, N) = 8
Weight updating in Dijkstra’s algorithm
4. When all the neighbours of a current node are explored, mark it as visited. Remove it
from unvisited vertex list. Mark the vertex from unvisited vertex list with minimum distance
and repeat the procedure.
5. Stop when the destination node is tested or when unvisited vertex list becomes empty.
Dynamic programming
Dynamic Programming (DP) is defined as a technique that solves some particular
type of problems in Polynomial Time. Dynamic Programming solutions are faster than
the exponential brute method and can be easily proved their correctness.
Characteristics of Dynamic Programming Algorithm:
In general, dynamic programming (DP) is one of the most powerful techniques for
solving a certain class of problems.
There is an elegant way to formulate the approach and a very simple thinking process,
and the coding part is very easy.
Essentially, it is a simple idea, after solving a problem with a given input, save the
result as a reference for future use, so you won’t have to re-solve it.. briefly ‘Remember
your Past’ :).
It is a big hint for DP if the given problem can be broken up into smaller sub-problems,
and these smaller subproblems can be divided into still smaller ones, and in this
process, you see some overlapping subproblems.
Additionally, the optimal solutions to the subproblems contribute to the optimal
solution of the given problem (referred to as the Optimal Substructure Property).
The solutions to the subproblems are stored in a table or array (memoization) or in a
bottom-up manner (tabulation) to avoid redundant computation.
The solution to the problem can be constructed from the solutions to the subproblems.
Dynamic programming can be implemented using a recursive algorithm, where the
solutions to subproblems are found recursively, or using an iterative algorithm, where
the solutions are found by working through the subproblems in a specific order.
Dynamic programming works on following principles:
Characterize structure of optimal solution, i.e. build a mathematical model of the
solution.
Recursively define the value of the optimal solution.
Using bottom-up approach, compute the value of the optimal solution for each possible
subproblems.
Construct optimal solution for the original problem using information computed in the
previous step.
Applications:
ynamic programming is used to solve optimization problems. It is used to solve many real-
life problems such as,
(i) Make a change problem
(ii) Knapsack problem
(iii) Optimal binary search tree
Floyd–Warshall algorithm
The Floyd–Warshall algorithm compares many possible paths through the graph between each
pair of vertices. It is guaranteed to find all shortest paths and is able to do this
with comparisons in a graph, even though there may be edges in the graph. It does
so by incrementally improving an estimate on the shortest path between two vertices, until the
estimate is optimal.
Example
Algorithm
Step 1 − Construct an adjacency matrix A with all the costs of edges present in the graph. If
there is no path between two vertices, mark the value as ∞.
Step 2 − Derive another adjacency matrix A1 from A keeping the first row and first column of
the original adjacency matrix intact in A1. And for the remaining values, say A1[i,j],
if A[i,j]>A[i,k]+A[k,j] then replace A1[i,j] with A[i,k]+A[k,j]. Otherwise, do not change the
values. Here, in this step, k = 1 (first vertex acting as pivot).
Step 3 − Repeat Step 2 for all the vertices in the graph by changing the k value for every pivot
vertex until the final matrix is achieved.
Step 4 − The final adjacency matrix obtained is the final solution with all the shortest paths.
Analysis
From the pseudocode above, the Floyd-Warshall algorithm operates using three for loops to
find the shortest distance between all pairs of vertices within a graph. Therefore, the time
complexity of the Floyd-Warshall algorithm is O(n3), where ‘n’ is the number of vertices in
the graph. The space complexity of the algorithm is O(n2).