Академический Документы
Профессиональный Документы
Культура Документы
Users Manual
Nikolaj van Omme
Laurent Perron
Vincent Furnon
License information
This document is provided under the terms of the
Trademarks
GOOGLE is a trademark of Google Inc.
Linux is a registered trademark of Linus Torvald in the United States, other countries, or both.
Java and all Java-based trademarks and logos are trademarks of Sun Microsystem Inc. in the
United States, other countries, or both.
Other companies, products, or service names may be trademarks or service marks of others.
Ackowledgments
We thank the following people for their helpful comments:
Dania El-Khechen, Hkan Kjellerstrand, Louis-Martin Rousseau, Thomas Carton de Wiart
FOREWORD
We are glad to welcome you to the or-tools users manual. In this foreword, we try to answer
most common questions a newcomer could have when discovering this manual or the library
for the rst time.
The or-tools library is a set of operations research tools developed at Google. If you have no
idea what operations research1 is, you still can use our library to solve common small problems
with the help of our Constraint Programming (CP) solver. If you do know what operations
research is and how difcult it is sometimes to nd efcient, easy to use and open source code,
we hope you will enjoy using our library. We have put a lot of efforts in order to make it user
friendly and continue to improve it on a daily basis. Furthermore, we encourage interactivity
and are always open to suggestions. See the section How to reach us? below. If you have
comments about this manual or the documentation in general, see the section Do you have
comments?.
What is or-tools?
The or-tools library is a set of operations research tools written in C++ at Google.
The main tools are:
A Constraint Programming solver.
A simple and unied interface to several linear programming and mixed integer programming solvers (GLPK, CLP, CBC and SCIP).
Knapsack algorithms.
Graph algorithms (shortest paths, min cost ow, max ow, linear sum assignment).
FlatZinc support.
The source code and the scripts used to generate the documentation will be available soon.
See for instance http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml for the Google C++ Style
Guide.
3
iv
Targeted audience
This manual is written with two types of readers in mind. First, someone who is not familiar with Constraint Programming nor is she a professional programmer. Second, an educated
reader who masters Constraint Programming and is quite at ease without necessarily mastering
one of the supported computer languages.
From time to time, we refer to scientic articles: you dont need to read and understand them
to follow the manual.
Did we succeed to write for such different proles? You tell us!
To explain some details that would break the ow of the text, we use a shadowed box.
This is an explanation that would break the ow of the text
This is why we prefer to put our explanation aside in a shadowed box.
To focus on some parts of the code, we omit non necessary code or code lines and replace them
by ". . . ".
Adapt the command lines to your type of terminal and operating system.
vi
http://or-tools.googlecode.com/svn/trunk/documentation/
documentation_hub.html#tutorial_examples
or under the following directory of the or-tools library:
documentation/tutorials/C++
If you prefer to code in Python, Java or C#, we have translated all the examples in your
favourite language. You can nd the complete examples on the documentation hub or under
the directories:
documentation/tutorials/Python
documentation/tutorials/Java
documentation/tutorials/Csharp.
Lab sessions
Theory is good but useless without practice and experience. For each chapter, we provide
exercises. Most of them are practical and consist in completing some C++ code. Even if you
dont (like to) code in C++, these lab sessions are helpful as we develop some concepts seen in
the manual more in details. Exercises vary between simple and straightforward to sometimes
really challenging. In the latter case, we mark these exercises as such. For all the exercises, we
provide solutions.
You can nd (soon!) the exercises and their solutions on the documentation hub:
http://or-tools.googlecode.com/svn/trunk/documentation/
documentation_hub.html#lab_sessions
or under the following directory of the or-tools library:
documentation/labs/C++
and post your questions, suggestions, remarks, . . . to the or-tools discussion group:
http://groups.google.com/group/or-tools-discuss
CONTENTS
Foreword
iii
Basics
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
3
3
9
12
18
21
22
23
23
.
.
.
.
.
.
.
25
26
31
33
40
44
46
47
.
.
.
.
.
.
.
.
.
.
49
50
51
54
56
59
60
64
64
68
68
69
4.1
II
5
What is reication? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69
Customization
Dening search primitives: the n-queens problem
5.1 The n-queens problem . . . . . . . . . . . . . . . . . .
5.2 Implementation of the basic model . . . . . . . . . . .
5.3 Basic working of the solver: the search algorithm . . . .
5.4 cpviz: how to visualize the search . . . . . . . . . . . .
5.5 Basic working of the solver: the phases . . . . . . . . .
5.6 Out of the box variables and values selection primitives
5.7 Customized search primitives . . . . . . . . . . . . . .
5.8 Breaking symmetries with SymmetryBreakers . . .
5.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . .
71
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
73
74
77
81
92
114
123
126
126
132
.
.
.
.
.
.
.
.
.
133
135
143
148
161
167
182
190
202
207
.
.
.
.
.
.
.
.
.
.
209
211
211
211
211
211
211
211
211
211
211
.
.
.
.
.
213
213
213
213
213
213
III
9
Routing
215
Travelling Salesman Problems with constraints: the TSP with time windows
9.1 A whole zoo of Routing Problems . . . . . . . . . . . . . . . . . . . . . .
9.2 The Routing Library (RL) in a nutshell . . . . . . . . . . . . . . . . . . .
9.3 The Travelling Salesman Problem (TSP) . . . . . . . . . . . . . . . . . .
9.4 The model behind the scenes: the main decision variables . . . . . . . . .
9.5 The model behind the scenes: overview . . . . . . . . . . . . . . . . . . .
9.6 The TSP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.7 The two phases approach . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.8 The Travelling Salesman Problem with Time Windows (TSPTW) . . . . .
9.9 The TSPTW in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
217
219
223
226
232
238
248
255
256
266
270
10 Vehicule Routing Problems with constraints: the capacitated vehicle routing problem
271
10.1 The Vehicle Routing Problem (VRP) . . . . . . . . . . . . . . . . . . . . . . 271
10.2 The VRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
10.3 The Capacitated Vehicle Routing Problem (CVRP) . . . . . . . . . . . . . . . 272
10.4 The CVRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
10.5 Multi-depots and vehicles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
10.6 Partial routes and Assigments . . . . . . . . . . . . . . . . . . . . . . . . . . 274
10.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
11 Arc Routing Problems with constraints: the Cumulative Chinese Postman Problem
275
11.1 The Chinese Postman Problem (CPP) . . . . . . . . . . . . . . . . . . . . . . 276
11.2 The Cumulative Chinese Postman Problem (CCPP) . . . . . . . . . . . . . . . 276
11.3 A rst implementation for the CCPP . . . . . . . . . . . . . . . . . . . . . . . 276
11.4 Disjunctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
11.5 A second implementation for the CCPP . . . . . . . . . . . . . . . . . . . . . 276
11.6 Partial routes and locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
11.7 Lower bounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
11.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
IV
Technicalities
277
12 Utilities
12.1 Logging . . .
12.2 Asserting . .
12.3 Timing . . .
12.4 Proling . .
12.5 Debugging .
12.6 Serializing .
12.7 Visualizing .
12.8 Randomizing
279
279
280
281
283
283
283
283
283
13 Modeling tricks
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
285
Apprendices
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
287
288
288
288
288
288
288
288
288
290
290
290
295
297
Bibliography
299
Index
301
Part I
Basics
CHAPTER
ONE
In this chapter, we introduce Constraint Programming (CP) and the or-tools library and its core
principles. We also present the content of this manual.
Overview:
The chapter is divided in three parts. First, we introduce Constraint Programming by looking
at a solving process done by our CP solver. Along the way, we will try to dene Constraint
Programming and show some practical problems where CP stands out. A little bit of theory
will lay the foundations for the whole manual. Second, we introduce a simple strategy (the
three-stage method) that can help us when confronted with a problem to solve. This method
will be applied repeatedly in this manual. Another recurrent idea in this manual is to be aware
of tradeoffs. This idea is the key to successful optimization and well worth a whole section.
Finally, we outline the general principles of the library and detail the content of this manual.
Prerequisites:
None. Being open minded, relaxed and prepared to enjoy the or-tools library helps
though.
The following gure illustrates a solution to the 4-queens problem: none of the 4 queens can
capture each other.
Although this particular problem isnt very impressive, keep in mind that you can generalize it
to chessboards with 4.
= 4.
(,) squares
This constraints ensure that we place 4 queens on the chessboard. In general, constraints only
permit possible combinations of values of variables corresponding to real solutions2 . In the next
section, we will see how the or-tools CP solver tries to solve this problem. More precisely,
how the solver will try to solve the model we will develop and explain in sections 5.1 and 5.23 .
Things are a little bit more complex than that but lets keep it simple for the moment. See subsection 1.3.2 for
more.
3
We dont need to know the details of the model right now.
4
These are two key elements of a Constraint Programming solving algorithm but there are many more!
5
Propagation is also called domain ltering, pruning or consistency techniques.
backtracking: from time to time, the solver is stuck because it tried to assign some
values to some variables that are just not possible (or desirable) because they dont respect
the constraints. The solver must then challenge its previous choices and try other values.
This is called backtracking. Backtracking also occurs when the solver nds a solution
but continues the search and tries to nd another solution.
To better understand Constraint Programming, lets have a look at a real solving process6 . In
the following Figures, crosses represent the action of removing values from variables domain.
Each step in the solving process is separated from the following one by an horizontal line.
The solver starts by placing the rst queen in the upper left corner. Because of the model we
gave to the solver, it knows that there cannot be any other queen in the same column, hence the
grey crosses on the following Figure. One constraint tells the solver that there cannot be another
queen on the same diagonal with a negative slope (the diagonals going down then right). The
red crosses show this impossibility.
One constraint tells the solver that no two queens can be on the same row, hence the next red
crosses.
After this rst step, only the white squares are still available to place the three remaining queens. The process of excluding some squares is what is called propagation.
The second step starts with the solver trying to place a second queen. It does so in the rst
available square from above in the second column. As in the rst step, the solver knows that no
other queen can be placed in a column where it just placed a queen, hence the new grey crosses
in the next Figure.
The propagation is as follow:
The same negative diagonal constraint as in step 1 tells the solver that no queen can be on the
negative diagonal of the second queen, hence the red cross.
6
You can nd this search process detailed in sections 5.2 and 5.4.
Another constraint for the diagonals with positive slopes (diagonals going up then right) tells
the solver that no queen can be placed on the positive diagonal of second queen, hence the red
cross.
Now, we have a failure as there is no possibility to place a third queen in the third column:
there simply can not be a solution with this conguration. The solver has to backtrack!
The solver decides to challenge its last decision to place the second queen in the third row from
above and places it in the fourth row.
The propagation is as follow:
First, the square with the red cross is removed because of the positive diagonal constraint. This
leaves only one possibility to place a queen in the fourth column.
The no two queen on the same row constraint removes one more square in the third column,
leaving only one square to place the last remaining queen.
This is of course not possible and the negative diagonal constraint tells the solver
that no queen can be on a negative diagonal from the fourth queen. Since there
is one, the solver concludes that there is a failure.
It has to backtrack again!
First, it tries to challenge its last choice for the second queen but it detects that there are no
more other choices. The solver has to challenge its rst choice to place the rst queen in the
rst row and places the rst queen in the rst column second row.
The propagation can now take place:
Two values are taken away because of the negative diagonal constraint:
Now comes the turn of the no two queen on the same row constraint and it is responsible of
removing the next three red crosses:
The positive diagonal constraint kicks in and forbids the red square leaving no choice to place
a third queen in the third column rst row.
The no two queen on the same row constraint forbids any other queen to be placed on the
fourth row:
and any other queen on the rst row, leaving no choice but to place the fourth queen in the
fourth column third row:
The solver nds out that the model is respected, so we have our rst solution! Should the
solver continue the search, it would have to backtrack and try to place the rst queen in the rst
column third row.
This new eld has its origins in a number of elds including Articial intelligence, Programming Languages,
Symbolic Computing, Computational Logic, etc. The rst articles related to CP are generally dated from the
seventies but CP really started in the eighties. As with every new eld, names, origins, etc. are not settled
and different people sometimes take different avenues. We carefully tried to use commonly accepted names,
techniques, etc.
2 = 0
=0
2
=0 = 1
2
1
=0
In some special cases, we are able to model the AllDifferent constraint in a more efcient manner.
Propagation is done globally on all involved variables but the propagation is done constraint by constraint.
10
Basically, you only need to be able to propagate (hopefully efciently) your constraints.
9
10
One of the curiosities of CP is its ability to deal with meta-constraints: constraints on constraints!
Take for instance the Element constraint. Let [0 , . . . , 1 ] be an array of integers variables
with domain {0, . . . , 1}, an integer variables with domain contained in {0, . . . , 1} and
with domain {0, . . . , 1}. The Element constraint assign the th variable in [0 , . . . , 1 ]
to , i.e.:
= .
If you change or the array [0 , . . . , 1 ], will change accordingly but remember that you
have an equality, so this works the other way around too. If you change then or/and the
array [0 , . . . , 1 ] will have to change! This technique is called reication and you can learn
more about it in chapter 4. The ease to model a problem and the possibility to add heterogeneous constraints sometimes make CP the preferred or only framework to model some difcult
problems with a lot of side-constraints.
See the section Basic working of the solver: the search algorithm for more details.
See next section for more.
13
In section Model, we will see a model with a search space of size 16 for the 4-queens problem.
14
Determining the exact (or even approximate) search space size is very often a (very) difcult problem by
itself.
15
Most of the time, we want good solutions quite rapidly. It might be more interesting to have a huge search
space but that we can easily visit than a smaller search space that is more difcult to scan. See the section Its
always a matter of tradeoffs.
16
Or a bunch of variables. Or it can just restrict the values some variables can take. Or a combination of both
but lets keep it simple for the moment: the solver assigns a value to one variable at a time.
12
11
CSP
Branch and prune
Prune:
Relax constraints
Propagate constraints
Reduce gap
Goal: Optimality
View: Objective oriented
a
Goal: Feasibility
View: Domain oriented
This is an aside for our MIP (Mix Integer Programming) colleagues. Its full of jargon on purpose.
12
1.3.1 Constraint Satisfaction Problems (CSP) and Constraint Optimization Problems (COP)
We illustrate the different components of a Constraint Satisfaction Problem with the 4-queens
problem we saw in section 1.1. A CSP consists of
a set of variables = {0 , . . . , 1 }.
Ex.: For the 4-queens problem, we have a binary variable indicating the presence or
not of a queen on square (, ):
= {00 , 01 , 02 , 03 , 10 , 11 , 12 , . . . , 33 }
for each variable , a nite set of possible values (its domain).
Ex.: Each variable is a binary variable, thus
00 = 01 = . . . = 33 = {0, 1}.
constraints that restrict the values the variables can take simultaneously.
Ex.: Constraints that avoid that two queens are on the same row:
row 0:
row 1:
row 2:
row 3:
00 +
10 +
20 +
30 +
01 +
11 +
21 +
31 +
02 +
12 +
22 +
32 +
03
13
23
33
1
1
1
1
Indeed, these constraints ensure that for each row at most one variable 0 , 1 , 2 or 3
could take the value 1. Actually, we could replace the inequalities by equalities because
we know that every feasible solution must have a queen on each row. Later, in section 5.2,
we will provide another model with other variables and constraints.
As we mentioned earlier, values dont need to be integers and constraints dont need to be
algebraic equations or inequalities17 .
If we want to optimize, i.e. to minimize or maximize an objective function, we talk about a
Constraint Optimization Problem (COP). The objective function can be one of the variables
of the problem or a function of some or all the variables.
A feasible solution to a CSP or a COP is a feasible assignment: every variable has been assigned a value from its domain in such a way that all the constraints of the model are respected.
The objective value of a feasible solution is the value of the objective function for this solution.
An optimal solution to a COP is a feasible solution such that there are no other solutions with
better objective values. Note that an optimal solution doesnt need to exist nor is it unique.
17
13
Most problems of practical interest belong to either categories but these two categories dont cover all problems.
19
Intractable problems are problems which in practice take too long to solve exactly, so there is a gap between
the theoretical denition (NP-Hard problems) and the practical denition (Intractable problems).
20
Technically, you could nd an exact solution but you would not be able to prove that it is indeed an exact
solution in general.
21
Roughly, we consider a problem to be hard to solve if we need a lot of time to solve it. Read on.
14
Intractability
One of the main difculties complexity experts faced in the 70s was to come up with a theoretical denition of the complexity of problems not algorithms. Indeed, it is relatively easy22
to dene a complexity measure of algorithms but how would you dene the complexity of a
problem? If you have an efcient algorithm to solve a problem, you could say that the problem
belongs to the set of easy problems but what about difcult problems? The fact that we dont
know an efcient algorithm to solve these doesnt mean these problems are really difcult.
Someone could come up one day with an efcient algorithm! The solution the experts came up
with was to build equivalence classes between problems and dene the complexity of a problem with respect to the complexity of other problems (so the notion of complexity is relative
not absolute): a problem is as hard as a problem if there exists an efcient transformation
that maps every instance of problem into an instance () = of problem such that if
solve , you solve .
A
(b) = a
Indeed, if there exists an efcient algorithm to solve problem , you can also solve efciently
problem : transform an instance into into an instance () = of problem and solve it
with the efcient algorithm known to solve problem . So problem is as difcult as problem
(because if you know an efcient algorithm to solve problem , you can solve problem as
efciently) and we write T and say that problem reduces efciently to problem or
that is an efcient reduction23 . The search for an efcient algorithm is replaced by the search
for an efcient reduction between instances of two problems to prove complexity.
This main idea leads to a lot of technicalities:
how to measure the complexity of an algorithm?
what is an efcient transformation?
what are the requirements for such a reduction?
...
We dont answer these interesting questions except the one on efciency. We consider a reduction efcient if there exist a polynomial-time bounded algorithm (this refers to the rst
question...) that can transform any instance of problem into an instance of problem
such that the solutions correspond. This also means that we consider an algorithm efcient if it
is polynomially time-bounded (otherwise the efciency of the reduction would be useless).
The class of problems that can be efciently solved is called , i.e. the class of problems that
22
Well, to a certain degree. You need to know what instances you consider, how these are encoded, what type
of machines you use and so on.
23
The T in T is in honor of Alan Turing. Different types of efcient reductions exist.
15
If you want to prove that a problem is NP-Hard (see below), take a problem that is NP-Complete,
like the HPP, and reduce it to your problem. This might sound easy but it is not!
24
For technical reasons, we dont compare problems but languages and only consider decision problems, i.e.
problems that have a yes/no answer. The Subset Sum Problem is such a problem. Given a nite set of integers,
is there a non-empty subset whose sum is zero? The answer is yes or no. By extension, we say an optimization
problem is in , if its equivalent decision problem is in . For instance, the Chinese Postman Problem (CPP) is
an optimization problem where one wants to nd a minimal route traversing all edges of a graph. The equivalent
decision problem is Is it possible to nd a feasible route with cost less or equal to ? where is a given
integer. By extension, we will say that the CPP is in (we should rather say that the CPP is in optimization).
25
This discussion is really about theoretical difculties of problems. Some problems that are theoretically easy
(such as solving a Linear System or a Linear Program) are difcult in practice and conversely, some problems that
are theoretically difcult, such as the Knapsack Problem are routinely solved on big instances.
26
The abbreviation refers to non-deterministic polynomial time, not to non-polynomial.
16
NPComplete
NP
If P = NP
?
The = question
The P versus NP problem is a major unsolved problem in Computer Science. Informally, it
asks whether every problem whose solution can be quickly veried by a computer ( NP)
can also be quickly solved by a computer ( P). It is one of the seven Millennium Prize
Problems selected by the Clay Mathematics Institute. The offered prize to the rst team to
solve this question is $1,000,000!
In 2002 and 2012, W. I. Gasarch (see [Gasarch2002] and [Gasarch2012]) conducted a poll
?
and asked his colleagues what they thought about the = question. Here are the
results:
Outcomea
%
%
(2002) (2012)
=
61
83
=
9
9
No idea
30
8
One possible outcome - mentioned by very few - is that this question could be... undecidable, i.e. there is no yes or no answerb !
a
We agglomerated all other answers into a category No idea although the poll allowed people to fully express themselves (some answered I dont care for instance). The rst poll (2002) involved 100 researchers
while the second one involved 152 researchers.
b
See Undecidable problem on Wikipedia.
If you are interested in this fascinating subject, we recommend that you read the classical book
27
17
This book was written in 1979 and so misses the last developments of the complexity theory but it clearly
explains the NP-Completeness theory and provides a long list of NP-Complete problems.
29
For MIP practitioners, this is equivalent to Lagrangian Relaxation.
30
In the case of optimization, a solution that isnt that different means a solution that has a good objective value,
preferably close to the optimum.
31
This list is much inspired from the excellent documentation provided by Helmut Simonis under the Creative
Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.
18
Transport
Stand allocation
Personnel assignment
Personnel requirement planning
Hardware design
Compilation
Financial applications
Placement
Industrial cutting
Air trafc control
Frequency allocation
Network conguration
Product design
Product blending
Time tabling
Production step planning
Crew rotation
Aircraft rotation
Supply chain management
Routing
Manufacturing
Resource allocation
Circuit verication
Simulation
...
With such a high success rate in different application, CP can be thus described as one efcient
tool in the toolbox of Operations Research experts.
19
38
Actually, this search for the holy grail is closely related to the famous P = NP question. If such algoritm
exists, then most probably P = NP. See the section Intractability.
39
See the subsection The ease to model a problem.
20
1.5.1 Describe
This step is often overlooked but is one of most important part of the overall solving process.
Indeed, a real problem is often too complex to be solved in its entirety: you have to discard
some constraints, to simplify certain hypothesizes, take into account the time to solve the problem (for instance if you have to solve the problem everyday, your algorithm can not take one
month to provide a solution). Do you really need to solve the problem exactly? Or can you
approximate it?
This step is really critical and need to be carefully planned and executed.
Is this manual, we will focus on three questions:
What is the goal of the problem we try to solve? What kind of solutions are we exactly
expected to provide?
40
If you are allergic to this academic approach, you probably will be happy to know that we only use this
three-stage method in the rst two partsc of this manual.
21
What are the decision variables? What are the variables whose values are crucial to
solve the problem?
What are the constraints? Are our constraints suited to solve the problem at hand?
1.5.2 Model
Again a difcult stage if not the most challenging part of the solving process. Modelling is
more of an Art than anything else. With experience, you will be able to model more easily
and use known and effective tricks. If you are a novice in Operations Research/Constraint
Programming, pay attention to the proposed models in this manual as they involve a lot of
knowledge and subtleties. Do not be discouraged if you do not understand them at rst. This
is perfectly normal. Take the time to read them several times until you master them. Beside, it
could be our fault if you do not understand them: maybe we did not explain them well?
When confronted with a new problem, you might not know what do to. We all face this situation. This is what research is all about!
1.5.3 Solve
The reader should be aware that this stage isnt only about pushing a solve button and waiting
for the results to be delivered by the solver. The solve stage involves reasoning to nd the best
way to solve a given model, i.e. how to traverse the search tree in a efcient way. We discuss
this stage in details in chapter 5.
22
At least, no one found one and with our actual knowledge, there is a strong suspicion that none exist.
Of course, we are talking about clever options.
Be conscious of the tradeoffs and that what seems the best option at a time might actually not
work that well no matter how clever the basic idea was. Ideas have to be tested and retested.
This testing is an uncompromising way to take decisions but also allows to get a better insight
of how and why an algorithm actually works (or fails).
CP and the or-tools library allow us to develop very quickly prototypes we can test, improve,
test, redesign, test, etc., you get the idea.
The good optimization researchers motto:
Its always a matter of tradeoffs
Writing this manual is no exception. What content do we introduce and how much details do
we add?
Ultimately, you are best aware of your problem and the (limited) resources you have to solve
it. As we said:
Its always a matter of tradeoffs.
We will refer to this motto from time to time in this manual.
23
CHAPTER
TWO
This chapter introduces the basics of the or-tools library. In particular, we show how to use
the Constraint Programming Solver (CP Solver). It takes a while to get used to the logic of
the library, but once you grasp the basics explained in this chapter, youre good to go and you
should be able to nd your way through the numerous examples provided with the library.
Overview:
We start with a discussion on the setup of the library, then walk through a complete example to
solve a cryptarithmetic puzzle. Along the way, we see how to create the CP solver and populate
it with a model, how to control the search with a DecisionBuilder, collect solutions with
SolutionCollectors and change the behavior of the program with parameters (through
the Google gags library). Finally, we say a few words about the other supported languages
(Python, Java and C#).
Section 2.3.1 summarizes in two Figures all the required steps to write a basic program.
Prerequisites:
Descriptions
Apache License
Main Makele
This le
Where all binary les will be created
Where third_party code will be downloaded and installed
Directory containing all java samples
Directory containing C# examples and a visual studio 2010
solution to build them
examples/cpp/
C++ examples
examples/python/
Python examples
examples/tests/
Unit tests
lib/
Where libraries and jar les will be created
makefiles/
Directory that contains sub-makeles
objs/
Where C++ objs les will be stored
src/algorithms/
A collection of OR algorithms (non graph related)
src/base/
Directory containing basic utilities
src/com/
Directory containing java and C# source code for the libraries
src/constraint_solver/ The main directory for the constraint solver library
src/gen/
The root directory for all generated code (java classes, protocol buffers, swig les)
src/graph/
Standard OR graph algorithms
src/linear_solver/
The main directory for the linear solver wrapper library
src/util/
More utilities needed by various libraries
tools/
Binaries and scripts needed by various platforms
26
If
you
are
on
opensuse
and
maybe
redhat,
the
make
install_python_module will fail.
One workaround is described on this page
http://stackoverow.com/questions/4495120/combine-user-with-prex-error-with-setup-pyinstall.
If you have root privilieges, you can replace the last line and install the python modules for all
users with the following command:
cd dependencies/sources/google-apputils
sudo python2.7 setup.py install
will clean all downloaded sources, all compiled dependencies, and Makefile.local. It is
useful to get a clean state, or if you have added an archive in dependencies.archives.
27
then edit Makefile.local to point to the correct python and java installation. Afterwards,
to use python, you need to install google-apputils:
cd dependencies/sources/google-apputils
c:\python27\python.exe setup.py install
will clean all downloaded sources, all compiled dependencies, and Makefile.local. It is
useful to get a clean state, or if you have added an archive in dependencies.archives.
If everything is OK, it will run a selection of examples from all technologies in C++, python,
java, and C#.
28
You can then compile the library, examples and python, java, and .NET wrappings for the
constraint solver, the linear solver wrappers, and the algorithms:
make all
or
make DEBUG="/Od /Zi" all
under windows.
You can clean everything using:
make clean
Python examples
For the python examples, as we have not installed the constraint_solver module, we need to use
the following command:
on windows:
set PYTHONPATH=%PYTHONPATH%;<path to or-tools>\src,
then
c:\Python27\python.exe python/sample.py.
On unix:
PYTHONPATH=src <python_binary> python/<sample.py>
As in
29
There is a special target in the makele to run python examples. The above example can be run
with
make rpy EX=golomb8
Java examples
You can run java examples with the run_<name> makele target as in:
make run_RabbitsPheasants
There is a special target in the makele to run java examples. The above example can be run
with
make rjava EX=RabbitsPheasants
.NET examples
If you have .NET support compiled in, you can build .NET libraries with the command: make
csharp.
You can compile C# examples typing:
make csharpexe.
There is a special target in the makele to run C# examples. The above example can be run
with
make rcs EX=csflow
30
One solution is C=2 P=3 I=7 S=4 F=9 U=6 N=8 T=1 R=0 E=5 because
2 3
+
7 4
+
9 6 8
--------= 1 0 6 5
Ideally, a good cryptarithmetic puzzle must have only one solution2 . We derogate from this
tradition. The above example has multiple solutions. We use it to show you how to collect all
solutions of a problem.
31
What are the constraints? The obvious constraint is the sum that has to be veried. But there
are other - implicit - constraints. First, two different letters represent two different digits. This
implies that all the variables must have different values in a feasible solution. Second, it is
implicit that the rst digit of a number can not be 0. Letters C, I, F and T can thus not represent
0. Third, there are 10 letters, so we need at least 10 different digits. The traditional decimal
base is sufcient but lets be more general and allow for a bigger base. We will use a constant
kBase. The fact that we need at least 10 digits is not really a CP constraint. After all, the base
is not a variable but a given integer that is chosen once and for all for the whole program3 .
Model
For each letter, we have a decision variable (we keep the same letters to name the variables).
Given a base b, digits range from 0 to b-1. Remember that variables corresponding to C, I, F
and T should be different from 0. Thus C, I, F and T have [1, b 1] as domain and P, S, U, N,
R and E have [0, b 1] as domain. Another possibility is to keep the same domain [0, b 1] for
all variables and force C, I, F and T to be different from 0 by adding inequalities. However,
restraining the domain to [1, b 1] is more efcient.
To model the sum constraint in any base b, we add the linear equation:
+
=
T 3
F 2
R 2
+
+
+
C + P
I + S
U + N
U +
The global constraint AllDifferent springs to mind to model that variables must all have
different values:
AllDifferent(C,P,I,S,F,U,N,T,R,E)
Solve
At this stage of our discovery of the library, we will not try to nd a good search strategy
to solve this model. A default basic strategy will do for the moment. Chapter ?? is entirely
devoted to the subject of search strategies.
3
We could have chosen the base as a variable. For instance, to consider such a question as: What are the
bases for which this puzzle has less than x solutions?
32
2.3.1 At a glance
33
2.3.2 Headers
To use the library, we need to include a few headers:
#include "base/logging.h"
#include "constraint_solver/constraint_solver.h"
The header logging.h is needed for some logging facilities and some assert-like macros.
The header constraint_solver.h is the main entry point to the CP solver and must be
included4 whenever you intend to use it.
34
return 0;
}
The only argument of the constructor is an identication string. The Solver class has one
additional constructor covered in section 2.5.
2.3.5 Variables
To create the model, we rst need to create the decision variables:
const int64 kBase
IntVar* const c =
IntVar* const p =
...
IntVar* const e =
= 10;
solver.MakeIntVar(1, kBase - 1, "C");
solver.MakeIntVar(0, kBase - 1, "P");
solver.MakeIntVar(0, kBase - 1, "E");
For each letter, we create an integer variable IntVar whose domain is [0, kBase 1] except
for the variables c, i, f and t that cannot take the value 0. The MakeIntVar(i, j,
name) method is a factory method that creates an integer variable whose domain is [, ] =
{, +1, . . . , 1, } and has a name name. It returns a pointer to an IntVar. The declaration
IntVar* const c may seem a little be complicated at rst. It is easier to understand if read
from right to left: c is a constant pointer to an IntVar. We can modify the object pointed by
c but this pointer, because it is constant, always refers to the same object.
Factory methods in or-tools
The solver API provides numerous factory methods to create different objects. These
methods start with Make and return a pointer to the newly created object.
The solver automatically takes ownership of these objects and deletes them appropriately.
Never delete explicitly an object created by a factory method! First, the solver deletes
all the objects for you. Second, deleting a pointer twice in C++ gives undened
behavioura !
a
It is possible to bypass the undened behaviour but you dont know what the solver needs to do,
so keep your hands off of the object pointers! ;-)
Beside integer variables, the solver provides factory methods to create interval variables
35
CHECK_GE(x,y) is a macro that checks if condition (x) >= (y) is true. If not, the program is aborted and the cause is printed:
[23:51:34] examples/cp_is_fun1.cc:108: Check failed:
(kBase) >= (letters.size())
Aborted
2.3.7 Constraints
To create an integer linear constraint, we need to know how to multiply an integer variable with
an integer constant and how to add two integer variables. We have seen that the solver creates a
variable and only provides a pointer to that variable. The solver also provides factory methods
to multiply an integer coefcient by an IntVar given by a pointer:
IntVar* const var1 = solver.MakeIntVar(0, 1, "Var1");
// var2 = var1 * 36
IntVar* const var2 = solver.MakeProd(var1,36)->Var();
Note how the method Var() is called to cast the result of MakeProd() into a pointer to
IntVar. Indeed, MakeProd() returns a pointer to an IntExpr. The class IntExpr is a
base class to represent any integer expression.
Note also the order of the arguments MakeProd() takes: rst the pointer to an IntVar and
then the integer constant.
To add two IntVar given by their respective pointers, the solver provides again a factory
method:
//var3 = var1 + var2
IntVar* const var3 = solver.MakeSum(var1,var2)->Var();
36
If the number of terms in the sum to construct is large, you can use MakeScalProd(). This
factory method accepts an std::vector of pointers to IntVars and an std::vector of
37
integer coefcients:
IntVar* const var1 = solver.MakeInt(...);
...
IntVar* const varN = solver.MakeInt(...);
std::vector<IntVar*> variables;
variables.push_back(var1);
...
variables.push_back(varN);
std::vector<int64> coefficients(N);
// fill vector with coefficients
...
IntVar* const sum = solver.MakeScalProd(variables, coefficients)->Var();
Adding the global AllDifferent constraint is a little bit easier because the solver provides
a factory method MakeAllDifferent(). This methods accepts an std::vector of
IntVar*:
std::vector<IntVar*> letters;
letters.push_back(c);
letters.push_back(p);
...
letters.push_back(e);
solver.AddConstraint(solver.MakeAllDifferent(letters));
38
The rst parameter of the method MakePhase is an std::vector with pointers to the
IntVar decision variables. The second parameter species how to choose the next IntVar
variable to be selected in the search. Here we choose the rst unbounded variable. The third
parameter indicates what value to assign to the selected IntVar. The solver will assign the
smallest available value.
To actually search for the next solution in the search tree, we call the method
NextSolution(). It returns true if a solution was found and false otherwise:
if (solver.NextSolution()) {
// Do something with the current solution
} else {
// The search is finished
}
<<
<<
<<
<<
<<
"
"
"
"
"
"
"
"
"
"
<<
<<
<<
<<
<<
"P="
"S="
"U="
"T="
"E="
<<
<<
<<
<<
<<
p->Value() <<
s->Value() <<
u->Value() <<
t->Value() <<
e->Value();
"
"
"
"
"
"
"
"
// Is CP + IS + FUN = TRUE?
CHECK_EQ(p->Value() + s->Value() + n->Value() +
kBase * (c->Value() + i->Value() + u->Value()) +
kBase * kBase * f->Value(),
e->Value() +
kBase * u->Value() +
6
Actually and contrary to the intuition, NextSolution() doesnt return a feasible solution per se. It all
depends of the involved DecisionBuilder. The solver considers any leaf of the search tree as a solution if it
doesnt fail (i.e. if it is accepted by several control mechanisms). See the section Basic working of the solver: the
search algorithm for more details.
39
We check the validity of the solution after printing: if the solution is not valid, we can see what
was found by the solver.
To obtain all the solutions, NextSolution() can be called repeatedly:
while (solver.NextSolution()) {
// Do something with the current solution
} else {
// The search is finished
}
This method ensures that the solver is ready for a new search and if you asked for a prole le,
this le is saved. You can nd more about the prole le in section 2.5.2. What happens if
you forget to end the search and didnt ask for a prole le? If you dont ask the solver to start
a new search, nothing bad will happen. It is just better practice to nish the search with the
method EndSearch().
See also What is the difference between NewSearch() and Solve()?.
40
2.4.1 SolutionCollectors
The SolutionCollector class is one of several specialized SearchMonitors classes.
i.e. SolutionCollector inherits from SearchMonitors. SearchMonitors provides a set of callbacks to monitor all search events. We will learn more about them in the next
chapter.
To collect solutions, several SolutionCollector are available:
FirstSolutionCollector: to collect the rst solution of the search;
LastSolutionCollector: to collect the last solution of the search;
BestValueSolutionCollector: to collect the best solution of the search;
AllSolutionCollector: to collect all solutions of the search.
The solver provides corresponding factory methods:
MakeFirstSolutionCollector();
MakeLastSolutionCollector();
MakeBestValueSolutionCollector();
MakeAllSolutionCollector().
The simplest way to use a SolutionCollector is to use it as is without any parameter.
This can be handy if you are only interested in global results such as the number of solutions:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
...
DecisionBuilder* const db = ...
...
solver.NewSearch(db, all_solutions);
while (solver.NextSolution()) {};
solver.EndSearch();
LOG(INFO) << "Number of solutions: " << all_solutions->solution_count();
In case you are curious about the number of solutions, there are 72 of them in base 10.
To effectively store some solutions in a SolutionCollector, you have to add the variables
you are interested in. Lets say you would like to know what the value of variable c is in the
rst solution found. First, you create a SolutionCollector:
FirstSolutionCollector* const first_solution =
solver.MakeFirstSolutionCollector();
Then you add the variable you are interested in to the SolutionCollector:
41
first_solution->Add(c);
The method Add() simply adds the variable c to the SolutionCollector. The
variable c is not tied to the solver, i.e. you will not be able to retrieve its value by
c->Value() after a search with the method Solve().
To launch the search:
solver.Solve(db,first_solution);
After the search, you can retrieve the value of c like this:
first_solution->solution(0)->Value(c)
In both cases, the index 0 denotes the rst solution found. If you nd it odd to specify the
index of the rst solution with a FirstSolutionCollector, dont forget that the API is
intended for generic SolutionCollectors including the AllSolutionCollector.
Lets use the AllSolutionCollector to store and retrieve the values of the 72 solutions:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the variables to the SolutionCollector
all_solutions->Add(letters);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);
// Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl;
for (int index = 0; index < number_solutions; ++index) {
LOG(INFO) << "Solution found:";
LOG(INFO) << "C=" << all_solutions->Value(index,c) << " "
<< "P=" << all_solutions->Value(index,p) << " "
...
<< "E=" << all_solutions->Value(index,e);
}
You are not limited to the variables of the model. For instance, lets say you are interested to
know the value of the expression kBase * c + p. Just construct a corresponding variable
and add it to the SolutionCollector:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the interesting variables to the SolutionCollector
all_solutions->Add(c);
all_solutions->Add(p);
// Create the variable kBase * c + p
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var();
42
2.4.2 Assignments
The or-tools library provides the class Assignment to store the solution (in parts or as a
whole). The class Assignment has a rich API that allows you to retrieve not only the values
of the variables in a solution but also additional information. You can also act on some of the
variables for instance to disable them during a search. We will see this class in more details in
chapter XXX.
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the interesting variables to the SolutionCollector
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var();
// Add it to the SolutionCollector
all_solutions->Add(v1);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);
// Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl;
for (int index = 0; index < number_solutions; ++index) {
Assignment* const solution = all_solutions->solution(index);
LOG(INFO) << "Solution found:";
LOG(INFO) << "v1=" << solution->Value(v1);
}
43
2.5. Parameters
or
solver.NewSearch();
while (solver.NextSolution()) {...};
solver.EndSearch();
With NewSearch() you can access the variables of the current solutions (no need for a
SolutionCollector). More importantly, you can interfere with the search.
2.5 Parameters
This section is divided in two parts. First, we show you how to use Googles command line
ag library. Second, we explain how to pass parameters to the CP solver.
44
Note that argc and argv are passed as pointers so that ParseCommandLineFlags() is
able to modify them.
All dened ags are accessible as normal variables with the prex FLAGS_ prepended:
const int64 kBase = FLAGS_base;
If you want to know what the purpose of a ag is, just type one of the special ags on the
command line:
--help: prints all the ags
--helpshort: prints all the ags dened in the same le as main()
--helpon=FILE: prints all the ags dened in le FILE
--helpmatch=S: prints all the ags dened in the les *S*.*
For other features and to learn more about this library, we refer you to the gags documentation.
We can now ask for a detailed report after the search is done:
// Save profile in file
solver.ExportProfilingOverview("profile.txt");
45
We will see how to prole more in details in the section 12.4. The SolverParameters
struct mainly deals with the internal usage of memory and is for advanced users.
SearchMonitors
Second, you can use SearchMonitors. We have already seen how to use them to collect
solutions in section 2.4. Suppose we want to limit the available time to solve a problem. To
pass this parameter on the command line, we dene a time_limit variable:
DEFINE_int64(time_limit, 10000, "Time limit in milliseconds");
46
become
2.7 Summary
summary
47
CHAPTER
THREE
In this chapter, we are not only looking for a feasible solution but we want the best solution!
Most of the time, the search is done in two steps. First, we nd the best solution1 . Second, we
prove that this solution is indeed the best (or as good as any other feasible solution in case there
are multiple optimal solutions) by scouring (preferably implicitly) the complete search tree.
Overview:
We start by stating the Golomb Ruler Problem (GRP) and showing that this problem is difcult. We implement ve models and compare them two by two. To do so, we introduce some
basic statistics about the search (time, failures, branches, ...). Two very useful techniques are
introduced: adding better bounds and breaking symmetries. Finally, we say a few words about
the strategies used by the solver to optimize an objective function.
Prerequisites:
The sums used in this chapter to model the GRP are tricky but you dont need to master
them. We do all the dirty work for you. In fact, you can completely skip them if you
wish. The basic ideas behind these sums are simple and are easy to follow.
We introduce two kinds of variables in our modelizations: the marks of the ruler and the
differences between the marks.
1
How do we know we have a best solution? Only when we have proven it to be so! The two steps are
intermingled. So why do we speak about two steps? Because, most of the time, it is easy to nd a best (good)
solution (heuristics, good search strategies in the search tree, ...). The time-consuming part of the search consist
in disregarding/visiting the rest of the search tree.
(1)
2
dif-
To be honest, if you really want to solve the Golomb Ruler Problem, you shouldnt use CP as, until now, no
one found how to use CP in an efcient manner to solve this difcult problem.
50
11
4
7
9
11
51
6
2
4
5
6
Days
1,572
3,006
24
Participants
41,803
124,387
2754
Visited nodes
555,551,924,848,254,200
52,898,840,308,130,480,000
3,185,174,774,663,455
The search for (27) started on February 24, 2009 and at that time was expected to take... 7
years! Still think it is an easy6 problem? You too can participate: The OGR Project.
You can nd all the known optimal Golomb rulers and more information on Wikipedia.
Why Golomb Rulers?
Golomb rulers have a wide variety of applications, including radio astronomy and information theory. In radio astronomy, when constrained to be lined up, telescopes collect more
accurate information if they are placed on the marks of a Golomb ruler. In information
theory, Golomb rulers are used for error detection and correction.
http://stats.distributed.net/projects.php?project_id=24
http://stats.distributed.net/projects.php?project_id=25
5
http://stats.distributed.net/projects.php?project_id=26
6
Although it is strongly suspected that the Golomb Ruler Problem is a very difcult problem, the computational complexity of this problem is unknown (see [Meyer-Papakonstantinou]).
4
52
or choose the unknowns to be the differences (and retrieve the marks). Lets try this second
approach and use the efcient AllDifferent constraint. There are (1) such differences.
2
What are the constraints? Using the differences as variables, we need to construct a Golomb
ruler, i.e. the structure of the Golomb ruler has to be respected (see next section).
Model
For each positive difference, we have a decision variable. We collect them in an array . Lets
order the differences so that we know which difference is represented by []. Figure 3.3
illustrates an ordered sequence of differences for a Golomb ruler of order 4.
1st
2nd
4
3rd
th
5th
6th
Figure 3.3: An ordered sequence of differences for the Golomb ruler of order 4.
We want to minimize the last difference in i.e. [ (1) 1] since the rst index of an array
2
is 0. When the order is = 4, we want to optimize [ 4(41) 1] = [5] which represents the
2
6th difference. Instead of writing [], we will also use the more convenient notation .
Figure 3.4 illustrates the structures than must be respected for a Golomb ruler of order 5. To impose the inner structure of the Golomb Ruler, we force 4 = 0 + 1 , 5 = 1 + 2
and so on as illustrated in Figure 3.4.
index
Y0
Y1
Y2
Y3
i=2
Y4
Y5
i=3
Y6
i=4
Y7
Y4
Y5
Y6
=
=
=
Y0
Y7
Y8
=
=
Y0
Y9
Y0
+
+
Y1
Y1
Y2
Y2
Y3
Y1
Y1
+ Y2
+ Y2
Y3
Y1
Y3
Y2
j=0
j=1
j=2
j=0
j=1
j=0
Y8
Y9
Or more generally from the index of(the rst difference that is the sum of two differences in our sequence
)
( 1) to the index of the last difference (1) 1 .
2
53
int index = n - 2;
for (int i = 2; i <= n - 1; ++i) {
for (int j = 0; j < n-i; ++j) {
++index;
Y[index] = Y[j] + ... + Y[j + i - 1];
}
}
Solve
Again, at this stage of our discovery of the library, we will not try to nd a good search strategy
to solve this model. A default basic strategy will do for the moment. The next chapter 5 is
entirely devoted to the subject of search strategies.
< .
3 22 + 2 1.
Most bounds are really bad and this one isnt an exception. The great mathematician Paul Erds
conjectured that
() < 2 .
This conjecture hasnt been proved yet but computational evidence has shown that the conjecture holds for < 65000 (see [Dimitromanolakis2002]).
This is perfect for our needs:
CHECK_LT(n, 65000);
const int64 max = n * n - 1;
54
Note that these two methods dont provide the same result! MakeIntVarArray()
appends num_vars IntVar* to the std::vector with names Y_i where i
goes from 0 to num_vars - 1.
It is a convenient shortcut to quickly create an std::vector<IntVar*> (or to append some IntVar*s to an existing
std::vector<IntVar*>).
StringPrintf() (shown in the rst example) is a helper function declared in the header
base/stringprintf.h that mimics the C function printf().
We use the AllDifferent constraint to ensure that the differences (in Y) are distinct:
s.AddConstraint(s.MakeAllDifferent(Y));
and the following constraints to ensure the inner structure of a Golomb ruler as we have seen
in the previous section8 :
int index = n - 2;
IntVar* v2 = NULL;
for (int i = 2; i <= n - 1; ++i) {
for (int j = 0; j < n-i; ++j) {
++index;
v2 = Y[j];
for (int p = j + 1; p <= j + i - 1 ; ++p) {
v2 = s.MakeSum(Y[p], v2)->Var();
}
s.AddConstraint(s.MakeEquality(Y[index], v2));
}
}
CHECK_EQ(index, num_vars - 1);
How do we tell the solver to optimize? Use an OptimizeVar to declare the objective function:
OptimizeVar* const length = s.MakeMinimize(Y[num_vars - 1], 1);
Remember the remark at the beginning of this chapter about the tricky sums!
55
In the section 3.9, we will explain how the solver optimizes and the meaning of the mysterious
parameter 1 in
... = s.MakeMinimize(Y[num_vars - 1], 1);
Y_4(1..24)
Y_5(1..24)
Y_6(1..24)
Y_7(1..24)
==
==
==
==
Var<(Y_1(1..24)
Var<(Y_2(1..24)
Var<(Y_3(1..24)
Var<(Y_2(1..24)
+
+
+
+
Y_0(1..24))>(2..48)
Y_1(1..24))>(2..48)
Y_2(1..24))>(2..48)
Var<(Y_1(1..24) +
Y_0(1..24))>(2..48))>(3..72)
...: Y_8(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) +
Y_1(1..24))>(2..48))>(3..72)
...: Y_9(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) +
Var<(Y_1(1..24) + Y_0(1..24))>(2..48))>(3..72))>(4..96)
These are exactly the constraints listed in Figure 3.4 page 53.
56
For the le
gives us:
...: BoundsAllDifferent(Y_0(1..24), Y_1(1..24), Y_2(1..24), Y_3(1..24),
Y_4(1..24), Y_5(1..24), Y_6(1..24), Y_7(1..24), Y_8(1..24),
Y_9(1..24))
This is the AllDifferent constraint on bounds where we see all the variables with their
initial domains.
Then:
...: cast((Y_1(1..24) + Y_0(1..24)), Var<(Y_1(1..24) + Y_0(1..24))>
(2..48))
57
...:
...:
...:
...:
Y_4(1..24)
Y_5(1..24)
Y_6(1..24)
Y_7(1..24)
==
==
==
==
Var<(Y_1(1..24)
Var<(Y_2(1..24)
Var<(Y_3(1..24)
Var<(Y_2(1..24)
+
+
+
+
Y_0(1..24))>(2..48)
Y_1(1..24))>(2..48)
Y_2(1..24))>(2..48)
Var<(Y_1(1..24) +
Y_0(1..24))>(2..48))>(3..72)
...: Y_8(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) +
Y_1(1..24))>(2..48))>(3..72)
...: Y_9(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) + Var<(Y_1(1..24) +
Y_0(1..24))>(2..48))>(3..72))>(4..96)
...: Forcing early failure
...: Check failed: (collector->solution_count()) == (1)
Aborted
All this output was generated from the following line in constraint_solver.cc:
LOG(INFO) << c->DebugString();
Indeed, we have 1 AllDifferent constraint, 6 equality constraints and 10 IntVar variables. Where does the rest come from?
To construct the equality constraints, we cast 10 times integer expressions into IntVar (remember the ...->Var() calls), hence the 10 integer expressions, the 10 supplementary
IntVar variables and the 10 sums. The 2 model extensions are the objective OptimizeVar
variable and the std::vector array of IntVar variables (VariableGroup).
Try the other ags!
58
3.5.1 Time
This is probably the most common statistic. There exist several timing libraries or tools to
measure the duration of an algorithm. The or-tools library offers a basic but portable timer.
This timer starts to measure the time from the creation of the solver.
solver("TicTac") s;
//
If you need the elapsed time since the creation of the timer, just call wall_time():
const int64 elapsed_time = s.wall_time();
The time is given in milliseconds. If you only want to measure the time spent to solve the
problem, just subtract times:
const int64 time1 = s.wall_time();
s.Solve(...);
const int64 time2 = s.wall_time();
LOG(INFO) << "The Solve method took " << (time2 - time1)/1000.0 <<
" seconds";
As its name implies, the time measured is the wall time, i.e. it is the difference between the
time at which a task nishes and the time at which the task started and not the actual time spent
by the computer to solve a problem.
For instance, on our computer, the program in golomb1.cc for = 9 takes
Time: 4,773 seconds
59
3.5.2 Failures
3.5.3 Branches
3.5.4 SearchLimits
3.6.1 Variables
You can nd the code in the le tutorials/cplusplus/chap3/golomb3.cc. Before
we dive into the code, lets be practical and ease our life a bit. One of the difculties of the code
in golomb1.cc is that we use the rst element of the array Y. There is no need to do so. In
golomb3.cc, we use X[1] as the rst mark (and not X[0]). In the same vain, we redene
the array kG such that kG(n) = G(n) (and not kG(n-1) = G(n)). Thus:
std::vector<IntVar*> X(n + 1);
X[0] = s.MakeIntConst(-1); // The solver doesnt allow NULL pointers
X[1] = s.MakeIntConst(0);
// X[1] = 0
We use an std::vector slightly bigger (by one more element) than absolutely necessary.
Because the solver doesnt allow NULL pointers, we have to assign a value to X[0]. The rst
mark X[1] is 0. We use again 2 1 as an upper bound for the marks:
// Upper bound on G(n), only valid for n <= 65 000
CHECK_LE(n, 65000);
const int64 max = n * n - 1;
...
for (int i = 2; i <= n; ++i) {
X[i] = s.MakeIntVar(1, max, StringPrintf("X%03d", i));
}
This time we dont use MakeIntVarArray() because we want a better control on the names
of the variables.
3.6.2 Constraints
To express that all the differences between all pairs of marks must be distinct, we use the
quaternary constraints9 :
[] [] = [] []
9
60
, , ,
Quaternary constraints is just a fancy way to say that the constraints each involves four variables.
Figure 3.5: Another ordered sequence of differences for the Golomb ruler of order 4.
With this order dened on the differences, we can easily generate all the quaternary constraints.
Take the rst difference and impose it to be different from the second difference, then to be
different from the third difference and so on as suggested in Figure 3.6. Take the second
1st
2nd
=
=
=
=
=
3rd
4th
5th
6th
=
=
=
=
3rd
4th
5th
6th
61
CHECK_GE(i, 1);
CHECK_GT(j, 1);
if (j == n) {
if (i == n - 1) {
return false;
} else {
*next_i = i + 1;
*next_j = i + 2;
}
} else {
*next_i = i;
*next_j = j + 1;
}
return true;
}
If there is a next interval, the function next_interval() returns true, false otherwise.
We can now construct our quaternary constraints11 :
IntVar* diff1;
IntVar* diff2;
int k, l, next_k, next_l;
for (int i = 1; i < n - 1; ++i) {
for (int j = i + 1; j <= n; ++j) {
k = i;
l = j;
diff1 = s.MakeDifference(X[j], X[i])->Var();
diff1->SetMin(1);
while (next_interval(n, k, l, &next_k, &next_l)) {
diff2 = s.MakeDifference(X[next_l], X[next_k])->Var();
diff2->SetMin(1);
s.AddConstraint(s.MakeNonEquality(diff1, diff2));
k = next_k;
l = next_l;
}
}
}
Note that we set the minimum value of the difference to 1, diff1->SetMin(1), to ensure
that the differences are positive and
1. Note also that the method MakeDifference()
doesnt allow us to give a name to the new variable, which is normal as this new variable is the
difference of two existing variables. Its name is simply name1 - name2.
Lets compare the rst and second implementation. The next table compares some global
statistics about the search for (9).
11
62
Remember again the remark at the beginning of this chapter about the tricky sums.
Impl1
4,712
51 833
103 654
51 836
Impl2
48,317
75 587
151 169
75 590
If the rst model was bad, what can we say about this one? What went wrong? The quaternary
constraints... These constraints are all disparate and thus dont allow efcient propagation.
and compare this improved version with the two others, again to compute (9):
Statistics
Time (s)
Failures
Branches
Backtracks
Impl1
4,712
51 833
103 654
51 836
Impl2
48,317
75 587
151 169
75 590
Impl2+
1,984
53 516
107 025
53 519
Although we have more failures, more branches and we do backtrack more than in the rst
model, we were able to divide the time by 2! Can we do better? You bet!
63
Impl1
4,712
51 833
103 654
51 836
Impl2
48,317
75 587
151 169
75 590
Impl2+
1,984
53 516
107 025
53 519
Impl3
0,338
7 521
15 032
7 524
What an improvement! In the next section, we present two strategies that generally allow to
tighten a given model and thus improve, sometimes dramatically, the search time.
There exist other techniques. Later, in section XXX, we will see how over-constraining can improve the
search.
13
This short explanation is certainly too simple to describe all the subtleties of search strategies. After all,
modelling is an art!
64
(3.1)
(3.2)
would have been two different solutions and we would explicitly have had to tell the solver not
to generate the second one:
for (int i = 1; i < n; ++i) {
s.AddConstraint(s.MakeLess(X[i],X[i+1]));
}
Thanks to diff1->SetMin(1) and diff2->SetMin(1) and the two for loops, the
ordered variables [1], [2], [3], [4] have only increasing values, i.e. if
then []
[]. Solutions (3.1) and (3.2) are said to be symmetric and avoiding the second one while
accepting the rst one is called breaking symmetry.
There is a well-known symmetry in the Golomb Ruler Problem that we didnt break. Whenever
you have a Golomb ruler, there exist another Golomb ruler with the same length that is called
the mirror ruler. Figure 3.8 illustrates two mirror Golomb rulers of order 4.
14
Declaring variables in an std::vector doesnt tell anything about their respective values!
65
Later on, in section 5.8, we will see how to provide some rules to the solver (by implementing
SymmetryBreakers) so that it generates itself the constraints to break symmetries. These
constraints are generated on the y during the search!
These transformations were discovered in the beginning of the 20th century without any computer! See
http://www.research.ibm.com/people/s/shearer/grtab.html.
66
Constraint * AllDifferent(Solver* s,
const std::vector<std::vector<IntVar *> > & vars) {
std::vector<IntVar*> vars_flat;
for (int i = 0; i < vars.size(); ++i) {
for (int j = 0; j < vars[i].size(); ++j) {
if (vars[i][j] != NULL) {
vars_flat.push_back(vars[i][j]);
}
}
}
return s->MakeAllDifferent(vars_flat);
}
These are static bounds, i.e. they dont change during the search. Dynamic bounds are even
better as they improve during the search and tighten the domains even more.
For instance, note that
[1][2] + [2][3] + ... + [][] + ... + [ 1][] = []
so
[][] = [] { [1][2] + [2][3] + ... + [ 1][] + [][ + 1] + ... + [ 1][]}
The differences on the right hand side of this expression are a set of different integers and there
are 1 + of them. If we minimize the sum of these consecutive differences, we actually
maximize the right hand side, i.e. we bound [][] from above:
[][]
[] ( 1 + )( + )/2
We can add:
for (int i = 1; i < n; ++i) {
for (int j = i + 1; j <= n; ++j) {
s.AddConstraint(s.MakeLessOrEqual(s.MakeDifference(
Y[i][j],X[n])->Var(), -(n - 1 - j + i)*(n - j + i)/2));
}
}
Lets compare our tightened third implementation with the rest, again to compute (9):
Statistics
Time (s)
Failures
Branches
Backtracks
Impl1
4,712
51 833
103 654
51 836
Impl2
48,317
75 587
151 169
75 590
Impl2+
1,984
53 516
107 025
53 519
Impl3
0,338
7 521
15 032
7 524
tightened Impl3
0,137
2288
4572
2291
67
68
CHAPTER
FOUR
REIFICATION
Overview:
Overview...
Prerequisites:
Classes under scrutiny:
Files:
Part II
Customization
CHAPTER
FIVE
This chapter is about the customization of the search. What stategy(ies) to use to branch, i.e.
what variables to select and what value(s) to assign to them? How to use nested searches, i.e.
searches in subtrees? And so on.
The or-tools CP solver is quite exible and comes with several tools (Decisions,
DecisionBuilders, ...) that we call search primitives. Some are predened and can be
used right out of the box while others can be customized thanks to callbacks. You can also
combine different search strategies.
To efciently use your tools, you need to know them a little and this chapter introduces you in
a gentle manner to the inner working of the solver. The covered material is enough for you to
understand how you can customize your search primitives without being drowned in the often
tedious details of the implementation1 . To illustrate the customization of the search, we try to
solve the n-queen problem we have already met in chapter 1.
Prerequisites:
Decision,
DecisionBuilder,
TreeMonitor.
1
DecisionVisitor,
SearchMonitor,
If you take a look at the source code, we hope you will enjoy the clarity (?) of our code. We believe that the
most efcient code should remain simple and allow for more complicated but more efcient specializations when
needed.
Files:
In computer science jargon, we say that the problem of nding one solution for the n-queens problem is in
. Actually, its the decision version of this problem but to keep it simple, lets say that nding one solution is
straightforward and easy and shouldnt take too long.
74
al. proposed a simple algorithm to return a solution of the n-queens problem [Hoffman1969].
So we have to be careful when we talk about the n-queens problem. There are at least three
different problems that people refer to when talking about the n-queens problem:
nding one solution3 ,
counting the number of solutions and
nding (explicitly) all these solutions.
While the rst problem is easy, the two others are difcult4 .
As with the Golomb rulers problem, the experts could only nd the number of all the solutions
for small values. The biggest number of queens for which we know precisely the number
of solutions is = 26. The On-Line Encyclopedia of Integer Sequences keeps track of the
number of solutions (sequence A002562 for unique solutions (up to a symmetry) and sequence
A000170 for distinct solutions). The next table reports the number of unique and distinct
solutions for several values of .
n:
unique:
distinct:
1
1
1
2
0
0
3
0
0
4
1
2
5
2
10
6
1
4
7
6
40
8
12
92
9
46
352
10
92
724
11
341
2,680
12
1,787
14,200
13
9,233
73,712
14
45,752
365,596
Notice that there are more solutions for = 5 than = 6. What about the last three known
values? Here there are:
n:
unique:
distinct:
24
28,439,272,956,934
227,514,171,973,736
25
275,986,683,743,434
2,207,893,435,808,352
26
2,789,712,466,510,289
22,317,699,616,364,044
Quite impressive, isnt it? Its even more impressive when you know that these numbers were
obtained by explicitly nding all these solutions!
Is the n-queens problem only a toy problem?
While the n-queens problem is a wonderful problem to study backtracking systems and
is intensively used in benchmarks to test these systems, there are real problems that can
be modelled and solved as n-queens problems. For instance, it has been used for parallel memory storage schemes, VLSI testing, trafc control and deadlock prevention (see
[Jordan2009]).
75
Describe
What is the goal of the n-queens problem? We will focus on nding one or all solutions. Given
a size for the chessboard, place queens5 so that no two queens attack each other.
What are the decision variables (unknowns)? We have different choices. One clever way to
reduce the number of variables is to introduce only one variable for each queen.
What are the constraints? No two queens can attack each other. This means to place queens
on the chessboard such that no two queens are placed on the same row, the same column or the
same diagonal.
Model
We know that no two queens can be placed on the same column and that we have as much
queens as columns. We will use one variable to place one queen on each column. The value of
the variable will denote the row of the corresponding queen. Figure 5.1 illustrates the variables
we will use to solve the n-queens problem in this chapter.
It is not obvious that for every , there exist at least a solution. In fact, for = 2 and = 3 there are no
solution. Hoffman et al. proved that there are solutions for every 4 in [Hoffman1969].
76
"base/commandlineflags.h"
"base/logging.h"
"base/stringprintf.h"
"constraint_solver/constraint_solver.h"
The header nqueens_utilities.h contains some helper functions (see the subsection
The helper functions below): among other CheckNumberOfSolutions() to check the
known number of solutions (unique or distinct) of the n-queens problem and several functions to print the solutions recorded by a SolutionCollector. To be able to collect only unique solutions (up to a symmetry), we will use SymmetryBreakers in section 5.8 page 126. A boolean gag FLAGS_use_symmetry allows or disallows the use of
SymmetryBreakers. This ag is dened in the header ./nqueens_utilities.h and
to be able to use it in our main le, we need to declare it:
DECLARE_bool(use_symmetry);
For the moment we dont implement any symmetry related mechanism and abort in the main
function if FLAGS_use_symmetry is set to true:
int main(int argc, char **argv) {
google::ParseCommandLineFlags(&argc, &argv, true);
if (FLAGS_use_symmetry) {
LOG(FATAL) << "Symmetries not yet implemented!";
}
if (FLAGS_size != 0) {
operations_research::NQueens(FLAGS_size);
} else {
for (int n = 1; n < 12; ++n) {
operations_research::NQueens(n);
}
}
return 0;
}
We offer the possibility to print the rst solution (ag print set to true) or all solutions (ag
print_all set to true)6 . By default, the program doesnt output any solution.
6
77
This AllDifferent(0 , . . . , 1 ) basically ensures no two queens remain on the same row
but we could have a solution like the one depicted on the Figure 5.2.
(5.1)
0 = 1 + 1, 0 = 2 + 2, 0 = 3 + 3, . . .
(5.2)
(5.1) is equivalent to
Take the second queen 1 . We only have to look for the queens to her right. To impose that 1
doesnt attack any queen 2 , 3 , . . . on a diagonal with slope +1, we can add
1 1 = 2 , 1 2 = 3 , 1 3 = 4 , . . .
(5.3)
1 = 2 + 1, 1 = 3 + 2, 1 = 4 + 3, . . .
(5.4)
or equivalently
78
(5.5)
: > 7 .
This means that we can restrict ourselves to inequalities only involving + terms. Each
of these terms must be different from all others. Doesnt this ring a bell? Yep, this is the
AllDifferent constraint:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3, 4 + 4, . . .)
(5.6)
(5.7)
ensures that no two queens are on the same diagonal with slope 1 (diagonals that slope downand-right).
We can thus add:
std::vector<IntVar*> vars(size);
for (int i = 0; i < size; ++i) {
vars[i] = s.MakeSum(queens[i], i)->Var();
}
s.AddConstraint(s.MakeAllDifferent(vars));
for (int i = 0; i < size; ++i) {
vars[i] = s.MakeSum(queens[i], -i)->Var();
}
s.AddConstraint(s.MakeAllDifferent(vars));
To collect the rst solution and count all the solutions, we use SolutionCollectors as
usual:
SolutionCollector* const solution_counter =
s.MakeAllSolutionCollector(NULL);
SolutionCollector* const collector = s.MakeFirstSolutionCollector();
collector->Add(queens);
std::vector<SearchMonitor*> monitors;
monitors.push_back(solution_counter);
monitors.push_back(collector);
// go!
79
80
return;
}
You might wonder why we cast the return value of collector->Value() into an int?
The value() method returns an int64.
10
0
0,055
11
0
0,259
12
0
1,309
13
0
7,059
14
0,003
40,762
To nd all solutions, the solver shows a typical exponential behaviour for intractable problems.
The sizes are too small to conclude anything about the problem of nding one solution. In the
next Table, we try bigger sizes. The results are again in seconds.
Problem
First solution
25
0,048
26
0,392
27
0,521
28
3,239
29
1,601
30
63,08
31
14,277
It looks like our solver has some troubles to nd one solution. This is perfectly normal because
we didnt use a specic search strategy. In the rest of this chapter, we will try other search
strategies and compare them. We will also customize our strategies, i.e. dene strategies of our
own but before we do so, we need to learn a little bit about the basic working of the solver.
81
xi = 2
xi = 2
82
Not to be confused with a binary search tree (BST) used to store ordered sets.
Callbacks
To customize the search, we use callbacks. A callback is a reference to a piece of executable
code (like a function or an object) that is passed as an argument to another code. This is a very
common and handy way to pass high level code to low level code. For example, the search
algorithm is low level code. You dont want to change this code but you would like to change
the behaviour of the search algorithm to your liking. How do you do this? Callbacks are to the
rescue! At some places in the low level code, some functions are called and you can redene
those functions. There are several techniques available. In this section, we redene some virtual
functions of an abstract class. In section XXX, we will see another similar mechanism.
An example will clarify this mechanism. Take a SearchMonitor class. If you want to
implement your own search monitor, you inherit from SearchMonitor and you redene the
methods you need:
class MySearchMonitor: public SearchMonitor {
...
void EnterSearch() {
LG << "Search entered...";
}
...
};
At the beginning of a search, the solver calls the virtual method EnterSearch() i.e. your
EnterSearch() method. Dont forget to delete your SearchMonitor after use. You
can also use a smart pointer or even better, let the solver take ownership of the object with the
RevAlloc() method (see subsection 5.8.3).
Phases
The CP solver allows you to combine several searches, i.e. different types of sub-searches. You
can search a subtree of the search tree differently from the rest of your search. This is called
nested search while the whole search is called a top-level search. There are no limitations and
you can nest as many searches as you like. You can also restart a (top level or nested) search.
83
In or-tools, each time you use a new DecisionBuilder, we say you are in a new phase.
This is where the name MakePhase comes from.
Apply()
Refute()
84
// Overwrites default.
The real code deals with a lots of subtleties to implement different variants of the search algorithm.
NextSolution();
const bool solution_found = searches_.back()->solution_counter() > 0;
EndSearch();
return solution_found;
6
7
8
9
10
searches_ is an std::vector of Searches because we can nest our searches (i.e search
differently in a subtree using another phase/DecisionBuilder). Here we take the current
search (searches_.back()) and tell the solver that the search was initiated by a Solve()
call:
searches_.back()->set_created_by_solve(true);
// Overwrites default.
Indeed, the solver needs to know if it let you interfere during the search process or not.
You might wonder why there is only one call to NextSolution()? The reason is simple.
If the search was initiated by the caller (you) with the NewSearch() - NextSolution()
- EndSearch() mechanism, the solver stops the search after a NextSolution() call.
If the search was initiated by a Solve() call, you tell the solver when to stop the search
with SearchMonitors. By default, the solver stops after the rst solution found (if any).
You can overwrite this behaviour by implementing the AtSolution() callback of the
SearchMonitor class. If this method returns true, the search continues, otherwise the
solver ends it.
5.3.3 The basic search algorithm and the callback hooks for the
SearchMonitors
SearchMonitors contain a set of callbacks called on search tree events, such as entering/exiting search, applying/refuting decisions, failing, accepting solutions... In this section,
we present the callbacks of the SearchMonitor class10 listed in Table 5.1 and show you
exactly when they are called in the search algorithm.
We draw again your attention to the fact that the algorithm shown here is a simplied version
of the search algorithm. In particular, we dont show how the nested searches and the restart of
a search are implemented. We nd this so important that we reuse our warning box:
We describe a simplied version of the main loop of the search algorithm.
We use exceptions in our simplied version while the actual implementation uses the more
efcient (and cryptic) setjmp - longjmp mechanism.
We describe briey what nested searches are in the section Nested searches but you will have
to wait until the chapter Under the hood and the section Nested searches to learn the juicy
details11 .
10
85
Table 5.1: Basic search algorithm callbacks from the SearchMonitor class.
Methods
EnterSearch()
ExitSearch()
BeginNextDecision(DecisionBuilder*
const b)
EndNextDecision(DecisionBuilder*
const b, Decision* const d)
ApplyDecision(Decision* const d)
RefuteDecision(Decision* const d)
AfterDecision(Decision* const d,
bool apply)
BeginFail()
EndFail()
BeginInitialPropagation()
EndInitialPropagation()
AcceptSolution()
AtSolution()
NoMoreSolutions()
Descriptions
Beginning of the search.
End of the search.
Before calling DecisionBuilder::Next().
After calling DecisionBuilder::Next(),
along with the returned decision.
Before applying the Decision.
Before refuting the Decision.
Just after refuting or applying the Decision,
apply is true after Apply(). This is called only
if the Apply() or Refute() methods have not
failed.
Just when the failure occurs.
After completing the backtrack.
Before the initial propagation.
After the initial propagation.
This method is called when a solution is found. It
asserts if the solution is valid. A value of false indicates that the solution should be discarded.
This method is called when a valid solution is found.
If the return value is true, then search will resume.
If the result is false, then search will stop there.
When the search tree has been visited.
To follow the main search algorithm, it is best to know in what states the solver can be. The
enum SolverState enumerates the possibilities in the following table:
Value
OUTSIDE_SEARCH
IN_ROOT_NODE
IN_SEARCH
AT_SOLUTION
NO_MORE_SOLUTIONS
PROBLEM_INFEASIBLE
Meaning
Before search, after search.
Executing the root node.
Executing the search code.
After successful NextSolution() and before EndSearch().
After failed NextSolution() and before EndSearch().
After search, the model is infeasible.
NewSearch()
This is how the NewSearch() method might have looked in a simplied version of the main
search algorithm. The Search class is used internally to monitor the search. Because the CP
solver allows nested searches, we take a pointer to the current search object each time we call
the NewSearch(), NextSolution() and EndSearch() methods.
1
2
3
4
5
6
86
// Init:
// Install
// Install
// Install
// Install
// Install
...
8
9
10
11
12
13
14
15
search->EnterSearch();
16
// SEARCHMONITOR CALLBACK
17
18
19
20
state_ = IN_ROOT_NODE;
search->BeginInitialPropagation();
21
22
// SEARCHMONITOR CALLBACK
23
try {
// Initial constraint propagation
ProcessConstraints();
search->EndInitialPropagation(); // SEARCHMONITOR CALLBACK
...
state_ = IN_SEARCH;
} catch (const FailException& e) {
...
state_ = PROBLEM_INFEASIBLE;
}
24
25
26
27
28
29
30
31
32
33
34
return;
35
36
The initialization part consists in installing the backtracking and propagation mechanisms, the
monitors and the print trace if needed. If everything goes smoothly, the solver is in state
IN_SEARCH.
NextSolution()
The NextSolution() method returns true if if nds the next solution, false otherwise.
Notice that the statistics are not reset whatsoever from one call of NextSolution() to the
next one.
We present and discuss this algorithm below. SearchMonitors callbacks are indicated by
the comment:
// SEARCHMONITOR CALLBACK
Here is how it might have looked in a simplied version of the main search algorithm:
1
2
3
bool Solver::NextSolution() {
Search* const search = searches_.back();
Decision* fd = NULL;// failed decision
4
5
6
87
case PROBLEM_INFEASIBLE:
return false;
case NO_MORE_SOLUTIONS:
return false;
case AT_SOLUTION: {// We need to backtrack
// SEARCHMONITOR CALLBACK
// BacktrackOneLevel() calls search->EndFail()
if (BacktrackOneLevel(&fd)) {// No more solutions.
search->NoMoreSolutions();// SEARCHMONITOR CALLBACKS
state_ = NO_MORE_SOLUTIONS;
return false;
}
state_ = IN_SEARCH;
break;
}
case OUTSIDE_SEARCH: {
state_ = IN_ROOT_NODE;
search->BeginInitialPropagation();// SEARCHMONITOR CALLBACKS
try {
ProcessConstraints();
search->EndInitialPropagation();// SEARCHMONITOR CALLBACKS
...
state_ = IN_SEARCH;
} catch(const FailException& e) {
...
state_ = PROBLEM_INFEASIBLE;
return false;
}
break;
}
case IN_SEARCH:
break;
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
88
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
return result;
114
115
89
Lets dissect the algorithm. First of all, you might wonder where does the propagation take
place? In a few words: Constraints are responsible of attaching Demons to variables.
These Demons are on their turn responsible for implementing the actual propagation. Whenever the domain of a variable changes, the corresponding Demons are triggered. In the main
search algorithm, this happens twice: when we Apply() a Decision (line 75) and when we
Refute() a Decision (line 53).
Back to the algorithm. On line 2, the solver grabs the last search. Indeed, several searches can
be nested and queued.
The Search object is responsible of monitoring the search for one DecisionBuilder (one
phase) and triggers the callbacks of the installed SearchMonitors at the right moments.
Following the solvers state, some action is needed (see lines 6-39). The case AT_SOLUTION
is worth an explanation. NextSolution() was called and the solver found a feasible
solution. The solver thus needs to backtrack (method BacktrackOneLevel() on line
14). If a right branch exists, it is stored in the Decision pointer fd (failed decision) and
BacktrackOneLevel() returns false. If there are no more right branches to visit,
the search tree has been exhausted and the method returns true. Next, the corresponding
DecisionBuilder to the current search is kept on line 41.
We are now inside the main loop of the NextSolution() method. Two Boolean variables
are dened12
finish: becomes true when the search is over;
result: denotes if a feasible solution was indeed found or not.
These two variables are declared volatile to allow their use between setjmp and
longjmp, otherwise the compiler might optimize certain portions of code away. Basically,
it tells the compiler that these variables can be changed from the outside.
This main loop starts at line 47 and ends at line 108.
The try - catch mechanism allows to easily explain the backtrack mechanism. Whenever
we need to backtrack in the search, a FailException is thrown13 .
If the Decision pointer fd is not NULL, this means that we have backtracked to the rst
available (non visited) right branch in the search tree. This corresponds to refuting the decision
(lines 50-57).
The solver now tries to explore as much as possible left branches and this is done in the while
loop (line 62-81).
The DecisionBuilder produces its next Decision on line 64. If it detects that this
branch is a dead-end, it is allowed to return a FailDecision which the solver tests at line
67.
If the search tree is empty, the DecisionBuilder returns NULL. The solver tests this possibility on line 73. If the DecisionBuilder found a next Decision, it is applied on line
75.
12
These two variables play a role when we use nested searches, restart or nish a search but these possibilities
are not shown here.
13
Did we already mention that the try - catch mechanism is not used in the production code? ;-)
90
Whenever the solver cannot nd a next left branch to explore, it exits the while(true) loop.
We are now ready to test if we have found a feasible solution at the leaf of a left branch. This
test is done one line 85. The method AcceptSolution() decides if the solution is feasible
or not. After nding a feasible solution, the method AtSolution() decides if we continue
or stop the search.
You might recognize these two methods as callbacks of a SearchMonitor. These
two methods call the corresponding methods of all installed SearchMonitors no
matter what they return, i.e.
you are guaranteed that all SearchMonitors will
be called.
If one SearchMonitor has its method AcceptSolution() returning false, search->AcceptSolution() returns false.
On the contrary, if only one SearchMonitor has its AtSolution() method returning true,
search->AtSolution() returns true.
The test on line 87 is a little bit complex:
test = !search->AtSolution() || !CurrentlyInSolve()
Remember that AtSolution() returns true if we want to resume the search (i.e.
if at least one SearchMonitor->AtSolution() returns true), false otherwise.
CurrentlyInSolve() returns true if the solve process was called with the Solve()
method and false if it was called with the NextSolution() method.
Thus, test is true (and we stop the search in NextSolution()) if all
SearchMonitors decided to stop the search (search->AtSolution() returns then
false) or if at least one SearchMonitor decided to continue but the solve process was
called by NextSolution(). Indeed, a user expects NextSolution() to stop whenever
it encounters a feasible solution.
Whenever a backtrack is necessary, a FailException is caught and the solver backtracks
to the next available right branch if possible.
Finally, the current state of the solver is set and the method NextSolution() returns if a solution has been found and accepted by all SearchMonitors or there is no solution anymore.
It then returns true if the test above is true, false otherwise.
A solution is dened as a leaf of the search tree with respect to the given
DecisionBuilder for which there is no failure. What this means is that, contrary to intuition, a solution may not have all variables of the model bound. It is
the responsibility of the DecisionBuilder to keep returning decisions until all
variables are indeed bound. The most extreme counterexample is calling Solve()
with a trivial DecisionBuilder whose Next() method always returns NULL.
In this case, Solve() immediately returns true, since not assigning any variable
to any value is a solution, unless the root node propagation discovers that the model
is infeasible.
EndSearch()
The EndSearch() method cleans the solver and if required, writes the prole of the search
in a le. It also calls the ExitSearch() callbacks of all installed SearchMonitors.
91
Here is how it might have looked in a simplied version of the main search algorithm.
1
2
3
4
5
6
7
8
9
10
11
void Solver::EndSearch() {
Search* const search = searches_.back();
...
search->ExitSearch();// SEARCHMONITOR CALLBACK
search->Clear();
state_ = OUTSIDE_SEARCH;
if (!FLAGS_cp_profile_file.empty()) {
LOG(INFO) << "Exporting profile to " << FLAGS_cp_profile_file;
ExportProfilingOverview(FLAGS_cp_profile_file);
}
}
The important trick to understand is that the visualization is only available after the search is
done.
Please nd all necessary information and tools at:
http://sourceforge.net/projects/cpviz/
92
xsi:noNamespaceSchemaLocation="configuration.xsd" xmlns:xsi="http://
www.w3.org/2001/XMLSchema-instance">
<tool show="tree" type="layout" display="expanded" repeat="all"
width="700" height="700" fileroot="tree"/>
<tool show="viz" type="layout" display="expanded" repeat="all"
width="700" height="700" fileroot="viz"/>
</configuration>
Basically, it tells cpviz to produce the graphic les for the search tree (show="tree") and
the variables (show="viz") in the directory /tmp.
If you are really lazy, we even provide a factory method which generates automatically a default
conguration le:
SearchMonitor* const cpviz = s.MakeTreeMonitor(vars,
"configuration.xml",
"tree.xml",
"visualization.xml");
After your search is nished AND you have called (implicitley or explicitly) EndSearch()14 ,
you can run cpviz to digest the XML les representing your search by entering the viz/bin
directory and typing:
java ie.ucc.cccc.viz.Viz configuration.xml tree.xml visualization.xml
on a command line into a terminal near you. This will produce the following picture of the
search tree:
cpviz produces the construction of the search tree, step by step. In our case we try to solve the
n-queens problem with = 4 and cpviz generates 8 les.
This is probably not what you expected. First of all, this is not a binary tree and there seems to
be an extra dummy root node. A binary tree which is what is exactly constructed during the
search is not really suited for a graphical representation as it can quickly become very big
(compare the tree above with the actual search tree that is represented below). To avoid huge
14
93
trees, we have reduced their sizes by contracting several nodes. Except for the dummy root
node, each node corresponds to a variable during the search and only left branches are given
explicitly. The numbers along the branches denote the applied decisions (like [1] = 2) and the
numbers in the right corner above the variable names of the nodes are the number of values left
in the domain of the corresponding variable just before the decision was taken. Nodes coloured
in
green denote feasible solutions;
red denote sub-trees without any feasible solutions;
blue denote intermediate try nodes (these only exist during the search).
94
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x0 = 1
x1 = 2
node 6
node 2
node 3
node 5
x0 = 2
x0 = 2
node 8
node 7
x1 = 0
x1 = 0
node 10
node 9
<<
<<
<<
<<
<<
Lets see if we can deduce these statistics from the search tree. The three rst statistics are easy
to spot in the tree:
Number of solutions (2): There are indeed two distinct solutions denoted by the
two green leafs.
Failures (6): A failure occurs whenever the solver has to backtrack, whether it is
because of a real failure (nodes 2 3 and 9 10) or a success (nodes 5 and
7). Indeed, when the solver nds a solution, it has to backtrack to nd other
solutions. The method failures() returns the number of leaves of the
95
Step 0:
We start with a dummy node. This node is needed in our construction. Youll see in a moment
why.
Figure 5.6: Contruction of the real search tree from the cpviz tree: step 0
15
96
Actually, the very last backtrack happens when the solver is deleted.
Step 1:
node 0
(a) cpviz
Figure 5.7: Construction of the real search tree from the cpviz tree: step 1
Next, we start with the actual root node.
As you can see in our
cpviz output,
the dummy root node doesnt even have a name and
the little number 0 next to this non existing name doesnt mean
anything.
Step 2:
node 0
x0 = 0
node 1
(a) cpviz
Figure 5.8: Construction of the real search tree from the cpviz tree: step 2
You can see in our cpviz output that the solver has applied the Decision 0 = 0 but that
it couldnt realize if this was a good choice or not. The little number 4 next to the variable
name 0 means that before the decision was applied, the number of values in its domain was 4.
Indeed: 0 {0, 1, 2, 3} before being assigned the value 0.
97
Step 3:
node 0
x0 = 0
node 1
x1 = 2
node 2
(a) cpviz
Figure 5.9: Construction of the real search tree from the cpviz tree: step 3
After having applied the Decision 0 = 0 at step 2, the solver now applies the Decision
1 = 2 which leads, after propagation, to a failure.
Step 4:
node 0
x0 = 0
x0 = 0
node 1
x1 = 2
node 2
(a) cpviz
node 4
x1 = 2
node 3
Figure 5.10: Construction of the real search tree from the cpviz tree: step 4
Our cpviz output now clearly warns that taking 0 = 0 does not lead to a feasible solution.
This can only mean that the solver tried also to refute the Decision 1 = 2. So we know that
the branch 1 = 2 after the branch 0 = 0 is leading nowhere. We have to backtrack and to
refute the Decision 0 = 0. We have thus a new branch 0 = 0 in the real search tree.
98
Step 5:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x1 = 2
node 2
(a) cpviz
node 3
node 5
Figure 5.11: Construction of the real search tree from the cpviz tree: step 5
We nd a feasible solution when 0 = 1. Thus we add the branch 0 = 1 and indicate success.
Step 6:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x1 = 2
x0 = 1
node 6
node 2
node 3
node 5
x0 = 2
node 7
(a) cpviz
Figure 5.12: Construction of the real search tree from the cpviz tree: step 6
We nd a second feasible solution when 0 = 2. Before we can proceed by applying
Decision 0 = 2, we rst have to refute the Decision 0 = 1
99
Step 7:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x0 = 1
x1 = 2
node 6
node 2
node 3
node 5
x0 = 2
x0 = 2
node 8
node 7
(a) cpviz
Figure 5.13: Construction of the real search tree from the cpviz tree: step 7
We add a tentative branch in the cpviz output. The branch before we applied the Decision
2 = 0 that lead to a feasible solution, so now we know that the solver is trying to refute that
decision: 2 = 0.
Step 8:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x0 = 1
x1 = 2
node 6
node 2
node 3
node 5
x0 = 2
x0 = 2
node 8
node 7
x1 = 0
x1 = 0
node 10
node 9
(a) cpviz
Figure 5.14: Construction of the real search tree from the cpviz tree: step 8
The nal step is the branch 1 = 0 that leads to a failure. This means that when we apply and
refute 1 = 0, we get a failure. Thus we know that 0 = 1 and 0 = 1 both fail.
100
Propagation
To better understand the search, lets have a look at the propagation in details. First, we look at
the real propagation, then at our cpviz output.
You can nd an animated version of the propagation here.
We start at the root node with
node 0: 0 {0, 1, 2, 3}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. We apply the
Decision 0 = 0 which corresponds to our search strategy.
0 {0}, 1 {2, 3}, 2 {1, 3}, 3 {1, 2}. No more propagation is possible. We
then apply the Decision 1 = 2
node 2: 0 {0}, 1 {2}, 2 {1, 3}, 3 {1, 2}. The propagation is as follow:
AllDifferent(0 , 1 1, 2 2, 3 3) :
2 : 3
101
node 3: 0 {0}, 1 {3}, 2 {1, 3}, 3 {1, 2}. 1 is xed to 3 because we removed
the value 2 of its domain (refuting the Decision 1 = 2).
Propagation:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
3 : 1
102
apply
103
104
apply
the
105
106
node 8: 0 {3}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. 0 is xed because
there is only one value left in its domain.
Propagation:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
1 : 2, 2 : 1, 3 : 0
AllDifferent(0 , 1 , 2 , 3 ) :
1 : 3, 2 : 3, 3 : 3
0 {3}, 1 {0, 1}, 2 {0, 2}, 3 {1, 2}. No more propagation. We thus apply
our search strategy and apply Decision 1 = 0.
AllDifferent(0 , 1 1, 2 2, 3 3) :
3 : 2
107
0 {3}, 1 {0}, 2 , 3 {1, 2}. The empty domain for 2 indicates a failure
and we have to backtrack... to the root node as we have exhausted the search tree. The
search is thus nished and we have found 2 distinct solutions.
Our cpviz output of the propagation
For each step in the construction of the tree in our cpviz output corresponds a visualization
of the propagation and the states of the variables. Of course, as we try to limit the number of
108
nodes in the tree, we are constrained to display very little information about the propagation
process. In short, if we nd
a try node, we display the nal propagation at this node;
a solution, we display the solution;
a failure, we display the rst failure encountered and the values of the assigned variables.
We also display what variable we focus on next.
Lets go
umn our
tree and
Step 0:
(a) cpviz
109
Step 1:
node 0
Step 2:
node 0
x0 = 0
node 1
110
Step 3:
node 0
x0 = 0
node 1
x1 = 2
node 2
Step 4:
node 0
x0 = 0
x0 = 0
node 1
x1 = 2
node 2
node 4
x1 = 2
node 3
111
Step 5:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x1 = 2
node 2
node 3
node 5
Step 6:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x1 = 2
x0 = 1
node 6
node 2
node 3
node 5
x0 = 2
node 7
112
Step 7:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x1 = 2
x0 = 1
node 6
node 2
node 3
node 5
x0 = 2
x0 = 2
node 8
node 7
113
Step 8:
node 0
x0 = 0
x0 = 0
node 1
node 4
x0 = 1
x1 = 2
x0 = 1
x1 = 2
node 6
node 2
node 3
node 5
x0 = 2
x0 = 2
node 8
node 7
x1 = 0
x1 = 0
node 10
node 9
114
One could argue that these two scenarios are not really mutually exclusive. Indeed, we divide the scenarios in two cases depending on whether the DecisionBuilder returns a Decision or not. Some
DecisionBuilders delegate the creation process of Decisions to other DecisionBuilders.
115
ApplyBranchSelector: changes the way the branches are selected. For instance, the left branch can become the right branch and vice-versa. Have a look at
the Solver::DecisionModification enum for more.
LocalSearch: applies local search operators to nd a solution.
SolveOnce: stops the search as soon as it nds a solution with the help of another
DecisionBuilder.
NestedOptimize: optimizes the search sub-tree with the help of another
DecisionBuilder.
...
For your (and our) convenience, three more methods can be implemented:
virtual void AppendMonitors(Solver* const solver,
std::vector<SearchMonitor*>* const extras): to add some extra
SearchMonitors at the beginning of the search. Please note there are no checks at
this point for duplication.
virtual string DebugString() const:
method to give a name to your object.
the
116
These two pure virtual methods must be implemented in every Decision class.
A Decision object is returned by a DecisionBuilder through its Next() method.
Two more methods can be implemented:
virtual string DebugString() const:
method.
var_ and value_ are local private copies of the variable and the value.
The Apply() and Refute() methods are straithforward:
void Apply(Solver* const s) {
var_->SetValue(value_);
}
void Refute(Solver* const s) {
var_->RemoveValue(value_);
}
DecisionVisitors
DecisionVisitors are attached to Decisions. The corresponding methods of the
DecisionVisitor are triggered just before a Decision is applied19 .
19
In this case, the methods are triggered when Decision objects are created and these objects are created just
before their Apply() method is called. See the subsection Visitors for more.
117
method.
when a variable domain will be splitted in two by a given value, implement the
virtual void VisitSplitVariableDomain(IntVar* const var,
int64 value,
bool start_with_lower_half);
otherwise it is
var >
There is also a default option:
virtual void VisitUnknownDecision();
At each leaf of the search tree corresponding to the DecisionBuilder db1, the second
DecisionBuilder db2 is called.
The DecisionBuilder db search tree will be as follows:
118
db1
search tree
db2
db2
db2
db2
search tree
search tree
search tree
search tree
db search tree
db = s.Compose(db1, db2);
This composition of DecisionBuilders frequently happens in scheduling. For instance, in
the section The DecisionBuilders where we try to solve a Job-Shop Problem, the solving process is done in two consecutive phases: rst we rank the tasks for each machine, then we schedule each task at its earliest start time. To do so, we Compose() two DecisionBuilders.
You can Compose() more than two DecisionBuilders. There are two more specic
methods to Compose() three and even four DecisionBuilders. And if that is not enough,
use
DecisionBuilder* Compose(const std::vector<DecisionBuilder*>& dbs);
The DecisionBuilder db1 and the DecisionBuilder db2 are each called from the
top of the search tree one after the other.
The DecisionBuilder db search tree will be as follows:
119
db1
db2
search tree
search tree
db search tree
db = s.Try(db1, db2);
This combination is handy to try a DecisionBuilder db1 which partially explores the
search space. If it fails, you can use the DecisionBuilder db2 as a backup.
As with Compose(), you can Try() up to four DecisionBuilders and use
DecisionBuilder* Try(const std::vector<DecisionBuilder*>& dbs);
for more.
Beware that Try(db1, db2, db3, db4) will give an unbalanced tree to the
right, whereas Try(Try(db1, db2), Try(db3, db4)) will give a balanced tree.
120
NestedOptimize
NestedOptimize is similar to SolveOnce except that it seeks for an optimal solution
instead of just a feasible solution. If there are no solutions in this nested tree, it fails.
The factory method is MakeNestedOptimize(). Again, you can use none or up to four
SearchMonitors and use the version with an std::vector<SearchMonitor*>:
DecisionBuilder* MakeNestedOptimize(DecisionBuilder* const db,
Assignment* const solution,
bool maximize,
int64 step,
const std::vector<SearchMonitor*>& monitors);
121
You use a predened IntVarStrategy strategy to nd the next variable to branch on, provide your own callback IndexEvaluator2 to nd the next value to give to this variable and
an evaluator IndexEvaluator1 to break any tie between different values.
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator1* var_evaluator,
IntValueStrategy val_str);
This time, you provide an evaluator IndexEvaluator1 to nd the next variable but rely on
a predened IntValueStrategy strategy to nd the next value.
Several other combinations are provided.
20
122
If you want to know more about callbacks, see the section Callbacks in the chapter Under the hood.
and
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator2* evaluator,
IndexEvaluator1* tie_breaker,
EvaluatorStrategy str);
You might wonder what the EvaluatorStrategy strategy is. The selection is done by
scanning every pair <variable, possible value>. The next selected pair is the best among all
possibilities, i.e. the pair with the smallest evaluation given by the IndexEvaluator2. This
approach is costly and therefore we offer two options given by the EvaluatorStrategy
enum:
CHOOSE_STATIC_GLOBAL_BEST: Static evaluation: Pairs are compared at the rst
call of the selector, and results are cached. Next calls to the selector use the previous
computation, and are thus not up-to-date, e.g. some <variable, value> pairs may not be
possible due to propagation since the rst call.
CHOOSE_DYNAMIC_GLOBAL_BEST: Dynamic evaluation: Pairs are compared each
time a variable is selected. That way all pairs are relevant and evaluation is accurate. This
strategy runs in (number-of-pairs) at each variable selection, versus (1) in the static
version.
123
Most of the strategies are self-explanatory except maybe for CHOOSE_PATH. This selection
strategy is most convenient when you try to nd simple paths (paths with no repeated vertices)
in a solution and the variables correspond to nodes on the paths. When a variable i is bound
(has been assigned a value), the path connects variable i to the next variable vars[i] as on
the gure below:
3
5
1
We have
vars = [, 0, 3, 1, , ]
where corresponds to a variable that wasnt assigned a value. We have vars[2] = 3,
vars[3] = 1 and vars[1] = 0. The next variable to be choosen will be 0 and in this case
vars[0] {2, 4, 5}. What happens if vars[0] is assigned the value 2? This strategy will pick up
another unbounded variable.
In general, the selection CHOOSE_PATH will happen as follow:
1. Try to extend an existing path: look for an unbound variable, to which some other variable points.
2. If no such path is found, try to nd a start node of a path: look for an unbound variable,
to which no other variable can point.
3. If everything else fails, pick the rst unbound variable.
We will encounter paths again in third part of this manual, when well discuss routing.
125
ASSIGN_CENTER_VALUE Selects the rst available value that is the closest to the center of
the domain of the selected variable. The center is dened as (min + max) / 2.
SPLIT_LOWER_HALF Splits the domain in two around the center, and forces the variable to
take its value in the lower half rst.
SPLIT_UPPER_HALF Splits the domain in two around the center, and forces the variable to
take its value in the upper half rst.
5.6.3 Results
You can nd the code in the les tutorials/cplusplus/chap5/phases1.cc
and tutorials/cplusplus/chap5/solver_benchmark.h.
Just for fun, we have developed a SolverBenchmark class to test different search
strategies. Statistics are recorded thanks to SolverBenchmarkStats. You can nd both
classes in the solver_benchmark.h header.
In phases1.cc, we test different combinations of the above strategies to nd the variables
and the values to branch on. You can try it for yourself and see that basically no predened
strategy outperforms any other.
The most fun (and most efcient) way to use or-tools is to dene your own selection strategies
and search primitives. This is the subject of the next section.
mirror Golomb rulers in section 3.8.1 page 65. This time, we will use SymmetryBreakers.
As their name implies, their role is to break symmetries. In contrast to explicitly adding symmetry breaking constraints in the model before the solving process, SymmetryBreakers add
them automatically when required during the search, i.e. on the y.
(a) Solution 1
(b) Solution 2
5.8.2 SymmetryBreakers
You can nd the code in the le tutorials/cplusplus/chap5/nqueens7.cc.
Lets create a SymmetryBreaker for the vertical axial symmetry. Because the square has
lots of symmetries, we introduce a helper method to nd the symmetric indices of the variables
and the symmetric values for a given variable:
int symmetric(int index) const { return size_ - 1 - index}
where size_ denotes the number of variables and the range of possible values ([0, size_ 1])
in our model. Figure 5.25 illustrates the returned indices by the symmetric() method.
127
128
We also use two methods to do the translation between the indices and the variables. Given an
IntVar * var, Index(var) returns the index of the variable corresponding to var:
int Index(IntVar* const var) const {
return FindWithDefault(indices_, var, -1);
}
where vars_ is the private std::vector<IntVar*> with the variables of our model.
We create a base SymmetryBreaker for the n-queens problem:
class NQueenSymmetry : public SymmetryBreaker {
public:
NQueenSymmetry(Solver* const s, const std::vector<IntVar*>& vars)
: solver_(s), vars_(vars), size_(vars.size()) {
for (int i = 0; i < size_; ++i) {
indices_[vars[i]] = i;
}
}
virtual ~NQueenSymmetry() {}
protected:
int Index(IntVar* const var) const {
return FindWithDefault(indices_, var, -1);
}
IntVar* Var(int index) const {
return vars_[index];
}
int size() const { return size_; }
int symmetric(int index) const { return size_ - 1 - index; }
Solver* const solver() const { return solver_; }
private:
Solver* const solver_;
const std::vector<IntVar*> vars_;
std::map<IntVar*, int> indices_;
const int size_;
};
the corresponding symmetric assignation. We call this corresponding assignment a clause. This
clause only makes sense if the Decision assigns a value to an IntVar and this is why we
declare the corresponding clause only in the VisitSetVariableValue() method of the
SymmetryBreaker. All this might sound complicated but it is not:
// Vertical axis symmetry
class SY : public NQueenSymmetry {
public:
SY(Solver* const s, const std::vector<IntVar*>& vars) :
NQueenSymmetry(s, vars) {}
virtual ~SY() {}
virtual void VisitSetVariableValue(IntVar* const var, int64 value) {
const int index = Index(var);
IntVar* const other_var = Var(symmetric(index));
AddIntegerVariableEqualValueClause(other_var, value);
}
};
Given an IntVar* var that will be given the value value by a Decision during the
search, we ask the SymmetryManager to avoid the possibility that the variable other_var
could be assigned the same value value upon refutation of this Decision. This means that
the other_var variable will never be equal to value in the opposite branch of the search
tree where var is different than value. In this manner, we avoid searching a symmetrical
part of the search tree we have already explored.
What happens if another type of Decisions are returned by the DecisionBuilder during
the search? Nothing. The refutation of the clause will only be applied if a Decision triggers
a VisitSetVariableValue() callback.
The SymmetryBreaker class denes two other clauses:
AddIntegerVariableGreaterOrEqualValueClause(IntVar* const
var, int64 value) and
AddIntegerVariableLessOrEqualValueClause(IntVar* const
var, int64 value).
Their names are quite explicit and tell you what their purpose is. These methods would t
perfectly within a VisitSplitVariableDomain() call for instance.
5.8.3 RevAlloc
Whenever you dene your own subclass of BaseObject (and a SymmetryBreaker is a
BaseObject), it is good practice to register the given object as being reversible to the solver.
That is, the solver will take ownership of the object and delete it when it backtracks out of the
current state. To register an object as reversible, you invoke the RevAlloc() method of the
solver:
Solver s("nqueens");
...
NQueenSymmetry* const sy = s.RevAlloc(new SY(&s, queens));
130
RevAlloc() returns a pointer to the newly created and registered object. You can thus invoke
this method with arguments in the constructor of the constructed object without having to keep
a pointer to this object.
The solver will now take care of your object. If you have an array of objects that are subclasses
of BaseObject, IntVar, IntExpr and Constraint, you can register your array with
RevAllocArray(). This method is also valid for arrays of ints, int64, uint64 and
bool. The array must have been allocated with the new[] operator.
If you take a look at the source code, you will see that the factories methods call RevAlloc()
to pass ownership of their objects to the solver.
objects
in
an
std::vector<SymmetryBreaker*> breakers;
NQueenSymmetry* const sy = s.RevAlloc(new SY(&s, queens));
breakers.push_back(sy);
NQueenSymmetry* const sx = s.RevAlloc(new SX(&s, queens));
breakers.push_back(sx);
NQueenSymmetry* const sd1 = s.RevAlloc(new SD1(&s, queens));
breakers.push_back(sd1);
NQueenSymmetry* const sd2 = s.RevAlloc(new SD2(&s, queens));
breakers.push_back(sd2);
NQueenSymmetry* const r90 = s.RevAlloc(new R90(&s, queens));
breakers.push_back(r90);
NQueenSymmetry* const r180 = s.RevAlloc(new R180(&s, queens));
breakers.push_back(r180);
NQueenSymmetry* const r270 = s.RevAlloc(new R270(&s, queens));
breakers.push_back(r270);
131
5.9. Summary
std::vector<SearchMonitor*> monitors;
...
monitors.push_back(symmetry_manager);
...
DecisionBuilder* const db = s.MakePhase(...);
...
s.Solve(db, monitors);
These seven SymmetryBreakers are enough to avoid duplicate solutions in the search, i.e.
they force the solver to nd only unique solutions up to a symmetry.
5.8.5 Results
Lets compare the time and the search trees again.
[TO BE DONE]
5.9 Summary
132
CHAPTER
SIX
We enter here in a new world where we dont try to solve a problem to optimality but seek a
good solution. Remember from sub-section 1.3.4 that some problems1 are hard to solve. No
matter how powerful our computers are2 , we quickly hit a wall if we try to solve these problems
to optimality. Do we give up? Of course not! If it is not possible to compute the best solutions,
we can try to nd very good solutions. Enter the fascinating world of (meta-)heuristics and
local search.
Throughout this chapter, we will use the job-shop problem as an illustrative example.
The job-shop problem is a typical difcult scheduling problem. Dont worry if you dont
know anything about scheduling or the job-shop problem, we explain this problem in details.
Scheduling is one of the elds where constraint programming has been applied with great
success. It is thus not surprising that the CP community has developed specic tools to solve
scheduling problems. In this chapter, we introduce the ones that have been implemented in
or-tools
Overview:
We start by describing the job-problem, the disjunctive model to represent it, two formats to encode job-shop problem instances (JSSP and Taillard) and our rst exact results. We next make a
short stop to describe the specic primitives implemented in or-tools to solve scheduling problems. For instance, instead of using IntVar variables, we use the dedicated IntervalVars
and SequenceVars.
After these preliminaries, we present local search and how it is implemented in the or-tools
library. Beside the job-shop problem, we use a dummy problem to watch the inner mechanisms
of local search in or-tools in action:
We minimize 0 + 1 + . . . + 1 where each variable has the same domain
[0, 1]. To complicate things a little bit, we add the constraint 0 1.
Once we understand how to use local search in or-tools, we use basic
LocalSearchOperators to solve the job-shop problem and compare the exact and approx1
on
molecular
quantum
computers
mechanics
imate results. Finally, to speed up the local search algorithm, we use LocalSearchFilters
for the dummy problem.
Prerequisites:
134
The les of this chapter are NOT the same as the ones in the example directory even if they
were inspired by them. In particular, job-shop instances with only one task per job are accepted
(not that this is extremely useful but...).
Content:
135
6.1. The job-shop problem, the disjunctive model and benchmark data
the literature and benchmark data are concerned by problems where each job is made of
tasks and each task in a job must be processed on a different machine, i.e. each job needs to be
processed exactly once on each machine.
We seek a schedule (solution) that minimizes the makespan (duration) of the whole process.
The makespan is the duration between the start of the rst task (across all machines) and the
completion of the last task (again across all machines). The classical notation for the makespan
is max .
The makespan can be dened as
max = max{ + }
or equivalently as the maximum time needed among all jobs to be completely processed. Recall
that denotes the number of tasks for job and that we count starting from 0. 1, denotes
thus the starting time of the last task of job and we have
max = max { 1, + 1, }
1,
Lets try to nd a schedule for our example. Suppose you want to favour job 1 because not only
did you see that it is the longest job to process but its last task takes 4 units of time. Here is the
Gantt chart of a possible schedule:
11111
00000
1111111
0000000
11111
00000
1111111
0000000
machine 0 00000
11111 000000000
1111111
0000000
111111111 00000
machine 1
111111
000000
111 11111 11111
000 00000 00000
111111111 00000
000000000 11111
111 11111 11111
000 00000 00000
11111
111111
000000
machine 2
111
000
11111
00000
2
11111
00000
4
10
12
1
0
1
0
1
0
1
0
1
0
1
0
1
0
1
0
job 0
job 1
job 2
This is a feasible schedule since tasks within every job are processed one after the other in the
right sequence and each task is processed on the right machine. The makespan is 12 units of
time. Can we do better? Focusing on one job is probably not the best strategy. Here is an
optimal solution:
11111111111
00000000000
11111111111
00000000000
machine 0 0000000
11111110000 000000000
1111 111111111
11111
00000 11111
111111 11 00000
000000 0000000000
machine 1
11111
00000
11111
00000
111111111
0
11111
00000
111111 11 11111
000000 00 00000
machine 2
11
00
11111
00000
2
11111
00000
8
10
1
0
1
0
1
0
1
0
1
0
1
0
12
1
0
1
0
job 0
job 1
job 2
136
e1
(0,2)
(1,2)
ma
chi
n
machine 0
(0,3)
(1,4)
(2,1)
(2,2)
m
e2
hin
ac
job 0
(1,4)
(2,3)
job 1
job 2
137
6.1. The job-shop problem, the disjunctive model and benchmark data
To determine a schedule we have to dene an ordering of all tasks processed on each machine.
This can be done by orienting all dotted or dashed edges such that each clique corresponding
to a machine becomes acyclic8 .
Our rst schedule is represented in the next gure.
(0,3)
(1,2)
(0,2)
(2,2)
(2,1)
(1,4)
job 0
(1,4)
job 1
job 2
(2,3)
We also want to avoid cycles between disjunctive and conjunctive arcs because they lead to
infeasible schedules. A feasible schedule is represented by a directed acyclic disjunctive graph.
In fact, the opposite is also true. A complete orientation of the edges in denes a feasible
schedule if and only if the resulting directed disjunctive graph is acyclic.
The makespan is given by the longest weighted path from to . This path - thickened in the
next gure - is called the critical path.
(0,3)
(1,2)
(0,2)
(2,2)
(2,1)
(1,4)
job 0
(1,4)
(2,3)
job 1
job 2
An acyclic graph is a graph without cycle. It can be shown that a complete directed acyclic graph induces a
total order on its vertices, i.e. a complete directed acyclic graph lets you order all its vertices unequivocally.
138
What are the constraints? In the disjunctive graph, we have two kind of edges to model a
feasible schedule:
conjunctive arcs modelling the order in which each task of a job has to be processed:
(, ) such that = and = :
+
or +
These constraints are called disjunctive constraints. They forbid cycles in a clique corresponding to a machine9 .
What is the objective function? The objective function (the makespan) max doesnt correspond to a variable of the model. We have to construct its value. Because we minimize the
makespan, we can use a little trick. Let be the set of all end tasks of all jobs. In our example,
= {20 (2, 2), 21 (1, 4), 12 (2, 3)}. The makespan must be greater than the overall time it
takes to process these tasks:
:
max
+ .
max
max
+
+
+
or +
0
(, )
(, ) : =
{, }
We will implement and solve this model in the next section but rst we need to read and process
the data representing instances of job-shop problems.
9
t2
t3
We have 1 +1
2 , 2 +2
3 and 3 +3
1 . Add these three inequalities and you obtain 1 +2 +3 0.
This is impossible if one of the is greater than 0 as every 0.
10
It is not obvious that this model produces optimal solutions that are feasible schedules but it can be shown
that it does.
139
6.1. The job-shop problem, the disjunctive model and benchmark data
(Table 1, instance 9)
11 17 10 18 12 11
12 20 10 19 13 12
13 37 7 38 3 40
...
...
...
This instance has 20 jobs to process on 15 machines. Each job is composed of exactly 15 tasks.
Each job corresponds to a line:
6 14
5 21
8 13
4 11
1 11 14 35 13 20 11 17 10 18 12 11
...
Each pair ( , ) corresponds to a task. For this rst job, the rst task needs 14 units of time
on machine 6, the second task needs 21 units of time on machine 5 and so on.
As is often the case, there is a one to one correspondence between the tasks and the machines.
Taillards format
Lets consider the beginning of le 20_5_01_ta001.txt:
20
5
873654221
0
468
54 79 16 66 58
1
325
11
We
copied
the
les
abz9
and
20_5_01_ta001.txt
manual/tutorials/cplusplus/chap6 for your convenience.
140
in
the
directory
83 3 89 58 56
2
923
15 11 49 31 20
3
513
71 99 15 68 85
...
This format is made for ow-shop problems and not job-shop problems. The two rst lines
indicate that this instance has 20 jobs to be processed on 5 machines. The next line (873654221)
is a random seed number. The jobs are numbered from 0 to 19. The data for the rst job are:
0
468
54 79 16 66 58
0 is the id or index of the rst job. The next number is not important for the job-shop problem.
The numbers in the last line correspond to processing times. We use the trick to assign these
times to machines 0, 1, 2 and so on. So job 0 is actually
[(0, 54), (1, 79), (2, 16), (3, 66), (4, 58)]
Because of this trick, one can not easily dene our problem instance above in this format and
we dont attempt to do it.
You can nd anything you ever wanted to know and more about this format in [Taillard1993].
JobshopData
The JobshopData class is a simple container for job-shop problem instances. It is dened
in the le jobshop.h. Basically, it wraps an std::vector<std::vector<Task> >
container where Task is a struct dened as follows:
struct Task {
Task(int j, int m, int d) : job_id(j), machine_id(m), duration(d) {}
int job_id;
int machine_id;
int duration;
};
Most part of the JobshopData class is devoted to the reading of both le formats.
The data le is processed at the creation of a JobShopData object:
explicit JobShopData(const string& filename) :
...
{
FileLineReader reader(filename_.c_str());
reader.set_line_callback(NewPermanentCallback(
this,
&JobShopData::ProcessNewLine));
reader.Reload();
if (!reader.loaded_successfully()) {
141
6.1. The job-shop problem, the disjunctive model and benchmark data
LOG(FATAL) << "Could not open job-shop file " << filename_;
}
To parse the data le and load the tasks for each job, we use a FileLineReader (declared
in base/filelinereader.h). In its Reload() method, it triggers the callback void
ProcessNewLine(char* const line) to read the le one line at a time
The public methods of the JobShopData class are
the getters:
machine_count(): number of machines;
job_count(): number of jobs;
name(): instance name;
horizon(): the sum of all durations (and a trivial upper bound on the makespan).
const std::vector<Task>& TasksOfJob(int job_id) const:
returns a reference to the corresponding std::vector<Task> of tasks.
two methods to report the content of the data le parsed:
void Report(std::ostream & out);
void ReportAll(std::ostream & out);
Just for fun, we have written the data le corresponding to our example above in JSSP format
in the le first_example_jssp.txt:
+++++++++++++++++++++++++++++
instance tutorial_first_jobshop_example
+++++++++++++++++++++++++++++
Simple instance of a job-shop problem in JSSP format
to illustrate the working of the or-tools library
3 3
0 3 1 2 2 2
0 2 2 1 1 4
1 4 2 3
142
and
the
data
in
the
Scheduling is one of the elds where Constraint Programming is heavily used and where
specialized constraints and variables have been developed12 . In this section, we will implement
the disjunctive model with dedicated variables (IntervalVar and SequenceVar) and
constraints (IntervalBinaryRelation and DisjunctiveConstraint).
Last but not least, we will see our rst real example of combining two DecisionBuilders
in a top-down fashion.
An IntervalVar represents one integer interval and is often used in scheduling. Its main
characteristics are its starting time, its duration and its ending time.
The CP solver has the factory method MakeFixedDurationIntervalVar() for xed
duration intervals:
const std::string name = StringPrintf("J%dM%dI%dD%d",
task.job_id,
task.machine_id,
task_index,
task.duration);
IntervalVar* const one_task =
solver.MakeFixedDurationIntervalVar(0,
horizon,
task.duration,
false,
name);
The rst two arguments of MakeFixedDurationIntervalVar() are a lower and an upper bound on the starting time of the IntervalVar. The fourth argument is a bool that
12
143
We will create the SequenceVar variables later when we will add the disjunctive constraints.
In the next section, we will examine other possibilities and also temporal relations between an
IntervalVar t and an integer d representing time.
144
To obtain the end time of an IntervalVar, use its EndExpr() method that returns an
IntExpr. You can also query the start time and duration:
StartExpr();
DurationExpr().
The factory method Solver::MakeSequenceVar(...) has been removed from the API.
145
Second, we dene the phase to schedule the ranked tasks. This is conveniently done by xing
the objective variable to its minimum value:
DecisionBuilder* const obj_phase = solver.MakePhase(objective_var,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);
Third, we combine both phases one after the other in the search tree with the Compose()
method:
DecisionBuilder* const main_phase =
solver.Compose(sequence_phase, obj_phase);
146
147
Job 1 (3,5)
Job 0 (4,6)
Job 0 (6,8)
Job 1 (6,10)
Job 2 (8,11)
Obj. val.
1015
986
Branches
131 733
6 242 194
Time (s)
26,756
1088,487
After a little bit more than 18 minutes (1088,487 seconds), the CP solver nds its 107 th solution
with an objective value of 986. This is quite far from the optimal value of... 679 [Adams1988].
An exact procedure to solve the job-shop problem is possible but only for small instances
and with specialized algorithms. We prefer to quickly nd (hopefully) good solutions (see
section 6.7). We will discover next what specialized tools are available in our library to handle
scheduling problems.
6.3.1 Variables
Two new types of variables are added to our arsenal: IntervalVars model tasks and
SequenceVars model sequences of tasks on one machine. Once you master these variables, you can use them in a variety of different contexts but for the moment keep in mind this
modelling association.
IntervalVars
An IntervalVar variable represents an integer interval variable. It is often used in scheduling to represent a task because it has:
a starting time: ;
a duration: and
148
StartMax()
EndMin()
StartRange
EndMax()
EndRange
The implementation optimizes different cases and thus doesnt necessarily correspond to the gure. Read on.
149
150
anymore (starting time greater than ending time, duration < 0...). You can get and set if an
IntervalVar must, may or cannot be performed with the following methods:
virtual bool MustBePerformed() const = 0;
virtual bool MayBePerformed() const = 0;
bool CannotBePerformed() const { return !MayBePerformed(); }
bool IsPerformedBound() {
return MustBePerformed() == MayBePerformed();
}
virtual void SetPerformed(bool val) = 0;
As for the starting time, the ending time and the duration of an IntervalVar variable, its
performedness is encapsulated in an IntExpr you can query with:
IntExpr* PerformedExpr();
will give you the exact minimal starting value if the variable is performed, the minimum between its minimal value and -1 if the variable may be performed and -1 if the variable is
unperformed.
SequenceVars
A SequenceVar variable is a variable which domain is a set of possible orderings of
IntervalVar variables. Because it allows the ordering of IntervalVar (tasks), it is often
used in scheduling. And for once it is not an abstract class! This is because these variables are
among the less rened variables in or-tools. They also have the least number of methods.
Basically, this class contains an array of IntervalVars and a precedence matrix indicating
15
Actually, it is an IntervalVar!
151
2
3
Precedence matrix
Current assignment
Array of IntervalVars
0
where the precedence matrix mat is such that mat(i,j) = 1 if i is ranked before j.
The IntervalVar are often given by their indices in the array of IntervalVars.
Ranked IntervalVars
Ranked IntervalVars are exactly that: already ranked variables in the sequence.
IntervalVars can be ranked at the beginning or at the end of the sequence in the
SequenceVar variable. unperformed IntervalVar can not be ranked17 . The next gure illustrates this:
1
?
?
2
Ranked sequence
Array of IntervalVars
0
Not ranked yet
1
Ranked
2
Ranked
3
unperformed
IntervalVar variables 1 and 2 are ranked (and performed) while IntervalVar variable
0 may be performed but is not performed yet and IntervalVar variable 3 is unperformed
and thus doesnt exist anymore.
To rank the IntervalVar variables, we say that we rank them rst or last. First and last
IntervalVar variables must be understood with respect to the unranked variables:
16
This looks very much like the actual implementation. The array is a scoped_array<IntervalVar*>
and the precedence matrix is given by a scoped_ptr<RevBitMatrix>. The actual class contains some more
data structures to facilitate and optimize the propagation.
17
Thus, unranked variables are variables that may be performed. Yeah, three-states situations that evolves with
time are nastier than a good old Manichean one.
152
19
()
Ranked rst
...
Ra
nk
t
irs
86
kF
1
Ranked sequence
La
st
n
Ra
()
42
23
Ranked last
to rank rst an IntervalVar variable means that this variable will be ranked before
all unranked variables and
to rank last an IntervalVar variable means that this variable will be ranked after all
unranked variables.
Public methods
All the following methods are updated with the current values of the SequenceVar. unperformed variables - unless explicitly stated in one of the arguments - are never considered.
First, you have the following getters:
void DurationRange(int64* const dmin, int64* const dmax) const:
Returns the minimum and maximum duration of the IntervalVar variables:
dmin is the total (minimum) duration of mandatory variables (those that must
be performed) and
dmax is the total (maximum) duration of variables that may be performed.
void HorizonRange(int64* const hmin, int64* const hmax) const:
Returns the minimum starting time hmin and the maximum ending time hmax of
all IntervalVar variables that may be performed.
void ActiveHorizonRange(int64* const hmin, int64* const hmax) const:
Same as above but for all unranked IntervalVar variables.
int Ranked() const: Returns the number of IntervalVar variables already
ranked.
int NotRanked() const: Returns
the
number
of
not-unperformed
IntervalVar variables that may be performed and that are not ranked
yet.
void ComputeStatistics(...): Computes the following statistics:
void ComputeStatistics(int* const ranked,
int* const not_ranked,
int* const unperformed) const;
153
the
index
th
rd
As you can see, there is a difference of one between the returned value and the
actual index of the IntervalVar in the array of IntervalVars variables.
int size() const: Returns the number of IntervalVar variables.
void FillSequence(...): a getter lling the three std::vector<int> of
rst ranked, last ranked and unperformed variables:
void FillSequence(std::vector<int>* const rank_first,
std::vector<int>* const rank_lasts,
std::vector<int>* const unperformed) const;
The method rst clears the three std::vectors and lls them with the
IntervalVar number in the sequence order of ranked variables. If all variables
are ranked, rank_first will contain all variables and rank_last will contain
none. unperformed will contain all the unperformed IntervalVar variables.
rank_first[0] corresponds to the rst IntervalVar of the sequence while
rank_last[0] corresponds to the last IntervalVar variable of the sequence,
i.e. the IntervalVar variables ranked last are given in the opposite order.
ComputePossibleFirstsAndLasts(...): a getter giving the possibilities
among unranked IntervalVar variables:
void ComputePossibleFirstsAndLasts(
std::vector<int>* const possible_firsts,
std::vector<int>* const possible_lasts);
This method computes the set of indices of IntervalVar variables that can be
ranked rst or last in the set of unranked activities.
Second, you have the following setters:
void RankFirst(int index): Ranks the index th IntervalVar variable in
front of all unranked IntervalVar variables. After the call of this method, the
IntervalVar variable is considered performed.
154
155
BinaryIntervalRelation constraints
You can specify a temporal relation between two IntervalVars t1 and t2:
ENDS_AFTER_END: t1 ends after t2 ends, i.e. End(t1) >= End(t2);
ENDS_AFTER_START: t1 ends after t2 starts, i.e. End(t1) >= Start(t2);
ENDS_AT_END: t1 ends at the end of t2, i.e. End(t1) == End(t2);
ENDS_AT_START: t1 ends at t2s start, i.e. End(t1) == Start(t2);
STARTS_AFTER_START: t1 starts after t2 starts, i.e.
Start(t2);
Start(t1) >=
TemporalDisjunction constraints
TemporalDisjunction constraints ensure that two ntervalVar variables are temporally
disjoint, i.e. they cannot be processed at the same time.
To create such a constraint, use:
solver = ...
...
IntervalVar * const t1 = ...
IntervalVar * const t2 = ...
...
Constraint * ct = solver.MakeTemporalDisjunction(t1, t2);
Maybe you can relate the decision on what has to happen rst to the value an IntVar takes:
156
...
IntVar * const decider = ...
Constraint * ct = solver.MakeTemporalDisjunction(t1, t2, decider)
If decider takes the value 0, then t1 has to happen before t2, otherwise it is the contrary.
This constraint works the other way around too: if t1 happens before t2, then the IntVar
decider is bound to 0 and else to a positive value (understand 1 in this case).
DisjunctiveConstraint constraints
DisjunctiveConstraint constraints are like TemporalDisjunction constraints but for an unlimited number of IntervalVar variables.
Think of the
DisjunctiveConstraint as a kind of AllDifferent constraints but on
IntervalVars.
The factory method is:
Constraint *
MakeDisjunctiveConstraint (
const std::vector< IntervalVar * > &intervals);
You remember that unperformed IntervalVars are non existing, dont you? And yes, we know that the
adjective rankable doesnt exist...
157
Constraint* MakeCumulative(const
const
int64
const
std::vector<IntervalVar*>& intervals,
std::vector<int64>& demands,
capacity,
string& name);
Here the capacity is modelled by an IntVar. This variable is really a capacity in the sense
that it is this variable that determines the capacity and it will not be adjusted to satisfy the
CumulativeConstraint constraint.
158
SequenceVars
For SequenceVar variables, there are basically two ways of choosing the next
SequenceVar to rank its IntervalVars:
SEQUENCE_DEFAULT = SEQUENCE_SIMPLE = CHOOSE_MIN_SLACK_RANK_FORWARD:
The CP solver chooses the SequenceVar which has the fewest opportunities of manoeuvre, i.e. the SequenceVar for which the horizon range (hmax - hmin, see the
HorizonRange() method above) is the closest to the total maximum duration of the
IntervalVars that may be performed (dmax in the DurationRange() method
above). In other words, we dene the slack to be
slack = (hmax hmin) dmax
and we choose the SequenceVar with the minimum slack. In case of a tie, we
choose the SequenceVar with the smallest active horizon range (see ahmin in the
ActiveHorizonRange() method above).
Once the best SequenceVar variable is chosen, the CP solver takes the rankable
IntervalVar with the minimum starting time (StartMin()) and ranks it rst.
CHOOSE_RANDOM_RANK_FORWARD: Among the SequenceVars for which there are still
IntervalVars to rank, the CP solver chooses one randomly. Then it randomly chooses
a rankable IntervalVar and ranks it rst.
SEQUENCE_DEFAULT, SEQUENCE_SIMPLE, CHOOSE_MIN_SLACK_RANK_FORWARD
and CHOOSE_RANDOM_RANK_FORWARD are given in the SequenceStrategy enum.
To create these search strategies, use the following factory method:
DecisionBuilder* Solver::MakePhase(
const std::vector<SequenceVar*>& sequences,
SequenceStrategy str);
IntervalVar may have been unnecessarily postponed. This is so important that we use
our warning box:
After the ranking of IntervalVars, the schedule is still loose and any
IntervalVar may have been unnecessarily postponed
If for instance, you are interested in the makespan, you might want to schedule each
IntervalVar at its earliest start time. As we have seen in the previous section, this can
be accomplished by minimizing the objective function corresponding to the ending times of all
IntervalVars:
IntVar * objective_var = ...
...
DecisionBuilder* const sequence_phase = solver.MakePhase(
all_sequences,
Solver::SEQUENCE_DEFAULT);
...
DecisionBuilder* const obj_phase = solver.MakePhase(objective_var,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);
By the way, the MakePhase() method has been optimized when the phase only handles one
or a few variables (up to 4), like in the above example for the obj_phase.
6.3.5 DependencyGraph
If you want to add more specic temporal constraints, you can use a data structure specialized
for scheduling: the DependencyGraph. It is meant to store simple temporal constraints and
to propagate efciently on the nodes of this temporal graph. One node in this graph corresponds
to an IntervalVar variable. You can build constraints on the start or the ending time of the
IntervalVar nodes.
Consider again our rst example (first_example_jssp.txt) and lets say that for whatever reason we want to impose that the rst task of job 2 must start at least after one unit of
time after the rst task of job 1. We could add this constraint in different ways but lets use the
DependencyGraph:
solver = ...
...
DependencyGraph * graph = solver.Graph();
graph->AddStartsAfterEndWithDelay(jobs_to_tasks[2][0],
jobs_to_tasks[1][0], 1);
Thats it!
Here is the output of an optimal solution found by the solver:
160
Objective value:
Machine_0: Job 1
Machine_1: Job 2
Machine_2: Job 1
13
(0,2)
(3,7)
(2,3)
Job 0 (2,5)
Job 0 (7,9) Job 1 (9,13)
Job 2 (7,10) Job 0 (10,12)
As you can see, the rst task of job 2 starts at 3 units of time and the rst task of job 1 ends at
2 units of time.
Other methods of the DependencyGraph include:
AddStartsAtEndWithDelay()
AddStartsAfterStartWithDelay()
AddStartsAtStartWithDelay()
The DependencyGraph and the DependencyGraphNode classes are declared in the
constraint_solver/constraint_solveri.h header.
161
162
Often, steps 1. and 2. are done simultaneously. This is the case in or-tools.
The following gure illustrates this process:
z
f
111111
000000 N
111111111111
000000000000
111111
000000
x1
Nx0
x0
Initial solution
x1
Local minimum
111
000 x
111
000 N
xi
x3
Nx2
x
x2
Global minimum
solution i
neighborhood
neighborhood of xi
163
The local search procedure starts from an initial feasible solution 0 and searches the
neighborhood 0 of this solution. The best solution found is 1 . The local search
procedure starts over again but with 1 as starting solution. In the neighborhood 1 , the
best solution found is 2 . The procedure continues on and on until stopping criteria are
met. Lets say that one of these criteria is met and the search ends with 3 . You can see
that while the method moves towards the local optima, it misses it and completely misses
the global optimum! This is why the method is called local search: it probably will nd
a local optimum (or come close to) but it is unable to nd a global optimum (except by
chance).
If we had continued the search, chances are that our procedure would have iterated around
the local optimum. In this case, we say that the local search algorithm is trapped by a
local optimum. Some LS methods - like Tabu Search - were developed to escape such
local optimum but again there is no guarantee whatsoever that they can succeed.
The gure above is very instructive. For instance, you can see that neighborhoods
dont have to be of equal size or centred around a variable . You can also see
that the relationship being in the neighborhood of is not necessarily symmetric:
1 0 but 0 1 24 ! In or-tools, you dene a neighborhood by implementing
the MakeNextNeighbor() callback method 25 from a LocalSearchOperator:
every time this method is called internally by the solver, it constructs one solution of the neighborhood If you have constructed a successful candidate, make
MakeNextNeighbor() returns true. When the whole neighborhood has been visited, make it returns false.
3. They nish the search when reaching a stopping criterion but usually without any
guarantee on the quality of the found solution(s):
Common stopping criteria include:
time limits:
for the whole solving process or
for some parts of the solving process.
maximum number of steps/iterations:
maximum number of branches;
maximum number of failures;
24
To be fair, we have to mention that most LS methods require this relation to be symmetric as a desirable
feature would be to be able to retrace our steps in case of a false start or to explore other possibilities. On the
gure, you might think about going left to explore wath is past the .
25
Well almost. The MakeNextNeighbor() callback is really low level and we have alleviate the task by
offering other higher level callbacks. See section 6.6 for more details.
164
165
but...:
Local search methods are strongly dependent on your knowledge of the problem and
your ability to use this knowledge during the search. For instance, very often the initial
solution plays a crucial role in the efciency of the local search. You might start from a
solution that is too far from a global (or local) optimum or worse you start from a solution
from which it is impossible to reach a global (or even local) optimum with your neighborhood denition. Several techniques have been proposed to tackle these annoyances. One
of them is to restart the search with different initial solutions. Another is to change the
denition of a neighborhood during the search like in Variable Neighbourhood Search
(VNS).
LS is a tradeoff between efciency and the fact that LS doesnt try to nd a global optimum,
i.e. in other words you are willing to give up the idea of nding a global optimum for the
satisfaction to quickly nd a (hopefully good) local optimum.
A certain blindness
LS methods are most of the time really blind when they search. Often you hear the analogy
between LS methods and descending a hilla to nd the lowest point in a valley (when we
minimize a function). It would be more appropriate to compare LS methods with going
down a valley ooded by mist: you dont see very far in what direction to go to continue
downhill. Sometimes you dont see anything at all and you dont even know if you are
allowed to set a foot in front of you!
a
If youve never heard this metaphor, skip this paragraph and dont bother.
Not to mention that some classes of problems are mathematically proven to have no possible guarantee on
their solution at all! (or only if P = NP).
166
If theory doesnt scare you, have a look at the subsection Approximation complexity for more about
approximation theory and quality guarantees.
b
The metric TSP is the classical TSP but on graphs that respect the triangle inequality, i.e. (, )
(, ) + (, ) where , and are nodes of the graph and () a distance function. The classical TSP itself
cannot be approximated within any constant factor (unless P = NP).
Tabu search, simulated annealing, guided local search and the like were designed to overcome some shortcomings of local search methods. Depending on the problem and how they are implemented, these methods can
also be seen as global search methods.
29
As all analogies, this one has certainly its limits!
167
Search. Its good to keep them in memory for the rest of this section. We then overview the implementation and describe some of its main components. Finally, we detail the inner workings
of the Local Search algorithm and indicate where the callbacks of the SearchMonitors are
called.
We present a simplied version of the local search algorithm. Yes, this is well worth a warning
box!
We describe a simplied version of the local search algorithm.
NestedSolveDecision
NestedSolveDecision::Apply(){
SolveAndCommit(FindOneNeighbor(ls));
}
NestedSolveDecision::Refute(){}
ls is the LocalSearchOperator that constructs the candidate solutions. The search tree
very quickly becomes completely unbalanced if we only keep nding solutions in the left
branches. Well see a balancing mechanism that involves one BalancingDecision at the
end of this section.
Speaking about candidate solutions, lets agree on some wordings. The next gure presents the
beginning of a Local Search. 0 is the initial solution. In or-tools, this solution is given by an
Assignment or a DecisionBuilder that the LocalSearch class uses to construct this
initial solution. 0 , 1 , 2 , . . . are solutions. As we have seen, the Local Search algorithm moves
from one solution to another. It takes a starting solution and visit the neighborhood dened
around to nd the next solution +1 . By visiting the neighborhood, we mean constructing
168
111111
000000 N
N
111111111111
000000000000
111111
000000
x1
x0
Candidate solutions
y1 y0 y2
x0
Initial solution
y3 y4
y5
x1
Current solution = starting solution for Nx1
The code consistently use the term neighbor to denote what we call a candidate solution in this
manual. We prefer to emphasize the fact that this neighbor solution is in fact a feasible solution
that the CP solver tests and accepts or rejects.
In this manual, we use the term candidate solution for what is consistently called a
neighbor in the code.
169
Candidate solution
Candidate solution
...
Candidate solution
We start with an initial feasible solution. The MakeOneNeighbor() callback method from
the local search operator(s)30 constructs candidate solutions one by one31 . These solutions are
checked by the CP solver and completed if needed. The best solution is chosen and the
process is repeated starting with this new improved solution32 .
The whole search process stops whenever a stopping criterion is reached or the CP solver
cannot improve anymore the current best solution.
Lets describe some pieces of the or-tools mechanism for local search:
initial solution: we need a feasible solution to start with. You can either pass an
Assignment or a DecisionBuilder to the LocalSearchs constructor.
LocalSearchPhaseParameters: the LocalSearchPhaseParameters parameter
holds the actual denition of the local search phase:
a SolutionPool that keep solution(s);
a LocalSearchOperator used to explore the neighborhood of the current
solution.
You can combine several LocalSearchOperators into one
LocalSearchOperator;
a complementary DecisionBuilder to instantiate unbound variables once an (incomplete) candidate solution has been dened by the LocalSearchOperator.
30
In the code, you are only allowed to use one LocalSearchOperator but you can combine several
LocalSearchOperators in one LocalSearchOperator. This is a common pattern in the code.
31
MakeOneNeighbor() is a convenient method. The real method to create a new candidate is
MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) but you have to deal
with the low level delta and deltadelta. We discuss these details in the section LocalSearchOperators:
the real thing!.
32
By default, the solver accepts the rst feasible solution and repeats the search starting with this new solution.
The idea is that if you combine the local search with an ObjectiveVar, the next feasible solution will be a
solution that beats the current best solution. You can change this behaviour with a SearchLimit. See below.
170
It will also complete the initial Assignment or the solution provided by the initial
DecisionBuilder.;
a Searchlimit specifying the stopping criteria each time we start searching a new
neighborhood;
an std::vector of LocalSearchFilters used to speed up the search by pruning
unfeasible (or undesirable) candidate solutions: instead of letting the solver nd out
if a candidate solution is feasible or not, you can help it by bypassing its checking
mechanism and telling it right away if a candidate solution is not feasible.
LocalSearchOperators are detailed in the next section and LocalSearchFilters in
section 6.8. We now detail these two basics ingredients that are the initial solution and the
LocalSearchPhaseParameters parameter.
The initial solution
To start the local search, we need an initial feasible solution. We can either give a starting
solution or we can ask the CP solver to nd one for us. To let the solver nd a solution for us, we
pass to it a DecisionBuilder. The rst solution discovered by this DecisionBuilder
will be taken as the initial solution.
There is a factory method for each one of the two options:
DecisionBuilder* Solver::MakeLocalSearchPhase(Assignment* assignment,
LocalSearchPhaseParameters* parameters)
DecisionBuilder* Solver::MakeLocalSearchPhase(
const std::vector<IntVar*>& vars,
DecisionBuilder* first_solution,
LocalSearchPhaseParameters* parameters)
171
dont have to provide one as it is constructed by default if you use the appropriate factory
method. If you want to keep intermediate solutions or want to modify these solutions
during the search, you might have to implement your own version. Four methods have to
be implemented:
void Initialize(Assignment* const assignment): This method
is called to initialize the SolutionPool with the initial Assignment.
void RegisterNewSolution(Assignment* const assignment):
This method is called when a new solution has been accepted by the local search
algorithm.
void GetNextSolution(Assignment* const assignment): This
method is called when the local search algorithm starts a new neighborhood.
assigment is the solution to start the new neighborhood search.
bool SyncNeeded(Assignment* const local_assignment): This
method checks if the current solution needs to be updated, i.e. the pool can oblige
the solver to start a new neighborhood search with the next solution given by the
pool (given by its GetNextSolution() method, see the Next() method of
the FindOneNeighbor DecisionBuilder class below).
A SolutionPool gives you complete control on the starting solution(s). Note that the
SolutionPool must take ownership of the Assignments it keeps33 .
a LocalSearchOperator: a LocalSearchOperator or a combination of
LocalSearchOperators explore the neighborhood of the current solution. We detail
them in the next section.
a DecisionBuilder: this complementary DecisionBuilder helps creating feasible solutions if your LocalSearchOperators only return partial solutions, i.e. solutions with unbounded variables. It also completes the initial solution if needed. If you
know that your candidate and the initial solutions are already feasible, you dont have to
provide this DecisionBuilder (set the corresponding pointer to NULL).
a SearchLimit: This SearchLimit limits the search of one neighborhood. The
most interesting statistic to limit is probably the number of found solutions:
SearchLimit * const limit = s.MakeSolutionsLimit(2);
This would limit the search to maximum two candidate solutions in the same neighborhood. By default, the CP solver stops the neighborhood search as soon as it nds a ltered
and feasible candidate solution. If you add an OptimizeVar to your model, once the
solver nds this good candidate solution, it changes the model to exclude solutions with
the same objective value. The second solution found can only be better than the rst one.
See section 3.9 to refresh your memory if needed. When the solver nds 2 solutions (or
when the whole neighborhood is explored), it stops and starts over again with the best
solution.
LocalSearchFilters: these lters speed up the search by bypassing the solver
checking mechanism if you know that the solution must be rejected (because it is not
33
Well, you could devise another way to keep track of the solutions and take care of their existence but anyhow,
you are responsible for these solutions.
172
feasible, because it is not good enough, ...). If the lters accept a solution, the solver
still tests the feasibility of this solution. LocalSearchFilters are discussed in section 6.8.
Several factory methods are available to create a LocalSearchPhaseParameters parameter. At least you need to declare a LocalSearchOperator and a complementary
DecisionBuilder:
LocalSearchPhaseParameters * Solver::MakeLocalSearchPhaseParameters(
LocalSearchOperator *const ls_operator,
DecisionBuilder *const
complementary_decision_builder);
The
LocalSearchOperator
will
nd
candidate
solutions
while
complementary_decision_builder DecisionBuilder will complete
candidate solutions if some of the variables are not assigned.
the
the
A handy way to create a DecisionBuilder to assist the local search operator(s) is to limit
one with MakeSolveOnce(). MakeSolveOnce is a DecisionBuilder that takes another DecisionBuilder db and SearchMonitors:
DecisionBuilder * const db = ...
SearchLimit* const limit = solver.MakeLimit(...);
DecisionBuilder * const complementary_decision_builder =
solver.MakeSolveOnce(db, limit);
6.5.3 The basic local search algorithm and the callback hooks for
the SearchMonitors
We feel compelled to use our warning box again:
We describe a simplied version of the Local Search algorithm.
173
If you want to know more, have a look at the section Local Search (LS) in the chapter Under
the hood.
In this subsection, we present the callbacks of the SearchMonitor listed in Table 6.1
and show you exactly when they are called in the search algorithm.
Table 6.1: Local Search algorithm callbacks from the SearchMonitor class.
Methods
LocalOptimum()
AcceptDelta(Assignment *delta,
Assignment *deltadelta)
AcceptNeighbor()
PeriodicCheck()
Descriptions
When a local optimum is reached. If true is
returned, the last solution is discarded and the
search proceeds to nd the next local optimum.
Handy when you implement a meta-heuristic with a
SearchMonitor.
When the LocalSearchOperator has produced
the next candidate solution given in the form of
delta and deltadelta. You can accept or reject
this new candidate solution.
After accepting a candidate solution during Local
Search.
Periodic call to check limits in long running search
procedures, like Local Search.
To ensure the communication between the local search and the global search, three utility functions are dened. These functions simply call their SearchMonitors counterparts, i.e. they
call the corresponding methods of the involved SearchMonitors:
bool LocalOptimumReached(): FalseExceptIfOneTrue.
bool AcceptDelta(): TrueExceptIfOneFalse.
void AcceptNeighbor(): Notication.
Before we delve into the core of the local search algorithm and the implementation of
the LocalSearch DecisionBuilders Next() method, we rst discuss the inner
workings of the FindOneNeighbor DecisionBuilder whose job is to nd the next
ltered and accepted candidate solution. This DecisionBuilder is used inside a
NestedSolveDecision that we study next. This Decision is returned by the Next()
method of the LocalSearch DecisionBuilder in the main loop of the local search algorithm. Finally, we address the LocalSearch DecisionBuilder class. In particular,
we study its initializing phase and its Next() method. We consider the case where an initial
DecisionBuilder constructs the initial solution.
SearchMonitors callbacks are indicated in the code by the comment:
// SEARCHMONITOR CALLBACK
174
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
DecisionBuilder* restore =
solver->MakeRestoreAssignment(assignment_copy);
if (sub_decision_builder_) {
restore = solver->Compose(restore, sub_decision_builder_);
}
Assignment* delta = solver->MakeAssignment();
Assignment* deltadelta = solver->MakeAssignment();
22
23
24
25
26
27
28
29
30
31
32
33
34
// MAIN LOOP
while (true) {
delta->Clear();
deltadelta->Clear();
// SEARCHMONITOR CALLBACK
solver->TopPeriodicCheck();
if (++counter >= FLAGS_cp_local_search_sync_frequency &&
pool_->SyncNeeded(reference_assignment_.get())) {
// SYNCHRONIZE ALL
...
counter = 0;
}
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
if (!limit_->Check()
&& ls_operator_->MakeNextNeighbor(delta, deltadelta)) {
solver->neighbors_ += 1;
// SEARCHMONITOR CALLBACK
const bool meta_heuristics_filter =
AcceptDelta(solver->ParentSearch(), delta, deltadelta);
const bool move_filter = FilterAccept(delta, deltadelta);
if (meta_heuristics_filter && move_filter) {
solver->filtered_neighbors_ += 1;
assignment_copy->Copy(reference_assignment_.get());
assignment_copy->Copy(delta);
if (solver->SolveAndCommit(restore)) {
solver->accepted_neighbors_ += 1;
assignment_->Store();
neighbor_found_ = true;
return NULL;
}
}
} else {
175
55
if (neighbor_found_) {
56
// SEARCHMONITOR CALLBACK
AcceptNeighbor(solver->ParentSearch());
pool_->RegisterNewSolution(assignment_);
// SYNCHRONIZE ALL
...
} else {
break;
}
57
58
59
60
61
62
63
64
}
solver->Fail();
return NULL;
65
66
67
68
You might wonder why there are so many lines of code but there are a some subtleties to
consider.
The code of lines 5 to 8 is only called the rst time the Next() method is invoked and allow
to synchronize the Local Search machinery with the initial solution. In general, the words
SYNCHRONIZE ALL in the comments mean that we synchronize the Local Search Operators
and the Local Search Filters with a solution.
reference_assignment_ is an Assignment with the initial solution while
assignment_ is an Assignment with the current solution. On line 10, we copy
reference_assignment_ to the local assignment_copy Assignment to be able
to dene the deltas. counter counts the number candidate solutions. This counter is used
on line 29 to test if we shouldnt start again the Local Search with another solution.
On lines 15-19, we dene the restore DecisionBuilder that will allow us to keep the
newly found candidate solution.
We construct the delta and deltadelta on lines 20 and 21 and are now ready to enter the
main loop to nd the next solution.
On lines 25 and 26 we clear our deltas and on line 28 we allow for a periodic check: for
searches that last long, we allow the SearchMonitors to interfere and test if the search
needs to continue or not and/or must be adapted.
Lines 29-34 allow to change the starting solution and ask the solution
pool pool_ for a new solution via its GetNextSolution().
The
FLAGS_cp_local_search_sync_frequency value corresponds to the number
of attempts before the CP solver tries to synchronize the Local Search with a new solution.
On line 36 and 37, the SearchLimits applied to the search of one neighborhood are tested.
If the limits are not reached and if the LocalSearchOperator succeeds to nd a new
candidate solution, we enter the if statement on line 38. The LocalSearchOperators
MakeNextNeighbor() method is called to create the next candidate solution in deltas
format.
If you overwrite the MakeNextNeighbor() method, you need to manage the deltas:
you must take care of applying and reverting the deltas yourself if needed. You
can use the ApplyChanges() and RevertChanges() helper functions to do so.
176
ApplyChanges() actually lls the deltas after you use the helper methods
SetValue(), Activate() and the like to change the current candidate solution. Once
we enter the if statement on line 38, we have a new candidate solution and we update the solution counter accordingly. It is now time to test this new solution candidate. The rst test comes from the SearchMonitors in their AcceptDelta() methods. If only one SearchMonitor rejects this solution, it is rejected. In or-tools,
we implement (meta-)heuristics with SearchMonitors. See chapter 7 for more. The
AcceptDelta() function is the global utility function we mentioned above. Well meet
LocalOptimumReached() and AcceptNeighbor() a few lines below.
The second test is the ltering test on line 42.
FilterAccept() returns a
TrueExceptIfOneFalse. If both tests are successful, we enter the if statement on
line 44. If not, we simply generate another candidate solution. On lines 44 and 46, we
update the counter of filtered_neighbors_ and store the candidate solution in the
assignment_copy Assignment.
On line 47, we try (and if needed complete) the candidate. If we succeed, the current solution
and the counter accepted_neighbors_ are updated. The Next() method returns NULL
because the FindOneNeighbor DecisionBuilder has nished its job at this node of
the search tree. If we dont succeed, the solver fails on line 66.
The SolveAndCommit() method is similar to the Solve() method except that
SolveAndCommit will not backtrack all modications at the end of the search and this is
why you should:
Use the SolveAndCommit() method only in the Next() method of a
DecisionBuilder!
If the if test on line 36 and 37 fails, we enter the else part of the statement on line 55. This
means that either one SearchLimit was reached or that the neighborhood is exhausted. If a
solution (stored in assignment_) was found during the local search, we register it and synchronize the LocalSearchOperators and LocalSearchFilters with a new solution
provided by the solution pool pool_ on lines 58-60. We also notify the SearchMonitors
177
on line 57. If no solution was found, we simply break out of the while() loop on line 62
and make the CP solver fail on line 66.
The NestedSolveDecision Decision
The NestedSolveDecision is the Decision that the LocalSearchs Next()
method returns to nd the next solution. This Decision is basically a Decision wrapper around a nested solve with a given DecisionBuilder and SearchMonitors. It
doesnt do anything in its right branch (in its Refute() method) and calls Solve() or
SolveAndCommit() depending on a restore bool in its left branch (in its Apply()
method).
The NestedSolveDecision Decision can be in three states that are also the three states
of the Local Search:
Value
DECISION_FAILED
DECISION_PENDING
DECISION_FOUND
Meaning
The nested search phase failed, i.e.
Solve() or
SolveAndCommit() failed.
The nested search hasnt been called yet. The local search
is in this state when it balances the search tree.
The nested search phase succeeded and found a solution,
i.e. Solve() or SolveAndCommit() succeeded and
returned true.
Consider the situation where we already have a LocalSearchPhaseParameters parameter set up and we let the CP solver construct the initial solution:
Solver s("Dummy LS");
...
std::vector<IntVar*> vars = ...
...
LocalSearchOperator * const ls_operator = ...
DecisionBuilder * const complementary_decision_builder = ...
...
LocalSearchPhaseParameters params =
s.MakeLocalSearchPhaseParameters(ls_operator,
complementary_decision_builder);
178
We can now add as many monitors as we want and launch the solving process:
std::vector<SearchMonitor *> monitors;
...
s.Solve(ls, monitors);
Its interesting to see how this initial solution is constructed in the LocalSearch class. First,
we create an Assignment to store this initial solution:
Assignment * const initial_sol = s.MakeAssignment();
initial_solution_and_store constructs
DecisionBuilder is used in a nested search:
this
initial
solution.
This
where:
limit is the SearchLimit given to the local search algorithm;
the NestedSolveDecision constructors arguments are respectively:
a DecisionBuilder to construct the next solution;
179
This is exactly the DecisionBuilder used when you give an initial solution to the
CP solver. The initial_solution DecisionBuilder is simply replaced with a
RestoreAssignment DecisionBuilder taking your initial Assignment.
Now that we have developed the machinery to nd and test the initial solution, we are ready to
wrap the nested solve process into a NestedSolveDecision:
// Main DecisionBuilder to find candidate solutions one by one
DecisionBuilder* find_neighbors =
solver->RevAlloc(new FindOneNeighbor(assignment_,
pool_,
ls_operator_,
sub_decision_builder_,
limit_,
filters_));
NestedSolveDecision* decision = solver->RevAlloc(
new NestedSolveDecision(find_neighbors,
false)));
180
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
fault. Once the search tree height is over 32, the NestedSolveDecision Decision
enters in action and when the height of the three gets higher than 32, we make the CP
solver Fail() to backtrack on line 23 thus keeping the height of the tree bounded.
Line 28: case DECISION_FOUND: The nested search found a solution that is the current solution. The LocalSearchs Next() method has done its job at the current
node and nothing needs to be done.
Solve(), SolveAndCommit(), SolveOnce(), etc...: what are the differences?
This topic is so important that the whole section 13.3 is devoted to it. You already can
jump ahead and read this section if youre curious.
0 ,..., 1
subject to:
0 + 1 + ... + 1
0 1.
{0, . . . , 1} for = 0 . . . 1.
6.6.1 LocalSearchOperators
The base class for all local search operators is LocalSearchOperator. The behaviour
of this class is similar to that of an iterator. The operator is synchronized with a feasible
solution (an Assignment that gives the current values of the variables). This is done in the
Start() method. Then one can iterate over the candidate solutions (the neighbors) using the
MakeNextNeighbor() method. Only the modied part of the solution (an Assignment
called delta) is broadcast. You can also dene a second Assignment representing the
changes to the last candidate solution dened by the local search operator (an Assignment
called deltadelta).
The CP solver takes care of these deltas and other hassles for the most common cases34 .
The next gure shows the LS Operators hierarchy.
34
182
LocalSearchOperator
IntVarLocalSearchOperator
SequenceVarLocalSearchOperator
PathOperator
At the time of writing, there are no LocalSearchOperators dened for IntervalVars. See subsection XXX for a workaround.
36
For instance, the SetValue() method is replaced by the SetForwardSequence() and
SetBackwardSequence() methods.
183
solution(s) again and again or if you dont provide any solution, the solver will not detect it (in
the second case, the solver will enter an innite loop). You are responsible to scour correctly
the neighborhood. Second, you have to be sure the variables you want to change do exist (i.e.
beware of going out of bounds on arrays).
Now the good news is that you dont have to test for feasibility: its the job of the solver. You
are even allowed to assign out of domain values to the variables. Again, the solver will discard
such solutions (you can also lter these solutions out, see the section Filtering).
Without further delay, here is the code for our custom LSO:
class DecreaseOneVar: public IntVarLocalSearchOperator {
public:
DecreaseOneVar(const std::vector<IntVar*>& variables)
: IntVarLocalSearchOperator(variables.data(), variables.size()),
variable_index_(0) {}
virtual ~MoveOneVar() {}
protected:
// Make a neighbor assigning one variable to its target value.
virtual bool MakeOneNeighbor() {
if (variable_index_ == Size()) {
return false;
}
const int64 current_value = Value(variable_index_);
SetValue(variable_index_, current_value - 1);
variable_index_ = variable_index_ + 1;
return true;
}
private:
virtual void OnStart() {
variable_index_ = 0;
}
int64 variable_index_;
};
Our custom LS Operator simply takes one variable at a time and decrease its value by 1. The
neighborhood visited from a given solution [0 , 1 , . . . , 1 ] is made of the following solutions
(when feasible):
{[0 , 1 , . . . , 1 ], [0 1, 1 , . . . , 1 ], [0 , 1 1, . . . , 1 ], . . . , [0 , 1 , . . . , 1 1]}
The given initial solution is also part of the neighborhood.
We have rewritten the protected method MakeOneNeighbor() to construct the
next solutions. The variable variable_index_ indicates the current variable we are
decreasing in the current solution. As long as there are remaining variables to decrease, MakeNextNeighbor() returns true. Once we have decreased the last variable
(variable_index_ is then equal to Size()), it returns false.
The private method OnStart() that is used whenever we start again with a new feasible
solution, simply resets the variable index to 0 to be able to decrease the rst variable 0 by 1.
We use the LS Operator DecreaseOneVar in the function SimpleLS() that starts as follow:
184
1.
The OptimizeVar SearchMonitor is very important as it will give the direction to follow for the local search algorithm. Without it, the local search would walk randomly wihout
knowing where to go.
Next, based on the Boolean variable FLAG_initial_phase, we create
DecisionBuilder to nd an initial solution or we construct an initial Assignment:
185
This log will print the objective value and some other interesting statistics every time a better
feasible solution is found or whenever we reach a 1000 more branches in the search tree.
Finally, we launch the search and print the objective value of the last feasible solution found:
s.Solve(ls, collector, obj, log);
LOG(INFO) << "Objective value = " << collector->objective_value(0);
The modulo operator (mod) nds the remainder of the division of one (integer) number by another: For
instance, 11 mod 5 = 1 because 11 = 2 5 + 1. When you want to test a positive number for parity, you can
test mod 2. If mod 2 = 0 then is even, otherwise it is odd. In C++, the mod operator is %.
186
Solution
Solution
Solution
Solution
Solution
Solution
Solution
Solution
Finished
As you can see, 10 solutions were generated with decreased objective values. Solution #0
is the initial solution given: [3, 2, 3, 2]. Then as expected, 9 neighborhoods were visited and
each time a better solution was chosen:
neighborhood 1 around [3, 2, 3, 2]: [2, 2, 3, 2] is immediately taken as it is a better solution
with value 9;
neighborhood 2 around [2, 2, 3, 2]: [1, 2, 3, 2] is a new better solution with value 8;
neighborhood 3 around [1, 2, 3, 2]: [0, 2, 3, 2] is rejected as infeasible, [1, 1, 3, 2] is a new better solution with value 7;
neighborhood 4 around [1, 1, 3, 2]: [0, 1, 3, 2] is rejected as infeasible, [1, 0, 3, 2] is a new better solution with value 6;
neighborhood 5 around [1, 0, 3, 2]: [0, 0, 3, 2], [0, 1, 3, 2] are rejected as infeasible,
[1, 0, 2, 2] is a new better solution with value 5;
neighborhood 6 around [1, 0, 2, 2]: [0, 1, 2, 2], [1, 1, 2, 2] are rejected as infeasible,
[1, 0, 1, 2] is a new better solution with value 4;
neighborhood 7 around [1, 0, 1, 2]: [0, 0, 1, 2], [1, 1, 1, 2] are rejected as infeasible,
[1, 0, 0, 2] is a new better solution with value 3;
neighborhood 8 around [1, 0, 0, 2]: [0, 0, 0, 2], [1, 1, 0, 2], [1, 0, 1, 2] are rejected as infeasible, [1, 0, 0, 1] is a new better solution with value 2;
neighborhood 9 around [1, 0, 0, 1]: [0, 0, 0, 1], [1, 1, 0, 1], [1, 0, 1, 1] are rejected as infeasible, [1, 0, 0, 0] is a new better solution with value 1;
At this point, the solver is able to recognize that there are no more possibilities. The two last
lines printed by the SearchLog summarize the local search:
Finished search tree, ..., neighbors = 23, filtered neighbors = 23,
accepted neighbors = 9, ...)
End search (time = 1 ms, branches = 67, failures = 64, memory used =
15.13 MB, speed = 67000 branches/s)
There were indeed 23 constructed candidate solutions among which 23 (ltered neighbors)
were accepted after ltering and 9 (accepted neighbors) were improving solutions.
If you take the last visited neighborhood (neighborhood 9), you might wonder if it was really
187
This creates a LocalSearchOperator which concatenates a vector of operators. Each operator from the vector is called sequentially. By default, when a candidate solution is accepted,
the neighborhood exploration restarts from the last active operator (the one which produced
this candidate solution).
This can be overriden by setting restart to true to force the exploration to start from the
rst operator in the vector:
LocalSearchOperator* Solver::ConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops, bool restart);
You can also use an evaluation callback to set the order in which the operators are explored
(the callback is called in LocalSearchOperator::Start()). The rst argument of the
callback is the index of the operator which produced the last move, the second argument is the
index of the operator to be evaluated. Ownership of the callback is taken by the solver.
Here is an example:
const int kPriorities = {10, 100, 10, 0};
int64 Evaluate(int active_operator, int current_operator) {
return kPriorities[current_operator];
}
LocalSearchOperator* concat =
solver.ConcatenateOperators(operators,
NewPermanentCallback(&Evaluate));
The elements of the operators vector will be sorted by increasing priority and explored in
that order (tie-breaks are handled by keeping the relative operator order in the vector). This
would result in the following order:
operators[3], operators[0], operators[2], operators[1].
Sometimes you dont know in what order to proceed. Then the following method might help
you:
188
LocalSearchOperator* Solver::RandomConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops);
MoveTowardTargetLS
Creates a local search operator that tries to move the assignment of some variables toward a
target. The target is given as an Assignment. This operator generates candidate solutions
which only have one variable that belongs to the target Assignment set to its target value.
There are two factory methods to create a MoveTowardTargetLS operator:
LocalSearchOperator* Solver::MakeMoveTowardTargetOperator(
const Assignment& target);
and
LocalSearchOperator* Solver::MakeMoveTowardTargetOperator(
const std::vector<IntVar*>& variables,
const std::vector<int64>& target_values);
The target is here given by two std::vectors: a vector of variables and a vector of associated target values. The two vectors should be of the same length and the variables and values
are ordered in the same way.
189
The variables are changed one after the other in the order given by the Assignment or the
vector of variables. When we restart from a new feasible solution, we dont start all over again
from the rst variable but keep changing variables from the last change.
DecrementValue and IncrementValue
These operators do exactly what their names say: they decrement and increment by 1 the value
of each variable one after the other.
To create them, use the generic factory method
LocalSearchOperator* Solver::MakeOperator(
const std::vector<IntVar*>& vars,
Solver::LocalSearchOperators op);
190
This method constructs the delta and deltadelta corresponding to the new candidate solution and returns true. If the neighborhood has been exhausted, i.e. the
LocalSearchOperator cannot nd another candidate solution, this method returns
false.
When you write your own MakeNextNeighbor() method, you have to provide the new
delta but you can skip the deltadelta if you prefer. This deltadelta can be convenient when you dene your lters and you can gain some efciency over the sole use of
deltas.
To help you construct these deltas, we provide an inner mechanism that constructs automatically these deltas when you use the following self-explanatory setters:
for IntVarLocalSearchOperators only:
SetValue(int64 index, int64 value);
for SequenceVarLocalSearchOperators only:
SetForwardSequence(int64 index, const std::vector<int>&
value);
SetBackwardSequence(int64 index, const
std::vector<int>& value);
for both:
Activate(int64 index);
Deactivate(int64 index).
191
If you only use these methods to change the current solution, you then can automatically construct the deltas by calling the ApplyChanges() method and revert these changes by
calling the RevertChanges() method.
We recommend to use the following template to dene your MakeNextNeighbor()
method:
virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (NEIGHBORHOOD EXHAUSTED) {
return false;
}
// CONSTRUCT NEW CANDIDATE SOLUTION
...
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}
Currently, ApplyChanges() always returns true but this might change in the future and
then you might have to revert the changes, hence the while() loop.
We also provide several getters:
for IntVarLocalSearchOperators only:
int64 Value(int64 index);
IntVar* Var(int64 index);
int64 OldValue(int64 index);
for SequenceVarLocalSearchOperators only:
const std::vector<int>& Sequence(int64 index);
SequenceVar* Var(int64 index);
const std::vector<int>& OldSequence(int64 index);
for both:
bool IsIncremental();
bool Activated(int64 index);
192
to
use
MakeNextNeighbor()
instead
of
One reason is efciency: you skip one callback. But the real reason is that you might
need other methods than the ones that are provided to construct your candidate solution.
In this case, you have no other choice than to reimplement the MakeNextNeighbor()
method.
Incrementality
[TO BE WRITTEN]
193
return;
}
This Increment() method returns a bool that indicates when the neighborhood is exhausted, i.e. it returns false when there are no more candidate to construct. Size() and
Var() are helper methods dened in the SequenceVarLocalSearchOperator class.
We start with current_var_, current_first_ and current_second_ all set to 0.
Pay attention to the fact that current_first_ and current_second_ are also updated
inside the if conditions.
194
If
Increment()
returns
false,
we
have
exhausted
the
neighborhood and MakeNextNeighbor() must return false.
Sequence()
and
SetForwardSequence()
are
two
helper
methods
from
the
SequenceVarLocalSearchOperator class that allow us to use the
ApplyChanges() method to construct the deltas.
And thats it! Our LocalSearchOperator operator is completed. Lets test it!
First, we need our LocalSearchOperator:
LocalSearchOperator* const swap_operator =
solver.RevAlloc(new SwapIntervals(all_sequences.data(),
all_sequences.size()));
Then we need a complementary DecisionBuilder to construct feasible candidate solutions. We dont want to spent too much time on the completion of our solutions. We will use
the CHOOSE_RANDOM_RANK_FORWARD strategy:
DecisionBuilder* const random_sequence_phase =
solver.MakePhase(all_sequences,
Solver::CHOOSE_RANDOM_RANK_FORWARD);
195
Value
1051
Candidates
31172
Solutions
26
Not very satisfactory: 1051 is really far from the optimal value of 679. Lets try to generalize
our operator. Instead of just swapping two IntervalVars, well shufe an arbitrary number
of IntervalVars per SequenceVar in the next subsection.
6.7.4 Exchanging
an
arbitrary
number
IntervalVars on a SequenceVar
of
contiguous
We accept the default values for the BidirectionalIterator and the Compare classes.
It will rearrange the elements in the range [first,last) into the next lexicographically
greater permutation. An example will clarify this jargon:
No
1
2
3
4
5
6
Permutations
012
021
102
120
201
210
This explanation is not rigorous but it is simple and you can ll the gaps. What happens if you start with 1 0
2? The std::next_permutation() function simply returns 1 2 0 (oops, there goes our rigour again!).
196
As usual with the std, the last element is not involved in the permutation. There is only one
more detail we have to pay attention to. We ask the user to provide the length of the permutation
with the gags ag FLAGS_shuffle_length. First, we have to test if this length makes
sense but we also have to adapt it to each SequenceVar variable.
Without delay,
we present
LocalSearchOperator:
the
constructor
of
the
ShuffleIntervals
vars and size are just the array of SequenceVars and its size. max_length is the length
of the sequence of IntervalVars to shufe. Because you can have less IntervalVars
for a given SequenceVar, we have named it max_length.
The indices are very similar to the ones of the SwapIntervals operator:
current_var_: the index of the processed SequenceVar;
current_first_: the index of the rst IntervalVar variable to shufe;
current_length_: the length of the current sub-array of indices to shufe. It must
be smaller or equal to the number of IntervalVars in the SequenceVar.
Here is the code to increment the next permutation:
bool Increment() {
if (!std::next_permutation(current_permutation_.begin(),
current_permutation_.end())) {
// No permutation anymore -> update indices
if (++current_first_ >
Var(current_var_)->size() - current_length_) {
if (++current_var_ >= Size()) {
return false;
}
current_first_ = 0;
current_length_ = std::min(Var(current_var_)->size(),
max_length_);
current_permutation_.resize(current_length_);
}
// Reset first permutation in case we have to increase
// the permutation.
for (int i = 0; i < current_length_; ++i) {
current_permutation_[i] = i;
}
// Start with the next permutation, not the identity
// just constructed.
if(!std::next_permutation(current_permutation_.begin(),
If you give it 2 1 0, this function returns false but there is a side effect as the array will be ordered! Thus in
our case, well get 0 1 2!
197
current_permutation_.end())) {
LOG(FATAL) << "Should never happen!";
}
}
return true;
}
Value
1016
1087
1034
1055
Candidates
4302
7505
70854
268478
Solutions
32
15
33
27
These results are typical for a local search operator. There certainly are several lessons to be
drawn from these results, but lets focus on one of the most basic and important ones. The
path taken to nd the local optimum is crucial. Even if the neighborhoods (theoretically) constructed with suffle_length set to 2 are all contained in the neighborhoods constructed
with suffle_length set to 3, we dont reach the same local optimum. This is very important to understand. The paths taken in both cases are different. The (practical) construction of
the neighbourhoods is dynamic and path-dependent. Good (meta-)heuristics are path-aware:
these heuristics take the path (and thus the history of the search) into account. Moreover, bigger neighbourhoods (shuffle_length = 3) arent necessarily better than smaller ones
(shuffle_length = 2). We obtain a better solution quicker with shuffle_length=2
than with suffle_length=3.
The best solution obtained so far has a value of 1016. Can we do better? Thats the topic of
next sub-section!
199
The main method in this callback is the virtual bool Run() method. This method
returns true if our limit has been reached and false otherwise. The time limit in ms
is given by global_time_limit. If the Search is still producing a certain amount
solution_nbr_tolerance of solutions, we let the search continue.
To initialize our rst local search that nds our initial solution, we use the same code as in the
le jobshop_ls2.cc (we call this rst solution first_solution).
To nd an initial solution, we use local search and start form the first_solution found.
We only use a ShuffleIntervals operator with a shufe length of 2. This time, we limit
this local search with our custom limit:
SearchLimit * initial_search_limit = solver.MakeCustomLimit(
new LSInitialSolLimit(&solver,
FLAGS_initial_time_limit_in_ms,
FLAGS_solutions_nbr_tolerance));
200
The
ConcatenateOperators()
method
takes
an
std::vector
of
LocalSearchOperator and a bool that indicates if we want to restart the operators one after the other in the order given by this vector once a solution has been found.
The rest of the code is similar to that in the le jobshop_ls2.cc.
Results
If we solve our problem instance (le first_example_jssp.txt), we still get the optimal
solution. No surprise here. What about the abz9 instance?
With our default value of
time_limit_in_ms = 0, thus no time limit;
shuffle_length = 4;
initial_time_limit_in_ms = 20000, thus a time of 20 seconds to nd an
initial solution with local search and the ShuffleIntervals operator with a shufe
length of 2 and;
solutions_nbr_tolerance = 1,
201
6.8. Filtering
Time
81,603
103,139
104,572
102,860
84,555
42,235
36,935
...
19,229
Value
983
936
931
931
931
1012
1012
...
1016
Candidates
49745
70944
70035
68359
63949
29957
26515
...
13017
Solutions
35
59
60
60
60
32
32
...
32
The rst column lists the times allowed to nd the initial solution with the
ShuffleIntervals operator (with its shufe length set to 2) and the second column collects the objective values of this initial solution. The more time given to the rst local search,
the better the objective values. The next four columns are the same as before.
You might think that starting from a better solution would give better results but it is no necessarily the case. Our best result, 931 is obtained when we start from solutions with an average
objective value. When we start with better solutions, like the one with an objective value of
1016, we completely miss the 931 solution! This 931 solution seems to be a local optimum
for our local search and it seems we can not escape it. In chapter 7, well see how some metaheuristics escape this local minimum. For now, we turn our attention to another preoccupation:
if you read the Candidates column and compare it with the Solutions column, you can see that
our algorithm produces lots of candidates and very few solutions. This is normal. Remember
that every time a candidate (a neighbor) is produced, the CP solver takes the time to verify if
this candidate is a feasible solution. This is costly. In the next section, well see a mechanism
to shortcut this verication and command the solver to disregard some candidates without the
need for the solver to test them explicitly.
6.8 Filtering
You can nd the code in the le dummy_ls_filtering.cc.
Our local search strategy of section 6.6 is not very efcient: we test lots of unfeasible
or undesirable candidate solutions. LocalSearchFilters allow to shortcut the solvers
solving and testing mechanism: we can tell the solver right away to skip a candidate solution.
6.8.1 LocalSearchFilters
LocalSearchFilters instruct the CP solver to skip (or not) the current
candidate solution.
You can nd the declaration and denition in the header
constraint_programming/constraint_solveri.h.
202
As you can see, these two methods are pure virtual methods and thus must be implemented.
The Accept() method returns true if you accept the current candidate solution to be
tested by the CP solver and false if you know you can skip this candidate solution. The
candidate solution is given in terms of delta and deltadelta. These are provided by
the MakeNextNeighbor() of the LocalSearchOperator. The Synchronize()
method, lets you synchronize the LocalSearchFilter with the current solution, which
allows you to reconstruct the candidate solutions given by the delta Assignment.
If your LocalSearchOperator is incremental, you must notice the CP solver by implementing the IsIncremental() method:
virtual bool IsIncremental() const { return true; }
For IntVar, the specialized IntVarLocalSearchFilter offers convenient methods and you should
rather implement the OnSynchronize() method that is called at the end of the Synchronize() method.
203
6.8. Filtering
First, we acquire the IntContainer and its size. Each Assignment has containers
to keep its IntVars, IntervalVars and SequenceVars (more precisely pointers to).
To access those containers, use the corresponding Container() methods if you dont
want to change their content, use the corresponding Mutable...Container() method
if you want to change their content. For instance, to change the SequenceVars, use the
MutableSequenceVarContainer() method.
For the sake of efciency, Assignment contains a light version of the variables. For instance,
an ntVarContainer contains IntVarElements and the call to
FindIndex(solution_delta.Element(index).Var(), &touched_var);
...
LocalSearchFilter * const filter = s.RevAlloc(
new ObjectiveValueFilter(vars));
std::vector<LocalSearchFilter*> filters;
filters.push_back(filter);
...
ls_params = s.MakeLocalSearchPhaseParameters(..., filters);
we obtain:
..., neighbors = 23, filtered neighbors = 23,
accepted neighbors = 9, ...
Lets write a LocalSearchFilter that lters infeasible candidate solutions. We dont need
to provide an OnSyncronize() method. Here is our version of the Accept() method:
virtual bool Accept(const Assignment* delta,
const Assignment* deltadelta) {
const Assignment::IntContainer& solution_delta =
delta->IntVarContainer();
const int solution_delta_size = solution_delta.Size();
for (int index = 0; index < solution_delta_size; ++index) {
const IntVarElement& element = solution_delta.Element(index);
if (!element.Var()->Contains(element.Value())) {
return false;
}
}
return true;
}
Aha, you probably expected an ad hoc solution rather than the general solution above, didnt
you?40 .
We now obtain:
..., neighbors = 23, filtered neighbors = 9,
accepted neighbors = 9, ...
40
To be fair, this solution is not as general as it should be. We didnt take into account the fact that some
IntervalVar variables can be non active but for IntVars and SequenceVars it works well.
205
6.8. Filtering
The ObjectiveFilter is more interesting and exists in different avors depending on:
the type of move that is accepted based on the current objective value:
The different possibilities are given by the LocalSearchFilterBound enum:
GE: Move is accepted when the candidate objective value >= objective.Min;
LE: Move is accepted when the candidate objective value <= objective.Max;
EQ: Move is accepted when the current objective value is in the interval
objective.Min ... objective.Max.
the type of operation used in the objective function:
The different possibilities are given in the LocalSearchOperation enum and concern the variables given to the MakeLocalSearchObjectiveFilter() method:
SUM: The objective is the sum of the variables;
PROD: The objective is the product of the variables;
MAX: The objective is the max of the variables;
MIN: The objective is the min of the variables.
the callbacks used: we refer the curious reader to the code in the le
constraint_programming/local_search.cc for more details about different
available callbacks.
For all these versions, the factory method is MakeLocalSearchObjectiveFilter().
Again, we refer the reader to the code to see all available renements.
206
6.9 Summary
6.9.1 What is missing?
6.9.2 To go further:
Examples:
Lab:
207
CHAPTER
SEVEN
Overview:
Prerequisites:
210
211
CHAPTER
EIGHT
8.2 Consistency
8.3 The AllDifferent constraint
8.4 Changing dynamically the improvement step with a
SearchMonitor
8.5 Summary
Part III
Routing
CHAPTER
NINE
The third part of this manual deals with Routing Problems: we have a graph1 and seek to nd
a set of routes covering some or all nodes and/or edges/arcs while optimizing an objective
function along the routes2 (time, vehicle costs, etc.) and respecting certain constraints (number
of vehicles, goods to pickup and deliver, xed depots, capacities, clients to serve, time windows,
etc.).
To solve these problems, the or-tools offers a dedicated Constraint Programming sub-library:
the Routing Library (RL).
The next three chapters each deal with one of three broad categories of Routing Problems:
Chapter 9 deals with Node Routing Problems where nodes must to be visited and served.
Chapter 10 deals with Vehicle Routing Problems where vehicles serve clients along the
routes.
Chapter 11 deals with Arc Routing Problems where arcs/edges must be visited and served.
These three categories of problems share common properties but they all have their own
paradigms and scientic communities.
In this chapter, well discover the RL with what is probably the most studied problem in Operations Research: the Travelling Salesman Problem (TSP)3 .
We use the excellent C++ ePiX library4 to visualize TSP solutions in TSPLIB format and
TSPTW solutions in Lpez-Ibez-Blum and da Silva-Urrutia formats.
1
A graph = (, ) is a set of vertices (the set ) connected by edges (the set ). A directed edge is called
an arc. When we have capacities on the edges, we talk about a network.
2
The transportation metaphor is helpful to visualize the problems but the class of Routing Problems is much
broader. The Transportation Problem for instance is really an Assignment Problem. Networks can be of any type:
telephone networks (circuit switching), electronic data networks (such as the internet), VLSI (the design of chips),
etc.
3
We use the Canadian (and British, and South African, and...) spelling of the verb travelling but youll nd
much more scientic articles under the American spelling: traveling.
4
A
The ePiX library uses the TEX/LTEX engine to create beautiful graphics.
Overview:
We start this chapter by presenting in broad terms the different categories of Routing Problems and describe the Routing Library (RL) in a nutshell. Next, we introduce the Travelling
Salesman Problem (TSP) and the TSPLIB instances. To better understand the RL, we say a
few words about its inner working and the CP model we use. Because most of the Routing
Problems are intractable, we use Local Search. We explain our two phases approach in details
and show how to model the TSP in a few lines. Finally, we model and solve the TSP with Time
Windows.
Prerequisites:
TSPLIB
keywords
and
the
218
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The 1/ /
Scheduling Problem
...
5
You can stop reading now if you want: this section involves neither Constraint Programming nor the or-tools
library.
6
From time to time, an article is published to propose a good classication but none has been adopted by the
community so far. See [Eksioglu2009] for instance.
7
Some people may actually disagree with the terms used in this manual.
8
Although Scheduling Problems and Routing Problems are not solved with the same techniques. See
[Prosser2003] for instance.
219
ture!
220
Node Routing Problems might even describe problems unrelated to Routing Problems in the scientic litera-
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
221
Authoritative source:
Golden, Bruce L.; Raghavan, S.; Wasil, Edward A. (Eds.). The Vehicle Routing Problem: Latest Advances and New Challenges. Springer, Series: Operations Research/Computer Science
Interfaces Series, Vol. 43, 2008, 589 p.
The CVRP:
The Capacitated Vehicle Routing Problem is...
[insert epix graphic]
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
9.2.1 Objectives
The objectives of the RL are to
model and solve generic routing problems out of the box;
provide modelling and solving blocks that can easily be reused;
make simple models simple to model;
allow extensibility.
In short, we provide specialized primitives that you can assemble and customize to your needs.
You can thus use the full power of the CP Solver and extend your models using the numerous
available constraints.
The RoutingModel class by itself only uses IntVars to model Routing Problems.
223
This is equivalent to calling the program with the gag routing_first_solution set to
PathCheapestArc:
./my_beautiful_routing_algorithm
--routing_first_solution=PathCheapestArc
9.2.6 Dimensions
Often, real problems need to take into account some accumulated quantities along (the edges
and/or the nodes of) the routes. To model such quantities, the RL proposes the concept of
dimensions. A dimension is basically a set of variables that describe some quantities (given
by callbacks) accumulated along the routes. These variables are associated with each node of
the graph. You can add as many dimensions as you wish in an automated and easy fashion:
just call the appropriate AddDimension() method(s) and the RL creates and manages these
variables automatically.
You can add upper bounds (we develop this concept later) on a dimension and a capacity limits
per route/vehicle on accumulated quantities for a given dimension.
Examples of dimensions are weight or volume carried, distance and time.
9.2.7 Disjunctions
Nodes dont have to be visited, i.e. some nodes can be optional. For this, the RL uses the
struct Disjunction which is basically a set of nodes. In our model, we visit at most one
224
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
node in each Disjunction. If these sets are singletons, then you have optional nodes. You
can also force to visit at least one node in each or some of the Disjunctions.
Again, we have automated and simplied (and optimized!) the process to create these sets: just
call the appropriate AddDisjunction() method(s).
9.2.10 Costs
Basically, costs are associated (with callbacks) to each edge/arc (i,j) and the objective function
sums these costs along the different routes in a solution. Our goal is to minimize this sum. The
RL let you easily add some penalties to for instance non-visited nodes, add some cost to use a
particular vehicle, etc. Actually, you are completely free to add whatever terms to this sum.
9.2.11 Limitations
There are several limitations10 as in any code. These limitations are mainly due to coding
choices and can often be worked around. We list the most important ones.
Only one model
We wrote several times that there is no universal solver11 for all the problems. This is of course
also true for the RL. We use a node-based model to solve quite a lot of different problems
but not all Routing Problems can be solved with the RL. In particular, common Arc Routing
Problems are probably best solved with a different model12 .
10
225
Number of nodes
The RoutingModel class has a limit on the maximum number of nodes it can handle13 .
Indeed, its constructors take an regular int as the number of nodes it can model:
RoutingModel(int nodes, ...);
226
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
and the TSPLIB which stands for the TSP library and is a library of sample instances for
the TSP (and related problems) from various origins and of various types. To read TSPLIB
data, we have implemented our own TSPData class as none of the available source code are
compatible with our licence. Feel free to use it! Finally, we like to visualize what we are
doing. To do so, we use the excellent ePiX library through our TSPEpixData class.
16
The record at the time of writing is the pla85900 instance in Gerd Reinelts TSPLIB. This instance is a
VLSI application with 85 900 nodes. For many other instances with millions of nodes, solutions can be found that
are guaranteed to be within 1% of an optimal tour!
17
At least for now and if you try to solve them to optimality.
227
The instance le
The TSPLIB not only deals with the TSP but also with related problems. We only detail one
type of TSP instance les. This is what the le a280.tsp18 looks like:
NAME : a280
COMMENT : drilling problem (Ludwig)
TYPE : TSP
DIMENSION: 280
EDGE_WEIGHT_TYPE : EUC_2D
NODE_COORD_SECTION
1 288 149
2 288 129
18
The le a280.tsp actually contains twice the same node (node 171 and 172 have the same coordinates)
but the name and the dimension have been kept. This is the only known defect in the TSPLIB.
228
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
3 270
4 256
5 256
6 246
...
EOF
133
141
157
157
Some of the attributes dont need any explanation. The TYPE keyword species the type of
data. We are only interested in:
TSP: Data for the symmetric TSP;
ATSP: Data for the asymmetric TSP and
TOUR: A collection of tours (see next subsection below).
DIMENSION is the number of nodes for the ATSP or TSP instances. EDGE_WEIGHT_TYPE
species how the edge weight are dened. In this case (EUC_2D), it is the Euclidean distance in
the plane. Several types of distances are considered. The NODE_COORD_SECTION keyword
starts the node coordinates section. Each line is made of three numbers:
Node_id x y
Node_id is a unique integer ( 1) node identier and (x,y) are Cartesian coordinates unless
otherwise stated. The coordinates dont have to be integers and can be any real numbers.
Not all instances have node coordinates.
There exist several other less obvious TSPLIB formats but we disregard them in this manual
(graphs can be given by different types of explicit matrices or by edge lists for example). Note
however that we take them into account in the code.
You might wonder how the depot is given. It is nowhere written where to start a tour. This is
normal because the TSP is not sensitive to the starting node: you can start a tour anywhere, the
total cost of the tour remains the same.
The solution le
Solution les are easier to deal with as they only contain tours. Every tour, called a sub-tour,
is a list of integers corresponding to the Node ids ended by -1.
This is what the le a280.opt.tour containing an optimal tour looks like:
NAME : ./TSPLIB/a280.tsp.opt.tour
TYPE : TOUR
DIMENSION : 280
TOUR_SECTION
1
2
242
243
...
279
3
229
280
-1
Since this le contains an optimal tour, there are no sub-tours and the list of integers contains
only one -1 at the end of the le.
Several scenarii are possible. With reference counting, when more than one pointer refer to an object,
it is only when the last pointer referring to the object is destroyed that the the object itself is destroyed. If
you want to know more about this helpful technique, look up RAII (Resource Acquisition Is Initialization).
method. It parses a le in TSPLIB format and loads the coordinates (if any) for further treatment. Note that the format is only partially checked: bad inputs might cause undened behaviour.
230
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
If during the parse phase an unknown keyword is encountered, the method exists and prints a
FATAL LOG message:
Unknown keyword: UNKNOWN
This method has been tested with almost all the les of the TSPLIB and should hopefully read
any correct TSPLIB format for the TSP.
To generate random TSP
To generate random TSP instances, the TSPData class provides the
RandomInitialize(const int size);
Assignment * solution,
std::string & epix_filename);
std::string & tpslib_solution_filename,
std::string & epix_filename);
The rst method takes an Assignment while the second method reads the solution from a
TSPLIB solution le.
You can dene the width and height of the generated image:
DEFINE_int32(epix_width, 10, "Width of the pictures in cm.");
DEFINE_int32(epix_height, 10, "Height of the pictures in cm.");
Once the ePiX le is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp
231
9.4. The model behind the scenes: the main decision variables
You can also print the node labels with the ag:
DEFINE_bool(tsp_epix_labels, false, "Print labels or not?");
For your (and our!) convenience, we wrote the small program tsplib_solution_to_epix. Its
implementation is in the le tsplib_solution_to_epix.cc. To use it, invoke:
./tsplib_solution_to_epix TSPLIB_data_file TSPLIB_solution_file >
epix_file.xp
232
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
next_node_index is the int64 index of the node following immediately the int64
current_node in the Assignment solution.
Before we present the main decision variables of our model, we need to understand the difference between NodeIndex node identiers and int64 indices representing nodes in solutions.
Not every node, only the nodes that lead somewhere in the solution. Keep reading.
Remember that we dont allow a node to be visited more than once, i.e. only one vehicle can visit a node in
a solution.
21
This sub-section is a simplied version of the section The auxiliary graph from the chapter Under the hood.
20
233
9.4. The model behind the scenes: the main decision variables
1
1
0
5
3
1
0
1
0
Starting depot
1
0Ending depot
1
0
1
0
You can of course number (or name) the nodes of the original graph any way you like. For
instance, in the TSPLIB, nodes are numbered from 1 to . In the RL, you must number your
original nodes from 0 to 1. If you dont follow this advice, you might get some surprises!
Always use NodeIndexes from 0 to 1 for your original graph!
There are nine nodes of which two are starting depots (1 and 3), one is an ending depot (7) and
one is a starting and ending depot (4). The NodeIndexes22 range from 0 to 8.
In this example, we take four vehicles/routes:
route 0: starts at 1 and ends at 4
route 1: starts at 3 and ends at 4
route 2: starts at 3 and ends at 7
route 3: starts at 4 and ends at 7
The auxiliary graph is obtained by keeping the transit nodes and adding a starting and ending
depot for each vehicle/route if needed like in the following gure:
1
5
6
1
04
1
0
1
0
1
0
1
0
1
70
1
0
1
0
1
0
1
0
Starting depot
1
0Ending depot
1
0
1
0
Node 1 is not duplicated because there is only one route (route 0) that starts from 1. Node 3 is
duplicated once because there are two routes (routes 1 and 2) that start from 3. Node 7 has been
duplicated once because two routes (routes 2 and 3) end at 7 and nally there are two added
copies of node 4 because two routes (routes 0 and 1) end at 4 and one route (route 3) starts from
4.
22
We should rather say NodeIndices but we pluralize the type name NodeIndex.
Note
also that the NodeIndex type lies inside the RoutingModel class, so we should rather use
RoutingModel::NodeIndex.
234
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The way these nodes are numbered doesnt matter for the moment. For our example, the next
gure shows this numbering:
1
5
3
6
8
9
1
0
1
0 4
1
0
10 0
1
1
0
1
11 0
1
0
1
012
1
0
1
0
Starting depot
1
0Ending depot
1
0
1
0
Note that the int64 indices dont depend on a given solution but only on the given
graph/network and the depots.
What is an auxiliary graph?
An auxiliary graph is a graph constructed from the original graph. It helps to model a problem. In our case, the auxiliary graph allows us to model different routes. Well meet other
auxiliary graphs in the chapter Arc Routing Problems with constraints: the Cumulative
Chinese Postman Problem.
Behind the scene, a static_cast is triggered. If you are following, youll understand that
RoutingModel::NodeIndex node = 12;
Have a look at base/int-type.h if you want to know more about the IntType class.
235
9.4. The model behind the scenes: the main decision variables
They are quicker and safer than a static_cast and ... give the correct results!
Try to avoid RoutingModel::NodeIndex::value() unless really necessary.
We have used the IsEnd(int64) method as condition to exit the for loop. This method
returns true if the int64 index represent an end depot. The RoutingModel class provides
also an IsStart(int64) method to identify if an int64 index corresponds to the start of
a route.
To access the main decision IntVar variables, we use the NextVar(int64) method.
236
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
0
Var 3
5
Var 6
Var 4
Path p0
4
Var 5
6
7
2
Path p1 3
8
NodeIndex : 0 . . . 8
Var (IntVar): 0 . . . 6
int64 : 0 . . . 8
Because nodes 5 and 8 are ending nodes, there is no nexts_ IntVar attached to them.
The solution depicted is:
Path 0 : 1 -> 0 -> 2 -> 3 -> 5
Path 1 : 7 -> 4 -> 6 -> 8
If we look at the internal int64 indices, we have:
Path 0 : 1 -> 0 -> 2 -> 3 -> 7
Path 1 : 6 -> 4 -> 5 -> 8
There are actually 9 int64 indices ranging from 0 to 8 because in this case there is no need to
duplicate a node. As you can see in the picture, there are only 7 nexts_ IntVar variables.
The following code:
LG << "Crash: " << Solution->Value(routing.NextVar(routing.End(0)));
As you can see, there is no internal control on the int64 index you can give to methods. If
you want to know more about the way we internally number the indices, have a look at subsection 14.11.2. Notice also that the internal int64 index of the node with NodeIndex 6
is... 5 and the int64 index of the node with NodeIndex 7 is...6!
9.4.6 To summarize
Here is a little summary:
237
What
True node Ids
Indices to follow
routes
Types
NodeIndex
int64
Comments
Unique for each original node from 0 to 1.
Not unique for each original node. Could be
bigger than 1 for the starting or ending node
of a route.
Internally, the RL uses int64 indices and duplicates some nodes if needed (the depots). The
main decision variables are IntVar only attached to internal nodes that lead somewhere. Each
variable has the whole range of int64 indices as domain24 .
To follow a route, use int64 indices. If you need to deal with the corresponding nodes, use
the NodeIndex IndexToNode(int64) method. The int64 index corresponding to the
rst node of route k is given by:
int64 first_node = routing.Start(k);
You can also test if an int64 index is the beginning or the ending of a route with the methods
bool IsStart(int64) and bool IsEnd(int64).
In a solution, to get the next int64 index next_node of a node given by an int64 index
current_node, use:
int64 next_node = solution->Value(routing.NextVar(current_node));
238
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
RoutingModel routing(...);
Solver* const solver = routing.solver();
Most desirable features for an RL are directly accessible through the RoutingModel class
though. The accessors (getters and setters) will be discussed throughout the third part of this
manual. But it is good to know that, as a last resort, you have a complete access (read control)
to the internals of the RL.
Basically, two constructors are available depending on the number of depots:
if there is only one depot:
// 42 nodes and 7 routes/vehicles
RoutingModel routing(42, 7);
// depot is node with NodeIndex 5
routing.SetDepot(5);
Note that the space between the two ending > in:
std::vector<std::pair<RoutingModel::NodeIndex,
RoutingModel::NodeIndex> > depots(2);
is mandatory.
9.5.2 Variables
Basically, there are two type of variables:
Path variables: the main decision variables and additional variables to describe the different routes and
Dimension variables: these variables allow to add side constraints like time-windows,
capacities, etc. and denote some quantities (the dimensions) along the routes.
From now on in this section, we only use the internal int64 indices except if the indices are
explicitly of type NodeIndex. This is worth a warning:
For the rest of this section, we only use the internal int64 indices except if the
indices are explicitly of type RoutingModel::NodeIndex.
239
Path variables
Path variables describe the different routes. There are three types of path variables that can be
accessed with the following methods:
NextVar(i): the main decision variables. NextVar(i) == j is true if j is the
node immediately reached from node i in the solution.
VehicleVar(i): represents the vehicle/route index to which node i belongs in the
solution.
ActiveVar(i): a Boolean variable that indicates if a node i is visited or not in the
solution.
Main decision variables
You can access the main variables with the method NextVar(int64):
IntVar* var = routing.NextVar(42);
var is a pointer to the IntVar corresponding to the node with the int64 42 index. In a
solution solution, the value of this variable gives the int64 index of the next node visited
after this node:
Assignment * const solution = routing.Solve();
...
int64 next_node = solution.Value(var);
Vehicles
Different routes/vehicles service different nodes. For each node i, VehicleVar(i) represents the IntVar* that represents the int index of the route/vehicle servicing node i in the
solution:
int route_number = solution->Value(routing.VehicleVar(i));
240
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
A node doesnt have to be visited. Nodes can be optional or part of a Disjunction, i.e. part
of a subset of nodes out of which at most one node can be visited in a solution.
ActiveVar(i) returns a boolean IntVar* (a IntVar variable with a {0, 1} domain)
indicating if the node i is visited or not in the solution. The way to describe a node that is not
visited is to make its NextVar(i) points to itself. Thus, and again with an abuse of notation,
we have:
ActiveVar(i) == (NextVar(i) != i).
Well discuss Disjunctions and optional nodes in details in section 11.4 when we will
transform a Cumulative Chinese Postman Problem (CCPP) into a Generalized TSP (GTSP). A
GTSP is similar to a TSP except that you have clusters of nodes you want to visit, i.e. you only
want to visit 1 node in each cluster.
Dimension variables
Dimension variables are used to accumulate quantities (or dimensions) along the routes. To denote a dimension, we use an std::string d. There are three types of dimension variables:
CumulVar(i, d): variables representing the quantity of dimension d when arriving
at the node i.
TransitVar(i, d): variables representing the quantity of dimension d added after
visiting the node i.
SlackVar(i, d): non negative slack variables such that (with the same abuse of
notation as above):
if NextVar(i) == j then CumulVar(j) = CumulVar(i) +
TransitVar(i) + SlackVar(i).
For a time dimension, you can think of waiting times.
You can add as many dimensions as you want25 .
The transit values can be constant, dened with callbacks, vectors or matrices. You can represent any quantities along routes with dimensions but not only. For instance, capacities and
time windows can be modelled with dimensions. Well play with dimensions at the end of
this chapter when well try to solve The Travelling Salesman Problem with Time Windows in
or-tools.
9.5.3 Constraints
In addition to the basics constraints that we discussed in the previous sub-section, the RL uses
constraints to avoid cycles, constraints to model the Disjunctions and pick-up and delivery
constraints.
25
241
No cycle constraint
One of the most difcult constraint to model is a constraint to avoid cycles in the solutions.
For one tour, we dont want to revisit some nodes. Often, we get partial solutions like the one
depicted on gure (a):
(a)
(b)
It is often easy to obtain optimal solutions when we allow cycles (like in gure (a)) but difcult
to obtain a real solution (like in gure (b)), i.e. without cycles. Several constraints have been
proposed in the scientic literature, each with its cons and pros. Sometimes, we can avoid
this constraint by modelling the problem in such a way that only solutions without cycles can
be produced but then we have to deal with huge and often numerically (and theoretically26 )
unstable models.
In the RL, we use our dedicated NoCycle constraint (dened in
constraint_solver/constraints.cc) in combination with an AllDifferent
constraint on the NextVar() variables. The NoCycle constraint is implicitly added to the
model.
The NoCycle constructor has the following signature:
NoCycle(Solver* const s,
const IntVar* const* nexts,
int size,
const IntVar* const* active,
ResultCallback1<bool, int64>* sink_handler,
bool owner,
bool assume_paths);
We will not spend too much time on the different arguments. The nexts and active arrays
are what their names imply. The sink_handler is just a callback that indicates if a node is
a sink or not. Sinks represent the depots, i.e. the nodes where paths start and end.
The bool owner allows the solver to take ownership of the callback or not and the bool
assume_paths indicates if we deal with real paths or with a forest (paths dont necessarily
end) in the auxiliary graph.
The constraint essentially performs two actions:
26
242
For the specialists: for instance, primal and dual degenerate linear models.
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
where nodes represents the group of nodes. This constraint is equivalent to:
ActiveVar() = 1.
Disjunction
You might want to use optional Disjunctions, i.e. a group of nodes out of which at most
one node can be visited. This time, use:
void AddDisjunction(const std::vector<NodeIndex>& nodes,
int64 penalty);
ActiveVar() = 1
Disjunction
where p is a boolean variable corresponding to the Disjunction and the objective function
has an added (p * penalty) term. If none of the variables in the Disjunction is vis
ited ( Disjunction ActiveVar() = 0), p must be equal to one and the penalty is added to the
objective function.
To be optional, the penalty penalty attributed to the Disjunction must be non-negative
(
0), otherwise the RL uses a simple Disjunction, i.e. exactly one node in the
Disjunction will be visited in the solutions.
Pick-up and delivery constraints
These constraints ensure that two nodes belong to the same route. For instance, if nodes i and
j must be visited/delivered by the same vehicle, use:
void AddPickupAndDelivery(NodeIndex i, NodeIndex j);
Whenever you have an equality constraint linking the vehicle variables of two nodes, i.e. you
want to force the two nodes to be visited by the same vehicle, you should add (because it speeds
up the search process!) the PickupAndDelivery constraint:
243
The RL solver tries to minimize this obj variable. The value of the objective function is the
sum of:
the costs of the arcs in each path;
a xed cost of each route/vehicle;
the penalty costs for not visiting optional Disjunctions.
We detail each of these costs.
27
Actually, only an AllDifferent constraint on the NextVars is added in the constructor of the
RoutingModel class. This constraint reinforces the fact that you cannot visit a node twice.
244
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
What follows is clearly C++ jargon. Basically, lets say that you need a method or a function
that returns the distances of the arcs. To pass it as argument to the SetCost() method, wrap it in a
NewPermanentCallback() call.
245
This int64 cost will only be added for each route that contains at least one visited node, i.e.
a different node than the start and end nodes of the route.
A penalty cost for missed Disjunctions
We have already seen the penalty costs for optional Disjunctions above. The penalty cost
is only added to the objective function for a missed Disjunction: the solution doesnt
visit any node of the Disjunction. If the given penalty cost is negative for an optional
Disjunction, this Disjunction becomes mandatory and the penalty is set to zero. The
penalty cost can be zero for optional Disjunction and you can model optional nodes by
using singletons for each Disjunction.
Different types of vehicles
The cost for the arcs and the used routes/vehicles can be customized for each route/vehicle.
To customize the costs of the arcs, use:
void SetVehicleCost(int vehicle, NodeEvaluator2* evaluator);
Lower bounds
You can ask the RL to compute a lower bound on the objective function of your routing model
by calling:
int64 RoutingModel::ComputeLowerBound();
246
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
1
5
4
(a)
5
(b)
On the left (gure (a)), we have an original graph with two depots: a starting depot 1 and an
ending depot 5 and three transit nodes 2, 3 and 4. On the right (gure (b)), we have a bipartite
graph29 with the same number of left and right nodes. The cost on an arc (l,r) is the real
transit cost from l to r. The Linear Assignment Problem consists in nding a perfect matching
of minimum cost, i.e. a bijection along the arcs between the two sets of nodes of the bipartite
graph for a minimum cost. On gure (b), such an optimal solution is depicted in thick blue
dashed lines. As is the case here, this solution doesnt necessarily produce a (set of) closed
route(s) from a starting depot to an ending depot.
The routing model must be closed before calling this method.
Routing Problems with node disjunction constraints (including optional nodes) and
non-homogenous costs are not supported yet (the method returns 0 in these cases).
If your model is linear, you also can use the linear relaxation of your model. We will explore
these and other lower bounds in section 11.7 when well try to solve the Cumulative Chinese
Postman Problem.
9.5.5 Miscellaneous
We discuss here several improvements and conveniences of the RL.
Cache
[TO BE WRITTEN]
Light constraints
To speed up the search, it is sometimes better to only propagate on the bounds instead of the
whole domains for the basic constraints. These light constraints are checking constraints,
only triggered on WhenBound() events. They provide very little (or no) domain ltering.
Basically, these constraints ensure that the variables are respecting the equalities of the basic
constraints. They only perform bound reduction on the variables when these variables are
bound.
You can trigger the use of these light constraints with the following ag:
29
This bipartite graph is not really the one used by the CP solver but its close enough to get the idea.
247
DEFINE_bool(routing_use_light_propagation, false,
"Use constraints with light propagation in routing model.");
When false, the RL uses the regular constraints seen in the previous parts of this manual.
Try it, sometimes you can get a serious speed up. These light constraints are especially useful
in Local Search.
Locks
Often during the search, you nd what appears to be good sub-solutions, i.e. partial routes that
seem promising and that you want to keep xed for a while during the search. This can easily
be achieved by using locks.
A lock is simply an std::vector<int64> that represents a partial route. Using this lock
ensures that
NextVar(lock[i]) == lock[i+1]
is true in the current solution. We will use locks in section 11.6 when we will try to solve the
Cumulative Chinese Postman Problem.
248
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
return ...;
}
int main(int argc, char **argv) {
RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle
TSP.SetCost(NewPermanentCallback(MyCost));
const Assignment * solution = TSP.Solve();
// Solution inspection
if (solution != NULL) {
std::cout << "Cost: " << solution->ObjectiveValue() << std::endl;
for (int64 index = TSP.Start(0); !TSP.IsEnd(index);
index = solution->Value(TSP.NextVar(index))) {
std::cout << TSP.IndexToNode(index) << " ";
}
std::cout << std::endl;
} else {
std::cout << "No solution found" << std::endl;
}
return 0;
}
Given an appropriate cost function, a TSP can be modelled and solved in 3 lines:
RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle
TSP.SetCost(NewPermanentCallback(MyCost));
const Assignment * solution = TSP.Solve();
The cost function is given as a callback to the routing solver through its SetCost() method.
Other alternatives are possible and will be detailed in the next sections.
249
#include "tsp.h"
#include "tsp_epix.h"
base/join.h contains the StrCat() function that we use to concatenate strings. tsp.h
contains the denition and declaration of the TSPData class to read TSPLIB format instances
and write TSPLIB format solution les while tsp_epix.h contains the TSPEpixData
class to visualize TSP solutions. Under the hood, tsp.h includes the header tsplib.h that
gathers the keywords, distance functions and constants from the TSPLIB. You should consider
tsp.h and tsplib.h as one huge header le. tsp_epix.h is only needed if you want to
use the ePiX library to visualize TSP solutions. tsp_epix.h depends on tsp.h (and thus
tsplib.h).
Parameters
Several command line parameters are dened in the les tsp.h, tsplib.h, tsp_epix.h
and tsp.cc:
Files
tsp.h
Parameter
Description
deterministic_random_seed Use deterministic random
seeds or not?
use_symmetric_distances Generate a symmetric TSP
instance or not?
min_distance
Minimum allowed distance
between two nodes.
max_distance
Maximum allowed distance
between two nodes.
tsp_epix.h epix_width
Width of the pictures in cm.
epix_height
Height of the pictures in cm.
tsp.cc
tsp_size
Size of TSP instance. If 0,
must be read from a TSPLIB
le.
tsp_depot
The starting node of the tour.
tsp_data_file
Input le with TSPLIB data.
tsp_distance_matrix_file Output le with distance matrix.
tsp_width_size
Width size of elds in output
les.
tsp_solution_file
Output le with generated solution in TSPLIB format.
tsp_epix_file
ePiX solution le.
30
tsp_time_limit_in_ms
Time limit in ms, 0 means no
limit.
30
Default value
true
true
10
100
10
10
0
1
empty string
empty string
6
empty string
empty string
0
250
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
251
tsp_data.LoadTSPLIBFile(FLAGS_tsp_data_file);
} else {
google::ShowUsageWithFlagsRestrict(argv[0], "tsp");
exit(-1);
}
operations_research::TSP(tsp_data);
return 0;
}
We start by writing the usage message that the user will see if she doesnt know what to
do. Next, we declare a TSPData object that will contain our TSP instance. As usual, all the
machinery is hidden in a function declared in the operations_research namespace:
TSP().
The TSP() function
We only detail the relevant parts of the TSP() function. First, we create the CP solver:
const int size = data.Size();
RoutingModel routing(size, 1);
routing.SetCost(NewPermanentCallback(&data, &TSPData::Distance));
The constructor of the RoutingModel class takes the number of nodes (size) and the number of vehicle (1) as parameters. The distance function is encoded in the TSPData object
given to the TSP() function.
Next, we dene some parameters:
// Disabling Large Neighborhood Search, comment out to activate it.
routing.SetCommandLineOption("routing_no_lns", "true");
if (FLAGS_tsp_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_tsp_time_limit_in_ms);
}
Because Large Neighborhood Search (LNS) can be quite slow, we deactivate it.
To dene the depot, we have to be careful as, internally, the CP solver starts counting the nodes
from 0 while in the TSPLIB format the counting starts from 1:
if (FLAGS_start_counting_at_1) {
CHECK_GT(FLAGS_tsp_depot, 0) << " Because we use the " <<
"TSPLIB convention, the depot id must be > 0";
}
RoutingModel::NodeIndex depot(FLAGS_start_counting_at_1 ?
FLAGS_tsp_depot -1 : FLAGS_tsp_depot);
routing.SetDepot(depot);
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
We use the method CheckSolution() of the TSPData class to ensure that the solution
returned by the CP Solver is valid. This method only checks if every node has been used only
once in the tour and if the objective cost matches the objective value of the tour.
Actually, when permitted, an arc (, ) with a distance is often replaced by a shortest path and its
value is the length of the shortest path between and . One drawback is that you have to keep in memory the
shortest paths used (or recompute them) but it is often more efcient than using the large value.
253
This method alters the existing distance matrix and replaces the distance of forbidden arcs by
the ag M:
DEFINE_int64(M, kint64max, "Big m value to represent infinity");
We have also dened a ag to switch between the two techniques and a ag for the percentage
of arcs to forbid randomly in the le tsp_forbidden_arcs.cc:
DEFINE_bool(use_M, false, "Use big m or not?");
DEFINE_int32(percentage_forbidden_arcs, 20,
"Percentage of forbidden arcs");
The code in RandomForbidArcs() simply computes the number of arcs to forbid and
uniformly tries to forbid arcs one after the other:
void RandomForbidArcs(const int percentage_forbidden_arcs)
CHECK_GT(size_, 0) << "Instance non initialized yet!";
Loosely speaking, the expression >>> max((, ) : , cities) means that is much much larger
that the largest distance between two cities.
254
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
Because our random number generator (as most random number generators) is not completely
random and uniform, we need to be sure to exit the while loop. This is why we introduce the
gag:
DEFINE_int32(percentage_forbidden_arcs_max, 94,
"Maximum percentage of arcs to forbid");
255
9.7.4 Filters
9.7.5 A Local Search heuristic for the TSP
256
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
although it restricts the search tree33 - renders the problem even more difcult in practice!
Indeed, the beautiful symmetry of the TSP34 (any permutation of cities is a feasible solution)
is broken and even the search for feasible solutions is difcult [Savelsbergh1985].
We present the TSPTW and two instances formats: the Lpez-Ibez-Blum and the da SilvaUrrutia formats. As in the case of the TSP, we have implemented a class to read those instances:
the TSPTWData class. We also use the ePix library to visualize feasible solutions using the
TSPTWEpixData class.
bi
si
time
In real application, the time spent at a client might be limited to the service. For instance, you
might wait in front of the clients ofce. Its common to consider that you start to service and
leave as soon as possible and this is our assumption in this chapter
33
34
257
Some authors ([Dash2010] for instance) assign two costs on the edges: a travel cost and a
travel time. While the travel times must respect the time windows constraints, the objective
value is the sum of the travel costs on the edges. In this chapter, we only have one cost on the
edges. The objective value and the real travel time are different: you might have to wait before
servicing a client.
Often, some conditions are applied to the time windows (in theory or practice). The only
condition35 we will impose is that , N, i.e. we impose that the bounds of the time
windows must be non negative integers. This also implies that the time windows and the
servicing times are nite.
The practical difculty of the TSPTW is such that only instances with about 100 nodes have
been solved to optimality36 and heuristics rarely challenge instances with more than 400 nodes.
The difculty of the problem not only depends on the number of nodes but also on the quality
of the time windows. Not many attempts can be found in the scientic literature about exact or
heuristic algorithms using CP to solve the TSPTW. Actually, not so many attempts have been
successful in solving this difcult problem in general. The scientic literature on this problem
is hence scarce.
We refer the interested reader to the two web pages cited in the next sub-section for some
relevant literature.
This condition doesnt hold in Rodrigo Ferreira da Silva and Sebastin Urrutias denition of a TSPTW. In
their article, they ask for (at least theoretically) , , R+ , i.e. non negative real numbers and
.
36
Instances with more than 100 nodes have been solved to optimality but no one - at least to the best of our
knowledge at the time of writing - can systematically solve to optimality instances with more than 40 nodes...
258
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
21
0 19 17 34 7 20 10 17 28 15 23 29 23 29 21 20 9 16 21 13 12
19 0 10 41 26 3 27 25 15 17 17 14 18 48 17 6 21 14 17 13 31
17 10 0 47 23 13 26 15 25 22 26 24 27 44 7 5 23 21 25 18 29
34 41 47 0 36 39 25 51 36 24 27 38 25 44 54 45 25 28 26 28 27
7 26 23 36 0 27 11 17 35 22 30 36 30 22 25 26 14 23 28 20 10
20 3 13 39 27 0 26 27 12 15 14 11 15 49 20 9 20 11 14 11 30
10 27 26 25 11 26 0 26 31 14 23 32 22 25 31 28 6 17 21 15 4
17 25 15 51 17 27 26 0 39 31 38 38 38 34 13 20 26 31 36 28 27
28 15 25 36 35 12 31 39 0 17 9 2 11 56 32 21 24 13 11 15 35
15 17 22 24 22 15 14 31 17 0 9 18 8 39 29 21 8 4 7 4 18
23 17 26 27 30 14 23 38 9 9 0 11 2 48 33 23 17 7 2 10 27
29 14 24 38 36 11 32 38 2 18 11 0 13 57 31 20 25 14 13 17 36
23 18 27 25 30 15 22 38 11 8 2 13 0 47 34 24 16 7 2 10 26
29 48 44 44 22 49 25 34 56 39 48 57 47 0 46 48 31 42 46 40 21
21 17 7 54 25 20 31 13 32 29 33 31 34 46 0 11 29 28 32 25 33
20 6 5 45 26 9 28 20 21 21 23 20 24 48 11 0 23 19 22 17 32
9 21 23 25 14 20 6 26 24 8 17 25 16 31 29 23 0 11 15 9 10
16 14 21 28 23 11 17 31 13 4 7 14 7 42 28 19 11 0 5 3 21
21 17 25 26 28 14 21 36 11 7 2 13 2 46 32 22 15 5 0 8 25
13 13 18 28 20 11 15 28 15 4 10 17 10 40 25 17 9 3 8 0 19
12 31 29 27 10 30 4 27 35 18 27 36 26 21 33 32 10 21 25 19 0
0
408
62
68
181
205
306
324
214
217
51
61
102
129
175
186
250
263
3
23
21
49
79
90
78
96
140
154
354
386
42
63
2
13
24
42
20
33
9
21
275
300
The rst line contains the number of nodes, including the depot. The n20w20.001 instance
has a depot and 20 nodes. The following 21 lines represent the distance matrix. This distance
typically represents the travel time between nodes and , plus the service time at node . The
distance matrix is not necessarily symmetrical. The last 21 lines represent the time windows
(earliest, latest) for each node, one per line. The rst node is the depot.
When then sum of service times is not 0, it is specied in a comment on the last line:
259
16.75 391
CUST NO. XCOORD. YCOORD. DEMAND [READY TIME] [DUE DATE] [SERVICE TIME]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
999
16.00
22.00
12.00
47.00
11.00
25.00
22.00
0.00
37.00
31.00
38.00
36.00
38.00
4.00
5.00
16.00
25.00
31.00
36.00
28.00
20.00
0.00
23.00
4.00
6.00
38.00
29.00
5.00
31.00
16.00
3.00
19.00
12.00
1.00
14.00
50.00
4.00
3.00
25.00
15.00
14.00
16.00
35.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
62.00
181.00
306.00
214.00
51.00
102.00
175.00
250.00
3.00
21.00
79.00
78.00
140.00
354.00
42.00
2.00
24.00
20.00
9.00
275.00
0.00
408.00
68.00
205.00
324.00
217.00
61.00
129.00
186.00
263.00
23.00
49.00
90.00
96.00
154.00
386.00
63.00
13.00
42.00
33.00
21.00
300.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
Having seen the same instance, you dont need much complementary info to understand this
format. The rst line of data (CUST NO. 1) represents the depot and the last line marks the
end of the le. As you can see, the authors are not really optimistic about solving instances
with more than 999 nodes! We dont use the DEMAND column and we round down the numbers
of the last three columns.
You might think that the translation from this second format to the rst one is obvious. It is
not! See the remark on Travel-time Computation on the Jeffrey Ohlmann and Barrett Thomas
benchmark page. In the code, we dont try to match the data between the two formats, so you
might encounter different solutions.
The same instances in the da Silva-Urrutia and the Lpez-Ibez-Blum formats
might be slightly different.
Solutions
We use a simple format to record feasible solutions:
260
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The objective value 378 is the sum of the costs of the arcs and not the time spent to travel
(which is 387 in this case).
A basic program check_tsptw_solutions.cc veries if a given solution is indeed feasible for a
given instance in Lpez-Ibez-Blum or da Silva-Urrutia formats:
./check_tsptw_solutions -tsptw_data_file=DSU_n20w20.001.txt
-tsptw_solution_file=n20w20.001.sol
This program checks if all the nodes have been serviced and if the solution is feasible:
bool IsFeasibleSolution() {
...
// for loop to test each node in the tour
for (...) {
// Test if we have to wait at client node
waiting_time = ReadyTime(node) - total_time;
if (waiting_time > 0) {
total_time = ReadyTime(node);
}
if (total_time + ServiceTime(node) > DueTime(node)) {
return false;
}
}
...
return true;
}
DueTime(node) true?
If not, the method returns false. If all the due times are respected, the method returns
true.
The output of the above command line is:
261
As you can see, the recorded objective value in the solution le is 378 while the value of the
computed objective value is 387. This is because the distance matrix computed is different
from the actual one really used to compute the objective value of the solution. We refer again
the reader to the remark on Travel-time Computation from Jeffrey Ohlmann and Barrett Thomas
cited above. If you use the right distance matrix as in the Lpez-Ibez-Blum format, you get:
TSPTW instance of type Lpez-Ibez-Blum format
Solution is feasible!
Loaded obj value: 378, Computed obj value: 378
Total computed travel time: 387
TSPTW file LIB_n20w20.001.txt (n=21, min=2, max=57, sym? yes)
Now both the given objective value and the computed one are equal. Note that the total travel
time is a bit longer: 387 for a total distance of 378.
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
int64 Horizon() const: returns the horizon of the instance, i.e. the maximal due
time;
int64 Distance(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const: returns the distance between the
two NodeIndexes;
RoutingModel::NodeIndex Depot() const: returns the depot. This the rst
node given in the instance and solutions les.
int64 ReadyTime(RoutingModel::NodeIndex i) const:
ready time of node i;
returns the
method.
This way, you can load solution les and test them with the bool
IsFeasibleSolution() method briey seen above. Actually, you should enquire if the
solution is feasible before doing anything with it.
Three methods help you deal with the existence/feasibility of the solution:
bool IsSolutionLoaded() const;
bool IsSolution() const;
bool IsFeasibleSolution() const;
With IsSolutionLoaded() you can check that indeed a solution was loaded/read from a
le. IsSolution() tests if the solution contains once and only once all the nodes of the
graph while IsFeasibleSolution() tests if the loaded solution is feasible, i.e. if all due
times are respected.
263
Once you are sure that a solution is valid and feasible, you can query the loaded solution:
int64 SolutionComputedTotalTravelTime() const: computes the total
travel time and returns it. The travel total time often differs from the objective value
because of waiting times;
int64 SolutionComputedObjective() const:
value and returns it;
int64 SolutionLoadedObjective() const:
stored in the instance le
These methods are also available if the solution was obtained by the solver (in this
case, SolutionLoadedObjective() returns -1 and IsSolutionLoaded() returns
false).
The TSPTWData class doesnt generate random instances. We wrote a little program for this
purpose.
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
By default, if the name of the instance is myInstance, tsptw_generator creates the three
les:
DSU_myInstance.txt;
LIB_myInstance.txt and
myInstance_init.sol.
myInstance_init.sol contains the random tour generated to create the instance. Files
with the same name are overwritten without mercy.
Assignment * solution,
std::string & epix_filename)
std::string & tpstw_solution_filename,
std::string & epix_filename);
The rst method takes an Assignment while the second method reads the solution from a
solution le.
You can dene the width and height of the generated image:
DEFINE_int32(epix_width, 10, "Width of the pictures in cm.");
DEFINE_int32(epix_height, 10, "Height of the pictures in cm.");
Once the ePiX le is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp
265
The dot in red in the center represents the depot or rst node. The arrows indicate the direction
of the tour. Because of the time windows, the solution is no longer planar, i.e. the tour crosses
itself.
You can also print the node labels and the time windows with the ags:
DEFINE_bool(tsptw_epix_labels, false, "Print labels or not?");
DEFINE_bool(tsptw_epix_time_windows, false,
"Print time windows or not?");
For your (and our!) convenience, we wrote a small program tsptw_solution_to_epix. Its
implementation is in the le tsptw_solution_to_epix.cc. To use it, invoke:
./tsptw_solution_to_epix TSPTW_instance_file TSPTW_solution_file >
epix_file.xp
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
data is an TSPTWData object with the instance details. To add a Dimension, we need to
compute the quantity that is added at each node. TSPTWData has a dedicated method to do
this:
int64 DistancePlusServiceTime(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const {
return Distance(from, to) + ServiceTime(from);
}
The astute reader will have noticed that there is a problem with the depot. Indeed, we want
to take the time to service the depot at the end of the tour, not the beginning. Fix the bool
fix_start_cumul_to_zero to true and the CumulVar() variable of the start node
of all vehicles will be set to 0.
To model the time windows of a node i, we simply bound the corresponding CumulVar(i)
variable:
267
We use the basic search strategy and turn off the large neighborhood search that can slow down
the overall algorithm:
routing.set_first_solution_strategy(
RoutingModel::ROUTING_DEFAULT_STRATEGY);
routing.SetCommandLineOption("routing_no_lns", "true");
Lets test this TSPTW solver on the following generated instance in da Silva-Urrutia format
(le DSU_test.tsptw):
!!
test
CUST NO.
1
2
3
4
5
999
XCOORD.
72.00
59.00
99.00
69.00
42.00
0.00
YCOORD.
22.00
3.00
8.00
46.00
72.00
0.00
DEMAND
0.00
0.00
0.00
0.00
0.00
0.00
READY TIME
0.00
197.00
147.00
242.00
56.00
0.00
DUE DATE
SERVICE TIME
504.00
216.00
165.00
254.00
67.00
0.00
2.00
2.00
9.00
3.00
9.00
0.00
We invoke:
./tsptw -instance_file=DSU_test.tsptw -solution_file=test.sol
and we obtain:
1 5 3 2 4
252
268
to
to
to
to
Nodes:
Releases:
Deadlines:
Services:
Durations:
Time:
4
4
2
2
1
1
3
56
56
147
147
197
197
242
67
67
165
165
216
216
254
9
9
9
9
2
2
3
58
9
86
9
40
2
44
58
67
153
162
202
204
248
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
serve
3
travel to
0
serve
0
Solution is feasible!
Obj value = 252
242
0
0
254
504
504
3
2
2
3
24
2
251
275
277
The reason is that the services times are added to the distances in this format.
check_tsptw_solution conrms this:
Actions:
Nodes:
travel to
serve
travel to
serve
travel to
serve
travel to
serve
travel to
serve
Solution is feasible!
Obj value = 277
4
4
2
2
1
1
3
3
0
0
Releases:
Deadlines:
Services:
Durations:
Time:
56
56
147
147
197
197
242
242
0
0
67
67
165
165
216
216
254
254
504
504
0
0
0
0
0
0
0
0
0
0
67
0
95
0
42
0
47
0
26
0
67
67
162
162
204
204
251
251
277
277
Real instances, like DSU_n20w20.001.txt, are out of reach for our basic tsptw. This is
mainly because nding a rst feasible solution is in itself a difcult problem. In the next subsection, well help the solver nding this rst feasible solution to start the local search.
9.10. Summary
9.10 Summary
summary
270
CHAPTER
TEN
Overview:
Prerequisites:
272
273
10.7 Summary
274
CHAPTER
ELEVEN
Overview:
Prerequisites:
Files:
276
Part IV
Technicalities
CHAPTER
TWELVE
UTILITIES
12.1 Logging
[TO BE REREAD]
We provide very basic logging tools: macros replaced by some basic logging objects. They are
dened in the header base/logging.h.
LG or LOG(INFO) is always working. You can print messages to std:cerr like this
LG << "This is my important message with " << var << " pancakes.";
Of course, var must overwrite the << operator. The message is automatically followed by a
\n that adds a new line.
If you didnt change the value of the gags ag log_prefix to false, youll see the following message:
[20:47:47] my_file.cc:42: This is my important message with 3 pancakes.
Your message is prexed by the hour, the le name and the line number of the code source
where your message was dened. You can disable this prex by setting log_prefix to
false.
We provide different levels of logging:
First, depending on the severity:
INFO;
WARNING;
ERROR;
FATAL.
To use them, just write LOG(severity) as in:
LOG(FATAL) << "This message will kill you!";
For the moment, INFO, ERROR and WARNING are treated the same way. FATAL works
as expected and the program aborts (calls abort()) after printing the message.
12.2. Asserting
Second, depending on the debug or release mode. When debugging, you can use
DLOG(severity) with the same levels (and the same results). If NDEBUG is dened,
you are in release mode and DLOG(severity) doesnt do anything except for FATAL
where it becomes a LOG(ERROR).
Finally, you can also use VLOG(level) with different levels. The higher the level, the
more detailed the information. By default, the level is set to 0. You can change this by
setting the right level value to the gags ag log_level.
So, if FLAGS_log_level = 1 the following message is printed:
VLOG(1) << "He, he, you can see me!";
12.2 Asserting
We provide several assert-like macros in the header base/logging.h.
Remember that the variable NDEBUG (NO DEBUG) is dened by the standard. By default,
the assert debugging mechanism dened in assert.h or the C++ equivalent cassert is on.
You have to explicitly turn it off by dening the variable NDEBUG.
Two types of assert-like macros are provided:
Debug-only checking and
Always-on checking.
Debug-only macros are only triggered in DEBUG mode (i.e. when the variable NDEBUG is not
dened) and start with the letter D. In NON DEBUG mode (the variable NDEBUG is dened), the
code inside Debug-only macros vanishes. Always-on macros are always on duty. For instance,
DCHECK(x) is Debug-only while CHECK() is Always-on.
Here are the macros listed:
280
Tests if
(x)
(x) >= (y)
(x) < (y)
(x) > (y)
(x) <= (y)
(x) == (y)
(x) != (y)
There is also the Always-on CHECK_NOTNULL(x) macro that tests if (x) != NULL.
are also dened. Note that these macros are always functional. If you prefer to use
safeguards that vanish in the release code, use their equivalent1 starting with a D:
DCHECK_LT(x, y), etc. and compile with the NDEBUG variable set to 1.
These macros are dened in the header logging.h:
12.3 Timing
We propose two timers: a basic timer (WallTimer) and a more advanced one
(CycleTimer). These two classes work under Windows, Linux and MacOS. The Solver
class uses by default a WallTimer internally.
Both timers are declared in the header base/timer.h.
281
12.3. Timing
Function
gettimeofday()
clock()
gettimeofday()
Function
clock_gettime()
QueryPerformanceCounter() and QueryPerformanceFrequency()
mach_absolute_time() and mach_timebase_info()
282
12.4 Proling
12.5 Debugging
12.6 Serializing
12.7 Visualizing
12.8 Randomizing
283
CHAPTER
THIRTEEN
MODELING TRICKS
Overview:
Prerequisites:
Classes under scrutiny:
Files:
13.1. Efciency
13.1 Efciency
13.1.1 Keep variables ordered
13.3.3 DecisionBuilders
SolveOnce
13.3.4 Decisions
NestedSolveDecision
13.3.5 Summary
286
CHAPTER
FOURTEEN
virtual
void
BeginNextDeci-
289
// Registers itself on the solver such that it gets notied of the search // and propagation events. virtual void Install();
Descriptions
CP Solver.
nodes_ (pr)
vehicles_ (pr)
start_end_count_
(pr)
kUnassigned (pu)
kNoPenalty (pu)
RoutingModel::
kFirstNode (pu)
RoutingModel::
kInvalidNodeIndex
(pu)
Size() (pu)
290
RoutingModel:: NodeIndex(-1)
Queries
Solver* solver()
const
int nodes() const
int vehicles()
const
None
kUnassigned
kNoPenalty
RoutingModel::
kFirstNode
RoutingModel::
kInvalidNodeIndex
Size()
(pu) stands for public and (pr) for private. The int64 Size() const method returns nodes_ + vehicles_ - start_end_count_, which is exactly the minimal number of variables needed to model the problem at hand with one variable per node (see next
subsection). kUnassigned is used for unassigned indices.
5
3
1
0
1
0
1
0
Starting depot
1
0Ending depot
1
0
1
0
There are nine nodes, two of which are starting depots (1 and 3), one is an ending depot (7) and
one is a starting and ending depot (4). The NodeIndexes range from 0 to 8.
There are start_end_count_ = 4 distinct depots (nodes 1, 3, 4 and 7) and nodes_ start_end_count_ = 5 transit nodes (nodes 0, 2, 5, 6 and 8).
In this example, we take four vehicles/routes:
route 0: starts at 1 and ends at 4
route 1: starts at 3 and ends at 4
route 2: starts at 3 and ends at 7
route 3: starts at 4 and ends at 7
Here is the code:
std::vector<std::pair<RoutingModel::NodeIndex,
RoutingModel::NodeIndex> > depots(4);
depots[0] = std::make_pair(1,4);
depots[1] = std::make_pair(3,4);
depots[2] = std::make_pair(3,7);
depots[3] = std::make_pair(4,7);
RoutingModel VRP(9, 4, depots);
The auxiliary graph is obtained by keeping the transit nodes and adding a starting and ending
depot for each vehicle/route if needed as shown in the following gure:
291
5
6
1
04
1
0
1
0
1
0
1
0
1
70
1
0
1
0
1
0
1
0
Starting depot
1
0Ending depot
1
0
1
0
Node 1 is not duplicated because there is only one route (route 0) that starts from 1. Node 3
is duplicated once because there are two routes (routes 1 and 2) that start from 3. Node 7 is
duplicated once because two routes (routes 2 and 3) end at 7 and nally there are two copies of
node 4 because two routes (routes 0 and 4) end at 4 and one route (route 3) starts from 4.
The number of variables is:
nodes_ + vehicles_ start_end_count_ = 9 + 4 4 = 9.
These nine variables correspond to all the nodes in the auxiliary graph leading somewhere, i.e.
starting depots and transit nodes in the auxiliary graph.
nexts_ variables
The main decision variables are IntVar* stored in an std::vector nexts_ and can be
accessed with the NextVar() method. The model uses one IntVar variable for each node
that can be linked to another node. If a node is the ending node of a route (and no route
starts from it), we dont use any NextVar() variable for that node. The minimal number of
nexts_ variables is:
nodes_ start_end_count_ + vehicles_
We need one variable for each node that is not a depot (nodes_ - start_end_count_)
and one variable for each vehicle (a starting depot: vehicles_).
Remember that the int64 Size() const method precisely returns this amount:
// Returns the number of next variables in the model.
int64 Size() const { return nodes_ + vehicles_ - start_end_count_; }
The domain of each IntVar is [0,Size() + vehicles_ - 1]. The end depots are
represented by the last vehicles_ indices.
292
5
3
6
8
9
1
0
1
0 4
1
0
10 0
1
1
0
1
11 0
1
0
1
012
1
0
1
0
Starting depot
1
0Ending depot
1
0
1
0
you get:
Number of nodes: 9
Number of vehicles: 4
Variable index 0 -> Node index 0
Variable index 1 -> Node index 1
Variable index 2 -> Node index 2
Variable index 3 -> Node index 3
Variable index 4 -> Node index 4
Variable index 5 -> Node index 5
Variable index 6 -> Node index 6
Variable index 7 -> Node index 8
Variable index 8 -> Node index 3
Variable index 9 -> Node index 4
Variable index 10 -> Node index 4
Variable index 11 -> Node index 7
Variable index 12 -> Node index 7
Node index 0 -> Variable index 0
Node index 1 -> Variable index 1
Node index 2 -> Variable index 2
293
Node
Node
Node
Node
Node
Node
index
index
index
index
index
index
3
4
5
6
7
8
->
->
->
->
->
->
Variable
Variable
Variable
Variable
Variable
Variable
index
index
index
index
index
index
3
4
5
6
-1
7
The variable indices are the int64 indices used internally in the RL. The Node Indexes
correspond to the unique NodeIndexes of each node in the original graph. Note that
NodeIndex 7 doesnt have a corresponding int64 index (-1 means exactly that) and that
NodeIndex 8 corresponds to int64 7 (not 8!).
Here is one possible solution:
1
5
6
3
1
04
1
0
1
0
1
0
1
0
1
70
1
0
1
0
1
0
1
0
Starting depot
1
0Ending depot
1
0
Starting and ending depot
Transit node
1
0
We output the routes, rst with the NodeIndexes and then with the internal int64 indices
with:
for (int p = 0; p < VRP.vehicles(); ++p) {
LG << "Route: " << p;
string route;
string index_route;
for (int64 index = VRP.Start(p); !VRP.IsEnd(index); index =
Solution->Value(VRP.NextVar(index))) {
route = StrCat(route,
StrCat(VRP.IndexToNode(index).value(), " -> "));
index_route = StrCat(index_route, StrCat(index, " -> "));
}
route = StrCat(route, VRP.IndexToNode(VRP.End(p)).value());
index_route = StrCat(index_route, VRP.End(p));
LG << route;
LG << index_route;
}
and get:
Route:
1 -> 0
1 -> 0
Route:
3 -> 5
3 -> 5
Route:
294
0
->
->
1
->
->
2
2 -> 4
2 -> 9
4
10
3 -> 6
8 -> 6
Route:
4 -> 8
4 -> 7
->
->
3
->
->
7
11
7
12
Some remarks
14.11.3 Variables
Path variables
Dimension variables
14.11.4 Constraints
NoCycle constraint
14.12 Summary
295
Part V
Apprendices
BIBLIOGRAPHY
Bibliography
[Jordan2009] Jordan and Brett. A survey of known results and research areas for n-queens,
Discrete Mathematics, Volume 309, Issue 1, 2009, pp 1-31.
[Garey1976] Garey, M. R., Johnson, D. S. and Sethi, R., The complexity of owshop and
jobshop scheduling, Mathematics of Operations Research, volume 1, pp 117-129, 1976.
[Kis2002] Kis, T., On the complexity of non-preemptive shop scheduling with two jobs, Computing, volume 69, nbr 1, pp 37-49, 2002.
[Taillard1993] Taillard, E., 1993. Benchmarks for basic scheduling problems, European Journal of Operational Research, Elsevier, vol. 64(2), pages 278-285, January.
[Adams1988] J. Adams, E. Balas, D. Zawack, The shifting bottleneck procedure for job shop
scheduling. Management Science, 34, pp 391-401, 1988.
[Christodes1976] Christodes, Nicos. Worst-case analysis of a new heuristic for the travelling salesman problem, Technical Report, Carnegie Mellon University, 388, 1976.
[Eksioglu2009] B. Eksioglu, A. Volkan Vural, A. Reisman, The vehicle routing problem: A
taxonomic review, Computers & Industrial Engineering, Volume 57, Issue 4, November
2009, Pages 1472-1483.
[Prosser2003] J. C. Beck, P. Prosser and E. Selensky, Vehicle Routing and Job Shop Scheduling: Whats the difference?, Proc. of the 13th International Conference on Automated Planning and Scheduling, 2003, pages 267276.
[Savelsbergh1985] M.W.P. Savelsbergh. Local search in routing problems with time windows,
Annals of Operations Research 4, 285305, 1985.
[Ferreira2010] R. Ferreira da Silva and S. Urrutia. A General VNS heuristic for the traveling
salesman problem with time windows, Discrete Optimization, V.7, Issue 4, pp. 203-211,
2010.
[Dash2010] S. Dash, O. Gnlk, A. Lodi, and A. Tramontani. A Time Bucket Formulation for
the Traveling Salesman Problem with Time Windows, INFORMS Journal on Computing,
v24, pp 132-147, 2012 (published online before print on December 29, 2010).
[Dumas1995] Dumas, Y., Desrosiers, J., Gelinas, E., Solomon, M., An optimal algorithm for
the travelling salesman problem with time windows, Operations Research 43 (2) (1995)
367-371.
300
INDEX
Symbols
FATAL, 279
cp_model_stats, 58
cp_no_solve, 57
cp_print_model, 57
cp_show_constraints, 57
help, 45, 57
helpmatch=S, 45
helpon=FILE, 45
helpshort, 45
A
AddConstraint(), 38
Assignment, 40
C
constraint
AllDifferent, 32
cpviz, 92
cryptarithmetic
puzzles, 30
D
DebugString(), 56
DecisionBuilder, 38
DEFINE_bool, 44
DEFINE_double, 44
DEFINE_int32, 44
DEFINE_int64, 44
DEFINE_string, 44
DEFINE_uint64, 44
DLOG, 279
DLOG_IF, 280
E
EndSearch(), 40
ERROR, 279
F
factory method, 35
gags, 44
log levels, 280
log prex, 279
parameters read from a le, 250
replacement
(routing.SetCommandLineOption()),
224
shortcuts, 45
types, 44
Golomb Ruler
Problem, 49
Golomb ruler, 51
I
INFO, 279
IntExpr, 36
IntVar, 35
L
LG, 279
LOG(ERROR), 279
LOG(FATAL), 279
LOG(INFO), 279
LOG(WARNING), 279
LOG_IF, 280
M
MakeAllDifferent(), 38
MakeAllSolutionCollector(), 40
MakeBestValueSolutionCollector(), 40
MakeDifference(), 62
MakeEquality(), 38
MakeFirstSolutionCollector(), 40
MakeIntConst(), 60
MakeIntVar(), 35
MakeIntVarArray(), 55
Index
MakeLastSolutionCollector(), 40
MakeLessOrEqual(), 65
MakeMinimize, 55
MakeNonEquality(), 62
MakePhase(), 38
MakeProd(), 36
MakeScalProd(), 37
MakeSum(), 36
MakeTimeLimit(), 45
N
n-queens problem, 74
namespace
operations_research, 34
NewSearch(), 39
NextSolution(), 39
O
objective functions, 49, 50
OptimizeVar, 55
P
ParseCommandLineFlags(), 44
Problem
Cryptarithmetic puzzles, 30
Golomb Ruler, 49
puzzles
cryptarithmetic, 30
S
SearchLimit, 60
in Local Search, 172
specialized for time, 46
SearchMonitor
as SolutionCollector, 40
as Solvers parameters, 46
callbacks, 85
SetCommandLineOption(), 224
SolutionCollector, 40
AllSolutionCollector, 40
BestValueSolutionCollector, 40
FirstSolutionCollector, 40
LastSolutionCollector, 40
Solve(), 41
Solver
creation, 35
parameters, 45
SolverParameters, 45
302
SolverParameters(), 45
StringPrintf(), 55
T
time
wall_time(), 59
V
variables
IntVar, 35
VLOG, 280
W
wall_time()
time, 59
WARNING, 279