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 +32 -0
- data/README.rdoc +15 -0
- data/Rakefile +49 -0
- data/VERSION +1 -0
- data/lib/doku.rb +4 -0
- data/lib/doku/dancing_links.rb +561 -0
- data/lib/doku/grid.rb +241 -0
- data/lib/doku/hexadoku.rb +56 -0
- data/lib/doku/hexamurai.rb +95 -0
- data/lib/doku/puzzle.rb +250 -0
- data/lib/doku/solver.rb +103 -0
- data/lib/doku/sudoku.rb +42 -0
- data/spec/dancing_links_spec.rb +212 -0
- data/spec/hexadoku_spec.rb +71 -0
- data/spec/hexamurai_spec.rb +38 -0
- data/spec/puzzle_spec.rb +147 -0
- data/spec/solution_spec.rb +278 -0
- data/spec/spec_helper.rb +19 -0
- data/spec/sudoku_spec.rb +52 -0
- data/spec/watch.rb +9 -0
- metadata +198 -0
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
|
data/README.rdoc
ADDED
@@ -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
|
data/Rakefile
ADDED
@@ -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
|
data/lib/doku.rb
ADDED
@@ -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
|