Packages
inplace
0.6.2
0.7.12
0.7.11
0.7.10
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.3
0.3.2
0.3.1
0.3.0
0.2.3
0.2.2
0.2.1
0.2.0
0.1.9
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
Mutable data structures
Current section
Files
Jump to
Current section
Files
lib/algorithms/exact_cover.ex
defmodule InPlace.ExactCover do
@moduledoc """
Implementation of Exact Cover.
Based on https://arxiv.org/pdf/cs/0011047 by Donald Knuth.
Modified to use "dancing cells" instead of "dancing links"
"""
alias InPlace.{Array, SparseSet}
def solve(options, solver_opts \\ []) do
state = init(options)
solver_opts = Keyword.merge(default_solver_opts(), solver_opts)
try do
search(1, Map.put(state, :solver_opts, solver_opts))
catch
:complete ->
:ok
end
end
defp default_solver_opts() do
[
solution_handler: fn options -> IO.inspect(options, label: :solution) end,
choose_item_fun: fn _step, state -> min_options_item(state) end,
stop_on: fn state -> num_solutions(state) == 1 end
]
end
def init(options) do
## Options are sets that contain item names.
## Build the state structures (roughly as described by D. Knuth)
{_, item_map, entry_count, option_membership, option_ranges} =
Enum.reduce(options,
{1, Map.new(), 0, [], Map.new()},
fn option,
{option_idx, directory, entry_idx, option_membership, option_ranges} = _acc ->
{directory, entry_count, membership} =
Enum.reduce(option, {directory, entry_idx, option_membership},
fn item_name,
{dir_acc, entry_idx_acc, membership_acc} ->
## 1-based index, for convenience
entry_idx_acc = entry_idx_acc + 1
{
Map.update(dir_acc, item_name, [entry_idx_acc], fn entries ->
[entry_idx_acc | entries]
end),
entry_idx_acc,
[option_idx | membership_acc]
}
end)
{option_idx + 1, directory, entry_count, membership, Map.put(option_ranges, option_idx, {entry_idx+1, entry_count})}
end)
num_items = map_size(item_map)
num_options = length(options)
## build item header, item and option lists
{item_names, item_lists} = Enum.unzip(item_map)
item_header =
SparseSet.new(num_items)
top = Array.new(entry_count, 0)
option_counts = Array.new(num_items, 0)
min_option_item = Array.new(2, 0)
{_, min_option_idx, min_option_count} =
Enum.reduce(item_lists, {1, nil, nil}, fn options, {item_idx, min_option_idx, min_option_count} ->
Enum.each(options, fn o -> Array.put(top, o, item_idx) end)
num_options = length(options)
Array.put(option_counts, item_idx, num_options)
{min_option_idx, min_option_count} =
if num_options < min_option_count do
{item_idx, num_options}
else
{min_option_idx, min_option_count}
end
{item_idx + 1, min_option_idx, min_option_count}
end)
update_min_item(min_option_item, min_option_idx, min_option_count)
## sparse-set for item lists
{_, item_options_map} = Enum.reduce(item_lists, {1, Map.new()}, fn options, {idx, acc} ->
{idx + 1, Map.put(acc, idx, options)}
end)
item_lists_ss = SparseSet.new(entry_count)
%{
num_items: num_items,
num_options: num_options,
item_header: item_header, ## Sparse set for tracking covered items
option_member_ids: init_option_member_ids(option_membership),
option_ranges: option_ranges, ## %{option_id => {first_entry_id, last_entry_id}
item_names: item_names,
top: top, ## top[i] points to the element of item header for i-th entry
item_lists: item_lists_ss, ## Sparse set for entries (contains entry ids)
item_options: item_options_map, ## %{item_id => options}
item_option_counts: %{
counts: option_counts, ## Count of active (uncovered) options per item
min_item: min_option_item ## The item with minimal option count
},
num_solutions: Array.new(1, 0),
## buffer for building current solution
solution: Array.new(num_options, 0)
}
end
defp search(
k,
%{
item_header: item_header,
solution: solution,
solver_opts: solver_opts
} = state
) do
stop_condition = solver_opts[:stop_on]
if stop_condition && stop_condition.(state) do
throw(:complete)
else
choose_item_fun = solver_opts[:choose_item_fun]
## Knuth:
# If R[h] = h, print the current solution and return.
##
if SparseSet.empty?(item_header) do
solution(state, Keyword.get(solver_opts, :solution_handler))
else
## Knuth:
# Otherwise choose a column object c (see below).
##
c = choose_item_fun.(k, state)
## Knuth:
# Cover column c.
##
if c do
cover(c, state)
## Knuth:
# For each r ← D[c], D[D[c]], . . . , while r != c,
#
iterate_column(
c,
fn r ->
## Knuth:
# set O[k] ← r;
##
add_to_solution(solution, k, r)
## Knuth:
# for each j ← R[r], R[R[r]], . . . , while j != r,
# cover column j
##
cover_option_columns(r, state)
search(k + 1, state)
## Knuth:
# for each j ← L[r], L[L[r]], . . . , while j != r,
# uncover column j.
##
uncover_option_columns(r, state)
end,
state
)
## Knuth:
# Uncover column c and return.
##
uncover(c, state)
end
end
end
end
defp cover_option_columns(option_pointer, state) do
iterate_row(
option_pointer,
fn j ->
cond do
j != option_pointer ->
# Tricky; cover/2 expects header (not item) pointer,
# so we need to convert
get_top(state, j)
|> cover(state)
true ->
:ok
end
end,
state
)
end
defp uncover_option_columns(option_pointer, state) do
iterate_row(
option_pointer,
fn j ->
cond do
j != option_pointer ->
# Tricky; cover/2 expects header (not item) pointer,
# so we need to convert
get_top(state, j)
|> uncover(state)
true ->
:ok
end
end,
state,
false
)
end
def min_options_item(%{item_header: item_header} = state) do
{min_item, _min_option_count} = get_min_item(state)
if covered?(min_item, state) do
## Note: we will go through all uncovered items
## and find the item with the smallest number of 'covered' options.
## We will also be updating global 'minimum' with the second smallest item,
## as the "minimum" item will be covered, so won't be in the item set.
##
SparseSet.reduce(
item_header, {nil, nil, nil, nil},
fn p, {min_p, min_acc, _prev_min_p, _prev_min_acc} = acc ->
## find min of option counts iterating over column (item) pointers
case get_item_options_count(state, p) do
0 -> {:halt, nil}
1 ->
{:halt, {p, 1}}
count ->
if count >= min_acc do
acc
else
{p, count, min_p, min_acc}
end
end
end
)
|> then(fn
nil -> nil
{min_item, _min_value} ->
min_item
{min_item, _min_value, second_min_item, second_min_value} ->
second_min_item && update_min_item(state, second_min_item, second_min_value)
min_item
end)
else
min_item
end
end
defp get_item_options_count(state, item_pointer) do
Array.get(state.item_option_counts.counts, item_pointer)
end
defp add_to_solution(solution, step, item) do
Array.put(solution, step, item)
end
defp solution(state, solution_handler) do
solution = state[:solution]
Array.update(state.num_solutions, 1, fn n -> n + 1 end)
Enum.reduce_while(1..state.num_options, [], fn idx, acc ->
case Array.get(solution, idx) do
0 ->
{:halt, acc}
option_entry ->
{:cont,
[
## solution_handler expects 0-based option index
(get_option_id(state, option_entry) - 1) | acc
]}
end
end)
|> solution_handler.()
end
def num_solutions(state) do
Array.get(state.num_solutions, 1)
end
## `column_pointer` is a pointer to
## an entry in "item header" list.
## This entry, in turn, contains the pointer to
## a head of a sublist in item_lists,
## from which we will handle (reduce) the options
## associated with the item.
##
def cover(
column_pointer,
%{
item_header: item_header,
item_lists: item_lists,
} = state
)
when is_integer(column_pointer) and column_pointer > 0 do
## Knuth:
# Set L[R[c]] ← L[c] and R[L[c]] ← R[c].
##
if !covered?(column_pointer, state) do
SparseSet.delete(item_header, column_pointer)
## Knuth:
# For each i ← D[c], D[D[c]] , . . . , while i != c,
##
iterate_column(
column_pointer,
## count of removed entries
## Knuth:
# For each j ← R[i], R[R[i]] , . . . , while j != i,
##
fn i ->
iterate_row(
i,
fn j ->
## Knuth:
# set U[D[j]] ← U[j], D[U[j]] ← D[j],
##
SparseSet.delete(item_lists, j)
# and set S[C[j]] ← S[C[j]] − 1
decrease_option_count(state, j)
end,
state
)
end,
state
)
end
end
defp covered?(column_pointer, %{item_header: item_header} = _state) do
!SparseSet.member?(item_header, column_pointer)
end
def uncover(
column_pointer,
%{
item_header: item_header,
item_lists: item_lists,
} = state
)
when is_integer(column_pointer) and column_pointer > 0 do
## Knuth:
# Set L[R[c]] ← L[c] and R[L[c]] ← R[c].
##
if SparseSet.undelete(item_header) do
## Knuth:
# For each i ← D[c], D[D[c]] , . . . , while i != c,
##
iterate_column(
column_pointer,
## count of removed entries
## Knuth:
# For each j ← R[i], R[R[i]] , . . . , while j != i,
##
fn i ->
iterate_row(
i,
fn j ->
## Knuth:
# set U[D[j]] ← U[j], D[U[j]] ← D[j],
##
#SparseSet.undelete(item_lists)
# and set S[C[j]] ← S[C[j]] − 1
increase_option_count(state, j)
end,
state,
false
)
|> tap(fn num_options -> SparseSet.undelete(item_lists, num_options) end)
end,
state
)
end
end
defp decrease_option_count(state, item_option_pointer) do
top = get_top(state, item_option_pointer)
update_option_count(state, top, fn val ->
new_val = val - 1
maybe_update_min_item(state, top, new_val)
new_val
end)
end
def increase_option_count(state, item_option_pointer) do
top = get_top(state, item_option_pointer)
update_option_count(state, top, fn val -> val + 1 end)
end
## 'update_fun/1' takes and updates current option count for given item header pointer
def update_option_count(state, item_header_pointer, update_fun)
when is_function(update_fun, 1) do
Array.update(state.item_option_counts.counts, item_header_pointer, update_fun)
end
## Mapping from 'absolute' option member ids (as in option_lists)
## to option ids they belong to
defp init_option_member_ids(option_membership) do
## option membership is in reverse order, i.e. the members that belong to the first
## option are in the end of the membership list.
l = length(option_membership)
arr = Array.new(l, 0)
Enum.reduce(option_membership, l, fn idx, acc ->
Array.put(arr, acc, idx)
acc - 1
end)
arr
end
defp get_option_id(%{option_member_ids: option_ids} = _state, option_entry) do
Array.get(option_ids, option_entry)
end
defp update_min_item(
%{item_option_counts: %{min_item: min_item}} = _state,
item_pointer,
option_count
) do
update_min_item(min_item, item_pointer, option_count)
end
defp update_min_item(min_item, item_pointer, option_count) do
Array.put(min_item, 1, item_pointer)
Array.put(min_item, 2, option_count)
end
defp get_min_item(%{item_option_counts: %{min_item: min_item}} = _state) do
{Array.get(min_item, 1), Array.get(min_item, 2)}
end
defp maybe_update_min_item(state, item_pointer, option_count) do
{_current_min_item, current_min_count} = get_min_item(state)
if current_min_count > option_count ||
(current_min_count == option_count && !covered?(item_pointer, state)) do
update_min_item(state, item_pointer, option_count)
else
:ok
end
end
defp get_top(%{top: top} = _state, el) do
get_top(top, el)
end
defp get_top(top, el) do
Array.get(top, el)
end
## `column pointer` is a pointer into `item_header` list.
defp iterate_column(
column_pointer,
iterator_fun,
%{item_lists: item_lists,
item_options: item_options} = _state
) do
columns_ss = Map.get(item_options, column_pointer)
Enum.each(columns_ss, fn el ->
if SparseSet.member?(item_lists, el), do: iterator_fun.(el)
end)
end
## `row_entry` is any option value that belongs to the item.
## We get the full option given this entry,
## then iterate over the option elements.
defp iterate_row(
row_pointer,
iterator_fun,
%{option_ranges: ranges} = state,
forward? \\ true
) do
row_id = get_option_id(state, row_pointer)
{first, last} = Map.get(ranges, row_id)
range = if forward? do
first..last
else
last..first//-1
end
Enum.reduce(range, 0, fn p, acc ->
if p != row_pointer do
iterator_fun.(p)
acc + 1
else
acc
end
end)
## Note: the result is only used for undeletion of multiple options;
## otherwise, the side effect of iterator_fun call would be all we need
##
end
end