3

Let's say I have the following (always binary) options:

import numpy as np
a=np.array([1, 1, 0, 0, 1, 1, 1])
b=np.array([1, 1, 0, 0, 1, 0, 1])
c=np.array([1, 0, 0, 1, 0, 0, 0])
d=np.array([1, 0, 1, 1, 0, 0, 0])

And I want to find the optimal combination of the above that get's me to at least, with minimal above:

req = np.array([50,50,20,20,100,40,10])

For example:

final = X1*a + X2*b + X3*c + X4*d
  1. Does this map to a known operational research problem? Or does it fall under mathematical programming?
  2. Is this NP-hard, or exactly solveable in a reasonable amount of time (I've assumed it's combinatorally impossible to solve exactly)
  3. Are there know solutions to this?

Note: The actual length of arrays are longer - think ~50, and the number of options are ~20 My current research has led me to some combination of the assignment problem and knapsack, but not too sure.

GooJ
  • 257
  • 3
  • 13
  • 1
    well, your example is rather easy. The result 40 can only be achieved from the array a, so X1 must be 40, then having set X1 you find that he result 10 only depends on a and b so X2 must be a+b=10; 40 + b = 10; b=-30. Then the second 20 depends on a and c, so X3=-20, and finally the first 20 depends only on d, so X4=20. Then you see that the other numbers don't match, meaning that there's no solution to your example –  Sep 07 '22 at 10:55
  • 1
    In the example above *kernellesation* is possible: `a` *dominates* `b` (all ones that in `b` are in the `a`), `d` dominates `c`. So we can solve for `a` and `d`. Index #4 is presented in `a` only, that's why we have to take `100` of `a`, index #2 is set in the `d` only, we have to take `20` of `d`. So far so good any minimal solution includes `120` items, `100` of `a` and `20` of `d`. The we subtract the *kernel* found and see that we don't want to solve any more – Dmitry Bychenko Sep 07 '22 at 11:27
  • 1
    Minimum vertex cover reduces easily to this problem, so it's certainly NP-hard: https://en.wikipedia.org/wiki/Vertex_cover – Matt Timmermans Sep 07 '22 at 12:18
  • SembeiNorimaki Yes maybe not the best example, my bad. DmitryBychenko Thank you for this, I'll look into this further, I really like the approach. MattTimmermans Thank you for the link, confirms my suspicions, where I didn't have the knowledge to link it. – GooJ Sep 07 '22 at 13:04

1 Answers1

1

It's a covering problem, easily solvable using an integer program solver (I used OR-Tools below). If the X variables can be fractional, substitute NumVar for IntVar. If the X variables are 0--1, substitute BoolVar.

import numpy as np

a = np.array([1, 1, 0, 0, 1, 1, 1])
b = np.array([1, 1, 0, 0, 1, 0, 1])
c = np.array([1, 0, 0, 1, 0, 0, 0])
d = np.array([1, 0, 1, 1, 0, 0, 0])
opt = [a, b, c, d]
req = np.array([50, 50, 20, 20, 100, 40, 10])


from ortools.linear_solver import pywraplp

solver = pywraplp.Solver.CreateSolver("SCIP")
x = [solver.IntVar(0, solver.infinity(), "x{}".format(i)) for i in range(len(opt))]
extra = [solver.NumVar(0, solver.infinity(), "y{}".format(j)) for j in range(len(req))]
for j, (req_j, extra_j) in enumerate(zip(req, extra)):
    solver.Add(extra_j == sum(opt_i[j] * x_i for (opt_i, x_i) in zip(opt, x)) - req_j)
solver.Minimize(sum(extra))
status = solver.Solve()
if status == pywraplp.Solver.OPTIMAL:
    print("Solution:")
    print("Objective value =", solver.Objective().Value())
    for i, x_i in enumerate(x):
        print("x{} = {}".format(i, x[i].solution_value()))
else:
    print("The problem does not have an optimal solution.")

Output:

Solution:
Objective value = 210.0
x0 = 40.0
x1 = 60.0
x2 = -0.0
x3 = 20.0
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • If you have time to add a bit of explanation as to what the code is doing, mainly around the solver that would be great. The documentation seems a bit sparse? – GooJ Sep 07 '22 at 13:36