Skip to content

Commit

Permalink
Add single knapsack algorithms (#22)
Browse files Browse the repository at this point in the history
  • Loading branch information
jmyrberg authored Jul 23, 2022
1 parent 12864bc commit ac04c58
Show file tree
Hide file tree
Showing 9 changed files with 459 additions and 52 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

Solving knapsack problems with Python using algorithms by [Martello and Toth](https://dl.acm.org/doi/book/10.5555/98124):

* Single 0-1 knapsack problem: MT1, MT2
* Multiple 0-1 knapsack problem: MTM, MTHM

Documentation is available [here](https://mknapsack.readthedocs.io).
Expand All @@ -28,6 +29,22 @@ Documentation is available [here](https://mknapsack.readthedocs.io).

## Example usage

### Single 0-1 Knapsack Problem

```python
from mknapsack import solve_single_knapsack

# Given ten items with the following profits and weights:
profits = [78, 35, 89, 36, 94, 75, 74, 79, 80, 16]
weights = [18, 9, 23, 20, 59, 61, 70, 75, 76, 30]

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

# Assign items into the knapsack while maximizing profit
res = solve_single_knapsack(profits, weights, capacity)
```

### Multiple 0-1 Knapsack Problem

```python
Expand Down
5 changes: 4 additions & 1 deletion mknapsack/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
__all__ = ['solve_multiple_knapsack']
"""Solving knapsack problems with Python."""

__all__ = ['solve_single_knapsack', 'solve_multiple_knapsack']

import os
import sys
Expand All @@ -11,4 +13,5 @@
os.add_dll_directory(extra_dll_dir)


from mknapsack._single import solve_single_knapsack # noqa: E402
from mknapsack._multiple import solve_multiple_knapsack # noqa: E402
8 changes: 8 additions & 0 deletions mknapsack/_algos.f
Original file line number Diff line number Diff line change
Expand Up @@ -5526,6 +5526,10 @@ subroutine mt1(n,p,w,c,z,x,jdim,jck,xx,min,psign,wsign,zsign)
c all the parameters are integer. on return of mt1 all the input
c parameters are unchanged.
c
cf2py intent(in) n, p, w, c, jdim, jck
cf2py intent(hide) xx, min, psign, wsign, zsign
cf2py intent(out) z, x
Cf2py depend(jdim) p, w, x, xx, min, psign, wsign, zsign
integer p(jdim),w(jdim),x(jdim),c,z
integer xx(jdim),min(jdim),psign(jdim),wsign(jdim),zsign(jdim)
integer ch,chs,diff,profit,r,t
Expand Down Expand Up @@ -6117,6 +6121,10 @@ subroutine mt2 ( n, p, w, c, z, x, jdim, jfo, jfs, jck, jub,
c all the parameters but ra are integer. on return of mt2 all the
c input parameters are unchanged.
c
cf2py intent(in) n, p, w, c, jdim, jfo, jfs, jck
cf2py intent(hide) ia1, ia2, ia3, ia4, ra
cf2py intent(out) z, x, jub
Cf2py depend(jdim) p, w, x, ia1, ia2, ia3, ia4, ra
integer p(jdim),w(jdim),x(jdim),c,z
integer ia1(jdim),ia2(jdim),ia3(jdim),ia4(jdim)
real ra(jdim)
Expand Down
18 changes: 16 additions & 2 deletions mknapsack/_exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ class FortranInputCheckError(Exception):
error_codes = {
'mtm': {
-1: 'Number of items/knapsacks is either too small or large',
-2: 'Profit or weight of at least one item is <= 0',
-2: 'Profit, weight or capacity is <= 0',
-3: 'Minimum weight is greater than smallest knapsack',
-4: 'Maximum weight is greater than largest knapsack',
-5: 'Total sum of weights is smaller than largest knapsack',
Expand All @@ -14,12 +14,26 @@ class FortranInputCheckError(Exception):
},
'mthm': {
-1: 'Number of items/knapsacks is either too small or large',
-2: 'Profit or weight of at least one item is <= 0',
-2: 'Profit, weight or capacity is <= 0',
-3: 'Minimum weight is greater than smallest knapsack',
-4: 'Maximum weight is greater than largest knapsack',
-5: 'Total sum of weights is smaller than largest knapsack',
-6: 'Items should be ordered in descending profit/weight order',
-7: 'Knapsacks should be ordered in ascending order'
},
'mt1': {
-1: 'Number of items is less than 2',
-2: 'Profit, weight or capacity is <= 0',
-3: 'One or more of weights is greater than knapsack capacity',
-4: 'Total weight is smaller than knapsack capacity',
-5: 'Items should be ordered in descending profit/weight order'
},
'mt2': {
-1: 'Number of items is less than 2',
-2: 'Profit, weight or capacity is <= 0',
-3: 'One or more of weights is greater than knapsack capacity',
-4: 'Total weight is smaller than knapsack capacity',
-5: 'Items should be ordered in descending profit/weight order'
}
}

Expand Down
53 changes: 5 additions & 48 deletions mknapsack/_multiple.py
Original file line number Diff line number Diff line change
@@ -1,30 +1,4 @@
"""Module for solving multiple 0-1 knapsack problems.
TODO:
mt1: Single 0-1 knapsack problem
mt2: Single 0-1 knapsack problem
skp1: Single 0-1 knapsack problem
skp2: Single 0-1 knapsack problem
kp01m: Single 0-1 knapsack problem, items need to be sorted according to
decreasing profit per unit weight.
mtb2: Bounded single 0-1 knapsack problem
mt1r: Single 0-1 knapsack problem with real parameters
mtu1: Unbounded single knapsack problem
mtu2: Unbounded single knapsack problem
mtp: Bin-packing problem
mts: Small subset-sum problem
mtsl: Subset-sum problem
mtc1: Change-making problem
mtc2: Unbounded change-making problem
mtcb: Bounded change-making problem
mtg: Generalized assignment problem
mthg: Generalized assignment problem with heuristics
"""
"""Module for solving multiple 0-1 knapsack problems."""


import logging
Expand All @@ -36,25 +10,12 @@

from mknapsack._algos import mtm, mthm
from mknapsack._exceptions import FortranInputCheckError
from mknapsack._utils import preprocess_array, pad_array


logger = logging.getLogger(__name__)


def preprocess_array(ar):
"""Preprocess array for Fortran inputs."""
return np.array(ar, dtype='int32', order='F')


def pad_array(ar, width):
"""Pad array with zeros to given length."""
assert ar.ndim == 1
new_ar = np.zeros((width, ), dtype='int32', order='F')
n = len(ar)
new_ar[:n] = ar
return new_ar


def process_results(profits, weights, capacities, x):
"""Preprocess multiple 0-1 knapsack results."""
given_knapsacks = pd.DataFrame({
Expand Down Expand Up @@ -100,7 +61,7 @@ def solve_multiple_knapsack(
- 'mtm' - provides a global optimum, but may take a long time
to solve for larger problem sizes
- 'mthm' - provides a fast heuristical solution that might not
be the global optimum
be the global optimum, but is suitable for larger problems
Defaults to 'mthm'.
method_kwargs:
Expand All @@ -110,8 +71,8 @@ def solve_multiple_knapsack(
* **max_backtracks** (int, optional) - Maximum number of
backtracks to perform. Setting -1 corresponds to exact
solution. Defaults to -1.
* **check_inputs** (int) - Whether to check inputs or not
(0=no, 1=yes). Defaults to 1.
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
- 'mthm'
* **call_stack** (int, optional) - Operations to perform on
top of the initial solution. Should be one of
Expand Down Expand Up @@ -146,10 +107,6 @@ def solve_multiple_knapsack(
)
References:
* Silvano Martello, Paolo Toth, Optimal and canonical solutions of the
change-making problem, European Journal of Operational Research,
1980.
* Silvano Martello, Paolo Toth, Knapsack Problems: Algorithms and
Computer Implementations, Wiley, 1990, ISBN: 0-471-92420-2,
LC: QA267.7.M37.
Expand Down
202 changes: 202 additions & 0 deletions mknapsack/_single.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
"""Module for solving multiple 0-1 knapsack problems.
TODO:
mt1r: Single 0-1 knapsack problem with real parameters
mtb2: Bounded single 0-1 knapsack problem
mtu1: Unbounded single knapsack problem
mtu2: Unbounded single knapsack problem
"""


import logging

from typing import List, Optional

import numpy as np
import pandas as pd

from mknapsack._algos import mt1, mt2
from mknapsack._exceptions import FortranInputCheckError
from mknapsack._utils import preprocess_array, pad_array


logger = logging.getLogger(__name__)


def process_results(profits, weights, capacity, x):
"""Preprocess single 0-1 knapsack results."""
given_knapsacks = pd.DataFrame({
'knapsack_id': [1],
'knapsack_capacity': [capacity]
})
no_knapsack = pd.DataFrame([{'knapsack_id': 0, 'knapsack_capacity': 0}])
knapsacks = pd.concat([no_knapsack, given_knapsacks], axis=0)
items = (
pd.DataFrame({
'item_id': np.arange(len(profits)) + 1,
'profit': profits,
'weight': weights,
'knapsack_id': x[:len(profits)]
})
.merge(knapsacks, on='knapsack_id', how='left')
.assign(assigned=lambda x: x['knapsack_id'] > 0)
)
return items


def solve_single_knapsack(
profits: List[int],
weights: List[int],
capacity: int,
method: str = 'mt2',
method_kwargs: Optional[dict] = None,
verbose: bool = False
) -> pd.DataFrame:
"""Solves the single 0-1 knapsack problem.
Given a set of items with profits and weights and a knapsack with given
capacity, which items should we pick in order to maximize profit?
Args:
profits: Profits of each item.
weights: Weight of each item.
capacity: Capacity of knapsack.
method:
Algorithm to use for solving, should be one of
- 'mt1' - provides a global optimum, but may take a long time
to solve for larger problem sizes
- 'mt2' - 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 'mt2'.
method_kwargs:
Keyword arguments to pass to a given `method`.
- 'mt1'
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
- 'mt2'
* **require_exact** (int, optional) - Whether to require an
exact solution or not (0=no, 1=yes). Defaults to 0.
* **check_inputs** (int, optional) - Whether to check
inputs or not (0=no, 1=yes). Defaults to 1.
Defaults to None.
Returns:
pd.DataFrame: The corresponding knapsack for each item, where
``knapsack_id=0`` means that the item is not assigned to a knapsack.
Raises:
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_single_knapsack
res = solve_single_knapsack(
profits=[78, 35, 89, 36, 94, 75, 74, 100, 80, 16],
weights=[18, 9, 23, 20, 59, 61, 70, 75, 76, 30],
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>`_
"""
profits = preprocess_array(profits)
weights = preprocess_array(weights)

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

# Sort items by profit/ratio ratio in ascending order
items_order_idx = (profits / weights).argsort()[::-1]
items_reverse_idx = np.argsort(items_order_idx)
profits = profits[items_order_idx]
weights = weights[items_order_idx]

n = len(profits)

method = method.lower()
method_kwargs = method_kwargs or {}
if method == 'mt1':
jdim = n + 1
p = pad_array(profits, jdim)
w = pad_array(weights, jdim)
z, x = mt1(
n=n,
p=p,
w=w,
c=capacity,
jdim=jdim,
jck=method_kwargs.pop('check_inputs', 1)
)

if z < 0:
raise FortranInputCheckError(method=method, z=z)

if verbose:
logger.info(f'Method: "{method}"')
logger.info(f'Total profit: {z}')
logger.info('Solution vector: '
f'{x[:n][items_reverse_idx].tolist()}')
elif method == 'mt2':
jdim = n + 3
p = pad_array(profits, jdim)
w = pad_array(weights, jdim)
z, x, jub = mt2(
n=n,
p=p,
w=w,
c=capacity,
jdim=jdim,
jfo=method_kwargs.pop('require_exact', 0),
jfs=1,
jck=method_kwargs.pop('check_inputs', 1)
)

if z < 0:
raise FortranInputCheckError(method=method, z=z)

if verbose:
logger.info(f'Method: "{method}"')
logger.info(f'Total profit: {z}')
logger.info('Solution vector: '
f'{x[:n][items_reverse_idx].tolist()}')
logger.info(f'Solution upper bound: {jub}')
else:
raise ValueError(f'Given method "{method}" not known')

# Inverse items and knapsacks to original order
profits = profits[items_reverse_idx]
weights = weights[items_reverse_idx]
x = np.array(x)[items_reverse_idx]

res = process_results(profits, weights, capacity, x)

if verbose:
knapsack_results = (
res
.groupby('knapsack_id')
.agg(
capacity_used=('weight', 'sum'),
capacity_available=('knapsack_capacity', 'first'),
profit=('profit', 'sum'),
items=('item_id', 'unique')
)
)
logger.info(f'Results by knapsack_id:\n{knapsack_results.to_string()}')

return res
Loading

0 comments on commit ac04c58

Please sign in to comment.