ConstraintSolver 0.1
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/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
|