M
Matthew Moss
I had to give this a couple tries before I just broke down and went
for what amounts to brute force. This solution can get slow pretty
quickly; I'm sure there are some easy speedups (i.e. narrow the search
space) that can be done, but I figured to put this up for now.
# Helpers
class Integer
def even?
(self % 2).zero?
end
end
class Symbol
def <=3D> other
self.to_s <=3D> other.to_s
end
end
# Constraint Solver class
class Problem
def initialize(&block)
@domain =3D {}
@consts =3D Hash.new { [] }
instance_eval(&block)
end
def variable(var, domain)
raise ArgumentError, "Cannot specify variable #{var} more than
once." if @domain.has_key?(var)
@domain[var] =3D domain.to_a
end
def constrain(*vars, &foo)
raise ArgumentError, 'Constraint requires at least one
variable.' if vars.size.zero?
vars.each do |var|
raise ArgumentError, "Unknown variable: #{var}" unless
@domain.has_key?(var)
end
@consts[vars] =3D @consts[vars] << foo
end
def solve
# Separate constraint keys into unary and non-unary.
unary, multi =3D @consts.keys.partition{ |vars| vars.size =3D=3D 1 }
# Process unary constraints first to narrow variable domains.
unary.each do |vars|
a =3D vars.first
@consts[vars].each do |foo|
@domain[a] =3D @domain[a].select { |d| foo.call(d) }
end
end
# Build fully-expanded domain (i.e. across all variables).
full =3D @domain.keys.map do |var|
@domain[var].map do |val|
{ var =3D> val }
end
end.inject do |m, n|
m.map do |a|
n.map do |b|
a.merge(b)
end
end.flatten
end
# Process non-unary constraints on full domain.
full.select do |d|
multi.all? do |vars|
@consts[vars].all? do |foo|
foo.call( vars.map { |v| d[v] } )
end
end
end
end
end
# A simple example
problem =3D Problem.new do
variablea, 0..10)
variableb, 0..10)
variablec, 0..10)
constraina) { |a| a.even? }
constraina, :b) { |a, b| b =3D=3D 2 * a }
constrainb, :c) { |b, c| c =3D=3D b - 3 }
end
puts "Simple example solutions:"
problem.solve.each { |sol| p sol }
# Calculate some primes... The constraint problem actually finds
# the non-primes, which we remove from our range afterward to get
# the primes.
problem =3D Problem.new do
variablea, 2..25)
variableb, 2..25)
variablec, 2..50)
constraina, :b) { |a, b| a <=3D b }
constraina, :b, :c) { |a, b, c| a * b =3D=3D c }
end
puts "The primes up to 50:"
puts ((2..50).to_a - problem.solve.map { |s| s[:c] }).join(", ")
puts
for what amounts to brute force. This solution can get slow pretty
quickly; I'm sure there are some easy speedups (i.e. narrow the search
space) that can be done, but I figured to put this up for now.
# Helpers
class Integer
def even?
(self % 2).zero?
end
end
class Symbol
def <=3D> other
self.to_s <=3D> other.to_s
end
end
# Constraint Solver class
class Problem
def initialize(&block)
@domain =3D {}
@consts =3D Hash.new { [] }
instance_eval(&block)
end
def variable(var, domain)
raise ArgumentError, "Cannot specify variable #{var} more than
once." if @domain.has_key?(var)
@domain[var] =3D domain.to_a
end
def constrain(*vars, &foo)
raise ArgumentError, 'Constraint requires at least one
variable.' if vars.size.zero?
vars.each do |var|
raise ArgumentError, "Unknown variable: #{var}" unless
@domain.has_key?(var)
end
@consts[vars] =3D @consts[vars] << foo
end
def solve
# Separate constraint keys into unary and non-unary.
unary, multi =3D @consts.keys.partition{ |vars| vars.size =3D=3D 1 }
# Process unary constraints first to narrow variable domains.
unary.each do |vars|
a =3D vars.first
@consts[vars].each do |foo|
@domain[a] =3D @domain[a].select { |d| foo.call(d) }
end
end
# Build fully-expanded domain (i.e. across all variables).
full =3D @domain.keys.map do |var|
@domain[var].map do |val|
{ var =3D> val }
end
end.inject do |m, n|
m.map do |a|
n.map do |b|
a.merge(b)
end
end.flatten
end
# Process non-unary constraints on full domain.
full.select do |d|
multi.all? do |vars|
@consts[vars].all? do |foo|
foo.call( vars.map { |v| d[v] } )
end
end
end
end
end
# A simple example
problem =3D Problem.new do
variablea, 0..10)
variableb, 0..10)
variablec, 0..10)
constraina) { |a| a.even? }
constraina, :b) { |a, b| b =3D=3D 2 * a }
constrainb, :c) { |b, c| c =3D=3D b - 3 }
end
puts "Simple example solutions:"
problem.solve.each { |sol| p sol }
# Calculate some primes... The constraint problem actually finds
# the non-primes, which we remove from our range afterward to get
# the primes.
problem =3D Problem.new do
variablea, 2..25)
variableb, 2..25)
variablec, 2..50)
constraina, :b) { |a, b| a <=3D b }
constraina, :b, :c) { |a, b, c| a * b =3D=3D c }
end
puts "The primes up to 50:"
puts ((2..50).to_a - problem.solve.map { |s| s[:c] }).join(", ")
puts