graph_matching 0.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.
- checksums.yaml +7 -0
- data/.gitignore +20 -0
- data/.rubocop.yml +112 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +205 -0
- data/Rakefile +9 -0
- data/benchmark/mcm_bipartite/complete_bigraphs/benchmark.rb +33 -0
- data/benchmark/mcm_bipartite/complete_bigraphs/compare.gnuplot +19 -0
- data/benchmark/mcm_bipartite/complete_bigraphs/edges_times_vertexes.data +500 -0
- data/benchmark/mcm_bipartite/complete_bigraphs/plot.gnuplot +21 -0
- data/benchmark/mcm_bipartite/complete_bigraphs/plot.png +0 -0
- data/benchmark/mcm_bipartite/complete_bigraphs/time.data +499 -0
- data/benchmark/mcm_general/complete_graphs/benchmark.rb +30 -0
- data/benchmark/mcm_general/complete_graphs/plot.gnuplot +19 -0
- data/benchmark/mcm_general/complete_graphs/plot.png +0 -0
- data/benchmark/mcm_general/complete_graphs/time.data +499 -0
- data/benchmark/mcm_general/complete_graphs/v_cubed.data +500 -0
- data/benchmark/mwm_bipartite/complete_bigraphs/benchmark.rb +43 -0
- data/benchmark/mwm_bipartite/complete_bigraphs/nmN.data +499 -0
- data/benchmark/mwm_bipartite/complete_bigraphs/nmN.xlsx +0 -0
- data/benchmark/mwm_bipartite/complete_bigraphs/plot.gnuplot +22 -0
- data/benchmark/mwm_bipartite/complete_bigraphs/plot.png +0 -0
- data/benchmark/mwm_bipartite/complete_bigraphs/time.data +299 -0
- data/benchmark/mwm_bipartite/misc/calc_d2/benchmark.rb +29 -0
- data/benchmark/mwm_general/complete_graphs/benchmark.rb +32 -0
- data/benchmark/mwm_general/complete_graphs/compare.gnuplot +19 -0
- data/benchmark/mwm_general/complete_graphs/mn_log_n.data +299 -0
- data/benchmark/mwm_general/complete_graphs/mn_log_n.xlsx +0 -0
- data/benchmark/mwm_general/complete_graphs/plot.gnuplot +22 -0
- data/benchmark/mwm_general/complete_graphs/plot.png +0 -0
- data/benchmark/mwm_general/complete_graphs/time.data +299 -0
- data/benchmark/mwm_general/incomplete_graphs/benchmark.rb +39 -0
- data/benchmark/mwm_general/incomplete_graphs/plot.gnuplot +22 -0
- data/benchmark/mwm_general/incomplete_graphs/plot.png +0 -0
- data/benchmark/mwm_general/incomplete_graphs/time_10_pct.data +299 -0
- data/benchmark/mwm_general/incomplete_graphs/time_20_pct.data +299 -0
- data/benchmark/mwm_general/incomplete_graphs/time_30_pct.data +299 -0
- data/graph_matching.gemspec +35 -0
- data/lib/graph_matching.rb +15 -0
- data/lib/graph_matching/algorithm/matching_algorithm.rb +23 -0
- data/lib/graph_matching/algorithm/mcm_bipartite.rb +118 -0
- data/lib/graph_matching/algorithm/mcm_general.rb +289 -0
- data/lib/graph_matching/algorithm/mwm_bipartite.rb +147 -0
- data/lib/graph_matching/algorithm/mwm_general.rb +1086 -0
- data/lib/graph_matching/algorithm/mwmg_delta_assertions.rb +94 -0
- data/lib/graph_matching/assertion.rb +41 -0
- data/lib/graph_matching/core_ext/set.rb +36 -0
- data/lib/graph_matching/directed_edge_set.rb +31 -0
- data/lib/graph_matching/errors.rb +23 -0
- data/lib/graph_matching/graph/bigraph.rb +37 -0
- data/lib/graph_matching/graph/graph.rb +63 -0
- data/lib/graph_matching/graph/weighted.rb +112 -0
- data/lib/graph_matching/graph/weighted_bigraph.rb +17 -0
- data/lib/graph_matching/graph/weighted_graph.rb +17 -0
- data/lib/graph_matching/integer_vertexes.rb +29 -0
- data/lib/graph_matching/matching.rb +120 -0
- data/lib/graph_matching/ordered_set.rb +59 -0
- data/lib/graph_matching/version.rb +6 -0
- data/lib/graph_matching/visualize.rb +93 -0
- data/profile/mcm_bipartite/compare.sh +15 -0
- data/profile/mcm_bipartite/publish.sh +12 -0
- data/profile/mwm_general/compare.sh +15 -0
- data/profile/mwm_general/profile.rb +28 -0
- data/profile/mwm_general/publish.sh +12 -0
- data/research/1965_edmonds.pdf +0 -0
- data/research/1975_even_kariv.pdf +0 -0
- data/research/1976_gabow.pdf +0 -0
- data/research/1980_micali_vazirani.pdf +0 -0
- data/research/1985_gabow.pdf +0 -0
- data/research/2002_tarjan.pdf +0 -0
- data/research/2013_zwick.pdf +0 -0
- data/research/examples/unweighted_general/1.txt +86 -0
- data/research/goodwin.pdf +0 -0
- data/research/kavathekar-scribe.pdf +0 -0
- data/research/kusner.pdf +0 -0
- data/research/van_rantwijk/mwm_example.py +19 -0
- data/research/van_rantwijk/mwmatching.py +945 -0
- data/spec/graph_matching/algorithm/matching_algorithm_spec.rb +14 -0
- data/spec/graph_matching/algorithm/mcm_bipartite_spec.rb +98 -0
- data/spec/graph_matching/algorithm/mcm_general_spec.rb +159 -0
- data/spec/graph_matching/algorithm/mwm_bipartite_spec.rb +82 -0
- data/spec/graph_matching/algorithm/mwm_general_spec.rb +439 -0
- data/spec/graph_matching/graph/bigraph_spec.rb +73 -0
- data/spec/graph_matching/graph/graph_spec.rb +53 -0
- data/spec/graph_matching/graph/weighted_spec.rb +29 -0
- data/spec/graph_matching/integer_vertexes_spec.rb +21 -0
- data/spec/graph_matching/matching_spec.rb +89 -0
- data/spec/graph_matching/visualize_spec.rb +38 -0
- data/spec/graph_matching_spec.rb +9 -0
- data/spec/spec_helper.rb +26 -0
- metadata +263 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require_relative '../graph/weighted_bigraph'
|
|
4
|
+
require_relative '../matching'
|
|
5
|
+
require_relative 'matching_algorithm'
|
|
6
|
+
|
|
7
|
+
module GraphMatching
|
|
8
|
+
module Algorithm
|
|
9
|
+
|
|
10
|
+
# `MWMBipartite` implements Maximum Weighted Matching in
|
|
11
|
+
# bipartite graphs. It extends Maximum Cardinality
|
|
12
|
+
# Matching for `Weighted` graphs.
|
|
13
|
+
class MWMBipartite < MCMBipartite
|
|
14
|
+
|
|
15
|
+
def initialize(graph)
|
|
16
|
+
assert(graph).is_a(Graph::WeightedBigraph)
|
|
17
|
+
super
|
|
18
|
+
|
|
19
|
+
# Optimization: Keeping a reference to the graph's weights
|
|
20
|
+
# in the instance, instead of calling `Weighted#w`
|
|
21
|
+
# thousands of times, is twice as fast.
|
|
22
|
+
@weight = graph.weight
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def match
|
|
26
|
+
m = []
|
|
27
|
+
dogs, cats = g.partition
|
|
28
|
+
u = init_duals(cats, dogs)
|
|
29
|
+
|
|
30
|
+
# For each stage
|
|
31
|
+
while true do
|
|
32
|
+
|
|
33
|
+
# Clear all labels and marks
|
|
34
|
+
# Label all single dogs with S
|
|
35
|
+
aug_path = nil
|
|
36
|
+
predecessors = {}
|
|
37
|
+
t = Set.new
|
|
38
|
+
s = Set.new(dogs) - m
|
|
39
|
+
q = s.dup.to_a
|
|
40
|
+
|
|
41
|
+
# While searching
|
|
42
|
+
while aug_path.nil? && i = q.pop do
|
|
43
|
+
|
|
44
|
+
# Follow the unmatched edges (if any) to free (unlabeled)
|
|
45
|
+
# cats. Only consider edges with slack (π) of 0.
|
|
46
|
+
unlabeled_across_unmatched_edges_from(i, g, m ,t).each do |j|
|
|
47
|
+
if π(u, i, j) == 0
|
|
48
|
+
t << j
|
|
49
|
+
predecessors[j] = i
|
|
50
|
+
|
|
51
|
+
# If there are matched edges, follow each to a dog and
|
|
52
|
+
# label the dog with S. Otherwise, backtrack to
|
|
53
|
+
# construct an augmenting path.
|
|
54
|
+
m_dogs = matched_adjacent(j, i, g, m)
|
|
55
|
+
|
|
56
|
+
m_dogs.each do |md|
|
|
57
|
+
s << md
|
|
58
|
+
predecessors[md] = j
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
if m_dogs.empty?
|
|
62
|
+
aug_path = backtrack_from(j, predecessors)
|
|
63
|
+
break
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# We have looked at every S-dog.
|
|
70
|
+
# If no `aug_path` was found, the search failed.
|
|
71
|
+
# Adjust the duals and search again.
|
|
72
|
+
if aug_path.nil?
|
|
73
|
+
d1 = calc_d1(s, u)
|
|
74
|
+
d2 = calc_d2(s, t, u)
|
|
75
|
+
d = [d1, d2].compact.min
|
|
76
|
+
|
|
77
|
+
# If d == d1, then the smallest dual is equal to the
|
|
78
|
+
# smallest slack, and the duals of all single dogs are
|
|
79
|
+
# zero. Therefore, we're totally done.
|
|
80
|
+
#
|
|
81
|
+
# Otherwise, adjust the duals by subtracting d from S-dog
|
|
82
|
+
# duals and adding d to T-cat duals.
|
|
83
|
+
if d == d1
|
|
84
|
+
break
|
|
85
|
+
else
|
|
86
|
+
s.each do |si| u[si] = u[si] - d end
|
|
87
|
+
t.each do |ti| u[ti] = u[ti] + d end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
else
|
|
91
|
+
m = augment(m, aug_path)
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Matching.gabow(m)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
private
|
|
99
|
+
|
|
100
|
+
def assert_weighted_bipartite(graph)
|
|
101
|
+
unless weighted_bipartite?(graph)
|
|
102
|
+
raise ArgumentError, 'Expected a weighted bipartite graph'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Returns d1, min of S-dog duals
|
|
107
|
+
def calc_d1(s, u)
|
|
108
|
+
u.values_at(*s).min
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Returns d2, the smallest slack between S-dogs and free
|
|
112
|
+
# cats. This is a fairly expensive method, due to the
|
|
113
|
+
# nested loop.
|
|
114
|
+
def calc_d2(s, t, u)
|
|
115
|
+
slacks = []
|
|
116
|
+
s.each do |s_dog|
|
|
117
|
+
g.each_adjacent(s_dog) do |cat|
|
|
118
|
+
unless t.include?(cat)
|
|
119
|
+
slacks.push π(u, s_dog, cat)
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
slacks.min
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Initialize the "dual" values
|
|
127
|
+
def init_duals(cats, dogs)
|
|
128
|
+
u = []
|
|
129
|
+
ui = g.max_w
|
|
130
|
+
dogs.each do |i| u[i] = ui end
|
|
131
|
+
cats.each do |j| u[j] = 0 end
|
|
132
|
+
u
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def weighted_bipartite?(graph)
|
|
136
|
+
graph.respond_to?(:partition) && graph.respond_to?(:w)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Returns the "slack" of an edge (Galil, 1986, p.30), the
|
|
140
|
+
# difference between an edge's duals and its weight.
|
|
141
|
+
def π(u, i, j)
|
|
142
|
+
u[i] + u[j] - @weight[i - 1][j - 1]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,1086 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require_relative '../graph/weighted_graph'
|
|
4
|
+
require_relative '../matching'
|
|
5
|
+
require_relative 'matching_algorithm'
|
|
6
|
+
|
|
7
|
+
module GraphMatching
|
|
8
|
+
module Algorithm
|
|
9
|
+
|
|
10
|
+
# `MWMGeneral` implements Maximum Weighted Matching in
|
|
11
|
+
# general graphs.
|
|
12
|
+
class MWMGeneral < MatchingAlgorithm
|
|
13
|
+
|
|
14
|
+
# If b is a top-level blossom,
|
|
15
|
+
# label[b] is 0 if b is unlabeled (free);
|
|
16
|
+
# 1 if b is an S-vertex/blossom;
|
|
17
|
+
# 2 if b is a T-vertex/blossom.
|
|
18
|
+
LBL_FREE = 0
|
|
19
|
+
LBL_S = 1
|
|
20
|
+
LBL_T = 2
|
|
21
|
+
LBL_CRUMB = 5
|
|
22
|
+
LBL_NAMES = ['Free', 'S', 'T', 'Crumb']
|
|
23
|
+
|
|
24
|
+
def initialize(graph)
|
|
25
|
+
assert(graph).is_a(Graph::WeightedGraph)
|
|
26
|
+
super
|
|
27
|
+
init_graph_structures
|
|
28
|
+
init_algorithm_structures
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# > As in Problem 3, the algorithm consists of O(n) *stages*.
|
|
32
|
+
# > In each stage we look for an augmenting path using the
|
|
33
|
+
# > labeling R12 and the two cases C1, C2 as in the simple
|
|
34
|
+
# > algorithm for Problem 2, except that we only use edges
|
|
35
|
+
# > with π<sub>ij</sub> = 0. (Galil, 1986, p. 32)
|
|
36
|
+
#
|
|
37
|
+
# Van Rantwijk's implementation (and, consequently, this port)
|
|
38
|
+
# supports both maximum cardinality maximum weight matching
|
|
39
|
+
# and MWM irrespective of cardinality.
|
|
40
|
+
def match(max_cardinality)
|
|
41
|
+
return Matching.new if g.num_edges == 0
|
|
42
|
+
|
|
43
|
+
# Iterative *stages*. Each stage augments the matching.
|
|
44
|
+
# There can be at most n stages, where n is num. vertexes.
|
|
45
|
+
while true do
|
|
46
|
+
init_stage
|
|
47
|
+
|
|
48
|
+
# *sub-stages* either augment or scale the duals.
|
|
49
|
+
augmented = false
|
|
50
|
+
while true do
|
|
51
|
+
|
|
52
|
+
# > The search is conducted by scanning the S-vertices
|
|
53
|
+
# > in turn. (Galil, 1986, p. 26)
|
|
54
|
+
until augmented || @queue.empty?
|
|
55
|
+
augmented = scan_vertex(@queue.pop)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
break if augmented
|
|
59
|
+
|
|
60
|
+
# > There is no augmenting path under these constraints;
|
|
61
|
+
# > compute delta and reduce slack in the optimization problem.
|
|
62
|
+
# > (Van Rantwijk, mwmatching.py, line 732)
|
|
63
|
+
delta, delta_type, delta_edge, delta_blossom = calc_delta(max_cardinality)
|
|
64
|
+
update_duals(delta)
|
|
65
|
+
optimum = act_on_minimum_delta(delta_type, delta_edge, delta_blossom)
|
|
66
|
+
break if optimum
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# > Stop when no more augmenting path can be found.
|
|
70
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
71
|
+
break unless augmented
|
|
72
|
+
|
|
73
|
+
expand_tight_s_blossoms
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
Matching.from_endpoints(@endpoint, @mate)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
# > Take action at the point where minimum delta occurred.
|
|
82
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
83
|
+
def act_on_minimum_delta(delta_type, delta_edge, delta_blossom)
|
|
84
|
+
optimum = false
|
|
85
|
+
case delta_type
|
|
86
|
+
when 1
|
|
87
|
+
# > No further improvement possible; optimum reached.
|
|
88
|
+
optimum = true
|
|
89
|
+
when 2
|
|
90
|
+
# > Use the least-slack edge to continue the search.
|
|
91
|
+
@tight_edge[delta_edge] = true
|
|
92
|
+
i, j = @edges[delta_edge].to_a
|
|
93
|
+
if @label[@in_blossom[i]] == LBL_FREE
|
|
94
|
+
i, j = j, i
|
|
95
|
+
end
|
|
96
|
+
assert_label(@in_blossom[i], LBL_S)
|
|
97
|
+
@queue.push(i)
|
|
98
|
+
when 3
|
|
99
|
+
# > Use the least-slack edge to continue the search.
|
|
100
|
+
@tight_edge[delta_edge] = true
|
|
101
|
+
i, j = @edges[delta_edge].to_a
|
|
102
|
+
assert_label(@in_blossom[i], LBL_S)
|
|
103
|
+
@queue.push(i)
|
|
104
|
+
when 4
|
|
105
|
+
# > Expand the least-z blossom.
|
|
106
|
+
expand_blossom(delta_blossom, false)
|
|
107
|
+
else
|
|
108
|
+
fail "Invalid delta_type: #{delta_type}"
|
|
109
|
+
end
|
|
110
|
+
optimum
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# > Construct a new blossom with given base, containing edge
|
|
114
|
+
# > k which connects a pair of S vertices. Label the new
|
|
115
|
+
# > blossom as S; set its dual variable to zero; relabel its
|
|
116
|
+
# > T-vertices to S and add them to the queue.
|
|
117
|
+
# > (Van Rantwijk, mwmatching.py, line 270)
|
|
118
|
+
def add_blossom(base, k)
|
|
119
|
+
v, w = @edges[k].to_a
|
|
120
|
+
bb = @in_blossom[base]
|
|
121
|
+
bv = @in_blossom[v]
|
|
122
|
+
bw = @in_blossom[w]
|
|
123
|
+
|
|
124
|
+
# Create a new top-level blossom.
|
|
125
|
+
b = @unused_blossoms.pop
|
|
126
|
+
@blossom_base[b] = base
|
|
127
|
+
@blossom_parent[b] = nil
|
|
128
|
+
@blossom_parent[bb] = b
|
|
129
|
+
|
|
130
|
+
# > Make list of sub-blossoms and their interconnecting
|
|
131
|
+
# > edge endpoints.
|
|
132
|
+
# > (Van Rantwijk, mwmatching.py, line 284)
|
|
133
|
+
#
|
|
134
|
+
# 1. Clear the existing lists
|
|
135
|
+
# 2. Trace back from v to base
|
|
136
|
+
# 3. Reverse lists, add endpoint that connects the pair of S vertices
|
|
137
|
+
# 4. Trace back from w to base
|
|
138
|
+
#
|
|
139
|
+
@blossom_children[b] = []
|
|
140
|
+
@blossom_endps[b] = []
|
|
141
|
+
trace_to_base(bv, bb) do |bv|
|
|
142
|
+
@blossom_parent[bv] = b
|
|
143
|
+
@blossom_children[b] << bv
|
|
144
|
+
@blossom_endps[b] << @label_end[bv]
|
|
145
|
+
end
|
|
146
|
+
@blossom_children[b] << bb
|
|
147
|
+
@blossom_children[b].reverse!
|
|
148
|
+
@blossom_endps[b].reverse!
|
|
149
|
+
@blossom_endps[b] << 2 * k
|
|
150
|
+
trace_to_base(bw, bb) do |bw|
|
|
151
|
+
@blossom_parent[bw] = b
|
|
152
|
+
@blossom_children[b] << bw
|
|
153
|
+
@blossom_endps[b] << (@label_end[bw] ^ 1)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# > Set label to S
|
|
157
|
+
assert(@label[bb]).eq(LBL_S)
|
|
158
|
+
@label[b] = LBL_S
|
|
159
|
+
@label_end[b] = @label_end[bb]
|
|
160
|
+
|
|
161
|
+
# > Set dual variable to zero.
|
|
162
|
+
@dual[b] = 0
|
|
163
|
+
|
|
164
|
+
# > Relabel vertices.
|
|
165
|
+
blossom_leaves(b).each do |v|
|
|
166
|
+
if @label[@in_blossom[v]] == LBL_T
|
|
167
|
+
# > This T-vertex now turns into an S-vertex because it
|
|
168
|
+
# > becomes part of an S-blossom; add it to the queue.
|
|
169
|
+
@queue << v
|
|
170
|
+
end
|
|
171
|
+
@in_blossom[v] = b
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# > Compute blossombestedges[b].
|
|
175
|
+
best_edge_to = rantwijk_array(nil)
|
|
176
|
+
@blossom_children[b].each do |bv|
|
|
177
|
+
if @blossom_best_edges[bv].nil?
|
|
178
|
+
# > This subblossom [bv] does not have a list of least-
|
|
179
|
+
# > slack edges. Get the information from the vertices.
|
|
180
|
+
nblists = blossom_leaves(bv).map { |v|
|
|
181
|
+
@neighb_end[v].map { |p|
|
|
182
|
+
p / 2 # floor division
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
else
|
|
186
|
+
# > Walk this subblossom's least-slack edges.
|
|
187
|
+
nblists = [@blossom_best_edges[bv]]
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
nblists.each do |nblist|
|
|
191
|
+
nblist.each do |k|
|
|
192
|
+
i, j = @edges[k].to_a
|
|
193
|
+
if @in_blossom[j] == b
|
|
194
|
+
i, j = j, i
|
|
195
|
+
end
|
|
196
|
+
bj = @in_blossom[j]
|
|
197
|
+
if bj != b &&
|
|
198
|
+
@label[bj] == LBL_S &&
|
|
199
|
+
(best_edge_to[bj] == nil || slack(k) < slack(best_edge_to[bj]))
|
|
200
|
+
best_edge_to[bj] = k
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
# > Forget about least-slack edges of the subblossom.
|
|
206
|
+
@blossom_best_edges[bv] = nil
|
|
207
|
+
@best_edge[bv] = nil
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
@blossom_best_edges[b] = best_edge_to.compact
|
|
211
|
+
|
|
212
|
+
# > Select bestedge[b]
|
|
213
|
+
@best_edge[b] = nil
|
|
214
|
+
@blossom_best_edges[b].each do |k|
|
|
215
|
+
if @best_edge[b].nil? || slack(k) < slack(@best_edge[b])
|
|
216
|
+
@best_edge[b] = k
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def assert_blossom_trace(b)
|
|
222
|
+
t = @label[b] == LBL_T
|
|
223
|
+
s = @label[b] == LBL_S
|
|
224
|
+
m = @label_end[b] == @mate[@blossom_base[b]]
|
|
225
|
+
unless t || s && m
|
|
226
|
+
fail <<-EOS
|
|
227
|
+
Assertion failed: Expected either:
|
|
228
|
+
1. Current Bv to be a T-blossom, or
|
|
229
|
+
2. Bv is an S-blossom and its base is matched to @label_end[bv]
|
|
230
|
+
EOS
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
def assert_label(ix, lbl)
|
|
235
|
+
unless @label[ix] == lbl
|
|
236
|
+
fail "Expected label at #{ix} to be #{LBL_NAMES[lbl]}"
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
# > Assign label t to the top-level blossom containing vertex w
|
|
241
|
+
# > and record the fact that w was reached through the edge with
|
|
242
|
+
# > remote endpoint p.
|
|
243
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
244
|
+
#
|
|
245
|
+
def assign_label(w, t, p = nil)
|
|
246
|
+
b = @in_blossom[w]
|
|
247
|
+
assert_label(w, LBL_FREE)
|
|
248
|
+
assert_label(b, LBL_FREE)
|
|
249
|
+
@label[w] = @label[b] = t
|
|
250
|
+
@label_end[w] = @label_end[b] = p
|
|
251
|
+
@best_edge[w] = @best_edge[b] = nil
|
|
252
|
+
if t == LBL_S
|
|
253
|
+
# b became an S-vertex/blossom; add it(s vertices) to the queue.
|
|
254
|
+
@queue.concat(blossom_leaves(b))
|
|
255
|
+
elsif t == LBL_T
|
|
256
|
+
# b became a T-vertex/blossom; assign label S to its mate.
|
|
257
|
+
# (If b is a non-trivial blossom, its base is the only vertex
|
|
258
|
+
# with an external mate.)
|
|
259
|
+
base = @blossom_base[b]
|
|
260
|
+
if @mate[base].nil?
|
|
261
|
+
fail "Expected blossom #{b}'s base (#{base}) to be matched"
|
|
262
|
+
end
|
|
263
|
+
|
|
264
|
+
# Assign label S to the mate of blossom b's base.
|
|
265
|
+
# Remember, `mate` elements are pointers to "endpoints".
|
|
266
|
+
# The bitwise XOR is very clever. `mate[x]` and `mate[x] ^ 1`
|
|
267
|
+
# are connected "endpoints".
|
|
268
|
+
base_edge_endpoints = [@mate[base], @mate[base] ^ 1]
|
|
269
|
+
assign_label(@endpoint[base_edge_endpoints[0]], LBL_S, base_edge_endpoints[1])
|
|
270
|
+
else
|
|
271
|
+
fail ArgumentError, "Unexpected label: #{t}"
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
# > Swap matched/unmatched edges over an alternating path
|
|
276
|
+
# > through blossom b between vertex v and the base vertex.
|
|
277
|
+
# > Keep blossom bookkeeping consistent.
|
|
278
|
+
# > (Van Rantwijk, mwmatching.py, line 448)
|
|
279
|
+
def augment_blossom(b, v)
|
|
280
|
+
t = immediate_subblossom_of(b, v)
|
|
281
|
+
|
|
282
|
+
# > Recursively deal with the first sub-blossom.
|
|
283
|
+
if t >= @nvertex
|
|
284
|
+
augment_blossom(t, v)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# > Move along the blossom until we get to the base.
|
|
288
|
+
j, jstep, endptrick = blossom_loop_direction(b, t)
|
|
289
|
+
i = j
|
|
290
|
+
while j != 0
|
|
291
|
+
# > Step to the next sub-blossom and augment it recursively.
|
|
292
|
+
j += jstep
|
|
293
|
+
p = @blossom_endps[b][j - endptrick] ^ endptrick
|
|
294
|
+
x = @endpoint[p]
|
|
295
|
+
augment_blossom_step(b, j, x)
|
|
296
|
+
|
|
297
|
+
# > Step to the next sub-blossom and augment it recursively.
|
|
298
|
+
j += jstep
|
|
299
|
+
x = @endpoint[p ^ 1]
|
|
300
|
+
augment_blossom_step(b, j, x)
|
|
301
|
+
|
|
302
|
+
# > Match the edge connecting those sub-blossoms.
|
|
303
|
+
match_endpoint(p)
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
# > Rotate the list of sub-blossoms to put the new base at
|
|
307
|
+
# > the front.
|
|
308
|
+
@blossom_children[b].rotate!(i)
|
|
309
|
+
@blossom_endps[b].rotate!(i)
|
|
310
|
+
@blossom_base[b] = @blossom_base[@blossom_children[b][0]]
|
|
311
|
+
assert(@blossom_base[b]).eq(v)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def augment_blossom_step(b, j, x)
|
|
315
|
+
t = @blossom_children[b][j]
|
|
316
|
+
if t >= @nvertex
|
|
317
|
+
augment_blossom(t, x)
|
|
318
|
+
end
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
# > Swap matched/unmatched edges over an alternating path
|
|
322
|
+
# > between two single vertices. The augmenting path runs
|
|
323
|
+
# > through edge k, which connects a pair of S vertices.
|
|
324
|
+
# > (Van Rantwijk, mwmatching.py, line 494)
|
|
325
|
+
def augment_matching(k)
|
|
326
|
+
v, w = @edges[k].to_a
|
|
327
|
+
[[v, 2 * k + 1], [w, 2 * k]].each do |(s, p)|
|
|
328
|
+
# > Match vertex s to remote endpoint p. Then trace back from s
|
|
329
|
+
# > until we find a single vertex, swapping matched and unmatched
|
|
330
|
+
# > edges as we go.
|
|
331
|
+
# > (Van Rantwijk, mwmatching.py, line 504)
|
|
332
|
+
while true
|
|
333
|
+
bs = @in_blossom[s]
|
|
334
|
+
assert_label(bs, LBL_S)
|
|
335
|
+
assert(@label_end[bs]).eq(@mate[@blossom_base[bs]])
|
|
336
|
+
# > Augment through the S-blossom from s to base.
|
|
337
|
+
if bs >= @nvertex
|
|
338
|
+
augment_blossom(bs, s)
|
|
339
|
+
end
|
|
340
|
+
@mate[s] = p
|
|
341
|
+
# > Trace one step back.
|
|
342
|
+
# If we reach a single vertex, stop
|
|
343
|
+
break if @label_end[bs].nil?
|
|
344
|
+
t = @endpoint[@label_end[bs]]
|
|
345
|
+
bt = @in_blossom[t]
|
|
346
|
+
assert_label(bt, LBL_T)
|
|
347
|
+
# > Trace one step back.
|
|
348
|
+
assert(@label_end[bt]).not_nil
|
|
349
|
+
s = @endpoint[@label_end[bt]]
|
|
350
|
+
j = @endpoint[@label_end[bt] ^ 1]
|
|
351
|
+
# > Augment through the T-blossom from j to base.
|
|
352
|
+
assert(@blossom_base[bt]).eq(t)
|
|
353
|
+
if bt >= @nvertex
|
|
354
|
+
augment_blossom(bt, j)
|
|
355
|
+
end
|
|
356
|
+
@mate[j] = @label_end[bt]
|
|
357
|
+
# > Keep the opposite endpoint;
|
|
358
|
+
# > it will be assigned to mate[s] in the next step.
|
|
359
|
+
p = @label_end[bt] ^ 1
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
# TODO: Optimize by returning lazy iterator
|
|
365
|
+
def blossom_leaves(b, ary = [])
|
|
366
|
+
if b < @nvertex
|
|
367
|
+
ary.push(b)
|
|
368
|
+
else
|
|
369
|
+
@blossom_children[b].each do |c|
|
|
370
|
+
if c < @nvertex
|
|
371
|
+
ary.push(c)
|
|
372
|
+
else
|
|
373
|
+
ary.concat(blossom_leaves(c))
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
ary
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# > Decide in which direction we will go round the blossom.
|
|
381
|
+
# > (Van Rantwijk, mwmatching.py, lines 385, 460)
|
|
382
|
+
def blossom_loop_direction(b, t)
|
|
383
|
+
j = @blossom_children[b].index(t)
|
|
384
|
+
if j.odd?
|
|
385
|
+
# > go forward and wrap
|
|
386
|
+
j -= @blossom_children[b].length
|
|
387
|
+
jstep = 1
|
|
388
|
+
endptrick = 0
|
|
389
|
+
else
|
|
390
|
+
# > go backward
|
|
391
|
+
jstep = -1
|
|
392
|
+
endptrick = 1
|
|
393
|
+
end
|
|
394
|
+
return j, jstep, endptrick
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def calc_delta(max_cardinality)
|
|
398
|
+
delta = nil
|
|
399
|
+
delta_type = nil
|
|
400
|
+
delta_edge = nil
|
|
401
|
+
delta_blossom = nil
|
|
402
|
+
|
|
403
|
+
# > Compute delta1: the minumum value of any vertex dual.
|
|
404
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
405
|
+
unless max_cardinality
|
|
406
|
+
delta_type = 1
|
|
407
|
+
delta = @dual[0, @nvertex].min
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
# > Compute delta2: the minimum slack on any edge between
|
|
411
|
+
# > an S-vertex and a free vertex.
|
|
412
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
413
|
+
(0 ... @nvertex).each do |v|
|
|
414
|
+
if @label[@in_blossom[v]] == LBL_FREE && !@best_edge[v].nil?
|
|
415
|
+
d = slack(@best_edge[v])
|
|
416
|
+
if delta_type == nil || d < delta
|
|
417
|
+
delta = d
|
|
418
|
+
delta_type = 2
|
|
419
|
+
delta_edge = @best_edge[v]
|
|
420
|
+
end
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# > Compute delta3: half the minimum slack on any edge between
|
|
425
|
+
# > a pair of S-blossoms.
|
|
426
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
427
|
+
(0 ... 2 * @nvertex).each do |b|
|
|
428
|
+
if @blossom_parent[b].nil? && @label[b] == LBL_S && !@best_edge[b].nil?
|
|
429
|
+
kslack = slack(@best_edge[b])
|
|
430
|
+
d = kslack / 2 # Van Rantwijk had some type checking here. Why?
|
|
431
|
+
if delta_type.nil? || d < delta
|
|
432
|
+
delta = d
|
|
433
|
+
delta_type = 3
|
|
434
|
+
delta_edge = @best_edge[b]
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# > Compute delta4: minimum z variable of any T-blossom.
|
|
440
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
441
|
+
(@nvertex ... 2 * @nvertex).each do |b|
|
|
442
|
+
top_t_blossom = top_level_blossom?(b) && @label[b] == LBL_T
|
|
443
|
+
if top_t_blossom && (delta_type.nil? || @dual[b] < delta)
|
|
444
|
+
delta = @dual[b]
|
|
445
|
+
delta_type = 4
|
|
446
|
+
delta_blossom = b
|
|
447
|
+
end
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
if delta_type.nil?
|
|
451
|
+
# > No further improvement possible; max-cardinality optimum
|
|
452
|
+
# > reached. Do a final delta update to make the optimum
|
|
453
|
+
# > verifyable.
|
|
454
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
455
|
+
assert(max_cardinality).eq(true)
|
|
456
|
+
delta_type = 1
|
|
457
|
+
delta = [0, @dual[0, @nvertex].min].max
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
return delta, delta_type, delta_edge, delta_blossom
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
# Returns nil if `k` is known to be an endpoint of a tight
|
|
464
|
+
# edge. Otherwise, calculates and returns the slack of `k`,
|
|
465
|
+
# and updates the `tight_edge` cache.
|
|
466
|
+
def calc_slack(k)
|
|
467
|
+
if @tight_edge[k]
|
|
468
|
+
nil
|
|
469
|
+
else
|
|
470
|
+
kslack = slack(k)
|
|
471
|
+
if kslack <= 0
|
|
472
|
+
@tight_edge[k] = true
|
|
473
|
+
end
|
|
474
|
+
kslack
|
|
475
|
+
end
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
# > w is a free vertex (or an unreached vertex inside
|
|
479
|
+
# > a T-blossom) but we can not reach it yet;
|
|
480
|
+
# > keep track of the least-slack edge that reaches w.
|
|
481
|
+
# > (Van Rantwijk, mwmatching.py, line 725)
|
|
482
|
+
def consider_loose_edge_to_free_vertex(w, k, kslack)
|
|
483
|
+
if @best_edge[w].nil? || kslack < slack(@best_edge[w])
|
|
484
|
+
@best_edge[w] = k
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# While scanning neighbors of `v`, a loose edge to an
|
|
489
|
+
# S-blossom is found, and the `@best_edge` cache may
|
|
490
|
+
# be updated.
|
|
491
|
+
#
|
|
492
|
+
# > keep track of the least-slack non-allowable [loose] edge
|
|
493
|
+
# > to a different S-blossom.
|
|
494
|
+
# > (Van Rantwijk, mwmatching.py, line 717)
|
|
495
|
+
#
|
|
496
|
+
def consider_loose_edge_to_s_blossom(v, k, kslack)
|
|
497
|
+
b = @in_blossom[v]
|
|
498
|
+
if @best_edge[b].nil? || kslack < slack(@best_edge[b])
|
|
499
|
+
@best_edge[b] = k
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
|
|
503
|
+
# > If we scan the S-vertex *i* and consider the edge (i,j),
|
|
504
|
+
# > there are two cases:
|
|
505
|
+
# >
|
|
506
|
+
# > * (C1) j is free; or
|
|
507
|
+
# > * (C2) j is an S-vertex
|
|
508
|
+
# >
|
|
509
|
+
# > C2 cannot occur in the bipartite case. The case in
|
|
510
|
+
# > which j is a T-vertex is discarded.
|
|
511
|
+
# > (Galil, 1986, p. 26-27)
|
|
512
|
+
#
|
|
513
|
+
def consider_tight_edge(k, w, p, v)
|
|
514
|
+
augmented = false
|
|
515
|
+
|
|
516
|
+
if @label[@in_blossom[w]] == LBL_FREE
|
|
517
|
+
|
|
518
|
+
# > (C1) w is a free vertex;
|
|
519
|
+
# > label w with T and label its mate with S (R12).
|
|
520
|
+
# > (Van Rantwijk, mwmatching.py, line 690)
|
|
521
|
+
#
|
|
522
|
+
# > In case C1 we apply R12. (Galil, 1986, p. 27)
|
|
523
|
+
#
|
|
524
|
+
# > * (R1) If (i, j) is not matched and i is an S-person
|
|
525
|
+
# > and j a free (unlabeled) person then label j by T; and
|
|
526
|
+
# > * (R2) If (i, j) is matched and j is a T-person
|
|
527
|
+
# > and i a free person, then label i by S.
|
|
528
|
+
# >
|
|
529
|
+
# > Modified from (Galil, 1986, p. 25) as follows:
|
|
530
|
+
#
|
|
531
|
+
# > Any time R1 is used and j is labeled by T, R2 is
|
|
532
|
+
# > immediately used to label the spouse of j with S.
|
|
533
|
+
# > (Since j was not labeled before, it must be married
|
|
534
|
+
# > and its spouse must be unlabeled.) We call this
|
|
535
|
+
# > rule R12. (Galil, 1986, p. 26)
|
|
536
|
+
#
|
|
537
|
+
assign_label(w, LBL_T, p ^ 1)
|
|
538
|
+
|
|
539
|
+
elsif @label[@in_blossom[w]] == LBL_S
|
|
540
|
+
|
|
541
|
+
# > (C2) w is an S-vertex (not in the same blossom);
|
|
542
|
+
# > follow back-links to discover either an
|
|
543
|
+
# > augmenting path or a new blossom.
|
|
544
|
+
# > (Van Rantwijk, mwmatching.py, line 694)
|
|
545
|
+
#
|
|
546
|
+
base = scan_blossom(v, w)
|
|
547
|
+
if base.nil?
|
|
548
|
+
# > Found an augmenting path; augment the
|
|
549
|
+
# > matching and end this stage.
|
|
550
|
+
# > (Van Rantwijk, mwmatching.py, line 703)
|
|
551
|
+
augment_matching(k)
|
|
552
|
+
augmented = true
|
|
553
|
+
else
|
|
554
|
+
# > Found a new blossom; add it to the blossom
|
|
555
|
+
# > bookkeeping and turn it into an S-blossom.
|
|
556
|
+
# > (Van Rantwijk, mwmatching.py, line 699)
|
|
557
|
+
add_blossom(base, k)
|
|
558
|
+
end
|
|
559
|
+
|
|
560
|
+
elsif @label[w] == LBL_FREE
|
|
561
|
+
|
|
562
|
+
# > w is inside a T-blossom, but w itself has not
|
|
563
|
+
# > yet been reached from outside the blossom;
|
|
564
|
+
# > mark it as reached (we need this to relabel
|
|
565
|
+
# > during T-blossom expansion).
|
|
566
|
+
# > (Van Rantwijk, mwmatching.py, line 709)
|
|
567
|
+
#
|
|
568
|
+
assert_label(@in_blossom[w], LBL_T)
|
|
569
|
+
@label[w] = LBL_T
|
|
570
|
+
@label_end[w] = p ^ 1
|
|
571
|
+
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
augmented
|
|
575
|
+
end
|
|
576
|
+
|
|
577
|
+
# > Expand the given top-level blossom.
|
|
578
|
+
# > (Van Rantwijk, mwmatching.py, line 361)
|
|
579
|
+
#
|
|
580
|
+
# Blossoms are expanded during slack adjustment delta type 4,
|
|
581
|
+
# and after all stages are complete (endstage will be true).
|
|
582
|
+
#
|
|
583
|
+
def expand_blossom(b, endstage)
|
|
584
|
+
promote_sub_blossoms_of(b, endstage)
|
|
585
|
+
|
|
586
|
+
# > If we expand a T-blossom during a stage, its sub-blossoms
|
|
587
|
+
# > must be relabeled.
|
|
588
|
+
if !endstage && @label[b] == LBL_T
|
|
589
|
+
expand_t_blossom(b)
|
|
590
|
+
end
|
|
591
|
+
|
|
592
|
+
recycle_blossom_number(b)
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
# > Start at the sub-blossom through which the expanding
|
|
596
|
+
# > blossom obtained its label, and relabel sub-blossoms until
|
|
597
|
+
# > we reach the base.
|
|
598
|
+
# > Figure out through which sub-blossom the expanding blossom
|
|
599
|
+
# > obtained its label initially.
|
|
600
|
+
# > (Van Rantwijk, mwmatching.py, line 378)
|
|
601
|
+
def expand_t_blossom(b)
|
|
602
|
+
assert(@label_end[b]).not_nil
|
|
603
|
+
entry_child = @in_blossom[@endpoint[@label_end[b] ^ 1]]
|
|
604
|
+
|
|
605
|
+
# > Move along the blossom until we get to the base.
|
|
606
|
+
j, jstep, endptrick = blossom_loop_direction(b, entry_child)
|
|
607
|
+
p = @label_end[b]
|
|
608
|
+
while j != 0
|
|
609
|
+
|
|
610
|
+
# > Relabel the T-sub-blossom.
|
|
611
|
+
@label[@endpoint[p ^ 1]] = LBL_FREE
|
|
612
|
+
@label[@endpoint[@blossom_endps[b][j - endptrick] ^ endptrick ^ 1]] = LBL_FREE
|
|
613
|
+
assign_label(@endpoint[p ^ 1], LBL_T, p)
|
|
614
|
+
|
|
615
|
+
# > Step to the next S-sub-blossom and note its forward endpoint.
|
|
616
|
+
@tight_edge[@blossom_endps[b][j - endptrick] / 2] = true # floor division
|
|
617
|
+
j += jstep
|
|
618
|
+
p = @blossom_endps[b][j - endptrick] ^ endptrick
|
|
619
|
+
|
|
620
|
+
# > Step to the next T-sub-blossom.
|
|
621
|
+
@tight_edge[p / 2] = true # floor division
|
|
622
|
+
j += jstep
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
# > Relabel the base T-sub-blossom WITHOUT stepping through to
|
|
626
|
+
# > its mate (so don't call assignLabel).
|
|
627
|
+
bv = @blossom_children[b][j]
|
|
628
|
+
@label[@endpoint[p ^ 1]] = @label[bv] = LBL_T
|
|
629
|
+
@label_end[@endpoint[p ^ 1]] = @label_end[bv] = p
|
|
630
|
+
@best_edge[bv] = nil
|
|
631
|
+
|
|
632
|
+
# > Continue along the blossom until we get back to entrychild.
|
|
633
|
+
j += jstep
|
|
634
|
+
while @blossom_children[b][j] != entry_child
|
|
635
|
+
|
|
636
|
+
# > Examine the vertices of the sub-blossom to see whether
|
|
637
|
+
# > it is reachable from a neighbouring S-vertex outside the
|
|
638
|
+
# > expanding blossom.
|
|
639
|
+
bv = @blossom_children[b][j]
|
|
640
|
+
if @label[bv] == LBL_S
|
|
641
|
+
# > This sub-blossom just got label S through one of its
|
|
642
|
+
# > neighbours; leave it.
|
|
643
|
+
j += jstep
|
|
644
|
+
next
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
# > If the sub-blossom contains a reachable vertex, assign
|
|
648
|
+
# > label T to the sub-blossom.
|
|
649
|
+
v = first_labeled_blossom_leaf(bv)
|
|
650
|
+
unless v.nil?
|
|
651
|
+
assert_label(v, LBL_T)
|
|
652
|
+
assert(@in_blossom[v]).eq(bv)
|
|
653
|
+
@label[v] = LBL_FREE
|
|
654
|
+
@label[@endpoint[@mate[@blossom_base[bv]]]] = LBL_FREE
|
|
655
|
+
assign_label(v, LBL_T, @label_end[v])
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
j += jstep
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
|
|
662
|
+
# > End of a stage; expand all S-blossoms which have dualvar = 0.
|
|
663
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
664
|
+
def expand_tight_s_blossoms
|
|
665
|
+
(@nvertex ... 2 * @nvertex).each do |b|
|
|
666
|
+
if top_level_blossom?(b) && @label[b] == LBL_S && @dual[b] == 0
|
|
667
|
+
expand_blossom(b, true)
|
|
668
|
+
end
|
|
669
|
+
end
|
|
670
|
+
end
|
|
671
|
+
|
|
672
|
+
def first_labeled_blossom_leaf(b)
|
|
673
|
+
blossom_leaves(b).find { |leaf| @label[leaf] != LBL_FREE }
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
# Starting from a vertex `v`, ascend the blossom tree, and
|
|
677
|
+
# return the sub-blossom immediately below `b`.
|
|
678
|
+
def immediate_subblossom_of(b, v)
|
|
679
|
+
t = v
|
|
680
|
+
while @blossom_parent[t] != b
|
|
681
|
+
t = @blossom_parent[t]
|
|
682
|
+
end
|
|
683
|
+
t
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
# Data structures used throughout the algorithm.
|
|
687
|
+
def init_algorithm_structures
|
|
688
|
+
# > If v is a vertex,
|
|
689
|
+
# > mate[v] is the remote endpoint of its matched edge, or -1 if it is single
|
|
690
|
+
# > (i.e. endpoint[mate[v]] is v's partner vertex).
|
|
691
|
+
# > Initially all vertices are single; updated during augmentation.
|
|
692
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
693
|
+
#
|
|
694
|
+
@mate = Array.new(@nvertex, nil)
|
|
695
|
+
|
|
696
|
+
# > If b is a top-level blossom,
|
|
697
|
+
# > label[b] is 0 if b is unlabeled (free);
|
|
698
|
+
# > 1 if b is an S-vertex/blossom;
|
|
699
|
+
# > 2 if b is a T-vertex/blossom.
|
|
700
|
+
# > The label of a vertex is found by looking at the label of its
|
|
701
|
+
# > top-level containing blossom.
|
|
702
|
+
# > If v is a vertex inside a T-blossom,
|
|
703
|
+
# > label[v] is 2 iff v is reachable from an S-vertex outside the blossom.
|
|
704
|
+
# > Labels are assigned during a stage and reset after each augmentation.
|
|
705
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
706
|
+
#
|
|
707
|
+
@label = rantwijk_array(LBL_FREE)
|
|
708
|
+
|
|
709
|
+
# > If b is a labeled top-level blossom,
|
|
710
|
+
# > labelend[b] is the remote endpoint of the edge through which b obtained
|
|
711
|
+
# > its label, or -1 if b's base vertex is single.
|
|
712
|
+
# > If v is a vertex inside a T-blossom and label[v] == 2,
|
|
713
|
+
# > labelend[v] is the remote endpoint of the edge through which v is
|
|
714
|
+
# > reachable from outside the blossom.
|
|
715
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
716
|
+
#
|
|
717
|
+
@label_end = rantwijk_array(nil)
|
|
718
|
+
|
|
719
|
+
# > If v is a vertex,
|
|
720
|
+
# > inblossom[v] is the top-level blossom to which v belongs.
|
|
721
|
+
# > If v is a top-level vertex, v is itself a blossom (a trivial blossom)
|
|
722
|
+
# > and inblossom[v] == v.
|
|
723
|
+
# > Initially all vertices are top-level trivial blossoms.
|
|
724
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
725
|
+
#
|
|
726
|
+
@in_blossom = (0 ... @nvertex).to_a
|
|
727
|
+
|
|
728
|
+
# > If b is a sub-blossom,
|
|
729
|
+
# > blossomparent[b] is its immediate parent (sub-)blossom.
|
|
730
|
+
# > If b is a top-level blossom, blossomparent[b] is -1.
|
|
731
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
732
|
+
#
|
|
733
|
+
@blossom_parent = rantwijk_array(nil)
|
|
734
|
+
|
|
735
|
+
# A 2D array representing a tree of blossoms.
|
|
736
|
+
#
|
|
737
|
+
# > The blossom structure of a graph is represented by a
|
|
738
|
+
# > *blossom tree*. Its nodes are the graph G, the blossoms
|
|
739
|
+
# > of G, and all vertices included in blossoms. The root is
|
|
740
|
+
# > G, whose children are the maximal blossoms. .. Any
|
|
741
|
+
# > vertex is a leaf.
|
|
742
|
+
# > (Gabow, 1985, p. 91)
|
|
743
|
+
#
|
|
744
|
+
# Van Rantwijk implements the blossom tree with an array in
|
|
745
|
+
# two halves. The first half is "trivial" blossoms, vertexes,
|
|
746
|
+
# the leaves of the tree. The second half are non-trivial blossoms.
|
|
747
|
+
#
|
|
748
|
+
# > Vertices are numbered 0 .. (nvertex-1).
|
|
749
|
+
# > Non-trivial blossoms are numbered nvertex .. (2*nvertex-1)
|
|
750
|
+
# > (Van Rantwijk, mwmatching.py, line 58)
|
|
751
|
+
#
|
|
752
|
+
# > If b is a non-trivial (sub-)blossom, blossomchilds[b]
|
|
753
|
+
# > is an ordered list of its sub-blossoms, starting with
|
|
754
|
+
# > the base and going round the blossom.
|
|
755
|
+
# > (Van Rantwijk, mwmatching.py, line 144)
|
|
756
|
+
#
|
|
757
|
+
@blossom_children = rantwijk_array(nil)
|
|
758
|
+
|
|
759
|
+
# > If b is a (sub-)blossom,
|
|
760
|
+
# > blossombase[b] is its base VERTEX (i.e. recursive sub-blossom).
|
|
761
|
+
# > (Van Rantwijk, mwmatching.py, line 153)
|
|
762
|
+
#
|
|
763
|
+
@blossom_base = (0 ... @nvertex).to_a + Array.new(@nvertex, nil)
|
|
764
|
+
|
|
765
|
+
# > If b is a non-trivial (sub-)blossom,
|
|
766
|
+
# > blossomendps[b] is a list of endpoints on its connecting edges,
|
|
767
|
+
# > such that blossomendps[b][i] is the local endpoint of
|
|
768
|
+
# > blossomchilds[b][i] on the edge that connects it to
|
|
769
|
+
# > blossomchilds[b][wrap(i+1)].
|
|
770
|
+
# > (Van Rantwijk, mwmatching.py, line 147)
|
|
771
|
+
#
|
|
772
|
+
@blossom_endps = rantwijk_array(nil)
|
|
773
|
+
|
|
774
|
+
# > If v is a free vertex (or an unreached vertex inside a T-blossom),
|
|
775
|
+
# > bestedge[v] is the edge to an S-vertex with least slack,
|
|
776
|
+
# > or -1 if there is no such edge.
|
|
777
|
+
# > If b is a (possibly trivial) top-level S-blossom,
|
|
778
|
+
# > bestedge[b] is the least-slack edge to a different S-blossom,
|
|
779
|
+
# > or -1 if there is no such edge.
|
|
780
|
+
# > This is used for efficient computation of delta2 and delta3.
|
|
781
|
+
# > (Van Rantwijk, mwmatching.py)
|
|
782
|
+
#
|
|
783
|
+
@best_edge = rantwijk_array(nil)
|
|
784
|
+
|
|
785
|
+
# > If b is a non-trivial top-level S-blossom,
|
|
786
|
+
# > blossombestedges[b] is a list of least-slack edges to neighbouring
|
|
787
|
+
# > S-blossoms, or None if no such list has been computed yet.
|
|
788
|
+
# > This is used for efficient computation of delta3.
|
|
789
|
+
# > (Van Rantwijk, mwmatching.py, line 168)
|
|
790
|
+
#
|
|
791
|
+
@blossom_best_edges = rantwijk_array(nil)
|
|
792
|
+
|
|
793
|
+
# > List of currently unused blossom numbers.
|
|
794
|
+
# > (Van Rantwijk, mwmatching.py, line 174)
|
|
795
|
+
@unused_blossoms = (@nvertex ... 2 * @nvertex).to_a
|
|
796
|
+
|
|
797
|
+
# > If v is a vertex,
|
|
798
|
+
# > dualvar[v] = 2 * u(v) where u(v) is the v's variable in the dual
|
|
799
|
+
# > optimization problem (multiplication by two ensures integer values
|
|
800
|
+
# > throughout the algorithm if all edge weights are integers).
|
|
801
|
+
# > If b is a non-trivial blossom, dualvar[b] = z(b)
|
|
802
|
+
# > where z(b) is b's variable in the dual optimization
|
|
803
|
+
# > problem. (Van Rantwijk, mwmatching.py, line 177)
|
|
804
|
+
#
|
|
805
|
+
@dual = Array.new(@nvertex, g.max_w) + Array.new(@nvertex, 0)
|
|
806
|
+
|
|
807
|
+
# Optimization: Cache of tight (zero slack) edges. *Tight*
|
|
808
|
+
# is a term I attribute to Gabow, though it may be earlier.
|
|
809
|
+
#
|
|
810
|
+
# > Edge ij is *tight* if equality holds in [its dual
|
|
811
|
+
# > value function]. (Gabow, 1985, p. 91)
|
|
812
|
+
#
|
|
813
|
+
# Van Rantwijk calls this cache `allowedge`, denoting its use
|
|
814
|
+
# in the algorithm.
|
|
815
|
+
#
|
|
816
|
+
# > If allowedge[k] is true, edge k has zero slack in the optimization
|
|
817
|
+
# > problem; if allowedge[k] is false, the edge's slack may or may not
|
|
818
|
+
# > be zero.
|
|
819
|
+
@tight_edge = Array.new(@edges.length, false)
|
|
820
|
+
|
|
821
|
+
# Queue of newly discovered S-vertices.
|
|
822
|
+
@queue = []
|
|
823
|
+
end
|
|
824
|
+
|
|
825
|
+
# Builds data structures about the graph. These structures
|
|
826
|
+
# are not modified by the algorithm.
|
|
827
|
+
def init_graph_structures
|
|
828
|
+
@weight = g.weight
|
|
829
|
+
|
|
830
|
+
# The size of the array (or part of an array) used for
|
|
831
|
+
# vertexes (as opposed to blossoms) throughout this
|
|
832
|
+
# algorithm. It is *not*, as one might assume from the
|
|
833
|
+
# name, the number of vertexes in the graph.
|
|
834
|
+
@nvertex = g.max_v.to_i + 1
|
|
835
|
+
|
|
836
|
+
# Make a local copy of the edges. We'll refer to edges
|
|
837
|
+
# by number throughout throughout the algorithm and it's
|
|
838
|
+
# important that the order be consistent.
|
|
839
|
+
@edges = g.edges.map { |e| [e.source, e.target] }
|
|
840
|
+
|
|
841
|
+
# In Joris van Rantwijk's implementation, there seems to be
|
|
842
|
+
# a concept of "edge numbers". His `endpoint` array has two
|
|
843
|
+
# elements for each edge. His `mate` array "points to" his
|
|
844
|
+
# `endpoint` array. (See below) I'm sure there's a reason,
|
|
845
|
+
# but I don't understand yet.
|
|
846
|
+
#
|
|
847
|
+
# > If p is an edge endpoint, endpoint[p] is the vertex to
|
|
848
|
+
# > which endpoint p is attached. Not modified by the
|
|
849
|
+
# > algorithm. (Van Rantwijk, mwmatching.py, line 93)
|
|
850
|
+
@endpoint = @edges.flatten
|
|
851
|
+
|
|
852
|
+
# > If v is a vertex, neighbend[v] is the list of remote
|
|
853
|
+
# > endpoints of the edges attached to v. Not modified by
|
|
854
|
+
# > the algorithm. (Van Rantwijk, mwmatching.py, line 98)
|
|
855
|
+
@neighb_end = init_neighb_end(@nvertex, @edges)
|
|
856
|
+
end
|
|
857
|
+
|
|
858
|
+
def init_neighb_end(nvertex, edges)
|
|
859
|
+
neighb_end = Array.new(nvertex) { [] }
|
|
860
|
+
edges.each_with_index do |(i, j), k|
|
|
861
|
+
neighb_end[i].push(2 * k + 1)
|
|
862
|
+
neighb_end[j].push(2 * k)
|
|
863
|
+
end
|
|
864
|
+
neighb_end
|
|
865
|
+
end
|
|
866
|
+
|
|
867
|
+
def init_stage
|
|
868
|
+
init_stage_caches
|
|
869
|
+
@queue = []
|
|
870
|
+
init_stage_labels
|
|
871
|
+
end
|
|
872
|
+
|
|
873
|
+
# Clear the Van Rantwijk "best edge" caches
|
|
874
|
+
def init_stage_caches
|
|
875
|
+
@best_edge = rantwijk_array(nil)
|
|
876
|
+
@blossom_best_edges.fill(nil, @nvertex)
|
|
877
|
+
@tight_edge = Array.new(@edges.length, false)
|
|
878
|
+
end
|
|
879
|
+
|
|
880
|
+
# > We start by labeling all single persons S
|
|
881
|
+
# > (Galil, 1986, p. 26)
|
|
882
|
+
#
|
|
883
|
+
# > Label single blossoms/vertices with S and put them in
|
|
884
|
+
# > the queue. (Van Rantwijk, mwmatching.py, line 649)
|
|
885
|
+
def init_stage_labels
|
|
886
|
+
@label = rantwijk_array(LBL_FREE)
|
|
887
|
+
(0 ... @nvertex).each do |v|
|
|
888
|
+
if @mate[v].nil? && @label[@in_blossom[v]] == LBL_FREE
|
|
889
|
+
assign_label(v, LBL_S)
|
|
890
|
+
end
|
|
891
|
+
end
|
|
892
|
+
end
|
|
893
|
+
|
|
894
|
+
# Add endpoint p's edge to the matching.
|
|
895
|
+
def match_endpoint(p)
|
|
896
|
+
@mate[@endpoint[p]] = p ^ 1
|
|
897
|
+
@mate[@endpoint[p ^ 1]] = p
|
|
898
|
+
end
|
|
899
|
+
|
|
900
|
+
# > Convert sub-blossoms [of `b`] into top-level blossoms.
|
|
901
|
+
# > (Van Rantwijk, mwmatching.py, line 364)
|
|
902
|
+
def promote_sub_blossoms_of(b, endstage)
|
|
903
|
+
@blossom_children[b].each do |s|
|
|
904
|
+
@blossom_parent[s] = nil
|
|
905
|
+
if s < @nvertex
|
|
906
|
+
@in_blossom[s] = s
|
|
907
|
+
elsif endstage && @dual[s] == 0
|
|
908
|
+
expand_blossom(s, endstage)
|
|
909
|
+
else
|
|
910
|
+
blossom_leaves(s).each do |v|
|
|
911
|
+
@in_blossom[v] = s
|
|
912
|
+
end
|
|
913
|
+
end
|
|
914
|
+
end
|
|
915
|
+
end
|
|
916
|
+
|
|
917
|
+
def recycle_blossom_number(b)
|
|
918
|
+
@label[b] = nil
|
|
919
|
+
@label_end[b] = nil
|
|
920
|
+
@blossom_children[b] = nil
|
|
921
|
+
@blossom_endps[b] = nil
|
|
922
|
+
@blossom_base[b] = nil
|
|
923
|
+
@blossom_best_edges[b] = nil
|
|
924
|
+
@best_edge[b] = nil
|
|
925
|
+
@unused_blossoms.push(b)
|
|
926
|
+
end
|
|
927
|
+
|
|
928
|
+
# Backtrack to find an augmenting path (returns nil) or the
|
|
929
|
+
# base of a new blossom (returns base).
|
|
930
|
+
#
|
|
931
|
+
# > Backtrack from i and j, using the labels, to the
|
|
932
|
+
# > single persons s<sub>i</sub> and s<sub>j</sub>
|
|
933
|
+
# > from which i and j got their S labels. If
|
|
934
|
+
# > s<sub>i</sub> ≠ s<sub>j</sub>, we find an augmenting
|
|
935
|
+
# > path from s<sub>i</sub> to s<sub>j</sub> and augment
|
|
936
|
+
# > the matching. (Galil, 1986, p. 27)
|
|
937
|
+
#
|
|
938
|
+
# > Trace back from vertices v and w to discover either a new
|
|
939
|
+
# > blossom or an augmenting path. Return the base vertex of
|
|
940
|
+
# > the new blossom or -1. (Van Rantwijk, mwmatching.py, line 233)
|
|
941
|
+
def scan_blossom(v, w)
|
|
942
|
+
# > Trace back from v and w, placing breadcrumbs as we go.
|
|
943
|
+
path = []
|
|
944
|
+
base = nil
|
|
945
|
+
until v.nil? && w.nil?
|
|
946
|
+
# > Look for a breadcrumb in v's blossom or put a new breadcrumb.
|
|
947
|
+
b = @in_blossom[v]
|
|
948
|
+
if @label[b] & 4 != 0
|
|
949
|
+
base = @blossom_base[b]
|
|
950
|
+
break
|
|
951
|
+
end
|
|
952
|
+
assert_label(b, LBL_S)
|
|
953
|
+
path.push(b)
|
|
954
|
+
@label[b] = LBL_CRUMB
|
|
955
|
+
# > Trace one step back.
|
|
956
|
+
assert(@label_end[b]).eq(@mate[@blossom_base[b]])
|
|
957
|
+
if @label_end[b].nil?
|
|
958
|
+
# > The base of blossom b is single; stop tracing this path.
|
|
959
|
+
v = nil
|
|
960
|
+
else
|
|
961
|
+
v = @endpoint[@label_end[b]]
|
|
962
|
+
b = @in_blossom[v]
|
|
963
|
+
assert_label(b, LBL_T)
|
|
964
|
+
# > b is a T-blossom; trace one more step back.
|
|
965
|
+
assert(@label_end[b]).not_nil
|
|
966
|
+
v = @endpoint[@label_end[b]]
|
|
967
|
+
end
|
|
968
|
+
|
|
969
|
+
# > Swap v and w so that we alternate between both paths.
|
|
970
|
+
unless w.nil?
|
|
971
|
+
v, w = w, v
|
|
972
|
+
end
|
|
973
|
+
end
|
|
974
|
+
remove_breadcrumbs(path)
|
|
975
|
+
base
|
|
976
|
+
end
|
|
977
|
+
|
|
978
|
+
def remove_breadcrumbs(path)
|
|
979
|
+
path.each do |b| @label[b] = LBL_S end
|
|
980
|
+
end
|
|
981
|
+
|
|
982
|
+
# Trace a path around a blossom, from sub-blossom `bx` to
|
|
983
|
+
# blossom base `bb`, by following `@label_end`. At each
|
|
984
|
+
# step, `yield` the sub-blossom `bx`.
|
|
985
|
+
def trace_to_base(bx, bb)
|
|
986
|
+
while bx != bb
|
|
987
|
+
yield bx
|
|
988
|
+
assert_blossom_trace(bx)
|
|
989
|
+
assert(@label_end[bx]).not_nil
|
|
990
|
+
bx = @in_blossom[@endpoint[@label_end[bx]]]
|
|
991
|
+
end
|
|
992
|
+
end
|
|
993
|
+
|
|
994
|
+
# Returns an array of size 2n, where n is the number of
|
|
995
|
+
# vertexes. Common in Van Rantwijk's implementation, but
|
|
996
|
+
# the idea may come from Gabow (1985) or earlier.
|
|
997
|
+
def rantwijk_array(fill)
|
|
998
|
+
Array.new(2 * @nvertex, fill)
|
|
999
|
+
end
|
|
1000
|
+
|
|
1001
|
+
# > Scanning a vertex means considering in turn all its edges
|
|
1002
|
+
# > except the matched edge. (There will be at most one).
|
|
1003
|
+
# > (Galil, 1986, p. 26)
|
|
1004
|
+
def scan_vertex(v)
|
|
1005
|
+
assert_label(@in_blossom[v], LBL_S)
|
|
1006
|
+
augmented = false
|
|
1007
|
+
|
|
1008
|
+
@neighb_end[v].each do |p|
|
|
1009
|
+
k = p / 2 # floor division
|
|
1010
|
+
w = @endpoint[p]
|
|
1011
|
+
|
|
1012
|
+
if @in_blossom[v] == @in_blossom[w]
|
|
1013
|
+
# > this edge is internal to a blossom; ignore it
|
|
1014
|
+
# > (Van Rantwijk, mwmatching.py, line 681)
|
|
1015
|
+
next
|
|
1016
|
+
end
|
|
1017
|
+
|
|
1018
|
+
# Calculate slack of `k`'s edge and update tight_edge cache.
|
|
1019
|
+
kslack = calc_slack(k)
|
|
1020
|
+
|
|
1021
|
+
# > .. we only use edges with π<sub>ij</sub> = 0.
|
|
1022
|
+
# > (Galil, 1986, p. 32)
|
|
1023
|
+
if @tight_edge[k]
|
|
1024
|
+
augmented = consider_tight_edge(k, w, p, v)
|
|
1025
|
+
break if augmented
|
|
1026
|
+
elsif @label[@in_blossom[w]] == LBL_S
|
|
1027
|
+
consider_loose_edge_to_s_blossom(v, k, kslack)
|
|
1028
|
+
elsif @label[w] == LBL_FREE
|
|
1029
|
+
consider_loose_edge_to_free_vertex(w, k, kslack)
|
|
1030
|
+
end
|
|
1031
|
+
end
|
|
1032
|
+
|
|
1033
|
+
augmented
|
|
1034
|
+
end
|
|
1035
|
+
|
|
1036
|
+
# Van Rantwijk's implementation of slack does not match Galil's.
|
|
1037
|
+
#
|
|
1038
|
+
# > Return 2 * slack of edge k (does not work inside blossoms).
|
|
1039
|
+
# > (Van Rantwijk, mwmatching.py, line 194)
|
|
1040
|
+
#
|
|
1041
|
+
def slack(k)
|
|
1042
|
+
i, j = @edges[k]
|
|
1043
|
+
@dual[i] + @dual[j] - 2 * @weight[i - 1][j - 1]
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
def top_level_blossom?(b)
|
|
1047
|
+
!@blossom_base[b].nil? && @blossom_parent[b].nil?
|
|
1048
|
+
end
|
|
1049
|
+
|
|
1050
|
+
# > .. we make the following changes in the dual
|
|
1051
|
+
# > variables. (Galil, 1986, p. 32)
|
|
1052
|
+
def update_duals(delta)
|
|
1053
|
+
(0 ... @nvertex).each do |v|
|
|
1054
|
+
case @label[@in_blossom[v]]
|
|
1055
|
+
when LBL_S
|
|
1056
|
+
@dual[v] -= delta
|
|
1057
|
+
when LBL_T
|
|
1058
|
+
@dual[v] += delta
|
|
1059
|
+
else
|
|
1060
|
+
# No change to free vertexes
|
|
1061
|
+
end
|
|
1062
|
+
end
|
|
1063
|
+
(@nvertex ... 2 * @nvertex).each do |b|
|
|
1064
|
+
if top_level_blossom?(b)
|
|
1065
|
+
case @label[b]
|
|
1066
|
+
when LBL_S
|
|
1067
|
+
@dual[b] += delta
|
|
1068
|
+
when LBL_T
|
|
1069
|
+
@dual[b] -= delta
|
|
1070
|
+
else
|
|
1071
|
+
# No change to free blossoms
|
|
1072
|
+
end
|
|
1073
|
+
end
|
|
1074
|
+
end
|
|
1075
|
+
end
|
|
1076
|
+
|
|
1077
|
+
# Uncomment to enable assertions. Slows down the algorithm
|
|
1078
|
+
# to O(n^4), but useful during development.
|
|
1079
|
+
#
|
|
1080
|
+
# require_relative 'mwmg_delta_assertions'
|
|
1081
|
+
# include MWMGDeltaAssertions
|
|
1082
|
+
# alias_method :calc_delta_without_assertions, :calc_delta
|
|
1083
|
+
# alias_method :calc_delta, :calc_delta_with_assertions
|
|
1084
|
+
end
|
|
1085
|
+
end
|
|
1086
|
+
end
|