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
12 # class to represent all information about a particular rotation of a particular piece
14 # an array (by location) containing a bit mask for how the piece maps at the given location.
15 # if the rotation is illegal at that location the mask will contain false
16 attr_reader
:start_masks
18 # maps a direction to a relative location. these differ depending on whether it is an even or
19 # odd row being mapped from
20 @
@rotation_even_adder = { :west => -1, :east => 1, :nw => -7, :ne => -6, :sw => 5, :se => 6 }
21 @
@rotation_odd_adder = { :west => -1, :east => 1, :nw => -6, :ne => -5, :sw => 6, :se => 7 }
23 def initialize( directions
)
24 @even_offsets, @odd_offsets = normalize_offsets( get_values( directions
))
26 @even_mask = mask_for_offsets( @even_offsets)
27 @odd_mask = mask_for_offsets( @odd_offsets)
29 @start_masks = Array
.new(60)
31 # create the rotational masks by placing the base mask at the location and seeing if
32 # 1) it overlaps the boundries and 2) it produces a prunable board. if either of these
33 # is true the piece cannot be placed
34 0.upto(59) do | offset
|
35 mask
= is_even(offset
) ? (@even_mask << offset
) : (@odd_mask << offset
)
36 if (blank_board
& mask
== 0 && !prunable(blank_board
| mask
, 0, true)) then
37 imask
= compute_required( mask
, offset
)
38 @start_masks[offset
] = [ mask
, imask
, imask
| mask
]
40 @start_masks[offset
] = false
45 def compute_required( mask
, offset
)
47 0.upto(offset
) { | i
| board
|= 1 << i
}
49 return 0 if (!prunable(board
| mask
, offset
))
50 board
= flood_fill(board
,58)
54 if (board
[i
] == 0) then
59 (count
> 0 && count
< 5) ? imask
: 0
62 def flood_fill( board
, location
)
63 return board
if (board
[location
] == 1)
64 board
|= 1 << location
65 row
, col
= location
.divmod(6)
66 board
= flood_fill( board
, location
- 1) if (col
> 0)
67 board
= flood_fill( board
, location
+ 1) if (col
< 4)
68 if (row
% 2 == 0) then
69 board
= flood_fill( board
, location
- 7) if (col
> 0 && row
> 0)
70 board
= flood_fill( board
, location
- 6) if (row
> 0)
71 board
= flood_fill( board
, location
+ 6) if (row
< 9)
72 board
= flood_fill( board
, location
+ 5) if (col
> 0 && row
< 9)
74 board
= flood_fill( board
, location
- 5) if (col
< 4 && row
> 0)
75 board
= flood_fill( board
, location
- 6) if (row
> 0)
76 board
= flood_fill( board
, location
+ 6) if (row
< 9)
77 board
= flood_fill( board
, location
+ 7) if (col
< 4 && row
< 9)
82 # given a location, produces a list of relative locations covered by the piece at this rotation
83 def offsets( location
)
84 if is_even( location
) then
85 @even_offsets.collect
{ | value
| value
+ location
}
87 @odd_offsets.collect
{ | value
| value
+ location
}
91 # returns a set of offsets relative to the top-left most piece of the rotation (by even or odd rows)
92 # this is hard to explain. imagine we have this partial board:
93 # 0 0 0 0 0 x [positions 0-5]
94 # 0 0 1 1 0 x [positions 6-11]
95 # 0 0 1 0 0 x [positions 12-17]
96 # 0 1 0 0 0 x [positions 18-23]
97 # 0 1 0 0 0 x [positions 24-29]
98 # 0 0 0 0 0 x [positions 30-35]
100 # The top-left of the piece is at position 8, the
101 # board would be passed as a set of positions (values array) containing [8,9,14,19,25] not necessarily in that
102 # sorted order. Since that array starts on an odd row, the offsets for an odd row are: [0,1,6,11,17] obtained
103 # by subtracting 8 from everything. Now imagine the piece shifted up and to the right so it's on an even row:
104 # 0 0 0 1 1 x [positions 0-5]
105 # 0 0 1 0 0 x [positions 6-11]
106 # 0 0 1 0 0 x [positions 12-17]
107 # 0 1 0 0 0 x [positions 18-23]
108 # 0 0 0 0 0 x [positions 24-29]
109 # 0 0 0 0 0 x [positions 30-35]
111 # Now the positions are [3,4,8,14,19] which after subtracting the lowest value (3) gives [0,1,5,11,16] thus, the
112 # offsets for this particular piece are (in even, odd order) [0,1,5,11,16],[0,1,6,11,17] which is what
113 # this function would return
114 def normalize_offsets( values
)
116 even_min
= is_even(min
)
117 other_min
= even_min
? min
+ 6 : min
+ 7
118 other_values
= values
.collect
do | value
|
119 if is_even(value
) then
120 value
+ 6 - other_min
122 value
+ 7 - other_min
125 values
.collect
! { | value
| value
- min
}
128 [values
, other_values
]
130 [other_values
, values
]
134 # produce a bitmask representation of an array of offset locations
135 def mask_for_offsets( offsets
)
137 offsets
.each
{ | value
| mask
= mask
+ ( 1 << value
) }
141 # finds a "safe" position that a position as described by a list of directions can be placed
142 # without falling off any edge of the board. the values returned a location to place the first piece
143 # at so it will fit after making the described moves
144 def start_adjust( directions
)
146 directions
.each
do | direction
|
147 east
+= 1 if ( direction
== :sw || direction
== :nw || direction
== :west )
148 south
+= 1 if ( direction
== :nw || direction
== :ne )
153 # given a set of directions places the piece (as defined by a set of directions) on the board at
154 # a location that will not take it off the edge
155 def get_values ( directions
)
156 start
= start_adjust(directions
)
158 directions
.each
do | direction
|
159 if (start
% 12 >= 6) then
160 start
+= @
@rotation_odd_adder[direction
]
162 start
+= @
@rotation_even_adder[direction
]
167 # some moves take you back to an existing location, we'll strip duplicates
172 # describes a piece and caches information about its rotations to as to be efficient for iteration
174 # rotations -- all the rotations of the piece
175 # type -- a numeic "name" of the piece
176 # masks -- an array by location of all legal rotational masks (a n inner array) for that location
177 # placed -- the mask that this piece was last placed at (not a location, but the actual mask used)
179 attr_reader
:rotations, :type, :masks
180 attr_accessor
:placed
182 # transform hashes that change one direction into another when you either flip or rotate a set of directions
183 @
@flip_converter = { :west => :west, :east => :east, :nw => :sw, :ne => :se, :sw => :nw, :se => :ne }
184 @
@rotate_converter = { :west => :nw, :east => :se, :nw => :ne, :ne => :east, :sw => :west, :se => :sw }
186 def initialize( directions
, type
)
188 @rotations = Array
.new();
191 generate_rotations( directions
)
192 directions
.collect
! { | value
| @
@flip_converter[value
] }
193 generate_rotations( directions
)
195 # creates the masks AND a map that returns [location, rotation] for any given mask
196 # this is used when a board is found and we want to draw it, otherwise the map is unused
197 @masks = Array
.new();
200 @masks[i
] = @rotations.collect
do | rotation
|
201 mask
= rotation
.start_masks
[i
]
202 @map[mask
[0]] = [ i
, rotation
] if (mask
)
209 # rotates a set of directions through all six angles and adds a Rotation to the list for each one
210 def generate_rotations( directions
)
212 rotations
.push( Rotation
.new(directions
))
213 directions
.collect
! { | value
| @
@rotate_converter[value
] }
217 # given a board string, adds this piece to the board at whatever location/rotation
218 # important: the outbound board string is 5 wide, the normal location notation is six wide (padded)
219 def fill_string( board_string
)
220 location
, rotation
= @map[@placed]
221 rotation
.offsets(location
).each
do | offset
|
222 row
, col
= offset
.divmod(6)
223 board_string
[ row
*5 + col
, 1 ] = @type.to_s
228 # a blank bit board having this form:
242 # where left lest significant bit is the top left and the most significant is the lower right
243 # the actual board only consists of the 0 places, the 1 places are blockers to keep things from running
244 # off the edges or bottom
246 0b111111100000100000100000100000100000100000100000100000100000100000
250 0b111111111111111111111111111111111111111111111111111111111111111111
253 # determines if a location (bit position) is in an even row
254 def is_even( location
)
258 # support function that create three utility maps:
259 # @@converter -- for each row an array that maps a five bit row (via array mapping)
260 # to the a a five bit representation of the bits below it
261 # @@bit_count -- maps a five bit row (via array mapping) to the number of 1s in the row
262 # @@new_regions -- maps a five bit row (via array mapping) to an array of "region" arrays
263 # a region array has three values the first is a mask of bits in the region,
264 # the second is the count of those bits and the third is identical to the first
266 # 0b10010 => [ 0b01100, 2, 0b01100 ], [ 0b00001, 1, 0b00001]
267 # 0b01010 => [ 0b10000, 1, 0b10000 ], [ 0b00100, 1, 0b00100 ], [ 0b00001, 1, 0b00001]
268 # 0b10001 => [ 0b01110, 3, 0b01110 ]
269 def create_collector_support
270 odd_map
= [0b11, 0b110, 0b1100, 0b11000, 0b10000]
271 even_map
= [0b1, 0b11, 0b110, 0b1100, 0b11000]
273 all_odds
= Array
.new(0b100000)
274 all_evens
= Array
.new(0b100000)
275 bit_counts
= Array
.new(0b100000)
276 new_regions
= Array
.new(0b100000)
277 0.upto(0b11111) do | i
|
278 bit_count
= odd
= even
= 0
280 if (i
[bit
] == 1) then
283 even
|= even_map
[bit
]
288 bit_counts
[i
] = bit_count
289 new_regions
[i
] = create_regions( i
)
293 10.times
{ | row
| @
@converter.push((row
% 2 == 0) ? all_evens
: all_odds
) }
294 @
@bit_counts = bit_counts
295 @
@regions = new_regions
.collect
{ | set
| set
.collect
{ | value
| [ value
, bit_counts
[value
], value
] } }
298 # determines if a board is punable, meaning that there is no possibility that it
299 # can be filled up with pieces. A board is prunable if there is a grouping of unfilled spaces
300 # that are not a multiple of five. The following board is an example of a prunable board:
308 # This board is prunable because the top left corner is only 3 bits in area, no piece will ever fit it
310 # board -- an initial bit board (6 bit padded rows, see blank_board for format)
311 # location -- starting location, everything above and to the left is already full
312 # slotting -- set to true only when testing initial pieces, when filling normally
313 # additional assumptions are possible
316 # The algorithm starts at the top row (as determined by location) and iterates a row at a time
317 # maintainng counts of active open areas (kept in the collector array) each collector contains
318 # three values at the start of an iteration:
319 # 0: mask of bits that would be adjacent to the collector in this row
320 # 1: the number of bits collected so far
321 # 2: a scratch space starting as zero, but used during the computation to represent
322 # the empty bits in the new row that are adjacent (position 0)
323 # The exact procedure is described in-code
324 def prunable( board
, location
, slotting
= false)
326 # loop accross the rows
327 (location
/ 6).to_i
.upto(9) do | row_on
|
328 # obtain a set of regions representing the bits of the curent row.
329 regions
= @
@regions[(board
>> (row_on
* 6)) & 0b11111]
330 converter
= @
@converter[row_on
]
332 # track the number of collectors at the start of the cycle so that
333 # we don't compute against newly created collectors, only existing collectors
334 initial_collector_count
= collectors
.length
336 # loop against the regions. For each region of the row
337 # we will see if it connects to one or more existing collectors.
338 # if it connects to 1 collector, the bits from the region are added to the
339 # bits of the collector and the mask is placed in collector[2]
340 # If the region overlaps more than one collector then all the collectors
341 # it overlaps with are merged into the first one (the others are set to nil in the array)
342 # if NO collectors are found then the region is copied as a new collector
343 regions
.each
do | region
|
344 collector_found
= nil
345 region_mask
= region
[2]
346 initial_collector_count
.times
do | collector_num
|
347 collector
= collectors
[collector_num
]
349 collector_mask
= collector
[0]
350 if (collector_mask
& region_mask
!= 0) then
351 if (collector_found
) then
352 collector_found
[0] |= collector_mask
353 collector_found
[1] += collector
[1]
354 collector_found
[2] |= collector
[2]
355 collectors
[collector_num
] = nil
357 collector_found
= collector
358 collector
[1] += region
[1]
359 collector
[2] |= region_mask
364 if (collector_found
== nil) then
365 collectors
.push(Array
.new(region
))
369 # check the existing collectors, if any collector overlapped no bits in the region its [2] value will
370 # be zero. The size of any such reaason is tested if it is not a muliple of five true is returned since
371 # the board is prunable. if it is a multiple of five it is removed.
372 # Collector that are still active have a new adjacent value [0] set based n the matched bits
373 # and have [2] cleared out for the next cycle.
374 collectors
.length
.times
do | collector_num
|
375 collector
= collectors
[collector_num
]
377 if (collector
[2] == 0) then
378 return true if (collector
[1] % 5 != 0)
379 collectors
[collector_num
] = nil
381 # if a collector matches all bits in the row then we can return unprunable early for the
383 # 1) there can be no more unavailable bits bince we fill from the top left downward
384 # 2) all previous regions have been closed or joined so only this region can fail
385 # 3) this region must be good since there can never be only 1 region that is nuot
387 # this rule only applies when filling normally, so we ignore the rule if we are "slotting"
388 # in pieces to see what configurations work for them (the only other time this algorithm is used).
389 return false if (collector
[2] == 0b11111 && !slotting
)
390 collector
[0] = converter
[collector
[2]]
396 # get rid of all the empty converters for the next round
399 return false if (collectors
.length
<= 1) # 1 collector or less and the region is fine
400 collectors
.any
? { | collector
| (collector
[1] % 5) != 0 } # more than 1 and we test them all for bad size
403 # creates a region given a row mask. see prunable for what a "region" is
404 def create_regions( value
)
408 if (value
[bit
] == 0) then
409 cur_region
|= 1 << bit
411 if (cur_region
!= 0 ) then
412 regions
.push( cur_region
)
417 regions
.push(cur_region
) if (cur_region
!= 0)
421 # find up to the counted number of solutions (or all solutions) and prints the final result
430 print
"#{@boards_found} solutions found\n\n"
431 print_full_board( @min_board)
433 print_full_board( @max_board)
437 # finds solutions. This special version of the main function is only used for the top level
438 # the reason for it is basically to force a particular ordering on how the rotations are tested for
439 # the first piece. It is called twice, first looking for placements of the odd rotations and then
440 # looking for placements of the even locations.
443 # Since any found solution has an inverse we want to maximize finding solutions that are not already found
444 # as an inverse. The inverse will ALWAYS be 3 one of the piece configurations that is exactly 3 rotations away
445 # (an odd number). Checking even vs odd then produces a higher probability of finding more pieces earlier
446 # in the cycle. We still need to keep checking all the permutations, but our probability of finding one will
447 # diminsh over time. Since we are TOLD how many to search for this lets us exit before checking all pieces
448 # this bennifit is very great when seeking small numbers of solutions and is 0 when looking for more than the
450 def find_top( rotation_skip
)
452 (@pieces.length-1
).times
do
453 piece
= @pieces.shift
454 piece
.masks
[0].each
do | mask
, imask
, cmask
|
455 if ((rotation_skip
+= 1) % 2 == 0) then
457 find( 1, 1, board
| mask
)
462 piece
= @pieces.shift
466 # the normail find routine, iterates through the available pieces, checks all rotations at the current location
467 # and adds any boards found. depth is acheived via recursion. the overall approach is described
468 # here: http://www-128.ibm.com/developerworks/java/library/j-javaopt/
470 # start_location -- where to start looking for place for the next piece at
471 # placed -- number of pieces placed
472 # board -- current state of the board
474 # see in-code comments
475 def find( start_location
, placed
, board
)
476 # find the next location to place a piece by looking for an empty bit
477 while board
[start_location
] == 1
481 @pieces.length
.times
do
482 piece
= @pieces.shift
483 piece
.masks
[start_location
].each
do | mask
, imask
, cmask
|
484 if ( board
& cmask
== imask
) then
486 if (placed
== 9) then
489 find( start_location
+ 1, placed
+ 1, board
| mask
)
498 def print_full_board( board_string
)
500 print
" " if (row
% 2 == 1)
502 print
"#{board_string[row*5 + col,1]} "
508 # when a board is found we "draw it" into a string and then flip that string, adding both to
509 # the list (hash) of solutions if they are unique.
511 board_string
= "99999999999999999999999999999999999999999999999999"
512 @all_pieces.each
{ | piece
| piece
.fill_string( board_string
) }
514 save( board_string
.reverse
)
517 # adds a board string to the list (if new) and updates the current best/worst board
518 def save( board_string
)
519 if (@all_boards[board_string
] == nil) then
520 @min_board = board_string
if (board_string
< @min_board)
521 @max_board = board_string
if (board_string
> @max_board)
522 @all_boards.store(board_string
,true)
525 # the exit motif is a time saver. Ideally the function should return, but those tests
526 # take noticable time (performance).
527 if (@boards_found == @stop_count) then
538 create_collector_support
540 Piece
.new( [ :nw, :ne, :east, :east ], 2),
541 Piece
.new( [ :ne, :se, :east, :ne ], 7),
542 Piece
.new( [ :ne, :east, :ne, :nw ], 1),
543 Piece
.new( [ :east, :sw, :sw, :se ], 6),
544 Piece
.new( [ :east, :ne, :se, :ne ], 5),
545 Piece
.new( [ :east, :east, :east, :se ], 0),
546 Piece
.new( [ :ne, :nw, :se, :east, :se ], 4),
547 Piece
.new( [ :se, :se, :se, :west ], 9),
548 Piece
.new( [ :se, :se, :east, :se ], 8),
549 Piece
.new( [ :east, :east, :sw, :se ], 3)
552 @all_pieces = Array
.new( @pieces)
554 @min_board = "99999999999999999999999999999999999999999999999999"
555 @max_board = "00000000000000000000000000000000000000000000000000"
556 @stop_count = ARGV[0].to_i
|| 2089
560 find_all
######## DO IT!!!