Stuff 'n Things

Elixir Sudoku

• elixir

Last week I was reading my friends blog Robot Dan and came across his sudoku post.

I’ve never written a sudoku solver before, naturally I thought of writing it in Elixir. I spent the evening playing around trying to come up with something that was at least somewhat idomatic.

My first solution was a very similar implementation to what Dan had on his site. It works ok. I found a harder board that made it take a bit longer. Interestingly I found that the memory usage was flat for easy or hard. I’m guessing that I got the tail recursion correct.

defmodule Sudoku do
  def easy, do: Sudoku.solve(Sudoku.easy_board)

  # Just using 0 rather than nil for display purposes
  def easy_board do
    [
      [8,3,0, 1,0,0, 6,0,5],
      [0,0,0, 0,0,0, 0,8,0],
      [0,0,0, 7,0,0, 9,0,0],

      [0,5,0, 0,1,7, 0,0,0],
      [0,0,3, 0,0,0, 2,0,0],
      [0,0,0, 3,4,0, 0,1,0],

      [0,0,4, 0,0,8, 0,0,0],
      [0,9,0, 0,0,0, 0,0,0],
      [3,0,2, 0,0,6, 0,4,7]
    ]
  end

  def solve(board) do
    case solve(board, 0, 0) do
      { :ok, solved_board } -> print_board(solved_board)
      { :error } -> IO.puts("Board is not valid")
    end
  end

  def solve(board, x, _) when x==9, do: { :ok, board }
  def solve(board, x, y) when y==9, do: solve(board, x + 1, 0)

  def solve(board, x, y) do
    if Enum.at(Enum.at(board, x), y) == 0 do
      solve(board, x, y, possible_values(board, x, y))
    else
      solve(board, x, y + 1)
    end
  end

  def solve(_, _, _, []), do: { :error }

  def solve(board, x, y, [head | tail]) do
    new_row = Enum.at(board, x) |> List.replace_at(y, head)
    new_board = List.replace_at(board, x, new_row)

    case solve(new_board, x, y + 1) do
      { :ok, solved_board } -> { :ok, solved_board }
      { :error } -> solve(board, x, y, tail)
    end
  end

  def possible_values(board, x, y) do
    vals = [values_in_row(board, x), values_in_column(board, y), values_in_cell(board, x, y)]
            |> List.flatten
            |> Enum.uniq

    (1..9) |> Enum.reject(&(Enum.member?(vals, &1)))
  end

  def values_in_row(board, x), do: Enum.at(board, x)
  def values_in_column(board, y), do: Enum.map(board, &(Enum.at(&1, y)))

  def values_in_cell(board, x, y) do
    start_x = div(x, 3) * 3
    start_y = div(y, 3) * 3
    Enum.slice(board, start_x, 3) |> Enum.map(&(Enum.slice(&1, start_y, 3))) |> List.flatten
  end

  def print_board([]), do: nil
  def print_board([row | rest]) do
    IO.inspect(row)
    print_board(rest)
  end
end

I wanted to do something a little more Elixiry though. When the recursive function selects all possible values for a cell, and then recurses, that’s the point that I thought I might be able to shoot out a bunch of processes and do all the calculations in parallel. A branching recursive parallel implementation. Sounded fun!

I implemented it using Elixir Tasks and process dictionaries.

Unfortunately I went wrong somewhere. I ran out of processes, or simply timed out. I even increased the number of processes to 5M without helping. It maxed out in RAM, CPU and times out.

I think I’ll stick with the recursive solution, but I’ve included the other below.

# The only board that works with this one on my machine is the easy.
# The others simply timeout.
defmodule PSudoku do
  def easy, do: PSudoku.solve(PSudoku.easy_board)

  # Just using 0 rather than nil for display purposes
  def easy_board do
    [
      [8,3,0, 1,0,0, 6,0,5],
      [0,0,0, 0,0,0, 0,8,0],
      [0,0,0, 7,0,0, 9,0,0],

      [0,5,0, 0,1,7, 0,0,0],
      [0,0,3, 0,0,0, 2,0,0],
      [0,0,0, 3,4,0, 0,1,0],

      [0,0,4, 0,0,8, 0,0,0],
      [0,9,0, 0,0,0, 0,0,0],
      [3,0,2, 0,0,6, 0,4,7]
    ]
  end

  def solve(board) do
    case solve(board, 0, 0) do
      { :ok, solved_board } -> print_board(solved_board)
      { :error } -> IO.puts("ERRRO")
    end
  end

  def solve(board, x, _) when x==9, do: { :ok, board }
  def solve(board, x, y) when y==9, do: solve(board, x + 1, 0)

  def solve(board, x, y) do
    if Enum.at(Enum.at(board, x), y) == 0 do
      vals = possible_values(board, x, y)
      if length(vals) > 0 do
        result = Enum.map(vals, fn(v) -> Task.async(fn -> solve(board, x, y, v) end) end)
        |> Enum.map(&(Task.await(&1, 30_000)))
        |> Enum.find({ :error }, &match?({ :ok, _ }, &1))
      else
        { :error }
      end
    else
      solve(board, x, y + 1)
    end
  end

  def solve(board, x, y, v) when is_integer(v) do
    new_row = Enum.at(board, x) |> List.replace_at(y, v)
    new_board = List.replace_at(board, x, new_row)
    solve(new_board, x, y + 1)
  end

  def possible_values(board, x, y) do
    vals = [values_in_row(board, x), values_in_column(board, y), values_in_cell(board, x, y)]
            |> List.flatten
            |> Enum.uniq

    (1..9) |> Enum.reject(&(Enum.member?(vals, &1)))
  end

  def values_in_row(board, x), do: Enum.at(board, x)
  def values_in_column(board, y), do: Enum.map(board, &(Enum.at(&1, y)))

  def values_in_cell(board, x, y) do
    start_x = div(x, 3) * 3
    start_y = div(y, 3) * 3
    Enum.slice(board, start_x, 3) |> Enum.map(&(Enum.slice(&1, start_y, 3))) |> List.flatten
  end

  def print_board([]), do: nil
  def print_board([row | rest]) do
    IO.inspect(row)
    print_board(rest)
  end
end
comments powered by Disqus