Hi I'm trying to generate all possible combinations of workers to buildings. (let me explain my scenario):
I'm playing MineColonies on minecraft. In this mod you have colonists whom can be assigned jobs at buildings. These workers have skills and a score assigned to them. (like Agility: 20, Strength:5 etc) and the work at the buildings are performed better when assigned a colonists whose skills compliment it...
So I've created a database of all the workers and buildings and want to optimize which workers work at which buildings.
buildings_dict = {1: ['Strength', 'Focus'],
2: ['Adaptability', 'Athletics'],
3: ['Knowledge', 'Dexterity'],
4: ['Adaptability', 'Knowledge'],
6: ['Stamina', 'Athletics'],
5: ['Athletics', 'Stamina'],
7: ['Focus', 'Agility'],
8: ['Dexterity', 'Creativity'],
9: ['Strength', 'Focus'],
10: ['Adaptability', 'Stamina'],
11: ['Agility', 'Adaptability'],
12: ['Mana', 'Knowledge'],
13: ['Strength', 'Stamina'],
14: ['Athletics', 'Strength'],
15: ['Creativity', 'Dexterity'],
16: ['Knowledge', 'Mana'],
17: ['Agility', 'Adaptability']}
workers_dict = {3: {'Mana': 30,
'Focus': 1,
'Agility': 3,
'Stamina': 3,
'Knowlege': 30,
'Strenght': 8,
'Athletics': 13,
'Dexterity': 6,
'Creativity': 10,
'Adaptability': 10,
'Intelligence': 10},
4: {'Mana': 29,
'Focus': 32,
'Agility': 22,
'Stamina': 28,
'Knowlege': 21,
'Strenght': 30,
'Athletics': 20,
'Dexterity': 31,
'Creativity': 31,
'Adaptability': 8,
'Intelligence': 18},
5: {'Mana': 13,
'Focus': 1,
'Agility': 9,
'Stamina': 27,
'Knowlege': 9,
'Strenght': 13,
'Athletics': 15,
'Dexterity': 21,
'Creativity': 16,
'Adaptability': 13,
'Intelligence': 28},
6: {'Mana': 17,
'Focus': 14,
'Agility': 10,
'Stamina': 17,
'Knowlege': 13,
'Strenght': 5,
'Athletics': 10,
'Dexterity': 15,
'Creativity': 1,
'Adaptability': 11,
'Intelligence': 4},
7: {'Mana': 1,
'Focus': 8,
'Agility': 6,
'Stamina': 27,
'Knowlege': 11,
'Strenght': 17,
'Athletics': 30,
'Dexterity': 1,
'Creativity': 5,
'Adaptability': 11,
'Intelligence': 5},
8: {'Mana': 6,
'Focus': 1,
'Agility': 12,
'Stamina': 30,
'Knowlege': 20,
'Strenght': 15,
'Athletics': 30,
'Dexterity': 9,
'Creativity': 17,
'Adaptability': 30,
'Intelligence': 19},
9: {'Mana': 5,
'Focus': 7,
'Agility': 19,
'Stamina': 5,
'Knowlege': 22,
'Strenght': 18,
'Athletics': 26,
'Dexterity': 10,
'Creativity': 24,
'Adaptability': 20,
'Intelligence': 22},
10: {'Mana': 8,
'Focus': 12,
'Agility': 27,
'Stamina': 3,
'Knowlege': 17,
'Strenght': 1,
'Athletics': 5,
'Dexterity': 9,
'Creativity': 7,
'Adaptability': 29,
'Intelligence': 1},
11: {'Mana': 1,
'Focus': 4,
'Agility': 5,
'Stamina': 30,
'Knowlege': 16,
'Strenght': 11,
'Athletics': 28,
'Dexterity': 11,
'Creativity': 5,
'Adaptability': 12,
'Intelligence': 4},
12: {'Mana': 7,
'Focus': 1,
'Agility': 17,
'Stamina': 25,
'Knowlege': 23,
'Strenght': 4,
'Athletics': 8,
'Dexterity': 26,
'Creativity': 15,
'Adaptability': 29,
'Intelligence': 22},
13: {'Mana': 2,
'Focus': 1,
'Agility': 5,
'Stamina': 21,
'Knowlege': 24,
'Strenght': 18,
'Athletics': 20,
'Dexterity': 10,
'Creativity': 12,
'Adaptability': 30,
'Intelligence': 5},
14: {'Mana': 9,
'Focus': 16,
'Agility': 14,
'Stamina': 25,
'Knowlege': 14,
'Strenght': 24,
'Athletics': 30,
'Dexterity': 9,
'Creativity': 19,
'Adaptability': 23,
'Intelligence': 18},
15: {'Mana': 23,
'Focus': 15,
'Agility': 5,
'Stamina': 12,
'Knowlege': 24,
'Strenght': 12,
'Athletics': 20,
'Dexterity': 29,
'Creativity': 5,
'Adaptability': 19,
'Intelligence': 12},
17: {'Mana': 21,
'Focus': 23,
'Agility': 30,
'Stamina': 18,
'Knowlege': 27,
'Strenght': 7,
'Athletics': 30,
'Dexterity': 10,
'Creativity': 5,
'Adaptability': 22,
'Intelligence': 18},
18: {'Mana': 11,
'Focus': 11,
'Agility': 4,
'Stamina': 7,
'Knowlege': 28,
'Strenght': 11,
'Athletics': 20,
'Dexterity': 28,
'Creativity': 13,
'Adaptability': 12,
'Intelligence': 30},
19: {'Mana': 11,
'Focus': 11,
'Agility': 4,
'Stamina': 7,
'Knowlege': 28,
'Strenght': 11,
'Athletics': 20,
'Dexterity': 28,
'Creativity': 13,
'Adaptability': 12,
'Intelligence': 30},
20: {'Mana': 15,
'Focus': 20,
'Agility': 28,
'Stamina': 22,
'Knowlege': 18,
'Strenght': 15,
'Athletics': 23,
'Dexterity': 19,
'Creativity': 20,
'Adaptability': 27,
'Intelligence': 20},
21: {'Mana': 30,
'Focus': 7,
'Agility': 9,
'Stamina': 7,
'Knowlege': 30,
'Strenght': 3,
'Athletics': 6,
'Dexterity': 17,
'Creativity': 4,
'Adaptability': 11,
'Intelligence': 28},
22: {'Mana': 9,
'Focus': 10,
'Agility': 28,
'Stamina': 26,
'Knowlege': 1,
'Strenght': 8,
'Athletics': 5,
'Dexterity': 26,
'Creativity': 1,
'Adaptability': 14,
'Intelligence': 16},
23: {'Mana': 4,
'Focus': 14,
'Agility': 19,
'Stamina': 5,
'Knowledge': 21,
'Strength': 25,
'Athletics': 12,
'Dexterity': 23,
'Creativity': 26,
'Adaptability': 21,
'Intelligence': 22},
24: {'Mana': 1,
'Focus': 1,
'Agility': 18,
'Stamina': 24,
'Knowledge': 25,
'Strength': 20,
'Athletics': 9,
'Dexterity': 14,
'Creativity': 19,
'Adaptability': 30,
'Intelligence': 7},
25: {'Mana': 12,
'Focus': 13,
'Agility': 21,
'Stamina': 23,
'Knowledge': 11,
'Strength': 16,
'Athletics': 18,
'Dexterity': 24,
'Creativity': 1,
'Adaptability': 20,
'Intelligence': 1},
26: {'Mana': 10,
'Focus': 14,
'Agility': 12,
'Stamina': 27,
'Knowledge': 17,
'Strength': 24,
'Athletics': 23,
'Dexterity': 21,
'Creativity': 5,
'Adaptability': 5,
'Intelligence': 28},
27: {'Mana': 11,
'Focus': 23,
'Agility': 21,
'Stamina': 12,
'Knowledge': 15,
'Strength': 24,
'Athletics': 17,
'Dexterity': 12,
'Creativity': 1,
'Adaptability': 11,
'Intelligence': 9},
28: {'Mana': 7,
'Focus': 21,
'Agility': 22,
'Stamina': 21,
'Knowledge': 14,
'Strength': 15,
'Athletics': 9,
'Dexterity': 16,
'Creativity': 2,
'Adaptability': 11,
'Intelligence': 5},
29: {'Mana': 12,
'Focus': 25,
'Agility': 29,
'Stamina': 6,
'Knowledge': 7,
'Strength': 10,
'Athletics': 14,
'Dexterity': 15,
'Creativity': 6,
'Adaptability': 13,
'Intelligence': 29},
30: {'Mana': 21,
'Focus': 17,
'Agility': 8,
'Stamina': 21,
'Knowledge': 22,
'Strength': 22,
'Athletics': 26,
'Dexterity': 13,
'Creativity': 15,
'Adaptability': 24,
'Intelligence': 13}}
Sorry for the long code block and yes I realize the ids aren't necessarily correct(wanted to make it reproducible).
So I'm using itertools.permutations
to get all combinations of workers to buildings:
import itertools
workers_ls = list(workers_dict.keys())
combinations = list(itertools.permutations(workers_ls, len(buildings_dict))
(I plan to score the combinations afterwards)
This of evidently has never completed running since it's something like 27! = 1×10²⁸. I'm wondering whether there's another solution for my problem or a way to determine the best solution without going through every combination. (I'm willing to work in other coding languages)
Thanks!
I am assuming that you want to maximize the sum of total production. For example, when no workers are assigned, the total production is zero (or some constant that does not depend on worker assignment). If you pair up a worker with Agility
2 and Focus
3 with a building with attributes [Agility, Focus]
, you add 2+3=5
to the total production.
Problems like this are usually solved with linear programming. I will use pulp
to help formulate the linear programming problem. I would also recommend checking out the Julia
package JuMP
.
The actual rule for computing total production can be more complicated. You can still use linear programming if (1) you can define some analog of the production matrix and (2) total production can be expressed as sum of productions of (worker, building) pairs.
Here are two ways to solve this problem. The first allows multiple workers per building, the second does not.
import pandas as pd
import numpy as np
# !pip install pulp
import pulp
df_buildings = pd.DataFrame(buildings_dict).T
df_workers = pd.DataFrame(workers_dict).T
# there are a few typos, e.g. Strenght vs. Strength and Knowlege vs. Knowledge
# let's fix this first
df_workers.Knowledge.fillna(df_workers.Knowlege, inplace=True)
df_workers.Strength.fillna(df_workers.Strenght, inplace=True)
del df_workers["Strenght"], df_workers["Knowlege"]
# fixing some notation
workers = df_workers.index.tolist() # list of workers
buildings = df_buildings.index.tolist() # list of building
# next, we define production matrix
# production[i,j] will contain the productivity of
# worker i when assigned to building j
# you could vectorize this step, though it seems fast enough here
production = pd.DataFrame(index=workers, columns=buildings)
for i in df_workers.index:
for j in df_buildings.index:
production.loc[i, j] = df_workers.loc[i, df_buildings.loc[j]].sum()
print(production.head())
# 1 2 3 4 6 5 7 8 9 10 11 12 13 14 15 16 17
# 3 9 23 36 40 16 16 4 16 9 13 13 60 11 21 16 60 13
# 4 62 28 52 29 48 48 54 62 62 36 30 50 58 50 62 50 30
# 5 14 28 30 22 42 42 10 37 14 40 22 22 40 28 37 22 22
# 6 19 21 28 24 27 27 24 16 19 28 21 30 22 15 16 30 21
# 7 25 41 12 22 57 57 14 6 25 38 17 12 44 47 6 12 17
prob = pulp.LpProblem("MineColoniesProblem", pulp.LpMaximize)
# in the solved problem, assignment[i,j] == 1 whenever i is assigned to j
assignment = pulp.LpVariable.dicts("Assignment", (workers, buildings), cat="Binary")
# our objective is to maximize the sum of production
prob += sum(assignment[i][j] * production.loc[i,j]
for i in workers for j in buildings)
# each worker can be assigned to at most one building:
for i in workers:
prob += sum(assignment[i][j] for j in buildings) <= 1
prob.solve()
# make sure that we got an optimal solution
assert prob.status == 1
# generically, we get an integer solution
assignment_dict = {i: j for i in workers for j in buildings
if assignment[i][j].varValue == 1}
print(f"Total production is {prob.objective.value()}") # 1401
# here is the the solution
# assignment_dict_saved = {3: 12, 4: 1, 5: 5, 6: 16, 7: 5, 8: 6, 9: 2, 10: 17, 11: 6, 12: 10, 13: 4, 14: 6, 15: 3, 17: 7, 18: 3, 19: 3, 20: 11, 21: 12, 22: 17, 23: 15, 24: 4, 25: 10, 26: 13, 27: 9, 28: 7, 29: 7, 30: 2}
prob = pulp.LpProblem("MineColoniesProblem", pulp.LpMaximize)
# in the solved problem, assignment[i,j] == 1 whenever i is assigned to j
assignment = pulp.LpVariable.dicts("Assignment", (workers, buildings), cat="Binary")
# our objective is to maximize the sum of production
prob += sum(assignment[i][j] * production.loc[i,j]
for i in workers for j in buildings)
# each worker can be assigned to at most one building:
for i in workers:
prob += sum(assignment[i][j] for j in buildings) <= 1
# each building has at most one worker
for j in buildings:
prob += sum(assignment[i][j] for i in workers) <= 1
prob.solve()
# make sure that we got an optimal solution
assert prob.status == 1
# generically, we get an integer solution
assignment_dict = {i: j for i in workers for j in buildings
if assignment[i][j].varValue == 1}
# assignment_dict_saved = {3: 16, 4: 9, 7: 5, 8: 2, 10: 11, 11: 6, 12: 10, 14: 14, 18: 3, 19: 8, 20: 17, 21: 12, 23: 15, 24: 4, 26: 13, 27: 1, 29: 7}
print(f"Total production is {prob.objective.value()}") # 929
We can see that the total production is higher when we allow multiple workers per building. This is expected, since the maximization problem has fewer constraints.
We can also compare the optimized production to production when workers are assigned to buildings at random. Vertical lines correspond to optimal production. It looks like we are doing alright.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With