defmodule InPlace.ExactCover do @moduledoc """ Implementation of Algorithm DLX (Exact cover via dancing links). Based on https://arxiv.org/pdf/cs/0011047 by Donald Knuth. Note: there is a never version of this algorithm (The Art of Computer Programming, vol. 4B, by Donald Knuth). It differs mostly by using more advanced internal state structure. """ alias InPlace.{LinkedList, Array} 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_lists, option_start_ids} = Enum.reduce(options, {Map.new(), 0, [], []}, fn option, {directory, entry_idx, option_items, option_start_ids} = _acc -> option_start_idx = entry_idx + 1 {directory, entry_count, items} = Enum.reduce(option, {directory, entry_idx, []}, fn item_name, {dir_acc, entry_idx_acc, option_items_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, [entry_idx_acc | option_items_acc] } end) {directory, entry_count, [items | option_items], [option_start_idx | option_start_ids]} end) num_items = map_size(item_map) ## build item header, item and option lists {item_names, item_lists} = Enum.unzip(item_map) item_header = LinkedList.new( Enum.map(1..num_items, fn header_idx -> ## Header pointers. ## They will be used as the 'heads' of correspondent item lists. ## For the test example: ## `:c` item will be in the header with pointer 1 and content 17 ## (as there are 16 entries total across all option lists) ## Then the item list that corresponds to :c, will form ## a (17, 1, 8) circuit. header_idx + entry_count end), deletion: :hide ) item_lists_ll = LinkedList.new(Enum.to_list(1..(entry_count + num_items)), deletion: :hide) |> tap(fn ll -> item_lists |> Enum.zip((entry_count + 1)..(entry_count + num_items)) |> Enum.each(fn {options, item_header} -> ## create sublists of options per item LinkedList.circuit(ll, [item_header | Enum.reverse(options)]) end) end) item_top_map = LinkedList.iterate( item_header, fn p, acc -> top = LinkedList.data(item_header, p) LinkedList.iterate( item_lists_ll, fn s, acc2 -> Map.put(acc2, s, p) end, start: top, initial_value: acc ) end, initial_value: Map.new() ) ## NOTE: we won't have to cover/uncover options, hence there is no "undoing" it ## We also do not need extra entries for header pointers, ## as was the case for item lists. ## option_lists_ll = LinkedList.new(Enum.to_list(1..entry_count), deletion: :hide) |> tap(fn ll -> Enum.each( option_lists, fn items -> LinkedList.circuit(ll, Enum.reverse(items)) end ) end) top = map_to_array(item_top_map) %{ item_header: item_header, option_start_ids: Enum.reverse(option_start_ids), item_names: item_names, top: top, item_lists: item_lists_ll, option_lists: option_lists_ll, item_option_counts: init_item_option_counts(item_lists), 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 LinkedList.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. ## # |> IO.inspect(label: :cover_top) # num_removed_entries = cover(c, state) ## if num_removed_entries > 0 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 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 first_item(%{item_header: item_header} = _state) do LinkedList.head(item_header) end def random_item(%{item_header: item_header} = _state) do header_size = LinkedList.size(item_header) random_position = Enum.random(1..header_size) LinkedList.iterate( item_header, fn p, acc -> if acc == random_position do {:halt, p} else {:cont, acc + 1} end end, initial_value: 1 ) end def min_options_item(%{item_header: item_header} = state) do {min_item, _option_count} = get_min_item(state) if covered?(min_item, state) do head = LinkedList.head(item_header) head_count = get_item_options_count(state, head) LinkedList.iterate( item_header, fn p, {_min_p, min_acc} -> ## find min of option counts iterating over column (item) pointers case get_item_options_count(state, p) do # it's a minimal count count when count <= 1 -> {:halt, {p, count}} count -> {p, min(count, min_acc)} end end, initial_value: {head, head_count} ) |> elem(0) 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, [ LinkedList.iterate( state.option_lists, fn p, acc -> case Enum.find_index(state.option_start_ids, fn val -> val == p end) do nil -> {:cont, acc} option_number -> {:halt, option_number} end end, start: option_entry, initial_value: false ) | 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]. ## LinkedList.delete_pointer(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], ## if i != j do LinkedList.delete_pointer(item_lists, j) # and set S[C[j]]  ← S[C[j]]  − 1 decrease_option_count(state, j) end end, state ) end, state ) end ## This variant of cover/2 is for debugging only. ## We won't need to pass item name/id, passing item pointer ## would be sufficient for the implementation def cover(item_name, state) do cover(column_pointer(item_name, state), state) end defp covered?(column_pointer, %{item_header: item_header} = _state) do LinkedList.pointer_deleted?(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]. ## LinkedList.restore_pointer(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], ## if i != j do LinkedList.restore_pointer(item_lists, j) # and set S[C[j]]  ← S[C[j]]  − 1 increase_option_count(state, j) end end, state, false ) end, state, false ) 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 defp map_to_array(map) do array = Array.new(map_size(map)) Enum.each(map, fn {key, value} -> Array.put(array, key, value) end) array end defp init_item_option_counts(item_lists) do num_items = length(item_lists) counts = Array.new(num_items) ## min_item[1] - pointer, min_item[2] - value (minimal number of options) min_item = Array.new(2) {min_item_idx, min_options_value} = item_lists |> Enum.with_index(1) |> Enum.reduce({nil, nil} ,fn {options, item_idx}, {_p_acc, value_acc} = min_acc -> num_options = length(options) Array.put(counts, item_idx, num_options) if value_acc > num_options do {item_idx, num_options} else min_acc end end) update_min_item(min_item, min_item_idx, min_options_value) %{counts: counts, min_item: min_item} 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 in `item_header` linked list. ## The element it points to is a 'top' of the column, ## which is a pointer in `item_lists` linked list defp iterate_column( column_pointer, iterator_fun, %{item_header: item_header, item_lists: columns} = _state, forward? \\ true ) do column_top = LinkedList.data(item_header, column_pointer) LinkedList.iterate( columns, fn column_element -> if column_element != column_top do iterator_fun.(column_element) end end, start: (forward? && LinkedList.next(columns, column_top)) || LinkedList.prev(columns, column_top), forward: forward? ) end ## `row_pointer` is any pointer in the list of `option_lists` items. ## `option_lists` is a linked list partitioned by option sublists ## , each sublist represents an option. defp iterate_row( row_pointer, iterator_fun, %{option_lists: rows} = _state, forward? \\ true ) do LinkedList.iterate( rows, fn p -> iterator_fun.(p) end, start: row_pointer, forward: forward? ) end defp column_pointer(item_name, %{item_names: item_names} = _state) do length(item_names) - Enum.find_index(item_names, fn name -> name == item_name end) end end