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,94 @@
|
|
|
1
|
+
module GraphMatching
|
|
2
|
+
module Algorithm
|
|
3
|
+
# Can be mixed into MWMGeneral to add runtime assertions
|
|
4
|
+
# about the data structures used for delta2/delta3 calculations.
|
|
5
|
+
#
|
|
6
|
+
# > Check delta2/delta3 computation after every substage;
|
|
7
|
+
# > only works on integer weights, slows down the algorithm to O(n^4).
|
|
8
|
+
# > (Van Rantwijk, mwmatching.py, line 34)
|
|
9
|
+
#
|
|
10
|
+
module MWMGDeltaAssertions
|
|
11
|
+
def calc_delta_with_assertions(*args)
|
|
12
|
+
# > Verify data structures for delta2/delta3 computation.
|
|
13
|
+
# > (Van Rantwijk, mwmatching.py, line 739)
|
|
14
|
+
check_delta2
|
|
15
|
+
check_delta3
|
|
16
|
+
calc_delta_without_assertions(*args)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# > Check optimized delta2 against a trivial computation.
|
|
20
|
+
# > (Van Rantwijk, mwmatching.py, line 580)
|
|
21
|
+
def check_delta2
|
|
22
|
+
(0 ... @nvertex).each do |v|
|
|
23
|
+
if @label[@in_blossom[v]] == MWMGeneral::LBL_FREE
|
|
24
|
+
bd = nil
|
|
25
|
+
bk = nil
|
|
26
|
+
@neighb_end[v].each do |p|
|
|
27
|
+
k = p / 2 # Note: floor division
|
|
28
|
+
w = @endpoint[p]
|
|
29
|
+
if @label[@in_blossom[w]] == MWMGeneral::LBL_S
|
|
30
|
+
d = slack(k)
|
|
31
|
+
if bk.nil? || d < bd
|
|
32
|
+
bk = k
|
|
33
|
+
bd = d
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
option1 = bk.nil? && @best_edge[v].nil?
|
|
38
|
+
option2 = !@best_edge[v].nil? && bd == slack(@best_edge[v])
|
|
39
|
+
unless option1 || option2
|
|
40
|
+
fail "Assertion failed: Free vertex #{v}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# > Check optimized delta3 against a trivial computation.
|
|
47
|
+
# > (Van Rantwijk, mwmatching.py, line 598)
|
|
48
|
+
def check_delta3
|
|
49
|
+
bk = nil
|
|
50
|
+
bd = nil
|
|
51
|
+
tbk = nil
|
|
52
|
+
tbd = nil
|
|
53
|
+
(0 ... 2 * @nvertex).each do |b|
|
|
54
|
+
if @blossom_parent[b].nil? && @label[b] == MWMGeneral::LBL_S
|
|
55
|
+
blossom_leaves(b).each do |v|
|
|
56
|
+
@neighb_end[v].each do |p|
|
|
57
|
+
k = p / 2 # Note: floor division
|
|
58
|
+
w = @endpoint[p]
|
|
59
|
+
if @in_blossom[w] != b &&
|
|
60
|
+
@label[@in_blossom[w]] == MWMGeneral::LBL_S
|
|
61
|
+
d = slack(k)
|
|
62
|
+
if bk.nil? || d < bd
|
|
63
|
+
bk = k
|
|
64
|
+
bd = d
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
unless @best_edge[b].nil?
|
|
70
|
+
i, j = @edges[@best_edge[b]].to_a
|
|
71
|
+
unless @in_blossom[i] == b || @in_blossom[j] == b
|
|
72
|
+
fail 'Assertion failed'
|
|
73
|
+
end
|
|
74
|
+
unless @in_blossom[i] != b || @in_blossom[j] != b
|
|
75
|
+
fail 'Assertion failed'
|
|
76
|
+
end
|
|
77
|
+
unless @label[@in_blossom[i]] == MWMGeneral::LBL_S &&
|
|
78
|
+
@label[@in_blossom[j]] == MWMGeneral::LBL_S
|
|
79
|
+
fail 'Assertion failed'
|
|
80
|
+
end
|
|
81
|
+
if tbk.nil? || slack(@best_edge[b]) < tbd
|
|
82
|
+
tbk = @best_edge[b]
|
|
83
|
+
tbd = slack(@best_edge[b])
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
unless bd == tbd
|
|
89
|
+
fail 'Assertion failed'
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module GraphMatching
|
|
4
|
+
# Provides expressive methods for common runtime assertions, e.g.
|
|
5
|
+
#
|
|
6
|
+
# assert(banana).is_a(Fruit)
|
|
7
|
+
#
|
|
8
|
+
class Assertion
|
|
9
|
+
attr_reader :obj
|
|
10
|
+
|
|
11
|
+
def initialize(obj)
|
|
12
|
+
@obj = obj
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def eq(other)
|
|
16
|
+
unless obj == other
|
|
17
|
+
fail "Expected #{other}, got #{obj}"
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def gte(other)
|
|
22
|
+
unless obj >= other
|
|
23
|
+
fail "Expected #{obj} to be >= #{other}"
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# rubocop:disable Style/PredicateName
|
|
28
|
+
def is_a(klass)
|
|
29
|
+
unless obj.is_a?(klass)
|
|
30
|
+
fail TypeError, "Expected #{klass}, got #{obj.class}"
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
# rubocop:enable Style/PredicateName
|
|
34
|
+
|
|
35
|
+
def not_nil
|
|
36
|
+
if obj.nil?
|
|
37
|
+
fail "Unexpected nil"
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'set'
|
|
4
|
+
|
|
5
|
+
# There are some methods we'd like to use which were not added
|
|
6
|
+
# until ruby 2.1. Fortunately, they are implemented in ruby,
|
|
7
|
+
# so we can simply copy them. If we ever drop support for ruby 2.0,
|
|
8
|
+
# this file can be deleted.
|
|
9
|
+
|
|
10
|
+
unless Set.instance_methods.include?(:intersect?)
|
|
11
|
+
|
|
12
|
+
# no-doc
|
|
13
|
+
class Set
|
|
14
|
+
# Returns true if the set and the given set have at least one
|
|
15
|
+
# element in common.
|
|
16
|
+
# http://www.ruby-doc.org/stdlib-2.2.0/libdoc/set/rdoc/Set.html#method-i-intersect-3F
|
|
17
|
+
def intersect?(set)
|
|
18
|
+
unless set.is_a?(Set)
|
|
19
|
+
fail ArgumentError, "value must be a set"
|
|
20
|
+
end
|
|
21
|
+
if size < set.size
|
|
22
|
+
any? { |o| set.include?(o) }
|
|
23
|
+
else
|
|
24
|
+
set.any? { |o| include?(o) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns true if the set and the given set have no element in
|
|
29
|
+
# common. This method is the opposite of intersect?.
|
|
30
|
+
# http://www.ruby-doc.org/stdlib-2.2.0/libdoc/set/rdoc/Set.html#method-i-disjoint-3F
|
|
31
|
+
def disjoint?(set)
|
|
32
|
+
!intersect?(set)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module GraphMatching
|
|
4
|
+
# A `DirectedEdgeSet` is simply a set of directed edges in a
|
|
5
|
+
# graph. Whether the graph is actually directed or not is
|
|
6
|
+
# irrelevant, we can still discuss directed edges in an undirected
|
|
7
|
+
# graph.
|
|
8
|
+
#
|
|
9
|
+
# The naive implementation would be to use ruby's `Set` and RGL's
|
|
10
|
+
# `DirectedEdge`. This class is optimized to use a 2D array
|
|
11
|
+
# instead. The sub-array at index i represents a set (or subset)
|
|
12
|
+
# of vertexes adjacent to i.
|
|
13
|
+
#
|
|
14
|
+
class DirectedEdgeSet
|
|
15
|
+
def initialize(graph_size)
|
|
16
|
+
@edges = Array.new(graph_size + 1) { [] }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def add(v, w)
|
|
20
|
+
edges[v] << w
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def adjacent_vertices(v)
|
|
24
|
+
edges[v]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :edges
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module GraphMatching
|
|
4
|
+
class GraphMatchingError < StandardError
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
# no-doc
|
|
8
|
+
class InvalidVertexNumbering < GraphMatchingError
|
|
9
|
+
def initialize(msg = nil)
|
|
10
|
+
msg ||= <<-EOS
|
|
11
|
+
Expected vertexes to be consecutive positive integers \
|
|
12
|
+
starting with zero
|
|
13
|
+
EOS
|
|
14
|
+
super(msg)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class DisconnectedGraph < GraphMatchingError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
class NotBipartite < GraphMatchingError
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'rgl/bipartite'
|
|
4
|
+
require_relative 'graph'
|
|
5
|
+
require_relative '../algorithm/mcm_bipartite'
|
|
6
|
+
|
|
7
|
+
module GraphMatching
|
|
8
|
+
module Graph
|
|
9
|
+
# A bipartite graph (or bigraph) is a graph whose vertices can
|
|
10
|
+
# be divided into two disjoint sets U and V such that every
|
|
11
|
+
# edge connects a vertex in U to one in V.
|
|
12
|
+
class Bigraph < Graph
|
|
13
|
+
def maximum_cardinality_matching
|
|
14
|
+
Algorithm::MCMBipartite.new(self).match
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# `partition` either returns two disjoint (complementary)
|
|
18
|
+
# proper subsets of vertexes or raises a NotBipartite error.
|
|
19
|
+
#
|
|
20
|
+
# An empty graph is partitioned into two empty sets. This
|
|
21
|
+
# seems natural, but unfortunately is not the behavior of
|
|
22
|
+
# RGL's new `bipartite_sets` function. So, we have to check
|
|
23
|
+
# for the empty case, but at least we don't have to implement
|
|
24
|
+
# the algorithm ourselves anymore!
|
|
25
|
+
#
|
|
26
|
+
def partition
|
|
27
|
+
if empty?
|
|
28
|
+
[Set.new, Set.new]
|
|
29
|
+
else
|
|
30
|
+
arrays = bipartite_sets
|
|
31
|
+
fail NotBipartite if arrays.nil?
|
|
32
|
+
[Set.new(arrays[0]), Set.new(arrays[1])]
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require 'rgl/adjacency'
|
|
4
|
+
require 'rgl/connected_components'
|
|
5
|
+
require 'set'
|
|
6
|
+
require_relative '../algorithm/mcm_general'
|
|
7
|
+
require_relative '../ordered_set'
|
|
8
|
+
|
|
9
|
+
autoload(:SecureRandom, 'securerandom')
|
|
10
|
+
|
|
11
|
+
module GraphMatching
|
|
12
|
+
module Graph
|
|
13
|
+
# Base class for all graphs.
|
|
14
|
+
class Graph < RGL::AdjacencyGraph
|
|
15
|
+
def self.[](*args)
|
|
16
|
+
super.tap(&:vertexes_must_be_integers)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize(*args)
|
|
20
|
+
super
|
|
21
|
+
vertexes_must_be_integers
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# `adjacent_vertex_set` is the same as `adjacent_vertices`
|
|
25
|
+
# except it returns a `Set` instead of an `Array`. This is
|
|
26
|
+
# an optimization, performing in O(n), whereas passing
|
|
27
|
+
# `adjacent_vertices` to `Set.new` would be O(2n).
|
|
28
|
+
def adjacent_vertex_set(v)
|
|
29
|
+
s = Set.new
|
|
30
|
+
each_adjacent(v) do |u| s.add(u) end
|
|
31
|
+
s
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def connected?
|
|
35
|
+
count = 0
|
|
36
|
+
each_connected_component { count += 1 }
|
|
37
|
+
count == 1
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def maximum_cardinality_matching
|
|
41
|
+
Algorithm::MCMGeneral.new(self).match
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def max_v
|
|
45
|
+
vertexes.max
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def print
|
|
49
|
+
base_filename = SecureRandom.hex(16)
|
|
50
|
+
Visualize.new(self).png(base_filename)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def vertexes
|
|
54
|
+
to_a
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def vertexes_must_be_integers
|
|
58
|
+
return if vertices.none? { |v| !v.is_a?(Integer) }
|
|
59
|
+
fail ArgumentError, 'All vertexes must be integers'
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
module GraphMatching
|
|
4
|
+
module Graph
|
|
5
|
+
# The `Weighted` module is mixed into undirected graphs to
|
|
6
|
+
# support edge weights. Directed graphs are not supported.
|
|
7
|
+
#
|
|
8
|
+
# Data Structure
|
|
9
|
+
# --------------
|
|
10
|
+
#
|
|
11
|
+
# Weights are stored in a 2D array. The weight of an edge i,j
|
|
12
|
+
# is stored twice, at `[i][j]` and `[j][i]`.
|
|
13
|
+
#
|
|
14
|
+
# Storing the weight twice wastes memory. A symmetrical matrix
|
|
15
|
+
# can be stored in a 1D array (http://bit.ly/1DMfLM3)
|
|
16
|
+
# However, translating the 2D coordinates into a 1D index
|
|
17
|
+
# marginally increases the cost of access, and this is a read-heavy
|
|
18
|
+
# structure, so maybe the extra memory is an acceptable trade-off.
|
|
19
|
+
# It's also conceptually simpler, for what that's worth.
|
|
20
|
+
#
|
|
21
|
+
# If directed graphs were supported (they are not) this 2D array
|
|
22
|
+
# would be an obvious choice.
|
|
23
|
+
#
|
|
24
|
+
# Algorithms which operate on weighted graphs are tightly
|
|
25
|
+
# coupled to this data structure due to optimizations.
|
|
26
|
+
#
|
|
27
|
+
module Weighted
|
|
28
|
+
def self.included(base)
|
|
29
|
+
base.extend ClassMethods
|
|
30
|
+
base.class_eval do
|
|
31
|
+
attr_accessor :weight
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# no-doc
|
|
36
|
+
module ClassMethods
|
|
37
|
+
# `.[]` is the recommended, convenient constructor for
|
|
38
|
+
# weighted graphs. Each argument should be an array with
|
|
39
|
+
# three integers; the first two represent the edge, the
|
|
40
|
+
# third, the weight.
|
|
41
|
+
def [](*args)
|
|
42
|
+
assert_weighted_edges(args)
|
|
43
|
+
weightless_edges = args.map { |e| e.slice(0..1) }
|
|
44
|
+
g = super(*weightless_edges.flatten)
|
|
45
|
+
g.init_weights
|
|
46
|
+
args.each do |edge|
|
|
47
|
+
i, j, weight = edge[0] - 1, edge[1] - 1, edge[2]
|
|
48
|
+
g.weight[i][j] = weight
|
|
49
|
+
g.weight[j][i] = weight
|
|
50
|
+
end
|
|
51
|
+
g
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# `assert_weighted_edges` asserts that `ary` is an array
|
|
55
|
+
# whose elements are all arrays of exactly three elements.
|
|
56
|
+
# (The first two represent the edge, the third, the weight)
|
|
57
|
+
def assert_weighted_edges(ary)
|
|
58
|
+
return if ary.is_a?(Array) && ary.all?(&method(:weighted_edge?))
|
|
59
|
+
fail 'Invalid array of weighted edges'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# `weighted_edge?` returns true if `e` is an array whose
|
|
63
|
+
# first two elements are integers, and whose third element
|
|
64
|
+
# is a real number.
|
|
65
|
+
def weighted_edge?(e)
|
|
66
|
+
e.is_a?(Array) &&
|
|
67
|
+
e.length == 3 &&
|
|
68
|
+
e[0, 2].all? { |i| i.is_a?(Integer) } &&
|
|
69
|
+
e[2].is_a?(Integer) || e[2].is_a?(Float)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def init_weights
|
|
74
|
+
@weight = Array.new(num_vertices) { |_| Array.new(num_vertices) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def max_w
|
|
78
|
+
edges.map { |edge| w(edge.to_a) }.max
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Returns the weight of an edge. Accessing `#weight` is much
|
|
82
|
+
# faster, so this method should only be used where
|
|
83
|
+
# clarity outweighs performance.
|
|
84
|
+
def w(edge)
|
|
85
|
+
i, j = edge
|
|
86
|
+
fail ArgumentError, "Invalid edge: #{edge}" if i.nil? || j.nil?
|
|
87
|
+
fail "Edge not found: #{edge}" unless has_edge?(*edge)
|
|
88
|
+
init_weights if @weight.nil?
|
|
89
|
+
@weight[i - 1][j - 1]
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# `set_w` sets a single weight. It not efficient, and is
|
|
93
|
+
# only provided for situations where constructing the entire
|
|
94
|
+
# graph with `.[]` is not convenient.
|
|
95
|
+
def set_w(edge, weight)
|
|
96
|
+
if edge[0].nil? || edge[1].nil?
|
|
97
|
+
fail ArgumentError, "Invalid edge: #{edge}"
|
|
98
|
+
end
|
|
99
|
+
unless weight.is_a?(Integer)
|
|
100
|
+
fail TypeError, "Edge weight must be integer"
|
|
101
|
+
end
|
|
102
|
+
init_weights if @weight.nil?
|
|
103
|
+
i, j = edge[0] - 1, edge[1] - 1
|
|
104
|
+
fail "Edge not found: #{edge}" unless has_edge?(*edge)
|
|
105
|
+
@weight[i] ||= []
|
|
106
|
+
@weight[j] ||= []
|
|
107
|
+
@weight[i][j] = weight
|
|
108
|
+
@weight[j][i] = weight
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# encoding: utf-8
|
|
2
|
+
|
|
3
|
+
require_relative 'weighted'
|
|
4
|
+
require_relative '../algorithm/mwm_bipartite'
|
|
5
|
+
|
|
6
|
+
module GraphMatching
|
|
7
|
+
module Graph
|
|
8
|
+
# A bigraph whose edges have weights. See `Weighted`.
|
|
9
|
+
class WeightedBigraph < Bigraph
|
|
10
|
+
include Weighted
|
|
11
|
+
|
|
12
|
+
def maximum_weighted_matching
|
|
13
|
+
Algorithm::MWMBipartite.new(self).match
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|