munkres 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (6) hide show
  1. data/History.txt +6 -0
  2. data/README.txt +64 -0
  3. data/Rakefile +33 -0
  4. data/lib/munkres.rb +271 -0
  5. data/test/munkres_test.rb +234 -0
  6. metadata +59 -0
@@ -0,0 +1,6 @@
1
+ === 0.1.0 / 2010-01-26
2
+
3
+ * 1 major enhancement
4
+
5
+ * Birthday!
6
+
@@ -0,0 +1,64 @@
1
+ = munkres
2
+
3
+ http://github.com/pdamer/munkres
4
+
5
+ == DESCRIPTION:
6
+
7
+ A ruby implementation of the kuhn-munkres or 'hungarian' algorithm for bipartite matching.
8
+
9
+ == FEATURES/PROBLEMS:
10
+
11
+ Match groups together 1 to 1
12
+
13
+ == SYNOPSIS:
14
+
15
+ Create a 2d matrix joining your two groups to match with each entry
16
+ being the cost of matching the corresponding members of the groups
17
+
18
+ require 'munkres'
19
+
20
+ cost_matrix = [[4,3],
21
+ [3,0]]
22
+
23
+ m = Munkres.new(cost_matrix)
24
+ p m.find_pairings
25
+
26
+
27
+ == REQUIREMENTS:
28
+
29
+ Uses test/spec for testing.
30
+
31
+ == INSTALL:
32
+
33
+ sudo gem install munkres
34
+
35
+ == DEVELOPERS:
36
+
37
+ After checking out the source, run:
38
+
39
+ $ rake test
40
+
41
+ == LICENSE:
42
+
43
+ (The MIT License)
44
+
45
+ Copyright (c) 2010
46
+
47
+ Permission is hereby granted, free of charge, to any person obtaining
48
+ a copy of this software and associated documentation files (the
49
+ 'Software'), to deal in the Software without restriction, including
50
+ without limitation the rights to use, copy, modify, merge, publish,
51
+ distribute, sublicense, and/or sell copies of the Software, and to
52
+ permit persons to whom the Software is furnished to do so, subject to
53
+ the following conditions:
54
+
55
+ The above copyright notice and this permission notice shall be
56
+ included in all copies or substantial portions of the Software.
57
+
58
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
59
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
60
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
61
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
62
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
63
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
64
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,33 @@
1
+ require 'rubygems'
2
+ require 'rake/testtask'
3
+ require 'rake/gempackagetask'
4
+
5
+ spec = Gem::Specification.new do |s|
6
+ s.name = "munkres"
7
+ s.version = "0.1.0"
8
+ s.author = "Paul Damer and Jim Wood"
9
+ s.email = 'pdamer@gmail.com'
10
+ s.homepage = "http://github.com/pdamer/munkres"
11
+ s.platform = Gem::Platform::RUBY
12
+ s.summary = "A Ruby implementation of the Hungarian Algorithm"
13
+ s.description = "A ruby implementation of the kuhn-munkres or 'hungarian' algorithm for bipartite matching."
14
+ s.rubyforge_project = "munkres"
15
+ s.test_files = ['test/munkres_test.rb']
16
+ s.files = %w[
17
+ lib/munkres.rb
18
+ test/munkres_test.rb
19
+ README.txt
20
+ Rakefile
21
+ History.txt
22
+ ]
23
+ end
24
+
25
+ Rake::GemPackageTask.new(spec) do |pkg|
26
+ pkg.need_tar = true
27
+ end
28
+
29
+ Rake::TestTask.new() do |t|
30
+ t.libs << "test"
31
+ t.test_files = FileList['test/*_test.rb']
32
+ t.verbose = true
33
+ end
@@ -0,0 +1,271 @@
1
+ class Munkres
2
+ MODE_MINIMIZE_COST = 1
3
+ MODE_MAXIMIZE_UTIL = 2
4
+
5
+ def initialize(matrix=[], mode=MODE_MINIMIZE_COST)
6
+ @matrix = matrix
7
+ @mode = mode
8
+ @original = Marshal.load(Marshal.dump(@matrix))
9
+ validate_and_pad
10
+ @covered_columns = []
11
+ @covered_rows = []
12
+ @starred_zeros = []
13
+ @primed_zeros = []
14
+
15
+ class << @matrix
16
+
17
+
18
+ def row(index)
19
+ self[index]
20
+ end
21
+
22
+ def column(index)
23
+ self.collect do |row|
24
+ row[index]
25
+ end
26
+ end
27
+
28
+ def columns
29
+ result = []
30
+ self.first.each_index do |i|
31
+ result << self.column(i)
32
+ end
33
+ result
34
+ end
35
+
36
+ def each_column_index &block
37
+ column_indices.each &block
38
+ end
39
+
40
+ def column_indices
41
+ @column_indices ||= (0...self.first.size).to_a
42
+ end
43
+
44
+ def row_indices
45
+ @row_indices ||= (0...self.size).to_a
46
+ end
47
+ end
48
+
49
+ end
50
+
51
+ def find_pairings
52
+ create_zero_in_rows
53
+ star_zeros
54
+ cover_columns_with_stars
55
+ while not done?
56
+ p = cover_zeros_and_create_more
57
+ find_better_stars p
58
+ #create series
59
+ cover_columns_with_stars
60
+ end
61
+ @pairings = @starred_zeros.delete_if{|row_index,col_index| col_index >= @original.first.size || row_index >= @original.size}
62
+ end
63
+
64
+ def total_cost_of_pairing
65
+ @pairings.inject(0) {|total, star| total + @original[star[0]][star[1]]}
66
+ end
67
+
68
+ def pretty_print
69
+ print_col_row
70
+ @matrix.each_with_index do |row,row_index|
71
+ print @covered_rows.include?(row_index) ? "-" : " "
72
+ row.each_with_index do |value, col_index|
73
+ print value
74
+ print "*" if @starred_zeros.include? [row_index,col_index]
75
+ print "'" if @primed_zeros.include? [row_index,col_index]
76
+ print "\t"
77
+ end
78
+ print @covered_rows.include?(row_index) ? "-" : " "
79
+ print "\n"
80
+ end
81
+ print_col_row
82
+ end
83
+
84
+ def print_col_row
85
+ print " "
86
+ @matrix.column_indices.each do |col_index|
87
+ print "|" if @covered_columns.include? col_index
88
+ print "\t"
89
+ end
90
+ print "\n"
91
+ end
92
+
93
+ protected
94
+
95
+ attr_accessor :matrix, :covered_columns, :covered_rows, :starred_zeros, :primed_zeros, :primed_starred_series
96
+
97
+ def create_zero_in_rows
98
+ @matrix.each_with_index do |row,row_index|
99
+ min_val = min_or_zero(row)
100
+ row.each_with_index do |value, col_index|
101
+ @matrix[row_index][col_index] = value - min_val
102
+ end
103
+ end
104
+ end
105
+
106
+ def star_zeros
107
+ unstarred_columns = @matrix.column_indices
108
+
109
+ @matrix.each_with_index do |row, row_index|
110
+ star = star_in_row?(row_index)
111
+ next if star
112
+ unstarred_columns.each do |col_index|
113
+ if (row[col_index] == 0 and !star_in_column?(col_index))
114
+ @starred_zeros << [row_index, col_index]
115
+ unstarred_columns -= [col_index]
116
+ break # go to next row
117
+ end
118
+ end
119
+ end
120
+ end
121
+
122
+ def cover_columns_with_stars
123
+ cols = @starred_zeros.collect {|z| z[1]}
124
+ cols.uniq!
125
+ @covered_columns += cols
126
+ end
127
+
128
+ def prime_first_uncovered_zero
129
+ my_cols = uncovered_columns #silly workaround to cache the list
130
+
131
+ uncovered_rows.each do |row_index|
132
+ my_cols.each do |col_index|
133
+ if @matrix[row_index][col_index] == 0
134
+ @primed_zeros << [row_index, col_index]
135
+ return [row_index, col_index]
136
+ end
137
+ end
138
+ end
139
+ nil
140
+ end
141
+
142
+ def smallest_uncovered_value
143
+ min_value = nil
144
+ my_cols = uncovered_columns #silly workaround to cache the list
145
+ uncovered_rows.each do |row_index|
146
+ my_cols.each do |col_index|
147
+ value = @matrix[row_index][col_index]
148
+ min_value ||= value
149
+ min_value = value if value < min_value
150
+ return 0 if min_value == 0
151
+ end
152
+ end
153
+ min_value
154
+ end
155
+
156
+ def add_and_subtract_for_step_6(delta=0)
157
+ covered_rows.each do |row_index|
158
+ covered_columns.each do |col_index|
159
+ @matrix[row_index][col_index] += delta
160
+ end
161
+ end
162
+
163
+
164
+ my_cols = uncovered_columns #silly workaround to cache the list
165
+
166
+ uncovered_rows.each do |row_index|
167
+ my_cols.each do |col_index|
168
+ @matrix[row_index][col_index] -= delta
169
+ end
170
+ end
171
+ end
172
+
173
+ def find_better_stars(first_zero)
174
+ primes_series = [first_zero]
175
+ stars_series = []
176
+ while next_star = @starred_zeros.detect{|row, col| col == primes_series.last[1] }
177
+ stars_series << next_star
178
+ primes_series << @primed_zeros.detect{|row, col| row == stars_series.last[0] }
179
+ end
180
+ stars_series.each do |star|
181
+ @starred_zeros.delete(star)
182
+ end
183
+
184
+ primes_series.each do |prime|
185
+ @starred_zeros << prime
186
+ end
187
+
188
+ @primed_zeros = []
189
+ @covered_columns = []
190
+ @covered_rows = []
191
+ end
192
+
193
+ def done?
194
+ @matrix.column_indices.size == covered_columns.size
195
+ end
196
+
197
+ def min_or_zero(collection)
198
+ collection.index(0) ? 0 : collection.min
199
+ end
200
+
201
+ def star_in_row?(index)
202
+ @starred_zeros.any? {|row_index,col_index| row_index == index}
203
+ end
204
+
205
+ def star_in_row(index)
206
+ #@starred_zeros.detect {|row_index,col_index| row_index == index}
207
+ @starred_zeros.assoc index
208
+ end
209
+
210
+ def star_in_column?(index)
211
+ @starred_zeros.any? {|row_index,col_index| col_index == index}
212
+ end
213
+
214
+ def uncovered_rows
215
+ @matrix.row_indices - @covered_rows
216
+ end
217
+
218
+ def uncovered_columns
219
+ @matrix.column_indices - @covered_columns
220
+ end
221
+
222
+ #step 4
223
+ def cover_zeros_and_create_more
224
+ loop do
225
+ while prime = prime_first_uncovered_zero
226
+ if star = star_in_row(prime[0])
227
+ @covered_rows << prime[0]
228
+ @covered_columns -= [star[1]]
229
+ else
230
+ return prime
231
+ end
232
+ end
233
+
234
+ add_and_subtract_for_step_6(smallest_uncovered_value)
235
+ end
236
+ end
237
+
238
+ def validate_and_pad
239
+ raise(ArgumentError, "Munkres matrix is empty") unless @matrix.first
240
+ raise(ArgumentError, "Munkres first row is empty") unless @matrix.first.size > 0
241
+ raise(ArgumentError, "Munkres matrix is not rectangular", caller) if @matrix.any? {|row| row.size != @matrix.first.size }
242
+ raise(ArgumentError, "Munkres matrix is wider than it is tall", caller) if @matrix.size < @matrix.first.size
243
+
244
+ if @matrix.size > @matrix.first.size
245
+ number_of_cols = @matrix.first.size
246
+ @matrix.each_with_index do |row, row_index|
247
+ (number_of_cols...@matrix.size).each do |col_index|
248
+ @matrix[row_index][col_index] = 0
249
+ end
250
+ end
251
+ end
252
+
253
+ if MODE_MAXIMIZE_UTIL == @mode
254
+ max_value = @matrix.flatten.max
255
+ @matrix.each_with_index do |row, row_index|
256
+ row.each_with_index do |value, col_index|
257
+ @matrix[row_index][col_index] = max_value - value
258
+ end
259
+ end
260
+ end
261
+
262
+ end
263
+
264
+ end
265
+
266
+ #for backwards compatiability with another library.
267
+ class Munkres::Problem < Munkres
268
+ def solve
269
+ find_pairings
270
+ end
271
+ end
@@ -0,0 +1,234 @@
1
+ require 'rubygems'
2
+ require "test/spec"
3
+ require "munkres"
4
+
5
+ context "An empty Munkres instance" do
6
+
7
+ setup do
8
+ @saved_protected_instance_methods = Munkres.protected_instance_methods
9
+ x = @saved_protected_instance_methods
10
+ Munkres.class_eval { public *x }
11
+ @m = Munkres.new [[0]]
12
+ end
13
+
14
+ teardown do
15
+ x = @saved_protected_instance_methods
16
+ Munkres.class_eval { protected *x }
17
+ end
18
+
19
+ specify "should track a matrix of values" do
20
+ @m.matrix.should == [[0]]
21
+ end
22
+
23
+ specify "should track covered columns" do
24
+ @m.covered_columns.should == []
25
+ end
26
+
27
+ specify "should track covered rows" do
28
+ @m.covered_rows.should == []
29
+ end
30
+
31
+ specify "should track starred zeros" do
32
+ @m.starred_zeros.should == []
33
+ end
34
+
35
+ specify "should track primed zeros" do
36
+ @m.primed_zeros.should == []
37
+ end
38
+ end
39
+
40
+ context "A Munkres solving instance" do
41
+ setup do
42
+ @saved_protected_instance_methods = Munkres.protected_instance_methods
43
+ x = @saved_protected_instance_methods
44
+ Munkres.class_eval { public *x }
45
+ @m = Munkres.new [[1,2,3],[2,4,6],[3,6,9]]
46
+ end
47
+
48
+ teardown do
49
+ x = @saved_protected_instance_methods
50
+ Munkres.class_eval { protected *x }
51
+ end
52
+
53
+ specify "create_zero_in_rows should create a zero in each row of the matrix" do
54
+ @m.create_zero_in_rows
55
+ @m.matrix.should == [[0,1,2],[0,2,4],[0,3,6]]
56
+ end
57
+
58
+ specify "should be able to retrieve any row of the matrix" do
59
+ @m.matrix.row(2).should == [3,6,9]
60
+ end
61
+
62
+ specify "should be able to retrieve any column of the matrix" do
63
+ @m.matrix.column(1).should == [2,4,6]
64
+ end
65
+
66
+ specify "should be able to find the min value of any collection" do
67
+ @m.min_or_zero([3,0,2,1]).should == 0
68
+ end
69
+
70
+ specify "star_zeros should star the first zero in this example" do
71
+ @m.create_zero_in_rows
72
+ @m.star_zeros
73
+ @m.starred_zeros.should == [[0,0]]
74
+ end
75
+
76
+ specify "star_in_column? should indicate a star in a column" do
77
+ @m.starred_zeros << [0,0]
78
+ @m.star_in_column?(0).should.be true
79
+ @m.star_in_column?(1).should.be false
80
+ end
81
+
82
+ specify "star_in_row? should indicate a star in a row" do
83
+ @m.starred_zeros << [0,1]
84
+ @m.star_in_row?(0).should.be true
85
+ @m.star_in_row?(1).should.be false
86
+ end
87
+
88
+ specify "cover_columns_with_stars should cover the first colum" do
89
+ @m.create_zero_in_rows
90
+ @m.star_zeros
91
+ @m.cover_columns_with_stars
92
+ @m.covered_columns.should == [0]
93
+ end
94
+
95
+ specify "should be done if all columns are covered" do
96
+ @m.done?.should.be false
97
+ @m.covered_columns = [0,1,2]
98
+ @m.done?.should.be true
99
+ end
100
+
101
+ specify "should be able to prime the first uncovered zero" do
102
+ @m.matrix[0][1] = 0
103
+ @m.matrix[1][1] = 0
104
+ @m.prime_first_uncovered_zero.should == [0,1]
105
+ @m.primed_zeros.should == [[0,1]]
106
+ end
107
+
108
+ specify "should be able to find the smallest uncovered value" do
109
+ @m.covered_columns = [0,1]
110
+ @m.covered_rows = [0]
111
+ @m.smallest_uncovered_value.should == 6
112
+ end
113
+
114
+ specify "should be able to add a value to covered rows an subtract it from uncovered columns" do
115
+ @m.covered_columns = [1]
116
+ @m.covered_rows = [0,2]
117
+ @m.add_and_subtract_for_step_6(2)
118
+ @m.matrix.should == [[1,4,3],[0,4,4],[3,8,9]]
119
+ end
120
+
121
+ specify "should construct a series of primed and starred zeros and reset things" do
122
+ @m.matrix = [[0,0,1],[0,1,3],[0,2,5]]
123
+ @m.covered_rows = [0]
124
+ @m.starred_zeros = [[0,0]]
125
+ @m.primed_zeros = [[0,1], [1,0]]
126
+ @m.find_better_stars [1,0]
127
+ [[0,1], [1,0]].each {|star| @m.starred_zeros.should.include star }
128
+ @m.primed_zeros.should.be.empty
129
+ @m.covered_rows.should.be.empty
130
+ @m.covered_columns.should.be.empty
131
+ end
132
+
133
+ specify "should return a list of optimal pairings" do
134
+ optimal_pairings = [[0,2],[1,1],[2,0]]
135
+ @m.find_pairings.sort.should == optimal_pairings.sort
136
+ end
137
+
138
+ end
139
+
140
+ context "An oddly shaped Munkres matrix" do
141
+ setup do
142
+ @saved_protected_instance_methods = Munkres.protected_instance_methods
143
+ x = @saved_protected_instance_methods
144
+ Munkres.class_eval { public *x }
145
+ end
146
+
147
+ teardown do
148
+ x = @saved_protected_instance_methods
149
+ Munkres.class_eval { protected *x }
150
+ end
151
+
152
+ specify "should zero pad a tall skinny on initialize" do
153
+ m = Munkres.new [[1,2],[2,4],[3,6]]
154
+
155
+ m.matrix.should == [[1,2,0],[2,4,0],[3,6,0]]
156
+ end
157
+
158
+ specify "should raise an error for wide inputs" do
159
+ should.raise(ArgumentError) { Munkres.new [[1,2,3],[4,5,6]] }
160
+ end
161
+
162
+ specify "should raise an error for irregular inputs" do
163
+ should.raise(ArgumentError) { Munkres.new [[1,2],[1,2,3]] }
164
+ end
165
+
166
+ specify "should raise an error for an empty matrix" do
167
+ should.raise(ArgumentError) { Munkres.new [] }
168
+ end
169
+
170
+ specify "should raise an error for an empty row" do
171
+ should.raise(ArgumentError) { Munkres.new [[],[]] }
172
+ end
173
+ end
174
+
175
+
176
+
177
+ context "Complex examples with know solutions" do
178
+ specify "should solve first example" do
179
+ optimal_pairings = [[0,5],[1,1],[2,2],[3,3],[4,4],[5,0]]
180
+ m = Munkres.new [[3,4,5,6,2,1],[3,0,1,2,3,4],[7,6,0,2,1,1],[4,4,5,0,1,2],[0,1,0,1,0,0],[0,3,2,2,2,0]]
181
+
182
+ m.find_pairings.sort.should == optimal_pairings.sort
183
+
184
+ end
185
+
186
+ specify "should solve a larger example" do
187
+ optimal_pairings = []
188
+ m = Munkres.new [[4,1,2,3],
189
+ [6,9,2,4],
190
+ [1,0,3,7],
191
+ [10,4,6,6]]
192
+ m.find_pairings
193
+ m.total_cost_of_pairing.should == 10
194
+ end
195
+
196
+ specify "should solve a non-square example" do
197
+ optimal_pairings = [[[0, 3], [1, 2], [2, 1], [5, 0]],
198
+ [[0, 1], [1, 2], [2, 0], [5, 3]]]
199
+ m = Munkres.new [[4,1,2,3],
200
+ [6,9,2,4],
201
+ [1,0,3,7],
202
+ [10,4,6,6],
203
+ [5,7,5,9],
204
+ [2,2,14,3]]
205
+ optimal_pairings.sort.should.include m.find_pairings.sort
206
+ m.total_cost_of_pairing.should == 7
207
+ end
208
+
209
+ specify "should solve a very large example" do
210
+ # Profile the code
211
+ #require 'ruby-prof'
212
+
213
+ arr = []
214
+ 100.times do |i|
215
+ arr[i] = []
216
+ 100.times do |j|
217
+ arr[i][j] = rand 20
218
+ end
219
+ end
220
+
221
+ #result = RubyProf.profile do
222
+ m = Munkres.new arr
223
+ m.find_pairings
224
+ #end
225
+
226
+ #printer = RubyProf::GraphHtmlPrinter.new(result)
227
+ #File.open('profile.html', 'w') do |file|
228
+ # printer.print(file, {:min_percent => 30, :print_file => true})
229
+ #end
230
+
231
+
232
+ end
233
+
234
+ end
metadata ADDED
@@ -0,0 +1,59 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: munkres
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Paul Damer and Jim Wood
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-01-26 00:00:00 -05:00
13
+ default_executable:
14
+ dependencies: []
15
+
16
+ description: A ruby implementation of the kuhn-munkres or 'hungarian' algorithm for bipartite matching.
17
+ email: pdamer@gmail.com
18
+ executables: []
19
+
20
+ extensions: []
21
+
22
+ extra_rdoc_files: []
23
+
24
+ files:
25
+ - lib/munkres.rb
26
+ - test/munkres_test.rb
27
+ - README.txt
28
+ - Rakefile
29
+ - History.txt
30
+ has_rdoc: true
31
+ homepage: http://github.com/pdamer/munkres
32
+ licenses: []
33
+
34
+ post_install_message:
35
+ rdoc_options: []
36
+
37
+ require_paths:
38
+ - lib
39
+ required_ruby_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ required_rubygems_version: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: "0"
50
+ version:
51
+ requirements: []
52
+
53
+ rubyforge_project: munkres
54
+ rubygems_version: 1.3.5
55
+ signing_key:
56
+ specification_version: 3
57
+ summary: A Ruby implementation of the Hungarian Algorithm
58
+ test_files:
59
+ - test/munkres_test.rb