ConstraintSolver 0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/bin/ConstraintSolver +24 -0
- data/doc/classes/Array.html +209 -0
- data/doc/classes/ConstraintSolver.html +242 -0
- data/doc/classes/ConstraintSolver/AbstractConstraint.html +317 -0
- data/doc/classes/ConstraintSolver/AllDifferentConstraint.html +451 -0
- data/doc/classes/ConstraintSolver/AllDifferentConstraintTest.html +397 -0
- data/doc/classes/ConstraintSolver/BinaryConstraint.html +483 -0
- data/doc/classes/ConstraintSolver/BinaryConstraintTest.html +367 -0
- data/doc/classes/ConstraintSolver/BinaryRelation.html +276 -0
- data/doc/classes/ConstraintSolver/BinaryRelationTest.html +194 -0
- data/doc/classes/ConstraintSolver/ConstraintList.html +208 -0
- data/doc/classes/ConstraintSolver/ConstraintListTest.html +252 -0
- data/doc/classes/ConstraintSolver/ConstraintSolver.html +353 -0
- data/doc/classes/ConstraintSolver/ConstraintSolverTest.html +403 -0
- data/doc/classes/ConstraintSolver/Domain.html +522 -0
- data/doc/classes/ConstraintSolver/DomainTest.html +356 -0
- data/doc/classes/ConstraintSolver/DomainWipeoutException.html +158 -0
- data/doc/classes/ConstraintSolver/Problem.html +239 -0
- data/doc/classes/ConstraintSolver/ProblemTest.html +227 -0
- data/doc/classes/ConstraintSolver/Solution.html +342 -0
- data/doc/classes/ConstraintSolver/SolutionTest.html +250 -0
- data/doc/classes/ConstraintSolver/UndoStackEmptyException.html +158 -0
- data/doc/classes/ConstraintSolver/Variable.html +418 -0
- data/doc/classes/ConstraintSolver/VariableTest.html +284 -0
- data/doc/classes/ExtensionsTest.html +233 -0
- data/doc/classes/Fixnum.html +153 -0
- data/doc/created.rid +1 -0
- data/doc/dot/f_0.dot +38 -0
- data/doc/dot/f_0.png +0 -0
- data/doc/dot/f_1.dot +392 -0
- data/doc/dot/f_1.png +0 -0
- data/doc/dot/f_10.dot +392 -0
- data/doc/dot/f_10.png +0 -0
- data/doc/dot/f_11.dot +38 -0
- data/doc/dot/f_11.png +0 -0
- data/doc/dot/f_12.dot +392 -0
- data/doc/dot/f_12.png +0 -0
- data/doc/dot/f_13.dot +392 -0
- data/doc/dot/f_13.png +0 -0
- data/doc/dot/f_14.dot +392 -0
- data/doc/dot/f_14.png +0 -0
- data/doc/dot/f_15.dot +392 -0
- data/doc/dot/f_15.png +0 -0
- data/doc/dot/f_16.dot +392 -0
- data/doc/dot/f_16.png +0 -0
- data/doc/dot/f_17.dot +392 -0
- data/doc/dot/f_17.png +0 -0
- data/doc/dot/f_18.dot +392 -0
- data/doc/dot/f_18.png +0 -0
- data/doc/dot/f_19.dot +392 -0
- data/doc/dot/f_19.png +0 -0
- data/doc/dot/f_2.dot +392 -0
- data/doc/dot/f_2.png +0 -0
- data/doc/dot/f_3.dot +392 -0
- data/doc/dot/f_3.png +0 -0
- data/doc/dot/f_4.dot +392 -0
- data/doc/dot/f_4.png +0 -0
- data/doc/dot/f_5.dot +392 -0
- data/doc/dot/f_5.png +0 -0
- data/doc/dot/f_6.dot +14 -0
- data/doc/dot/f_6.png +0 -0
- data/doc/dot/f_7.dot +392 -0
- data/doc/dot/f_7.png +0 -0
- data/doc/dot/f_8.dot +392 -0
- data/doc/dot/f_8.png +0 -0
- data/doc/dot/f_9.dot +392 -0
- data/doc/dot/f_9.png +0 -0
- data/doc/dot/m_10_0.dot +392 -0
- data/doc/dot/m_10_0.png +0 -0
- data/doc/dot/m_12_0.dot +392 -0
- data/doc/dot/m_12_0.png +0 -0
- data/doc/dot/m_13_0.dot +392 -0
- data/doc/dot/m_13_0.png +0 -0
- data/doc/dot/m_14_0.dot +392 -0
- data/doc/dot/m_14_0.png +0 -0
- data/doc/dot/m_15_0.dot +392 -0
- data/doc/dot/m_15_0.png +0 -0
- data/doc/dot/m_16_0.dot +392 -0
- data/doc/dot/m_16_0.png +0 -0
- data/doc/dot/m_17_0.dot +392 -0
- data/doc/dot/m_17_0.png +0 -0
- data/doc/dot/m_18_0.dot +392 -0
- data/doc/dot/m_18_0.png +0 -0
- data/doc/dot/m_19_0.dot +392 -0
- data/doc/dot/m_19_0.png +0 -0
- data/doc/dot/m_1_0.dot +392 -0
- data/doc/dot/m_1_0.png +0 -0
- data/doc/dot/m_2_0.dot +392 -0
- data/doc/dot/m_2_0.png +0 -0
- data/doc/dot/m_3_0.dot +392 -0
- data/doc/dot/m_3_0.png +0 -0
- data/doc/dot/m_4_0.dot +392 -0
- data/doc/dot/m_4_0.png +0 -0
- data/doc/dot/m_5_0.dot +392 -0
- data/doc/dot/m_5_0.png +0 -0
- data/doc/dot/m_7_0.dot +392 -0
- data/doc/dot/m_7_0.png +0 -0
- data/doc/dot/m_8_0.dot +392 -0
- data/doc/dot/m_8_0.png +0 -0
- data/doc/dot/m_9_0.dot +392 -0
- data/doc/dot/m_9_0.png +0 -0
- data/doc/files/lib/AbstractConstraint_rb.html +148 -0
- data/doc/files/lib/AllDifferentConstraint_rb.html +156 -0
- data/doc/files/lib/BinaryConstraint_rb.html +155 -0
- data/doc/files/lib/ConstraintList_rb.html +148 -0
- data/doc/files/lib/ConstraintSolver_rb.html +162 -0
- data/doc/files/lib/Domain_rb.html +155 -0
- data/doc/files/lib/Problem_rb.html +148 -0
- data/doc/files/lib/Solution_rb.html +148 -0
- data/doc/files/lib/Variable_rb.html +148 -0
- data/doc/files/lib/extensions_rb.html +108 -0
- data/doc/files/test/AllDifferentConstraintTest_rb.html +158 -0
- data/doc/files/test/BinaryConstraintTest_rb.html +158 -0
- data/doc/files/test/ConstraintListTest_rb.html +160 -0
- data/doc/files/test/ConstraintSolverTest_rb.html +164 -0
- data/doc/files/test/DomainTest_rb.html +156 -0
- data/doc/files/test/ProblemTest_rb.html +160 -0
- data/doc/files/test/SolutionTest_rb.html +159 -0
- data/doc/files/test/TestSuite_rb.html +113 -0
- data/doc/files/test/VariableTest_rb.html +157 -0
- data/doc/files/test/extensionsTest_rb.html +118 -0
- data/doc/fr_class_index.html +51 -0
- data/doc/fr_file_index.html +46 -0
- data/doc/fr_method_index.html +133 -0
- data/doc/index.html +24 -0
- data/examples/example.rb +7 -0
- data/examples/queens.rb +13 -0
- data/examples/soft.rb +14 -0
- data/lib/AbstractConstraint.rb +45 -0
- data/lib/AllDifferentConstraint.rb +160 -0
- data/lib/BinaryConstraint.rb +187 -0
- data/lib/ConstraintList.rb +31 -0
- data/lib/ConstraintSolver.rb +213 -0
- data/lib/Domain.rb +100 -0
- data/lib/GraphUtils.rb +293 -0
- data/lib/OneOfEqualsConstraint.rb +81 -0
- data/lib/Problem.rb +30 -0
- data/lib/Solution.rb +56 -0
- data/lib/TupleConstraint.rb +111 -0
- data/lib/Variable.rb +74 -0
- data/lib/extensions.rb +55 -0
- data/test/AllDifferentConstraintTest.rb +140 -0
- data/test/BinaryConstraintTest.rb +108 -0
- data/test/ConstraintListTest.rb +41 -0
- data/test/ConstraintSolverTest.rb +274 -0
- data/test/DomainTest.rb +83 -0
- data/test/GraphUtilsTest.rb +83 -0
- data/test/OneOfEqualsConstraintTest.rb +82 -0
- data/test/ProblemTest.rb +35 -0
- data/test/SolutionTest.rb +35 -0
- data/test/TestSuite.rb +10 -0
- data/test/TupleConstraintTest.rb +151 -0
- data/test/VariableTest.rb +47 -0
- data/test/extensionsTest.rb +57 -0
- metadata +212 -0
@@ -0,0 +1,31 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
module ConstraintSolver
|
4
|
+
# This class represents a list of constraints.
|
5
|
+
class ConstraintList < Array
|
6
|
+
# Returns the ConstraintList that contains all constraints that involve
|
7
|
+
# variable and have values assigned to not all variables involved.
|
8
|
+
def notAllAssignedWithVariable(variable)
|
9
|
+
ConstraintList.new(self.select { |constraint|
|
10
|
+
constraint.include?(variable) and not constraint.allAssigned?
|
11
|
+
})
|
12
|
+
end
|
13
|
+
#
|
14
|
+
# Returns the ConstraintList that contains all constraints that involve
|
15
|
+
# variable.
|
16
|
+
def allWithVariable(variable)
|
17
|
+
ConstraintList.new(self.select { |constraint|
|
18
|
+
constraint.include?(variable)
|
19
|
+
})
|
20
|
+
end
|
21
|
+
|
22
|
+
def sort(&block)
|
23
|
+
ConstraintList.new(self.sort!(&block))
|
24
|
+
end
|
25
|
+
|
26
|
+
def -(element)
|
27
|
+
new = self.to_a - (element.kind_of?(Array) ? element : [ element ])
|
28
|
+
return ConstraintList.new(new)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,213 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'log4r'
|
4
|
+
include Log4r
|
5
|
+
|
6
|
+
%w(AbstractConstraint AllDifferentConstraint BinaryConstraint
|
7
|
+
TupleConstraint OneOfEqualsConstraint ConstraintList ConstraintSolver Domain
|
8
|
+
extensions Problem Solution Variable).each { |file| require file }
|
9
|
+
|
10
|
+
module ConstraintSolver
|
11
|
+
class ConstraintSolver
|
12
|
+
attr_reader :log
|
13
|
+
# Initialises a new solver.
|
14
|
+
def initialize(log=nil)
|
15
|
+
if log.nil?
|
16
|
+
@log = Logger.new(self.class.to_s)
|
17
|
+
else
|
18
|
+
@log = log
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Attempts to solve the constraint satisfaction problem passed as an
|
23
|
+
# argument and returns a list of all solutions. If there are no
|
24
|
+
# solutions, an empty list is returned. The second argument is the time
|
25
|
+
# limit to find a solution to the problem in seconds. This parameter is optional.
|
26
|
+
# A queue of constraint lists to consider is assembled at the beginning
|
27
|
+
# and only contains the initial list of constraints. While the problem
|
28
|
+
# is solved, additional constraint lists may be added to the queue. The
|
29
|
+
# new lists are relaxed versions of the problem with soft constraints
|
30
|
+
# removed. These are processed one at a time.
|
31
|
+
def solve(problem, limit=false)
|
32
|
+
@limit = limit
|
33
|
+
@problem = problem
|
34
|
+
@constraintChecks = 0
|
35
|
+
@nodeChecks = 0
|
36
|
+
@solutions = []
|
37
|
+
@time = Time.now
|
38
|
+
sortedConstraints = @problem.constraints.sort { |a,b| a.violationCost <=> b.violationCost }
|
39
|
+
if @limit
|
40
|
+
a = @limit * 100
|
41
|
+
@discardTimes = Hash.new
|
42
|
+
b = Math.log((a * sortedConstraints.last.violationCost / @limit) + 1) / @limit
|
43
|
+
sortedConstraints.each { |cons|
|
44
|
+
time = Math.log((a * cons.violationCost / @limit) + 1) / b
|
45
|
+
if not @discardTimes.has_key?(time)
|
46
|
+
@discardTimes[time] = []
|
47
|
+
end
|
48
|
+
@discardTimes[time] << cons
|
49
|
+
}
|
50
|
+
end
|
51
|
+
@constraintsQueue = [ [ sortedConstraints, 0 ] ]
|
52
|
+
while not @constraintsQueue.empty?
|
53
|
+
@timeDroppedConstraints = []
|
54
|
+
constraints = @constraintsQueue.shift
|
55
|
+
retval = assignNextVariable(@problem.variables.sort { |a,b| b.merit <=> a.merit },
|
56
|
+
constraints[0], constraints[1])
|
57
|
+
break if (retval == :solution and @problem.firstSolution) or (retval == :timeout)
|
58
|
+
@constraintsQueue.uniq!
|
59
|
+
end
|
60
|
+
return @solutions, @nodeChecks, @constraintChecks
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
|
65
|
+
# Attempts to assign a value that is consistent with all constraints to
|
66
|
+
# the next variable. Arguments are the list of variables left to
|
67
|
+
# process, the list of constraints to consider, and a measure for the
|
68
|
+
# current violation of constraints.
|
69
|
+
# Returns :solution iff a solution to the constraint satisfaction problem was
|
70
|
+
# found; i.e. before the call there was only one unassigned variable and
|
71
|
+
# a value that is consistent with all constraints was found in its
|
72
|
+
# domain or the sum of the cost of all violated constraints is less than
|
73
|
+
# the maximum cost for the problem. Else :noSolution if no solution was
|
74
|
+
# found or :timeout if the time limit for solving was exceeded is
|
75
|
+
# returned.
|
76
|
+
# If a constraint is violated, but the cost still below the maximum
|
77
|
+
# cost, a new list of constraints without the offending constraint is
|
78
|
+
# added to the queue of list of constraints to process. It is not
|
79
|
+
# processed immediately because first all the prunings have to be
|
80
|
+
# undone, so the exploration of the current search tree finishes and
|
81
|
+
# cleans up after itself.
|
82
|
+
# This method calls checkNode to test a particular assignment.
|
83
|
+
def assignNextVariable(variables, constraints, violation)
|
84
|
+
if @limit
|
85
|
+
return :timeout unless (Time.now - @time) < @limit or @solutions.empty?
|
86
|
+
discard = @discardTimes.keys.partition { |t| t <= Time.now - @time }[0]
|
87
|
+
discardConstraints = discard.collect { |k| @discardTimes[k] }.flatten
|
88
|
+
discardConstraints &= constraints
|
89
|
+
unless discardConstraints.empty?
|
90
|
+
@log.info("Running out of time, discarding constraints " + discardConstraints.join(", "))
|
91
|
+
constraints -= discardConstraints
|
92
|
+
@timeDroppedConstraints += discardConstraints
|
93
|
+
end
|
94
|
+
end
|
95
|
+
retval = :noSolution
|
96
|
+
current = variables.first
|
97
|
+
if current.assigned?
|
98
|
+
allHolds = true
|
99
|
+
constraints.allWithVariable(current).each { |constraint|
|
100
|
+
@constraintChecks += 1
|
101
|
+
holds = constraint.holds?
|
102
|
+
if holds == false
|
103
|
+
if (violation + constraint.violationCost) <= @problem.maxViolation
|
104
|
+
@log.debug("Discarding constraint " + constraint.to_s +
|
105
|
+
", cost " + constraint.violationCost.to_s)
|
106
|
+
@constraintsQueue << [ constraints - constraint, violation + constraint.violationCost ]
|
107
|
+
end
|
108
|
+
allHolds = false
|
109
|
+
end
|
110
|
+
break unless allHolds
|
111
|
+
}
|
112
|
+
retval = checkNode(current, variables, constraints, violation) if allHolds
|
113
|
+
else
|
114
|
+
values = current.domain.sort { |a,b|
|
115
|
+
(@problem.meritMap.has_key?(b) ? @problem.meritMap[b] : 0) <=>
|
116
|
+
(@problem.meritMap.has_key?(a) ? @problem.meritMap[a] : 0)
|
117
|
+
}
|
118
|
+
values.each { |value|
|
119
|
+
current.value = value
|
120
|
+
retval = checkNode(current, variables, constraints, violation)
|
121
|
+
break if (retval == :solution and @problem.firstSolution) or (retval == :timeout)
|
122
|
+
}
|
123
|
+
current.reset
|
124
|
+
end
|
125
|
+
return retval
|
126
|
+
end
|
127
|
+
|
128
|
+
# Checks a particular node in the search tree. Arguments to the method
|
129
|
+
# are the current node denoted by an assigned variablem the list of
|
130
|
+
# variables to process, the list of constraints to consider, and the
|
131
|
+
# current measure of violation cost.
|
132
|
+
# The method revises the constraints that the current variable is
|
133
|
+
# involved in, terminates the search if a domain wipeout occured or
|
134
|
+
# records a solution if one is found. Violated constraints may be
|
135
|
+
# disregarded if the cost of their violation plus the current costs of
|
136
|
+
# constraint violations is less than the maximum violation allowed for
|
137
|
+
# the problem.
|
138
|
+
# If the assignment is consistent but there are still unprocessed
|
139
|
+
# variables, it recursively calls assignNextVariable. Returns :solution iff a
|
140
|
+
# solution was found, :noSolution if none was found and :timeout if the
|
141
|
+
# time limit for solving the problem was exceeded.
|
142
|
+
def checkNode(node, variables, constraints, violation)
|
143
|
+
retval = :noSolution
|
144
|
+
@nodeChecks += 1
|
145
|
+
@log.debug("Checking node " + node.to_s)
|
146
|
+
revisedDomains, wipeout, constraints, violation = reviseConstraints(node, constraints, violation)
|
147
|
+
unless wipeout # if a domain was wiped out we can skip the rest of the subtree
|
148
|
+
if variables.size == 1
|
149
|
+
@constraintChecks += @timeDroppedConstraints.size
|
150
|
+
violation += @timeDroppedConstraints.inject(0) { |cost,cons|
|
151
|
+
cost + (cons.holds? ? 0 : cons.violationCost )
|
152
|
+
}
|
153
|
+
solution = Solution.new(@problem.variables, @problem.meritMap, violation)
|
154
|
+
unless @solutions.include?(solution)
|
155
|
+
@log.info("Found solution " + solution.to_s)
|
156
|
+
@solutions << solution
|
157
|
+
end
|
158
|
+
retval = (@limit and (Time.now - @time) > @limit) ? :timeout : :solution
|
159
|
+
else
|
160
|
+
retval = assignNextVariable(variables.rest, constraints, violation)
|
161
|
+
end
|
162
|
+
end
|
163
|
+
revisedDomains.each { |domain| domain.undoPruning }
|
164
|
+
return retval
|
165
|
+
end
|
166
|
+
|
167
|
+
# Revises all constraints that involve <i>variable</i>. Additional
|
168
|
+
# arguments are the list of constraints to consider, and the current
|
169
|
+
# cost of constraint violations.
|
170
|
+
# If a domain is wiped out during pruning, a new list of constraints
|
171
|
+
# with the constraint that caused the wipeout removed is added to the
|
172
|
+
# queue of constraint lists to process, the offending constraint is
|
173
|
+
# removed from the list of constraints, the cost of violation updated,
|
174
|
+
# and the pruning of the domains undone.
|
175
|
+
# Returns the list of domains that have been revised, a boolean
|
176
|
+
# indicating whether a domain was wiped out during pruning, the list of
|
177
|
+
# constraints, and the current cost of violation.
|
178
|
+
def reviseConstraints(variable, constraints, violation)
|
179
|
+
revisedDomains = []
|
180
|
+
wipeout = false
|
181
|
+
queue = constraints.notAllAssignedWithVariable(variable)
|
182
|
+
while not queue.empty?
|
183
|
+
revisedConstraint = queue.shift
|
184
|
+
revisedVariables, checks, wipeout = revisedConstraint.revise
|
185
|
+
@log.debug("Revising constraint " + revisedConstraint.to_s + ", pruned " + revisedVariables.join(", "))
|
186
|
+
@constraintChecks += checks
|
187
|
+
if wipeout
|
188
|
+
if (violation + revisedConstraint.violationCost) <= @problem.maxViolation
|
189
|
+
constraints -= revisedConstraint
|
190
|
+
violation += revisedConstraint.violationCost
|
191
|
+
@log.debug("Discarding constraint " + revisedConstraint.to_s +
|
192
|
+
", cost " + revisedConstraint.violationCost.to_s)
|
193
|
+
@constraintsQueue << [ constraints,
|
194
|
+
violation ]
|
195
|
+
wipeout = false
|
196
|
+
revisedVariables.each { |var| var.domain.undoPruning }
|
197
|
+
else
|
198
|
+
revisedVariables.each { |var| revisedDomains << var.domain }
|
199
|
+
break
|
200
|
+
end
|
201
|
+
else
|
202
|
+
revisedVariables.each { |var|
|
203
|
+
revisedDomains << var.domain
|
204
|
+
constraints.notAllAssignedWithVariable(var).each { |constraint|
|
205
|
+
queue << constraint unless queue.include?(constraint) or constraint == revisedConstraint
|
206
|
+
}
|
207
|
+
}
|
208
|
+
end
|
209
|
+
end
|
210
|
+
return revisedDomains, wipeout, constraints, violation
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
data/lib/Domain.rb
ADDED
@@ -0,0 +1,100 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
module ConstraintSolver
|
6
|
+
# This class represents the domain of a variable, i.e. a set of values that
|
7
|
+
# can be assigned to the variable.
|
8
|
+
class Domain
|
9
|
+
attr_reader :values, :undoStack
|
10
|
+
# Initialises a new domain. Optionally, a set of initial values can be
|
11
|
+
# given.
|
12
|
+
def initialize(values=nil)
|
13
|
+
if not (values.nil? or values.kind_of?(Set))
|
14
|
+
raise ArgumentError, "Values must be a set!"
|
15
|
+
end
|
16
|
+
@values = values.nil? ? Set.new : values
|
17
|
+
@undoStack = Array.new
|
18
|
+
end
|
19
|
+
|
20
|
+
# Adds value to the domain.
|
21
|
+
def <<(value)
|
22
|
+
@values << value
|
23
|
+
end
|
24
|
+
|
25
|
+
# Deletes value from the domain.
|
26
|
+
def delete(value)
|
27
|
+
@values.delete(value)
|
28
|
+
end
|
29
|
+
|
30
|
+
def size
|
31
|
+
@values.size
|
32
|
+
end
|
33
|
+
|
34
|
+
def first
|
35
|
+
@values.entries[0]
|
36
|
+
end
|
37
|
+
|
38
|
+
def include_any?(enum)
|
39
|
+
@values.include_any?(enum)
|
40
|
+
end
|
41
|
+
|
42
|
+
def each
|
43
|
+
@values.each { |value|
|
44
|
+
yield value
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
def sort(&block)
|
49
|
+
@values.sort(&block)
|
50
|
+
end
|
51
|
+
|
52
|
+
def collect(&block)
|
53
|
+
values.collect(&block)
|
54
|
+
end
|
55
|
+
|
56
|
+
def empty?
|
57
|
+
@values.empty?
|
58
|
+
end
|
59
|
+
|
60
|
+
# Prunes the values from the domain.
|
61
|
+
def prune(values)
|
62
|
+
if not values.kind_of?(Set)
|
63
|
+
values = Set.new([ values ])
|
64
|
+
end
|
65
|
+
@undoStack.push(@values)
|
66
|
+
@values -= values
|
67
|
+
if @values.empty?
|
68
|
+
raise DomainWipeoutException
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Undoes pruning by replacing the current list of values with the one
|
73
|
+
# before the last time prune was called.
|
74
|
+
def undoPruning
|
75
|
+
if @undoStack.empty?
|
76
|
+
raise UndoStackEmptyException, "No more prunes to undo!"
|
77
|
+
end
|
78
|
+
@values = @undoStack.pop
|
79
|
+
end
|
80
|
+
|
81
|
+
def include?(value)
|
82
|
+
@values.include?(value)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
"{" + @values.entries.join(", ") + "}"
|
87
|
+
end
|
88
|
+
|
89
|
+
def ==(domain)
|
90
|
+
return false unless domain.kind_of?(Domain)
|
91
|
+
(@values == domain.values) and (@undoStack == domain.undoStack)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
class UndoStackEmptyException < Exception
|
96
|
+
end
|
97
|
+
|
98
|
+
class DomainWipeoutException < Exception
|
99
|
+
end
|
100
|
+
end
|
data/lib/GraphUtils.rb
ADDED
@@ -0,0 +1,293 @@
|
|
1
|
+
#!/usr/bin/ruby
|
2
|
+
|
3
|
+
require 'set'
|
4
|
+
require 'extensions'
|
5
|
+
|
6
|
+
module GraphUtils
|
7
|
+
# Represents a bipartite graph.
|
8
|
+
class BipartiteGraph
|
9
|
+
attr_reader :x
|
10
|
+
attr_accessor :e, :y
|
11
|
+
# First argument to the constructor is the first set of values, second
|
12
|
+
# argument is the second set. The third argument is a hash which maps
|
13
|
+
# elements from the first set to an array of elements from the second
|
14
|
+
# set, designating edges (adjancency list).
|
15
|
+
def initialize(x, y, e)
|
16
|
+
unless x.kind_of?(Set) and y.kind_of?(Set) and e.kind_of?(Hash) and
|
17
|
+
not x.empty? and not y.empty? and not e.empty?
|
18
|
+
raise ArgumentError,
|
19
|
+
"Two non-empty sets of values and a hash where key => [value] designates edges must be given!"
|
20
|
+
end
|
21
|
+
@x = x
|
22
|
+
@y = y
|
23
|
+
@e = e
|
24
|
+
@max_matching = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
# algorithm to compute maximum matching in O(\sqrt{V} E) taken from
|
28
|
+
# Perl package Graph::Bipartite
|
29
|
+
def maximum_matching
|
30
|
+
@e_double = @e.dup
|
31
|
+
@e.each_key { |k|
|
32
|
+
@e[k].each { |v|
|
33
|
+
if not @e_double.has_key?(v)
|
34
|
+
@e_double[v] = Set.new
|
35
|
+
end
|
36
|
+
@e_double[v].add(k)
|
37
|
+
}
|
38
|
+
}
|
39
|
+
matching = Hash.new
|
40
|
+
@e_double.each_key { |key|
|
41
|
+
matching[key] = nil
|
42
|
+
}
|
43
|
+
if @max_matching.nil?
|
44
|
+
recalc = true
|
45
|
+
else
|
46
|
+
recalc = false
|
47
|
+
@max_matching.each { |k,v|
|
48
|
+
if not @e[k].include?(v)
|
49
|
+
recalc = true
|
50
|
+
else
|
51
|
+
# recompute the new matching based on the old matching
|
52
|
+
matching[k] = v
|
53
|
+
matching[v] = k
|
54
|
+
end
|
55
|
+
}
|
56
|
+
end
|
57
|
+
if recalc
|
58
|
+
level = Hash.new
|
59
|
+
while sbfs(matching, level) > 0
|
60
|
+
sdfs(matching, level)
|
61
|
+
end
|
62
|
+
@max_matching = matching.delete_if { |k,v| @y.include?(k) or v.nil? }
|
63
|
+
end
|
64
|
+
return @max_matching
|
65
|
+
end
|
66
|
+
|
67
|
+
# Computes a mapping from x value to a set of y values. Each mapping
|
68
|
+
# designates an arc which can be removed because it doesn't belong to
|
69
|
+
# any matching. The method requires a matching as an argument.
|
70
|
+
def removable_values(matching)
|
71
|
+
edges = Array.new
|
72
|
+
label = Hash.new
|
73
|
+
@e.each_key { |k|
|
74
|
+
@e[k].each { |v|
|
75
|
+
if matching.has_key?(k) and matching[k] == v
|
76
|
+
edge = DirectedEdge.new(k, v)
|
77
|
+
edges << edge
|
78
|
+
label[edge] = :used
|
79
|
+
else
|
80
|
+
edges << DirectedEdge.new(v, k)
|
81
|
+
end
|
82
|
+
}
|
83
|
+
}
|
84
|
+
|
85
|
+
# edges in strongly connected components
|
86
|
+
GraphUtils::strongly_connected_components(@x | @y, edges).each { |component|
|
87
|
+
componentHelper = Hash.new
|
88
|
+
component.each { |i| componentHelper[i] = 1 }
|
89
|
+
(edges.select { |edge|
|
90
|
+
componentHelper.has_key?(edge.startVertex) and componentHelper.has_key?(edge.endVertex)
|
91
|
+
}).each { |edge|
|
92
|
+
label[edge] = :used
|
93
|
+
}
|
94
|
+
}
|
95
|
+
|
96
|
+
# edges traversed during breadth-first search for m-alternating
|
97
|
+
# paths starting at m-free vertices
|
98
|
+
((@x - matching.keys) + (@y - matching.values)).each { |vertex|
|
99
|
+
GraphUtils::find_paths(vertex, edges).each { |edge|
|
100
|
+
label[edge] = :used
|
101
|
+
}
|
102
|
+
}
|
103
|
+
|
104
|
+
pruneMap = Hash.new
|
105
|
+
(edges.delete_if { |edge| label.has_key?(edge) }).each { |edge|
|
106
|
+
if not pruneMap.has_key?(edge.endVertex)
|
107
|
+
pruneMap[edge.endVertex] = Set.new
|
108
|
+
end
|
109
|
+
pruneMap[edge.endVertex].add(edge.startVertex)
|
110
|
+
}
|
111
|
+
|
112
|
+
return pruneMap
|
113
|
+
end
|
114
|
+
|
115
|
+
private
|
116
|
+
|
117
|
+
def sbfs(matching, level)
|
118
|
+
stack1 = Array.new
|
119
|
+
stack2 = Array.new
|
120
|
+
@x.each { |x|
|
121
|
+
if matching[x].nil?
|
122
|
+
level[x] = 0
|
123
|
+
stack1 << x
|
124
|
+
else
|
125
|
+
level[x] = -1
|
126
|
+
end
|
127
|
+
}
|
128
|
+
@y.each { |y|
|
129
|
+
level[y] = -1
|
130
|
+
}
|
131
|
+
while not stack1.empty?
|
132
|
+
stack2.clear
|
133
|
+
free = nil
|
134
|
+
while not stack1.empty?
|
135
|
+
x = stack1.pop
|
136
|
+
@e_double[x].each { |y|
|
137
|
+
if matching[x] != y and level[y] == -1
|
138
|
+
level[y] = level[x] + 1
|
139
|
+
stack2 << y
|
140
|
+
free = y if matching[y].nil?
|
141
|
+
end
|
142
|
+
}
|
143
|
+
end
|
144
|
+
if not free.nil?
|
145
|
+
return 1
|
146
|
+
end
|
147
|
+
stack1.clear
|
148
|
+
while not stack2.empty?
|
149
|
+
y = stack2.pop
|
150
|
+
@e_double[y].each { |x|
|
151
|
+
if matching[y] == x and level[x] == -1
|
152
|
+
level[x] = level[y] + 1
|
153
|
+
stack1 << x
|
154
|
+
end
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
return 0
|
160
|
+
end
|
161
|
+
|
162
|
+
def sdfs(matching, level)
|
163
|
+
@x.each { |x|
|
164
|
+
if matching[x].nil?
|
165
|
+
rec_sdfs(matching, level, x, nil)
|
166
|
+
end
|
167
|
+
}
|
168
|
+
end
|
169
|
+
|
170
|
+
def rec_sdfs(matching, level, x, y)
|
171
|
+
if y.nil?
|
172
|
+
@e_double[x].each { |y|
|
173
|
+
if (matching[x] != y) and (level[y] == level[x] + 1)
|
174
|
+
if rec_sdfs(matching, level, nil, y) == 1
|
175
|
+
matching[x] = y
|
176
|
+
matching[y] = x
|
177
|
+
level[x] = -1
|
178
|
+
return 1
|
179
|
+
end
|
180
|
+
end
|
181
|
+
}
|
182
|
+
level[x] = -1
|
183
|
+
else
|
184
|
+
if matching[y].nil?
|
185
|
+
level[y] = -1
|
186
|
+
return 1
|
187
|
+
else
|
188
|
+
@e_double[y].each { |x|
|
189
|
+
if (matching[y] == x) and (level[x] == level[y] + 1)
|
190
|
+
if rec_sdfs(matching, level, x, nil) == 1
|
191
|
+
level[y] = -1
|
192
|
+
return 1
|
193
|
+
end
|
194
|
+
end
|
195
|
+
}
|
196
|
+
end
|
197
|
+
level[y] = -1
|
198
|
+
end
|
199
|
+
return 0
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
# Represents a directed edge in a graph.
|
204
|
+
class DirectedEdge
|
205
|
+
attr_reader :startVertex, :endVertex
|
206
|
+
def initialize(startVertex, endVertex)
|
207
|
+
@startVertex = startVertex
|
208
|
+
@endVertex = endVertex
|
209
|
+
end
|
210
|
+
|
211
|
+
def include?(v)
|
212
|
+
v == startVertex or v == endVertex
|
213
|
+
end
|
214
|
+
|
215
|
+
def ==(edge)
|
216
|
+
edge.kind_of?(DirectedEdge) and @startVertex == edge.startVertex and @endVertex == edge.endVertex
|
217
|
+
end
|
218
|
+
|
219
|
+
def to_s
|
220
|
+
@startVertex.to_s + " -> " + @endVertex.to_s
|
221
|
+
end
|
222
|
+
end
|
223
|
+
|
224
|
+
class << self
|
225
|
+
# Performs a breadth-first traversal of the graph given by the list of
|
226
|
+
# edges starting at startVertex.
|
227
|
+
# Returned is the list of edges that were traversed.
|
228
|
+
def find_paths(startVertex, edges)
|
229
|
+
arcs = Set.new
|
230
|
+
edgeQueue = edges.find_all { |edge| edge.startVertex == startVertex }.to_set
|
231
|
+
while not edgeQueue.empty?
|
232
|
+
currentEdge = edgeQueue.find { |i| true }
|
233
|
+
edgeQueue.delete(currentEdge)
|
234
|
+
arcs.add(currentEdge)
|
235
|
+
nextEdges = edges.find_all { |edge| edge.startVertex == currentEdge.endVertex }
|
236
|
+
edgeQueue.merge(nextEdges.delete_if { |edge| arcs.include?(edge) })
|
237
|
+
end
|
238
|
+
|
239
|
+
return arcs.to_a
|
240
|
+
end
|
241
|
+
|
242
|
+
# Computes the strongly connected components in a graph given by its
|
243
|
+
# edges. The first argument is a set of nodes in the graph, the second
|
244
|
+
# argument is a list of directed edges.
|
245
|
+
# Returned is an array of strongly connected components, each one an array
|
246
|
+
# of nodes in the component.
|
247
|
+
# Algorithm due Robert Tarjan (1972).
|
248
|
+
def strongly_connected_components(nodes, edges)
|
249
|
+
edge_cache = Hash.new
|
250
|
+
nodes.each { |node|
|
251
|
+
edge_cache[node] = Array.new
|
252
|
+
}
|
253
|
+
edges.each { |edge|
|
254
|
+
edge_cache[edge.startVertex] << edge
|
255
|
+
}
|
256
|
+
retval = Array.new
|
257
|
+
nodeQueue = nodes.dup
|
258
|
+
while not nodeQueue.empty?
|
259
|
+
retval += tarjan(nodeQueue.find { |i| true }, nodeQueue, edge_cache, [], 0, {}, {})
|
260
|
+
end
|
261
|
+
|
262
|
+
return retval
|
263
|
+
end
|
264
|
+
|
265
|
+
private
|
266
|
+
|
267
|
+
def tarjan(node, nodeQueue, edges, stack, distance, distances, lowlink)
|
268
|
+
retval = Array.new
|
269
|
+
distances[node] = distance
|
270
|
+
lowlink[node] = distance
|
271
|
+
stack << node
|
272
|
+
nodeQueue.delete(node)
|
273
|
+
edges[node].each { |e|
|
274
|
+
distance += 1
|
275
|
+
v = e.endVertex
|
276
|
+
if nodeQueue.include?(v)
|
277
|
+
retval = tarjan(v, nodeQueue, edges, stack, distance, distances, lowlink)
|
278
|
+
lowlink[node] = lowlink[v] if lowlink[v] < lowlink[node]
|
279
|
+
elsif stack.include?(v)
|
280
|
+
lowlink[node] = distances[v] if distances[v] < lowlink[node]
|
281
|
+
end
|
282
|
+
}
|
283
|
+
if lowlink[node] == distances[node]
|
284
|
+
nodes = stack.slice!(stack.index(node), stack.size)
|
285
|
+
if nodes.size > 1
|
286
|
+
retval << nodes
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
return (retval.nil? ? nil : retval.compact)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|