Packages
inplace
0.7.7
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/examples/sudoku.ex
defmodule InPlace.Examples.Sudoku do
@moduledoc """
Sudoku puzzle.
The instance of puzzle is a string of DxD length
, where D is a dimension of Sudoku puzzle (would be 9 by default).
The values 1 to 9 represent pre-filled cells (clues, givens etc...);
any other values represent hidden cells.
"""
alias InPlace.ExactCover
alias InPlace.BitSet
alias InPlace.Array
require Logger
@numbers ?1..?9
@doc """
Solver
"""
def solve(instance, opts \\ []) do
## Build the options for the exact cover, and some supplemental data
%{options: options, dimension: d, instance: instance_array} = init(instance)
opts = Keyword.merge(default_opts(), opts)
## Plug in solution handler
top_solution_handler = Keyword.get(opts, :solution_handler)
checker = Keyword.get(opts, :checker)
opts =
Keyword.put(opts, :solution_handler, fn solution ->
solution
|> solution_to_sudoku(instance_array, options, d)
|> tap(fn solution -> checker && check_solution(solution) end)
|> top_solution_handler.()
end)
## Solve with exact cover
ExactCover.solve(options, opts)
end
defp default_opts() do
[
solution_handler: fn solution -> Logger.info(inspect(solution)) end,
checker: fn correct? -> !correct? && Logger.error("invalid solution") end
]
end
defp init(instance) when is_binary(instance) do
l = String.length(instance)
d = :math.sqrt(l) |> floor()
if d * d != l, do: throw(:invalid_instance)
sqrt_d = :math.sqrt(d) |> floor()
if sqrt_d * sqrt_d != d, do: throw(:invalid_instance)
%{
options: init_exact_cover(instance, d),
dimension: d,
instance: instance_to_array(instance)
}
end
defp init_exact_cover(instance, d) when is_binary(instance) do
## Turn the instance to a list of options
## columns (items in "exact-cover" terms):
## - one for each cell (d^2);
## - d^2 for columns: each of the d columns must have each possible value 1-d.
## - d^2 for rows: each of the d rows must have each possible value 1-d.
## - d^2 for blocks: Each of the d sqrt(d)×sqrt(d) blocks must have each possible value 1-d.
## ?? - initial state: Since there is an initial state for the board, one option will describe the initial state and this item ensures that it is always part of any solution found.
## Rows (options in "exact-cover" terms)
## There will be (d*d - num_open_cells) * d rows (options)
## for hidden cells, plus one that matches open cells
##
covered_set = BitSet.new(0, d * d * 4)
{_position, options} =
for <<cell <- instance>>, reduce: {0, []} do
{cell_number, options_acc} ->
{cell_number + 1,
if hidden_cell?(cell) do
Enum.reduce(0..(d - 1), options_acc, fn value, opt_acc ->
option = create_option(cell_number * d + value, d)
#BitSet.disjoint?(BitSet.new(option), covered_set) &&
[option | opt_acc]
#|| opt_acc
end)
else
Enum.each(
create_option(cell_number * d + cell_value(cell) - 1, d),
fn val -> BitSet.put(covered_set, val)
end)
options_acc
end
}
end
Enum.reject(options, fn opt ->
Enum.any?(opt, fn val -> BitSet.member?(covered_set, val) end)
end)
end
defp hidden_cell?(ascii_code) do
ascii_code not in @numbers
end
defp create_option(row, d) do
[
cell_item(row, d),
row_item(row, d),
column_item(row, d),
block_item(row, d)
]
end
defp cell_value(ascii_code) do
ascii_code - 48
end
def cell_item(row, d) do
div(row, d)
end
def row_item(row, d) do
d_squared = d * d
d_squared + d * div(row, d_squared) + rem(row, d)
end
def column_item(row, d) do
d_squared = d * d
2 * d_squared + rem(row, d_squared)
end
def block_item(row, d) do
d_squared = d * d
sqrt_d = floor(:math.sqrt(d))
3 * d_squared +
div(row, sqrt_d * d_squared) * d * sqrt_d +
(div(row, sqrt_d * d) |> rem(sqrt_d)) * d + rem(row, d)
end
defp instance_to_array(instance) when is_binary(instance) do
l = String.length(instance)
arr = Array.new(l, 0)
for <<cell <- instance>>, reduce: 1 do
acc ->
!hidden_cell?(cell) && Array.put(arr, acc, cell_value(cell))
acc + 1
end
arr
end
@doc """
`solution` is a "cover" (list of indices into options
built off the Sudoku instance).
Note: the options only represent "hidden" cells.
"""
def solution_to_sudoku(solution, instance, options, d) do
grid_size = d * d
solution
|> Enum.each(fn opt_idx ->
[_, r, c, _] = Enum.at(options, opt_idx)
{val, row, col} =
{
rem(r - grid_size, d) + 1,
div(r - grid_size, d),
div(c - 2 * grid_size, d)
}
Array.put(instance, row * d + col + 1, val)
end)
Array.to_list(instance)
end
@spec check_solution([integer()]) :: boolean()
@doc """
`solution` is a list of integers
representing the 'flattened' solved puzzle
"""
def check_solution(solution) when is_list(solution) do
dim = :math.sqrt(length(solution)) |> floor()
grid = Enum.chunk_every(solution, dim)
transposed = Enum.zip_with(grid, &Function.identity/1)
squares = group_by_subsquares(grid)
checker_fun = fn line -> Enum.sort(line) == Enum.to_list(1..dim) end
Enum.all?([grid, transposed, squares], fn arrangement ->
Enum.all?(arrangement, checker_fun)
end)
end
defp group_by_subsquares(cells) do
square = :math.sqrt(length(cells)) |> floor
for i <- 0..(square - 1), j <- 0..(square - 1) do
for k <- (i * square)..(i * square + square - 1),
l <- (j * square)..(j * square + square - 1) do
Enum.at(cells, k) |> Enum.at(l)
end
end
end
end