# This is a 2D fit-first greedy heuristic algorithm that tries to fit each box into the trunk, largest first. If no box fits,
# a new trunk is created.
#
# I'm representing trunks as 2D tables of squares (with one unit of hidden padding around), then fill it starting
# from top left. The fitter-first finds a row with enough empty space to fit the box and then checks if the next box-height
# rows also contain the space. If they do, the box is drawn to the rows.
# Printing is easy because the trunk already is in printable format.
# Not a particularly fast way to do it, mind you :)
#
# If you're interested about different algorithms for solving the 2D bin packing problem,
# here's a paper:http://www.dei.unipd.it/~fisch/ricop/tesi/tesi_dottorato_Lodi_1999.pdf
class Array
#delete the first item (box) found. We don't want to use
#delete() because of any other boxes of the same size
def delete_first item
delete_at index(item)
end
#If a box is [10, 14] this will return a copy in the form
#of [14, 10] effectivley flipping it
def rotate
d = dup
d.push d.shift
d
end
end #end Array class
class Trunk
def initialize(w,h)
@w = w
@h = h
@items = []
@rows = (1..@h+2).map{ "_"*(@w+2) }
end #end initialize
#try adding the box both ways, if either fits we assume its now
#in the Trunk and it will be deleted from the list of boxes
def add box
try_adding(box) or try_adding(box.rotate)
end #end add
def try_adding box
#create a new box row, expanding on both sides. If you expand the Trunk and all the boxes, you can
#essentially forget about the padding, as long as you remember to strip it out later. Also note that boxrow
#is built as a String of "_" characters, so it will be used to find empty Trunk space.
boxrow = "_"*(box[0]+2)
@rows.each_with_index do |r,i|
#break out of the loop as soon as we run out of enough @rows to hold the box (and 2 extra padding @rows)
break if i > @rows.size - (box[1]+2)
#verify that this row holds the box, or move on
next unless r.include?(boxrow)
#fetch the index where the box would fit on @rows needed to hold it below this one
idxs = @rows[i+1, box[1]+1].map{|s| s.index boxrow }
#verify that we got an index for all needed lines, or move on.
next unless idxs.all?
#find the highest of those indices.
idx = idxs.max
#verify that the box does fit on all needed @rows at the highest found index, or move on
next unless @rows[i, box[1]+2].all?{|s| s[idx,boxrow.size] == boxrow }
#if we made it this far, we have a match, so we will draw the box in place. We can
#ignore the padding now, since we already proved there is room.
@rows[i+1, box[1]].each{|s| s[idx+1, box[0]] = "#" * box[0] }
#add the box to the @items, so we know the Trunk isn't empty?(), and return the box to indicate
#a match
@items.push box
return box
end #end each_with_index
nil #default false return, if we didn't find a match
end #end try_adding
#simple... trunk has no items in it
def empty?
@items.empty?
end #end empty?
#this formatting method is called before the trunk is printed.
#the extea space is stripped out of the trunk here
def to_s
@rows[1..-2].map{|r|r[1..-2]}.join("\n")
end #end to_s
end #end Trunk class
###
# Get the trunk size and the box sizes, then split them on "x" and put
# into arrays like this:
# trunk = [10, 20]
# boxes = [[1, 3], [4, 8], [3, 7], [9, 4], [10, 14]]
###
# trunk = "10x20".strip.split("x").map{|i| i.to_i}
# boxes = "1x3 4x8 3x7 9x4 10x14".strip.split(" ").map{|s| s.split("x").map{|i| i.to_i} }
trunk = gets.strip.split("x").map{|i| i.to_i}
boxes = gets.strip.split(" ").map{|s| s.split("x").map{|i| i.to_i} }
#sort the boxes from largest to smallest based on their area (w*h)
boxes = boxes.sort_by{|b| b.inject{|f,i| f*i} }.reverse
#create a new trunk object inside an array. Use of the splat operator on the "trunk"
#variable which separates out the array into width and height
trunks = [Trunk.new(*trunk)]
#while there are boxes this loop will try to fit them
#into the last Trunk. We are going through the boxes from largest to smallest
#and last Trunk will always be the one with the most open space. If a big box doesn't
#have room there, all the smaller boxes will be tried by find()
until boxes.empty?
#try to add the box to the last trunk, if it doesn't go we get nil
fitting = boxes.find{|box| trunks.last.add box }
#if the box fit
if fitting
#remove the box from the boxes that need to be placed
boxes.delete_first fitting
#if the last trunk was empty
elsif trunks.last.empty?
#raise an error because the box is too big to fit into the Trunk
#and a solution is impossible
raise "Can't fit #{boxes.inspect} into the trunk"
#box didn't fit but the last trunk did have items in it
else
#make a new trunk and try to fit it again
trunks << Trunk.new(*trunk) unless boxes.empty?
end #end if fitting
end #end until
#print out the array of Trunk objects, note that to_s gets called
#on each Trunk before it is printed, and we have a custom def for that.
puts
puts trunks.join("\n\n~~~~~~~~~~~~~~~~~~~~~~~~~~~\n\n")

ACCESSION NUMBER: [ABSTRACT: ADA391201] [DTIC HANDLE TO PDF FULL TEXT]
The Distributer's Three-Dimensional Pallet-Packing Problem: A Human Intelligence-Based Heuristic Approach
MAR 2001 :: Baltacioglu, Erhan :: AIR FORCE INST OF TECH WRIGHT-PATTERSON AFB OH
Synchronicity! Of all the random things, I realized just today that an important aspect of a problem I've been working on for a couple of days is in fact a specialized case of a one-dimensional bin packing problem!