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.
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