my-solutions/problems/0037-sudoku-solver/main.rb

170 lines
3.8 KiB
Ruby
Raw Normal View History

2022-12-11 17:50:16 +05:00
# @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