Minimum Spanning Trees
Minimum Spanning Tree (MST)
To find the tree of a given connected, undirected and weighted graph
Boruvka’s Algorithm
-
Note that each round will take O( V + E ). - The number of rounds is at most \(\log_2(V)\), because each round reduces the number of components by 2. Why? Since contracting an edge removes exactly one vertex, if k edges are selected then k vertices are removed. There must be at least n/2 edges and thus n/2 vertices will be removed and Boruvka’s algorithm will take at most \(\log_2 n\) rounds of contracting.
Running Time: Boruvka’s running time is $$O(( | E | + | V | )\log | V | ) = O( | E | \log | V | + | V | \log | V | )\(. We just keep on saying\)O( | E | \log | V | )\(since\) | E | \(can be at worse\)V^2\(so the\)V\logV$$ is ignored because we care about the bigger term for big-O. |
Modification of the implementation here
from typing import List
# -1 represents an uninitialized value
UNINIT_FLAG = -1
class Graph:
""" Class to represent a graph. Includes Union-Find utilities
Edges are represented by 0-indexed integers.
"""
def __init__(self, num_vertices: int) -> None:
self.V = num_vertices # number of vertices
self.edges = [] # default dictionary to store graph
def addEdge(self, u: int, v: int, w: float) -> None:
""" add an edge to graph """
self.edges.append([u,v,w])
def find(self, parent: List[int], i: int) -> int:
"""
A utility function to find set of an element i
(uses path compression technique)
Args:
parent: list with parent for each node stored at that node's index
i: node to query its parent for
"""
if parent[i] == i:
return i
return self.find(parent, parent[i])
def union(self, parent: List[int], rank: List[int], x: int, y: int) -> None:
"""
A function that does union of two sets of x and y
(uses union by rank). Updates ranks and parents in place by ref.
Args:
parent: list with parent for each node stored at that node's index
rank: list of ranks per set
x: leader of set 1
y: leader of set 2
"""
xroot = self.find(parent, x)
yroot = self.find(parent, y)
if xroot == yroot:
return
# Attach smaller rank tree under root of high rank tree
# (Union by Rank)
if rank[xroot] > rank[yroot]:
parent[yroot] = xroot
else:
# means that rank[xroot] <= rank[yroot]
parent[xroot] = yroot
if rank[xroot] == rank[yroot]:
#If ranks are same, then make one as root and increment
# its rank by one
rank[yroot] += 1
def boruvkaMST(self):
"""
The main function to construct MST using Kruskal's algorithm
Initialize a forest T of n one-vertex trees.
While T has more than one connected component:
pick the lightest edge out of each such components
add it to the tree
Union the components connected by those edges
"""
# Initially there are V different trees.
# Finally there will be one tree that will be MST
numTrees = self.V
MSTweight = 0
# Create V subsets with single elements
# Each node is its own parent at the beginning
parent = list(range(self.V))
rank = [1] * self.V
# An array to store index of the cheapest edge of
# subset. It store [u,v,w] for each component
# Mark all initially as un-initialized.
cheapest = [UNINIT_FLAG] * self.V
# While T has more than one component
# Keep combining components (or sets) until all
# components are not combined into single MST
while numTrees > 1:
print("Starting new round of Boruvka...")
# Traverse through all edges and update
# cheapest of every component
for i in range(len(self.edges)):
# Find components (or sets) of two corners
# of current edge
u,v,w = self.edges[i]
# Find the index for each set's leader
set1 = self.find(parent, u)
set2 = self.find(parent, v)
# If two corners of current edge belong to
# same set, ignore current edge. Else check if
# current edge is closer to previous
# cheapest edges of set1 and set2
if set1 != set2:
if cheapest[set1] == UNINIT_FLAG or cheapest[set1][2] > w:
cheapest[set1] = [u,v,w]
if cheapest[set2] == UNINIT_FLAG or cheapest[set2][2] > w:
cheapest[set2] = [u,v,w]
pdb.set_trace()
# Consider the above picked cheapest edges and add them
# to MST
# for each component whose cheapest edge is initialized
for node in range(self.V):
#Check if cheapest for current set exists
# Add its cheapest edge to T
if cheapest[node] != -1:
u,v,w = cheapest[node]
set1 = self.find(parent, u)
set2 = self.find(parent, v)
if set1 != set2:
MSTweight += w
self.union(parent, rank, set1, set2)
print (f"\tEdge {u}-{v} with weight {w} included in MST")
numTrees = numTrees - 1
#reset array w/ cheapest edge per node before next round
cheapest = [-1] * self.V
print ("Weight of MST is %d" % MSTweight)
def test_boruvka1():
""" """
g = Graph(5)
g.addEdge(0,1,1)
g.addEdge(0,2,3)
g.addEdge(1,2,5)
g.addEdge(1,4,4)
g.addEdge(2,4,7)
g.addEdge(3,4,11)
g.addEdge(2,3,2)
pdb.set_trace()
g.boruvkaMST()
Yao’s Algorithm
In 1975, Andy Yao [1] introduced an \(O(|E|\log \log |V|)\) algorithm for the MST problem.
First partition the set of edges incident with each node \(v\) into \(k\) levels.
This can be done in O(E \log k) time by repeatedly applying the linear median-finding algorithm, FastSelect (which finds the k’th smallest element in an unsorted array).
$$O( | E | \log k + \frac{ | E | }{k} \log | V | )\(which is\)O( | E | \log \log | V | )\(if we choose\)k\(to be\)\log | V | $$. |
Yao Implementation
Consider 3 sets: \(T\), \(VS\), and \(ES\).
- \(T\) is used to collect edges of the final spanning tree.
- \(VS\) contains the vertex sets corresponding to the connected compoetnts of the spanning tree found so far.
- \(ES\) contains, for each vertex set \(W\) in \(VS\), an edge set \(E(W)\).
Initialize \(VS = \{ \{ v \} \mid v \in V \}\). The algorithm uses an integer parameter \(k\). Initialize \(ES = \{ \{ all the edges incident upon v\} for v \in V \}\).