Packages
inplace
0.6.0
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(
Enum.with_index(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)
{directory, entry_count, membership, Map.put(option_ranges, option_idx, {entry_idx+1, entry_count})}
end)
num_items = map_size(item_map)
## 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)
%{
item_header: item_header,
option_member_ids: init_option_member_ids(option_membership),
option_ranges: option_ranges,
item_names: item_names,
top: top,
item_lists: item_lists_ss,
item_options: item_options_map,
item_option_counts: %{counts: option_counts, min_item: min_option_item},
num_solutions: Array.new(1, 0),
## buffer for building current solution
solution: Array.new(length(options))
}
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
##
# {_num_covered_columns, _num_removed_entries} =
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)
# uncover(r, num_covered_columns, num_removed_entries, state)
# uncover(r, )
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 random_item(%{item_header: item_header} = _state) do
header_size = SparseSet.size(item_header)
random_position = Enum.random(1..header_size)
SparseSet.reduce(
item_header, 1,
fn p, acc ->
if acc == random_position do
{:halt, p}
else
{:cont, acc + 1}
end
end
)
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
SparseSet.reduce(
item_header, {nil, nil},
fn p, {_min_p, min_acc} = acc ->
## find min of option counts iterating over column (item) pointers
case get_item_options_count(state, p) do
0 -> acc
1 ->
{:halt, {p, 1}}
count ->
{p, min(count, min_acc)}
end
end
)
|> then(fn {min_item, min_value} ->
if min_option_count > min_value do
update_min_item(state, min_item, min_value)
end
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..Array.size(solution), [], fn idx, acc ->
case Array.get(solution, idx) do
nil ->
{: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
)
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.each(range, fn p -> if p != row_pointer, do: iterator_fun.(p) end)
end
end