Problem 37: sudoku solver
This commit is contained in:
parent
f9aced1a77
commit
ca927211b5
2 changed files with 227 additions and 0 deletions
169
problems/37-sudoku-solver/main.rb
Normal file
169
problems/37-sudoku-solver/main.rb
Normal file
|
@ -0,0 +1,169 @@
|
||||||
|
# @param {Character[][]} board
|
||||||
|
# @return {Void}
|
||||||
|
# Do not return anything, modify board in-place instead.
|
||||||
|
def solve_sudoku(board)
|
||||||
|
# Find all possible options for each cell.
|
||||||
|
# Board may be modified.
|
||||||
|
# This pre-check greatly reduces the number of values to iterate over.
|
||||||
|
options = generate_options(board)
|
||||||
|
|
||||||
|
# Remember numbers that are already present
|
||||||
|
fixed_numbers = get_fixed_numbers(board)
|
||||||
|
|
||||||
|
i = 0
|
||||||
|
j = 0
|
||||||
|
|
||||||
|
while i <= 8
|
||||||
|
# Skip this cell
|
||||||
|
if fixed_numbers[i][j]
|
||||||
|
i, j = increment_indices(i, j)
|
||||||
|
next
|
||||||
|
end
|
||||||
|
|
||||||
|
# This cell is not solved
|
||||||
|
|
||||||
|
board[i][j] = options[i][j][0] if board[i][j] == '.'
|
||||||
|
|
||||||
|
# Find first good digit
|
||||||
|
while !cell_valid?(board, i, j) && options[i][j].include?(board[i][j])
|
||||||
|
board[i][j] = next_digit(board[i][j], options[i][j])
|
||||||
|
end
|
||||||
|
|
||||||
|
# Error somewhere else
|
||||||
|
if !options[i][j].include?(board[i][j])
|
||||||
|
board[i][j] = '.'
|
||||||
|
i, j = decrement_indices(i, j, fixed_numbers)
|
||||||
|
board[i][j] = next_digit(board[i][j], options[i][j])
|
||||||
|
# Looks good, move to the next cell
|
||||||
|
else
|
||||||
|
i, j = increment_indices(i, j)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns a 3-dimensional array with all possible values for each cell.
|
||||||
|
# Returns an empty array for cells with numbers.
|
||||||
|
# If there is only one option, then this value is written on the board.
|
||||||
|
def generate_options(board)
|
||||||
|
options = []
|
||||||
|
|
||||||
|
board.each_with_index do |row, i|
|
||||||
|
options.push([])
|
||||||
|
row.each_with_index do |cell, j|
|
||||||
|
options[i].push([])
|
||||||
|
|
||||||
|
# Do not generate options for filled cells
|
||||||
|
next unless cell == '.'
|
||||||
|
|
||||||
|
# Find values from the corresponding row, column and square
|
||||||
|
row = board[i]
|
||||||
|
column = get_column(board, j)
|
||||||
|
square = get_square(board, i, j)
|
||||||
|
|
||||||
|
# Find options
|
||||||
|
9.times do |x|
|
||||||
|
char = (x + 1).to_s
|
||||||
|
# Add number to options if doesn't repeat
|
||||||
|
options[i][j].push(char) unless row.include?(char) || column.include?(char) || square.include?(char)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Only one option exists
|
||||||
|
if options[i][j].length == 1
|
||||||
|
board[i][j] = options[i][j].first
|
||||||
|
options[i][j] = []
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
options
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns 9x9 matrix with boolean values.
|
||||||
|
# Value is true if the corresponding number is already on the board.
|
||||||
|
def get_fixed_numbers(board)
|
||||||
|
fixed = []
|
||||||
|
board.each do |row|
|
||||||
|
fixed.push([])
|
||||||
|
row.each do |cell|
|
||||||
|
fixed.last.push(cell != '.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
fixed
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the indices of the next cell
|
||||||
|
def increment_indices(i, j)
|
||||||
|
return [i, j + 1] if j < 8
|
||||||
|
|
||||||
|
[i + 1, 0]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Returns the indices of the previous cell.
|
||||||
|
# Executes recursively until it finds a cell with no initial value.
|
||||||
|
def decrement_indices(i, j, fixed_numbers)
|
||||||
|
if j.positive?
|
||||||
|
j -= 1
|
||||||
|
else
|
||||||
|
j = 8
|
||||||
|
i -= 1
|
||||||
|
end
|
||||||
|
|
||||||
|
return decrement_indices(i, j, fixed_numbers) if fixed_numbers[i][j]
|
||||||
|
|
||||||
|
[i, j]
|
||||||
|
end
|
||||||
|
|
||||||
|
# Checks if this cell doesn't break the rules of sudoku
|
||||||
|
def cell_valid?(board, row_index, column_index)
|
||||||
|
# Check row
|
||||||
|
return false unless seq_valid?(board[row_index])
|
||||||
|
# Check column
|
||||||
|
return false unless seq_valid?(get_column(board, column_index))
|
||||||
|
# Check square
|
||||||
|
return false unless seq_valid?(get_square(board, row_index, column_index))
|
||||||
|
|
||||||
|
# Everything is ok
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
# Check if all values are unique (except for '.')
|
||||||
|
def seq_valid?(cells)
|
||||||
|
values = []
|
||||||
|
cells.each do |v|
|
||||||
|
return false if values.include?(v)
|
||||||
|
|
||||||
|
values.push(v) if v != '.'
|
||||||
|
end
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_column(board, column_index)
|
||||||
|
values = []
|
||||||
|
board.each do |row|
|
||||||
|
values.push(row[column_index])
|
||||||
|
end
|
||||||
|
values
|
||||||
|
end
|
||||||
|
|
||||||
|
def get_square(board, row_index, column_index)
|
||||||
|
# Calculate square location
|
||||||
|
a = row_index / 3 * 3
|
||||||
|
b = column_index / 3 * 3
|
||||||
|
|
||||||
|
# Get values
|
||||||
|
values = []
|
||||||
|
3.times do |x|
|
||||||
|
3.times do |y|
|
||||||
|
values.push(board[a + x][b + y])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
values
|
||||||
|
end
|
||||||
|
|
||||||
|
def next_digit(cell, options)
|
||||||
|
index = options.find_index(cell)
|
||||||
|
return cell.next if index + 1 == options.length
|
||||||
|
|
||||||
|
options[index + 1]
|
||||||
|
end
|
58
problems/37-sudoku-solver/test.rb
Normal file
58
problems/37-sudoku-solver/test.rb
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
require_relative 'main'
|
||||||
|
require 'test/unit'
|
||||||
|
|
||||||
|
class TestSudokuSolver < Test::Unit::TestCase
|
||||||
|
def test_simple_sudoku
|
||||||
|
input = [
|
||||||
|
%w[5 3 . . 7 . . . .],
|
||||||
|
%w[6 . . 1 9 5 . . .],
|
||||||
|
%w[. 9 8 . . . . 6 .],
|
||||||
|
%w[8 . . . 6 . . . 3],
|
||||||
|
%w[4 . . 8 . 3 . . 1],
|
||||||
|
%w[7 . . . 2 . . . 6],
|
||||||
|
%w[. 6 . . . . 2 8 .],
|
||||||
|
%w[. . . 4 1 9 . . 5],
|
||||||
|
%w[. . . . 8 . . 7 9]
|
||||||
|
]
|
||||||
|
output = [
|
||||||
|
%w[5 3 4 6 7 8 9 1 2],
|
||||||
|
%w[6 7 2 1 9 5 3 4 8],
|
||||||
|
%w[1 9 8 3 4 2 5 6 7],
|
||||||
|
%w[8 5 9 7 6 1 4 2 3],
|
||||||
|
%w[4 2 6 8 5 3 7 9 1],
|
||||||
|
%w[7 1 3 9 2 4 8 5 6],
|
||||||
|
%w[9 6 1 5 3 7 2 8 4],
|
||||||
|
%w[2 8 7 4 1 9 6 3 5],
|
||||||
|
%w[3 4 5 2 8 6 1 7 9]
|
||||||
|
]
|
||||||
|
solve_sudoku(input)
|
||||||
|
assert_equal(output, input)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_hard_sudoku
|
||||||
|
input = [
|
||||||
|
%w[. . . . . 7 . . 9],
|
||||||
|
%w[. 4 . . 8 1 2 . .],
|
||||||
|
%w[. . . 9 . . . 1 .],
|
||||||
|
%w[. . 5 3 . . . 7 2],
|
||||||
|
%w[2 9 3 . . . . 5 .],
|
||||||
|
%w[. . . . . 5 3 . .],
|
||||||
|
%w[8 . . . 2 3 . . .],
|
||||||
|
%w[7 . . . 5 . . 4 .],
|
||||||
|
%w[5 3 1 . 7 . . . .]
|
||||||
|
]
|
||||||
|
output = [
|
||||||
|
%w[3 1 2 5 4 7 8 6 9],
|
||||||
|
%w[9 4 7 6 8 1 2 3 5],
|
||||||
|
%w[6 5 8 9 3 2 7 1 4],
|
||||||
|
%w[1 8 5 3 6 4 9 7 2],
|
||||||
|
%w[2 9 3 7 1 8 4 5 6],
|
||||||
|
%w[4 7 6 2 9 5 3 8 1],
|
||||||
|
%w[8 6 4 1 2 3 5 9 7],
|
||||||
|
%w[7 2 9 8 5 6 1 4 3],
|
||||||
|
%w[5 3 1 4 7 9 6 2 8]
|
||||||
|
]
|
||||||
|
solve_sudoku(input)
|
||||||
|
assert_equal(output, input)
|
||||||
|
end
|
||||||
|
end
|
Loading…
Reference in a new issue