Here is my solution.
It uses breadth first search (unidirectional) and is quite fast.
I use a trick to find all possible steps fast: While reading the words I =
=20
store them in a hash of arrays, e.g. when adding the word "dog", I =20
register "dog" as step for "\0og", "d\0g" and "do\0", later while look fo=
r =20
a step for "dot" I will use all words registered for "\0ot", "d\0t" or =20
"do\0". So reading the dictionary takes some time (and memory), but =20
afterwards finding the shortest chains is really fast. I also use symbols=
=20
instead of strings in some places for faster hash-lookup.
Of course I have also implemented my extra bonus challenge, to find a =20
longest shortest chain and Simon's suggestion to allow words with =20
different length.
Some examples:
$ time ruby word_chain.rb duck ruby
duck
luck
luce
lube
rube
ruby
real 0m1.414s
user 0m1.326s
sys 0m0.028s
$ time ruby word_chain.rb ape human
ape
are
arm
aum
hum
huma
human
real 0m2.604s
user 0m2.484s
sys 0m0.052s
$ time ruby word_chain.rb computer a
computer
compute
compote
compose
compos
compo
campo
camp
cam
aam
aa
a
real 0m12.550s
user 0m11.890s
sys 0m0.232s
The examples with words of different length take longer because much more=
=20
words from the dictionary have to be added to the hash.
$ time ruby longest_shortest.rb 3
edh
edo
ado
aho
pho
phi
poi
poa
pya
mya
real 0m4.840s
user 0m4.615s
sys 0m0.055s
$ time ruby longest_shortest.rb 6
zounds
wounds
woundy
[49 lines]
pantry
paltry
peltry
real 1m12.614s
user 1m10.113s
sys 0m0.655s
$ time ruby longest_shortest.rb 9
unpausing
unpassing
unpasting
unwasting
unwaiting
unwailing
unfailing
unfalling
unfilling
untilling
untelling
unselling
unsealing
unhealing
unhearing
unwearing
unweaving
unreaving
unreeving
unreeling
unfeeling
unfeeding
unheeding
real 0m9.252s
user 0m8.836s
sys 0m0.158s
$ time ruby longest_shortest.rb 12
modification
codification
conification
sonification
sinification
vinification
vilification
bilification
real 0m7.492s
user 0m7.127s
sys 0m0.138s
Dominik
The code:
# word_chain.rb
####################
DEFAULT_DICTIONARY =3D "/usr/share/dict/words"
# Data structure that efficiently stores words from a dictionary in a way=
, =20
that
# it is easy to find all words that differ from a given word only at one
# letter (words that could be the next step in a word chain).
# Example: when adding the word "dog", add_word will register "dog" as =20
step for
# "\0og", "d\0g" and "do\0", later each_possible_step("cat") will yield =20
all words
# registered for "\0at", "c\0t" or "ca\0".
class WordSteps
def initialize
@steps =3D Hash.new { |h, k| h[k] =3D [] }
@words =3D {}
end
# yields all words (as strings) that were added with add_word
def each_word(&block)
@words.each_key(&block)
end
# add all steps for word (a string) to the steps
def add_word(word)
sym =3D word.to_sym
wdup =3D word.dup
for i in 0...word.length
wdup
=3D 0
@steps[wdup] << sym
wdup =3D word
end
@words[word] =3D sym # for allow_shorter and each_word
end
# yields each possible next step for word (a string) as symbol, some
# possible steps might be yielded multiple times
# if allow_shorter is true, word[0..-2].to_sym will also be yielded =
if
# available
# if allow_longer is true, all words that match /#{word}./ will be =20
yielded
def each_possible_step(word, allow_shorter =3D false, allow_longer =3D=
=20
false)
wdup =3D word.dup
for i in 0...word.length
wdup =3D 0
if @steps.has_key?(wdup)
@steps[wdup].each { |step| yield step }
end
wdup =3D word
end
if allow_shorter && @words.has_key?(tmp =3D word[0..-2])
yield @words[tmp]
end
if allow_longer && @steps.has_key?(tmp =3D word + "\0")
@steps[tmp].each { |step| yield step }
end
end
# tries to find a word chain between word1 and word2 (strings) using=
=20
all
# available steps
# returns the chain as array of symbols or nil, if no chain is found
# shorter/longer determines if shorter or longer words are allowed i=
n =20
the
# chain
def build_word_chain(word1, word2, shorter =3D false, longer =3D fal=
se)
# build chain with simple breadth first search
current =3D [word1.to_sym]
pre =3D { current[0] =3D> nil } # will contain the predecessors
target =3D word2.to_sym
catchdone) do
until current.empty?
next_step =3D []
current.each do |csym|
each_possible_step(csym.to_s, shorter, longer) do =20
|ssym|
# have we seen this word before?
unless pre.has_key? ssym
pre[ssym] =3D csym
throwdone) if ssym =3D=3D target
next_step << ssym
end
end
end
current =3D next_step
end
return nil # no chain found
end
# build the chain (in reverse order)
chain =3D [target]
chain << target while target =3D pre[target]
chain.reverse
end
# builds and returns a WordSteps instance "containing" all words wit=
h
# length in length_range from the file file_name
def self.load_from_file(file_name, length_range)
word_steps =3D new
IO.foreach(file_name) do |line|
# only load words with correct length
if length_range =3D=3D=3D (word =3D line.strip).length
word_steps.add_word(word.downcase)
end
end
word_steps
end
end
if $0 =3D=3D __FILE__
dictionary =3D DEFAULT_DICTIONARY
# parse arguments
if ARGV[0] =3D=3D "-d"
ARGV.shift
dictionary =3D ARGV.shift
end
unless ARGV.size =3D=3D 2
puts "usage: #$0 [-d path/to/dictionary] word1 word2"
exit 1
end
word1, word2 =3D ARGV[0].strip.downcase, ARGV[1].strip.downcase
shorter =3D word1.length > word2.length
longer =3D word1.length < word2.length
length_range =3D if longer
word1.length..word2.length
else
word2.length..word1.length
end
# read dictionary
warn "Loading dictionary..." if $DEBUG
word_steps =3D WordSteps.load_from_file(dictionary, length_range)
word_steps.add_word(word2) # if it is not in dictionary
# build chain
warn "Building chain..." if $DEBUG
chain =3D word_steps.build_word_chain(word1, word2, shorter, longer)
# print result
puts chain || "No chain found!"
end
# longest_shortest.rb
####################
require "word_chain"
class WordSteps
# builds a longest shortest chain starting with word and returns it =
as
# array of symbols
# if the found chain is shorter than old_max, then it is possible to
# determine other words, whose longest shortest chain will also be n=
ot
# longer than old_max, those words will be added to not_longer, that=
=20
way
# the caller can avoid searching a longest chain for them
def longest_word_chain(word, not_longer =3D {}, old_max =3D 0)
# build chain with simple breadth first search until no more ste=
ps =20
are
# possible, one of the last steps is then a longest shortest cha=
in
current =3D [word.to_sym]
pre =3D { current[0] =3D> nil } # will contain the predecessors
target =3D nil
iterations =3D []
loop do
next_step =3D []
iterations << current
current.each do |csym|
each_possible_step(csym.to_s) do |ssym|
unless pre.has_key? ssym
pre[ssym] =3D csym
next_step << ssym
end
end
end
if next_step.empty?
target =3D current[0]
break
else
current =3D next_step
end
end
# build the chain (in reverse order)
chain =3D [target]
chain << target while target =3D pre[target]
# add words to not_longer if possible (see above)
if chain.size < old_max
(0...([old_max+1-chain.size, iterations.size].min)).each do =
|i|
iterations.each { |w| not_longer[w] =3D true }
end
end
chain.reverse
end
end
if $0 =3D=3D __FILE__
dictionary =3D DEFAULT_DICTIONARY
# parse arguments
if ARGV[0] =3D=3D "-d"
ARGV.shift
dictionary =3D ARGV.shift
end
word_length =3D [ARGV.shift.to_i, 1].max
# read dictionary
warn "Loading dictionary..." if $DEBUG
word_steps =3D WordSteps.load_from_file(dictionary, word_length)
# search chain
warn "Searching longest chain..." if $DEBUG
max =3D 0
chain =3D nil
not_longer =3D {}
word_steps.each_word { |w|
next if not_longer[w.to_sym]
cur =3D word_steps.longest_word_chain(w, not_longer, max)
if cur.size > max
chain =3D cur
max =3D cur.size
warn chain.inspect if $DEBUG
end
}
# print result
puts chain || "No chain found!"
end