ptkwt
1/2/2006 3:46:00 AM
I've always wanted to participate in a Ruby quiz and now I finally had some
time... this is (sad to say) my first submission.
I wasted a lot of time trying to get a recursive solution to work but stack
overruns kept foiling that effort, so finally I went with this iterative
approach. It seems to work OK, but it's quite slow on larger problems. I
finally added a max_path cheat (see code below) to help out some - however, I
didn't get around to making max_path dynamic (see comment in code below) -
though I have a sneaking suspicion that there is some theorem out there
somewhere for determining the upper bound on the length of the search given
the absolute value of the difference between start and end.
Phil
##########################################################################
class Integer
def odd?
self%2 == 1
end
def even?
self%2 == 0
end
end
class Value
include Comparable
attr_reader :value
attr_reader :ops
def initialize val
@value = val
@ops = [:double, :add_two, :halve]
end
def <=> other
case other
when Value
@value <=> other.value
when Numeric
@value <=> other
end
end
def has_ops?
! @ops.empty?
end
def next_op
@ops.pop
end
def each_op
@ops.each {|op|
yield op
}
end
def do_op op
self.send op
end
#ops
def double
@value*2
end
def halve
@value/2
end
def add_two
@value+2
end
def to_s
@value.to_s
end
def to_i
@value.to_i
end
end
class NumMazeSolver
MAX_DEPTH = 20
attr_reader :solution
def initialize(start, finish)
@start,@finish = start,finish
@val_list = []
@solution = nil
# Set max value that a given solution will contain:
# -> if the finish value is the largest there is no need to go
# beyond finish*2 in any search
# -> if the start value is the largest there is no need to go
# beyond start*3 in any search:
@largest = finish > start ? finish*2+2 : start*3
#NOTE: max_depth should be dynamic:
#either:
#1) it should be increased if a solution was not found(and employ
memoization)
#2) it should be determined mathematically from the absolute difference of
# start and finish (I suspect this would be possible, just not sure how
# to do it - there must be a theorem somewhere (?))
@max_depth = MAX_DEPTH
end
def solve start=@start, finish=@finish
#handle the trivial case:
if start == finish
@solution = [start]
return
end
first = Value.new(start)
@val_list << first
prev_op= first.ops.last
while !(@val_list.empty?) && @val_list.last.has_ops?
next_op = @val_list.last.next_op
unless (next_op == :halve && @val_list.last.to_i.odd?) || (next_op == :halve && prev_op == :double) || (next_op == :double && prev_op ==:halve)
new_val = @val_list.last.send(next_op)
#ensure there are no cycles before adding new_val to val_list:
#NOTE: I suspect we're spending a lot of time in find
if new_val < (@largest) && !(@val_list.find{|v| v.to_i == new_val})
@val_list << ( Value.new(new_val) )
end
if new_val == finish
puts "Found a solution, length is: #{@val_list.length}" if $DEBUG
@solution ||= @val_list.clone #first time
if @solution.size > @val_list.size
@solution = @val_list.clone #take a snapshot
puts "new best solution: [ #{@solution.map{|v| v.to_i}.join(",")}
] length: #{@solution.length}" if @solution && $DEBUG
end
dest = @val_list.pop
end
end
if (@solution && @val_list.size >= @solution.size ) || @val_list.size >
@max_depth
#A solution already exists which is shorter (or max_depth reached)
#no need to go any further on this branch, prune the search
p = @val_list.pop
end
back_track #take values with empty ops off the list
prev_op = next_op
end #while
end
# back_track: clear out entries with empty ops list
def back_track
while @val_list.last && !@val_list.last.has_ops?
poppedval = @val_list.pop
end
end
def to_s
@solution.map{|v| v.to_s }.join(',')
end
end
if $0 == __FILE__
require 'benchmark'
include Benchmark
bm(6) do |x|
#s = Solver.new(2,9)
puts "9 -> 2"
s =NumMazeSolver.new(9,2)
x.report("9->2") {s.solve}
puts s.solution.map{|v| v.to_i }.join(",")
puts "2 -> 9"
s =NumMazeSolver.new(2,9)
x.report("2->9") {s.solve}
puts s.solution.map{|v| v.to_i }.join(",")
s =NumMazeSolver.new(1,25)
x.report("1->25") {s.solve }
puts s.solution.map{|v| v.to_i }.join(",")
#this one takes a while on my slow machine...
s =NumMazeSolver.new(22,999)
x.report("22->999") {s.solve }
puts s.solution.map{|v| v.to_i }.join(",")
end
end