Beruflich Dokumente
Kultur Dokumente
User’s Manual
Nikolaj van Omme
Laurent Perron
Vincent Furnon
License information
We kindly ask you not to make this document available on the Internet. This document should
only be available at the following address:
http://or-tools.googlecode.com/svn/trunk/documentation/
documentation_hub.html
This is the address of our documentation hub where you can find other useful sources of
documentation about the or-tools library.
All the cliparts used in this manual are public domain except the oxygen-style mimetypes icons
from the open icon library licensed under the GNU Lesser General Public License version 3.
Trademarks
Ackowledgments
We are glad to welcome you to the or-tools user’s manual. In this foreword, we try to answer
most common questions a newcomer could have when discovering this manual or the library
for the first 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 difficult it is sometimes to find efficient, 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 unified interface to several linear programming and mixed integer program-
ming solvers (CBC, CLP, GLOP, GLPK, Gurobi, SCIP and Sulum).
• Knapsack algorithms.
• Graph algorithms (shortest paths, min cost flow, max flow, linear sum assignment).
• FlatZinc support.
1
If you are curious: Wikipedia article on Operations research.
In short, the or-tools library is:
• Open source and free Everything, including the examples, the implementations of
the algorithms, the various documentations2 , is licenced under the Apache License
2.0 and is available for download. If you make substantial improvements to our code,
please share it with the whole community.
• Alive The library is actively maintained and updates and improvements are made on an
almost daily basis.
• Documented OK, we just started to write the documentation but there are already nu-
merous examples written in C++, Python, Java and C#!
• Portable Because it is made by Google, the code conforms strictly to the Google coding
styles3 . The code is known to compile on:
– gcc 4.4.x on ubuntu 10.04 and up (10.10, 11.04, 11.10 and 12.04).
– xcode >= 3.2.3 on Mac OS X Snow Leopard and Mac OS X Lion (gcc 4.2.1).
– Microsoft Visual Studio 10.
Both 32 bit and 64 bit architectures are supported, although the code is optimized to run
in 64 bit mode.
• Efficient All we can say is that we use it internally at Google.
• Accessible Everything is coded in C++ but is available through SWIG in Python, Java,
and .NET (using Mono on non-Windows platforms).
• User-friendly We try to make our code as easy to use as possible (especially in Python
and C#). Of course, there is a (small) learning curve to use our library but once you
master several basic concepts, it is quite straightforward to code with the or-tools library.
• Tested We use it internally at Google since a few years and the community of users is
growing.
This manual is intended to give you the necessary knowledge to use the library and explore
the reference manual by yourself. We describe the basic concepts but also how to customize
your search in Constraint Programming (CP). One of the strength of our library is its routing
solver in CP to solve node- and vehicle routing problems with constraints. We describe how to
customize your routing algorithms. After reading this manual, you will be able to understand
our way of coding and how to use the full potential of our library.
We detail the content of the manual in section 1.8.
2
The source code and the scripts used to generate the documentation will be available soon.
3
See for instance http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml for the Google C++ Style
Guide.
iv
What you will not learn in this document
You could read this document from cover to cover but we have put a lot of efforts to make
each chapter stands on its own. The best way to read this manual is to look for a specific
answer, use the index or the table of contents to find a reference to that information. If you are
missing some requirements to understand a section, you can always backtrack on prerequisite
knowledge. For each chapter, we list those prerequisites. This non-linear way of reading is
probably the most efficient and rewarding one!
That said, the manual is kept short so that you can read it in its entirety. The first part (Basics)
is an introduction on how to use the CP solver to solve small problems. For real problems, you
need to customize your search and this is explained in the second part (Customization). If you
are interested in the routing part of the library, the third part is for you (Routing). Finally, some
utilities and tricks are described in the last part (Technicalities).
Targeted audience
This manual is written with two types of readers in mind. First, someone who is not famil-
iar 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 scientific articles: you don’t need to read and understand them
to follow the manual.
Did we succeed to write for such different profiles? You tell us!
v
Conventions used in this manual
All the code is systematically written in monospace font. Function and method’s
names are followed by parentheses. The method MakeSomething() and the parameter
something are two beautiful examples of this convention.
To draw your attention on important matters, we use a box with a danger warning sign.
To explain some details that would break the flow of the text, we use 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.
All the examples in this manual are coded in C++. For the most important code snippets, you
can find complete examples on the documentation hub:
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 find the complete examples on the documentation hub or under
the directories:
documentation/tutorials/Python
documentation/tutorials/Java
documentation/tutorials/Csharp.
http://code.google.com/p/or-tools/
You can follow us on Google+:
https://plus.google.com/u/0/108010024297451468877/posts
and post your questions, suggestions, remarks, . . . to the or-tools discussion group:
http://groups.google.com/group/or-tools-discuss
vii
Do you have comments?
If you have comments, suggestions, corrections, feedback, . . . , about this document or about the
documentation of the or-tools library in general, please send them to ortools.doc@gmail.com.
Thank you very much.
Happy reading!
i
CONTENTS
Foreword iii
I Basics 1
1 Introduction to constraint programming 3
1.1 The 4-Queens Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 What is constraint programming? . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Real examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4 A little bit of theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.5 The three-stage method: describe, model and solve . . . . . . . . . . . . . . . 22
1.6 It’s always a matter of tradeoffs . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7 The Google or-tools library . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8 The content of the manual . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29
4 Reification 73
4.1 What is reification? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73
II Customization 75
5 Defining search primitives: the n-Queens Problem 77
5.1 The n-Queens Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.2 Implementation of a basic model . . . . . . . . . . . . . . . . . . . . . . . . 81
5.3 Basic working of the solver: the search algorithm . . . . . . . . . . . . . . . . 86
5.4 cpviz: how to visualize the search . . . . . . . . . . . . . . . . . . . . . . . . 97
5.5 Basic working of the solver: the phases . . . . . . . . . . . . . . . . . . . . . 119
5.6 Out of the box variables and values selection primitives . . . . . . . . . . . . 128
5.7 Customized search primitives: DecisionBuilders and Decisions . . . 132
5.8 Breaking symmetries with SymmetryBreakers . . . . . . . . . . . . . . . 141
5.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
10 Vehicule Routing Problems with constraints: the capacitated vehicle routing prob-
lem 345
10.1 The Vehicle Routing Problem (VRP) . . . . . . . . . . . . . . . . . . . . . . 346
10.2 The VRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
10.3 The Capacitated Vehicle Routing Problem (CVRP) . . . . . . . . . . . . . . . 358
10.4 The CVRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
10.5 Multi-depots and vehicles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
10.6 Partial routes and Assigments . . . . . . . . . . . . . . . . . . . . . . . . . . 369
10.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375
IV Technicalities 377
11 Utilities 379
11.1 Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
11.2 Asserting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
11.3 Timing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
11.4 Profiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
11.5 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.6 Serializing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.7 Visualizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.8 Randomizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.9 Using FlatZinc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
V Appendices 397
Bibliography 399
Index 403
Part I
Basics
CHAPTER
ONE
INTRODUCTION TO CONSTRAINT
PROGRAMMING
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:
This chapter is divided in three parts. First, we introduce Constraint Programming by looking at
an example of the solving process of our CP solver in section 1.1. We give a brief definition of
Constraint Programming in section 1.2 and show some practical problems where CP stands out
in section 1.3. Second, we lay some practical and theoretical foundations for the whole manual.
A little bit of theory in section 1.4 provides some theoretical backgrounds with interesting
practical implications. If you don’t know how to tackle a problem, we introduce a (very)
simple strategy (the three-stage method in section 1.5) that can help you 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 the whole section 1.6. Finally, we outline the general principles of the library
in section 1.7 and detail the content of this manual in section 1.8.
Prerequisites:
• None. Being open minded, relaxed and prepared to enjoy the or-tools library helps
though.
The 4-Queens Problem1 consists in placing four queens on a 4 x 4 chessboard so that no two
queens can capture each other. That is, no two queens are allowed to be placed on the same
row, the same column or the same diagonal.
The following figure illustrates a solution to the 4-Queens Problem: none of the 4 queens can
capture each other.
Although this particular problem isn’t very impressive, keep in mind that you can generalize it
to n × n chessboards with n > 4.
In Constraint Programming we translate a real problem into a mathematical model with vari-
ables and constraints. Variables represent decisions and constraints restrain the variables from
taking arbitrary values. For instance, to model the 4-Queens Problem, we could use a binary
variable xij that indicates if a queen is present on the given (i, j) square (xij = 1) or not
(xij = 0). The first index i denotes the ith row and the second index j the j th column. We need
several constraints to model that no two queens can capture each other. To limit the number of
queens to 4, we can add the following constraint:
X
xij = 4.
(i,j)∈ squares
This constraint ensures 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 .
4
Chapter 1. Introduction to constraint programming
those values to keep all the variables values compatible with the model. In Constraint
Programming, clever algorithms are devised to remove those values in an efficient man-
ner. These algorithms propagate the current state of the solver and removes incompatible
or undesirable values.
• 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 don’t 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 finds a solution
but continues the search and tries to find another solution.
To better understand Constraint Programming, let’s 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 first 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
gray 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 first step, only the white squares are still available to place the three re-
maining 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 first
available square from the top in the second column. As in the first step, the solver knows that
no other queen can be placed in a column where it just placed a queen, hence the new gray
crosses in the next Figure.
6
You can find this search process detailed in sections 5.2 and 5.4.
5
1.1. The 4-Queens Problem
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 configuration. The solver has to backtrack!
The solver decides to challenge its last decision to place the second queen in the third row from
the top 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.
6
Chapter 1. Introduction to constraint programming
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 first choice to place the first queen in the
first row and places the first queen in the first 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 squares marked by red crosses:
7
1.1. The 4-Queens Problem
The positive diagonal constraint kicks in and forbids the red square leaving no choice to place
a third queen in the third column first 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 first row, leaving no choice but to place the fourth queen in the
fourth column third row:
The solver finds out that the model is respected, so we have our first solution! Should the
solver continue the search, it would have to backtrack and try to place the first queen in the first
column third row.
8
Chapter 1. Introduction to constraint programming
This section was written with different readers in mind. The ones described in the
preface but also our colleagues from operations research that are new to CP. From
time to time, we compare CP with their field and we use some jargon. Don’t be
afraid if you don’t understand those asides and just read on.
9
1.2. What is constraint programming?
If you are used to (non-)linear programming, you know how difficult it is to model some con-
straints (forcing them to be linear, use of big M for disjunctions, replacing one constraint by a
bunch of linear constraints, relying on the direction of optimization (minimizing or maximiz-
ing), etc.). None of this happens in CP because constraints can be any constraints. They even
don’t have to be numerical and can deal with symbolic variables! This allows to model your
problems in a very natural fashion.
One of the most well-known global constraints is the AllDifferent constraint. This con-
straint ensures that the variables have different values in a feasible solution. For instance
AllDifferent(t0 , t1 , t2 ) forces the three variables t0 , t1 and t2 to have different values. Say
that t0 , t1 and t2 can take the integer values in [0, 2].
Compare
AllDifferent(t0 , t1 , t2 )
to the classical way (see [Williams2001]) of translating this constraint in linear integer pro-
gramming for instance:
ti − 2j=0 jλij = 0
P
∀i
P2
λij = 1 ∀i
Pj=0
2
i=0 λij 6 1 ∀j
To model the AllDifferent(t0 , . . . , tn−1 ) constraint8 with ti ∈ [0, n − 1], we already need n2
auxiliary variables λij :
1 if ti takes value j
λij =
0 otherwise
and 3n linear equations!
Of course if AllDifferent(t0 , t1 , t2 ) was being replaced by its linear integer programming trans-
lation for instance, it would only be syntactic sugar but it is not. Specialized and efficient
propagation algorithms were (and are still!) developed to ensure t0 , t1 and t2 keep different
values during the search.
Numerous specialized and general global constraints exist. The Global Constraint Catalog
references 354 global constraints at the time of writing.
Because CP deals locally9 with each constraints, adding constraints, even on the fly (i.e. during
the search), is not a problem. This makes CP a perfect framework to prototype and test ideas:
you can change the model without changing (too much) your search strategy/algorithm.
Because the type of relationships among variables that can be modelled in CP is quite large10 ,
you can play with quite heterogeneous constraints and mix all type of variables.
8
In some special cases, we are able to model the AllDifferent constraint in a more efficient manner.
9
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 efficiently) your constraints.
10
Chapter 1. Introduction to constraint programming
One of the curiosities of CP is its ability to deal with meta-constraints: constraints on con-
straints!
Take for instance the Element constraint. Let [x0 , . . . , xn−1 ] be an array of integers variables
with domain {0, . . . , n−1}, y an integer variables with domain contained in {0, . . . , n−1} and
z with domain {0, . . . , n − 1}. The Element constraint assign the y th variable in [x0 , . . . , xn−1 ]
to z, i.e.:
z = xy .
If you change y or the array [x0 , . . . , xn−1 ], z will change accordingly but remember that you
have an equality, so this works the other way around too. If you change z then y or/and the
array [x0 , . . . , xn−1 ] will have to change! This technique is called reification and you can learn
more about it in chapter 4. The ease to model a problem and the possibility to add heteroge-
neous constraints sometimes make CP the preferred or only framework to model some difficult
problems with a lot of side-constraints.
Propagation is not enough to find a feasible solution most of the time. The solver needs to test
partial or complete assignments of the variables. The basic search algorithm (and the one im-
plemented in or-tools) is a systematic search algorithm: it systematically generates all possible
assignments one by one11 , trying to extend a partial solution toward a complete solution. If it
finds an impossibility along the way, it backtracks and reconsiders the last assignment (or last
assignments) as we have seen in the previous section.
There exist numerous refinements (some implemented in or-tools too) to this basic version.
The assignment possibilities define the search space12 . In our 4-queens example, the search
space is defined by all possible assignments for the 16 variables xij . For each of them, we have
2 possibilities: 0 or 1. Thus in total, we have 162 = 256 possibilities. This is the size of the
search space. It’s important to understand that the search space is defined by the variables and
their domain (i.e. the model) and not by the problem itself13 . Actually, it is also defined by the
constraints you added to the model because those constraints reduce the possibilities and thus
the search space14 .
The search algorithm visits systematically the whole search space. The art of optimization is
to model a problem such that the search space is not too big and such that the search algorithm
visits only interesting portions of the search space quickly15 .
When the solver has done its propagation and has not found a solution, it has to assign a value
to a variable16 . Say variable x21 . Because we don’t want to miss any portion of the search
11
See the section Basic working of the solver: the search algorithm for more details.
12
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) difficult 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 difficult to scan. See the section It’s
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 let’s keep it simple for the moment: the solver assigns a value to one variable at a time.
11
1.3. Real examples
space, we want to visit solutions where x21 = 1 but also solutions where x21 = 0. This choice
is called branching. Most systematic search algorithms are called branch-and-something:
• branch and bound;
• branch and prune;
• branch and cut;
• branch and price;
• ...
In Constraint Programming, we use Branch and prune where pruning is another term for prop-
agation. You can also combine different techniques. For instance branch, price and cut.
MIP CSP
Branch and bound Branch and prune
Bound: Prune:
• Relax constraints • Propagate constraints
Since the ‘90s, CP is used by small and major companies (including Google) around the world.
It has become the technology of choice for some problems in scheduling, rostering, timetabling,
and configuration.
Here is a non-exhaustive list17 where CP has been used with success:
• Production sequencing
• Production scheduling
• Satellite tasking
• Maintenance planning
17
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.
12
Chapter 1. Introduction to constraint programming
• Transport
• Stand allocation
• Personnel assignment
• Personnel requirement planning
• Hardware design
• Compilation
• Financial applications
• Placement
• Industrial cutting
• Air traffic control
• Frequency allocation
• Network configuration
• Product design
• Product blending
• Time tabling
• Production step planning
• Crew rotation
• Aircraft rotation
• Supply chain management
• Routing
• Manufacturing
• Resource allocation
• Circuit verification
• Simulation
• ...
With such a high success rate in different application, CP can be thus described as one efficient
tool in the toolbox of Operations Research experts.
We could list hundreds of success stories were CP made a - sometimes huge - difference but
we don’t want to advertise any company. You’ll find plenty on the web. Let us just advertise
CP as a very efficient and convenient tool to solve industrial problems.
13
1.3. Real examples
From time to time, people search for the holy grail of Computer Science. We could define
it as the pursuit to solving arbitrary combinatorial optimization problems with one universal
algorithm. As E. Freuder (see [Freuder1997]) states it:
The user states the problem, the computer solves it.
For instance, David Abramson and Marcus Randall in their 1997 article (see [Abramson1997])
tried to apply Simulated Annealing19 to solve arbitrary combinatorial optimization problems20 .
Modeling languages ( AIMMS, AMPL, GAMS, Xpress-Mosel, etc) are yet another attempt at
engineering this universal algorithm. You write your model in a common algebraic/symbolic
language, often close to the mathematical language21 . It is then translated for an appropriate
solver of your choice. Some modeling languages even let you write high level algorithms.
One of the great advantages of modeling languages is the possibility to quickly prototype your
algorithm and to try it online (and for free!) with the NEOS server22 .
All these approaches don’t compare23 to dedicated algorithms tailored for a specific problem24 .
Until now, all these attempts have been vain. That said, CP - because of its particularity of
dealing with constraints locally25 - is probably the closest technique to the holy grail. Actually,
we didn’t cite E. Freuder fully (see [Freuder1997]):
Constraint Programming represents one of the closest approaches computer science has yet
made to the Holy Grail of programming: the user states the problem, the computer solves it.
18
This is common knowledge in the field.
19
You can learn more about Simulated Annealing (SA) in the section Simulated Annealing (SA).
20
This implies that any problem can be translated into a combinatorial problem!
21
See Wikipedia Algebraic languages.
22
The NEOS server proposes several state-of-the-art solvers. As stated on its website: “Optimization problems
are solved automatically with minimal input from the user. Users only need a definition of the optimization
problem; all additional information required by the optimization solver is determined automatically.”
23
Luckily, otherwise we would be jobless! ,
24 ?
Actually, this search for the holy grail is closely related to the famous P = NP question. If such algorithm
exists, then most probably P = NP. See the section Intractability.
25
See the subsection The ease to model a problem.
14
Chapter 1. Introduction to constraint programming
Here you will find some important ideas and the vocabulary we use throughout this manual.
As is often the case with theory, if you don’t have some first-hand experience with practice
(in this case mathematical optimization), you might find it difficult to follow the theoretical
abstraction and technicalities. To help you get mixed up, experts often mix terms and use
(wrongly formulated) shortcuts when they describe their theory. Optimization is certainly no
exception. We’ll try to be as clear as possible and use specific examples to illustrate the most
important concepts. We also try not to oversimplify too much but from time to time, we do
to give you a a simple formulation to a complex theory. In subsection 1.4.1, we cover the
basic vocabulary used to describe the problems we solve in CP. Section 1.4.4 is an informal
introduction to complexity theory26 . One of the difficulties of this theory is that there are lots
of technical details no to be missed. We introduce you to important ideas without being drawn
into too many details (some unavoidable details can be found in the footnotes).
Complexity theory is relatively new (it really started in the ’70s) and is not easy (and after
reading this section, you’ll probably have more questions than answers). If you are allergic to
theory, skip subsection 1.4.4. We are convinced - we took the time to write it, right? - that you
would benefit from reading this section in its entirety but it is up to you. You might want to
read the important practical implications of this complexity theory in subsection 1.4.4 though.
We illustrate the different components of a Constraint Satisfaction Problem with the 4-Queens
Problem we saw in section 1.1. A Constraint Satisfaction Problem (CSP) consists of
• a set of variables X = {x0 , . . . , xn−1 }.
For the 4-Queens Problem, we have a binary variable xij indicating the presence or not
of a queen on square (i, j):
• a set of constraints that restrict the values the variables can take simultaneously.
For the 4-Queens Problem, we have a set of constraints that forbid two queens (or more)
26
We talk here about Time-complexity theory, i.e. we are concerned with the time we need to solve problems.
There exist other complexity theories, for instance the Space-complexity theory where we are concerned with the
memory space needed to solve problems.
15
1.4. A little bit of theory
Indeed, these constraints ensure that for each row i at most one variable xi0 , xi1 , xi2 or
xi3 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 row27 . Later, in
section 5.2, we will provide another model with different variables and constraints.
As we mentioned earlier, values don’t need to be integers and constraints don’t need to be
algebraic equations or inequalities28 .
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. Most of the problems used in this
manual fall into this category. In this manual, we’ll discuss among others:
• Cryptarithmetic Puzzles (CSP)
• The Golomb Ruler Problem (COP)
• The n-Queens Problem (CSP)
• The Job-Shop Problem (COP)
• The Travelling Salesman Problems with time windows (COP)
• The Capacitated Vehicle Routing Problem (COP)
We will not go into details about what a mathematical problem exactly is. As an example,
we met the n-Queens Problem in section 1.1. We would like to take the time to differenti-
ate mathematical problems from real problems though. Let’s take an example. Let’s say you
are a company and want to give your users the ability to view streets from the comfort of their
browsers as if they were physically present in remote locations. To provide such a service, you
might want to scour the globe with special cars and other mobile means to take some pictures.
This is a real problem from real life. How do you provide such a service? Mathematical opti-
mization to the rescue! Subdivide this real (and challenging!) problem into smaller ones and
translate them into (solvable) mathematical problems:
• Taking pictures? All and well but how do you actually do this? How many pictures
do you need to take? At what angle? How do you morph them together and minimize
distortion? Etc....
27
We have to cautious here about the exact definition of the n-Queens Problem. The version we talk about is
a CSP where we know that we have to place n queens on an n x n chessboard so that no two queens attack each
other.
28
Basically, the only requirement for a constraint in CP is its ability to be propagated. See chapter Custom
constraints: the alldifferent_except_0 constraint.
16
Chapter 1. Introduction to constraint programming
• How do you scour the streets of a city? At what speed? How close to pictured objects?
With how many cars? At what time of the day? After covering a street, where should the
car head next? What about turns? Etc....
• How do you keep such information on the servers? How do you match the pictures with
the correct locations? How do you respond to a user request such that she gets the most
relevant information as quickly as possible? Etc....
• ...
Believe it or not, you can translate those questions into mathematical problems. And these
(some of them at least) mathematical problems can be (and are!) solved with tools like the
or-tools library.
Mathematical problems are a translation of such real problems into mathematical terms. We’ll
see many of them in this manual. Interestingly enough, the or-tools CP solver doesn’t have a
Model class on its own as the model is constructed in/by the solver itself but you have some
classes that relate to the mathematical model the solver is trying to solve. Among them, you
have a ModelVisitor class that lets you visit the model and act upon it.
These mathematical problems are sort of theoretical templates. What you want to solve with a
computer are specific versions of a problem. For instance, the n-Queens problem with n = 37
or n = 6484, or a Travelling Salesman Problem on a particular graph representing the city of
New York. We call these practical materialization of a mathematical problem instances. You
try to find mathematical theoretical methods to solve mathematical problems and solve specific
instances on your computer29 . When you ask the CP solver to print a model, you are in fact
asking for a specific instance.
A solution of an instance is an assignment of the variables: each variable is assigned a value of
its domain. This doesn’t mean that a solution solves your problem: the assignment doesn’t need
to respect the constraints! A feasible solution to a CSP or a COP is a feasible assignment: ev-
ery variable has been assigned a value from its domain in such a way that all the constraints of
the model are satisfied. The or-tools CP solver uses the class Assignment to represent a so-
lution. As its mathematical counterpart, it can represent a valid (i.e. a feasible) solution or not.
When we have a feasible solution, we’ll talk about a Solution (SolutionCollector
and SolutionPool classes or the NextSolution() method for instance) in or-tools al-
though there is no Solution class. The objective value of a feasible solution is the value of
the objective function for this solution. In or-tools, the objective function is modeled by the
OptimizeVar class, i.e. it is a variable. 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 solu-
tion doesn’t need to exist nor is it unique in general. For instance, we’ll see that the n-Queens
Problem or the Golomb Ruler Problem both possess several optimal solutions.
Let us emphasize that the or-tools CP-solver deals only with discrete and finite variables, i.e.
the values a variable can take are elements of a finite set. If you want to solve a continuous
problem, you need to discretize it, i.e. model your problem with variables over a finite domain.
29
Of course, things are never that simple. The difference between a mathematical problem and an instance
is not always that clear. You might want to solve generically the n-Queens Problem with a meta-algorithm for
instance, or the n-Queens Problem could be viewed as an instance of a broader category of problems.
17
1.4. A little bit of theory
If you prefer not to read the next section (or if you want a good preparation to read it!), we have
summarized its main ideas:
• problems are divided in two categories30 : easy (P problems) and hard (NP-Hard or
NP-Complete problems) problems. Hard problems are also called intractable31 and in
general we only can find approximate solutions for such problems32 . Actually, the ques-
tion of being able to find exact solutions to hard problems is still open (See the box “The
?
P = N P question” below);
Some problems such as the Travelling Salesman Problem (see chapter 9) are hard to solve33 :
no one could ever come up with an efficient algorithm to solve this problem. On the other
hand, other problems, like the n-Queens Problem (see chapter 5), are solved very efficiently34 .
In the ’70s, complexity experts were able to translate this fact into a beautiful complexity theory.
Hard to solve problems are called intractable problems. When you cannot solve an in-
tractable problem to optimality, you can try to find good solutions or/and approximate the
problem. In the ‘90s, complexity experts continued their investigation on the complexity of
solving problems and developed what is now known as the approximation complexity theory.
Both theories are quite new, very interesting and ... not easy to understand. We try the tour
the force to introduce the basics of the general complexity theory in a few lines. We willingly
kept certain technical details out of the way. These technical details are important and actually
without them, you can not construct a complexity theory.
Intractability
One of the main difficulties complexity experts faced in the ‘70s was to come up with a theo-
retical definition of the complexity of problems not algorithms. Indeed, it is relatively easy35
to define a complexity measure of algorithms but how would you define the complexity of a
problem? If you have an efficient algorithm to solve a problem, you could say that the problem
belongs to the set of easy problems but what about difficult problems? The fact that we don’t
know an efficient algorithm to solve these doesn’t mean these problems are really difficult.
Someone could come up one day with an efficient algorithm. The solution the experts came up
30
Stated like this, it sounds pretty obvious but this complexity theory is really subtle and full of beautiful (and
useful) results. Actually, most problems of practical interest belong to either categories but these two categories
don’t cover all problems.
31
Intractable problems are problems which in practice take too long to solve exactly, so there is a gap between
the theoretical definition (NP-Hard problems) and the practical definition (Intractable problems).
32
Technically, you could find an exact solution but you would not be able to prove that it is indeed an exact
solution in general.
33
Roughly, we consider a problem to be hard to solve if we need a lot of time to solve it. Read on.
34
The Travelling Salesman Problem is said to be NP-hard while (a version of) the n-Queens Problem is said to
be in P.
35
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.
18
Chapter 1. Introduction to constraint programming
with was to build equivalence classes between problems and define the complexity of a problem
with respect to the complexity of other problems (so the notion of complexity is relative not
absolute36 ): a problem A is as hard as a problem B if there exists an efficient transformation τ
that maps every instance b of problem B into an instance τ (b) = a of problem A such that if
you solve a, you solve b.
A B
τ
τ (b) = a b
Indeed, if there exists an efficient algorithm to solve problem A, you can also solve efficiently
problem B: transform an instance b into into an instance τ (b) = a of problem A and solve it
with the efficient algorithm known to solve problem A. We write B 6T A and say that problem
B reduces efficiently to problem A and that τ is an efficient reduction37 . The search for an
efficient algorithm is replaced by the search for an efficient reduction between instances of two
problems to prove “equivalent” complexities.
This main idea leads to a lot of technicalities:
• how to exactly measure the complexity of an algorithm?
• what is an efficient transformation/reduction?
• what are the requirements for such a reduction?
• ...
We don’t answer these interesting questions except the one on efficiency. We consider a re-
duction τ efficient if there exist a polynomial-time bounded algorithm (this refers to the first
question...) that can transform any instance b of problem B into an instance a of problem A
such that the solutions correspond. This also means that we consider an algorithm efficient if it
is polynomially time-bounded (otherwise the efficiency of the reduction would be useless).
The class of problems that can be efficiently solved is called P , i.e. the class of problems that
can be solved by a polynomial-time bounded algorithm3839 .
Some problems are difficult to solve but once you have an answer, it is quite straightforward
to verify that a given solution is indeed the solution of the problem. One such problem is the
36
Well, for... hard problems.
37
The T in 6T is in honor of Alan Turing. Different types of efficient reductions exist.
38
For technical reasons, we don’t 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 finite 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 P , if its equivalent decision problem is in P . For instance, the Chinese Postman Problem (CPP) is
an optimization problem where one wants to find a minimal route traversing all edges of a graph. The equivalent
decision problem is ” Is it possible to find a feasible route with cost less or equal to k ? ” where k is a given
integer. By extension, we will say that the CPP is in P (we should rather say that the CPP is in P − optimization).
39
This discussion is really about theoretical difficulties of problems. Some problems that are theoretically easy
(such as solving a Linear System or a Linear Program) are difficult in practice and conversely, some problems that
are theoretically difficult, such as the Knapsack Problem are routinely solved on big instances.
19
1.4. A little bit of theory
Hamiltonian Path Problem (HPP). Given a graph, is there a path that visits each vertex exactly
once? Finding such a path is difficult but verifying that a given path is indeed an Hamiltonian
path, i.e. that it passes exactly once through each vertex, can be easily done. Problems for
which solutions are easy to verify, i.e. for which this verification can be done in polynomial
time, are said to be in the class N P 40 . P ⊂ N P because if you can find a solution in polynomial
time, you can also verify this solution in polynomial time (just construct it). Whether we have
equality or not between these two sets is one of the major unsolved theoretical questions in
?
Operations Research (see the box “The P = N P question” below).
Not all problems in NP seem to be of equal complexity. Some problems, such as the HPP are
as hard as any problem in NP. Remember our classification of the complexity of problems?
This means that every problem in NP can be transformed in polynomial time into the HPP. The
hardest problems of NP form the class of NP-Complete problems.
How can you prove that all problems in NP are reducible to a single problem?
Wait a minute. There is an infinite number of problems in NP, many of which are unknown
to us. So, how is it possible to prove that all problems in NP can be reduced to a single
problem?
This can done in two steps:
1. First, notice that the reduction is transitive. This means that if A 6T B and B 6T C
then A 6T C. Thus, if you have one problem Z such that all problems Ai in NP are
reducible to Z, i.e. Ai 6T Z, then to prove that all problems Ai in NP reduce to a
problem X, you just need to prove that Z reduces to X. Indeed, if Z 6T X then
Ai 6T Z 6T X a .
This only works if you can find such problem Z. Well, such problem has been found,
see next point.
The funny fact is that if X is in NP, then X 6T Z also. Such problems are called
NP-Complete and we just showed that if you can solve one problem in NP-Complete
efficiently, you can solve all the problems in NP efficiently!
2. Several researchers (like for example Cook and Levin in the early ‘70s, see
Wikipedia on the Cook-Levin Theorem), were able to prove that all problems in
NP are reducible in polynomial time to the Boolean Satisfiability Problem (SAT)
and this problem is of course in NP.
Proving that the SAT problem is NP-Complete is a major achievement in the com-
plexity theory (all existing proves are highly technical).
a
If you want to prove that a problem Y 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!
20
Chapter 1. Introduction to constraint programming
NP−Hard
NP−Complete
NP
If P 6= NP
?
The P = N P 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 verified 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 first 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 P = N P question. Here are the
results:
Outcomea % %
(2002) (2012)
P 6= N P 61 83
P = NP 9 9
No idea 30 8
One possible outcome - mentioned by very few - is that this question could be... undecid-
able, 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 ex-
press themselves (some answered “I don’t care” for instance). The first 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
Computers and Intractability: A Guide to the Theory of NP-Completeness from M. R. Garey
and D. S. Johnson (see [Garey1979]42 ).
42
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.
21
1.5. The three-stage method: describe, model and solve
If you try to solve a problem that is proven to be NP-Hard, you know that it is probably an
intractable problem (if P 6= N P ). At least, you know that no one could ever come with an
efficient algorithm to solve it and that it is unlikely to happen soon. Thus, you are not able to
solve exactly “big” instances of your problem. What can you do?
Maybe there are special cases that can be solved in polynomial time? If you are not interested
in those cases and your instances are too big to be solved exactly, even with parallel and/or
decomposition algorithms, then there is only one thing to do: approximate your problem and/or
the solutions.
You could simplify your problem and/or be satisfied with an approximation, i.e. a solution that
is not exact nor optimal. One way to do this in CP is to relax the model by softening some
constraints43 . In a nutshell, you soften a constraint by allowing this constraint to be violated.
In a approximate solution where the constraint is violated, you penalize the objective function
by a certain amount corresponding to the violation. The bigger the violation, the bigger the
penalty. The idea is to find a solution that doesn’t violate too much the soft constraints in the
hope that such approximate solution isn’t that different from an exact or optimal solution44 .
Another possible research avenue is to use (meta-)heuristics: algorithms that hopefully return
good or even near-optimal solutions. Some of these algorithms give a guarantee on the quality
of the produced solutions, some don’t and you just hope for the best. You can also monitor
the quality of the solutions by trying to close the optimality gap as much as possible. Given an
instance, compute a lower bound α and an upper bound β on the optimal value z ∗ . So you know
that z ∗ ∈ [α, β]. Closing the optimality gap is trying to shrink this interval by producing better
upper and lower bounds. If you manage to produce solutions with objective values belonging
to this interval, you know how close (or not) these values are from the optimal value of your
instance.
As with exact optimization, an approximation complexity theory emerged. It started in the
‘90 and is now a mature domain that improves greatly our comprehension of what we can or
can not (theorically) compute. There is a whole zoo of complexity classes. Some problems
can be approximated but without any guarantee on the quality of the solutions, others can
be approximated with as much precision as you desire but you have to pay the price for this
precision: the more precision you want the slower your algorithms will be. For some problems
it is hard to find approximations and for others, it is even impossible to find an approximation
with any guarantee whatsoever on its quality!
We propose a recipe that belongs to the folklore. Like all recipes it is only a guide and should
not be followed blindly45 . When it comes to research, everything is permitted and your imagi-
nation is the limit.
43
For MIP practitioners, this is equivalent to Lagrangian Relaxation.
44
In the case of optimization, a solution that isn’t that different means a solution that has a good objective value,
preferably close to the optimum.
45
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 first two parts of this manual.
22
Chapter 1. Introduction to constraint programming
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 prob-
lem (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?
• 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?
23
1.6. It’s always a matter of tradeoffs
1.5.2 Model
Again a difficult stage if not the most challenging part of the solving process. Modeling 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 first. This
is perfectly normal. Take the time to read them several times until you master them.
When confronted with a new problem, you might not know what do to. We all face this situa-
tion. This is what research is all about!
1.5.3 Solve
The reader should be aware that this stage isn’t only about pushing a solve button and waiting
for the results to be delivered by the solver. The solve stage involves reasoning to find the best
way to solve a given model, i.e. how to traverse the search tree in a efficient way. We discuss
this stage in details in chapter 5.
24
Chapter 1. Introduction to constraint programming
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.
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:
The Google or-tools open source library was publicly released in September 2010. Since then
it has evolved to be a huge project containing more than 65K lines of code written in C++48 !
The constraint programming part - including the important vehicle routing engine - has the
lion’s share with approximatively 55K lines of code. That’s right, the or-tools library is really
a constraint programming library. More specifically, a finite-domain constraint programming
library49 .
Contrary to other constraint programming libraries, our goal is not to provide a complete set of
algorithms, constraints, etc. We use our code internally at Google and we adapt it to our needs.
From time to time we open source some parts and share it with the world. As our goal is to
solve specific (and sometimes really challenging problems), we use a generic and lightweight
approach. We believe that specialized problems need specialized code. Our library aims at
simplicity because simple code, when well-written, is efficient and can be easily adapted to
solve complex problems. As an example of this lightweight approach, we don’t really offer a
matrix API. If you look at the results of the 2014 MiniZinc challenge, it seems our approach is
not too bad50 .
That said, our library is quite robust and can be used to solve a trove of problems. If you are
missing a known constraint programming algorithm, ask us on the OR Tools Discussion group,
we might have what you are looking for in store or even surprise you and implement it if we
see a good fit for our needs.
48
Beside C++, you can use the library through SWIG in Python, Java, and .NET (using mono on non
Windows platforms).
49
Most constraint programming libraries and frameworks use finite domains. Actually, if you use a computer,
you must - in a way or another - use a finite domain.
50
Yes, our constraint solver can read FlatZinc models. Lear more about how to use the FlatZinc API in sec-
tion 11.9.
25
1.8. The content of the manual
Chapter 2: First steps with or-tools: cryptarithmetic puzzles: We start by helping you down-
load and install the or-tools library. Be careful to know exactly what third-party libraries
you want to use with or-tools. We then use the very basic functionalities of the CP
solver. We’ll encounter the Solver class and use the integer variables IntVar. The
model used in this chapter is very simple and we’ll add basic algebraic equalities with the
help of MakeSum(), MakeProd(), MakeEquality() and AddConstraint().
The AllDifferent constraint will make its first apparence too. More importantly,
we’ll use a DecisionBuilder to define the search phase and launch the search with
NextSolution(). To conduct the search, we use SearchMonitors and collect
solutions with SolutionCollectors and Assigments. Finally, we’ll say a few
words about the way to pass read-only parameters to the solver and about the other avail-
able programming languages in or-tools (Python, Java and C#). Although this chapter
is a gentle introduction to the basic use of the library, it also focuses on some basic but
important manipulations needed to get things right. Don’t miss them!
Chapter 3: Using objectives in constraint programming: the Golomb Ruler Problem: In
this chapter, we not only look for a feasible solution but for an optimal solution, i.e.
a solution that optimizes an objective function. To solve the Golomb Ruler Problem,
we’ll try five different models and compare them two by two. To have an intuition of
the models passed to the solver and the progress of the search, we show you how to
inspect the model you constructed and how to collect some statistics about the search.
Several flags are available to tune the search, collect statistics, etc. We present some of
them and how to trigger them. To limit the search in some way, use SearchLimits.
As SearchLimits use custom made functions or methods, this will be our first (but
26
Chapter 1. Introduction to constraint programming
certainly not last) encounter with callbacks and functors. Two very useful techniques
to tighten a model are introduced: adding better bounds and breaking symmetries.
Finally, we explain how our CP solver optimizes while it basically “only” finds feasible
solutions.
Chapter 4: Reification: [TO BE WRITTEN]
Chapter 5: Defining search primitives: the n-Queens Problem: The or-tools CP solver is
quite flexible and comes with several tools (Decisions, DecisionBuilders, ...)
that we call search primitives. Some are predefined and can be used right out of the
box while others can be customized thanks to callbacks. You can also combine dif-
ferent search strategies. SearchMonitors allow you to guide the search thanks to
callbacks. DecisionBuilders and Decisions define the search tree. We explain
their mechanisms and how they are embedded in the main search algorithm of the CP
solver. We also show where exactly in this main search algorithm most of the callbacks
of the SearchMonitors are triggered. The presented algorithm is a simplified version
of the real algorithm but you’ll get a pretty clear idea of the real algorithm. To better
understand all these tools, we use the wonderful cpviz library to visualize the search tree
and the variable propagations. The basic branching in the search tree is done by selecting
variables, then selecting values these variables can or can not hold. We list the avail-
able branching strategies. Once you master all these basic search concepts, we show you
how to customize them, i.e. how to create your own search primitives. This chapter is
difficult but essential to understand the basic working of the CP solver. To reward your
efforts and struggles to master this chapter, we end it with some cool stuff about how
to break symmetries during the search (on the fly!) using SymmetryManagers and
SymmetryBreakers.
Chapter 6: Local Search: the Job-Shop Problem: Scheduling is one of the fields where con-
straint programming has been applied with great success. It is thus not surprising that
the CP community has developed specific tools to solve scheduling problems. In this
chapter, we introduce the ones that have been implemented in or-tools. To address dif-
ficult problems - like the job-shop problem - we make use of (meta-)heuristics. Local
search is a general framework to seek a better solution starting from an initial solu-
tion. We explain what local search is and show how it’s done in or-tools. We present
a simplified version of our local search algorithm but, again, you’ll have a pretty clear
idea of the real algorithm and where exactly the callbacks of the SearchMonitors
are triggered. LocalSearchOperators are the main actors: they are in charge to
find candidate solutions given an initial solution. We show how to construct your own
customized LocalSearchOperators and present the most interesting ones that are
already implemented in or-tools. The CP solver verifies the feasibility of all constructed
candidate solutions but if you know how to quickly disregard some candidate solutions
(because you know they are infeasible or not desirable), you can help the CP solver by
creating your own LocalSearchFilters. We’ll show you how and also present a
list of available LocalSearchFilers that you might want to use.
Chapter 7: Meta-heuristics: several previous problems: [TO BE WRITTEN]
27
1.8. The content of the manual
Chapter 9: Travelling Salesman Problems with constraints: the TSP with time windows:
This chapter is our first encounter with the Routing Library (RL) and what better problem
than the Travelling Salesman Problem (TSP) to introduce it? We overview the library
and the problems it can solve. We then delve into the specifics of the mathematical
model we use to represent all these problems: first the variables, then the constraints.
In particular, we’ll see the auxiliary graph that we use to model multiple depots. Every
calculation is done on the auxiliary graph and you just have to translate the solutions
back to your original nodes. We show you how to switch between our auxiliary graph
and your original graph. To solve the Routing Problems, we use Local Search. Several
specialized PathOperators are implemented and we show you how to create your
customized versions. We try to solve the TSPLIB instances. You can add “quantities”
along the arcs. This is done by adding Dimensions. The quantities can represent
goods, people, volumes, ... but also distances and times. We model time windows with
Dimensions for instance.
Chapter 10: Vehicule Routing Problems with constraints: the capacitated vehicle routing problem:
[TO BE WRITTEN]
Chapter 11: Utilities: This chapter is about supplementary tools you can use to enhance your
work-flow with the or-tools library. We’ll cover:
• Logging:
• Asserting:
• Timing:
• Profiling:
• Debugging:
• Serializing:
• Visualizing:
• Randomizing:
• Using FlatZinc:
Chapter 12: Modeling tricks: [TO BE WRITTEN]
Chapter 13: Under the hood: [TO BE WRITTEN]
28
Chapter 1. Introduction to constraint programming
1.8.5 Appendices
In this last part of the manual, you’ll find a bibliography and an index.
1.9 Summary
29
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, you’re good to go and you
should be able to find 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 gflags library). Finally, we say a few words about the other supported languages
(Python, Java and C#).
Section 2.4.1 summarizes in two Figures all the required steps to write a basic program.
Prerequisites:
Files:
You’ll find all what is needed to compile the or-tools library and its third-party libraries in the
wiki page Getting Started.
This wiki page is always up to date with the most current instructions on how to install the li-
brary, whether from binaries or when compiling the source code. You will also find instructions
on how to run the examples.
Pay attention to the fact that this wiki page (and thus the installation procedure) changes reg-
ularly and that you might have to reinstall the library and its third-party dependencies from
scratch from time to time. If you use the variables defined in our makefiles, and your code
doesn’t compile or cannot be linked after an update or reinstallation of the or-tools library, you
might have to adapt your own makefiles accordingly.
The tutorial examples are the examples provided with this manual. You can find them in doc-
umentation/tutorials/cplusplus . For the moment, we only provide a C++ version. For each
chapter in this manual (except for the first chapter and the last part of the manual), we provide
tutorial examples and a makefile to use them.
32
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
Executing
Cleaning
Don’t use
make clean
as you will erase the generated files for the whole library!
33
2.3. The cryptarithmetic puzzle problem and a first model
Now that your system is up and running (if not, see section 2.1), let us solve a cryptarithmetic
puzzle with the help of the or-tools library. In this section, we describe the problem and propose
a first model to solve it. This model is by no means efficient but allows us a gentle introduction
to the library.
A cryptarithmetic puzzle is a mathematical game where the digits of some numbers are rep-
resented by letters (or symbols). Each letter represents a unique digit. The goal is to find the
digits such that a given mathematical equation is verified1 .
Here is an example:
C P
+ I S
+ F U N
---------
= T R U E
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.
Describe
The first stage is to describe the problem, preferably in natural language. What is the goal of
the puzzle? To replace letters by digits such that the sum CP+IS+FUN=TRUE is verified.
What are the unknowns (decision variables)? The digits that the letters represent. In other
words, for each letter we have one decision variable that can take any digit as value.
1
This the mathematical term to specify that the equation is true.
2
Like the famous SEND + MORE = MONEY ... in base 10.
34
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
What are the constraints? The obvious constraint is the sum that has to be verified. 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 first 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 sufficient but let’s 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 efficient.
To model the sum constraint in any base b, we add the linear equation:
+ C·b + P
+ I·b + S
+ F · b2 + U·b + N
= T · b3 + R · b2 + U·b + E
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 find 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?”
35
2.4. Anatomy of a basic C++ code
In this section, we code the model developed in section 2.3. We quickly scan through the code
and describe the basic constituents needed to solve a cryptarithmetic puzzle in C++. In the next
chapters, we will cover some of them in more details.
2.4.1 At a glance
36
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.4.2 Headers
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.
The whole library is nested in the namespace operations_research. We follow the same
convention in all our examples and code inside this namespace:
namespace operations_research {
IntVar* const MakeBaseLine2(...) {
...
}
...
void CPIsFun() {
// Magic happens here!
}
} // namespace operations_research
37
2.4. Anatomy of a basic C++ code
return 0;
}
The CP solver is the main engine to solve a problem instance. It is also responsible for the
creation of the model. It has a very rich Application Programming Interface (API) and provides
a lots of functionalities.
The CP solver is created as follows:
Solver solver("CP is fun!");
The only argument of the constructor is an identification string. The Solver class has one
additional constructor covered in section 2.6.
2.4.5 Variables
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 [i, j] =
{i, i+1, . . . , j −1, j} and has a name name. It returns a pointer to an IntVar. The declaration
IntVar* const c may seem a little be complicated at first. 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.
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 undefined
behavioura !
a
It is possible to bypass the undefined behaviour but you don’t 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
38
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
It is always a good idea to program defensively. We use several assert-like macros defined in
the header logging.h to assert some expressions. We know that the base has to be greater
than or equal to 10, so we add a check for this:
// Check if we have enough digits
CHECK_GE(kBase, letters.size());
CHECK_GE(x,y) is a macro that checks if condition (x) >= (y) is true. If not, the pro-
gram is aborted and the cause is printed:
[23:51:34] examples/cp_is_fun1.cc:108: Check failed:
(kBase) >= (letters.size())
Aborted
You can find more about the assert-like macros in section 11.2.
2.4.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 coefficient 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: first 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();
39
2.4. Anatomy of a basic C++ code
Never store a pointer to an IntExpr nor a BaseIntExpr in the code. The safe
code should always call Var() on an expression built by the solver, and store the
object as an IntVar*.
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
40
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
integer coefficients:
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
...
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));
41
2.4. Anatomy of a basic C++ code
A DecisionBuilder is responsible for creating the actual search tree, i.e. it is responsible
for the search. The solver provides a factory method MakePhase() that returns a pointer to
the newly created DecisionBuilder object:
DecisionBuilder* const db = solver.MakePhase(letters,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);
The first parameter of the method MakePhase is an std::vector with pointers to the
IntVar decision variables. The second parameter specifies how to choose the next IntVar
variable to be selected in the search. Here we choose the first 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
}
// 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() doesn’t 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
doesn’t 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.
42
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
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 profile file,
this file is saved. You can find more about the profile file in section 2.6.2. What happens if
you forget to end the search and didn’t ask for a profile file? If you don’t ask the solver to start
a new search, nothing bad will happen. It is just better practice to finish the search with the
method EndSearch().
See also What is the difference between NewSearch() and Solve()?.
The or-tools library let you collect and store the solutions of your searches with the help of
SolutionCollectors and Assignments. We use them to store the solutions of our
cryptarithmetic puzzle.
43
2.5. SolutionCollectors and Assignments to collect solutions
2.5.1 SolutionCollectors
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. Let’s say you would like to know what the value of variable c is in the
first 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:
44
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
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 first solution found. If you find it odd to specify the
index of the first solution with a FirstSolutionCollector, don’t forget that the API is
intended for generic SolutionCollectors including the AllSolutionCollector.
Let’s 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);
You are not limited to the variables of the model. For instance, let’s 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();
45
2.5. SolutionCollectors and Assignments to collect solutions
2.5.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.
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);
46
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.6 Parameters
The Google’s flags library is quite similar to other command line flags libraries with the notice-
able difference that the flag definitions may be scattered in different files.
To define a flag, we use the corresponding macro. Google’s flags library supports six types:
• DEFINE_bool: Boolean
• DEFINE_int32: 32-bit integer
• DEFINE_int64: 64-bit integer
• DEFINE_uint64: unsigned 64-bit integer
• DEFINE_double: double
• DEFINE_string: C++ string
Each of them takes the same three arguments: the name of the flag, its default value, and
a help string. In file tutorials/cplusplus/chap2/cp_is_fun3.cc, we parse the
base value on the command line. We first include the corresponding header and define the flag
“base“ in the global namespace:
...
#include "base/commandlineflags.h"
...
DEFINE_int64(base, 10, "Base used to solve the problem.");
...
47
2.6. Parameters
namespace operations_research {
...
Note that argc and argv are passed as pointers so that ParseCommandLineFlags() is
able to modify them.
All defined flags are accessible as normal variables with the prefix FLAGS_ prepended:
const int64 kBase = FLAGS_base;
If you want to know what the purpose of a flag is, just type one of the special flags on the
command line:
• --help: prints all the flags
• --helpshort: prints all the flags defined in the same file as main()
• --helpon=FILE: prints all the flags defined in file FILE
• --helpmatch=S: prints all the flags defined in the files *S*.*
For other features and to learn more about this library, we refer you to the gflags documentation.
First, you can invoke the constructor of the Solver that takes a SolverParameters struct:
// Use some profiling and change the default parameters of the solver
SolverParameters solver_params = SolverParameters();
// Change the profile level
solver_params.profile_level = SolverParameters::NORMAL_PROFILING;
We can now ask for a detailed report after the search is done:
48
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
We will see how to profile more in details in the section 11.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.5. Suppose we want to limit the available time to solve a problem. To
pass this parameter on the command line, we define a time_limit variable:
DEFINE_int64(time_limit, 10000, "Time limit in milliseconds");
Everything is coded in C++ and available through SWIG in Python, Java, and .NET (using
mono on non windows platforms).
What language you use is a matter of taste. If you main concern is efficiency and your problem
is really difficult, we advise you to use C++ for meanly two reasons:
• C++ is faster than Python, Java or C# and even more importantly
• you can tweak the library to your needs without worrying about SWIG.
That said, you might not notice differences in time executions between the different languages.
We have tried to add syntactic sugar when possible, particularly in Python and C#. If you
aim for the ease of use to, for instance, prototype, Python or C# are our preferred languages.
Most methods are available in all four flavors with the following naming convention:
49
2.8. Summary
2.8 Summary
This chapter helped you make your first steps with the CP solver. We used the Cryptarithmetic
Puzzle Problem to illustrate the basic working of the solver. Basically, we saw how:
• a mathematical model is designed (variables and constraints through factory methods);
• the solver simplifies memory management (the solver takes ownership of (almost) all
objects and deletes them when it doesn’t need them anymore);
• the search is defined (through one (or more) DecisionBuilders);
• the solving process is launched (NewSearch, NextSolution() and
EndSearch() or Solve());
• solution values are collected (through SolutionCollectors);
• all or the best solution is collected (again through SolutionCollectors);
• solutions are stored (through SolutionCollectors and Assigments);
• parameters are passed to the solver (use of Google’s gflags on the command line, with
a SolverParameters struct given to a solver and through SearchMonitors that
guides the search).
These are the building blocks to use the CP solver. All these concepts were just touched and
will be detailed with care in the next chapters.
50
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 find 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 diffi-
cult. We implement five 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:
Remarks:
• The sums used in this chapter to model the GRP are tricky but you don’t 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.
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 find 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.
3.1. Objective functions and how to compare search strategies
• We introduce two kinds of variables in our modelizations: the marks of the ruler and the
differences between the marks.
• When written, this chapter improved each algorithm one section after the other. Since
then, the compiler has been partly rewritten and the algorithms now have different effi-
ciencies. You can read more about their updated efficiencies in the section First results
of the section Default search.
Files:
In this chapter, we want to construct a ruler with minimal length. Not any ruler, a Golomb ruler.
The ruler we seek has to obey certain constraints, i.e. it has to be a feasible solutions of the
models we develop to represent the Golomb Ruler Problem.
52
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
The objective function to minimize is the length of the Golomb ruler. You can see the objective
function as a variable: you want to find out what is the minimum or maximum value this
variable can hold for any given feasible solution.
Don’t worry if this is not all too clear. There are numerous examples in this manual and you
will quickly learn these concepts without even realizing it.
We search for the smallest Golomb ruler but we also want to do it fast2 . We will devise different
models and compare them. To do so, we will look at the following statistics:
• time: This our main criteria. The faster the better!
• failures: How many times do we need to backtrack in the search tree? Many failures
might be an indication that there exist better search strategies and/or models.
• branches: How many times do we need to branch? Faster algorithms tend to visit fewer
branches and better models tend to have smaller search trees.
Later, in chapter 5, we will devise different search strategies and compare them using the same
statistics.
The Golomb Ruler Problem (GRP) is one of these problems that are easy to state but that are
extremely difficult to solve despite their apparent simplicity.
In this section, we describe the problem and propose a first model to solve it. This model is not
very efficient and we will develop better models in the next sections.
A Golomb ruler is a sequence of non-negative integers such that every difference of two integers
in the sequence is distinct. Conceptually, this is similar to construct a ruler in such a way that
no two pairs of marks measure the same distance, i.e. the differences must all be distinct. The
number of marks (elements in the sequence) is called the order of the Golomb ruler. Figure 3.1
illustrates a Golomb ruler of order 4 and all its - distinct - differences.
0 2 7 11
2 5 4
7
9
11
53
3.2. The Golomb ruler problem and a first model
The Golomb ruler is {0, 2, 7, 11} and its length is 11. Because we are interested in Golomb
rulers with minimal length, we can fix the first mark to 0. Figure 3.2 illustrates an optimal
Golomb ruler of order 4 and all its - distinct - differences.
0 1 4 6
1 3 2
4
5
6
Its length, 6, is optimal: it is not possible to construct a Golomb ruler with 4 marks with a length
smaller than 6. We denote this optimal value by G(4) = 6. More generally, for a Golomb ruler
of order n, we denote by G(n) its optimal value. The Golomb Ruler Problem (GRP) is to find,
for a given order n, the smallest Golomb ruler with n marks.
You might be surprised to learn that the largest order for which the experts have found an
optimal Golomb ruler so far is... 26. And it was a huge hunt involving hundreds of people!
The next table compares the number of days, the number of participants on the Internet and the
number of visited nodes in the search tree to find and prove G(24), G(25) and G(26)345 .
Orders Days Participants Visited nodes
24 1,572 41,803 555,551,924,848,254,200
25 3,006 124,387 52,898,840,308,130,480,000
26 24 2754 3,185,174,774,663,455
The search for G(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 find all the known optimal Golomb rulers and more information on Wikipedia.
We follow again the classical three-stage method described in section 1.5: describe, model and
solve.
3
http://stats.distributed.net/projects.php?project_id=24
4
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 difficult problem, the computa-
tional complexity of this problem is unknown (see [Meyer-Papakonstantinou]).
54
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
Describe
What is the goal of the Golomb Ruler Problem? To find a minimal Golomb ruler for a given
order n. Our objective function is the length of the ruler or the largest integer in the Golomb
ruler sequence.
What are the decision variables (unknowns)? We have at least two choices. We can either view
the unknowns as the marks of the ruler (and retrieve all the differences from these variables)
or choose the unknowns to be the differences (and retrieve the marks). Let’s try this second
approach and use the efficient AllDifferent constraint. There are n(n−1)
2
such differences.
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 Y. Let’s
order the differences so that we know which difference is represented by Y [i]. Figure 3.3
illustrates an ordered sequence of differences for a Golomb ruler of order 4.
1st
2nd
3rd
th
4
5th
6th
Figure 3.3: An ordered sequence of differences for the Golomb ruler of order 4.
Figure 3.4 illustrates the structures than must be respected for a Golomb ruler of or-
der 5. To impose the inner structure of the Golomb Ruler, we force Y4 = Y0 + Y1 , Y5 = Y1 + Y2
and so on as illustrated in Figure 3.4.
index
Y0 Y1 Y2 Y3
i=2 Y4 = Y0 + Y1 j=0
Y4 Y5 = Y1 + Y2 j=1
Y6 = Y2 + Y3 j=2
Y5
i=3 Y7 = Y0 + Y1 + Y2 j=0
Y6 Y8 = Y1 + Y2 + Y3 j=1
i=4 Y9 = Y0 + Y1 + Y2 + Y3 j=0
Y7
Y8
Y9
55
3.3. An implementation of the first model
An easy way to construct these equality constraints is to use an index index going from 4 to 97 ,
an index i to count the number of terms in a given equality and an index j to indicate the rank
of the starting term in each equality:
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 find 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.
In this section, we code the first model developed in section 3.2. You can find the code in the
file tutorials/cplusplus/chap3/golomb1.cc. We take the order (the number of
marks) from the command line:
DEFINE_int64(n, 5, "Number of marks.");
Several upper bounds exist on Golomb rulers. For instance, we could take n3 − 2n2 + 2n − 1.
Indeed, it can be shown that the sequence
Φ(a) = na2 + a 0 6 a < n.
forms a Golomb ruler. As its largest member is n3 − 2n2 + 2n − 1 (when a = n − 1), we have
an upper bound on the length of a Golomb ruler of order n:
G(n) 6 n3 − 2n2 + 2n − 1.
Most bounds are really bad and this one isn’t an exception. The great mathematician Paul Erdös
conjectured that
G(n) < n2 .
This conjecture hasn’t been proved yet but computational evidence has shown that the conjec-
ture holds for n < 65000 (see [Dimitromanolakis2002]).
This is perfect for our needs:
7
Or more generally from the index ofthe first difference
that is the sum of two differences in our sequence
n(n−1)
(n − 1) to the index of the last difference 2 −1 .
56
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
CHECK_LT(n, 65000);
const int64 max = n * n - 1;
We can now define our variables but instead of creating single instances of IntVars like this:
const int64 num_vars = (n*(n - 1))/2;
std::vector<IntVar*> Y(num_vars);
for (int i = 0; i < num_vars; ++i) {
Y[i] = s.MakeIntVar(1, max, StringPrintf("Y%03d", i));
}
Note that these two methods don’t 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 cre-
ate an std::vector<IntVar*> (or to append some IntVar*s to an existing
std::vector<IntVar*>).
StringPrintf() (shown in the first 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 func-
tion:
8
Remember the remark at the beginning of this chapter about the tricky sums!
57
3.4. What model did I pass to the solver?
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);
The model we developed to solve the cryptarithmetic puzzle in section 2.3 was quite simple.
The first model proposed to solve the Golumb Ruler Problem in the two previous sections is
more complex. We suppose our model is theoretically correct. How do we know we gave the
right model to the solver, i.e. how do we know that our implementation is correct? In this sec-
tion, we present two tools to debug the model we passed to the solver: the DebugString()
method and via the default command line flags of the CP solver.
58
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
These are exactly the constraints listed in Figure 3.4 page 55.
By default, the CP solver is able to return some information about the model. If you try
./golomb1 --help
in the terminal, you get all possible command line flags. For the file
constraint_solver.cc, these are:
Flags from src/constraint_solver/constraint_solver.cc:
-cp_export_file (Export model to file using CPModelProto.)
type: string default: ""
-cp_model_stats (use StatisticsModelVisitor on model before solving.)
type: bool default: false
-cp_name_cast_variables (Name variables casted from expressions)
type: bool default: false
-cp_name_variables (Force all variables to have names.)
type: bool default: false
-cp_no_solve (Force failure at the beginning of a search.)
type: bool default: false
-cp_print_model (use PrintModelVisitor on model before solving.)
type: bool default: false
-cp_profile_file (Export profiling overview to file.)
type: string default: ""
-cp_show_constraints (show all constraints added to the solver.)
type: bool default: false
-cp_trace_propagation (Trace propagation events (constraint and demon
executions, variable modifications).)
type: bool default: false
-cp_trace_search (Trace search events)
type: bool default: false
-cp_verbose_fail (Verbose output when failing.)
type: bool default: false
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:
59
3.4. What model did I pass to the solver?
All this output was generated from the following line in constraint_solver.cc:
LOG(INFO) << c->DebugString();
60
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
In section 3.1, we talked about some global statistics about the search. In this section we
review them one by one.
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 portable timer. This
timer starts to measure the time from the creation of the solver.
solver("TicTac") s; // Starts the timer of the solver.
If you need the elapsed time since the creation of the solver, 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
finishing time of a task and its starting time and not the actual time spent by the computer to
solve a problem.
For instance, on our computer, the program in golomb1.cc for n = 9 takes
Time: 4,773 seconds
3.5.2 Failures
A failure occurs whenever the solver has to backtrack whether it is because of a real failure,
a success or because the search is restarted or continued. There are two main statistics about
failures that you can retrieve like so:
Solver s("Global statistics");
LOG(INFO) << "Failures: " << s.failures() << std::endl;
LOG(INFO) << "Fail stamps: " << s.fail_stamp() << std::endl;
failures() returns the number of leaves in the search tree. This is exactly the number of
time the solver has to backtrack whenever it doesn’t find a solution or it does find a solution.
61
3.5. Some global statistics about the search and how to limit the search
Indeed, whenever a solution is found, the corresponding branch of the tree doesn’t need to grow
any further and the solver needs to backtrack to other branches to find other solutions.
fail_stamp() adds the failures counted by failures() with some additional internal
ones. These failures are specific to our algorithm and are not really important at this stage.
3.5.3 Branches
This one is easy as branches() returns exactly the number of branches in the search tree:
Solver s("I count my branches");
LOG(INFO) << "Branches: " << s.branches() << std::endl;
3.5.4 SearchLimits
When you try to solve a difficult problem, it might happen that the solver runs for a long time
without finding a solution or a good enough solution. In such cases, you might want to limit
the behaviour of the solver. Some other statistics can be bounded during the search.
The class SearchLimit permits to limit
• the time;
• the number of visited branches;
• the number of failures;
• the number of solutions found;
• another stopping criteria you define yourself.
You can limit these statistics for the whole search or for each sub-search where the solver tries
to find the next feasible solution.
If you are only interested in limiting one of these statistics, individual methods are provided:
SearchLimit* Solver::MakeTimeLimit(int64 time);
SearchLimit* Solver::MakeBranchesLimit(int64 branches);
SearchLimit* Solver::MakeFailuresLimit(int64 failures);
SearchLimit* Solver::MakeSolutionsLimit(int64 solutions);
These methods only apply to the individual sub-searches and each time a sub-search is started,
counters are set to 0 again.
As SearchLimits are SearchMonitors, you can pass them as arguments to the solver’s
NewSearch() or Solve() methods:
Solver s("Don’t want to fail too much");
...
SearchLimit * const fail_limit = s.MakeFailuresLimit(3);
DecisionBuilder * const db = s.MakePhase(...);
...
s.Solve(db, fail_limit, ...);
62
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
If you want to limit some statistics for the whole search, you can use the following method:
SearchLimit* Solver::MakeLimit(int64 time,
int64 branches,
int64 failures,
int64 solutions,
bool smart_time_check,
bool cumulative);
Setting smart_time_check to true entails that, for a certain number of calls, the real
time limit will not be tested9 . This is done to avoid the costly call to wall_time().
If you want the statistics to be tested globally, set cumulative to true. If you don’t
want to impose a limit say on the number of failures, pass the maximum number of failures
(kint64max). These limits are tested 10 in SearchMonitors BeginNextDecision()
and RefuteDecision() methods. You’ll learn about these two methods (and a bunch of
others) in section 5.3.3. These are the logical places to test for the number of branches, failures
and solutions but it is good to know if you want to implement your own custom limit.
SearchLimits can be combined:
Solver s("I combine my limits");
...
SearchLimit * const limit1 = s.MakeLimit(...);
SearchLimit * const limit2 = s.MakeLimit(...);
SearchLimit * const ORLimit = s.MakeLimit(limit1, limit2);
The ORLimit is reached when either of the underlying SearchLimit limit1 or limit2
is reached.
You also can implement your own SearchLimit with a callback:
SearchLimit* MakeCustomLimit(ResultCallback<bool>* limiter);
If the Run() method returns true at a leaf of the search tree, the corresponding
solution will be rejected!
In subsection 6.7.5 about solving the job-shop problem with local search, we will con-
struct our own SearchLimit. SearchLimits can also be updated during the search using
the following method:
void Solver::UpdateLimits(int64 time,
int64 branches,
int64 failures,
int64 solutions,
SearchLimit* limit);
9
See the method RegularLimit::TimeDelta() in file search.cc if you want to know the details.
10
To be precise, some limits are tested at some other places of the search algorithm depending on the type of
search.
63
3.6. A second model and its implementation
Our first model is really bad. One of the reasons is that we use too many variables: n(n−1)
2
dif-
ferences. What happens if we only consider the n marks as variables instead of the differences?
3.6.1 Variables
We use an std::vector slightly bigger (by one more element) than absolutely necessary.
Because the solver doesn’t allow NULL pointers, we have to assign a value to X[0]. The first
mark X[1] is 0. We use again n2 − 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 don’t 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 constraints11 :
We don’t need all combinations of (i, j, k, l) with i 6= k and j 6= l. For instance, combination
(3, 2, 6, 4) and combination (2, 3, 4, 6) would both give the same constraint. One way to avoid
such redundancy is to impose an order on unique positive differences12 . Take again n = 4 and
define the sequence of differences as in Figure 3.5.
With this order defined on the differences, we can easily generate all the quaternary constraints.
Take the first 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
11
Quaternary constraints is just a fancy way to say that the constraints each involves four variables.
12
In section Breaking symmetries with constraints we’ll use another trick.
64
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
1st
2nd
3rd
4th
5th
6th
Figure 3.5: Another ordered sequence of differences for the Golomb ruler of order 4.
1st 6= 2nd
6 = 3rd
6 = 4th
6 = 5th
6 = 6th
difference and impose it to be different from the third difference, then to be different from the
fourth difference and so on as suggested in Figure 3.7.
2nd 6= 3rd
6= 4th
6= 5th
6= 6th
We define a helper function that, given a difference (i, j) corresponding to an interval X[j] −
X[i] computes the next difference in the sequence:
bool next_interval(const int n, const int i, const int j, int* next_i,
int* next_j) {
CHECK_LT(i, n);
CHECK_LE(j, n);
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;
65
3.6. A second model and its implementation
return true;
}
If there is a next interval, the function next_interval() returns true, false otherwise.
We can now construct our quaternary constraints13 :
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()
doesn’t 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.
Let’s compare the first and second implementation. The next table compares some global
statistics about the search for G(9).
Statistics Impl1 Impl2
Time (s) 4,712 48,317
Failures 51 833 75 587
Branches 103 654 151 169
Backtracks 51 836 75 590
If the first model was bad, what can we say about this one? What went wrong? The quaternary
constraints... These constraints are all disparate and thus don’t allow efficient propagation.
66
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
and compare this improved version with the two others, again to compute G(9):
Statistics Impl1 Impl2 Impl2+
Time (s) 4,712 48,317 1,984
Failures 51 833 75 587 53 516
Branches 103 654 151 169 107 025
Backtracks 51 836 75 590 53 519
Although we have more failures, more branches and we do backtrack more than in the first
model, we were able to divide the time by 2! Can we do better? You bet!
By using the same variables Y[i][j] = X[j] - X[i] in our improved version of our
second model in the previous section, we were able to couple the effect of the propagations of
the “quaternary constraints”. But these constraints lack a global vision of the propagation and
this is were the global AllDifferent constraint comes into the picture.
67
3.8. How to tighten the model?
}
s.AddConstraint(s.MakeAllDifferent(Y));
Generally speaking, if we are able to reduce the size of the search tree (to tighten the model),
we can speed up the search. We are talking about visiting (preferably implicitly) the whole
search tree to be able to prove optimality (other techniques exist to find good nodes in the
search tree). We present two14 such techniques here. Breaking symmetries allows to disregard
entire subtrees in the search tree that wouldn’t bring any new information to the search
while bounding reduces the variable domains and thus reduces the number of branching and
augments the efficiency of the propagation techniques15 .
In the section 3.6, when we declared the variables X representing the marks of a Golomb ruler,
we implicitly took for granted that X[1] < X[2] < ... < X[n]. That is exactly what
we did when we imposed the differences to be positive:
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));
14
There exist other techniques. Later, we will see how over-constraining can improve the search.
15
This short explanation is certainly too simple to describe all the subtleties of search strategies. After all,
modelling is an art!
68
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
k = next_k;
l = next_l;
}
}
}
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 X[1], X[2], X[3], X[4] have only increasing values, i.e. if i 6 j then X[i] 6
X[j]. Solutions (3.1) and (3.2) are said to be symmetric and avoiding the second one while
accepting the first one is called breaking symmetry.
There is a well-known symmetry in the Golomb Ruler Problem that we didn’t 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.
0 1 4 6
6 5 2 0
Golomb ruler {0, 1, 4, 6} has {0, 2, 5, 6} as mirror Golomb ruler. Both have exactly the same
length and can be considered symmetric solutions. To break this symmetry and allow the search
for the first one but not the second one, just add X[2]-X[1] < X[n] - X[n-1]:
s.AddConstraint(s.MakeLess(s.MakeDifference(X[2],X[1])->Var(),
s.MakeDifference(X[n],X[n-1])->Var()));
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 fly during the search!
16
Declaring variables in an std::vector doesn’t tell anything about their respective values!
69
3.8. How to tighten the model?
In all implementations, we used n2 − 1 as an upper bound on G(n). In the case of the Golomb
Ruler Problem, finding good upper bounds is a false problem. Very efficient techniques exist
to find optimal or near optimal upper bounds. If we use those bounds, we reduce dramatically
the domains of the variables. We can actually use G(n) as an upper bound for n 6 25 as these
bounds can be obtained by projective and affine projections in the plane17 .
The search can also benefit from lower bounds. Every difference must in itself be a Golomb
ruler. Thus Y[i][j] can be bounded by below by the corresponding optimal Golomb ruler.
In this section, we use a 2-dimensional array to collect the differences: Y[i][j] = X[j]
- X[i]:
std::vector<std::vector<IntVar *> > Y(n + 1,
std::vector<IntVar *>(n + 1));
for (int i = 1; i < n; ++i) {
for (int j = i + 1; j <= n; ++j) {
Y[i][j] = s.MakeDifference(X[j], X[i])->Var();
if ((i > 1) || (j < n)) {
Y[i][j]->SetMin(kG[j-i +1]); // Lower bound G(j - 1 + 1)
} else {
Y[i][j]->SetMin(kG[j-i] + 1); // Lower bound on Y[1][n] (i=1,j=n)
}
}
}
These are static bounds, i.e. they don’t 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
70
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem
so
The differences on the right hand side of this expression are a set of different integers and there
are n − 1 − j + i of them. If we minimize the sum of these consecutive differences, we actually
maximize the right hand side, i.e. we bound Y [i][j] from above:
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));
}
}
Let’s compare our tightened third implementation with the rest, again to compute G(9):
Statistics Impl1 Impl2 Impl2+ Impl3 tightened Impl3
Time (s) 4,712 48,317 1,984 0,338 0,137
Failures 51 833 75 587 53 516 7 521 2288
Branches 103 654 151 169 107 025 15 032 4572
Backtracks 51 836 75 590 53 519 7 524 2291
The interested reader can find other dynamic bounds in [GalinierEtAl].
71
3.10. Summary
but beware that by default, there isn’t any smart algorithm implemented in or-tools to change
this improvement step during the search. If the solver finds a solution with an objective value of
237, it will try next to find a solution with an objective value of 19518 . If it finds one, it will try
to find another solution with an objective value of 153 but if doesn’t find any feasible solution
with a value of 195, it will stop the search and return the “best” solution with a value of 237!
If you use an improvement step of 1, you can be sure to reach an optimal solution if you provide
the solver with enough time and memory but you could also devise your own algorithm to
change this improvement step during the search.
3.10 Summary
18
It took us a while but we are pretty sure that 237 − 42 = 195.
72
CHAPTER
FOUR
REIFICATION
Overview:
Overview...
Prerequisites:
Files:
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 flexible and comes with several tools (Decisions,
DecisionBuilders, ...) that we call search primitives. Some are predefined and can be
used right out of the box while others can be customized thanks to callbacks. You can also
combine different search strategies. If needed, you can define your own search primitive classes
by inheriting from base classes.
To efficiently 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 that we have introduced in chapter 1.
Overview:
We first discuss the n-Queen Problem and present a basic model to solve it. To go one step
further and devise a search algorithm to solve this problem, we present a little bit the inner
working of the solver. Specifically, we show how it traverses and constructs the search tree.
We even can visualize the search thanks to the wonderful cpviz framework that we introduce
next. Equipped with this knowledge and visualization capacity, we can better understand out
of the box and customized primitives and apply them to solve the n-Queen Problem. We end
this chapter with a relatively advanced feature: SymmetryBreakers that allow to break
symmetries during the search (on the fly).
Prerequisites:
• Basic knowledge of Constraint Programming and the n-Queens Problem (see chapter 1).
• Basic knowledge of the Constraint Programming Solver (see chapter 2).
• The willingness to roll up your sleeves and be prepared to look a little under the hood.
Files:
We have discussed the 4-Queens Problem (and defined its solutions) in chapter 1. We will now
generalize this problem to an arbitrary number n.
78
Chapter 5. Defining search primitives: the n-Queens Problem
n: 1 2 3 4 5 6 7 8 9 10 11 12 13 14
unique: 1 0 0 1 2 1 6 12 46 92 341 1,787 9,233 45,752
distinct: 1 0 0 2 10 4 40 92 352 724 2,680 14,200 73,712 365,596
Notice that there are more solutions for n = 5 than n = 6. What about the last three known
values? Here there are:
n: 24 25 26
unique: 28,439,272,956,934 275,986,683,743,434 2,789,712,466,510,289
distinct: 227,514,171,973,736 2,207,893,435,808,352 22,317,699,616,364,044
Quite impressive, isn’t it? It’s even more impressive when you know that these numbers were
obtained by explicitly finding all these solutions!
You can learn much more about this problem and the best available techniques5 from the cur-
rent world record (n = 26) holder: the Queens@TUD team from the Technische Universität
Dresden: http://queens.inf.tu-dresden.de/?l=en&n=0.
2
In computer science jargon, we say that the problem of finding one solution for the n-Queens Problem is in
P . Actually, it’s the decision version of this problem but to keep it simple, let’s say that finding one solution is
straightforward and easy and shouldn’t take too long.
3
By solution, we mean feasible solution.
4
These two problems are NP-Hard. See [Jordan2009].
5
This time, backtracking and thus Constraint Programming are among the most efficient techniques. However,
to compute all the solutions for n = 26, there is no way a general purpose CP solver can compete with specialized
bitwise representations of the problem and massively parallel specialized hardware!
79
5.1. The n-Queens Problem
We follow again the classical three-stage method described in section 1.5: describe, model and
solve.
Describe
What is the goal of the n-Queens Problem? We will focus on finding one or all solutions. Given
a size n for the n × n chessboard, place n queens6 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. As we know
that we can have at maximum one queen per column (or row), we can use one variable per
column (or row) to denote the presence of a queen or not.
What are the constraints? No two queens can attack each other. This means to place n 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 queen7 . Figure 5.1 illustrates the variables
we will use to solve the n-Queens Problem in this chapter.
The solution depicted is {x0 = 2, x1 = 0, x2 = 3, x3 = 1}. The fact that the queens cannot
be on the same column is directly encoded into the model without needing a constraint. The
domains of the variables ([0, n−1]) also ensure that every column will be populated by a queen.
We have to ensure that the variables cannot take the same value. This is easily done with
AllDifferent(x0 , . . . , xn−1 ). We have to ensure that no two queens can be on the same
diagonal. It would be nice to have the variables on the diagonals so that we could use again the
AllDifferent constraint. Actually, we know when two queens are on the same diagonal.
We’ll use a known trick to model this constraint in the next section.
6
In fact, for n = 2 and n = 3 there are no solution and we have seen that for every n > 4, there exist at least
a solution by the construction given in [Hoffman1969].
7
We start counting at 0, right?
80
Chapter 5. Defining search primitives: the n-Queens Problem
Solve
This time we will... test some search strategies. We will not devise a good search strategy
because we don’t know yet what possibilities are implemented in the CP solver. We will test
different search strategies and see what works and why.
We have introduced the ideas of a basic model in the previous section. We complete
this model and implement it in this section.
The model is defined in the NQueens() function. The beginning of the function shouldn’t
surprise you:
void NQueens(int size) {
CHECK_GE(size, 1);
Solver s("nqueens");
// model
std::vector<IntVar*> queens;
for (int i = 0; i < size; ++i) {
queens.push_back(s.MakeIntVar(0, size - 1,
StringPrintf("queen%04d", i)));
}
s.AddConstraint(s.MakeAllDifferent(queens));
...
This AllDifferent(x0 , . . . , xn−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.
Of course, this is not what we want. To forbid two queens to be on the same diagonal with
slope +1 (diagonals that slope up-and-right), we could impose non-equality relations between
81
5.2. Implementation of a basic model
our variables. For instance, to impose that the first queen represented by x0 doesn’t attack any
other queen on those diagonals, we can impose that
x0 − 1 6= x1 , x0 − 2 6= x2 , x0 − 3 6= x3 , . . . (5.1)
(5.1) is equivalent to
x0 6= x1 + 1, x0 6= x2 + 2, x0 6= x3 + 3, . . . (5.2)
Take the second queen x1 . We only have to look for the queens to her right. To impose that x1
doesn’t attack any queen x2 , x3 , . . . on a diagonal with slope +1, we can add
x1 − 1 6= x2 , x1 − 2 6= x3 , x1 − 3 6= x4 , . . . (5.3)
or equivalently
x1 6= x2 + 1, x1 6= x3 + 2, x1 6= x4 + 3, . . . (5.4)
In general, for queen xi , we impose that xi 6= xj + j − i. Now, here comes the trick. If you add
1 to all members of (5.4), you get
x1 + 1 6= x2 + 2, x1 + 1 6= x3 + 3, x1 + 1 6= x4 + 4, . . . (5.5)
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3, x4 + 4, . . .) (5.6)
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3, x4 − 4, . . .) (5.7)
ensures that no two queens are on the same diagonal with slope −1 (diagonals that slope down-
and-right).
We can thus add:
8
∀ j : j > i simply means that we consider all j greater than i.
82
Chapter 5. Defining search primitives: the n-Queens Problem
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 first 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);
In the next sections, we will test different DecisionBuilders. To help us print and test
solutions for the n-Queens Problem, we will use two helper functions defined next.
83
5.2. Implementation of a basic model
} else {
if (size - 1 < kKnownSolutions) {
CHECK_EQ(num_solutions, kNumSolutions[size - 1]);
} else if (!FLAGS_cp_no_solve) {
CHECK_GT(num_solutions, 0);
}
}
return;
}
return;
}
You might wonder why we cast the return value of collector->Value() into an int?
The value() method returns an int64.
84
Chapter 5. Defining search primitives: the n-Queens Problem
#include "base/commandlineflags.h"
#include "base/logging.h"
#include "base/stringprintf.h"
#include "constraint_solver/constraint_solver.h"
For the moment we don’t 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) << "Symmetry breaking methods 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 first solution (flag print set to true) or all solutions (flag
print_all set to true). By default, the program doesn’t output any solution.
Because finding all solutions is hard, we expect the solver to face more and more diffi-
culties as the size n grows but what about the easy problem of finding only one solution?
In the file nqueens2.cc, we stop the search as soon as a solution has been found.
The following Table collects the results of our experiment with the same DecisionBuilder
and same model as above. The results are given in seconds.
Problem 10 11 12 13 14
First solution 0 0 0 0 0,003
All Solutions 0,055 0,259 1,309 7,059 40,762
85
5.3. Basic working of the solver: the search algorithm
To find all solutions, the solver shows a typical exponential behaviour for intractable problems.
The sizes are too small to conclude anything about the problem of finding one solution. In the
next Table, we try bigger sizes. The results are again in seconds.
Problem 25 26 27 28 29 30 31
First solution 0,048 0,392 0,521 3,239 1,601 63,08 14,277
It looks like our solver has some troubles to find one solution. This is perfectly normal because
we didn’t use a specific 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. define strategies of our
own but before we do so, we need to learn a little bit about the basic working of the solver.
Wikipedia9 has a very nice animation of our search strategy (CHOOSE_FIRST_UNBOUND
and ASSIGN_MIN_VALUE) to find the first feasible solution:
https://en.wikipedia.org/wiki/Eight_queens_puzzle#Animation_of_the_recursive_solution
Compare the output of:
./nqueens1 -size=8 -print
Figure 5.3: The first solution obtained with our default search strategy.
86
Chapter 5. Defining search primitives: the n-Queens Problem
The real implementation is more complex (and a little bit different!) and deals with other cases
not mentioned here (especially nested searches and restarting the search). For the juicy details,
we refer the reader to chapter 13 or the source code itself.
Let’s agree on some wording we will use throughout this chapter and the rest of the manual.
Search trees
A search tree represents the search space that the search algorithm will, implicitly or explicitly,
traverse or explore. Each node of the tree corresponds to a state of the search. Take an array
of variables x[] and a valid index i. At one node in the search tree, we divide the search space
in two exclusive search subspaces by imposing x[i] = 2 at one branch and x[i] 6= 2 at another
branch like in Figure 5.4.
xi = 2 xi 6= 2
Each subspace is now smaller and we hope easier to solve. We continue this divide and conquer
mechanism until we know that a subspace doesn’t contain a feasible solution or if we find all
feasible solutions of a subtree. The first node is called the root node and represent the complete
search space.
When we divide the search space by applying a decision (x[i] = 2) in one branch and by
refuting this decision (x[i] 6= 2) in another, we obtain a binary search trees10 . This way of
dividing the search tree in two is basically the algorithm used by the CP solver to explore a
search tree.
The divide mechanism can be more complex. For instance by dividing a subspace in more
than two subspaces. The subspaces don’t need to be mutually exclusive, you can have different
numbers of them at each node, etc.
10
Not to be confused with a binary search tree (BST) used to store ordered sets.
87
5.3. Basic working of the solver: the search algorithm
Callbacks
At the beginning of a search, the solver calls the virtual method EnterSearch() i.e. your
EnterSearch() method. Don’t 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.7.1).
11
In design pattern jargon, we talk about the Template method design pattern. See
http://en.wikipedia.org/wiki/Template_method_pattern for more. And by the way, the name template has
nothing to do with C++ templates.
88
Chapter 5. Defining search primitives: the n-Queens Problem
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.
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.
The basic idea12 is very simple yet effective. A DecisionBuilder is responsible to return
a Decision at a node. A decision would be for instance, x[4] = 3. We divide the sub search
tree at this node by applying this decision (left branch: x[4] = 3) and by refuting this decision
(right branch: x[4] 6= 3).
At the current node, the DecisionBuilder of the current search returns a Decision.
The Decision class basically tells the solver what to do going left (Apply()) or right
(Refute()) as illustrated on the next figure.
Decision
Apply() Refute()
From the root node, we follow the left branch whenever possible and backtrack to the first
available right branch when needed. When you see a search tree produced by the CP solver,
you can easily track the search by following a preorder traversal of the binary search tree.
There are basically two ways to ask the CP solver to find a solution (or solutions) as we
have seen in chapter 2. Either you configure SearchMonitors and you call the Solver‘s
Solve() method, either you use the finer grained NewSearch() - NextSolution() -
EndSearch() mechanism. In the first case, you are not allowed to interfere with the search
process while in the second case you can act every time a solution is found. Solve() is
implemented with this second mechanism:
12
The real code deals with a lots of subtleties to implement different variants of the search algorithm.
89
5.3. Basic working of the solver: the search algorithm
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 first 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 enter-
ing/exiting search, applying/refuting decisions, failing, accepting solutions... In this section,
we present the callbacks of the SearchMonitor class13 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 simplified version
of the search algorithm. In particular, we don’t show how the nested searches and the restart of
a search are implemented. We find this so important that we reuse our warning box:
We use exceptions in our simplified version while the actual implementation uses the more
efficient (and cryptic) setjmp - longjmp mechanism.
We describe briefly 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
13
There are a few more callbacks defined in a SearchMonitor. See 13.8.1
90
Chapter 5. Defining search primitives: the n-Queens Problem
Table 5.1: Basic search algorithm callbacks from the SearchMonitor class.
Methods Descriptions
EnterSearch() Beginning of the search.
ExitSearch() End of the search.
BeginNextDecision(DecisionBuilder* Before calling DecisionBuilder::Next().
const b)
EndNextDecision(DecisionBuilder* After calling DecisionBuilder::Next(),
const b, Decision* const d) along with the returned decision.
ApplyDecision(Decision* const d) Before applying the Decision.
RefuteDecision(Decision* const d) Before refuting the Decision.
AfterDecision(Decision* const d, Just after refuting or applying the Decision,
bool apply) apply is true after Apply(). This is called only
if the Apply() or Refute() methods have not
failed.
BeginFail() Just when the failure occurs.
EndFail() After completing the backtrack.
BeginInitialPropagation() Before the initial propagation.
EndInitialPropagation() After the initial propagation.
AcceptSolution() This method is called when a solution is found. It
asserts if the solution is valid. A value of false in-
dicates that the solution should be discarded.
AtSolution() 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.
NoMoreSolutions() When the search tree has been visited.
details14 .
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 Meaning
OUTSIDE_SEARCH Before search, after search.
IN_ROOT_NODE Executing the root node.
IN_SEARCH Executing the search code.
AT_SOLUTION After successful NextSolution() and before EndSearch().
NO_MORE_SOLUTIONS After failed NextSolution() and before EndSearch().
PROBLEM_INFEASIBLE After search, the model is infeasible.
NewSearch()
This is how the NewSearch() method might have looked in a simplified 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.
14
Of course, you can have a peak right now but some more background will probably help you understand this
mechanism better. Beside, you don’t need to understand the inner mechanism to be able to use nested search!
91
5.3. Basic working of the solver: the search algorithm
8 // Init:
9 // Install the main propagation monitor
10 // Install DemonProfiler if needed
11 // Install customer’s SearchMonitors
12 // Install DecisionBuilder’s SearchMonitors
13 // Install print trace if needed
14 ...
15
21 state_ = IN_ROOT_NODE;
22 search->BeginInitialPropagation(); // SEARCHMONITOR CALLBACK
23
24 try {
25 // Initial constraint propagation
26 ProcessConstraints();
27 search->EndInitialPropagation(); // SEARCHMONITOR CALLBACK
28 ...
29 state_ = IN_SEARCH;
30 } catch (const FailException& e) {
31 ...
32 state_ = PROBLEM_INFEASIBLE;
33 }
34
35 return;
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 finds 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. SearchMonitor‘s callbacks are indicated by
the comment:
// SEARCHMONITOR CALLBACK
Here is how it might have looked in a simplified version of the main search algorithm:
92
Chapter 5. Defining search primitives: the n-Queens Problem
1 bool Solver::NextSolution() {
2 Search* const search = searches_.back();
3 Decision* fd = NULL;// failed decision
4
93
5.3. Basic working of the solver: the search algorithm
56 fd = NULL;
57 }
58
94
Chapter 5. Defining search primitives: the n-Queens Problem
111 ...
112 state_ = ...;
113
Let’s 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. When-
ever 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 solver’s 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 defined15
• 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 thrown16 .
If the Decision pointer fd is not NULL, this means that we have backtracked to the first
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
15
These two variables play a role when we use nested searches, restart or finish a search but these possibilities
are not shown here.
16
Did we already mention that the try - catch mechanism is not used in the production code? ;-)
95
5.3. Basic working of the solver: the search algorithm
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 pos-
sibility on line 73. If the DecisionBuilder found a next Decision, it is applied on line
75.
Whenever the solver cannot find 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 finding 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() re-
turning false, search->AcceptSolution() returns false. On the con-
trary, 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 so-
lution 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 defined as a leaf of the search tree with respect to the given
DecisionBuilder for which there is no failure. What this means is that, con-
trary 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.
96
Chapter 5. Defining search primitives: the n-Queens Problem
EndSearch()
The EndSearch() method cleans the solver and if required, writes the profile of the search
in a file. It also calls the ExitSearch() callbacks of all installed SearchMonitors.
Here is how it might have looked in a simplified version of the main search algorithm.
1 void Solver::EndSearch() {
2 Search* const search = searches_.back();
3 ...
4 search->ExitSearch();// SEARCHMONITOR CALLBACK
5 search->Clear();
6 state_ = OUTSIDE_SEARCH;
7 if (!FLAGS_cp_profile_file.empty()) {
8 LOG(INFO) << "Exporting profile to " << FLAGS_cp_profile_file;
9 ExportProfilingOverview(FLAGS_cp_profile_file);
10 }
11 }
To get a better feeling of the way the CP solver explores the search tree, we will use the
wonderful open-source visualization toolkit for finite domain constraint programming cpviz.
Here is a description from their website of what this toolkit provides:
It provides visualization for search trees, variables and global
constraints through a post-mortem analysis of trace logs.
The important trick to understand is that the visualization is only available after the search is
done.
Please find all necessary information and tools at:
http://sourceforge.net/projects/cpviz/
97
5.4. cpviz: how to visualize the search
Basically, it tells cpviz to produce the graphic files for the search tree (show="tree") and
the variables (show="viz") in the directory /tmp17 .
If you are really lazy, we even provide a factory method which generates automatically a default
configuration file:
SearchMonitor* const cpviz = s.MakeTreeMonitor(vars,
"configuration.xml",
"tree.xml",
"visualization.xml");
After your search is finished and you have called (implicitly or explicitly) EndSearch()18 ,
you can run cpviz to digest the XML files 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 n = 4 and cpviz generates 8 files.
17
/tmp is a temporary directory in linux. Change this directory accordingly in you are working under another
OS.
18
tree.xml and visualization.xml are generated in the ExitSearch() callback of the
TreeMonitor class.
98
Chapter 5. Defining search primitives: the n-Queens Problem
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
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 x[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).
To better understand the output of cpviz and to follow the search with precision, let’s
trace the search and the propagation of our program nqueens4:
./nqueens4 --size=4 --cp_trace_search --cp_trace_propagation 2>
cpviz_nqueens4_basic.txt
AllDifferent(x0 , x1 , x2 , x3 )
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3)
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3)
By reading the file cpviz_nqueens4_basic.txt, we can retrace the search and recon-
struct the search tree:
99
5.4. cpviz: how to visualize the search
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2
node 8
node 7
x1 = 0 x1 6= 0
node 10
node 9
As you can see, at each node, the solver took a Decision: the left branch to apply the
Decision and the right branch to refute this Decision. The leaf nodes in red denote sub-
trees that are not worth exploring explicitly: we cannot find any feasible solution along these
branches of the tree. The leaf nodes in green denote on the contrary feasible solutions. The
nodes are numbered in the order of creation and we can see that the search tree is traversed in
pre-order by the solver.
In the file nqeens4.cc, we have printed some statistics about the search:
std::cout << "Number of solutions: " << num_solutions << std::endl;
std::cout << "Failures: " << s.failures() << std::endl;
std::cout << "Branches: " << s.branches() << std::endl;
std::cout << "Backtracks: " << s.fail_stamp() << std::endl;
std::cout << "Stamps: " << s.stamp() << std::endl;
Let’s see if we can deduce these statistics from the search tree. The three first statistics are easy
to spot in the tree:
Number of solutions (2): There are indeed two distinct solutions denoted by the
two green leafs.
100
Chapter 5. Defining search primitives: the n-Queens Problem
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 finds a solution, it has to backtrack to find other
solutions. The method failures() returns the number of leaves of the
search tree. In our case, 6.
Branches (10): Number of branches in the tree, indeed 10.
The two last statistics are more difficult to understand by only looking at the search
tree.
Backtracks (9): Because of the way the search is coded, the fail_stamp
counter starts already at 2 before any top level search. There are 6 failures
(one for each node, see Failures above) and this brings the counter to 8. To
end the search, a last backtrack19 is necessary to reach the root node and undo
the search which brings the counter to 9.
Stamps (29): This statistic is more an internal statistic than a real indicator of the
search. It is related to the queue actions during the search. The queue is
responsible for the propagation which occurs when one or more variables
domains change. Every time the propagation process is triggered, the stamp
counter is increased. Other queue actions also increase this counter. For
instance, when the queue is frozen. For a simple search, this statistic is more
or less equivalent to the length of a pre-order traversal of the search tree (20
in our case). This statistic reflects the amount of work needed by the solver
during the search. We refer the curious reader to the source code for more
details.
How can we compare the real tree with our cpviz output? The trick is to observe the con-
struction of the tree one node at a time. We construct the real tree node by node from the tree
produced by cpviz. The left image is the cpviz output while the right image is the actual tree.
Step 0:
We start with a dummy node. This node is needed in our construction. You’ll see in a moment
why.
Figure 5.7: Contruction of the real search tree from the cpviz tree: step 0
19
Actually, the very last backtrack happens when the solver is deleted.
101
5.4. cpviz: how to visualize the search
Step 1:
node 0
Figure 5.8: 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 doesn’t even have a name and
the little number 0 next to this non existing name doesn’t mean
anything.
Step 2:
node 0
x0 = 0
node 1
Figure 5.9: 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 x0 = 0 but that
it couldn’t realize if this was a good choice or not. The little number 4 next to the variable
name x0 means that before the decision was applied, the number of values in its domain was 4.
Indeed: x0 ∈ {0, 1, 2, 3} before being assigned the value 0.
102
Chapter 5. Defining search primitives: the n-Queens Problem
Step 3:
node 0
x0 = 0
node 1
x1 = 2
node 2
Figure 5.10: Construction of the real search tree from the cpviz tree: step 3
After having applied the Decision x0 = 0 at step 2, the solver now applies the Decision
x1 = 2 which leads, after propagation, to a failure.
Step 4:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x1 = 2 x1 6= 2
node 2 node 3
Figure 5.11: Construction of the real search tree from the cpviz tree: step 4
Our cpviz output now clearly warns that taking x0 = 0 does not lead to a feasible solution.
This can only mean that the solver tried also to refute the Decision x1 = 2. So we know that
the branch x1 6= 2 after the branch x0 = 0 is leading nowhere. We have to backtrack and to
refute the Decision x0 = 0. We have thus a new branch x0 6= 0 in the real search tree.
103
5.4. cpviz: how to visualize the search
Step 5:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2
Figure 5.12: Construction of the real search tree from the cpviz tree: step 5
We find a feasible solution when x0 = 1. Thus we add the branch x0 = 1 and indicate success.
Step 6:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 = 2
node 7
Figure 5.13: Construction of the real search tree from the cpviz tree: step 6
104
Chapter 5. Defining search primitives: the n-Queens Problem
Step 7:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2
node 8
node 7
Figure 5.14: 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
x2 = 0 that lead to a feasible solution, so now we know that the solver is trying to refute that
decision: x2 6= 0.
Step 8:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2
node 8
node 7
x1 = 0 x1 6= 0
node 10
node 9
Figure 5.15: Construction of the real search tree from the cpviz tree: step 8
The final step is the branch x1 = 0 that leads to a failure. This means that when we apply and
refute x1 = 0, we get a failure. Thus we know that x0 = 1 and x0 6= 1 both fail.
105
5.4. cpviz: how to visualize the search
Propagation
To better understand the search, let’s have a look at the propagation in details. First, we look at
the real propagation, then at our cpviz output.
You can find an animated version of the propagation here.
We start at the root node with
node 0: x0 ∈ {0, 1, 2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We apply the
Decision x0 = 0 which corresponds to our search strategy.
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x1 : 1 , x2 : 2 , x3 : 3
AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 0 , x2 : 0 , x3 : 0
x0 ∈ {0}, x1 ∈ {2, 3}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. No more propagation is possible. We
then apply the Decision x1 = 2
node 2: x0 ∈ {0}, x1 ∈ {2}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. The propagation is as follow:
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x2 : 3
106
Chapter 5. Defining search primitives: the n-Queens Problem
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x2 : 1
node 3: x0 ∈ {0}, x1 ∈ {3}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. x1 is fixed to 3 because we removed
the value 2 of its domain (refuting the Decision x1 = 2).
Propagation:
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x3 : 1
AllDifferent(x0 , x1 , x2 , x3 ) :
x2 : 3
107
5.4. cpviz: how to visualize the search
node 4: x0 ∈ {1, 2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We apply
Decision x0 = 1 which complies with our search strategy.
108
Chapter 5. Defining search primitives: the n-Queens Problem
AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 1 , x2 : 1 , x3 : 1
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x2 : 2
AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 3
AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 0
109
5.4. cpviz: how to visualize the search
node 6: x0 ∈ {2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We apply the
Decision x0 = 2.
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x1 : 3
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x1 : 1 , x2 : 0
AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 2 , x2 : 2 , x3 : 2
110
Chapter 5. Defining search primitives: the n-Queens Problem
111
5.4. cpviz: how to visualize the search
node 8: x0 ∈ {3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. x0 is fixed because
there is only one value left in its domain.
Propagation:
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x1 : 2 , x2 : 1 , x3 : 0
x0 ∈ {3}, x1 ∈ {0, 1}, x2 ∈ {0, 2}, x3 ∈ {1, 2}. No more propagation. We thus apply
our search strategy and apply Decision x1 = 0.
112
Chapter 5. Defining search primitives: the n-Queens Problem
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
113
5.4. cpviz: how to visualize the search
nodes in the tree, we are constrained to display very little information about the propagation
process. In short, if we find
• a try node, we display the final propagation at this node;
• a solution, we display the solution;
• a failure, we display the first failure encountered and the values of the assigned variables.
We also display what variable we focus on next.
Let’s go again through the 9 steps. We display in the left col-
umn our cpviz tree output, in the middle column the actual search
tree and in the right column our cpviz output of the propagation.
Step 0:
Nothing happens as we add a dummy root node. Notice that the variables are numbered from
1 to 4.
114
Chapter 5. Defining search primitives: the n-Queens Problem
Step 1:
node 0
(a) cpviz tree (b) Real search tree (c) cpviz propagation
The yellow rectangle tells us that the focus is on variable 1(x0 ), which means that at the next
step a value will be assigned to this variable.
Step 2:
node 0
x0 = 0
node 1
(a) cpviz tree (b) Real search tree (c) cpviz propagation
The red square indicates that the variable x0 was fixed to 0. The dark green squares show the
propagation. The focus is on variable 2 (x1 ).
115
5.4. cpviz: how to visualize the search
Step 3:
node 0
x0 = 0
node 1
x1 = 2
node 2
(a) cpviz tree (b) Real search tree (c) cpviz propagation
The red rectangle warns of a failure: there is no feasible solution with x0 = 0 and x1 = 2.
Step 4:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x1 = 2 x1 6= 2
node 2 node 3
(a) cpviz tree (b) Real search tree (c) cpviz propagation
There is not much information here: only that the last variable tried was x1 and that we ended
up with a failure.
116
Chapter 5. Defining search primitives: the n-Queens Problem
Step 5:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2
(a) cpviz tree (b) Real search tree (c) cpviz propagation
Solution found.
Step 6:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 = 2
node 7
(a) cpviz tree (b) Real search tree (c) cpviz propagation
Solution found.
117
5.4. cpviz: how to visualize the search
Step 7:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2
node 8
node 7
118
Chapter 5. Defining search primitives: the n-Queens Problem
Step 8:
node 0
x0 = 0 x0 6= 0
node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1
node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2
node 8
node 7
x1 = 0 x1 6= 0
node 10
node 9
A phase corresponds to a type of (sub)search in the search tree20 . You can have several
phases/searches in your quest to find a feasible or optimal solution. In or-tools, a phase is
constructed by and corresponds to a DecisionBuilder. We postpone the discussion on
the DecisionBuilders and Decisions for scheduling until the dedicated section 6.3.4
in the next chapter.
To better understand how phases and DecisionBuilders work, we will implement our own
DecisionBuilder and Decision classes in section 5.7. In this section, we show you how
to use these primitives and some very basic examples21 .
20
Well, sort of. Read on!
21
DecisionBuilders and Decisions are used internally and you cannot access them directly. To use
them, invoke the corresponding factory methods.
119
5.5. Basic working of the solver: the phases
120
Chapter 5. Defining search primitives: the n-Queens Problem
The Decision class together with the DecisionBuilder class implement the branch-
ing rules of the search, i.e. how to branch (or divide the search sub-tree) at a given node in
the search tree. Although a DecisionBuilder could return several types of Decisions
during a search, we recommend to stick to one Decision for a DecisionBuilder per
phase.
DecisionVisitors is a class whose methods are triggered just before a Decision is
applied. Your are notified of the concrete decision that will be applied and are thus able to take
action.
Decisions
The Decision class is responsible to tell the solver what to do on left branches through its
Apply() method:
121
5.5. Basic working of the solver: the phases
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: the usual DebugString()
method.
• virtual void Accept(DecisionVisitor* const visitor) const:
accepts the given visitor.
Several Decision classes are available. We enumerate the different strategies implemented
by the available Decision classes dealing with IntVars in the next section. In the next
subsection, we detail a basic example.
AssignOneVariableValue as an example
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_);
}
DecisionVisitors
122
Chapter 5. Defining search primitives: the n-Queens Problem
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);
var 6 value
otherwise it is
Compose()
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:
123
5.5. Basic working of the solver: the phases
db1
search tree
db search tree
db = s.Compose(db1, db2);
Try()
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:
124
Chapter 5. Defining search primitives: the n-Queens Problem
db1 db2
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 bal-
anced tree.
Nested searches are searches in sub-trees that are initiated from a particular node in the global
search tree. Another way of looking at things is to say that nested searches collapse a search tree
described by one or more DecisionBuilders and sets of SearchMonitors and wrap it
into a single node in the main search tree.
Local search (LocalSearch) is implemented as a nested search but we delay its description
until the next chapter.
SolveOnce
125
5.5. Basic working of the solver: the phases
NestedOptimize
Variables and values are chosen in two steps: first a variable is chosen and only then is a value
chosen to be assigned to this variable.
The basic version of the MakePhase() method is:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IntVarStrategy var_str,
IntValueStrategy val_str);
where IntVarStrategy is an enum with different strategies to find the next variable to
branch on and IntValueStrategy is an enum with different strategies to find the next
value to assign to this variable. We detail the different available strategies in the next section.
126
Chapter 5. Defining search primitives: the n-Queens Problem
What if you want to use your own strategies? One way to do this is to develop your
own Decisions and DecisionBuilders. Another way is to provide callbacks to the
MakePhase() method. These callbacks evaluate different variables and values you can as-
sign to a chosen variable. The best choice is each time the one that minimizes the values
returned (through the Run() method) by the callbacks. We will explore both ways in the
section 5.7. There are two types of callbacks24 accepted by MakePhase():
typedef ResultCallback1<int64, int64> IndexEvaluator1;
typedef ResultCallback2<int64, int64, int64> IndexEvaluator2;
You use a predefined IntVarStrategy strategy to find the next variable to branch on, pro-
vide your own callback IndexEvaluator2 to find 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 find the next variable but rely on
a predefined IntValueStrategy strategy to find the next value.
Several other combinations are provided.
24
If you want to know more about callbacks, see the section Callbacks in the chapter Under the hood.
127
5.6. Out of the box variables and values selection primitives
Sometimes this 2-step approach isn’t satisfactory. You may want to test all combinations of
variables/values. We provide two versions of the MakePhase() method just to do that:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator2* evaluator,
EvaluatorStrategy str);
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 first
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 first 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 O(number-of-pairs) at each variable selection, versus O(1) in the static
version.
To choose among the IntVar variables and the int64 values when branching, several vari-
ables and values selection primitives are available. As stated before (see the subsection The
2-steps approach in the previous section for more), the selection is done in two steps:
• First, select the variable;
• Second, select an available value for this variable.
To construct the corresponding DecisionBuilder, use one of the MakePhase() factory
methods. For instance:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IntVarStrategy var_str,
IntValueStrategy val_str);
128
Chapter 5. Defining search primitives: the n-Queens Problem
The IntVarStrategy enum describes the available strategies to select the next branching
variable at each node during a phase search:
INT_VAR_DEFAULT The default behaviour is CHOOSE_FIRST_UNBOUND.
INT_VAR_SIMPLE The simple selection is CHOOSE_FIRST_UNBOUND.
CHOOSE_FIRST_UNBOUND Selects the first unbound variable. Variables are considered in
the order of the vector of IntVars used to create the selector.
CHOOSE_RANDOM Randomly select one of the remaining unbound variables.
CHOOSE_MIN_SIZE_LOWEST_MIN Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the lowest min value. In case of tie, the first one is selected, first
being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE_HIGHEST_MIN Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the highest min value. In case of tie, the first one is selected,
first being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE_LOWEST_MAX Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the lowest max value. In case of tie, the first one is selected, first
being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE_HIGHEST_MAX Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the highest max value. In case of tie, the first one is selected,
first being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_LOWEST_MIN Among unbound variables, selects the variable with the smallest
minimal value. In case of tie, the first one is selected, first being defined by the order
in the vector of IntVars used to create the selector.
CHOOSE_HIGHEST_MAX Among unbound variables, selects the variable with the highest
maximal value. In case of tie, the first one is selected, first being defined by the order in
the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE Among unbound variables, selects the variable with the smallest size.
In case of tie, the first one is selected, first being defined by the order in the vector of
IntVars used to create the selector.
CHOOSE_MAX_SIZE Among unbound variables, selects the variable with the highest size.
In case of tie, the first one is selected, first being defined by the order in the vector of
IntVars used to create the selector.
CHOOSE_MAX_REGRET Among unbound variables, selects the variable with the biggest gap
between the first and the second values of the domain.
CHOOSE_PATH Selects the next unbound variable on a path, the path being defined by the
variables: vars[i] corresponds to the index of the next variable following variable i.
129
5.6. Out of the box variables and values selection primitives
Most of the strategies are self-explanatory except maybe for CHOOSE_PATH. This selection
strategy is most convenient when you try to find 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 figure below:
3
1 2
0 4
We have
vars = [−, 0, 3, 1, −, −]
The IntValueStrategy enum describes the strategies available to select the next value(s)
for the already chosen variable at each node during the search:
INT_VALUE_DEFAULT The default behaviour is ASSIGN_MIN_VALUE.
INT_VALUE_SIMPLE The simple selection is ASSIGN_MIN_VALUE.
ASSIGN_MIN_VALUE Selects the minimum available value of the selected variable.
ASSIGN_MAX_VALUE Selects the maximum available value of the selected variable.
ASSIGN_RANDOM_VALUE Selects randomly one of the available values of the selected vari-
able.
130
Chapter 5. Defining search primitives: the n-Queens Problem
ASSIGN_CENTER_VALUE Selects the first available value that is the closest to the center of
the domain of the selected variable. The center is defined 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 first.
SPLIT_UPPER_HALF Splits the domain in two around the center, and forces the variable to
take its value in the upper half first.
5.6.3 Results
Just for fun, we have developed a SolverBenchmark class to test different search
strategies. Statistics are recorded thanks to the SolverBenchmarkStats class. You can
find both classes in the solver_benchmark.h header.
In phases1.cc, we test different combinations of the above strategies to find the variables
and the values to branch on. You can try it for yourself and see that basically no predefined
strategy really outperforms any other.
The program writes a report text file with the best statistics about the search of the CP Solver
to solve the n-Queens Problem given a specified size. The text files have report_n.txt as
name with n the size considered.
We summarize the results when we run the program with sizes 5, 6, 8 and 10 in the following
table:
131
5.7. Customized search primitives: DecisionBuilders and Decisions
The search tree is traversed in a linear fashion: you go down left to assign values until it
is no longer possible and you backtrack whenever necessary and go right25 . This means
that you cannot jump from one branch of the tree to another26 . But what you can do how-
ever is define the tree thanks to combinations of DecisionBuilders and Decisions27 .
To compare our customized search primitives with the basic search strategy used until
now (CHOOSE_FIRST_UNBOUND and ASSIGN_MIN_VALUE), we’ll gather some statistics.
Then we’ll define two customized search strategies using two different mechanisms: in the first
one, we’ll use callbacks and in the second one, we’ll define our own DecisionBuilder
class. In neither cases we’ll use rocket science: the goal is to give you a taste of what you
can do and how you can do it. Finally, we’ll say a few words about which search primitives to
customize and why.
Some customizable search primitives are declared in the header
constraint_solver/constraint_solveri.h (notice the i at the
end).
Before we go on, let’s pause and take a second to discuss the inner mechanism used by the CP
Solver to keep track of some objects.
We have seen that most objects can be created with a factory method (the Make...() meth-
ods). This allows the CP solver to keep track of the created objects and delete them when
they are no longer required. When backtracking, some objects are no longer needed and the
CP Solver knows exactly when and what to delete. The BaseObject class is the root of all
reversibly28 allocated objects.
Whenever you define your own subclass of BaseObject (Decisions,
DecisionBuilders and for instance the SymmetryBreakers we’ll see in the next
section are BaseObjects), 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 back-
tracks out of the current state. To register an object as reversible, you invoke the RevAlloc()
method29 of the solver. For instance, the Next() method of the BaseAssignVariables
DecisionBuilder is implemented as follow:
25
We keep our visualization of our binary search tree, with going left to assign some values to variables and
going right to do the contrary, i.e. to avoid the assignment of these values to the same variables. Of course, there is
no left nor right in the tree. The basic idea is to partition the sub-tree at this point in the search into two sub-trees.
26
Well, this is not totally exact. The search tree is conceptual and depending on the view you have of the tree,
you can visit it in different ways but let us keep it simple for the moment. We have one search tree and the CP
Solver use a pre-order traversal to traverse this tree. Reread the section Basic working of the solver: the search
algorithm if needed.
27
And other primitives like SymmetryBreakers, subject of section 5.8.
28
This is in reference to the backtracking mechanism of the CP Solver.
29
Or one of its sibling methods.
132
Chapter 5. Defining search primitives: the n-Queens Problem
RevAlloc() returns a pointer to the newly created and registered object, in this case the
returned AssignOneVariableValue object. You can thus invoke this method with ar-
guments 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.
Note that if no variable can be selected, the Next() method returns a nullptr which is the
way to tell the CP solver that this DecisionBuilder has done its job.
133
5.7. Customized search primitives: DecisionBuilders and Decisions
Number of solutions: 2
Failures: 6
Branches: 10
Backtracks: 9
Stamps: 29
But then, n = 4 and n = 5 are really small numbers. And indeed, with n = 6, things start to
look really ugly:
That’s a real nightmare! The statistics collected during the search confirm this:
============================
size: 6
The Solve method took 0.005 seconds
Number of solutions: 4
Failures: 36
Branches: 70
Backtracks: 39
Stamps: 149
134
Chapter 5. Defining search primitives: the n-Queens Problem
n 7 8 9 10 11 12
Time (s) 0,014 0,052 0,25 0,899 4,236 21,773
Number of sol. 40 92 352 724 2680 14200
Failures 110 396 1546 6079 27246 131006
Branches 218 790 3090 12156 54490 262010
Backtracks 113 399 1549 6082 27249 131009
Stamps 445 1583 6189 24321 108989 524029
We clearly see the exponential pattern of intractable problems30 .
We have seen in the previous section that the other implemented search strategies didn’t seem
to do better. Can we do better? Let’s try!
5.7.3 First try: start from the center (and use callbacks)
Figure 5.25: 9 forbidden variable-value combinations on the left but 11 on the right.
in the left upper corner forbids 9 variable-value combinations, while the queen on the right in
the middle forbids 11 variable-value combinations.
Maybe it would be worth trying to first select the variables from the middle? To do so, let’s
use a callback and define a simple way to evaluate the middleness of a variable index. We use
30
This is not a proof of course. Maybe another search strategy would yield a better algorithm but we do know
that this problem is intractable.
135
5.7. Customized search primitives: DecisionBuilders and Decisions
a simple callback IndexEvaluator1 that allows to evaluate the next variable to branch on
by giving the index of this variable in the std::vector<IntVar*> for unbounded variables:
typedef ResultCallback1<int64, int64> IndexEvaluator1;
As stated in 5.5.5, the smallest value returned by this callback will give the index of the chosen
variable to branch on. Define b n2 c to be the middle of a n × n chessboard. An easy way to
measure the distance of the index i of a variable to the middle of the chessboard is:
n
|b c − i|
2
where |X| denotes the absolute value of X, i.e. X if X > 0 or −X if X < 0. Translated into
code, we have the next callback:
class MiddleVariableIndexSelector : public IndexEvaluator1 {
public:
MiddleVariableIndexSelector(const int64 n): n_(n),
middle_var_index_((n-1)/2) {}
~MiddleVariableIndexSelector() {}
int64 Run(int64 index) {
return abs(middle_var_index_ - index);
}
private:
const int64 n_;
const int64 middle_var_index_;
};
To assign a value, we can use the predefined ASSIGN_CENTER_VALUE strategy that selects
the first available value that is the closest to the center of the domain of the selected variable.
Our code becomes:
MiddleVariableIndexSelector * index_evaluator =
new MiddleVariableIndexSelector(size);
136
Chapter 5. Defining search primitives: the n-Queens Problem
137
5.7. Customized search primitives: DecisionBuilders and Decisions
5.7.4 Second try: dynamic variable selection (and define our own
DecisionBuilder class)
In this sub-section, we will implement our own DecisionBuilder class. The idea
is to give you a glimpse of what can be done, not to solve the n-Queens Problem. The idea
proposed here can be further developed but this would lead us too far.
We continue with our first intuition to assign variables “from the center” as in our first try but
this time we select the variables dynamically. We could have constructed our own customized
Decision class but because we only assign one variable at a time, we can reuse the already
implemented AssignVariableValue Decision class. As usual, this class is not directly
available and we use the corresponding factory method MakeAssignVariableValue().
To mimic this implementation choice, we also use the anonymous namespace31 :
namespace {
... // code here is inaccessible in another unit
}
and use a factory method for our customized DecisionBuilder class. This is just to show
you why you cannot (and should not!) use some classes directly32 .
We name our customized DecisionBuilder NQueensDecisionBuilder:
class NQueensDecisionBuilder : public DecisionBuilder {
public:
NQueensDecisionBuilder(const int size,
const std::vector<IntVar*>& vars):
size_(size), vars_(vars), middle_var_index_((size-1)/2) {
CHECK_EQ(vars_.size(), size_);
}
~NQueensDecisionBuilder() {}
...
private:
const int size_;
const std::vector<IntVar*> vars_;
const int middle_var_index_;
};
As you see, we construct a middle index for our variables in the constructor and assign this
value to middle_var_index_ as we did in our first try.
31
Although anonymous namespaces are no longer required in C++11.
32
You can access the class definitions in the same file though.
138
Chapter 5. Defining search primitives: the n-Queens Problem
We again use a two-stages approach: we first select a variable to branch on and then we select
a value from its domain to assign it in the Apply() section of the search tree. Remem-
ber that this is exactly what a DecisionBuilder does: at each node in the search tree,
it returns a Decision that is applied on one branch (Apply()) and refuted on the other
branch (Refute()). We select the variable that has the smallest domain, i.e. the smallest
number of values still available to assign to this variable. We could have chosen one of the
CHOOSE_MIN_SIZE_XXX variable selection strategies except that in the case of a tie, we
explicitly want to select the variable that is the “most in the middle”. To select this variable, we
use the following method:
IntVar* SelectVar(Solver* const s) {
IntVar* selected_var = nullptr;
int64 id = -1;
int64 min_domain_size = kint64max;
if (id == -1) {
return nullptr;
} else {
return selected_var;
}
}
This method simply returns (a pointer to) the selected IntVar variable. Of interest are the
methods Bound() that returns true if the variable is bounded, i.e. if it is assigned a value
and Size() that returns the size of the current domain of a variable. If we can not find
any variable, we return a nullptr. The idea behind this selection strategy is to choose a
variable that can make the solver fail as soon as possible. Indeed, if a queen doesn’t have many
possibilities, it’s probably because there is not enough room for her. This is strategy is called
the first fail principle.
Once a variable is selected, we select a value to be assigned to it. Different scenarii are possible.
Let’s try to select the row with the least compatible columns, i.e. the row that has the least
number of possibilities to be occupied by a queen. The idea is that we want to choose the row
139
5.7. Customized search primitives: DecisionBuilders and Decisions
that has the least possibilities to welcome a queen because this might lead to the best choice for
a queen on her column. Indeed, other choices might even reduce the number of possibilities
on this row. This strategy, the one that will most probably lead to a solution, is called the best
success principle.
We devise a simple method to count the number of incompatibilities for one row:
int64 count_number_of_row_incompatibilities(int64 row) {
int64 count = 0;
for (int64 i = 0; i < size_; ++i) {
if (!vars_[i]->Contains(row)) {
++count;
}
}
return count;
}
Given a row row, we count the impossibilities, i.e. the number of queens that cannot be placed
on this row. Note the use of the method Contains() that returns if a given value is in the
domain or not. The corresponding SelectValue() method is straightforward and we don’t
discuss it here. We now turn to the main method of the DecisionBuilder: its Next()
method. It is deceivingly simple:
Decision* Next(Solver* const s) {
IntVar* const var = SelectVar(s);
if (nullptr != var) {
const int64 value = SelectValue(var);
return s->MakeAssignVariableValue(var, value);
}
return nullptr;
}
140
Chapter 5. Defining search primitives: the n-Queens Problem
So, yes, we did better by using some knowledge of the problem to prune the search tree but to
really tackle this problem, we need a deeper understanding of... symmetries. This is the subject
of the next section.
Don’t hesitate to have a look at the code. The routing library in particular has very specialized
implementations of DecisionBuilders and Decisions. In particular, have a look at the
different IntVarFilteredDecisionBuilders and you may want to implement your
versions only if you add lots of knowledge of the problem at hand. Otherwise, you can reuse
the already implemented (and more general and tested) versions.
Basically, think about an algorithm and implement whatever you need. To customize search
primitives, you can, in order of complexity:
• combine existing ones;
• redefine callbacks;
• implement your own version of some search primitive classes;
• mix all of the above.
Often, there is more than one way to do it. Choose the simplest way first to prototype your
algorithm. Combining existing chunks of code shouldn’t ask too much coding effort and will
tell you right away if your idea has some potential or not. Being able to rapidly construct an
algorithm in a few lines of code is one of the strength of the or-tools library. Once your idea
is implemented, profile your code to see if you really need an improved version or not. Maybe
the existing code is too general and you should adapt it to your specific problem with the help
of callbacks. Again, profile your code. Last situation, maybe there isn’t any piece of code to
do what you want. In this case, you’ll need to implement your own version. The library was
conceived with reuse and extension in mind. If you find yourself stuck, maybe it is because you
are not using/redefining the right search primitives? In section 12.5.1, we discuss what search
primitives to customize. If you think your piece of code might be general enough and could be
used by others, ask on the mailing list if we are not interested in implementing it ourselves or
if by any chance we don’t already have some code that we can open source.
Last but not least, if you have produced a piece of code of general interest, share it with the
community.
Now that we have seen the Decision and DecisionVisitor classes in details and
that we are trying to solve the n-Queens Problem, how could we resist to introduce
SymmetryBreakers?
Breaking symmetries of a model or a problem is a very effective technique to reduce the size of
the search tree and, most of the time, it also permits to reduce - sometimes spectacularly - the
search time. We have already seen this effectiveness when we introduced a constraint to avoid
141
5.8. Breaking symmetries with SymmetryBreakers
mirror Golomb rulers in section 3.8.1 page 68. This time, we will use SymmetryBreakers.
As their name implies, their role is to break symmetries. In contrast to explicitly adding sym-
metry breaking constraints in the model before the solving process, SymmetryBreakers add
them automatically when required during the search, i.e. on the fly.
The basic idea is quite simple. Consider again the 4-Queens Problem. Figure 5.26 represents
two symmetric solutions.
These two solutions are symmetric along a vertical axis dividing the square in two equal parts.
If we have x1 = 1 (or x1 = 0) during the search, we know that we don’t have to test a solution
with x2 = 1 (or x2 = 0) as every solution with x1 = 1 (x1 = 0) has an equivalent symmetric
solution with x2 = 1 (x2 = 0).
You can tell the CP solver not to visit the branch x2 = c if during the search we already have
tried to set x1 = c. To do this, we use a SymmetryManager and a SymmetryBreaker.
The SymmetryManager collects SymmetryBreakers for a given problem. During the
search, each Decision is visited by all the SymmetryBreakers. If there is a match be-
tween the Decision and a SymmetryBreaker, the SymmetryManager will, upon refu-
tation of that Decision issue a Constraint to forbid the symmetrical exploration of the
search tree. As you might have guessed, SymmetryManagers are SearchMonitors and
SymmetryBreakers are DecisionVisitors.
5.8.2 SymmetryBreakers
Let’s create a SymmetryBreaker for the vertical axial symmetry. Because the square has
lots of symmetries, we introduce a helper method to find 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.27 illustrates the returned indices by the symmetric() method.
142
Chapter 5. Defining search primitives: the n-Queens Problem
143
5.8. Breaking symmetries with SymmetryBreakers
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_;
};
144
Chapter 5. Defining search primitives: the n-Queens Problem
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() {}
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 defines 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 fit
perfectly within a VisitSplitVariableDomain() call for instance.
Because the n-Queens Problem is defined on a square, we have a lots of symmetries we can
avoid:
• Vertical axis symmetry: we already defined the SY class;
• Horizontal axis symmetry: class SX;
• First diagonal symmetry: class SD1;
• Second diagonal symmetry: class SD2;
145
5.8. Breaking symmetries with SymmetryBreakers
These seven SymmetryBreakers are enough to avoid duplicate solutions in the search, i.e.
they force the solver to find only unique solutions up to a symmetry.
5.8.4 Results
Let’s compare33 some statistics of this algorithm with our best try from previous section:
33
Don’t forget to use the use_symmetry flag!
146
Chapter 5. Defining search primitives: the n-Queens Problem
5.9 Summary
This chapter is the first one of the second part of this manual and we have switched into high
gear. The first part was about the basics of the solver, now we entered the core part: how to
customize the CP solver to our needs. This cannot be done without a basic understanding of the
solver and this chapter provided you with enough insights into the main solving algorithm. In
particular, we saw some search primitives and how to customize them. We also saw how they
are called in the main search algorithm.
Our illustrative problem for this chapter is the n-Queens Problem. We have only proposed a
basic model as our focus was to customize search primitives, not devise a good model. To
better compare search strategies, we saw how to use the cpviz library to visualize the search
tree and the propagations of the constraints.
Search primitives are blocks used by the CP solver to construct the search tree and conduct the
search. Among them, we saw in details:
• SearchMonitors to control and monitor the search;
• DecisionBuilders and Decisions to create the search tree.
To customize search primitives, you can, in order of complexity:
• combine existing ones;
• redefine callbacks;
• implement your own version of some search primitive classes;
• mix all of the above.
For each case, we have seen an example.
147
5.9. Summary
Finally, we continued our discussion of symmetries in the search tree and saw a new way to skip
them on the fly with the help of SymmetryBreakers. By devising better search primitives
and algorithms, we went from a few seconds to only a few milli-seconds to solve the n-Queens
Problem for n = 7..12.
You might be curious, as we are, to combine our best search strategy and the
SymmetryBreakers. This is done in the next sub-section.
We have improved our search strategy and search time little by little. We started by a
simple approach (file nqueens1.cc and algorithm A in the next table) and ended up with
our best algorithm discussed in section Second try: dynamic variable selection (and define
our own DecisionBuilder class). We then saw a completely different approach and used
SymmetryBreakers with a default search algorithm (file nqueens7.cc and algorithm B
in the next table). What about combining both approaches (file nqueens8.cc and algorithm
C in the next table)?
n Statistics Algorithm A Algorithm B Algorithm C
7 Failures 92 24 24
Branches 82 44 41
8 Failures 328 71 54
Branches 654 139 100
9 Failures 1216 272 186
Branches 2430 541 367
10 Failures 4500 1074 642
Branches 8998 2142 1275
11 Failures 17847 4845 2465
Branches 35692 9686 4924
12 Failures 86102 23159 11770
Branches 172202 46312 23511
To give you an idea of the progress we made, the following table gives you the time needed to
solve the 14-Queens Problem with the three algorithms:
Algorithms Time (s)
A 21,582
B 6,096
C 2.945
Combining both approaches did result in a clear cut gain but it is not always the case. Some-
times, two good ideas don’t mix that well.
148
CHAPTER
SIX
We enter here in a new world where we don’t try to solve a problem to optimality but seek a
good solution. Remember from sub-section 1.4.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 find 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 difficult scheduling problem. Don’t worry if you don’t
know anything about scheduling or the Job-Shop Problem, we explain this problem in details.
Scheduling is one of the fields where constraint programming has been applied with great
success. It is thus not surprising that the CP community has developed specific 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-Shop Problem, the disjunctive model to represent it, two formats
to encode Job-Shop Problem instances (JSSP and Taillard) and our first exact results. We
next make a short stop to describe the specific 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 x0 + x1 + . . . + xn−1 where each variable has the same domain
[0, n − 1]. To complicate things a little bit, we add the constraint x0 > 1.
1
Actually, most interesting problems!
2
But watch out for the next generations of computers: molecular computers
(http://en.wikipedia.org/wiki/Molecular_computer) and computers based on quantum mechanics
(http://en.wikipedia.org/wiki/Quantum_computer)!
Once we understand how to use Local Search in or-tools, we use basic
LocalSearchOperators to solve the Job-Shop Problem and compare the ex-
act and approximate results. Finally, to speed up the Local Search algorithm, we use
LocalSearchFilters for the dummy problem.
Prerequisites:
Files:
150
Chapter 6. Local Search: the Job-Shop Problem
We describe the Job-Shop Problem, a first model and the benchmark data. The Job-Shop Prob-
lem belongs to the intractable problems (∈ NP). Only few very special cases can be solved in
polynomial time (see [Garey1976] and [Kis2002]). The definition of this fascinating problem
is not that complicated but you probably will need some extra attention if this is your first
encounter with it. Once you grasp its definition, the next subsections should flow easily.
In the classical Job-Shop Problem there are n jobs that must be processed on m machines.
Each job consists of a sequence of different tasks3 . Each task needs to be processed during an
uninterrupted period of time on a given machine.
We use4 aij to denote the ith task of job j.
Given a set J of jobs, a set M of machines and a set T of tasks, we denote by τj the number of
tasks for a given job j ∈ J. To each task aij corresponds an ordered pair (mij , pij ): the task aij
needs to be processed on machine mij ∈ M for a period of pij units of time.
Here is an example with m = 3 machines and n = 3 jobs. We count jobs, machines and tasks
starting from 0.
• job 0 = [(0, 3), (1, 2), (2, 2)]
• job 1 = [(0, 2), (2, 1), (1, 4)]
• job 2 = [(1, 4), (2, 3)]
In this example, job 2 consists of τ2 = 2 tasks: task a02 which must be processed on machine
m02 = 1 during p02 = 4 units of time and task a12 which must be processed on machine
m12 = 2 during p12 = 3 units of time.
To have a Job-Shop Problem, the tasks must be processed in the order given by the sequence:
for job 0 this means that task a00 on machine 0 must be processed before task a10 on machine
1 that itself must be processed before task a20 on machine 2. It is not mandatory but most of
the literature and benchmark data are concerned by problems where each job is made of m
3
Tasks are also called operations.
4
We use a slightly different and we hope easier notation than the ones used by the scheduling community.
151
6.1. The Job-Shop Problem, the disjunctive model and benchmark data
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 first task (across all machines) and the
completion of the last task (again across all machines). The classical notation for the makespan
is Cmax .
Let’s define tij as the starting time of the processing of task aij .
The makespan can be defined as
or equivalently as the maximum time needed among all jobs to be completely processed. Recall
that τj denotes the number of tasks for job j and that we count starting from 0. tτj −1,j denotes
thus the starting time of the last task of job j and we have
Let’s try to find a schedule for our example. Suppose you want to favour job 1 because you did
see that all jobs have the same processing time (7) and that job 1 has its last task requiring 4
units of time. Here is the Gantt chart of a possible schedule:
11111
00000
0000000
1111111
00000
11111
0000000
1111111 0110
machine 0 11111
00000
0000000
1111111 1010
machine 1111111
000000
job 0
000 11111
111 00000
000000000
111111111
00000
11111
000000000
111111111 00000
11111
00000
11111 1010
000000
111111
000 11111
00000 11111 0110
job 1
000
111
111 00000
00000
11111
00000
11111
machine 2 job 2
2 4 6 8 10 12
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 01
0000000
1111111
00000001111
machine 0 1111111
0000
0000
1111 1010
000000
111111
job 0
000000
111111
00000
11111
11 00000
11111
000000000
111111111
00
00000
11111
00000
11111
011111
111111111
00000000 101
010
00000
11111 0110
machine 1 job 1
00
11
00
1100000
00000
11111
00000
11111
machine 2 job 2
2 4 6 8 10 12
152
Chapter 6. Local Search: the Job-Shop Problem
are completely left shifted on the Gantt chart6 , we can define a feasible schedule by giving the
sequence of jobs processed on each machine.
The first schedule can be described by:
• Machine 0: job 1, job 0
• Machine 1: job 2, job 1, job 0
• Machine 2: job 1, job 2, job 0
and the second optimal one by
• Machine 0: job 0, job 1
• Machine 1: job 2, job 0, job 1
• Machine 2: job 1, job 0, job 2
The Gantt chart offers a nice visualization of schedules but it doesn’t really give any insight
into the problem7 . The disjunctive graph allows a better understanding of the structure of the
problem.
Figure 6.1 represents the disjunctive graph of our example. The graph is G = (V, C ∪D) where
e2
hin
ac
(0,2) (2,1) (1,4)
e1
m
n
s t job 1
chi
ma
job 2
(1,4) (2,3)
• V is the set of vertices corresponding to the tasks. Two fictive vertices s and t are added
to represent the start and end times. Each vertex has a weight corresponding to the
processing time of the task it represents. Vertices s and t have weight 0.
• C are the conjunctive arcs between the ith and (i + 1)th tasks of a job. We also add
conjunctive arcs from s to the first task of every job and from the last task of every job to
t. These arcs are plain in figure 6.1.
• D are the disjunctive arcs between task to be processed on the same machine. These arcs
are dotted or dashed in figure 6.1.
6
A rigorous definition of schedules where all tasks are completely left shifted on the Gantt chart is beyond the
scope of this manual. In scheduling jargon, such schedules are called semi-active schedules.
7
Except if you see the disjunctive graph in the Gantt chart!
153
6.1. The Job-Shop Problem, the disjunctive model and benchmark data
To determine a schedule we have to define 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 first schedule is represented in the next figure.
(0,3) (1,2) (2,2)
job 0
job 2
(1,4) (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 D defines a feasible
schedule if and only if the resulting directed disjunctive graph is acyclic.
The makespan is given by the longest weighted path from s to t. This path - thickened in the
next figure - is called the critical path.
(0,3) (1,2) (2,2)
job 0
job 2
(1,4) (2,3)
This model is a straightforward translation of the definition of a Job-Shop Problem and its
disjunctive graph representation.
We again rely on the The three-stage method: describe, model and solve. What are the decision
variables? We use the variables tij to store the starting time of task i of job j. We could use
two fictive variables corresponding to the fictive vertices s and t but this is not necessary.
To simplify the notation, we will use the notation tk where k denotes a vertex (a task) of the
disjunctive graph. We use the same simplified notation for the processing times (p) and the
machine ids (m).
8
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.
154
Chapter 6. Local Search: the Job-Shop Problem
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:
These constraints are called disjunctive constraints. They forbid cycles in a clique corre-
sponding to a machine9 .
What is the objective function? The objective function (the makespan) Cmax doesn’t corre-
spond to a variable of the model. We have to construct its value. Because we minimize the
makespan, we can use a little trick. Let S be the set of all end tasks of all jobs. In our example,
S = {a20 (2, 2), a21 (1, 4), a12 (2, 3)}. The makespan must be greater than the overall time it
takes to process these tasks:
∀k ∈ S :
Cmax > tk + pk .
We will implement and solve this model in the next section but first we need to read and process
the data representing instances of Job-Shop Problems.
9
Here is why. Consider the following situation
t1
t2
t3
We have t1 +p1 6 t2 , t2 +p2 6 t3 and t3 +p3 6 t1 . Add these three inequalities and you obtain p1 +p2 +p3 6 0.
This is impossible if one of the pi is greater than 0 as every pi > 0.
10
It is not obvious that this model produces optimal solutions that are feasible schedules but it can be shown
that it does.
155
6.1. The Job-Shop Problem, the disjunctive model and benchmark data
To collect the data, we use two different file formats: JSSP and professor Taillard’s format.
In the directory data/jobshop, you can find data files for the Job-Shop Problem11 . The file
jobshop.h lets you read both formats and store the data into a JobshopData class. We
will use this class throughout this chapter.
JSSP stands for Job Shop Scheduling Problem. Let’s consider the beginning of file abz9:
+++++++++++++++++++++++++++++
instance abz9
+++++++++++++++++++++++++++++
Adams, Balas, and Zawack 15 x 20 instance (Table 1, instance 9)
20 15
6 14 5 21 8 13 4 11 1 11 14 35 13 20 11 17 10 18 12 11 ...
1 35 5 31 0 13 3 26 6 14 9 17 7 38 12 20 10 19 13 12 ...
0 30 4 35 2 40 10 35 6 30 14 23 8 29 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 (mij , pij ) corresponds to a task. For this first job, the first 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.
Taillard’s format
156
Chapter 6. Local Search: the Job-Shop Problem
15 11 49 31 20
3
513
71 99 15 68 85
...
This format is made for flow-shop problems and not job-shop problems. The two first 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 first job are:
0
468
54 79 16 66 58
0 is the id or index of the first 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 define our problem instance above in this format and
we don’t attempt to do it.
You can find anything you ever wanted to know and more about this format in [Taillard1993].
JobshopData
You can find the code in the files jobshop.h and report_jobshopdata.cc and the
data in the files abz9, 20_5_01_ta001.txt and first_example_jssp.txt.
The JobshopData class is a simple container for job-shop problem instances. It is defined
in the file jobshop.h. Basically, it wraps an std::vector<std::vector<Task> >
container where Task is a struct defined 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 file formats.
The data file 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();
157
6.1. The Job-Shop Problem, the disjunctive model and benchmark data
if (!reader.loaded_successfully()) {
LOG(FATAL) << "Could not open job-shop file " << filename_;
}
To parse the data file 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 file 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 file parsed:
void Report(std::ostream & out);
void ReportAll(std::ostream & out);
Just for fun, we have written the data file corresponding to our example above in JSSP format
in the file 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
158
Chapter 6. Local Search: the Job-Shop Problem
The file report_jobshopdata.cc contains a simple program to test the content of data
files for the Job-Shop Problem.
You can find the code in the file jobshop.cc and the data in the
files first_example_jssp.txt and abz9.
Scheduling is one of the fields 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 first real example of combining two DecisionBuilders
in a top-down fashion.
We create one IntervalVar for each task. Remember the Task struct we use in the
JobShopData class:
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;
};
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 fixed
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);
159
6.2. An implementation of the disjunctive model
We will create the SequenceVar variables later when we will add the disjunctive constraints.
Recall that the conjunctive constraints ensure the sequence order of tasks inside a job is re-
spected. If IntervalVar t1 is the task right before IntervalVar t2 in a job, we
can add an IntervalBinaryRelation constraint with the right relation between the two
IntervalVars. In this case, the relation is STARTS_AFTER_END:
Constraint* const prec =
solver.MakeIntervalVarRelation(t2, Solver::STARTS_AFTER_END, t1);
In the next section, we will examine other possibilities and also temporal relations between an
IntervalVar t and an integer d representing time.
The disjunctive constraints ensure that the tasks are correctly processed on each machine, i.e.
a task is processed entirely before or after another task on a single machine. The CP solver
provides DisjunctiveConstraints and a corresponding factory method:
const std::string name = StringPrintf("Machine_%d", machine_id);
DisjunctiveConstraint* const ct =
solver.MakeDisjunctiveConstraint(machines_to_tasks[machine_id],
name);
160
Chapter 6. Local Search: the Job-Shop Problem
To create the makespan variable, we simply collect the last tasks of all the jobs and store the
maximum of their end times:
// Creates array of end_times of jobs.
std::vector<IntVar*> all_ends;
for (int job_id = 0; job_id < job_count; ++job_id) {
const int task_count = jobs_to_tasks[job_id].size();
IntervalVar* const task = jobs_to_tasks[job_id][task_count - 1];
all_ends.push_back(task->EndExpr()->Var());
}
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 solving process is done in two sequential phases: first we rank the tasks for
each machine, then we schedule each task at its earliest start time. This is done
with two DecisionBuilders that are combined in a top-down fashion, i.e. one
DecisionBuilder is applied and then when we reach a leaf in its search tree, the second
DecisionBuilder kicks in. Since this chapter is about local search, we will use default
search strategies for both phases.
13
The factory method Solver::MakeSequenceVar(...) has been removed from the API.
161
6.2. An implementation of the disjunctive model
Second, we define the phase to schedule the ranked tasks. This is conveniently done by fixing
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);
162
Chapter 6. Local Search: the Job-Shop Problem
163
6.3. Scheduling in or-tools
Scheduling problems deal with the allocation of resources and the sequencing of tasks to pro-
duce goods and services. The Job-Shop problem is a good example of such problems.
Constraint programming has been proved successful in solving some Scheduling Problems
with dedicated variables and strategies [Philippe2001]. In or-tools, the CP solver offers some
variable types (IntervalVars and SequenceVars) and roughly one specialized search
strategy with some variants. This part of the CP solver is not quite as developed as the rest of
the library and expect more to come. We summarize most of the or-tools features dealing with
scheduling in this section.
This part of the CP Solver is not quite settled yet. In case of doubt, check the code.
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 vari-
ables, you can use them in a variety of different contexts but for the moment keep in mind this
modelling association.
IntervalVars
164
Chapter 6. Local Search: the Job-Shop Problem
• an ending time: e.
s, d and e are IntVar expressions based on the ranges these items can have. You can retrieve
these expressions with the following methods:
• IntExpr* StartExpr();
• IntExpr* DurationExpr();
• IntExpr* EndExpr();
If the corresponding IntervalVar variable is unperformed (see next sub-section), you can-
not use these methods. Well, if you do, nothing bad will happen but you will get gibberish as
the IntervalVar is no longer updated. These methods have corresponding “safe” versions
if you need them (see next sub-section).
Don’t use
• IntExpr* StartExpr();
• IntExpr* DurationExpr();
• IntExpr* EndExpr();
if the corresponding IntervalVar variable is unperformed!
DurationRange
DurationMin()
StartRange EndRange
165
6.3. Scheduling in or-tools
The first factory method creates a FixedInterval: its starting time, dura-
tion and ending time are all fixed. MakeFixedDurationIntervalVar() and
MakeFixedDurationIntervalVarArray() create respectively an IntervalVar
and an std::vector<IntervalVar*> with count elements. The start_min and
start_max parameters give a range for the IntervalVars to start. The duration is fixed
and equal to duration for all the variables. The optional bool indicates if the variables
can be unperformed or not. When an array is created, the name of its elements are simply
name with their position in the array (0, 1, ..., count − 1) appended, like so:
name0, name1, name2, ... .
166
Chapter 6. Local Search: the Job-Shop Problem
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 be-
tween its minimal value and -1 if the variable may be performed and -1 if the variable is
unperformed.
SequenceVars
167
6.3. Scheduling in or-tools
how the IntervalVars are ranked. You can conceptualize16 this class as depicted in the
following figure:
0 1 2 3
0 0 0 0 1
Precedence matrix 1 1 0 0 1
2 1 1 0 1
3 0 0 0 0
2 1 0 3
Current assignment
Array of IntervalVars
0 1 2 3
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 fig-
ure illustrates this:
1 ? ? 2
Ranked sequence
Array of IntervalVars
0 1 2 3
Not ranked yet Ranked Ranked 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 doesn’t exist anymore.
To rank the IntervalVar variables, we say that we rank them first 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.
168
Chapter 6. Local Search: the Job-Shop Problem
42 19
()
Ra
st
n
La
kF
1 86
nk
23 6 2
irs
Ra
t
...
()
Ranked sequence
• to rank first 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. unper-
formed 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;
169
6.3. Scheduling in or-tools
th
• IntervalVar* Interval(int index) const: Returns the index
IntervalVar from the array of IntervalVars.
• IntVar* Next(int index) const: To each IntervalVar corresponds an
associated IntVar that represents the “ranking” of the IntervalVar in the
ranked sequence. The Next() method returns this IntVar variable for the in-
dex th IntervalVar in the array of IntervalVars.
rd
For instance, if you want to know what is the next IntervalVar after the 3
IntervalVar in the sequence, use the following code:
SequenceVar * seq = ...;
...
IntVar * next_var = seq->Next(2);
if (next_var->Bound()) { // OK, ranked
LG << "The next IntervalVar after the 3rd IntervalVar in " <<
"the sequence is " << next_var->Value() - 1;
}
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 filling the three std::vector<int> of
first 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 first clears the three std::vectors and fills 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 first 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 first 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.
170
Chapter 6. Local Search: the Job-Shop Problem
IntervalUnaryRelation constraints
171
6.3. Scheduling in or-tools
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 t2‘s start, i.e. End(t1) == Start(t2);
• STARTS_AFTER_START: t1 starts after t2 starts, i.e. Start(t1) >=
Start(t2);
• STARTS_AFTER_END: t1 starts after t2 ends, i.e. Start(t1) >= End(t2);
• STARTS_AT_END: t1 starts at t2‘s end, i.e. Start(t1) == End(t2);
• STARTS_AT_START: t1 starts when t2 starts, i.e. Start(t1) == Start(t2);
• STAYS_IN_SYNC: STARTS_AT_START and ENDS_AT_END combined together.
These possibilities are enclosed in the BinaryIntervalRelation enum and the factory
method is:
Constraint* Solver::MakeIntervalVarRelation(IntervalVar* const t1,
Solver::BinaryIntervalRelation r,
IntervalVar* const t2)
TemporalDisjunction constraints
Maybe you can relate the decision on what has to happen first to the value an IntVar takes:
...
IntVar * const decider = ...
Constraint * ct = solver.MakeTemporalDisjunction(t1, t2, decider)
172
Chapter 6. Local Search: the Job-Shop Problem
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
CumulativeConstraint constraints
This constraint forces, for any integer t, the sum of the demands corresponding to an interval
containing t to not exceed the given capacity.
Intervals and demands should be vectors of equal size.
Demands should only contain non-negative values. Zero values are supported, and the corre-
sponding intervals are filtered out, as they neither impact nor are impacted by this constraint.
Here is one factory method with a limited static capacity:
Constraint* MakeCumulative(const std::vector<IntervalVar*>& intervals,
const std::vector<int64>& demands,
18
You remember that unperformed IntervalVars are “non existing”, don’t you? And yes, we know that the
adjective “rankable” doesn’t exist...
173
6.3. Scheduling in or-tools
int64 capacity,
const 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.
There are none for the time being. Nobody prevents you from implementing one though.
This sub-section is going to be very brief. Indeed, even if room has been made in the code
to welcome several alternative strategies, at the moment of writing (revision r3804, Decem-
ber 18 th 2014) there is “only one real” strategy implemented to deal with IntervalVars
and SequenceVars. The RankFirstIntervalVars DecisionBuilder for
SequenceVars and the SetTimesForward DecisionBuilder for IntervalVars
both try to rank the IntervalVars one after the other starting with the first “available” ones.
If you’re curious about the implementation details, we refer you to the code (mainly to the file
constraint_solver/sched_search.cc).
If you need specialized DecisionBuilders and Decisions, you now know the inner
working of the CP solver well enough to construct ones to suit your needs. Although nothing
prevents you from creating tools that mix IntVars, IntervalVars and SequenceVars,
we strongly advice you to keep different types of variables separated and combine different
phases together instead.
IntervalVars
For IntervalVar variables, there are two strategies implemented even if there are four en-
tries in the IntervalStrategy enum:
• INTERVAL_DEFAULT = INTERVAL_SIMPLE = INTERVAL_SET_TIMES_FORWARD:
The CP solver simply schedules the IntervalVar with the lowest starting time
(StartMin()) and in case of a tie, the IntervalVar with the lowest ending
time (StartMax()).
• INTERVAL_SET_TIMES_BACKWARD: The CP Solver simply schedules the
IntervalVar with the highest ending time (EndMax()) and in case of a
tie, the IntervalVar with the highest starting time (StartMin()).
174
Chapter 6. Local Search: the Job-Shop Problem
For the first strategy, the DecisionBuilder class is the SetTimesForward class. It
returns a ScheduleOrPostpone Decision in its Next() method. This Decision
fixes the starting time of the IntervalVar to its minimum starting time (StartMin()) in
its Apply() method and, in its Refute() method, delays the execution of the corresponding
task by 1 unit of time, i.e. the IntervalVar cannot be scheduled before StartMin() +
1.
The second strategy is quite similar and the equivalent DecisionBuilder class is the
SetTimesBackward class.
You create the corresponding phase with the good old MakePhase factory method:
DecisionBuilder * MakePhase (
const std::vector< IntervalVar * > &intervals,
IntervalStrategy str);
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 define the slack to be
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 first.
• 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 first.
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);
175
6.3. Scheduling in or-tools
We are thus assured to visit the complete search tree... of solutions of ranked IntervalVars
if needed. After the ranking of IntervalVars, the schedule is still loose and any
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 specific temporal constraints, you can use a data structure specialized
for scheduling: the DependencyGraph. It is meant to store simple temporal constraints and
to propagate efficiently 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 first example (first_example_jssp.txt) and let’s say that for what-
ever reason we want to impose that the first task of job 2 must start at least after one unit of
time after the first task of job 1. We could add this constraint in different ways but let’s use the
DependencyGraph:
solver = ...
...
DependencyGraph * graph = solver.Graph();
graph->AddStartsAfterEndWithDelay(jobs_to_tasks[2][0],
jobs_to_tasks[1][0], 1);
That’s it!
176
Chapter 6. Local Search: the Job-Shop Problem
As you can see, the first task of job 2 starts at 3 units of time and the first 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.
In the toolbox of Operations Research practitioners, Local Search (LS) is very important as it
is often the best (and sometimes only) method to solve difficult problems. We start this section
by describing what Local Search is and what Local Search methods have in common. Then we
discuss their efficiency and compare them with global methods.
Some paragraphs are quite dense, so don’t be scared if you don’t “get it all” after the first
reading. With time and practice, the use of Local Search methods will become a second nature.
Local Search is a whole bunch of families of (meta-)heuristics19 that roughly share the follow-
ing ingredients:
1. They start with a solution (feasible or not);
2. They improve locally this solution;
3. They finish the search when reaching a stopping criterion but usually without any guar-
antee on the quality of the found solution(s).
We will discuss these three ingredients in details in a moment but before here are some exam-
ples of Local Search (meta-)heuristics20 :
19
If the (subtle) difference between meta-heuristics and heuristics escapes you, read the box What is it with the
word meta?.
20
The numbers are the number of results obtained on Google Scholar on August 5, 2012. There isn’t much
we can say about those numbers but we though it would be fun to show them. The search for “GRASP” or
“Greedy Adaptive Search Procedure” didn’t return any meaningful results. The methods in bold are implemented
in or-tools.
177
6.4. What is Local Search (LS)?
178
Chapter 6. Local Search: the Job-Shop Problem
This is the tricky part to understand. Improvements to the initial solution are done locally.
This means that you need to define a neighborhood (explicitly or implicitly) for a given
solution and a way to explore this neighborhood. Two solutions can be close (i.e. they
belong to the same neighborhood) or very far apart depending on the definition of a
neighborhood.
The idea is to (partially or completely) explore a neighborhood around an initial solution,
find a good (or the best) solution in this neighborhood and start all over again until a
stopping criterion is met.
Let’s denote by Nx the neighborhood of a solution x.
In its very basic form, we could formulate Local Search like this:
Often, steps 1. and 2. are done simultaneously. This is the case in or-tools.
The following figure illustrates this process:
111111
000000
000000
111111
Nx0
N
111111111111
000000000000
x1
Nx2
x0 x1 x3 x2 x
111
000 x
000 N
111
i solution i
neighborhood
xi neighborhood of xi
179
6.4. What is Local Search (LS)?
The Local Search procedure starts from an initial feasible solution x0 and searches the
neighborhood Nx0 of this solution. The “best” solution found is x1 . The Local Search
procedure starts over again but with x1 as starting solution. In the neighborhood Nx1 , the
best solution found is x2 . The procedure continues on and on until stopping criteria are
met. Let’s say that one of these criteria is met and the search ends with x3 . 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 find
a local optimum (or come close to) but it is unable to find 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 figure above is very instructive. For instance, you can see that neighborhoods
don’t have to be of equal size or centred around a variable xi . You can also see
that the relationship “being in the neighborhood of” is not necessarily symmetric:
x1 ∈ Nx0 but x0 6∈ Nx1 24 ! In or-tools, you define 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 so-
lution of the neighborhood If you have constructed a successful candidate, make
MakeNextNeighbor() returns true. When the whole neighborhood has been vis-
ited, make it returns false.
3. They finish 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. If this relation is symmetric, we would be to be able to retrace our steps in case of a false start or to
explore other possibilities. On the figure, you might think about going left to explore what is past the z − axis.
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.
180
Chapter 6. Local Search: the Job-Shop Problem
181
6.4. What is Local Search (LS)?
• 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 efficiency 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 neighbor-
hood definition. 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
definition of a neighborhood during the search like in Variable Neighbourhood Search
(VNS).
LS is a tradeoff between efficiency and the fact that LS doesn’t try to find a global optimum,
i.e. in other words you are willing to give up the idea of finding a global optimum for the
satisfaction to quickly find 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 find 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 flooded by mist: you don’t see very far in what direction to go to continue
downhill. Sometimes you don’t see anything at all and you don’t even know if you are
allowed to set a foot in front of you!
a
If you’ve never heard this metaphor, skip this paragraph and don’t bother.
Sometimes, we can have some kind of guarantee on the quality of the solutions found and we
speak about approximations, sometimes we don’t have a clue of what we are doing and we just
hope for the best.
Most of the time, we face two non satisfactory situations:
• a good guarantee is expensive to compute (sometimes as expensive as finding a good
solution or even more!);
• a guarantee that isn’t very expensive to compute but that is close to being useless.
In either cases, it is not worth computing this guarantee27 .
Not having a theoretical guarantee on the quality of a solution doesn’t mean that the solution
found is not a good solution (it might even be the best solution), just that we don’t know how
good (or bad) this solution is!
27
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).
182
Chapter 6. Local Search: the Job-Shop Problem
Meta-heuristics and heuristics can also work globally28 . The challenge with global methods
is that very often the global search space for real industrial instances is huge and contains
lots of dimensions (sometimes millions or even more!). More often than not, global exact
optimization algorithms take prohibitive times to solve such instances. Global (meta-)heuristics
cannot dredge the search space too much in details for the same reason.
So, on one hand we can skim through the whole space search but not too much in details and
on the other hand we have (very) efficient local methods that (hopefully) lead to local optima.
Could we have the best of these two worlds?
You’ve guessed it: we use global methods to find portions of the search space that might contain
good or even optimal solutions and we try to find those with Local Search methods. As always,
there is a tradeoff between the two.
To take again an analogy29 , looking for a good solution this way is a bit like trying to find crude
oil (or nowadays tar sands and the like): you send engineers, geologists, etc. to some places on
earth to prospect (global method). If they find a promising spot, you send a team to drill and
find out (local method).
In this section, we present the implementation of Local Search in or-tools. First, we sketch
the main basic idea and then we list the main actors (aka classes) that participate in the Local
Search. It’s good to keep them in memory for the rest of this section. We then overview the im-
plementation and describe some of its main components. Finally, we detail the inner workings
28
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!
183
6.5. Basic working of the solver: Local Search
of the Local Search algorithm and indicate where the callbacks of the SearchMonitors are
called.
We present a simplified version of the Local Search algorithm. Yes, this is well worth a warning
box!
Wow, this went fast! Let’s summarize all this in the next picture:
LocalSearch::Next()
NestedSolveDecision
NestedSolveDecision::Apply(){ NestedSolveDecision::Refute(){}
SolveAndCommit(FindOneNeighbor(ls));
}
ls is the LocalSearchOperator that constructs the candidate solutions. The search tree
very quickly becomes completely unbalanced if we only keep finding solutions in the left
branches. We’ll see a balancing mechanism that involves one BalancingDecision at the
end of this section.
Speaking about candidate solutions, let’s agree on some wordings. The next figure presents the
beginning of a Local Search. x0 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. x0 , x1 , x2 , . . . are solutions. As we have seen, the Local Search algorithm moves
from one solution to another. It takes a starting solution xi and visit the neighborhood defined
around xi to find the next solution xi+1 . By visiting the neighborhood, we mean constructing
and testing feasible solutions y0 = xi , y1 , y2 , . . . of this neighborhood. We call these solutions
candidate solutions. In the code, they are called neighbors. The LocalSearchOperator
184
Chapter 6. Local Search: the Job-Shop Problem
produces these candidates and the FindOneNeighbor DecisionBuilder filter these out
to keep the interesting candidate solutions only. When a stopping criteria is met or the neigh-
borhood has been exhausted, the current solution of the CP solver is the next starting solution.
Let’s illustrate this:
111111
000000
111111111111
000000000000
000000
111111
Nx0
Nx1
Candidate solutions
y1 y0 y2 y3 y4 y5
x0 x1
Initial solution 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.
185
6.5. Basic working of the solver: Local Search
The next figure illustrates the basic mechanism of Local Search in or-tools:
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.
Let’s 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 LocalSearch‘s constructor.
• LocalSearchPhaseParameters: the LocalSearchPhaseParameters parameter
holds the actual definition 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;
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 first 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. Read
on. The LocalSearch class is also deeply coupled to the Metaheuristic class or more generally to a
SearchMonitor. This is the subject of next chapter.
186
Chapter 6. Local Search: the Job-Shop Problem
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 find one for us. To let the solver find a
solution for us, we pass to it a DecisionBuilder. The first 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)
187
6.5. Basic working of the solver: Local Search
This would limit the search to maximum two candidate solutions in the same neighbor-
hood. By default, the CP solver stops the neighborhood search as soon as it finds a filtered
and feasible candidate solution. If you add an OptimizeVar to your model, once the
solver finds 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 first one.
See section 3.9 to refresh your memory if needed. When the solver finds 2 solutions (or
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.
188
Chapter 6. Local Search: the Job-Shop Problem
when the whole neighborhood is explored), it stops and starts over again with the best
solution.
• LocalSearchFilters: these filters speed up the search by bypassing the solver
checking mechanism if you know that the solution must be rejected (because it is not
feasible, because it is not good enough, ...). If the filters accept a solution, the solver
still tests the feasibility of this solution. LocalSearchFilters are discussed in sec-
tion 6.8.
Several factory methods are available to create a LocalSearchPhaseParameters pa-
rameter. At least you need to declare a LocalSearchOperator and a complementary
DecisionBuilder:
LocalSearchPhaseParameters * Solver::MakeLocalSearchPhaseParameters(
LocalSearchOperator *const ls_operator,
DecisionBuilder *const
complementary_decision_builder);
6.5.3 The basic Local Search algorithm and the callback hooks for
the SearchMonitors
189
6.5. Basic working of the solver: Local Search
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 Descriptions
LocalOptimum() When a local optimum is reached. If true is
returned, the last solution is discarded and the
search proceeds to find the next local optimum.
Handy when you implement a meta-heuristic with a
SearchMonitor.
AcceptDelta(Assignment *delta, When the LocalSearchOperator has produced
Assignment *deltadelta) the next candidate solution given in the form of
delta and deltadelta. You can accept or reject
this new candidate solution.
AcceptNeighbor() After accepting a candidate solution during Local
Search.
PeriodicCheck() 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 defined. These functions simply call their SearchMonitor‘s counterparts, i.e.
they call the corresponding methods of the involved SearchMonitors:
• bool LocalOptimumReached(): FalseExceptIfOneTrue.
• bool AcceptDelta(): TrueExceptIfOneFalse.
• void AcceptNeighbor(): Notification.
Before we delve into the core of the Local Search algorithm and the implementation
of the LocalSearch DecisionBuilder‘s Next() method, we first discuss the in-
ner workings of the FindOneNeighbor DecisionBuilder whose job is to find the
next filtered 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.
SearchMonitor‘s callbacks are indicated in the code by the comment:
// SEARCHMONITOR CALLBACK
190
Chapter 6. Local Search: the Job-Shop Problem
This DecisionBuilder tries to find the next filtered and accepted candidate so-
lution. It tests (and sometimes completes) the candidate solutions given by the
LocalSearchOperator.
We present its Next() method and discuss it after:
1 Decision* FindOneNeighbor::Next(Solver* const solver) {
2
15 DecisionBuilder* restore =
16 solver->MakeRestoreAssignment(assignment_copy);
17 if (sub_decision_builder_) {
18 restore = solver->Compose(restore, sub_decision_builder_);
19 }
20 Assignment* delta = solver->MakeAssignment();
21 Assignment* deltadelta = solver->MakeAssignment();
22
23 // MAIN LOOP
24 while (true) {
25 delta->Clear();
26 deltadelta->Clear();
27 // SEARCHMONITOR CALLBACK
28 solver->TopPeriodicCheck();
29 if (++counter >= FLAGS_cp_local_search_sync_frequency &&
30 pool_->SyncNeeded(reference_assignment_.get())) {
31 // SYNCHRONIZE ALL
32 ...
33 counter = 0;
34 }
35
36 if (!limit_->Check()
37 && ls_operator_->MakeNextNeighbor(delta, deltadelta)) {
38 solver->neighbors_ += 1;
39 // SEARCHMONITOR CALLBACK
40 const bool meta_heuristics_filter =
41 AcceptDelta(solver->ParentSearch(), delta, deltadelta);
42 const bool move_filter = FilterAccept(delta, deltadelta);
43 if (meta_heuristics_filter && move_filter) {
44 solver->filtered_neighbors_ += 1;
45 assignment_copy->Copy(reference_assignment_.get());
46 assignment_copy->Copy(delta);
47 if (solver->SolveAndCommit(restore)) {
191
6.5. Basic working of the solver: Local Search
48 solver->accepted_neighbors_ += 1;
49 assignment_->Store();
50 neighbor_found_ = true;
51 return NULL;
52 }
53 }
54 } else {
55 if (neighbor_found_) {
56 // SEARCHMONITOR CALLBACK
57 AcceptNeighbor(solver->ParentSearch());
58 pool_->RegisterNewSolution(assignment_);
59 // SYNCHRONIZE ALL
60 ...
61 } else {
62 break;
63 }
64 }
65 }
66 solver->Fail();
67 return NULL;
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 first 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 define the deltas. counter counts the number candidate solutions. This counter is used
on line 29 to test if we shouldn’t start again the Local Search with another solution.
On lines 15-19, we define 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 find 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 find a new
candidate solution, we enter the if statement on line 38. The LocalSearchOperator‘s
192
Chapter 6. Local Search: the Job-Shop Problem
if (!MakeOneNeighbor()) {
return false;
}
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}
ApplyChanges() actually fills 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 up-
date the solution counter accordingly. It is now time to test this new solution candi-
date. The first test comes from the SearchMonitors in their AcceptDelta() meth-
ods. 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. We’ll meet
LocalOptimumReached() and AcceptNeighbor() a few lines below.
The second test is the filtering 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 finished its job at this node of
the search tree. If we don’t succeed, the solver fails on line 66.
The SolveAndCommit() method is similar to the Solve() method except that
SolveAndCommit will not backtrack all modifications at the end of the search and this is
why you should:
193
6.5. Basic working of the solver: Local Search
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 syn-
chronize the LocalSearchOperators and LocalSearchFilters with a new solution
provided by the solution pool pool_ on lines 58-60. We also notify the SearchMonitors
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.
We first consider the initialization phase and then we discuss in details its Next() method.
Initialization
194
Chapter 6. Local Search: the Job-Shop Problem
...
LocalSearchPhaseParameters params =
s.MakeLocalSearchPhaseParameters(ls_operator,
complementary_decision_builder);
We can now add as many monitors as we want and launch the solving process:
std::vector<SearchMonitor *> monitors;
...
s.Solve(ls, monitors);
It’s 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();
where:
• limit is the SearchLimit given to the Local Search algorithm;
195
6.5. Basic working of the solver: Local Search
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 find 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)));
196
Chapter 6. Local Search: the Job-Shop Problem
197
6.6. Local Search Operators
try to keep the search tree balanced and force its height to be bounded.
kLocalSearchBalancedTreeDepth is set to 32. So as long as the tree
height is smaller than 32, the LocalSearch DecisionBuilder returns the same
BalancingDecision on line 21. BalancingDecisions don’t do anything by de-
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 cur-
rent solution. The LocalSearch‘s Next() method has done its job at the current
node and nothing needs to be done.
We will use a dummy example throughout this section so we can solely focus on the
basic ingredients provided by the or-tools library to do the Local Search.
Our fictive example consists in minimizing the sum of n IntVars {x0 , . . . , xn−1 } each with
domain [0, n − 1]. We add the fictive constraint x0 > 1 (and thus ask for n > 2):
Of course, we already know the optimal solution. Can we find it by Local Search?
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 modified part of the solution (an Assignment
called delta) is broadcast. You can also define a second Assignment representing the
changes to the last candidate solution defined 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 .
34
deltas and deltadeltas are explained in more details in section 6.8.
198
Chapter 6. Local Search: the Job-Shop Problem
LocalSearchOperator
VarLocalSearchOperator
IntVarLocalSearchOperator SequenceVarLocalSearchOperator
PathOperator
We will construct an LS Operator for an array of IntVars but the API for an array of
SequenceVars is similar38 .
There are two methods to overwrite:
• OnStart(): this private method is called each time the operator is synced with a
new feasible solution;
• MakeOneNeighbor(): this protected method creates a new feasible solution. As
long as there are new solutions constructed it returns true, false otherwise.
Some helper methods are provided:
• int64 Value(int64 index): returns the value in the current Assignment of
the variable of given index;
• int64 OldValue(int64 index): returns the value in the last Assignment (the
initial solution or the last accepted solution) of the variable of given index;
35
The PathOperator classes have some particularities. For instance, they use a special customized
MakeNeighbor() method instead of the MakeOneNeighbor() method.
36
The VarLocalSearchOperator class is the real base operator class for operators manipulating vari-
ables but its existence for our understanding of the LocalSearchOperators is irrelevant here.
37
At the time of writing, December 18 th , 2014, there are no LocalSearchOperators defined for
IntervalVars.
38
For instance, the SetValue() method is replaced by the SetForwardSequence() and
SetBackwardSequence() methods.
199
6.6. Local Search Operators
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 [x0 , x1 , . . . , xn−1 ] is made of the following solutions
(when feasible):
200
Chapter 6. Local Search: the Job-Shop Problem
if (init_phase) {
db = s.MakePhase(vars,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MAX_VALUE);
} else {
initial_solution->Add(vars);
for (int i = 0; i < n; ++i) {
if (i % 2 == 0) {
initial_solution->SetValue(vars[i], n - 1);
} else {
initial_solution->SetValue(vars[i], n - 2);
}
}
}
201
6.6. Local Search Operators
[n − 1, n − 1, . . . , n − 1].
[n − 1, n − 2, n − 1, n − 2, . . . , n − {1 + (n + 1) mod 2}]
DecisionBuilder* ls = NULL;
if (init_phase) {
ls_params = s.MakeLocalSearchPhaseParameters(&one_var_ls, db);
ls = s.MakeLocalSearchPhase(vars, db, ls_params);
} else {
ls_params = s.MakeLocalSearchPhaseParameters(&one_var_ls, NULL);
ls = s.MakeLocalSearchPhase(initial_solution, ls_params);
}
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);
39
The modulo operator (mod) finds 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 n for parity, you can
test n mod 2. If n mod 2 = 0 then n is even, otherwise it is odd. In C++, the mod operator is %.
202
Chapter 6. Local Search: the Job-Shop Problem
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 bet-
ter 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 bet-
ter 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 infea-
sible, [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 infea-
sible, [1, 0, 0, 0] is a new better solution with value 1;
203
6.6. Local Search Operators
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 (filtered neighbors)
were accepted after filtering and 9 (accepted neighbors) were improving solutions.
If you take the last visited neighborhood (neighborhood 9), you might wonder if it was really
necessary to construct “solutions” [0, 0, 0, 1], [1, −1, 0, 1] and [1, 0, −1, 1] and let the solver
decide if they were interesting or not. The answer is no. We could have filtered those solutions
out and told the solver to disregard them. We didn’t filter out any solution (and this is the reason
why the number of constructed neighbors is equal to the number of filtered neighbors). You
can learn more about filtering in the section Filtering.
If you want, you can try to start with the solution provided by the DecisionBuilder
([3, 3, 3, 3] when n = 4) and see if you can figure out what the 29 constructed candidate solu-
tions (neighbors) and 11 accepted solutions are.
Often, you want to combine several LocalSearchOperators. This can be done with the
ConcatenateOperators() method:
LocalSearchOperator* ConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops);
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 first 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];
}
204
Chapter 6. Local Search: the Job-Shop Problem
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 don’t know in what order to proceed. Then the following method might help
you:
LocalSearchOperator* Solver::RandomConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops);
NeighborhoodLimit
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:
205
6.7. The Job-Shop Problem: and now with Local Search!
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 associ-
ated target values. The two vectors should be of the same length and the variables and values
are ordered in the same way.
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 don’t start all over again
from the first variable but keep changing variables from the last change.
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);
And last but not least, in or-tools, Large Neighborhood Search is implemented with
LocalSearchOperators but this is the topic of section 7.7.
We have seen in the previous section how to implement Local Search on our dummy
206
Chapter 6. Local Search: the Job-Shop Problem
example. This time, we apply Local Search on a real problem and present the real thing: the
MakeNextNeighbor() method, the delta and deltadelta Assignments40 .
To solve the Job-Shop Problem, we’ll define two basic LocalSearchOperators. First,
we’ll apply them separately and then we’ll combine them to get better results. In doing so, we
will discover that Local Search is very sensitive to the initial solution used to start it and that
the search is path-dependent.
The idea behind the Deltas and DeltaDeltas is really simple: efficiency. Only the modi-
fied part of the solution is broadcast:
• Delta: the difference between the initial solution that defines the neighborhood and the
current candidate solution.
• DeltaDelta: the difference between the current candidate solution and the previous
candidate solution.
Delta and DeltaDelta are just Assignments only containing the changes.
MakeNextNeighbor()
This method constructs the delta and deltadelta corresponding to the new can-
didate solution and returns true. If the neighborhood has been exhausted, i.e. the
LocalSearchOperator cannot find 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 con-
venient when you define your filters and you can gain some efficiency over the sole use of
deltas.
To help you construct these deltas, we provide an inner mechanism that constructs automat-
ically these deltas when you use the following self-explanatory setters:
40
But not how to implement incremental LocalSearchOperators. This is a very advanced feature.
41
Remember our footnote about the PathOperator classes? They use a special customized
MakeNeighbor() method instead of the MakeOneNeighbor() method.
207
6.7. The Job-Shop Problem: and now with Local Search!
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);
208
Chapter 6. Local Search: the Job-Shop Problem
We let the CP solver construct the initial solution for us. What about reusing
the DecisionBuilder defined in section 6.2 and grab its first feasible solution?
// This decision builder will rank all tasks on all machines.
DecisionBuilder* const sequence_phase =
solver.MakePhase(all_sequences, Solver::SEQUENCE_DEFAULT);
// After the ranking of tasks, the schedule is still loose and any
// task can be postponed at will. Fix the objective variable to its
// minimum value.
DecisionBuilder* const obj_phase =
solver.MakePhase(objective_var,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);
// The main decision builder (ranks all tasks, then fixes the
// objective_variable).
DecisionBuilder* const first_solution_phase =
solver.Compose(sequence_phase, obj_phase, store_db);
209
6.7. The Job-Shop Problem: and now with Local Search!
<< first_solution->ObjectiveValue();
} else {
LOG(INFO) << "No initial solution found!";
return;
}
You’ll find the code in the file jobshop_ls1.cc and the SwapIntervals operator in the
file jobshop_ls.
The idea of exchanging two IntervalVars on a SequenceVar is very common and the
corresponding operator is often referred to as the 2-opt-, 2-exchange- or swap- opera-
tor.
We implement a basic version that systematically exchanges all IntervalVars for all
SequenceVars one after the other in the order given by the std::vectors. We use three
indices:
• int current_var_: the index of the processed SequenceVar;
• int current_first_: the index of the first IntervalVar variable to swap;
• int current_second_: the index of the second IntervalVar variable to swap.
We proceed sequentially with the first SequenceVar (current_var_ = 0) and exchange
the first and second IntervalVars, then the first and the third IntervalVars and so on
until exhaustion of all possibilities. Here is the code that increments these indices to create
each candidate solution:
bool Increment() {
const SequenceVar* const var = Var(current_var_);
if (++current_second_ >= var->size()) {
if (++current_first_ >= var->size() - 1) {
current_var_++;
current_first_ = 0;
}
current_second_ = current_first_ + 1;
}
return current_var_ < Size();
}
This Increment() method returns a bool that indicates when the neighborhood is ex-
hausted, i.e. it returns false when there are no more candidate to construct. Size() and
Var() are helper methods defined in the SequenceVarLocalSearchOperator class.
We start with current_var_, current_first_ and current_second_ all set to 0.
210
Chapter 6. Local Search: the Job-Shop Problem
Pay attention to the fact that current_first_ and current_second_ are also updated
inside the if conditions.
We are now ready to define the OnStart() and MakeNextNeighbor() methods.
The OnStart() method is straightforward:
virtual void OnStart() {
current_var_ = 0;
current_first_ = 0;
current_second_ = 0;
}
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}
211
6.7. The Job-Shop Problem: and now with Local Search!
Solver::CHOOSE_RANDOM_RANK_FORWARD);
You’ll find the code in the file jobshop_ls2.cc and the ShuffleIntervals operator
in the file jobshop_ls.
After having implemented the SwapIntervals operator, the only real difficulty that remains
is to implement a permutation. This is not an easy task but we’ll elude this difficulty by only ex-
changing contiguous IntervalVars and using the std::next_permutation() func-
tion. You can find the declaration of this function in the header algorithm. Its customizable
version reads like:
template <class BidirectionalIterator, class Compare>
bool next_permutation (BidirectionalIterator first,
BidirectionalIterator last, Compare comp);
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 Permutations
1 012
2 021
3 102
4 120
5 201
6 210
We have generated the permutations of 0,1,2 with std::next_permutation(). There
are 3! = 6 permutations (the first permutation is given to std::next_permutation()
and is not generated by it) and you can see that the permutations are ordered by value, i.e. 0 1
2 is smaller than 0 2 1 that itself is smaller than 1 0 2, etc42 .
42
This explanation is not rigorous but it is simple and you can fill the gaps. What happens if you start with 1 0
212
Chapter 6. Local Search: the Job-Shop Problem
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 gflags flag 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 the constructor of the ShuffleIntervals
LocalSearchOperator:
ShuffleIntervals(const std::vector<operations_research::SequenceVar*>& \
vars,
int max_length) :
SequenceVarLocalSearchOperator(vars, size),
max_length_(max_length),
current_var_(-1),
current_first_(-1),
current_length_(-1) {}
2? The std::next_permutation() function simply “returns” 1 2 0 (oops, there goes our rigour again!).
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, we’ll get 0 1 2!
213
6.7. The Job-Shop Problem: and now with Local Search!
if(!std::next_permutation(current_permutation_.begin(),
current_permutation_.end())) {
LOG(FATAL) << "Should never happen!";
}
}
return true;
}
214
Chapter 6. Local Search: the Job-Shop Problem
Local search is strongly dependent on the initial solution. Investing time in finding a good
solution is a good idea. We’ll use... Local Search to find an initial solution to get the real Local
Search started! The idea is that maybe we can find an even better solution in the vicinity of
this initial solution. We don’t want to spend too much time to find it though and we’ll limit
ourselves to a custom-made SearchLimit. To define this SearchLimit, we construct a
callback:
215
6.7. The Job-Shop Problem: and now with Local Search!
return limit_reached;
}
private:
Solver * solver_;
int64 global_time_limit_;
int solution_nbr_tolerance_;
int64 time_at_beginning_;
int solutions_at_beginning_;
int solutions_since_last_check_;
};
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 first Local Search that finds our initial solution, we use the same code as in the
file jobshop_ls2.cc (we call this first solution first_solution).
To find an initial solution, we use Local Search and start form the first_solution found.
We only use a ShuffleIntervals operator with a shuffle 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,
216
Chapter 6. Local Search: the Job-Shop Problem
FLAGS_solutions_nbr_tolerance));
Results
If we solve our problem instance (file 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 find an
initial solution with Local Search and the ShuffleIntervals operator with a shuffle
length of 2 and;
• solutions_nbr_tolerance = 1,
217
6.8. Filtering
6.8 Filtering
Our local search strategy of section 6.6 is not very efficient: we test lots of unfeasible
or undesirable candidate solutions. LocalSearchFilters allow to shortcut the solver’s
solving and testing mechanism: we can tell the solver right away to skip a candidate solution.
6.8.1 LocalSearchFilters
218
Chapter 6. Local Search: the Job-Shop Problem
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 imple-
menting the IsIncremental() method:
virtual bool IsIncremental() const { return true; }
We will filter the dummy example from the file dummy_ls.cc. You can find the code in the
file dummy_ls_filtering.cc.
Because we use an OptimizeVar SearchMonitor, we know that each time a feasible
solution is found, the CP solver gladly adds a new constraint to prevent other solutions with the
same objective value from being feasible. Thus, candidate solutions with the same or higher
objective value will be rejected by the CP solver. Let’s help the busy solver and tell him right
away to discard such candidate solutions.
We are using IntVars and thus we’ll inherit from IntVarLocalSearchFilter and
instead of implementing the Synchronize() method, we’ll implement the specialized
OnSynchronize() method.
The constructor of the ObjectiveValueFilter class is straightforward:
ObjectiveValueFilter(const std::vector<IntVar*>& vars) :
IntVarLocalSearchFilter(vars.data(), vars.size()), obj_(0) {}
219
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 don’t
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 efficiency, 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);
220
Chapter 6. Local Search: the Job-Shop Problem
...
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, ...
which is exactly the same output without the filtering. Of course! Our
LocalSearchOperator systematically produces candidate solutions with a smaller
objective value than the current solution (the same value minus one)! Does it mean that we
have worked for nothing? Well, this is a dummy example, isn’t? Our main purpose was to
learn how to write a custom LocalSearchFilter and we did it!
OK, you’re not satisfied and neither are we. We know that x0 > 1 and that the other variables
must be equal or greater than 0.
Let’s write a LocalSearchFilter that filters infeasible candidate solutions. We don’t 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();
Aha, you probably expected an ad hoc solution rather than the general solution above, didn’t
you?44 .
We now obtain:
..., neighbors = 23, filtered neighbors = 9,
accepted neighbors = 9, ...
44
To be fair, this solution is not as general as it should be. We didn’t take into account the fact that some
IntervalVar variables can be non active but for IntVars and SequenceVars it works well.
221
6.9. Summary
As its name implies, it rejects assignments to values outside the domain of the variables.
The ObjectiveFilter is more interesting and exists in different flavors 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 con-
cern 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 file
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 refinements.
6.9 Summary
222
Chapter 6. Local Search: the Job-Shop Problem
• what kind of customized classes are used to tackle Scheduling Problems in or-tools;
• the Job-Shop Problem as an illustrative example.
More precisely, we saw a description of the Job-Shop Problem and how to model it with the
disjunctive model. This allowed us to introduce the IntervalVars and SequenceVars
variables that are typically used in Scheduling. We also combined two DecisionBuilders
in a top-down fashion. After describing general features of Local Search, we have
seen its general implementation in or-tools and in particular where the callbacks of the
SearchMonitors are called in the Local Search algorithm. Finally, we saw how by imple-
menting LocalSearchOperators and LocalSearchFilters we can define our neigh-
borhoods for the Local Search algorithm.
There are some interesting examples in the directory examples/cpp and the reader is en-
couraged to read them carefully.
223
CHAPTER
SEVEN
META-HEURISTICS: SEVERAL
PREVIOUS PROBLEMS
Our meta-heuristics only kick in when we already have reached a local optimum
with the Local Search.
A last word of advice. In this chapter, we decided to show you the code in full details. Real
code, especially in optimization, is complicated and has to deal with lots of subtleties. While
we try to explain most of the intricate details (but not all), you only see the end results. The
code we show has been polished and is used in production (and changes sometimes!). Some
parts have a long history of trial and error and there is no way we can explain all the details. We
cannot emphasize enough that you’ll learn how to use the or-tools by actually coding with it,
not only reading existing code. That said, the exposed code gives you good examples on what
can be done and how the makers of the or-tools library decided to do it. Beside, you’ll have a
pretty good idea of what actually our meta-heuristic implementations do and how to adapt the
code to solve your problems.
Overview:
Before diving in the heart of the matter, we address two ways to improve the search. First,
because our meta-heuristics never stop, we detail how SearchLimits can be used to stop
2
Local Search and meta-heuristics are so intricate that it is difficult to separate the two concepts. This is also
the case in the ortools library. The LocalSearch class was devised with the Metaheuristic class in mind
and vice-versa. Reread the section 6.4 if needed. It also describes what a meta-heuristic is.
226
Chapter 7. Meta-heuristics: several previous problems
a search. Second, we discuss restarting a search. Recent research shows that restarting a
search might improve the overall search. Next, we detail our Metaheuristic class and
our three specialized TabuSearch, SimulatedAnnealing and GuidedLocalSearch
implementations. To finish this chapter, we present another meta-heuristic, Large
Neighborhood Search3 , that is not implemented with a SearchMonitor but with a
LocalSearchOperator. Next we answer the question “What do you do when you have
no idea of a (good) search strategy?”. Answer? You throw in lots of heuristic and random be-
havior in your top-level search. In or-tools, we implemented the DefaultIntegerSearch
DecisionBuilder to do just that.
Prerequisites:
Files:
227
7.1. Search limits and SearchLimits
To control the search, we also need a way to stop the search when some criteria are
met. By default, our meta-heuristics never stop. This can easily be done with the help of a
SearchLimit.
Our meta-heuristic search never stops. Add a SearchLimit to limit the search.
We will use the SearchLimits defined in limits.h throughout the whole chapter.
We have already met SearchLimits in sub-section 3.5.4 and even used callbacks to define
our SearchLimit in sub-section 6.7.5 where we disregarded a given time limit if the search
produced enough solutions between checks. We go a little bit more into details here. It is not
always possible to create a CustomLimit with an appropriate callback to meet a desired end
search criteria. For instance, let’s say we would like to stop the search after a certain number
of solutions have been found but without any improvements in the objective value. To do so,
we need to have access to the objective value every time a new solution has been found. The
Solver class doesn’t provide any method to access its current objective value. What we need
is a custom SearchMonitor. Or more precisely, a custom SearchLimit.
SearchLimits are specialized SearchMonitors to end a Search. The SearchLimit
class itself is a pure virtual base class for all SearchLimits. Its only constructor is:
explicit SearchLimit(Solver* const s);
228
Chapter 7. Meta-heuristics: several previous problems
• bool Check(): check the status of the limit. A return value of true indicates
that we have indeed crossed the limit. In that case, this method will not be called
again and the remaining search will be discarded. This method is called in the
BeginNextDecision() and RefuteDecision() callbacks.
• void Init(): called when the search limit is initialized in the EnterSearch()
callback.
• void Copy(const SearchLimit* const limit): copies a limit. Warning:
this leads to a direct (no check) down-casting of limit, so one needs to be sure both
SearchLimits are of the same type.
• SearchLimit* MakeClone() const: allocates a clone of the limit.
OK, let’s get our hands dirty and code! You can find the code in the file limits.h.
Our class inherits publicly from SearchLimit:
class NoImprovementLimit : public SearchLimit {
...
};
We’ll consider both minimizing and maximazing depending on a bool in the constructor4 :
class NoImprovementLimit : public SearchLimit {
public:
NoImprovementLimit(Solver * const solver,
IntVar * const objective_var,
const int solution_nbr_tolerance,
const bool minimize = true);
4 prototype_->Store();
5
4
We don’t follow the code convention of using a maximize bool but the fully attentive reader noticed it,
didn’t she?
229
7.1. Search limits and SearchLimits
CTRL-C (the Ctrl key in combination with the C key) sends the SIGINT signal which will
interrupt the application except if we catch this signal and exit peacefully. Because meta-
heuristics can take a long time before even producing a solution or find a better solution, we
have implemented a CatchCTRLBreakLimit class that allows the CP Solver to fail peace-
fully instead of abruptly interrupting the search process. The code involved is beyond the scope
of this manual (if you are curious, have a look at the file limits.h). As usual, we have de-
fined a factory method:
SearchLimit * MakeCatchCTRLBreakLimit(Solver * const solver);
230
Chapter 7. Meta-heuristics: several previous problems
Restarting the search - especially after having gathered some information - can dramatically
improve the search5 (see [Gomes1998] for instance). It is well known that the first decisions
taken at the beginning of a search can have an enormous impact on the search tree size and the
time required to visit this tree in part or in whole.
The CP solver provides two restart strategies:
• constant restart and
• Luby restart.
Both restart strategies are implemented by SearchMonitors. We detail both strategies in
the next sub-sections. Some information is collected during a search and is kept to guide a
restarted search. NoGoods (when used) are among them and we discuss them below6 .
As its name implies, constant restart restarts the search periodically. The factory method
SearchMonitor* Solver::MakeConstantRestart(int frequency);
7.2.2 Luby
In a now well-known article [Luby1993], Luby et al. describe an optimal (universal) strategy
S univer = (t1 , t2 , t3 , . . .) to restart a search (an algorithm) that has a certain randomness in it
but that is guaranteed to converge to a solution given enough time. ti is called a run length: a
number of iterations in the algorithm. To apply this strategy, you first let the algorithm run for
t1 iterations, than restart it and let it run for t2 iterations and so on.
We will not go into the (rather technical) details of this article. For the curious reader, here is
this optimal strategy:
S univer = (1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, . . .)
or more formally:
(
2k−1 , if i = 2k − 1
ti =
ti−2k−1 +1 , if 2k−1 6 i < 2k − 1.
with i, k = 1, 2, . . .. Notice that all the run lengths ti are power of 2 and that each sequence is
repeated before a new power of 2 is tried.
The factory method of the Solver class is the following:
5
For both heuristic and exact searches.
6
What else is kept during a restart of the search? Essentially, impact-based searches also keep their estimations.
See section 7.8.1 in this chapter for more about impact-based searches.
231
7.2. Restarting the search
7.2.3 NoGoods
This is an advanced feature that hasn’t been tested as thoroughly as the rest of the code.
The basic idea of a nogood is simple: keeping a list of contradictory variable “assignments”.
These contradictory variable assignments can be stated before or/and during the search. The
idea is to use these nogoods in the search to - hopefully - cut short some branches for the
search tree without having to rediscover these contradictory assignments again and again. This
is especially useful when restarting the search.
An example will clarify our discussion. Let’s say that during the search, a procedure finds that
the following variable assignments lead to a failure:
x1 = 2, x8 = 4 and x3 6= −2.
The list of the 3 variable “assignments” is a nogood. Whenever we have these three conditions,
we know for sure that they lead to a failure. This information could lead to some refinements in
the search. In our (basic) implementation of nogoods, we only use this information to propagate
the nogood7 .
Each clause (such as x1 = 2 and x3 6= 3) are held in an abstract NoGoodTerm class. At the
time of writing (7 th of January 2015, r3832), only the clauses xi = a and xj 6= b have been
implemented, both in the specialized IntegerVariableNoGoodTerm class. A Boolean
assign variable distinguishes both cases. The NoGood class is basically a container for such
clauses. Its only (private) variable is:
std::vector<NoGoodTerm*> terms_;
The NoGood class has two methods to add these two types of clauses:
void NoGood::AddIntegerVariableEqualValueTerm(IntVar* const var,
int64 value) {
terms_.push_back(new IntegerVariableNoGoodTerm(var, value, true));
}
and
void NoGood::AddIntegerVariableNotEqualValueTerm(IntVar* const var,
int64 value) {
terms_.push_back(new IntegerVariableNoGoodTerm(var, value, false));
}
7
This process is really basic: if there is only one undecided clause (term) and all the other clauses are verified,
then the opposite of this undecided clause is added to the solver.
232
Chapter 7. Meta-heuristics: several previous problems
As its name implies, this class doesn’t do much more than collect and propagate the nogoods
but it can be used as a base example to develop your own NoGoodManager.
Its only (private) variable is a list of nogoods:
std::vector<NoGood*> nogoods_;
To see the NoGoods in action, we refer the reader to the code written for the default search
in the file default_search.cc, especially the RestartMonitor SearchMonitor
that uses a NoGoodManager. A (basic) mechanism is implemented to create and collect
NoGoods obtained from diving heuristics8 . Once such heuristic finds a contradictory variable
assignment, a NoGood is created and the search might or might not be restarted.
To help you (and us) in the building of meta-heuristics, the or-tools library offers an abstract
Metaheuristic which your specialized class can inherit from. We deal with optimization
problems: we either minimize or maximize an objective function. The way meta-heuristics are
implemented in or-tools is to let the meta-heuristic itself decide what to do when the objective
value changes: it can add a new constraint on the objective value and/or the next solution to be
found. This means that you don’t use the OptimizeVar class to encapsulate your objective
function9 but instead use the Metaheuristic class to add complementary constraints on
your objective.
Don’t use the OptimizeVar class to encapsulate your objective function when
using a Metaheuristic class.
233
7.3. Meta-heuristics in or-tools
search algorithm and the SearchMonitor class, we invite you to quickly (re)read section 5.3
about the basic working of the search algorithm and especially section 6.5 about the Local
Search algorithm. We do sketch what the methods of the SearchMonitor class do in this
chapter thought. We reiterate our warning concerning the start of our meta-heuristics:
Our meta-heuristics only kick in when we have already reached a local optimum
with the Local Search.
This is a choice in our implementations of the meta-heuristics. See the box Why wait for a local
optimum to start a meta-heuristic? below for more.
One last thing before digging further into this fascinating subject. Because details matter very
much in the case of meta-heuristic implementations, we present the full code of all the classes
discussed in this and the next three sections.
The basic idea behind meta-heuristics is to enhance the Local Search. In particular, most meta-
heuristics share (at least) the three following main features:
• avoid being trapped in local optimums;
• intensify the search in a promising neighborhood (intensification) and
• keep better solutions met during the search (aspiration criterion).
We discuss these three features more in details next.
Let’s reexamine the figure used in the previous chapter to illustrate Local Search:
x0 x1 x3 x2 x
234
Chapter 7. Meta-heuristics: several previous problems
Intensification vs diversification
Avoiding being trapped in a local optimum can be seen as a special case of diversification of
the search: the algorithm explores different parts of the search space in the hope of finding
promising neighborhoods that a basic Local Search algorithm would not explore on its own.
The opposite is also desirable. Once a promising neighborhood has been found, it is probably
worth exploring it in more details. This is called intensification.
Often the intensification and diversification mechanisms are conflicting and the meta-heuristic
oscillates between these two phases in the search.
The base Metaheuristic class is quite simple. It only implements three basic virtual meth-
ods:
• virtual void EnterSearch();
• virtual bool AtSolution() and
• virtual void RefuteDecision(Decision* const d).
It also defines five basic variables:
protected:
IntVar* const objective_;
int64 step_;
int64 current_;
int64 best_;
bool maximize_;
The names of the variables are self-explanatory. maximize_ is set to true when maximizing
and false when minimizing.
We reproduce the implementation of the three virtual methods integrally. Not only are they
pedagogically interesting, it is also good to know them to avoid reinventing the wheel.
235
7.3. Meta-heuristics in or-tools
EnterSearch()
EnterSearch is called at the beginning of the search and is the perfect place to initialize our
variables:
virtual void Metaheuristic::EnterSearch() {
if (maximize_) {
best_ = objective_->Min();
current_ = kint64min;
} else {
best_ = objective_->Max();
current_ = kint64max;
}
}
The other variables are set in the constructor. The current_ variable holds the value of the
current solution and is used to bound the objective value. We start with a very high value to
allow the meta-heuristic to find solutions that have worst objective values than the best solution
encountered so far.
AtSolution()
The AtSolution() method is called whenever a valid solution is found. If the return value
is true, then the search will resume, otherwise the search will stop there. This is the perfect
place to update the current and best solution:
virtual bool Metaheuristic::AtSolution() {
current_ = objective_->Value();
if (maximize_) {
best_ = std::max(current_, best_);
} else {
best_ = std::min(current_, best_);
}
return true;
}
By default, we don’t want the Metaheuristic class to halt the search so we return true.
RefuteDecision()
236
Chapter 7. Meta-heuristics: several previous problems
}
} else if (objective_->Min() > best_ - step_) {
solver()->Fail();
}
}
If our meta-heuristic is not able to beat the best solution, we make the search fail at this node
and let the solver continue the search at another node. We don’t take any action before a
Decision is applied by default because each meta-heuristic has its own way of dealing with
an applied Decision. You’ll probably need to overwrite the ApplyDecision() method
for each meta-heuristic.
We briefly review some of the callbacks of the SearchMonitor class that we might want
to implement when designing a new meta-heuristic. We have already seen a basic use for
EnterSearch(), AtSolution() and RefuteDecision() above. We probably need
to extend/specialize these basic implementations but for now, we discuss other callbacks of the
SearchMonitor class that can or must be used to implement a meta-heuristic. Don’t forget
that we use Local Search.
ApplyDecision()
237
7.3. Meta-heuristics in or-tools
LocalOptimum()
AcceptNeighbor()
This method is called whenever a neighbor (what we called a candidate solution in the previous
chapter) has been selected (accepted and filtered) in one neighborhood. This is probably one
iteration of your algorithm and the perfect time to update your search strategy depending on
the new candidate solution.
The following figure illustrates the hierarchy situation between the different implemented
Metaheuristic classes:
238
Chapter 7. Meta-heuristics: several previous problems
SearchMonitor
Metaheuristic
Tabu Search, Simulated Annealing and Guided Local Search are three well-known meta-
heuristics.
In the following sections, we’ll have a detailed look at each of these classes and apply them to
solve one or several problems we have already encountered10 . Meta-heuristic are more a gen-
eral canvas within which much liberty is offered for a concrete and specialized implementation.
Our implementation is only one among many.
These three meta-heuristics can be used within the Routing Library (RL) by switching a com-
mand line flag:
- routing_guided_local_search
- routing_simulated_annealing
- routing_tabu_search
All three flags are set to false by default. You can only use one meta-heuristic at a time.
This meta-heuristic was invented in the eighties and has been quite successful to produce very
good solutions for most problems. Well implemented, it can be very efficient. We describe
here our generic and therefor simple implementation. We’ll again develop and discuss the
whole code. Not only will you know exactly what to expect from our implementation but it
can also serve as an example for your own implementation of a meta-heuristic with the or-tools
library.
The basic idea is to avoid being trapped in a local optimum by making some features of a
solution tabu: we don’t want to produce any solution with these features for a certain period
of time. This period of time is called a tenure. If we choose these features well, not only do
we have the guarantee that we will not reproduce the local optimum again (because it has these
features) but we might get out of the vicinity of this local optimum and explore more promising
neighborhoods. This is called the diversification phase: we seek to find solutions with different
features than previously obtained. Once you find a good solution, you might want to explore
10
We will not see an example for the GLS meta-heuristic because our implementation is tailored to solve
Routing Problems.
11
We only give a you a very basic idea barely enough to understand our implementation. Actually, some Tabu
Search experts might even not agree with our presentation of the main idea.
239
7.4. Tabu Search (TS)
solutions “close” to it. If you manage to find what features is important for a solution, you
might want to keep them to explore similar solutions. This is called the intensification phase:
we seek to find solutions with similar (or the same) features to a (or several) given solution(s).
We keep two lists: one for forbidden features and one for features to keep in the next solutions.
The Tabu Search oscillates between these two phases: diversification to avoid being trapped in
local optima and also explore other parts of the search space and intensification to search more
in details a promising neighborhood.
We only have scratched the surface of the Tabu Search meta-heuristic. If you want to know
more about TS, the classical book by Glover and Laguna [Glover1997] is still a good reference.
Another good reference is [Gendreau2005]. To have an up to date account on this topic, search
the Internet: there are plenty of documents about TS.
Our implementation only deals with IntVar variables. Because our implementation is quite
generic and can be used with any LocalSearchOperator and any problem, the features
of a solution we consider are the given IntVar variables and their values. In intensification
mode, we will keep certain variables fixed, i.e. bounded to a certain value and in diversification
mode, we will forbid some variables to take some values. Actually, we mix both modes: we
use two tenures: keep_tenure and forbid_tenure. We also keep two lists of variables:
keep_tabu_list_ and forbid_tabu_list_: variables in the keep_tabu_list_
must keep their values and variables in the forbid_tabu_list_ can not use the corre-
sponding values. To see what variables are selected and added to those lists, you have to look
at the code below. They will be kept in the lists for a certain tenure. The tenure can be seen
as a certain amount of time or a certain number of iterations. In our case, we consider each
time a new neighbor (candidate solution) is selected by the Local Search algorithm. We keep a
variable stamp_ that counts the number of iterations. Because we only start the Tabu Search
after the first optimum is reached, this variable is at 0 until the first local optimum is reached.
After that, every time a candidate solution is selected (including the starting solution when the
Local Search starts again after finding a local optimum), this variable is incremented.
Our aspiration criterion is simple: a solution is accepted no matter what if it is better than
any other solution encountered so far. Because of this aspiration criterion, our TabuSearch
Metaheuristic only add constraints. No variable has its domain skimmed or its value
fixed. These additional constraints always allow for a better solution to be accepted. To do so,
we use a Boolean IntVar aspiration variable and a Boolean IntVar tabu variable:
one of the two has to be equal to 1.
Some helpers
To know what a given variable should keep as value or not and how long, we define a simple
VarValue struct:
struct VarValue {
VarValue(IntVar* const var, int64 value, int64 stamp)
: var_(var), value_(value), stamp_(stamp) {}
IntVar* const var_;
240
Chapter 7. Meta-heuristics: several previous problems
The std::list can be used as a FIFO list, i.e. a queue12 . You can add an element at
the beginning of the queue/list with its push_front() method and retrieve an element at
the end of the queue/list with its pop_back() method. To update a list with respect to the
time/iterations/tenures, we chop off the tail of the list whenever an element is outdated:
void AgeList(int64 tenure, TabuList* list) {
while (!list->empty() && list->back().stamp_ < stamp_ - tenure) {
list->pop_back();
}
}
The real method called is AgeLists() because it updates both lists and increment the
stamp_ variable.
void AgeLists() {
AgeList(keep_tenure_, &keep_tabu_list_);
AgeList(forbid_tenure_, &forbid_tabu_list_);
++stamp_;
}
vars_ are the variables to use in both Tabu lists. Because of the genericity of our implementa-
tion, these variables must be used (changed) by the LocalSearchOperator13 . We keep an
Assignment with the last found solution and store its objective value in the last_ variable.
With the variables inherited from the Metaheuristic class, this will allow us to play with
the last_, current_ and best_ values.
12
FIFO stands for First In, First Out. Basically, a queue is a data structure that allows you to add elements and
retrieve them in their order of appearance.
13
Variables that are not changed by the LocalSearchOperator will not enter any Tabu list.
241
7.4. Tabu Search (TS)
Both Tabu lists and corresponding tenures have quite explicit names: keep_tabu_list_
and keep_tenure_ for the to be kept features list and forbid_tabu_list_ and
forbid_tenure_ for the forbidden features list.
The tabu_factor_ variable is a percentage of the number of variables that are following the
Tabu guidelines. If equal to 1.0, it means that all variables must follow the Tabu guidelines or
in other words, no violation of the Tabu criteria is allowed (except when the aspiration criterion
kicks in). If equal to 0.0, no variable must follow the Tabu guidelines or in other words, the
Tabu criteria can be fully violated.
stamp_ is a counter with the number of iterations. Until the Tabu Search is really launched,
this counter stays at 0. Finally, found_initial_solution_ is a Boolean that indicates if
an initial solution has been found or not.
The constructor is quite straightforward:
TabuSearch(Solver* const s,
bool maximize,
IntVar* objective,
int64 step,
const std::vector<IntVar*>& vars,
int64 keep_tenure,
int64 forbid_tenure,
double tabu_factor)
step denotes the decrease/increase of the objective value sought after each found feasible
solution. Because it is not dynamically changed, you probably want to keep this variable at 1.
This value is kept in the inherited variable step_.
Now that we have met the variables and helpers, we can discuss the callbacks of the
TabuSearch class. We ordered them below in a way to gradually have a big picture of
our algorithmic implementation, not in the order they are called.
LocalOptimum()
This method launches the Tabu Search. Indeed, by calling AgeLists(), the stamp_ vari-
able is incremented and all the code in the if (0 != stamp_) condition (see below) can
be executed. As the Local Search will be relaunched, this method also updates the current_
242
Chapter 7. Meta-heuristics: several previous problems
variable. Finally, if an initial solution has been found, the method returns True to continue the
Local Search. If no solution has been previously found, the search is aborted.
AcceptNeighbor()
We age both lists and increment the stamp_ variable to acknowledge the iteration if we have
already obtained a local optimum in the Local Search.
AtSolution()
The AtSolution() method is called whenever a solution is found and accepted in the Local
Search. It called in the NextSolution() method of the current search of the CP Solver.
1 bool AtSolution() {
2 if (!Metaheuristic::AtSolution()) {
3 return false;
4 }
5 found_initial_solution_ = true;
6 last_ = current_;
7
8 if (0 != stamp_) {
9 for (int i = 0; i < vars_.size(); ++i) {
10 IntVar* const var = vars_[i];
11 const int64 old_value = assignment_.Value(var);
12 const int64 new_value = var->Value();
13 if (old_value != new_value) {
14 VarValue keep_value(var, new_value, stamp_);
15 keep_tabu_list_.push_front(keep_value);
16 VarValue forbid_value(var, old_value, stamp_);
17 forbid_tabu_list_.push_front(forbid_value);
18 }
19 }
20 }
21 assignment_.Store();
22
23 return true;
24 }
On lines 2 to 6, we update the current_, last_ and best_ values. We also set
found_initial_solution_ since we have a solution and hence an initial solution. On
14
I.e. filtered and accepted, see section 6.5.3 if needed.
243
7.4. Tabu Search (TS)
line 21 we store the current solution. This allows us to keep the values of the variables and
compare them with a new solution as we do in the lines 8 to 20. These lines are only pro-
cessed if we have reached our first local optimum (if (stamp_ != 0) {...}). If this
is the case, we consider among the variables given to the constructor, those that have changed
since last solution (if (old_value != new_value) {}). We force those variables to
keep their new values for keep_tenure iterations and forbid them to take their old value for
forbid_tenure iterations. Depending on the value of forbid_tenure, you can forbid
a variable to take several values.
Because we want the search to resume, we return true.
ApplyDecision()
18 std::vector<IntVar*> tabu_vars;
19 for (const VarValue& vv : keep_tabu_list_) {
20 IntVar* tabu_var = s->MakeBoolVar();
21 Constraint* keep_cst =
22 s->MakeIsEqualCstCt(vv.var_, vv.value_, tabu_var);
23 s->AddConstraint(keep_cst);
24 tabu_vars.push_back(tabu_var);
25 }
26 for (const VarValue& vv : forbid_tabu_list_) {
27 IntVar* tabu_var = s->MakeBoolVar();
28 Constraint* forbid_cst =
29 s->MakeIsDifferentCstCt(vv.var_, vv.value_, tabu_var);
30 s->AddConstraint(forbid_cst);
31 tabu_vars.push_back(tabu_var);
32 }
33 if (tabu_vars.size() > 0) {
34 IntVar* tabu = s->MakeBoolVar();
35 s->AddConstraint(s->MakeIsGreaterOrEqualCstCt(
36 s->MakeSum(tabu_vars)->Var(),
37 tabu_vars.size() * tabu_factor_,
244
Chapter 7. Meta-heuristics: several previous problems
38 tabu));
39 s->AddConstraint(s->MakeGreaterOrEqual(s->MakeSum(aspiration, tabu),
40 1LL));
41 }
42
43 if (maximize_) {
44 const int64 bound = (current_ > kint64min) ?
45 current_ + step_ : current_;
46 s->AddConstraint(s->MakeGreaterOrEqual(objective_, bound));
47 } else {
48 const int64 bound = (current_ < kint64max) ?
49 current_ - step_ : current_;
50 s->AddConstraint(s->MakeLessOrEqual(objective_, bound));
51 }
52
53 if (found_initial_solution_) {
54 s->AddConstraint(s->MakeNonEquality(objective_, last_));
55 }
56 }
After quickly screening the content of this method, you might be curious about why we didn’t
encapsulate any code in a if (0 != stamp_) {...}. The reason is simple, we need to
be able to “revert” to “any” solution at any time if the aspiration criteria is used. At the same
time (lines 43 to 51), we force the solver to produce better solutions as an OptimizeVar
SearchMonitor would15 . We also add (lines 53 to 55) a constraint to forbid obtaining
solutions with the same values and thus avoid plateaus (i.e. solutions with the exact same
objective value one after the other). Let’s discuss the other lines.
Lines 3 to 5 are technical. Whenever we have the unique BalancingDecision, we know
that we shouldn’t do anything and let it pass. Lines 7 to 16 is about our aspiration criterion:
we accept a neighbor (candidate solution) if it improves the best solution found so far. These
constraints allow to accept solutions that will be found in the future and are considered Tabu.
Lines 18 to 41 are dedicated to add the corresponding constraints for the Tabu Search mech-
anism. First (lines 19 to 25) to store some features then (lines 26 to 32) to forbid some other
features. Each time, a BoolVar variable corresponding to the added equality (lines 21 to
23) or inequality (lines 28 to 30) is created and added to the std::vector<IntVar*>
tabu_vars list. This list is then used (lines 35 to 38) to control how many of these variables
(tabu_vars.size() * tabu_factor_) should really follow or not the Tabu criteria.
Finally, line 39 adds a constraint to balance the Tabu and aspiration criteria.
You can find the code in the files jobshop.h, jobshop_ls.h, limits.h,
jobshop_tabu1.cc and jobshop_tabu2.cc and the data in the file abz9.
The code found in the file jobshop_ts1.cc is very similar to the code in
jobshop_ls1.cc from the previous chapter. We only use the SwapIntervals
15
Although the OptimizeVar SearchMonitor does not add constraints.
245
7.4. Tabu Search (TS)
Let’s compare the results of jobshop_ls1.cc and jobshop_ts1.cc for different pa-
rameters of the Tabu Search on the problem defined in file abz9. But first, recall that the Local
16
If you don’t remember anything about the Job-Shop Problem, don’t panic. You can read about it in the
previous chapter and if you prefer you can completely skip the problem definition and our implementation to
solve it (but not the use of LocalSearchOperators as our implementation only works with Local Search).
We only discuss here the use of the TabuSearch meta-heuristic.
17
You might want to reread the section 6.3.1 about variables used for scheduling in ortools.
246
Chapter 7. Meta-heuristics: several previous problems
Can we do better than our best Local Search described in jobshop_ls3.cc? The
best objective value was 931. You can read all about in section 6.7.5.
247
7.5. Simulated Annealing (SA)
tive value of 924 (in 201,604 seconds). Much better than 931 but still very far from the optimal
value of 679. And we had roughly to work twice as hard to go from 931 to 924. This is typical.
To get even better results, one must implement specialized algorithms.
In 1983, the world of combinatorial optimization was literally shattered by a paper of Kirk-
patrick et al. [Kirkpatrick1983] in which it was shown that a new heuristic approach called
Simulated Annealing could converge to an optimal solution of a combinatorial problem, albeit
in infinite computing time20 . This was really unheard of: an heuristic that could converge, if you
give it the time, to an optimal solution! This new approach spurred the interest of the research
community and opened a new domain of what is now known as meta-heuristic algorithms.
We first present the basic idea of SA, keeping a eye on our basic implementation that we present
in details next. As with Tabu Search, we present some first results on the Job-Shop Problem.
248
Chapter 7. Meta-heuristics: several previous problems
Initialization
The initialization is quite simple: start with an initial solution (in or-tools: a local optimum) x0
and compute its energy level e = Energy(x0 ). The initial temperature plays an important role in
the algorithm: higher means that the algorithm (theoretically) has more chances of converging
toward a global optimum [Granville1994]. However, it also means that the algorithm will take
much longer to converge.
Stopping criteria
The usual stopping criteria can be used. However, often, a limited number of iterations
are allowed as we need the T emperature() to decrease towards 0 to compute the transi-
tion/acceptance probabilities P (e, etest , t) and let the algorithm converge.
249
7.5. Simulated Annealing (SA)
Acceptance probabilities
The algorithm accepts or rejects a new solution xtest depending on the computed acceptance
probabilities P (e, etest , t). When the temperature decreases towards 0 with time, this probability
should converge towards 0 when e < etest , i.e. the algorithm only accepts new states if their
energy level is lower than the energy level of the current (last) state. The classical aspiration
criteria is to accept any solution that is better, i.e. P (e, etest , t) = 1 if e > etest . For the algorithm
to be able to escape a local optimum, it must be able to move towards solution of higher energy,
i.e. P (e, etest , t) > 0 even if e < etest .
The way these probabilities are computed is really problem dependent and is a key operation
for a good convergence of the algorithm.
States or iterations?
Each meta-heuristic has its own vocabulary and SA is no exception. Broadly speak-
ing, a state corresponds to an accepted solution x by the SA algorithm while each
test of a neighbor (candidate solution) is considered as an iteration k. Typically, the
Temperature(...) depends on the current iteration and we speak about the energy
level of a state (even if we compute this energy level at each iteration).
Our basic implementation of the SA algorithm differs slightly from the classical
implementation.
250
Chapter 7. Meta-heuristics: several previous problems
Most variables are self-explanatory. The ACMRandom rand_ variable is our random generator
as we need some randomness to generate the probabilities at each iteration. An iteration is
obtained each time we choose a new neighbor (candidate) solution.
The constructor is quite straightforward:
SimulatedAnnealing(Solver* const s, bool maximize,
IntVar* objective, int64 step,
int64 initial_temperature)
: Metaheuristic(s, maximize, objective, step),
temperature0_(initial_temperature),
iteration_(0),
rand_(654),
found_initial_solution_(false) {}
To compute the temperature for each iteration, we use the following (private) method:
float Temperature() const {
if (iteration_ > 0) {
return (1.0 * temperature0_) / iteration_; // Cauchy annealing
} else {
return 0.;
}
}
LocalOptimum()
As you know by now, this is the callback that triggers the meta-heuristic and we set our iteration
counter to 1 when this method is called for the first time.
bool LocalOptimum() {
if (maximize_) {
current_ = kint64min;
} else {
current_ = kint64max;
}
++iteration_;
return found_initial_solution_ && Temperature() > 0;
}
You might be surprised by the redundant test of Temperature() but our code might change
in the future and we need the temperature to be greater than 0. This is called defensive pro-
gramming.
AcceptNeighbor()
As in the Tabu Search, we increase our iteration counter for each accepted solution:
251
7.5. Simulated Annealing (SA)
void AcceptNeighbor() {
if (iteration_ > 0) {
++iteration_;
}
}
AtSolution()
ApplyDecision()
This is again the place to add some constraints to manage the search. We show the code first
and discuss it after:
1 void ApplyDecision(Decision* const d) {
2 Solver* const s = solver();
3 if (d == s->balancing_decision()) {
4 return;
5 }
6
9 if (maximize_) {
10 const int64 bound =
11 (current_ > kint64min) ? current_ + step_ + energy_bound :
12 current_;
13 s->AddConstraint(s->MakeGreaterOrEqual(objective_, bound));
14 } else {
15 const int64 bound =
16 (current_ < kint64max) ? current_ - step_ - energy_bound :
17 current_;
18 s->AddConstraint(s->MakeLessOrEqual(objective_, bound));
19 }
20 }
252
Chapter 7. Meta-heuristics: several previous problems
and 1.0), so the bound is always positive or 0. The higher this bound, the more likely the Local
Search can escape a local optimum by accepting worse solutions. With time, as the temperature
decreases, this bound converges towards the classical bound current_ - step_ which is
the bound used in a regular Local Search: we only go downhill.
Is SA really efficient?
Among the three meta-heuristic we describe in this manual (Tabu Search, Guided Local
Search and SA), SA is certainly the most basic one. Simulated annealing can be seen as a
random walk on a search graph (See [Michiels2007] for instance). This means that a basic
version of SA is unlikely to give good results. As we said above, SA was one of the first
meta-heuristic on the market. Again, the implementation details are very important and a
good SA implementation can beat a sloppy Tabu Search for instance. Moreover, SA is, as
all meta-heuristics are, a canvas that can be further explored and transformed.
You can find the code in the files jobshop.h, jobshop_ls.h, limits.h,
jobshop_sa1.cc and jobshop_sa2.cc and the data in the file abz9.
As with the Tabu Search, we’ll try to improve the search coded in jobshop_ls1.cc
from the previous chapter. You can find the code in the file jobshop_sa1.cc. We only use
the SwapIntervals LocalSearchOperator in the Local Search to solve the Job-Shop
Problem because we want to quickly reach a Local Optimum and compare both Local Searches
with and without Simulated Annealing. We also use the same SearchLimits to stop the
search.
Again, we don’t use an OptimizeVar variable for the objective function as we let the
SimulatedAnnealing Metaheuristic do its job.
The factory method is the following:
SearchMonitor* Solver::MakeSimulatedAnnealing(bool maximize,
IntVar* const v,
int64 step,
int64 initial_temperature) {
return RevAlloc(
new SimulatedAnnealing(this, maximize, v, step, initial_temperature));
}
We don’t post any result as this version is too basic to improve on the local optimum found in
the previous chapter. You can see that the search tries to escape this local optimum but without
253
7.6. Guided Local Search (GLS)
luck: the Local Search is really trapped even when we start with a very high temperature. We
can see that the efficiency of the algorithm also depends on the LocalSearchOperators
used. In this case, this operator simply cycles between a set of solutions.
Will we have better luck if we use the two LocalSearchOperators as in file
jobshop_ls3.cc? Let’s try. The code is in the file jobshop_sa2.cc22 . We don’t
present any code here as the changes are similar to what was done in file jobshop_ts2.cc
to add Tabu Search.
With different initial temperatures, we obtain the same result: a better value of 1016 (coming
from a local optimum of 1051). Not bad but still far from the optimal value 679. The reason
is again that our LocalSearchOperators cycle through some solutions and don’t visit the
whole search space.
Guided Local Search is another successful meta-heuristic that emerged in the ‘90. It has been
successfully applied to a large number of difficult problems and has been particularly successful
in Routing Problems.
Our Guided Local Search implementation is especially tailored for the Routing Library. It uses
a callback to a cost function that takes two int64 indices corresponding to 2 nodes23 , i.e. the
cost of traversing an arc. If you can successfully translate the cost of using two variables i
and j one after the other in your objective function for your specific problem, then you can
use our implementation out of the box. Otherwise, you’ll have to create your own version.
We hope that after reading this section you’ll have a better idea on how you can do it. The
last sub-section gives you some hints if you want to adapt our implementation to solve your
problem.
Our Guided Local Search implementation is especially tailored for the Routing Li-
brary
Along the way, we’ll give you enough information to fully understand (almost) all the code and
understand the Routing Library (RL) conventions24 .
Among the three implemented meta-heuristics implemented in or-tools, GLS has certainly the
most refined and efficient (and thus complicated) implementation.
The GLS is a penalty-based method that sits on top of a Local Search. Its originality and
efficiency stems from the way it penalizes some features of a solution along the search. We
22
We use an OptimizeVar in file jobshop_sa2.cc because the initial solution is found by Local Search.
The OptimizeVar variable is not used in the Simulated Annealing.
23
There is also a version with 3 indices i, j and k where the cost function returns the cost of traversing an arc
(i, j) with a vehicle k, i.e. the cost of traversing an arc depends on the type of vehicles used. Read on.
24
See the sections 9.5 and 13.11 to understand the juicy details. We omit these details here as they are not
important for the understanding of the GLS algorithm.
254
Chapter 7. Meta-heuristics: several previous problems
assume minimization.
The GLS meta-heuristic penalizes some features of a local optimum. Let pi be a penalty at-
tached to a feature i and f denote the original objective function. The GLS meta-heuristic uses
the following augmented objective function g:
X
g(x) = f (x) + λ (Ii (x) · pi )
i
The idea is to let the Local Search find solutions with this new augmented objective function.
λ is called the penalty factor and can be used to tune the search to find similar solutions (a low
λ value, intensification) or completely different solutions (a high λ value, diversification).
Penalties usually start with a 0 value and are incremented by 1 with each local optimum. The
originality and efficiency of the GLS is that a feature is only penalized if its utility is large
enough. The idea is to penalize costly features but not penalize them too much if they often
show up. The utility function for a feature i in a solution x is defined as follows:
ci (x)
ui (x) = Ii (x) .
1 + pi
where ci () denotes the cost associated with feature i in solution x. If a feature i is not present
in a solution x, its utility for this solution is 0 (Ii (x) = 0). Otherwise, the utility is proportional
to the cost ci (x) of this feature in the solution x but tends to disappear whenever this feature
i is often penalized. A feature that shows up regularly in local optima might be part of a good
solution.
Our implementation is at the same time specific for Routing Problems but also generic for any
Routing Problem. The chosen features of a solution is the fact that an arc (i,j) is traversed
or not for this solution. So, we will speak of a (i,j) Arc feature (and talk about cij , uij and
pij ).
Our implementation is practically following the basic GLS guidelines by the book.
Let’s denote by dij the cost of traversing an arc (i, j) in a given solution. In our case, this is
given by the cost of the objective function for that arc and we have cij = dij . This cost can
depend on the type of vehicle used if we use different types of vehicles.
255
7.6. Guided Local Search (GLS)
Within the Routing Library, the penalty factor λ is given by the gflags command line flag
routing_guided_local_search_lambda_coefficient and is set to the value 0,1
by default.
7.6.3 GuidedLocalSearchPenalties
The GuidedLocalSearch class is a pure abstract base class. Two specialized implementa-
tions exist:
• BinaryGuidedLocalSearch (2-indices version): when all vehicles have the same
cost to traverse any arc (i, j) and
• TernaryGuidedLocalSearch (3-indices version): when the cost of traversing an
arc (i, j) also depends on the type of vehicle k.
We discuss these two classes in details later on.
25
The hash_map data structure is compiler dependent but it is exactly what its name says: a hash map.
256
Chapter 7. Meta-heuristics: several previous problems
This struct is called a functor (or function object) and is basically a function call encap-
sulated in a class (or a struct). This is done by overloading the function call operator
(operator()) of the class (or struct)26 .
Notice that we compare the double values attached to each Arcs in the (Arc, double)
pairs. We’ll use this Comparator struct to compare utilities attached to Arcs.
257
7.6. Guided Local Search (GLS)
where step is the usual step used to force the objective function to improve.
The pure virtual methods that must be defined in a specialized GuidedLocalSearch class
are:
virtual int64 AssignmentElementPenalty(const Assignment& assignment,
int index) = 0;
virtual int64 AssignmentPenalty(const Assignment& assignment, int index,
int64 next) = 0;
virtual bool EvaluateElementValue(const Assignment::IntContainer&
container,
int64 index, int* container_index,
int64* penalty) = 0;
virtual IntExpr* MakeElementPenalty(int index) = 0;
The used of 2 indices (in the signature of AssignmentPenalty) indicates that our
GuidedLocalSearch class is really tailored to deal with arcs. The best way to understand
what these methods are supposed to do is to study their implementations in details.
• AssignmentElementPenalty() returns the penalized value associated to the arc
leaving node i in a given solution assignment. This penalized value is (for minimiza-
tion) equal to λ · pij (x) · cij (x) for a given solution x.
We need to do do a little incursion in the Routing Library (RL) before we can go on.
The RL (Routing Library) encodes the traversing of an arc (i, j) in a solution with
vars_[i] = j, i.e. from node i go to node j where vars_[i] denotes the IntVar
variable corresponding to node i and vars_ is an std::vector of such variables.
Back to AssignmentElementPenalty.
Here is the implementation of this method for the BinaryGuidedLocalSearch
class:
int64 AssignmentElementPenalty(
const Assignment& assignment, int index) {
258
Chapter 7. Meta-heuristics: several previous problems
This cost is the same for all vehicles. In the case of the
TernaryGuidedLocalSearch class, we need to take the type of vehicle
traversing the arc (i, j) into account. We added a reference to a given Assignment
assignment to induce from this solution assignment what the type of vehicle
traversing arc (i, j) is. The type of vehicle traversing from node i is given by the
secondary_vars_[i] variable:
int64 AssignmentPenalty(const Assignment& assignment,
int index, int64 next) {
return objective_function_->Run(index, next,
assignment.Value(secondary_vars_[index]));
}
259
7.6. Guided Local Search (GLS)
ized value is stored in a variable pointed to by penalty and the method returns true,
otherwise it returns false.
Here is the implementation for the BinaryGuidedLocalSearch class:
bool EvaluateElementValue(
const Assignment::IntContainer& container,
int64 index,
int* container_index,
int64* penalty) {
const IntVarElement& element = container.Element(*container_index);
if (element.Activated()) {
*penalty = PenalizedValue(index, element.Value());
return true;
}
return false;
}
This method updates the penalty of the whole solution given by a delta Assignment
and is only called in AcceptDelta(). Recall that this delta is the difference be-
tween the last accepted solution xi of the Local Search and the candidate solution we are
currently testing. We will not go into all the details. Just notice how the penalized value
(variable penalty) is updated on lines 14 and 20.
260
Chapter 7. Meta-heuristics: several previous problems
EnterSearch()
LocalOptimum()
The LocalOptimum() method is called whenever a nested Local Search has finished. If
one SearchMonitor returns true in its LocalOptimum() callback, the Local Search is
restarted and the search continues. In this method, we penalize the features of the local optimum
solution according to their utility. Recall that the feature used here is whether the solution
261
7.6. Guided Local Search (GLS)
traverses an arc (i, j) or not. We use the utility function described earlier:
( c (x)
ij
if arc (i, j) is used;
uij (x) = 1+pij
0 otherwise.
and penalize the most expensive used arcs (i, j) according to their utility.
Let’s recall the way the RL (Routing Library) encodes the traversing of an arc (i, j) in a solution
with vars_[i] = j, i.e. from node i go to node j where vars_[i] denotes the IntVar
variable corresponding to node i. If no arc is traversed from node i (for instance node i is an
arrival depot or is not visited at all in a solution), RL’s convention is to set vars_[i] = i.
Because we only update the penalties in this callback, notice that the GLS is only triggered
after a local optimum has been found.
We are now ready to read the code:
1 bool LocalOptimum() {
2 std::vector<std::pair<Arc, double> > utility(vars_.size());
3 for (int i = 0; i < vars_.size(); ++i) {
4 if (!assignment_.Bound(vars_[i])) {
5 // Never synced with a solution, problem infeasible.
6 return false;
7 }
8 const int64 var_value = assignment_.Value(vars_[i]);
9 const int64 value =
10 (var_value != i) ? AssignmentPenalty(assignment_, i, var_value) : 0;
11 const Arc arc(i, var_value);
12 const int64 penalty = penalties_->Value(arc);
13 utility[i] = std::pair<Arc, double>(arc, value / (penalty + 1.0));
14 }
15 Comparator comparator;
16 std::stable_sort(utility.begin(), utility.end(), comparator);
17 int64 utility_value = utility[0].second;
18 penalties_->Increment(utility[0].first);
19 for (int i = 1; i < utility.size() &&
20 utility_value == utility[i].second; ++i) {
21 penalties_->Increment(utility[i].first);
22 }
23 if (maximize_) {
24 current_ = kint64min;
25 } else {
26 current_ = kint64max;
27 }
28 return true;
29 }
262
Chapter 7. Meta-heuristics: several previous problems
var_value)), we compute its cost value: 0 if the arc is not traversed in the solution or
AssignmentPenalty(assignment_, i, var_value) otherwise, i.e. the cost to
c
traverse arc (i, j) in the solution. On line 13, the utility pijij+1 is computed for the outgoing arc
(i, j).
In the second section (lines 15 to 22), we only penalize arcs with the highest utility. First, we
sort the utilities in descending order with the help of our Comparator in lines 15 and 16. On
lines 17 and 18, we penalize the arc with the highest utility. The for loop on lines 19 to 22, pe-
nalize only the arcs with the same utility (utility_value == utility[i].second).
The third section (lines 23 to 28) is by now no surprise. We reset the value of the current_
variable such that we can bound the solutions in the Local Search by a higher value than for
instance the value of the best solution: this allows the meta-heuristic to escape local optima.
AtSolution()
The AtSolution() method is called whenever a solution is found and accepted in the Local
Search
bool AtSolution() {
if (!Metaheuristic::AtSolution()) {
return false;
}
if (penalized_objective_ != nullptr) { // no move has been found
current_ += penalized_objective_->Value();
}
assignment_.Store();
return true;
}
ApplyDecision()
263
7.6. Guided Local Search (GLS)
264
Chapter 7. Meta-heuristics: several previous problems
it like an ObjectiveVar by modifying the upper bound on the domain of the objective
variable. This avoids one more constraint and is perfectly in line with our aspiration criterion
to accept better solution.
The test penalties_->HasValues() on line 7 is true if there is at least one arc with a
positive penalty.
If there is one or more penalties, we enter the code on the lines 8 to 32. For each arc (i, j) ((i,
assignment_.Value(vars_[i]))) , we create an Element expression corresponding
to the Element constraint for the corresponding penalty on line 9. All these Element ex-
pressions are collected into a sum stored in the variable penalized_objective_ on line
18. Lines 11 to 14 compute and store the penalized part of the augmented objective function
individually for each node. We skip lines 16 and 17 as they update variables to use with the
deltas. Finally, we add the constraint mentioned right after the code in lines 19 to 33. Notice
that the part “objective <= current penalized cost - penalized_objective - step” of this constraint
for the current solution reduces to “objective <= objective - step” and that the second part allows
us to accept better solutions (aspiration criterion).
AcceptDelta()
This meta-heuristic is coded efficiently and uses the delta and deltadelta of the
LocalSearchOperators. A quick reminder:
• delta: the difference between the initial solution that defines the neighborhood and the
current candidate solution.
• deltadelta: the difference between the current candidate solution and the previous
candidate solution. We say that the LocalSearchOperator is incremental.
The AcceptDelta() method of a SearchMonitor can accept or refuse a candidate
solution. It is filtering the solutions and the result of this callback in the main Local
Search algorithm (see the sub-section The basic Local Search algorithm and the callback
hooks for the SearchMonitors) is stored in a variable that has a very interesting name:
meta_heuristics_filter.
The AcceptDelta() callback from the GuidedLocalSearch class computes the penal-
ized value corresponding to the deltas and modifies their objective bound accordingly.
1 bool AcceptDelta(Assignment* delta, Assignment* deltadelta) {
2 if ((delta != nullptr || deltadelta != nullptr) &&
3 penalties_->HasValues()) {
4 int64 penalty = 0;
5 if (!deltadelta->Empty()) {
6 if (!incremental_) {
7 penalty = Evaluate(delta,
8 assignment_penalized_value_,
9 current_penalized_values_.get(),
10 true);
11 } else {
12 penalty = Evaluate(deltadelta,
13 old_penalized_value_,
14 delta_cache_.get(),
15 true);
265
7.6. Guided Local Search (GLS)
16 }
17 incremental_ = true;
18 } else {
19 if (incremental_) {
20 for (int i = 0; i < vars_.size(); ++i) {
21 delta_cache_[i] = current_penalized_values_[i];
22 }
23 old_penalized_value_ = assignment_penalized_value_;
24 }
25 incremental_ = false;
26 penalty = Evaluate(delta,
27 assignment_penalized_value_,
28 current_penalized_values_.get(),
29 false);
30 }
31 old_penalized_value_ = penalty;
32 if (!delta->HasObjective()) {
33 delta->AddObjective(objective_);
34 }
35 if (delta->Objective() == objective_) {
36 if (maximize_) {
37 delta->SetObjectiveMin(
38 std::max(std::min(current_ + step_ - penalty, best_ + step_),
39 delta->ObjectiveMin()));
40 } else {
41 delta->SetObjectiveMax(
42 std::min(std::max(current_ - step_ - penalty, best_ - step_),
43 delta->ObjectiveMax()));
44 }
45 }
46 }
47 return true;
48 }
This method returns true on line 47 as it accepts every delta. The whole update can only be
applied if at least a delta is present and if penalties exist. This is precisely the test on lines 2
and 3. The code on lines 4 to 31 updates the penalized value of the candidate solution. The code
is a little bit intricate because it has to be generic: we test the presence of the deltadelta and
delta data structures and update the incremental_ parameter accordingly. When then use
the best (aka most efficient) method to update this penalized value with a call to Evaluate().
On lines 35 to 45, we update the bound of delta: this can speed up the process to accept or
reject this candidate solution.
266
Chapter 7. Meta-heuristics: several previous problems
BinaryGuidedLocalSearch
The BinaryGuidedLocalSearch class is used for Routing Problems where the traversing
of an edge doesn’t depend on the type of vehicles, i.e. the cost is the same for all vehicles.
Here is the constructor:
BinaryGuidedLocalSearch::BinaryGuidedLocalSearch(
Solver* const solver,
IntVar* const objective,
Solver::IndexEvaluator2* objective_function,
bool maximize,
int64 step,
const std::vector<IntVar*>& vars,
double penalty_factor)
: GuidedLocalSearch(solver,
objective,
maximize,
step,
vars,
penalty_factor),
objective_function_(objective_function) {
objective_function_->CheckIsRepeatable();
}
The variables vars are the main variables corresponding to the nodes. The objective function
is a callback that takes two int64 and returns an int64. Basically, it’s the cost of traversing
the arc (i, j).
The corresponding factory method is:
SearchMonitor* Solver::MakeGuidedLocalSearch(
bool maximize,
IntVar* const objective,
ResultCallback2<int64, int64, int64>* objective_function,
int64 step,
const std::vector<IntVar*>& vars,
double penalty_factor) {
return RevAlloc(new BinaryGuidedLocalSearch(this,
objective,
objective_function,
maximize,
step,
vars,
penalty_factor));
}
TernaryGuidedLocalSearch
This version was especially made to deal with heterogeneous costs for different vehicles in the
Routing Library: the cost of an arc also depends on the vehicle used. At the initialization of the
Routing Solver, the GuidedLocalSearch meta-heuristic is created as follow:
267
7.6. Guided Local Search (GLS)
...
switch (metaheuristic) {
case ROUTING_GUIDED_LOCAL_SEARCH:
if (CostsAreHomogeneousAcrossVehicles()) {
optimize = solver_->MakeGuidedLocalSearch(
false, cost_,
NewPermanentCallback(this, &RoutingModel::GetHomogeneousCost),
FLAGS_routing_optimization_step, nexts_,
FLAGS_routing_guided_local_search_lambda_coefficient);
} else {
optimize = solver_->MakeGuidedLocalSearch(
false, cost_,
NewPermanentCallback(this, &RoutingModel::GetArcCostForVehicle),
FLAGS_routing_optimization_step, nexts_, vehicle_vars_,
FLAGS_routing_guided_local_search_lambda_coefficient);
}
break;
...
}
If the costs are the same for all vehicles, we use the int64
RoutingModel::GetHomogeneousCost(int64 i, int64 j) costs. This
method takes two int64: the index of the first node i and the index of the second node j. If
on the contrary, the costs depend on the vehicle traversing an arc (i, j), we use the int64
RoutingModel::GetArcCostForVehicle(int64 i, int64 j, int64 k)
costs: the third int64 k corresponds to the index of the vehicle type used to traverse the arc
(i, j).
The corresponding factory method is:
SearchMonitor* Solver::MakeGuidedLocalSearch(
bool maximize,
IntVar* const objective,
ResultCallback3<int64, int64, int64, int64>* objective_function,
int64 step,
const std::vector<IntVar*>& vars,
const std::vector<IntVar*>& secondary_vars,
double penalty_factor) {
return RevAlloc(new TernaryGuidedLocalSearch(this,
objective,
objective_function,
maximize,
step,
vars,
secondary_vars,
penalty_factor));
}
The secondary secondary_vars variables are simply the variables corresponding to the
vehicles.
268
Chapter 7. Meta-heuristics: several previous problems
GLS is a good meta-heuristic and it might be worth to give it a try to solve your problem.
As we have seen, our implementation of the GLS is heavily optimized: not only do we use
GLS filtering (AcceptDelta()) but the implementation
P is especially tailored for Routing
Problems and objective functions of the form (i,j) cij . What if you have a problem that
doesn’t fit into this canvas? Create your own version of the GLS!
We give you some hints on how to do that in this sub-section.
First, you have to change the call to a 2-indices or 3-indices callbacks to compute the objective
function value.
Second, if you look carefully at the code of the abstract GuidedLocalSearch class, you’ll
find that the only method that really depends on 2 indices is the AssignmentPenalty()
method. This method is only used in the LocalOptimum() callback.
Third, you have to adapt all 2- and 3-indices data structures such as for instance the
GuidedLocalSearchPenalties classes.
Finally, you have to decide if you need GLS filtering or not.
All in all, the GuidedLocalSearch, BinaryGuidedLocalSearch,
TernaryGuidedLocalSearch and GuidedLocalSearchPenalties,
GuidedLocalSearchPenaltiesTable, GuidedLocalSearchPenaltiesMap
classes give you a good example on how to implement your own GLS.
We have seen in the previous chapter that one of the difficulties of Local Search is to define the
right notion of neighborhood:
• too small and you might get stuck in a local optimum;
• too big and you might loose precious time exploring huge neighbourhoods without any
guarantee to find a good solution.
Could we combine advantages of both approaches? Visit huge neighborhoods but only paying
the cost needed to visit small neighborhoods? This is what Very Large-Scale Neighbourhood
(VLSN)27 methods try to achieve. The basic idea is to create large neighborhoods but to only
(heuristically) visit the more interesting parts of it.
Large Neighbourhood Search (LNS) is one of those VLN methods and is especially well
suited to be combined with Constraint Programming.
27
Very Large-Scale Neighbourhood methods are more defined by the fact that the neighborhoods considered
are growing exponentially in the size of the input than the way these neighborhoods are explored. But if you want
to explore these huge neighborhoods efficiently, you must do so heuristically, hence our shortcut in the “definition”
of Very Large-Scale Neighbourhood methods.
269
7.7. Large neighborhood search (LNS): the Job-Shop Problem
The Large Neighborhood Search (LNS) meta-heuristic was proposed by Shaw in 1998
[Shaw1998]. The neighborhood of a solution is defined implicitly by a destroy and a repair
methods. A destroy method destroys part of the current solution while a repair method re-
builds the destroyed solution. Typically, the destroy method contains some randomness such
that different parts of the current solution are destroyed and... different parts of the search tree
visited! This means that the neighborhoods can be seen as larger than in “classical” Local
Search, hence the name.
In its very basic form, we could formulate large neighborhood search like this:
Often, steps 1. and 2. are done simultaneously. This is the case in or-tools.
It looks very much like Local Search, the only difference is the way the neighborhoods are
constructed.
As always, the definition of the destroy and repair methods is a matter of trade-off.
An important concept is the degree of destruction: if only a small part of a solution is destructed,
the LNS misses its purpose and merely becomes a “classical” Local Search method acting on
small neighborhoods. If a very large part (or the entirety) of the solution is destructed, then the
reconstruction process consists in repeated (full) optimizations from scratch.
Various scenarios are possible for the repair method ranging from reconstructing optimally the
destructed (partial) solution or using weak but very quick heuristics to reconstruct it. In the
first case, you obtain the best possible completed solution but it is often costly, in the second
case you obtain a probably bad solution but very quickly. Most probably, you’ll want to use an
intermediate scenario: devise an heuristic that reconstruct quite quickly not too bad solutions.
When Large Neighborhood Search is used in combination with Constraint Programming, we
often use the term fix for the destroy method and optimize for the repair method. Indeed, the
destruction is done by freeing some variables and thus fixing the remaining ones to their current
values and the repairing consists in optimizing this solution while keeping the fixed variables
to their current values.
270
Chapter 7. Meta-heuristics: several previous problems
For IntVars, you can use the BaseLNS class. In this LocalSearchOperator, we have
redefined the OnStart() and MakeOneNeighbor() methods like this:
LocalSearchOperator BaseLNS
OnStart() InitFragments()
MakeOneNeighbor() NextFragment()
A Fragment is just an std::vector<int> containing the indices of the IntVars to “de-
stroy”, i.e. to free. The other IntVars keep their current values. The complementary
DecisionBuilder given to the LocalSearchOperator will repair the current solu-
tion. The signature of the NextFragment() is as follow:
virtual bool NextFragment(std::vector<int>* fragment) = 0;
This method is a pure virtual method and must be defined. To free some variables, you
fill the fragment vector with the corresponding indices. This method returns true if
their are still candidates solutions in the neighborhood, false otherwise (exactly like the
MakeOneNeighbor() method).
Let’s use a basic LNS to solve our basic problem. We’ll free one variable at a time in the order
given by the std::vector of IntVars. First, we initialize the index of the first variable in
InitFragments():
virtual void InitFragments() { index_ = 0; }
where index_ is a private int indicating the current index of the variable we are about to
destroy.
The NextFragment() method is straightforward:
virtual bool NextFragment(std::vector<int>* fragment) {
const int size = Size();
if (index_ < size) {
fragment->push_back(index_);
++index_;
return true;
} else {
return false;
}
}
271
7.7. Large neighborhood search (LNS): the Job-Shop Problem
This time, let’s repair optimally the destroyed solution. The NestedOptimize
DecisionBuilder is exactly what we need as a complementary DecisionBuilder.
It will collapse a search tree described by a DecisionBuilder db and a set of monitors
and wrap it into a single point.
There exist several factory methods to construct such a NestedOptimize
DecisionBuilder but all need an Assignment to store the optimal solution found:
Assignment * const optimal_candidate_solution = s.MakeAssignment();
optimal_candidate_solution->Add(vars);
optimal_candidate_solution->AddObjective(sum_var);
where db is the DecisionBuilder used to optimize, solution stores the optimal solu-
tion found (if any), maximize is a bool indicating if we maximize or minimize and step is
the classical step used to optimize.
For our basic example, we use a basic DecisionBuilder to optimize:
DecisionBuilder * optimal_complementary_db = s.MakeNestedOptimize(
s.MakePhase(vars,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MAX_VALUE),
optimal_candidate_solution,
false,
1);
272
Chapter 7. Meta-heuristics: several previous problems
5 solutions were generated with decreased objective values. Solution #0 is the initial solu-
tion given: [3, 2, 3, 2]. For the next 4 solutions, the NestedOptimize DecisionBuilder
did its job and optimized the partial solution:
neighborhood 1 around [3, 2, 3, 2]: [−, 2, 3, 2] is immediately taken as the complementary
DecisionBuilder transforms it into the optimal (for this DecisionBuilder) so-
lution [1, 2, 3, 2] with an objective value of 8.
neighborhood 2 around [1, 2, 3, 2]: [−, 2, 3, 2] is rejected as the optimal solution [1, 2, 3, 2]
doesn’t have a better objective value than 8. [1, −, 3, 2] is immediately accepted as the
optimal solution constructed is [1, 0, 3, 2] with an objective value of 6.
neighborhood 3 around [1, 0, 3, 2]: [−, 0, 3, 2] and [1, −, 3, 2] are rejected and [1, 0, −, 2] is
accepted as the optimal solution constructed is [1, 0, 0, 2] with an objective value of 3.
neighborhood 4 around [1, 0, 0, 2]: [−, 0, 0, 2], [1, −, 0, 2] and [1, 0, −, 2] are rejected while
[1, 0, 0, −] is accepted as the optimal solution constructed [1, 0, 0, 0] has an objective
value of 1.
The two last lines printed by the SearchLog summarize the local search:
Finished search tree, ..., neighbors = 10, filtered neighbors = 10,
accepted neigbors = 4, ...)
End search (time = 1 ms, branches = 58, failures = 57, memory used =
15.21 MB, speed = 58000 branches/s)
Objective value = 1
There were indeed 10 constructed candidate solutions among which 10 (filtered neighbors)
were accepted after filtering (there is none!) and 4 (accepted neighbors) were improving solu-
tions.
For this basic example, repairing optimally led to the optimal solution but this is not necessarily
the case.
At the moment of writing (28 th of February 2015, rev 3845), there are only a few specialized
LNS operators. All concern IntVars:
• There are two basic LNS operators:
273
7.7. Large neighborhood search (LNS): the Job-Shop Problem
SimpleLNS
RandomLNS
274
Chapter 7. Meta-heuristics: several previous problems
fragment->push_back(rand_.Uniform(Size()));
}
return true;
}
You can find the code in the files jobshop_ls.h, jobshop_lns.h, jobshop_lns.cc
and jobshop_heuristic.cc and the data file in abz9.
SequenceLns
Ranked sequences
...
To allow for some diversity, from time to time this operator destroys completely two
SequenceVars.
For SequenceVars, there are no specialized LNS operators. We thus inherit from
SequenceVarLocalSearchOperator:
275
7.7. Large neighborhood search (LNS): the Job-Shop Problem
random_ is again an object of type ACMRandom and max_length is the maximal num-
ber of IntervalVars to destroy in each SequenceVar. It’s a upper bound because the
SequenceVar could contain less IntervalVars.
We use again our template for the MakeNextNeighbor() method:
virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (random_.Uniform(2) == 0) {
FreeTimeWindow();
} else {
FreeTwoResources();
}
if (ApplyChanges(delta, deltadelta)) {
VLOG(1) << "Delta = " << delta->DebugString();
return true;
}
}
return false;
}
276
Chapter 7. Meta-heuristics: several previous problems
std::vector<int> backward;
for (int j = sequence.size() - 1;
j >= start_position + current_length;
--j) {
backward.push_back(sequence[j]);
}
SetForwardSequence(i, forward);
SetBackwardSequence(i, backward);
}
}
We use the SequenceLNS in the file jobshop_lns.cc to solve the Job-Shop Problem.
Four parameters are defined through gflags flags:
• time_limit_in_ms: Time limit in ms, 0 means no limit;
• sub_sequence_length: The sub sequence length for the ShuffleIntervals
LS operator;
• lns_seed: The seed for the LNS random search;
• lns_limit: maximal number of candidate solutions to consider for each neighborhood
search in the LNS.
When we try to solve the abz9 instance with our default parameters, we quickly find this
solution
Solution #190 (objective value = 802, ..., time = 10612 ms, ...,
neighbors = 1884, ..., accepted neighbors = 190, ...)
After only 10 seconds, we obtain a feasible solution with an objective value of 802. Much better
than what we obtained in the previous chapter (the best value was 931)! Large Neighborhood
Search (and its randomness) widens the scope of the neighborhood definition and allows to
search a bigger portion of the search space but still it doesn’t avoid the local trap. jobshop_lns
seems to get stuck with this solution. In the next sub-section, we use the Local Search operators
defined in the previous chapter and the SequenceLNS operator together.
Everything together
277
7.8. Default search
Since we discussed the code in file jobshop_ls3.cc, the CP Solver has evolved and has
been improved in general. Unfortunately, for this precise code the Solver seems to be stuck in
a local optimum with a of value of 809. This might change in the future.
What do you do if you face a problem that doesn’t inspire you? Or that is too complicated
to devise a customized search strategy? Use a default search! Several search strategies were
devised to tackle any problem. To do so, they use generic methods that can be used with-
out too much specific knowledge of the problem at hand. How can they do that? Simple.
They use the model you provide and test some hypotheses to devise a dynamic search strat-
egy. This concept is rather advanced but you can easily use our DefaultIntegerSearch
DecisionBuilder. As its name implies, this DecisionBuilder only deals with
IntVars.
Several general-purpose strategies for Constraint Programming are based on the concept of
impacts. While the basic ideas are quite simple, the implementation details are cumbersome
and must be analyzed with great details.
Roughly speaking, an impact measures the search space reduction of a basic variable assign-
ment xi = v. When a variable is assigned, the constraints are propagated in a way or another
and, hopefully, the domains of the other variables shrink and the overall search space dimin-
ishes.
In the section Second try: dynamic variable selection (and define our own DecisionBuilder
class), we have encountered and implemented the first fail principle:
To succeed, try first where you are most likely to fail,
and the best success principle:
To find a solution, keep your options as large as possible.
Both principles are popular among CP experts. In practice one chooses first the variables that
are the most constrained or that have the smallest domains (first fail principle) and then, once a
variable has been chosen, choose a value that maximizes the number of possibilities for future
assignments in the hope that if a solution exists which such assignment you will find it (best
success principle).
In the section Second try: dynamic variable selection (and define our own DecisionBuilder
class), we used these two principles: first we choose the queens that had the smallest domains
28
A side note for MIP practitioners: there are strong similarities between impacts and pseudo-costs. Indeed
impacts were devised with pseudo costs and general branching schemes from MIP in mind. See [refalo2004] for
more.
278
Chapter 7. Meta-heuristics: several previous problems
starting from the center, and then we placed these queens in the best way to keep the most
options open for the other queens choosing the row with the least compatible columns, again
starting from the center.
Impact-based searches try to replicate exactly that: balancing these those principles. Most of
the time, this is done dynamically, i.e. impacts (or rather estimates of impacts) are evaluated
at each node of the search tree. It is also efficient to take some time before the search starts
to evaluate good variable candidates or to restart the search with the knowledge obtained from
the previous search(es). The idea here is to construct a search tree that is as small (efficient) as
possible.
Other ingredients can also be added to the mix.
Definition of an impact
For a nice introduction to the concept of impacts, we refer the reader to [refalo2004]. We use
the same notation as in this article.
Consider the number of all possible combinations of values for the variables as an estimation
P of the size of the search tree:
If we look at this product before (Pbef ore ) and after (Paf ter ) an assignment xi = a we have an
estimation of the importance of this assignment for reducing the search space:
Paf ter
I(xi = a) = 1 −
Pbef ore
7.8.2 DefaultPhaseParameters
279
7.8. Default search
As you can see, we try to maximize the impact for the selected variable, following the
first fail principle.
• value_selection_schema: This parameter describes which value to select for a
given variable. Its type is the following enum:
enum ValueSelection {
SELECT_MIN_IMPACT = 0,
SELECT_MAX_IMPACT = 1,
};
This time, we propose both the minimization or maximization of the impact. By default,
we try to minimize it, following the best success principle.
• run_all_heuristics (bool): The default phase will run heuristics periodically.
This Boolean parameter indicates if we should run all heuristics, or a randomly selected
one. Check the file default_search.cc to see the different heuristics chosen to
assign variables and values. Most of them are a combination between specific search
strategies and randomness.
• heuristic_period (int): The distance in nodes between each run of the heuris-
tics. A negative or null value means that no heuristic is run.
• heuristic_num_failures_limit (int): The failure limit for each heuristic
that we run.
280
Chapter 7. Meta-heuristics: several previous problems
• persistent_impact (bool): Whether to keep the impact from the first search
for other searches or to recompute the impact for each new search.
• random_seed (int): Seed used to initialize the random part in some heuristics.
• decision_builder (DecisionBuilder*): When defined, this overrides the
default impact based DecisionBuilder.
We use the Golomb Ruler Problem from the chapter Using objectives in constraint pro-
gramming: the Golomb Ruler Problem to illustrate the use of the default search phase. No
need to remember the Golomb Ruler Problem, we just want to compare our default strategy
(CHOOSE_FIRST_UNBOUND then ASSIGN_MIN_VALUE) with the default phase search.
We take exactly the same model in both cases (see golomb7.cc).
There are two factory methods you can use to define a DefaultIntegerSearch
DecisionBuilder:
DecisionBuilder* Solver::MakeDefaultPhase(
const std::vector<IntVar*>& vars) {
DefaultPhaseParameters parameters;
return MakeDefaultPhase(vars, parameters);
}
DecisionBuilder* Solver::MakeDefaultPhase(
const std::vector<IntVar*>& vars,
const DefaultPhaseParameters& parameters) {
return RevAlloc(new DefaultIntegerSearch(this, vars, parameters));
}
The first one uses the DefaultPhaseParameters struct with its default values, the
second one accepts a customized DefaultPhaseParameters struct.
Let’s try the default DefaultPhaseParameters (file
golomb_default_search1.cc) and the Default Search to solve the Golomb Ruler
Problem with n=9. Let’s compare our new result with the results of the chapter Using
objectives in constraint programming: the Golomb Ruler Problem in the next Table29 :
Impl1 Impl2 Impl2+ Impl3 Impl3+ Default Search
1,513 0,79 0,812 0,163 0,059 1,378
Times are given in seconds.
We need to tweak a little bit our DefaultPhaseParameters struct if we
want to have a chance of beating the implementations Impl2 to Impl3+ (file
golomb_default_search2.cc):
29
If you compare the results with the ones written in section 3.7, you’ll see that not only did we change of
computer but that the library has evolved since we wrote chapter 3.
281
7.9. Summary
DefaultPhaseParameters parameters;
parameters.var_selection_schema =
DefaultPhaseParameters::CHOOSE_MAX_VALUE_IMPACT;
parameters.value_selection_schema =
DefaultPhaseParameters::SELECT_MAX_IMPACT;
parameters.heuristic_period = -1;
parameters.restart_log_size = -5;
parameters.use_no_goods = false;
7.9 Summary
All the meta-heuristics seen in this chapter sits on top of a Local Search. In or-tools, we made
the choice to only trigger a meta-heuristic after a local optimum has been reached.
We have seen in details the three meta-heuristics implemented in or-tools with a
Metaheuristic class:
• Tabu Search;
• Simulated Annealing and
• Guided Local Search.
For each of them, we have seen our implementation in full details.
We have also seen how we implemented Large Neighborhood Search with specialized
LocalSearchOperators.
Because our meta-heuristics never stop, we have seen how to implement customized
SearchLimits that stop the search when a specific stopping criterion is reached. When you
don’t know what do to do, we have seen that you can use default search strategies that evolve dy-
namically. In particular, we have seen the DefaultIntegerSearch DecisionBuilder
and how restarting a search might help.
30
These parameters were obtained after some trials.
31
Once again, until now, no-one could ever come with a clever algorithm using only Constraint Programming.
282
Chapter 7. Meta-heuristics: several previous problems
As we have seen in this chapter, our three meta-heuristics aren’t a silver bullet and this is
normal: our implementations are too generic. To solve a problem efficiently, you really need
to use the structure of the problem to help the solver as much as possible. We have also seen
that the quality of the LocalSearchOperators involved is crucial: the meta-heuristics can
only guide the Local Search. If the Local Search limits itself to visit only a small part of the
search space, the meta-heuristics are helpless.
The strength of meta-heuristics based on Local Search comes from learning while searching.
First of all, most meta-heuristics depend on some parameters. Finding the right parameters to
solve a specific instance is in itself a hard problem. Second, these parameters can be changed
dynamically during the search and the most efficient searches evolve dynamically. Think about
intensification versus diversification. Tabu Search learns about what features to forbid or keep
and Guided Local Search penalizes some features (and thus rewards others). Learning which
parameters to favor or not happens during the search and we talk about reactive search. One
could say that escaping a local optimum is only a (although a very important) byproduct of this
learning process: the meta-heuristic “understands” it is stuck and seeks to move forward past
this local optimum, taking the history of the search into account.
283
CHAPTER
EIGHT
Files:
8.1.1 Definition
return MakeAllDifferent(vars);
} else {
return RevAlloc(new AllDifferentExcept(this, vars, escape_value));
}
}
8.1.3 Use
8.2.1 BaseObject
8.2.2 Demons
PropagationBaseObject
286
Chapter 8. Custom constraints: the alldifferent_except_0 constraint
virtual ~AllDifferentExcept() {}
private:
std::vector<IntVar*> vars_;
const int64 escape_value_;
};
basic_constraint_example
287
8.5. First approach: model the constraint
8.7.1 Well
alldifferent_except_0
8.8 Summary
288
Part III
Routing
CHAPTER
NINE
The third part of this manual deals with Routing Problems: we have a graph1 and seek to find
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, fixed 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 two chapters each deal with one of two 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 ?? 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 scientific communities.
In this chapter, we’ll discover the RL with what is probably the most studied problem in Oper-
ations 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 López-Ibáñez-Blum and da Silva-Urrutia formats.
1
A graph G = (V, E) is a set of vertices (the set V ) connected by edges (the set E). 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 you’ll find
much more scientific articles under the American spelling: traveling.
4
The ePiX library uses the TEX/LATEX engine to create beautiful graphics.
Overview:
We start this chapter by presenting in broad terms the different categories of Routing Prob-
lems 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:
Files:
292
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
This section is meant to make you aware that the classification of Routing Problems is intricate5 .
Actually, there is no real and widely adopted classification67 .
All the Routing Problems are somewhat related to each others and to Scheduling Problems8 .
We can roughly divide Routing Problems in three broad - and often overlapping - categories:
• Node Routing Problems (NRP)
• Vehicle Routing Problems (VRP)
• Arc Routing Problems (ARP)
For each category, we give an informal definition, list some known mathematical problems,
refer an authoritative source and present quickly the examples we detail in each chapter of part
III.
Be aware of the complexity of the classification of Routing Problems when you
search for a specific routing problem.
Most problems have variants and sometimes are known under different names. For instance,
the Cumulative Travelling Salesman Problem is also known as:
• The Travelling Salesman Problem with cumulative costs
• The Travelling Repairman Problem
• The Deliveryman Problem
• The Minimum Latency Problem
P
• The 1/sjk / Cj 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 classification 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.
293
9.1. A whole zoo of Routing Problems
We now present the three broad categories of Routing Problems. All are Optimization Problems
where we try not only to find a solution but a good solution or even a best solution. Most
problems minimize an objective function along the routes defined in the solution. Typically,
the objective function is the sum of the weights of the edges/arcs/nodes the solution is made of
and a cost for each of the vehicles when more than one is involved.
One main difference between Arc Routing Problems and Node Routing Problems is that basic
ARPs (like the Chinese Postman Problem on undirected and directed graphs) are easy problems
while basic NRPs (like the Metric Travelling Salesman Problem) are intractable. But add some
basic constraints and/or consider mixed graphs and the ARPs too become intractable. More
often than not, the size of ARPs we are able to solve are an order of magnitude smaller than
the size of the corresponding NRPs we are able to solve. This can be partly explained by
the fact that NRPs received (and still receive) more attention than their equivalent ARPs from
the scientific community but ARP specialists tend to believe that ARPs are intrinsically more
difficult than NRPs.
VRPs are often used to model real transportation problems where goods/services/people are
moved from one point to another and as such must respect lots of side constraints (capacities,
delivery times, etc.).
Informal definition:
The term Node Routing Problem (NRP) is seldom used9 and mainly refers to Travelling Sales-
man Problems (TSP)-like problems. In this manual, when we refer to NRP, we mean TSP-like
problems, i.e. routing problems where nodes must be visited and served. We use it to refer to
node-related Routing Problems and in contrast to arc-related Routing Problems. Most of the
NRPs consider 1 vehicle of ∞ capacity, i.e. we seek one tour that covers all the required nodes.
Some problems
294
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
Authoritative source:
The TSPTW:
Informal definition:
Vehicle Routing Problems (VRPs) are concerned with a fleet of (maybe heterogeneous) vehi-
cles. The number of vehicles can be fixed in advance or be a variable of the problem. Generally,
a vehicle has a certain capacity (number of people, number of tons of goods, etc.) and must re-
spect some “time”-constraints (like the total duration of a route, time windows to serve clients,
etc.). Clients are usually modelled by nodes and to solve a VRP, one seeks to find several routes
(1 per vehicle) that visit all clients and respect all given constraints.
Some problems
295
9.1. A whole zoo of Routing Problems
Authoritative source:
Golden, Bruce L.; Raghavan, S.; Wasil, Edward A. (Eds.). The Vehicle Routing Problem: Lat-
est Advances and New Challenges. Springer, Series: Operations Research/Computer Science
Interfaces Series, Vol. 43, 2008, 589 p.
The CVRP:
Informal definition:
In Arc Routing Problems, we visit and serve edges and/or arcs. Most of the problems consider
1 vehicle of ∞ capacity, i.e. we seek one tour that covers all the required edges and/or arcs.
Some problems
Authoritative source:
Dror, M. (Ed.). Arc Routing: Theory, Solutions and Applications. Kluwer Academic Publish-
ers, Dordrecht, 2000.
The CCPP:
296
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The vehicle routing library lets one model and solve generic routing problems ranging from
the Travelling Salesman Problem to more complex problems such as the Capacitated Vehicle
Routing Problem with Time Windows. In this section, we present its main characteristics.
9.2.1 Objectives
To be precise, the RL only uses one model to solve different Routing Problems. It’s a one
fits all. This approach has its advantages and disadvantages. On one side, the model already
exists, has been tested and fine-tuned by our team and you can reuse it to solve several Routing
Problems (meaning the learning curve is low). On the other side, if you need to solve a very
difficult Routing Problem, you probably would like to build one specialized model yourself.
Our RL can then serve as an inspiration.
The RL lets you model a wide range of vehicle routing problems from the Travelling Sales-
man Problem (and its variants, ATSP, TSPTW, ...) to multi-vehicles problems with dimension
constraints (capacities, time windows, ...) and various routing constraints (optional nodes, al-
ternate nodes, ...). Have a look at subsections 9.2.6 and and 9.2.7 below to have an idea of the
additional constraints you can use in this model.
The RL is a layer above the CP Solver. Most of the internal cabling is hidden but can be
accessed anytime. Everything is contained is one single class: the RoutingModel class.
This class internaly uses an object of type Solver that can be accessed and queried:
RoutingModel routing(...);
Solver* const solver = routing.solver();
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.
297
9.2. The Routing Library (RL) in a nutshell
We are mainly using CP-based Local Search and Large Neighborhood Search using routing-
specific neighborhoods. Implementations of Tabu Search (TS), Simulated Annealing (SA) and
Guided Local Search (GLS) are available too and have proven to give good results (especially
GLS).
To tune and parametrize the search, use command-line gflags. For instance, you might want to
use Tabu Search and limit the allowed solving time to 3 minutes:
./my_beautiful_routing_algorithm --routing_tabu_search=true
--routing_time_limit=180000
This is equivalent to calling the program with the gflag 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 don’t 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
298
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 simplified (and optimized!) the process to create these sets: just
call the appropriate AddDisjunction() method(s).
The same way that nodes don’t have to be visited, vehicles/routes don’t have to be used, i.e.
some vehicles/routes can be optional. You might want to minimize the number of vehicles
needed as part of your problem.
The RL offers the possibility to deal with different vehicles with each its own
cost(s)/particularities.
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.
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
Or can you call them features of the RL?
11
At least, to the best of our knowledge. See the subsection Can CP be compared to the holy grail of Operations
Research? for more.
12
See the chapter on Arc Routing for a discussion about which Arc Routing Problems can be solved by the RL.
299
9.3. The Travelling Salesman Problem (TSP)
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, ...);
The way the model is coded (see section ??) doesn’t allow you to visit a node more than once.
You can have several vehicles at one depot though.
A depot is a depot
This means you can only start from a depot and/or arrive to a depot, not transit through a depot.
Most Routing Problems are intractable and we are mainly interested in good approximations.
This is not really a limitation. You just need to know that by default you won’t have any
guarantee on the quality of the returned solution(s). You can force the RL to return proven
optimal solutions but the RL wasn’t coded with exact solutions and procedures in mind.
The Travelling Salesman Problem (TSP) is probably the most known and studied prob-
lem in Operations Research. In this section, we briefly15 present this fascinating problem
13
And thus the number of vehicles too!
14
If your platform restricts you too much, you can always adapt the code!
15
Google TSP, Traveling Saleman Problem or Travelling Salesman Problem to find lots of examples, explana-
tions, applications, etc.
300
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.
Given a graph G = (V, E) and pairwise distances between nodes, the TSP consists in finding
the shortest possible path that visits each node exactly once and returns to the starting node.
You can think about a salesman that must visit several cities and come back to his hometown,
hence the name the problem.
The cost we want to minimize is the sum of the distances along the path. Although there is
a special vertex called the depot from which the tour starts and ends, we are really concerned
with the overall cost of the tour, i.e. the we could start and end the tour at every node without
changing the objective cost of the tour.
Below you can find a picture of a solution of the TSP with 280 cities (a280) in the section
Visualization with ePix.
The best algorithms can now routinely solve TSP instances with then thousands of nodes to
optimality16 .
These instances are out of scope of the Constraint Programming paradigm17 . CP shines when
you consider complicated side constraints like the addition of time windows: each customer
(represented by a node) has to be serviced inside a given time interval.
16
The record at the time of writing is the pla85900 instance in Gerd Reinelt’s 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.
301
9.3. The Travelling Salesman Problem (TSP)
If you want to know more about the TSP, visit the TSP page which is the central place to
discover this fascinating problem and hosts the best known implementation to solve the TSP
(and it’s open source!). You also might be interested in the 8th DIMACS Implementation
Challenge held in 2001 about the TSP.
Several known benchmark data sources are available on the internet. One of the most known is
the TSPLIB page. It’s a little bit outdated but it contains a lot of instances and their proven op-
timal solutions. Their TSPLIB format is the de facto standard format to encode TSP instances.
The TSPLIB format is explained in great details in the document TSPLIB95. Here is a small
excerpt to understand the basics. Refer to the TSPLIB95 document for more. The complete
TSPLIB collection of problems has been successfully solved to optimality with the Concorde
code in 2005-2006.
The convention in the TSPLIB is to number the nodes starting at 1. We’ll adopt this convention
here too. The Routing Library (RL) on the contrary starts numbering its nodes at 0.
Nodes are numbered from 1 to n in the TSPLIB and we keep this convention in this
chapter.
The TSPLIB not only deals with the TSP but also with related problems. We only detail one
type of TSP instance files. This is what the file 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 file 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.
302
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
3 270 133
4 256 141
5 256 157
6 246 157
...
EOF
Some of the attributes don’t need any explanation. The TYPE keyword specifies 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
specifies how the edge weight are defined. 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 identifier and (x,y) are Cartesian coordinates unless
otherwise stated. The coordinates don’t 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.
Solution files 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 file 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
303
9.3. The Travelling Salesman Problem (TSP)
280
-1
Since this file contains an optimal tour, there are no sub-tours and the list of integers contains
only one -1 at the end of the file.
The TSPData class basically encapsulates a 2-dimensional matrix containing the distances
between all nodes. For efficiency reasons, we use a 1-dimensional matrix with a smart pointer
defined in the header base/scoped_ptr.h:
private:
scoped_array<int64> matrix_;
method. It parses a file in TSPLIB format and loads the coordinates (if any) for further treat-
ment. Note that the format is only partially checked: bad inputs might cause undefined be-
haviour.
304
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 files of the TSPLIB and should hopefully read
any correct TSPLIB format for the TSP.
To visualize the solutions, we use the excellent ePiX library. The file tsp_epix.h contains
the TSPEpixData class. A TSPEpixData object is related to a RoutingModel and a
TSPData. Its unique constructor signature is
TSPEpixData(const RoutingModel & routing, const TSPData & data);
The first method takes an Assignment while the second method reads the solution from a
TSPLIB solution file.
You can define 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 file is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp
305
9.4. The model behind the scenes: the main decision variables
You can also print the node labels with the flag:
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 file tsplib_solution_to_epix.cc. To use it, invoke:
./tsplib_solution_to_epix TSPLIB_data_file TSPLIB_solution_file >
epix_file.xp
We present the main decision variables of the model used in the RL. In section 13.11, we
describe the inner mechanisms of the RL in details. A Routing Problem is defined on a graph
(or a network). The nodes of the graph have unique NodeIndex identifiers. Internally, we use
an auxiliary graph to model the Routing Problem. In RL jargon, the identifiers of the nodes of
this auxiliary graph are called int64 indices. Be careful not to mix them up. To distinguish
one from the other, we use two non-compatible types: NodeIndex and int64.
A node of the original graph can be:
• a transit node;
• a starting depot;
• an ending depot;
• a starting and an ending depot.
A depot cannot be an transit node and a transit node can only be visited by at most one vehicle
in a solution. The number of vehicles can be arbitrary (within the limit of an int).
306
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The model is node based: routes are paths linking nodes. For almost each node19 , we
keep an IntVar* variable (stored internally in a private std::vector<IntVar*>
nexts_) that tells us where to go next (i.e. to which node). To access these variables, use
the NextVar() method (see below). These variables are the main decision variables of our
model.
For a transit node that is uniquely visited by a vehicle20 , we only need one variable. For a depot
where only a route finishes, it is even easier since we don’t need any variable at all because the
route stops at this depot and there is no need to know where to go next. The situation is a little
bit messier if for instance we have two vehicles starting from the same depot. One variable will
not do. In the RL, we deal with this situation by duplicating this depot and give each node its
own IntVar* variable in the std::vector<IntVar*> nexts_.
Internally, we use int64 indices to label the nodes and their duplicates. These int64 indices
are the identifiers of the nodes of an auxiliary graph we present in the next sub-section.
The domains of the IntVar nexts_ variables consist of these int64 indices. Let’s say we
have a solution solution and a RoutingModel object routing. In the following code:
int64 current_node = ...
int64 next_node_index = solution.Value(routing.NextVar(current_node));
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 dif-
ference between NodeIndex node identifiers and int64 indices representing nodes in solu-
tions.
To understand how the auxiliary graph is constructed, we need to consider a more general
Routing Problem than just a TSP with one vehicle. We’ll use a VRP with four vehicles/routes.
Let’s take the original graph of the next figure:
19
Not every node, only the nodes that lead somewhere in the solution. Keep reading.
20
Remember that we don’t 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 simplified version of the section The auxiliary graph from the chapter Under the hood.
307
9.4. The model behind the scenes: the main decision variables
1 0 4
1
0
2
8
5
3 6 7
1
0
0
1
Starting depot
1
0
0Ending depot
1
1
0 Starting and ending depot
Transit node
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 n. In the RL, you must number your
original nodes from 0 to n − 1. If you don’t follow this advice, you might get some surprises!
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 figure:
1 0 2 1
0
0
14
0
1 8
5 0
1
0
1
3 6 71
0
1
00
1
01
10
Starting depot
1
0
0Ending depot
1
1
0 Starting and ending depot
Transit node
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 finally 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.
308
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The way these nodes are numbered doesn’t matter for the moment. For our example, the next
figure shows this numbering:
1 0 2 1
0
9
0
1
0 4
1 7
5 10 1
0
0
1
3 6 11 1
0
0
1
0
1
01
1 0
12
8
Starting depot
1Ending depot
0
0
1
1
0 Starting and ending depot
Transit node
Note that the int64 indices don’t depend on a given solution but only on the given
graph/network and the depots.
A NodeIndex behaves like a regular int but it is in fact an IntType. We use IntTypes
to avoid annoying automatic castings between different integer types and to preserve a certain
type-safety. A NodeIndex is a NodeIndex and shouldn’t be compatible with anything else.
A value() method allows the cast thought:
RoutingModel::NodeIndex node(12);
// the next statement fails to compile
int64 myint = node;
// this is permitted
int64 myint = node.value();
Behind the scene, a static_cast is triggered. If you are following, you’ll understand that
RoutingModel::NodeIndex node = 12;
They are quicker and safer than a static_cast and ... give the correct results!
23
Have a look at base/int-type.h if you want to know more about the IntType class.
309
9.4. The model behind the scenes: the main decision variables
How can you find the int64 index of a depot? You shouldn’t use the method
NodeToIndex() to determine the int64 index of a starting or ending node in a route.
Use instead
int64 Start(int vehicle) const;
int64 End(int vehicle) const;
Once you have a solution, you can query it and follow its routes using the int64 indices:
RoutingModel routing(10000, 78); // 10000 nodes, 78 vehicles/routes
...
const Assignment* solution = routing.Solve();
...
const int route_number = 7;
for (int64 node = routing.Start(route_number); !routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {
RoutingModel::NodeIndex node_id = routing.IndexToNode(node);
// Do something with node_id
...
}
const int64 last_node = routing.End(route_number);
RoutingModel::NodeIndex node_id = routing.IndexToNode(last_node);
// Do something with last node_id
...
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.
Only internal nodes that can lead somewhere possess a decision variable. Only the nodes that
are visited and the starting depots have a main decision IntVar variable. There are 9 original
310
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
nodes in the next figure. They have a NodeIndex ranging from 0 to 8. There are 2 starting
depots (1 and 7) and 2 ending depot (5 and 8). Route 0 starts at 1 and ends at 5 while route 1
starts at 7 and ends at 8.
Var 1 Var 4
1 Path p0
Var 0 4
5
0 Var 5
Var 6 6
Var 2
Var 3
2 7
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 p0 : 1 -> 0 -> 2 -> 3 -> 5
• Path p1 : 7 -> 4 -> 6 -> 8
If we look at the internal int64 indices, we have:
• Path p0 : 1 -> 0 -> 2 -> 3 -> 7
• Path p1 : 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 sub-
section 13.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
311
9.5. The model behind the scene: overview
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));
In this section, we give an overview of the main basic components of our model. Most of these
components will be detailed in this chapter and the next two chapters. In section 13.11, we
describe the inner mechanisms of the RL in details.
If you haven’t already read the section 9.4 about the main decision variables and the
auxiliary graph, we strongly recommend that you do so before reading this section.
All ingredients are defined within the RoutingModel class. This class is declared in the
header constraint_solver/routing.h.
As already mentionned, the RL is a layer above the CP Solver and the internal cabling is
accessible through the underlying solver:
24
The CP solver does an initial propagation to quickly skim these domains.
312
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
For the rest of this section, we only use the internal int64 indices except if the
indices are explicitly of type RoutingModel::NodeIndex.
313
9.5. The model behind the scene: overview
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.
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) repre-
sents 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));
314
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
A node doesn’t 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).
Dimension variables
Dimension variables are used to accumulate quantities (or dimensions) along the routes. To de-
note 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, defined with callbacks, vectors or matrices. You can rep-
resent any quantities along routes with dimensions but not only. For instance, capacities and
time windows can be modelled with dimensions. We’ll play with dimensions at the end of
this chapter when we’ll 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.
No cycle constraint
One of the most difficult constraint to model is a constraint to avoid cycles in the solutions.
For one tour, we don’t want to revisit some nodes. Often, we get partial solutions like the one
25
Well, as many as your memory allows...
315
9.5. The model behind the scene: overview
(a) (b)
It is often easy to obtain optimal solutions when we allow cycles (like in figure (a)) but difficult
to obtain a real solution (like in figure (b)), i.e. without cycles. Several constraints have been
proposed in the scientific 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 (defined 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 don’t necessarily
end) in the auxiliary graph.
The constraint essentially performs two actions:
• forbid partial paths from looping back to themselves and
• ensure each variable/node can be connected to a sink.
26
For the specialists: for instance, primal and dual degenerate linear models.
316
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
We refer the reader to subsection 13.11.4 for a detailed description of our internal NoCycle
constraint.
Disjunction constraints
Disjunctions on a group of nodes allow to visit at most one of the nodes in this group. If
you want to visit exactly one node in a Disjunction, use:
void AddDisjunction(const std::vector<NodeIndex>& nodes);
where nodes represents the group of nodes. This constraint is equivalent to:
X
ActiveVar(i) = 1.
i∈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);
where p is a boolean variable corresponding to the Disjunction and the objective function
has anPadded (p * penalty) term. If none of the variables in the Disjunction is vis-
ited ( i∈Disjunction ActiveVar(i) = 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.
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:
Solver* const solver = routing.solver();
solver->AddConstraint(solver->MakeEquality(
routing.VehicleVar(routing.NodeToIndex(i)),
routing.VehicleVar(routing.NodeToIndex(j))));
routing.AddPickupAndDelivery(i, j);
317
9.5. The model behind the scene: overview
Because we don’t completely define the model when we construct the RoutingModel class,
most of the (implicit or explicit) constraints27 and the objective function are added in a special
CloseModel() method. This method is automatically called before a call to Solve() but
if you want to inspect the model before, you need to call this method explicitly. This method is
also automatically called when you deal with Assignments. In particular, it is called by
• ReadAssignment();
• RestoreAssignment() and
• ReadAssignmentFromRoutes().
The objective function is defined by an IntVar. To get access to it, call CostVar():
IntVar* const obj = routing.CostVar();
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 fixed cost of each route/vehicle;
• the penalty costs for not visiting optional Disjunctions.
We detail each of these costs.
To set the cost of each arc, use a NodeEvaluator2 callback to return the cost of each (i,j)
arc:
void SetCost(NodeEvaluator2* evaluator);
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.
318
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
Routes/Vehicles don’t all have to be used. It might cost less not to use a route/vehicle. To add
a fixed cost for each route/vehicle, use:
void SetRouteFixedCost(int64 cost);
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.
28
What follows is clearly C++ jargon. Basically, let’s 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”.
319
9.5. The model behind the scene: overview
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 doesn’t
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.
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();
3 1 3 3
5
4 4
4 5 5
(a) (b)
On the left (figure (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 (figure (b)), we have a bipartite
320
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
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 finding 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 figure (b), such an optimal solution is depicted in thick blue
dashed lines. As is the case here, this solution doesn’t 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.
9.5.5 Miscellaneous
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 filtering.
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 flag:
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 find what appears to be good sub-solutions, i.e. partial routes that
seem promising and that you want to keep fixed for a while during the search. This can easily
be achieved by using locks.
29
This bipartite graph is not really the one used by the CP solver but it’s close enough to get the idea.
321
9.6. The TSP in or-tools
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 ?? when we will try to solve the
Cumulative Chinese Postman Problem.
You can find the code in the files tsp.h, tsp_epix.h, tsp_minimal.cc, tsp.cc,
tsplib_solution_to_epix.cc and tsp_forbidden_arcs.cc and the data in the
files tsp_parameters.txt, a280.tsp and a280.opt.tour.
Only a few lines of codes are needed to solve the TSP with the help of the RL:
#include <iostream>
#include "constraint_solver/routing.h"
using operations_research;
// Cost function
int64 MyCost(RoutingModel::NodeIndex from, RoutingModel::NodeIndex to) {
...
return ...;
}
// 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) << " ";
322
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
}
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));
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.
This time we use the TSPData (see 9.3.3) and TSPEpixData (see 9.3.4) classes to
read TSP instances and write TSP solutions in TSPLIB format. We use also several
parameters to guide the search.
Headers
#include "base/commandlineflags.h"
#include "constraint_solver/routing.h"
#include "base/join.h"
#include "tsp.h"
#include "tsp_epix.h"
base/join.h contains the StrCat() function that we use to concatenate strings. tsp.h
contains the definition and declaration of the TSPData class to read TSPLIB format instances
and write TSPLIB format solution files 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 file. 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).
323
9.6. The TSP in or-tools
Parameters
Several command line parameters are defined in the files tsp.h, tsplib.h, tsp_epix.h
and tsp.cc:
When parameters start to pile up, writing them every time on the command line isn’t very
practical. The gflags library provides the possibility to load the parameters from a text file.
For instance, a parameters file tsp_parameters.txt for our TSPData class might look
like this:
--tsp_depot=2
--deterministic_random_seed=true
--use_symmetric_distances=true
--min_distance=23
--max_distance=748
--tsp_initial_heuristic=PathCheapestArc
--tsp_size=101
--tsp_solution_file=tsp_sol.txt
30
This flag is redundant with the routing_time_limit flag provided in routing.cc but we wanted to
underline the fact that this limit is given in milliseconds.
324
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
google::SetUsageMessage(usage);
google::ParseCommandLineFlags(&argc, &argv, true);
operations_research::TSPData tsp_data;
if (FLAGS_tsp_size > 0) {
tsp_data.RandomInitialize(FLAGS_tsp_size);
} else if (FLAGS_tsp_data_file != "") {
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 doesn’t 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().
325
9.6. The TSP in or-tools
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 num-
ber of vehicle (1) as parameters. The distance function is encoded in the TSPData object
given to the TSP() function.
Next, we define 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 define 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);
326
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
string route;
const int route_nbr = 0;
for (int64 node = routing.Start(route_nbr);
!routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {
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.
The classical way to deal with forbidden arcs between two cities when an algorithm expects
a complete graph as input is to assign a large value M to these arcs. Arcs with such a large
distance will never be chosen31 . M can be considered as infinity.
In Constraint Programming, we can deal with forbidden arcs more elegantly: we simply remove
the forbidden values from the variable domains. We’ll use both techniques and compare them.
First, we have to define M . We suppose that M >>> max(d(x, y) : x, y ∈ cities)32 and we
take the largest allowed value kint64max.
We have implemented a RandomForbidArcs() method in the TSPData class to randomly
forbid a percentage of arcs:
void RandomForbidArcs(const int percentage_forbidden_arcs);
This method alters the existing distance matrix and replaces the distance of forbidden arcs by
the flag M:
DEFINE_int64(M, kint64max, "Big m value to represent infinity");
We have also defined a flag to switch between the two techniques and a flag for the percentage
of arcs to forbid randomly in the file tsp_forbidden_arcs.cc:
31
Actually, when permitted, an arc (i, j) with a distance M is often replaced by a shortest path i → j and its
value is the length of the shortest path between i and j. One drawback is that you have to keep in memory the
shortest paths used (or recompute them) but it is often more efficient than using the large M value.
32
Loosely speaking, the expression M >>> max(d(x, y) : x, y ∈ cities) means that M is much much larger
that the largest distance between two cities.
327
9.6. The TSP in or-tools
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!";
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
gflag:
DEFINE_int32(percentage_forbidden_arcs_max, 94,
"Maximum percentage of arcs to forbid");
328
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
329
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)
TwoOpt
Relocate
OrOpt
Exchange
Cross
Inactive
SwapActive
ExtendedSwapActive
PathLNS
UnActiveLNS
9.7.4 Filters
You can find the code in the file tsp.h, tsp_epix.h, tsp_minimal.cc, tsp.cc,
tsplib_solution_to_epix.cc and tsp_forbidden_arcs.cc and the data in the
files tsp_parameters.txt, a280.tsp and a280.opt.tour.
The Travelling Salesman Problem with Time Windows is similar to the TSP except that
cities (or clients) must be visited within a given time window. This added time constraint -
330
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
although it restricts the search tree33 - renders the problem even more difficult 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 difficult [Savelsbergh1985].
We present the TSPTW and two instances formats: the López-Ibáñez-Blum and the da Silva-
Urrutia 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.
You might be surprised to learn that there is no common definition that is widely accepted
within the scientific community. The basic idea is to find a “tour” that visits each node within
a time window but several variants exist.
We will use the definition given in Rodrigo Ferreira da Silva and Sebastián Urrutia’s 2010
article [Ferreira2010]. Instead of visiting cities as in the TSP, we visit and service customers.
The Travelling Salesman Problem with Time Windows (TSPTW) consists in finding a mini-
mum cost tour starting and ending at a given depot and visiting all customers. Each customer i
has:
• a service time βi : this is the time needed to service the customer;
• a ready time ai (sometimes called release time): you cannot start to serve the customer
before her ready time and
• a due time bi (sometimes called deadline): you must serve the client before her due time.
You only can (and must) visit each client once. The costs on the arcs represent the travel times
(and sometimes also the service times). The total cost of a tour is the sum of the costs on the
arcs used in the tour. The ready and due times of a client i define a time window [ai , bi ] within
which the client has to be served. You are allowed to visit the client before the ready time but
you’ll have to wait until the ready time before you can service her. Due times must be respected
and tours that fail to serve clients before their due time are considered infeasible.
Let’s illustrate a visit to a client i. To do so, let’s define:
• the arrival time ti : the time you arrive at the client and
• the service start time si : the time you start to service the client.
time spent at the client
service time βi
In real application, the time spent at a client might be limited to the service. For instance, you
might wait in front of the client’s office. It’s common to consider that you start to service and
leave as soon as possible and this is our assumption in this chapter
33
All TSP solutions are not TSPTW solutions!
34
Notice how the depot is important for the TSPTW while it is not for the TSP.
331
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)
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 ai , bi ∈ 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 finite.
The practical difficulty 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 difficulty 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 scientific literature about exact or
heuristic algorithms using CP to solve the TSPTW. Actually, not so many attempts have been
successful in solving this difficult problem in general. The scientific 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.
There isn’t a real standard. Basically, you’ll find two types of formats and their variants. We
refer you to two web pages because their respective authors took great care in formatting all
the instances uniformly.
Manuel López-Ibáñez and Christian Blum have collected benchmark instances from different
sources in the literature. Their Benchmark Instances for the TSPTW page contains about 300
instances.
Rodrigo Ferreira da Silva and Sebastián Urrutia also collected benchmark from different
sources in the literature. Their The TSPTW - Approaches & Additional Resources page con-
tains about 100 instances.
Both pages provide best solutions and sum up the relevant literature.
We present the same instance proposed by Dumas et al. [Dumas1995] in both formats.
Here is the content of the file n20w20.001.txt (LIB_n20w20.001.txt in our directory
/tutorials/cplusplus/chap9/):
35
This condition doesn’t hold in Rodrigo Ferreira da Silva and Sebastián Urrutia’s definition of a TSPTW. In
their article, they ask for (at least theoretically) ai , bi , βi ∈ R+ , i.e. non negative real numbers and ai 6 bi .
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...
332
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 first 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 i and j, plus the service time at node i. The
distance matrix is not necessarily symmetrical. The last 21 lines represent the time windows
(earliest, latest) for each node, one per line. The first node is the depot.
When then sum of service times is not 0, it is specified in a comment on the last line:
333
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)
We present exactly the same instance as above. Here is the file n20w20.001.txt
(DSU_n20w20.001.txt in our directory /tutorials/cplusplus/chap9/):
!! n20w20.001 16.75 391
CUST NO. XCOORD. YCOORD. DEMAND [READY TIME] [DUE DATE] [SERVICE TIME]
Having seen the same instance, you don’t need much complementary info to understand this
format. The first line of data (CUST NO. 1) represents the depot and the last line marks the
end of the file. As you can see, the authors are not really optimistic about solving instances
with more than 999 nodes! We don’t 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 first 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 don’t try to match the data between the two formats, so you
might encounter different solutions.
Solutions
334
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 verifies if a given solution is indeed feasible for a
given instance in López-Ibáñez-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;
}
335
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)
As you can see, the recorded objective value in the solution file 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 López-Ibáñez-Blum format, you get:
TSPTW instance of type López-Ibáñez-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.
336
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 first
node given in the instance and solutions files.
• int64 ReadyTime(RoutingModel::NodeIndex i) const: returns the
ready time of node i;
• int64 DueTime(RoutingModel::NodeIndex i) const: returns the due
time of node i
• int64 ServiceTime(RoutingModel::NodeIndex i) const: returns the
service time of node i.
The ServiceTime() method only makes sense when an instance is given in the da Silva-
Urrutia format. In the López-Ibáñez-Blum format, the service times are added to the arc costs
in the “distance” matrix and the ServiceTime() method returns 0.
To model the time windows in the RT, we use Dimensions, i.e. quantities that are accumu-
lated along the routes at each node. At a given node to, the accumulated time is the travel cost
of the arc (from, to) plus the time to service the node to. The TSPTWData class has a
special method to return this quantity:
int64 CumulTime(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const {
return Distance(from, to) + ServiceTime(from);
}
method. This way, you can load solution files and test them with the bool
IsFeasibleSolution() method briefly 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
file. 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.
337
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)
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: computes the objective
value and returns it;
• int64 SolutionLoadedObjective() const: returns the objective value
stored in the instance file
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 doesn’t generate random instances. We wrote a little program for this
purpose.
338
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
files:
• 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.
To visualize the solutions, we rely again on the excellent ePiX library. The file
tsptw_epix.h contains the TSPTWEpixData class. This class is similar to the
TSPEpixData class. Its unique constructor reads:
RoutingModel routing(...);
...
TSPTWData data(...);
...
TSPTWEpixData(const RoutingModel& routing,
const TSPTWData& data);
The first method takes an Assignment while the second method reads the solution from a
solution file.
You can define 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 file is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp
339
9.9. The TSPTW in or-tools
The dot in red in the center represents the depot or first 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 flags:
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 file tsptw_solution_to_epix.cc. To use it, invoke:
./tsptw_solution_to_epix TSPTW_instance_file TSPTW_solution_file >
epix_file.xp
You can find the code in the file tsp.h, tsp_epix.h, tsp_minimal.cc, tsp.cc,
tsplib_solution_to_epix.cc and tsp_forbidden_arcs.cc and the data in the
files tsp_parameters.txt, a280.tsp and a280.opt.tour.
In this section, we try to solve the TSPTW Problem. First, we use Dimensions to
model the time windows and the default routing strategy. Then, we use a basic heuristic to
create a starting solution for the Local Search.
340
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:
341
9.9. The TSPTW in or-tools
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");
Let’s test this TSPTW solver on the following generated instance in da Silva-Urrutia format
(file DSU_test.tsptw):
!! test
CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME
We invoke:
./tsptw -instance_file=DSU_test.tsptw -solution_file=test.sol
and we obtain:
1 5 3 2 4
252
travel to 4 56 67 9 58 58
serve 4 56 67 9 9 67
travel to 2 147 165 9 86 153
serve 2 147 165 9 9 162
travel to 1 197 216 2 40 202
serve 1 197 216 2 2 204
travel to 3 242 254 3 44 248
342
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
The reason is that the services times are added to the distances in this format.
check_tsptw_solution confirms this:
travel to 4 56 67 0 67 67
serve 4 56 67 0 0 67
travel to 2 147 165 0 95 162
serve 2 147 165 0 0 162
travel to 1 197 216 0 42 204
serve 1 197 216 0 0 204
travel to 3 242 254 0 47 251
serve 3 242 254 0 0 251
travel to 0 0 504 0 26 277
serve 0 0 504 0 0 277
Solution is feasible!
Obj value = 277
Real instances, like DSU_n20w20.001.txt, are out of reach for our basic tsptw. This is
mainly because finding a first feasible solution is in itself a difficult problem. In the next sub-
section, we’ll help the solver finding this first feasible solution to start the local search.
[TO BE WRITTEN]
343
9.10. Summary
9.10 Summary
summary
344
CHAPTER
TEN
The Vehicle Routing Problem (VRP) is a Routing Problem where you seek to service a number
of customers with a fleet of (homogeneous or heterogeneous) vehicles starting from one depot.
The basic idea is to service clients - represented by nodes in a graph - by the vehicles. Lots
of theoric and industrial problems can be modelled as a VRP. The problem’s origins can be
traced back to the fifties (see [Dantzig1959] for instance). It includes the TSP (a VRP with one
vehicle) as a special case, and is, as such, a computationally complex problem.
We again use the excellent C++ ePiX library1 to visualize VRP and CVRP solutions in TSPLIB
format.
Overview:
We first introduce the VRP and the TSPLIB instances for the Capacitated Routing Vehicle
Problem. The TPSLIB instance format is the de facto format to represent CVRP instances in
the scientific community. We then present a basic program to solve the bare VRP. To do so, we
show how to interact directly with the underlying CP solver. Next, the CVRP is introduced and
explained. Capacities are modelled with Dimensions. Finally, we discuss the multi-depots
variant of the VRP in general and how to fix some parts of the routes while letting the CP solver
assign the other clients to vehicles, i.e. how to complete the partial solution.
Prerequisites:
Files:
346
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
In this section, we briefly present one of the basic versions of the Vehicle Routing Problem.
Most of the time, there are additional constraints2 . This basic version is very little studied in
the scientific literature and in this manual we use it as a gentle introduction to the CVRP.
The usual format to encode CVRP instances is from the TSPLIB. There is no TSPLIB for-
mat for the basic VRP, so we simply read CVRP and forget about the demands of the CVRP
instances to solve a basic version of the VRP.
We can reuse our TSPLIBReader class as it also manages to read CVRP instances. We
use again the excellent ePiX library through our CVRPEpixData class to visualize CVRP
instances and their solutions.
We didn’t program specialized VRP classes. Instead we’ll use and present more general CVRP
classes.
Given a graph G = (V, E) and pairwise distances between nodes, the VRP consists in finding
one or several routes to visit each node once. Each tour starts and ends at the same depot. The
cost of a tour is given by the sum of the distances along the route and the total cost of a feasible
solution for the VRP is the sum of the costs of all tours.
This “definition” remains vague enough to encompass the most known variants of the VRP.
Indeed, not only does the VRP exist in different flavors (capacitated, multi-depots, with time-
windows, with pick-up and delivery, ...) but several slightly different definitions exist in the
literature. In particular, some instances fix - in a way or another - the number of vehicles to be
used and a total travelled distance limit is imposed on the vehicles.
In this manual, we will use the definition given by Gilbert Laporte in [Laporte1992]. In this
article, a VRP is designed in such a way that
• there is only one depot and each vehicle route starts and ends at this depot;
• each node - except the depot - has to be visited (or serviced) exactly once by exactly one
vehicle;
• the fleet of vehicles is homogeneous, i.e. the cost to travel an arc is the same for all the
vehicles;
• the number of vehicles can be fixed, bounded or free;
• the distances between two nodes don’t need to be equal;
• the objective function is the sum of the arcs traversed by all vehicles that service at least
one node with no additional cost for each used vehicle.
• some side constraints are satisfied.
The last point is important. Indeed, without side constraints and if the graph obeys the triangle
inequality (i.e. d(x, z) 6 d(x, y) + d(y, z)), then there is no need to use more than one vehicle.
2
This basic version of the VRP is better known under the name mTSP or the Multiple TSP. We refer the reader
to [Bektas2006] for more.
347
10.1. The Vehicle Routing Problem (VRP)
For instance, the solution with two vehicles in the next figure (left) costs more than the same
solution where only one vehicle follows the two routes one after the other (right):
z
x
y
As d(x, z) 6 d(x, y) + d(y, z), the shortcut to go immediately from x to z without passing by
the depot y costs less.
The most common side constraints include:
• capacity restrictions: a non-negative weight (or demand) di is attached to each city i
(except the depot) and the sum of weights of any vehicle route may not exceed the vehicle
capacity. Capacity-constrained VRP are referred to as CVRP and will be studied in this
chapter.
• maximum number of cities that can be visited: the number of cities on any route is
bounded above by q. This is a special case of CVRP with di = 1 for all i except the
depot and capacity equal to q for all vehicles.
• total time (or distance) restrictions: each route has its length bounded by a certain amount
T of time that cannot be exceeded by each vehicle. Such VRP are often denoted as DVRP
or distance-constrained VRP.
• time-windows: each city must be serviced within a time-window [ai , bi ] and waiting times
are allowed.
• precedence relations between pair of cities: city j cannot be visited before city i. Among
such problems are the VRPPD: the Vehicle Routing Problems with Pickup and Delivery.
A number of goods need to be moved from certain pickup locations to other delivery
locations.
And the list goes on.
For our basic version of the VRP, all vehicles must be used. This version of the VRP is better
known as the mTSP3 . Some problems can be coined as mTSP and we refer again the reader to
[Bektas2006] to find some examples.
Below you’ll find a picture of a solution of a VRP with 32 cities and 5 vehicles (A-n32-k5)
in the sub-section Visualization with ePix.
Several known benchmark data sources are available on the internet. We refer you to three:
• The VRPLIB page: http://www.or.deis.unibo.it/research_pages/ORinstances/VRPLIB/VRPLIB.html
and
3
Not to be confused with the k-TSP where only k nodes/cities must be visited/serviced.
348
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
The VRP in the TSPLIB format are only CVRP, i.e. capacitated problems. We will ignore the
demands on the nodes to solve our basic VRP. Don’t forget the TSPLIB convention to number
the nodes starting at 1.
Nodes are numbered from 1 to n in the TSPLIB and we keep this convention in this
chapter.
The type is CVRP and the capacity of the vehicles is specified after the keyword CAPACITY.
The demands on the node are specified in a DEMAND_SECTION section. The TSPLIB format
requires the depot to be listed in the the DEMAND_SECTION section and have a demand of 0.
Note that there is no specification whatsoever on the number of vehicles.
If you use other instances, be careful that they fully comply to the TSPLIB format (or change
the code).
349
10.1. The Vehicle Routing Problem (VRP)
While there exists a TSPLIB format for the solutions of (C)VRP, it is seldom used. We’ll follow
the trend and use the most commonly adopted format.
This is what the file opt-A-n32-k5 containing an optimal solution for the CVRP
A-n32-k5 instance above looks like:
Route #1: 21 31 19 17 13 7 26
Route #2: 12 1 16 30
Route #3: 27 24
Route #4: 29 18 8 9 22 15 10 25 5 20
Route #5: 14 28 11 4 23 3 2 6
cost 784
Routes are numbered starting form 1 while the nodes in the solution file are numbered starting
from... 0! Also, note that the depot is not listed.
TSPLIBReader tsp_data_reader(instance_file);
A little program crvp_data_generator generates random instances of CVRP. You can invoke
it as follows:
350
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
To represent a (C)VRP solution, we have defined the CVRPSolution class. Two constructors
are available:
CVRPSolution(const CVRPData & data, std::string filename);
CVRPSolution(const CVRPData & data,
const RoutingModel * routing,
const Assignment * sol);
351
10.1. The Vehicle Routing Problem (VRP)
is written as follows:
int64 CVRPSolution::ComputeObjectiveValue() const {
int64 obj = 0;
RoutingModel::NodeIndex from_node, to_node;
return obj;
}
Because this method is constant and doesn’t change the solution, it uses constant iterators. The
CVRPSolution class also provides the following non constant iterators:
• vehicle_iterator and
• node_iterator.
Because there is no TPSLIB format to encode VRP, we don’t provide a VRPData class. In-
stead, we use the more general CVRPData class and disregard the demands. We provide two
ways to create a CVRPData object: you can read a TSPLIB file or randomly generate an
instance.
As usual, you need to give a TSPLIBReader to the CVRPData constructor:
CVRPData cvrp_data(tsp_data_reader);
Basically, the CVRPData class contains the distance matrix, the nodes coordinates (if any) and
the clients demands.
You can visualize a (C)VRP instance and/or a solution with the CVRPEpixData class. First,
link it to a CVRPData object:
352
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
CVRPData cvrp_data(...);
CVRPEpixData epix_data(cvrp_data);
For your (and our!) convenience, we have written the small program cvrp_solution_to_epix
to visualize a CVRP solution. To create a pdf image of the opt-A-n32-k5 solution, invoke
it as follows:
./cvrp_solution_to_epix -instance_file=A-n32-k5.vrp
-solution_file=opt-A-n32-k5 > opt-A-n32-k5.xp
The same flags as for the program tsp_solution_to_epix can be applied. See sub-section 9.3.4.
353
10.2. The VRP in or-tools
One way to force all vehicles to service at least one city is to forbid each vehicle to return
immediately to its ending depot. In this fashion, the vehicle will have to service at least one
city. To acheive this, we simply remove the end depots from the domain of the NextVar()
variables corresponding to begin depots.
To obtain the NextVar() corresponding to begin depots is easy:
IntVar* const start_var =
routing.NextVar(routing.Start(vehicle_nbr));
To obtain the int64 indices corresponding to end depots is not more complicated. We detail
the auxiliary graph in the sub-section The auxiliary graph. The internal numbering of its nodes
is done such that all end depots are numbered last, having int64 indices from Size() to
Size() + vehicles() non-inclusive where Size() returns the number of NextVar()
variables and vehicles() the number of vehicles.
To remove the int64 indices, we need to have a complete model with all variables defined.
To complete the model, we must invoke the CloseModel() method:
RoutingModel routing(...);
...
routing.CloseModel();
354
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
return 0;
}
if (FLAGS_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_time_limit_in_ms);
}
// Setting depot
CHECK_GT(FLAGS_depot, 0) << " Because we use the"
<< " TSPLIB convention, the depot id must be > 0";
RoutingModel::NodeIndex depot(FLAGS_depot -1);
routing.SetDepot(depot);
routing.CloseModel();
...
We must invoke the CloseModel() to finalize the model for our instance. We need the
complete model to be able to interact with it.
We continue the inspection of the VRP_solver() function:
void VRPSolver (const CVRPData & data) {
...
// Forbidding empty routes
// See above.
// SOLVE
const Assignment* solution = routing.Solve();
355
10.2. The VRP in or-tools
...
You could inspect the solution as usual, only taking into account that there are more than one
vehicle:
if (solution != NULL) {
// Solution cost.
LG << "Obj value: " << solution->ObjectiveValue();
// Inspect solution.
std::string route;
for (int vehicle_nbr = 0; vehicle_nbr < FLAGS_number_vehicles;
++vehicle_nbr) {
route = "";
for (int64 node = routing.Start(vehicle_nbr);
!routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {
route = StrCat(route,
StrCat(routing.IndexToNode(node).value() + 1 , " -> "));
}
route = StrCat(route,
routing.IndexToNode(routing.End(vehicle_nbr)).value() + 1 );
LG << "Route #" << vehicle_nbr + 1 << std::endl
<< route << std::endl;
}
} else {
LG << "No solution found.";
}
Let’s see if our trick works. We invoke the vrp program with the flag number_vehicles
equal to 2, 3, 4 and 5. For instance:
356
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
There are indeed two routes. Don’t forget that this solution doesn’t con-
tain the depot and that the nodes are numbered from 0 (Remember the
numbering_solution_nodes_from_zero flag?).
Here are different outputs obtained with the ePiX library, with number_vehicles equal to
2, 3, 4 and 5:
357
10.3. The Capacitated Vehicle Routing Problem (CVRP)
Without a specialized search strategy, we obtain solutions that are not very interesting. When
we’ll solve real CVRPs, we’ll devise specialized search strategies.
You can find the code in the files cvrp_data.h and check_cvrp_solution.cc and
the data in the files A-n32-k5.vrp and opt-A-n32-k5.
We don’t have much to say about the CVRP as much has been said in the section The
Vehicle Routing Problem (VRP). The side constraint we add here to the VRP is a capacity
constraint: each vehicle must respect its capacity on the route it traverses, i.e. the sum of the
node capacities of all the nodes serviced by one vehicle must be smaller than the vehicule
capacity - for all vehicles.
The Capacitated Vehicle Routing Problem (CVRP) is one class of Vehicle Routing Problems
where the vehicles have a certain capacity they can not exceed. A client can only be serviced
by one vehicle. The depot is the same for all vehicles and doesn’t need to be serviced.
In this version, all vehicles have the same capacity C that cannot be exceeded on a tour and
clients (nodes) have demands. A client i has a demand di that has to be met when a vehicle
services this client. Think of demands as quantities of goods that the vehicle picks up when
visiting clients: the total among of goods/demands picked up along the route cannot exceed its
capacity.
The program check_cvrp_solution allows you to check if indeed the capacities of the vehicles
are respected along the routes. For instance, invoking:
./check_cvrp_solution -log_level=1 -instance_file=A-n32-k5.vrp
-solution_file=opt-A-n32-k5
prints:
Route 1 with capacity 100
Servicing node 22 with demand 12 (capacity left: 88)
Servicing node 32 with demand 0 (capacity left: 88)
358
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
As you can see, each vehicle has its capacity respected. In case you wonder, we use the real
node identifiers (from the original graph) in this output.
In the next section, we show you how to customize each vehicle (its usage cost, its capacity, its
edge traversal cost).
Exact methods can solve instances with 100 clients (see [Roberti2012]) but even heuristics
are limited to solve instances with more or less 1200 clients (see [Groër2011]). Most effi-
cient heuristics are a combination of integer programing with local search (see [Toth2008] or
[Groër2011]).
359
10.4. The CVRP in or-tools
To play with instances and see how solutions can change when you change some parts of the
instances, the CVRPData class provides some setters:
void SetDepot(RoutingModel::NodeIndex d);
void SetDemand(const RoutingModel::NodeIndex i, int64 demand);
void SetCapacity(int64 capacity);
You even can change the distance between two nodes i and j:
CVRPData cvrp_data(...);
...
RoutingModel::NodeIndex i = ...;
RoutingModel::NodeIndex j = ...;
int64 new_distance = ...;
cvrp_data.SetDistance(i, j) = new_distance;
Note that the call to SetDistance() is different because it returns an lvalue (left value)4 .
You also have the corresponding getters:
RoutingModel::NodeIndex Depot() const;
int64 Demand(const RoutingModel::NodeIndex i) const;
int64 TotalDemand() const;
int64 Capacity() const;
int64 Distance(RoutingModel::NodeIndex i,
RoutingModel::NodeIndex j) const;
The TotalDemand() method returns the sum of all demands for all clients.
To solve a CVRP with or-tools, we’ll use our homemade CVRP classes (CVRPData,
CVRPSolution and CVRPEpixData). The main difficulty that remains is how to model
the demands in or-tools. Simple: with Dimensions. We will also detail how to provide an
initial solution, how to tweak the search strategy and finally how to deal with an heterogenous
fleet of vehicles.
360
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
The accumulation of demands along the routes makes the Dimension variables perfect candi-
dates to model demands. We suggest you to read the sub-section Time windows as a Dimension
to refresh your memory if needed. The situation is a little easier here as the demands only de-
pend on the client and not the arcs that the vehicles traverse to reach these clients.
As usual, the solving process is encapsulated in a void CVRPBasicSolver(const
CVRPData & data) function inside the operations_research namespace called
from the main function:
int main(int argc, char **argv) {
...
operations_research::TSPLIBReader tsplib_reader(instance_file);
operations_research::CVRPData cvrp_data(tsplib_reader);
operations_research::CVRPBasicSolver(cvrp_data);
}
CHECK_GT(FLAGS_number_vehicles, 1)
<< "We need at least two vehicles!";
// Little check to see if we have enough vehicles
CHECK_GT(capacity, data.TotalDemand()/FLAGS_number_vehicles)
<< "No enough vehicles to cover all the demands";
...
This quick check is handy: no need to find a feasible solution when none exists. The distances
and the depot are passed to the solver in the usual way:
void CVRPBasicSolver (const CVRPData & data) {
...
RoutingModel routing(size, FLAGS_number_vehicles);
routing.SetCost(NewPermanentCallback(&data, &CVRPData::Distance));
if (FLAGS_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_time_limit_in_ms);
}
// Setting depot
CHECK_GT(FLAGS_depot, 0) << " Because we use the"
<< " TSPLIB convention, the depot id must be > 0";
RoutingModel::NodeIndex depot(FLAGS_depot -1);
routing.SetDepot(depot);
...
To add the client demands and the capacity constraints, we can use the
AddVectorDimension() method. To use this method, we need a demands array
with the int64 demands such that demands[i] corresponds to the demand of client i.
void CVRPBasicSolver (const CVRPData & data) {
...
361
10.4. The CVRP in or-tools
std::vector<int64> demands(size);
for (RoutingModel::NodeIndex i(RoutingModel::kFirstNode);
i < size; ++i) {
demands[i.value()] = data.Demand(i);
}
...
Because the C++ language guarantees that the values in an std::vector are contiguous, we
can pass the address of its first element:
void CVRPBasicSolver (const CVRPData & data) {
...
routing.AddVectorDimension(&demands[0], capacity, true, "Demand");
...
The bool argument indicates if the demand of the depot is set to demands[0] (when
false) or to 0 (when true) . As this demand is 0 for CVRP, this argument doesn’t really
matter and is, hence, set to true.
Now, come the solving process and the inspection if any solution is found:
void CVRPBasicSolver (const CVRPData & data) {
...
const Assignment* solution = routing.Solve();
if (solution != NULL) {
CVRPSolution cvrp_sol(data, &routing, solution);
cvrp_sol.SetName(StrCat("Solution for instance ", data.Name(),
" computed by vrp.cc"));
// test solution
if (!cvrp_sol.IsFeasibleSolution()) {
LOG(ERROR) << "Solution is NOT feasible!";
} else {
LG << "Solution is feasible and has an obj value of "
<< cvrp_sol.ComputeObjectiveValue();
// SAVE SOLUTION IN CVRP FORMAT
if (FLAGS_solution_file != "") {
cvrp_sol.Write(FLAGS_solution_file);
} else {
cvrp_sol.Print(std::cout);
}
}
} else {
LG << "No solution found.";
}
}
362
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
It is quite far from the optimal solution opt-A-n32-k5 with an objective value of 784. Using
GreedyDescent is not very clever but first, before we change the search strategy, let’s give
a hand to the solver and allow for the introduction of a known initial solution to start the local
search.
The routes are lists of nodes traversed by the vehicles. The indices of the outer
std::vector in routes correspond to the vehicles identifiers, the inner std::vector
contains the nodes on the routes for the given vehicles. The inner std::vectors must not
contain the start and end nodes, as these are determined by the RoutingModel class itself.
This is exactly what the Routes() method of the CVRPSolution returns.
With ignore_inactive_nodes set to false, this method will fail in case some of the
nodes in the routes are deactivated; when set to true, these nodes will be skipped.
If close_routes is set to true, the routes are closed; otherwise they are kept open.
The RoutesToAssignment method sets the NextVar() variables of the Assigment to
the corresponding values contained in the std::vector<...> routes. You don’t need
to add manually these variables in the Assignment: if they are missing, the method adds
them automatically. The method returns true if the routes are successfully loaded. However,
363
10.4. The CVRP in or-tools
such assignment might still not be a valid solution to the routing problem. This is due to more
complex constraints that are not tested. To verify that the solution is indeed feasible for your
model, call the CP solver CheckSolution() method.
One last thing, you cannot call the RoutesToAssignment() if the routing model is not
closed beforehand.
Time for some code:
void CVRPBasicSolver (const CVRPData & data) {
...
routing.CloseModel();
if (routing.solver()->CheckAssignment(initial_sol)) {
CVRPSolution temp_sol(data, &routing, initial_sol);
LG << "Initial solution provided is feasible with obj = "
<< temp_sol.ComputeObjectiveValue();
} else {
LG << "Initial solution provided is NOT feasible... exit!";
return;
}
}
A few comments are in order here. If an initial file is provided, we create the initial_sol
Assignment with the solver’s MakeAssignment() method. Remember that this cre-
ates an hollow shell to contain some variables that you have to add yourself. We don’t need
to do this here as the RoutesToAssignment() method will do it for you but only for
the NextVar() variables. We check the feasibility of the initial solution by calling the
CheckAssignment() method of the CP solver. The CheckAssignment() method cre-
ates a new Search and propagates the initial constraints of the model with the given solution.
It returns true if the solver didn’t fail which means that the given solution is feasible.
We previously have seen that to compute the objective value of a solution, you somehow
need to give this solution to the solver and let it solve the model. One way is to use a
SolutionCollector, another is to use DecisionBuilders: a StoreAssignment
and a RestoreAssignment with an Assignment to which you have attached the objec-
tive variable. However, this is not needed here since the CVRPSolution class computes an
objective value from an Assignment with assigned NextVar() variables. This is precisely
the role of the temp_sol object.
364
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
Finally, the Solve() method takes into account this initial solution. Only the main
NextVar() variables are needed. This initial solution is reconstructed and tested by the
CP Routing solver. If initial_sol is NULL then the solving process is started from scratch
and the CP Routing solver tries to find an initial solution for the local search procedure.
We will see more in details the different methods provided by the RoutingModel class to
switch from routes to Assignment and vice-versa in the section Assignments and partial
Assignments.
Until now, we considered an homogeneous fleet of vehicles: all vehicles are exactly the same.
What happens if you have (very) different types of vehicles? The RL allows you to customize
each class of vehicles.
A different cost might be assigned to each type of vehicles. This can be done by the
SetVehicleFixedCost() method:
void SetVehicleFixedCost(int vehicle, int64 cost);
The cost of using a certain type of vehicles can be higher or lower than others. If a vehicle is
used, i.e. this vehicle serves at least one node, this cost is added to the objective function.
Different types of vehicles have different capacities? No problem. This is allowed in the RL:
void AddDimensionWithVehicleCapacity(NodeEvaluator2* evaluator,
int64 slack_max,
VehicleEvaluator* vehicle_capacity,
bool fix_start_cumul_to_zero,
const string& name);
365
10.5. Multi-depots and vehicles
Some instances have different depots. This is not a problem for the RL. You can have as many
depots as you want (within the limit of an int). These depots can be starting, ending or
starting and ending depots. Each problem in the RL is modelled with routes: each route starts
at a depot, finishes at a depot and is serviced by one vehicle. The auxiliary graph used internally
is constructed in such a way that every vehicle has its own starting and ending depots.
Each route and vehicle are in a one to one correspondence in the RL. They are represented by
VehicleVar() variables and divided among several VehicleClasses.
366
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
depots[1] = std::make_pair(3,4);
depots[2] = std::make_pair(3,7);
depots[3] = std::make_pair(4,7);
The number of vehicles (4) and the length of the std::vector with the pairs of depots
(depots.length()) must be equal5 .
There are several restrictions on the depots and practical facts about the RL model that are
worth mentioning.
All vehicles are not necessarily used in a solution but if a vehicle is used it respects its starting
and ending depots. When a vehicle is not used, the NextVar() variable corresponding to the
starting depot of this vehicle points to the ending depot of this vehicle, i.e. if you have:
int vehicle = ...;
IntVar * start_var = routing.NextVar(routing.Start(vehicle)).
Assignment * solution = routing.Solve(...);
returns true6 .
The method IsVehicleUsed() of the RoutingModel class tests exactly this.
As mentioned earlier, a depot cannot be a transit node: you can only start, finish or start and
finish a tour at a depot.
In the RL, there is a one to one correspondence between vehicles and routes. You proba-
bly noticed that we interchangeably used the terms route and vehicle in this manual. When
you declare v vehicles/routes in your model, the RL solver creates a model with v vehi-
cles/routes numbered from 0 to vehicles() - 1. These vehicles/routes are divided in
different VehicleClasses (see next sub-section).
The VehicleVar(int64 i) method returns the IntVar* corresponding to the node with
int64 index i: this variable indicates which vehicle services node i, i.e. if node i is serviced
by vehicle vehicle_number in a solution (with the same abuse of notation as before):
5
If not, you trigger an assert().
6
Remember that there are no NextVar() variables for end depots.
367
10.5. Multi-depots and vehicles
VehicleVar(i) == vehicle_number.
You can grab all VehicleVar() variables at once with:
const std::vector<IntVar*>& VehicleVars() const;
Depots are always active and thus can not be part of a Disjunction.
10.5.4 VehicleClasses
368
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
Sometimes, while searching for a good solution, you find some promising partial routes
or you may already know that some routes or partial routes should be part of a solution.
Wouldn’t it be nifty to be able to fix some parts of the solution and let the CP routing solver
assign the rest of the solution? Dream no more: this possibility is integrated in the RL and we
detail it in this section.
Before we go on, let’s agree on the terminology. Our routing problems are modelled with a
graph G = (V, E ∪ A) with V the set of all vertices, E the set of edges and A the set of arcs.
Here are key terms that we will use throughout the rest of part III:
paths: We use path in the common graph theoretic sense. A path is a sequence of adjacent8
edges and/or arcs with the possibility to traverse these edges and/or arcs one after the
other9 . The first and last vertices belong to the path and are called respectively start end
end vertices.
cycles: A cycle is a path such that the start and end vertices are the same.
simple paths: A simple path is a path that doesn’t intersect itself.
simple cycles: A simple cycle is a simple path such that the start and end vertices coincide.
routes: A route is a simple path or simple cycle that connects a starting depot and an ending
depot and that is traversed by only one vehicle.
empty routes: An empty route is a pair of starting and ending depots that are assigned to the
same vehicle.
partial routes: A partial route is a simple path that is traversed by only one vehicle. The idea
is to name “parts” of contiguous edges/arcs that could be extended - in both incoming and
outgoing directions - to form a route. A route can be considered as a partial route only if
the starting and ending depots are not the same. This partial route cannot be extended at
both its end depot vertices but we still call it a partial route.
8
Two edges are adjacent if they share a common vertex.
9
We don’t distinguish between paths with only edges (paths), only arcs (directed paths) or containing edges
and arcs (mixed paths). In the same vein, we don’t distinguish between cycles with only edges (cycles), only arcs
(circuits) or containing edges and arcs (mixed cycles).
369
10.6. Partial routes and Assigments
370
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
371
10.6. Partial routes and Assigments
Let’s refresh our memory about the data instance before we look at the results.
The routes depots are:
• route 1: 2 and 5;
• route 2: 4 and 5;
• route 3: 4 and 8;
• route 4: 5 and 8.
The defined locks are:
• p[0]: 1 -> 3 -> 18 -> 27 -> 22;
• p[1]: 24 -> 19 -> 16 -> 14;
• p[2]:
The fact that we only applied locks for the 3 first routes while the model has 4 routes means
that the fourth route will not be used in the search.
If you set FLAGS_close_routes to true, you’ll get a partial solution that is not feasible
and the following expected result:
No solution found.
If you set FLAGS_close_routes to false, the partial solution made up by the locks is
completed by the CP routing solver:
Obj value: 804
Route #1
2 -> 1 -> 3 -> 18 -> 27 -> 22 -> 26 -> 5
Route #2
4 -> 24 -> 19 -> 16 -> 14 -> 17 -> 21 -> 25 -> 29 -> 5
Route #3
4 -> 6 -> 7 -> 9 -> 10 -> 11 -> 12 -> 13 -> 15 -> 20 -> 23 -> 28 -> 8
Route #4
5 -> 8
If you find the ApplyLocksToAllVehicles() method too restrictive for your needs, you
can always construct a partial Assignment and pass it to the CP routing solver as we will do
in the next sub-section.
372
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
The first method writes the current solution to a file and the second method loads the
Assignment contained in the file as the current solution. The format used is the pro-
tocol buffer from Google10 . These two methods are shortcuts. WriteAssignment()
takes the current solution and invokes its Save() method while ReadAssignment()
invokes the Load() method of an Assignment and restores this Assignment as the
current solution with the RestoreAssignment DecisionBuilder.
To test if everything went fine, use WriteAssignment() and
ReadAssignment(). The former returns true if the Assignment was suc-
cessfully saved and false otherwise. The latter returns NULL if it could not load the
Assignment contained in the file as the current solution.
If you already have an Assignment at hand, you can restore it as the current solution
with
Assignment* RestoreAssignment(const Assignment& solution);
373
10.6. Partial routes and Assigments
In the file vrp_IO.cc, we print the vector and for the instance above, we obtain:
Solution saved into an std::vector of size 4
Route #1 with starting depot 2 and ending depot 5
1 -> 3 -> 18 -> 27 -> 22 -> 26
As you can see, no depot is saved into this std::vector. This is exactly the kind of
std::vector that you need to pass to RoutesToAssignment():
Assignment* const restored_sol =
routing.ReadAssignmentFromRoutes(sol, false);
This method restores the solution contained in the vector as the current solution.
In contrast to the RoutesToAssignment() method, the solution passed to
ReadAssignmentFromRoutes() must be a complete solution, i.e. all NextVar()
mandatory variables must be assigned.
We also remind the reader that in contrast to all other loading methods presented here,
RoutesToAssignment() doesn’t reconstruct a feasible solution and deals only with
NextVar() variables. If your model has many complicated side constraints (like
Dimensions with slack variables), the CP routing solver might need some time to recon-
struct a feasible solution from the NextVar() variables.
374
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem
10.7 Summary
375
Part IV
Technicalities
CHAPTER
ELEVEN
UTILITIES
11.1 Logging
11.2 Asserting
11.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.
This basic timer is defined by the WallTimer class. This class proposes the usual methods:
• void Start()
• void Stop()
11.3. Timing
• bool Reset()
• void Restart()
• bool IsRunning() const
• int64 GetInMs() const
• double Get() const
GetInMs() returns the elapsed time in milliseconds while Get() returns this time in sec-
onds.
If you need even more precise timing, use the following method:
• static int64 GetTimeInMicroSeconds()
that returns the time in microseconds.
To measure the time, we query the system time and add or subtract the queried times.
System Function
Linux clock_gettime()
Windows QueryPerformanceCounter() and QueryPerformanceFrequency()
MacOS mach_absolute_time() and mach_timebase_info()
This timer is defined by the CycleTimer class. Actually, the CycleTimer class uses... the
WallTimer class internally. More precisely, the CycleTimer class is based on the static
int64 GetTimeInMicroSeconds() method of the WallTimer class.
Its methods are
• void Reset()
380
Chapter 11. Utilities
• void Start()
• void Stop()
• int64 GetInUsec() const
• int64 GetInMs() const
GetInUsec() returns the elapsed time in microseconds and GetInMs() converts this time
in milliseconds.
The Solver class comes with an integrated timer. By default, this timer is a WallTimer
(We use a typedef ClockTimer for a WallTimer).
This timer starts counting at the creation of the solver and is never reset.
11.4 Profiling
// This struct holds all parameters for the Solver object. // SolverParameters is only used by the
Solver constructor to define solving // parameters such as the trail compression or the profile
level. // Note this is for advanced users only. struct SolverParameters {
enum ProfileLevel { NO_PROFILING, NORMAL_PROFILING };
enum TraceLevel { NO_TRACE, NORMAL_TRACE }
// Support for profiling propagation. LIGHT supports only a reduced // version of the
summary. COMPLETE supports the full version of the // summary, as well as the csv
export. ProfileLevel profile_level;
// Support for full trace of propagation. TraceLevel trace_level;
DEFINE_bool(cp_trace_propagation, false, “Trace propagation events (constraint and de-
mon executions,” ” variable modifications).”);
DEFINE_bool(cp_trace_search, false, “Trace search events”); DE-
FINE_bool(cp_show_constraints, false,
“show all constraints added to the solver.”);
DEFINE_bool(cp_print_model, false, “use PrintModelVisitor on model before solving.”);
381
11.5. Debugging
11.5 Debugging
Naming variables
11.6 Serializing
11.7 Visualizing
11.8 Randomizing
382
CHAPTER
TWELVE
MODELING TRICKS
Overview:
Prerequisites:
Files:
384
Chapter 12. Modeling tricks
12.2 Efficiency
Solve()
SolveAndCommit
MakeNestedOptimize()
12.4.3 DecisionBuilders
385
SolveOnce
13.1. Main files and directories
CHAPTER
THIRTEEN
Methods
Factories
Caches
Callbacks
Visitors
Listeners
13.3.1 BaseObjects
13.3.2 PropagationBaseObjects
13.3.3 Callbacks
NewPermanentCallback()
388
13.4 The Trail struct
Chapter 13. Under the hood
389
13.9. Local Search (LS)
// Registers itself on the solver such that it gets notified of the search // and propa-
gation events. virtual void Install();
390
Chapter 13. Under the hood
(pu) stands for public and (pr) for private. The int64 Size() const method re-
turns nodes_ + vehicles_ - start_end_count_, which is exactly the minimal num-
ber of variables needed to model the problem at hand with one variable per node (see next
subsection). kUnassigned is used for unassigned indices.
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 figure:
391
13.11. The Routing Library (RL)
1 0 2 1
0
0
14
0
1 8
5 0
1
0
1
3 6 71
0
1
00
1
01
10
Starting depot
1Ending depot
0
0
1
1
0 Starting and ending depot
Transit node
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 finally 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:
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 don’t use any NextVar() variable for that node. The minimal number of
nexts_ variables is:
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.
392
Chapter 13. Under the hood
1 0 2 1
0
9
0
1
0 4
1 7
5 10 1
0
0
1
3 6 11 1
0
0
1
0
1
01
1 0
12
8
Starting depot
1
0
0Ending depot
1
01 Starting and ending depot
Transit node
If you set the FLAGS_log_level to 2 and skip the log prefix:
./rl_auxiliary_graph --log_level=2 --log_prefix=false
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
393
13.11. The Routing Library (RL)
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 doesn’t 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 0 2 1
04
0
1
0
1 8
5 0
1
0
1
6 71
0
1
00
1
3 01
10
Starting depot
0
1
0Ending depot
1
01Starting and ending depot
Transit node
We output the routes, first 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: 0
1 -> 0 -> 2 -> 4
1 -> 0 -> 2 -> 9
Route: 1
3 -> 5 -> 4
3 -> 5 -> 10
Route: 2
394
Chapter 13. Under the hood
3 -> 6 -> 7
8 -> 6 -> 11
Route: 3
4 -> 8 -> 7
4 -> 7 -> 12
Some remarks
13.11.3 Variables
Path variables
Dimension variables
13.11.4 Constraints
NoCycle constraint
13.12 Summary
395
Part V
Appendices
BIBLIOGRAPHY
[Hoffman1969] Hoffman, Loessi and Moore. Constructions for the Solution of the m Queens
Problem, Mathematics Magazine, p. 66-72, 1969.
[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 flowshop 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, Com-
puting, volume 69, nbr 1, pp 37-49, 2002.
[Taillard1993] Taillard, E., 1993. Benchmarks for basic scheduling problems, European Jour-
nal 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.
[Philippe2001] 2. Philippe, C. Le Pape, and W. Nuijten. Constraint-based scheduling: ap-
plying constraint programming to scheduling problems. Vol. 39. Springer, 2001.
[Christofides1976] Christofides, Nicos. Worst-case analysis of a new heuristic for the travel-
ling salesman problem, Technical Report, Carnegie Mellon University, 388, 1976.
[Luby1993] 13. Luby, A. Sinclair and D. Zuckerman, Optimal speedup of Las Vegas algo-
rithms, Information Processing Letters, Volume 47, Issue 4, 1993, pp 173-180.
[Gomes1998] C. P. Gomes, B. Selman, and H. Kautz, Boosting Combinatorial Search Through
Randomization, presented at National Conference on Artificial Intelligence (AAAI), Madi-
son, WI, 1998.
[Glover1997] 6. Glover and M. Laguna. Tabu Search, Kluwer Academic Publishers, 1997.
[Gendreau2005] M. Gendreau and J.-Y. Potvin. Tabu search. In E. K. Burke and G. Kendall,
editors, Search Methodologies: Introductory Tutorials in Optimization and Decision Sup-
port Techniques. Springer-Verlag, 2005
[Kirkpatrick1983] S. Kirkpatrick, C. D. Gelatt Jr., M. P. Vecchi. Optimization by Simulated
Annealing. Science. New Series, Vol. 220, No. 4598, 1983, pp. 671-680.
[Granville1994] 22. Granville, M. Krivanek, J.-P. Rasson. Simulated annealing: A proof of
convergence. IEEE Transactions on Pattern Analysis and Machine Intelligence 16 (6),
pp 652–656, 1994.
[Michiels2007] W. Michiels, E. Aarts and J. Korst, Chapter 8: Asymptotic Convergence of
Simulated Annealing in Theoretical Aspects of Local Search, Monographs in Theoretical
Computer Science, Springer, 2007
[Shaw1998] P. Shaw. Using constraint programming and local search methods to solve vehicle
routing problems, Fourth International Conference on Principles and Practice of Constraint
Programming, v 1520, Lecture Notes in Computer Science, pp 417–431, 1998.
[refalo2004] P. Refalo, Impact-Based Search Strategies for Constraint Programming in Prin-
ciples and Practice of Constraint Programming – CP 2004, Lecture Notes in Computer
Science, Springer 2004, pp 557-571.
400
Bibliography
401
INDEX
Symbols domain, 15
–cp_model_stats, 60
–cp_no_solve, 59
E
–cp_print_model, 59 efficient reduction, 18
–cp_show_constraints, 59 empty route, 369
–help, 48, 59 EndSearch(), 43
–helpmatch=S, 48 F
–helpon=FILE, 48
factory method, 38
–helpshort, 48
feasible solution, 17
A first fail principle, 139
AddConstraint(), 41 G
Assignment, 43
gflags, 47
B parameters read from a file, 324
replacement (rout-
best success principle, 139
ing.SetCommandLineOption()),
C 298
shortcuts, 48
clause, 144
types, 47
complexity theory, 18
Golomb Ruler
constraint, 15
Problem, 51
AllDifferent, 35
Golomb ruler, 53
Constraint Optimization Problem, 16
graph
Constraint Satisfaction Problem, 15
cycle, 369
cpviz, 97
empty route, 369
cryptarithmetic
partial route, 369
puzzles, 33
path, 369
cycle, 369
route, 369
D simple cycle, 369
simple path, 369
DebugString(), 58
DecisionBuilder, 41 I
DEFINE_bool, 47
instance of a problem, 17
DEFINE_double, 47
IntExpr, 39
DEFINE_int32, 47
intractable problems, 18
DEFINE_int64, 47
IntVar, 38
DEFINE_string, 47
DEFINE_uint64, 47
Index
M R
MakeAllDifferent(), 41 real problem, 16
MakeAllSolutionCollector(), 43 route, 369
MakeBestValueSolutionCollector(), 43
MakeDifference(), 66 S
MakeEquality(), 41 SearchLimit, 62
MakeFirstSolutionCollector(), 43 in Local Search, 188
MakeIntConst(), 64 specialized for time, 49
MakeIntVar(), 38 SearchMonitor
MakeIntVarArray(), 57 as SolutionCollector, 43
MakeLastSolutionCollector(), 43 as Solver’s parameters, 49
MakeLessOrEqual(), 69 callbacks, 90
MakeMinimize, 57 SetCommandLineOption(), 298
MakeNonEquality(), 66 simple cycle, 369
MakePhase(), 41 simple path, 369
MakeProd(), 39 solution, 17
MakeScalProd(), 40 SolutionCollector, 43
MakeSum(), 39 AllSolutionCollector, 43
MakeTimeLimit(), 48 BestValueSolutionCollector, 43
mathematical problem, 16 FirstSolutionCollector, 43
LastSolutionCollector, 43
N Solve(), 44
n-queens problem, 78 Solver
namespace creation, 38
operations_research, 37 parameters, 48
NewSearch(), 42 SolverParameters, 48
NextSolution(), 42 SolverParameters(), 48
NP, 18 StringPrintf(), 57
NP-Complete, 18
NP-Hard, 18 T
time
O wall_time(), 61
objective function, 16
objective functions, 51, 52 V
objective solution, 17 variable, 15
OptimizeVar, 57 variables
IntVar, 38
P
P, 18 W
ParseCommandLineFlags(), 47 wall_time()
partial route, 369 time, 61
path, 369
Problem
Cryptarithmetic puzzles, 33
Golomb Ruler, 51
puzzles
cryptarithmetic, 33
404