bipartite_graph 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 +22 -0
- data/.rspec +2 -0
- data/.travis.yml +3 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +47 -0
- data/Rakefile +7 -0
- data/bin/bundler +16 -0
- data/bin/htmldiff +16 -0
- data/bin/ldiff +16 -0
- data/bin/rake +16 -0
- data/bin/rspec +16 -0
- data/bipartite_graph.gemspec +23 -0
- data/lib/bipartite_graph/alternating_tree.rb +53 -0
- data/lib/bipartite_graph/edge.rb +3 -0
- data/lib/bipartite_graph/edge_set.rb +48 -0
- data/lib/bipartite_graph/graph.rb +34 -0
- data/lib/bipartite_graph/hungarian_algorithm.rb +161 -0
- data/lib/bipartite_graph/subgraph.rb +34 -0
- data/lib/bipartite_graph/version.rb +3 -0
- data/lib/bipartite_graph.rb +17 -0
- data/spec/acceptance/weighted_matching_spec.rb +56 -0
- data/spec/bipartite_graph_spec.rb +12 -0
- data/spec/spec_helper.rb +2 -0
- data/spec/unit/alternating_tree_spec.rb +38 -0
- data/spec/unit/edge_set_spec.rb +54 -0
- data/spec/unit/hungarian_algorithm_spec.rb +89 -0
- metadata +124 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 9a66f77f8c4a20067ac92fc48800665accb90811
|
4
|
+
data.tar.gz: b4bab5f71d800b0da8fc8bff0c347b39e70eb1e4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 8f448ee0be1644f62e2365cfcdfc433f45309a8393cef8986491dc9129f4c795ca2757bd0aa059c9c652fa6ea08cc2d792287a515e3784d96a824d6b696c42a2
|
7
|
+
data.tar.gz: e32b1adc8829db74a08d1ac12c4fb54eaf56d4c7d3830991fc8031abe4712456e23380accb4304069ece23a269a1bac52363aad6ab5ec7e6d97e766fb7f92db1
|
data/.gitignore
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
*.gem
|
2
|
+
*.rbc
|
3
|
+
.bundle
|
4
|
+
.config
|
5
|
+
.yardoc
|
6
|
+
Gemfile.lock
|
7
|
+
InstalledFiles
|
8
|
+
_yardoc
|
9
|
+
coverage
|
10
|
+
doc/
|
11
|
+
lib/bundler/man
|
12
|
+
pkg
|
13
|
+
rdoc
|
14
|
+
spec/reports
|
15
|
+
test/tmp
|
16
|
+
test/version_tmp
|
17
|
+
tmp
|
18
|
+
*.bundle
|
19
|
+
*.so
|
20
|
+
*.o
|
21
|
+
*.a
|
22
|
+
mkmf.log
|
data/.rspec
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Tom Close
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,47 @@
|
|
1
|
+
# BipartiteGraph
|
2
|
+
|
3
|
+
Finds maximum matchings in weighted bipartite graphs, using the
|
4
|
+
[Hungarian algorithm](http://en.wikipedia.org/wiki/Hungarian_algorithm).
|
5
|
+
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Add this line to your application's Gemfile:
|
10
|
+
|
11
|
+
gem 'bipartite_graph'
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install bipartite_graph
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
graph = BipartiteGraph.new
|
25
|
+
|
26
|
+
graph.add_edge('x1', 'y1', 1)
|
27
|
+
graph.add_edge('x1', 'y2', 6)
|
28
|
+
graph.add_edge('x2', 'y2', 8)
|
29
|
+
graph.add_edge('x2', 'y3', 6)
|
30
|
+
graph.add_edge('x3', 'y1', 4)
|
31
|
+
graph.add_edge('x3', 'y3', 1)
|
32
|
+
|
33
|
+
matching = graph.max_weight_matching
|
34
|
+
|
35
|
+
matching.edges.map { |e| [e.from, e.to] } #=> ['x1', 'y2'], ['x2', 'y3'], ['x3', 'y1']
|
36
|
+
|
37
|
+
matching.edges.sum(&:weight) #=> 16
|
38
|
+
|
39
|
+
```
|
40
|
+
|
41
|
+
## Contributing
|
42
|
+
|
43
|
+
1. Fork it ( https://github.com/tomclose/bipartite_graph/fork )
|
44
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
45
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
46
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
47
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
data/bin/bundler
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'bundler' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('bundler', 'bundler')
|
data/bin/htmldiff
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'htmldiff' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('diff-lcs', 'htmldiff')
|
data/bin/ldiff
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'ldiff' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('diff-lcs', 'ldiff')
|
data/bin/rake
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rake' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rake', 'rake')
|
data/bin/rspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
#
|
3
|
+
# This file was generated by Bundler.
|
4
|
+
#
|
5
|
+
# The application 'rspec' is installed as part of a gem, and
|
6
|
+
# this file is here to facilitate running it.
|
7
|
+
#
|
8
|
+
|
9
|
+
require 'pathname'
|
10
|
+
ENV['BUNDLE_GEMFILE'] ||= File.expand_path("../../Gemfile",
|
11
|
+
Pathname.new(__FILE__).realpath)
|
12
|
+
|
13
|
+
require 'rubygems'
|
14
|
+
require 'bundler/setup'
|
15
|
+
|
16
|
+
load Gem.bin_path('rspec-core', 'rspec')
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'bipartite_graph/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "bipartite_graph"
|
8
|
+
spec.version = BipartiteGraph::VERSION
|
9
|
+
spec.authors = ["Tom Close"]
|
10
|
+
spec.email = ["tom.close@cantab.net"]
|
11
|
+
spec.summary = %q{Finds maximum matchings in weighted bipartite graphs}
|
12
|
+
spec.homepage = ""
|
13
|
+
spec.license = "MIT"
|
14
|
+
|
15
|
+
spec.files = `git ls-files -z`.split("\x0")
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ["lib"]
|
19
|
+
|
20
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
21
|
+
spec.add_development_dependency "rake"
|
22
|
+
spec.add_development_dependency "rspec"
|
23
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module BipartiteGraph
|
2
|
+
# an alternating tree isn't a graph in its own right
|
3
|
+
# it's a subgraph - you can't add things that aren't in the main graph
|
4
|
+
class AlternatingTree
|
5
|
+
attr_accessor :root, :graph
|
6
|
+
def initialize(graph, root)
|
7
|
+
raise "Root can't be nil" if root.nil?
|
8
|
+
@root = root
|
9
|
+
@graph = graph
|
10
|
+
|
11
|
+
@node_map = {}
|
12
|
+
@node_map[root] = [nil, nil]
|
13
|
+
end
|
14
|
+
|
15
|
+
def has_node?(node)
|
16
|
+
@node_map.has_key?(node)
|
17
|
+
end
|
18
|
+
|
19
|
+
def nodes
|
20
|
+
@node_map.keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def sources
|
24
|
+
graph.sources.select {|node| has_node?(node) }
|
25
|
+
end
|
26
|
+
def sinks
|
27
|
+
graph.sinks.select {|node| has_node?(node) }
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_edge(edge)
|
31
|
+
#raise "Tree has no existing node #{edge.from}" unless has_node?(edge.from)
|
32
|
+
#raise "Tree already contains node #{edge.to}" if has_node?(edge.to)
|
33
|
+
if has_node?(edge.from)
|
34
|
+
@node_map[edge.to] = [edge.from, edge]
|
35
|
+
elsif has_node?(edge.to)
|
36
|
+
@node_map[edge.from] = [edge.to, edge]
|
37
|
+
else
|
38
|
+
raise "Nodes not in tree: #{edge.from} or #{edge.to}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def path_to(leaf)
|
43
|
+
path = []
|
44
|
+
next_node, edge = @node_map[leaf]
|
45
|
+
while edge
|
46
|
+
path << edge
|
47
|
+
next_node, edge = @node_map[next_node]
|
48
|
+
end
|
49
|
+
path.reverse
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
module BipartiteGraph
|
2
|
+
class EdgeSet
|
3
|
+
include Enumerable
|
4
|
+
attr_reader :edges, :filter
|
5
|
+
|
6
|
+
def initialize(edges = Set.new, filter = {})
|
7
|
+
@edges = edges
|
8
|
+
@filter = filter
|
9
|
+
end
|
10
|
+
|
11
|
+
def <<(edge)
|
12
|
+
add(edge)
|
13
|
+
end
|
14
|
+
|
15
|
+
def add(edge)
|
16
|
+
edges.add(edge)
|
17
|
+
end
|
18
|
+
|
19
|
+
def delete(edge)
|
20
|
+
edges.delete(edge)
|
21
|
+
end
|
22
|
+
|
23
|
+
def length
|
24
|
+
to_a.length
|
25
|
+
end
|
26
|
+
|
27
|
+
def from(node_or_nodes)
|
28
|
+
from_set = Set.new(Array(node_or_nodes))
|
29
|
+
self.class.new(edges, filter.merge({ from: from_set }))
|
30
|
+
end
|
31
|
+
|
32
|
+
def not_to(node_or_nodes)
|
33
|
+
not_to_set = Set.new(Array(node_or_nodes))
|
34
|
+
self.class.new(edges, filter.merge({ not_to: not_to_set }))
|
35
|
+
end
|
36
|
+
|
37
|
+
def each
|
38
|
+
from_set = filter[:from]
|
39
|
+
not_to_set = filter[:not_to]
|
40
|
+
edges.each do |edge|
|
41
|
+
from_cond = !from_set || from_set.include?(edge.from)
|
42
|
+
not_to_cond = !not_to_set || !not_to_set.include?(edge.to)
|
43
|
+
|
44
|
+
yield edge if from_cond && not_to_cond
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module BipartiteGraph
|
2
|
+
class Graph
|
3
|
+
attr_reader :sources, :sinks, :edges, :nodes
|
4
|
+
def initialize
|
5
|
+
clear
|
6
|
+
end
|
7
|
+
|
8
|
+
def clear
|
9
|
+
@nodes = Set.new
|
10
|
+
@sources = Set.new
|
11
|
+
@sinks = Set.new
|
12
|
+
@edges = EdgeSet.new
|
13
|
+
end
|
14
|
+
|
15
|
+
def add_edge(from, to, weight=1)
|
16
|
+
@sources << from
|
17
|
+
@sinks << to
|
18
|
+
@nodes = @sources + @sinks
|
19
|
+
|
20
|
+
edge = Edge.new(from, to, weight)
|
21
|
+
@edges << edge
|
22
|
+
edge
|
23
|
+
end
|
24
|
+
|
25
|
+
def node_for(key)
|
26
|
+
@nodes[key]
|
27
|
+
end
|
28
|
+
|
29
|
+
|
30
|
+
def max_weight_matching
|
31
|
+
HungarianAlgorithm.new(self).solution
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,161 @@
|
|
1
|
+
module BipartiteGraph
|
2
|
+
class HungarianAlgorithm
|
3
|
+
attr_reader :labelling, :matching, :graph
|
4
|
+
|
5
|
+
def initialize(graph)
|
6
|
+
@graph = graph
|
7
|
+
@labelling = Labelling.new(graph)
|
8
|
+
@matching = create_initial_matching
|
9
|
+
end
|
10
|
+
|
11
|
+
|
12
|
+
def create_initial_matching
|
13
|
+
eq_graph = labelling.equality_graph
|
14
|
+
|
15
|
+
matching = Matching.new(graph)
|
16
|
+
|
17
|
+
eq_graph.sources.each do |source|
|
18
|
+
eq_graph.edges.from(source).each do |edge|
|
19
|
+
included = matching.has_node?(edge.to)
|
20
|
+
|
21
|
+
if !included
|
22
|
+
matching.add_edge(edge)
|
23
|
+
next
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
matching
|
28
|
+
end
|
29
|
+
|
30
|
+
class Labelling
|
31
|
+
# subgraph of edges where weight = sum of end labels
|
32
|
+
attr_reader :equality_graph, :graph, :labels
|
33
|
+
|
34
|
+
def initialize(graph)
|
35
|
+
@labels = {}
|
36
|
+
@graph = graph
|
37
|
+
@equality_graph = Subgraph.new(graph)
|
38
|
+
|
39
|
+
graph.sources.each do |node|
|
40
|
+
edges = graph.edges.from(node)
|
41
|
+
max_weight = edges.map(&:weight).max
|
42
|
+
@labels[node] = max_weight
|
43
|
+
end
|
44
|
+
|
45
|
+
graph.sinks.each do |node|
|
46
|
+
@labels[node] = 0
|
47
|
+
end
|
48
|
+
|
49
|
+
recalculate_equality_graph
|
50
|
+
end
|
51
|
+
|
52
|
+
def label_for(node)
|
53
|
+
@labels[node]
|
54
|
+
end
|
55
|
+
|
56
|
+
def recalculate_equality_graph
|
57
|
+
equality_graph.clear
|
58
|
+
|
59
|
+
graph.edges.select {|e| e.weight == labels[e.from] + labels[e.to]}.each do |edge|
|
60
|
+
equality_graph.add_edge(edge)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def update(nodes_to_increase, nodes_to_decrease, change)
|
65
|
+
raise "Change must be positive" unless change > 0
|
66
|
+
nodes_to_increase.each { |n| labels[n] += change }
|
67
|
+
nodes_to_decrease.each { |n| labels[n] -= change }
|
68
|
+
|
69
|
+
recalculate_equality_graph #could be cleverer about this ..
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
class Matching
|
74
|
+
attr_reader :edges, :graph
|
75
|
+
|
76
|
+
def initialize(graph)
|
77
|
+
@graph = graph
|
78
|
+
@edges = EdgeSet.new
|
79
|
+
end
|
80
|
+
|
81
|
+
def edge_for(node)
|
82
|
+
@edges.find {|edge| edge.to == node || edge.from == node }
|
83
|
+
end
|
84
|
+
|
85
|
+
def has_node?(node)
|
86
|
+
!!edge_for(node)
|
87
|
+
end
|
88
|
+
|
89
|
+
def sources
|
90
|
+
graph.sources.select {|node| has_node?(node) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def perfect?
|
94
|
+
2 * edges.length == graph.nodes.length
|
95
|
+
end
|
96
|
+
|
97
|
+
def add_edge(edge)
|
98
|
+
edges << edge
|
99
|
+
end
|
100
|
+
|
101
|
+
def delete_edge(edge)
|
102
|
+
edges.delete(edge)
|
103
|
+
end
|
104
|
+
|
105
|
+
def apply_alternating_path(path)
|
106
|
+
path.each_with_index do |edge, i|
|
107
|
+
i.even? ? add_edge(edge) : delete_edge(edge)
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def solution
|
113
|
+
while !matching.perfect?
|
114
|
+
root = (graph.sources - matching.sources).first
|
115
|
+
add_to_matching(root)
|
116
|
+
end
|
117
|
+
|
118
|
+
matching
|
119
|
+
end
|
120
|
+
|
121
|
+
def equality_graph
|
122
|
+
labelling.equality_graph
|
123
|
+
end
|
124
|
+
|
125
|
+
def add_to_matching(root)
|
126
|
+
tree = AlternatingTree.new(graph, root)
|
127
|
+
matching_found = false
|
128
|
+
|
129
|
+
while !matching_found
|
130
|
+
new_edge = equality_graph.edges.from(tree.sources).not_to(tree.sinks).first
|
131
|
+
|
132
|
+
if new_edge.nil?
|
133
|
+
augment_labelling_using(tree)
|
134
|
+
else
|
135
|
+
tree.add_edge(new_edge)
|
136
|
+
new_sink = new_edge.to
|
137
|
+
|
138
|
+
existing_edge = matching.edge_for(new_sink)
|
139
|
+
if existing_edge
|
140
|
+
tree.add_edge(existing_edge)
|
141
|
+
else
|
142
|
+
matching.apply_alternating_path(tree.path_to(new_edge.to))
|
143
|
+
matching_found = true
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def augment_labelling_using(tree)
|
150
|
+
target_edges = graph.edges.from(tree.sources).not_to(tree.sinks)
|
151
|
+
|
152
|
+
slack = target_edges.map do |edge|
|
153
|
+
labelling.label_for(edge.from) + labelling.label_for(edge.to) - edge.weight
|
154
|
+
end
|
155
|
+
|
156
|
+
max_decrease = slack.min
|
157
|
+
|
158
|
+
labelling.update(tree.sinks, tree.sources, max_decrease)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module BipartiteGraph
|
2
|
+
class Subgraph
|
3
|
+
attr_reader :sources, :sinks, :edges, :graph, :nodes
|
4
|
+
def initialize(graph)
|
5
|
+
@graph = graph
|
6
|
+
clear
|
7
|
+
end
|
8
|
+
|
9
|
+
def clear
|
10
|
+
@sources = Set.new
|
11
|
+
@sinks = Set.new
|
12
|
+
@nodes = Set.new
|
13
|
+
@edges = EdgeSet.new
|
14
|
+
end
|
15
|
+
|
16
|
+
def add_edge(edge)
|
17
|
+
@edges << edge
|
18
|
+
@sources << edge.from
|
19
|
+
@sinks << edge.to
|
20
|
+
@nodes = @sources + @sinks
|
21
|
+
end
|
22
|
+
|
23
|
+
def has_node?(node)
|
24
|
+
nodes.include?(node)
|
25
|
+
end
|
26
|
+
|
27
|
+
def sources
|
28
|
+
graph.sources.select {|node| has_node?(node) }
|
29
|
+
end
|
30
|
+
def sinks
|
31
|
+
graph.sinks.select {|node| has_node?(node) }
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "bipartite_graph/version"
|
2
|
+
require "bipartite_graph/graph"
|
3
|
+
require "bipartite_graph/edge"
|
4
|
+
require "bipartite_graph/edge_set"
|
5
|
+
require "bipartite_graph/hungarian_algorithm"
|
6
|
+
require "bipartite_graph/alternating_tree"
|
7
|
+
require "bipartite_graph/subgraph"
|
8
|
+
|
9
|
+
module BipartiteGraph
|
10
|
+
|
11
|
+
|
12
|
+
def self.new
|
13
|
+
Graph.new
|
14
|
+
end
|
15
|
+
|
16
|
+
|
17
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "weighted matchings" do
|
4
|
+
describe "simple example 1" do
|
5
|
+
#http://www.cse.ust.hk/~golin/COMP572/Notes/Matching.pdf
|
6
|
+
let(:graph) { BipartiteGraph.new }
|
7
|
+
|
8
|
+
before do
|
9
|
+
[
|
10
|
+
['x1', 'y1', 1],
|
11
|
+
['x1', 'y2', 6],
|
12
|
+
['x2', 'y2', 8],
|
13
|
+
['x2', 'y3', 6],
|
14
|
+
['x3', 'y1', 4],
|
15
|
+
['x3', 'y3', 1]
|
16
|
+
].each { |from, to, weight| graph.add_edge(from, to, weight) }
|
17
|
+
end
|
18
|
+
|
19
|
+
it "finds the solution" do
|
20
|
+
matching = graph.max_weight_matching.edges.map {|e| [e.from, e.to] }
|
21
|
+
|
22
|
+
expect(matching).to match_array([
|
23
|
+
['x1', 'y2'], ['x2', 'y3'], ['x3', 'y1']
|
24
|
+
])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "simple example 2" do
|
29
|
+
let(:graph) { BipartiteGraph.new }
|
30
|
+
|
31
|
+
before do
|
32
|
+
[
|
33
|
+
['x1', 'y1', 3],
|
34
|
+
['x1', 'y2', 1],
|
35
|
+
['x2', 'y1', 11],
|
36
|
+
['x2', 'y2', 8],
|
37
|
+
['x2', 'y3', 6],
|
38
|
+
['x3', 'y1', 10],
|
39
|
+
['x3', 'y2', 9],
|
40
|
+
['x3', 'y3', 8],
|
41
|
+
['x3', 'y4', 7],
|
42
|
+
['x4', 'y3', 6],
|
43
|
+
['x4', 'y4', 4],
|
44
|
+
].each { |from, to, weight| graph.add_edge(from, to, weight) }
|
45
|
+
end
|
46
|
+
|
47
|
+
it "finds the solution" do
|
48
|
+
matching = graph.max_weight_matching.edges.map {|e| [e.from, e.to] }
|
49
|
+
|
50
|
+
expect(matching).to match_array([
|
51
|
+
['x1', 'y2'], ['x2', 'y1'], ['x3', 'y4'], ['x4', 'y3']
|
52
|
+
])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "AlternatingTree" do
|
4
|
+
let(:graph) { BipartiteGraph.new }
|
5
|
+
let(:edge1) { graph.add_edge('Abe', 'Homer') }
|
6
|
+
let(:edge2) { graph.add_edge('Homer', 'Bart') }
|
7
|
+
let(:edge3) { graph.add_edge('Homer', 'Lisa') }
|
8
|
+
let(:edge4) { graph.add_edge('Homer', 'Maggie') }
|
9
|
+
|
10
|
+
let(:tree) { BipartiteGraph::AlternatingTree.new(graph, 'Abe') }
|
11
|
+
describe "empty" do
|
12
|
+
it "includes the root" do
|
13
|
+
expect(tree.has_node?("Abe")).to be true
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "with edges" do
|
18
|
+
before do
|
19
|
+
tree.add_edge edge1
|
20
|
+
tree.add_edge edge2
|
21
|
+
tree.add_edge edge3
|
22
|
+
tree.add_edge edge4
|
23
|
+
end
|
24
|
+
|
25
|
+
it "#nodes" do
|
26
|
+
expect(tree.nodes).to match_array(%w(Abe Homer Bart Lisa Maggie))
|
27
|
+
end
|
28
|
+
|
29
|
+
it "#has_node?" do
|
30
|
+
expect(tree.has_node?("Bart")).to be true
|
31
|
+
expect(tree.has_node?("Marge")).to be false
|
32
|
+
end
|
33
|
+
|
34
|
+
it "path_to" do
|
35
|
+
expect(tree.path_to("Maggie")).to eq([edge1, edge4])
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe 'EdgeSet' do
|
4
|
+
let(:edge_set) { BipartiteGraph::EdgeSet.new }
|
5
|
+
|
6
|
+
let(:edge1) { BipartiteGraph::Edge.new('0C0', '1C0') }
|
7
|
+
let(:edge2) { BipartiteGraph::Edge.new('0C0', '1C1') }
|
8
|
+
let(:edge3) { BipartiteGraph::Edge.new('1C0', '2C0') }
|
9
|
+
let(:edge4) { BipartiteGraph::Edge.new('1C0', '2C1') }
|
10
|
+
let(:edge5) { BipartiteGraph::Edge.new('1C1', '2C1') }
|
11
|
+
let(:edge6) { BipartiteGraph::Edge.new('1C1', '2C2') }
|
12
|
+
|
13
|
+
before do
|
14
|
+
edge_set << edge1
|
15
|
+
edge_set << edge2
|
16
|
+
edge_set << edge3
|
17
|
+
edge_set << edge4
|
18
|
+
edge_set << edge5
|
19
|
+
edge_set << edge6
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "enumerablility" do
|
23
|
+
it "(for example) can be converted to an array" do
|
24
|
+
expect(edge_set.to_a).to match_array([edge1, edge2, edge3, edge4, edge5, edge6])
|
25
|
+
end
|
26
|
+
|
27
|
+
it "(for example) can be mapped" do
|
28
|
+
expect(edge_set.map(&:from)).to match_array(%w(0C0 0C0 1C0 1C0 1C1 1C1))
|
29
|
+
end
|
30
|
+
|
31
|
+
it "(for example) can do an any?" do
|
32
|
+
expect(edge_set.any? {|e| e.to == '2C2'}).to be true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "length" do
|
37
|
+
it "will give its length" do
|
38
|
+
expect(edge_set.length).to eq(6)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "filtering" do
|
43
|
+
let(:filtered_edge_set) { edge_set.from('1C1').not_to('2C1') }
|
44
|
+
|
45
|
+
it "returns an edge set" do
|
46
|
+
expect(filtered_edge_set).to be_a BipartiteGraph::EdgeSet
|
47
|
+
end
|
48
|
+
|
49
|
+
it "filters appropriately" do
|
50
|
+
expect(filtered_edge_set.to_a).to match_array([edge6])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe "hungarian algorithm" do
|
4
|
+
|
5
|
+
let(:simple_example_edges) do
|
6
|
+
#http://www.cse.ust.hk/~golin/COMP572/Notes/Matching.pdf
|
7
|
+
[
|
8
|
+
['x1', 'y1', 1],
|
9
|
+
['x1', 'y2', 6],
|
10
|
+
['x2', 'y2', 8],
|
11
|
+
['x2', 'y3', 6],
|
12
|
+
['x3', 'y1', 4],
|
13
|
+
['x3', 'y3', 1]
|
14
|
+
]
|
15
|
+
end
|
16
|
+
|
17
|
+
let(:simple_example_solution) do
|
18
|
+
[
|
19
|
+
['x1', 'y2'],
|
20
|
+
['x2', 'y3'],
|
21
|
+
['x3', 'y1']
|
22
|
+
]
|
23
|
+
end
|
24
|
+
|
25
|
+
let(:graph) { BipartiteGraph.new }
|
26
|
+
before do
|
27
|
+
simple_example_edges.each do |from, to, weight|
|
28
|
+
graph.add_edge(from, to, weight)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
let(:alg) { BipartiteGraph::HungarianAlgorithm.new(graph) }
|
32
|
+
|
33
|
+
describe "setup" do
|
34
|
+
|
35
|
+
it "calculates an initial labelling" do
|
36
|
+
expect(alg.labelling.labels).to eq({
|
37
|
+
'x1' => 6, 'x2' => 8, 'x3' => 4, 'y1' => 0, 'y2' => 0, 'y3' => 0
|
38
|
+
})
|
39
|
+
end
|
40
|
+
|
41
|
+
it "calculates the equality graph" do
|
42
|
+
expect(alg.labelling.equality_graph.edges.map {|e| [e.from, e.to] }).to match_array([
|
43
|
+
['x1', 'y2'], ['x2', 'y2'], ['x3', 'y1']
|
44
|
+
])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "calculates an initial matching" do
|
48
|
+
matching = alg.matching.edges.map {|e| "#{e.from} -> #{e.to}" }.sort
|
49
|
+
|
50
|
+
allowed_matching1 = ['x1 -> y2', 'x3 -> y1']
|
51
|
+
allowed_matching2 = ['x2 -> y2', 'x3 -> y1']
|
52
|
+
|
53
|
+
expect([allowed_matching1, allowed_matching2]).to include(matching)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "full iteration" do
|
58
|
+
before do
|
59
|
+
# have to assume we started with matching1 for algorithm to progress like this
|
60
|
+
matching = alg.matching.edges.map {|e| "#{e.from} -> #{e.to}" }.sort
|
61
|
+
allowed_matching1 = ['x1 -> y2', 'x3 -> y1']
|
62
|
+
expect(matching).to eq(allowed_matching1)
|
63
|
+
|
64
|
+
alg.add_to_matching('x2')
|
65
|
+
end
|
66
|
+
|
67
|
+
it "updates the labelling" do
|
68
|
+
expect(alg.labelling.labels).to eq({
|
69
|
+
'x1' => 4, 'x2' => 6, 'x3' => 4, 'y1' => 0, 'y2' => 2, 'y3' => 0
|
70
|
+
})
|
71
|
+
end
|
72
|
+
|
73
|
+
it "updates the equality graph" do
|
74
|
+
expect(alg.labelling.equality_graph.edges.map {|e| [e.from, e.to] }).to match_array([
|
75
|
+
['x1', 'y2'], ['x2', 'y2'], ['x2', 'y3'], ['x3', 'y1']
|
76
|
+
])
|
77
|
+
end
|
78
|
+
|
79
|
+
it "updates the matching" do
|
80
|
+
matching = alg.matching.edges.map {|e| "#{e.from} -> #{e.to}" }.sort
|
81
|
+
|
82
|
+
expect(matching).to eq(['x1 -> y2', 'x2 -> y3', 'x3 -> y1'])
|
83
|
+
end
|
84
|
+
|
85
|
+
|
86
|
+
|
87
|
+
end
|
88
|
+
|
89
|
+
end
|
metadata
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: bipartite_graph
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Tom Close
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-02-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ~>
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.6'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ~>
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.6'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - '>='
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
description:
|
56
|
+
email:
|
57
|
+
- tom.close@cantab.net
|
58
|
+
executables:
|
59
|
+
- bundler
|
60
|
+
- htmldiff
|
61
|
+
- ldiff
|
62
|
+
- rake
|
63
|
+
- rspec
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- .gitignore
|
68
|
+
- .rspec
|
69
|
+
- .travis.yml
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE.txt
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- bin/bundler
|
75
|
+
- bin/htmldiff
|
76
|
+
- bin/ldiff
|
77
|
+
- bin/rake
|
78
|
+
- bin/rspec
|
79
|
+
- bipartite_graph.gemspec
|
80
|
+
- lib/bipartite_graph.rb
|
81
|
+
- lib/bipartite_graph/alternating_tree.rb
|
82
|
+
- lib/bipartite_graph/edge.rb
|
83
|
+
- lib/bipartite_graph/edge_set.rb
|
84
|
+
- lib/bipartite_graph/graph.rb
|
85
|
+
- lib/bipartite_graph/hungarian_algorithm.rb
|
86
|
+
- lib/bipartite_graph/subgraph.rb
|
87
|
+
- lib/bipartite_graph/version.rb
|
88
|
+
- spec/acceptance/weighted_matching_spec.rb
|
89
|
+
- spec/bipartite_graph_spec.rb
|
90
|
+
- spec/spec_helper.rb
|
91
|
+
- spec/unit/alternating_tree_spec.rb
|
92
|
+
- spec/unit/edge_set_spec.rb
|
93
|
+
- spec/unit/hungarian_algorithm_spec.rb
|
94
|
+
homepage: ''
|
95
|
+
licenses:
|
96
|
+
- MIT
|
97
|
+
metadata: {}
|
98
|
+
post_install_message:
|
99
|
+
rdoc_options: []
|
100
|
+
require_paths:
|
101
|
+
- lib
|
102
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
103
|
+
requirements:
|
104
|
+
- - '>='
|
105
|
+
- !ruby/object:Gem::Version
|
106
|
+
version: '0'
|
107
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - '>='
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: '0'
|
112
|
+
requirements: []
|
113
|
+
rubyforge_project:
|
114
|
+
rubygems_version: 2.4.4
|
115
|
+
signing_key:
|
116
|
+
specification_version: 4
|
117
|
+
summary: Finds maximum matchings in weighted bipartite graphs
|
118
|
+
test_files:
|
119
|
+
- spec/acceptance/weighted_matching_spec.rb
|
120
|
+
- spec/bipartite_graph_spec.rb
|
121
|
+
- spec/spec_helper.rb
|
122
|
+
- spec/unit/alternating_tree_spec.rb
|
123
|
+
- spec/unit/edge_set_spec.rb
|
124
|
+
- spec/unit/hungarian_algorithm_spec.rb
|