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 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
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.travis.yml ADDED
@@ -0,0 +1,3 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bipartite_graph.gemspec
4
+ gemspec
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
@@ -0,0 +1,7 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
7
+
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,3 @@
1
+ module BipartiteGraph
2
+ Edge = Struct.new(:from, :to, :weight)
3
+ 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,3 @@
1
+ module BipartiteGraph
2
+ VERSION = "0.0.1"
3
+ 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
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+
3
+ describe BipartiteGraph do
4
+ it 'has a version number' do
5
+ expect(BipartiteGraph::VERSION).not_to be nil
6
+ end
7
+
8
+ it "has a new method" do
9
+ expect(BipartiteGraph.new).not_to be nil
10
+
11
+ end
12
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'bipartite_graph'
@@ -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