wassign is a tool to solve many kinds of assignment or bipartite matching problems that are preference based (like the assignment of college students to exercise groups). Here are some useful links to the most relevant parts of this document if you want to get started with wassign:
We will use the following scenario throughout this introduction in order to introduce all the features of wassign:
You are planning a convention where every participant has given preferences to different workshops that will be held at this convention. There are four events:
and 10 participants which gave the following preferences (between 0 and 10) to each of the workshops:
Ethan | Fanny | Gavin | Hanna | Isaac | July | Kevin | Lily | Mark | Norah | |
---|---|---|---|---|---|---|---|---|---|---|
A | 10 | 8 | 10 | 5 | 8 | 8 | 0 | 10 | 10 | 9 |
B | 6 | 10 | 4 | 0 | 5 | 0 | 0 | 9 | 3 | 5 |
C | 0 | 0 | 1 | 0 | 5 | 0 | 10 | 6 | 0 | 1 |
D | 5 | 4 | 7 | 10 | 10 | 10 | 0 | 5 | 0 | 10 |
We now want to assign each of our 10 participants to one of the four events using wassign. To do this, we first have to translate our problem into terms that wassign can understand. We can use two core concepts of wassign to solve our example problem:
You can see that in our example, the workshops are the choices and our participants are the choosers. We now have to put our data into an input file that wassign understands. For our example, a basic input file would look like this:
+choice("How to become famous", bounds(1, 4));
+choice("Paleo cooking for beginners", bounds(2, 9));
+choice("Left-handed scissors: A critical review", bounds(2, 5));
+choice("Should you invest in bitcoin now?", bounds(1, 6));
+chooser("Ethan", [10, 6, 0, 5]);
+chooser("Fanny", [8, 10, 0, 4]);
+chooser("Gavin", [10, 4, 1, 7]);
+chooser("Hanna", [5, 0, 0, 10]);
+chooser("Isaac", [8, 5, 5, 10]);
+chooser("July", [8, 0, 0, 10]);
+chooser("Kevin", [0, 0, 10, 0]);
+chooser("Lily", [10, 9, 6, 5]);
+chooser("Mark", [10, 3, 0, 0]);
+chooser("Norah", [9, 5, 1, 10]);
The details of the syntax required for these input files (and other neat features like reading things from CSV files so you don’t have to type all these lines by yourself) can be found in the manual. If we put this input file through wassign with
wassign -i our-input-file.txt -o output-file
we promptly get back the following result as a CSV file (called output-file.assignment.csv
):
"Choice", "Generated Slot"
"Ethan", "Paleo cooking for beginners"
"Fanny", "Paleo cooking for beginners"
"Gavin", "How to become famous"
"Hanna", "Should you invest in bitcoin now?"
"Isaac", "Left-handed scissors: A critical review"
"July", "Should you invest in bitcoin now?"
"Kevin", "Left-handed scissors: A critical review"
"Lily", "Paleo cooking for beginners"
"Mark", "How to become famous"
"Norah", "Should you invest in bitcoin now?"
Which translates to the following assignment:
You may note the “Generated Slot” column header in the output file. This is present because we did not specify any slots in our input (so wassign generated one for us); in the next section we will look at slots and what they can be used for.
Lets say that the time schedule of our convention (which is held in a single day) looks as follows:
10:00 – 11:00 | Welcome speech |
11:00 – 12:30 | Worshops I |
12:30 – 14:00 | Lunch break |
14:00 – 15:30 | Workshops II |
15:30 – 17:00 | After-convention party |
As you can see, we actually have two timeslots where workshops will be held. But which workshop should go into which timeslot? We could now go on and assign our workshops to the timeslots by hand, but we can actually use wassign to do this for us based on the preferences the participants gave, so workshops are assigned to timeslots in such a way that the preferences of our participants can be satisfied as much as possible.
To do this, we use another core concept of wassign called slots. Slots are simply “buckets” where the choices will be distributed to. Then, every participant gets assigned to a choice in every slot.
We just have to modify our input file slightly:
+slot("Workshops I");
+slot("Workshops II");
+choice("How to become famous", bounds(1, 4));
+choice("Paleo cooking for beginners", bounds(2, 9));
+choice("Left-handed scissors: A critical review", bounds(2, 5));
+choice("Should you invest in bitcoin now?", bounds(1, 6));
+chooser("Ethan", [10, 6, 0, 5]);
+chooser("Fanny", [8, 10, 0, 4]);
... (the rest of our "old" input file) ...
If we now input this into wassign with
wassign -i our-input-file.txt -o output-file -t 3s
it will give us the following two output files after 3 seconds (the -t 3s
option we gave wassign this time tells the program to search exactly 3 seconds for the best possible solution):
A file called output-file.scheduling.csv
"Choice", "Slot"
"How to become famous", "Workshops II"
"Paleo cooking for beginners", "Workshops II"
"Left-handed scissors: A critical review", "Workshops I"
"Should you invest in bitcoin now?", "Workshops I"
describing the scheduling of the workshops into the slots and a file called output-file.assignment.csv
"Chooser", "Workshops I", "Workshops II"
"Ethan", "Should you invest in bitcoin now?", "Paleo cooking for beginners"
"Fanny", "Should you invest in bitcoin now?", "Paleo cooking for beginners"
"Gavin", "Should you invest in bitcoin now?", "How to become famous"
"Hanna", "Should you invest in bitcoin now?", "How to become famous"
"Isaac", "Left-handed scissors: A critical review", "Paleo cooking for beginners"
"July", "Should you invest in bitcoin now?", "How to become famous"
"Kevin", "Left-handed scissors: A critical review", "Paleo cooking for beginners"
"Lily", "Left-handed scissors: A critical review", "Paleo cooking for beginners"
"Mark", "Left-handed scissors: A critical review", "How to become famous"
"Norah", "Should you invest in bitcoin now?", "Paleo cooking for beginners"
describing the assignment of the participants into the workshops.
As you can see, we got exactly what we were looking for: wassign tells us which workshop should go into which time slot and at the same time gives us the corresponding assignment for the participants.
We now assume that Lily called us beforehand and told us that she can’t attend the paleo cooking workshop because she is a vegetarian. We can use another feature of wassign called constraints to tell wassign that Lily must not be assigned to the paleo workshop under any circumstance. We do this by adding the following line to our input file:
+constraint( chooser("Lily").choices.contains_not(choice("Paleo")) );
Note that we do not have to specify the full name of the workshop; if there are no ambiguities, you can just use the first word of the name (in this case “Paleo”).
wassign will now give us a different solution where Lily is not in the paleo workshop. Try it out!
If you want to get an exhaustive overview over all features of wassign, you should consult the manual. Examples of things wassign can also do that were not covered in this short introduction:
+choice("XYZ", optional)
.+choice("XYZ", parts(3))
.To build wassign under Linux, you need the following prerequisites:
After making sure all prerequisites are met you can build wassign:
git clone --recursive https://github.com/MaximilianAzendorf/wassign
cd wassign
mkdir build && cd build
cmake ..
make
Binaries will be written into the directory build/bin
. If you want to execute the tests you can run the wassign-test
executable.
Building on Windows is currently untested.
You can build this documentation using pandoc and the respective makefile. Some figures require a LaTeX distribution, inkscape and ghostscript.
The following section contains the complete documentation of all wassign features and how to use them.
Input files are chaiscript scripts, so all typical programming constructs like variables (var x = ...
), loops (for(...)
), conditionals (if(...)
) and others are available to construct the input. In addition, the following API is used to interact with wassign.
slot(name) |
---|
Creates a new slot with the given name or returns the slot with the given name if a slot with such name was already created. Note that if you create a new slot you still have to add it to the input with add or + . |
choice(name) choice(name, args...) |
---|
Creates a new choice with the given name and arguments (see choice arguments for more information) or (if just a name is given) returns the choice with the given name if a choice with such name was already created. Note that if you create a new choice you still have to add it to the input with add or + . |
chooser(name) chooser(name, preferences) |
---|
Creates a new chooser with the given name and preference list (e.g. chooser("Karl", [100, 50, 0]) ) or (if just a name is given) returns the chooser with the given name if a chooser with such name was already created. The preferences have to be in the same order in which the corresponding choices were added to the input. Note that if you create a new choice you still have to add it to the input with add or + . |
constraint(expression) |
---|
Creates a new constraint from the given expression. For more information on how to construct constraint expressions, see the respective section Note that if you create a new constraint you still have to add it to the input with add or + . |
add(arg) +arg (operator) |
---|
Adds the given argument arg (a newly created slot, choice, chooser or constraint) to the input. |
min(x) |
The choice must have at least x participants (e.g. +choice("Foo", min(5)) ). The default is 1. |
max(x) |
The choice must have at most x participants (e.g. +choice("Foo", max(10) ). The default is 1. |
bounds(x, y) |
The choice must have at least x and at most y participants. This is the same as min(x), max(y) . |
parts(x) |
The choice has x parts. This means that the choice will be scheduled to x consecutive slots (in the order they were added to the input), and each chooser assigned to this choice will have this choice in the assignment at each of the slots. |
optional |
The choice is optional, this means that this choice may not be scheduled at all. If the choice is not scheduled there will also be no chooser that get assigned to this choice. |
optional_if(x) |
The choice is optional if x is true . |
As an example, a choice named Foo
that must have between 5 and 12 participants, is optional and has 3 parts would look like this in the input:
+choice("Foo", bounds(5, 12), parts(3), optional);
Constraints are relations between two constaint objects. All slots, choices and choosers are simple constraint objects. In addition to them, the following derived constraint objects can be used to construct constraints:
SLOT.choices |
---|
A list constraint object describing all choices that are scheduled to the slot SLOT . |
SLOT.size |
---|
A numerical constraint object describing the number of choices that are scheduled to the slot SLOT . |
CHOICE.slot CHOICE.slot(part) |
---|
A simple constraint object describing the the slot to which choice CHOICE is scheduled to. You can specify a part index (starting at 0) to refer to a specific part of the choice; by default, part=0 . |
CHOICE.choosers |
---|
A list constraint object describing the the list of choosers that are assigned to the choice CHOICE . |
CHOOSER.choices |
---|
A list constraint object describing the the list of choices that the chooser CHOOSER is assigned to. |
Relations between simple constraint objectsleft == right (equality)left != right (inequality) |
---|
States that the simple constraint object left must be equal (or inequal) to the simple constraint object right . |
Relations between numerical constraint objectsleft == right (equality)left != right (inequality)left > right (greater-than)left >= right (greater-or-equal-than)left < right (less-than)left <= right (less-or-equal-than) |
---|
States that the given relation must hold true between the numerical constraint object left and the numerical constraint object right . Note that right can also be a numerical constant. |
Relations between list constraint objectsleft == right (equality)left != right (inequality)left.contains(right) (contains-relation)left.contains_not(right) (contains-not-relation) |
---|
States that the given relation must hold true between the list constraint object left and the list constraint object right , or (in the case of contains and contains_not ) the simple constraint object right . |
choice("X").slot != slot("Y")
: Choice “X” must not be in slot “Y”.slot("Y").size <= 5
: Slot “Y” must have 5 or less choices scheduled to it.chooser("X").choices.contains_not(choice("Y"))
: Chooser “Y” must not be assigned to choice “Y”.read_csv(filename) read_csv(filename, separator) |
---|
Reads the given file as a CSV file, optionally specifying a separator (the default is , ). |
CSVFILE.row(n) CSVFILE[n] |
---|
Returns the row with number n (numbering starting at 0) of the CSVFILE (returned by read_csv ). Rows are just plain lists of the row values, so individual values of a row can be accessed with the [] operator. |
CSVFILE.rows |
---|
Returns all rows of the CSVFILE (returned by read_csv ). |
Given is the following CSV file called workshops.csv
:
"Choice", "minimum", "maxiumum", "optional?"
"A", 5, 10, "no"
"B", 5, 20, "no"
"C", 10, 30, "yes"
We can then use this file to generate choices for our input as follows:
var file = read_csv("workshops.csv");
for(row : file.rows.slice(1, end))
{
+choice(row[0], bounds(row[1], row[2]), optional_if(row[3] == "yes"));
// or add(choice(...))
}
Note that we have to skip the first row (with .slice(1, end)
) because it contains the column headers.
LIST.slice(x, y) |
---|
Returns a list that only contains the element of the list LIST with indices between x (inclusive) and y (inclusive). Note that index numbering starts at 0. You can give end as the value for y as a replacement for LIST.length - 1 . |
range(x, y) |
---|
Returns a list that contains all integers between x (inclusive) and y (inclusive) in order. |
-h , --help |
Show a short summary of all program options. |
--version |
Show the current version of wassign. |
-i [file] , --input [file] |
Read the input from the specified file(s). If this option is present more than once, all files will be read in the order they were given. |
-o [prefix] , --output [prefix] |
Write the output to files starting with the given prefix. The files generated will be named [prefix].assignment.csv and [prefix].scheduling.csv . |
-v [n] , --verbosity [n] |
A number n between 0 and 3 indicating how much status information should be output. |
-a , --any |
If this option is given, wassign will not optimize any solution and will just return the first solution it finds. |
-p [exp] , --pref-exp [exp] |
Sets the preference exponent to the given value. See the respective section for more information. |
-t [time] , --timeout [time] |
Sets the optimization timeout. The syntax for this argument is described under the respective section. |
--cs-timeout [time] |
Sets the timeout for attempting to satisfy critical sets of a certain preference level. Higher values may lead to better initial solutions, but it may take longer to find an initial solution in the first place. The syntax for this argument is described under the respective section. |
--no-cs |
If this option is given, no critical set analysis is performed. |
-j [n] , --threads [n] |
Specifies the maximum number of computation threads. By default, wassign will use as many threads as there are logical CPU cores on the system. |
-n [n] , --max-neighbors [n] |
Specifies the maximum number of neighbor schedulings that will be explored per hill climbing iteration. |
-g , --greedy |
If this option is given, wassign will not use the worst-preference scoring as a primary score and will instead just use sum-based scoring instead. |
The preference exponent is a parameter of wassign that directly affects which solutions are considered “fairer” than others.
As a rule of thumb: The higher the preference exponent, the more “even” the assignment will be, in the sense that e.g. wassign will prefer to give many people their second choice instead of giving few their first and many their third.
Program options like --timeout
require a time string that has the following format: A time string consists of one or more components of the form [number][unit]
, concatenated without spaces. Examples:
10s
is a timespan of ten seconds.5h
is a timespan of five hours.1d30m
is a timespan of one day and 30 minutes.2w3d5h7m11s
is a timespan of 2 weeks, 3 days, 5 hours, 7 minutes and 11 seconds.The algorithm is based on shotgun hill climbing. More details about each step are found under the respective section:
Perform Critical Set Analysis: This step is optional (and is only performed at the very beginning), but the results of the critical set analysis are used in the scheduling as well as the assignment solver to drastically decrease the time it takes for good solutions to be found.
Solving the scheduling: Find a possible scheduling using a randomized depth-first search.
Solving the assignment: Find the best possible assignment for the given scheduling by constructing a corresponding linear optimization or (depending on the given extra constraints) mixed integer programming problem instance and solving it using an MIP solver.
Perfoming hill climbing: Find a “neighbor scheduling” of the given scheduling (by moving a single event to another slot, if possible) and solve the assignment again. Repeat this until the solution of the neighbor scheduling is worse than the one before (this is the hill climbing part).
Performing shotgun hill climbing: Do the above again and again until the timeout is reached (this is the shotgun part). The best solution found by then is the output of the program.
This part of the manual is rather theory-heavy, so we have to introduce some common notation that is used from here onwards:
\(\mathbb{S}\), \(\mathbb{W}\) and \(\mathbb{P}\) are the non-empty sets of slots, choices and choosers respectively.
\(\min(w)\in\mathbb{N}\) and \(\max(w)\in\mathbb{N}\) are the minimum and maximum number of choosers of a choice \(w\in\mathbb{W}\). Generally, \(0<\min(w)\leq\max(w)\).
\(\varphi_p(w)\in\mathbb{N}\) is the preference given by a chooser \(p\in\mathbb{P}\) to the choice \(w\). We oftentimes use \(\varphi_p=\left[\varphi_p(w_1), \varphi_p(w_2), ..., \varphi_p(w_{|\mathbb{W}|})\right]\) as an abbreviation for the “preference vector” of chooser \(p\) (with respect to an “obvious” ordering of \(\mathbb{W}=\left\{w_1,...,w_{|\mathbb{W}|}\right\}\)).
\(\Phi\) is the set of all preferences that are given by at least one chooser to at least one choice, so \[\Phi=\bigcup_{p\in\mathbb{P}}\left\{\varphi_p(w)\mid w\in\mathbb{W}\right\}\text{ .}\]
A scheduling (generally called \(\mathcal{S}\)) is a mapping \(\mathcal{S}:\mathbb{W}\to\mathbb{S}\) (which means that a choice \(w\) was scheduled to slot \(\mathcal{S}(w)\) in a given scheduling). More on that in the the respective section.
An assignment (generally called \(\mathcal{A}\)) is a mapping \(\mathcal{A}:\mathbb{P}\times\mathbb{S}\to\mathbb{W}\) (which means that a chooser \(p\) was assigned to the choice \(\mathcal{A}(p, s)\) in slot \(s\)). More on that in the respective section.
A solution (generally \(\mathcal{L}\)) is a pair \(\mathcal{L}=(\mathcal{S}_\mathcal{L},\mathcal{A}_\mathcal{L})\) of a scheduling and an assignment (that is based on \(\mathcal{S}_\mathcal{L}\)). We call \(\mathbb{L}\) the set of all solutions.
\(\varphi_\max(\mathcal{L})\) is the maximum preference that a chooser \(c\) has given to a choice \(w\) where \(c\) was assigned to \(w\). It is defined as \[\varphi_\max(\mathcal{L})=\max\left\{\varphi_p(w)|p\in\mathbb{P},w\in\mathbb{W}\wedge \exists s\in\mathbb{S}:\mathcal{A}_\mathcal{L}(p,s)=w\right\}\text{ .}\]
A \(\varphi\)-solution is a solution \(\mathcal{L}\) with \(\varphi_\max(\mathcal{L})=\varphi\).
It is very important to note that throughout this section, preferences are “mirrored”, meaning a lower preference given by a chooser to a choice means that the chooser “likes” the choice more. This is because
So, for example, if a chooser has the preferences \[100, 50, 0, 70, 30, 22\] given in the input, they will be converted to \[0, 50, 100, 30, 70, 78\] assuming that 100 is the maximum preference given in the whole input.
When we want to compute a scheduling, we want to assign each of the choices one of the slots available. Given a scheduling \(\mathcal{S}\), \(\mathcal{S}(c)=s\) means that the choice \(c\) was assigned to the slot \(s\). We also define \(\mathcal{S}^{-1}(s)=\left\{w\mid \mathcal{S}(w)=s\right\}\) as the inverse scheduling (the set of all choices that are scheduled to be in slot \(s\)).
This scheduling \(\mathcal{S}\) has to adhere to some rules because we want to also calculate an assignment based on the scheduling:
The sum of the minima and maxima of all choices in a slot have to allow for the number of choosers, so \[\sum_{w\in\mathcal{S}^{-1}(s)}\min(w) \,\leq \,|\mathbb{P}|\, \leq \sum_{w\in\mathcal{S}^{-1}(s)}\max(w)\] has to hold for each slot \(s\), because else it would be impossible to assign every chooser to a choice in a slot where this constraint is violated.
Additional constraints that are present in the input have to be satisfied.
Ideally, we also want to get schedulings that allow for a better assignment solution. We use critical set analysis as a heuristic for this.
Because of the first requirement (sum of minima and maxima), this problem is NP-hard (proof).
Fortunately, despite the problem being NP-hard, it generally isn’t hard at all to find valid schedulings for real-world input data.
Because of this, valid schedulings are generated using a randomized (meaning solutions are visited in a semi-randomized order) depth-first backtracking search. A single backtracking step consists of deciding the slot of a single choice.
There are multiple heuristics at play to improve the performance of the scheduling solver.
Heuristic for choice order: The order in which choices are handled by the solver is determined by the number of scheduling constraints that apply to the single choices. Choices with the most constraints are handled first. Apart from that, the order of choices is random.
Heuristic for set order: Given the current partial solution \(\mathcal{S}_p\), which slots are tried first in the backtracking is determined by \(\sum_{w\in\mathcal{S}^{-1}_p(s)}\max(w)\) (so basically by how “full” the slot \(s\) already is in terms of choice maxima). Slots with a lower value are tried first.
Critical set analysis: Critical set analysis is used by trying to satisfy all critical sets for a set preference level \(p\) until a timeout is reached. If the timeout is reached without finding a valid solution, \(p\) is raised to the next level (so all critical sets with a preference of \(p\) are discarded), and the process is repeated until a solution is found.
When we want to compute an assignment \(\mathcal{A}\) based on a given scheduling \(\mathcal{S}\), we want to assign each chooser to exactly one choice per slot (so for every pair of a slot \(s\) and a chooser \(p\), there is exactly one choice \(\mathcal{A}(s, p)\)).
We also want the resultion solution \(\mathcal{L}=(\mathcal{S}, \mathcal{A})\) to be the “best” one for the given scheduling, but we first have to define a metric for how “good” a solution is in order to be able to compare it to other solutions.
We define a scoring function \(F:\mathbb{L}\to\mathbb{R}^2\) as follows
\[F(\mathcal{L})=\left(\varphi_\max(\mathcal{L}), \sum_{s\in\mathbb{S},p\in\mathbb{P}} \varphi_p\left(\mathcal{A}_\mathcal{L}(s,p)\right)^{\gamma}\right)\]
where \(\gamma\in\mathbb{R}^+\) is the so-called preference exponent that can be choosen freely and affects which solutions are favored over others. It can be thought of as the “fairness paramter”. For more information on this parameter see the respective section in the manual.
We use this function to assign a score to each solution, where lower scores (defined by the usual order relation \(<\) over \(\mathbb{R}^2\)) are assigned to “better” solutions.
Note that we have a scoring function with two components, the first one being \(\varphi_\max(\mathcal{L})\). This means that a solution \(\mathcal{L}_1\) is always “better” than a solution \(\mathcal{L}_2\) if \(\varphi_\max(\mathcal{L}_1)<\varphi_\max(\mathcal{L}_2)\).
We may also look at the two components of the scoring function separately. We then call them the major and minor term \(F_\textrm{maj}\) and \(F_\min\) of \(F\), so \(F(\mathcal{L})=\left(F_\textrm{maj}(\mathcal{L}), F_\min(\mathcal{L})\right)\).
We calculate the optimal assignment for a given scheduling by modelling the problem as a minimum-cost flow problem instance like in this example with 2 slots, 3 choices and 4 choosers and a scheduling \(\mathcal{S}^{-1}(s_1)=\{w_1, w_2\}\), \(\mathcal{S}^{-1}(s_2)=\{w_3\}\):
Or more formally: Given a scheduling \(\mathcal{S}\), we can construct a minimum-cost flow network \(N=(V,E,z,c,a)\) with
The optimal flow \(f\) of \(N\) then is also directly the optimal assignment \(\mathcal{A}\) with the following conversion: \[f(q^p_s, w)=1 \Leftrightarrow \mathcal{A}(p, s) = w \text{ .}\] Note that due to the flow integrality theorem, the resulting optimal flow is guaranteed to have integer flow values for every edge.
Furthermore, because how we defined our edge costs, the total cost of the flow is also the minor score \(F_\min((\mathcal{S}, \mathcal{A}))\) of the resulting solution.
To model many kinds of additional assignment constraints one may want the solution to adhere to, we need to expand the standard model of a minimum-cost flow problem by what we from here on call the set of edge implications \(I\subseteq E^2\) that have the following semantics: \[ (u,v)\in I \Leftrightarrow f(u)\leq f(v) \] So, if \((u,v)\in I\), a flow of \(f(u)=1\) implies a flow of \(f(v)=1\) on the other edge.
Because such a modified network \(N=(V,E,I,z,c,a)\) is not a regular minimu-cost flow instance when \(I\neq\emptyset\) and we have to make sure that the flow we are computing still is integer, we are going to translate the network into an mixed integer programming instance (where each edge is a variable and constraints are based on flow conservation in flow networks) and solve it using a general MIP solver instead.
Because we gave to make sure that the flow we are calculating still consists of integer values when \(I\) is not empty, we have to declare some variables in our MIP problem as integer variables. The naive approach to this would be to simply make every variable an integer variable, but because integer variables are the main hurdle for finding solutions to an MIP problem fast, we generally want to minimize the number of integer variables we have to introduce.
To find out which variables we need to make integer we can view the pair \((E, I)\) as a directed graph (called the implication graph). Note that the edges of the flow network (or equivalently the variables of the respective MIP instance) are the vertices of the implication graph and every implication is a directed edge. To avoid confusion, we will from now on call the vertices of the implication graph \(variables\) and the edges \(implications\).
We then can calculate which variables we need to declare as integer variables so that the whole optimal flow stays integer in a two-step process.
The first step becomes obvious if we observe that all variables in a strongly connected components (abbreviated as SCC) of the implication graph have to have the same value (proof). This means that for every SCC, we have to make one arbitrary variable in the SCC an integer variable in order for every variable in the SCC to become integer too. Let the set of these variables be \(D_1\).
The second step is based on the observation that for an implication \((u,v)\), it is sufficient that either \(v\) or \(w\) are integer variables (because then the implication becomes a normal integer constraint in each subproblem inside the branch-and-cut process of the MIP solver which does not violate the prerequisites of the integrality theorem). To use this fact, we just have to look at the remaining implication graph \((E', I')\) where we removed
We then calculate a dominating set \(D_2\) of \((E', I')\). Because finding the minimal dominating set is an NP-hard problem we can resort to a simple greedy algorithm.
This whole process gives us a set of variables \(D=D_1\cup D_2\) that we have to declare as integer variables in order for the optimal flow to be also integer.
So far, we are only optimizing by the minor score \(F_\min\). In order to find the optimal solution by \(F\), we just do binary search to find the minimum \(F_\textrm{maj}\) for which there exists a solution.
The most important (and most computationally expensive) heuristic used throughout wassign is called critical set analysis. It is computed once at the start of the computation and its result is a set \(\mathbb{C}\) of so called critical sets.
A critical set \(C\) is a set is a pair \(C=(\varphi_C, W_C)\) with the following properties:
\(\varphi_C\in\Phi\) and \(W_C\subseteq\mathbb{W}\),
There exists a chooser \(p\in\mathbb{P}\) for which \(W_C=\left\{w\mid \varphi_p(w)\leq\varphi_C\right\}\) holds true.
The critical set analysis (in short CSA) \(\mathbb{C}=\left\{C_1, C_2, ..., C_{|\mathbb{C}|}\right\}\) is the set of all critical sets.
We further call \(\mathbb{C}(\varphi)=\left\{C\in\mathbb{C}\mid \varphi_C=\varphi\right\}\) the \(\varphi\)-relevant critical set analysis (\(\varphi\)-relevant CSA).
Example: Given slots \(\mathbb{S}=\{s_1,s_2,s_3\}\), choices \(\mathbb{W}=\{a, b, c, d, e, f\}\) and a single chooser \(\mathbb{P}=\{p\}\) with preferences \(\varphi_p=\left[100, 50, 0, 90, 70, 100\right]\), all valid critical sets would be \[C_1=(100, \{a, b, c, d, e, f\}), \quad C_2=(90, \{b, c, d, e\}),\] \[C_3=(70, \{b, c, e\}), \quad C_4=(50, \{b, c\}), \quad C_5=(0, \{c\})\text{ .}\]
Given a scheduling \(\mathcal{S}\), we call a critical set \(C\) satisfied if \[\bigcup_{w\in C}\left\{\mathcal{S}(w)\right\} = \mathbb{S}\text{ .}\]
Or in plain terms: A critical set is satisfied by a scheduling if, in this scheduling, every slot contains at least one choice in the critical set.
We can then use critical sets as a heuristic for how “good” assignments based on a given scheduling can be: If \(\mathcal{S}\) does not satisfy all critical sets of \(\mathbb{C}(\varphi)\), there can not be a solution based on \(\mathcal{S}\) that has a maximum preference of \(\varphi\) or lower (proof).
A useful piece of information we can immediately get out of the critical sets is a lower bound of the maximum preference a solution can have. We call this bound \(\overline{\varphi}\) the preference bound; it is defined as \[\overline{\varphi}=\min\left\{\varphi_C\mid C\in\mathbb{C}\wedge |C|<|\mathbb{S}|\right\}\text{ .}\] Or in plain words: If a critical set has fewer elements than there are slots in the input, this critical set can not be satisfied by any scheduling, and therefore there can not be any solution with a maximum preference lower than \(\overline{\varphi}\).
We can eliminate most of the critical sets in a CSA by the following observation: Given two critical sets \(C_1=(\varphi_1, W_1)\) and \(C_2=(\varphi_2, W_2)\), if \(\varphi_1\leq\varphi_2\) and \(W_1\supseteq W_2\) then all schedulings satisfying \(C_1\) also satisfy \(C_2\). We say that \(C_1\) is covered by \(C_2\).
We can therefore discard any critical sets that are already covered by another critical set; we then just have to alter our definition of \(\varphi\)-relevant critical sets to include all critical sets that have the same or greater preference: \[\mathbb{C}(\varphi)=\left\{C\in\mathbb{C}\mid\varphi_C\geq\varphi\right\} \text{ .}\]
Note: If we need \(\mathbb{C}(\varphi)\) e.g. in the scheduling solver, we can also discard all critical sets that are simply superset of another critical set in \(\mathbb{C}(\varphi)\).
So far, we only optimize the solution for a given scheduling. In order to find a locally optimal scheduling (and the corresponding optimal assignment), we perform first-choice hill climbing by first finding any valid scheduling \(S\) (using the scheduling solving algorithm as described above) and then modifying this scheduling by moving a single event to a different slot (we call this scheduling a neighbor of \(S\)) until we can not find any neighbors of \(S\) that has a better solution than \(S\) itself.
Because the number of neighbors can be very large (and solving the assignment is quite expensive), we limit the number of neighbors that get considered as the next step.
We now optimize to a local optimum. To find the global optimum (or at least a very good local one), we simply repeat the hill climbing process over and over and choose the best solution found after a certain timeout; this is also called shotgun hill climbing or random-restart hill climbing.
This (very simple) approach has three main advantages over other optimization methods:
We reduce PARTITION to SCHEDULING.
Given a PARTITION-Instance \(S=\{n_1, n_2, ..., n_k\}\), let \(\sigma=\frac{1}{2}\sum_{i=1}^k n_k\). We define the following slot, choice and chooser sets \[\mathbb{S}= \{s_1, s_2\},\; \mathbb{W}=\{w_1, w_2, ..., w_k\},\; \mathbb{P}=\{p_1, p_2, ..., p_{\sigma}\}\] with \[\min(w_k)=\max(w_k)=n_k \text{ .}\]
Because \(\min(w_k)=\max(w_k)=n_k\), the scheduling constraint (for a slot \(s\)) \[\sum_{w\in\mathcal{S}^{-1}(s)}\min(w) \leq |\mathbb{P}| \leq \sum_{w\in\mathcal{S}^{-1}(s)}\max(w)\text{ ,}\] implies \[\sum_{w\in\mathcal{S}^{-1}(s_1)}\min(w) = \sum_{w\in\mathcal{S}^{-1}(s_2)}\min(w)\]
Therefore, if and only if there exists a valid scheduling for \((\mathbb{S}, \mathbb{W}, \mathbb{P})\) there exists a perfect partitioning of \(S\).
Proof by contradiction: Suppose there is a \(\varphi\)-solution \(\mathcal{L}=(\mathcal{S}, \mathcal{A})\) and let \(C\in\mathbb{C}(\varphi)\) be a \(\varphi\)-relevant critical set that is not satisfied by \(\mathcal{S}\).
Then, because \(C\) is not satisfied by \(\mathcal{S}\), there is a slot \(s\in\mathbb{S}\setminus\bigcup_{w\in C}\{\mathcal{S}(w)\}\) and a chooser \(p\) with \[\min\left\{\varphi_p(w)\mid w\in\mathcal{S}^{-1}(s)\right\} > \varphi\] and therefore \(\varphi_p\left(\mathcal{A}(p, s)\right) > \varphi\), so \(\varphi_\max(\mathcal{L})>\varphi\) and \(\mathcal{L}\) can not be a \(\varphi\)-solution.
Given are the variables \(V=\{v_1, v_2, ..., v_n\}\) in a strongly connected component of an implication graph \(G=(V, I)\). For each pair of variables \(u,v\in V\), we know that \(v\) is reachable from \(u\) and vice versa in \(G\) through some paths \((u, a_1, ..., a_i, v)\) and \((v, b_1, ..., b_j, u)\). Applying the semantics of an implication graph, this is equivalent to \[ u \leq a_1 \leq ... \leq a_i \leq v \;\wedge\; v \leq b_1 \leq ... \leq b_j \leq u \] which (by transitivity and antisymmetry) implies \(u=v\).