4

I was attempting to write an algorithm for solving any sudoku. However, my original approach was flawed as I failed to realise that a well-formed sudoku should only have one solution.

As such, the resultant algorithm is not optimised for sudoku solving and is far more general: it recursively generates every possibly allowed outcome/layout for the current sudoku layout. The algorithm can therefore theoretically find every solution to a blank sudoku (but I/we/the human race will probably not be around to see the output).

My first question is: for this type of searching algorithm, what is the optimal approach - how can my algorithm be improved? The general strategy was to:

  • try every possible solution at a branching point
  • terminate the branch if a cell cannot be solved
  • terminate the branch if a solution is reached

The structure and approach was adapted from the time-decoupled solution outlined for solving the eight-queens problem in SICP (https://mitpress.mit.edu/sicp/full-text/book/book-Z-H-15.html) and it seems like this is a back-tracking algorithm (with the state of "time" pushed into the machine), is this true in my case?

My second question is, how can I use the Ruby >= 2.x Lazy functionality to generate an enumerable when dealing with a recursive algorithm?

The desired outcome is to do something like solve(sudoku).first(x) to retrieve the first x solutions. I.e. the solutions are stored as a stream/sequence. The difficulty I am having is that the recursion generates nested enumerables and I am struggling to find a means of passing a force method through the whole tree.

I've included the key code below, but the full method suite can be found here: https://gist.github.com/djtango/fe9322748cf8a055fc0e

def solve(sudoku)
  guess(simplify_sudoku(sudoku))
end

def guess(sudoku, row = 0, column = 0)
  return sudoku.flatten                 if end_of_grid?(row, column)
  return guess(sudoku, row + 1, 0)      if end_of_column?(column)
  return guess(sudoku, row, column + 1) if filled?(sudoku, row, column)
  return nil                            if insoluble?(sudoku, row, column)
  permissible_values(sudoku, row, column).map do |value|
    guess(update_sudoku(sudoku, value, row, column), row, column + 1)
  end
end

def simplify_sudoku(sudoku)
  return sudoku if no_easy_solutions?(sudoku)
  simplify_sudoku(fill_in(sudoku))
end

def fill_in(sudoku)
  new_sudoku = copy_sudoku(sudoku)
  new_sudoku.map.with_index do |row, row_index|
    row.map.with_index do |column, column_index|
      solution = permissible_values(new_sudoku, row_index, column_index)
      easy_and_not_filled?(solution, new_sudoku, row_index, column_index) ? [solution.first] : column
    end
  end
end

When attempting to implement lazy evaluation, the most natural point to introduce the lazy method is the guess method like so:

  permissible_values(sudoku, row, column).map do |value|
    guess(update_sudoku(sudoku, value, row, column), row, column + 1)
  end

But when implementing this, the result is:

solve(blank_sdk)
 => #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 8, 9]>:map> 

solve(blank_sdk).first(10)
 => [#<Enumerator::Lazy: #<Enumerator::Lazy: [2, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 8]>:map>] 

solve(blank_sdk).first(1000000000000)
 => [#<Enumerator::Lazy: #<Enumerator::Lazy: [2, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 3, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 4, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 5, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 6, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 7, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 8, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 9]>:map>, #<Enumerator::Lazy: #<Enumerator::Lazy: [1, 2, 3, 4, 5, 6, 7, 8]>:map>] 
djtango
  • 81
  • 5
  • 3
    Those who cast a vote to close probably did so because they believed your question, interesting as it may be, is not suitable for SO, that the sister-site [Code Review](http://codereview.stackexchange.com/) is the right place for it, in part because the latter requires working code, whereas SO is mainly for non-working efforts. I tend to agree, but did not vote to close. If you post this on Code Review I expect you will be rewarded with several insightful and well-crafted answers. – Cary Swoveland Nov 30 '15 at 18:38
  • Thank you for this reply - I had a look on Code Review (I'm new to programming + SO) and agree that my post would be more appropriate there. On looking for posts similar to this one there, I was also able to find new topics to fuel my research, namely Algorithm X/dancing links, and more generally, the topic of exact cover problems which is exactly what I was looking for! On a side note though, I am unable to get enumeration for this task to work - would that portion of the post be appropriate to SO? – djtango Dec 01 '15 at 11:13

1 Answers1

1

You don't need to try every possible solution at each branch point. You can do better by finding the cell that has the least number of options remaining and try the variants there. This way you quickly set all the cells that have only one option remaining. And when you need to try different solutions you will likely have only a few variants to try (2-3) instead of all possibilities so it should be faster. If none of them work for that cell you can stop because there's no solution.

Sorin
  • 11,863
  • 22
  • 26