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!
Advertisement
Answer
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.
Setup
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
Allowing Multiple Workers per Building
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}
At most one worker per building
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.