header image


December 15th, 2008

rq_candles.png Solved another Ruby Quiz:

, I purchased a number of scented candles recently for sending out to friends and family. , While I could be accused of being lazy by getting candles for several people, , I’d like to mix up the candles a bit so that each recipient gets a different combination of scents. Please help me out! Your task is to write a method that randomizes and mixes up the individual candles into groups, , one per recipient, , in order to minimize group duplication. , So, , for example:

  1. candles = { :orange   => 3, ,
  2.             :vanilla  => 2, ,
  3.             :lavender => 2, ,
  4.             :garden   => 4 }
  5.  
  6. recipients = %w(janet nancy susan)
  7.  
  8. candles_per_recipient = 3
  9.  
  10. mix_and_match(candles, , recipients, , candles_per_recipient)
  11.  
  12. => { "janet" => [:garden, , :lavender, , :orange], ,
  13.      "nancy" => [:garden, , :orange, , :vanilla], ,
  14.      "susan" => [:garden, , :lavender, , :vanilla], ,
  15.      :extra  => { :orange   => 1, ,
  16.                   :vanilla  => 0, ,
  17.                   :lavender => 0, ,
  18.                   :garden   => 1
  19.                  }
  20.     }

, If it is impossible to have a unique combination for every recipient, , you should still generate some set of combinations, , minimizing repetition of combinations. If the number of recipients times the number of candles per recipient is more than the supply, , generate an error.

Proposed solution: (for the impatient, , the source code is here.) In my interpretation, , this is a simple combinatorial problem: say the number of recipients is r and candles_per_recipient is c, , then you are looking for a (preferably non-repeating) random selection of r elements of c-combinations of the original set of candles. , (In fact it’s a bit more complicated than that: the c-combinations have to be recalculated from the remaining candles each time you give away a group of candles, , so we’ll get to that). , Sounds confusing? Don’t worry, , after the implementation everything will be clear!

So first, , define a k-combination for a histogram (a Hash like candles above, , where keys are elements and values are cardinalities):

  1. class Hash
  2.   def comb(group_size)
  3.     result = []   
  4.     inner_comb = lambda do |head, ,tail|
  5.       tail[0..-(group_size-head.size)].each do |e|
  6.         if (head.size >= group_size-1)
  7.           tail.each {|t| result << head + [t]}
  8.         else
  9.           inner_comb[head + [e], , tail[tail.index(e)+1..-1]]
  10.         end
  11.       end
  12.     end
  13.     inner_comb[[], ,self.inject([]) {|a, ,v| v[1].times{a << v[0]}; a}]
  14.     result.uniq   
  15.   end

e.g.:

  1. candles = { :orange   => 2, ,
  2.             :vanilla  => 1, ,
  3.             :lavender => 1, ,
  4.             :garden => 1 }
  5.  
  6. pp candles.comb(3)
  7.  
  8. => [[:lavender, , :garden, , :orange], ,
  9.     [:lavender, , :garden, , :vanilla], ,
  10.     [:lavender, , :orange, , :orange], ,
  11.     [:lavender, , :orange, , :vanilla], ,
  12.     [:garden, , :orange, , :orange], ,
  13.     [:garden, , :orange, , :vanilla], ,
  14.     [:orange, , :orange, , :vanilla]]

so for a set of candles, , this method generates all possible 3-combinations of the candles. , We can then pick one and assign it to one of the recipients. , Then recalculate the above from the remaining candles, , give it to the next recipient - and so on and so forth. , That’s the basic idea, , but we also need to ensure the candle combinations are as non-repeating as possible. , So let’s define some further utility methods:

  1. class Hash
  2.   def remove_set(set)
  3.     set.each {|e| self[e] -= 1}
  4.   end 
  5. end

The above code adjusts the number of candles in the original hash once we give away some of them. , So for example:

  1. candles = { :orange   => 2, ,
  2.             :vanilla  => 1, ,
  3.             :lavender => 1, ,
  4.             :garden => 1 }
  5.  
  6. candles.remove_set([:orange, ,:orange, ,:lavender])
  7. p candles
  8. => {:lavender=>0, , :garden=>1, , :orange=>0, , :vanilla=>1}

and some Array extensions:

  1. class Array
  2.   def rand
  3.     uniqs = self.select{|e| e.uniq.size == e.size}
  4.   uniqs.empty? ? self[Kernel.rand(length)] : uniqs[Kernel.rand(uniqs.length)]
  5.   end
  6.  
  7.   def unordered_include?(other)
  8.     self.map{|e| e.map{|s| s.to_s}.sort}.include? other.map{|s| s.to_s}.sort
  9.   end 
  10. end

Array#rand is trying to pick a random non-repeating combination if there is one (so e.g. , [:orange, , :lavender, , :garden]) or, , if there is no such combination, , then just a random one (e.g. , [:orange, , :orange, , :garden] - orange is repeating, , but we have no other choice).

Array#unordered_include? is like normal Array#include?, , but disregards the ordering of the elements. , So for example:

  1. [[:lavender, , :garden, , :orange]].include? [:lavender, , :orange, , :garden] => false
  2.   [[:lavender, , :garden, , :orange]].unordered_include? [:lavender, , :orange, , :garden] => true

Hmm… , it would have been much more effective to use a set here rather than the above CPU-sucker, , but now I am lazy to change it ;-)

OK, , so finally for the solution:

  1. ERROR_STRING = "The number of recipients times the number of candles per recipient is more than the supply!"
  2.  
  3. def mix_and_match(candles, , recipients, , candles_per_recipient)
  4.   return ERROR_STRING if ((candles.values.inject{|a, ,v| a+v}) < (recipients.size * candles_per_recipient))
  5.   candle_set = recipients.inject({}) do |a, ,v|
  6.     tried = []
  7.     tries = 0
  8.     loop do
  9.       random_pick = candles.comb(candles_per_recipient).rand
  10.       tried << random_pick unless tried.unordered_include? random_pick
  11.       break unless a.values.unordered_include? random_pick
  12.       break if (tries+=1) > candles.values.size * 2
  13.     end
  14.     candles.remove_set(tried.last)
  15.     a[v] = tried.last
  16.     a 
  17.   end
  18.   candle_set.merge({:extra => candles})
  19. end

So, , in the inner loop we randomly pick a candles-per-recipient-combination of all the possible combinations; If no one has that combo yet, , we assign it to the next recipient. , If someone has it already, , we try to find an unique combination (loop on), , unless it is impossible (checked on line #12). , In this case we simply start giving out any combinations. , Once we give away a set of candles, , we remove them from the original set. , Easy-peasy.

You can check out the source code here.

This was a great quiz, , too bad that not many people took a stab at it (so far 1 except me ;-)). , The hardest part for me was the implementation of the k-combination (and the result looks awful to me - I didn’t check any algorithm/pseudocode/other solution though, , I wanted to roll my own) - after that the problem was pretty simple. , Cheers for the Ruby Quiz guys (== ["Matthew Moss"] I guess?) for this quiz.

Similar Posts:cheap xanax



If you liked the article, subscribe to the feed   and follow me on twitter!.


      

4 Responses to “”

  1. Balint Erdi Says:

    , Hey Peter, ,

    , I would like to give this quiz a try but there is no link to it in your post and I have not found it on the ruby quiz list with a simple search. Could you give a pointer? Thank you.

  2. peter Says:

    , You have to be subscribed to the ruby-lang ML. The quiz announcement is here btw: http://www.ruby-forum.com/topic/173504. Be quick - I am not sure what’s the deadline, , but it’s max a few days from now.

  3. THE TRAGIC STORY OF MONTE CASSINO (David Shribman) — But As For Me Says:

    , [...] Ruby Quiz - Mix and Match [...]

  4. ANGRY AMERICANS MAY BE READY TO LOOK AT INCOME INEQUALITY (Cynthia Tucker) — But As For Me Says:

    , [...] Ruby Quiz - Mix and Match [...]

Leave a Reply




Bad Behavior has blocked 940 access attempts in the last 7 days.