Current section

Files

Jump to
inplace lib adt sparse_set.ex
Raw

lib/adt/sparse_set.ex

defmodule InPlace.SparseSet do
@moduledoc """
Sparse set implementation based on
https://youtu.be/PUJ_XdmSDZw?si=41ySCBvOdoNCV-zR
The main purpose is to support delete/undo operations
on the set, so we can use it for backtracking.
NOTE:
The code is intentionally kept close to the material in above video,
even if it may not adhere to a conventional Elixir style.
The set is a permutation on 1..domain_size.
Note: it's different from Knuth's implementation, where the set values are 0-based.
Options:
:mapper - function of arity 2. Allows to associate elements of the set with values.
"""
alias InPlace.Array
def new(domain_size, opts \\ []) do
opts = Keyword.merge(default_opts(), opts)
dom = Array.new(domain_size)
idom = Array.new(domain_size)
Enum.each(1..domain_size, fn el ->
Array.put(dom, el, el)
Array.put(idom, el, el)
end)
size = Array.new(1)
Array.put(size, 1, domain_size)
%{
dom: dom,
idom: idom,
mapper: Keyword.get(opts, :mapper),
max_size: domain_size,
size: size
}
end
def delete(set, el) when is_integer(el) and el > 0 do
case member_impl(set, el) do
nil -> false
r -> delete_impl(set, r, el)
end
end
def undelete(set) do
if undelete(set, 1) do
Array.get(set.dom, size(set))
end
end
def undelete(set, num) do
inc_size(set, num)
end
def size(%{size: size} = _set) do
Array.get(size, 1)
end
def empty?(set) do
size(set) == 0
end
def member?(set, el) do
member_impl(set, el) && true
end
def get(%{mapper: mapper_fun} = set, el) do
mapper_fun.(set, el)
end
def copy(set) do
Map.take(set, [:max_size, :mapper])
|> Map.put(:size, Array.copy(set.size))
|> Map.put(:dom, Array.copy(set.dom))
|> Map.put(:idom, Array.copy(set.idom))
end
def serialize(set) do
Map.take(set, [:max_size, :mapper])
|> Map.put(:size_as_list, Array.to_list(set.size))
|> Map.put(:dom_as_list, Array.to_list(set.dom))
|> Map.put(:idom_as_list, Array.to_list(set.idom))
end
def deserialize(serialized_set) do
Map.take(serialized_set, [:max_size, :mapper])
|> Map.put(:size, Array.from_list(serialized_set.size_as_list))
|> Map.put(:dom, Array.from_list(serialized_set.dom_as_list))
|> Map.put(:idom, Array.from_list(serialized_set.idom_as_list))
end
defp member_impl(%{idom: idom} = set, el) do
r = Array.get(idom, el)
if r <= size(set) do
r
end
end
defp inc_size(%{size: size, max_size: max_size} = _set, increase)
when is_integer(increase) and increase > 0 do
Array.update(size, 1, fn s ->
s = s + increase
if s <= max_size do
s
end
end)
end
defp inc_size(set, _) do
size(set)
end
defp delete_impl(%{dom: dom, idom: idom, size: size} = _set, r, el) do
Array.update(size, 1, fn s ->
if s > 1 do
l = Array.get(dom, s)
Array.put(dom, r, l)
Array.put(idom, l, r)
Array.put(dom, s, el)
Array.put(idom, el, s)
end
s - 1
end)
end
defp default_opts() do
[
mapper: fn _set, el -> el end
]
end
def reduce(set, initial, reducer) when is_function(reducer, 2) do
iterate(set, initial, reducer)
end
def each(set, action) when is_function(action, 1) do
reduce(set, nil, fn el, _acc -> action.(el) end)
:ok
end
def iterate(set, initial, reducer) when is_function(reducer, 2) do
iterate_impl(set, initial, size(set), reducer)
end
defp iterate_impl(_set, initial, 0, _reducer) do
initial
end
defp iterate_impl(%{dom: dom} = set, acc, position, reducer) do
el = Array.get(dom, position)
case reducer.(el, acc) do
{:halt, acc2} -> acc2
{:cont, acc2} -> iterate_impl(set, acc2, position - 1, reducer)
acc2 -> iterate_impl(set, acc2, position - 1, reducer)
end
end
@doc """
Iterate over set positions in increasing order (as opposed to iterating over set values).
This is equivalent to:
`iterate(set, initial, redicer)`
, but the above will process set elements in arbitrary order.
The intent is to not having to sort the result of the iteration.
This may or may not be more effective than the plain iteration.
"""
def iterate_ordered(set, mapper) when is_function(mapper, 1) do
iterate_ordered(set, [], fn pos, acc -> [mapper.(pos) | acc ] end) |> Enum.reverse()
end
def iterate_ordered(set, initial, reducer) when is_function(reducer, 2) do
case size(set) do
0 ->
initial
set_size ->
Enum.reduce_while(1..set.max_size, {set_size, initial}, fn pos,
{count_remainder, result_acc} =
acc ->
if member?(set, pos) do
case apply_reduction(pos, result_acc, reducer) do
{:cont, result_acc} ->
## Check if we've seen all set members
count_remainder = count_remainder - 1
if count_remainder == 0 do
{:halt, {0, result_acc}}
else
{:cont, {count_remainder, result_acc}}
end
{:halt, result_acc} ->
{:halt, {count_remainder, result_acc}}
end
else
## Position is not a member of the set
{:cont, acc}
end
end)
|> elem(1)
end
end
def to_list(set) do
iterate(set, [], fn el, acc -> [el | acc] end)
end
defp apply_reduction(el, acc, reducer) do
case reducer.(el, acc) do
{:halt, acc2} -> {:halt, acc2}
{:cont, acc2} -> {:cont, acc2}
acc2 -> {:cont, acc2}
end
end
end