[SUMMARY] Word Search (#107)

R

Ruby Quiz

I had a few moments this weekend and sat down to work this problem. I was quite
surprised to find it tougher than I had expected. I fiddled around for about an
hour trying to find a solution I felt was elegant, but never really got there.
Luckily, we have many submitters smarter than me.

Almost all of the solutions this week took different approaches and they are all
quite interesting. This doesn't seem to be one of those problems we have a
standard approach for. Some solutions did a boring search using X and Y
coordinates; others started by building a map of points in the puzzle to letters
or even letters to points; one even works by performing board transformations.
I'm going to show the basic X and Y search in this summary, but the other method
were equally interesting and well worth a look.

Let's talk about the solution from Bob Showalter, which begins like this:

class WordSearch

class Board < Array

def to_s
collect {|s| s.split(//).join(' ')}.join("\n")
end

end

# ...

Here we see an Array subclass with a trivial modification. Board objects are
just two-dimensional Arrays that know how to draw themselves in the quiz output
format.

Let's move on to the setup code:

# ...

attr_reader :board, :solution

# creates a new, empty solver
def initialize
@board = Board.new
@solution = Board.new
end

# resets the solution
def reset
@solution.clear
@board.each {|row| @solution << row.gsub(/./, '+')}
end

# ...

Here we have the initialization for two Board objects. One will hold the actual
board, or puzzle object, and the other the solution-in-progress. reset() gives
us hints at the solution object structure, which is cleared to a board full of
plus characters.

The board is loaded with the following code:

# ...

# checks that the board contains only letters and that it has a uniform
# rectangular shape
def validate
@board.size > 0 or raise "Board has no rows"
@board.grep(/[^A-Z]/).empty? or raise "Board contains non-letters"
w = @board.collect {|row| row.size}.uniq
w.size == 1 or raise "Board rows are not all the same length"
w.first > 0 or raise "Board has no columns"
end

# parses the board by reading lines from io until a blank line (or EOF)
# is read.
def parse(io = ARGV)
@board.clear
while line = io.gets
line = line.strip.upcase
break if line == ''
@board << line
end
validate
reset
end

# ...

validate() is just a reality check for the board. It verifies that we have some
rows, those rows contain letters, there are columns in each row, and in fact the
same number of columns. That check is run towards the end of parse(), which
just reads line by line filling the board object. Note that reset() is also
called to prepare the solution object.

The real work comes in the methods that search for words:

# ...

# search for word. returns number of times found. solution is updated
# with all occurences.
def search(word)
found = 0
0.upto(board.size-1) do |y|
0.upto(board[y].size-1) do |x|
[-1, 0, 1].each do |dy|
[-1, 0, 1].each do |dx|
next if dx == 0 and dy == 0
found += 1 if search_for(word.strip.upcase, x, y, dx, dy)
end
end
end
end
found
end

# search for word in board starting at position (x,y) and moving in
# direction (dx,dy). returns true if found, false if not found.
def search_for(word, x, y, dx, dy)
return false if x < 0 or
x >= board.first.size or
y < 0 or
y >= board.size
return false if board[y][x] != word[0]
prev = solution[y][x]
solution[y][x] = board[y][x]
return true if word.length <= 1
found = search_for(word[1,word.length-1], x + dx, y + dy, dx, dy)
solution[y][x] = prev unless found
found
end

# ...

The search() method manages the hunt for a single term. It's a trivial
brute-force find from every starting square in all eight directions. The method
maintains and returns a count, for all occurrences found during the search. The
actual letter match is handed off to search_for() though.

In search_for() a recursive search is used to find letter by letter. The tricky
part here is that the solution object is modified as we search, assuming we will
find the word. This means that an extra walk isn't needed to update the
solution later, but it also means the code must revert the changes to the
solution object if the word isn't found. Those are the gymnastics you see in
the second half of the method.

The next two methods wrap an interface around these calls:

# ...

# creates a new puzzle by parsing the board from io. see
# WordSearch#parse
def self.parse(io = ARGF)
obj = new
obj.parse(io)
obj
end

def to_s
solution.to_s
end

end

# ...

The class method parse() constructs an object and parses the board from the
passed IO object. The instance method to_s() can then be used to show final
results, after one or more calls to search().

The solution ends with code to kick-off the process:

# ...

# parse the board first
p = WordSearch.parse

# parse the words until a blank line is read
words = []
while line = ARGF.gets
line = line.strip.upcase
break if line == ''
words += line.gsub(',', ' ').split
end

# submit each word and show how many times it was found
for word in words.sort.uniq
n = p.search(word)
puts word + ' was ' + (n == 0 ? 'not found' :
n == 1 ? 'found once' :
"found #{n} times")
end

# show the solution
puts p

Here we see the WordSearch object constructed and the board parsed out of the
input. The remainder of the input is divided into words and search() is called
for each one. A found count is printed for each word as the search is made,
then the final solution is displayed.

+ + + M + + + + + +
+ + + + Y + + + + +
T H A N K S + + + +
O + + + + + + + + +
+ L L A + T H E + +
+ + + + C + + + + +
+ + S O L V E R S +
+ + + + e + + + + +
+ + + + V + + + + +
+ + + + E + + + + +
+ + + + R + + + + +

Tomorrow we have a new word game, so stay tuned...
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,007
Latest member
obedient dusk

Latest Threads

Top