I wrote my first ruby program recently, a word-chain finder, and I
would like some hints on how to speed it up. When I looked for a
priority queue to use in the a-star search, I found that this problem
was also a ruby quiz last year. Anyway, I profiled my application with
require ‘profile’ and it says most of the time is spent in Array.each.
I’m looking mostly for ruby tips, not really algorithm tips, to the
extent that they can be separated. I use a regular a-star search where
the heuristic is number of characters different. I don’t do any
preprocessing, except store the dictionary entries in an array along
with other words of the same length. Anyway, the code is below and
also at:
http://www.people.virginia.edu/~kjl3d/dict.rb
http://www.people.virginia.edu/~kjl3d/PQ.rb
Thanks so much in advance!
--------dict.rb:
#!/usr/bin/ruby -w
require ‘PQ’
$apos = /'/
$cap = /^[A-Z]/
class Dictionary
def initialize
@dicts = Hash.new { |h, v| h[v] = [] }
IO.foreach(“american-english”) do |line|
line.chomp!
line.downcase!
if (!$apos.match(line) && !$cap.match(line) && line.length > 0)
@dicts[line.length] << line
end
end
end
def neighbors(word)
nbrs_same = []
for i in 0 … word.length
regex = Regexp.new(word[0…i] + “.” + word[(i+1)…-1])
nbrs_same << @dicts[word.length].find_all { |dword| regex =~ dword }
end
return nbrs_same.flatten!
end
end
class Node
attr_reader :value, :parent, :so_far, :estDist
def initialize(parent, estDistance, value, so_far)
@parent = parent
@estDist = estDistance
@value = value
@so_far = so_far
end
def getTotalH
return @so_far + @estDist
end
def getChain
return prependToChain(“”).chop!
end
def prependToChain(str)
str = value + " " + str
if (nil != parent)
return parent.prependToChain(str);
else
return str
end
end
end
class Finder
def initialize(dict)
@dict = dict;
@q = PQ.new { |obj| obj.getTotalH }
@already = Hash.new
end
def Finder.calcH(word1, goal)
sum = 0
if (word1.length < goal.length)
short = word1
long = goal
else
short = goal
long = word1
end
for i in 0 … short.length do
if (short[i] != long[i])
sum += 1
end
end
return sum + (long.length - short.length)
end
def getNeighbors(n, goal)
word_neighbors = @dict.neighbors(n.value)
arr = []
word_neighbors.each do |w|
if (!@already.has_key?(w))
@already[w] = 1
arr << Node.new(n, Finder.calcH(w,goal), w, n.so_far + 1)
end
end
return arr
end
def findNode(word1, goal)
val = Finder.calcH(word1,goal)
init = Node.new(nil, val, word1, 0)
@q.add(init)
while ((n = @q.get_smallest) != nil)
puts “checking #{n.getChain} t = #{n.getTotalH} h = #{n.estDist}”
return n if (n.value == goal)
neighbors = getNeighbors(n, goal)
neighbors.each do |nbr|
puts “\t” + nbr.value + " " + nbr.getTotalH.to_s
@q.add(nbr)
end
end
return nil
end
def findPath(word1, goal)
n = findNode(word1, goal)
# trace ancestors of n
if (n == nil)
puts "got nothing!"
else
puts n.getChain
end
end
end
if ($0 == FILE)
if (ARGV.length > 1)
dictionary = Dictionary.new
finder = Finder.new(dictionary)
finder.findPath(ARGV[0], ARGV[1])
else
puts “pass in 2 strings”
end
end
----------------PQ.rb:
class PQ
def initialize(&getMin)
puts “pass a block to PQ” if (getMin == nil)
@getMin = getMin
@storage = Hash.new { |hash, key| hash[key] = [] }
end
def add(node)
@storage[@getMin.call(node)].unshift(node)
end
def get_smallest
return nil if @storage.empty?
key, val = *@storage.min
result = val.shift
@storage.delete(key) if val.empty?
return result
end
end