Powered by AppSignal & Oban Pro

Union-find

livebooks/union_find.livemd

Union-find

Mix.install(
  [
    :kino,
    {:kino_libgraph, "~> 0.1.0"}, 
    {:libgraph, "~> 0.16.0"},
    {:inplace, path: Path.join(System.get_env("HOME"), "projects/inplace")} 
  ]
)

alias InPlace.UnionFind

print_sets_fun = fn(uf, label) ->
  reps = Enum.map(1..uf.size, fn el -> UnionFind.find(uf, el) end)
  Enum.group_by(Enum.with_index(reps, 1), 
    fn {x, _idx} -> x end, 
    fn {_x, idx} -> idx end)
  |> Map.values()
  |> IO.inspect(charlists: :as_lists, label: label)
end

representative_fun = fn(uf, subset) ->
  Enum.uniq_by(subset, fn el -> UnionFind.find(uf, el) end)
  |> hd()
end 

What is union-find?

Also known as Disjoint Set Union (DSU).

Union-find is a data structure that stores a collection of disjoint (non-overlapping) sets.

It provides operations for adding new sets, merging sets (replacing them with their union), and finding a representative member of a set.

The last operation makes it possible to determine efficiently whether any two elements belong to the same set or to different sets.

Functionality


# Create union-find with capacity of 10 elements
uf_size = 10
uf = UnionFind.new(uf_size)
print_sets_fun.(uf, :initial)
## Union few elements
## This will put them into a disjoint set.
subset1 = Enum.take(1..uf_size, 3)
IO.inspect(subset1, label: :subset1)

:ok = UnionFind.union(uf, subset1)
print_sets_fun.(uf, :first_union)

## Unionized elements have the same representative
## (that's how we know they belong to the same subset).
subset1_rep = representative_fun.(uf, subset1)
|> IO.inspect(label: :subset1_representative)

rest = Enum.to_list(1..uf_size) |> Kernel.--(subset1)

Enum.map(rest, fn el -> UnionFind.find(uf, el) end) 
|> IO.inspect(charlists: :as_lists, label: :others)

subset2 = Enum.take(rest, 3) |> IO.inspect(label: :subset2)
## Let's unionize some more
UnionFind.union(uf, subset2)

subset2_rep = representative_fun.(uf, subset2)
|> IO.inspect(label: :rep_subset2)

### Union two sets
UnionFind.union(uf, Enum.random(subset1), Enum.random(subset2))

#UnionFind.find(uf, 5)
#reps = Enum.map(1..10, fn el -> UnionFind.find(uf, el) end)
print_sets_fun.(uf, :subset_subset2)

:ok

Application: Minimum Spanning Tree

https://en.wikipedia.org/wiki/Minimum_spanning_tree

File.read!("/Users/bokner/projects/inplace/livebooks/Minimum_spanning_tree.svg")
|> Kino.Image.new(:svg)

Kruskal’s algorithm

Pseudocode (https://en.wikipedia.org/wiki/Kruskal’s_algorithm)

function Kruskal(Graph G) is
    F:= ∅
    for each v in G.Vertices do
        MAKE-SET(v)

    for each {u, v} in G.Edges ordered by increasing weight({u, v}) do
        if FIND-SET(u) ≠ FIND-SET(v) then
           ## vertices u and v are not in the same set, meaning
           ## adding {u, v} edge to the tree will not result in a cycle

            F := F ∪ { {u, v} }
            ## Join the subtree with vertex u and the the subtree with vertex v 
            UNION(FIND-SET(u), FIND-SET(v))
    return F

InPlace.Examples.Kruskal

alias InPlace.Examples.MST

edges = [
  {:A, :B, 3},
  {:A, :D, 6},
  {:A, :G, 9},
  {:B, :C, 2},
  {:B, :D, 4},
  {:B, :G, 9},
  {:B, :F, 9},
  {:C, :D, 2},
  {:C, :E, 9},
  {:C, :F, 8},
  {:D, :E, 9},
  {:E, :F, 7},
  {:E, :H, 4},
  {:E, :I, 5},
  {:F, :G, 8},
  {:F, :I, 9},
  {:F, :J, 10},
  {:G, :J, 11},
  {:H, :I, 1},
  {:H, :J, 4},
  {:I, :J, 3}
]

{mst, weight} = MST.kruskal(edges)
%{mst: Enum.sort(mst), weight: weight}
undirected_graph =
  Enum.reduce(edges, Graph.new(type: :undirected), fn {v1, v2, weight}, acc ->
    Graph.add_edge(acc, Graph.Edge.new(v1, v2, weight: weight, label: to_string(weight)))
    end)