Skip to content

Commit 52566d8

Browse files
Added Kruskal Algo
1 parent c1fc189 commit 52566d8

File tree

1 file changed

+375
-0
lines changed

1 file changed

+375
-0
lines changed

go/graph/Kruskal.go

Lines changed: 375 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,375 @@
1+
// KruskalMST.go
2+
//
3+
// Kruskal's Algorithm - Minimum Spanning Tree (numeric node IDs)
4+
//
5+
// Description:
6+
// Kruskal's algorithm finds a minimum spanning tree (MST) of a connected,
7+
// undirected, weighted graph by sorting edges and adding them if they
8+
// connect different components (using union-find).
9+
//
10+
// Purpose / Use cases:
11+
// - Compute MST and its total weight.
12+
// - Useful for network design, clustering approximations, etc.
13+
//
14+
// Approach / Methodology:
15+
// - Represent undirected weighted graph with adjacency list and an edges slice.
16+
// - Sort all unique undirected edges by (weight, u, v).
17+
// - Use Union-Find to pick edges that join different components.
18+
// - If after processing all edges the MST contains V-1 edges, return it;
19+
// otherwise the graph was disconnected and no spanning tree exists.
20+
//
21+
// Complexity Analysis:
22+
// - Time: O(E log E) due to sorting + near-constant union-find ops.
23+
// - Space: O(E + V)
24+
//
25+
// File contents:
26+
// - Graph type and methods (AddEdge, AddNode).
27+
// - Union-Find implementation.
28+
// - Kruskal() that returns []MEdge (MST) and total weight, or nil if not found.
29+
// - Tests print MST after each test and indicate pass/fail.
30+
31+
package main
32+
33+
import (
34+
"fmt"
35+
"os"
36+
"sort"
37+
"strconv"
38+
)
39+
40+
// MEdge represents an undirected weighted edge (u -- weight -- v).
41+
type MEdge struct {
42+
U, V int
43+
Weight int
44+
}
45+
46+
// Graph represents an undirected weighted graph with integer node IDs.
47+
type Graph struct {
48+
adj map[int][]MEdge // adjacency list: node -> list of neighbor edges
49+
edges []MEdge // unique undirected edges (stored with U < V)
50+
}
51+
52+
// NewGraph creates and returns an empty Graph.
53+
func NewGraph() *Graph {
54+
return &Graph{
55+
adj: make(map[int][]MEdge),
56+
edges: []MEdge{},
57+
}
58+
}
59+
60+
// AddNode ensures a node entry exists in the adjacency map.
61+
func (g *Graph) AddNode(id int) {
62+
if _, ok := g.adj[id]; !ok {
63+
g.adj[id] = []MEdge{}
64+
}
65+
}
66+
67+
// AddEdge adds an undirected weighted edge between a and b.
68+
// If nodes don't exist yet they are created automatically.
69+
// Edges slice stores each undirected edge exactly once with U < V to avoid duplicates.
70+
func (g *Graph) AddEdge(a, b, w int) {
71+
g.AddNode(a)
72+
g.AddNode(b)
73+
// Add adjacency entries for traversal convenience (both directions)
74+
g.adj[a] = append(g.adj[a], MEdge{U: a, V: b, Weight: w})
75+
g.adj[b] = append(g.adj[b], MEdge{U: b, V: a, Weight: w})
76+
77+
// Store unique undirected edge normalized so U < V
78+
u, v := a, b
79+
if u > v {
80+
u, v = v, u
81+
}
82+
g.edges = append(g.edges, MEdge{U: u, V: v, Weight: w})
83+
}
84+
85+
// -------- Union-Find (Disjoint Set) --------
86+
87+
type UnionFind struct {
88+
parent map[int]int
89+
rank map[int]int
90+
}
91+
92+
func NewUnionFind(nodes []int) *UnionFind {
93+
uf := &UnionFind{
94+
parent: make(map[int]int, len(nodes)),
95+
rank: make(map[int]int, len(nodes)),
96+
}
97+
for _, n := range nodes {
98+
uf.parent[n] = n
99+
uf.rank[n] = 0
100+
}
101+
return uf
102+
}
103+
104+
func (uf *UnionFind) Find(x int) int {
105+
// path compression
106+
if uf.parent[x] != x {
107+
uf.parent[x] = uf.Find(uf.parent[x])
108+
}
109+
return uf.parent[x]
110+
}
111+
112+
func (uf *UnionFind) Union(x, y int) bool {
113+
rx := uf.Find(x)
114+
ry := uf.Find(y)
115+
if rx == ry {
116+
return false
117+
}
118+
// union by rank
119+
if uf.rank[rx] < uf.rank[ry] {
120+
uf.parent[rx] = ry
121+
} else if uf.rank[ry] < uf.rank[rx] {
122+
uf.parent[ry] = rx
123+
} else {
124+
uf.parent[ry] = rx
125+
uf.rank[rx]++
126+
}
127+
return true
128+
}
129+
130+
// -------- Kruskal's algorithm --------
131+
132+
// Kruskal computes an MST of the whole graph.
133+
// Returns (mstEdges, totalWeight).
134+
// If the graph is empty -> returns nil,0.
135+
// If the graph has exactly 1 node -> returns empty MST slice and weight 0.
136+
// If the graph is disconnected -> returns nil,0.
137+
func (g *Graph) Kruskal() ([]MEdge, int) {
138+
if len(g.adj) == 0 {
139+
return nil, 0
140+
}
141+
142+
// Prepare nodes list for union-find
143+
nodes := make([]int, 0, len(g.adj))
144+
for n := range g.adj {
145+
nodes = append(nodes, n)
146+
}
147+
148+
// Sort edges by (weight, u, v) deterministically
149+
edges := make([]MEdge, len(g.edges))
150+
copy(edges, g.edges)
151+
sort.Slice(edges, func(i, j int) bool {
152+
if edges[i].Weight != edges[j].Weight {
153+
return edges[i].Weight < edges[j].Weight
154+
}
155+
if edges[i].U != edges[j].U {
156+
return edges[i].U < edges[j].U
157+
}
158+
return edges[i].V < edges[j].V
159+
})
160+
161+
uf := NewUnionFind(nodes)
162+
mst := make([]MEdge, 0, len(g.adj)-1)
163+
total := 0
164+
165+
for _, e := range edges {
166+
if uf.Find(e.U) != uf.Find(e.V) {
167+
uf.Union(e.U, e.V)
168+
mst = append(mst, e)
169+
total += e.Weight
170+
}
171+
// quick exit: if mst has V-1 edges we can stop early
172+
if len(mst) == len(g.adj)-1 {
173+
break
174+
}
175+
}
176+
177+
// Check if we built a spanning tree
178+
if len(mst) != len(g.adj)-1 {
179+
// Special-case: single node graph -> mst len == 0 and len(adj)-1 == 0 -> OK
180+
if len(g.adj) == 1 && len(mst) == 0 {
181+
return mst, total
182+
}
183+
return nil, 0
184+
}
185+
return mst, total
186+
}
187+
188+
// -------- helpers for tests and comparison --------
189+
190+
// normalizeEdgeKey returns a canonical string key for an undirected edge+weight
191+
// (min,max,weight) so we can compare MST edge sets ignoring order.
192+
func normalizeEdgeKey(e MEdge) string {
193+
u, v := e.U, e.V
194+
if u > v {
195+
u, v = v, u
196+
}
197+
return fmt.Sprintf("%d-%d-%d", u, v, e.Weight)
198+
}
199+
200+
// edgesEqualSet checks whether two edge slices represent the same undirected set
201+
// (order-insensitive). Nil == Nil; nil != empty slice.
202+
func edgesEqualSet(a []MEdge, b []MEdge) bool {
203+
if a == nil && b == nil {
204+
return true
205+
}
206+
if (a == nil) != (b == nil) {
207+
return false
208+
}
209+
if len(a) != len(b) {
210+
return false
211+
}
212+
m := make(map[string]int)
213+
for _, e := range a {
214+
m[normalizeEdgeKey(e)]++
215+
}
216+
for _, e := range b {
217+
k := normalizeEdgeKey(e)
218+
if m[k] == 0 {
219+
return false
220+
}
221+
m[k]--
222+
}
223+
for _, v := range m {
224+
if v != 0 {
225+
return false
226+
}
227+
}
228+
return true
229+
}
230+
231+
// sortEdgesForPrint returns a stable, human-friendly ordering for printing (u,v,w) by u,v,w.
232+
func sortEdgesForPrint(edges []MEdge) []MEdge {
233+
cp := make([]MEdge, len(edges))
234+
copy(cp, edges)
235+
sort.Slice(cp, func(i, j int) bool {
236+
if cp[i].U == cp[j].U {
237+
if cp[i].V == cp[j].V {
238+
return cp[i].Weight < cp[j].Weight
239+
}
240+
return cp[i].V < cp[j].V
241+
}
242+
return cp[i].U < cp[j].U
243+
})
244+
return cp
245+
}
246+
247+
// printMST prints MST edges and total weight in a readable way.
248+
func printMST(mst []MEdge, total int) {
249+
if mst == nil {
250+
fmt.Printf("MST: nil (graph empty or disconnected)\n")
251+
return
252+
}
253+
if len(mst) == 0 {
254+
fmt.Printf("MST: (no edges) total weight = %d\n", total)
255+
return
256+
}
257+
s := sortEdgesForPrint(mst)
258+
fmt.Printf("MST edges (u --w--> v):\n")
259+
for _, e := range s {
260+
fmt.Printf(" %d --%d--> %d\n", e.U, e.Weight, e.V)
261+
}
262+
fmt.Printf("Total weight = %d\n", total)
263+
}
264+
265+
// expect checks result against expected and prints pass/fail (and MST).
266+
func expect(got []MEdge, gotTotal int, expected []MEdge, expectedTotal int, testName string) {
267+
fmt.Printf("%s - Computed MST:\n", testName)
268+
printMST(got, gotTotal)
269+
fmt.Println("Expected MST:")
270+
printMST(expected, expectedTotal)
271+
272+
pass := edgesEqualSet(got, expected) && (got == nil && expected == nil || gotTotal == expectedTotal)
273+
if pass {
274+
fmt.Printf("[PASS] %s\n\n", testName)
275+
} else {
276+
fmt.Printf("[FAIL] %s\n\n", testName)
277+
}
278+
}
279+
280+
// runTests builds small weighted graphs and runs deterministic tests.
281+
func runTests() {
282+
fmt.Println("Kruskal's Algorithm Tests (numeric nodes)\n")
283+
284+
// Test Graph 1: same sample as used before
285+
// Nodes: 1..6
286+
// edges:
287+
// 1-2:3, 1-3:1, 2-3:7, 2-4:5, 3-4:2, 3-5:4, 4-5:6, 4-6:8, 5-6:9
288+
// Known MST (one valid MST): edges
289+
// (1-3,1), (3-4,2), (1-2,3), (3-5,4), (4-6,8) total = 18
290+
g1 := NewGraph()
291+
g1.AddEdge(1, 2, 3)
292+
g1.AddEdge(1, 3, 1)
293+
g1.AddEdge(2, 3, 7)
294+
g1.AddEdge(2, 4, 5)
295+
g1.AddEdge(3, 4, 2)
296+
g1.AddEdge(3, 5, 4)
297+
g1.AddEdge(4, 5, 6)
298+
g1.AddEdge(4, 6, 8)
299+
g1.AddEdge(5, 6, 9)
300+
301+
expected1 := []MEdge{
302+
{U: 1, V: 3, Weight: 1},
303+
{U: 3, V: 4, Weight: 2},
304+
{U: 1, V: 2, Weight: 3},
305+
{U: 3, V: 5, Weight: 4},
306+
{U: 4, V: 6, Weight: 8},
307+
}
308+
got1, tot1 := g1.Kruskal()
309+
expect(got1, tot1, expected1, 18, "Test 1: sample graph")
310+
311+
// Test 2: same graph, ensure result is independent of edge insertion order
312+
// Rebuild with different insertion sequence (but same edges)
313+
g1b := NewGraph()
314+
g1b.AddEdge(4, 6, 8)
315+
g1b.AddEdge(5, 6, 9)
316+
g1b.AddEdge(1, 3, 1)
317+
g1b.AddEdge(3, 4, 2)
318+
g1b.AddEdge(1, 2, 3)
319+
g1b.AddEdge(3, 5, 4)
320+
g1b.AddEdge(4, 5, 6)
321+
g1b.AddEdge(2, 4, 5)
322+
g1b.AddEdge(2, 3, 7)
323+
got2, tot2 := g1b.Kruskal()
324+
expect(got2, tot2, expected1, 18, "Test 2: sample graph (different insertion order)")
325+
326+
// Test 3: disconnected graph -> no spanning tree (nil expected)
327+
g2 := NewGraph()
328+
g2.AddEdge(1, 2, 1)
329+
g2.AddEdge(3, 4, 2)
330+
got3, tot3 := g2.Kruskal()
331+
expect(got3, tot3, nil, 0, "Test 3: disconnected graph (expect nil)")
332+
333+
// Test 4: single isolated node (node exists but no edges) -> MST is empty edges, total=0
334+
g3 := NewGraph()
335+
g3.AddNode(7)
336+
got4, tot4 := g3.Kruskal()
337+
expect(got4, tot4, []MEdge{}, 0, "Test 4: single isolated node => empty MST")
338+
339+
// Test 5: empty graph => nil
340+
empty := NewGraph()
341+
got5, tot5 := empty.Kruskal()
342+
expect(got5, tot5, nil, 0, "Test 5: empty graph => nil")
343+
344+
fmt.Println("Tests completed.")
345+
}
346+
347+
func main() {
348+
// CLI: if an integer arg provided, run Kruskal on the sample graph and print MST.
349+
if len(os.Args) > 1 {
350+
_, err := strconv.Atoi(os.Args[1])
351+
if err != nil {
352+
fmt.Printf("Invalid arg. Provide integer (ignored for Kruskal CLI example).\n")
353+
return
354+
}
355+
// build sample graph (same as Test 1)
356+
g := NewGraph()
357+
g.AddEdge(1, 2, 3)
358+
g.AddEdge(1, 3, 1)
359+
g.AddEdge(2, 3, 7)
360+
g.AddEdge(2, 4, 5)
361+
g.AddEdge(3, 4, 2)
362+
g.AddEdge(3, 5, 4)
363+
g.AddEdge(4, 5, 6)
364+
g.AddEdge(4, 6, 8)
365+
g.AddEdge(5, 6, 9)
366+
367+
mst, total := g.Kruskal()
368+
fmt.Printf("Kruskal's MST for sample graph:\n")
369+
printMST(mst, total)
370+
return
371+
}
372+
373+
// default: run tests
374+
runTests()
375+
}

0 commit comments

Comments
 (0)