ConstraintSolver 0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (155) hide show
  1. data/bin/ConstraintSolver +24 -0
  2. data/doc/classes/Array.html +209 -0
  3. data/doc/classes/ConstraintSolver.html +242 -0
  4. data/doc/classes/ConstraintSolver/AbstractConstraint.html +317 -0
  5. data/doc/classes/ConstraintSolver/AllDifferentConstraint.html +451 -0
  6. data/doc/classes/ConstraintSolver/AllDifferentConstraintTest.html +397 -0
  7. data/doc/classes/ConstraintSolver/BinaryConstraint.html +483 -0
  8. data/doc/classes/ConstraintSolver/BinaryConstraintTest.html +367 -0
  9. data/doc/classes/ConstraintSolver/BinaryRelation.html +276 -0
  10. data/doc/classes/ConstraintSolver/BinaryRelationTest.html +194 -0
  11. data/doc/classes/ConstraintSolver/ConstraintList.html +208 -0
  12. data/doc/classes/ConstraintSolver/ConstraintListTest.html +252 -0
  13. data/doc/classes/ConstraintSolver/ConstraintSolver.html +353 -0
  14. data/doc/classes/ConstraintSolver/ConstraintSolverTest.html +403 -0
  15. data/doc/classes/ConstraintSolver/Domain.html +522 -0
  16. data/doc/classes/ConstraintSolver/DomainTest.html +356 -0
  17. data/doc/classes/ConstraintSolver/DomainWipeoutException.html +158 -0
  18. data/doc/classes/ConstraintSolver/Problem.html +239 -0
  19. data/doc/classes/ConstraintSolver/ProblemTest.html +227 -0
  20. data/doc/classes/ConstraintSolver/Solution.html +342 -0
  21. data/doc/classes/ConstraintSolver/SolutionTest.html +250 -0
  22. data/doc/classes/ConstraintSolver/UndoStackEmptyException.html +158 -0
  23. data/doc/classes/ConstraintSolver/Variable.html +418 -0
  24. data/doc/classes/ConstraintSolver/VariableTest.html +284 -0
  25. data/doc/classes/ExtensionsTest.html +233 -0
  26. data/doc/classes/Fixnum.html +153 -0
  27. data/doc/created.rid +1 -0
  28. data/doc/dot/f_0.dot +38 -0
  29. data/doc/dot/f_0.png +0 -0
  30. data/doc/dot/f_1.dot +392 -0
  31. data/doc/dot/f_1.png +0 -0
  32. data/doc/dot/f_10.dot +392 -0
  33. data/doc/dot/f_10.png +0 -0
  34. data/doc/dot/f_11.dot +38 -0
  35. data/doc/dot/f_11.png +0 -0
  36. data/doc/dot/f_12.dot +392 -0
  37. data/doc/dot/f_12.png +0 -0
  38. data/doc/dot/f_13.dot +392 -0
  39. data/doc/dot/f_13.png +0 -0
  40. data/doc/dot/f_14.dot +392 -0
  41. data/doc/dot/f_14.png +0 -0
  42. data/doc/dot/f_15.dot +392 -0
  43. data/doc/dot/f_15.png +0 -0
  44. data/doc/dot/f_16.dot +392 -0
  45. data/doc/dot/f_16.png +0 -0
  46. data/doc/dot/f_17.dot +392 -0
  47. data/doc/dot/f_17.png +0 -0
  48. data/doc/dot/f_18.dot +392 -0
  49. data/doc/dot/f_18.png +0 -0
  50. data/doc/dot/f_19.dot +392 -0
  51. data/doc/dot/f_19.png +0 -0
  52. data/doc/dot/f_2.dot +392 -0
  53. data/doc/dot/f_2.png +0 -0
  54. data/doc/dot/f_3.dot +392 -0
  55. data/doc/dot/f_3.png +0 -0
  56. data/doc/dot/f_4.dot +392 -0
  57. data/doc/dot/f_4.png +0 -0
  58. data/doc/dot/f_5.dot +392 -0
  59. data/doc/dot/f_5.png +0 -0
  60. data/doc/dot/f_6.dot +14 -0
  61. data/doc/dot/f_6.png +0 -0
  62. data/doc/dot/f_7.dot +392 -0
  63. data/doc/dot/f_7.png +0 -0
  64. data/doc/dot/f_8.dot +392 -0
  65. data/doc/dot/f_8.png +0 -0
  66. data/doc/dot/f_9.dot +392 -0
  67. data/doc/dot/f_9.png +0 -0
  68. data/doc/dot/m_10_0.dot +392 -0
  69. data/doc/dot/m_10_0.png +0 -0
  70. data/doc/dot/m_12_0.dot +392 -0
  71. data/doc/dot/m_12_0.png +0 -0
  72. data/doc/dot/m_13_0.dot +392 -0
  73. data/doc/dot/m_13_0.png +0 -0
  74. data/doc/dot/m_14_0.dot +392 -0
  75. data/doc/dot/m_14_0.png +0 -0
  76. data/doc/dot/m_15_0.dot +392 -0
  77. data/doc/dot/m_15_0.png +0 -0
  78. data/doc/dot/m_16_0.dot +392 -0
  79. data/doc/dot/m_16_0.png +0 -0
  80. data/doc/dot/m_17_0.dot +392 -0
  81. data/doc/dot/m_17_0.png +0 -0
  82. data/doc/dot/m_18_0.dot +392 -0
  83. data/doc/dot/m_18_0.png +0 -0
  84. data/doc/dot/m_19_0.dot +392 -0
  85. data/doc/dot/m_19_0.png +0 -0
  86. data/doc/dot/m_1_0.dot +392 -0
  87. data/doc/dot/m_1_0.png +0 -0
  88. data/doc/dot/m_2_0.dot +392 -0
  89. data/doc/dot/m_2_0.png +0 -0
  90. data/doc/dot/m_3_0.dot +392 -0
  91. data/doc/dot/m_3_0.png +0 -0
  92. data/doc/dot/m_4_0.dot +392 -0
  93. data/doc/dot/m_4_0.png +0 -0
  94. data/doc/dot/m_5_0.dot +392 -0
  95. data/doc/dot/m_5_0.png +0 -0
  96. data/doc/dot/m_7_0.dot +392 -0
  97. data/doc/dot/m_7_0.png +0 -0
  98. data/doc/dot/m_8_0.dot +392 -0
  99. data/doc/dot/m_8_0.png +0 -0
  100. data/doc/dot/m_9_0.dot +392 -0
  101. data/doc/dot/m_9_0.png +0 -0
  102. data/doc/files/lib/AbstractConstraint_rb.html +148 -0
  103. data/doc/files/lib/AllDifferentConstraint_rb.html +156 -0
  104. data/doc/files/lib/BinaryConstraint_rb.html +155 -0
  105. data/doc/files/lib/ConstraintList_rb.html +148 -0
  106. data/doc/files/lib/ConstraintSolver_rb.html +162 -0
  107. data/doc/files/lib/Domain_rb.html +155 -0
  108. data/doc/files/lib/Problem_rb.html +148 -0
  109. data/doc/files/lib/Solution_rb.html +148 -0
  110. data/doc/files/lib/Variable_rb.html +148 -0
  111. data/doc/files/lib/extensions_rb.html +108 -0
  112. data/doc/files/test/AllDifferentConstraintTest_rb.html +158 -0
  113. data/doc/files/test/BinaryConstraintTest_rb.html +158 -0
  114. data/doc/files/test/ConstraintListTest_rb.html +160 -0
  115. data/doc/files/test/ConstraintSolverTest_rb.html +164 -0
  116. data/doc/files/test/DomainTest_rb.html +156 -0
  117. data/doc/files/test/ProblemTest_rb.html +160 -0
  118. data/doc/files/test/SolutionTest_rb.html +159 -0
  119. data/doc/files/test/TestSuite_rb.html +113 -0
  120. data/doc/files/test/VariableTest_rb.html +157 -0
  121. data/doc/files/test/extensionsTest_rb.html +118 -0
  122. data/doc/fr_class_index.html +51 -0
  123. data/doc/fr_file_index.html +46 -0
  124. data/doc/fr_method_index.html +133 -0
  125. data/doc/index.html +24 -0
  126. data/examples/example.rb +7 -0
  127. data/examples/queens.rb +13 -0
  128. data/examples/soft.rb +14 -0
  129. data/lib/AbstractConstraint.rb +45 -0
  130. data/lib/AllDifferentConstraint.rb +160 -0
  131. data/lib/BinaryConstraint.rb +187 -0
  132. data/lib/ConstraintList.rb +31 -0
  133. data/lib/ConstraintSolver.rb +213 -0
  134. data/lib/Domain.rb +100 -0
  135. data/lib/GraphUtils.rb +293 -0
  136. data/lib/OneOfEqualsConstraint.rb +81 -0
  137. data/lib/Problem.rb +30 -0
  138. data/lib/Solution.rb +56 -0
  139. data/lib/TupleConstraint.rb +111 -0
  140. data/lib/Variable.rb +74 -0
  141. data/lib/extensions.rb +55 -0
  142. data/test/AllDifferentConstraintTest.rb +140 -0
  143. data/test/BinaryConstraintTest.rb +108 -0
  144. data/test/ConstraintListTest.rb +41 -0
  145. data/test/ConstraintSolverTest.rb +274 -0
  146. data/test/DomainTest.rb +83 -0
  147. data/test/GraphUtilsTest.rb +83 -0
  148. data/test/OneOfEqualsConstraintTest.rb +82 -0
  149. data/test/ProblemTest.rb +35 -0
  150. data/test/SolutionTest.rb +35 -0
  151. data/test/TestSuite.rb +10 -0
  152. data/test/TupleConstraintTest.rb +151 -0
  153. data/test/VariableTest.rb +47 -0
  154. data/test/extensionsTest.rb +57 -0
  155. 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