41
Algorithm X in Ruby
Knuth's recursive backtracker. We'll write it without the Dancing Links optimization — Ruby's hash-of-sets reads like the spec.
- Represent the problem as two lookups: per-set list of elements, and per-element list of sets containing it. Both are Hashes; both are kept consistent on remove/restore.
class ExactCover attr_reader :sets, :covers def initialize(sets) @sets = sets.dup # name => Set of elements @covers = Hash.new { |h, k| h[k] = Set.new } sets.each { |name, els| els.each { |e| @covers[e] << name } } end end - The choose step picks the element with fewest covering sets — Knuth's S-heuristic, which is what makes the search fast in practice. (Tie-break: any element will do.)
def choose_element @covers.min_by { |_, names| names.size }&.first end - Search: pick an uncovered element, branch on each set covering it, recurse. The cover/uncover operations are the heart of DLX — here we do them by simple hash mutation.
def search(partial = [], &blk) return blk.call(partial) if @covers.empty? e = choose_element return if @covers[e].empty? @covers[e].dup.each do |name| removed = cover(name) search(partial + [name], &blk) uncover(removed) end end def cover(name) removed = [] @sets[name].each do |e| @covers[e].each do |other| next if other == name @sets[other].each { |x| @covers[x].delete(other) unless x == e } removed << [other, @sets.delete(other)] end @covers.delete(e) end removed end def uncover(removed) removed.each do |name, els| @sets[name] = els els.each { |e| @covers[e] << name } end end - Run it. Sudoku encodes as exact cover with 729 sets (one per (row, col, digit) cell-fill) over a 324-element universe. DLX cracks a "hard" 17-clue Sudoku in milliseconds.
sets = { a: Set[1, 4, 7], b: Set[1, 4], c: Set[4, 5, 7], d: Set[3, 5, 6], e: Set[2, 3, 6, 7], f: Set[2, 7] } ExactCover.new(sets).search { |sol| puts "cover: #{sol.inspect}" } # cover: [:b, :d, :f]