Powered by AppSignal & Oban Pro

Sudoku with exact cover

livebooks/sudoku_with_exact_cover.livemd

Sudoku with exact cover

Mix.install(
  [{:inplace, path: Path.join(System.get_env("HOME"), "projects/inplace")},
    :kino
  ]
)


Logger.configure(level: :notice)

defmodule RenderHTML do
  use Kino.JS

  def new(html) when is_binary(html) do
    Kino.JS.new(__MODULE__, html)
  end

  asset "main.js" do
    """
    export function init(ctx, html) {
      ctx.root.innerHTML = html;
    }
    """
  end

  ## Sudoku rendering
  ## Grid is NxN array
  def render_sudoku(grid) when is_list(hd(grid)) do
    header = """
    
    """

    body = Enum.reduce(grid, "", fn row, acc -> acc <> row_to_string(row) end)

    footer = """
    
    """

    ## Render
    new(header <> body <> footer)
  end

  def render_sudoku(str) when is_binary(str) do
    for <> do
      case cell - 48 do
        n when n in 1..9 -> n
          _n -> 0  
      end
    end
    |> render_sudoku()
  end
  
  def render_sudoku(array1d) when is_list(array1d) do
    dim = floor(:math.sqrt(length(array1d)))    
    Enum.chunk_every(array1d, dim)
    |> render_sudoku()
  end

  defp row_to_string(row) do
    "\n" <> Enum.map_join(row, "\n", fn cell -> sudoku_cell(cell) end) <> "\n"
  end

  defp sudoku_cell(value) do
    """
    
    """
  end
end

Puzzle

alias InPlace.Examples.Sudoku

puzzle =
  #"48.3............71.2.......7.5....6....2..8.............1.76...3.....4......5...."
  #"000500301400020000000000000260001000000300900004000080090700000000080040600000000"
  ## slow :list, fast :heap
  #"000800004010020000092000000300600087000000050000090000000400200700005000000000100"
  #"760040000000000801000500000000100200040000050030000000108600000000090030200000000"
  #"200000700000000010000400000500600200980000000000300000010080040030001000000090500"
  #"030070000000000102000000600000005040600000030100600000050210000007000800004000000"
  "400000601020800000000000000001250000600000900000008030030500080700090000000000000"

RenderHTML.render_sudoku(puzzle)

Solution

{tc, solution} = :timer.tc(fn -> Sudoku.solve(puzzle, 
  checker: false,
  choose_item_fun: fn _, data -> InPlace.ExactCover.min_options_item(data) end,
  solution_handler: fn s -> send(self(), s) end) end)

solution = receive do
  msg -> msg
end

if Sudoku.check_solution(solution) do
  IO.puts("Solution is correct, solved in #{div(tc, 1000)} ms")
else
  IO.puts("Solution is incorrect")  
end

RenderHTML.render_sudoku(solution)


Benchmark

puzzle_file = 
  "data/sudoku/clue17"
  #"data/sudoku/top95"
  #"data/sudoku/hardest"
  #"data/sudoku/puzzles5_forum_hardest_1905_11+"
  #"data/sudoku/quasi_uniform_834"
num_instances = 100_000
puzzles = File.read!(Path.join("/Users/bokner/projects/cpsolver", puzzle_file)) 
  |> String.split("\n") |> Enum.take(-num_instances)

res = Enum.map(Enum.with_index(puzzles, 1), fn {p, idx} -> 
  {tc, _} = 
  :timer.tc(fn -> Sudoku.solve(p, checker: false, 
    choose_item_fun: fn _, data -> InPlace.ExactCover.min_options_item(data) end,
    solution_handler: fn _ -> :ok end) end)
  |> tap(fn {tc, _idx} -> IO.puts("#{idx} : #{round(tc/1000)} ms") end)

  {tc, idx}
  end)

times = Enum.map(res, fn {time, _idx} -> time end) |> Enum.sort()
stats = %{shortest: Enum.min(times), 
  longest: Enum.max(times), 
  average: Enum.sum(times) / length(times), 
  total: Enum.sum(times),
  sorted: Enum.sort(times),
  median: Enum.at(times, div(length(times), 2))
}
{duration, idx} = Enum.max_by(res, fn {time, _idx} -> time end)
longest = Enum.at(puzzles, idx - 1)
{duration, longest}