3 # The Computer Language Shootout
4 # http://shootout.alioth.debian.org
5 # contributed by Kevin Barnes (Ruby novice)
7 # PROGRAM: the main body is at the bottom.
8 # 1) read about the problem here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/
9 # 2) see how I represent a board as a bitmask by reading the blank_board comments
10 # 3) read as your mental paths take you
15 # class to represent all information about a particular rotation of a particular piece
17 # an array (by location) containing a bit mask for how the piece maps at the given location.
18 # if the rotation is invalid at that location the mask will contain false
19 attr_reader :start_masks
21 # maps a direction to a relative location. these differ depending on whether it is an even or
22 # odd row being mapped from
23 @@rotation_even_adder = { :west => -1, :east => 1, :nw => -7, :ne => -6, :sw => 5, :se => 6 }
24 @@rotation_odd_adder = { :west => -1, :east => 1, :nw => -6, :ne => -5, :sw => 6, :se => 7 }
26 def initialize( directions )
27 @even_offsets, @odd_offsets = normalize_offsets( get_values( directions ))
29 @even_mask = mask_for_offsets( @even_offsets)
30 @odd_mask = mask_for_offsets( @odd_offsets)
32 @start_masks = Array.new(60)
34 # create the rotational masks by placing the base mask at the location and seeing if
35 # 1) it overlaps the boundries and 2) it produces a prunable board. if either of these
36 # is true the piece cannot be placed
37 0.upto(59) do | offset |
38 mask = is_even(offset) ? (@even_mask << offset) : (@odd_mask << offset)
39 if (blank_board & mask == 0 && !prunable(blank_board | mask, 0, true)) then
40 imask = compute_required( mask, offset)
41 @start_masks[offset] = [ mask, imask, imask | mask ]
43 @start_masks[offset] = false
48 def compute_required( mask, offset )
50 0.upto(offset) { | i | board |= 1 << i }
52 return 0 if (!prunable(board | mask, offset))
53 board = flood_fill(board,58)
57 if (board[i] == 0) then
62 (count > 0 && count < 5) ? imask : 0
65 def flood_fill( board, location)
66 return board if (board[location] == 1)
67 board |= 1 << location
68 row, col = location.divmod(6)
69 board = flood_fill( board, location - 1) if (col > 0)
70 board = flood_fill( board, location + 1) if (col < 4)
71 if (row % 2 == 0) then
72 board = flood_fill( board, location - 7) if (col > 0 && row > 0)
73 board = flood_fill( board, location - 6) if (row > 0)
74 board = flood_fill( board, location + 6) if (row < 9)
75 board = flood_fill( board, location + 5) if (col > 0 && row < 9)
77 board = flood_fill( board, location - 5) if (col < 4 && row > 0)
78 board = flood_fill( board, location - 6) if (row > 0)
79 board = flood_fill( board, location + 6) if (row < 9)
80 board = flood_fill( board, location + 7) if (col < 4 && row < 9)
85 # given a location, produces a list of relative locations covered by the piece at this rotation
86 def offsets( location)
87 if is_even( location) then
88 @even_offsets.collect { | value | value + location }
90 @odd_offsets.collect { | value | value + location }
94 # returns a set of offsets relative to the top-left most piece of the rotation (by even or odd rows)
95 # this is hard to explain. imagine we have this partial board:
96 # 0 0 0 0 0 x [positions 0-5]
97 # 0 0 1 1 0 x [positions 6-11]
98 # 0 0 1 0 0 x [positions 12-17]
99 # 0 1 0 0 0 x [positions 18-23]
100 # 0 1 0 0 0 x [positions 24-29]
101 # 0 0 0 0 0 x [positions 30-35]
103 # The top-left of the piece is at position 8, the
104 # board would be passed as a set of positions (values array) containing [8,9,14,19,25] not necessarily in that
105 # sorted order. Since that array starts on an odd row, the offsets for an odd row are: [0,1,6,11,17] obtained
106 # by subtracting 8 from everything. Now imagine the piece shifted up and to the right so it's on an even row:
107 # 0 0 0 1 1 x [positions 0-5]
108 # 0 0 1 0 0 x [positions 6-11]
109 # 0 0 1 0 0 x [positions 12-17]
110 # 0 1 0 0 0 x [positions 18-23]
111 # 0 0 0 0 0 x [positions 24-29]
112 # 0 0 0 0 0 x [positions 30-35]
114 # Now the positions are [3,4,8,14,19] which after subtracting the lowest value (3) gives [0,1,5,11,16] thus, the
115 # offsets for this particular piece are (in even, odd order) [0,1,5,11,16],[0,1,6,11,17] which is what
116 # this function would return
117 def normalize_offsets( values)
119 even_min = is_even(min)
120 other_min = even_min ? min + 6 : min + 7
121 other_values = values.collect do | value |
122 if is_even(value) then
123 value + 6 - other_min
125 value + 7 - other_min
128 values.collect! { | value | value - min }
131 [values, other_values]
133 [other_values, values]
137 # produce a bitmask representation of an array of offset locations
138 def mask_for_offsets( offsets )
140 offsets.each { | value | mask = mask + ( 1 << value ) }
144 # finds a "safe" position that a position as described by a list of directions can be placed
145 # without falling off any edge of the board. the values returned a location to place the first piece
146 # at so it will fit after making the described moves
147 def start_adjust( directions )
149 directions.each do | direction |
150 east += 1 if ( direction == :sw || direction == :nw || direction == :west )
151 south += 1 if ( direction == :nw || direction == :ne )
156 # given a set of directions places the piece (as defined by a set of directions) on the board at
157 # a location that will not take it off the edge
158 def get_values ( directions )
159 start = start_adjust(directions)
161 directions.each do | direction |
162 if (start % 12 >= 6) then
163 start += @@rotation_odd_adder[direction]
165 start += @@rotation_even_adder[direction]
170 # some moves take you back to an existing location, we'll strip duplicates
175 # describes a piece and caches information about its rotations to as to be efficient for iteration
177 # rotations -- all the rotations of the piece
178 # type -- a numeic "name" of the piece
179 # masks -- an array by location of all legal rotational masks (a n inner array) for that location
180 # placed -- the mask that this piece was last placed at (not a location, but the actual mask used)
182 attr_reader :rotations, :type, :masks
183 attr_accessor :placed
185 # transform hashes that change one direction into another when you either flip or rotate a set of directions
186 @@flip_converter = { :west => :west, :east => :east, :nw => :sw, :ne => :se, :sw => :nw, :se => :ne }
187 @@rotate_converter = { :west => :nw, :east => :se, :nw => :ne, :ne => :east, :sw => :west, :se => :sw }
189 def initialize( directions, type )
191 @rotations = Array.new();
194 generate_rotations( directions )
195 directions.collect! { | value | @@flip_converter[value] }
196 generate_rotations( directions )
198 # creates the masks AND a map that returns [location, rotation] for any given mask
199 # this is used when a board is found and we want to draw it, otherwise the map is unused
200 @masks = Array.new();
203 @masks[i] = @rotations.collect do | rotation |
204 mask = rotation.start_masks[i]
205 @map[mask[0]] = [ i, rotation ] if (mask)
212 # rotates a set of directions through all six angles and adds a Rotation to the list for each one
213 def generate_rotations( directions )
215 rotations.push( Rotation.new(directions))
216 directions.collect! { | value | @@rotate_converter[value] }
220 # given a board string, adds this piece to the board at whatever location/rotation
221 # important: the outbound board string is 5 wide, the normal location notation is six wide (padded)
222 def fill_string( board_string)
223 location, rotation = @map[@placed]
224 rotation.offsets(location).each do | offset |
225 row, col = offset.divmod(6)
226 board_string[ row*5 + col, 1 ] = @type.to_s
231 # a blank bit board having this form:
245 # where left lest significant bit is the top left and the most significant is the lower right
246 # the actual board only consists of the 0 places, the 1 places are blockers to keep things from running
247 # off the edges or bottom
249 0b111111100000100000100000100000100000100000100000100000100000100000
253 0b111111111111111111111111111111111111111111111111111111111111111111
256 # determines if a location (bit position) is in an even row
257 def is_even( location)
261 # support function that create three utility maps:
262 # $converter -- for each row an array that maps a five bit row (via array mapping)
263 # to the a a five bit representation of the bits below it
264 # $bit_count -- maps a five bit row (via array mapping) to the number of 1s in the row
265 # @@new_regions -- maps a five bit row (via array mapping) to an array of "region" arrays
266 # a region array has three values the first is a mask of bits in the region,
267 # the second is the count of those bits and the third is identical to the first
269 # 0b10010 => [ 0b01100, 2, 0b01100 ], [ 0b00001, 1, 0b00001]
270 # 0b01010 => [ 0b10000, 1, 0b10000 ], [ 0b00100, 1, 0b00100 ], [ 0b00001, 1, 0b00001]
271 # 0b10001 => [ 0b01110, 3, 0b01110 ]
272 def create_collector_support
273 odd_map = [0b11, 0b110, 0b1100, 0b11000, 0b10000]
274 even_map = [0b1, 0b11, 0b110, 0b1100, 0b11000]
276 all_odds = Array.new(0b100000)
277 all_evens = Array.new(0b100000)
278 bit_counts = Array.new(0b100000)
279 new_regions = Array.new(0b100000)
280 0.upto(0b11111) do | i |
281 bit_count = odd = even = 0
283 if (i[bit] == 1) then
286 even |= even_map[bit]
291 bit_counts[i] = bit_count
292 new_regions[i] = create_regions( i)
296 10.times { | row | $converter.push((row % 2 == 0) ? all_evens : all_odds) }
297 $bit_counts = bit_counts
298 $regions = new_regions.collect { | set | set.collect { | value | [ value, bit_counts[value], value] } }
301 # determines if a board is punable, meaning that there is no possibility that it
302 # can be filled up with pieces. A board is prunable if there is a grouping of unfilled spaces
303 # that are not a multiple of five. The following board is an example of a prunable board:
311 # This board is prunable because the top left corner is only 3 bits in area, no piece will ever fit it
313 # board -- an initial bit board (6 bit padded rows, see blank_board for format)
314 # location -- starting location, everything above and to the left is already full
315 # slotting -- set to true only when testing initial pieces, when filling normally
316 # additional assumptions are possible
319 # The algorithm starts at the top row (as determined by location) and iterates a row at a time
320 # maintainng counts of active open areas (kept in the collector array) each collector contains
321 # three values at the start of an iteration:
322 # 0: mask of bits that would be adjacent to the collector in this row
323 # 1: the number of bits collected so far
324 # 2: a scratch space starting as zero, but used during the computation to represent
325 # the empty bits in the new row that are adjacent (position 0)
326 # The exact procedure is described in-code
327 def prunable( board, location, slotting = false)
329 # loop accross the rows
330 (location / 6).to_i.upto(9) do | row_on |
331 # obtain a set of regions representing the bits of the curent row.
332 regions = $regions[(board >> (row_on * 6)) & 0b11111]
333 converter = $converter[row_on]
335 # track the number of collectors at the start of the cycle so that
336 # we don't compute against newly created collectors, only existing collectors
337 initial_collector_count = collectors.length
339 # loop against the regions. For each region of the row
340 # we will see if it connects to one or more existing collectors.
341 # if it connects to 1 collector, the bits from the region are added to the
342 # bits of the collector and the mask is placed in collector[2]
343 # If the region overlaps more than one collector then all the collectors
344 # it overlaps with are merged into the first one (the others are set to nil in the array)
345 # if NO collectors are found then the region is copied as a new collector
346 regions.each do | region |
347 collector_found = nil
348 region_mask = region[2]
349 initial_collector_count.times do | collector_num |
350 collector = collectors[collector_num]
352 collector_mask = collector[0]
353 if (collector_mask & region_mask != 0) then
354 if (collector_found) then
355 collector_found[0] |= collector_mask
356 collector_found[1] += collector[1]
357 collector_found[2] |= collector[2]
358 collectors[collector_num] = nil
360 collector_found = collector
361 collector[1] += region[1]
362 collector[2] |= region_mask
367 if (collector_found == nil) then
368 collectors.push(Array.new(region))
372 # check the existing collectors, if any collector overlapped no bits in the region its [2] value will
373 # be zero. The size of any such reaason is tested if it is not a muliple of five true is returned since
374 # the board is prunable. if it is a multiple of five it is removed.
375 # Collector that are still active have a new adjacent value [0] set based n the matched bits
376 # and have [2] cleared out for the next cycle.
377 collectors.length.times do | collector_num |
378 collector = collectors[collector_num]
380 if (collector[2] == 0) then
381 return true if (collector[1] % 5 != 0)
382 collectors[collector_num] = nil
384 # if a collector matches all bits in the row then we can return unprunable early for the
386 # 1) there can be no more unavailable bits bince we fill from the top left downward
387 # 2) all previous regions have been closed or joined so only this region can fail
388 # 3) this region must be good since there can never be only 1 region that is nuot
390 # this rule only applies when filling normally, so we ignore the rule if we are "slotting"
391 # in pieces to see what configurations work for them (the only other time this algorithm is used).
392 return false if (collector[2] == 0b11111 && !slotting)
393 collector[0] = converter[collector[2]]
399 # get rid of all the empty converters for the next round
402 return false if (collectors.length <= 1) # 1 collector or less and the region is fine
403 collectors.any? { | collector | (collector[1] % 5) != 0 } # more than 1 and we test them all for bad size
406 # creates a region given a row mask. see prunable for what a "region" is
407 def create_regions( value )
411 if (value[bit] == 0) then
412 cur_region |= 1 << bit
414 if (cur_region != 0 ) then
415 regions.push( cur_region)
420 regions.push(cur_region) if (cur_region != 0)
424 # find up to the counted number of solutions (or all solutions) and prints the final result
433 print "#{@boards_found} solutions found\n\n"
434 print_full_board( @min_board)
436 print_full_board( @max_board)
440 # finds solutions. This special version of the main function is only used for the top level
441 # the reason for it is basically to force a particular ordering on how the rotations are tested for
442 # the first piece. It is called twice, first looking for placements of the odd rotations and then
443 # looking for placements of the even locations.
446 # Since any found solution has an inverse we want to maximize finding solutions that are not already found
447 # as an inverse. The inverse will ALWAYS be 3 one of the piece configurations that is exactly 3 rotations away
448 # (an odd number). Checking even vs odd then produces a higher probability of finding more pieces earlier
449 # in the cycle. We still need to keep checking all the permutations, but our probability of finding one will
450 # diminsh over time. Since we are TOLD how many to search for this lets us exit before checking all pieces
451 # this bennifit is very great when seeking small numbers of solutions and is 0 when looking for more than the
453 def find_top( rotation_skip)
455 (@pieces.length-1).times do
456 piece = @pieces.shift
457 piece.masks[0].each do | mask, imask, cmask |
458 if ((rotation_skip += 1) % 2 == 0) then
460 find( 1, 1, board | mask)
465 piece = @pieces.shift
469 # the normail find routine, iterates through the available pieces, checks all rotations at the current location
470 # and adds any boards found. depth is acheived via recursion. the overall approach is described
471 # here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/
473 # start_location -- where to start looking for place for the next piece at
474 # placed -- number of pieces placed
475 # board -- current state of the board
477 # see in-code comments
478 def find( start_location, placed, board)
479 # find the next location to place a piece by looking for an empty bit
480 while board[start_location] == 1
484 @pieces.length.times do
485 piece = @pieces.shift
486 piece.masks[start_location].each do | mask, imask, cmask |
487 if ( board & cmask == imask) then
489 if (placed == 9) then
492 find( start_location + 1, placed + 1, board | mask)
501 def print_full_board( board_string)
503 print " " if (row % 2 == 1)
505 print "#{board_string[row*5 + col,1]} "
511 # when a board is found we "draw it" into a string and then flip that string, adding both to
512 # the list (hash) of solutions if they are unique.
514 board_string = "99999999999999999999999999999999999999999999999999"
515 @all_pieces.each { | piece | piece.fill_string( board_string ) }
517 save( board_string.reverse)
520 # adds a board string to the list (if new) and updates the current best/worst board
521 def save( board_string)
522 if (@all_boards[board_string] == nil) then
523 @min_board = board_string if (board_string < @min_board)
524 @max_board = board_string if (board_string > @max_board)
525 @all_boards.store(board_string,true)
528 # the exit motif is a time saver. Ideally the function should return, but those tests
529 # take noticable time (performance).
530 if (@boards_found == @stop_count) then
541 create_collector_support
543 Piece.new( [ :nw, :ne, :east, :east ], 2),
544 Piece.new( [ :ne, :se, :east, :ne ], 7),
545 Piece.new( [ :ne, :east, :ne, :nw ], 1),
546 Piece.new( [ :east, :sw, :sw, :se ], 6),
547 Piece.new( [ :east, :ne, :se, :ne ], 5),
548 Piece.new( [ :east, :east, :east, :se ], 0),
549 Piece.new( [ :ne, :nw, :se, :east, :se ], 4),
550 Piece.new( [ :se, :se, :se, :west ], 9),
551 Piece.new( [ :se, :se, :east, :se ], 8),
552 Piece.new( [ :east, :east, :sw, :se ], 3)
555 @all_pieces = Array.new( @pieces)
557 @min_board = "99999999999999999999999999999999999999999999999999"
558 @max_board = "00000000000000000000000000000000000000000000000000"
559 @stop_count = ARGV[0].to_i || 2089
563 find_all ######## DO IT!!!