Chapter 24 - Mastering Ruby's Pathfinders: Kruskal and Prim Build Bridges, Not Walls

Ruby's Role in Navigating the Complex World of Minimum Spanning Trees, where Math Meets Elegance

Chapter 24 - Mastering Ruby's Pathfinders: Kruskal and Prim Build Bridges, Not Walls

When diving into the cool world of graphs and their algorithms, Kruskal’s and Prim’s algorithms stand out as seasoned masters of finding Minimum Spanning Trees (MST). Imagine you have a sprawling network of cities and roads, and you’re tasked with connecting each city with the fewest possible miles of road. This is where these two algorithms shine, finding the most efficient path without any unnecessary detours. I’ve been working with Ruby to bring these algorithms to life, and it’s been a delightful journey, merging logic with the beauty of this language.

Kruskal’s algorithm is the easier of the two, I think, to wrap your head around. It’s like sorting pebbles before connecting them, starting with the smallest ones. The idea is to sort all the edges in non-decreasing order of their weight. Then, one by one, we pick the smallest edge. If including this edge doesn’t form a cycle with the spanning-tree formed so far, we include it. Otherwise, we discard it.

In Ruby, implementing Kruskal’s can be an exercise in playing with array sorting and maintaining a disjoint set to check for cycles. Here’s a little snippet to get your hands dirty:

class Kruskal
  def initialize(vertices)
    @vertices = vertices
    @edges = []
  end

  def add_edge(u, v, weight)
    @edges << [weight, u, v]
  end

  def find(parent, i)
    return i if parent[i] == i
    find(parent, parent[i])
  end

  def union(parent, rank, x, y)
    xroot = find(parent, x)
    yroot = find(parent, y)

    if rank[xroot] < rank[yroot]
      parent[xroot] = yroot
    elsif rank[xroot] > rank[yroot]
      parent[yroot] = xroot
    else
      parent[yroot] = xroot
      rank[xroot] += 1
    end
  end

  def kruskal_mst
    result = []
    i, e = 0, 0

    @edges.sort!
    parent = []
    rank = []
    @vertices.times do |node|
      parent[node] = node
      rank[node] = 0
    end

    while e < @vertices - 1
      (w, u, v) = @edges[i]
      i += 1
      x = find(parent, u)
      y = find(parent, v)

      if x != y
        e += 1
        result << [u, v, w]
        union(parent, rank, x, y)
      end
    end

    result.each do |(u, v, weight)|
      puts "#{u} -- #{v} == #{weight}"
    end
  end
end

graph = Kruskal.new(4)
graph.add_edge(0, 1, 10)
graph.add_edge(0, 2, 6)
graph.add_edge(0, 3, 5)
graph.add_edge(1, 3, 15)
graph.add_edge(2, 3, 4)
graph.kruskal_mst

Handling these edges in Ruby felt as smooth as spreading butter on warm toast. The sorting of edges before tackling the MST reminded me of arranging books on a shelf by size – you just know it’ll save you time later.

Switching gears to Prim’s algorithm, it’s like watching a spider spin a web, expanding outward from a single thread. Prim’s starts from a single vertex and grows the MST one edge at a time, always selecting the minimum weight edge that connects to vertices already in the tree. It’s all about greedy choices, ensuring you’re making the optimal choice at every branch without backpedaling.

This snippet can help guide you through Prim’s algorithm in Ruby:

def prim_mst(graph, start)
  num_vertices = graph.size
  mst_set = Array.new(num_vertices, false)
  key = Array.new(num_vertices, Float::INFINITY)
  parent = Array.new(num_vertices, -1)

  key[start] = 0

  (num_vertices - 1).times do
    min = Float::INFINITY
    min_index = -1

    key.each_with_index do |k, v|
      if !mst_set[v] && k < min
        min = k
        min_index = v
      end
    end

    u = min_index
    mst_set[u] = true

    graph[u].each_with_index do |weight, v|
      if weight > 0 && !mst_set[v] && weight < key[v]
        key[v] = weight
        parent[v] = u
      end
    end
  end

  parent.each_with_index do |u, v|
    puts "#{u} -- #{v} == #{graph[v][u]}" if u != -1
  end
end

graph = [
  [0, 2, 0, 6, 0],
  [2, 0, 3, 8, 5],
  [0, 3, 0, 0, 7],
  [6, 8, 0, 0, 9],
  [0, 5, 7, 9, 0]
]
prim_mst(graph, 0)

Running this code felt like watching tiny ants collaborating to form the shortest path to a sugar hill. Each choice made by the algorithm is about eliminating all unnecessary weight, keeping the structure as lean as it can be.

But why learn these algorithms? Well, think about the internet pipelines, water distribution systems, and even flight routes we rely on every day. Kruskal’s is like the pragmatic architect when you’re working from scratch with a roadmap, picking the leanest way forward from raw data. Prim’s, on the other hand, is your efficient project manager, iteratively expanding and maintaining network designs for existing infrastructures.

In terms of complexity, Kruskal’s can be swallowed fairly easily, with its time complexity primarily driven by edge sorting, making it O(E log E), where E is the number of edges. This fits nicely when your graphs are sparse. Meanwhile, Prim’s algorithm, when implemented with simple min-heap-based priority queue, mingles at O(V^2) with a more efficient version involving Fibonacci heaps promising O(V log V + E), hinting at Prim’s aptitude for dense graphs.

With Ruby, both these algorithms showcased their strengths and quirks in managing graph-related challenges. They taught patience in letting algorithms do their magic and showed me the beauty in efficient path-finding, inspiring further exploration into the labyrinth of data structures and efficient coding practices. This journey wasn’t just about understanding concepts but embracing the crafting of elegant solutions in one of my favorite programming languages.