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>]