1

I have a list of items, a, b, c,..., each of which has a weight and a value.

The 'ordinary' Knapsack algorithm will find the selection of items that maximises the value of the selected items, whilst ensuring that the weight is below a given constraint.

The problem I have is slightly different. I wish to minimise the value (easy enough by using the reciprocal of the value), whilst ensuring that the weight is at least the value of the given constraint, not less than or equal to the constraint.

I have tried re-routing the idea through the ordinary Knapsack algorithm, but this can't be done. I was hoping there is another combinatorial algorithm that I am not aware of that does this.

Rodrigo de Azevedo
  • 1,097
  • 9
  • 17
  • If you run the ordinary knapsack with the constraint set to the total weight of all the items less the given constraint, won't the set of items you haven't chosen be the set you want? – dmuir Feb 01 '18 at 11:46

1 Answers1

1

In the german wiki it's formalized as:

finite set of objects U
w: weight-function
v: value-function

w: U -> R
v: U -> R
B in R    # constraint rhs

Find subset K in U subject to:
    sum( w(u) <= B ) | all w in K
such that: 
    max sum( v(u) )  | all u in K

So there is no restriction like nonnegativity.

Just use negative weights, negative values and a negative B. The basic concept is:

 sum( w(u) ) <=  B | all w in K
<->
-sum( w(u) ) >= -B | all w in K

So in your case:

classic constraint: x0 + x1 <=  B    | 3 + 7 <= 12 Y | 3 + 10 <= 12 N
becomes:           -x0 - x1 <= -B    |-3 - 7 <=-12 N |-3 - 10 <=-12 Y 

So for a given implementation it depends on the software if this is allowed. In terms of the optimization-problem, there is no problem. The integer-programming formulation for your case is as natural as the classic one (and bounded).

Python Demo based on Integer-Programming

Code

import numpy as np
import scipy.sparse as sp
from cylp.cy import CyClpSimplex
np.random.seed(1)

""" INSTANCE """
weight = np.random.randint(50, size = 5)
value = np.random.randint(50, size = 5)
capacity = 50

""" SOLVE """
n = weight.shape[0]
model = CyClpSimplex()
x = model.addVariable('x', n, isInt=True)
model.objective = value                            # MODIFICATION: default = minimize!
model += sp.eye(n) * x >= np.zeros(n)              # could be improved
model += sp.eye(n) * x <= np.ones(n)               # """
model += np.matrix(-weight) * x <= -capacity       # MODIFICATION
cbcModel = model.getCbcModel()
cbcModel.logLevel = True
status = cbcModel.solve()
x_sol = np.array(cbcModel.primalVariableSolution['x'].round()).astype(int)  # assumes existence

print("INSTANCE")
print("    weights: ", weight)
print("    values: ", value)
print("    capacity: ", capacity)
print("Solution")
print(x_sol)
print("sum weight: ", x_sol.dot(weight))
print("value: ", x_sol.dot(value))

Small remarks

  • This code is just a demo using a somewhat low-level like library and there are other tools available which might be better suited (e.g. windows: pulp)
  • it's the classic integer-programming formulation from wiki modifies as mentioned above
  • it will scale very well as the underlying solver is pretty good
  • as written, it's solving the 0-1 knapsack (only variable bounds would need to be changed)

Small look at the core-code:

# create model
model = CyClpSimplex()

# create one variable for each how-often-do-i-pick-this-item decision
# variable needs to be integer (or binary for 0-1 knapsack)
x = model.addVariable('x', n, isInt=True)

# the objective value of our IP: a linear-function
# cylp only needs the coefficients of this function: c0*x0 + c1*x1 + c2*x2...
#     we only need our value vector
model.objective = value                            # MODIFICATION: default = minimize!

# WARNING: typically one should always use variable-bounds
#     (cylp problems...)
#  workaround: express bounds lower_bound <= var <= upper_bound as two constraints
#  a constraint is an affine-expression
#  sp.eye creates a sparse-diagonal with 1's
#  example: sp.eye(3) * x >= 5
#           1 0 0 -> 1 * x0 + 0 * x1 + 0 * x2 >= 5
#           0 1 0 -> 0 * x0 + 1 * x1 + 0 * x2 >= 5
#           0 0 1 -> 0 * x0 + 0 * x1 + 1 * x2 >= 5
model += sp.eye(n) * x >= np.zeros(n)              # could be improved
model += sp.eye(n) * x <= np.ones(n)               # """

# cylp somewhat outdated: need numpy's matrix class
# apart from that it's just the weight-constraint as defined at wiki
# same affine-expression as above (but only a row-vector-like matrix)
model += np.matrix(-weight) * x <= -capacity       # MODIFICATION

# internal conversion of type neeeded to treat it as IP (or else it would be 
LP)
cbcModel = model.getCbcModel()
cbcModel.logLevel = True
status = cbcModel.solve()

# type-casting
x_sol = np.array(cbcModel.primalVariableSolution['x'].round()).astype(int)  

Output

Welcome to the CBC MILP Solver 
Version: 2.9.9 
Build Date: Jan 15 2018 

command line - ICbcModel -solve -quit (default strategy 1)
Continuous objective value is 4.88372 - 0.00 seconds
Cgl0004I processed model has 1 rows, 4 columns (4 integer (4 of which binary)) and 4 elements
Cutoff increment increased from 1e-05 to 0.9999
Cbc0038I Initial state - 0 integers unsatisfied sum - 0
Cbc0038I Solution found of 5
Cbc0038I Before mini branch and bound, 4 integers at bound fixed and 0 continuous
Cbc0038I Mini branch and bound did not improve solution (0.00 seconds)
Cbc0038I After 0.00 seconds - Feasibility pump exiting with objective of 5 - took 0.00 seconds
Cbc0012I Integer solution of 5 found by feasibility pump after 0 iterations and 0 nodes (0.00 seconds)
Cbc0001I Search completed - best objective 5, took 0 iterations and 0 nodes (0.00 seconds)
Cbc0035I Maximum depth 0, 0 variables fixed on reduced cost
Cuts at root node changed objective from 5 to 5
Probing was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Gomory was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Knapsack was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
Clique was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
MixedIntegerRounding2 was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
FlowCover was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)
TwoMirCuts was tried 0 times and created 0 cuts of which 0 were active after adding rounds of cuts (0.000 seconds)

Result - Optimal solution found

Objective value:                5.00000000
Enumerated nodes:               0
Total iterations:               0
Time (CPU seconds):             0.00
Time (Wallclock seconds):       0.00

Total time (CPU seconds):       0.00   (Wallclock seconds):       0.00

INSTANCE
    weights:  [37 43 12  8  9]
    values:  [11  5 15  0 16]
    capacity:  50
Solution
[0 1 0 1 0]
sum weight:  51
value:  5
sascha
  • 32,238
  • 6
  • 68
  • 110
  • Sacha, many thanks for your reply. Having read through you Wiki explanation, that makes sense now, however, I will not pretend that I understand your code implementation. Could you give me a general brief of what the code is doing? Once again, thank you for your reply, very helpful – user3745220 Feb 02 '18 at 07:49
  • @user3745220 I added some descriptions. It's a trivial integer-program and only the library makes it look dubious at first. – sascha Feb 02 '18 at 12:43