mirror of
https://github.com/rembo10/headphones.git
synced 2026-07-22 17:04:01 +01:00
munkres: 1.0.6 -> 1.1.4
This commit is contained in:
+180
-362
@@ -1,8 +1,3 @@
|
|||||||
#!/usr/bin/env python
|
|
||||||
# -*- coding: iso-8859-1 -*-
|
|
||||||
|
|
||||||
# Documentation is intended to be processed by Epydoc.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Introduction
|
Introduction
|
||||||
============
|
============
|
||||||
@@ -11,266 +6,10 @@ The Munkres module provides an implementation of the Munkres algorithm
|
|||||||
(also called the Hungarian algorithm or the Kuhn-Munkres algorithm),
|
(also called the Hungarian algorithm or the Kuhn-Munkres algorithm),
|
||||||
useful for solving the Assignment Problem.
|
useful for solving the Assignment Problem.
|
||||||
|
|
||||||
Assignment Problem
|
For complete usage documentation, see: https://software.clapper.org/munkres/
|
||||||
==================
|
|
||||||
|
|
||||||
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.maxsize
|
|
||||||
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.maxsize - 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.maxsize - 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.maxsize - 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'
|
__docformat__ = 'markdown'
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Imports
|
# Imports
|
||||||
@@ -278,23 +17,43 @@ __docformat__ = 'restructuredtext'
|
|||||||
|
|
||||||
import sys
|
import sys
|
||||||
import copy
|
import copy
|
||||||
|
from typing import Union, NewType, Sequence, Tuple, Optional, Callable
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Exports
|
# Exports
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
__all__ = ['Munkres', 'make_cost_matrix']
|
__all__ = ['Munkres', 'make_cost_matrix', 'DISALLOWED']
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Globals
|
# Globals
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
AnyNum = NewType('AnyNum', Union[int, float])
|
||||||
|
Matrix = NewType('Matrix', Sequence[Sequence[AnyNum]])
|
||||||
|
|
||||||
# Info about the module
|
# Info about the module
|
||||||
__version__ = "1.0.6"
|
__version__ = "1.1.4"
|
||||||
__author__ = "Brian Clapper, bmc@clapper.org"
|
__author__ = "Brian Clapper, bmc@clapper.org"
|
||||||
__url__ = "http://software.clapper.org/munkres/"
|
__url__ = "https://software.clapper.org/munkres/"
|
||||||
__copyright__ = "(c) 2008 Brian M. Clapper"
|
__copyright__ = "(c) 2008-2020 Brian M. Clapper"
|
||||||
__license__ = "BSD-style license"
|
__license__ = "Apache Software License"
|
||||||
|
|
||||||
|
# Constants
|
||||||
|
class DISALLOWED_OBJ(object):
|
||||||
|
pass
|
||||||
|
DISALLOWED = DISALLOWED_OBJ()
|
||||||
|
DISALLOWED_PRINTVAL = "D"
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Exceptions
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
class UnsolvableMatrix(Exception):
|
||||||
|
"""
|
||||||
|
Exception raised for unsolvable matrices
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Classes
|
# Classes
|
||||||
@@ -317,30 +76,18 @@ class Munkres:
|
|||||||
self.marked = None
|
self.marked = None
|
||||||
self.path = None
|
self.path = None
|
||||||
|
|
||||||
def make_cost_matrix(profit_matrix, inversion_function):
|
def pad_matrix(self, matrix: Matrix, pad_value: int=0) -> Matrix:
|
||||||
"""
|
|
||||||
**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.
|
Pad a possibly non-square matrix to make it square.
|
||||||
|
|
||||||
:Parameters:
|
**Parameters**
|
||||||
matrix : list of lists
|
|
||||||
matrix to pad
|
|
||||||
|
|
||||||
pad_value : int
|
- `matrix` (list of lists of numbers): matrix to pad
|
||||||
value to use to pad the matrix
|
- `pad_value` (`int`): value to use to pad the matrix
|
||||||
|
|
||||||
:rtype: list of lists
|
**Returns**
|
||||||
:return: a new, possibly padded, matrix
|
|
||||||
|
a new, possibly padded, matrix
|
||||||
"""
|
"""
|
||||||
max_columns = 0
|
max_columns = 0
|
||||||
total_rows = len(matrix)
|
total_rows = len(matrix)
|
||||||
@@ -356,34 +103,35 @@ class Munkres:
|
|||||||
new_row = row[:]
|
new_row = row[:]
|
||||||
if total_rows > row_len:
|
if total_rows > row_len:
|
||||||
# Row too short. Pad it.
|
# Row too short. Pad it.
|
||||||
new_row += [0] * (total_rows - row_len)
|
new_row += [pad_value] * (total_rows - row_len)
|
||||||
new_matrix += [new_row]
|
new_matrix += [new_row]
|
||||||
|
|
||||||
while len(new_matrix) < total_rows:
|
while len(new_matrix) < total_rows:
|
||||||
new_matrix += [[0] * total_rows]
|
new_matrix += [[pad_value] * total_rows]
|
||||||
|
|
||||||
return new_matrix
|
return new_matrix
|
||||||
|
|
||||||
def compute(self, cost_matrix):
|
def compute(self, cost_matrix: Matrix) -> Sequence[Tuple[int, int]]:
|
||||||
"""
|
"""
|
||||||
Compute the indexes for the lowest-cost pairings between rows and
|
Compute the indexes for the lowest-cost pairings between rows and
|
||||||
columns in the database. Returns a list of (row, column) tuples
|
columns in the database. Returns a list of `(row, column)` tuples
|
||||||
that can be used to traverse the matrix.
|
that can be used to traverse the matrix.
|
||||||
|
|
||||||
:Parameters:
|
**WARNING**: This code handles square and rectangular matrices. It
|
||||||
cost_matrix : list of lists
|
does *not* handle irregular matrices.
|
||||||
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
|
**Parameters**
|
||||||
matrices. It does *not* handle irregular matrices.
|
|
||||||
|
|
||||||
:rtype: list
|
- `cost_matrix` (list of lists of numbers): The cost matrix. If this
|
||||||
:return: A list of ``(row, column)`` tuples that describe the lowest
|
cost matrix is not square, it will be padded with zeros, via a call
|
||||||
cost path through the matrix
|
to `pad_matrix()`. (This method does *not* modify the caller's
|
||||||
|
matrix. It operates on a copy of the matrix.)
|
||||||
|
|
||||||
|
|
||||||
|
**Returns**
|
||||||
|
|
||||||
|
A list of `(row, column)` tuples that describe the lowest cost path
|
||||||
|
through the matrix
|
||||||
"""
|
"""
|
||||||
self.C = self.pad_matrix(cost_matrix)
|
self.C = self.pad_matrix(cost_matrix)
|
||||||
self.n = len(self.C)
|
self.n = len(self.C)
|
||||||
@@ -422,18 +170,18 @@ class Munkres:
|
|||||||
|
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def __copy_matrix(self, matrix):
|
def __copy_matrix(self, matrix: Matrix) -> Matrix:
|
||||||
"""Return an exact copy of the supplied matrix"""
|
"""Return an exact copy of the supplied matrix"""
|
||||||
return copy.deepcopy(matrix)
|
return copy.deepcopy(matrix)
|
||||||
|
|
||||||
def __make_matrix(self, n, val):
|
def __make_matrix(self, n: int, val: AnyNum) -> Matrix:
|
||||||
"""Create an *n*x*n* matrix, populating it with the specific value."""
|
"""Create an *n*x*n* matrix, populating it with the specific value."""
|
||||||
matrix = []
|
matrix = []
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
matrix += [[val for j in range(n)]]
|
matrix += [[val for j in range(n)]]
|
||||||
return matrix
|
return matrix
|
||||||
|
|
||||||
def __step1(self):
|
def __step1(self) -> int:
|
||||||
"""
|
"""
|
||||||
For each row of the matrix, find the smallest element and
|
For each row of the matrix, find the smallest element and
|
||||||
subtract it from every element in its row. Go to Step 2.
|
subtract it from every element in its row. Go to Step 2.
|
||||||
@@ -441,15 +189,22 @@ class Munkres:
|
|||||||
C = self.C
|
C = self.C
|
||||||
n = self.n
|
n = self.n
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
minval = min(self.C[i])
|
vals = [x for x in self.C[i] if x is not DISALLOWED]
|
||||||
|
if len(vals) == 0:
|
||||||
|
# All values in this row are DISALLOWED. This matrix is
|
||||||
|
# unsolvable.
|
||||||
|
raise UnsolvableMatrix(
|
||||||
|
"Row {0} is entirely DISALLOWED.".format(i)
|
||||||
|
)
|
||||||
|
minval = min(vals)
|
||||||
# Find the minimum value for this row and subtract that minimum
|
# Find the minimum value for this row and subtract that minimum
|
||||||
# from every element in the row.
|
# from every element in the row.
|
||||||
for j in range(n):
|
for j in range(n):
|
||||||
self.C[i][j] -= minval
|
if self.C[i][j] is not DISALLOWED:
|
||||||
|
self.C[i][j] -= minval
|
||||||
return 2
|
return 2
|
||||||
|
|
||||||
def __step2(self):
|
def __step2(self) -> int:
|
||||||
"""
|
"""
|
||||||
Find a zero (Z) in the resulting matrix. If there is no starred
|
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
|
zero in its row or column, star Z. Repeat for each element in the
|
||||||
@@ -464,11 +219,12 @@ class Munkres:
|
|||||||
self.marked[i][j] = 1
|
self.marked[i][j] = 1
|
||||||
self.col_covered[j] = True
|
self.col_covered[j] = True
|
||||||
self.row_covered[i] = True
|
self.row_covered[i] = True
|
||||||
|
break
|
||||||
|
|
||||||
self.__clear_covers()
|
self.__clear_covers()
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
def __step3(self):
|
def __step3(self) -> int:
|
||||||
"""
|
"""
|
||||||
Cover each column containing a starred zero. If K columns are
|
Cover each column containing a starred zero. If K columns are
|
||||||
covered, the starred zeros describe a complete set of unique
|
covered, the starred zeros describe a complete set of unique
|
||||||
@@ -478,7 +234,7 @@ class Munkres:
|
|||||||
count = 0
|
count = 0
|
||||||
for i in range(n):
|
for i in range(n):
|
||||||
for j in range(n):
|
for j in range(n):
|
||||||
if self.marked[i][j] == 1:
|
if self.marked[i][j] == 1 and not self.col_covered[j]:
|
||||||
self.col_covered[j] = True
|
self.col_covered[j] = True
|
||||||
count += 1
|
count += 1
|
||||||
|
|
||||||
@@ -489,7 +245,7 @@ class Munkres:
|
|||||||
|
|
||||||
return step
|
return step
|
||||||
|
|
||||||
def __step4(self):
|
def __step4(self) -> int:
|
||||||
"""
|
"""
|
||||||
Find a noncovered zero and prime it. If there is no starred zero
|
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,
|
in the row containing this primed zero, Go to Step 5. Otherwise,
|
||||||
@@ -499,11 +255,11 @@ class Munkres:
|
|||||||
"""
|
"""
|
||||||
step = 0
|
step = 0
|
||||||
done = False
|
done = False
|
||||||
row = -1
|
row = 0
|
||||||
col = -1
|
col = 0
|
||||||
star_col = -1
|
star_col = -1
|
||||||
while not done:
|
while not done:
|
||||||
(row, col) = self.__find_a_zero()
|
(row, col) = self.__find_a_zero(row, col)
|
||||||
if row < 0:
|
if row < 0:
|
||||||
done = True
|
done = True
|
||||||
step = 6
|
step = 6
|
||||||
@@ -522,7 +278,7 @@ class Munkres:
|
|||||||
|
|
||||||
return step
|
return step
|
||||||
|
|
||||||
def __step5(self):
|
def __step5(self) -> int:
|
||||||
"""
|
"""
|
||||||
Construct a series of alternating primed and starred zeros as
|
Construct a series of alternating primed and starred zeros as
|
||||||
follows. Let Z0 represent the uncovered primed zero found in Step 4.
|
follows. Let Z0 represent the uncovered primed zero found in Step 4.
|
||||||
@@ -558,7 +314,7 @@ class Munkres:
|
|||||||
self.__erase_primes()
|
self.__erase_primes()
|
||||||
return 3
|
return 3
|
||||||
|
|
||||||
def __step6(self):
|
def __step6(self) -> int:
|
||||||
"""
|
"""
|
||||||
Add the value found in Step 4 to every element of each covered
|
Add the value found in Step 4 to every element of each covered
|
||||||
row, and subtract it from every element of each uncovered column.
|
row, and subtract it from every element of each uncovered column.
|
||||||
@@ -566,34 +322,44 @@ class Munkres:
|
|||||||
lines.
|
lines.
|
||||||
"""
|
"""
|
||||||
minval = self.__find_smallest()
|
minval = self.__find_smallest()
|
||||||
|
events = 0 # track actual changes to matrix
|
||||||
for i in range(self.n):
|
for i in range(self.n):
|
||||||
for j in range(self.n):
|
for j in range(self.n):
|
||||||
|
if self.C[i][j] is DISALLOWED:
|
||||||
|
continue
|
||||||
if self.row_covered[i]:
|
if self.row_covered[i]:
|
||||||
self.C[i][j] += minval
|
self.C[i][j] += minval
|
||||||
|
events += 1
|
||||||
if not self.col_covered[j]:
|
if not self.col_covered[j]:
|
||||||
self.C[i][j] -= minval
|
self.C[i][j] -= minval
|
||||||
|
events += 1
|
||||||
|
if self.row_covered[i] and not self.col_covered[j]:
|
||||||
|
events -= 2 # change reversed, no real difference
|
||||||
|
if (events == 0):
|
||||||
|
raise UnsolvableMatrix("Matrix cannot be solved!")
|
||||||
return 4
|
return 4
|
||||||
|
|
||||||
def __find_smallest(self):
|
def __find_smallest(self) -> AnyNum:
|
||||||
"""Find the smallest uncovered value in the matrix."""
|
"""Find the smallest uncovered value in the matrix."""
|
||||||
minval = sys.maxsize
|
minval = sys.maxsize
|
||||||
for i in range(self.n):
|
for i in range(self.n):
|
||||||
for j in range(self.n):
|
for j in range(self.n):
|
||||||
if (not self.row_covered[i]) and (not self.col_covered[j]):
|
if (not self.row_covered[i]) and (not self.col_covered[j]):
|
||||||
if minval > self.C[i][j]:
|
if self.C[i][j] is not DISALLOWED and minval > self.C[i][j]:
|
||||||
minval = self.C[i][j]
|
minval = self.C[i][j]
|
||||||
return minval
|
return minval
|
||||||
|
|
||||||
def __find_a_zero(self):
|
|
||||||
|
def __find_a_zero(self, i0: int = 0, j0: int = 0) -> Tuple[int, int]:
|
||||||
"""Find the first uncovered element with value 0"""
|
"""Find the first uncovered element with value 0"""
|
||||||
row = -1
|
row = -1
|
||||||
col = -1
|
col = -1
|
||||||
i = 0
|
i = i0
|
||||||
n = self.n
|
n = self.n
|
||||||
done = False
|
done = False
|
||||||
|
|
||||||
while not done:
|
while not done:
|
||||||
j = 0
|
j = j0
|
||||||
while True:
|
while True:
|
||||||
if (self.C[i][j] == 0) and \
|
if (self.C[i][j] == 0) and \
|
||||||
(not self.row_covered[i]) and \
|
(not self.row_covered[i]) and \
|
||||||
@@ -601,16 +367,16 @@ class Munkres:
|
|||||||
row = i
|
row = i
|
||||||
col = j
|
col = j
|
||||||
done = True
|
done = True
|
||||||
j += 1
|
j = (j + 1) % n
|
||||||
if j >= n:
|
if j == j0:
|
||||||
break
|
break
|
||||||
i += 1
|
i = (i + 1) % n
|
||||||
if i >= n:
|
if i == i0:
|
||||||
done = True
|
done = True
|
||||||
|
|
||||||
return (row, col)
|
return (row, col)
|
||||||
|
|
||||||
def __find_star_in_row(self, row):
|
def __find_star_in_row(self, row: Sequence[AnyNum]) -> int:
|
||||||
"""
|
"""
|
||||||
Find the first starred element in the specified row. Returns
|
Find the first starred element in the specified row. Returns
|
||||||
the column index, or -1 if no starred element was found.
|
the column index, or -1 if no starred element was found.
|
||||||
@@ -623,7 +389,7 @@ class Munkres:
|
|||||||
|
|
||||||
return col
|
return col
|
||||||
|
|
||||||
def __find_star_in_col(self, col):
|
def __find_star_in_col(self, col: Sequence[AnyNum]) -> int:
|
||||||
"""
|
"""
|
||||||
Find the first starred element in the specified row. Returns
|
Find the first starred element in the specified row. Returns
|
||||||
the row index, or -1 if no starred element was found.
|
the row index, or -1 if no starred element was found.
|
||||||
@@ -636,7 +402,7 @@ class Munkres:
|
|||||||
|
|
||||||
return row
|
return row
|
||||||
|
|
||||||
def __find_prime_in_row(self, row):
|
def __find_prime_in_row(self, row) -> int:
|
||||||
"""
|
"""
|
||||||
Find the first prime element in the specified row. Returns
|
Find the first prime element in the specified row. Returns
|
||||||
the column index, or -1 if no starred element was found.
|
the column index, or -1 if no starred element was found.
|
||||||
@@ -649,20 +415,22 @@ class Munkres:
|
|||||||
|
|
||||||
return col
|
return col
|
||||||
|
|
||||||
def __convert_path(self, path, count):
|
def __convert_path(self,
|
||||||
|
path: Sequence[Sequence[int]],
|
||||||
|
count: int) -> None:
|
||||||
for i in range(count+1):
|
for i in range(count+1):
|
||||||
if self.marked[path[i][0]][path[i][1]] == 1:
|
if self.marked[path[i][0]][path[i][1]] == 1:
|
||||||
self.marked[path[i][0]][path[i][1]] = 0
|
self.marked[path[i][0]][path[i][1]] = 0
|
||||||
else:
|
else:
|
||||||
self.marked[path[i][0]][path[i][1]] = 1
|
self.marked[path[i][0]][path[i][1]] = 1
|
||||||
|
|
||||||
def __clear_covers(self):
|
def __clear_covers(self) -> None:
|
||||||
"""Clear all covered matrix cells"""
|
"""Clear all covered matrix cells"""
|
||||||
for i in range(self.n):
|
for i in range(self.n):
|
||||||
self.row_covered[i] = False
|
self.row_covered[i] = False
|
||||||
self.col_covered[i] = False
|
self.col_covered[i] = False
|
||||||
|
|
||||||
def __erase_primes(self):
|
def __erase_primes(self) -> None:
|
||||||
"""Erase all prime markings"""
|
"""Erase all prime markings"""
|
||||||
for i in range(self.n):
|
for i in range(self.n):
|
||||||
for j in range(self.n):
|
for j in range(self.n):
|
||||||
@@ -673,51 +441,56 @@ class Munkres:
|
|||||||
# Functions
|
# Functions
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def make_cost_matrix(profit_matrix, inversion_function):
|
def make_cost_matrix(
|
||||||
|
profit_matrix: Matrix,
|
||||||
|
inversion_function: Optional[Callable[[AnyNum], AnyNum]] = None
|
||||||
|
) -> Matrix:
|
||||||
"""
|
"""
|
||||||
Create a cost matrix from a profit matrix by calling
|
Create a cost matrix from a profit matrix by calling `inversion_function()`
|
||||||
'inversion_function' to invert each value. The inversion
|
to invert each value. The inversion function must take one numeric argument
|
||||||
function must take one numeric argument (of any type) and return
|
(of any type) and return another numeric argument which is presumed to be
|
||||||
another numeric argument which is presumed to be the cost inverse
|
the cost inverse of the original profit value. If the inversion function
|
||||||
of the original profit.
|
is not provided, a given cell's inverted value is calculated as
|
||||||
|
`max(matrix) - value`.
|
||||||
|
|
||||||
This is a static method. Call it like this:
|
This is a static method. Call it like this:
|
||||||
|
|
||||||
.. python::
|
from munkres import Munkres
|
||||||
|
|
||||||
cost_matrix = Munkres.make_cost_matrix(matrix, inversion_func)
|
cost_matrix = Munkres.make_cost_matrix(matrix, inversion_func)
|
||||||
|
|
||||||
For example:
|
For example:
|
||||||
|
|
||||||
.. python::
|
from munkres import Munkres
|
||||||
|
|
||||||
cost_matrix = Munkres.make_cost_matrix(matrix, lambda x : sys.maxsize - x)
|
cost_matrix = Munkres.make_cost_matrix(matrix, lambda x : sys.maxsize - x)
|
||||||
|
|
||||||
:Parameters:
|
**Parameters**
|
||||||
profit_matrix : list of lists
|
|
||||||
The matrix to convert from a profit to a cost matrix
|
|
||||||
|
|
||||||
inversion_function : function
|
- `profit_matrix` (list of lists of numbers): The matrix to convert from
|
||||||
The function to use to invert each entry in the profit matrix
|
profit to cost values.
|
||||||
|
- `inversion_function` (`function`): The function to use to invert each
|
||||||
|
entry in the profit matrix.
|
||||||
|
|
||||||
:rtype: list of lists
|
**Returns**
|
||||||
:return: The converted matrix
|
|
||||||
|
A new matrix representing the inversion of `profix_matrix`.
|
||||||
"""
|
"""
|
||||||
|
if not inversion_function:
|
||||||
|
maximum = max(max(row) for row in profit_matrix)
|
||||||
|
inversion_function = lambda x: maximum - x
|
||||||
|
|
||||||
cost_matrix = []
|
cost_matrix = []
|
||||||
for row in profit_matrix:
|
for row in profit_matrix:
|
||||||
cost_matrix.append([inversion_function(value) for value in row])
|
cost_matrix.append([inversion_function(value) for value in row])
|
||||||
return cost_matrix
|
return cost_matrix
|
||||||
|
|
||||||
def print_matrix(matrix, msg=None):
|
def print_matrix(matrix: Matrix, msg: Optional[str] = None) -> None:
|
||||||
"""
|
"""
|
||||||
Convenience function: Displays the contents of a matrix of integers.
|
Convenience function: Displays the contents of a matrix.
|
||||||
|
|
||||||
:Parameters:
|
**Parameters**
|
||||||
matrix : list of lists
|
|
||||||
Matrix to print
|
|
||||||
|
|
||||||
msg : str
|
- `matrix` (list of lists of numbers): The matrix to print
|
||||||
Optional message to print before displaying the matrix
|
- `msg` (`str`): Optional message to print before displaying the matrix
|
||||||
"""
|
"""
|
||||||
import math
|
import math
|
||||||
|
|
||||||
@@ -728,16 +501,21 @@ def print_matrix(matrix, msg=None):
|
|||||||
width = 0
|
width = 0
|
||||||
for row in matrix:
|
for row in matrix:
|
||||||
for val in row:
|
for val in row:
|
||||||
width = max(width, int(math.log10(val)) + 1)
|
if val is DISALLOWED:
|
||||||
|
val = DISALLOWED_PRINTVAL
|
||||||
|
width = max(width, len(str(val)))
|
||||||
|
|
||||||
# Make the format string
|
# Make the format string
|
||||||
format = '%%%dd' % width
|
format = ('%%%d' % width)
|
||||||
|
|
||||||
# Print the matrix
|
# Print the matrix
|
||||||
for row in matrix:
|
for row in matrix:
|
||||||
sep = '['
|
sep = '['
|
||||||
for val in row:
|
for val in row:
|
||||||
sys.stdout.write(sep + format % val)
|
if val is DISALLOWED:
|
||||||
|
val = DISALLOWED_PRINTVAL
|
||||||
|
formatted = ((format + 's') % val)
|
||||||
|
sys.stdout.write(sep + formatted)
|
||||||
sep = ', '
|
sep = ', '
|
||||||
sys.stdout.write(']\n')
|
sys.stdout.write(']\n')
|
||||||
|
|
||||||
@@ -767,11 +545,51 @@ if __name__ == '__main__':
|
|||||||
[9, 7, 4]],
|
[9, 7, 4]],
|
||||||
18),
|
18),
|
||||||
|
|
||||||
|
# Square variant with floating point value
|
||||||
|
([[10.1, 10.2, 8.3],
|
||||||
|
[9.4, 8.5, 1.6],
|
||||||
|
[9.7, 7.8, 4.9]],
|
||||||
|
19.5),
|
||||||
|
|
||||||
# Rectangular variant
|
# Rectangular variant
|
||||||
([[10, 10, 8, 11],
|
([[10, 10, 8, 11],
|
||||||
[9, 8, 1, 1],
|
[9, 8, 1, 1],
|
||||||
[9, 7, 4, 10]],
|
[9, 7, 4, 10]],
|
||||||
15)]
|
15),
|
||||||
|
|
||||||
|
# Rectangular variant with floating point value
|
||||||
|
([[10.01, 10.02, 8.03, 11.04],
|
||||||
|
[9.05, 8.06, 1.07, 1.08],
|
||||||
|
[9.09, 7.1, 4.11, 10.12]],
|
||||||
|
15.2),
|
||||||
|
|
||||||
|
# Rectangular with DISALLOWED
|
||||||
|
([[4, 5, 6, DISALLOWED],
|
||||||
|
[1, 9, 12, 11],
|
||||||
|
[DISALLOWED, 5, 4, DISALLOWED],
|
||||||
|
[12, 12, 12, 10]],
|
||||||
|
20),
|
||||||
|
|
||||||
|
# Rectangular variant with DISALLOWED and floating point value
|
||||||
|
([[4.001, 5.002, 6.003, DISALLOWED],
|
||||||
|
[1.004, 9.005, 12.006, 11.007],
|
||||||
|
[DISALLOWED, 5.008, 4.009, DISALLOWED],
|
||||||
|
[12.01, 12.011, 12.012, 10.013]],
|
||||||
|
20.028),
|
||||||
|
|
||||||
|
# DISALLOWED to force pairings
|
||||||
|
([[1, DISALLOWED, DISALLOWED, DISALLOWED],
|
||||||
|
[DISALLOWED, 2, DISALLOWED, DISALLOWED],
|
||||||
|
[DISALLOWED, DISALLOWED, 3, DISALLOWED],
|
||||||
|
[DISALLOWED, DISALLOWED, DISALLOWED, 4]],
|
||||||
|
10),
|
||||||
|
|
||||||
|
# DISALLOWED to force pairings with floating point value
|
||||||
|
([[1.1, DISALLOWED, DISALLOWED, DISALLOWED],
|
||||||
|
[DISALLOWED, 2.2, DISALLOWED, DISALLOWED],
|
||||||
|
[DISALLOWED, DISALLOWED, 3.3, DISALLOWED],
|
||||||
|
[DISALLOWED, DISALLOWED, DISALLOWED, 4.4]],
|
||||||
|
11.0)]
|
||||||
|
|
||||||
m = Munkres()
|
m = Munkres()
|
||||||
for cost_matrix, expected_total in matrices:
|
for cost_matrix, expected_total in matrices:
|
||||||
@@ -781,6 +599,6 @@ if __name__ == '__main__':
|
|||||||
for r, c in indexes:
|
for r, c in indexes:
|
||||||
x = cost_matrix[r][c]
|
x = cost_matrix[r][c]
|
||||||
total_cost += x
|
total_cost += x
|
||||||
print(('(%d, %d) -> %d' % (r, c, x)))
|
print(('(%d, %d) -> %s' % (r, c, x)))
|
||||||
print(('lowest cost=%d' % total_cost))
|
print(('lowest cost=%s' % total_cost))
|
||||||
assert expected_total == total_cost
|
assert expected_total == total_cost
|
||||||
|
|||||||
Reference in New Issue
Block a user