|
- #!/usr/bin/env python
- # -*- coding: iso-8859-1 -*-
-
- # Documentation is intended to be processed by Epydoc.
-
- """
- Introduction
- ============
-
- The Munkres module provides an implementation of the Munkres algorithm
- (also called the Hungarian algorithm or the Kuhn-Munkres algorithm),
- useful for solving the Assignment Problem.
-
- Assignment Problem
- ==================
-
- Let *C* be an *n*\ x\ *n* matrix representing the costs of each of *n* workers
- to perform any of *n* jobs. The assignment problem is to assign jobs to
- workers in a way that minimizes the total cost. Since each worker can perform
- only one job and each job can be assigned to only one worker the assignments
- represent an independent set of the matrix *C*.
-
- One way to generate the optimal set is to create all permutations of
- the indexes necessary to traverse the matrix so that no row and column
- are used more than once. For instance, given this matrix (expressed in
- Python)::
-
- matrix = [[5, 9, 1],
- [10, 3, 2],
- [8, 7, 4]]
-
- You could use this code to generate the traversal indexes::
-
- def permute(a, results):
- if len(a) == 1:
- results.insert(len(results), a)
-
- else:
- for i in range(0, len(a)):
- element = a[i]
- a_copy = [a[j] for j in range(0, len(a)) if j != i]
- subresults = []
- permute(a_copy, subresults)
- for subresult in subresults:
- result = [element] + subresult
- results.insert(len(results), result)
-
- results = []
- permute(range(len(matrix)), results) # [0, 1, 2] for a 3x3 matrix
-
- After the call to permute(), the results matrix would look like this::
-
- [[0, 1, 2],
- [0, 2, 1],
- [1, 0, 2],
- [1, 2, 0],
- [2, 0, 1],
- [2, 1, 0]]
-
- You could then use that index matrix to loop over the original cost matrix
- and calculate the smallest cost of the combinations::
-
- n = len(matrix)
- minval = sys.maxint
- for row in range(n):
- cost = 0
- for col in range(n):
- cost += matrix[row][col]
- minval = min(cost, minval)
-
- print minval
-
- While this approach works fine for small matrices, it does not scale. It
- executes in O(*n*!) time: Calculating the permutations for an *n*\ x\ *n*
- matrix requires *n*! operations. For a 12x12 matrix, that's 479,001,600
- traversals. Even if you could manage to perform each traversal in just one
- millisecond, it would still take more than 133 hours to perform the entire
- traversal. A 20x20 matrix would take 2,432,902,008,176,640,000 operations. At
- an optimistic millisecond per operation, that's more than 77 million years.
-
- The Munkres algorithm runs in O(*n*\ ^3) time, rather than O(*n*!). This
- package provides an implementation of that algorithm.
-
- This version is based on
- http://www.public.iastate.edu/~ddoty/HungarianAlgorithm.html.
-
- This version was written for Python by Brian Clapper from the (Ada) algorithm
- at the above web site. (The ``Algorithm::Munkres`` Perl version, in CPAN, was
- clearly adapted from the same web site.)
-
- Usage
- =====
-
- Construct a Munkres object::
-
- from munkres import Munkres
-
- m = Munkres()
-
- Then use it to compute the lowest cost assignment from a cost matrix. Here's
- a sample program::
-
- from munkres import Munkres, print_matrix
-
- matrix = [[5, 9, 1],
- [10, 3, 2],
- [8, 7, 4]]
- m = Munkres()
- indexes = m.compute(matrix)
- print_matrix(matrix, msg='Lowest cost through this matrix:')
- total = 0
- for row, column in indexes:
- value = matrix[row][column]
- total += value
- print '(%d, %d) -> %d' % (row, column, value)
- print 'total cost: %d' % total
-
- Running that program produces::
-
- Lowest cost through this matrix:
- [5, 9, 1]
- [10, 3, 2]
- [8, 7, 4]
- (0, 0) -> 5
- (1, 1) -> 3
- (2, 2) -> 4
- total cost=12
-
- The instantiated Munkres object can be used multiple times on different
- matrices.
-
- Non-square Cost Matrices
- ========================
-
- The Munkres algorithm assumes that the cost matrix is square. However, it's
- possible to use a rectangular matrix if you first pad it with 0 values to make
- it square. This module automatically pads rectangular cost matrices to make
- them square.
-
- Notes:
-
- - The module operates on a *copy* of the caller's matrix, so any padding will
- not be seen by the caller.
- - The cost matrix must be rectangular or square. An irregular matrix will
- *not* work.
-
- Calculating Profit, Rather than Cost
- ====================================
-
- The cost matrix is just that: A cost matrix. The Munkres algorithm finds
- the combination of elements (one from each row and column) that results in
- the smallest cost. It's also possible to use the algorithm to maximize
- profit. To do that, however, you have to convert your profit matrix to a
- cost matrix. The simplest way to do that is to subtract all elements from a
- large value. For example::
-
- from munkres import Munkres, print_matrix
-
- matrix = [[5, 9, 1],
- [10, 3, 2],
- [8, 7, 4]]
- cost_matrix = []
- for row in matrix:
- cost_row = []
- for col in row:
- cost_row += [sys.maxint - col]
- cost_matrix += [cost_row]
-
- m = Munkres()
- indexes = m.compute(cost_matrix)
- print_matrix(matrix, msg='Highest profit through this matrix:')
- total = 0
- for row, column in indexes:
- value = matrix[row][column]
- total += value
- print '(%d, %d) -> %d' % (row, column, value)
-
- print 'total profit=%d' % total
-
- Running that program produces::
-
- Highest profit through this matrix:
- [5, 9, 1]
- [10, 3, 2]
- [8, 7, 4]
- (0, 1) -> 9
- (1, 0) -> 10
- (2, 2) -> 4
- total profit=23
-
- The ``munkres`` module provides a convenience method for creating a cost
- matrix from a profit matrix. Since it doesn't know whether the matrix contains
- floating point numbers, decimals, or integers, you have to provide the
- conversion function; but the convenience method takes care of the actual
- creation of the cost matrix::
-
- import munkres
-
- cost_matrix = munkres.make_cost_matrix(matrix,
- lambda cost: sys.maxint - cost)
-
- So, the above profit-calculation program can be recast as::
-
- from munkres import Munkres, print_matrix, make_cost_matrix
-
- matrix = [[5, 9, 1],
- [10, 3, 2],
- [8, 7, 4]]
- cost_matrix = make_cost_matrix(matrix, lambda cost: sys.maxint - cost)
- m = Munkres()
- indexes = m.compute(cost_matrix)
- print_matrix(matrix, msg='Lowest cost through this matrix:')
- total = 0
- for row, column in indexes:
- value = matrix[row][column]
- total += value
- print '(%d, %d) -> %d' % (row, column, value)
- print 'total profit=%d' % total
-
- References
- ==========
-
- 1. http://www.public.iastate.edu/~ddoty/HungarianAlgorithm.html
-
- 2. Harold W. Kuhn. The Hungarian Method for the assignment problem.
- *Naval Research Logistics Quarterly*, 2:83-97, 1955.
-
- 3. Harold W. Kuhn. Variants of the Hungarian method for assignment
- problems. *Naval Research Logistics Quarterly*, 3: 253-258, 1956.
-
- 4. Munkres, J. Algorithms for the Assignment and Transportation Problems.
- *Journal of the Society of Industrial and Applied Mathematics*,
- 5(1):32-38, March, 1957.
-
- 5. http://en.wikipedia.org/wiki/Hungarian_algorithm
-
- Copyright and License
- =====================
-
- This software is released under a BSD license, adapted from
- <http://opensource.org/licenses/bsd-license.php>
-
- Copyright (c) 2008 Brian M. Clapper
- All rights reserved.
-
- Redistribution and use in source and binary forms, with or without
- modification, are permitted provided that the following conditions are met:
-
- * Redistributions of source code must retain the above copyright notice,
- this list of conditions and the following disclaimer.
-
- * Redistributions in binary form must reproduce the above copyright notice,
- this list of conditions and the following disclaimer in the documentation
- and/or other materials provided with the distribution.
-
- * Neither the name "clapper.org" nor the names of its contributors may be
- used to endorse or promote products derived from this software without
- specific prior written permission.
-
- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
- AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
- IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
- ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
- LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
- CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
- SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
- INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
- CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
- ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
- POSSIBILITY OF SUCH DAMAGE.
- """
-
- __docformat__ = 'restructuredtext'
-
- # ---------------------------------------------------------------------------
- # Imports
- # ---------------------------------------------------------------------------
-
- import sys
-
- # ---------------------------------------------------------------------------
- # Exports
- # ---------------------------------------------------------------------------
-
- __all__ = ['Munkres', 'make_cost_matrix']
-
- # ---------------------------------------------------------------------------
- # Globals
- # ---------------------------------------------------------------------------
-
- # Info about the module
- __version__ = "1.0.5.4"
- __author__ = "Brian Clapper, bmc@clapper.org"
- __url__ = "http://software.clapper.org/munkres/"
- __copyright__ = "(c) 2008 Brian M. Clapper"
- __license__ = "BSD-style license"
-
- # ---------------------------------------------------------------------------
- # Classes
- # ---------------------------------------------------------------------------
-
- class Munkres:
- """
- Calculate the Munkres solution to the classical assignment problem.
- See the module documentation for usage.
- """
-
- def __init__(self):
- """Create a new instance"""
- self.C = None
- self.row_covered = []
- self.col_covered = []
- self.n = 0
- self.Z0_r = 0
- self.Z0_c = 0
- self.marked = None
- self.path = None
-
- def make_cost_matrix(profit_matrix, inversion_function):
- """
- **DEPRECATED**
-
- Please use the module function ``make_cost_matrix()``.
- """
- import munkres
- return munkres.make_cost_matrix(profit_matrix, inversion_function)
-
- make_cost_matrix = staticmethod(make_cost_matrix)
-
- def pad_matrix(self, matrix, pad_value=0):
- """
- Pad a possibly non-square matrix to make it square.
-
- :Parameters:
- matrix : list of lists
- matrix to pad
-
- pad_value : int
- value to use to pad the matrix
-
- :rtype: list of lists
- :return: a new, possibly padded, matrix
- """
- max_columns = 0
- total_rows = len(matrix)
-
- for row in matrix:
- max_columns = max(max_columns, len(row))
-
- total_rows = max(max_columns, total_rows)
-
- new_matrix = []
- for row in matrix:
- row_len = len(row)
- new_row = row[:]
- if total_rows > row_len:
- # Row too short. Pad it.
- new_row += [0] * (total_rows - row_len)
- new_matrix += [new_row]
-
- while len(new_matrix) < total_rows:
- new_matrix += [[0] * total_rows]
-
- return new_matrix
-
- def compute(self, cost_matrix):
- """
- Compute the indexes for the lowest-cost pairings between rows and
- columns in the database. Returns a list of (row, column) tuples
- that can be used to traverse the matrix.
-
- :Parameters:
- cost_matrix : list of lists
- The cost matrix. If this cost matrix is not square, it
- will be padded with zeros, via a call to ``pad_matrix()``.
- (This method does *not* modify the caller's matrix. It
- operates on a copy of the matrix.)
-
- **WARNING**: This code handles square and rectangular
- matrices. It does *not* handle irregular matrices.
-
- :rtype: list
- :return: A list of ``(row, column)`` tuples that describe the lowest
- cost path through the matrix
-
- """
- self.C = self.pad_matrix(cost_matrix)
- self.n = len(self.C)
- self.original_length = len(cost_matrix)
- self.original_width = len(cost_matrix[0])
- self.row_covered = [False for i in range(self.n)]
- self.col_covered = [False for i in range(self.n)]
- self.Z0_r = 0
- self.Z0_c = 0
- self.path = self.__make_matrix(self.n * 2, 0)
- self.marked = self.__make_matrix(self.n, 0)
-
- done = False
- step = 1
-
- steps = { 1 : self.__step1,
- 2 : self.__step2,
- 3 : self.__step3,
- 4 : self.__step4,
- 5 : self.__step5,
- 6 : self.__step6 }
-
- while not done:
- try:
- func = steps[step]
- step = func()
- except KeyError:
- done = True
-
- # Look for the starred columns
- results = []
- for i in range(self.original_length):
- for j in range(self.original_width):
- if self.marked[i][j] == 1:
- results += [(i, j)]
-
- return results
-
- def __copy_matrix(self, matrix):
- """Return an exact copy of the supplied matrix"""
- return copy.deepcopy(matrix)
-
- def __make_matrix(self, n, val):
- """Create an *n*x*n* matrix, populating it with the specific value."""
- matrix = []
- for i in range(n):
- matrix += [[val for j in range(n)]]
- return matrix
-
- def __step1(self):
- """
- For each row of the matrix, find the smallest element and
- subtract it from every element in its row. Go to Step 2.
- """
- C = self.C
- n = self.n
- for i in range(n):
- minval = min(self.C[i])
- # Find the minimum value for this row and subtract that minimum
- # from every element in the row.
- for j in range(n):
- self.C[i][j] -= minval
-
- return 2
-
- def __step2(self):
- """
- Find a zero (Z) in the resulting matrix. If there is no starred
- zero in its row or column, star Z. Repeat for each element in the
- matrix. Go to Step 3.
- """
- n = self.n
- for i in range(n):
- for j in range(n):
- if (self.C[i][j] == 0) and \
- (not self.col_covered[j]) and \
- (not self.row_covered[i]):
- self.marked[i][j] = 1
- self.col_covered[j] = True
- self.row_covered[i] = True
-
- self.__clear_covers()
- return 3
-
- def __step3(self):
- """
- Cover each column containing a starred zero. If K columns are
- covered, the starred zeros describe a complete set of unique
- assignments. In this case, Go to DONE, otherwise, Go to Step 4.
- """
- n = self.n
- count = 0
- for i in range(n):
- for j in range(n):
- if self.marked[i][j] == 1:
- self.col_covered[j] = True
- count += 1
-
- if count >= n:
- step = 7 # done
- else:
- step = 4
-
- return step
-
- def __step4(self):
- """
- Find a noncovered zero and prime it. If there is no starred zero
- in the row containing this primed zero, Go to Step 5. Otherwise,
- cover this row and uncover the column containing the starred
- zero. Continue in this manner until there are no uncovered zeros
- left. Save the smallest uncovered value and Go to Step 6.
- """
- step = 0
- done = False
- row = -1
- col = -1
- star_col = -1
- while not done:
- (row, col) = self.__find_a_zero()
- if row < 0:
- done = True
- step = 6
- else:
- self.marked[row][col] = 2
- star_col = self.__find_star_in_row(row)
- if star_col >= 0:
- col = star_col
- self.row_covered[row] = True
- self.col_covered[col] = False
- else:
- done = True
- self.Z0_r = row
- self.Z0_c = col
- step = 5
-
- return step
-
- def __step5(self):
- """
- Construct a series of alternating primed and starred zeros as
- follows. Let Z0 represent the uncovered primed zero found in Step 4.
- Let Z1 denote the starred zero in the column of Z0 (if any).
- Let Z2 denote the primed zero in the row of Z1 (there will always
- be one). Continue until the series terminates at a primed zero
- that has no starred zero in its column. Unstar each starred zero
- of the series, star each primed zero of the series, erase all
- primes and uncover every line in the matrix. Return to Step 3
- """
- count = 0
- path = self.path
- path[count][0] = self.Z0_r
- path[count][1] = self.Z0_c
- done = False
- while not done:
- row = self.__find_star_in_col(path[count][1])
- if row >= 0:
- count += 1
- path[count][0] = row
- path[count][1] = path[count-1][1]
- else:
- done = True
-
- if not done:
- col = self.__find_prime_in_row(path[count][0])
- count += 1
- path[count][0] = path[count-1][0]
- path[count][1] = col
-
- self.__convert_path(path, count)
- self.__clear_covers()
- self.__erase_primes()
- return 3
-
- def __step6(self):
- """
- Add the value found in Step 4 to every element of each covered
- row, and subtract it from every element of each uncovered column.
- Return to Step 4 without altering any stars, primes, or covered
- lines.
- """
- minval = self.__find_smallest()
- for i in range(self.n):
- for j in range(self.n):
- if self.row_covered[i]:
- self.C[i][j] += minval
- if not self.col_covered[j]:
- self.C[i][j] -= minval
- return 4
-
- def __find_smallest(self):
- """Find the smallest uncovered value in the matrix."""
- minval = sys.maxint
- for i in range(self.n):
- for j in range(self.n):
- if (not self.row_covered[i]) and (not self.col_covered[j]):
- if minval > self.C[i][j]:
- minval = self.C[i][j]
- return minval
-
- def __find_a_zero(self):
- """Find the first uncovered element with value 0"""
- row = -1
- col = -1
- i = 0
- n = self.n
- done = False
-
- while not done:
- j = 0
- while True:
- if (self.C[i][j] == 0) and \
- (not self.row_covered[i]) and \
- (not self.col_covered[j]):
- row = i
- col = j
- done = True
- j += 1
- if j >= n:
- break
- i += 1
- if i >= n:
- done = True
-
- return (row, col)
-
- def __find_star_in_row(self, row):
- """
- Find the first starred element in the specified row. Returns
- the column index, or -1 if no starred element was found.
- """
- col = -1
- for j in range(self.n):
- if self.marked[row][j] == 1:
- col = j
- break
-
- return col
-
- def __find_star_in_col(self, col):
- """
- Find the first starred element in the specified row. Returns
- the row index, or -1 if no starred element was found.
- """
- row = -1
- for i in range(self.n):
- if self.marked[i][col] == 1:
- row = i
- break
-
- return row
-
- def __find_prime_in_row(self, row):
- """
- Find the first prime element in the specified row. Returns
- the column index, or -1 if no starred element was found.
- """
- col = -1
- for j in range(self.n):
- if self.marked[row][j] == 2:
- col = j
- break
-
- return col
-
- def __convert_path(self, path, count):
- for i in range(count+1):
- if self.marked[path[i][0]][path[i][1]] == 1:
- self.marked[path[i][0]][path[i][1]] = 0
- else:
- self.marked[path[i][0]][path[i][1]] = 1
-
- def __clear_covers(self):
- """Clear all covered matrix cells"""
- for i in range(self.n):
- self.row_covered[i] = False
- self.col_covered[i] = False
-
- def __erase_primes(self):
- """Erase all prime markings"""
- for i in range(self.n):
- for j in range(self.n):
- if self.marked[i][j] == 2:
- self.marked[i][j] = 0
-
- # ---------------------------------------------------------------------------
- # Functions
- # ---------------------------------------------------------------------------
-
- def make_cost_matrix(profit_matrix, inversion_function):
- """
- Create a cost matrix from a profit matrix by calling
- 'inversion_function' to invert each value. The inversion
- function must take one numeric argument (of any type) and return
- another numeric argument which is presumed to be the cost inverse
- of the original profit.
-
- This is a static method. Call it like this:
-
- .. python::
-
- cost_matrix = Munkres.make_cost_matrix(matrix, inversion_func)
-
- For example:
-
- .. python::
-
- cost_matrix = Munkres.make_cost_matrix(matrix, lambda x : sys.maxint - x)
-
- :Parameters:
- profit_matrix : list of lists
- The matrix to convert from a profit to a cost matrix
-
- inversion_function : function
- The function to use to invert each entry in the profit matrix
-
- :rtype: list of lists
- :return: The converted matrix
- """
- cost_matrix = []
- for row in profit_matrix:
- cost_matrix.append([inversion_function(value) for value in row])
- return cost_matrix
-
- def print_matrix(matrix, msg=None):
- """
- Convenience function: Displays the contents of a matrix of integers.
-
- :Parameters:
- matrix : list of lists
- Matrix to print
-
- msg : str
- Optional message to print before displaying the matrix
- """
- import math
-
- if msg is not None:
- print msg
-
- # Calculate the appropriate format width.
- width = 0
- for row in matrix:
- for val in row:
- width = max(width, int(math.log10(val)) + 1)
-
- # Make the format string
- format = '%%%dd' % width
-
- # Print the matrix
- for row in matrix:
- sep = '['
- for val in row:
- sys.stdout.write(sep + format % val)
- sep = ', '
- sys.stdout.write(']\n')
-
- # ---------------------------------------------------------------------------
- # Main
- # ---------------------------------------------------------------------------
-
- if __name__ == '__main__':
-
-
- matrices = [
- # Square
- ([[400, 150, 400],
- [400, 450, 600],
- [300, 225, 300]],
- 850 # expected cost
- ),
-
- # Rectangular variant
- ([[400, 150, 400, 1],
- [400, 450, 600, 2],
- [300, 225, 300, 3]],
- 452 # expected cost
- ),
-
- # Square
- ([[10, 10, 8],
- [ 9, 8, 1],
- [ 9, 7, 4]],
- 18
- ),
-
- # Rectangular variant
- ([[10, 10, 8, 11],
- [ 9, 8, 1, 1],
- [ 9, 7, 4, 10]],
- 15
- ),
- ]
-
- m = Munkres()
- for cost_matrix, expected_total in matrices:
- print_matrix(cost_matrix, msg='cost matrix')
- indexes = m.compute(cost_matrix)
- total_cost = 0
- for r, c in indexes:
- x = cost_matrix[r][c]
- total_cost += x
- print '(%d, %d) -> %d' % (r, c, x)
- print 'lowest cost=%d' % total_cost
- assert expected_total == total_cost
|