Skip to content

Commit

Permalink
Add mtcb algorithm (#27)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmyrberg authored Jul 29, 2022
1 parent 47b5786 commit 228b181
Show file tree
Hide file tree
Showing 8 changed files with 341 additions and 8 deletions.
21 changes: 20 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Solving knapsack problems with Python using algorithms by [Martello and Toth](ht
* Unbounded knapsack problem: MTU1, MTU2
* Multiple 0-1 knapsack problem: MTM, MTHM
* Change-making problem: MTC2
* Bounded change-making problem: MTCB

Documentation is available [here](https://mknapsack.readthedocs.io).

Expand Down Expand Up @@ -59,7 +60,7 @@ from mknapsack import solve_bounded_knapsack
profits = [78, 35, 89, 36, 94, 75, 74, 79, 80, 16]
weights = [18, 9, 23, 20, 59, 61, 70, 75, 76, 30]

# ..and the number of items for each item type:
# ...and the number of items available for each item type:
n_items = [1, 2, 3, 2, 2, 1, 2, 2, 1, 4]

# ...and a knapsack with the following capacity:
Expand Down Expand Up @@ -116,6 +117,24 @@ capacity = 190
res = solve_change_making(weights, capacity)
```

### Bounded Change-Making Problem

```python
from mknapsack import solve_bounded_change_making

# Given ten item types with the following weights:
weights = [18, 9, 23, 20, 59, 61, 70, 75, 76, 30]

# ...and the number of items available for each item type:
n_items = [1, 2, 3, 2, 1, 1, 1, 2, 1, 2]

# ...and a knapsack with the following capacity:
capacity = 190

# Fill the knapsack while minimizing the number of items
res = solve_bounded_change_making(weights, n_items, capacity)
```

## References

* [Knapsack problems: algorithms and computer implementations](https://dl.acm.org/doi/book/10.5555/98124) by S. Martello and P. Toth, 1990
Expand Down
2 changes: 2 additions & 0 deletions mknapsack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

__all__ = [
'solve_bounded_knapsack',
'solve_bounded_change_making',
'solve_change_making',
'solve_multiple_knapsack',
'solve_single_knapsack',
Expand All @@ -21,6 +22,7 @@


from mknapsack._bounded import solve_bounded_knapsack # noqa: E402
from mknapsack._bounded_change_making import solve_bounded_change_making # noqa: E402, E501
from mknapsack._change_making import solve_change_making # noqa: E402
from mknapsack._multiple import solve_multiple_knapsack # noqa: E402
from mknapsack._single import solve_single_knapsack # noqa: E402
Expand Down
13 changes: 10 additions & 3 deletions mknapsack/_algos.f
Original file line number Diff line number Diff line change
Expand Up @@ -6906,7 +6906,7 @@ subroutine mtc2 ( n, w, c, z, x, jdn, jdl, jfo, back, jck, xx, wr,

return
end
subroutine mtcb(n,w,b,c,z,x,jdn,jdl,jfo,jck,xx,wr,br,pr,m,l)
subroutine mtcb(n,w,b,c,z,x,jdn,jdl,jfo,back,jck,xx,wr,br,pr,m,l)

c*********************************************************************72
c
Expand Down Expand Up @@ -6948,6 +6948,7 @@ subroutine mtcb(n,w,b,c,z,x,jdn,jdl,jfo,jck,xx,wr,br,pr,m,l)
c Modified:
c
c 06 December 2009
c 27 July 2022 (jmyrberg)
c
c Author:
c
Expand All @@ -6973,6 +6974,7 @@ subroutine mtcb(n,w,b,c,z,x,jdn,jdl,jfo,jck,xx,wr,br,pr,m,l)
c to the largest possible value);
c jfo = 1 if optimal solution is required,
c = 0 if approximate solution is required (at most 100000
c back = maximum number of backtracks to perform when jfo = 0
c backtrackings are performed);
c jck = 1 if check on the input data is desired,
c = 0 otherwise.
Expand All @@ -6989,14 +6991,19 @@ subroutine mtcb(n,w,b,c,z,x,jdn,jdl,jfo,jck,xx,wr,br,pr,m,l)
c all the parameters are integer. on return of mtcb all the input
c parameters are unchanged.
c
integer w(jdn),b(jdn),x(jdn),c,z
cf2py intent(in) n, w, b, c, jdn, jdl, jfo, back, jck
cf2py intent(hide) xx, m, l, wr, br, pr
cf2py intent(out) z, x
cf2py depend(jdn) w, b, x, xx, wr, br, pr
cf2py depend(jdl) m, l
integer w(jdn),b(jdn),x(jdn),c,z,back
integer xx(jdn),wr(jdn),br(jdn),pr(jdn)
integer m(jdl),l(jdl)
z = c + 1
if ( jck .eq. 1 ) call chmtcb(n,w,b,c,z,jdn)
if ( z .lt. 0 ) return
maxbck = - 1
if ( jfo .eq. 0 ) maxbck = 100000
if ( jfo .eq. 0 ) maxbck = back
c
c sorting.
c
Expand Down
125 changes: 125 additions & 0 deletions mknapsack/_bounded_change_making.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
"""Module for solving bounded change-making problem."""


import logging

from typing import List, Optional

import numpy as np

from mknapsack._algos import mtcb
from mknapsack._exceptions import FortranInputCheckError, NoSolutionError
from mknapsack._utils import preprocess_array, pad_array


logger = logging.getLogger(__name__)


def solve_bounded_change_making(
weights: List[int],
n_items: List[int],
capacity: int,
method: str = 'mtcb',
method_kwargs: Optional[dict] = None,
verbose: bool = False
) -> np.ndarray:
"""Solves the bounded change-making problem.
Given a number of items for item types with weights, and a knapsack with
capacity, find the minimum number of items that add up to the capacity.
Args:
weights: Weight of each item type.
n_items: Number of items available for each item type.
capacity: Capacity of knapsack.
method:
Algorithm to use for solving, should be one of
- 'mtcb' - provides a fast heuristical solution that might not
be the global optimum, but is suitable for larger problems,
or an exact solution if required
Defaults to 'mtcb'.
method_kwargs:
Keyword arguments to pass to a given `method`.
- 'mtcb'
* **require_exact** (int, optional) - Whether to require an
exact solution or not (0=no, 1=yes). Defaults to 0.
* **max_backtracks** (int, optional) - The maximum number
of backtracks to perform when ``require_exact=0``.
Defaults to 100000.
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
Returns:
np.ndarray: Number of items for each item type.
Raises:
NoSolutionError: No feasible solution found.
FortranInputCheckError: Something is wrong with the inputs when
validated in the original Fortran source code side.
ValueError: Something is wrong with the given inputs.
Example:
.. code-block:: python
from mknapsack import solve_bounded_change_making
res = solve_bounded_change_making(
weights=[18, 9, 23, 20, 59, 61, 70, 75, 76, 30],
n_items=[1, 2, 3, 2, 1, 1, 1, 2, 3, 2],
capacity=190
)
References:
* Silvano Martello, Paolo Toth, Knapsack Problems: Algorithms and
Computer Implementations, Wiley, 1990, ISBN: 0-471-92420-2,
LC: QA267.7.M37.
* `Original Fortran77 source code by Martello and Toth\
<https://people.sc.fsu.edu/~jburkardt/f77_src/knapsack/knapsack.f>`_
"""
weights = preprocess_array(weights)
n_items = preprocess_array(n_items)

n = len(weights)

if len(weights) != len(n_items):
raise ValueError('Weights length must be equal to n_items '
f'({len(weights) != len(n_items)}')

method = method.lower()
method_kwargs = method_kwargs or {}
if method == 'mtcb':
jdn = n + 1
jdl = np.max(weights) - 1
w = pad_array(weights, jdn)
b = pad_array(n_items, jdn)
z, x = mtcb(
n=n,
b=b,
w=w,
c=capacity,
jdn=jdn,
jdl=jdl,
jfo=method_kwargs.get('require_exact', 0),
back=method_kwargs.get('max_backtracks', 100_000),
jck=method_kwargs.get('check_inputs', 1)
)

if z == 0:
raise NoSolutionError('No feasible solution found')
elif z < 0:
raise FortranInputCheckError(method=method, z=z)

if verbose:
logger.info(f'Method: "{method}"')
logger.info(f'Total number of items: {z}')
logger.info(f'Solution vector: {x}')
else:
raise ValueError(f'Given method "{method}" not known')

return np.array(x)[:n]
4 changes: 2 additions & 2 deletions mknapsack/_change_making.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ def solve_change_making(
* **require_exact** (int, optional) - Whether to require an
exact solution or not (0=no, 1=yes). Defaults to 0.
* **max_backtracks** (int, optional) - The maximum number
of backtracks to perform when ``require_exact=1``.
of backtracks to perform when ``require_exact=0``.
Defaults to 100000.
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
Expand Down Expand Up @@ -101,7 +101,7 @@ def solve_change_making(
)

if z == 0:
raise NoSolutionError('No feasible solution exists')
raise NoSolutionError('No feasible solution found')
elif z < 0:
raise FortranInputCheckError(method=method, z=z)

Expand Down
12 changes: 11 additions & 1 deletion mknapsack/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@ class FortranInputCheckError(Exception):
-2: 'Profit, weight, capacity or n_items is <= 0',
-3: 'Total weight (weight * total_n_items) of one or more item '
'types is greater than the knapsack capacity',
-4: 'Total weight of all items is smaller than knapsack capacity',
-4: 'Total weight of all items is smaller than or equal to '
'knapsack capacity',
-5: 'Problem with preprocessing before Fortran code',
-6: 'Items should be ordered in descending profit/weight order'
},
Expand All @@ -69,6 +70,15 @@ class FortranInputCheckError(Exception):
-1: 'Number of items is less than 2',
-2: 'Weight or capacity is <= 0',
-3: 'One or more of weights is greater than knapsack capacity'
},
'mtcb': {
-1: 'Number of items is less than 2',
-2: 'Weight, number of items or capacity is <= 0',
-3: 'One or more of weights is greater than knapsack capacity',
-4: 'Total weight (weight * total_n_items) of an item type is '
'greater than knapsack capacity',
-5: 'Total weight of all items is smaller than or equal to '
'knapsack capacity'
}
}

Expand Down
Loading

0 comments on commit 228b181

Please sign in to comment.