Ok, I'm pretty sure that it's been over 48 hours, so here's my solution,
sudoku_solve.rb:
#!/usr/bin/env ruby
#
# =Description
#
# Solves a Su Doku puzzle. Prints the solution to stdout.
#
# =Usage
#
# sudoku_solve.rb <puzzle.txt>
#
# puzzle.txt is a text file containing a sudoku puzzle in the following format:
#
# +-------+-------+-------+
# | _ _ 2 | _ _ 5 | _ 7 9 |
# | 1 _ 5 | _ _ 3 | _ _ _ |
# | _ _ _ | _ _ _ | 6 _ _ |
# +-------+-------+-------+
# | _ 1 _ | 4 _ _ | 9 _ _ |
# | _ 9 _ | _ _ _ | _ 8 _ |
# | _ _ 4 | _ _ 9 | _ 1 _ |
# +-------+-------+-------+
# | _ _ 9 | _ _ _ | _ _ _ |
# | _ _ _ | 1 _ _ | 3 _ 6 |
# | 6 8 _ | 3 _ _ | 4 _ _ |
# +-------+-------+-------+
#
# Characters '-', '+', '|', and whitespace are ignored, and thus optional. The
# file just has to have 81 characters that are either numbers or other
# printables besides the above mentioned. Any non-numeric character is
# considered a blank (unsolved) grid entry.
#
# The puzzle can also be passed in via stdin, e.g.:
# cat puzzle.txt | sudoku_solve.rb
require 'rdoc/usage'
#==============================================================================
# ----- Classes -----
#==============================================================================
class UnsolvableException < Exception
end
# Represents one grid space. Holds known value or list of candidate values.
class Space
def initialize(num = nil)
@value = num
@cands = num ? [] : [1, 2, 3, 4, 5, 6, 7, 8, 9]
end
def value() @value end
def value=(val) @value = val; @cands.clear end
def remove_cand(val) @cands.delete(val) end
def cand_size() @cands.size end
def first_cand() @cands[0] end
end
# Represents puzzle grid. Grid has 81 spaces, composing 9 rows, 9 columns, and
# 9 "squares":
#
# Colums
# Spaces 012 345 678
#
# 0 1 2| 3 4 5| 6 7 8 0 | |
# 9 10 11|12 13 14|15 16 17 1 0 | 1 | 2 <- Squares
# 18 19 20|21 22 23|24 25 26 2 | | |
# --------+--------+-------- R ---+---+--- |
# 27 28 29|30 31 32|33 34 35 o 3 | | |
# 36 37 38|39 40 41|42 43 44 w 4 3 | 4 | 5 <----+
# 45 46 47|48 49 50|51 52 53 s 5 | | |
# --------+--------+-------- ---+---+--- |
# 54 55 56|57 58 59|60 61 62 6 | | |
# 63 64 65|66 67 68|69 70 71 7 6 | 7 | 8 <----+
# 72 73 74|75 76 77|78 79 80 8 | |
class Board
# Stores which spaces compose each square
@@squares = []
@@squares[0] = [ 0, 1, 2, 9, 10, 11, 18, 19, 20].freeze
@@squares[1] = [ 3, 4, 5, 12, 13, 14, 21, 22, 23].freeze
@@squares[2] = [ 6, 7, 8, 15, 16, 17, 24, 25, 26].freeze
@@squares[3] = [27, 28, 29, 36, 37, 38, 45, 46, 47].freeze
@@squares[4] = [30, 31, 32, 39, 40, 41, 48, 49, 50].freeze
@@squares[5] = [33, 34, 35, 42, 43, 44, 51, 52, 53].freeze
@@squares[6] = [54, 55, 56, 63, 64, 65, 72, 73, 74].freeze
@@squares[7] = [57, 58, 59, 66, 67, 68, 75, 76, 77].freeze
@@squares[8] = [60, 61, 62, 69, 70, 71, 78, 79, 80].freeze
@@squares.freeze
# Takes a string containing the text of a valid puzzle file as described in
# the Usage comment at the top of this file
def initialize(grid = nil)
@spaces = Array.new(81) { |n| Space.new }
if grid
count = 0
chars = grid.split(//).delete_if { |c| c =~ /[\+\-\|\s]/ }
chars.each do |c|
set(count, c.to_i) if c =~ /\d/
count += 1
break if count == 81
end
end
end
def set(idx, val)
@spaces[idx].value = val
adjust_cands_from(idx)
end
# Remove indicated space's value from candidates of all spaces in its
# row/col/square
def adjust_cands_from(sidx)
val = @spaces[sidx].value
row_each(which_row(sidx)) do |didx|
@spaces[didx].remove_cand(val)
end
col_each(which_col(sidx)) do |didx|
@spaces[didx].remove_cand(val)
end
square_each(which_square(sidx)) do |didx|
@spaces[didx].remove_cand(val)
end
end
# Return number of row/col/square containing the given space index
def which_row(idx) idx / 9 end
def which_col(idx) idx % 9 end
def which_square(idx)
@@squares.each_with_index { |s, n| return n if s.include?(idx) }
end
# Yield each space index in the row/col/square indicated by number
def row_each(row) ((row * 9)...((row + 1) * 9)).each { |n| yield(n) } end
def col_each(col) 9.times { yield(col); col += 9 } end
def square_each(squ) @@squares[squ].each { |n| yield(n) } end
def solved?() @spaces.all? { |sp| sp.value } end
# For each empty space that has only one candidate, set the space's value to
# that candidate and update all related spaces. Repeat process until no
# empty spaces with only one candidate remain
def deduce_all
did = true
while did
did = false
@spaces.each_index do |idx|
sp = @spaces[idx]
raise UnsolvableException if ((!sp.value) && sp.cand_size == 0)
if (sp.cand_size == 1)
sp.value = sp.first_cand
adjust_cands_from(idx)
did = true
end
end
end
end
def to_s
div = "+-------+-------+-------+\n"
ret = "" << div
@spaces.each_index do |idx|
ret << "|" if (idx % 9 == 0)
ret << " " + (@spaces[idx].value || '_').to_s
ret << " |" if (idx % 3 == 2)
ret << "\n" if (idx % 9 == 8)
ret << div if ([26, 53, 80].include?(idx))
end
ret
end
def solve()
# Solve
deduce_all
return if solved?
# Find an unsolved space with the fewest candidate values and store its
# index and first candidate
min_count = nil
test_idx = nil
@spaces.each_with_index do |sp, n|
if !sp.value
if (!min_count) || (sp.cand_size < min_count)
test_idx, min_count = n, sp.cand_size
end
end
end
test_cand = @spaces[test_idx].first_cand
# Solve clone of board in which the value of the space found above is
# set to it's first candidate value
str = ""
@spaces.each_index do |idx|
str << (idx == test_idx ? test_cand.to_s :
(@spaces[idx].value || '_').to_s)
end
b_clone = Board.new(str)
begin
b_clone.solve
initialize(b_clone.to_s) # Take state from clone
rescue UnsolvableException
@spaces[test_idx].remove_cand(test_cand)
solve
end
end
end
#==============================================================================
# ----- Script start -----
#==============================================================================
b_str = ARGF.readlines().join()
board = Board.new(b_str)
begin
board.solve()
puts board.to_s
rescue UnsolvableException
puts "This puzzle has no solution!"
end
--
Karl von Laudermann - karlvonl(a)rcn.com -
http://www.geocities.com/~karlvonl
#!/usr/bin/env ruby
require "complex";w=39;m=2.0;w.times{|y|w.times{|x|c=Complex.new((m*x/w)-1.5,
(2.0*y/w)-1.0);z=c;e=false;49.times{z=z*z+c;if z.abs>m then e=true;break;end}
print(e ?" ":"@@");puts if x==w-1;}}