doku 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/Gemfile ADDED
@@ -0,0 +1,32 @@
1
+ source "http://rubygems.org"
2
+
3
+ # This gem uses new features of ruby:
4
+ # require_relative
5
+ # Enumerator
6
+ #
7
+ # To get access to these features in older versions, we use the
8
+ # backports gem:
9
+ gem "backports", :platforms => :ruby_18
10
+
11
+ # For development gem, these gems are recommended:
12
+ group :development do
13
+ gem "rspec"
14
+ gem "bundler"
15
+ gem "jeweler", "~> 1.6.2"
16
+ gem "rcov", ">= 0", :platforms => :mri_18
17
+ gem "yard"
18
+ gem "watchr"
19
+ gem "ruby-prof", :platforms => :mri
20
+
21
+ platform :mri_19 do
22
+ # We need the latest version of linecache19 for debugging to work.
23
+ # http://stackoverflow.com/questions/8251349/ruby-threadptr-data-type-error
24
+ gem 'linecache19', :git => 'git://github.com/mark-moseley/linecache'
25
+
26
+ # ruby-debug-base19 0.11.26 is not available on ruby gems, needs to be manually
27
+ # preinstalled. See ruby1.9.3_dev_setup.sh
28
+ gem 'ruby-debug-base19', '>= 0.11.26'
29
+
30
+ gem 'ruby-debug19'
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ = Doku: solve Sudoku-like puzzles!
2
+
3
+ Doku is a Ruby[http://www.rubylang.org/] gem for solving Sudoku-like puzzles using the {Dancing Links}[http://en.wikipedia.org/wiki/Dancing_Links] algorithm by Donald Knuth.
4
+
5
+ == Installation
6
+
7
+ At the command line, type
8
+
9
+ gem install doku
10
+
11
+ You might need to prefix that with <code>sudo</code> depending on where your gems are stored.
12
+
13
+ == Example code
14
+
15
+ :include: example.rb
@@ -0,0 +1,49 @@
1
+ require 'rubygems'
2
+ begin
3
+ require 'rake'
4
+ require 'jeweler'
5
+ require 'rspec/core'
6
+ require 'rspec/core/rake_task'
7
+ require 'yard'
8
+ rescue LoadError => e
9
+ $stderr.puts "Run `gem install bundler && bundle install` to install missing gems."
10
+ exit 1
11
+ end
12
+
13
+ Jeweler::Tasks.new do |gem|
14
+ # gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
15
+ gem.name = "doku"
16
+ gem.homepage = "http://github.com/DavidEGrayson/doku"
17
+ gem.license = "MIT"
18
+ gem.files = %w{.document *.txt *.rdoc VERSION
19
+ Gemfile Rakefile
20
+ lib/doku.rb
21
+ lib/doku/*.rb
22
+ spec/*.rb
23
+ }
24
+
25
+ gem.summary = "Ruby library for solving sudoku, hexadoku, and similar puzzles."
26
+ gem.description = <<END
27
+ This gem allows you to represent Sudoku-like puzzles
28
+ (Sudoku, Hexadoku, and Hexamurai) as objects and find
29
+ solutions for them.
30
+
31
+ This gem contains a reusable, pure ruby implementation of the
32
+ Dancing Links algorithm by Donald Knuth.
33
+ END
34
+ gem.email = "davidegrayson@gmail.com"
35
+ gem.authors = ["David Grayson"]
36
+ # dependencies defined in Gemfile
37
+ end
38
+ Jeweler::RubygemsDotOrgTasks.new
39
+
40
+ RSpec::Core::RakeTask.new(:spec) do |spec|
41
+ spec.pattern = FileList['spec/**/*_spec.rb']
42
+ end
43
+
44
+ YARD::Rake::YardocTask.new do |t|
45
+ t.files = %w{lib/**/*.rb}
46
+ t.options = %w{}
47
+ end
48
+
49
+ task :default => :spec
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 1.0.0
@@ -0,0 +1,4 @@
1
+ require 'backports' unless defined? require_relative
2
+ require_relative 'doku/sudoku.rb'
3
+ require_relative 'doku/hexadoku.rb'
4
+ require_relative 'doku/hexamurai.rb'
@@ -0,0 +1,561 @@
1
+ require 'backports' unless defined?(Enumerator)
2
+ require 'set'
3
+
4
+ module Doku; end
5
+
6
+ # This module contains a general-purpose implementation of the
7
+ # {http://arxiv.org/abs/cs/0011047 Dancing Links}
8
+ # algorithm discovered by Donald Knuth for solving
9
+ # {http://en.wikipedia.org/wiki/Exact_cover exact cover problems}.
10
+ # This module is included in the Doku gem for convenience, but it really has
11
+ # nothing to do with solving Sudoku-like puzzles; it can be applied to any
12
+ # exact cover problem.
13
+ # The main class in this module is {LinkMatrix}. All the other classes and
14
+ # modules are helpers for this class.
15
+ module Doku::DancingLinks
16
+ # The data structures used here are too complicated
17
+ # and interconnected for Ruby to efficiently inspect them.
18
+ # Without this module, even a 7x6 {LinkMatrix} takes
19
+ # many many seconds to inspect.
20
+ module Uninspectable
21
+ # Simply calls to_s.
22
+ # @return (String)
23
+ def inspect
24
+ to_s
25
+ end
26
+ end
27
+
28
+ # This is a class that lets us concisely enumerate a certain
29
+ # set of objects by traveling either up, down, left, or right
30
+ # from a starting object until we wrap around and reach that same node.
31
+ # Since this class includes the Enumerable class, it has several fancy
32
+ # methods available with it such as "max_by", "collect", or "to_a".
33
+ class LinkEnumerator
34
+ include Enumerable
35
+
36
+ # @param link (Symbol) The link to follow. Should be :up, :down, :left, or :right.
37
+ # @param start (Object) The starting object. Typically a {LinkMatrix::Node}, {LinkMatrix::Column} (column header), or {LinkMatrix} (root node).
38
+ # @param include_start (Boolean) True if we want to include the starting object in this enumeration.
39
+ def initialize(link, start, include_start=false)
40
+ @link, @start, @include_start = link, start, include_start
41
+ end
42
+
43
+ # Iterates through objects by starting at the starting object
44
+ # and going in the specified direction until the start point is
45
+ # found again.
46
+ # The starting object will be yielded first, if this LinkEnumerator
47
+ # was configured to yield it {#initialize}.
48
+ # @yield (obj)
49
+ def each
50
+ yield @start if @include_start
51
+
52
+ n = @start
53
+ while true
54
+ n = n.send @link
55
+ return if n == @start
56
+ yield n
57
+ end
58
+ end
59
+ end
60
+
61
+ # This module is mixed into objects to give them their "left"
62
+ # and "right" links, and to give some convenient methods for
63
+ # changing those links.
64
+ # Every class this module is added to gains to attr_accessors:
65
+ # left and right.
66
+ module HorizontalLinks
67
+ include Uninspectable
68
+
69
+ def self.included(klass)
70
+ klass.instance_eval do
71
+ attr_accessor :left, :right
72
+ end
73
+ end
74
+
75
+ # Removes this object from the horizontal linked list by making the
76
+ # left and right neighbors point at each other instead of this object.
77
+ # This can later be undone with {#reinsert_horizontal}
78
+ def remove_horizontal
79
+ right.left, left.right = left, right
80
+ end
81
+
82
+ # Reinserts this object into the horizontal linked list by making the
83
+ # former left and right neighors point to this object instead of each other.
84
+ # The former left and right neighbors are simply found by looking at the
85
+ # "left" and "right" links for this object, which still point to them.
86
+ # This undoes the effect of {#remove_horizontal}.
87
+ def reinsert_horizontal
88
+ left.right = right.left = self
89
+ end
90
+
91
+ # Inserts this object to the left of the specified object in the
92
+ # horizontal list.
93
+ def insert_left(obj)
94
+ self.left, self.right = obj.left, obj
95
+ reinsert_horizontal
96
+ end
97
+ end
98
+
99
+ # This module is mixed into objects to give them their "up"
100
+ # and "down" links, and to give some convenient methods for
101
+ # changing those links.
102
+ # Every class this module is added to gains to attr_accessors:
103
+ # up and down.
104
+ module VerticalLinks
105
+ include Uninspectable
106
+
107
+ def self.included(klass)
108
+ klass.instance_eval do
109
+ attr_accessor :up, :down
110
+ end
111
+ end
112
+
113
+ # Removes this object from the vertical linked list by making the
114
+ # up and down neighbors point at each other instead of this object.
115
+ # This can later be undone with {#reinsert_vertical}
116
+ def remove_vertical
117
+ down.up, up.down = up, down
118
+ end
119
+
120
+ # Reinserts this object into the vertical linked list by making the
121
+ # former up and down neighors point to this object instead of each other.
122
+ # The former up and down neighbors are simply found by looking at the
123
+ # "up" and "down" links for this object, which still point to them.
124
+ # This undoes the effect of {#remove_vertical}.
125
+ def reinsert_vertical
126
+ up.down = down.up = self
127
+ end
128
+
129
+ # Inserts this object above the specified object in the
130
+ # vertical list.
131
+ def insert_above(other)
132
+ self.up, self.down = other.up, other
133
+ reinsert_vertical
134
+ end
135
+ end
136
+
137
+ # A LinkMatrix object is the Root object from Donald Knuth's paper on
138
+ # Dancing Links. It also represents the matrix as a whole, so it has
139
+ # methods for building the matrix and finding exact covers.
140
+ # This data structure is used to efficiently implement Algorithm X,
141
+ # allowing us to find exact covers.
142
+ class LinkMatrix
143
+
144
+ # The Column Header object from Knuth.
145
+ # This object represents a requirement that needs to be satisfied
146
+ # at least once in the exact coer problem.
147
+ class Column
148
+ include HorizontalLinks, VerticalLinks
149
+
150
+ # An ID object provided by the user to give meaning to the column.
151
+ # This is the N relation from Knuth.
152
+ # The ID can be any object.
153
+ attr_reader :id
154
+
155
+ # The current number of nodes in this column.
156
+ # If this is zero, it means the column can not be covered, given
157
+ # choices that have already been made.
158
+ attr_accessor :size
159
+
160
+ # Initializes an empty column with the specified id.
161
+ # The ID can be any object.
162
+ def initialize(id)
163
+ @up = @down = self
164
+ @id = id
165
+ @size = 0
166
+ end
167
+
168
+ # All the nodes in this column, starting at the top one and going down.
169
+ def nodes_downward
170
+ LinkEnumerator.new :down, self
171
+ end
172
+
173
+ # All the nodes in this column, starting at the bottom one and going up.
174
+ def nodes_upward
175
+ LinkEnumerator.new :up, self
176
+ end
177
+
178
+ alias :nodes :nodes_downward
179
+
180
+ # Covers the column.
181
+ # This algorithm comes from page 6 of Knuth's paper.
182
+ # This operation removes the column from the list of
183
+ # columns that needs to be covered and it removes all
184
+ # rows that would cover this column.
185
+ # This can be efficiently undone with {#uncover}.
186
+ #
187
+ # The word "cover" here means the same thing it does in
188
+ # the phrase "exact cover problem". Our goal is to
189
+ # cover every column exactly once using this method.
190
+ def cover
191
+ remove_horizontal
192
+ nodes_downward.each do |i|
193
+ i.nodes_except_self_rightward.each do |j|
194
+ j.remove_vertical
195
+ j.column.size -= 1
196
+ end
197
+ end
198
+ end
199
+
200
+ # Uncovers the column.
201
+ # This algorithm comes from page 6 of Knuth's paper.
202
+ # This operation undoes the effects of {#cover}.
203
+ def uncover
204
+ nodes_upward.each do |i|
205
+ i.nodes_except_self_leftward.each do |j|
206
+ j.column.size += 1
207
+ j.reinsert_vertical
208
+ end
209
+ end
210
+ reinsert_horizontal
211
+ end
212
+
213
+ # True if there are no more nodes in this column.
214
+ # @return (Boolean)
215
+ def empty?
216
+ size == 0 # Equivalent to (down == self)
217
+ end
218
+ end
219
+
220
+ # This class represents a normal node in Knuth's {LinkMatrix}.
221
+ # Every node belongs to a column and a row, and it represents the
222
+ # fact that the row (i.e. set) "covers" the column.
223
+ class Node
224
+ include HorizontalLinks, VerticalLinks
225
+
226
+ # The {Column} object that this node belongs to.
227
+ attr_accessor :column
228
+
229
+ # The user-assigned ID of the row this node belongs to.
230
+ attr_accessor :row_id
231
+
232
+ # All nodes in the same row, starting with self and going to the right.
233
+ def nodes_rightward
234
+ LinkEnumerator.new :right, self, true
235
+ end
236
+
237
+ # All nodes in the same row, starting with self and going to the right,
238
+ # but not including self.
239
+ def nodes_except_self_rightward
240
+ LinkEnumerator.new :right, self
241
+ end
242
+
243
+ # All nodes in the same row, starting with self and going to the left,
244
+ # but not including self.
245
+ def nodes_except_self_leftward
246
+ LinkEnumerator.new :left, self
247
+ end
248
+
249
+ alias :nodes_except_self :nodes_except_self_rightward
250
+
251
+ alias :nodes :nodes_rightward
252
+
253
+ # Removes a row from the {LinkMatrix} by covering every
254
+ # column that it touches. This represents (tentatively)
255
+ # choosing the node's row to be in our exact cover.
256
+ # When that choice is proven to not work, this action can
257
+ # be efficiently undone with {#unchoose}.
258
+ def choose
259
+ nodes_except_self_rightward.each do |node|
260
+ node.column.cover
261
+ end
262
+ end
263
+
264
+ # Undoes the effect of {#choose}, putting
265
+ # the nodes of the row back into the {LinkMatrix}.
266
+ def unchoose
267
+ nodes_except_self_leftward.each do |node|
268
+ node.column.uncover
269
+ end
270
+ end
271
+
272
+ end
273
+
274
+ # Since the LinkMatrix object is the root node, it has left and right links.
275
+ include HorizontalLinks
276
+
277
+ # Creates a new, empty matrix with no columns and no rows.
278
+ def initialize
279
+ @left = @right = self
280
+ @columns = {} # column_id object => Column
281
+ @rows = {} # row_id object => Node
282
+ end
283
+
284
+ # Enumerable for all the {Column}s in the matrix.
285
+ def columns
286
+ LinkEnumerator.new :right, self
287
+ end
288
+
289
+ # True if there are no more columns left in the matrix (they were all covered).
290
+ # @return (Boolean)
291
+ def empty?
292
+ right == self
293
+ end
294
+
295
+ # Creates a new column with the specified ID and inserts it
296
+ # into the matrix as the right-most column.
297
+ # @param id (Object) Any object that uniquely identifies the column.
298
+ # @return (Column) Newly created column.
299
+ def create_column(id)
300
+ column = Column.new(id)
301
+ column.insert_left self
302
+ return @columns[id] = column
303
+ end
304
+
305
+ # Retrieves a column object by its ID or returns nil if there is
306
+ # no column with that ID.
307
+ # @return (Column)
308
+ def column(id)
309
+ @columns[id]
310
+ end
311
+
312
+ # Retrieves a column object by its ID or creates a new one if
313
+ # it didn't exist already.
314
+ # @return (Column)
315
+ def find_or_create_column(id)
316
+ @columns[id] || create_column(id)
317
+ end
318
+
319
+ # Creates a new {LinkMatrix} to represent an
320
+ # {http://en.wikipedia.org/wiki/Exact_cover exact cover problem}.
321
+ #
322
+ # Every set in the exact cover problem will be represented by a row in the
323
+ # matrix.
324
+ #
325
+ # Every element in the universe of the exact cover problem will be represented
326
+ # by a column in the matrix. The universe is inferred by taking the union
327
+ # of all the sets in the sets parameter, but if you want to have control over
328
+ # the order of the columns then you can also make a universe array and
329
+ # pass it in to the universe parameter.
330
+ #
331
+ # In {LinkMatrix}, every row has a row id. The row id is used to express
332
+ # exact covers when they are found.
333
+ # You can just let all the row ids be equal to the sets themselves by
334
+ # making the sets parameter be an Array or Set of sets, or
335
+ # you can specify the row ids explicitly by if you make the sets parameter
336
+ # be a hash that associates row ids to sets.
337
+ #
338
+ # @param (Object) sets Either a hash associating row_ids to sets, or just
339
+ # an array of sets. A set is an Array or Set of objects in the
340
+ # universe of the exact cover problem.
341
+ # @param universe (Array) This parameter is optional. If provided, it
342
+ # will define the order of the first columns of the link matrix.
343
+ # It is OK if there are elements in the sets that are not present in
344
+ # this array.
345
+ # @return (LinkMatrix)
346
+ def self.from_sets(sets, universe=[])
347
+ matrix = new
348
+ universe.each do |column_id|
349
+ matrix.find_or_create_column column_id
350
+ end
351
+
352
+ if sets.is_a? Hash
353
+ sets.each do |row_id, column_ids|
354
+ matrix.add_row column_ids, row_id
355
+ end
356
+ else
357
+ sets.each do |column_ids|
358
+ matrix.add_row column_ids
359
+ end
360
+ end
361
+
362
+ matrix
363
+ end
364
+
365
+ # Adds a row to the matrix.
366
+ # If a column_id is not recognized, it will be added to the matrix
367
+ # as a new column.
368
+ #
369
+ # @param column_ids (Enumerable) The column_ids that are in this row.
370
+ # @param row_id (Object) The id of this row. This is used to express express exact covers and as the argument to {#remove_row}.
371
+ def add_row(column_ids, row_id=column_ids.dup)
372
+ first_node = nil
373
+ Set.new(column_ids).each do |column_id|
374
+ column = find_or_create_column(column_id)
375
+ node = Node.new
376
+
377
+ # Set the vertical links and column.
378
+ node.column = column
379
+ node.insert_above column
380
+
381
+ # Set the horizontal links and row_id.
382
+ node.row_id = row_id
383
+ if first_node.nil?
384
+ @rows[row_id] = first_node = node.left = node.right = node
385
+ else
386
+ node.insert_left first_node
387
+ end
388
+
389
+ column.size += 1
390
+ end
391
+ end
392
+
393
+ # Removes a row from the matrix.
394
+ # @param row_id (Object) The ID of the row that was specified when
395
+ # {#add_row} was called.
396
+ def remove_row(row_id)
397
+ raise ArgumentError, "Row with id #{row_id} not found." if !@rows[row_id]
398
+ @rows[row_id].nodes_rightward.each do |node|
399
+ node.column.cover
400
+ end
401
+ end
402
+
403
+ # Retrieves a node in the row with the specified ID or returns nil if there is
404
+ # no row with that ID.
405
+ # @param id (Object) The ID of the row that was specified when
406
+ # {#add_row} was called.
407
+ # @return (Node)
408
+ def row(id)
409
+ @rows[id]
410
+ end
411
+
412
+ # This is a recursive method that finds the first exact cover of a
413
+ # LinkMatrix that represents an exact cover problem, using the
414
+ # the algorithm described on page 5 of Donald Knuth's paper "Dancing Links".
415
+ # This method is just here for purists who want to be sure they are using
416
+ # Donald Knuth's algorithm.
417
+ # For most uses, it is recommended to use the more flexible, non-recursive
418
+ # function {#each_exact_cover} and the methods based on it: {#exact_covers}
419
+ # and {#find_exact_cover}.
420
+ # @return (Array) Array of row_ids of the rows/sets that are in the cover,
421
+ # or nil if no exact cover was found.
422
+ def find_exact_cover_recursive(k=0, o=[])
423
+ if right == self
424
+ return o[0...k].collect &:row_id # Success
425
+ end
426
+
427
+ c = smallest_column
428
+ c.cover
429
+
430
+ c.nodes_downward.each do |r|
431
+ o[k] = r
432
+
433
+ r.nodes_except_self_rightward.each do |j|
434
+ j.column.cover
435
+ end
436
+
437
+ if answer = find_exact_cover_recursive(k+1, o)
438
+ return answer # Success
439
+ end
440
+
441
+ r.nodes_except_self_leftward.each do |j|
442
+ j.column.uncover
443
+ end
444
+ end
445
+
446
+ c.uncover
447
+ return nil
448
+ end
449
+
450
+ # TODO: see if recursive or non-recursive algorithm is faster.
451
+
452
+ # Searches for an exact cover.
453
+ # NOTE: This method mutates the LinkMatrix.
454
+ # @return (Array) Array of row ids of the rows/sets that are in the cover,
455
+ # or nil if no exact cover was found.
456
+ def find_exact_cover
457
+ exact_covers.first
458
+ end
459
+
460
+ # Returns an enumerable that searches for exact covers as its elements
461
+ # are enumerated.
462
+ # NOTE: This method mutates the LinkMatrix.
463
+ # @return (Enumerable) Enumerable of exact covers. Each exact cover is
464
+ # an array of row ids of the rows/sets that are in the cover.
465
+ def exact_covers
466
+ Enumerator.new do |y|
467
+ each_exact_cover do |ec|
468
+ y << ec
469
+ end
470
+ end
471
+ end
472
+
473
+ # Searches for exact covers and yields them as it finds them.
474
+ # NOTE: This method mutates the LinkMatrix while it is running, but
475
+ # when it is finished the matrix will be back to its original state.
476
+ # @yield exact_cover
477
+ # @yieldparam exact_cover (Array) Array of row_ids of the rows/sets that are
478
+ # in the cover.
479
+ def each_exact_cover
480
+ nodes = [] # List of nodes that are currently "covered"
481
+
482
+ while true
483
+
484
+ if empty?
485
+ # Success. Matrix is empty because every column is covered once.
486
+ yield nodes.collect &:row_id
487
+ end
488
+
489
+ if column = choose_column
490
+ # Cover a new column and pick the first node in it.
491
+ column.cover
492
+ node = column.down
493
+ else
494
+ # Uncover columns until we find one with a node we haven't tried.
495
+ node = backtrack!(nodes)
496
+ return if node.nil? # Tried everything
497
+ end
498
+
499
+ # Try the node (push it and cover its columns).
500
+ nodes.push node
501
+ node.choose
502
+
503
+ end
504
+ end
505
+
506
+ protected
507
+
508
+ def choose_column
509
+ return nil if empty?
510
+ column = smallest_column
511
+ return nil if column.empty?
512
+ return column
513
+ end
514
+
515
+ # When choosing a column, we use Knuth's S heuristic.
516
+ # Assumption: The matrix has at least one column.
517
+ def smallest_column
518
+ # Slow but concise version of this method:
519
+ #return columns.min_by &:size
520
+
521
+ column = smallest = right
522
+ min_size = column.size
523
+ while true
524
+ column = column.right
525
+ return smallest if column == self
526
+
527
+ if column.size < min_size
528
+ smallest, min_size = column, column.size
529
+ return smallest if min_size == 0
530
+ end
531
+ end
532
+ end
533
+
534
+ # This is used by each_exact_cover.
535
+ # Picks the next node to try by back-tracking until we get
536
+ # to a column where we haven't tried all the nodes, uncovering
537
+ # nodes and columns as it goes.
538
+ # Returns nil if we are done searching the entire solution space.
539
+ def backtrack!(nodes)
540
+ while true
541
+ return nil if nodes.empty? # backtracked back to 0, so we are done
542
+
543
+ # We tried nodes.last and it didn't work, so
544
+ # pop it off and uncover the corresponding columns.
545
+ node = nodes.pop
546
+ node.unchoose
547
+
548
+ # Try the next node in this column.
549
+ x = node.down
550
+
551
+ return x unless x.is_a? Column
552
+
553
+ # Our downwards iteration has gone full-circle
554
+ # back to the column object where it started.
555
+ x.uncover # Uncover the column.
556
+ end
557
+ end
558
+
559
+ end
560
+
561
+ end