prelude: | #!/usr/bin/env ruby benchmark: so_meteor_contest: | # # The Computer Language Shootout # http://shootout.alioth.debian.org # contributed by Kevin Barnes (Ruby novice) # PROGRAM: the main body is at the bottom. # 1) read about the problem here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/ # 2) see how I represent a board as a bitmask by reading the blank_board comments # 3) read as your mental paths take you def print *args end # class to represent all information about a particular rotation of a particular piece class Rotation # an array (by location) containing a bit mask for how the piece maps at the given location. # if the rotation is invalid at that location the mask will contain false attr_reader :start_masks # maps a direction to a relative location. these differ depending on whether it is an even or # odd row being mapped from @@rotation_even_adder = { :west => -1, :east => 1, :nw => -7, :ne => -6, :sw => 5, :se => 6 } @@rotation_odd_adder = { :west => -1, :east => 1, :nw => -6, :ne => -5, :sw => 6, :se => 7 } def initialize( directions ) @even_offsets, @odd_offsets = normalize_offsets( get_values( directions )) @even_mask = mask_for_offsets( @even_offsets) @odd_mask = mask_for_offsets( @odd_offsets) @start_masks = Array.new(60) # create the rotational masks by placing the base mask at the location and seeing if # 1) it overlaps the boundaries and 2) it produces a prunable board. if either of these # is true the piece cannot be placed 0.upto(59) do | offset | mask = is_even(offset) ? (@even_mask << offset) : (@odd_mask << offset) if (blank_board & mask == 0 && !prunable(blank_board | mask, 0, true)) then imask = compute_required( mask, offset) @start_masks[offset] = [ mask, imask, imask | mask ] else @start_masks[offset] = false end end end def compute_required( mask, offset ) board = blank_board 0.upto(offset) { | i | board |= 1 << i } board |= mask return 0 if (!prunable(board | mask, offset)) board = flood_fill(board,58) count = 0 imask = 0 0.upto(59) do | i | if (board[i] == 0) then imask |= (1 << i) count += 1 end end (count > 0 && count < 5) ? imask : 0 end def flood_fill( board, location) return board if (board[location] == 1) board |= 1 << location row, col = location.divmod(6) board = flood_fill( board, location - 1) if (col > 0) board = flood_fill( board, location + 1) if (col < 4) if (row % 2 == 0) then board = flood_fill( board, location - 7) if (col > 0 && row > 0) board = flood_fill( board, location - 6) if (row > 0) board = flood_fill( board, location + 6) if (row < 9) board = flood_fill( board, location + 5) if (col > 0 && row < 9) else board = flood_fill( board, location - 5) if (col < 4 && row > 0) board = flood_fill( board, location - 6) if (row > 0) board = flood_fill( board, location + 6) if (row < 9) board = flood_fill( board, location + 7) if (col < 4 && row < 9) end board end # given a location, produces a list of relative locations covered by the piece at this rotation def offsets( location) if is_even( location) then @even_offsets.collect { | value | value + location } else @odd_offsets.collect { | value | value + location } end end # returns a set of offsets relative to the top-left most piece of the rotation (by even or odd rows) # this is hard to explain. imagine we have this partial board: # 0 0 0 0 0 x [positions 0-5] # 0 0 1 1 0 x [positions 6-11] # 0 0 1 0 0 x [positions 12-17] # 0 1 0 0 0 x [positions 18-23] # 0 1 0 0 0 x [positions 24-29] # 0 0 0 0 0 x [positions 30-35] # ... # The top-left of the piece is at position 8, the # board would be passed as a set of positions (values array) containing [8,9,14,19,25] not necessarily in that # sorted order. Since that array starts on an odd row, the offsets for an odd row are: [0,1,6,11,17] obtained # by subtracting 8 from everything. Now imagine the piece shifted up and to the right so it's on an even row: # 0 0 0 1 1 x [positions 0-5] # 0 0 1 0 0 x [positions 6-11] # 0 0 1 0 0 x [positions 12-17] # 0 1 0 0 0 x [positions 18-23] # 0 0 0 0 0 x [positions 24-29] # 0 0 0 0 0 x [positions 30-35] # ... # Now the positions are [3,4,8,14,19] which after subtracting the lowest value (3) gives [0,1,5,11,16] thus, the # offsets for this particular piece are (in even, odd order) [0,1,5,11,16],[0,1,6,11,17] which is what # this function would return def normalize_offsets( values) min = values.min even_min = is_even(min) other_min = even_min ? min + 6 : min + 7 other_values = values.collect do | value | if is_even(value) then value + 6 - other_min else value + 7 - other_min end end values.collect! { | value | value - min } if even_min then [values, other_values] else [other_values, values] end end # produce a bitmask representation of an array of offset locations def mask_for_offsets( offsets ) mask = 0 offsets.each { | value | mask = mask + ( 1 << value ) } mask end # finds a "safe" position that a position as described by a list of directions can be placed # without falling off any edge of the board. the values returned a location to place the first piece # at so it will fit after making the described moves def start_adjust( directions ) south = east = 0; directions.each do | direction | east += 1 if ( direction == :sw || direction == :nw || direction == :west ) south += 1 if ( direction == :nw || direction == :ne ) end south * 6 + east end # given a set of directions places the piece (as defined by a set of directions) on the board at # a location that will not take it off the edge def get_values( directions ) start = start_adjust(directions) values = [ start ] directions.each do | direction | if (start % 12 >= 6) then start += @@rotation_odd_adder[direction] else start += @@rotation_even_adder[direction] end values += [ start ] end # some moves take you back to an existing location, we'll strip duplicates values.uniq end end # describes a piece and caches information about its rotations to as to be efficient for iteration # ATTRIBUTES: # rotations -- all the rotations of the piece # type -- a numeic "name" of the piece # masks -- an array by location of all legal rotational masks (a n inner array) for that location # placed -- the mask that this piece was last placed at (not a location, but the actual mask used) class Piece attr_reader :rotations, :type, :masks attr_accessor :placed # transform hashes that change one direction into another when you either flip or rotate a set of directions @@flip_converter = { :west => :west, :east => :east, :nw => :sw, :ne => :se, :sw => :nw, :se => :ne } @@rotate_converter = { :west => :nw, :east => :se, :nw => :ne, :ne => :east, :sw => :west, :se => :sw } def initialize( directions, type ) @type = type @rotations = Array.new(); @map = {} generate_rotations( directions ) directions.collect! { | value | @@flip_converter[value] } generate_rotations( directions ) # creates the masks AND a map that returns [location, rotation] for any given mask # this is used when a board is found and we want to draw it, otherwise the map is unused @masks = Array.new(); 0.upto(59) do | i | even = true @masks[i] = @rotations.collect do | rotation | mask = rotation.start_masks[i] @map[mask[0]] = [ i, rotation ] if (mask) mask || nil end @masks[i].compact! end end # rotates a set of directions through all six angles and adds a Rotation to the list for each one def generate_rotations( directions ) 6.times do rotations.push( Rotation.new(directions)) directions.collect! { | value | @@rotate_converter[value] } end end # given a board string, adds this piece to the board at whatever location/rotation # important: the outbound board string is 5 wide, the normal location notation is six wide (padded) def fill_string( board_string) location, rotation = @map[@placed] rotation.offsets(location).each do | offset | row, col = offset.divmod(6) board_string[ row*5 + col, 1 ] = @type.to_s end end end # a blank bit board having this form: # # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 0 0 0 0 0 1 # 1 1 1 1 1 1 # # where left lest significant bit is the top left and the most significant is the lower right # the actual board only consists of the 0 places, the 1 places are blockers to keep things from running # off the edges or bottom def blank_board 0b111111100000100000100000100000100000100000100000100000100000100000 end def full_board 0b111111111111111111111111111111111111111111111111111111111111111111 end # determines if a location (bit position) is in an even row def is_even( location) (location % 12) < 6 end # support function that create three utility maps: # $converter -- for each row an array that maps a five bit row (via array mapping) # to the a five bit representation of the bits below it # $bit_count -- maps a five bit row (via array mapping) to the number of 1s in the row # @@new_regions -- maps a five bit row (via array mapping) to an array of "region" arrays # a region array has three values the first is a mask of bits in the region, # the second is the count of those bits and the third is identical to the first # examples: # 0b10010 => [ 0b01100, 2, 0b01100 ], [ 0b00001, 1, 0b00001] # 0b01010 => [ 0b10000, 1, 0b10000 ], [ 0b00100, 1, 0b00100 ], [ 0b00001, 1, 0b00001] # 0b10001 => [ 0b01110, 3, 0b01110 ] def create_collector_support odd_map = [0b11, 0b110, 0b1100, 0b11000, 0b10000] even_map = [0b1, 0b11, 0b110, 0b1100, 0b11000] all_odds = Array.new(0b100000) all_evens = Array.new(0b100000) bit_counts = Array.new(0b100000) new_regions = Array.new(0b100000) 0.upto(0b11111) do | i | bit_count = odd = even = 0 0.upto(4) do | bit | if (i[bit] == 1) then bit_count += 1 odd |= odd_map[bit] even |= even_map[bit] end end all_odds[i] = odd all_evens[i] = even bit_counts[i] = bit_count new_regions[i] = create_regions( i) end $converter = [] 10.times { | row | $converter.push((row % 2 == 0) ? all_evens : all_odds) } $bit_counts = bit_counts $regions = new_regions.collect { | set | set.collect { | value | [ value, bit_counts[value], value] } } end # determines if a board is punable, meaning that there is no possibility that it # can be filled up with pieces. A board is prunable if there is a grouping of unfilled spaces # that are not a multiple of five. The following board is an example of a prunable board: # 0 0 1 0 0 # 0 1 0 0 0 # 1 1 0 0 0 # 0 1 0 0 0 # 0 0 0 0 0 # ... # # This board is prunable because the top left corner is only 3 bits in area, no piece will ever fit it # parameters: # board -- an initial bit board (6 bit padded rows, see blank_board for format) # location -- starting location, everything above and to the left is already full # slotting -- set to true only when testing initial pieces, when filling normally # additional assumptions are possible # # Algorithm: # The algorithm starts at the top row (as determined by location) and iterates a row at a time # maintainng counts of active open areas (kept in the collector array) each collector contains # three values at the start of an iteration: # 0: mask of bits that would be adjacent to the collector in this row # 1: the number of bits collected so far # 2: a scratch space starting as zero, but used during the computation to represent # the empty bits in the new row that are adjacent (position 0) # The exact procedure is described in-code def prunable( board, location, slotting = false) collectors = [] # loop across the rows (location / 6).to_i.upto(9) do | row_on | # obtain a set of regions representing the bits of the current row. regions = $regions[(board >> (row_on * 6)) & 0b11111] converter = $converter[row_on] # track the number of collectors at the start of the cycle so that # we don't compute against newly created collectors, only existing collectors initial_collector_count = collectors.length # loop against the regions. For each region of the row # we will see if it connects to one or more existing collectors. # if it connects to 1 collector, the bits from the region are added to the # bits of the collector and the mask is placed in collector[2] # If the region overlaps more than one collector then all the collectors # it overlaps with are merged into the first one (the others are set to nil in the array) # if NO collectors are found then the region is copied as a new collector regions.each do | region | collector_found = nil region_mask = region[2] initial_collector_count.times do | collector_num | collector = collectors[collector_num] if (collector) then collector_mask = collector[0] if (collector_mask & region_mask != 0) then if (collector_found) then collector_found[0] |= collector_mask collector_found[1] += collector[1] collector_found[2] |= collector[2] collectors[collector_num] = nil else collector_found = collector collector[1] += region[1] collector[2] |= region_mask end end end end if (collector_found == nil) then collectors.push(Array.new(region)) end end # check the existing collectors, if any collector overlapped no bits in the region its [2] value will # be zero. The size of any such reaason is tested if it is not a multiple of five true is returned since # the board is prunable. if it is a multiple of five it is removed. # Collector that are still active have a new adjacent value [0] set based n the matched bits # and have [2] cleared out for the next cycle. collectors.length.times do | collector_num | collector = collectors[collector_num] if (collector) then if (collector[2] == 0) then return true if (collector[1] % 5 != 0) collectors[collector_num] = nil else # if a collector matches all bits in the row then we can return unprunable early for the # following reasons: # 1) there can be no more unavailable bits bince we fill from the top left downward # 2) all previous regions have been closed or joined so only this region can fail # 3) this region must be good since there can never be only 1 region that is nuot # a multiple of five # this rule only applies when filling normally, so we ignore the rule if we are "slotting" # in pieces to see what configurations work for them (the only other time this algorithm is used). return false if (collector[2] == 0b11111 && !slotting) collector[0] = converter[collector[2]] collector[2] = 0 end end end # get rid of all the empty converters for the next round collectors.compact! end return false if (collectors.length <= 1) # 1 collector or less and the region is fine collectors.any? { | collector | (collector[1] % 5) != 0 } # more than 1 and we test them all for bad size end # creates a region given a row mask. see prunable for what a "region" is def create_regions( value ) regions = [] cur_region = 0 5.times do | bit | if (value[bit] == 0) then cur_region |= 1 << bit else if (cur_region != 0 ) then regions.push( cur_region) cur_region = 0; end end end regions.push(cur_region) if (cur_region != 0) regions end # find up to the counted number of solutions (or all solutions) and prints the final result def find_all find_top( 1) find_top( 0) print_results end # show the board def print_results print "#{@boards_found} solutions found\n\n" print_full_board( @min_board) print "\n" print_full_board( @max_board) print "\n" end # finds solutions. This special version of the main function is only used for the top level # the reason for it is basically to force a particular ordering on how the rotations are tested for # the first piece. It is called twice, first looking for placements of the odd rotations and then # looking for placements of the even locations. # # WHY? # Since any found solution has an inverse we want to maximize finding solutions that are not already found # as an inverse. The inverse will ALWAYS be 3 one of the piece configurations that is exactly 3 rotations away # (an odd number). Checking even vs odd then produces a higher probability of finding more pieces earlier # in the cycle. We still need to keep checking all the permutations, but our probability of finding one will # diminsh over time. Since we are TOLD how many to search for this lets us exit before checking all pieces # this bennifit is very great when seeking small numbers of solutions and is 0 when looking for more than the # maximum number def find_top( rotation_skip) board = blank_board (@pieces.length-1).times do piece = @pieces.shift piece.masks[0].each do | mask, imask, cmask | if ((rotation_skip += 1) % 2 == 0) then piece.placed = mask find( 1, 1, board | mask) end end @pieces.push(piece) end piece = @pieces.shift @pieces.push(piece) end # the normail find routine, iterates through the available pieces, checks all rotations at the current location # and adds any boards found. depth is achieved via recursion. the overall approach is described # here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/ # parameters: # start_location -- where to start looking for place for the next piece at # placed -- number of pieces placed # board -- current state of the board # # see in-code comments def find( start_location, placed, board) # find the next location to place a piece by looking for an empty bit while board[start_location] == 1 start_location += 1 end @pieces.length.times do piece = @pieces.shift piece.masks[start_location].each do | mask, imask, cmask | if ( board & cmask == imask) then piece.placed = mask if (placed == 9) then add_board else find( start_location + 1, placed + 1, board | mask) end end end @pieces.push(piece) end end # print the board def print_full_board( board_string) 10.times do | row | print " " if (row % 2 == 1) 5.times do | col | print "#{board_string[row*5 + col,1]} " end print "\n" end end # when a board is found we "draw it" into a string and then flip that string, adding both to # the list (hash) of solutions if they are unique. def add_board board_string = "99999999999999999999999999999999999999999999999999" @all_pieces.each { | piece | piece.fill_string( board_string ) } save( board_string) save( board_string.reverse) end # adds a board string to the list (if new) and updates the current best/worst board def save( board_string) if (@all_boards[board_string] == nil) then @min_board = board_string if (board_string < @min_board) @max_board = board_string if (board_string > @max_board) @all_boards.store(board_string,true) @boards_found += 1 # the exit motif is a time saver. Ideally the function should return, but those tests # take noticeable time (performance). if (@boards_found == @stop_count) then print_results exit(0) end end end ## ## MAIN BODY :) ## create_collector_support @pieces = [ Piece.new( [ :nw, :ne, :east, :east ], 2), Piece.new( [ :ne, :se, :east, :ne ], 7), Piece.new( [ :ne, :east, :ne, :nw ], 1), Piece.new( [ :east, :sw, :sw, :se ], 6), Piece.new( [ :east, :ne, :se, :ne ], 5), Piece.new( [ :east, :east, :east, :se ], 0), Piece.new( [ :ne, :nw, :se, :east, :se ], 4), Piece.new( [ :se, :se, :se, :west ], 9), Piece.new( [ :se, :se, :east, :se ], 8), Piece.new( [ :east, :east, :sw, :se ], 3) ]; @all_pieces = Array.new( @pieces) @min_board = "99999999999999999999999999999999999999999999999999" @max_board = "00000000000000000000000000000000000000000000000000" @stop_count = ARGV[0].to_i || 2089 @all_boards = {} @boards_found = 0 find_all ######## DO IT!!! loop_count: 1