munkres 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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