There was some debate on the list about which NP-complete problem this actually
is. I personally thought it was a variant of the partitioning problem when I
put it together, but others made good cases for similar problems. In the end,
two things are important: it's difficult and tricky to get right.
Some people thought you could work with the treasures largest to smallest to
save time. A few edge cases were pointed out for this approach though. The
easiest example to understand being this one posted by Avi Bryant:
聽聽$ ruby loot.rb 3 3 3 3 2 2 2 2 2 2 2 2 2
聽聽1: 3 2 2 2
聽聽2: 3 2 2 2
聽聽3: 3 2 2 2
Here we are looking for totals of nine. If we work only with the big values, we
will find 3 3 3 as an option, but then it is impossible to break the remaining
even numbers into totals of nine. For this reason, you must consider all of the
possibilities.
Manuel Kasten posted many of the edge cases that solutions had trouble with, in
addition to the first solution that seems to be able to find them all. Let's
look at that code now, from the bottom up:
聽聽# ...
聽聽
聽聽if $0 == __FILE__
聽聽聽聽pirates = ARGV.shift.to_i
聽聽聽聽treasure = ARGV.map{ |gem| gem.to_i }.sort
聽聽聽聽si = SplitIt.new(pirates, treasure)
聽聽聽聽si.go
聽聽聽聽si.done
聽聽end
That's the first bit of code run when Manuel's program is launched. We can see
that it reads and converts arguments, builds some SplitIt object with them, and
finally calls SplitIt#go and SplitIt#done.
Time to dig into SplitIt:
聽聽class SplitIt
聽聽聽聽def initialize pirates, treasure
聽聽聽聽聽聽@pirates = pirates
聽聽聽聽聽聽@treasure = treasure
聽聽聽聽聽聽@bags = []
聽聽聽聽聽聽(0...@pirates).each{ |pirate| @bags[pirate] = [[], 0] }
聽聽聽聽聽聽loot = @treasure.inject(0){ |res, gem| res + gem }
聽聽聽聽聽聽done unless loot % @pirates == 0
聽聽聽聽聽聽@share = loot/@pirates
聽聽聽聽end
聽聽聽聽
聽聽聽聽# ...
The only remotely tricky thing in here is the creation of @bags. Each pirate is
given a two-element Array. The first element is another Array, which will be
filled with the gems placed in their share. The second element is just the
total of their share thus far.
Also note that this method may immediately force a call to SplitIt#done, if the
treasure does not evenly divide. Let's skip down to done now:
聽聽聽聽# ...
聽聽聽聽
聽聽聽聽def done
聽聽聽聽聽聽puts
聽聽聽聽聽聽if (@treasure.length == 0)
聽聽聽聽聽聽聽聽@bags.each_with_index do |bag, pirate|
聽聽聽聽聽聽聽聽聽聽puts "#{pirate+1}: #{bag[0].sort.inspect}"
聽聽聽聽聽聽聽聽end
聽聽聽聽聽聽else
聽聽聽聽聽聽聽聽puts "The #{@pirates} pirates won't be able to " +
聽聽聽聽聽聽聽聽聽聽聽聽聽"split their loot fairly. Take cover!"
聽聽聽聽聽聽end
聽聽聽聽聽聽exit
聽聽聽聽end
聽聽end
This method just shows the final results. If all the treasure is gone, we split
it up correctly and the shares are printed. Otherwise, we give the error
message. Either way Kernel#exit is called, because we're all finished.
That leaves only one method to examine:
聽聽聽聽# ...
聽聽聽聽
聽聽聽聽def go
聽聽聽聽聽聽done if @treasure.length == 0
聽聽聽聽聽聽gem = @treasure.pop
聽聽聽聽聽聽(0...@pirates).each do |pirate|
聽聽聽聽聽聽聽聽if @bags[pirate][1] + gem <= @share
聽聽聽聽聽聽聽聽聽聽@bags[pirate][1] += gem
聽聽聽聽聽聽聽聽聽聽@bags[pirate][0].push gem
聽聽聽聽聽聽聽聽聽聽go
聽聽聽聽聽聽聽聽聽聽@bags[pirate][0].pop
聽聽聽聽聽聽聽聽聽聽@bags[pirate][1] -= gem
聽聽聽聽聽聽聽聽聽聽# it doesn't matter which pirate is which,
聽聽聽聽聽聽聽聽聽聽# as long as their bags are empty
聽聽聽聽聽聽聽聽聽聽break if @bags[pirate][1] == 0
聽聽聽聽聽聽聽聽end
聽聽聽聽聽聽end
聽聽聽聽聽聽@treasure.push gem
聽聽聽聽end
聽聽聽聽
聽聽聽聽# ...
This method does all the work, with a brute-force recursive search. A gem is
pulled off the pile here and tried in each pirate's bag. After it is placed in
a bag, the method recurses to try the remaining treasures.
If a recursive call returns, there was no split found (because SplitIt#done
would have been called if there was). Since that is the case, the treasure is
pulled back out of the bags and replaced on the pile.
The break line is just a minor optimization. If a treasure didn't work in one
pirate's empty bag, it won't work in any of the empty bags following his and we
can skip those checks.
Since that is an exhaustive search, it will find a correct split eventually, as
long as one exists. Let's look at another variation of the same technique, this
time by Simon Kroeger:
聽聽require 'set'
聽聽
聽聽def choose_bags nr, bags, choice = Set[]
聽聽聽聽return [] if choice.size == nr
聽聽聽聽bags.each_with_index do |b, i|
聽聽聽聽聽聽c = (choice & b).empty? && choose_bags(nr, bags, choice | b)
聽聽聽聽聽聽return [i] + c if c
聽聽聽聽end && nil
聽聽end
聽聽
聽聽def split_loot nr, *treasures
聽聽聽聽each = (sum = treasures.sort!.reverse!.inject{|s, t| s + t}) / nr
聽聽聽聽return nil if (sum % nr).nonzero?
聽聽聽聽
聽聽聽聽piles = Hash.new([]).merge!({0 => [[]]})
聽聽聽聽treasures.each_with_index do |t, i|
聽聽聽聽聽聽piles.dup.each do |k, v|
聽聽聽聽聽聽聽聽if k + t <= each && k + sum >= each
聽聽聽聽聽聽聽聽聽聽v.each{|a| piles[k + t] += [a + [i]]}
聽聽聽聽聽聽聽聽end
聽聽聽聽聽聽end
聽聽聽聽聽聽sum -= t
聽聽聽聽end
聽聽聽聽return nil if piles[each].empty?
聽聽聽聽return nil if !bags = choose_bags(treasures.size, piles[each])
聽聽聽聽
聽聽聽聽piles[each].values_at(*bags).map{|b| b.map{|t| treasures[t]}}
聽聽end
聽聽
聽聽loot = split_loot(*ARGV.map{|p| p.to_i})
聽聽puts(loot ? loot.map{|a| a.join(' ')} : 'impossible!')
Here again, we see the treasures are converted from the command-line arguments
and this time they are sent to #split_loot. The first line of #split_loot sums
the treasures, and places the share total needed in a variable called each. The
second line just cancels the search if the total sum is not easily divided.
The next bit of Hash manipulation is the magic here. Each treasure is pulled
off the pile and combined with all other totals as long as the resulting total
will be less than or equal to the share goal and it will leave us enough
treasures to reach that goal.
How they are stored in the Hash is interesting. The keys are sums of shares
found thus far. The values for those sums are an Array of the shares that can
make that sum (another Array). The individual items in the shares are indexes
into the treasure pile, instead of the treasures themselves. For example, if I
feed Simon's program the problem 2 3 2 1, the piles Hash ends up as:
聽聽{ 0 => [ [] ],
聽聽聽聽2 => [ [1] ],
聽聽聽聽3 => [ [0], [1, 2] ] }
The 0 empty set split is what the code primes the Hash with, so it has something
to add to. The other two are totals it calculated. Higher totals weren't
figured because they aren't needed. And remember 0, 1, and 2 inside those sets
refer to indexes of treasures.
I want to mention one more gotcha in this code, before we move on. Note that
the default Hash object is set to an Array. Usually you want to avoid doing
that without the block form of Hash#new, because you will get the same Array
each time. However, whenever this code calls it up, it is to add it to another
Array, which returns a third Array (the combined results) that actually gets
stored in the Hash.
The rest of #split_loot makes sure a solution is still possible, tries to divide
it out with a call to #choose_bags, and finally translates all those treasure
indexes back to actual gems. (Note that Simon's use of these unique indexes,
saved him the trouble of pulling non-unique elements from an Array one-at-a-time
that was a discussion topic spawned by this problem.)
That leaves the only mystery as #choose_bags. Here's another look at that code:
聽聽require 'set'
聽聽
聽聽def choose_bags nr, bags, choice = Set[]
聽聽聽聽return [] if choice.size == nr
聽聽聽聽bags.each_with_index do |b, i|
聽聽聽聽聽聽c = (choice & b).empty? && choose_bags(nr, bags, choice | b)
聽聽聽聽聽聽return [i] + c if c
聽聽聽聽end && nil
聽聽end
聽聽
聽聽# ...
This method obviously uses the Set library to do its work. Here each bag is
tried one by one and checked that it doesn't contain any already-used elements.
That check is accomplished with a set intersection (&) test. If clear, the
method recurses to find the other bags, after performing a set union (|) with
the selected bag and all bags chosen thus far.
When the first line of the method informs us that we have used all of the
treasures, we can start passing the results back up the callstack. The result
set starts as an empty Array, but each level adds in the bag used, again by
index of the bag. If we take one more peek at the final line of #split_loot, we
will see that those bag indexes are resolved with a call to Array#values_at,
just before the results are returned:
聽聽聽聽# ...
聽聽聽聽
聽聽聽聽piles[each].values_at(*bags).map{|b| b.map{|t| treasures[t]}}
聽聽end
聽聽
聽聽# ...
Finally, one last trick is worth mentioning in #choose_bags. There is a funny
... && nil on the last line. The reason is that Enumerable#each_with_index
always returns a true value, but this method needs a false result if a bag could
not be added. You can get that by &&ing any true value with nil, or Simon could
have just returned a nil at the end of the method for identical results.
As usual, all solutions were educational. My thanks to all for the code and
excellent discussion this time around.
Tomorrow we will begin a run of at least three non-NP-complete problems by
myself and others...