doku 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|