[QUIZ] Pen and Paper (#90)

R

Rick DeNatale

Here's my solution for the nonce <G>.

I've probably been focusing more on packaging this and less on
optimizing it than I should.

The approach is a recursive search.
0. Place the first number
1. Pick the best available direction and place the next number there.
2. Try to place the rest of the numbers (by recursive call to step 2)
3. If 2 fails , remove the number placed in step 1, pick the next
direction and try again.
4. If we run out of directions, report failure to the calling recursion.

Picking the best available direction is based on Eliott's suggested
heuristic of trying to use squares which have less available next
steps first.

I did this in two files. pen_and_paper.rb is the main program, it
does parameter parsing, and can benchmark or profile the code as
dictated by the parameters. Right now, it only searches for a
solution starting in row 0, column 1. This should be a parameter and
I should also try alternatives if the starting position doesn't work.

The second file ruby_quiz_90.rb is a module containing the classes
Board, Player and Move.

Most of the time actually still seems to be spent in checking whether
a square is occupied or not. My initial stab at this did bounds
checking since ruby Arrays just return nil for out of bounds indices.
The current iteration uses 0 instead of nil in empty cells and uses
guard rows above and below the board in order to avoid bounds
checking. I might next try eliminating the guard rows and just define
[] for NilClass to return nil.

Anyway, here's what I've got now
=== pen_and_paper.rb ===
require 'benchmark'
include Benchmark

require 'optparse'

require 'ruby_quiz_90'

board_size = 5
benchmark = false
profile = false
iterations = 1

opts = OptionParser.new
opts.on('-sBOARDSIZE',
'-s BOARDSIZE',
'--size=BOARDSIZE',
'--size BOARDSIZE',
'Height and width of board, default is 5x5',
Integer) {|val| board_size = val }
opts.on('--bench',
'Specifies that a benchmark will be done.'
) { | val | benchmark = true }

opts.on('--profile',
'Profile execution') { profile = true}

opts.on('--iterations=COUNT',
'--iterations COUNT',
'COUNT specifies the number of iterations',
'for benchmarking or profiling',
Integer) { | val | iterations = val }

opts.on('--help',
'produce this help text') { puts opts.to_s
exit }

begin
opts.parse(ARGV)
rescue
puts opts.to_s
exit
end

puts "board_size is #{board_size}x#{board_size}"
puts "benchmark=#{benchmark}, profile=#{profile}, iterations=#{iterations}"

player = nil

if benchmark
bm(iterations) do | test |
test.report("#{board_size}x#{board_size}:") do
player = RubyQuiz90::player.new(board_size)
player.play(0,1)
end
end
else
if profile
require 'profile'
end
iterations.times do
player = RubyQuiz90::player.new(board_size)
player.play(0,1)
end
end
puts player.board
=== ruby_quiz_90.rb ===
module RubyQuiz90
class Board

# Changes:
# pad row with 3 guard rows above and below
# this allows available? to avoid having to catch
# nil not understanding []
def initialize(size)
@row = Array.new(size+6)
(0..2).each { | i | @row = Array.new(size) }
(3..size+2).each { | i | @row = Array.new(size, 0) }
(size+3..size+5).each { | i | @row = Array.new(size) }
@size = size
# @range = (0..size-1)
end

def take(row, col, value)
@row[row+3][col] = value
end

def available?(row, col)
@row[row+3][col] == 0 rescue false
end

def take_back(row, col)
@row[row+3][col] = 0
end

def value(row, col)
@row[row+3][col]
end

def to_s
elem_width = ((@size * @size).to_s.length) + 1
header = '+-' + ('-' *(@size * elem_width)) + '+'
result = header.dup
(0...@size).each do | i |
row = @row[i+3]
result << "\n|"
row.each do | elem |
result << ((elem == 0) ?
(" " * elem_width) :
sprintf("%#{elem_width}d", elem))
end
result << ' |'
end
result << "\n"
result << header
end

def self.testboard(s)
b = new(s)
(0..s-1).each do | r |
(0..s-1).each do | c |
b.take(r, c, c + 1 + (r*s))
end
end
b
end

end

class Player
Direction_vectors = [
[0, 3], #move east
[2, 2], #move southeast
[3, 0], #move south
[2, -2], #move southwest
[0, -3], #move west
[-2, -2], #move northwest
[-3, -3], #move north
[-2, 2], #move northeast
]

# No_more_moves = Direction_vectors.length
#
# Last_move = (No_more_moves) - 1

def initialize(size, debug = 2)
@board = RubyQuiz90::Board.new(size)
@last = size * size
@debug = debug
end

def play (start_row=nil, start_col=nil)
row = start_row || rand(@board.size)
col = start_col || rand(@board.size)
@board.take(row, col, 1)
fill_all_from(start_row, start_col)
end

def board
@board
end

# Fill the board after the last number was placed at r,c
# If the board can be filled return true
# If the board cannot be filled restore the board to its
# initial state at the time of this call and return false
def fill_all_from(r, c)

value = @board.value(r,c) + 1

puts "Trying to fill board starting with #{value}, from #{r},
#{c}}" if @debug > 2
puts @board if @debug >= 3
return true if value > @last # our work is done!

#now try to fill the next value
optimized_moves(r, c).each do | move |
row = move.row
col = move.col
puts "Trying to move placing #{value} at #{row}, #{col}" if @debug > 2
@board.take(row, col, value)
puts "Placed #{value} at #{row}, #{col}" if @debug > 2
if fill_all_from(row, col)
return true
else
@board.take_back(row, col)
end
end

# if we get here, it's time to back out
puts "All trials failed at #{value}" if @debug > 2
puts @board if @debug > 2
return false
end
# return a list of moves from row, col optimized so that
# squares with more possible further moves come first
def optimized_moves(row, col)
moves = []
Direction_vectors.each do | dx, dy |
r = row + dy
c = col + dx
if @board.available?(r,c)
moves << Move.new(r, c, availability_count(r, c))
end
end
moves.sort!
moves
end

# return the number of available squares from row, col
def availability_count(row, col)
Direction_vectors.inject(0) do | sum, (dx, dy)|
@board.available?(row+dy, col+dx) ? sum + 1 : sum
end

end
end

class Move
attr_accessor :row, :col, :count
def initialize(r, c, count)
@row = r
@col = c
@Count = count
end

# a move is greater (should come later) if it has a lower
# available move count than another
def <=>(move)
count - move.count
end

def to_s
"Count=#{@count}, row=#{@row}, col=#{@col}"
end
end

end
 
J

James Edward Gray II

In all fairness, I really would love to go back and add the solver,
but I am very, very busy this weekend. :( We will see if I can
steal the time.

I found the time. Here are some fun timings with it:

$ time ruby grid_fill.rb -s 19
------------------------------------------------------------------------
-----
| 203 229 224 204 230 223 209 233 220 210 234 219 211 235 160 194 236
161 167 |
| 268 174 201 34 173 200 35 172 199 352 171 198 312 170 197 163 169
196 164 |
| 227 205 38 228 225 60 231 222 69 232 221 70 159 218 71 238 166
193 237 |
| 202 33 269 207 36 55 208 353 54 213 311 351 212 310 313 195 309
162 168 |
| 267 175 226 61 39 89 84 59 216 81 68 217 72 75 240 348 74
239 165 |
| 270 206 37 56 97 65 53 98 66 354 99 79 158 350 78 307 314
192 308 |
| 49 32 40 90 85 58 215 88 83 214 156 82 241 361 73 242 333
347 243 |
| 266 176 110 62 52 111 128 64 116 80 67 355 100 76 319 349 77
306 315 |
| 271 91 50 57 96 134 86 107 155 87 342 3 157 343 4 360 317
191 334 |
| 48 31 41 112 129 63 115 130 127 17 119 104 320 1 103 303 332
346 244 |
| 265 177 109 135 51 108 136 133 117 106 154 356 101 6 318 344 5
305 316 |
| 272 92 45 28 95 143 126 18 148 131 341 2 120 302 331 359 283
190 335 |
| 47 30 42 113 124 138 114 123 137 16 118 105 321 357 102 304 329
345 245 |
| 264 178 26 144 44 27 149 132 340 11 153 293 338 7 284 301 336
250 282 |
| 273 93 46 29 94 142 125 19 147 122 322 15 121 297 330 358 254
189 328 |
| 182 24 43 290 25 139 291 12 150 292 339 10 285 294 337 251 281
300 246 |
| 263 179 21 145 261 20 146 141 323 14 152 296 325 8 257 298 327
249 255 |
| 274 289 183 275 288 184 276 287 185 277 286 186 278 252 187 279 253
188 280 |
| 181 23 262 180 22 140 260 13 151 259 324 9 258 295 326 248 256
299 247 |
------------------------------------------------------------------------
-----

real 0m0.013s
user 0m0.007s
sys 0m0.005s
$ time ruby grid_fill.rb -s 19
------------------------------------------------------------------------
-----
| 282 308 303 283 309 302 288 312 299 289 313 298 290 314 239 273 315
240 246 |
| 347 253 280 113 252 279 114 251 278 70 250 277 30 249 276 242 248
275 243 |
| 306 284 117 307 304 139 310 301 148 311 300 149 238 297 150 317 245
272 316 |
| 281 112 348 286 115 134 287 71 133 292 29 69 291 28 31 274 27
241 247 |
| 346 254 305 140 118 168 163 138 295 160 147 296 151 154 319 66 153
318 244 |
| 349 285 116 135 176 144 132 177 145 72 178 158 237 68 157 25 32
271 26 |
| 128 111 119 169 164 137 294 167 162 293 235 161 320 79 152 321 51
65 322 |
| 345 255 189 141 131 190 207 143 195 159 146 73 179 155 37 67 156
24 33 |
| 350 170 129 136 175 213 165 186 234 166 60 82 236 61 83 78 35
270 52 |
| 127 110 120 191 208 142 194 209 206 96 198 183 38 80 182 21 50
64 323 |
| 344 256 188 214 130 187 215 212 196 185 233 74 180 85 36 62 84
23 34 |
| 351 171 124 107 174 222 205 97 227 210 59 81 199 20 49 77 1
269 53 |
| 126 109 121 192 203 217 193 202 216 95 197 184 39 75 181 22 47
63 324 |
| 343 257 105 223 123 106 228 211 58 90 232 11 56 86 2 19 54
329 361 |
| 352 172 125 108 173 221 204 98 226 201 40 94 200 15 48 76 333
268 46 |
| 261 103 122 8 104 218 9 91 229 10 57 89 3 12 55 330 360
18 325 |
| 342 258 100 224 340 99 225 220 41 93 231 14 43 87 336 16 45
328 334 |
| 353 7 262 354 6 263 355 5 264 356 4 265 357 331 266 358 332
267 359 |
| 260 102 341 259 101 219 339 92 230 338 42 88 337 13 44 327 335
17 326 |
------------------------------------------------------------------------
-----

real 0m0.012s
user 0m0.007s
sys 0m0.005s
$ time ruby grid_fill.rb -s 19
------------------------------------------------------------------------
-----
| 139 165 160 140 166 159 145 169 156 146 170 155 147 171 96 130 172
97 103 |
| 204 110 137 331 109 136 332 108 135 288 107 134 248 106 133 99 105
132 100 |
| 163 141 335 164 161 357 167 158 5 168 157 6 95 154 7 174 102
129 173 |
| 138 330 205 143 333 352 144 289 351 149 247 287 148 246 249 131 245
98 104 |
| 203 111 162 358 336 25 20 356 152 17 4 153 8 11 176 284 10
175 101 |
| 206 142 334 353 33 1 350 34 2 290 35 15 94 286 14 243 250
128 244 |
| 346 329 337 26 21 355 151 24 19 150 92 18 177 297 9 178 269
283 179 |
| 202 112 46 359 349 47 64 361 52 16 3 291 36 12 255 285 13
242 251 |
| 207 27 347 354 32 70 22 43 91 23 278 300 93 279 301 296 253
127 270 |
| 345 328 338 48 65 360 51 66 63 314 55 40 256 298 39 239 268
282 180 |
| 201 113 45 71 348 44 72 69 53 42 90 292 37 303 254 280 302
241 252 |
| 208 28 342 325 31 79 62 315 84 67 277 299 56 238 267 295 219
126 271 |
| 344 327 339 49 60 74 50 59 73 313 54 41 257 293 38 240 265
281 181 |
| 200 114 323 80 341 324 85 68 276 308 89 229 274 304 220 237 272
186 218 |
| 209 29 343 326 30 78 61 316 83 58 258 312 57 233 266 294 190
125 264 |
| 118 321 340 226 322 75 227 309 86 228 275 307 221 230 273 187 217
236 182 |
| 199 115 318 81 197 317 82 77 259 311 88 232 261 305 193 234 263
185 191 |
| 210 225 119 211 224 120 212 223 121 213 222 122 214 188 123 215 189
124 216 |
| 117 320 198 116 319 76 196 310 87 195 260 306 194 231 262 184 192
235 183 |
------------------------------------------------------------------------
-----

real 0m0.012s
user 0m0.007s
sys 0m0.005s

It even always gets the bonus points for the circular solution.
<laughs>

OK, I admit finding circular solutions on the big boards sucks. ;)

Here's the code:

#!/usr/bin/env ruby -w

require "enumerator"

class PenAndPaperGame
def self.circular_solutions
@circular ||= if File.exist?("circular_solutions.dump")
File.open("circular_solutions.dump") { |file| Marshal.load
(file) }
else
Array.new
end
end

def initialize(size)
@size = size
@largest = @size * @size

@grid = Array.new(@largest)
end

def solve
if self.class.circular_solutions[@size].nil?
solve_manually
else
@grid = self.class.circular_solutions[@size]
offset = @grid[rand(@grid.size)]
@grid.map! { |n| (n + offset) % @largest + 1 }
to_s
end
end

def solve_manually
x, y = rand(@size), rand(@size)
count = mark(x, y)

loop do
to = jumps(x, y)
return self.class.new(@size).solve_manually if to.empty?

scores = rate_jumps(to)
low = scores.min
next_jump = to.enum_for:)each_with_index).select do |jump|
scores[jump.last] == low
end.sort_by { rand }.first.first

count = mark(*(next_jump + [count]))
x, y = next_jump

if count > @largest
if circular?
self.class.circular_solutions[@size] = @grid
File.open("circular_solutions.dump", "w") do |file|
Marshal.dump(self.class.circular_solutions, file)
end
return to_s
else
puts "Found this solution:"
puts to_s
puts "Continuing search for a circular solution..."
return self.class.new(@size).solve_manually
end
end
end
end

def to_s
width = @largest.to_s.size
border = " -" + (["-" * width] * @size).join("-") + "- \n"

border +
@grid.enum_for:)each_slice, @size).inject(String.new) do |grid,
row|
grid + "| " + row.map { |n| n.to_s.center(width) }.join(" ") +
" |\n"
end +
border
end

private

def at(x, y)
x + y * @size
end

def mark(current_x, current_y, mark = 1)
@grid[at(current_x, current_y)] = mark
mark + 1
end

def jumps(from_x, from_y, grid = @grid)
[ [-3, 0],
[3, 0],
[0, -3],
[0, 3],
[2, 2],
[-2, 2],
[2, -2],
[-2, -2] ].map do |jump|
[from_x + jump.first, from_y + jump.last]
end.select do |jump|
jump.all? { |to| (0...@size).include? to } and grid[at
(*jump)].nil?
end
end

def rate_jumps(choices)
choices.map { |jump| jumps(*jump).size }
end

def circular?
grid = @grid.dup
grid[grid.index(@largest)] = nil

x, y = grid.index(1).divmod(@size).reverse

not jumps(x, y, grid).empty?
end
end

if __FILE__ == $PROGRAM_NAME
size = ARGV.first && ARGV.first =~ /\A-s(?:ize)?\Z/ ?
ARGV.last.to_i : 5
puts PenAndPaperGame.new(size).solve
end

__END__

James Edward Gray II
 
M

Mitchell Koch

--9amGYk9869ThD9tj
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

Here's my solution. It starts in the middle of the board and hops to
one of the next spaces that has a large amount of moves that can be
made from there. It's not all that fast though, I didn't really spend
too much time optimizing.

Mitchell Koch

--9amGYk9869ThD9tj
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="penandpaper.rb"

#!/usr/bin/env ruby
# Pen and Paper

class PenAndPaper
MOVES = {
:n => Proc.new { |x,y| [x,y-3] },
:ne => Proc.new { |x,y| [x+2,y-2] },
:e => Proc.new { |x,y| [x+3,y] },
:se => Proc.new { |x,y| [x+2,y+2] },
:s => Proc.new { |x,y| [x,y+3] },
:sw => Proc.new { |x,y| [x-2,y+2] },
:w => Proc.new { |x,y| [x-3,y] },
:nw => Proc.new { |x,y| [x-2,y-2] }
}

def initialize(n)
@n = n
end

def solve(method=:lookfill)
begin
$stderr.print '.'
@board = Board.new(@n)
@count = 1
send(method)
end until @board.complete?
$stderr.puts
@board
end

def randfill
move(rand(@n),rand(@n))
until valid_moves.empty?
dir = valid_moves.sort_by{rand}.first
move(*new_pos(@pos, dir))
end
end

def lookfill
move(@n/2, @n/2)
until valid_moves.empty?
dir = valid_moves.sort_by{rand}.sort do |a,b|
valid_moves(new_pos(@pos,a)).size <=>
valid_moves(new_pos(@pos,b)).size
end.first
move(*new_pos(@pos, dir))
end
end

def move(x,y)
@pos = [x,y]
@board[x,y] = @count
@count += 1
end

def valid_moves(start=@pos)
MOVES.keys.select {|d| valid_pos(*new_pos(start,d))}
end

def valid_pos(x,y)
0 <= x && x < @n && 0 <= y && y < @n && @board[x,y] == '.'
end

def new_pos(start, dir)
MOVES[dir].call(*start)
end
end

class Board
def initialize(x, y=x)
@x, @y = x, y
@arr = (1..y).inject([]) { |m,n| m << ['.']*x }
end

def []=(x, y, val)
@arr[y][x] = val
end

def [](x, y)
@arr[y][x]
end

def complete?
@arr.each do |row|
row.each do |space|
return false if space == '.'
end
end
true
end

def to_s
numdigits = 1
@arr.each do |row|
row.each do |num|
digits = num.to_s.size
numdigits = digits if digits > numdigits
end
end
s = ''
bookend = '+' + '-'*((numdigits+1)*@x+1) + "+\n"
s << bookend
@y.times do |y|
s << '|'
s << @arr[y].inject('') { |m,n| m << " %#{numdigits}s"%n }
s << " |\n"
end
s << bookend
end
end

if __FILE__ == $0
n = ARGV[0] ? ARGV[0].to_i : 5
puts PenAndPaper.new(n).solve
end

--9amGYk9869ThD9tj--
 
M

Morton Goldberg

This is my submission for Ruby Quiz #90.

My solution's best feature is that it's based on a simple search
algorithm and that's also its worst feature. The randomized depth-
first search I use is easy to understand, was easy to code, and
required almost no debugging. But it has O(e**n) worst-case behavior.
Which means it works well enough for small n, but it quickly becomes
intolerable as n grows. When does it begin to suck? Depends on how
patient you are. I ran out of patience at n = 7.

The search is programmed to terminate as soon as it finds one, cyclic
solution. Of course, from any cyclic solution, you can generate n**2
- 1 additional cyclic solutions by applying the included Grid#shift
method for n = 1 to 35.

My solution's second best feature is that it's short. Even with a
fair amount of commenting, it comes in at less than 200 lines.

Here are some results from running it with n = 5 and n = 6.

~/Projects/Ruby/Ruby Quiz mg: time ./quiz_90.rb
------------------------
| 1 16 19 2 15 |
| 11 22 5 12 21 |
| 18 8 25 17 7 |
| 4 13 20 3 14 |
| 10 23 6 9 24 |
------------------------
real 0m0.095s
user 0m0.053s
sys 0m0.014s

~/Projects/Ruby/Ruby Quiz mg: time ./quiz_90.rb
------------------------
| 1 21 6 9 20 |
| 14 11 3 17 12 |
| 5 8 25 22 7 |
| 2 18 13 10 19 |
| 15 23 4 16 24 |
------------------------
real 0m0.048s
user 0m0.017s
sys 0m0.011s

~/Projects/Ruby/Ruby Quiz mg: time ./quiz_90.rb
------------------------
| 1 13 21 18 12 |
| 6 16 24 9 4 |
| 22 19 2 14 20 |
| 25 10 5 17 11 |
| 7 15 23 8 3 |
------------------------
real 0m0.166s
user 0m0.108s
sys 0m0.014s

~/Projects/Ruby/Ruby Quiz mg: time ./quiz_90.rb
----------------------------
| 1 15 22 36 5 23 |
| 26 9 19 25 10 18 |
| 21 29 6 16 30 35 |
| 2 14 11 33 4 24 |
| 27 8 20 28 7 17 |
| 12 32 3 13 31 34 |
----------------------------
real 0m3.004s
user 0m2.827s
sys 0m0.034s

~/Projects/Ruby/Ruby Quiz mg: time ./quiz_90.rb
----------------------------
| 1 8 31 36 7 30 |
| 27 13 16 24 12 17 |
| 20 35 2 9 32 3 |
| 15 23 28 14 6 29 |
| 26 10 19 25 11 18 |
| 21 34 5 22 33 4 |
----------------------------
real 1m18.337s
user 1m12.836s
sys 0m0.567s

~/Projects/Ruby/Ruby Quiz mg: time ./quiz_90.rb
----------------------------
| 1 15 29 36 16 28 |
| 9 6 18 26 7 19 |
| 22 33 11 14 30 35 |
| 2 25 8 5 17 27 |
| 10 13 21 34 12 20 |
| 23 32 3 24 31 4 |
----------------------------
real 0m1.126s
user 0m1.027s
sys 0m0.021s

Notice the wide variation in run times. You have to be lucky to get a
really short run. This suggests some interesting statistical questions:

1. What is the distribution of run times as a function of n?

2. What is the mean number of backtracking events over all the
possible searches as a function of n?

3. What is the probability that a search proceeds to a solution with
at most k backtracks as a function of n?

I don't know the answer to any of these questions, but I would like to.

<code>
#! /usr/bin/ruby -w
# Author: Morton Goldberg
#
# Date: August 15, 2006
#
# Ruby Quiz #90 -- Pen and Paper Game

# A grid is a matrix of cells.
class Grid

def initialize(dims)
@rows, @cols = dims
@size = @rows * @cols
@grid = Array.new(@rows) {|i| Array.new(@cols) {|j| Cell.new
(i, j)}}
end

# Return a deep copy.
def copy
result = Grid.new(dimensions)
result.grid.each_with_index do |row, i|
row.each_with_index {|cell, j| cell.val = self[i, j].val}
end
result
end

# Shifts the values of the cells in the grid by <offset> under the
# constraint that values are folded into the range 1..@size.
def shift!(offset)
@grid.each do |row|
row.each do |cell|
val = (cell.val + offset) % @size
cell.val = (val == 0 ? @size : val)
end
end
self
end

# Return the dimensions of the grid.
def dimensions
[@rows, @cols]
end

# Return the cell at positon [row, col].
# Call as <grid-name>[row, col]
def [](*args)
row, col = args
@grid[row][col]
end

# Assigns a cell to the positon [row, col].
# Call as <grid-name>[row, col] = cell
def []=(*args)
row, col, cell = args
@grid[row][col] = cell
end

# Format the grid as a bordered table.
def to_s
rule = '-' * (4 * @cols + 4) + "\n"
grid_str = ""
@grid.each do |row|
grid_str << row.inject("| ") do |row_str, cell|
row_str << ("%2d " % cell.val)
end
grid_str << "|\n"
end
"" << rule << grid_str << rule
end

attr_reader :rows, :cols, :size, :grid

end

# A path is an array of cells, where no two cells occupy the same
location
# in some grid. A complete path fills the grid.
class Path < Array

# Make a deep copy of a path.
def copy
result = Path.new
each {|cell| result << cell.dup}
result
end

# Sequentially number the cells in the path.
def number!
each_with_index {|cell, i| cell.val = i + 1}
self
end

# Report whether or not a path is cyclic.
def cyclic?
p0, p1 = self[0], self[-1]
delta = [(p1.row - p0.row).abs, (p1.col - p0.col).abs]
delta == [3, 0] || delta == [0, 3] || delta == [2, 2]
end

# Make a grid from a path.
# Warning: if the path isn't complete, the resulting grid wont't
# represent a solution.
def to_grid(size)
result = Grid.new([size, size])
each {|cell| result[cell.row, cell.col] = cell}
result
end

end

# A cell is a simple object that knows its value and its position in
# a grid. It also encodes the game's movement rule.
class Cell

def initialize(row, col, val=0)
@row, @col = row, col
@val = val
end

# Return a list of targets -- an array containing all the cells
in the
# grid reachable from this cell in one step.
def targets(grid)
size = grid.rows
result = []
result << grid[@row, @col - 3] if @col - 3 >= 0 # north
result << grid[@row + 2, @col - 2] \
if @row + 2 < size && @col - 2 >= 0 # northeast
result << grid[@row + 3, @col] if @row + 3 < size # east
result << grid[@row + 2, @col + 2] \
if @row + 2 < size && @col + 2 < size # southeast
result << grid[@row, @col + 3] if @col + 3 < size # south
result << grid[@row - 2, @col + 2] \
if @row - 2 >= 0 && @col + 2 < size # southwest
result << grid[@row - 3, @col] if @row - 3 >= 0 # west
result << grid[@row - 2, @col - 2] \
if @row - 2 >= 0 && @col - 2 >= 0 # northwest
result
end

attr_accessor :row, :col, :val

end

# A solver uses a depth-first search to find one solution for a
square grid # of a given size.
class Solver

def initialize(size)
@size = size
@solution = nil
@test_grid = Grid.new([@size, @size])
cell = @test_grid[0, 0]
targets = cell.targets(@test_grid)
goals = targets.dup
@backtrack_chain = [[Path.new << cell, targets, goals]]
end

# Return a new link for the backtrack chain if it can be extended;
# otherwise, return nil.
def next_link
path, targets, goals = @backtrack_chain.last
return nil if targets.empty? || goals.empty?
# Here is where the randomization takes place.
cell = targets[rand(targets.size)]
next_path = path.dup << cell
# Restricts target list to accessible cells not already on the
path.
next_targets = cell.targets(@test_grid) - path
next_goals = goals.dup
next_goals.delete(cell) if goals.include?(cell)
# Make sure cell won't be picked again if backtrack occurs.
targets.delete(cell)
[next_path, next_targets, next_goals]
end

# The algorithm is a randomized depth-first search.
def solve
final_size = @size * @size
loop do
link = next_link
if link then
@backtrack_chain.push(link)
else
@solution = @backtrack_chain.pop.first
break if @solution.size == final_size
if @backtrack_chain.empty? then
raise(RuntimeError, "No solution for #@size x #@size
grid")
end
end
end
@solution.number!
end

attr_reader :solution

end

SIZE = 5
solver = Solver.new(SIZE)
puts solver.solve.to_grid(SIZE)
</code>

Regards, Morton
 
M

Morton Goldberg

Just want to correct a misstatement. The worse-case behavior is
actually O(e**n**2), which better explains why it gets bad so fast.

Regards, Morton
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,755
Messages
2,569,536
Members
45,008
Latest member
HaroldDark

Latest Threads

Top