diff --git a/graph/kruskal.go b/graph/kruskal.go index c6f094020..0543e2829 100644 --- a/graph/kruskal.go +++ b/graph/kruskal.go @@ -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 @@ -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 } diff --git a/graph/kruskal_test.go b/graph/kruskal_test.go index 86f062d53..09ed6949d 100644 --- a/graph/kruskal_test.go +++ b/graph/kruskal_test.go @@ -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) } }) } diff --git a/graph/unionfind.go b/graph/unionfind.go new file mode 100644 index 000000000..7a922f3cc --- /dev/null +++ b/graph/unionfind.go @@ -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 +} diff --git a/graph/unionfind_test.go b/graph/unionfind_test.go new file mode 100644 index 000000000..b95547649 --- /dev/null +++ b/graph/unionfind_test.go @@ -0,0 +1,32 @@ +package graph + +import ( + "testing" +) + +func TestUnionFind(t *testing.T) { + u := NewUnionFind(10) // Creating a Union-Find data structure with 10 elements + + //union operations + u = u.Union(0, 1) + u = u.Union(2, 3) + u = u.Union(4, 5) + u = u.Union(6, 7) + + // Testing the parent of specific elements + t.Run("Test Find", func(t *testing.T) { + if u.Find(0) != u.Find(1) || u.Find(2) != u.Find(3) || u.Find(4) != u.Find(5) || u.Find(6) != u.Find(7) { + t.Error("Union operation not functioning correctly") + } + }) + + u = u.Union(1, 5) // Additional union operation + u = u.Union(3, 7) // Additional union operation + + // Testing the parent of specific elements after more union operations + t.Run("Test Find after Union", func(t *testing.T) { + if u.Find(0) != u.Find(5) || u.Find(2) != u.Find(7) { + t.Error("Union operation not functioning correctly") + } + }) +}