My solution so far.
I use 'BoxSorter' to keep all the boxes in a hash indexed by both
dimensions so its easy to find one that fits. The 'TrunkSet' keeps
track of the loaded boxes using arrays of characters.
The algorithm is kind of the opposite of Ilmari's: it iterates over
empty spaces in the trunk, looking for boxes that fit, instead of
iterating over the boxes.
-Adam
-----------------------------------------------------------------
require 'delegate'
class Box
def initialize w,h
@w,@h=w,h
end
def [] n
n==0 ? @w :@h
end
def rotate!
@w,@h = @h,@w
end
def other n
n==@h ? @w :@h
end
def <=> box2 #compares the non-matching dimension
if @w == box2[0] then @h <=> box2[1]
elsif @w== box2[1] then @h <=> box2[0]
elsif @h == box2[0] then @w <=> box2[1]
else @w <=> box2[0]
end
end
end
class BoxSorter
def initialize boxset
@h = Hash.new
boxset.each{|b|@h[b[0]]||=[];@h[b[0]]<<b;@h[b[1]]||=[];@h[b[1]]<<b; @h}
@h.each {|k,v| (v.sort!||v).reverse!}
# hash has key for each box side,
# containing array of boxes sorted by size of the other side
end
def size
@h.size
end
def find_best_fit w,h
while w>0
set = @h[w]
box = set.find{|b| b.other(w)<=h } if set
if box
self.remove box
box.rotate! if box[0] != w
return box
end
w-=1;
end
end
def remove box
@h.delete_if {|k,v| v.delete(box); v.empty? }
end
end
class TrunkSet < DelegateClass(Array)
def initialize w,h
@width,@height=w,h
super []
grow
end
def grow
@openrow=0
self<< Array.new(@height){Array.new(@width){" "}}
end
def first_open_row
loop do
break if last[@openrow].find{|s| s==' '}
grow if @height == (@openrow +=1)
end
last[@openrow]
end
def first_open_space
gaps,lastchar = [],nil
first_open_row.each_with_index do |c,i|
if c==' '
if c==lastchar then gaps[-1][0]+=1
else gaps << [1,i]; end
end
lastchar = c
end
gaps.max
end
def pad_out
last[@openrow].map!{|c| if c==' ' then '+' else c end }
first_open_row
end
def add_box box, col
size,height = box[0],box[1]
(0..height).each do |row|
fillchar = (row == height) ? ['+','-'] : ['|','#']
if nil != (fillrow = last[@openrow+row])
fillrow[col-1] = fillchar[0] if (col-1>=0 )
size.times {|i| fillrow[col+i] = fillchar[1] }
fillrow[col+size] = fillchar[0] if ( col+size < @width )
end
end
end
def rows_remaining
@height-@openrow
end
def has_no_boxes?
last.each{|r| return false if r.find{|c| c == '#'} }
true
end
end
class Packer
def initialize size, boxes
@loose_boxes = BoxSorter.new(boxes)
@trunks = TrunkSet.new(*size)
end
def pack_a_box
column,nextbox = nil,nil
loop do
space_available,column = @trunks.first_open_space
nextbox = @loose_boxes.find_best_fit(space_available,
@trunks.rows_remaining)
break if nextbox
@trunks.pad_out #if no box fits, need to fill row with pads
end
@trunks.add_box(nextbox,column)
end
def pack
until @loose_boxes.size == 0
pack_a_box
end
(@trunks.rows_remaining).times { @trunks.pad_out }
end
def show
@trunks.pop if @trunks.has_no_boxes?
@trunks.each do |bin|
bin.each { |row|puts row.join }
puts ""
end
puts "#{@trunks.size} loads"
end
end
class PackerParser
attr_reader :binsize, :blocks
def initialize file
@binsize = file.readline.chomp
@blocks = file.readline.chomp.split " "
end
def size
@binsize.match(/(\d*)x(\d*)/)
[$1.to_i,$2.to_i]
end
def boxes
@blocks.map{|s| s.match(/(\d*)x(\d*)/);Box.new($1.to_i,$2.to_i)}
end
end
if __FILE__ == $0
pp = PackerParser.new(ARGF)
puts pp.binsize
puts pp.blocks.join(' ')
pk = Packer.new(pp.size, pp.boxes)
pk.pack
pk.show
end