Graphs - 2
Graphs - 2
If we are given an undirected and connected graph, a spanning tree means a tree
that contains all the vertices of the same. For a given graph, we can have multiple
spanning trees.
If there are n vertices and e edges in the graph, then any spanning tree
corresponding to that graph contains n vertices and n-1 edges.
1
Properties of spanning trees:
● A connected and undirected graph can have more than one spanning tree.
● The spanning tree is free of loops, i.e., it is acyclic.
● Removing any one of the edges will make the graph disconnected.
● Adding an extra edge to the spanning tree will create a loop in the graph.
In a weighted graph, the MST is a spanning tree with minimum weight than all other
spanning trees of that graph. Refer to the image below for better understanding.
2
Kruskal’s Algorithm:
This algorithm is used to find MST for the given graph. It builds the spanning tree by
adding edges one at a time. We start by picking the edge with minimum weight,
adding that edge into the MST, and increasing the count of edges in the spanning
tree by one. Now, we will be picking the minimum weighted edge by excluding the
already chosen ones and correspondingly increasing the count. While choosing the
edge, we will also make sure that the graph remains acyclic after including the
same. This process will continue until the count of edges in the MST reaches n-1.
Ultimately, the graph obtained will be MST.
3
This is the final MST obtained using Kruskal’s algorithm. It can be checked manually
that the final graph is the MST for the given graph.
Cycle Detection
While inserting a new edge in the MST, we have to check if introducing that edge
makes the MST cyclic or not. If not, then we can include that edge, otherwise not.
Now, let’s figure out a way to detect the cycle in a graph. The following are the
possible cases:
● By including an edge between the nodes A and B, if both nodes A and B are
not present in the graph, then it is safe to include that edge as including it,
will not bring a cycle to the graph.
● Out of two vertices, if any one of them has not been visited (or not present in
the MST), then that vertex can also be included in the MST.
● If both the vertices are already present in the graph, they can introduce a
cycle in the MST. It means we can’t use this method to detect the presence of
the cycle.
4
Let’s think of a better approach. We have already solved the hasPath question in
the previous module, which returns true if there is a path present between two
vertices v1 and v2, otherwise false.
Now, before adding an edge to the MST, we will check if a path between two
vertices of that edge already exists in the MST or not. If not, then it is safe to add
that edge to the MST.
Union-Find Algorithm:
Before adding any edge to the graph, we will check if the two vertices of the edge lie
in the same component of MST or not. If not, then it is safe to add that edge to the
MST.
5
Look at the following example, for better understanding:
6
Note: While finding the parent of the vertex, we will be finding the topmost parent
(Oldest Ancestor). For example: suppose, the vertex 0 and the vertex 1 were
7
connected, where the parent of 0 is 1, and the parent of 1 is 1. Now, while
determining the parent of the vertex 0, we will visit the parent array and check the
vertex at index 0. In our case, it is 1. Now we will go to index 1 and check the parent
of index 1, which is also 1. Hence, we can’t go any further as the index is the parent
of itself. This way, we will be determining the parent of any vertex.
The time complexity of the union-find algorithm becomes O(V) for each vertex in
the worst case due to skewed-tree formation, where V is the number of vertices in
the graph. Here, we can see that time complexity for cycle detection has
significantly improved compared to the previous approach.
8
Kruskal’s Algorithm: Implementation
Till now, we have studied the logic behind Kruskal’s algorithm for finding MST. Now,
let’s discuss how to implement it in code.
Consider the code below and follow the comments for a better understanding.
def kruskal(edges,n,E):
edges = sorted(edges,key = lambda edge:edge.wt) #Inbuilt sort
output = [] # Array to store final edges of MST
parent [i for i in range(n)]
count = 0
i = 0
while count < (n-1):
currentEdge = edges[i]
# Figuring out the parent of each edge's vertices
srcParent = getParent(currentEdge.src,parent)
destParent = getParent(currentEdge.dest,parent)
# Parents are not equal-> then add the edge to output
if srcParent != destParent:
output.append(currentEdge)
count+=1 # Increased the count
parent[srcParent] = destParent #Update Parent array
i+=1
return output
9
n= li[0]
E= li[1]
edges = []
for i in range(E) :
curr_input = [int(ele) for ele in input().split()]
src = curr_input[0]
dest = curr_input[1]
wt = curr_input[2]
edge = Edge(src,dest,wt)
edges.append(edge)
output = kruskal(edges,n,E)
for ele in output:# Finally, printing the MST obtained.
if(ele.src < ele.dest):
print(str(ele.src) +" "+ str(ele.dest)+" "+ str(ele.wt)
else:
print(str(ele.dest) + " "+ str(ele.src)+ " "+ str(ele.wt)
10
algorithms is that we will be avoiding the formation of skewed-tree structure, which
reduces the time complexity for each vertex to O(log(E)).
Prim’s Algorithm
This algorithm is used to find MST for a given undirected-weighted graph (which
can also be achieved using Kruskal’s Algorithm).
In this algorithm, the MST is built by adding one edge at a time. In the beginning,
the spanning tree consists of only one vertex, which is chosen arbitrarily from the
set of all vertices of the graph. Then the minimum weighted edge, outgoing from
this vertex, is selected and simultaneously inserted into the MST. Now, the tree
contains two edges. Further, we will be selecting the edge with the minimum weight
such that one end is already present there in the MST and the other one from the
unselected set of vertices. This process is repeated until we have inserted a total of
(n-1) edges in the MST.
11
Implementation:
● We are considering the starting vertex to be 0 with a parent equal to -1, and
weight is equal to 0 (The weight of the edge from vertex 0 to vertex 0 itself).
● The parent of all other vertices is assumed to be NIL, and the weight will be
equal to infinity, which means that the vertex has not been visited yet.
● We will mark the vertex 0 as visited and the rest as unvisited. If we add any
vertex to the MST, then that vertex will be shifted from the unvisited section
to the visited section.
● Now, we will update the weights of direct neighbors of vertex 0 with the edge
weights as these are smaller than infinity. We will also update the parent of
these vertices and assign them 0 as we reached these vertices from vertex 0.
● This way, we will keep updating the weights and parents, according to the
edge, which has the minimum weight connected to the respective vertex.
Let’s look at the code now:
12
class Graph:
def addEdge(self,v1,v2,wt):
self.adjMatrix[v1][v2] = wt
self.adjMatrix[v2][v1] = wt
def __getMinVertex(self,visited,weight):
minVertex = -1 # Initialized to -1 means there is no vertex
for i in range(self.nVertices):
# Conditions :
#the vertex must be unvisited and either minVertex value is -1
#or if minVertex has some vertex to it, then weight of
#currentVertex should be less than the weight of the minVertex.
a = (minVertex == -1 or (weight[minVertex] > weight[i])))
if(visited[i] is False and a:
minVertex = i
return minVertex
def prims(self):
# Initially, the visited array is assigned to false
#and weights array is set to infinity.
visited = [False for i in range(self.nVertices)]
parent = [-1 for i in range(self.nVertices)]
weight = [sys.maxsize for i in range(self.nVertices)]
13
a = str(i) +" "+ str(parent[i])+" "+ str(weight[i])
print(a)
else:
a = str(parent[i]) +" "+ str(i) +" "+ str(weight[i])
print(a)
def removeEdge(self,v1,v2):
if not self.containsEdge(v1,v2):
return
self.adjMatrix[v1][v2] = 0
self.adjMatrix[v2][v2] = 0
def containsEdge(self,v1,v2):
return True if self.adjMatrix[v1][v2] > 0 else False
● The time complexity for finding the minimum weighted vertex is O(n) for
each iteration. So for (n-1) edges, it becomes O(n2).
● Similarly, for exploring the neighbor vertices, the time taken is O(n2).
It means the time complexity of Prim’s algorithm is O(n2). We can improve this in
the following ways:
● For exploring neighbors, we are required to visit every vertex because of the
adjacency matrix. We can improve this by using an adjacency list instead of a
matrix.
14
● Now, the second important thing is the time taken to find the minimum
weight vertex, which is also taking a time of O(n2). Here, out of the available
list, we are trying to figure out the one with minimum weight. This can be
optimally achieved using a priority queue where the priority will be taken as
weights of the vertices. This will take O(log(n)) time complexity to remove a
vertex from the priority queue.
Dijkstra’s Algorithm
This algorithm is used to find the shortest distance between any two vertices in a
weighted non-cyclic graph.
1. We want to calculate the shortest path between the source vertex C and all
other vertices in the following graph.
15
2. While executing the algorithm, we will mark every node with its minimum
distance to the selected node, which is C in our case. Obviously, for node C
itself, this distance will be 0, and for the rest of the nodes, we will assume
that the distance is infinity, which also denotes that these vertices have not
been visited till now.
3. Now, we will check for the neighbors of the current node, which are A, B, and
D. Now, we will add the minimum cost of the current node to the weight of
16
the edge connecting the current node and the particular neighbor node. For
example, for node B, it’s weight will become minimum(infinity, 0+7) = 7. This
same process is repeated for other neighbor nodes.
4. Now, as we have updated the distance of all the neighbor nodes of the
current node, we will mark the current node as visited.
5. After this, we will be selecting the minimum weighted node among the
remaining vertices. In this case, it is node A. Take this node as the current
node.
17
6. Now, we will repeat the above steps for the rest of the vertices. The pictorial
representation of the same is shown below:
18
7. Finally, we will get the graph as follows:
19
The distances finally marked at each node are minimum from node C.
Implementation:
20
Let’s look at the code below for a better explanation:(Code is nearly same as that of
Prim’s algorithm, just a change while updating the distance)
class Graph:
def addEdge(self,v1,v2,wt):
self.adjMatrix[v1][v2] = wt
self.adjMatrix[v2][v1] = wt
def __getMinVertexD(self,visited,weight):
minVertex = -1 # Initialized to -1 means there is no vertex
for i in range(self.nVertices):
a = (minVertex == -1 or (weight[minVertex] > weight[i])))
if(visited[i] is False and a:
minVertex = i
return minVertex
def djikstra(self):
# Initially, the visited array is assigned to false
#and weights array is set to infinity.
visited = [False for i in range(self.nVertices)]
dist = [sys.maxsize for i in range(self.nVertices)]
dist[0]=0
for i in range(self.nVertices - 1):
# Find min vertex
minVertex = self.__getMinVertexD(visited,weight)
visited[minVertex] = True
# Explore unvisited neighbors
for j in range(self.nVertices):
if(self.adjMatrix[minVertex][j]>0 and visited[j] is False):
if(dist[j] > dist[minVertex]+ self.adjMatrix[minVertex][j]):
dist[j] = self.adjMatrix[minVertex][j] +dist[minVertex]
parent[j] = minVertex
21
li = [int(ele) for ele in input().split()]
n = li[0]
E= li[1]
g= Graph(n) #Inbuilt Graph class
for in range(E):
curr_edge = [int(ele) for ele in input().split()]
g.addEdge(curr_edge[0], curr_edge[1], curr_edge[2])
g.djikstra()
22