[QUIZ] Numeric Maze (#60)

Simon wrote:

Hello dear quizzers,

could someone please confirm (or disprove) that this i correct?

my subproblem solution to (868056,651040) gave the same result as your,
in 76 secs.

I investigated the ideas suggested by Ilmari and Greg, but found that
there is no way of knowing if the result is optimal. The result are
impressing, but misses often trivial cases. It seems as hard to patch
the non optimal solution as it is to solve it normally.

Are you using a different approach?

Christer

···

--
Posted via http://www.ruby-forum.com/\.

Actually this seems to be a better view of the pattern.
Notice that the leftmost bits are coaxed into matching the leftmost bits of
the solution first, and then work to the right.

[snip 8 solutions]

I like that view.

Does that mean you can go from the source to any prefix of the target? The
shortest route from a prefix to the target is known, even though you show two
solutions near the end (for an odd number; their length is equal and both
follow a strict recipe).

If so, I'm sure it helps when the target is bigger than the source (or more
precisely, when the target is bigger than an intermediate number) and it
does not hurt when the target is smaller. However, I doubt it beats the
approach where searching starts from both source and target (as I saw at
least once, today), unless the target is significantly bigger (i.e. the
route from the prefix to the target is longer than from the source to the
prefix).

I do not see any shortcuts from the source to the prefix.
Anybody see a way to prune that?

Kero wrote:

Here it is, my solution. From mathematical and algorithmic point of
view, you won't find anything new.

Kero, this is really the quickest solution so far, passing my little
test suite. Before you, Ryan Sobol, was the leader, clocking in at 2.376
secs with #60.20 on the Quiz list. Your #60.22, clocks in at 1.972
seconds, a decrease by 17%.

I'm challenging every quizzer: There is more to fetch!

By refactoring Kero's code, I was able to squeeze the execution time to
0.37 seconds, that's another 80%.

I'm not submitting this code, as I'm sure there are more improvements to
be done.

This is my test suite:

solve(2,9), [2,4,8,16,18,9]
solve(22,99).size, 8
solve(222,999).size, 16
solve(2222,9999).size, 25
solve(22222,99999).size, 36

Christer Nilsson

···

--
Posted via http://www.ruby-forum.com/\.

0x002A@gmail.com wrote:

now without :* but much shorter..

Still doesn't solve (222,999) correctly. One step too much.

222 224 112 56 58 60 62 124 248 496 498 996 998 1996 1998 999
expected but was
222 111 113 115 117 119 121 123 246 248 496 498 996 1992 1994 997 999

Christer

···

--
Posted via http://www.ruby-forum.com/\.

Jeffrey Dik wrote:

Heh, I couldn't figure out how to get a negative number using double,
halve,
and add_two from the number 1.

You are correct Jeffrey, see the matrix in my last reply. Starting with
a positive number, you can only reach positive numbers.

This quiz should be interesting enough, using only positive numbers, as
any positive number can be reached from any positive number.

Christer Nilsson

···

--
Posted via http://www.ruby-forum.com/\.

(It's specified that you can't halve odd numbers)

m.s.

···

On Dec 30, 2005, at 23:37, Jim Freeze wrote:

On 12/30/05, Christer Nilsson <janchrister.nilsson@gmail.com> wrote:

J. Ryan Sobol wrote:

Would it be appropriate to post my potential answers (not code) to
this question?

Some examples:

solve(1, 1) # => [1]

Is this a degenerate case? If to go from a to b in (a,b) through
transformation x,y or z, wouldn't there be two possible shortest solutions:

  [1,2,1]
  [1,0.5,1]

Jim Freeze wrote:

solve(1, 1) # => [1]

Is this a degenerate case? If to go from a to b in (a,b) through
transformation x,y or z, wouldn't there be two possible shortest
solutions:

  [1,2,1]
  [1,0.5,1]

Only positive integers are allowed.
That means you can't divide odd numbers with two (operation halve)

If start equals target, zero steps are needed.

Christer Nilsson

···

--
Posted via http://www.ruby-forum.com/\.

J. Ryan Sobol wrote:

Would it be appropriate to post my potential answers (not code) to
this question?

Some examples:

solve(1, 1) # => [1]

Is this a degenerate case? If to go from a to b in (a,b) through
transformation x,y or z, wouldn't there be two possible shortest solutions:

  [1,2,1]
  [1,0.5,1]

Hmm, let's go back to the quiz and see if we can find an answer:

···

On Dec 30, 2005, at 5:37 PM, Jim Freeze wrote:

On 12/30/05, Christer Nilsson <janchrister.nilsson@gmail.com> wrote:

On Dec 30, 2005, at 7:37 AM, Ruby Quiz wrote:

You have a set of three operations:

  double
  halve (Odd numbers cannot be halved.)
  add_two

Problem: Move from the starting point to the target, minimizing the number of
operations.

The key phrase seems to be "minimizing the number of operations". I don't think we can get any smaller than zero and I don't see anything disallowing it, so I imagine it's fine.

Just my opinion on an obviously debatable topic.

James Edward Gray II

Are negative numbers and zero allowed to be a starting point or
target? I'm assuming no, but I'd like a confirmation.

I made a quick analysis:

   target
   - 0 +
- ok ok ok
0 no ok ok
+ no no ok
start

but, to make it simpler: let the domain be positive integers.
It all depends on the operators used.
With the proposed operators, it is possible to go from any positive
number to any positive number.

Works for me. Let's do that.

Bah!

I have a generic solution which is perfectly happy with negative numbers
(and Bignums) which is unlikely to be the fastest solution at all. Now you
allow others to use even more optimizations!

But given the table, I suppose I should add some code to prevent infinite
loops from the "no" entries of the table above. ah, well.

Bye,
Kero.

In article <5cd596d60512301537ydd6f53anf9cf2c9129fb5367@mail.gmail.com>,

···

Jim Freeze <jim@freeze.org> wrote:

------=_Part_16057_14936009.1135985849520
Content-Type: text/plain; charset=ISO-8859-1
Content-Transfer-Encoding: quoted-printable
Content-Disposition: inline

On 12/30/05, Christer Nilsson <janchrister.nilsson@gmail.com> wrote:

J. Ryan Sobol wrote:
> Would it be appropriate to post my potential answers (not code) to
> this question?

Some examples:

solve(1, 1) # =3D> [1]

Is this a degenerate case? If to go from a to b in (a,b) through
transformation x,y or z, wouldn't there be two possible shortest solutions:

[1,2,1]
[1,0.5,1]

But you can't halve an odd number (from the original rules).

Phil

Stephen Waits wrote:

Since that's about all I'm going to be able to do on this quiz, here
are some of my test cases. I'm pretty confident in the shorter
solutions, but have less confidence in the longer ones. These were
all discovered via stochastic search.

I hope this won't qualify as "code", as mentioned above - since the
idea is that everyone may test their solutions against this, the fact
that it's already coded as test cases seems naturally acceptable to me.

I'm very much looking forward to the solutions on this one!

For now, I've posted them on http://swaits.com/

--Steve

Steve, there seem to be at least one difference:

<[300, 302, 304, 152, ... , 2, 1]> expected but was
<[300, 150, 152, ... , 2, 1]>.

Christer

···

--
Posted via http://www.ruby-forum.com/\.

Yes, but how does that get you a negative number?

James Edward Gray II

···

On Dec 31, 2005, at 11:47 AM, Malte Milatz wrote:

James Edward Gray II:

You can't get a negative, unless you start with one.

Doesn't 2 / 2 == 1 evaluate as true?

He meant "You can't get a negative, unless you start with a negative"
and not "You can't get a negative, unless you start with 1".

I know, I read "one" as "1" the first time also :slight_smile:

···

On Dec 31, 2005, at 10:47 AM, Malte Milatz wrote:

James Edward Gray II:

You can't get a negative, unless you start with one.

Doesn't 2 / 2 == 1 evaluate as true?

Damn. I got it down from 7 minutes to 18 seconds on a 667 MHz PPC, but
it looks like I have a bit more work to do.

Jim

···

On 12/31/05, James Edward Gray II <james@grayproductions.net> wrote:

On Dec 30, 2005, at 5:51 PM, Robert Retzbach wrote:

> Hello.
> I really enjoyed this puzzle, but my brain and my cpu are burning.
>
> In a german ruby channel we used the example 22->999.
> My really slow brute force algo came up with
> 122002000200212
>
> 0 - double
> 1 - halve
> 2 - add_two
>
> It took 20-30min :smiley:

$ time ruby numeric_maze.rb 22 999
22, 24, 26, 28, 30, 60, 62, 124, 248, 496, 498, 996, 1992, 1994, 997,
999

real 0m0.635s
user 0m0.381s
sys 0m0.252s

:wink:

James Edward Gray II

--
Jim Menard, jim.menard@gmail.com, jimm@io.com
http://www.io.com/~jimm
"Generics in Java are an apology for having collections of Objects, and are
somewhat like the puritan's suspicion that someone, somewhere, might be
enjoying themselves."
    -- Steven T Abell in comp.lang.smalltalk

James Gray wrote:

It took 20-30min :smiley:

$ time ruby numeric_maze.rb 22 999
22, 24, 26, 28, 30, 60, 62, 124, 248, 496, 498, 996, 1992, 1994, 997,
999

real 0m0.635s
user 0m0.381s
sys 0m0.252s

:wink:

James Edward Gray II

Very new Rubyist here. :slight_smile:

solve(22, 999)
result:
Total time taken in seconds: 0.782
[22, 24, 26, 28, 30, 60, 62, 124, 248, 496, 498, 996, 998, 1996, 1998,
999]

solve(222, 9999)
result:
Total time taken in seconds: 56.0
[222, 224, 226, 228, 230, 232, 234, 236, 238, 240, 242, 244, 246, 248,
250, 252, 254, 256, 258, 260, 262, 264, 266, 268, 270, 272, 274, 276,
278, 280, 282, 284, 286, 288, 290, 292, 294, 296, 298, 300, 302, 304,
306, 308, 310, 312, 624, 1248, 2496, 2498, 4996, 4998, 9996, 9998,
19996, 19998, 9999]

AMD Athlon 1400, 1.6 GHz

···

On Dec 30, 2005, at 5:51 PM, Robert Retzbach wrote:

--
Posted via http://www.ruby-forum.com/\.

I think I've improved upon this test code a bit. I found this one to
be a bit brittle, as it will fail solutions with unanticipated paths
of the same or smaller length. I lost some of the metadata in the
switch, as Steve has the different solutions ordered by type (i.e.
primes, powers of two, etc).

http://rafb.net/paste/results/qwhnUg32.nln.html

Any feedback is, of course, appreciated.

I'm just pounding away at this test case code because my mathematician
buddy is busy at the moment. This is quite the nontrivial problem...

···

On 12/31/05, Stephen Waits <steve@waits.net> wrote:

Since that's about all I'm going to be able to do on this quiz, here
are some of my test cases. I'm pretty confident in the shorter
solutions, but have less confidence in the longer ones. These were
all discovered via stochastic search.

I hope this won't qualify as "code", as mentioned above - since the
idea is that everyone may test their solutions against this, the fact
that it's already coded as test cases seems naturally acceptable to me.

I'm very much looking forward to the solutions on this one!

For now, I've posted them on http://swaits.com/

--Steve

I guess we're allowed to submit solutions now... here's my first ever ruby
quiz solution (these quizzes are a great idea, btw):

It's is an iterated-depth DFS w/ memoization written in a functional style
for fun.

It exploits the fact that the optimal path through the maze will never have
consecutive double/halves, which reduces the avg branching factor of the
search tree to ~2. Keeping track of this state makes the code a little
uglier, but faster on the longer searches.

Maurice

#! /usr/bin/ruby

# These return the next number & state
ARROWS = [lambda { |x,state| (state != :halve) ? [x*2, :double] : nil }, #
double
          lambda { |x,state| (x.modulo(2).zero? and state != :double) ?
[x/2, :halve] : nil }, #halve
          lambda { |x,state| [x+2, :initial]}] # add_two

def solver(from, to)

  # constraining depth on a DFS
  retval = nil
  depth = 1
  @memo = nil

  # special case
  return [from] if from == to

  while (retval.nil?)
    retval = memo_solver(from, to, depth)
    depth += 1
  end

  retval
end

# cant use hash default proc memoization since i dont want to memoize on the

# state parameter, only on the first 3
def memo_solver(from, to, maxdepth, state=:initial)
  @memo ||= Hash.new

  key = [from, to, maxdepth]

  if not @memo.has_key? key
    @memo[key] = iter_solver(from, to, maxdepth, state)
    @memo[key]
  else
    @memo[key]
  end
end

def iter_solver(from, to, maxdepth, state)
  return nil if maxdepth.zero?

  # generate next numbers in our graph
  next_steps = ARROWS.map { |f| f.call(from, state) }.compact

  if next_steps.assoc(to)
    [from, to]
  else
    # havent found it yet, recur
    kids = next_steps.map { |x,s| memo_solver(x, to, maxdepth-1, s)
}.compact

    if kids.length.zero?
      nil
    else
      # found something further down the tree.. return the shortest list up
      best = kids.sort_by { |x| x.length }.first
      [from, *best]
    end
  end
end

list = [ [1,1], [1,2], [2,9], [9,2], [2, 1234], [1,25], [12,11], [17,1],
        [22, 999], [2, 9999], [222, 9999] ]

require 'benchmark'

list.each do |i|
  puts Benchmark.measure {
    p solver(*i)
  }
end

···

On 1/1/06, Stephen Waits <steve@waits.net> wrote:

On Jan 1, 2006, at 8:04 AM, Matthew Smillie wrote:

> but the first thing that struck me when reading the problem was
> "dynamic programming" - did anyone take this route and hence give
> me a bit of satisfaction that my instincts were right?

I had the same thought, then decided it doesn't fit. But too, I'm
interested in watching these solutions.

I have a feeling they'll be based on exhaustive searches with a pinch
of pruning and a dash of optimization.

--Steve

This seems odd to me. Wouldn't a recursive breadth-first (or even iterative depth-first) only ever recurse as deep as there are numbers in the solution? Everything we've seen so far should be well within solvable that way, unless I'm missing something obvious here...

James Edward Gray II

···

On Jan 1, 2006, at 4:37 PM, Phil Tomson wrote:

I kept thinking that a recursive solution would be the easiest way to go, but
I quickly found that even some of the most trival testcases overwhelmed the
stack :frowning: (not too surprising, I guess, given the size of the search space).

Nuts. :slight_smile:

Kudos, Kero!

~ ryan ~

···

On Jan 3, 2006, at 8:01 AM, Christer Nilsson wrote:

Before you, Ryan Sobol, was the leader, clocking in at 2.376
secs with #60.20 on the Quiz list. Your #60.22, clocks in at 1.972
seconds, a decrease by 17%.

Here's my solution.
It's a bi-directional depth-first search. I'm not sure anyone else did that.
It's reasonably fast for small numbers, but takes 25 secs to do 22222->99999

It also solves Quiz60B, if you adjust the costs. use 22->34 for an
example where the different costs affect the solution.

-Adam

···

----
class NumericMazeSolver
  #the allowable operations - changing the order changes the solution
  #this order favors smaller numbers
  OPS = {:FWD=>[:halve, :add2,:double],:REV=>[:double, :sub2,:halve]}
  #create a hash mapping operations to their inverse.
  ANTIOP=OPS[:FWD].zip(OPS[:REV]).inject({}){|h,s|h[s[0]]=s[1];h[s[1]]=s[0];h}

  #change this line to solve Quiz60B
  #COST = {:halve=>1, :add2=>1, :double=>1,:sub2=>1}
  COST = {:halve=>4, :add2=>1, :double=>2,:sub2=>1}

  def initialize(noisy=false)
    @noisy = noisy
  end

  # a Trail holds a set of operations
  class Trail
    attr_reader :path,:dir,:cost
    def initialize direction, value=nil, path=[], cost = nil
      @dir =direction
      @path = path
      @path.push value if value
      @cost = if cost && value
        cost + calc_cost(value)
      else
        path.inject(0){|sum,op| c=calc_cost(op); sum+=c if c; sum}
      end
    end

    def grow operation
      return Trail.new(@dir,operation,@path.dup,@cost)
    end
    def inspect
      s=@path.inject("$#{@cost}:"){|s,v| s+"#{v}->"}
      s[0..-3]
    end
    def invert
      Trail.new(@dir, nil, @path.reverse.map{|v| ANTIOP[v] || v},@cost)
    end
    def calc_cost operation
      @dir == :FWD ? COST[operation] : COST[ANTIOP[operation]]
    end
  end

  #the operations
  def double a
    return a*2
  end
  def halve a
    return a/2 if a%2==0
  end
  def add2 a
    return a+2
  end
  def sub2 a
    return a-2
  end

  #store the cheapest trail to each number in the solution hash
  def expand(val)
      trail = @sset[val]
      OPS[trail.dir ].each do |op|
        result= self.send(op,val)
        if result
          newpath = trail.grow(op)
          if (foundpath = @sset[result] )
            if foundpath.dir != newpath.dir
              cost = foundpath.cost+newpath.cost
              @match= [newpath,result, cost] if (!@match || (cost <
@match.last))
            elsif foundpath.cost > newpath.cost
              @sset[result] = newpath
            end
          else
            @sset[result] = newpath
            if (!@match || (newpath.cost+@depth) < @match.last)
              @newvals.push(result) #only check if total cost can be
less than match cost
            end
          end
        end
      end
  end

  def solve(start,target)
    return nil if start<0 || target < 1
    return [start] if start==target
    @sset = {start=>Trail.new(:FWD,start) ,
                   target=>Trail.new(:REV,target) }
    @newvals=[start,target]
    solution = nil
    @match=nil
    @depth=0
    while true do
      val = @newvals.shift
      break if !val
      expand(val)
      @depth+=1
    end
    trail, matchnum = solution
    trail, matchnum = @match
    if trail.dir == :FWD
      first,last = trail,@sset[matchnum].invert
    else
      first,last = @sset[matchnum],trail.invert
    end
# p first,last
    get_solution(first,last)
  end

  def get_solution(first,last)
    puts "SOLUTION = " + first.inspect + "->" + last.inspect if @noisy
    p = first.path + last.path
    s=[p.shift]
    p.each{ |v| s << self.send(v,s[-1]) unless v.is_a? Integer}
    s
  end

end

if __FILE__ == $0
  start, goal, is_noisy = ARGV[0].to_i, ARGV[1].to_i, ARGV[2]!=nil
  puts "usage: @$0 start goal [noisy]" unless start && goal
  p NumericMazeSolver.new(is_noisy).solve(start, goal)
end