This question is lacking a good model. Different formulations of the objective/losses will lead to completely different behaviour/solutions.
Here is a mixed-integer programming approach (as your problem is already np-hard):
Model A
- First preference gives satisfaction X, second: Y, ... (9, 4, 1, 0) = inverse squared
- This is equivalent to a least-squares solution / l2-norm solution (= bigger misses are harder penalized than smaller ones)
- We want to maximize the total satisfaction
Effects of the model
- No try of giving each person the same amount of lollies
Code
import math
import numpy as np
from cvxpy import *
# PROBLEM
# -------
# colors = orange, green, red, blue
n_colors = 4
n_lollies_per_color = 5
n_lollies = n_colors * n_lollies_per_color
prefs = np.asarray([[0, 1, 2, 3],
[1, 3, 2, 0],
[0, 2, 3, 1]])
n_persons = prefs.shape[0]
n_lollies_per_person = n_lollies / n_persons
# SOLVER
# ------
per_person_per_lolly_satisfaction_vector = np.asarray([[n_colors -1 - np.where(prefs[p, :] == c)[0]
for c in range(n_colors)]
for p in range(n_persons)]).reshape((3,4))
# Decision-vars
X = Int(n_persons, n_colors)
# Constraints
constraints = []
# non-zero lollies
constraints.append(X >= 0)
# exactly n_lollies_per_color are distributed
for c in range(n_colors):
constraints.append(sum_entries(X[:,c]) == n_lollies_per_color)
# Objective
objective = Maximize(sum_entries(mul_elemwise(np.square(per_person_per_lolly_satisfaction_vector), X)))
problem = Problem(objective, constraints)
problem.solve(verbose=False)
print(problem.value)
print(np.round(X.value)) # Rows -> persons; Columns -> amount of lollies they got
Output
129.9999999837688
[[ 1. 0. 0. 0.]
[ 0. 5. 0. 5.]
[ 4. 0. 5. 0.]]
Observations
- Person one only gets 1 lolly while the others get 9/10 (as expected)
Model B
- Same as A, but we are only allowing a obtained-number-of-lollies-difference of 1 between each person -> this is hard-coded into the constraints (more powerful soft-constraints are much harder to implement -> keyword: convexity)
Effects
- Fair distribution of lollies in regards to the amount of lollies obtained
Code Difference
Just add these constraints:
# lower and upper bound on lollies received -> at most 1 lolly less than everyone else
for p in range(n_persons):
constraints.append(sum_entries(X[p, :]) >= math.floor(n_lollies_per_person))
constraints.append(sum_entries(X[p, :]) <= math.ceil(n_lollies_per_person))
Output
119.99999984107899
[[ 5. 0. 0. 1.]
[-0. 5. 0. 2.]
[ 0. 0. 5. 2.]]
Observations
- We obtained a fair distribution at a cost:
- We lost total satisfaction (120 vs. 130)