Skip to content

Instantly share code, notes, and snippets.

@fern4lvarez
Created March 9, 2012 10:23
Show Gist options
  • Save fern4lvarez/2005971 to your computer and use it in GitHub Desktop.
Save fern4lvarez/2005971 to your computer and use it in GitHub Desktop.
Implementation of a Sudoku solver in Ruby
#
# This module defines a Sudoku::Puzzle class to represent a 9x9
# Sudoku puzzle and also defines exception classes raised for
# invalid input and over-constrained puzzles. This module also defines
# the method Sudoku.solve to solve a puzzle. The solve method uses
# the Sudoku.scan method, which is also defined here.
#
# Use this module to solve Sudoku puzzles with code like this:
#
# require 'sudoku'
# puts Sudoku.solve(Sudoku::Puzzle.new(ARGF.readlines))
#
module Sudoku
#
# The Sudoku::Puzzle class represents the state of a 9x9 Sudoku puzzle.
#
# Some definitions and terminology used in this implementation:
#
# - Each element of a puzzle is called a "cell".
# - Rows and columns are numbered from 0 to 8, and the coordinates [0,0]
# refer to the cell in the upper-left corner of the puzzle.
# - The nine 3x3 subgrids are known as "boxes" and are also numbered from
# 0 to 8, ordered from left to right and top to bottom. The box in
# the upper-left is box 0. The box in the upper-right is box 2. The
# box in the middle is box 4. The box in the lower-right is box 8.
#
# Create a new puzzle with Sudoku::Puzzle.new, specifying the initial
# state as a string or as an array of strings. The string(s) should use
# the characters 1 through 9 for the given values, and '.' for cells
# whose value is unspecified. Whitespace in the input is ignored.
#
# Read and write access to individual cells of the puzzle is through the
# [] and []= operators, which expect two-dimensional [row,column] indexing.
# These methods use numbers (not characters) 0 to 9 for cell contents.
# 0 represents an unknown value.
#
# The has_duplicates? predicate returns true if the puzzle is invalid
# because any row, column, or box includes the same digit twice.
#
# The each_unknown method is an iterator that loops through the cells of
# the puzzle and invokes the associated block once for each cell whose
# value is unknown.
#
# The possible method returns an array of integers in the range 1..9.
# The elements of the array are the only values allowed in the specified
# cell. If this array is empty, then the puzzle is over-specified and
# cannot be solved. If the array has only one element, then that element
# must be the value for that cell of the puzzle.
#
class Puzzle
# These constants are used for translating between the external
# string representation of a puzzle and the internal representation.
ASCII = ".123456789"
BIN = "\000\001\002\003\004\005\006\007\010\011"
# This is the initialization method for the class. It is automatically
# invoked on new Puzzle instances created with Puzzle.new. Pass the input
# puzzle as an array of lines or as a single string. Use ASCII digits 1
# to 9 and use the '.' character for unknown cells. Whitespace,
# including newlines, will be stripped.
def initialize(lines)
if (lines.respond_to? :join) # If argument looks like an array of lines
s = lines.join # Then join them into a single string
else # Otherwise, assume we have a string
s = lines.dup # And make a private copy of it
end
# Remove whitespace (including newlines) from the data
# The '!' in gsub! indicates that this is a mutator method that
# alters the string directly rather than making a copy.
s.gsub!(/\s/, "") # /\s/ is a Regexp that matches any whitespace
# Raise an exception if the input is the wrong size.
# Note that we use unless instead of if, and use it in modifier form.
raise Invalid, "Grid is the wrong size" unless s.size == 81
# Check for invalid characters, and save the location of the first.
# Note that we assign and test the value assigned at the same time.
if i = s.index(/[^123456789\.]/)
# Include the invalid character in the error message.
# Note the Ruby expression inside #{} in string literal.
raise Invalid, "Illegal character #{s[i,1]} in puzzle"
end
# The following two lines convert our string of ASCII characters
# to an array of integers, using two powerful String methods.
# The resulting array is stored in the instance variable @grid
# The number 0 is used to represent an unknown value.
s.tr!(ASCII, BIN) # Translate ASCII characters into bytes
@grid = s.unpack('c*') # Now unpack the bytes into an array of numbers
# Make sure that the rows, columns, and boxes have no duplicates.
raise Invalid, "Initial puzzle has duplicates" if has_duplicates?
end
# Return the state of the puzzle as a string of 9 lines with 9
# characters (plus newline) each.
def to_s
# This method is implemented with a single line of Ruby magic that
# reverses the steps in the initialize() method. Writing dense code
# like this is probably not good coding style, but it demonstrates
# the power and expressiveness of the language.
#
# Broken down, the line below works like this:
# (0..8).collect invokes the code in curly braces 9 times--once
# for each row--and collects the return value of that code into an
# array. The code in curly braces takes a subarray of the grid
# representing a single row and packs its numbers into a string.
# The join() method joins the elements of the array into a single
# string with newlines between them. Finally, the tr() method
# translates the binary string representation into ASCII digits.
(0..8).collect{|r| @grid[r*9,9].pack('c9')}.join("\n").tr(BIN,ASCII)
end
# Return a duplicate of this Puzzle object.
# This method overrides Object.dup to copy the @grid array.
def dup
copy = super # Make a shallow copy by calling Object.dup
@grid = @grid.dup # Make a new copy of the internal data
copy # Return the copied object
end
# We override the array access operator to allow access to the
# individual cells of a puzzle. Puzzles are two-dimensional,
# and must be indexed with row and column coordinates.
def [](row, col)
# Convert two-dimensional (row,col) coordinates into a one-dimensional
# array index and get and return the cell value at that index
@grid[row*9 + col]
end
# This method allows the array access operator to be used on the
# lefthand side of an assignment operation. It sets the value of
# the cell at (row, col) to newvalue.
def []=(row, col, newvalue)
# Raise an exception unless the new value is in the range 0 to 9.
unless (0..9).include? newvalue
raise Invalid, "illegal cell value"
end
# Set the appropriate element of the internal array to the value.
@grid[row*9 + col] = newvalue
end
# This array maps from one-dimensional grid index to box number.
# It is used in the method below. The name BoxOfIndex begins with a
# capital letter, so this is a constant. Also, the array has been
# frozen, so it cannot be modified.
BoxOfIndex = [
0,0,0,1,1,1,2,2,2,0,0,0,1,1,1,2,2,2,0,0,0,1,1,1,2,2,2,
3,3,3,4,4,4,5,5,5,3,3,3,4,4,4,5,5,5,3,3,3,4,4,4,5,5,5,
6,6,6,7,7,7,8,8,8,6,6,6,7,7,7,8,8,8,6,6,6,7,7,7,8,8,8
].freeze
# This method defines a custom looping construct (an "iterator") for
# Sudoku puzzles. For each cell whose value is unknown, this method
# passes ("yields") the row number, column number, and box number to the
# block associated with this iterator.
def each_unknown
0.upto 8 do |row| # For each row
0.upto 8 do |col| # For each column
index = row*9+col # Cell index for (row,col)
next if @grid[index] != 0 # Move on if we know the cell's value
box = BoxOfIndex[index] # Figure out the box for this cell
yield row, col, box # Invoke the associated block
end
end
end
# Returns true if any row, column, or box has duplicates.
# Otherwise returns false. Duplicates in rows, columns, or boxes are not
# allowed in Sudoku, so a return value of true means an invalid puzzle.
def has_duplicates?
# uniq! returns nil if all the elements in an array are unique.
# So if uniq! returns something then the board has duplicates.
0.upto(8) {|row| return true if rowdigits(row).uniq! }
0.upto(8) {|col| return true if coldigits(col).uniq! }
0.upto(8) {|box| return true if boxdigits(box).uniq! }
false # If all the tests have passed, then the board has no duplicates
end
# This array holds a set of all Sudoku digits. Used below.
AllDigits = [1, 2, 3, 4, 5, 6, 7, 8, 9].freeze
# Return an array of all values that could be placed in the cell
# at (row,col) without creating a duplicate in the row, column, or box.
# Note that the + operator on arrays does concatenation but that the -
# operator performs a set difference operation.
def possible(row, col, box)
AllDigits - (rowdigits(row) + coldigits(col) + boxdigits(box))
end
private # All methods after this line are private to the class
# Return an array of all known values in the specified row.
def rowdigits(row)
# Extract the subarray that represents the row and remove all zeros.
# Array subtraction is set difference, with duplicate removal.
@grid[row*9,9] - [0]
end
# Return an array of all known values in the specified column.
def coldigits(col)
result = [] # Start with an empty array
col.step(80, 9) {|i| # Loop from col by nines up to 80
v = @grid[i] # Get value of cell at that index
result << v if (v != 0) # Add it to the array if non-zero
}
result # Return the array
end
# Map box number to the index of the upper-left corner of the box.
BoxToIndex = [0, 3, 6, 27, 30, 33, 54, 57, 60].freeze
# Return an array of all the known values in the specified box.
def boxdigits(b)
# Convert box number to index of upper-left corner of the box.
i = BoxToIndex[b]
# Return an array of values, with 0 elements removed.
[
@grid[i], @grid[i+1], @grid[i+2],
@grid[i+9], @grid[i+10], @grid[i+11],
@grid[i+18], @grid[i+19], @grid[i+20]
] - [0]
end
end # This is the end of the Puzzle class
# An exception of this class indicates invalid input,
class Invalid < StandardError
end
# An exception of this class indicates that a puzzle is over-constrained
# and that no solution is possible.
class Impossible < StandardError
end
#
# This method scans a Puzzle, looking for unknown cells that have only
# a single possible value. If it finds any, it sets their value. Since
# setting a cell alters the possible values for other cells, it
# continues scanning until it has scanned the entire puzzle without
# finding any cells whose value it can set.
#
# This method returns three values. If it solves the puzzle, all three
# values are nil. Otherwise, the first two values returned are the row and
# column of a cell whose value is still unknown. The third value is the
# set of values possible at that row and column. This is a minimal set of
# possible values: there is no unknown cell in the puzzle that has fewer
# possible values. This complex return value enables a useful heuristic
# in the solve() method: that method can guess at values for cells where
# the guess is most likely to be correct.
#
# This method raises Impossible if it finds a cell for which there are
# no possible values. This can happen if the puzzle is over-constrained,
# or if the solve() method below has made an incorrect guess.
#
# This method mutates the specified Puzzle object in place.
# If has_duplicates? is false on entry, then it will be false on exit.
#
def Sudoku.scan(puzzle)
unchanged = false # This is our loop variable
# Loop until we've scanned the whole board without making a change.
until unchanged
unchanged = true # Assume no cells will be changed this time
rmin,cmin,pmin = nil # Track cell with minimal possible set
min = 10 # More than the maximal number of possibilities
# Loop through cells whose value is unknown.
puzzle.each_unknown do |row, col, box|
# Find the set of values that could go in this cell
p = puzzle.possible(row, col, box)
# Branch based on the size of the set p.
# We care about 3 cases: p.size==0, p.size==1, and p.size > 1.
case p.size
when 0 # No possible values means the puzzle is over-constrained
raise Impossible
when 1 # We've found a unique value, so set it in the grid
puzzle[row,col] = p[0] # Set that position on the grid to the value
unchanged = false # Note that we've made a change
else # For any other number of possibilities
# Keep track of the smallest set of possibilities.
# But don't bother if we're going to repeat this loop.
if unchanged && p.size < min
min = p.size # Current smallest size
rmin, cmin, pmin = row, col, p # Note parallel assignment
end
end
end
end
# Return the cell with the minimal set of possibilities.
# Note multiple return values.
return rmin, cmin, pmin
end
# Solve a Sudoku puzzle using simple logic, if possible, but fall back
# on brute-force when necessary. This is a recursive method. It either
# returns a solution or raises an exception. The solution is returned
# as a new Puzzle object with no unknown cells. This method does not
# modify the Puzzle it is passed. Note that this method cannot detect
# an under-constrained puzzle.
def Sudoku.solve(puzzle)
# Make a private copy of the puzzle that we can modify.
puzzle = puzzle.dup
# Use logic to fill in as much of the puzzle as we can.
# This method mutates the puzzle we give it, but always leaves it valid.
# It returns a row, a column, and set of possible values at that cell.
# Note parallel assignment of these return values to three variables.
r,c,p = scan(puzzle)
# If we solved it with logic, return the solved puzzle.
return puzzle if r == nil
# Otherwise, try each of the values in p for cell [r,c].
# Since we're picking from a set of possible values, the guess leaves
# the puzzle in a valid state. The guess will either lead to a solution
# or to an impossible puzzle. We'll know we have an impossible
# puzzle if a recursive call to scan throws an exception. If this happens
# we need to try another guess, or re-raise an exception if we've tried
# all the options we've got.
p.each do |guess| # For each value in the set of possible values
puzzle[r,c] = guess # Guess the value
begin
# Now try (recursively) to solve the modified puzzle.
# This recursive invocation will call scan() again to apply logic
# to the modified board, and will then guess another cell if needed.
# Remember that solve() will either return a valid solution or
# raise an exception.
return solve(puzzle) # If it returns, we just return the solution
rescue Impossible
next # If it raises an exception, try the next guess
end
# If we get here, then none of our guesses worked out
# so we must have guessed wrong sometime earlier.
raise Impossible
end
end
@vishless
Copy link

Please specify a sample input that could be used to illustrate its working?

@muhammetfaik
Copy link

Help!!!

16] pry(main)> Sudoku::Puzzle.new(81)
NoMethodError: undefined method `gsub!' for 81:Integer

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment