Here is my solution that tries to make all possible arrangements and
displays solutions with the maximum number of overlaps.
Eric
Are you interested in on-site Ruby training that uses well-designed,
real-world, hands-on exercises? http://LearnRuby.com
====
Solution to Ruby Q. #149 provided by LearnRuby.com
The code to this solution can also be found at:
This solution tries to find all solutions and then selects those
with the maximum number of overlaps. Good words to try are
“absolvable” and “chincherinchee”. It is implemented with a
recursive search.
The Grid class is used to hold a grid where each cell contains a
character or, if it’s empty, nil. Furthermore, the grid keeps track
of how many overlaps there are in the word that spins within it,
since that’s the measure of how good the arrangement is.
class Grid
attr_reader :overlaps
def initialize
@grid = Array.new
@overlaps = 0
end
def
sub_array(position[0])[position[1]]
end
def []=(position, value)
sub_array(position[0])[position[1]] = value
end
def inc_overlaps
@overlaps += 1
end
def dec_overlaps
@overlaps -= 1
end
def to_s
@grid.map do |row|
if row
row.map { |c| c || ’ ’ }.join(‘’)
else
‘’
end
end.join(“\n”)
end
def dup
other = Grid.new
@grid.each_with_index do |row, i|
other.grid[i] = row.dup unless row.nil?
end
other.overlaps = @overlaps
other
end
Trim grid so it’s as small as possible. Assumes that filled areas
are contiguous, so that an empty row would not sit between
non-empty rows.
def trim!
@grid.delete_if { |row| row.nil? || row.all? { |c| c.nil? } }
trim_columns = empty_columns
@grid.each_index do |i|
@grid[i] = @grid[i][trim_columns…-1]
@grid[i].pop while @grid[i].last.nil?
end
self
end
protected
attr_reader :grid
attr_writer :overlaps
Utility method that returns the sub-array of @grid at index.
Since the sub-array may have not yet been created, will create it
if necessary.
def sub_array(index)
sub_array = @grid[index]
sub_array = @grid[index] = Array.new if sub_array.nil?
sub_array
end
returns the number of entries at the start of each sub-array that
are empty, so the grid can be “left-trimmed”.
def empty_columns
index = 0
index += 1 while @grid.all? { |row| row.nil? || row[index].nil? }
index
end
end
This module contains some convenience methods to move a position in
a direction, and to figure out a new direction when a right turn is
made.
module Direction
RightTurns = {
[0, 1] => [1, 0],
[1, 0] => [0, -1],
[0, -1] => [-1, 0],
[-1, 0] => [0, 1],
}
returns the rightwards direction
def self.rightwards
[0, 1]
end
def self.turn_right(direction)
RightTurns[direction]
end
def self.move(position, direction)
[position[0] + direction[0], position[1] + direction[1]]
end
end
Recursively tries placing characters on grid until the full word is
placed or a conflict is found. During the recursion we try two
options – continuing in same direction or making a right turn.
Results are accumulated in result parameter, which is an array of
Grids.
def find_loops(word, letter_index, grid, position, direction, result)
new_position = Direction.move(position, direction)
character_placed = false
one of three conditions – cell is empty, cell contains a letter
that matches the current word letter, cell contains a letter that
doesn’t match current word letter
if grid[new_position].nil?
# empty cell – put character in
grid[new_position] = word[letter_index, 1]
character_placed = true
elsif grid[new_position] == word[letter_index, 1]
# cell with matching character, increment overlap count
grid.inc_overlaps
else
# cell with non-matching character, quit searching this branch
return
end
have we placed the entire word; add to results, otherwise recurse
continuting in same direction and after making a right turn
if letter_index == word.size - 1
result << grid.dup
else
# try going in the same direction and also try making a right turn
find_loops(word, letter_index + 1, grid,
new_position, direction, result)
find_loops(word, letter_index + 1, grid,
new_position, Direction.turn_right(direction), result)
end
restore the grid or overlap count depending on earlier condition
if character_placed
grid[new_position] = nil
else
grid.dec_overlaps
end
end
This is the entry point to the loop-finding process. It places
first letter in a new grid and calls the recursive find_loops
methods.
def solve(word)
size = word.length
grid = Grid.new
starting position allows room for word to go leftwards or upwards
without bumping into left or top edges; assumes initially heading
rightwards and only right turns can be made
position = [size - 5, size - 4]
direction = Direction.rightwards
put first character at starting position
grid[position] = word[0, 1]
generate solutions in results parameter
results = []
find_loops(word, 1, grid, position, direction, results)
results
end
find the best results and display them
def display_best_results(results)
the best results have most overlaps, so sort so most overlaps
appear at front of array, and then use the first element’s
overlaps as best score
results = results.sort_by { |r| -r.overlaps }
best_score = results.first.overlaps
puts “Number of overlaps: #{best_score}”
puts “=” * 40
results.select { |r| r.overlaps == best_score }.each do |result|
puts result.trim!
puts “=” * 40
end
end
if $0 == FILE
if ARGV.size == 1
display_best_results(solve(ARGV[0].downcase))
else
$stderr.puts “Usage: #{$0} word”
exit 1
end
end