mirror of
https://github.com/rembo10/headphones.git
synced 2026-01-08 22:38:08 -05:00
munkres: 1.0.6 -> 1.1.4
This commit is contained in:
542
lib/munkres.py
542
lib/munkres.py
@@ -1,8 +1,3 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: iso-8859-1 -*-
|
||||
|
||||
# Documentation is intended to be processed by Epydoc.
|
||||
|
||||
"""
|
||||
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),
|
||||
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.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.
|
||||
For complete usage documentation, see: https://software.clapper.org/munkres/
|
||||
"""
|
||||
|
||||
__docformat__ = 'restructuredtext'
|
||||
__docformat__ = 'markdown'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Imports
|
||||
@@ -278,23 +17,43 @@ __docformat__ = 'restructuredtext'
|
||||
|
||||
import sys
|
||||
import copy
|
||||
from typing import Union, NewType, Sequence, Tuple, Optional, Callable
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Exports
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
__all__ = ['Munkres', 'make_cost_matrix']
|
||||
__all__ = ['Munkres', 'make_cost_matrix', 'DISALLOWED']
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Globals
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
AnyNum = NewType('AnyNum', Union[int, float])
|
||||
Matrix = NewType('Matrix', Sequence[Sequence[AnyNum]])
|
||||
|
||||
# Info about the module
|
||||
__version__ = "1.0.6"
|
||||
__version__ = "1.1.4"
|
||||
__author__ = "Brian Clapper, bmc@clapper.org"
|
||||
__url__ = "http://software.clapper.org/munkres/"
|
||||
__copyright__ = "(c) 2008 Brian M. Clapper"
|
||||
__license__ = "BSD-style license"
|
||||
__url__ = "https://software.clapper.org/munkres/"
|
||||
__copyright__ = "(c) 2008-2020 Brian M. Clapper"
|
||||
__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
|
||||
@@ -317,30 +76,18 @@ class Munkres:
|
||||
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):
|
||||
def pad_matrix(self, matrix: Matrix, pad_value: int=0) -> Matrix:
|
||||
"""
|
||||
Pad a possibly non-square matrix to make it square.
|
||||
|
||||
:Parameters:
|
||||
matrix : list of lists
|
||||
matrix to pad
|
||||
**Parameters**
|
||||
|
||||
pad_value : int
|
||||
value to use to pad the matrix
|
||||
- `matrix` (list of lists of numbers): matrix to pad
|
||||
- `pad_value` (`int`): value to use to pad the matrix
|
||||
|
||||
:rtype: list of lists
|
||||
:return: a new, possibly padded, matrix
|
||||
**Returns**
|
||||
|
||||
a new, possibly padded, matrix
|
||||
"""
|
||||
max_columns = 0
|
||||
total_rows = len(matrix)
|
||||
@@ -356,34 +103,35 @@ class Munkres:
|
||||
new_row = row[:]
|
||||
if total_rows > row_len:
|
||||
# Row too short. Pad it.
|
||||
new_row += [0] * (total_rows - row_len)
|
||||
new_row += [pad_value] * (total_rows - row_len)
|
||||
new_matrix += [new_row]
|
||||
|
||||
while len(new_matrix) < total_rows:
|
||||
new_matrix += [[0] * total_rows]
|
||||
new_matrix += [[pad_value] * total_rows]
|
||||
|
||||
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
|
||||
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.
|
||||
|
||||
: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.
|
||||
|
||||
**WARNING**: This code handles square and rectangular
|
||||
matrices. It does *not* handle irregular matrices.
|
||||
**Parameters**
|
||||
|
||||
:rtype: list
|
||||
:return: A list of ``(row, column)`` tuples that describe the lowest
|
||||
cost path through the matrix
|
||||
- `cost_matrix` (list of lists of numbers): 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.)
|
||||
|
||||
|
||||
**Returns**
|
||||
|
||||
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)
|
||||
@@ -422,18 +170,18 @@ class Munkres:
|
||||
|
||||
return results
|
||||
|
||||
def __copy_matrix(self, matrix):
|
||||
def __copy_matrix(self, matrix: Matrix) -> Matrix:
|
||||
"""Return an exact copy of the supplied 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."""
|
||||
matrix = []
|
||||
for i in range(n):
|
||||
matrix += [[val for j in range(n)]]
|
||||
return matrix
|
||||
|
||||
def __step1(self):
|
||||
def __step1(self) -> int:
|
||||
"""
|
||||
For each row of the matrix, find the smallest element and
|
||||
subtract it from every element in its row. Go to Step 2.
|
||||
@@ -441,15 +189,22 @@ class Munkres:
|
||||
C = self.C
|
||||
n = self.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
|
||||
# from every element in the row.
|
||||
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
|
||||
|
||||
def __step2(self):
|
||||
def __step2(self) -> int:
|
||||
"""
|
||||
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
|
||||
@@ -464,11 +219,12 @@ class Munkres:
|
||||
self.marked[i][j] = 1
|
||||
self.col_covered[j] = True
|
||||
self.row_covered[i] = True
|
||||
break
|
||||
|
||||
self.__clear_covers()
|
||||
return 3
|
||||
|
||||
def __step3(self):
|
||||
def __step3(self) -> int:
|
||||
"""
|
||||
Cover each column containing a starred zero. If K columns are
|
||||
covered, the starred zeros describe a complete set of unique
|
||||
@@ -478,7 +234,7 @@ class Munkres:
|
||||
count = 0
|
||||
for i 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
|
||||
count += 1
|
||||
|
||||
@@ -489,7 +245,7 @@ class Munkres:
|
||||
|
||||
return step
|
||||
|
||||
def __step4(self):
|
||||
def __step4(self) -> int:
|
||||
"""
|
||||
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,
|
||||
@@ -499,11 +255,11 @@ class Munkres:
|
||||
"""
|
||||
step = 0
|
||||
done = False
|
||||
row = -1
|
||||
col = -1
|
||||
row = 0
|
||||
col = 0
|
||||
star_col = -1
|
||||
while not done:
|
||||
(row, col) = self.__find_a_zero()
|
||||
(row, col) = self.__find_a_zero(row, col)
|
||||
if row < 0:
|
||||
done = True
|
||||
step = 6
|
||||
@@ -522,7 +278,7 @@ class Munkres:
|
||||
|
||||
return step
|
||||
|
||||
def __step5(self):
|
||||
def __step5(self) -> int:
|
||||
"""
|
||||
Construct a series of alternating primed and starred zeros as
|
||||
follows. Let Z0 represent the uncovered primed zero found in Step 4.
|
||||
@@ -558,7 +314,7 @@ class Munkres:
|
||||
self.__erase_primes()
|
||||
return 3
|
||||
|
||||
def __step6(self):
|
||||
def __step6(self) -> int:
|
||||
"""
|
||||
Add the value found in Step 4 to every element of each covered
|
||||
row, and subtract it from every element of each uncovered column.
|
||||
@@ -566,34 +322,44 @@ class Munkres:
|
||||
lines.
|
||||
"""
|
||||
minval = self.__find_smallest()
|
||||
events = 0 # track actual changes to matrix
|
||||
for i in range(self.n):
|
||||
for j in range(self.n):
|
||||
if self.C[i][j] is DISALLOWED:
|
||||
continue
|
||||
if self.row_covered[i]:
|
||||
self.C[i][j] += minval
|
||||
events += 1
|
||||
if not self.col_covered[j]:
|
||||
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
|
||||
|
||||
def __find_smallest(self):
|
||||
def __find_smallest(self) -> AnyNum:
|
||||
"""Find the smallest uncovered value in the matrix."""
|
||||
minval = sys.maxsize
|
||||
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]:
|
||||
if self.C[i][j] is not DISALLOWED and minval > self.C[i][j]:
|
||||
minval = self.C[i][j]
|
||||
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"""
|
||||
row = -1
|
||||
col = -1
|
||||
i = 0
|
||||
i = i0
|
||||
n = self.n
|
||||
done = False
|
||||
|
||||
while not done:
|
||||
j = 0
|
||||
j = j0
|
||||
while True:
|
||||
if (self.C[i][j] == 0) and \
|
||||
(not self.row_covered[i]) and \
|
||||
@@ -601,16 +367,16 @@ class Munkres:
|
||||
row = i
|
||||
col = j
|
||||
done = True
|
||||
j += 1
|
||||
if j >= n:
|
||||
j = (j + 1) % n
|
||||
if j == j0:
|
||||
break
|
||||
i += 1
|
||||
if i >= n:
|
||||
i = (i + 1) % n
|
||||
if i == i0:
|
||||
done = True
|
||||
|
||||
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
|
||||
the column index, or -1 if no starred element was found.
|
||||
@@ -623,7 +389,7 @@ class Munkres:
|
||||
|
||||
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
|
||||
the row index, or -1 if no starred element was found.
|
||||
@@ -636,7 +402,7 @@ class Munkres:
|
||||
|
||||
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
|
||||
the column index, or -1 if no starred element was found.
|
||||
@@ -649,20 +415,22 @@ class Munkres:
|
||||
|
||||
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):
|
||||
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):
|
||||
def __clear_covers(self) -> None:
|
||||
"""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):
|
||||
def __erase_primes(self) -> None:
|
||||
"""Erase all prime markings"""
|
||||
for i in range(self.n):
|
||||
for j in range(self.n):
|
||||
@@ -673,51 +441,56 @@ class Munkres:
|
||||
# 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
|
||||
'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.
|
||||
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 value. If the inversion function
|
||||
is not provided, a given cell's inverted value is calculated as
|
||||
`max(matrix) - value`.
|
||||
|
||||
This is a static method. Call it like this:
|
||||
|
||||
.. python::
|
||||
|
||||
from munkres import Munkres
|
||||
cost_matrix = Munkres.make_cost_matrix(matrix, inversion_func)
|
||||
|
||||
For example:
|
||||
|
||||
.. python::
|
||||
|
||||
from munkres import Munkres
|
||||
cost_matrix = Munkres.make_cost_matrix(matrix, lambda x : sys.maxsize - x)
|
||||
|
||||
:Parameters:
|
||||
profit_matrix : list of lists
|
||||
The matrix to convert from a profit to a cost matrix
|
||||
**Parameters**
|
||||
|
||||
inversion_function : function
|
||||
The function to use to invert each entry in the profit matrix
|
||||
- `profit_matrix` (list of lists of numbers): The matrix to convert from
|
||||
profit to cost values.
|
||||
- `inversion_function` (`function`): The function to use to invert each
|
||||
entry in the profit matrix.
|
||||
|
||||
:rtype: list of lists
|
||||
:return: The converted matrix
|
||||
**Returns**
|
||||
|
||||
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 = []
|
||||
for row in profit_matrix:
|
||||
cost_matrix.append([inversion_function(value) for value in row])
|
||||
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:
|
||||
matrix : list of lists
|
||||
Matrix to print
|
||||
**Parameters**
|
||||
|
||||
msg : str
|
||||
Optional message to print before displaying the matrix
|
||||
- `matrix` (list of lists of numbers): The matrix to print
|
||||
- `msg` (`str`): Optional message to print before displaying the matrix
|
||||
"""
|
||||
import math
|
||||
|
||||
@@ -728,16 +501,21 @@ def print_matrix(matrix, msg=None):
|
||||
width = 0
|
||||
for row in matrix:
|
||||
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
|
||||
format = '%%%dd' % width
|
||||
format = ('%%%d' % width)
|
||||
|
||||
# Print the matrix
|
||||
for row in matrix:
|
||||
sep = '['
|
||||
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 = ', '
|
||||
sys.stdout.write(']\n')
|
||||
|
||||
@@ -767,11 +545,51 @@ if __name__ == '__main__':
|
||||
[9, 7, 4]],
|
||||
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
|
||||
([[10, 10, 8, 11],
|
||||
[9, 8, 1, 1],
|
||||
[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()
|
||||
for cost_matrix, expected_total in matrices:
|
||||
@@ -781,6 +599,6 @@ if __name__ == '__main__':
|
||||
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))
|
||||
print(('(%d, %d) -> %s' % (r, c, x)))
|
||||
print(('lowest cost=%s' % total_cost))
|
||||
assert expected_total == total_cost
|
||||
|
||||
Reference in New Issue
Block a user