Gecode
Even in the simple examples in the previous section, we see that the difficulty was mostly in programming the propagating of constraints and in the branching. Wouldn't it be easier if we could just program with the constraints? So for instance in the ordered list example, what we would like to do is something like the following python-like pseudo-code:
# my domain of possible solutions
current = [set(range(7)) for i in range(4)]
# The following constraints should apply
for i in range(3):
addConstraint(l, l[i] < l[i+1])
# search using backtracking
result = search(l)
There are libraries we could use in Python that would allow us to do exactly this. Moreover, there are several whole programming languages which are built around the idea of search using backtracking. We will be using the library GeCode for C++. What will achieve is, the ability to write programs purely using mathematical relations. As with everything in C++, we need to spend some time setting up everything.
The first gecode example
GeCode is object oriented and uses composition and inheritence to construct a model of the problem. The first concept we need to learn is the Space. The Space encapsulates all the aspects of a problem, the variables needed, the constraints and how they are propagated, the branchers, and how we want the data to be copied when we are searching for a solution.
When you are solving a problem you need to create a class which inherits from Space. Lets look at an example of computing a list of integers without any constraints.
class NoConstraintList : public Space {
protected:
// An array of possible solutions
IntVarArray l;
public:
// l is an 3 element array with values 0..5
NoConstraintList(void) : l(*this, 3, 0, 5) {
// setup of brancher
branch(*this, l, INT_VAR_SIZE_MIN(), INT_VAL_MIN());
}
// two methods used during search - leave them like this
NoConstraintList(bool share, NoConstraintList& s) : Space(share, s) {
l.update(*this, share, s.l);
}
Space* copy(bool share) {
return new NoConstraintList(share, *this);
}
// What should be printed when we print a solution
void print(void) const {
std::cout << l << std::endl;
}
};
The first thing we in the class see is the variable l, which has type IntVarArray, and which is initialised with
l(*this, 3, 0, 5)
The meaning is that l is an array with $3$ elements, where each element has values $0..5$. To see what $l$ contains we can run
NoConstraintList* m = new NoConstraintList; m->print();
to obtain the result
{[0..5],[0..5],[0..5]}
So l is not a list of integers. It is better to think of l as a partial solution to our problem, similar to current in the python examples in the previous section. This partial solution will be refined to solutions using propagation of constraints and branching during search. Gecode has a selection of types of variables that can be used, including bools and sets. It is also possible to extend GeCode by implementing our own variable types.
We will not include any constraints in this first example, however we do need a brancher which is set up by
branch(*this, l, INT_VAR_SIZE_MIN(), INT_VAL_MIN());
This brancher will select the entry in l with the smallest amount of possible values, and then branch using the smallest value. So for instance
{[1,2,3,4],[2,3,4]}
will be branched to the two lists
{[1,2,3,4],[2]}
{[1,2,3,4],[3,4]}
There are many branchers to choose from, and we can extend GeCode by implementing our own.
The class above should be thought of as the minimal amount of code needed to start solving problems. Lets see how we can search through the solutions
// We create a model of type NoConstraintList
NoConstraintList* m = new NoConstraintList;
// We initialise and get ready for search - search has not yet begun
// DFS = depth first search
DFS
// The model is no longer needed, searchengine has made a copy
delete m;
// We loop through and print all solutions to the constraint problem
// i.e. all tuples (0..5, 0..5, 0..5), all 216 of them
while (NoConstraintList* s = searchengine.next()) {
s->print(); delete s;
}
After creating the model, we give it as a parameter to a searchengine. Here we choose depth-first search as the method of search. As with anything else in GeCode, there are several choices for search engines, and we may also write our own. We then search through and print all solutions to the console. Note the
delete s;
in the while loop. When we retrieve a result we gain ownership of that model, and it is our responsibility to release the memory allocated. The program prints all $216$ lists of length $3$ with elements $0..5$.
Distinct lists
The example above should be thought of as an empty example, in the sense that we have not added any constraints yet. We will now do exactly that, by computing all lists with distinct elements. That is, we will compute all lists of length three, with entries $0..5$ so that each number occurs at most once. We create a new similar class with constructor
DistinctList(void) : l(*this, 3, 0, 5) {
rel(*this, l[0], IRT_NQ, l[1]); // l[0] != l[1]
rel(*this, l[0], IRT_NQ, l[2]); // l[0] != l[2]
rel(*this, l[1], IRT_NQ, l[2]); // l[1] != l[2]
branch(*this, l, INT_VAR_SIZE_MIN(), INT_VAL_MIN());
}
We have now indicated what constraints we would like hold for our lists, and GeCode has created a propagator for us. There are several possible propagators to choose from, and we can of course extend GeCode by programming our own.
GeCode comes with a simple modelling language which simplifies the creation of constraints. Here is the simpler alternative
rel(*this, l[0] != l[1]);
rel(*this, l[0] != l[2]);
rel(*this, l[1] != l[2]);
There is also a selection of predefined constraints available. The above constraint could be implemented by simply typing
distinct(*this, l);
Increasing lists
We solve the problem of computing lists with increasing integers using gecode. We implement the class DistinctOrderedList with constructor
DistinctOrderedList(void) : l(*this, 5, 0, 5) {
rel(*this, l[0] < l[1]);
rel(*this, l[1] < l[2]);
rel(*this, l[2] < l[3]);
rel(*this, l[3] < l[4]);
branch(*this, l, INT_VAR_SIZE_MIN(), INT_VAL_MIN());
}
Communicating with GeCode
We might need to get more information from GeCode about what is going on internally, to debug or be more active in the search for solutions.
One approach is to use the built in GUI (called Gist) which gives you graphical interface where you can control and look more carefully at what happens during a search.
You can also add traces on variables, for instance by writing
trace(*this, l);
in the example above. The state of the variable l will then be printed out during search.
Finally, GeCode can throw exceptions if you do something wrong. To catch those, you can replace your main function by
try {
// code as above
catch (Exception e) {
std::cerr << "Gecode Exception: " << e.what() << std::endl;
return 1;
}
return 0;
to print out any error messages that GeCode produces.