Skip to content

Commit

Permalink
Feat: Add Union Find Algorithm, Test: Add test for Union Find Algorit…
Browse files Browse the repository at this point in the history
…hm (#687)

* feat:Add Union Find(Dynamic Connectivity)Algorithm

* test: Add test for Union Find Algorithm

* docs: made changes to comment structure

* fix: removed an error in the code

* fix: removed errors in unionfind_test.go

* fix: updata kruskal's algorithm

* fix: update kruskal_test.go

* fix: updated comments kruskal.go

* fix: removed code redundancy

* fix: removed main function

* fix: changes to code

* fix: updated the code

* fix: updated code

* fix: removed redundant spaces between comments

* fix: formatted code with gofmt

* fix: updated code

* fix: formatted the files again
  • Loading branch information
MugdhaBehere authored Oct 24, 2023
1 parent e255e17 commit f2de286
Show file tree
Hide file tree
Showing 4 changed files with 149 additions and 191 deletions.
102 changes: 23 additions & 79 deletions graph/kruskal.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
// KRUSKAL'S ALGORITHM
// https://cp-algorithms.com/data_structures/disjoint_set_union.html
// https://cp-algorithms.com/graph/mst_kruskal_with_dsu.html
// Reference: Kruskal's Algorithm: https://www.scaler.com/topics/data-structures/kruskal-algorithm/
// Reference: Union Find Algorithm: https://www.scaler.com/topics/data-structures/disjoint-set/
// Author: Author: Mugdha Behere[https://github.com/MugdhaBehere]
// Worst Case Time Complexity: O(E log E), where E is the number of edges.
// Worst Case Space Complexity: O(V + E), where V is the number of vertices and E is the number of edges.
// see kruskal.go, kruskal_test.go

package graph

Expand All @@ -10,104 +14,44 @@ import (

type Vertex int

// Edge describes the edge of a weighted graph
type Edge struct {
Start Vertex
End Vertex
Weight int
}

// DisjointSetUnionElement describes what an element of DSU looks like
type DisjointSetUnionElement struct {
Parent Vertex
Rank int
}

// DisjointSetUnion is a data structure that treats its elements as separate sets
// and provides fast operations for set creation, merging sets, and finding the parent
// of the given element of a set.
type DisjointSetUnion []DisjointSetUnionElement

// NewDSU will return an initialised DSU using the value of n
// which will be treated as the number of elements out of which
// the DSU is being made
func NewDSU(n int) *DisjointSetUnion {

dsu := DisjointSetUnion(make([]DisjointSetUnionElement, n))
return &dsu
}

// MakeSet will create a set in the DSU for the given node
func (dsu DisjointSetUnion) MakeSet(node Vertex) {

dsu[node].Parent = node
dsu[node].Rank = 0
}

// FindSetRepresentative will return the parent element of the set the given node
// belongs to. Since every single element in the path from node to parent
// has the same parent, we store the parent value for each element in the
// path. This reduces consequent function calls and helps in going from O(n)
// to O(log n). This is known as path compression technique.
func (dsu DisjointSetUnion) FindSetRepresentative(node Vertex) Vertex {

if node == dsu[node].Parent {
return node
}

dsu[node].Parent = dsu.FindSetRepresentative(dsu[node].Parent)
return dsu[node].Parent
}

// unionSets will merge two given sets. The naive implementation of this
// always combines the secondNode's tree with the firstNode's tree. This can lead
// to creation of trees of length O(n) so we optimize by attaching the node with
// smaller rank to the node with bigger rank. Rank represents the upper bound depth of the tree.
func (dsu DisjointSetUnion) UnionSets(firstNode Vertex, secondNode Vertex) {

firstNode = dsu.FindSetRepresentative(firstNode)
secondNode = dsu.FindSetRepresentative(secondNode)

if firstNode != secondNode {

if dsu[firstNode].Rank < dsu[secondNode].Rank {
firstNode, secondNode = secondNode, firstNode
}
dsu[secondNode].Parent = firstNode

if dsu[firstNode].Rank == dsu[secondNode].Rank {
dsu[firstNode].Rank++
}
}
}

// KruskalMST will return a minimum spanning tree along with its total cost
// to using Kruskal's algorithm. Time complexity is O(m * log (n)) where m is
// the number of edges in the graph and n is number of nodes in it.
func KruskalMST(n int, edges []Edge) ([]Edge, int) {
// Initialize variables to store the minimum spanning tree and its total cost
var mst []Edge
var cost int

var mst []Edge // The resultant minimum spanning tree
var cost int = 0

dsu := NewDSU(n)
// Create a new UnionFind data structure with 'n' nodes
u := NewUnionFind(n)

// Initialize each node in the UnionFind data structure
for i := 0; i < n; i++ {
dsu.MakeSet(Vertex(i))
u.parent[i] = i
u.size[i] = 1
}

// Sort the edges in non-decreasing order based on their weights
sort.SliceStable(edges, func(i, j int) bool {
return edges[i].Weight < edges[j].Weight
})

// Iterate through the sorted edges
for _, edge := range edges {

if dsu.FindSetRepresentative(edge.Start) != dsu.FindSetRepresentative(edge.End) {

// Check if adding the current edge forms a cycle or not
if u.Find(int(edge.Start)) != u.Find(int(edge.End)) {
// Add the edge to the minimum spanning tree
mst = append(mst, edge)
// Add the weight of the edge to the total cost
cost += edge.Weight
dsu.UnionSets(edge.Start, edge.End)
// Merge the sets containing the start and end vertices of the current edge
u = u.Union(int(edge.Start), int(edge.End))
}
}

// Return the minimum spanning tree and its total cost
return mst, cost
}
147 changes: 35 additions & 112 deletions graph/kruskal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,157 +5,80 @@ import (
"testing"
)

func Test_KruskalMST(t *testing.T) {

func TestKruskalMST(t *testing.T) {
// Define test cases with inputs, expected outputs, and sample graphs
var testCases = []struct {
n int
graph []Edge
cost int
}{
// Test Case 1
{
n: 5,
graph: []Edge{
{
Start: 0,
End: 1,
Weight: 4,
},
{
Start: 0,
End: 2,
Weight: 13,
},
{
Start: 0,
End: 3,
Weight: 7,
},
{
Start: 0,
End: 4,
Weight: 7,
},
{
Start: 1,
End: 2,
Weight: 9,
},
{
Start: 1,
End: 3,
Weight: 3,
},
{
Start: 1,
End: 4,
Weight: 7,
},
{
Start: 2,
End: 3,
Weight: 10,
},
{
Start: 2,
End: 4,
Weight: 14,
},
{
Start: 3,
End: 4,
Weight: 4,
},
{Start: 0, End: 1, Weight: 4},
{Start: 0, End: 2, Weight: 13},
{Start: 0, End: 3, Weight: 7},
{Start: 0, End: 4, Weight: 7},
{Start: 1, End: 2, Weight: 9},
{Start: 1, End: 3, Weight: 3},
{Start: 1, End: 4, Weight: 7},
{Start: 2, End: 3, Weight: 10},
{Start: 2, End: 4, Weight: 14},
{Start: 3, End: 4, Weight: 4},
},
cost: 20,
},
// Test Case 2
{
n: 3,
graph: []Edge{
{
Start: 0,
End: 1,
Weight: 12,
},
{
Start: 0,
End: 2,
Weight: 18,
},
{
Start: 1,
End: 2,
Weight: 6,
},
{Start: 0, End: 1, Weight: 12},
{Start: 0, End: 2, Weight: 18},
{Start: 1, End: 2, Weight: 6},
},
cost: 18,
},
// Test Case 3
{
n: 4,
graph: []Edge{
{
Start: 0,
End: 1,
Weight: 2,
},
{
Start: 0,
End: 2,
Weight: 1,
},
{
Start: 0,
End: 3,
Weight: 2,
},
{
Start: 1,
End: 2,
Weight: 1,
},
{
Start: 1,
End: 3,
Weight: 2,
},
{
Start: 2,
End: 3,
Weight: 3,
},
{Start: 0, End: 1, Weight: 2},
{Start: 0, End: 2, Weight: 1},
{Start: 0, End: 3, Weight: 2},
{Start: 1, End: 2, Weight: 1},
{Start: 1, End: 3, Weight: 2},
{Start: 2, End: 3, Weight: 3},
},
cost: 4,
},
// Test Case 4
{
n: 2,
graph: []Edge{
{
Start: 0,
End: 1,
Weight: 4000000,
},
{Start: 0, End: 1, Weight: 4000000},
},
cost: 4000000,
},
// Test Case 5
{
n: 1,
graph: []Edge{
{
Start: 0,
End: 0,
Weight: 0,
},
{Start: 0, End: 0, Weight: 0},
},
cost: 0,
},
}

for i := range testCases {

// Iterate through the test cases and run the tests
for i, testCase := range testCases {
t.Run(fmt.Sprintf("Test Case %d", i), func(t *testing.T) {
// Execute KruskalMST for the current test case
_, computed := KruskalMST(testCase.n, testCase.graph)

_, computed := KruskalMST(testCases[i].n, testCases[i].graph)
if computed != testCases[i].cost {
t.Errorf("Test Case %d, Expected: %d, Computed: %d", i, testCases[i].cost, computed)
// Compare the computed result with the expected result
if computed != testCase.cost {
t.Errorf("Test Case %d, Expected: %d, Computed: %d", i, testCase.cost, computed)
}
})
}
Expand Down
59 changes: 59 additions & 0 deletions graph/unionfind.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// Union Find Algorithm or Dynamic Connectivity algorithm, often implemented with the help
//of the union find data structure,
// is used to efficiently maintain connected components in a graph that undergoes dynamic changes,
// such as edges being added or removed over time
// Worst Case Time Complexity: The time complexity of find operation is nearly constant or
//O(α(n)), where where α(n) is the inverse Ackermann function
// practically, this is a very slowly growing function making the time complexity for find
//operation nearly constant.
// The time complexity of the union operation is also nearly constant or O(α(n))
// Worst Case Space Complexity: O(n), where n is the number of nodes or element in the structure
// Reference: https://www.scaler.com/topics/data-structures/disjoint-set/
// Author: Mugdha Behere[https://github.com/MugdhaBehere]
// see: unionfind.go, unionfind_test.go

package graph

// Defining the union-find data structure
type UnionFind struct {
parent []int
size []int
}

// Initialise a new union find data structure with s nodes
func NewUnionFind(s int) UnionFind {
parent := make([]int, s)
size := make([]int, s)
for k := 0; k < s; k++ {
parent[k] = k
size[k] = 1
}
return UnionFind{parent, size}
}

// to find the root of the set to which the given element belongs, the Find function serves the purpose
func (u UnionFind) Find(q int) int {
for q != u.parent[q] {
q = u.parent[q]
}
return q
}

// to merge two sets to which the given elements belong, the Union function serves the purpose
func (u UnionFind) Union(a, b int) UnionFind {
rootP := u.Find(a)
rootQ := u.Find(b)

if rootP == rootQ {
return u
}

if u.size[rootP] < u.size[rootQ] {
u.parent[rootP] = rootQ
u.size[rootQ] += u.size[rootP]
} else {
u.parent[rootQ] = rootP
u.size[rootP] += u.size[rootQ]
}
return u
}
Loading

0 comments on commit f2de286

Please sign in to comment.