On Fri, 30 Mar 2007 20:55:46 +0900, Ruby Quiz wrote:

> The three rules of Ruby Quiz:
> 
> 1.  Please do not post any solutions or spoiler discussion for this quiz
> until 48 hours have passed from the time on this message.
> 
> 2.  Support Ruby Quiz by submitting ideas as often as you can:
> 
> http://www.rubyquiz.com/
> 
> 3.  Enjoy!
> 
> Suggestion:  A [QUIZ] in the subject of emails about the problem helps
> everyone on Ruby Talk follow the discussion.  Please reply to the
> original quiz message, if you can.
> 
> -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-
=-=-=-=-=
> 
> by Matthew Moss
> 
> Microwave ovens have had a significant impact on how we cook today. One
> report from 1997 indicated that 90% of US households owned one. Assuming
> the promise of faster cooking times, that's a lot of time saved.
> 
> But I imagine there are microwave users out there who know the trick to
> saving even more time. Knowing that many microwave ovens recognize 90
> seconds as the same as 1 minute 30 seconds, finger-travel distance is
> saved. (Yes, it's rather insignificant, but don't tell them... us...
> whatever.)
> 
> Your task is to write a function in Ruby that determines the optimal
> pattern of buttons to hit based on this example button pad (where * is
> "Cook"):
> 
> 	+---+---+---+
> 	| 1 | 2 | 3 |
> 	+---+---+---+
> 	| 4 | 5 | 6 |
> 	+---+---+---+
> 	| 7 | 8 | 9 |
> 	+---+---+---+
> 	    | 0 | * |
> 	    +---+---+
> 
> Your function should accept an integral time value representing desired
> seconds and should output an integer that indicates the buttons to press
> on the microwave's input pad. The metric for determining what input is
> more efficient is distance (not number of buttons hit). Distance to the
> Cook button must be included in your efficiency calculation. For
> simplicity in distance calculations, you may consider the shape of each
> button to be square.
> 
> Examples:
> 
> 	# 99 seconds is 1:39, but 99 is less movement than 139 microwave
(99) =>
> 	99
> 	
> 	# 71 seconds is only two keys, but entering 111 is far less 
movement.
> 	microwave(71) => 111
> 	
> 	# 120 seconds is 2 minutes, and 200 is slightly less movement 
than 120
> 	microwave(120) => 200
> 	
> 	# 123 seconds is 2:03, but 203 is a lot more distance microwave
(123) =>
> 	123
> 
> Once you've done the basic version, try modifying your code enough to
> handle these:
> 
> 1. We often don't care to be exact. 99 seconds, for example, is
> basically the same as 95 seconds, but more efficient to enter. Modify
> your function to accept a tolerance in seconds, and return answers that
> are within that tolerance of the desired time. Try +-5 and +-10 seconds.
> 
> 2. Try changing the efficiency metric, to something like number of
> buttons pressed, or Manhattan distance.
> 
> 3. Try changing the button dimensions... For example, what happens if
> each button is twice as wide as it is high?

require 'matrix'
require 'enumerator'
class Matrix
   # returns the row and column of the value requested, or nil if not
   # found
   def find value
      row_vectors.each_with_index do |row,rownum|
	 row=row.to_a
	 row.each_with_index do |col,colnum|
	    return rownum,colnum if col==value
	 end
      end
   end
end

#these functions are the distance metrics

def euclidian_distance array1, array2
   Math.sqrt(array1.zip(array2).inject(0){|a,(v1,v2)| a+(v2-v1)**2})
end
def manhattan_distance array1, array2
   array1.zip(array2).inject(0){|a,(v1,v2)| a+(v2-v1).abs}
end
def num_buttons array1, array2
   1
end
def rand_metric array1, array2
   rand
end

#make it easy to try out different distance metrics by changing these 
#aliases
alias distance_metric euclidian_distance
alias tiebreaker_metric num_buttons

# now we compute acutal Primary for all pairs
# if we wanted, we could write a function that computes this every
# time rather than memoizing it in a hash
Positions=Matrix[['1','2','3'],['4','5','6'],['7','8','9'],['-','0','*']]
Primary={}
Tiebreaker={}

('0'..'9').each do |from|
   ('0'..'9').each do |to|
      Primary[[from,to]]=distance_metric(
	 Positions.find(from),
	 Positions.find(to))
      Tiebreaker[[from,to]]=tiebreaker_metric(
	 Positions.find(from),
	 Positions.find(to))
   end
   Primary[[from,'*']]=distance_metric(
      Positions.find(from),
      Positions.find('*'))
   Tiebreaker[[from,'*']]=tiebreaker_metric(
      Positions.find(from),
      Positions.find('*'))
end


# computes the distance and the string used for a specific (possibly
# improper) number of minutes and seconds to be entered into the
# microwave
def make_array min,sec
   ("%d%02d*" % [min,sec]).gsub(/^0+([^*])/,'\1').split(//)
end

def compute_dist array,distances
   array.enum_cons(2).inject(0){|a,v| a+distances[v]}
end

# given the number of seconds to run the microwave for, this function
# returns the shortest path of buttons that one can press to make the
# microwave run for that period of time
#
# if both possibilites have the same total distance, then the function
# just picks one in some undefined way
def compute_best_distance sec
   min_improper,sec_improper=(min_proper,sec_proper=sec.divmod(60))
   if min_improper>0 and sec_improper<40
      min_improper-=1
      sec_improper+=60
   else
      #the improper time will be the same as the proper time, which
      #isn't a problem
   end
   proper=make_array(min_proper,sec_proper)
   improper=make_array(min_improper,sec_improper)
   [[
     compute_dist(proper,Primary),
     compute_dist(proper,Tiebreaker),
     proper
    ],[
     compute_dist(improper,Primary),
     compute_dist(improper,Tiebreaker),
     improper
   ]].sort[0][-1].join
end

#print a the values for runs up to 5 minutes long
(0..300).each do |x|
   printf "%d (%s): %s\n", x, "%d:%02d" % x.divmod(60),
      compute_best_distance(x)
end

-- 
Ken Bloom. PhD candidate. Linguistic Cognition Laboratory.
Department of Computer Science. Illinois Institute of Technology.
http://www.iit.edu/~kbloom1/