[QUIZ] Numeric Maze (#60)

I did that, too :wink:

My solution uses unidirectional BFS (like many others). The NumericMaze class is generic, meaning that it can also work with other ops, so naturally it doesn't have any optimizations for this special problem. I only limit the search space in the "if $0 == __FILE__" part.

Dominik

class NumericMaze

     OP_DOUBLE = lambda { |x| x * 2 }
     OP_HALVE = lambda { |x| x % 2 == 0 ? x / 2 : nil }
     OP_ADD_TWO = lambda { |x| x + 2 }

     # ops is an array of lambdas, each lambda returns a next step for a given
     # number, or nil if no next step is possible for the given number
     def initialize(ops = [OP_DOUBLE, OP_HALVE, OP_ADD_TWO])
         @ops = ops
     end

     def solve(start, target, max_num = nil)
         # build chain with simple breadth first search
         current = [start]
         return current if start == target
         pre = { start => nil } # will contain the predecessors
         catch(:done) do
             until current.empty?
                 next_step =
                 current.each do |num|
                     @ops.each do |op|
                         unless (step_num = op[num]).nil?
                             # have we seen this number before?
                             unless pre.has_key?(step_num) ||
                                     (max_num && step_num > max_num)
                                 pre[step_num] = num
                                 throw(:done) if step_num == target
                                 next_step << step_num
                             end
                         end
                     end
                 end
                 current = next_step
             end
             return nil # no chain found
         end
         # build the chain (in reverse order)
         chain = [target]
         chain << target while target = pre[target]
         chain.reverse
     end

end

if $0 == __FILE__
     a, b, = *ARGV.map { |str| Integer(str) }
     p NumericMaze.new.solve(a, b, [a, b].max.abs * 3)
end

···

On Sun, 01 Jan 2006 19:27:56 +0100, <horndude77@gmail.com> wrote:

For this quiz I took what I made for the word chain quiz and modified
it a bit.

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.

11011110 222
11100000 224
1110000 112
111000 56
11100 28
1110 14
10000 16
10010 18
100100 36
100110 38
1001100 76
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
10011100001110 9998
100111000011100 19996
100111000011110 19998
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
11100 28
1110 14
10000 16
10010 18
100100 36
100110 38
1001100 76
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
100111000011000 19992
100111000011010 19994
10011100001101 9997
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
11100 28
11110 30
1111 15
10001 17
10011 19
100110 38
1001100 76
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
10011100001110 9998
100111000011100 19996
100111000011110 19998
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
11100 28
11110 30
1111 15
10001 17
10011 19
100110 38
1001100 76
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
100111000011000 19992
100111000011010 19994
10011100001101 9997
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
11100 28
11110 30
100000 32
100010 34
100100 36
100110 38
1001100 76
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
10011100001110 9998
100111000011100 19996
100111000011110 19998
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
11100 28
11110 30
100000 32
100010 34
100100 36
100110 38
1001100 76
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
100111000011000 19992
100111000011010 19994
10011100001101 9997
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
111010 58
11101 29
11111 31
100001 33
100011 35
100101 37
100111 39
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
10011100001110 9998
100111000011100 19996
100111000011110 19998
10011100001111 9999

11011110 222
11100000 224
1110000 112
111000 56
111010 58
11101 29
11111 31
100001 33
100011 35
100101 37
100111 39
1001110 78
10011100 156
100111000 312
1001110000 624
10011100000 1248
100111000000 2496
100111000010 2498
1001110000100 4996
1001110000110 4998
10011100001100 9996
100111000011000 19992
100111000011010 19994
10011100001101 9997
10011100001111 9999

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

- One of the few times that class variables come in handy: I put the
   halve/double operations into Integer and then decided to make the do-all
   method Integer.solve_num_maze but the steps are done in
   Integer#handle_num_maze

- Both optimizations are done in the method #enqueue

My permanent URL has header, license anouncement and large testsuite (orig
from swaits.com, unaccessible; not updated) included:
   http://members.chello.nl/k.vangelder/ruby/quiz/

The code itself:

class Integer
  def even?
    self[0] == 0
  end

  def odd?
    self[0] == 1
  end

  def halve
    self / 2 if self.even?
  end

  def double
    self * 2
  end

  # add inverse for add_two (we're doing DynProg)
  def sub2
    self - 2
  end

  Step = Struct.new(:dist, :next)

  def self.source; @@source; end
  def self.route; @@route; end
  def self.queue; @@queue; end

  def source; @@source; end
  def route; @@route; end
  def queue; @@queue; end

  def self.solve_num_maze(source, target)
    raise ArgumentError.new("Can't solve from >=0 to <0") if target < 0 and source >= 0
    raise ArgumentError.new("Can't solve from >0 to 0") if target <= 0 and source > 0
    @@source = source
    @@route = {}
    @@queue = []
    @@max = [(source + 2) * 2, target * 2].max
    # @@max = [source, target].max << 2 # and other attempts
    queue << target
    route[target] = Step.new(0, nil)
    while (job = queue.shift) != source
      job.handle_num_maze
    end

    result = [source]
    step = source
    while step != target
      step = route[step].next
      result << step
    end
    result
  end

  def enqueue(job)
    # optimization 1, do not go through pending searches when effectively done
    queue.clear if job == source

    # optimization 2, do not look for solutions where it is not necessary
    queue << job if job <= @@max
  end

  def handle_num_maze
    if route[double].nil? or route[self].dist + 1 < route[double].dist
      route[double] = Step.new(route[self].dist+1, self)
      enqueue double
    end
    # mind the extra check on existence of #halve
    if halve and (route[halve].nil? or route[self].dist + 1 < route[halve].dist)
      route[halve] = Step.new(route[self].dist+1, self)
      enqueue halve
    end
    if route[sub2].nil? or route[self].dist + 1 < route[sub2].dist
      route[sub2] = Step.new(route[self].dist+1, self)
      enqueue sub2
    end
  end
end

p Integer.solve_num_maze(ARGV[0].to_i, ARGV[1].to_i)

Hello dear quizzers,

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

[222222222, 222222224, 111111112, 55555556, 27777778, 27777780, 13888890, 13888892, 6944446, 6944448, 3472224, 1736112, 868056, 434028, 217014, 217016, 108508, 54254, 54256, 27128, 13564, 6782, 6784, 3392, 1696, 848, 424, 212, 106, 108, 54, 27, 29, 31, 33, 35, 37, 39, 78, 156, 158, 316, 632, 634, 1268, 1270, 2540, 2542, 5084, 5086, 10172, 20344, 40688, 40690, 81380, 162760, 325520, 651040, 1302080, 1302082, 2604164, 2604166, 5208332, 10416664, 10416666, 20833332, 41666664, 41666666, 83333332, 166666664, 166666666, 333333332, 666666664, 666666666, 333333333]

75 items

Time: 62.015s

There seems to be more potential in the brute force attack than i thought possible.

cheers

Simon

0x002A@gmail.com wrote:

okay here's my shot at the quiz, it's quite large for such a simple
problem, but i couldn't resist to use
:* in my code :wink:

42,

you seem to produce a non optimal solution:

[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]

(17 steps instead of 16)

Christer

···

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

0x002A@gmail.com wrote:

okay here's my shot at the quiz, it's quite large for such a simple
problem, but i couldn't resist to use
:* in my code :wink:

now without :* but much shorter..

class Integer
    def even?
        self % 2 == 0
    end

    def odd?
        not even?
    end
end

# solves rubyquiz #60
class Solver
    def initialize(start, goal)
        @start, @goal = start, goal
        @visited = {}
        @queue = [[@goal, ]]
        @ops =
        @ops << lambda {|x| x - 2 if x > 1 }
        @ops << lambda {|x| x * 2 if x.odd? or @goal < @start }
        @ops << lambda {|x| x / 2 if x.even? }
    end

    # are we there yet?
    def done_with?(temp_goal)
        @start == temp_goal
    end

    # transforms the carried steps into a valid solution
    def solution(steps)
        steps.reverse.unshift @start
    end

    # does all the work
    def solve
        while current = @queue.shift
            temp_goal, steps = *current

            return solution(steps) if done_with? temp_goal
            # been there, done that
            next if @visited[temp_goal]

            @visited[temp_goal] = true
            new_steps = steps + [temp_goal]

            @ops.each do |op|
                if (new_goal = op.call temp_goal)
                    @queue << [new_goal, new_steps]
                end
            end
        end
        raise "no solution found"
    end

    # creates a new solver and attempts to solve(a,b)
    def self.solve(a,b)
        new(a,b).solve
    end
end

# for the testcases
def solve(a, b)
    Solver.solve(a,b)
end

Yup, my solution is really slow. I made a minor change which reduces
the search space a little (and avoids things such as double followed by
halve, and v-v). Still rather slow, but I didn't expect it to be fast.

class Integer
   def even?
      self & 1 == 0
   end

   def maze_adj
      if even?
         [self + 2, self * 2, self / 2]
      else
         [self + 2, self * 2]
      end
   end
end

def solve(a, b)
   known = []

   i = 0
   steps = [[a]]
   known << a

   until steps[i].include?(b)
      i += 1
      steps[i] = []
      steps[i-1].each do |x|
         x.maze_adj.each { |y| steps[i] << y unless known.include?(y) }
      end
      known.concat steps[i]
   end

   i -= 1
   path = [b]
   i.downto(0) do |k|
      s = steps[k].find_all { |x| x.maze_adj.include? path.last }
      path << s.sort_by { rand }.first
   end

   path.reverse
end

# Examples
p solve(9, 2)
p solve(2, 9)
p solve(22, 99)
p solve(222, 999)

Sorry, crazy day. It's up now.

James Edward Gray II

···

On Jan 6, 2006, at 12:49 PM, Robert Retzbach wrote:

Don't let us wait too long for that dice riddle :>

Heh, I couldn't figure out how to get a negative number using double, halve,
and add_two from the number 1. Then I realized the problem wasn't with my
math, but with my English :slight_smile:

That, or I'm really confused.
Jeff

···

On Sat, Dec 31, 2005 at 05:36:58AM +0900, James Edward Gray II wrote:

On Dec 30, 2005, at 2:18 PM, J. Ryan Sobol wrote:

>Are negative numbers and zero allowed to be a starting point or target?

Zero seems fine as a starting number. You can always add_two.

You can't get a negative, unless you start with one. If you do, other
negative targets and even a target of 0, seem fine. Some numbers would be
impossible to reach though.

Works for me. Let's do that.

James Edward Gray II

···

On Dec 30, 2005, at 2:57 PM, Christer Nilsson wrote:

J. Ryan Sobol wrote:

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

~ ryan ~

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.

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]

···

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]

--
Jim Freeze

Robert Retzbach wrote:

J. Ryan Sobol schrieb:

this question? For example,

~ ryan ~

PS- I'm new here and I hope I'm not over overstepping the rules.

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:

1->15 : 2000021, took about .5s

But thinking about a no BF solution makes my head hurt :frowning:
I have created some algos but they all don't find the shortest way.

I also have to mention that there are often more than 1 'shortest' ways.

I exspect the best of you!
Good luck.

Robert, your solution is correct. There may be different solutions with
the same number of steps.

[22, 24, 26, 28, 30, 60, 62, 124, 248, 496, 498, 996, 1992, 1994, 997,
999]
[22, 11, 13, 15, 30, ...]

I agree, it shouldn't take so long time. Try shaving off one magnitude.

Christer

···

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

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

···

On Dec 30, 2005, at 2:27 PM, James Edward Gray II wrote:

On Dec 30, 2005, at 4:17 PM, J. Ryan Sobol wrote:

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

Absolutely. Please do.

$ 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

···

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:

James Edward Gray II:

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

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

Malte

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

···

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?

My solution is not optimal nor fast.. but different than the other
solutions so far.

···

--
Simon Strandgaard

def solve(a, b)
  t = "h#{b}"
  t = "h#{b*=2}" + t while b < a
  s = "#{a}d#{a*2}"
  a *= 2;b *= 2
  s += "a#{a+=2}" while a < b
  s += t
  loop do
    l = s.length
    s.gsub!(/(\d+)d\d+a\d+a/) { "#{$1}a#{$1.to_i + 2}d" }
    s.gsub!(/4a6a8/, '4d8')
    s.gsub!(/(\D|^)(\d+)(?:\D\d+)+\D\2(\D|$)/) {$1 + $2 + $3}
    break if s.length == l
  end
  s.scan(/\d+/).map{|i|i.to_i}
end

In article <C95D3FF1-B62F-468B-B791-431D961E29F6@sms.ed.ac.uk>,

···

Matthew Smillie <M.B.Smillie@sms.ed.ac.uk> wrote:

On Jan 1, 2006, at 15:47, James Edward Gray II wrote:

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

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

Here's my simple breadth-first search with a single optimization
(described in comments). It's not at all fast enough for the big
examples people have been throwing around, but it's pretty simple.

OK, so I didn't have time to code it up and see for sure, what with
some transatlantic travel and New Year's and all that stuff, 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 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).

If I have time to get it done I'll post my non-recursive version (that is
unfortuneately becoming quite bloated, I think).

Phil

My solution's similar in style [breadth first, don't duplicate search],
though I've also roof-limited it so it can't explore the search space
above 2*[max(target,start)] + 4 which provides a fair speed up for e.g.
222, 9999. I'm not sure if that loses the optimality property though.

I use
  @@max = [(source + 2) * 2, target * 2].max
which matches your roof almost exactly (The small assymetry is due to the
missing sub2 operator, naturally.). The speedup is huge, as you can see
in some other posting of mine. I'm not entirely certain about the optimality
property, but look at the following example:

13, 15, 30, 32, 16, 8 # what I thought of myself, pushing through the roof
13, 26, 28, 14, 16, 8 # what the program found, not going through the roof

Where the point is that an odd number either +1 or +3 can always be halved
twice, and there is no point in adding two first (though maybe in between).

I'm even thinking
  [source*2+2, target*2].max
could be the roof, but hey, what's 2 more or less.

Ah well, this is not my field of expertise. Somebody can shed more light on
this?

Bye,
Kero.

I toyed with writing a optimize-function, which would take the
midstate list and find patterns that are known to be suboptimal. One
pattern to look for would be going from odd number to another, where
the distance is even and less than 32, which is the point where the
multiply, add, divide, etc starts being less ops than add 2.

The optimizer iterates from start of path, and then for each number,
find from end of path the farthest matching number, replace the ops
between with optimized version.

E.g. for [255, 510, 512, 514, 257], start from 255, then iterate
remaining numbers from end to start, 257 matches, so replace the ops
between with +2 to get [255, 257].

Other patterns? Hmm, dunno. 12 -> 11 is suboptimal, 14 -> 13 too, both
demonstrating a faster solution that would be divide to odd, add 2s to
match. I hypothesize that this only happens with small numbers, where
the divide-to-odd + add 2s is less ops.

[14, 16, 8, 4, 6, 12, 24, 26, 13] -> [14, 7, 9, 11, 13]

(This is also weird in that Gregory's solver gives a one step shorter
solution: [14, 16, 8, 10, 12, 24, 26, 13])

Food for thought,

Ilmari

···

On 1/2/06, Christer Nilsson <janchrister.nilsson@gmail.com> wrote:

Ilmari Heikkinen wrote:
> #
> # Problems appear when trying to make 255 into 257:
> # 11111111 -> 100000001
> #
> # The shortest way is by adding 2.
> # But the algorithm below fails at that and goes the long way:
> # 11111111 << 1
> # 111111110 + 2
> # 1000000000 + 2
> # 1000000010 >> 1
> # 100000001
> #

Ilmari, I tried to replicate your solution, before seeing it, and
strangely run into exactly the same behaviour. The code is not complete,
but the idea is to go downwards to 1 with both numbers. I'm using the
complement of +2, -2 for the target number.

      t up(255), [255, 510, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1]
                  --> --> --v
                  <-- <-- <--
    t down(257), [257, 514, 512, 256, 128, 64, 32, 16, 8, 4, 2, 1]

Having these two sequences, I find the largest common number, in this
case 512. This gives me the non optimal solution [255,510,512,514,257],
which is exactly the same as yours. I'm not sure if identifying
shortcuts afterwards, will find the shortest path, though, in all cases.
(Finding 255 + 2 = 257, is easy, but what about finding 255 -> 259 if
257 is missing.)