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:
- candles = { :orange => 3, ,
- :vanilla => 2, ,
- :lavender => 2, ,
- :garden => 4 }
- recipients = %w(janet nancy susan)
- candles_per_recipient = 3
- mix_and_match(candles, , recipients, , candles_per_recipient)
- => { "janet" => [:garden, , :lavender, , :orange], ,
- "nancy" => [:garden, , :orange, , :vanilla], ,
- "susan" => [:garden, , :lavender, , :vanilla], ,
- :extra => { :orange => 1, ,
- :vanilla => 0, ,
- :lavender => 0, ,
- :garden => 1
- }
- }
, 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):
- class Hash
- def comb(group_size)
- result = []
- inner_comb = lambda do |head, ,tail|
- tail[0..-(group_size-head.size)].each do |e|
- if (head.size >= group_size-1)
- tail.each {|t| result << head + [t]}
- else
- inner_comb[head + [e], , tail[tail.index(e)+1..-1]]
- end
- end
- end
- inner_comb[[], ,self.inject([]) {|a, ,v| v[1].times{a << v[0]}; a}]
- result.uniq
- end
e.g.:
- candles = { :orange => 2, ,
- :vanilla => 1, ,
- :lavender => 1, ,
- :garden => 1 }
- pp candles.comb(3)
- => [[:lavender, , :garden, , :orange], ,
- [:lavender, , :garden, , :vanilla], ,
- [:lavender, , :orange, , :orange], ,
- [:lavender, , :orange, , :vanilla], ,
- [:garden, , :orange, , :orange], ,
- [:garden, , :orange, , :vanilla], ,
- [: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:
- class Hash
- def remove_set(set)
- set.each {|e| self[e] -= 1}
- end
- end
The above code adjusts the number of candles in the original hash once we give away some of them. , So for example:
- candles = { :orange => 2, ,
- :vanilla => 1, ,
- :lavender => 1, ,
- :garden => 1 }
- candles.remove_set([:orange, ,:orange, ,:lavender])
- p candles
- => {:lavender=>0, , :garden=>1, , :orange=>0, , :vanilla=>1}
and some Array extensions:
- class Array
- def rand
- uniqs = self.select{|e| e.uniq.size == e.size}
- uniqs.empty? ? self[Kernel.rand(length)] : uniqs[Kernel.rand(uniqs.length)]
- end
- def unordered_include?(other)
- self.map{|e| e.map{|s| s.to_s}.sort}.include? other.map{|s| s.to_s}.sort
- end
- 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:
- [[:lavender, , :garden, , :orange]].include? [:lavender, , :orange, , :garden] => false
- [[: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:
- ERROR_STRING = "The number of recipients times the number of candles per recipient is more than the supply!"
- def mix_and_match(candles, , recipients, , candles_per_recipient)
- return ERROR_STRING if ((candles.values.inject{|a, ,v| a+v}) < (recipients.size * candles_per_recipient))
- candle_set = recipients.inject({}) do |a, ,v|
- tried = []
- tries = 0
- loop do
- random_pick = candles.comb(candles_per_recipient).rand
- tried << random_pick unless tried.unordered_include? random_pick
- break unless a.values.unordered_include? random_pick
- break if (tries+=1) > candles.values.size * 2
- end
- candles.remove_set(tried.last)
- a[v] = tried.last
- a
- end
- candle_set.merge({:extra => candles})
- 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

December 16th, 2008 at 3:45 am
, 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.
December 16th, 2008 at 3:50 am
, 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.
March 21st, 2009 at 6:19 pm
, [...] Ruby Quiz - Mix and Match [...]
March 21st, 2009 at 6:26 pm
, [...] Ruby Quiz - Mix and Match [...]