# [SOLUTION] Numeric Maze (#60)

Discussion in 'Ruby' started by Phil Tomson, Jan 2, 2006.

1. ### Phil TomsonGuest

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
def initialize val
@value = val
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

@value+2
end

def to_s
@value.to_s
end

def to_i
@value.to_i
end

end

class NumMazeSolver
MAX_DEPTH = 20
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

Phil Tomson, Jan 2, 2006