Reputation: 644
I'm struggling to write an algorithm for this problem. What approach can be used to solve it?
There's a company called CratePushers, whose employees are willing to push crates for money. Their employees have certain attributes:
Now let's say I want to enlist their services. I have several crates in my back yard, and I have a certain budget for each crate. For example, let's say I have 5 crates, labeled A through E, and I'm willing to spend $20 on each crate, but my budget for each crate can only be used to push one specific crate. My goal is to maximize the sum of the distance pushed for all the crates. And the question is: how do I allocate my money optimally? That is, who do I pay to push which crates, and for how much money?
The list of employees and their attributes, e.g.
[
{
name: 'Alice',
capableOfPushing: ['A', 'B', 'C'],
paymentLimit: 35,
crateLimit: 2,
efficiency: 3, // Feet per dollar
},
{
name: 'Bob',
capableOfPushing: ['A', 'D'],
paymentLimit: null,
crateLimit: null,
efficiency: 2,
}
]
along with my per-crate budget, e.g.
[
{
crate: 'A',
budget: 20,
},
{
crate: 'B',
budget: 20
},
{
crate: 'C',
budget: 20
},
{
crate: 'D',
budget: 20
},
{
crate: 'E',
budget: 20
},
]
Any optimal solution is acceptable. One solution for the above example would be:
[
{
employee: 'Alice',
crate: 'B',
amountPaid: 20,
},
{
employee: 'Alice',
crate: 'C',
amountPaid: 15,
},
{
employee: 'Bob',
crate: 'A',
amountPaid: 20,
},
{
employee: 'Bob',
crate: 'D',
amountPaid: 20,
}
]
(No one can push crate E, so my $20 budget for that crate remains in my pocket.)
If my budget for crate B was anything less than $5, the optimal solution would then be to pay Alice $15 for crate A and $20 for crate C, and pay Bob $5 for crate A and $20 for crate D. That would mean we're leaving crate B unpushed, but we're still maximizing the total sum of the distance pushed when considering all the crates.
There's the brute force solution, where we create all possible combinations of crates pushed. For example, we consider all 3 scenarios where Alice pushes (A,B), (A,C), (B,C). This solution is way too inefficient because it scales out of control when multiple employees have crate limits, or even if a single employee has larger numbers for their attributes. Like if Alice could push up to 30 crate types and had a crate limit of 10, the number of scenarios would already be in the tens of millions.
I feel like dynamic programming could work, but again it's the crate limits that cause issues in terms of knowing the optimal solution at each point.
I've looked for similar questions on LeetCode but haven't yet found any that are sufficiently related.
This problem vaguely resembles the knapsack problem, but doesn't really fit.
It also resembles multi-commodity max flow problems, but doesn't quite fit that either? I'm struggling to figure out a viable way to represent the employees as a graph, and AGAIN the crate limits are really problematic. This seems like the most promising direction though.
Upvotes: 3
Views: 157
Reputation: 20596
The problem can be formulated as a generic mathematical optimization.
e an employee
c a crate
Pec the amount paid to e to push c
Fe employee efficiency
Cec 1 if e can push c, 0 otherwise
PLe pay limit for e
CLe crate limit for e
(sum over all c ) Pec < PLe
Iec = 0 if Pec = 0, 1 otherwise
(sum over all c) Iec < CLe
Max: (sum over all e, c ) Pec * Fe
This formulation makes the problem solvable by many different algorithms: simplex, annealing, ... It can even by solved by a straightforward search through the solution space.
Complete application code for an exhaustive solution space searcher at https://github.com/JamesBremner/so79058180
Results for the example problem ( Optimum found in ~ 1 microsecond )
Employee 0 ( 20 for crate 1 15 for crate 2 )
Employee 1 ( 20 for crate 0 20 for crate 3 )
total Distance 185
raven::set::cRunWatch code timing profile
Calls Mean (secs) Total Scope
1 1.1e-06 1.1e-06 cSSex::search
The sample solution space is 160 points.
If you would tell me the maximum problem size you expect ... and the performance you need.
I have added an option to use the simulated annealing algorithm. For such a small solution space, this algorithm behaves poorly, so I won't post it here. If you provide a larger sample problem, then it might be worthwhile.
Upvotes: 0
Reputation: 20596
This problem can be solved using the Edmonds–Karp implementation of the Ford–Fulkerson method to compute the maximum flow in a flow network
https://en.wikipedia.org/wiki/Edmonds%E2%80%93Karp_algorithm
The flow network for the example problem in the question looks like this
As usual, this problem has some special constraints which need care to be handled successfully.
Crate Budgets
Set the capacity of the links between the crate and the sink to the crate budget. ( Shown in above image )
Employee Efficiencies
When creating the flow graph, create the employee nodes in order of decreasing efficiency. This ensures the high efficiency employees are employed preferentially
Employee Pay Limits
Set the capacity of the links between source and employee to the employee pay limit ( Shown in above diagram )
Employee Crate Limits
This is tricky. An iterative approach is required. Run the max flow algorithm on the graph shown above. If any employee exceeds their crate limit, zero the capacity of the link between the employee and the crate with the smallest budget. If the employee is linked to crates all with the same budget, then zero the link to the crate with the fewest alternative pushers. Run the max flow algorithm again and repeat until no employees exceeds their crate limit.
Test Application
I have developed a test application to demonstrate this solution to the example problem. The user can test different crate budgets and employee pay limits. The complete C++ code for the application is at the github repository https://github.com/JamesBremner/so79058180. V1.0.0 release is available at https://github.com/JamesBremner/so79058180/releases/tag/v1.0.0
Screenshot of application run
Upvotes: 1
Reputation: 644
Thank you @nneonneo for suggesting MILP! That's a great solution for this.
The example problem could be formulated like this, which provides the right answer:
import gurobipy as gp
from gurobipy import GRB
model = gp.Model("Pushing crates")
a_distance = model.addVar(vtype=GRB.CONTINUOUS, name="a_distance")
b_distance = model.addVar(vtype=GRB.CONTINUOUS, name="b_distance")
c_distance = model.addVar(vtype=GRB.CONTINUOUS, name="c_distance")
d_distance = model.addVar(vtype=GRB.CONTINUOUS, name="d_distance")
e_distance = model.addVar(vtype=GRB.CONTINUOUS, name="e_distance")
model.setObjective(a_distance + b_distance + c_distance + d_distance + e_distance, GRB.MAXIMIZE)
alice_efficiency = 3
alice_payment_limit = 35
alice_crate_limit = 2
bob_efficiency = 2
budget_for_a = 20
budget_for_b = 20
budget_for_c = 20
budget_for_d = 20
budget_for_e = 20
dollars_to_alice_for_a = model.addVar(vtype=GRB.INTEGER, name="dollars_to_alice_for_a", lb=0, ub=budget_for_a)
dollars_to_alice_for_b = model.addVar(vtype=GRB.INTEGER, name="dollars_to_alice_for_b", lb=0, ub=budget_for_b)
dollars_to_alice_for_c = model.addVar(vtype=GRB.INTEGER, name="dollars_to_alice_for_c", lb=0, ub=budget_for_c)
dollars_to_bob_for_a = model.addVar(vtype=GRB.INTEGER, name="dollars_to_bob_for_a", lb=0, ub=budget_for_a)
dollars_to_bob_for_d = model.addVar(vtype=GRB.INTEGER, name="dollars_to_bob_for_d", lb=0, ub=budget_for_d)
dollars_for_e = model.addVar(vtype=GRB.INTEGER, name="dollars_for_e", lb=0, ub=budget_for_e)
value_1_for_alice_crate_limit = model.addVar(vtype=GRB.INTEGER, name="value_1_for_alice_crate_limit", lb=0, ub=1)
value_2_for_alice_crate_limit = model.addVar(vtype=GRB.INTEGER, name="value_2_for_alice_crate_limit", lb=0, ub=1)
value_3_for_alice_crate_limit = model.addVar(vtype=GRB.INTEGER, name="value_3_for_alice_crate_limit", lb=0, ub=1)
model.addConstr(dollars_to_alice_for_a + dollars_to_bob_for_a <= budget_for_a)
model.addConstr(dollars_to_alice_for_a + dollars_to_alice_for_b + dollars_to_alice_for_c <= alice_payment_limit)
model.addConstr(value_1_for_alice_crate_limit + value_2_for_alice_crate_limit + value_3_for_alice_crate_limit <= alice_crate_limit)
model.addConstr(a_distance <= alice_efficiency * dollars_to_alice_for_a * value_1_for_alice_crate_limit + bob_efficiency * dollars_to_bob_for_a)
model.addConstr(b_distance <= alice_efficiency * dollars_to_alice_for_b * value_2_for_alice_crate_limit)
model.addConstr(c_distance <= alice_efficiency * dollars_to_alice_for_c * value_3_for_alice_crate_limit)
model.addConstr(d_distance <= bob_efficiency * dollars_to_bob_for_d)
model.addConstr(e_distance <= 0 * dollars_for_e)
model.optimize()
for var in model.getVars():
print(f"{var.varName}: {var.x}")
print(model.ObjVal)
And the last step would be to generalize this to handle various inputs, and format the final variable values into the desired output.
Upvotes: 5