I've taken on running brackets on my bowling league. It's small league, just 60 people (12 teams). Every week we bowl three games. Each person can be in multiple brackets as their scores are not, to a certain extent, determined by who they are on the lanes with.
I go around each week and ask who wants to be in brackets and how many they want to be in. So a typical input would look like
array[_] of 1..maxEntries: reqEntries =
[ 1, maxEntries, 2, maxEntries, 1, 1, maxEntries, 1, 2, maxEntries, 1, 1, 3, maxEntries, 2, 1, 1, maxEntries, 1, 2
];
That is, twenty people want to get into brackets. Most want to get into just one bracket, a few want to be in at most 2 brackets, one wants to get into at most 3 brackets, and six people want to be in as many brackets as we can place them in.
There are a few additional constraints that make this a bit more interesting:
At first I was just randomly seeding people in brackets. But in the above example, that results in at most 5 brackets being generated most of the time. Trying to generate all possible solutions to find the maximum number of brackets possible is not viable because the solution space is so large. I asked some colleagues and they suggested giving MiniZinc a try.
After trying a couple of different approaches, I've come up with the following
include "alldifferent.mzn";
include "member.mzn";
int: maxBrackets = 8;
int: maxEntries = maxBrackets;
array[_] of 1..maxEntries: reqEntries =
[ 1, maxEntries, 2, maxEntries, 1, 1, maxEntries, 1, 2, maxEntries, 1, 1, 3, maxEntries, 2, 1, 1, maxEntries, 1, 2 ];
set of int: Bowler = index_set(reqEntries);
set of int: Bracket = 1 .. maxBrackets;
set of int: Slot = 1..8;
array[Bracket,Slot] of var Bowler: brackets;
array[Bowler] of var 1..maxEntries: entries;
% everyone should go in a bracket
% and actual entries should not exceed the requested entries
% and "store" the count for output
constraint forall (bwl in Bowler)(
let { var int: c = count(b in Bracket, s in Slot)(brackets[b, s] = bwl); }
in
1 <= c
/\ c <= reqEntries[bwl]
/\ entries[bwl] = c
);
% each slot in each bracket is a different person
constraint forall (bkt in Bracket)(
all_different(row(brackets, bkt))
);
% each matchup is unique
array[1..(maxBrackets*4)] of var set of Bowler: matchups;
constraint forall (bkt in Bracket, s in Slot)(
let { var int: i = 4*(bkt - 1) + ceil(s / 2); }
in card(matchups[i]) = 2 /\ member(matchups[i], brackets[bkt,s])
);
constraint all_different(matchups);
solve satisfy;
output
[ "reqEntries = "
, show(reqEntries)
, "\nentries = "
, show(entries)
, "\nbrackets = \n"
, show2d(brackets)
, "\nmatchups = \n"
, show(matchups)
]
In the MiniZinc IDE on my MacOS laptop, OR Tools CP-SAT 9.11.4210 is able to find a solution in ~1.7s. HiGHS 1.7.2 is able to find a solution in ~13.5s.
Overall, I'm pretty happy with that result but I'd like to see if it can be improved. Mainly, what I'd like to be able to do is have it automatically find a solution which maximizes the number of brackets. With this solution, I'd have to try with maxBrackets
equal to 6, 7, 8, or 9 to see how many brackets might be created. I wouldn't mind that so much, but when trying with maxBrackets = 9
, instead of telling me it's not satisfiable, it just runs for a very long time so I end up stopping it. Obviously it is trying to find a solution in a large potential space and isn't able to find one, but can't provable say there isn't one without finishing the search. I'd like to know if anyone sees a way to improve the model or has an idea for a completely different model that might make this work better. Otherwise I'll just have to put a time limit on running MiniZinc (something like 20s).
I was able to find a solution which satisfied both the missing requirements.
There are two key changes that I made:
array[Bracket,Slot] of var Bowler: brackets
, we now have array[1..maxBrackets,Bowler] of var 0..numEntrants: brackets
. This shuttle shift of making any non-zero value an inverse simplifies the constraints and I believe it narrows the solution space, allowing solutions to be found much faster and even determine if it's unsatisfiable in a a short amount of time.brackets
array at the fixed size of the maximum number of brackets, but introduced a variable numBrackets
, which is used to bound how many brackets we actually try to create within brackets
. This means that everywhere we were looking at brackets using brkt in 1..maxBrackets
, we now use brkt in 1..numBrackets
. This does require that we're careful to constrain the values in brackets
we aren't using to zero, but that's pretty simple.Using OR-Tools as the solver we are able to find a solution, with the original input, with numBrackets = 8
in just a few seconds.
include "alldifferent.mzn";
include "nvalue_fn.mzn";
int: maxEntries = 10;
array[_] of 1..maxEntries: reqEntries;
int: totalEntries = sum(reqEntries);
int: numEntrants = length(reqEntries);
int: maxBrackets = totalEntries div 8;
int: minBrackets = numEntrants div 8;
set of int: Bowler = 1..numEntrants;
var int: numBrackets;
constraint (minBrackets <= numBrackets /\ numBrackets <= maxBrackets);
array[1..maxBrackets,Bowler] of var 0..numEntrants: brackets;
% brackets not in the solution should all be zero'd out
constraint forall(nonBkt in numBrackets+1..maxBrackets, bwl in Bowler)(
brackets[nonBkt,bwl] = 0
);
% brackets can only have 8 people in them
constraint forall(bkt in 1 .. numBrackets)(
count(bwl in Bowler)(brackets[bkt,bwl] != 0) = 8
);
% when a person is in a bracket, they are matched up with someone who is not themself
constraint forall(bkt in 1 .. numBrackets, bwl in Bowler)(
brackets[bkt,bwl] != 0 ->
brackets[bkt, brackets[bkt, bwl]] = bwl /\ brackets[bkt, bwl] != bwl
);
array[Bowler] of var 0..maxEntries: entries;
% everyone is in a bracket if there enough brackets
% and they are not in any more than they requested
constraint forall(bwl in Bowler)(
let { var int: c = count(bkt in 1..numBrackets)(brackets[bkt, bwl] != 0); }
in
(numBrackets * 8 > numEntrants -> 0 < c)
/\ c <= reqEntries[bwl]
/\ entries[bwl] = c
);
constraint forall (bwl in Bowler)(
all_different_except_0(col(brackets, bwl))
);
array[Bowler] of var int: leftovers;
constraint forall(bwl in Bowler)(
leftovers[bwl] = reqEntries[bwl] - entries[bwl]
);
constraint forall(x in Bowler, y in Bowler)(
(reqEntries[x] = reqEntries[y]) ->
leftovers[y] - 1 <= leftovers[x] /\ leftovers[x] <= leftovers[y] + 1
);
solve maximize numBrackets;