Вы находитесь на странице: 1из 315

or-tools open source library

Users Manual
Nikolaj van Omme
Laurent Perron
Vincent Furnon

June 21, 2013


(A4 version)

Copyright 2012-2013, Google

Welcome to the or-tools users manual!

Copyright 2012-2013, Google

License information
This document is provided under the terms of the

Apache License 2.0


You can nd the complete license text at the following address: http://www.apache.
org/licenses/LICENSE-2.0.
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 nd 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
GOOGLE is a trademark of Google Inc.
Linux is a registered trademark of Linus Torvald in the United States, other countries, or both.
Java and all Java-based trademarks and logos are trademarks of Sun Microsystem Inc. in the
United States, other countries, or both.
Other companies, products, or service names may be trademarks or service marks of others.

Ackowledgments
We thank the following people for their helpful comments:
Dania El-Khechen, Hkan Kjellerstrand, Louis-Martin Rousseau, Thomas Carton de Wiart

FOREWORD

We are glad to welcome you to the or-tools users manual. In this foreword, we try to answer
most common questions a newcomer could have when discovering this manual or the library
for the rst time.
The or-tools library is a set of operations research tools developed at Google. If you have no
idea what operations research1 is, you still can use our library to solve common small problems
with the help of our Constraint Programming (CP) solver. If you do know what operations
research is and how difcult it is sometimes to nd efcient, easy to use and open source code,
we hope you will enjoy using our library. We have put a lot of efforts in order to make it user
friendly and continue to improve it on a daily basis. Furthermore, we encourage interactivity
and are always open to suggestions. See the section How to reach us? below. If you have
comments about this manual or the documentation in general, see the section Do you have
comments?.

What is or-tools?
The or-tools library is a set of operations research tools written in C++ at Google.
The main tools are:
A Constraint Programming solver.
A simple and unied interface to several linear programming and mixed integer programming solvers (GLPK, CLP, CBC and SCIP).
Knapsack algorithms.
Graph algorithms (shortest paths, min cost ow, max ow, linear sum assignment).
FlatZinc support.

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 numerous 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.
Efcient 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.

What you will learn in this document


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.
See for instance http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml for the Google C++ Style
Guide.
3

iv

What you will not learn in this document


This document is by no means a tutorial on Operations Research nor on Contraint Programming. It is also NOT a reference manual (refer to the documentation hub to nd the reference
manual). There are way too many methods, parameters, functions, etc. to explain them all in
details. Once you understand the concepts and methods explained in this manual, you shouldnt
have any trouble scanning the reference manual and nd the right method, parameter, function,
. . . or code them yourselves!
We dont document the non Constraint Programming (CP) part of the library. If you have any
questions about the non-CP part of the library, dont hesitate to ask them on the mailing list.
See the section How to reach us? below.
We dont discuss the flatzinc implementation nor the parallel solving process.
This document will not describe how to use the library (and the syntactic sugar introduced
when possible) with Python, Java nor C#. This could possibly change in the future. The
tutorial examples (see below) exist also in Python, Java and C# though.

How to read 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 specic
answer, use the index or the table of contents to nd 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 efcient and rewarding one!
That said, the manual is kept short so that you can read it in its entirety. The rst 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 familiar with Constraint Programming nor is she a professional programmer. Second, an educated
reader who masters Constraint Programming and is quite at ease without necessarily mastering
one of the supported computer languages.
From time to time, we refer to scientic articles: you dont need to read and understand them
to follow the manual.
Did we succeed to write for such different proles? You tell us!

Conventions used in this manual


All the code is systematically written in monospace font. Function and methods
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.

You have been warned!

To explain some details that would break the ow of the text, we use a shadowed box.
This is an explanation that would break the ow of the text
This is why we prefer to put our explanation aside in a shadowed box.
To focus on some parts of the code, we omit non necessary code or code lines and replace them
by ". . . ".

In this example, the parameters of the function MakeBaseLine2() are stripped


as are the content of this method and the code lines that follow the denition of
this function. The purpose of this example is to show that the code is written inside
the namespace operations_research.
All commands are issued from a Unix-like terminal:

Adapt the command lines to your type of terminal and operating system.

Accompanying code for this manual


All the examples in this manual are coded in C++. For the most important code snippets, you
can nd 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 nd the complete examples on the documentation hub or under
the directories:
documentation/tutorials/Python
documentation/tutorials/Java
documentation/tutorials/Csharp.

Lab sessions
Theory is good but useless without practice and experience. For each chapter, we provide
exercises. Most of them are practical and consist in completing some C++ code. Even if you
dont (like to) code in C++, these lab sessions are helpful as we develop some concepts seen in
the manual more in details. Exercises vary between simple and straightforward to sometimes
really challenging. In the latter case, we mark these exercises as such. For all the exercises, we
provide solutions.
You can nd (soon!) the exercises and their solutions on the documentation hub:
http://or-tools.googlecode.com/svn/trunk/documentation/
documentation_hub.html#lab_sessions
or under the following directory of the or-tools library:
documentation/labs/C++

How to reach us?


The whole project or-tools is hosted on Google code:
http://code.google.com/p/or-tools/
You can follow us on Google+:
https://plus.google.com/u/0/108010024297451468877/posts
vii

and post your questions, suggestions, remarks, . . . to the or-tools discussion group:
http://groups.google.com/group/or-tools-discuss

How to reference this document?


Use this simple reference:
N. van Omme, L. Perron and V. Furnon, or-tools users manual, Google, 2013.
Here is a bibtex entry:
@TECHREPORT{or-tools-user-manual,
author = Nikolaj van Omme and Laurent Perron and Vincent
Furnon,
title = or-tools users manual,
institution = Google,
year = 2013
}

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!
The or-tools team

CONTENTS

Foreword

iii

Basics

Introduction to constraint programming


1.1 The 4-queens problem . . . . . . . . . . . . . . .
1.2 What is constraint programming? . . . . . . . . .
1.3 A little bit of theory . . . . . . . . . . . . . . . .
1.4 Real examples . . . . . . . . . . . . . . . . . . .
1.5 The three-stage method: describe, model and solve
1.6 Its always a matter of tradeoffs . . . . . . . . . .
1.7 The Google or-tools library . . . . . . . . . . . .
1.8 The content of the manual . . . . . . . . . . . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

First steps with or-tools: cryptarithmetic puzzles


2.1 Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.2 The cryptarithmetic puzzle problem and a rst model . . . . . . . .
2.3 Anatomy of a basic C++ code . . . . . . . . . . . . . . . . . . . .
2.4 SolutionCollectors and Assignments to collect solutions
2.5 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2.6 Other supported languages . . . . . . . . . . . . . . . . . . . . . .
2.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.

Using objectives in constraint programming: the Golomb ruler problem


3.1 Objective functions and how to compare search strategies . . . . . .
3.2 The Golomb ruler problem and a rst model . . . . . . . . . . . . .
3.3 An implementation of the rst model . . . . . . . . . . . . . . . . .
3.4 What model did I pass to the solver? . . . . . . . . . . . . . . . . . .
3.5 Some global statistics about the search and how to limit the search . .
3.6 A second model and its implementation . . . . . . . . . . . . . . . .
3.7 A third model and its implementation . . . . . . . . . . . . . . . . .
3.8 How to tighten the model? . . . . . . . . . . . . . . . . . . . . . . .
3.9 How does the solver optimize? . . . . . . . . . . . . . . . . . . . . .
3.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Reication

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

3
3
9
12
18
21
22
23
23

.
.
.
.
.
.
.

25
26
31
33
40
44
46
47

.
.
.
.
.
.
.
.
.
.

49
50
51
54
56
59
60
64
64
68
68
69

4.1

II
5

What is reication? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

Customization
Dening search primitives: the n-queens problem
5.1 The n-queens problem . . . . . . . . . . . . . . . . . .
5.2 Implementation of the basic model . . . . . . . . . . .
5.3 Basic working of the solver: the search algorithm . . . .
5.4 cpviz: how to visualize the search . . . . . . . . . . . .
5.5 Basic working of the solver: the phases . . . . . . . . .
5.6 Out of the box variables and values selection primitives
5.7 Customized search primitives . . . . . . . . . . . . . .
5.8 Breaking symmetries with SymmetryBreakers . . .
5.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . .

71
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

Local search: the job-shop problem


6.1 The job-shop problem, the disjunctive model and benchmark data
6.2 An implementation of the disjunctive model . . . . . . . . . . .
6.3 Scheduling in or-tools . . . . . . . . . . . . . . . . . . . . . . .
6.4 What is local search (LS)? . . . . . . . . . . . . . . . . . . . . .
6.5 Basic working of the solver: Local Search . . . . . . . . . . . . .
6.6 Local Search Operators . . . . . . . . . . . . . . . . . . . . . .
6.7 The jobshop problem: and now with local search! . . . . . . . . .
6.8 Filtering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Meta-heuristics: several previous problems
7.1 Meta-heuristics in or-tools . . . . . . . . . . . . . . . .
7.2 Search limits and SearchLimits . . . . . . . . . . .
7.3 Large neighborhood search (LNS): the job-shop problem
7.4 Restarting the search . . . . . . . . . . . . . . . . . . .
7.5 Tabu search (TS) . . . . . . . . . . . . . . . . . . . . .
7.6 Simulated annealing (SA) . . . . . . . . . . . . . . . .
7.7 Guided local search (GLS) . . . . . . . . . . . . . . . .
7.8 Variable Neigborhood Search (VNS) . . . . . . . . . .
7.9 Default search . . . . . . . . . . . . . . . . . . . . . .
7.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

Custom constraints: the alldifferent_except_0 constraint


8.1 Basic working of the solver: constraints . . . . . . . . . . . . . . . . .
8.2 Consistency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8.3 The AllDifferent constraint . . . . . . . . . . . . . . . . . . . .
8.4 Changing dynamically the improvement step with a SearchMonitor
8.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.

.
.
.
.
.
.
.
.
.

73
74
77
81
92
114
123
126
126
132

.
.
.
.
.
.
.
.
.

133
135
143
148
161
167
182
190
202
207

.
.
.
.
.
.
.
.
.
.

209
211
211
211
211
211
211
211
211
211
211

.
.
.
.
.

213
213
213
213
213
213

III
9

Routing

215

Travelling Salesman Problems with constraints: the TSP with time windows
9.1 A whole zoo of Routing Problems . . . . . . . . . . . . . . . . . . . . . .
9.2 The Routing Library (RL) in a nutshell . . . . . . . . . . . . . . . . . . .
9.3 The Travelling Salesman Problem (TSP) . . . . . . . . . . . . . . . . . .
9.4 The model behind the scenes: the main decision variables . . . . . . . . .
9.5 The model behind the scenes: overview . . . . . . . . . . . . . . . . . . .
9.6 The TSP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.7 The two phases approach . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.8 The Travelling Salesman Problem with Time Windows (TSPTW) . . . . .
9.9 The TSPTW in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . .
9.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.

217
219
223
226
232
238
248
255
256
266
270

10 Vehicule Routing Problems with constraints: the capacitated vehicle routing problem
271
10.1 The Vehicle Routing Problem (VRP) . . . . . . . . . . . . . . . . . . . . . . 271
10.2 The VRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272
10.3 The Capacitated Vehicle Routing Problem (CVRP) . . . . . . . . . . . . . . . 272
10.4 The CVRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273
10.5 Multi-depots and vehicles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
10.6 Partial routes and Assigments . . . . . . . . . . . . . . . . . . . . . . . . . . 274
10.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274
11 Arc Routing Problems with constraints: the Cumulative Chinese Postman Problem
275
11.1 The Chinese Postman Problem (CPP) . . . . . . . . . . . . . . . . . . . . . . 276
11.2 The Cumulative Chinese Postman Problem (CCPP) . . . . . . . . . . . . . . . 276
11.3 A rst implementation for the CCPP . . . . . . . . . . . . . . . . . . . . . . . 276
11.4 Disjunctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
11.5 A second implementation for the CCPP . . . . . . . . . . . . . . . . . . . . . 276
11.6 Partial routes and locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
11.7 Lower bounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
11.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276

IV

Technicalities

277

12 Utilities
12.1 Logging . . .
12.2 Asserting . .
12.3 Timing . . .
12.4 Proling . .
12.5 Debugging .
12.6 Serializing .
12.7 Visualizing .
12.8 Randomizing

279
279
280
281
283
283
283
283
283

13 Modeling tricks

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.

285

13.1 Efciency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286


13.2 False friends and counter-intuitive ideas . . . . . . . . . . . . . . . . . . . . . 286
13.3 What are my solving options? . . . . . . . . . . . . . . . . . . . . . . . . . . 286
14 Under the hood
14.1 Main les and directories . . . . . . . . . . .
14.2 Naming conventions and programming idioms
14.3 Main classes, structures and typedefs . . . . .
14.4 The Trail struct . . . . . . . . . . . . . . .
14.5 The Search class . . . . . . . . . . . . . . .
14.6 The Queue class . . . . . . . . . . . . . . . .
14.7 Variables and Assignments . . . . . . . . .
14.8 SearchMonitors . . . . . . . . . . . . . .
14.9 Local Search (LS) . . . . . . . . . . . . . . .
14.10 Meta-heuristics and SearchMonitors . . .
14.11 The Routing Library (RL) . . . . . . . . . . .
14.12 Summary . . . . . . . . . . . . . . . . . . . .

Apprendices

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

.
.
.
.
.
.
.
.
.
.
.
.

287
288
288
288
288
288
288
288
288
290
290
290
295

297

Bibliography

299

Index

301

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:

The chapter is divided in three parts. First, we introduce Constraint Programming by looking
at a solving process done by our CP solver. Along the way, we will try to dene Constraint
Programming and show some practical problems where CP stands out. A little bit of theory
will lay the foundations for the whole manual. Second, we introduce a simple strategy (the
three-stage method) that can help us when confronted with a problem to solve. This method
will be applied repeatedly in this manual. Another recurrent idea in this manual is to be aware
of tradeoffs. This idea is the key to successful optimization and well worth a whole section.
Finally, we outline the general principles of the library and detail the content of this manual.
Prerequisites:

None. Being open minded, relaxed and prepared to enjoy the or-tools library helps
though.

1.1 The 4-queens problem


We present a well-known problem among Constraint Programming practitioners: the 4-queens
problem. We shall encounter this problem again and generalize it in Chapter 5.

1.1.1 The problem


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.
1

See section 5.1 for a more precise denition of this problem.

1.1. The 4-queens problem

The following gure illustrates a solution to the 4-queens problem: none of the 4 queens can
capture each other.

Although this particular problem isnt very impressive, keep in mind that you can generalize it
to chessboards with 4.

1.1.2 A mathematical translation of the problem


In Constraint Programming we translate a real problem to a mathematical model with variables
and constraints. Variables represent decisions and constraints restraint the variables of taking
arbitrary values altogether. For instance, to model the 4-queens problem, we could use a binary
variable that indicates if a queen is present on the given (, ) square ( = 1) or not
( = 0). The rst index denotes the th row and the second index the th column. We
need several constraints to model that no two queens can capture each other. We also need to
constraint the need for 4 queens. We could add the constraint:

= 4.
(,) squares

This constraints ensure that we place 4 queens on the chessboard. In general, constraints only
permit possible combinations of values of variables corresponding to real solutions2 . In the next
section, we will see how the or-tools CP solver tries to solve this problem. More precisely,
how the solver will try to solve the model we will develop and explain in sections 5.1 and 5.23 .

1.1.3 Propagation and search


Constrainst Programming solvers are mainly based on two concepts4 :
propagation5 : variables can have different values but the solver must remove some of
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 efcient manner. These algorithms propagate the current state of the solver and removes incompatible
or undesirable values.
2

Things are a little bit more complex than that but lets keep it simple for the moment. See subsection 1.3.2 for
more.
3
We dont need to know the details of the model right now.
4
These are two key elements of a Constraint Programming solving algorithm but there are many more!
5
Propagation is also called domain ltering, pruning or consistency techniques.

Chapter 1. Introduction to constraint programming

backtracking: from time to time, the solver is stuck because it tried to assign some
values to some variables that are just not possible (or desirable) because they dont respect
the constraints. The solver must then challenge its previous choices and try other values.
This is called backtracking. Backtracking also occurs when the solver nds a solution
but continues the search and tries to nd another solution.
To better understand Constraint Programming, lets have a look at a real solving process6 . In
the following Figures, crosses represent the action of removing values from variables domain.
Each step in the solving process is separated from the following one by an horizontal line.
The solver starts by placing the rst queen in the upper left corner. Because of the model we
gave to the solver, it knows that there cannot be any other queen in the same column, hence the
grey crosses on the following Figure. One constraint tells the solver that there cannot be another
queen on the same diagonal with a negative slope (the diagonals going down then right). The
red crosses show this impossibility.

One constraint tells the solver that no two queens can be on the same row, hence the next red
crosses.

After this rst step, only the white squares are still available to place the three remaining queens. The process of excluding some squares is what is called propagation.
The second step starts with the solver trying to place a second queen. It does so in the rst
available square from above in the second column. As in the rst step, the solver knows that no
other queen can be placed in a column where it just placed a queen, hence the new grey crosses
in the next Figure.
The propagation is as follow:
The same negative diagonal constraint as in step 1 tells the solver that no queen can be on the
negative diagonal of the second queen, hence the red cross.
6

You can nd this search process detailed in sections 5.2 and 5.4.

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 conguration. The solver has to backtrack!
The solver decides to challenge its last decision to place the second queen in the third row from
above and places it in the fourth row.
The propagation is as follow:
First, the square with the red cross is removed because of the positive diagonal constraint. This
leaves only one possibility to place a queen in the fourth column.

The no two queen on the same row constraint removes one more square in the third column,
leaving only one square to place the last remaining queen.

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 rst choice to place the rst queen in the
rst row and places the rst queen in the rst column second row.
The propagation can now take place:
Two values are taken away because of the negative diagonal constraint:

while the positive diagonal constraint one:

Now comes the turn of the no two queen on the same row constraint and it is responsible of
removing the next three red crosses:

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 rst row.

The no two queen on the same row constraint forbids any other queen to be placed on the
fourth row:

and any other queen on the rst row, leaving no choice but to place the fourth queen in the
fourth column third row:

The solver nds out that the model is respected, so we have our rst solution! Should the
solver continue the search, it would have to backtrack and try to place the rst queen in the rst
column third row.

Chapter 1. Introduction to constraint programming

1.2 What is constraint programming?


Constraint Programming (CP) is an emergent eld in operations research7 . It is based on feasibility (i.e. nding a feasible solution) rather than optimization (i.e. nding an optimal solution)
and focuses on the constraints and variables domain rather than the objective function.
Although quite new, it already possesses a strong theoretical foundation, a widespread and
very active community around the world with dedicated scientic journals and conferences and
an arsenal of different solving techniques. CP has been successfully applied in planning and
scheduling and in numerous other problems with heterogeneous constraints (see section 1.4
for a description of some of its achievements). The problems CP deals (mostly) with are
called Constraint Satisfaction Problems (CSP). Roughly, a CSP is a mathematical model with
constraints and the goal is to nd a feasible solution i.e. to assign values to the variables of
the model such that every constraint is satised. One of the most well-known such problem
is the Boolean SATisability Problem (boolean SAT). (See Wikipedia Constraint satisfaction
problem and Boolean satisability problem entries.)
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 eld and we use some jargon. Dont be
afraid if you dont understand those asides and just read on.
Constraint Programming does optimization too!
When faced with an optimization problem, CP rst nds a feasible solution 0 with an
objective value of (0 ). It then adds the constraint () < (0 ) and tries to nd a
feasible solution for this enhanced model.
The same trick is applied again and again until the addition of constraint () < ( ) for
a feasible solution renders the model incoherent, i.e. there is no feasible solution for
this model. The last feasible solution is thus an optimal solution.

1.2.1 Strength of Constraint Programming


Two of the main assets of CP are:
the ease to model a problem and
the possibility to add heterogeneous constraints.
7

This new eld has its origins in a number of elds including Articial intelligence, Programming Languages,
Symbolic Computing, Computational Logic, etc. The rst articles related to CP are generally dated from the
seventies but CP really started in the eighties. As with every new eld, names, origins, etc. are not settled
and different people sometimes take different avenues. We carefully tried to use commonly accepted names,
techniques, etc.

1.2. What is constraint programming?

The ease to model a problem


If you are used to (non-)linear programming, you know how difcult it is to model some constraints (forcing them to be linear, use of big for disjunctions, replacing one constraints by
a bunch of linear constraints, relying on the direction of optimization (minimizing or maximizing), etc.). None of this happens in CP because constraints can be any constraints. They even
dont 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 contraints is the AllDifferent constraint. This constraint ensures that the variables have different values in a feasible solution. For instance
AllDifferent(0 , 1 , 2 ) forces the three variables 0 , 1 and 2 to have different values. Say
that 0 , 1 and 2 can take the integer values in [0, 2].
Compare
AllDifferent(0 , 1 , 2 )
to the classical way (see [Williams2001]) of translating
gramming for instance:

2 = 0
=0
2
=0 = 1
2
1
=0

this constraint in linear integer pro

To model the AllDifferent(0 , . . . , 1 ) constraint8 with [0, 1], we already need 2


auxiliary variables :
{
1
if takes value
=
0
otherwise
and 3 linear equations!
Of course if AllDifferent(0 , 1 , 2 ) was being replaced by its linear integer programming translation for instance, it would only be syntactic sugar but it is not. Specialized and efcient
propagation algorithms were (and are still!) developed to ensure 0 , 1 and 2 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 y (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.
The possibility to add heterogeneous constraints
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 efcient manner.
Propagation is done globally on all involved variables but the propagation is done constraint by constraint.
10
Basically, you only need to be able to propagate (hopefully efciently) your constraints.
9

10

Chapter 1. Introduction to constraint programming

One of the curiosities of CP is its ability to deal with meta-constraints: constraints on constraints!
Take for instance the Element constraint. Let [0 , . . . , 1 ] be an array of integers variables
with domain {0, . . . , 1}, an integer variables with domain contained in {0, . . . , 1} and
with domain {0, . . . , 1}. The Element constraint assign the th variable in [0 , . . . , 1 ]
to , i.e.:
= .
If you change or the array [0 , . . . , 1 ], will change accordingly but remember that you
have an equality, so this works the other way around too. If you change then or/and the
array [0 , . . . , 1 ] will have to change! This technique is called reication and you can learn
more about it in chapter 4. The ease to model a problem and the possibility to add heterogeneous constraints sometimes make CP the preferred or only framework to model some difcult
problems with a lot of side-constraints.

1.2.2 The search


Propagation is not enough to nd 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 implemented 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
nds 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 renements (some implemented in or-tools too) to this basic version.
The assignment possibilities dene the search space12 . In our 4-queens example, the search
space is dened by all possible assignments for the 16 variables . 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. Its important to understand that the search space is dened by the variables and
their domain (i.e. the model) and not by the problem itself13 . Actually, it is also dened 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 21 . Because we dont want to miss any portion of the search
11

See the section Basic working of the solver: the search algorithm for more details.
See next section for more.
13
In section Model, we will see a model with a search space of size 16 for the 4-queens problem.
14
Determining the exact (or even approximate) search space size is very often a (very) difcult problem by
itself.
15
Most of the time, we want good solutions quite rapidly. It might be more interesting to have a huge search
space but that we can easily visit than a smaller search space that is more difcult to scan. See the section Its
always a matter of tradeoffs.
16
Or a bunch of variables. Or it can just restrict the values some variables can take. Or a combination of both
but lets keep it simple for the moment: the solver assigns a value to one variable at a time.
12

11

1.3. A little bit of theory


space, we want to visit solutions where 21 = 1 but also solutions where 21 = 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 propagation. You can also combine different techniques. For instance branch, price and cut.
CP for the MIP practitionersa
There are strong similarities between the two basic search algorithms used to
solve an MIP and a CSP.
MIP
Branch and bound
Bound:

CSP
Branch and prune
Prune:

Relax constraints

Propagate constraints

Reduce gap

Reduce variable domains

Goal: Optimality
View: Objective oriented
a

Goal: Feasibility
View: Domain oriented

This is an aside for our MIP (Mix Integer Programming) colleagues. Its full of jargon on purpose.

1.3 A little bit of theory


[MUST BE REREAD] We give you in a nutshell the important ideas and the vocabulary we
use throughout this manual. In section 1.3.1, we cover the basic vocabulary to talk about the
problems we solve in CP. Section 1.3.4 introduces informally complexity theory. One of the
difculties of this theory is that there are a lot of technical details no to be missed. We have
tried in this section to introduce you to important ideas without being drawn into too many
details (some inescapable details are in the footnotes). Complexity theory is quite new (it really
started in the 70s) and is not easy (and after reading this section, youll have more questions
than answers). If you are allergic to theory, read the next two subsections but skip the rest. We
are convinced - we took the time to write it, right? - that you would benet from reading this
section in its entirety but it is up to you.

12

Chapter 1. Introduction to constraint programming

1.3.1 Constraint Satisfaction Problems (CSP) and Constraint Optimization Problems (COP)
We illustrate the different components of a Constraint Satisfaction Problem with the 4-queens
problem we saw in section 1.1. A CSP consists of
a set of variables = {0 , . . . , 1 }.
Ex.: For the 4-queens problem, we have a binary variable indicating the presence or
not of a queen on square (, ):
= {00 , 01 , 02 , 03 , 10 , 11 , 12 , . . . , 33 }
for each variable , a nite set of possible values (its domain).
Ex.: Each variable is a binary variable, thus
00 = 01 = . . . = 33 = {0, 1}.
constraints that restrict the values the variables can take simultaneously.
Ex.: Constraints that avoid that two queens are on the same row:
row 0:
row 1:
row 2:
row 3:

00 +
10 +
20 +
30 +

01 +
11 +
21 +
31 +

02 +
12 +
22 +
32 +

03
13
23
33

1
1
1
1

Indeed, these constraints ensure that for each row at most one variable 0 , 1 , 2 or 3
could take the value 1. Actually, we could replace the inequalities by equalities because
we know that every feasible solution must have a queen on each row. Later, in section 5.2,
we will provide another model with other variables and constraints.
As we mentioned earlier, values dont need to be integers and constraints dont need to be
algebraic equations or inequalities17 .
If we want to optimize, i.e. to minimize or maximize an objective function, we talk about a
Constraint Optimization Problem (COP). The objective function can be one of the variables
of the problem or a function of some or all the variables.
A feasible solution to a CSP or a COP is a feasible assignment: every variable has been assigned a value from its domain in such a way that all the constraints of the model are respected.
The objective value of a feasible solution is the value of the objective function for this solution.
An optimal solution to a COP is a feasible solution such that there are no other solutions with
better objective values. Note that an optimal solution doesnt need to exist nor is it unique.
17

Basically, the only requirement for a constraint in CP is its ability to be propagated.

13

1.3. A little bit of theory

1.3.2 Problems, instances and solutions


1.3.3 Two important ideas of the complexity theory for the hurried
reader
If you prefer not to read the next section, we have summarized its main ideas:
problems are divided in two categories18 : easy (P problems) and hard (NP-Hard or
NP-Complete problems) problems. Hard problems are also called intractable19 and in
general we only can nd approximate solutions for such problems20 . Actually, the question of being able to nd exact solutions to hard problems is still open (See the box The
?
= question below);
good solutions (vs. exact solutions) are called approximations and since the 90s a considerable effort was invested in designing a complexity theory of approximations. There
is a whole zoo of complexity classes. Some problems can be approximated but without
any guarantee on the quality of the solution, 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 algorithm will be. For some problems it is hard to nd
approximations and for others, it is even impossible to nd an approximation with any
guarantee on its quality!

1.3.4 Complexity theory in a few lines


Some problems such as the Travelling Salesman Problem (see chapter 9) are hard to solve21 : no
one could ever come up with a very efcient algorithm to solve this problem. On the other hand,
other problems, like the Chinese Postman Problem (see chapter 11), are solved very efciently.
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 intractable problem to optimality, you can try to nd 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 them 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.
18

Most problems of practical interest belong to either categories but these two categories dont cover all problems.
19
Intractable problems are problems which in practice take too long to solve exactly, so there is a gap between
the theoretical denition (NP-Hard problems) and the practical denition (Intractable problems).
20
Technically, you could nd an exact solution but you would not be able to prove that it is indeed an exact
solution in general.
21
Roughly, we consider a problem to be hard to solve if we need a lot of time to solve it. Read on.

14

Chapter 1. Introduction to constraint programming

Intractability
One of the main difculties complexity experts faced in the 70s was to come up with a theoretical denition of the complexity of problems not algorithms. Indeed, it is relatively easy22
to dene a complexity measure of algorithms but how would you dene the complexity of a
problem? If you have an efcient algorithm to solve a problem, you could say that the problem
belongs to the set of easy problems but what about difcult problems? The fact that we dont
know an efcient algorithm to solve these doesnt mean these problems are really difcult.
Someone could come up one day with an efcient algorithm! The solution the experts came up
with was to build equivalence classes between problems and dene the complexity of a problem with respect to the complexity of other problems (so the notion of complexity is relative
not absolute): a problem is as hard as a problem if there exists an efcient transformation
that maps every instance of problem into an instance () = of problem such that if
solve , you solve .
A

(b) = a

Indeed, if there exists an efcient algorithm to solve problem , you can also solve efciently
problem : transform an instance into into an instance () = of problem and solve it
with the efcient algorithm known to solve problem . So problem is as difcult as problem
(because if you know an efcient algorithm to solve problem , you can solve problem as
efciently) and we write T and say that problem reduces efciently to problem or
that is an efcient reduction23 . The search for an efcient algorithm is replaced by the search
for an efcient reduction between instances of two problems to prove complexity.
This main idea leads to a lot of technicalities:
how to measure the complexity of an algorithm?
what is an efcient transformation?
what are the requirements for such a reduction?
...
We dont answer these interesting questions except the one on efciency. We consider a reduction efcient if there exist a polynomial-time bounded algorithm (this refers to the rst
question...) that can transform any instance of problem into an instance of problem
such that the solutions correspond. This also means that we consider an algorithm efcient if it
is polynomially time-bounded (otherwise the efciency of the reduction would be useless).
The class of problems that can be efciently solved is called , i.e. the class of problems that
22

Well, to a certain degree. You need to know what instances you consider, how these are encoded, what type
of machines you use and so on.
23
The T in T is in honor of Alan Turing. Different types of efcient reductions exist.

15

1.3. A little bit of theory


can be solved by a polynomial-time bounded algorithm2425 .
Some problems are difcult 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
Hamiltonian Path Problem (HPP). Given a graph, is there a path that visits each vertex exactly
once? Finding such a path is difcult but verifying that a given path is indeed an Hamiltonian
path, i.e. that passes exactly once through each vertex, can be easily done. Problems for which
it is easy to verify their solutions, i.e. for which this verication can be done in polynomial time,
are said to be in the class 26 . because if you can nd 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 = 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 classication 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 problem?
Wait a minute. There is an innite 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 problem
(and hereby proving that this problem belongs to the set of NP-Complete problems?
This is done in two steps:
1. First, you have to know that the reduction is transitive. This means that if T
and T then T . Thus, if you have one problem such that all problems
in NP are reducible to , i.e. T , then to prove that all problems in
NP reduce to a problem , you just need to prove that reduces to . Indeed, if
T then T T a .
The funny fact is that if is in NP, then T also. If you can solve one problem
in NP-Complete efciently, you can solve all the problems in NP efciently!
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 satisability problem (SAT).
Proving that the SAT problem is NP-Complete is a major achievement in the complexity theory (the proof is highly technical).
a

If you want to prove that a problem is NP-Hard (see below), take a problem that is NP-Complete,
like the HPP, and reduce it to your problem. This might sound easy but it is not!
24

For technical reasons, we dont compare problems but languages and only consider decision problems, i.e.
problems that have a yes/no answer. The Subset Sum Problem is such a problem. Given a nite set of integers,
is there a non-empty subset whose sum is zero? The answer is yes or no. By extension, we say an optimization
problem is in , if its equivalent decision problem is in . For instance, the Chinese Postman Problem (CPP) is
an optimization problem where one wants to nd a minimal route traversing all edges of a graph. The equivalent
decision problem is Is it possible to nd a feasible route with cost less or equal to ? where is a given
integer. By extension, we will say that the CPP is in (we should rather say that the CPP is in optimization).
25
This discussion is really about theoretical difculties of problems. Some problems that are theoretically easy
(such as solving a Linear System or a Linear Program) are difcult in practice and conversely, some problems that
are theoretically difcult, such as the Knapsack Problem are routinely solved on big instances.
26
The abbreviation refers to non-deterministic polynomial time, not to non-polynomial.

16

Chapter 1. Introduction to constraint programming

Finally, if a problem is as hard as an NP-Complete problem, it is called an NP-Hard problem.


Optimization problems, whose decision version belong to NP-Complete, fall into this category.
The next gure summarizes the relations between the complexity classes27 we have seen as
most of the experts believe they stand, i.e. P = NP.
NPHard

NPComplete

NP

If P = NP
?

The = question
The P versus NP problem is a major unsolved problem in Computer Science. Informally, it
asks whether every problem whose solution can be quickly veried by a computer ( NP)
can also be quickly solved by a computer ( P). It is one of the seven Millennium Prize
Problems selected by the Clay Mathematics Institute. The offered prize to the rst team to
solve this question is $1,000,000!
In 2002 and 2012, W. I. Gasarch (see [Gasarch2002] and [Gasarch2012]) conducted a poll
?
and asked his colleagues what they thought about the = question. Here are the
results:
Outcomea

%
%
(2002) (2012)
=
61
83
=
9
9
No idea
30
8
One possible outcome - mentioned by very few - is that this question could be... undecidable, i.e. there is no yes or no answerb !
a

We agglomerated all other answers into a category No idea although the poll allowed people to fully express themselves (some answered I dont care for instance). The rst poll (2002) involved 100 researchers
while the second one involved 152 researchers.
b
See Undecidable problem on Wikipedia.

If you are interested in this fascinating subject, we recommend that you read the classical book
27

Be aware that there are many more complexity classes.

17

1.4. Real examples

Computers and Intractability: A Guide to the Theory of NP-Completeness from M. R. Garey


and D. S. Johnson (see [Garey1979]28 ).
The practical aspect of intractability
If you try to solve a problem that is proven to be NP-Hard, you know that it is probably an
intractable problem (if = ). At least, you know that no one could ever come with an
efcient algorithm to solve it and that it is unlikely to happen soon. Thus, you can not 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 these 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 satised 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
constraints29 . 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 nd a solution that doesnt violate too much the soft constraints in the
hope that such approximate solution isnt that different from an exact or optimal solution30 .
Approximation complexity
[TO BE DONE]

1.4 Real examples


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 conguration.
Here is a non-exhaustive list31 where CP has been used with success:
Production sequencing
Production scheduling
Satellite tasking
Maintenance planning
28

This book was written in 1979 and so misses the last developments of the complexity theory but it clearly
explains the NP-Completeness theory and provides a long list of NP-Complete problems.
29
For MIP practitioners, this is equivalent to Lagrangian Relaxation.
30
In the case of optimization, a solution that isnt that different means a solution that has a good objective value,
preferably close to the optimum.
31
This list is much inspired from the excellent documentation provided by Helmut Simonis under the Creative
Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.

18

Chapter 1. Introduction to constraint programming

Transport
Stand allocation
Personnel assignment
Personnel requirement planning
Hardware design
Compilation
Financial applications
Placement
Industrial cutting
Air trafc control
Frequency allocation
Network conguration
Product design
Product blending
Time tabling
Production step planning
Crew rotation
Aircraft rotation
Supply chain management
Routing
Manufacturing
Resource allocation
Circuit verication
Simulation
...
With such a high success rate in different application, CP can be thus described as one efcient
tool in the toolbox of Operations Research experts.

1.4.1 Success stories


We could list hundreds of success stories were CP made a - sometimes huge - difference but
we dont want to advertise any company. Youll nd plenty on the web. Let us just advertise
CP as a very efcient and convenient tool to solve industrial problems.

19

1.4. Real examples


Most of the time, CP32 :
solves your problem within minutes;
only needs the push of a button (after setting some parameters and providing some data
of course);
is very exible and easily allows you to add or remove side constraints;
improve solutions found by hand by up to sometimes 30%.
All of this can only happen if you nd the right software that is readily well-adapted to your
problem. Otherwise, a good option is to develop the product yourself, using or-tools for instance.

1.4.2 Can CP be compared to the holy grail of Operations Research?


From time to time, people search for the holy grail of Computer Science. We could dene
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 Annealing33 to solve arbitrary combinatorial optimization problems34 .
Modelling 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 language35 . It is then translated for an appropriate
solver of your choice. Some modelling languages even let you write high level algorithms.
One of the great advantages of modelling languages is the possibility to quickly prototype your
algorithm and to try it online (and for free!) with the NEOS server36 .
All these approaches dont compare37 to dedicated algorithms tailored for a specic problem38 .
Until now, all these attempts have been vain. That said, CP - because of its particularity of
dealing with contraints localy39 - is probably the closest technique to the holy grail. Actually,
we didnt 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.
32

This is common knowledge in the eld.


You can learn more about Simulated Annealing (SA) in the section Simulated annealing (SA).
34
This implies that any problem can be translated into a combinatorial problem!
35
See Wikipedia Algebraic languages.
36
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 denition of the optimization
problem; all additional information required by the optimization solver is determined automatically.
37
Luckily, otherwise we would be jobless!
33

38

Actually, this search for the holy grail is closely related to the famous P = NP question. If such algoritm
exists, then most probably P = NP. See the section Intractability.
39
See the subsection The ease to model a problem.

20

Chapter 1. Introduction to constraint programming

1.5 The three-stage method: describe, model and solve


We propose a recipe that belongs to the folklore. Like all recipes it is only a guide and should
not be followed blindly40 . When it comes to research, everything is permitted and your imagination is the limit.
As Feyerabend (Wikipedia) once said:
Anything goes
In short, we propose to address a problem a in three stages:
describe
model
solve
The three stages are inter-mingled and are not intended to be followed one after the other rigidly
as each stage inuence the two other ones. The way you describe a problem will lead you to
privilege certain types of models. You cannot propose a model without anticipating the way
you will solve this model. A beautiful model that cannot be solved is useless.
Conversely, when you are used to model some types of problems, you will probably try to cast
a real problem into a problem that is known and well-solved. Problems do evolve with time as
does your knowledge of them. Accordingly, you will change your description of the problem,
the models you use and the way you solve these models.
One of the strength of constraint programming is its ability to describe quite naturally problems. For instance, if you need to ensure that some variables must hold different values, simply
use the AllDifferent constraint on these variables. Reication allows you to express some
constraints that are simply unavailable in other solving paradigms. Another strength of constraint programming is its malleability. You can add side constraints very easily and adapt your
models accordingly making constraint programming ideal to prototype new ideas.

1.5.1 Describe
This step is often overlooked but is one of most important part of the overall solving process.
Indeed, a real problem is often too complex to be solved in its entirety: you have to discard
some constraints, to simplify certain hypothesizes, take into account the time to solve the problem (for instance if you have to solve the problem everyday, your algorithm can not take one
month to provide a solution). Do you really need to solve the problem exactly? Or can you
approximate it?
This step is really critical and need to be carefully planned and executed.
Is this manual, we will focus on three questions:
What is the goal of the problem we try to solve? What kind of solutions are we exactly
expected to provide?
40

If you are allergic to this academic approach, you probably will be happy to know that we only use this
three-stage method in the rst two partsc of this manual.

21

1.6. Its always a matter of tradeoffs

What are the decision variables? What are the variables whose values are crucial to
solve the problem?
What are the constraints? Are our constraints suited to solve the problem at hand?

1.5.2 Model
Again a difcult stage if not the most challenging part of the solving process. Modelling is
more of an Art than anything else. With experience, you will be able to model more easily
and use known and effective tricks. If you are a novice in Operations Research/Constraint
Programming, pay attention to the proposed models in this manual as they involve a lot of
knowledge and subtleties. Do not be discouraged if you do not understand them at rst. This
is perfectly normal. Take the time to read them several times until you master them. Beside, it
could be our fault if you do not understand them: maybe we did not explain them well?
When confronted with a new problem, you might not know what do to. We all face this situation. This is what research is all about!

1.5.3 Solve
The reader should be aware that this stage isnt only about pushing a solve button and waiting
for the results to be delivered by the solver. The solve stage involves reasoning to nd the best
way to solve a given model, i.e. how to traverse the search tree in a efcient way. We discuss
this stage in details in chapter 5.

1.6 Its always a matter of tradeoffs


There is no universal algorithm or paradigm to solve every problem41 .
When confronted to a real problem to solve, we rst need to translate it into a mathematical
problem. Generally speaking, the more elements from reality we consider, the bigger and
nastier the model becomes. There is a tradeoff between the precision with which our model
reects the real problem and the need to keep the model simple enough to be able to solve
it efciently. When developing algorithms, we are always making decisions among several
options, each with its advantages and disadvantages42 . Lets say we are implementing our own
constraint with our own propagation algorithm (this is exactly what we will do in chapter ??).
We might develop a very clever ltering algorithm for our constraint that allows to disregard
lots of undesirable variables. Wow, what a brilliant algorithm! Well, maybe not. First, the time
needed to lter the domains might be prohibitive. Maybe another - less clever - algorithm that
lters less variables would t better and allow an overall quicker search because for instance the
search tree could be visited more quickly. Second, the clever ltering algorithm could disregard
some variables that other ltering algorithms or branching schemes are based on, i.e. the clever
algorithm is not so clever when it works in collaboration with others!
41
42

22

At least, no one found one and with our actual knowledge, there is a strong suspicion that none exist.
Of course, we are talking about clever options.

Chapter 1. Introduction to constraint programming

Be conscious of the tradeoffs and that what seems the best option at a time might actually not
work that well no matter how clever the basic idea was. Ideas have to be tested and retested.
This testing is an uncompromising way to take decisions but also allows to get a better insight
of how and why an algorithm actually works (or fails).
CP and the or-tools library allow us to develop very quickly prototypes we can test, improve,
test, redesign, test, etc., you get the idea.
The good optimization researchers motto:
Its always a matter of tradeoffs
Writing this manual is no exception. What content do we introduce and how much details do
we add?
Ultimately, you are best aware of your problem and the (limited) resources you have to solve
it. As we said:
Its always a matter of tradeoffs.
We will refer to this motto from time to time in this manual.

1.7 The Google or-tools library


1.7.1 Coding philosophy

1.8 The content of the manual


1.8.1 Part I: Basics
1.8.2 Part II: Customization
1.8.3 Part III: Routing
1.8.4 Part IV: Technicalities
1.8.5 Appendices

23

CHAPTER

TWO

FIRST STEPS WITH OR-TOOLS: CRYPTARITHMETIC


PUZZLES

This chapter introduces the basics of the or-tools library. In particular, we show how to use
the Constraint Programming Solver (CP Solver). It takes a while to get used to the logic of
the library, but once you grasp the basics explained in this chapter, youre good to go and you
should be able to nd your way through the numerous examples provided with the library.
Overview:

We start with a discussion on the setup of the library, then walk through a complete example to
solve a cryptarithmetic puzzle. Along the way, we see how to create the CP solver and populate
it with a model, how to control the search with a DecisionBuilder, collect solutions with
SolutionCollectors and change the behavior of the program with parameters (through
the Google gags library). Finally, we say a few words about the other supported languages
(Python, Java and C#).
Section 2.3.1 summarizes in two Figures all the required steps to write a basic program.
Prerequisites:

basic knowledge of C++.


basic knowledge of Constraint Programming (see chapter 1).
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap2.


The les inside this directory are:
cp_is_fun1.cc: Our rst example: a simple cryptarithmetic puzzle to show the basics.
cp_is_fun2.cc: Use of SolutionCollectors to collect some or all solutions.
cp_is_fun3.cc: Use of the Google gags library to parse command line parameters.
cp_is_fun4.cc: Use of read-only solver parameters.

2.1. Getting started

2.1 Getting started


In case of conict with the http://code.google.com/p/or-tools/wiki/AGettingStarted page,
please disregard this section.
Last updated: May 04, 2012

2.1.1 Getting the source


Please visit http://code.google.com/p/or-tools/source/checkout to checkout the sources of ortools.

2.1.2 Content of the Archive


Upon untarring the given operation_research tar le, you will get the following structure:
or-tools/
Files/directories
LICENSE-2.0.txt
Makefile
README
bin/
dependencies/
examples/com/
examples/csharp/

Descriptions
Apache License
Main Makele
This le
Where all binary les will be created
Where third_party code will be downloaded and installed
Directory containing all java samples
Directory containing C# examples and a visual studio 2010
solution to build them
examples/cpp/
C++ examples
examples/python/
Python examples
examples/tests/
Unit tests
lib/
Where libraries and jar les will be created
makefiles/
Directory that contains sub-makeles
objs/
Where C++ objs les will be stored
src/algorithms/
A collection of OR algorithms (non graph related)
src/base/
Directory containing basic utilities
src/com/
Directory containing java and C# source code for the libraries
src/constraint_solver/ The main directory for the constraint solver library
src/gen/
The root directory for all generated code (java classes, protocol buffers, swig les)
src/graph/
Standard OR graph algorithms
src/linear_solver/
The main directory for the linear solver wrapper library
src/util/
More utilities needed by various libraries
tools/
Binaries and scripts needed by various platforms

26

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

2.1.3 Installation on unix platforms


For linux users, please intall zlib-devel, bison, ex, autoconf, libtool, python-setuptools, and
python-dev.
The command on ubuntu is the following:
sudo apt-get install bison flex python-setuptools python-dev autoconf \
libtool zlib-devel

The fedora command is:


sudo yum install subversion bison flex python-setuptools python-dev \
autoconf libtool zlib-devel

If you wish to use glpk, please download glpk from http://ftp.gnu.org/gnu/glpk/glpk-4.47.tar.gz


and put the archive in or-tools/dependencies/archives. If you have the license,
please download ziboptsuite-2.1.1.tgz from http://zibopt.zib.de/download.shtml and
put the archive in or-tools/dependencies/archives.
If you wish to use .NET, you need to install mono. On linux platforms, just install the
mono-devel package. You need a recent one (at least 2.8 I believe) to work correctly. If
mono is not supported on your platform, you can install it using the procedure for Mac OS X.
On Mac OS X, you need 64 bit support. Thus you need to build mono by hand. Copy
the mono archive http://download.mono-project.com/sources/mono/mono-2.10.8.1.tar.gz to
dependencies/archives. You can use dependencies/install/bin/gmcs to
compile C# les and dependencies/install/bin/mono to run resulting .exe les.
run:
make third_party
make install_python_modules

If
you
are
on
opensuse
and
maybe
redhat,
the
make
install_python_module will fail.
One workaround is described on this page
http://stackoverow.com/questions/4495120/combine-user-with-prex-error-with-setup-pyinstall.
If you have root privilieges, you can replace the last line and install the python modules for all
users with the following command:
cd dependencies/sources/google-apputils
sudo python2.7 setup.py install

It should create the Makefile.local automatically.


Please note that the command:
make clean_third_party

will clean all downloaded sources, all compiled dependencies, and Makefile.local. It is
useful to get a clean state, or if you have added an archive in dependencies.archives.

27

2.1. Getting started

2.1.4 Installation on Windows


Create the or-tools svn copy where you want to work.
Install python from http://www.python.org/download/releases/2.7/
Install java JDK from http://www.oracle.com/technetwork/java/javase/downloads/jdk-7u2download-1377129.html
You need to install python-setuptools for windows.
http://pypi.python.org/pypi/setuptools#les .

Please fetch it from

If you wish to use glpk, please download glpk from http://ftp.gnu.org/gnu/glpk/glpk-4.47.tar.gz


and put the archive in or-tools/dependencies/archives.
Then you can download all dependencies and build them using:
make third_party

then edit Makefile.local to point to the correct python and java installation. Afterwards,
to use python, you need to install google-apputils:
cd dependencies/sources/google-apputils
c:\python27\python.exe setup.py install

Please note that the command:


make clean_third_party

will clean all downloaded sources, all compiled dependencies, and Makefile.local. It is
useful to get a clean state, or if you have added an archive in dependencies.archives.

2.1.5 Running tests


You can check that everything is running correctly by running:
make test

If everything is OK, it will run a selection of examples from all technologies in C++, python,
java, and C#.

2.1.6 Compiling libraries and running examples


Compiling libraries
All build rules use make (gnu make), even on windows. A make.exe binary is provided in the
tools sub-directory.
You can query the list of targets just by typing
make

28

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

You can then compile the library, examples and python, java, and .NET wrappings for the
constraint solver, the linear solver wrappers, and the algorithms:
make all

To compile in debug mode, please use


make DEBUG=-g all

or
make DEBUG="/Od /Zi" all

under windows.
You can clean everything using:
make clean

When everything is compiled, you will nd under or-tools/bin and or-tools/lib:


Some static libraries (libcp.a, libutil.a and libbase.a, and more)
One binary per C++ example (e.g. nqueens)
C++ wrapping libraries (pywrapcp.so, linjniwrapconstraint_solver.so)
Java jars (com.google.ortools.constraintsolver.jar...)
C# assemblies
C++ examples
You can execute C++ examples just by running then:
./bin/magic_square

Python examples
For the python examples, as we have not installed the constraint_solver module, we need to use
the following command:
on windows:
set PYTHONPATH=%PYTHONPATH%;<path to or-tools>\src,

then
c:\Python27\python.exe python/sample.py.

On unix:
PYTHONPATH=src <python_binary> python/<sample.py>

As in
29

2.1. Getting started

PYTHONPATH=src python2.6 python/golomb8.py

There is a special target in the makele to run python examples. The above example can be run
with
make rpy EX=golomb8

Java examples
You can run java examples with the run_<name> makele target as in:
make run_RabbitsPheasants

There is a special target in the makele to run java examples. The above example can be run
with
make rjava EX=RabbitsPheasants

.NET examples
If you have .NET support compiled in, you can build .NET libraries with the command: make
csharp.
You can compile C# examples typing:
make csharpexe.

To run a C# example, on windows, just type the name


bin\csflow.exe

On unix, use the mono interpreter:


mono bin/csflow.exe

There is a special target in the makele to run C# examples. The above example can be run
with
make rcs EX=csflow

30

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

2.2 The cryptarithmetic puzzle problem and a rst


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 rst model to solve it. This model is by no means efcient but allows us a gentle introduction
to the library. A better model is presented in the corresponding lab session.

2.2.1 Description of the problem


A cryptarithmetic puzzle is a mathematical game where the digits of some numbers are represented by letters (or symbols). Each letter represents a unique digit. The goal is to nd the
digits such that a given mathematical equation is veried1 .
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.

2.2.2 How to solve the problem?


We follow the classical three-stage method described in section 1.5.
Describe
The rst 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 veried.
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
2

This the mathematical term to specify that the equation is true.


Like the famous SEND + MORE = MONEY ... in base 10.

31

2.2. The cryptarithmetic puzzle problem and a rst model

What are the constraints? The obvious constraint is the sum that has to be veried. But there
are other - implicit - constraints. First, two different letters represent two different digits. This
implies that all the variables must have different values in a feasible solution. Second, it is
implicit that the rst digit of a number can not be 0. Letters C, I, F and T can thus not represent
0. Third, there are 10 letters, so we need at least 10 different digits. The traditional decimal
base is sufcient but lets be more general and allow for a bigger base. We will use a constant
kBase. The fact that we need at least 10 digits is not really a CP constraint. After all, the base
is not a variable but a given integer that is chosen once and for all for the whole program3 .
Model
For each letter, we have a decision variable (we keep the same letters to name the variables).
Given a base b, digits range from 0 to b-1. Remember that variables corresponding to C, I, F
and T should be different from 0. Thus C, I, F and T have [1, b 1] as domain and P, S, U, N,
R and E have [0, b 1] as domain. Another possibility is to keep the same domain [0, b 1] for
all variables and force C, I, F and T to be different from 0 by adding inequalities. However,
restraining the domain to [1, b 1] is more efcient.
To model the sum constraint in any base b, we add the linear equation:

+
=

T 3

F 2

R 2

+
+
+

C + P
I + S
U + N

U +

The global constraint AllDifferent springs to mind to model that variables must all have
different values:
AllDifferent(C,P,I,S,F,U,N,T,R,E)

What is the AllDifferenta constraint?


The AllDifferent constraint enforces a set of variables to take distinct values. For
instance, the solution C=2 P=3 I=7 S=4 F=9 U=6 N=8 T=1 R=0 E=5 for our
cryptarithmetic puzzle satises the AllDifferent constraint as all the values taken are
pairwise different. There exist a variety of propagation algorithms for this constraint. The
one used in or-tools is bound based (see [Lopez-Ortiz2003]).
a

We talk here about the generic AllDifferent constraint.


MakeAllDifferent().

In or-tools, we use the method

Solve
At this stage of our discovery of the library, we will not try to nd a good search strategy
to solve this model. A default basic strategy will do for the moment. Chapter ?? is entirely
devoted to the subject of search strategies.
3

We could have chosen the base as a variable. For instance, to consider such a question as: What are the
bases for which this puzzle has less than x solutions?

32

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

2.3 Anatomy of a basic C++ code


In this section, we code the model developed in section 2.2. You can nd the code in the le
tutorials/cplusplus/chap2/cp_is_fun1.cc. We quickly scan through the code
and describe the basic constituents needed to solve the cryptarithmetic puzzle in C++. In the
next chapters, we will cover some of them in more details.

2.3.1 At a glance

33

2.3. Anatomy of a basic C++ code

2.3.2 Headers
To use the library, we need to include a few headers:
#include "base/logging.h"
#include "constraint_solver/constraint_solver.h"

The header logging.h is needed for some logging facilities and some assert-like macros.
The header constraint_solver.h is the main entry point to the CP solver and must be
included4 whenever you intend to use it.

2.3.3 The namespace operations_research


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

MakeBaseLine2, MakeBaseLine3 and MakeBaseLine4 are helper functions to create


the model. We detail these functions later in section 2.3.7 but for the moment, lets concentrate
on CPIsFun() where all the magic happens. It is called from the main5 function:
int main(int argc, char **argv) {
operations_research::CPIsFun();
4

Directly or indirectly when it is included in another header you include.


The main function does not lie inside the namespace operations_research, hence the use of the
operations_research identier to call the function CPIsFun().
5

34

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

return 0;
}

2.3.4 The CP solver


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 identication string. The Solver class has one
additional constructor covered in section 2.5.

2.3.5 Variables
To create the model, we rst need to create the decision variables:
const int64 kBase
IntVar* const c =
IntVar* const p =
...
IntVar* const e =

= 10;
solver.MakeIntVar(1, kBase - 1, "C");
solver.MakeIntVar(0, kBase - 1, "P");
solver.MakeIntVar(0, kBase - 1, "E");

For each letter, we create an integer variable IntVar whose domain is [0, kBase 1] except
for the variables c, i, f and t that cannot take the value 0. The MakeIntVar(i, j,
name) method is a factory method that creates an integer variable whose domain is [, ] =
{, +1, . . . , 1, } and has a name name. It returns a pointer to an IntVar. The declaration
IntVar* const c may seem a little be complicated at rst. It is easier to understand if read
from right to left: c is a constant pointer to an IntVar. We can modify the object pointed by
c but this pointer, because it is constant, always refers to the same object.
Factory methods in or-tools
The solver API provides numerous factory methods to create different objects. These
methods start with Make and return a pointer to the newly created object.
The solver automatically takes ownership of these objects and deletes them appropriately.
Never delete explicitly an object created by a factory method! First, the solver deletes
all the objects for you. Second, deleting a pointer twice in C++ gives undened
behavioura !
a

It is possible to bypass the undened behaviour but you dont know what the solver needs to do,
so keep your hands off of the object pointers! ;-)

Beside integer variables, the solver provides factory methods to create interval variables

35

2.3. Anatomy of a basic C++ code

(IntervalVar), sequence variables (SequenceVar) and variables to encapsulate objectives (OptimizeVar).

2.3.6 Assert-like macros


It is always a good idea to program defensively. We use several assert-like macros dened 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 program is aborted and the cause is printed:
[23:51:34] examples/cp_is_fun1.cc:108: Check failed:
(kBase) >= (letters.size())
Aborted

You can nd more about the assert-like macros in section 12.2.

2.3.7 Constraints
To create an integer linear constraint, we need to know how to multiply an integer variable with
an integer constant and how to add two integer variables. We have seen that the solver creates a
variable and only provides a pointer to that variable. The solver also provides factory methods
to multiply an integer coefcient by an IntVar given by a pointer:
IntVar* const var1 = solver.MakeIntVar(0, 1, "Var1");
// var2 = var1 * 36
IntVar* const var2 = solver.MakeProd(var1,36)->Var();

Note how the method Var() is called to cast the result of MakeProd() into a pointer to
IntVar. Indeed, MakeProd() returns a pointer to an IntExpr. The class IntExpr is a
base class to represent any integer expression.
Note also the order of the arguments MakeProd() takes: rst the pointer to an IntVar and
then the integer constant.
To add two IntVar given by their respective pointers, the solver provides again a factory
method:
//var3 = var1 + var2
IntVar* const var3 = solver.MakeSum(var1,var2)->Var();

36

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

Is the call to Var() really necessary?


Yes! Var() not only transforms a constraint into a variable but also a stateless expression
into a stateful and monotonic variable.
Variables are stateful objects that provide a rich API. On the other hand,
subclasses of BaseIntExpr represent range-only stateless objects.
That is,
MakeMin(MakeSum(A,B),a) is recomputed each time as MakeMin(A,a) +
MakeMin(B,a). Furthermore, sometimes the propagation on an expression is not complete. For instance, if A is an IntVar with domain [0..5], and B another IntVar
with domain [0..5] then MakeSum(A, B) has domain [0, 10]. If we apply
MakeMax(MakeSum(A, B), 4)) then we will deduce that both A and B will have
domain [0..4]. In that case, the max of MakeMax(MakeSum(A, B),4) is 8 and
not 4. To get back monotonicity, we need to cast the expression into a variable using the
Var() method: MakeMax(MakeSum(A, B),4)->Var(). The resulting variable is
stateful and monotonic.
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*.
To construct a sum, we use a combination of MakeSum() and MakeProd() factory
methods:
const int64 kBase = 10;
IntVar* const c = solver.MakeInt(1, kBase - 1, "C");
IntVar* const p = ...;
...
IntVar* const s = ...;
IntVar* const term1 = solver.MakeSum(solver.MakeProd(c,kBase),p)->Var();
IntVar* const term2 = solver.MakeSum(solver.MakeProd(i,kBase),s)->Var();

There is no need to cast the result of MakeProd(c,kBbase) into an IntVar because


MakeSum() takes two pointers to an IntExpr.
The combination of MakeSum() and MakeProd() can quickly become tedious. We use
helper functions to construct sums. For example, to construct the rst term of our cryptarithmetic puzzle "kBase c + p", we call MakeBaseLine2():
IntVar* const term1 = MakeBaseLine2(&solver, c, p, kBase);

The function MakeBaseLine2() is dened as follow:


IntVar* const MakeBaseLine2(Solver* s,
IntVar* const v1,
IntVar* const v2,
const int64 base) {
return s->MakeSum(s->MakeProd(v1, base), v2)->Var();
}

If the number of terms in the sum to construct is large, you can use MakeScalProd(). This
factory method accepts an std::vector of pointers to IntVars and an std::vector of
37

2.3. Anatomy of a basic C++ code

integer coefcients:
IntVar* const var1 = solver.MakeInt(...);
...
IntVar* const varN = solver.MakeInt(...);
std::vector<IntVar*> variables;
variables.push_back(var1);
...
variables.push_back(varN);
std::vector<int64> coefficients(N);
// fill vector with coefficients
...
IntVar* const sum = solver.MakeScalProd(variables, coefficients)->Var();

In the code, we use MakeScalProd() in the helper functions MakeBaseLine3() and


MakeBaseLine4().
To create the sum constraint, we use the factory method MakeEquality() that returns a
pointer to a Constraint object:
IntVar* const term1 = ...
IntVar* const term2 = ...
IntVar* const term3 = ...
IntVar* const sum_terms = solver.MakeSum(solver.MakeSum(term1,
term2),
term3)->Var();
IntVar* const sum = ...
Constraint* const sum_constraint = solver.MakeEquality(sum_terms, sum);

Finally, to add a constraint, we use the method AddConstraint():


solver.AddConstraint(sum_constraint);

In the code, we immediately add the constraint:


solver.AddConstraint(solver.MakeEquality(sum_terms, sum));

Adding the global AllDifferent constraint is a little bit easier because the solver provides
a factory method MakeAllDifferent(). This methods accepts an std::vector of
IntVar*:
std::vector<IntVar*> letters;
letters.push_back(c);
letters.push_back(p);
...
letters.push_back(e);
solver.AddConstraint(solver.MakeAllDifferent(letters));

38

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

2.3.8 The Decision Builder


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 rst parameter of the method MakePhase is an std::vector with pointers to the
IntVar decision variables. The second parameter species how to choose the next IntVar
variable to be selected in the search. Here we choose the rst unbounded variable. The third
parameter indicates what value to assign to the selected IntVar. The solver will assign the
smallest available value.

2.3.9 The search and the solutions


To prepare for a new search:
DecisionBuilder* const db = ...
solver.NewSearch(db);

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
}

We print out the found solution and check if it is valid6 :


if (solver.NextSolution()) {
LOG(INFO) << "Solution found:";
LOG(INFO) << "C=" << c->Value()
<< "I=" << i->Value()
<< "F=" << f->Value()
<< "N=" << n->Value()
<< "R=" << r->Value()

<<
<<
<<
<<
<<

"
"
"
"
"

"
"
"
"
"

<<
<<
<<
<<
<<

"P="
"S="
"U="
"T="
"E="

<<
<<
<<
<<
<<

p->Value() <<
s->Value() <<
u->Value() <<
t->Value() <<
e->Value();

"
"
"
"

"
"
"
"

// Is CP + IS + FUN = TRUE?
CHECK_EQ(p->Value() + s->Value() + n->Value() +
kBase * (c->Value() + i->Value() + u->Value()) +
kBase * kBase * f->Value(),
e->Value() +
kBase * u->Value() +
6

Actually and contrary to the intuition, NextSolution() doesnt return a feasible solution per se. It all
depends of the involved DecisionBuilder. The solver considers any leaf of the search tree as a solution if it
doesnt fail (i.e. if it is accepted by several control mechanisms). See the section Basic working of the solver: the
search algorithm for more details.

39

2.4. SolutionCollectors and Assignments to collect solutions

kBase * kBase * r->Value() +


kBase * kBase * kBase * t->Value());
} else {
LOG(INFO) << "Cannot solve problem.";
} // if (solver.NextSolution())

The output is:


$[23:51:34] examples/cp_is_fun1.cc:132: Solution found:
$[23:51:34] examples/cp_is_fun1.cc:133: C=2 P=3 I=7 S=4 F=9 U=6 N=8 T=1
R=0 E=5

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
}

2.3.10 The end of the search


To nish the search, invoke:
solver.EndSearch();

This method ensures that the solver is ready for a new search and if you asked for a prole le,
this le is saved. You can nd more about the prole le in section 2.5.2. What happens if
you forget to end the search and didnt ask for a prole le? If you dont ask the solver to start
a new search, nothing bad will happen. It is just better practice to nish the search with the
method EndSearch().
See also What is the difference between NewSearch() and Solve()?.

2.4 SolutionCollectors and Assignments to collect


solutions
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.
You can nd the code in the le tutorials/chap2/cplusplus/cp_is_fun2.cc.

40

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

2.4.1 SolutionCollectors
The SolutionCollector class is one of several specialized SearchMonitors classes.
i.e. SolutionCollector inherits from SearchMonitors. SearchMonitors provides a set of callbacks to monitor all search events. We will learn more about them in the next
chapter.
To collect solutions, several SolutionCollector are available:
FirstSolutionCollector: to collect the rst solution of the search;
LastSolutionCollector: to collect the last solution of the search;
BestValueSolutionCollector: to collect the best solution of the search;
AllSolutionCollector: to collect all solutions of the search.
The solver provides corresponding factory methods:
MakeFirstSolutionCollector();
MakeLastSolutionCollector();
MakeBestValueSolutionCollector();
MakeAllSolutionCollector().
The simplest way to use a SolutionCollector is to use it as is without any parameter.
This can be handy if you are only interested in global results such as the number of solutions:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
...
DecisionBuilder* const db = ...
...
solver.NewSearch(db, all_solutions);
while (solver.NextSolution()) {};
solver.EndSearch();
LOG(INFO) << "Number of solutions: " << all_solutions->solution_count();

Instead of using NewSearch(), NextSolution() repeatedly and EndSearch(), you


can use the Solve() method:
solver.Solve(db,all_solutions);

In case you are curious about the number of solutions, there are 72 of them in base 10.
To effectively store some solutions in a SolutionCollector, you have to add the variables
you are interested in. Lets say you would like to know what the value of variable c is in the
rst solution found. First, you create a SolutionCollector:
FirstSolutionCollector* const first_solution =
solver.MakeFirstSolutionCollector();

Then you add the variable you are interested in to the SolutionCollector:

41

2.4. SolutionCollectors and Assignments to collect solutions

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)

or through the shortcut:


first_solution->Value(0,c)

In both cases, the index 0 denotes the rst solution found. If you nd it odd to specify the
index of the rst solution with a FirstSolutionCollector, dont forget that the API is
intended for generic SolutionCollectors including the AllSolutionCollector.
Lets use the AllSolutionCollector to store and retrieve the values of the 72 solutions:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the variables to the SolutionCollector
all_solutions->Add(letters);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);
// Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl;
for (int index = 0; index < number_solutions; ++index) {
LOG(INFO) << "Solution found:";
LOG(INFO) << "C=" << all_solutions->Value(index,c) << " "
<< "P=" << all_solutions->Value(index,p) << " "
...
<< "E=" << all_solutions->Value(index,e);
}

You are not limited to the variables of the model. For instance, lets say you are interested to
know the value of the expression kBase * c + p. Just construct a corresponding variable
and add it to the SolutionCollector:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the interesting variables to the SolutionCollector
all_solutions->Add(c);
all_solutions->Add(p);
// Create the variable kBase * c + p
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var();

42

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

// Add it to the SolutionCollector


all_solutions->Add(v1);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);
// Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl;
for (int index = 0; index < number_solutions; ++index) {
LOG(INFO) << "Solution found:";
LOG(INFO) << "v1=" << all_solutions->Value(index,v1);
}

2.4.2 Assignments
The or-tools library provides the class Assignment to store the solution (in parts or as a
whole). The class Assignment has a rich API that allows you to retrieve not only the values
of the variables in a solution but also additional information. You can also act on some of the
variables for instance to disable them during a search. We will see this class in more details in
chapter XXX.
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the interesting variables to the SolutionCollector
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var();
// Add it to the SolutionCollector
all_solutions->Add(v1);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);
// Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl;
for (int index = 0; index < number_solutions; ++index) {
Assignment* const solution = all_solutions->solution(index);
LOG(INFO) << "Solution found:";
LOG(INFO) << "v1=" << solution->Value(v1);
}

In section 12.6, well use it to serialize a solution.

43

2.5. Parameters

What is the difference between NewSearch() and Solve()?


Depending on the search, Solve() is equivalent to either
solver.NewSearch();
solver.NextSolution();
solver.EndSearch();

or
solver.NewSearch();
while (solver.NextSolution()) {...};
solver.EndSearch();

With NewSearch() you can access the variables of the current solutions (no need for a
SolutionCollector). More importantly, you can interfere with the search.

2.5 Parameters
This section is divided in two parts. First, we show you how to use Googles command line
ag library. Second, we explain how to pass parameters to the CP solver.

2.5.1 Googles gags


The Googles ags library is quite similar to other command ags libraries with the noticeable
difference that the ag denitions may be scattered in different les.
To dene a ag, we use the corresponding macro. Googles ags 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 ag, its default value, and
a help string. In le tutorials/cplusplus/chap2/cp_is_fun3.cc, we parse the
base value on the command line. We rst include the corresponding header and dene the ag
base in the global namespace:
...
#include "base/commandlineflags.h"
...
DEFINE_int64(base, 10, "Base used to solve the problem.");
...
namespace operations_research {
...

44

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

and then parse the command line:


int main(int argc, char **argv) {
google::ParseCommandLineFlags(&argc, &argv, true);
operations_research::CPIsFun();
return 0;
}

Note that argc and argv are passed as pointers so that ParseCommandLineFlags() is
able to modify them.
All dened ags are accessible as normal variables with the prex FLAGS_ prepended:
const int64 kBase = FLAGS_base;

To change the base with a command line argument:


./cp_is_fun4 --base=12

If you want to know what the purpose of a ag is, just type one of the special ags on the
command line:
--help: prints all the ags
--helpshort: prints all the ags dened in the same le as main()
--helpon=FILE: prints all the ags dened in le FILE
--helpmatch=S: prints all the ags dened in the les *S*.*
For other features and to learn more about this library, we refer you to the gags documentation.

2.5.2 CP Solvers parameters


Youll nd the code in the le tutorials/chap2/cplusplus/cp_is_fun4.cc.
Parameters can be transferred to the solver in several ways.
The SolverParameters struct
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;
// Constraint programming engine
Solver solver("CP is fun!", solver_params);

We can now ask for a detailed report after the search is done:
// Save profile in file
solver.ExportProfilingOverview("profile.txt");

45

2.6. Other supported languages

We will see how to prole more in details in the section 12.4. The SolverParameters
struct mainly deals with the internal usage of memory and is for advanced users.
SearchMonitors
Second, you can use SearchMonitors. We have already seen how to use them to collect
solutions in section 2.4. Suppose we want to limit the available time to solve a problem. To
pass this parameter on the command line, we dene a time_limit variable:
DEFINE_int64(time_limit, 10000, "Time limit in milliseconds");

Since SearchLimit inherits from SearchMonitor, Solve() accepts it:


SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
...
// Add time limit
SearchLimit* const time_limit = solver.MakeTimeLimit(FLAGS_time_limit);
solver.Solve(db, all_solutions, time_limit);

The search time is now limited to time_limit milliseconds.


The DefaultPhaseParameters struct
A third way is to pass parameters through the DefaultPhaseParameters struct but we
delay the discussion of this topic until the chapter ??.

2.6 Other supported languages


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 efciency and your problem
is really difcult, 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 avors with the following naming convention:

46

Chapter 2. First steps with or-tools: cryptarithmetic puzzles

Naming convention for methods in or-tools


A C++ method named MYMethod() becomes:
MyMethod() in Python (pascal case);
myMethod() in Java (camel case);
MyMethod() in C# (pascal case).
Methods
with
_
(underscore)
like
objective_value()
ObjectiveValue() or objectiveValue().

become

2.7 Summary
summary

47

CHAPTER

THREE

USING OBJECTIVES IN CONSTRAINT


PROGRAMMING: THE GOLOMB RULER PROBLEM

In this chapter, we are not only looking for a feasible solution but we want the best solution!
Most of the time, the search is done in two steps. First, we nd the best solution1 . Second, we
prove that this solution is indeed the best (or as good as any other feasible solution in case there
are multiple optimal solutions) by scouring (preferably implicitly) the complete search tree.
Overview:

We start by stating the Golomb Ruler Problem (GRP) and showing that this problem is difcult. We implement ve models and compare them two by two. To do so, we introduce some
basic statistics about the search (time, failures, branches, ...). Two very useful techniques are
introduced: adding better bounds and breaking symmetries. Finally, we say a few words about
the strategies used by the solver to optimize an objective function.
Prerequisites:

Basic knowledge of C++.


Basic knowledge of Constraint Programming (see chapter 1).
Basic knowledge of the Constraint Programming Solver (see chapter 2).
Remarks:

The sums used in this chapter to model the GRP are tricky but you dont need to master
them. We do all the dirty work for you. In fact, you can completely skip them if you
wish. The basic ideas behind these sums are simple and are easy to follow.
We introduce two kinds of variables in our modelizations: the marks of the ruler and the
differences between the marks.
1

How do we know we have a best solution? Only when we have proven it to be so! The two steps are
intermingled. So why do we speak about two steps? Because, most of the time, it is easy to nd a best (good)
solution (heuristics, good search strategies in the search tree, ...). The time-consuming part of the search consist
in disregarding/visiting the rest of the search tree.

3.1. Objective functions and how to compare search strategies

Classes under scrutiny:


Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap3.


The les inside this directory are:
Makefile.
golomb1.cc: A rst implementation. We show how to tell the solver to optimize an
objective function. We use the (1) differences as variables.
2
golomb2.cc: Same le as golomb1.cc but with some global statistics about the
search added so we can see how well or bad a model behave.
golomb3.cc: A second implementation. This time, we only use the marks as variables
and introduce the quaternary inequality constraints.
golomb4.cc: We improve the second implementation by reintroducing the
ferences variables.

(1)
2

dif-

golomb5.cc: In this third implementation, we replace the inequality constraints by the


more powerful globlal AllDifferent constraint.
golomb6.cc: The last implementation is a tightening of the model used in the third
implementation. We add better upper and lower bounds and break a symmetry in the
search tree.
In all the codes, we use the same strategy to select the next variable to branch
on (CHOOSE_FIRST_UNBOUND) and the same strategy to assign it a value
(ASSIGN_MIN_VALUE). The times we compare not only measure the solve process
but also the time needed to construct the model.

3.1 Objective functions and how to compare search


strategies
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.
The objective function to minimize is the length of the Golomb ruler. You can see the objective
function as a variable: you want to nd out what is the minimum or maximum value this
variable can hold for any given feasible solution.
Dont 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 differ2

To be honest, if you really want to solve the Golomb Ruler Problem, you shouldnt use CP as, until now, no
one found how to use CP in an efcient manner to solve this difcult problem.

50

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem
ent models and search strategies 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.
branches: How many times do we need to branch? Faster algorithms tend to visit fewer
branches.

3.2 The Golomb ruler problem and a rst model


The Golomb Ruler Problem (GRP) is one of these problems that are easy to state but that are
extremely difcult to solve despite their apparent simplicity.
In this section, we describe the problem and propose a rst model to solve it. This model is not
very efcient and we will develop better models in the next sections.

3.2.1 Description of the problem


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

11
4

7
9
11

Figure 3.1: A non optimal Golomb ruler of order 4.


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 x the rst mark to 0. Figure 3.2 illustrates an optimal
Golomb ruler of order 4 and all its - distinct - differences.
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 (4) = 6. More generally, for a Golomb ruler
of order , we denote by () its optimal value. The Golomb Ruler Problem (GRP) is to nd,
for a given order , the smallest Golomb ruler with 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!

51

3.2. The Golomb ruler problem and a rst model


0

6
2

4
5
6

Figure 3.2: An optimal Golomb ruler of order 4.


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 nd and prove (24), (25) and (26)345 .
Orders
24
25
26

Days
1,572
3,006
24

Participants
41,803
124,387
2754

Visited nodes
555,551,924,848,254,200
52,898,840,308,130,480,000
3,185,174,774,663,455

The search for (27) started on February 24, 2009 and at that time was expected to take... 7
years! Still think it is an easy6 problem? You too can participate: The OGR Project.
You can nd all the known optimal Golomb rulers and more information on Wikipedia.
Why Golomb Rulers?
Golomb rulers have a wide variety of applications, including radio astronomy and information theory. In radio astronomy, when constrained to be lined up, telescopes collect more
accurate information if they are placed on the marks of a Golomb ruler. In information
theory, Golomb rulers are used for error detection and correction.

3.2.2 How to solve the 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 Golomb Ruler Problem? To nd a minimal Golomb ruler for a given
order . 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)
3

http://stats.distributed.net/projects.php?project_id=24
http://stats.distributed.net/projects.php?project_id=25
5
http://stats.distributed.net/projects.php?project_id=26
6
Although it is strongly suspected that the Golomb Ruler Problem is a very difcult problem, the computational complexity of this problem is unknown (see [Meyer-Papakonstantinou]).
4

52

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem

or choose the unknowns to be the differences (and retrieve the marks). Lets try this second
approach and use the efcient AllDifferent constraint. There are (1) such differences.
2
What are the constraints? Using the differences as variables, we need to construct a Golomb
ruler, i.e. the structure of the Golomb ruler has to be respected (see next section).
Model
For each positive difference, we have a decision variable. We collect them in an array . Lets
order the differences so that we know which difference is represented by []. Figure 3.3
illustrates an ordered sequence of differences for a Golomb ruler of order 4.
1st

2nd
4

3rd

th

5th
6th

Figure 3.3: An ordered sequence of differences for the Golomb ruler of order 4.
We want to minimize the last difference in i.e. [ (1) 1] since the rst index of an array
2
is 0. When the order is = 4, we want to optimize [ 4(41) 1] = [5] which represents the
2
6th difference. Instead of writing [], we will also use the more convenient notation .
Figure 3.4 illustrates the structures than must be respected for a Golomb ruler of order 5. To impose the inner structure of the Golomb Ruler, we force 4 = 0 + 1 , 5 = 1 + 2
and so on as illustrated in Figure 3.4.
index
Y0

Y1

Y2

Y3

i=2

Y4
Y5

i=3
Y6
i=4

Y7

Y4
Y5
Y6

=
=
=

Y0

Y7
Y8

=
=

Y0

Y9

Y0

+
+

Y1
Y1

Y2
Y2

Y3

Y1
Y1

+ Y2
+ Y2

Y3

Y1

Y3

Y2

j=0
j=1
j=2
j=0
j=1
j=0

Y8
Y9

Figure 3.4: The inner structure of a Golomb ruler of order 5.


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:
7

Or more generally from the index of(the rst difference that is the sum of two differences in our sequence
)
( 1) to the index of the last difference (1) 1 .
2

53

3.3. An implementation of the rst model

int index = n - 2;
for (int i = 2; i <= n - 1; ++i) {
for (int j = 0; j < n-i; ++j) {
++index;
Y[index] = Y[j] + ... + Y[j + i - 1];
}
}

Solve
Again, at this stage of our discovery of the library, we will not try to nd a good search strategy
to solve this model. A default basic strategy will do for the moment. The next chapter 5 is
entirely devoted to the subject of search strategies.

3.3 An implementation of the rst model


In this section, we code the rst model developed in section 3.2. You can nd the code in the
le tutorials/cplusplus/chap3/golomb1.cc. We take the order (the number of
marks) from the command line:
DEFINE_int64(n, 5, "Number of marks.");

3.3.1 An upper bound


Several upper bounds exist on Golomb rulers. For instance, we could take 3 22 + 2 1.
Indeed, it can be shown that the sequence
() = 2 +

< .

forms a Golomb ruler. As its largest member is 3 22 + 2 1 (when = 1), we have


an upper bound on the length of a Golomb ruler of order :
()

3 22 + 2 1.

Most bounds are really bad and this one isnt an exception. The great mathematician Paul Erds
conjectured that
() < 2 .
This conjecture hasnt been proved yet but computational evidence has shown that the conjecture holds for < 65000 (see [Dimitromanolakis2002]).
This is perfect for our needs:
CHECK_LT(n, 65000);
const int64 max = n * n - 1;

54

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem

3.3.2 The rst model


We can now dene 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));
}

we use the MakeIntVarArray() method:


const int64 num_vars = (n*(n - 1))/2;
std::vector<IntVar*> Y;
s.MakeIntVarArray(num_vars, 1, max, "Y_", &Y);

Note that these two methods dont provide the same result! MakeIntVarArray()
appends num_vars IntVar* to the std::vector with names Y_i where i
goes from 0 to num_vars - 1.
It is a convenient shortcut to quickly create an std::vector<IntVar*> (or to append some IntVar*s to an existing
std::vector<IntVar*>).
StringPrintf() (shown in the rst example) is a helper function declared in the header
base/stringprintf.h that mimics the C function printf().
We use the AllDifferent constraint to ensure that the differences (in Y) are distinct:
s.AddConstraint(s.MakeAllDifferent(Y));

and the following constraints to ensure the inner structure of a Golomb ruler as we have seen
in the previous section8 :
int index = n - 2;
IntVar* v2 = NULL;
for (int i = 2; i <= n - 1; ++i) {
for (int j = 0; j < n-i; ++j) {
++index;
v2 = Y[j];
for (int p = j + 1; p <= j + i - 1 ; ++p) {
v2 = s.MakeSum(Y[p], v2)->Var();
}
s.AddConstraint(s.MakeEquality(Y[index], v2));
}
}
CHECK_EQ(index, num_vars - 1);

How do we tell the solver to optimize? Use an OptimizeVar to declare the objective function:
OptimizeVar* const length = s.MakeMinimize(Y[num_vars - 1], 1);

and give the variable length to the Solve() method:


8

Remember the remark at the beginning of this chapter about the tricky sums!

55

3.4. What model did I pass to the solver?

s.Solve(db, collector, length);

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);

3.4 What model did I pass to the solver?


The model we developed to solve the cryptarithmetic puzzle in section 2.2 was quite simple.
The rst 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 section, we present two tools to debug the model we passed to the solver: the DebugString()
method and via the default command line ags of the CP solver.

3.4.1 Inspect objects with DebugString()


You can nd the code in the le tutorials/cplusplus/chap3/golomb2.cc. Most
of the mathematical classes in or-tools inherit from the BaseObject class. Its only public
method is a virtual DebugString(). If you are curious or just in doubt about the object
you just constructed, DebugString() is for you.
Lets have a closer look at the constraints that model the inner structure of the Golomb ruler of
order 5:
const int n = 5;
...
for (int i = 2; i <= n - 1; ++i) {
for (int j = 0; j < n-i; ++j) {
...
c = s.MakeEquality(Y[index], v2);
s.AddConstraint(c);
LOG(INFO) << c->DebugString();
}
}

The output is:


...:
...:
...:
...:

Y_4(1..24)
Y_5(1..24)
Y_6(1..24)
Y_7(1..24)

==
==
==
==

Var<(Y_1(1..24)
Var<(Y_2(1..24)
Var<(Y_3(1..24)
Var<(Y_2(1..24)

+
+
+
+

Y_0(1..24))>(2..48)
Y_1(1..24))>(2..48)
Y_2(1..24))>(2..48)
Var<(Y_1(1..24) +
Y_0(1..24))>(2..48))>(3..72)
...: Y_8(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) +
Y_1(1..24))>(2..48))>(3..72)
...: Y_9(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) +
Var<(Y_1(1..24) + Y_0(1..24))>(2..48))>(3..72))>(4..96)

These are exactly the constraints listed in Figure 3.4 page 53.
56

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem

3.4.2 Use the default ags


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 ags.


constraint_solver.cc, these are:

For the le

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

We are interested in the constraints. Invoking


./golomb1 --n=5 --cp_no_solve --cp_show_constraints

gives us:
...: BoundsAllDifferent(Y_0(1..24), Y_1(1..24), Y_2(1..24), Y_3(1..24),
Y_4(1..24), Y_5(1..24), Y_6(1..24), Y_7(1..24), Y_8(1..24),
Y_9(1..24))

This is the AllDifferent constraint on bounds where we see all the variables with their
initial domains.
Then:
...: cast((Y_1(1..24) + Y_0(1..24)), Var<(Y_1(1..24) + Y_0(1..24))>
(2..48))

The cast to transform the sum 1 + 0 into an IntVar.


And then:

57

3.4. What model did I pass to the solver?

...:
...:
...:
...:

Y_4(1..24)
Y_5(1..24)
Y_6(1..24)
Y_7(1..24)

==
==
==
==

Var<(Y_1(1..24)
Var<(Y_2(1..24)
Var<(Y_3(1..24)
Var<(Y_2(1..24)

+
+
+
+

Y_0(1..24))>(2..48)
Y_1(1..24))>(2..48)
Y_2(1..24))>(2..48)
Var<(Y_1(1..24) +
Y_0(1..24))>(2..48))>(3..72)
...: Y_8(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) +
Y_1(1..24))>(2..48))>(3..72)
...: Y_9(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) + Var<(Y_1(1..24) +
Y_0(1..24))>(2..48))>(3..72))>(4..96)
...: Forcing early failure
...: Check failed: (collector->solution_count()) == (1)
Aborted

All this output was generated from the following line in constraint_solver.cc:
LOG(INFO) << c->DebugString();

where c is a pointer to a Constraint.


Invoking
./golomb1 --n=5 --cp_no_solve --cp_model_stats

we obtain some statistics about the model:


...: Model has:
...:
- 17 constraints.
...:
* 1 AllDifferent
...:
* 6 Equal
...:
* 10 CastExpressionIntoVariable
...:
- 20 integer variables.
...:
- 10 integer expressions.
...:
* 10 Sum
...:
- 10 expressions casted into variables.
...:
- 0 interval variables.
...:
- 0 sequence variables.
...:
- 2 model extensions.
...:
* 1 VariableGroup
...:
* 1 Objective

Indeed, we have 1 AllDifferent constraint, 6 equality constraints and 10 IntVar variables. Where does the rest come from?
To construct the equality constraints, we cast 10 times integer expressions into IntVar (remember the ...->Var() calls), hence the 10 integer expressions, the 10 supplementary
IntVar variables and the 10 sums. The 2 model extensions are the objective OptimizeVar
variable and the std::vector array of IntVar variables (VariableGroup).
Try the other ags!

58

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem

3.5 Some global statistics about the search and how to


limit the search
In section 3.1, we talked about some global statistics about the search. In this section we
review them one by one.
You can nd the code in the le tutorials/cplusplus/chap3/golomb2.cc.

3.5.1 Time
This is probably the most common statistic. There exist several timing libraries or tools to
measure the duration of an algorithm. The or-tools library offers a basic but portable timer.
This timer starts to measure the time from the creation of the solver.
solver("TicTac") s;

//

Starts the timer of the solver.

If you need the elapsed time since the creation of the timer, just call wall_time():
const int64 elapsed_time = s.wall_time();

The time is given in milliseconds. If you only want to measure the time spent to solve the
problem, just subtract times:
const int64 time1 = s.wall_time();
s.Solve(...);
const int64 time2 = s.wall_time();
LOG(INFO) << "The Solve method took " << (time2 - time1)/1000.0 <<
" seconds";

As its name implies, the time measured is the wall time, i.e. it is the difference between the
time at which a task nishes and the time at which the task started and not the actual time spent
by the computer to solve a problem.
For instance, on our computer, the program in golomb1.cc for = 9 takes
Time: 4,773 seconds

59

3.6. A second model and its implementation

3.5.2 Failures
3.5.3 Branches
3.5.4 SearchLimits

3.6 A second model and its implementation


Our rst model is really bad. One of the reasons is that we use too many variables: (1) dif2
ferences. What happens if we only consider the marks as variables instead of the differences?

3.6.1 Variables
You can nd the code in the le tutorials/cplusplus/chap3/golomb3.cc. Before
we dive into the code, lets be practical and ease our life a bit. One of the difculties of the code
in golomb1.cc is that we use the rst element of the array Y. There is no need to do so. In
golomb3.cc, we use X[1] as the rst mark (and not X[0]). In the same vain, we redene
the array kG such that kG(n) = G(n) (and not kG(n-1) = G(n)). Thus:
std::vector<IntVar*> X(n + 1);
X[0] = s.MakeIntConst(-1); // The solver doesnt allow NULL pointers
X[1] = s.MakeIntConst(0);
// X[1] = 0

We use an std::vector slightly bigger (by one more element) than absolutely necessary.
Because the solver doesnt allow NULL pointers, we have to assign a value to X[0]. The rst
mark X[1] is 0. We use again 2 1 as an upper bound for the marks:
// Upper bound on G(n), only valid for n <= 65 000
CHECK_LE(n, 65000);
const int64 max = n * n - 1;
...
for (int i = 2; i <= n; ++i) {
X[i] = s.MakeIntVar(1, max, StringPrintf("X%03d", i));
}

This time we dont use MakeIntVarArray() because we want a better control on the names
of the variables.

3.6.2 Constraints
To express that all the differences between all pairs of marks must be distinct, we use the
quaternary constraints9 :
[] [] = [] []
9

60

, , ,

Quaternary constraints is just a fancy way to say that the constraints each involves four variables.

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem
We dont need all combinations of (, , , ) with = and = . 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 differences10 . Take again = 4 and
dene the sequence of differences as in Figure 3.5.
1st
2nd
3rd
4th
5th
6th

Figure 3.5: Another ordered sequence of differences for the Golomb ruler of order 4.
With this order dened on the differences, we can easily generate all the quaternary constraints.
Take the rst difference and impose it to be different from the second difference, then to be
different from the third difference and so on as suggested in Figure 3.6. Take the second
1st

2nd

=
=
=
=
=

3rd
4th
5th
6th

Figure 3.6: How to generate the quaternary constraints, part I.


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

=
=
=
=

3rd
4th
5th
6th

Figure 3.7: How to generate the quaternary constraints, part II.


We dene a helper function that, given a difference (, ) corresponding to an interval []
[] 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);
10

In section Breaking symmetries with constraints well use another trick.

61

3.6. A second model and its implementation

CHECK_GE(i, 1);
CHECK_GT(j, 1);
if (j == n) {
if (i == n - 1) {
return false;
} else {
*next_i = i + 1;
*next_j = i + 2;
}
} else {
*next_i = i;
*next_j = j + 1;
}
return true;
}

If there is a next interval, the function next_interval() returns true, false otherwise.
We can now construct our quaternary constraints11 :
IntVar* diff1;
IntVar* diff2;
int k, l, next_k, next_l;
for (int i = 1; i < n - 1; ++i) {
for (int j = i + 1; j <= n; ++j) {
k = i;
l = j;
diff1 = s.MakeDifference(X[j], X[i])->Var();
diff1->SetMin(1);
while (next_interval(n, k, l, &next_k, &next_l)) {
diff2 = s.MakeDifference(X[next_l], X[next_k])->Var();
diff2->SetMin(1);
s.AddConstraint(s.MakeNonEquality(diff1, diff2));
k = next_k;
l = next_l;
}
}
}

Note that we set the minimum value of the difference to 1, diff1->SetMin(1), to ensure
that the differences are positive and
1. Note also that the method MakeDifference()
doesnt allow us to give a name to the new variable, which is normal as this new variable is the
difference of two existing variables. Its name is simply name1 - name2.
Lets compare the rst and second implementation. The next table compares some global
statistics about the search for (9).
11

62

Remember again the remark at the beginning of this chapter about the tricky sums.

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem
Statistics
Time (s)
Failures
Branches
Backtracks

Impl1
4,712
51 833
103 654
51 836

Impl2
48,317
75 587
151 169
75 590

If the rst model was bad, what can we say about this one? What went wrong? The quaternary
constraints... These constraints are all disparate and thus dont allow efcient propagation.

3.6.3 An improved version


You can nd the code in the le tutorials/cplusplus/chap3/golomb4.cc. Lets
improve our second model by using variables to denote the differences and dene variables
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();
Y[i][j]->SetMin(1);
}
}

Then we can use the Y variables in the equality constraints:


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;
while (next_interval(n, k, l, &next_k, &next_l)) {
s.AddConstraint(s.MakeNonEquality(Y[i][j],Y[next_k][next_l]));
k = next_k;
l = next_l;
}
}
}

and compare this improved version with the two others, again to compute (9):
Statistics
Time (s)
Failures
Branches
Backtracks

Impl1
4,712
51 833
103 654
51 836

Impl2
48,317
75 587
151 169
75 590

Impl2+
1,984
53 516
107 025
53 519

Although we have more failures, more branches and we do backtrack more than in the rst
model, we were able to divide the time by 2! Can we do better? You bet!

63

3.7. A third model and its implementation

3.7 A third model and its implementation


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.
You can nd the code in the le tutorials/cplusplus/chap3/golomb5.cc.
Lets replace the quaternary constraints by the AllDifferent constraint on the Y variables:
std::vector<IntVar*> Y;
for (int i = 1; i <= n; ++i) {
for (int j = i + 1; j <= n; ++j) {
IntVar* const diff = s.MakeDifference(X[j], X[i])->Var();
Y.push_back(diff);
diff->SetMin(1);
}
}
s.AddConstraint(s.MakeAllDifferent(Y));

and compare our three implementations, again to compute (9):


Statistics
Time (s)
Failures
Branches
Backtracks

Impl1
4,712
51 833
103 654
51 836

Impl2
48,317
75 587
151 169
75 590

Impl2+
1,984
53 516
107 025
53 519

Impl3
0,338
7 521
15 032
7 524

What an improvement! In the next section, we present two strategies that generally allow to
tighten a given model and thus improve, sometimes dramatically, the search time.

3.8 How to tighten the model?


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 nd good nodes in the
search tree). We present two12 such techniques here. Breaking symmetries allows to disregard
entire subtrees in the search tree that wouldnt bring any new information to the search
while bounding reduces the variable domains and thus reduces the number of branching and
augments the efciency of the propagation techniques13 .
You can nd the code in the le tutorials/cplusplus/chap3/golomb6.cc.
12

There exist other techniques. Later, in section XXX, we will see how over-constraining can improve the
search.
13
This short explanation is certainly too simple to describe all the subtleties of search strategies. After all,
modelling is an art!

64

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem

3.8.1 Breaking symmetries with constraints


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));
k = next_k;
l = next_l;
}
}
}

In trying to avoid duplicating certain quaternary constraints, we actually declared implicitly to


the solver that X[1] < X[2] < ... < X[n]. Hadnt we done so, there was no way the
solver could have guessed that the marks are in an increasing sequence14 . For the solver, the
solution
[1] = 0, [2] = 1, [3] = 4, [4] = 6

(3.1)

[1] = 4, [2] = 1, [3] = 6, [4] = 0

(3.2)

and the solution

would have been two different solutions and we would explicitly have had to tell the solver not
to generate the second one:
for (int i = 1; i < n; ++i) {
s.AddConstraint(s.MakeLess(X[i],X[i+1]));
}

Thanks to diff1->SetMin(1) and diff2->SetMin(1) and the two for loops, the
ordered variables [1], [2], [3], [4] have only increasing values, i.e. if
then []
[]. Solutions (3.1) and (3.2) are said to be symmetric and avoiding the second one while
accepting the rst one is called breaking symmetry.
There is a well-known symmetry in the Golomb Ruler Problem that we didnt break. Whenever
you have a Golomb ruler, there exist another Golomb ruler with the same length that is called
the mirror ruler. Figure 3.8 illustrates two mirror Golomb rulers of order 4.
14

Declaring variables in an std::vector doesnt tell anything about their respective values!

65

3.8. How to tighten the model?


0

Figure 3.8: Two mirror Golomb rulers of order 4.


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 rst 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 y during the search!

3.8.2 Better bounds helps


In all implementations, we used 2 1 as an upper bound on (). In the case of the Golomb
Ruler Problem, nding good upper bounds is a false problem. Very efcient techniques exist
to nd optimal or near optimal upper bounds. If we use those bounds, we reduce dramatically
the domains of the variables. We can actually use () as an upper bound for 25 as these
bounds can be obtained by projective and afne projections in the plane15 .
The search can also benet 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)
}
}
}

where kG[n] is ().


The AllDifferent constraint doesnt take a 2-dimensional array as parameter but it is easy
to create one by attening the array:
15

These transformations were discovered in the beginning of the 20th century without any computer! See
http://www.research.ibm.com/people/s/shearer/grtab.html.

66

Chapter 3. Using objectives in constraint programming: the Golomb ruler


problem

Constraint * AllDifferent(Solver* s,
const std::vector<std::vector<IntVar *> > & vars) {
std::vector<IntVar*> vars_flat;
for (int i = 0; i < vars.size(); ++i) {
for (int j = 0; j < vars[i].size(); ++j) {
if (vars[i][j] != NULL) {
vars_flat.push_back(vars[i][j]);
}
}
}
return s->MakeAllDifferent(vars_flat);
}

These are static bounds, i.e. they dont change during the search. Dynamic bounds are even
better as they improve during the search and tighten the domains even more.
For instance, note that
[1][2] + [2][3] + ... + [][] + ... + [ 1][] = []
so
[][] = [] { [1][2] + [2][3] + ... + [ 1][] + [][ + 1] + ... + [ 1][]}
The differences on the right hand side of this expression are a set of different integers and there
are 1 + of them. If we minimize the sum of these consecutive differences, we actually
maximize the right hand side, i.e. we bound [][] from above:
[][]

[] ( 1 + )( + )/2

We can add:
for (int i = 1; i < n; ++i) {
for (int j = i + 1; j <= n; ++j) {
s.AddConstraint(s.MakeLessOrEqual(s.MakeDifference(
Y[i][j],X[n])->Var(), -(n - 1 - j + i)*(n - j + i)/2));
}
}

Lets compare our tightened third implementation with the rest, again to compute (9):
Statistics
Time (s)
Failures
Branches
Backtracks

Impl1
4,712
51 833
103 654
51 836

Impl2
48,317
75 587
151 169
75 590

Impl2+
1,984
53 516
107 025
53 519

Impl3
0,338
7 521
15 032
7 524

tightened Impl3
0,137
2288
4572
2291

The interested reader can nd other dynamic bounds in [GalinierEtAl].

67

3.9. How does the solver optimize?

3.9 How does the solver optimize?


3.10 Summary
Not a good approach. For example with our tightened implementation 3 we need 35.679
seconds to solve (11).
Further reading: see [SmithEtAl]

68

CHAPTER

FOUR

REIFICATION

Overview:

Overview...
Prerequisites:
Classes under scrutiny:
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap4.


The les inside this directory are:

4.1 What is reication?

Part II
Customization

CHAPTER

FIVE

DEFINING SEARCH PRIMITIVES: THE N-QUEENS


PROBLEM

This chapter is about the customization of the search. What stategy(ies) to use to branch, i.e.
what variables to select and what value(s) to assign to them? How to use nested searches, i.e.
searches in subtrees? And so on.
The or-tools CP solver is quite exible and comes with several tools (Decisions,
DecisionBuilders, ...) that we call search primitives. Some are predened and can be
used right out of the box while others can be customized thanks to callbacks. You can also
combine different search strategies.
To efciently use your tools, you need to know them a little and this chapter introduces you in
a gentle manner to the inner working of the solver. The covered material is enough for you to
understand how you can customize your search primitives without being drowned in the often
tedious details of the implementation1 . To illustrate the customization of the search, we try to
solve the n-queen problem we have already met in chapter 1.
Prerequisites:

Basic knowledge of C++.


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.
Classes under scrutiny:

Decision,
DecisionBuilder,
TreeMonitor.
1

DecisionVisitor,

SearchMonitor,

If you take a look at the source code, we hope you will enjoy the clarity (?) of our code. We believe that the
most efcient code should remain simple and allow for more complicated but more efcient specializations when
needed.

5.1. The n-queens problem

Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap5.


The les inside this directory are:
Makefile.
nqueens_utilities.h: Contains two helper functions to test the number of solutions found and to print a solution.
nqueen1.cc: A rst implementation of our basic model to nd all solutions.
nqueen2.cc: The same implementation as in nqueen1.cc but this time to nd only
one solution.
nqueen3.cc: The same implementation as in nqueen2.cc but this time we use a
TreeMonitor to visualize the search with cpviz.
nqueen4.cc: The same implementation as in nqueen3.cc but with some added
statistics.
cpviz_nqueens4_basic.txt: cleaned output of ./nqueens4 --size=4
--cp_trace_search --cp_trace_propagation.
solver_benchmark.h: a basic SolverBenchmark class to benchmark different
search strategies.
phases1.cc: we use the SolverBenchmark class to test different search strategies
to nd the next variables and values to branch on among the predened choices in the
IntVarStrategy and IntValueStrategy enums.
nqueen5.cc:
nqueen6.cc:
nqueen7.cc:

5.1 The n-queens problem


We have discussed the n-queens problem (and dened what a solution is) in chapter 1.

5.1.1 The n-queen problem in more details


In the general n-queens problem, a set of n queens is to be placed on an n x n chessboard so
that no two queens attack each other.
Little is known that nding one solution for every is... quite easy2 . Indeed, there exist
polynomial-time algorithms that compute a solution given a size . For instance, Hoffman et
2

In computer science jargon, we say that the problem of nding one solution for the n-queens problem is in
. Actually, its the decision version of this problem but to keep it simple, lets say that nding one solution is
straightforward and easy and shouldnt take too long.

74

Chapter 5. Dening search primitives: the n-queens problem

al. proposed a simple algorithm to return a solution of the n-queens problem [Hoffman1969].
So we have to be careful when we talk about the n-queens problem. There are at least three
different problems that people refer to when talking about the n-queens problem:
nding one solution3 ,
counting the number of solutions and
nding (explicitly) all these solutions.
While the rst problem is easy, the two others are difcult4 .
As with the Golomb rulers problem, the experts could only nd the number of all the solutions
for small values. The biggest number of queens for which we know precisely the number
of solutions is = 26. The On-Line Encyclopedia of Integer Sequences keeps track of the
number of solutions (sequence A002562 for unique solutions (up to a symmetry) and sequence
A000170 for distinct solutions). The next table reports the number of unique and distinct
solutions for several values of .
n:
unique:
distinct:

1
1
1

2
0
0

3
0
0

4
1
2

5
2
10

6
1
4

7
6
40

8
12
92

9
46
352

10
92
724

11
341
2,680

12
1,787
14,200

13
9,233
73,712

14
45,752
365,596

Notice that there are more solutions for = 5 than = 6. What about the last three known
values? Here there are:
n:
unique:
distinct:

24
28,439,272,956,934
227,514,171,973,736

25
275,986,683,743,434
2,207,893,435,808,352

26
2,789,712,466,510,289
22,317,699,616,364,044

Quite impressive, isnt it? Its even more impressive when you know that these numbers were
obtained by explicitly nding all these solutions!
Is the n-queens problem only a toy problem?
While the n-queens problem is a wonderful problem to study backtracking systems and
is intensively used in benchmarks to test these systems, there are real problems that can
be modelled and solved as n-queens problems. For instance, it has been used for parallel memory storage schemes, VLSI testing, trafc control and deadlock prevention (see
[Jordan2009]).

5.1.2 How to solve the problem?


We follow again the classical three-stage method described in section 1.5: describe, model and
solve.
3
4

By solution, we mean feasible solution.


These two problems are NP-Hard. See [Jordan2009].

75

5.1. The n-queens problem

Describe
What is the goal of the n-queens problem? We will focus on nding one or all solutions. Given
a size for the chessboard, place queens5 so that no two queens attack each other.
What are the decision variables (unknowns)? We have different choices. One clever way to
reduce the number of variables is to introduce only one variable for each queen.
What are the constraints? No two queens can attack each other. This means to place queens
on the chessboard such that no two queens are placed on the same row, the same column or the
same diagonal.
Model
We know that no two queens can be placed on the same column and that we have as much
queens as columns. We will use one variable to place one queen on each column. The value of
the variable will denote the row of the corresponding queen. Figure 5.1 illustrates the variables
we will use to solve the n-queens problem in this chapter.

Figure 5.1: Variables to model the n-queens problem.


The solution depicted is {0 = 2, 1 = 0, 2 = 3, 3 = 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, 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(0 , . . . , 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.
Well use a known trick to model this constraint in the next section.
Solve
This time we will... test some search strategies. We will not devise a good search strategy
because we dont know yet what possibilities are implemented in the CP solver. We will test
different search strategies and see what works and why.
5

It is not obvious that for every , there exist at least a solution. In fact, for = 2 and = 3 there are no
solution. Hoffman et al. proved that there are solutions for every 4 in [Hoffman1969].

76

Chapter 5. Dening search primitives: the n-queens problem

5.2 Implementation of the basic model


You can nd the code in the le tutorials/cplusplus/chap5/nqueens1.cc.
After the needed headers from the or-tools library:
#include
#include
#include
#include

"base/commandlineflags.h"
"base/logging.h"
"base/stringprintf.h"
"constraint_solver/constraint_solver.h"

we add our header:


#include "./nqueens_utilities.h"

The header nqueens_utilities.h contains some helper functions (see the subsection
The helper functions below): among other CheckNumberOfSolutions() to check the
known number of solutions (unique or distinct) of the n-queens problem and several functions to print the solutions recorded by a SolutionCollector. To be able to collect only unique solutions (up to a symmetry), we will use SymmetryBreakers in section 5.8 page 126. A boolean gag FLAGS_use_symmetry allows or disallows the use of
SymmetryBreakers. This ag is dened in the header ./nqueens_utilities.h and
to be able to use it in our main le, we need to declare it:
DECLARE_bool(use_symmetry);

For the moment we dont implement any symmetry related mechanism and abort in the main
function if FLAGS_use_symmetry is set to true:
int main(int argc, char **argv) {
google::ParseCommandLineFlags(&argc, &argv, true);
if (FLAGS_use_symmetry) {
LOG(FATAL) << "Symmetries not yet implemented!";
}
if (FLAGS_size != 0) {
operations_research::NQueens(FLAGS_size);
} else {
for (int n = 1; n < 12; ++n) {
operations_research::NQueens(n);
}
}
return 0;
}

We offer the possibility to print the rst solution (ag print set to true) or all solutions (ag
print_all set to true)6 . By default, the program doesnt output any solution.
6

The code to print all the solutions is not shown here.

77

5.2. Implementation of the basic model

5.2.1 The model


The model is dened in the NQueens() function. The beginning of the function shouldnt
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(0 , . . . , 1 ) basically ensures no two queens remain on the same row
but we could have a solution like the one depicted on the Figure 5.2.

Figure 5.2: A solution with no queen on the same row.


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
our variables. For instance, to impose that the rst queen represented by 0 doesnt attack any
other queen on those diagonals, we can impose that
0 1 = 1 , 0 2 = 2 , 0 3 = 3 , . . .

(5.1)

0 = 1 + 1, 0 = 2 + 2, 0 = 3 + 3, . . .

(5.2)

(5.1) is equivalent to

Take the second queen 1 . We only have to look for the queens to her right. To impose that 1
doesnt attack any queen 2 , 3 , . . . on a diagonal with slope +1, we can add
1 1 = 2 , 1 2 = 3 , 1 3 = 4 , . . .

(5.3)

1 = 2 + 1, 1 = 3 + 2, 1 = 4 + 3, . . .

(5.4)

or equivalently

78

Chapter 5. Dening search primitives: the n-queens problem


In general, for queen , we impose that = + . Now, here comes the trick. If you add
1 to all members of (5.4), you get
1 + 1 = 2 + 2, 1 + 1 = 3 + 3, 1 + 1 = 4 + 4, . . .
and more generally = + becomes simply + = +

(5.5)

: > 7 .

This means that we can restrict ourselves to inequalities only involving + terms. Each
of these terms must be different from all others. Doesnt this ring a bell? Yep, this is the
AllDifferent constraint:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3, 4 + 4, . . .)

(5.6)

With a similar reasoning,


AllDifferent(0 , 1 1, 2 2, 3 3, 4 4, . . .)

(5.7)

ensures that no two queens are on the same diagonal with slope 1 (diagonals that slope downand-right).
We can thus add:
std::vector<IntVar*> vars(size);
for (int i = 0; i < size; ++i) {
vars[i] = s.MakeSum(queens[i], i)->Var();
}
s.AddConstraint(s.MakeAllDifferent(vars));
for (int i = 0; i < size; ++i) {
vars[i] = s.MakeSum(queens[i], -i)->Var();
}
s.AddConstraint(s.MakeAllDifferent(vars));

To collect the rst solution and count all the solutions, we use SolutionCollectors as
usual:
SolutionCollector* const solution_counter =
s.MakeAllSolutionCollector(NULL);
SolutionCollector* const collector = s.MakeFirstSolutionCollector();
collector->Add(queens);
std::vector<SearchMonitor*> monitors;
monitors.push_back(solution_counter);
monitors.push_back(collector);

We keeps our basic search strategy:


DecisionBuilder* const db = s.MakePhase(queens,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);
s.Solve(db, monitors);

// go!

In the next sections, we will test different DecisionBuilders.


7

: > simply means that we consider all greater than .

79

5.2. Implementation of the basic model

5.2.2 The helper functions


To test our model (and the solver!), we use the function CheckNumberOfSolutions()
to check the number of known solutions, unique up to a symmetry when we use
SymmetryBreakers and otherwise distinct:
void CheckNumberOfSolutions(int size, int num_solutions) {
if (FLAGS_use_symmetry) {
if (size - 1 < kKnownUniqueSolutions) {
CHECK_EQ(num_solutions, kNumUniqueSolutions[size - 1]);
} else if (!FLAGS_cp_no_solve) {
CHECK_GT(num_solutions, 0);
}
} else {
if (size - 1 < kKnownSolutions) {
CHECK_EQ(num_solutions, kNumSolutions[size - 1]);
} else if (!FLAGS_cp_no_solve) {
CHECK_GT(num_solutions, 0);
}
}
return;
}

kNumUniqueSolutions[] and kNumSolutions[] are static arrays with the proven


number of solutions. We restrict ourselves to testing the number of all distinct solutions up
to kKnownSolutions = 18 and unique solutions up to kKnownUniqueSolutions =
19. By unique solution we mean a unique solution up to a symmetry (see the section Breaking
symmetries with SymmetryBreakers for more).
The print helper functions are all based on PrintSolution():
void PrintSolution(const int size,
const std::vector<IntVar*>& queens,
SolutionCollector* const collector,
const int solution_number) {
if (collector->solution_count() > solution_number && size < 100) {
// go through lines
for (int j = 0; j < size; ++j) {
// go through queens
for (int i = 0; i < size; ++i) {
const int pos =
static_cast<int>(collector->Value(solution_number, queens[i]));
std::cout << std::setw(2);
if (pos == j) {
std::cout << i;
} else {
std::cout << ".";
}
std::cout << " ";
}
std::cout << std::endl;
}
}

80

Chapter 5. Dening search primitives: the n-queens problem

return;
}

You might wonder why we cast the return value of collector->Value() into an int?
The value() method returns an int64.

5.2.3 First results


Because nding all solutions is hard, we expect the solver to face more and more difculties as
the size grows but what about the easy problem of nding only one solution?
In the le 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
First solution
All Solutions

10
0
0,055

11
0
0,259

12
0
1,309

13
0
7,059

14
0,003
40,762

To nd all solutions, the solver shows a typical exponential behaviour for intractable problems.
The sizes are too small to conclude anything about the problem of nding one solution. In the
next Table, we try bigger sizes. The results are again in seconds.
Problem
First solution

25
0,048

26
0,392

27
0,521

28
3,239

29
1,601

30
63,08

31
14,277

It looks like our solver has some troubles to nd one solution. This is perfectly normal because
we didnt use a specic search strategy. In the rest of this chapter, we will try other search
strategies and compare them. We will also customize our strategies, i.e. dene strategies of our
own but before we do so, we need to learn a little bit about the basic working of the solver.

5.3 Basic working of the solver: the search algorithm


Basically, the CP solver consists of three main components:
the main search algorithm that permits to traverse/construct the search tree and to call
the callbacks at the right moments;
the Trail that is responsible for reversibility (when backtracking, you have to restore the
previous states) and
the Queue where the propagation takes place thanks to the Demons.
In this section, we only discuss the main search algorithm.
We present a simplied version of the main search algorithm. Although far from being complete, it gathers all the necessary basic elements and allows you to understand when some of
the callbacks of the SearchMonitors are called.

81

5.3. Basic working of the solver: the search algorithm

We describe a simplied version of the main search algorithm.


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 14 or the source code itself.

5.3.1 Basic denitions


Lets 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 [] and a valid index . At one node in the search tree, we divide the search space
in two exclusive search subspaces by imposing [] = 2 at one branch and [] = 2 at another
branch like in Figure 5.3.

xi = 2

xi = 2

Figure 5.3: The search space is divided in two search sub-trees


Each subspace is now smaller and we hope easier to solve. We continue this divide and conquer
mechanism until we know that a subspace doesnt contain a feasible solution or if we nd all
feasible solutions of a subtree. The rst node is called the root node and represent the complete
search space.
When we divide the search space by applying a decision ([] = 2) in one branch and by
refuting this decision ([] = 2) in another, we obtain a binary search trees8 . 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 dont need to be mutually exclusive, you can have different
numbers of them at each node, etc.
8

82

Not to be confused with a binary search tree (BST) used to store ordered sets.

Chapter 5. Dening search primitives: the n-queens problem

What exactly is a search tree?


A search tree is more a concept than a real object. It is made of nodes but these nodes
dont have to exist and can be (and most of them will be) virtual. Sometimes we use the
term search tree to denote the whole search space, sometimes to denote only the visited
nodes during a search or a part of the search space depending on the context.

Callbacks
To customize the search, we use callbacks. A callback is a reference to a piece of executable
code (like a function or an object) that is passed as an argument to another code. This is a very
common and handy way to pass high level code to low level code. For example, the search
algorithm is low level code. You dont want to change this code but you would like to change
the behaviour of the search algorithm to your liking. How do you do this? Callbacks are to the
rescue! At some places in the low level code, some functions are called and you can redene
those functions. There are several techniques available. In this section, we redene some virtual
functions of an abstract class. In section XXX, we will see another similar mechanism.
An example will clarify this mechanism. Take a SearchMonitor class. If you want to
implement your own search monitor, you inherit from SearchMonitor and you redene the
methods you need:
class MySearchMonitor: public SearchMonitor {
...
void EnterSearch() {
LG << "Search entered...";
}
...
};

You then pass this SearchMonitor to the solver:


Solver solver("Test my new SearchMonitor");
MySearchMonitor* const sm = new MySearchMonitor(&solver);
DecisionBuilder* const db = ...;
solver.NewSearch(db, sm);
delete sm;

At the beginning of a search, the solver calls the virtual method EnterSearch() i.e. your
EnterSearch() method. Dont forget to delete your SearchMonitor after use. You
can also use a smart pointer or even better, let the solver take ownership of the object with the
RevAlloc() method (see subsection 5.8.3).
Phases
The CP solver allows you to combine several searches, i.e. different types of sub-searches. You
can search a subtree of the search tree differently from the rest of your search. This is called
nested search while the whole search is called a top-level search. There are no limitations and
you can nest as many searches as you like. You can also restart a (top level or nested) search.

83

5.3. Basic working of the solver: the search algorithm

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.

5.3.2 The basic idea


The basic idea9 is very simple yet effective. A DecisionBuilder is responsible to return a
Decision at a node. A decision would be for instance, [4] = 3. We divide the sub search
tree at this node by applying this decision (left branch: [4] = 3) and by refuting this decision
(right branch: [4] = 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 gure.
Decision

Apply()

Refute()

Figure 5.4: Apply(): go left, Refute(): go right.


From the root node, we follow the left branch whenever possible and backtrack to the rst
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 (see the box What is a preorder traversal of a binary tree?) of the binary search tree.
What is a pre-order traversal of a binary tree?
The search tree depicted on the Figure The actual search tree of our search has its node
numbered in the order given by a pre-order traversal. There are two other traversals: inorder and post-order. We invite the curious reader to google pre-order traversal of a tree
to nd more. There are a number of applets showing the different traversals.
There are basically two ways to ask the CP solver to nd a solution (or solutions) as we
have seen in chapter 2. Either you congure SearchMonitors and you call the Solvers
Solve() method, either you use the ner grained NewSearch() - NextSolution() EndSearch() mechanism. In the rst 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:
1
2
3
4
5

bool Solver::Solve(DecisionBuilder* const db,


SearchMonitor* const * monitors,
int size) {
NewSearch(db, monitors, size);
searches_.back()->set_created_by_solve(true);
9

84

// Overwrites default.

The real code deals with a lots of subtleties to implement different variants of the search algorithm.

Chapter 5. Dening search primitives: the n-queens problem

NextSolution();
const bool solution_found = searches_.back()->solution_counter() > 0;
EndSearch();
return solution_found;

6
7
8
9
10

searches_ is an std::vector of Searches because we can nest our searches (i.e search
differently in a subtree using another phase/DecisionBuilder). Here we take the current
search (searches_.back()) and tell the solver that the search was initiated by a Solve()
call:
searches_.back()->set_created_by_solve(true);

// Overwrites default.

Indeed, the solver needs to know if it let you interfere during the search process or not.
You might wonder why there is only one call to NextSolution()? The reason is simple.
If the search was initiated by the caller (you) with the NewSearch() - NextSolution()
- EndSearch() mechanism, the solver stops the search after a NextSolution() call.
If the search was initiated by a Solve() call, you tell the solver when to stop the search
with SearchMonitors. By default, the solver stops after the rst solution found (if any).
You can overwrite this behaviour by implementing the AtSolution() callback of the
SearchMonitor class. If this method returns true, the search continues, otherwise the
solver ends it.

5.3.3 The basic search algorithm and the callback hooks for the
SearchMonitors
SearchMonitors contain a set of callbacks called on search tree events, such as entering/exiting search, applying/refuting decisions, failing, accepting solutions... In this section,
we present the callbacks of the SearchMonitor class10 listed in Table 5.1 and show you
exactly when they are called in the search algorithm.
We draw again your attention to the fact that the algorithm shown here is a simplied version
of the search algorithm. In particular, we dont show how the nested searches and the restart of
a search are implemented. We nd this so important that we reuse our warning box:
We describe a simplied version of the main loop of the search algorithm.
We use exceptions in our simplied version while the actual implementation uses the more
efcient (and cryptic) setjmp - longjmp mechanism.
We describe briey what nested searches are in the section Nested searches but you will have
to wait until the chapter Under the hood and the section Nested searches to learn the juicy
details11 .
10

There are a few more callbacks dened in a SearchMonitor. See XXX


Of course, you can have a peak right now but some more background will probably help you understand this
mechanism better. Beside, you dont need to understand the inner mechanism to be able to use nested search!
11

85

5.3. Basic working of the solver: the search algorithm

Table 5.1: Basic search algorithm callbacks from the SearchMonitor class.
Methods
EnterSearch()
ExitSearch()
BeginNextDecision(DecisionBuilder*
const b)
EndNextDecision(DecisionBuilder*
const b, Decision* const d)
ApplyDecision(Decision* const d)
RefuteDecision(Decision* const d)
AfterDecision(Decision* const d,
bool apply)

BeginFail()
EndFail()
BeginInitialPropagation()
EndInitialPropagation()
AcceptSolution()

AtSolution()

NoMoreSolutions()

Descriptions
Beginning of the search.
End of the search.
Before calling DecisionBuilder::Next().
After calling DecisionBuilder::Next(),
along with the returned decision.
Before applying the Decision.
Before refuting the Decision.
Just after refuting or applying the Decision,
apply is true after Apply(). This is called only
if the Apply() or Refute() methods have not
failed.
Just when the failure occurs.
After completing the backtrack.
Before the initial propagation.
After the initial propagation.
This method is called when a solution is found. It
asserts if the solution is valid. A value of false indicates that the solution should be discarded.
This method is called when a valid solution is found.
If the return value is true, then search will resume.
If the result is false, then search will stop there.
When the search tree has been visited.

To follow the main search algorithm, it is best to know in what states the solver can be. The
enum SolverState enumerates the possibilities in the following table:
Value
OUTSIDE_SEARCH
IN_ROOT_NODE
IN_SEARCH
AT_SOLUTION
NO_MORE_SOLUTIONS
PROBLEM_INFEASIBLE

Meaning
Before search, after search.
Executing the root node.
Executing the search code.
After successful NextSolution() and before EndSearch().
After failed NextSolution() and before EndSearch().
After search, the model is infeasible.

NewSearch()
This is how the NewSearch() method might have looked in a simplied version of the main
search algorithm. The Search class is used internally to monitor the search. Because the CP
solver allows nested searches, we take a pointer to the current search object each time we call
the NewSearch(), NextSolution() and EndSearch() methods.
1
2
3

void Solver::NewSearch(DecisionBuilder* const db,


SearchMonitor* const * monitors,
int size {

4
5
6

Search* const search = searches_.back();


state_ = OUTSIDE_SEARCH;

86

Chapter 5. Dening search primitives: the n-queens problem

// Init:
// Install
// Install
// Install
// Install
// Install
...

8
9
10
11
12
13
14

the main propagation monitor


DemonProfiler if needed
customers SearchMonitors
DecisionBuilders SearchMonitors
print trace if needed

15

search->EnterSearch();

16

// SEARCHMONITOR CALLBACK

17

// Set decision builder.


search->set_decision_builder(db);

18
19
20

state_ = IN_ROOT_NODE;
search->BeginInitialPropagation();

21
22

// SEARCHMONITOR CALLBACK

23

try {
// Initial constraint propagation
ProcessConstraints();
search->EndInitialPropagation(); // SEARCHMONITOR CALLBACK
...
state_ = IN_SEARCH;
} catch (const FailException& e) {
...
state_ = PROBLEM_INFEASIBLE;
}

24
25
26
27
28
29
30
31
32
33
34

return;

35
36

The initialization part consists in installing the backtracking and propagation mechanisms, the
monitors and the print trace if needed. If everything goes smoothly, the solver is in state
IN_SEARCH.
NextSolution()
The NextSolution() method returns true if if nds the next solution, false otherwise.
Notice that the statistics are not reset whatsoever from one call of NextSolution() to the
next one.
We present and discuss this algorithm below. SearchMonitors callbacks are indicated by
the comment:
// SEARCHMONITOR CALLBACK

Here is how it might have looked in a simplied version of the main search algorithm:
1
2
3

bool Solver::NextSolution() {
Search* const search = searches_.back();
Decision* fd = NULL;// failed decision

4
5
6

// Take action following solver state


switch (state_) {

87

5.3. Basic working of the solver: the search algorithm

case PROBLEM_INFEASIBLE:
return false;
case NO_MORE_SOLUTIONS:
return false;
case AT_SOLUTION: {// We need to backtrack
// SEARCHMONITOR CALLBACK
// BacktrackOneLevel() calls search->EndFail()
if (BacktrackOneLevel(&fd)) {// No more solutions.
search->NoMoreSolutions();// SEARCHMONITOR CALLBACKS
state_ = NO_MORE_SOLUTIONS;
return false;
}
state_ = IN_SEARCH;
break;
}
case OUTSIDE_SEARCH: {
state_ = IN_ROOT_NODE;
search->BeginInitialPropagation();// SEARCHMONITOR CALLBACKS
try {
ProcessConstraints();
search->EndInitialPropagation();// SEARCHMONITOR CALLBACKS
...
state_ = IN_SEARCH;
} catch(const FailException& e) {
...
state_ = PROBLEM_INFEASIBLE;
return false;
}
break;
}
case IN_SEARCH:
break;

7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

40
41

DecisionBuilder* const db = search->decision_builder();

42
43
44
45

// MAIN SEARCH LOOP TO FIND THE NEXT SOLUTION IF ANY


volatile bool finish = false;
volatile bool result = false;

46
47
48
49
50
51
52
53
54
55
56
57

while (!finish) {// Try to find next solution


try {
// Explore right branch of the tree on backtrack
if (fd != NULL) {// We have a right branch
...
search->RefuteDecision(fd);// SEARCHMONITOR CALLBACK
fd->Refute(this);
search->AfterDecision(fd, false);// SEARCHMONITOR CALLBACK
...
fd = NULL;
}

58

// Explore left branches of the tree


Decision* d = NULL;
// Go left as often as possible

59
60
61

88

Chapter 5. Dening search primitives: the n-queens problem

while (true) {// Trying to branch left


search->BeginNextDecision(db);// SEARCHMONITOR CALLBACK
d = db->Next(this);
search->EndNextDecision(db, d);// SEARCHMONITOR CALLBACK
// Dead-end? This is a shortcut
if (d == fail_decision_) {
search->BeginFail();// SEARCHMONITOR CALLBACK
// fail now instead of after 2 branches.
throw FailException();
}
// Explore next left branch of the tree
if (d != NULL) {
search->ApplyDecision(d);// SEARCHMONITOR CALLBACK
d->Apply(this);
search->AfterDecision(d, true);// SEARCHMONITOR CALLBACK
...
} else {// No Decision left, the DecisionBuilder has finished
break;
}
}// while (true)

62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82

// We can not go further left... test Solution


// SEARCHMONITOR CALLBACK
if (search->AcceptSolution()) {// Accept Solution
// SEARCHMONITOR CALLBACK
if (!search->AtSolution() || !CurrentlyInSolve()) {
result = true;
finish = true;
} else {
search->BeginFail();// SEARCHMONITOR CALLBACK
throw FailException();
}
} else {
search->BeginFail();// SEARCHMONITOR CALLBACK
throw FailException();
}
} catch (const FailException& e) {
// We must backtrack
// SEARCHMONITOR CALLBACK
// BacktrackOneLevel() calls search->EndFail()
if (BacktrackOneLevel(&fd)) { // no more solutions.
search->NoMoreSolutions();// SEARCHMONITOR CALLBACK
result = false;
finish = true;
}
}
}// while (!finish)

83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109

// Set solver current state


...
state_ = ...;

110
111
112
113

return result;

114
115

89

5.3. Basic working of the solver: the search algorithm

Lets dissect the algorithm. First of all, you might wonder where does the propagation take
place? In a few words: Constraints are responsible of attaching Demons to variables.
These Demons are on their turn responsible for implementing the actual propagation. Whenever the domain of a variable changes, the corresponding Demons are triggered. In the main
search algorithm, this happens twice: when we Apply() a Decision (line 75) and when we
Refute() a Decision (line 53).
Back to the algorithm. On line 2, the solver grabs the last search. Indeed, several searches can
be nested and queued.
The Search object is responsible of monitoring the search for one DecisionBuilder (one
phase) and triggers the callbacks of the installed SearchMonitors at the right moments.
Following the solvers state, some action is needed (see lines 6-39). The case AT_SOLUTION
is worth an explanation. NextSolution() was called and the solver found a feasible
solution. The solver thus needs to backtrack (method BacktrackOneLevel() on line
14). If a right branch exists, it is stored in the Decision pointer fd (failed decision) and
BacktrackOneLevel() returns false. If there are no more right branches to visit,
the search tree has been exhausted and the method returns true. Next, the corresponding
DecisionBuilder to the current search is kept on line 41.
We are now inside the main loop of the NextSolution() method. Two Boolean variables
are dened12
finish: becomes true when the search is over;
result: denotes if a feasible solution was indeed found or not.
These two variables are declared volatile to allow their use between setjmp and
longjmp, otherwise the compiler might optimize certain portions of code away. Basically,
it tells the compiler that these variables can be changed from the outside.
This main loop starts at line 47 and ends at line 108.
The try - catch mechanism allows to easily explain the backtrack mechanism. Whenever
we need to backtrack in the search, a FailException is thrown13 .
If the Decision pointer fd is not NULL, this means that we have backtracked to the rst
available (non visited) right branch in the search tree. This corresponds to refuting the decision
(lines 50-57).
The solver now tries to explore as much as possible left branches and this is done in the while
loop (line 62-81).
The DecisionBuilder produces its next Decision on line 64. If it detects that this
branch is a dead-end, it is allowed to return a FailDecision which the solver tests at line
67.
If the search tree is empty, the DecisionBuilder returns NULL. The solver tests this possibility on line 73. If the DecisionBuilder found a next Decision, it is applied on line
75.
12

These two variables play a role when we use nested searches, restart or nish a search but these possibilities
are not shown here.
13
Did we already mention that the try - catch mechanism is not used in the production code? ;-)

90

Chapter 5. Dening search primitives: the n-queens problem

Whenever the solver cannot nd a next left branch to explore, it exits the while(true) loop.
We are now ready to test if we have found a feasible solution at the leaf of a left branch. This
test is done one line 85. The method AcceptSolution() decides if the solution is feasible
or not. After nding a feasible solution, the method AtSolution() decides if we continue
or stop the search.
You might recognize these two methods as callbacks of a SearchMonitor. These
two methods call the corresponding methods of all installed SearchMonitors no
matter what they return, i.e.
you are guaranteed that all SearchMonitors will
be called.
If one SearchMonitor has its method AcceptSolution() returning false, search->AcceptSolution() returns false.
On the contrary, if only one SearchMonitor has its AtSolution() method returning true,
search->AtSolution() returns true.
The test on line 87 is a little bit complex:
test = !search->AtSolution() || !CurrentlyInSolve()

Remember that AtSolution() returns true if we want to resume the search (i.e.
if at least one SearchMonitor->AtSolution() returns true), false otherwise.
CurrentlyInSolve() returns true if the solve process was called with the Solve()
method and false if it was called with the NextSolution() method.
Thus, test is true (and we stop the search in NextSolution()) if all
SearchMonitors decided to stop the search (search->AtSolution() returns then
false) or if at least one SearchMonitor decided to continue but the solve process was
called by NextSolution(). Indeed, a user expects NextSolution() to stop whenever
it encounters a feasible solution.
Whenever a backtrack is necessary, a FailException is caught and the solver backtracks
to the next available right branch if possible.
Finally, the current state of the solver is set and the method NextSolution() returns if a solution has been found and accepted by all SearchMonitors or there is no solution anymore.
It then returns true if the test above is true, false otherwise.
A solution is dened as a leaf of the search tree with respect to the given
DecisionBuilder for which there is no failure. What this means is that, contrary to intuition, a solution may not have all variables of the model bound. It is
the responsibility of the DecisionBuilder to keep returning decisions until all
variables are indeed bound. The most extreme counterexample is calling Solve()
with a trivial DecisionBuilder whose Next() method always returns NULL.
In this case, Solve() immediately returns true, since not assigning any variable
to any value is a solution, unless the root node propagation discovers that the model
is infeasible.
EndSearch()
The EndSearch() method cleans the solver and if required, writes the prole of the search
in a le. It also calls the ExitSearch() callbacks of all installed SearchMonitors.

91

5.4. cpviz: how to visualize the search

Here is how it might have looked in a simplied version of the main search algorithm.
1
2
3
4
5
6
7
8
9
10
11

void Solver::EndSearch() {
Search* const search = searches_.back();
...
search->ExitSearch();// SEARCHMONITOR CALLBACK
search->Clear();
state_ = OUTSIDE_SEARCH;
if (!FLAGS_cp_profile_file.empty()) {
LOG(INFO) << "Exporting profile to " << FLAGS_cp_profile_file;
ExportProfilingOverview(FLAGS_cp_profile_file);
}
}

5.4 cpviz: how to visualize the search


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 nite 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 nd all necessary information and tools at:
http://sourceforge.net/projects/cpviz/

5.4.1 TreeMonitors to provide the cpviz input


You can nd the code in the le tutorials/cplusplus/chap5/nqueens3.cc.
To monitor the search, we use SearchMonitors. To produce the les needed by
cpviz to visualize the search, we use a specialized SearchMonitor: the TreeMonitor
class. cpviz needs two les as input: tree.xml and visualization.xml.
To produce these two les, add a TreeMonitor among your SearchMonitors in your
code:
vector<SearchMonitor*> monitors;
...
SearchMonitor* const cpviz = s.MakeTreeMonitor(vars, "tree.xml",
"visualization.xml");
monitors.push_back(cpviz);

You need also a conguration le (named configuration.xml) as this one:


<?xml version="1.0" encoding="UTF-8"?>
<configuration version="1.0" directory="/tmp"

92

Chapter 5. Dening search primitives: the n-queens problem

xsi:noNamespaceSchemaLocation="configuration.xsd" xmlns:xsi="http://
www.w3.org/2001/XMLSchema-instance">
<tool show="tree" type="layout" display="expanded" repeat="all"
width="700" height="700" fileroot="tree"/>
<tool show="viz" type="layout" display="expanded" repeat="all"
width="700" height="700" fileroot="viz"/>
</configuration>

Basically, it tells cpviz to produce the graphic les for the search tree (show="tree") and
the variables (show="viz") in the directory /tmp.
If you are really lazy, we even provide a factory method which generates automatically a default
conguration le:
SearchMonitor* const cpviz = s.MakeTreeMonitor(vars,
"configuration.xml",
"tree.xml",
"visualization.xml");

After your search is nished AND you have called (implicitley or explicitly) EndSearch()14 ,
you can run cpviz to digest the XML les representing your search by entering the viz/bin
directory and typing:
java ie.ucc.cccc.viz.Viz configuration.xml tree.xml visualization.xml

on a command line into a terminal near you. This will produce the following picture of the
search tree:

cpviz produces the construction of the search tree, step by step. In our case we try to solve the
n-queens problem with = 4 and cpviz generates 8 les.
This is probably not what you expected. First of all, this is not a binary tree and there seems to
be an extra dummy root node. A binary tree which is what is exactly constructed during the
search is not really suited for a graphical representation as it can quickly become very big
(compare the tree above with the actual search tree that is represented below). To avoid huge
14

tree.xml and visualization.xml are generated in the ExitSearch() callback of the


TreeMonitor class.

93

5.4. cpviz: how to visualize the search

trees, we have reduced their sizes by contracting several nodes. Except for the dummy root
node, each node corresponds to a variable during the search and only left branches are given
explicitly. The numbers along the branches denote the applied decisions (like [1] = 2) and the
numbers in the right corner above the variable names of the nodes are the number of values left
in the domain of the corresponding variable just before the decision was taken. Nodes coloured
in
green denote feasible solutions;
red denote sub-trees without any feasible solutions;
blue denote intermediate try nodes (these only exist during the search).

5.4.2 Interpreting the graphical results


You can nd the code in the le tutorials/cplusplus/chap5/nqueens4.cc.
To better understand the output of cpviz and to follow the search with precision, lets
trace the search and the propagation of our program nqueens4:
./nqueens4 --size=4 --cp_trace_search --cp_trace_propagation 2>
cpviz_nqueens4_basic.txt

We redirect std::err into the le cpviz_nqueens4_basic.txt.


We will transcribe the information contained in the le cpviz_nqueens4_basic.txt but
in a more graphical way. Pay attention to the order in which the variables and the constraints
are processed.
Recall that we are solving the problem of nding all distinct solutions of the n-queens
problem with 4 queens. Our search strategy is to choose the rst variable with a non
empty domain with a least two elements (Solver::CHOOSE_FIRST_UNBOUND). Once
this variable is chosen, we give it the smallest possible value contained in its domain
(Solver::ASSIGN_MIN_VALUE). We have 4 variables 0 , 1 , 2 and 3 introduced in that
order. The 3 constraints are all AllDifferent constraints introduced in the following order:
AllDifferent(0 , 1 , 2 , 3 )
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3)
AllDifferent(0 , 1 1, 2 2, 3 3)
The search tree
By reading the le cpviz_nqueens4_basic.txt, we can retrace the search and reconstruct the search tree:
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 subtrees that are not worth exploring explicitly: we cannot nd any feasible solution along these
branches of the tree. The leaf nodes in green denote on the contrary feasible solutions. The

94

Chapter 5. Dening search primitives: the n-queens problem


node 0
x0 = 0

x0 = 0

node 1

node 4
x0 = 1

x1 = 2

x0 = 1

x1 = 2

node 6
node 2

node 3

node 5

x0 = 2

x0 = 2

node 8
node 7
x1 = 0

x1 = 0
node 10

node 9

Figure 5.5: The actual search tree of our search


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 le nqeens4.cc, we have printed some statistics about the search:
std::cout
std::cout
std::cout
std::cout
std::cout

<<
<<
<<
<<
<<

"Number of solutions: " << num_solutions << std::endl;


"Failures: " << s.failures() << std::endl;
"Branches: " << s.branches() << std::endl;
"Backtracks: " << s.fail_stamp() << std::endl;
"Stamps: " << s.stamp() << std::endl;

and with size = 4, we get as output:


Number of solutions: 2
Failures: 6
Branches: 10
Backtracks: 9
Stamps: 29

Lets see if we can deduce these statistics from the search tree. The three rst statistics are easy
to spot in the tree:
Number of solutions (2): There are indeed two distinct solutions denoted by the
two green leafs.
Failures (6): A failure occurs whenever the solver has to backtrack, whether it is
because of a real failure (nodes 2 3 and 9 10) or a success (nodes 5 and
7). Indeed, when the solver nds a solution, it has to backtrack to nd other
solutions. The method failures() returns the number of leaves of the
95

5.4. cpviz: how to visualize the search


search tree. In our case, 6.
Branches (10): Number of branches in the tree, indeed 10.
The two last statistics are more difcult 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 backtrack15 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 reects the amount of work needed by the solver
during the search. We refer the curious reader to the source code for more
details.
Our cpviz output of the search tree
How can we compare the real tree with our cpviz output? The trick is to observe the construction 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. Youll see in a moment
why.

Figure 5.6: Contruction of the real search tree from the cpviz tree: step 0

15

96

Actually, the very last backtrack happens when the solver is deleted.

Chapter 5. Dening search primitives: the n-queens problem

Step 1:

node 0

(a) cpviz

(b) Real search tree

Figure 5.7: Construction of the real search tree from the cpviz tree: step 1
Next, we start with the actual root node.
As you can see in our
cpviz output,
the dummy root node doesnt even have a name and
the little number 0 next to this non existing name doesnt mean
anything.
Step 2:

node 0
x0 = 0
node 1

(a) cpviz

(b) Real search tree

Figure 5.8: Construction of the real search tree from the cpviz tree: step 2
You can see in our cpviz output that the solver has applied the Decision 0 = 0 but that
it couldnt realize if this was a good choice or not. The little number 4 next to the variable
name 0 means that before the decision was applied, the number of values in its domain was 4.
Indeed: 0 {0, 1, 2, 3} before being assigned the value 0.

97

5.4. cpviz: how to visualize the search

Step 3:

node 0
x0 = 0
node 1
x1 = 2

node 2

(a) cpviz

(b) Real search tree

Figure 5.9: Construction of the real search tree from the cpviz tree: step 3
After having applied the Decision 0 = 0 at step 2, the solver now applies the Decision
1 = 2 which leads, after propagation, to a failure.

Step 4:

node 0
x0 = 0

x0 = 0

node 1
x1 = 2

node 2

(a) cpviz

node 4
x1 = 2

node 3

(b) Real search tree

Figure 5.10: Construction of the real search tree from the cpviz tree: step 4
Our cpviz output now clearly warns that taking 0 = 0 does not lead to a feasible solution.
This can only mean that the solver tried also to refute the Decision 1 = 2. So we know that
the branch 1 = 2 after the branch 0 = 0 is leading nowhere. We have to backtrack and to
refute the Decision 0 = 0. We have thus a new branch 0 = 0 in the real search tree.

98

Chapter 5. Dening search primitives: the n-queens problem

Step 5:

node 0
x0 = 0

x0 = 0
node 1

node 4
x0 = 1

x1 = 2

x1 = 2

node 2

(a) cpviz

node 3

node 5

(b) Real search tree

Figure 5.11: Construction of the real search tree from the cpviz tree: step 5
We nd a feasible solution when 0 = 1. Thus we add the branch 0 = 1 and indicate success.

Step 6:

node 0
x0 = 0

x0 = 0

node 1

node 4
x0 = 1

x1 = 2

x1 = 2

x0 = 1
node 6

node 2

node 3

node 5
x0 = 2
node 7

(a) cpviz

(b) Real search tree

Figure 5.12: Construction of the real search tree from the cpviz tree: step 6
We nd a second feasible solution when 0 = 2. Before we can proceed by applying
Decision 0 = 2, we rst have to refute the Decision 0 = 1

99

5.4. cpviz: how to visualize the search

Step 7:

node 0
x0 = 0

x0 = 0
node 1

node 4
x0 = 1

x1 = 2

x0 = 1

x1 = 2

node 6
node 2

node 3

node 5

x0 = 2

x0 = 2

node 8
node 7

(a) cpviz

(b) Real search tree

Figure 5.13: Construction of the real search tree from the cpviz tree: step 7
We add a tentative branch in the cpviz output. The branch before we applied the Decision
2 = 0 that lead to a feasible solution, so now we know that the solver is trying to refute that
decision: 2 = 0.

Step 8:

node 0
x0 = 0

x0 = 0

node 1

node 4
x0 = 1

x1 = 2

x0 = 1

x1 = 2

node 6
node 2

node 3

node 5

x0 = 2

x0 = 2

node 8
node 7
x1 = 0

x1 = 0
node 10

node 9

(a) cpviz

(b) Real search tree

Figure 5.14: Construction of the real search tree from the cpviz tree: step 8
The nal step is the branch 1 = 0 that leads to a failure. This means that when we apply and
refute 1 = 0, we get a failure. Thus we know that 0 = 1 and 0 = 1 both fail.

100

Chapter 5. Dening search primitives: the n-queens problem

Propagation
To better understand the search, lets have a look at the propagation in details. First, we look at
the real propagation, then at our cpviz output.
You can nd an animated version of the propagation here.
We start at the root node with
node 0: 0 {0, 1, 2, 3}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. We apply the
Decision 0 = 0 which corresponds to our search strategy.

node 1: 0 {0}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3} The propagation is


done in the following order.
AllDifferent(0 , 1 1, 2 2, 3 3) :
1 : 1, 2 : 2, 3 : 3

0 {0}, 1 {0, 2, 3}, 2 {0, 1, 3}, 3 {0, 1, 2}


AllDifferent(0 , 1 , 2 , 3 ) :
1 : 0, 2 : 0, 3 : 0

0 {0}, 1 {2, 3}, 2 {1, 3}, 3 {1, 2}. No more propagation is possible. We
then apply the Decision 1 = 2

node 2: 0 {0}, 1 {2}, 2 {1, 3}, 3 {1, 2}. The propagation is as follow:
AllDifferent(0 , 1 1, 2 2, 3 3) :
2 : 3

101

5.4. cpviz: how to visualize the search

0 {0}, 1 {2}, 2 {1}, 3 {1, 2}.


AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
2 : 1

0 {0}, 1 {2}, 2 , 3 {1, 2}. We have a failure as the domain of 2 is empty.


We backtrack to node 1 and refute the Decision 1 = 2.

node 3: 0 {0}, 1 {3}, 2 {1, 3}, 3 {1, 2}. 1 is xed to 3 because we removed
the value 2 of its domain (refuting the Decision 1 = 2).
Propagation:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
3 : 1

0 {0}, 1 {3}, 2 {1, 3}, 3 {2}.


AllDifferent(0 , 1 , 2 , 3 ) :
2 : 3

102

Chapter 5. Dening search primitives: the n-queens problem

0 {0}, 1 {3}, 2 {1}, 3 {2}.


This is of course not possible and the following propagation detects this impossibility:
AllDifferent(0 , 1 1, 2 2, 3 3) :
2 : 1

0 {0}, 1 {3}, 2 , 3 {2}. We have again a failure as the domain of 2 is


empty. We need to backtrack to the root node and refute the Decision 0 = 0.

node 4: 0 {1, 2, 3}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. We


Decision 0 = 1 which complies with our search strategy.

apply

node 5: 0 {1}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. Propagation:


AllDifferent(0 , 1 1, 2 2, 3 3) :
1 : 2, 2 : 3

0 {1}, 1 {0, 1, 3}, 2 {0, 1, 2}, 3 {0, 1, 2, 3}.


AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
1 : 0

103

5.4. cpviz: how to visualize the search


0 {1}, 1 {1, 3}, 2 {0, 1, 2}, 3 {0, 1, 2, 3}.
AllDifferent(0 , 1 , 2 , 3 ) :
1 : 1, 2 : 1, 3 : 1

0 {1}, 1 {3}, 2 {0, 2}, 3 {0, 2, 3}.


AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
2 : 2

0 {1}, 1 {3}, 2 {0}, 3 {0, 2, 3}.


AllDifferent(0 , 1 , 2 , 3 ) :
3 : 3

0 {1}, 1 {3}, 2 {0}, 3 {0, 2}.


AllDifferent(0 , 1 , 2 , 3 ) :
3 : 0

104

Chapter 5. Dening search primitives: the n-queens problem

0 {1}, 1 {3}, 2 {0}, 3 {2}.


We have a solution! We have now to backtrack to node 4 and refute Decision 0 = 1.

node 6: 0 {2, 3}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. We


Decision 0 = 2.

apply

the

node 7: 0 {2}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. Propagation:


AllDifferent(0 , 1 1, 2 2, 3 3) :
1 : 3

0 {2}, 1 {0, 1, 2}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}.


AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
1 : 1, 2 : 0

0 {2}, 1 {0, 2}, 2 {1, 2, 3}, 3 {0, 1, 2, 3}.


AllDifferent(0 , 1 , 2 , 3 ) :
1 : 2, 2 : 2, 3 : 2

105

5.4. cpviz: how to visualize the search

0 {2}, 1 {0}, 2 {1, 3}, 3 {0, 1, 3}.


AllDifferent(0 , 1 1, 2 2, 3 3) :
2 : 1

0 {2}, 1 {0}, 2 {3}, 3 {0, 1, 3}.


AllDifferent(0 , 1 , 2 , 3 ) :
3 : 0

0 {2}, 1 {0}, 2 {3}, 3 {1, 3}.


AllDifferent(0 , 1 , 2 , 3 ) :
3 : 3

106

Chapter 5. Dening search primitives: the n-queens problem


0 {2}, 1 {0}, 2 {3}, 3 {1} and we have a second distinct solution! We
backtrack to node 6 and refute Decision 0 = 2.

node 8: 0 {3}, 1 {0, 1, 2, 3}, 2 {0, 1, 2, 3}, 3 {0, 1, 2, 3}. 0 is xed because
there is only one value left in its domain.
Propagation:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
1 : 2, 2 : 1, 3 : 0

0 {3}, 1 {0, 1, 3}, 2 {0, 2, 3}, 3 {1, 2, 3}.

AllDifferent(0 , 1 , 2 , 3 ) :
1 : 3, 2 : 3, 3 : 3

0 {3}, 1 {0, 1}, 2 {0, 2}, 3 {1, 2}. No more propagation. We thus apply
our search strategy and apply Decision 1 = 0.

node 9: 0 {3}, 1 {0}, 2 {0, 2}, 3 {1, 2}. Propagation:

AllDifferent(0 , 1 1, 2 2, 3 3) :
3 : 2

107

5.4. cpviz: how to visualize the search


0 {3}, 1 {0}, 2 {0, 2}, 3 {1}.
AllDifferent(0 , 1 , 2 , 3 ) :
3 : 0

0 {3}, 1 {0}, 2 {2}, 3 {1} which is impossible as the next propagation


shows:
AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
2 : 2

0 {3}, 1 {0}, 2 , 3 {1}. As the domain of 2 is empty, we have failure


and have to backtrack to node 8 and refute Decision 1 = 0.

node 10: 0 {3}, 1 {1}, 2 {0, 2}, 3 {1, 2}. Propagation:


AllDifferent(0 , 1 1, 2 2, 3 3) :
2 : 2

0 {3}, 1 {0}, 2 {0}, 3 {1, 2}.


AllDifferent(0 , 1 + 1, 2 + 2, 3 + 3) :
2 : 0

0 {3}, 1 {0}, 2 , 3 {1, 2}. The empty domain for 2 indicates a failure
and we have to backtrack... to the root node as we have exhausted the search tree. The
search is thus nished and we have found 2 distinct solutions.
Our cpviz output of the propagation
For each step in the construction of the tree in our cpviz output corresponds a visualization
of the propagation and the states of the variables. Of course, as we try to limit the number of
108

Chapter 5. Dening search primitives: the n-queens problem

nodes in the tree, we are constrained to display very little information about the propagation
process. In short, if we nd
a try node, we display the nal propagation at this node;
a solution, we display the solution;
a failure, we display the rst failure encountered and the values of the assigned variables.
We also display what variable we focus on next.
Lets go
umn our
tree and

again through the 9 steps.


cpviz tree output,
in the
in the right column our

We display in the left colmiddle column the actual search


cpviz output of the propagation.

Step 0:

(a) cpviz

(b) cpviz propagations output

Figure 5.15: 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.

109

5.4. cpviz: how to visualize the search

Step 1:

node 0

(a) cpviz tree

(b) Real search tree

(c) cpviz propagation

Figure 5.16: cpviz output of the propagation: step 1


The yellow rectangle tells us that the focus is on variable 1(0 ), 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

Figure 5.17: cpviz output of the propagation: step 2


The red square indicates that the variable 0 was xed to 0. The dark green squares show the
propagation. The focus is on variable 2 (1 ).

110

Chapter 5. Dening search primitives: the n-queens problem

Step 3:

node 0
x0 = 0
node 1
x1 = 2

node 2

(a) cpviz tree

(b) Real search tree

(c) cpviz propagation

Figure 5.18: cpviz output of the propagation: step 3


The red rectangle warns of a failure: there is no feasible solution with 0 = 0 and 1 = 2.

Step 4:

node 0
x0 = 0

x0 = 0

node 1
x1 = 2

node 2

(a) cpviz tree

node 4
x1 = 2

node 3

(b) Real search tree

(c) cpviz propagation

Figure 5.19: cpviz output of the propagation: step 4


There is not much information here: only that the last variable tried was 1 and that we ended
up with a failure.

111

5.4. cpviz: how to visualize the search

Step 5:

node 0
x0 = 0

x0 = 0
node 1

node 4
x0 = 1

x1 = 2

x1 = 2

node 2

(a) cpviz tree

node 3

node 5

(c) cpviz propagation

(b) Real search tree

Figure 5.20: cpviz output of the propagation: step 5


Solution found.

Step 6:

node 0
x0 = 0

x0 = 0

node 1

node 4
x0 = 1

x1 = 2

x1 = 2

x0 = 1
node 6

node 2

node 3

node 5
x0 = 2
node 7

(a) cpviz tree

(b) Real search tree

(c) cpviz propagation

Figure 5.21: cpviz output of the propagation: step 6


Solution found.

112

Chapter 5. Dening search primitives: the n-queens problem

Step 7:

node 0
x0 = 0

x0 = 0

node 1

node 4
x0 = 1

x1 = 2

x1 = 2

x0 = 1
node 6

node 2

node 3

node 5
x0 = 2

x0 = 2
node 8

node 7

(a) cpviz tree

(b) Real search tree

(c) cpviz propagation

Figure 5.22: cpviz output of the propagation: step 7


End of propagation at node 8 and focus on variable 1 .

113

5.5. Basic working of the solver: the phases

Step 8:

node 0
x0 = 0

x0 = 0

node 1

node 4
x0 = 1

x1 = 2

x0 = 1

x1 = 2

node 6
node 2

node 3

node 5

x0 = 2

x0 = 2

node 8
node 7
x1 = 0

x1 = 0
node 10

node 9

(a) cpviz tree

(b) Real search tree

(c) cpviz propagation

Figure 5.23: cpviz output of the propagation: step 8


Failure. The rst failure was when 1 = 0.

5.5 Basic working of the solver: the phases


A phase corresponds to a type of (sub)search in the search tree16 . You can have several
phases/searches in your quest to nd 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 examples17 .
16

Well, sort of. Read on!


DecisionBuilders and Decisions are used internally and you cannot access them directly. To use
them, invoke the corresponding factory methods.
17

114

Chapter 5. Dening search primitives: the n-queens problem

5.5.1 DecisionBuilders and phases


DecisionBuilders (combined with SearchMonitors) are responsible of directing the
search at the current node in the search tree. The DecisionBuilder class controls the
search through its main Next() method:
virtual Decision* Next(Solver* const s) = 0;

It is a pure virtual method, so it must be implemented in all derived DecisionBuilder


classes.
To notify the solver that the DecisionBuilder has nished its job at the current
node, let Next() return NULL. The solver will then pass the control to the next
available DecisionBuilder or stop the search at this node if there are no more
DecisionBuilders left to deal with it.
We use DecisionBuilders in two scenarios18 :
1. The basic scenario is to divide the search sub-tree in two (preferably non overlapping)
search sub-trees. To do so, the DecisionBuilder returns a (pointer to a) Decision
through its Next() method.
The Decision class tells the solver what to do on the left branch (through its Apply()
method) and the right branch (through its Refute() method).
Some available DecisionBuilders that divide the search sub-tree in two are:
BaseAssignVariables: the main DecisionBuilder for IntVars. Its
the basic DecisionBuilder used for assigning values to IntVar variables.
When you invoke:
DecisionBuilder * const db = MakePhase(vars,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);

the returned (pointer to a) DecisionBuilder object is a (pointer to a)


BaseAssignVariables object. See the subsection The MakePhase() method
more in details below.
AssignVariablesFromAssignment: assigns values to variables from an
Assignment and if needed passes the hand to another DecisionBuilder to
continue the search. The factory method to create this DecisionBuilder is
MakeDecisionBuilderFromAssignment().
...
2. A DecisionBuilder doesnt have to split the search sub-tree in two: it can collect
data about the search, modify the model, etc. It also can solve the sub-tree with the help
of other DecisionBuilders and allow for nested searches.
In this case, take the appropriate action in the Next() method and return NULL to notify
the solver that the DecisionBuilder has nished its work at the current node.
18

One could argue that these two scenarios are not really mutually exclusive. Indeed, we divide the scenarios in two cases depending on whether the DecisionBuilder returns a Decision or not. Some
DecisionBuilders delegate the creation process of Decisions to other DecisionBuilders.

115

5.5. Basic working of the solver: the phases

Some examples of available DecisionBuilders that do some stuff at a node without


splitting the search sub-tree in two:
StoreAssignment and RestoreAssignment: respectively store and restore
Assignments during the search.
AddConstraintDecisionBuilder:
search.

adds a Constraint during the

ApplyBranchSelector: changes the way the branches are selected. For instance, the left branch can become the right branch and vice-versa. Have a look at
the Solver::DecisionModification enum for more.
LocalSearch: applies local search operators to nd a solution.
SolveOnce: stops the search as soon as it nds a solution with the help of another
DecisionBuilder.
NestedOptimize: optimizes the search sub-tree with the help of another
DecisionBuilder.
...
For your (and our) convenience, three more methods can be implemented:
virtual void AppendMonitors(Solver* const solver,
std::vector<SearchMonitor*>* const extras): to add some extra
SearchMonitors at the beginning of the search. Please note there are no checks at
this point for duplication.
virtual string DebugString() const:
method to give a name to your object.

the usual DebugString()

virtual void Accept(ModelVisitor* const visitor) const:


usual Accept() method to let you visit the model and take appropriate actions.

the

5.5.2 Decisions and DecisionVisitors


The Decision class together with the DecisionBuilder class implement the branching 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 notied 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:

116

Chapter 5. Dening search primitives: the n-queens problem

virtual void Apply(Solver* const s) = 0;

and the right branch through its Refute() method:


virtual void Refute(Solver* const s) = 0;

These two pure virtual methods must be implemented in every Decision class.
A Decision object is returned by a DecisionBuilder through its Next() method.
Two more methods can be implemented:
virtual string DebugString() const:
method.

the usual DebugString()

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
An obvious choice for a Decision class for IntVars is probably
AssignOneVariableValue. This class assigns a value to a variable in the left
branch and forbids this assignment in the right branch.
The constructor takes the variable to branch on and the value to assign to it:
AssignOneVariableValue(IntVar* const v, int64 val)
: var_(v), value_(val) {
}

var_ and value_ are local private copies of the variable and the value.
The Apply() and Refute() methods are straithforward:
void Apply(Solver* const s) {
var_->SetValue(value_);
}
void Refute(Solver* const s) {
var_->RemoveValue(value_);
}

DecisionVisitors
DecisionVisitors are attached to Decisions. The corresponding methods of the
DecisionVisitor are triggered just before a Decision is applied19 .
19

In this case, the methods are triggered when Decision objects are created and these objects are created just
before their Apply() method is called. See the subsection Visitors for more.

117

5.5. Basic working of the solver: the phases

When dealing with IntVars, two possibilities can be audited:


when a variable will be assigned a value, implement the
virtual void VisitSetVariableValue(IntVar* const var, int64 value);

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);

method. If start_with_lower_half is true, the decision to be applied is


var

otherwise it is
var >
There is also a default option:
virtual void VisitUnknownDecision();

In section 5.8, we present a concept that uses DecisionVisitors.

5.5.3 Combining DecisionBuilders


We propose two ways to combine DecisionBuilders:
Compose(): combine sequential searches, i.e. DecisionBuilders are used one
after the other;
Try(): combine parallel searches, i.e. DecisionBuilders are used in parallel.
You can of course combine the two.
Compose()
Creates a DecisionBuilder which sequentially composes DecisionBuilders.
Solver s(...);
...
DecisionBuilder * const db1 = ...;
DecisionBuilder * const db2 = ...;
DecisionBuilder * const db = s.Compose(db1, db2);

At each leaf of the search tree corresponding to the DecisionBuilder db1, the second
DecisionBuilder db2 is called.
The DecisionBuilder db search tree will be as follows:
118

Chapter 5. Dening search primitives: the n-queens problem

db1
search tree

db2

db2

db2

db2

search tree

search tree

search tree

search tree

db search tree
db = s.Compose(db1, db2);
This composition of DecisionBuilders frequently happens in scheduling. For instance, in
the section The DecisionBuilders where we try to solve a Job-Shop Problem, the solving process is done in two consecutive phases: rst we rank the tasks for each machine, then we schedule each task at its earliest start time. To do so, we Compose() two DecisionBuilders.
You can Compose() more than two DecisionBuilders. There are two more specic
methods to Compose() three and even four DecisionBuilders. And if that is not enough,
use
DecisionBuilder* Compose(const std::vector<DecisionBuilder*>& dbs);

where you can Compose() as many DecisionBuilders as you like!


Try()
Creates a DecisionBuilder which tries DecisionBuilders in parallel.
Solver s(...);
...
DecisionBuilder * const db1 = ...;
DecisionBuilder * const db2 = ...;
DecisionBuilder * const db = s.Try(db1, db2);

The DecisionBuilder db1 and the DecisionBuilder db2 are each called from the
top of the search tree one after the other.
The DecisionBuilder db search tree will be as follows:

119

5.5. Basic working of the solver: the phases

db1

db2

search tree

search tree

db search tree
db = s.Try(db1, db2);
This combination is handy to try a DecisionBuilder db1 which partially explores the
search space. If it fails, you can use the DecisionBuilder db2 as a backup.
As with Compose(), you can Try() up to four DecisionBuilders and use
DecisionBuilder* Try(const std::vector<DecisionBuilder*>& dbs);

for more.
Beware that Try(db1, db2, db3, db4) will give an unbalanced tree to the
right, whereas Try(Try(db1, db2), Try(db3, db4)) will give a balanced tree.

5.5.4 Nested searches


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
SolveOnce is a DecisionBuilder that searches a sub-tree with a given
DecisionBuilder and a set of SearchMonitors and returns the rst solution
encountered. If there are no solutions in this nested sub-tree, then SolveOnce will fail.
The factory method is MakeSolveOnce().
You have to invoke it with another
DecisionBuilder. You can add none or up to four SearchMonitors and if you want to
use more than four SearchMonitors, use
DecisionBuilder* MakeSolveOnce(DecisionBuilder* const db,
const std::vector<SearchMonitor*>& monitors);

120

Chapter 5. Dening search primitives: the n-queens problem

NestedOptimize
NestedOptimize is similar to SolveOnce except that it seeks for an optimal solution
instead of just a feasible solution. If there are no solutions in this nested tree, it fails.
The factory method is MakeNestedOptimize(). Again, you can use none or up to four
SearchMonitors and use the version with an std::vector<SearchMonitor*>:
DecisionBuilder* MakeNestedOptimize(DecisionBuilder* const db,
Assignment* const solution,
bool maximize,
int64 step,
const std::vector<SearchMonitor*>& monitors);

NestedOptimize is used for:


Testing.
Local search: see next chapter.
To control the backtracking.
...

5.5.5 The MakePhase() method more in details


We only discuss the MakePhase() methods for std::vector<IntVar*>. For
std::vector<IntervalVar*> and std::vector<SequenceVar*>, see section 6.3 in the next chapter.
The MakePhase() method is overloaded with different arguments and we discuss
most of them in this subsection.
The 2-steps approach
Variables and values are chosen in two steps: rst 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 nd the next variable to


branch on and IntValueStrategy is an enum with different strategies to nd the next
value to assign to this variable. We detail the different available strategies in the next section.

121

5.5. Basic working of the solver: the phases

Callbacks to the rescue


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 assign 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 callbacks20 accepted by MakePhase():
typedef ResultCallback1<int64, int64> IndexEvaluator1;
typedef ResultCallback2<int64, int64, int64> IndexEvaluator2;

IndexEvaluator1 allows to evaluate the next variable to branch on by giving


the index of this variable in the std::vector<IntVar*> for unbounded variables.
IndexEvaluator2 allows to evaluate the available values (second index) for the chosen
variable (rst index). In each case, the variable and the value chosen will correspond to the
smallest value returned by the evaluators. In case of a tie for the values, the last value with
the minimum score will be chosen. You can also provide an IndexEvaluator1 to break
the tie between several values. Last but not least, you can combine callbacks with the available
IntVarStrategy or IntValueStrategy strategies.
Ownership of the callbacks is always passed to the DecisionBuilder.
We detail some combinations:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator1* var_evaluator,
IndexEvaluator2* val_eval);

You provide both evaluators.


DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IntVarStrategy var_str,
IndexEvaluator2* val_eval,
IndexEvaluator1* tie_breaker);

You use a predened IntVarStrategy strategy to nd the next variable to branch on, provide your own callback IndexEvaluator2 to nd the next value to give to this variable and
an evaluator IndexEvaluator1 to break any tie between different values.
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator1* var_evaluator,
IntValueStrategy val_str);

This time, you provide an evaluator IndexEvaluator1 to nd the next variable but rely on
a predened IntValueStrategy strategy to nd the next value.
Several other combinations are provided.
20

122

If you want to know more about callbacks, see the section Callbacks in the chapter Under the hood.

Chapter 5. Dening search primitives: the n-queens problem

When the 2-steps approach isnt enough


Sometimes this 2-step approach isnt 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 rst
call of the selector, and results are cached. Next calls to the selector use the previous
computation, and are thus not up-to-date, e.g. some <variable, value> pairs may not be
possible due to propagation since the rst call.
CHOOSE_DYNAMIC_GLOBAL_BEST: Dynamic evaluation: Pairs are compared each
time a variable is selected. That way all pairs are relevant and evaluation is accurate. This
strategy runs in (number-of-pairs) at each variable selection, versus (1) in the static
version.

5.6 Out of the box variables and values selection primitives


To choose among the IntVar variables and the int64 values when branching, several variables 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);

123

5.6. Out of the box variables and values selection primitives

5.6.1 IntVarStrategy enums to select the next variable


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 rst 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 rst one is selected, rst
being dened 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 rst one is selected,
rst being dened 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 rst one is selected, rst
being dened 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 rst one is selected,
rst being dened 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 rst one is selected, rst being dened 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 rst one is selected, rst being dened 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 rst one is selected, rst being dened 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 rst one is selected, rst being dened 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 rst and the second values of the domain.
CHOOSE_PATH Selects the next unbound variable on a path, the path being dened by the
variables: vars[i] corresponds to the index of the next variable following variable i.
124

Chapter 5. Dening search primitives: the n-queens problem

Most of the strategies are self-explanatory except maybe for CHOOSE_PATH. This selection
strategy is most convenient when you try to nd simple paths (paths with no repeated vertices)
in a solution and the variables correspond to nodes on the paths. When a variable i is bound
(has been assigned a value), the path connects variable i to the next variable vars[i] as on
the gure below:
3
5
1

We have
vars = [, 0, 3, 1, , ]
where corresponds to a variable that wasnt assigned a value. We have vars[2] = 3,
vars[3] = 1 and vars[1] = 0. The next variable to be choosen will be 0 and in this case
vars[0] {2, 4, 5}. What happens if vars[0] is assigned the value 2? This strategy will pick up
another unbounded variable.
In general, the selection CHOOSE_PATH will happen as follow:
1. Try to extend an existing path: look for an unbound variable, to which some other variable points.
2. If no such path is found, try to nd a start node of a path: look for an unbound variable,
to which no other variable can point.
3. If everything else fails, pick the rst unbound variable.
We will encounter paths again in third part of this manual, when well discuss routing.

5.6.2 IntValueStrategy enums to select the next value


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 variable.

125

5.7. Customized search primitives

ASSIGN_CENTER_VALUE Selects the rst available value that is the closest to the center of
the domain of the selected variable. The center is dened as (min + max) / 2.
SPLIT_LOWER_HALF Splits the domain in two around the center, and forces the variable to
take its value in the lower half rst.
SPLIT_UPPER_HALF Splits the domain in two around the center, and forces the variable to
take its value in the upper half rst.

5.6.3 Results
You can nd the code in the les tutorials/cplusplus/chap5/phases1.cc
and tutorials/cplusplus/chap5/solver_benchmark.h.
Just for fun, we have developed a SolverBenchmark class to test different search
strategies. Statistics are recorded thanks to SolverBenchmarkStats. You can nd both
classes in the solver_benchmark.h header.
In phases1.cc, we test different combinations of the above strategies to nd the variables
and the values to branch on. You can try it for yourself and see that basically no predened
strategy outperforms any other.
The most fun (and most efcient) way to use or-tools is to dene your own selection strategies
and search primitives. This is the subject of the next section.

5.7 Customized search primitives


5.7.1 The basic search strategy visualized
5.7.2 First try: start from the center
5.7.3 Second try: dynamic variable selection
5.7.4 DecisionBuilders and Decisions more in details
BaseAssignVariables as an example

5.8 Breaking symmetries with SymmetryBreakers


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
126

Chapter 5. Dening search primitives: the n-queens problem

mirror Golomb rulers in section 3.8.1 page 65. This time, we will use SymmetryBreakers.
As their name implies, their role is to break symmetries. In contrast to explicitly adding symmetry breaking constraints in the model before the solving process, SymmetryBreakers add
them automatically when required during the search, i.e. on the y.

5.8.1 The basic idea


The basic idea is quite simple. Consider again the 4-queens problem. Figure 5.24 represents
two symmetric solutions.

(a) Solution 1

(b) Solution 2

Figure 5.24: Two symmetric solutions for the 4-queens problem.


These two solutions are symmetric along a vertical axis dividing the square in two equal parts.
If we have 1 = 1 (or 1 = 0) during the search, we know that we dont have to test a solution
with 2 = 1 (or 2 = 0) as every solution with 1 = 1 (1 = 0) has an equivalent symmetric
solution with 2 = 1 (2 = 0).
You can tell the CP solver not to visit the branch 2 = if during the search we already have
tried to set 1 = . 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 between the Decision and a SymmetryBreaker, the SymmetryManager will, upon refutation 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
You can nd the code in the le tutorials/cplusplus/chap5/nqueens7.cc.
Lets create a SymmetryBreaker for the vertical axial symmetry. Because the square has
lots of symmetries, we introduce a helper method to nd the symmetric indices of the variables
and the symmetric values for a given variable:
int symmetric(int index) const { return size_ - 1 - index}

where size_ denotes the number of variables and the range of possible values ([0, size_ 1])
in our model. Figure 5.25 illustrates the returned indices by the symmetric() method.
127

5.8. Breaking symmetries with SymmetryBreakers

Figure 5.25: The indices returned by the symmetric() method.

128

Chapter 5. Dening search primitives: the n-queens problem

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);
}

FindWithDefault() is dened in the header base/map-util.h.


Given an
std::map<IntVar*, int> like indices_, it returns the corresponding int if it nds
the IntVar *. If it doesnt nd the IntVar *, it returns the default argument given, 1 in
this case.
To do the converse translation, we use the Var() method:
IntVar* Var(int index) const {
return vars_[index];
}

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_;
};

Now, we can specialize it for each symmetry we want to break.


How do we tell a SymmetryBreaker to notify the SymmetryManager to add a corresponding constraint upon refutation of a given Decision? For the n-queens problem, we can use the AddIntegerVariableEqualValueClause() method of the
SymmetryBreaker class. Given the assignation of a value to an IntVar, give this method
129

5.8. Breaking symmetries with SymmetryBreakers

the corresponding symmetric assignation. We call this corresponding assignment a clause. This
clause only makes sense if the Decision assigns a value to an IntVar and this is why we
declare the corresponding clause only in the VisitSetVariableValue() method of the
SymmetryBreaker. All this might sound complicated but it is not:
// Vertical axis symmetry
class SY : public NQueenSymmetry {
public:
SY(Solver* const s, const std::vector<IntVar*>& vars) :
NQueenSymmetry(s, vars) {}
virtual ~SY() {}
virtual void VisitSetVariableValue(IntVar* const var, int64 value) {
const int index = Index(var);
IntVar* const other_var = Var(symmetric(index));
AddIntegerVariableEqualValueClause(other_var, value);
}
};

Given an IntVar* var that will be given the value value by a Decision during the
search, we ask the SymmetryManager to avoid the possibility that the variable other_var
could be assigned the same value value upon refutation of this Decision. This means that
the other_var variable will never be equal to value in the opposite branch of the search
tree where var is different than value. In this manner, we avoid searching a symmetrical
part of the search tree we have already explored.
What happens if another type of Decisions are returned by the DecisionBuilder during
the search? Nothing. The refutation of the clause will only be applied if a Decision triggers
a VisitSetVariableValue() callback.
The SymmetryBreaker class denes two other clauses:
AddIntegerVariableGreaterOrEqualValueClause(IntVar* const
var, int64 value) and
AddIntegerVariableLessOrEqualValueClause(IntVar* const
var, int64 value).
Their names are quite explicit and tell you what their purpose is. These methods would t
perfectly within a VisitSplitVariableDomain() call for instance.

5.8.3 RevAlloc
Whenever you dene your own subclass of BaseObject (and a SymmetryBreaker is a
BaseObject), it is good practice to register the given object as being reversible to the solver.
That is, the solver will take ownership of the object and delete it when it backtracks out of the
current state. To register an object as reversible, you invoke the RevAlloc() method of the
solver:
Solver s("nqueens");
...
NQueenSymmetry* const sy = s.RevAlloc(new SY(&s, queens));

130

Chapter 5. Dening search primitives: the n-queens problem

RevAlloc() returns a pointer to the newly created and registered object. You can thus invoke
this method with arguments in the constructor of the constructed object without having to keep
a pointer to this object.
The solver will now take care of your object. If you have an array of objects that are subclasses
of BaseObject, IntVar, IntExpr and Constraint, you can register your array with
RevAllocArray(). This method is also valid for arrays of ints, int64, uint64 and
bool. The array must have been allocated with the new[] operator.
If you take a look at the source code, you will see that the factories methods call RevAlloc()
to pass ownership of their objects to the solver.

5.8.4 The SymmetryManager


Because the n-queens problem is dened on a square, we have a lots of symmetries we can
avoid:
Vertical axis symmetry: we already dened the SY class;
Horizontal axis symmetry: class SX;
First diagonal symmetry: class SD1;
Second diagonal symmetry: class SD2;
1/4 turn rotation symmetry: class R90;
1/2 turn rotation symmetry: class R180;
3/4 turn rotation symmetry: class R270.
We
store
the
corresponding
SymmetryBreaker
std::vector<SymmetryBreaker*>:

objects

in

an

std::vector<SymmetryBreaker*> breakers;
NQueenSymmetry* const sy = s.RevAlloc(new SY(&s, queens));
breakers.push_back(sy);
NQueenSymmetry* const sx = s.RevAlloc(new SX(&s, queens));
breakers.push_back(sx);
NQueenSymmetry* const sd1 = s.RevAlloc(new SD1(&s, queens));
breakers.push_back(sd1);
NQueenSymmetry* const sd2 = s.RevAlloc(new SD2(&s, queens));
breakers.push_back(sd2);
NQueenSymmetry* const r90 = s.RevAlloc(new R90(&s, queens));
breakers.push_back(r90);
NQueenSymmetry* const r180 = s.RevAlloc(new R180(&s, queens));
breakers.push_back(r180);
NQueenSymmetry* const r270 = s.RevAlloc(new R270(&s, queens));
breakers.push_back(r270);

We then create a SymmetryManager:


SearchMonitor* const symmetry_manager = s.MakeSymmetryManager(breakers);

and add this SearchMonitor to the other SearchMonitors:

131

5.9. Summary

std::vector<SearchMonitor*> monitors;
...
monitors.push_back(symmetry_manager);
...
DecisionBuilder* const db = s.MakePhase(...);
...
s.Solve(db, monitors);

These seven SymmetryBreakers are enough to avoid duplicate solutions in the search, i.e.
they force the solver to nd only unique solutions up to a symmetry.

5.8.5 Results
Lets compare the time and the search trees again.
[TO BE DONE]

5.9 Summary

132

CHAPTER

SIX

LOCAL SEARCH: THE JOB-SHOP PROBLEM

We enter here in a new world where we dont try to solve a problem to optimality but seek a
good solution. Remember from sub-section 1.3.4 that some problems1 are hard to solve. No
matter how powerful our computers are2 , we quickly hit a wall if we try to solve these problems
to optimality. Do we give up? Of course not! If it is not possible to compute the best solutions,
we can try to nd very good solutions. Enter the fascinating world of (meta-)heuristics and
local search.
Throughout this chapter, we will use the job-shop problem as an illustrative example.
The job-shop problem is a typical difcult scheduling problem. Dont worry if you dont
know anything about scheduling or the job-shop problem, we explain this problem in details.
Scheduling is one of the elds where constraint programming has been applied with great
success. It is thus not surprising that the CP community has developed specic tools to solve
scheduling problems. In this chapter, we introduce the ones that have been implemented in
or-tools
Overview:

We start by describing the job-problem, the disjunctive model to represent it, two formats to encode job-shop problem instances (JSSP and Taillard) and our rst exact results. We next make a
short stop to describe the specic primitives implemented in or-tools to solve scheduling problems. For instance, instead of using IntVar variables, we use the dedicated IntervalVars
and SequenceVars.
After these preliminaries, we present local search and how it is implemented in the or-tools
library. Beside the job-shop problem, we use a dummy problem to watch the inner mechanisms
of local search in or-tools in action:
We minimize 0 + 1 + . . . + 1 where each variable has the same domain
[0, 1]. To complicate things a little bit, we add the constraint 0 1.
Once we understand how to use local search in or-tools, we use basic
LocalSearchOperators to solve the job-shop problem and compare the exact and approx1

Actually, most interesting problems!


But watch out for the next generations of computers:
(http://en.wikipedia.org/wiki/Molecular_computer) and computers based
(http://en.wikipedia.org/wiki/Quantum_computer)!
2

on

molecular
quantum

computers
mechanics

imate results. Finally, to speed up the local search algorithm, we use LocalSearchFilters
for the dummy problem.
Prerequisites:

Basic knowledge of C++.


Basic knowledge of Constraint Programming (see chapter 1).
Basic knowledge of the Constraint Programming Solver (see chapter 2).
Basic knowledge about how to dene an objective function (see section 3.3).
Section 5.3 on the inner working of the solver helps but is not mandatory.
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap6.


The les inside this directory are:
Makefile.
jobshop.h: This le contains the JobShopData class that records the data for jobshop problem instances. This le is used throughout all the job-shop examples.
report_jobshopdata.cc: a simple program to report the content of job-shop problem instances in JSSP or Taillards formats.
abz9: a job-shop problem instance in JSSP format.
20_5_01_ta001.txt: a job-shop problem instance in Taillards format.
first_example_jssp.txt: our rst example in JSSP format.
jobshop.cc: A basic exact implementation of the disjunctive model with
IntervalVar and SequenceVar variables.
dummy_ls.cc: A very basic example to understand the API of Local Search in ortools.
jobshop_ls.h: two basic LocalSearchOperators for the job-shop problem.
jobshop_ls1.cc:
A basic implementation of Local Search with the
SwapIntervals LocalSearchOperator.
jobshop_ls2.cc:
A basic implementation of Local Search with the
ShuffleIntervals LocalSearchOperator.
jobshop_ls3.cc: A basic implementation of Local Search with both the
SwapIntervals and ShuffleIntervals LocalSearchOperators. We use
also local search to nd an initial solution.
dummy_ls_filtering.cc: The basic example extended with ltering.

134

Chapter 6. Local search: the job-shop problem

The les of this chapter are NOT the same as the ones in the example directory even if they
were inspired by them. In particular, job-shop instances with only one task per job are accepted
(not that this is extremely useful but...).
Content:

6.1 The job-shop problem, the disjunctive model and


benchmark data
You can nd the code in the les jobshop.h and report_jobshopdata.cc and the
data in the les abz9, 20_5_01_ta001.txt and first_example_jssp.txt.
We describe the job-shop problem, a rst model and the benchmark data. The job-shop
problem belongs to the intractable problems ( NP). Only few very special cases can be
solved in polynomial time (see [Garey1976] and [Kis2002]). The denition of this fascinating
problem is not that complicated but you probably will need some extra attention if this is your
rst encounter with it. Once you grasp its denition, the next subsections should ow easily.

6.1.1 Description of the problem


In the classical job-shop problem there are jobs that must be processed on 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 to denote the th task of job .
Given a set of jobs, a set of machines and a set of tasks, we denote by the number
of tasks for a given job . To each task corresponds an ordered pair ( , ): the task
needs to be processed on machine for a period of units of time.
Here is an example with = 3 machines and = 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 02 which must be processed on machine
02 = 1 during 02 = 4 units of time and task 12 which must be processed on machine
02 = 2 during 02 = 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 00 on machine 0 must be processed before task 10 on machine
1 that itself must be processed before task 20 on machine 2. It is not mandatory but most of
3
4

Tasks are also called operations.


We use a slightly different and we hope easier notation than the ones used by the scheduling community.

135

6.1. The job-shop problem, the disjunctive model and benchmark data
the literature and benchmark data are concerned by problems where each job is made of
tasks and each task in a job must be processed on a different machine, i.e. each job needs to be
processed exactly once on each machine.
We seek a schedule (solution) that minimizes the makespan (duration) of the whole process.
The makespan is the duration between the start of the rst task (across all machines) and the
completion of the last task (again across all machines). The classical notation for the makespan
is max .
The makespan can be dened as
max = max{ + }

or equivalently as the maximum time needed among all jobs to be completely processed. Recall
that denotes the number of tasks for job and that we count starting from 0. 1, denotes
thus the starting time of the last task of job and we have
max = max { 1, + 1, }
1,

Lets try to nd a schedule for our example. Suppose you want to favour job 1 because not only
did you see that it is the longest job to process but its last task takes 4 units of time. Here is the
Gantt chart of a possible schedule:

11111
00000
1111111
0000000
11111
00000
1111111
0000000
machine 0 00000
11111 000000000
1111111
0000000
111111111 00000
machine 1
111111
000000
111 11111 11111
000 00000 00000
111111111 00000
000000000 11111
111 11111 11111
000 00000 00000
11111
111111
000000
machine 2
111
000
11111
00000
2

11111
00000
4

10

12

1
0
1
0
1
0
1
0
1
0
1
0

1
0
1
0

job 0
job 1
job 2

This is a feasible schedule since tasks within every job are processed one after the other in the
right sequence and each task is processed on the right machine. The makespan is 12 units of
time. Can we do better? Focusing on one job is probably not the best strategy. Here is an
optimal solution:

11111111111
00000000000
11111111111
00000000000
machine 0 0000000
11111110000 000000000
1111 111111111
11111
00000 11111
111111 11 00000
000000 0000000000
machine 1
11111
00000
11111
00000
111111111
0
11111
00000
111111 11 11111
000000 00 00000
machine 2
11
00
11111
00000
2

11111
00000
8

10

1
0
1
0
1
0
1
0
1
0
1
0
12

1
0
1
0

job 0
job 1
job 2

Its makespan is 11 units of time.


How can we simply describe a schedule? Let us dene as the starting time of task .
A feasible schedule can then be dened as a set5 of non negative integers { } such that the
denition of a job-shop problem is respected. If we only consider schedules where all tasks
5

136

And a correspondence rule between those integers and the tasks.

Chapter 6. Local search: the job-shop problem


are completely left shifted on the Gantt chart6 , we can dene a feasible schedule by giving the
sequence of jobs processed on each machine.
The rst 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 doesnt really give any insight
into the problem7 . The disjunctive graph allows a better understanding of the structure of the
problem.

6.1.2 The disjunctive graph


Figure 6.1 represents the disjunctive graph of our example. The graph is = (, ) where

e1

(0,2)

(1,2)

ma
chi
n

machine 0

(0,3)

(1,4)

(2,1)

(2,2)
m

e2
hin
ac

job 0
(1,4)

(2,3)

job 1
job 2

Figure 6.1: A disjunctive graph.


is the set of vertices corresponding to the tasks. Two ctive vertices and 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 and have weight 0.
are the conjunctive arcs between the th and ( + 1)th tasks of a job. We also add
conjunctive arcs from to the rst task of every job and from the last task of every job to
. These arcs are plain in gure 6.1.
are the disjunctive arcs between task to be processed on the same machine. These arcs
are dotted or dashed in gure 6.1.
6
A rigorous denition 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!

137

6.1. The job-shop problem, the disjunctive model and benchmark data

To determine a schedule we have to dene an ordering of all tasks processed on each machine.
This can be done by orienting all dotted or dashed edges such that each clique corresponding
to a machine becomes acyclic8 .
Our rst schedule is represented in the next gure.
(0,3)

(1,2)

(0,2)

(2,2)

(2,1)

(1,4)

job 0
(1,4)

job 1
job 2

(2,3)

We also want to avoid cycles between disjunctive and conjunctive arcs because they lead to
infeasible schedules. A feasible schedule is represented by a directed acyclic disjunctive graph.
In fact, the opposite is also true. A complete orientation of the edges in denes a feasible
schedule if and only if the resulting directed disjunctive graph is acyclic.
The makespan is given by the longest weighted path from to . This path - thickened in the
next gure - is called the critical path.
(0,3)

(1,2)

(0,2)

(2,2)

(2,1)

(1,4)

job 0
(1,4)

(2,3)

job 1
job 2

Its length is 0 + 4 + 4 + 2 + 2 + 0 = 12.


We can now dene the job-shop problem as a graph problem: nd a complete orientation of
the edges of a disjunctive graph such that the resulting directed graph is acyclic and the longest
weighted path from to is minimized. We will use this representation of the problem to
design our rst model.

6.1.3 The disjunctive model


This model is a straightforward translation of the denition of a job-shop problem and its
disjunctive graph reprensentation.
We again rely on the The three-stage method: describe, model and solve. What are the decision
variables? We use the variables to store the starting time of task of job . We could use
two ctive variables corresponding to the ctive vertices and but this is not necessary.
To simplify the notation, we will use the notation where denotes a vertex (a task) of the
disjunctive graph. We use the same simplied notation for the processing times () and the
machine ids ().
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.

138

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:
(, ) such that = and = :
+

These constraints are called conjunctive constraints.


disjunctive edges modelling the order in which tasks have to be processed on a single
machine:
(, ) such that =
+

or +

These constraints are called disjunctive constraints. They forbid cycles in a clique corresponding to a machine9 .
What is the objective function? The objective function (the makespan) max doesnt correspond to a variable of the model. We have to construct its value. Because we minimize the
makespan, we can use a little trick. Let be the set of all end tasks of all jobs. In our example,
= {20 (2, 2), 21 (1, 4), 12 (2, 3)}. The makespan must be greater than the overall time it
takes to process these tasks:
:

max

+ .

Here is the model10 :


min
s.t.:

max
max
+
+

+
or +
0


(, )
(, ) : =
{, }

We will implement and solve this model in the next section but rst we need to read and process
the data representing instances of job-shop problems.
9

Here is why. Consider the following situation


t1

t2

t3

We have 1 +1
2 , 2 +2
3 and 3 +3
1 . Add these three inequalities and you obtain 1 +2 +3 0.
This is impossible if one of the is greater than 0 as every 0.
10
It is not obvious that this model produces optimal solutions that are feasible schedules but it can be shown
that it does.

139

6.1. The job-shop problem, the disjunctive model and benchmark data

6.1.4 The data and le formats


To collect the data, we use two different le formats: JSSP and professor Taillards format.
In the directory data/jobshop, you can nd data les for the job-shop problem11 . The le
jobshop.h lets you read both formats and store the data into a JobshopData class. We
will use this class throughout this chapter.
JSSP format
JSSP stands for Job Shop Scheduling Problem. Lets consider the beginning of le abz9:
+++++++++++++++++++++++++++++
instance abz9
+++++++++++++++++++++++++++++
Adams, Balas, and Zawack 15 x 20 instance
20 15
6 14 5 21 8 13 4 11 1 11 14 35 13 20
1 35 5 31 0 13 3 26 6 14 9 17 7 38
0 30 4 35 2 40 10 35 6 30 14 23 8 29
...

(Table 1, instance 9)
11 17 10 18 12 11
12 20 10 19 13 12
13 37 7 38 3 40

...
...
...

The rst line of real data is


20 15

This instance has 20 jobs to process on 15 machines. Each job is composed of exactly 15 tasks.
Each job corresponds to a line:
6 14

5 21

8 13

4 11

1 11 14 35 13 20 11 17 10 18 12 11

...

Each pair ( , ) corresponds to a task. For this rst job, the rst task needs 14 units of time
on machine 6, the second task needs 21 units of time on machine 5 and so on.
As is often the case, there is a one to one correspondence between the tasks and the machines.
Taillards format
Lets consider the beginning of le 20_5_01_ta001.txt:
20
5
873654221
0
468
54 79 16 66 58
1
325
11

We
copied
the
les
abz9
and
20_5_01_ta001.txt
manual/tutorials/cplusplus/chap6 for your convenience.

140

in

the

directory

Chapter 6. Local search: the job-shop problem

83 3 89 58 56
2
923
15 11 49 31 20
3
513
71 99 15 68 85
...

This format is made for ow-shop problems and not job-shop problems. The two rst lines
indicate that this instance has 20 jobs to be processed on 5 machines. The next line (873654221)
is a random seed number. The jobs are numbered from 0 to 19. The data for the rst job are:
0
468
54 79 16 66 58

0 is the id or index of the rst job. The next number is not important for the job-shop problem.
The numbers in the last line correspond to processing times. We use the trick to assign these
times to machines 0, 1, 2 and so on. So job 0 is actually
[(0, 54), (1, 79), (2, 16), (3, 66), (4, 58)]
Because of this trick, one can not easily dene our problem instance above in this format and
we dont attempt to do it.
You can nd anything you ever wanted to know and more about this format in [Taillard1993].
JobshopData
The JobshopData class is a simple container for job-shop problem instances. It is dened
in the le jobshop.h. Basically, it wraps an std::vector<std::vector<Task> >
container where Task is a struct dened as follows:
struct Task {
Task(int j, int m, int d) : job_id(j), machine_id(m), duration(d) {}
int job_id;
int machine_id;
int duration;
};

Most part of the JobshopData class is devoted to the reading of both le formats.
The data le is processed at the creation of a JobShopData object:
explicit JobShopData(const string& filename) :
...
{
FileLineReader reader(filename_.c_str());
reader.set_line_callback(NewPermanentCallback(
this,
&JobShopData::ProcessNewLine));
reader.Reload();
if (!reader.loaded_successfully()) {

141

6.1. The job-shop problem, the disjunctive model and benchmark data

LOG(FATAL) << "Could not open job-shop file " << filename_;
}

To parse the data le and load the tasks for each job, we use a FileLineReader (declared
in base/filelinereader.h). In its Reload() method, it triggers the callback void
ProcessNewLine(char* const line) to read the le one line at a time
The public methods of the JobShopData class are
the getters:
machine_count(): number of machines;
job_count(): number of jobs;
name(): instance name;
horizon(): the sum of all durations (and a trivial upper bound on the makespan).
const std::vector<Task>& TasksOfJob(int job_id) const:
returns a reference to the corresponding std::vector<Task> of tasks.
two methods to report the content of the data le parsed:
void Report(std::ostream & out);
void ReportAll(std::ostream & out);

Just for fun, we have written the data le corresponding to our example above in JSSP format
in the le first_example_jssp.txt:
+++++++++++++++++++++++++++++
instance tutorial_first_jobshop_example
+++++++++++++++++++++++++++++
Simple instance of a job-shop problem in JSSP format
to illustrate the working of the or-tools library
3 3
0 3 1 2 2 2
0 2 2 1 1 4
1 4 2 3

The ReportAll() method outputs:


Job-shop problem instance in JSSP format read from file
first_example_jssp.txt
Name: tutorial_first_jobshop_example
Jobs: 3
Machines: 3
==========================================
Job: 0
(0,3) (1,2) (2,2)
Job: 1
(0,2) (2,1) (1,4)
Job: 2
(1,4) (2,3)

142

Chapter 6. Local search: the job-shop problem

The le report_jobshopdata.cc contains a simple program to test the content of data


les for the job-shop problem.

6.2 An implementation of the disjunctive model


You can nd the code in the le jobshop.cc
les first_example_jssp.txt and abz9.

and

the

data

in

the

Scheduling is one of the elds where Constraint Programming is heavily used and where
specialized constraints and variables have been developed12 . In this section, we will implement
the disjunctive model with dedicated variables (IntervalVar and SequenceVar) and
constraints (IntervalBinaryRelation and DisjunctiveConstraint).
Last but not least, we will see our rst real example of combining two DecisionBuilders
in a top-down fashion.

6.2.1 The IntervalVar variables


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 xed
duration intervals:
const std::string name = StringPrintf("J%dM%dI%dD%d",
task.job_id,
task.machine_id,
task_index,
task.duration);
IntervalVar* const one_task =
solver.MakeFixedDurationIntervalVar(0,
horizon,
task.duration,
false,
name);

The rst two arguments of MakeFixedDurationIntervalVar() are a lower and an upper bound on the starting time of the IntervalVar. The fourth argument is a bool that
12

The next section is entirely dedicated to scheduling in or-tools.

143

6.2. An implementation of the disjunctive model

indicates if the IntervalVar can be unperformed or not. Unperformed IntervalVars


simply dont exist anymore. This can happen when the IntervalVar is not consistent anymore. By setting the argument to false, we dont allow this variable to be unperformed.
To be able to easily retrieve the tasks corresponding to a job or a machine, we use two matrices:
std::vector<std::vector<IntervalVar*> > jobs_to_tasks(job_count);
std::vector<std::vector<IntervalVar*> >
machines_to_tasks(machine_count);

and populate them:


// Creates all individual interval variables.
for (int job_id = 0; job_id < job_count; ++job_id) {
const std::vector<JobShopData::Task>& tasks = data.TasksOfJob(job_id);
for (int task_index = 0; task_index < tasks.size(); ++task_index) {
const JobShopData::Task& task = tasks[task_index];
CHECK_EQ(job_id, task.job_id);
const string name = ...
IntervalVar* const one_task = ...
jobs_to_tasks[task.job_id].push_back(one_task);
machines_to_tasks[task.machine_id].push_back(one_task);
}
}

We will create the SequenceVar variables later when we will add the disjunctive constraints.

6.2.2 The conjunctive constraints


Recall that the conjunctive constraints ensure the sequence order of tasks inside a job is respected. 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.

6.2.3 The disjunctive constraints and SequenceVars


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);

144

Chapter 6. Local search: the job-shop problem

A SequenceVar variable is a variable whose domain is a set of possible orderings of the


IntervalVars. It allows ordering tasks.
You can only create13 SequenceVars with the MakeSequenceVar() method of the
DisjunctiveConstraint class:
std::vector<SequenceVar*> all_sequences;
for (int machine_id = 0; machine_id < machine_count; ++machine_id) {
const string name = StringPrintf("Machine_%d", machine_id);
DisjunctiveConstraint* const ct =
solver.MakeDisjunctiveConstraint(machines_to_tasks[machine_id], name);
solver.AddConstraint(ct);
all_sequences.push_back(ct->MakeSequenceVar());
}

6.2.4 The objective function


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());
}
// Objective: minimize the makespan (maximum end times of all tasks)
// of the problem.
IntVar* const objective_var = solver.MakeMax(all_ends)->Var();
OptimizeVar* const objective_monitor =
solver.MakeMinimize(objective_var, 1);

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().

6.2.5 The DecisionBuilders


The solving process is done in two sequential phases: rst 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 the 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.

145

6.2. An implementation of the disjunctive model

First, we dene the phase to rank the tasks on all machines:


DecisionBuilder* const sequence_phase =
solver.MakePhase(all_sequences, Solver::SEQUENCE_DEFAULT);

Second, we dene the phase to schedule the ranked tasks. This is conveniently done by xing
the objective variable to its minimum value:
DecisionBuilder* const obj_phase = solver.MakePhase(objective_var,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);

Third, we combine both phases one after the other in the search tree with the Compose()
method:
DecisionBuilder* const main_phase =
solver.Compose(sequence_phase, obj_phase);

6.2.6 The search and rst results


We use the usual SearchMonitors:
// Search log.
const int kLogFrequency = 1000000;
SearchMonitor* const search_log =
solver.MakeSearchLog(kLogFrequency, objective_monitor);
SearchLimit* limit = NULL;
if (FLAGS_time_limit_in_ms > 0) {
limit = solver.MakeTimeLimit(FLAGS_time_limit_in_ms);
}
SolutionCollector* const collector =
solver.MakeLastSolutionCollector();
collector->Add(all_sequences);
collector->AddObjective(objective_var);

and launch the search:


// Search.
if (solver.Solve(main_phase,
search_log,
objective_monitor,
limit,
collector)) {
for (int m = 0; m < machine_count; ++m) {
LOG(INFO) << "Objective value: " <<
collector->objective_value(0);
SequenceVar* const seq = all_sequences[m];
LOG(INFO) << seq->name() << ": "
<< IntVectorToString(collector->ForwardSequence(0, seq), ", ");
}
}

146

Chapter 6. Local search: the job-shop problem

collector->ForwardSequence(0, seq) is a shortcut to return the


std::vector<int> containing the order in which the tasks are processed on each
machine for solution 0 (which is the last and thus optimal solution).
This order corresponds exactly to the job ids because the tasks are sorted by job id on each
machine. The result for our instance is:
[09:21:44] jobshop.cc:150: Machine_0: 0, 1
[09:21:44] jobshop.cc:150: Machine_1: 2, 0, 1
[09:21:44] jobshop.cc:150: Machine_2: 1, 0, 2

which is exactly the optimal solution depicted in the previous section.


What about getting the start and end times for all tasks?
Declare the corresponding variables in the SolutionCollector:
SolutionCollector* const collector =
solver.MakeLastSolutionCollector();
collector->Add(all_sequences);
collector->AddObjective(objective_var);
for (int seq = 0; seq < all_sequences.size(); ++seq) {
const SequenceVar * sequence = all_sequences[seq];
const int sequence_count = sequence->size();
for (int i = 0; i < sequence_count; ++i) {
IntervalVar * t = sequence->Interval(i);
collector->Add(t->StartExpr()->Var());
collector->Add(t->EndExpr()->Var());
}
}

and then print the desired information:


for (int m = 0; m < machine_count; ++m) {
SequenceVar* const seq = all_sequences[m];
std::ostringstream s;
s << seq->name() << ": ";
const std::vector<int> & sequence =
collector->ForwardSequence(0, seq);
const int seq_size = sequence.size();
for (int i = 0; i < seq_size; ++i) {
IntervalVar * t = seq->Interval(sequence[i]);
s << "Job " << sequence[i] << " (";
s << collector->Value(0,t->StartExpr()->Var());
s << ",";
s << collector->Value(0,t->EndExpr()->Var());
s << ") ";
}
s.flush();
LOG(INFO) << s.str();
}

The result for our instance is:

147

6.3. Scheduling in or-tools

...: Machine_0: Job 0 (0,3)


...: Machine_1: Job 2 (0,4)
...: Machine_2: Job 1 (5,6)

Job 1 (3,5)
Job 0 (4,6)
Job 0 (6,8)

Job 1 (6,10)
Job 2 (8,11)

Lets try the abz9 instance:


Sol. nbr.
87
107

Obj. val.
1015
986

Branches
131 733
6 242 194

Time (s)
26,756
1088,487

After a little bit more than 18 minutes (1088,487 seconds), the CP solver nds its 107 th solution
with an objective value of 986. This is quite far from the optimal value of... 679 [Adams1988].
An exact procedure to solve the job-shop problem is possible but only for small instances
and with specialized algorithms. We prefer to quickly nd (hopefully) good solutions (see
section 6.7). We will discover next what specialized tools are available in our library to handle
scheduling problems.

6.3 Scheduling in or-tools


Scheduling problems deal with the allocation of resources and the sequencing of tasks to produce 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 [ref]. 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 variables, you can use them in a variety of different contexts but for the moment keep in mind this
modelling association.
IntervalVars
An IntervalVar variable represents an integer interval variable. It is often used in scheduling to represent a task because it has:
a starting time: ;
a duration: and
148

Chapter 6. Local search: the job-shop problem


an ending time: .
, and 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 cannot 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.
Dont use
IntExpr* StartExpr();
IntExpr* DurationExpr();
IntExpr* EndExpr();
if the corresponding IntervalVar variable is unperformed!
The IntervalVar can be virtually conceptualized14 as in the next gure:
DurationMax()
DurationRange
DurationMin()
StartMin()

StartMax()

EndMin()

StartRange

EndMax()
EndRange

and you have the following setters and getters:


virtual int64 StartMin() const = 0;
virtual int64 StartMax() const = 0;
virtual void SetStartMin(int64 m) = 0;
virtual void SetStartMax(int64 m) = 0;
virtual void SetStartRange(int64 mi, int64 ma) = 0;
virtual int64 DurationMin() const = 0;
virtual int64 DurationMax() const = 0;
virtual void SetDurationMin(int64 m) = 0;
virtual void SetDurationMax(int64 m) = 0;
virtual void SetDurationRange(int64 mi, int64 ma) = 0;
14

The implementation optimizes different cases and thus doesnt necessarily correspond to the gure. Read on.

149

6.3. Scheduling in or-tools

virtual int64 EndMin() const = 0;


virtual int64 EndMax () const = 0;
virtual void SetEndMin (int64 m) = 0;
virtual void SetEndMax (int64 m) = 0;
virtual void SetEndRange (int64 mi, int64 ma) = 0;
As usual, the IntervalVar class is an abstract base class and several specialized sub-classes
exist. For instance, we saw the FixedDurationPerformedIntervalVar class in the
previous section (created with MakeFixedDurationIntervalVar()).
To create IntervalVar variables, use the factory methods provided by the solver. For instance:
IntervalVar* Solver:MakeFixedInterval(int64 start,
int64 duration,
const string& name);
IntervalVar* Solver::MakeFixedDurationIntervalVar(int64 start_min,
int64 start_max,
int64 duration,
bool optional,
const string& name);
void Solver::MakeFixedDurationIntervalVarArray(int count,
int64 start_min,
int64 start_max,
int64 duration,
bool optional,
const string& name,
std::vector<IntervalVar*>* array);

The rst factory method creates a FixedInterval:


its starting time, duration and ending time are all xed.
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 xed
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, ..., 1) appended, like so:
name0, name1, name2, ... .

Several other factory methods are dened in the le interval.cc.


Variables that perform... or not
An important aspect of IntervalVars is optionality. An IntervalVar can be performed
or not. If unperformed, then it simply does not exist (and its characteristics are meaningless). An IntervalVar is automatically marked as unperformed when it is not consistent

150

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();

The corresponding IntExpr acts like a 0 1 IntervalVar15 . If its minimum value is 1,


the corresponding IntervalVar variables must be performed. If its maximal value is 0, the
corresponding IntervalVar is unperformed and if min = 0 and max = 1, the corresponding
IntervalVar might be performed.
The use of an IntExpr allows expressiveness and the use of sophisticated constraints.
As we have seen, if the IntervalVar is unperformed, we cannot use StartExpr(),
DurationExpr() and EndExpr(). You can however call their safe versions:
IntExpr* SafeStartExpr(int64 unperformed_value);
IntExpr* SafeDurationExpr(int64 unperformed_value);
IntExpr* SafeEndExpr(int64 unperformed_value)
If the variable is performed, these expressions will return their exact values, otherwise they will
return obvious values (see the le sched_expr.cc for more details). For instance:
IntExpr * start_exp = interval_var->SafeStartExpr(-1);
IntVar * start_var = start_exp->Var();
LG << "Minimum start value is " << start_var->Min();

will give you the exact minimal starting value if the variable is performed, the minimum between its minimal value and -1 if the variable may be performed and -1 if the variable is
unperformed.
SequenceVars
A SequenceVar variable is a variable which domain is a set of possible orderings of
IntervalVar variables. Because it allows the ordering of IntervalVar (tasks), it is often
used in scheduling. And for once it is not an abstract class! This is because these variables are
among the less rened variables in or-tools. They also have the least number of methods.
Basically, this class contains an array of IntervalVars and a precedence matrix indicating
15

Actually, it is an IntervalVar!

151

6.3. Scheduling in or-tools


how the IntervalVars are ranked. You can conceptualize16 this class as depicted in the
following gure:
0

2
3

Precedence matrix

Current assignment

Array of IntervalVars
0

where the precedence matrix mat is such that mat(i,j) = 1 if i is ranked before j.
The IntervalVar are often given by their indices in the array of IntervalVars.
Ranked IntervalVars
Ranked IntervalVars are exactly that: already ranked variables in the sequence.
IntervalVars can be ranked at the beginning or at the end of the sequence in the
SequenceVar variable. unperformed IntervalVar can not be ranked17 . The next gure illustrates this:
1
?
?
2
Ranked sequence

Array of IntervalVars
0
Not ranked yet

1
Ranked

2
Ranked

3
unperformed

IntervalVar variables 1 and 2 are ranked (and performed) while IntervalVar variable
0 may be performed but is not performed yet and IntervalVar variable 3 is unperformed
and thus doesnt exist anymore.
To rank the IntervalVar variables, we say that we rank them rst or last. First and last
IntervalVar variables must be understood with respect to the unranked variables:
16

This looks very much like the actual implementation. The array is a scoped_array<IntervalVar*>
and the precedence matrix is given by a scoped_ptr<RevBitMatrix>. The actual class contains some more
data structures to facilitate and optimize the propagation.
17
Thus, unranked variables are variables that may be performed. Yeah, three-states situations that evolves with
time are nastier than a good old Manichean one.

152

Chapter 6. Local search: the job-shop problem


Possible IntervalVar variables
to be ranked last

Possible IntervalVar variables


to be ranked rst

19

()

Ranked rst

...

Ra
nk

t
irs

86

kF

1
Ranked sequence

La
st

n
Ra

()

42

23

Ranked last

to rank rst an IntervalVar variable means that this variable will be ranked before
all unranked variables and
to rank last an IntervalVar variable means that this variable will be ranked after all
unranked variables.
Public methods
All the following methods are updated with the current values of the SequenceVar. unperformed variables - unless explicitly stated in one of the arguments - are never considered.
First, you have the following getters:
void DurationRange(int64* const dmin, int64* const dmax) const:
Returns the minimum and maximum duration of the IntervalVar variables:
dmin is the total (minimum) duration of mandatory variables (those that must
be performed) and
dmax is the total (maximum) duration of variables that may be performed.
void HorizonRange(int64* const hmin, int64* const hmax) const:
Returns the minimum starting time hmin and the maximum ending time hmax of
all IntervalVar variables that may be performed.
void ActiveHorizonRange(int64* const hmin, int64* const hmax) const:
Same as above but for all unranked IntervalVar variables.
int Ranked() const: Returns the number of IntervalVar variables already
ranked.
int NotRanked() const: Returns
the
number
of
not-unperformed
IntervalVar variables that may be performed and that are not ranked
yet.
void ComputeStatistics(...): Computes the following statistics:
void ComputeStatistics(int* const ranked,
int* const not_ranked,
int* const unperformed) const;

ranked + not_ranked + unperformed is equal to size().

153

6.3. Scheduling in or-tools

IntervalVar* Interval(int index) const: Returns


IntervalVar from the array of IntervalVars.

the

index

th

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 index th IntervalVar in the array of IntervalVars.
For instance, if you want to know what is the next IntervalVar after the 3
IntervalVar in the sequence, use the following code:

rd

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 lling the three std::vector<int> of
rst ranked, last ranked and unperformed variables:
void FillSequence(std::vector<int>* const rank_first,
std::vector<int>* const rank_lasts,
std::vector<int>* const unperformed) const;

The method rst clears the three std::vectors and lls them with the
IntervalVar number in the sequence order of ranked variables. If all variables
are ranked, rank_first will contain all variables and rank_last will contain
none. unperformed will contain all the unperformed IntervalVar variables.
rank_first[0] corresponds to the rst IntervalVar of the sequence while
rank_last[0] corresponds to the last IntervalVar variable of the sequence,
i.e. the IntervalVar variables ranked last are given in the opposite order.
ComputePossibleFirstsAndLasts(...): a getter giving the possibilities
among unranked IntervalVar variables:
void ComputePossibleFirstsAndLasts(
std::vector<int>* const possible_firsts,
std::vector<int>* const possible_lasts);

This method computes the set of indices of IntervalVar variables that can be
ranked rst or last in the set of unranked activities.
Second, you have the following setters:
void RankFirst(int index): Ranks the index th IntervalVar variable in
front of all unranked IntervalVar variables. After the call of this method, the
IntervalVar variable is considered performed.

154

Chapter 6. Local search: the job-shop problem


void RankNotFirst(int index): Indicates that the index th IntervalVar
variable will not be ranked rst among all currently unranked IntervalVar variables.
void RankLast(int index): Ranks the index th IntervalVar variable rst
among all unranked IntervalVar variables. After the call of this method, the
IntervalVar variable is considered performed.
void RankNotLast(int index): Indicates that the index th IntervalVar
variable will not be ranked rst among all currently unranked IntervalVar variables.
void RankSequence(...): a setter acting on three std::vector<int> of
rst, last and unperformed variables:
void RankSequence(const std::vector<int>& rank_firsts,
const std::vector<int>& rank_lasts,
const std::vector<int>& unperformed);

Ranks the IntervalVars in the given order. Again, the rank_firsts


std::vector<int> gives the IntervalVars in order (rank_firsts[0]
if the rst ranked IntervalVar and so on) and the rank_lasts
std::vector<int> gives the IntervalVar in the opposite order
(rank_lasts[0] is the last IntervalVar and so on). All IntervalVar
variables in the unperformed std::vector<int> will be marked as such
and all IntervalVar variables in the rank_firsts and rank_lasts
std::vector<int> will be marked as performed.

6.3.2 Constraints on IntervalVars


Most of the common constraints on IntervalVars are implemented in the library.
IntervalUnaryRelation constraints
You can specify a temporal relation between an IntervalVar t and an integer d:
ENDS_AFTER: t ends after d, i.e. End(t) >= d;
ENDS_AT: t ends at d, i.e. End(t) == d;
ENDS_BEFORE: t ends before d, i.e. End(t) <= d;
STARTS_AFTER: t starts after d, i.e. Start(t) >= d;
STARTS_AT: t starts at d, i.e. Start(t) == d;
STARTS_BEFORE: t starts before d, i.e. Start(t) <= d;
CROSS_DATE: STARTS_BEFORE and ENDS_AFTER at the same time, i.e. d is in t;
AVOID_DATE: STARTS_AFTER or ENDS_BEFORE, i.e. d is not in t.

155

6.3. Scheduling in or-tools

The possibilities are enclosed in the UnaryIntervalRelation enum. The corresponding


constraints are IntervalUnaryRelation constraints and the factory method is:
Constraint* Solver::MakeIntervalVarRelation(IntervalVar* const t,
Solver::UnaryIntervalRelation r,
int64 d);

BinaryIntervalRelation constraints
You can specify a temporal relation between two IntervalVars t1 and t2:
ENDS_AFTER_END: t1 ends after t2 ends, i.e. End(t1) >= End(t2);
ENDS_AFTER_START: t1 ends after t2 starts, i.e. End(t1) >= Start(t2);
ENDS_AT_END: t1 ends at the end of t2, i.e. End(t1) == End(t2);
ENDS_AT_START: t1 ends at t2s start, i.e. End(t1) == Start(t2);
STARTS_AFTER_START: t1 starts after t2 starts, i.e.
Start(t2);

Start(t1) >=

STARTS_AFTER_END: t1 starts after t2 ends, i.e. Start(t1) >= End(t2);


STARTS_AT_END: t1 starts at t2s 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
TemporalDisjunction constraints ensure that two ntervalVar variables are temporally
disjoint, i.e. they cannot be processed at the same time.
To create such a constraint, use:
solver = ...
...
IntervalVar * const t1 = ...
IntervalVar * const t2 = ...
...
Constraint * ct = solver.MakeTemporalDisjunction(t1, t2);

Maybe you can relate the decision on what has to happen rst to the value an IntVar takes:

156

Chapter 6. Local search: the job-shop problem

...
IntVar * const decider = ...
Constraint * ct = solver.MakeTemporalDisjunction(t1, t2, decider)

If decider takes the value 0, then t1 has to happen before t2, otherwise it is the contrary.
This constraint works the other way around too: if t1 happens before t2, then the IntVar
decider is bound to 0 and else to a positive value (understand 1 in this case).
DisjunctiveConstraint constraints
DisjunctiveConstraint constraints are like TemporalDisjunction constraints but for an unlimited number of IntervalVar variables.
Think of the
DisjunctiveConstraint as a kind of AllDifferent constraints but on
IntervalVars.
The factory method is:
Constraint *

MakeDisjunctiveConstraint (
const std::vector< IntervalVar * > &intervals);

In the current implementation, the created constraint is a FullDisjunctiveConstraint


which means that the IntervalVars will be disjoint.
The DisjunctiveConstraint class itself is a pure abstract class. Subclasses must implement the following method:
virtual SequenceVar* MakeSequenceVar() = 0;

This method creates a SequenceVar containing the rankable18 IntervalVars given in


the intervals std::vector<IntervalVar *>.
SequenceVar variables are so closely tied to a sequence of IntervalVars that obeys a
DisjunctiveConstraint constraint that it is quite natural to nd such a method. In the
current implementation, it is the only available method to create a SequenceVar variable!
The use of the MakeSequenceVar() method of a DisjunctiveConstraint
constraint is the only way to create a SequenceVar variable in the current implementation. This might change in the future.
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 corresponding intervals are ltered out, as they neither impact nor are impacted by this constraint.
Here is one factory method with a limited static capacity:
18

You remember that unperformed IntervalVars are non existing, dont you? And yes, we know that the
adjective rankable doesnt exist...

157

6.3. Scheduling in or-tools

Constraint* MakeCumulative(const
const
int64
const

std::vector<IntervalVar*>& intervals,
std::vector<int64>& demands,
capacity,
string& name);

If you need more exibility, use the following factory method:


Constraint* MakeCumulative(const std::vector<IntervalVar*>& intervals,
const std::vector<int64>& demands,
IntVar* const 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.

6.3.3 Constraints on SequenceVars


There are none for the time being. Nobody prevents you from implementing one though.

6.3.4 DecisionBuilders and Decisions for scheduling


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 r2502, January
11 th 2013) 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 rst available ones.
When well implement different strategies, we will update the manual. If youre curious about the implementation details, we refer you to the code (mainly to the le
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 is only one strategy implemented even if there are three
entries 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()).

158

Chapter 6. Local search: the job-shop problem

The DecisionBuilder class is the SetTimesForward class.


It returns a
ScheduleOrPostpone Decision in its Next() method. This Decision xes 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.
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 dene the slack to be
slack = (hmax hmin) dmax
and we choose the SequenceVar with the minimum slack. In case of a tie, we
choose the SequenceVar with the smallest active horizon range (see ahmin in the
ActiveHorizonRange() method above).
Once the best SequenceVar variable is chosen, the CP solver takes the rankable
IntervalVar with the minimum starting time (StartMin()) and ranks it rst.
CHOOSE_RANDOM_RANK_FORWARD: Among the SequenceVars for which there are still
IntervalVars to rank, the CP solver chooses one randomly. Then it randomly chooses
a rankable IntervalVar and ranks it rst.
SEQUENCE_DEFAULT, SEQUENCE_SIMPLE, CHOOSE_MIN_SLACK_RANK_FORWARD
and CHOOSE_RANDOM_RANK_FORWARD are given in the SequenceStrategy enum.
To create these search strategies, use the following factory method:
DecisionBuilder* Solver::MakePhase(
const std::vector<SequenceVar*>& sequences,
SequenceStrategy str);

In both cases, we use the RankFirstIntervalVars class as DecisionBuilder.


Its Next() method returns a RankFirst Decision that ranks rst the selected
IntervalVar in its Apply() method and doesnt rank it rst in its Refute() method.
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
159

6.3. Scheduling in or-tools

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);

and then compose the two DecisionBuilders sequentially:


DecisionBuilder* const main_phase = solver.Compose(sequence_phase,
obj_phase);

By the way, the MakePhase() method has been optimized when the phase only handles one
or a few variables (up to 4), like in the above example for the obj_phase.

6.3.5 DependencyGraph
If you want to add more specic temporal constraints, you can use a data structure specialized
for scheduling: the DependencyGraph. It is meant to store simple temporal constraints and
to propagate efciently on the nodes of this temporal graph. One node in this graph corresponds
to an IntervalVar variable. You can build constraints on the start or the ending time of the
IntervalVar nodes.
Consider again our rst example (first_example_jssp.txt) and lets say that for whatever reason we want to impose that the rst task of job 2 must start at least after one unit of
time after the rst task of job 1. We could add this constraint in different ways but lets use the
DependencyGraph:
solver = ...
...
DependencyGraph * graph = solver.Graph();
graph->AddStartsAfterEndWithDelay(jobs_to_tasks[2][0],
jobs_to_tasks[1][0], 1);

Thats it!
Here is the output of an optimal solution found by the solver:

160

Chapter 6. Local search: the job-shop problem

Objective value:
Machine_0: Job 1
Machine_1: Job 2
Machine_2: Job 1

13
(0,2)
(3,7)
(2,3)

Job 0 (2,5)
Job 0 (7,9) Job 1 (9,13)
Job 2 (7,10) Job 0 (10,12)

As you can see, the rst task of job 2 starts at 3 units of time and the rst task of job 1 ends at
2 units of time.
Other methods of the DependencyGraph include:
AddStartsAtEndWithDelay()
AddStartsAfterStartWithDelay()
AddStartsAtStartWithDelay()
The DependencyGraph and the DependencyGraphNode classes are declared in the
constraint_solver/constraint_solveri.h header.

6.4 What is local search (LS)?


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 difcult problems. We start this section
by describing what local search is and what local search methods have in common. Then we
discuss their efciency and compare them with global methods.
Some paragraphs are quite dense, so dont be scared if you dont get it all after the rst
reading. With time and practice, the use of local search methods will become a second nature.

6.4.1 The basic ingredients


Local Search is a whole bunch of families of (meta-)heuristics19 that roughly share the following ingredients:
1. They start with a solution (feasible or not);
2. They improve locally this solution;
3. They nish the search when reaching a stopping criterion but usually without any guarantee on the quality of the found solution(s).
We will discuss these three ingredients in details in a moment but before here are some examples of local search (meta-)heuristics20 :
Tabu Search | (62 100)
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 isnt 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 didnt return any meaningful results. The methods in bold are implemented
in or-tools.

161

6.4. What is local search (LS)?

Hill Climbing | (54 300)


Scatter Search | (5 600)
Simulated Annealing | (474 000)
Beam Search | (12 700)
Particle Swarm Optimization | (74 500)
Greedy Descent | (263)
Gradient Search | (16 300)
Variable Neighbourhood Search | (1 620)
Guided Local Search | (2 020)
Genetic Algorithms | (530 000)
Ant Colony Optimization | (31 100)
Greedy Adaptive Search Procedure (GRASP)
...
and there are a lot more! Most of these methods are quite recent in Research Operations (from
the eighties and later on).
Most successful methods take into account their search history to guide the search. Even better
- when well implemented - reactive methods21 learn and adapt themselves during the search.
As you might have guessed from the long list of different local search (meta-) heuristics, there
is no universal solving method22 . The more insight/knowledge of the structure of your specic
problem you gather, the better you can shape your algorithm to solve efciently your problem.
Lets discuss the three common ingredients and their implementation in or-tools.
1. They start with a solution (feasible or not):
To improve locally a solution, you need to start with a solution. In or-tools this solution
has to be feasible. You can produce an initial solution and give it to the solver or let the
solver nd one for you with a DecisionBuilder that you provide the local search
algorithm with.
What if your problem is to nd a feasible solution? You relax the constraints23 until
you can construct a starting solution for that relaxed model. From there, you enforce
the relaxed constraints by adding corresponding terms in the objective function (like in
a Lagrangian relaxation for instance). Youll nd a detailed example of this kind of
relaxation and the use of local search in the lab exercises XXX where we will try to nd
a solution to the n-queens problem with local search. It might sound complicated but it
really isnt.
21

See Wikipedia Reactive search optimization or reactive-search.org.


Google No Free Lunch Theorem in optimization to learn more about this.
23
Relaxing a constraint means that you remove this constraint or weaken it. For instance, you can replace
1
1 by 1
2. This last constraint is weaker than the rst one because it allows more solutions to the
problem. Of course, it is preferable to weaken constraints in a meaningful way!
22

162

Chapter 6. Local search: the job-shop problem

2. They improve locally this solution:


This is the tricky part to understand. Improvements to the initial solution are done locally.
This means that you need to dene 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 denition of a
neighborhood.
The idea is to (partially or completely) explore a neighborhood around an initial solution,
nd a good (or the best) solution in this neighborhood and start all over again until a
stopping criterion is met.
Lets denote by the neighborhood of a solution .
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 gure illustrates this process:
z
f

111111
000000 N
111111111111
000000000000
111111
000000
x1

Nx0

x0
Initial solution

x1
Local minimum

111
000 x
111
000 N

xi

x3

Nx2
x

x2
Global minimum

solution i
neighborhood
neighborhood of xi

This gure depicts a function to minimize. Dont be fooled by its 2-dimensionality.


The -axis represents solutions in a multi-dimensional space. The -axis represents a
1-dimensional space with the values of the objective function .
Lets zoom in on the neighborhoods and found solutions:

163

6.4. What is local search (LS)?

The local search procedure starts from an initial feasible solution 0 and searches the
neighborhood 0 of this solution. The best solution found is 1 . The local search
procedure starts over again but with 1 as starting solution. In the neighborhood 1 , the
best solution found is 2 . The procedure continues on and on until stopping criteria are
met. Lets say that one of these criteria is met and the search ends with 3 . You can see
that while the method moves towards the local optima, it misses it and completely misses
the global optimum! This is why the method is called local search: it probably will nd
a local optimum (or come close to) but it is unable to nd a global optimum (except by
chance).
If we had continued the search, chances are that our procedure would have iterated around
the local optimum. In this case, we say that the local search algorithm is trapped by a
local optimum. Some LS methods - like Tabu Search - were developed to escape such
local optimum but again there is no guarantee whatsoever that they can succeed.
The gure above is very instructive. For instance, you can see that neighborhoods
dont have to be of equal size or centred around a variable . You can also see
that the relationship being in the neighborhood of is not necessarily symmetric:
1 0 but 0 1 24 ! In or-tools, you dene a neighborhood by implementing
the MakeNextNeighbor() callback method 25 from a LocalSearchOperator:
every time this method is called internally by the solver, it constructs one solution of the neighborhood If you have constructed a successful candidate, make
MakeNextNeighbor() returns true. When the whole neighborhood has been visited, make it returns false.
3. They nish the search when reaching a stopping criterion but usually without any
guarantee on the quality of the found solution(s):
Common stopping criteria include:
time limits:
for the whole solving process or
for some parts of the solving process.
maximum number of steps/iterations:
maximum number of branches;
maximum number of failures;
24

To be fair, we have to mention that most LS methods require this relation to be symmetric as a desirable
feature would be to be able to retrace our steps in case of a false start or to explore other possibilities. On the
gure, you might think about going left to explore wath is past the .
25
Well almost. The MakeNextNeighbor() callback is really low level and we have alleviate the task by
offering other higher level callbacks. See section 6.6 for more details.

164

Chapter 6. Local search: the job-shop problem

maximum number of solutions;


...
improvements criteria:
stop if no improvement for n number of steps/x time;
stop if gap between estimate of optimal solution and best solution obtained so
far is smaller than x;
...
These stopping criteria can be further divided in:
absolute: for instance, a global maximal number of iterations;
relative: for instance, the improvements are too small with respect to the time, the
number of iterations, the number of solutions, ... .
Most of the time, you combine some of these criteria together. You can also update
them during the search. In or-tools, stopping criteria are implemented using specialized
SearchMonitors: SearchLimits (see subsection 3.5.4).
What is it with the word metaa ?
A heuristic is an algorithm that provides a (hopefully) good solution for a given problem.
A meta-heuristic is more like a theoretical framework to solve problems: you have to adapt
the meta-heuristic to your needs. For instance, Genetic Algorithms use a recombination of
parts of solutions (the genes) but for a specic problem, you have to nd out what parts
of solution you can combine and how you can combine them. A meta-heuristic gives you
guidelines to construct your algorithm.
Its a recipe on how to write a recipe. You have one level of indirection like in metaprogramming where you write code to generate code.
a

See Wikipedia meta for the meaning of the word.

6.4.2 Is Local Search efcient?


In two words: yes but...26
Lets dissect this terse answer:
yes:
To really answer this question, you need to know what exactly you mean by efcient.
If youre looking for a global optimum then local search - at least in its basic form but
read the subsection Global optimization methods and local search below - is probably
not for you. If you are looking for a guarantee on the quality of the solution(s) found,
then again you might want to look for another tool.
26

Okay, okay and three more lower dots.

165

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 efciency of the local search. You might start from a
solution that is too far from a global (or local) optimum or worse you start from a solution
from which it is impossible to reach a global (or even local) optimum with your neighborhood denition. Several techniques have been proposed to tackle these annoyances. One
of them is to restart the search with different initial solutions. Another is to change the
denition of a neighborhood during the search like in Variable Neighbourhood Search
(VNS).
LS is a tradeoff between efciency and the fact that LS doesnt try to nd a global optimum,
i.e. in other words you are willing to give up the idea of nding a global optimum for the
satisfaction to quickly nd a (hopefully good) local optimum.
A certain blindness
LS methods are most of the time really blind when they search. Often you hear the analogy
between LS methods and descending a hilla to nd the lowest point in a valley (when we
minimize a function). It would be more appropriate to compare LS methods with going
down a valley ooded by mist: you dont see very far in what direction to go to continue
downhill. Sometimes you dont see anything at all and you dont even know if you are
allowed to set a foot in front of you!
a

If youve never heard this metaphor, skip this paragraph and dont bother.

6.4.3 What about the quality of the solutions found by local


search?
Sometimes, we can have some kind of guarantee on the quality of the solutions found and we
speak about approximations, sometimes we dont 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 nding a good
solution or even more!);
a guarantee that isnt 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 doesnt mean that the solution
found is not a good solution (it might even be the best solution), just that we dont 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).

166

Chapter 6. Local search: the job-shop problem

What do we mean by a guarantee on the solution?


Several concepts of guarantee have been developed. We will not go into detailsa about the
concept of guarantee but lets give an example. In a now famous report [Christodes1976],
Christodes proposed and analyzed a heuristic that is guaranteed to solve the metric Travelling Salesman Problemb within a 3/2 factor, i.e. no matter the instance, this heuristic will
always return a solution whose cost is at most 3/2 times the cost of the optimal solution.
This means that in the worst case, the returned solution costs 3/2 times the cost of the
optimal solution. This is guaranteed!
See Wikipedia Approximation Algorithm.
a

If theory doesnt scare you, have a look at the subsection Approximation complexity for more about
approximation theory and quality guarantees.
b
The metric TSP is the classical TSP but on graphs that respect the triangle inequality, i.e. (, )
(, ) + (, ) where , and are nodes of the graph and () a distance function. The classical TSP itself
cannot be approximated within any constant factor (unless P = NP).

6.4.4 Global optimization methods and local search


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) efcient local methods that (hopefully) lead to local optima.
Could we have the best of these two worlds?
Youve guessed it: we use global methods to nd portions of the search space that might contain
good or even optimal solutions and we try to nd 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 nd 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 nd a promising spot, you send a team to drill and
nd out (local method).

6.5 Basic working of the solver: Local Search


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
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!

167

6.5. Basic working of the solver: Local Search

Search. Its good to keep them in memory for the rest of this section. We then overview the implementation and describe some of its main components. Finally, we detail the inner workings
of the Local Search algorithm and indicate where the callbacks of the SearchMonitors are
called.
We present a simplied version of the local search algorithm. Yes, this is well worth a warning
box!
We describe a simplied version of the local search algorithm.

6.5.1 The basic idea


The Local Search algorithm is implemented with the LocalSearch DecisionBuilder
which
returns
NestedSolveDecisions
(through
its
Next()
method).
These NestedSolveDecisions in turn collect the solutions returned by the
FindOneNeighbor DecisionBuilder in their left branches (and dont do anything in
their right branches). As its name implies, the FindOneNeighbor DecisionBuilder
tries to nd one solution. The LocalSearch DecisionBuilder stops the search
when stopping criteria are met or when it can not improve the last solution found. This
solution is thus a local optimum w.r.t. the chosen neighborhood. If needed, the search can
be restarted again around a new initial solution. The LocalSearch DecisionBuilder
then acts like a multi-restart DecisionBuilder. We exploit this property in chapter 7
when we implement (meta-)heuristics based on local searches that restart from a given solution.
Wow, this went fast! Lets summarize all this in the next picture:
LocalSearch::Next()

NestedSolveDecision
NestedSolveDecision::Apply(){
SolveAndCommit(FindOneNeighbor(ls));
}

NestedSolveDecision::Refute(){}

ls is the LocalSearchOperator that constructs the candidate solutions. The search tree
very quickly becomes completely unbalanced if we only keep nding solutions in the left
branches. Well see a balancing mechanism that involves one BalancingDecision at the
end of this section.
Speaking about candidate solutions, lets agree on some wordings. The next gure presents the
beginning of a Local Search. 0 is the initial solution. In or-tools, this solution is given by an
Assignment or a DecisionBuilder that the LocalSearch class uses to construct this
initial solution. 0 , 1 , 2 , . . . are solutions. As we have seen, the Local Search algorithm moves
from one solution to another. It takes a starting solution and visit the neighborhood dened
around to nd the next solution +1 . By visiting the neighborhood, we mean constructing

168

Chapter 6. Local search: the job-shop problem


and testing feasible solutions 0 = , 1 , 2 , . . . of this neighborhood. We call these solutions
candidate solutions. In the code, they are called neighbors. The LocalSearchOperator
produces these candidates and the FindOneNeighbor DecisionBuilder lter these out
to keep the interesting candidate solutions only. When a stopping criteria is met or the neighborhood has been exhausted, the current solution of the CP solver is the next starting solution.
Lets illustrate this:

111111
000000 N
N
111111111111
000000000000
111111
000000
x1

x0

Candidate solutions

y1 y0 y2
x0
Initial solution

y3 y4

y5

x1
Current solution = starting solution for Nx1

The code consistently use the term neighbor to denote what we call a candidate solution in this
manual. We prefer to emphasize the fact that this neighbor solution is in fact a feasible solution
that the CP solver tests and accepts or rejects.
In this manual, we use the term candidate solution for what is consistently called a
neighbor in the code.

The main actors


The main classes involved in the local search algorithm are:
LocalSearch: This DecisionBuilder controls the local search algorithm.
LocalSearchPhaseParameters: This class gathers the components to dene the
current local search.
LocalSearchOperators: This class is responsible of constructing the candidate solutions.
FindOneNeighbor: This DecisionBuilder lters the candidate solutions given
by the LocalSearchOperator and only constructs ltered and accepted (solutions
accepted by the CP solver as feasible solutions) solutions.
NestedSolveDecision: This Decision invokes a nested search with another
DecisionBuilder (FindOneNeighbor in this case) in its left branch (Apply()
method) and does nothing in its right branch (Refute() method).
LocalSearchFilter: This lter allows to immediately skip (discard) a candidate
solution. It is used by FindOneNeighbor to lter the candidate solutions.
We will not discuss the ltering mechanism here (see the dedicated section Filtering).

169

6.5. Basic working of the solver: Local Search

6.5.2 Overview of the Local Search Mechanism in or-tools


The next gure illustrates the basic mechanism of local search in or-tools:
Solution
Local Search Operator(s)

Candidate solution

Candidate solution

...

Candidate solution

CP Check + Solve sub-problem

We start with an initial feasible solution. The MakeOneNeighbor() callback method from
the local search operator(s)30 constructs candidate solutions one by one31 . These solutions are
checked by the CP solver and completed if needed. The best solution is chosen and the
process is repeated starting with this new improved solution32 .
The whole search process stops whenever a stopping criterion is reached or the CP solver
cannot improve anymore the current best solution.
Lets describe some pieces of the or-tools mechanism for local search:
initial solution: we need a feasible solution to start with. You can either pass an
Assignment or a DecisionBuilder to the LocalSearchs constructor.
LocalSearchPhaseParameters: the LocalSearchPhaseParameters parameter
holds the actual denition of the local search phase:
a SolutionPool that keep solution(s);
a LocalSearchOperator used to explore the neighborhood of the current
solution.
You can combine several LocalSearchOperators into one
LocalSearchOperator;
a complementary DecisionBuilder to instantiate unbound variables once an (incomplete) candidate solution has been dened by the LocalSearchOperator.
30
In the code, you are only allowed to use one LocalSearchOperator but you can combine several
LocalSearchOperators in one LocalSearchOperator. This is a common pattern in the code.
31
MakeOneNeighbor() is a convenient method. The real method to create a new candidate is
MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) but you have to deal
with the low level delta and deltadelta. We discuss these details in the section LocalSearchOperators:
the real thing!.
32
By default, the solver accepts the rst feasible solution and repeats the search starting with this new solution.
The idea is that if you combine the local search with an ObjectiveVar, the next feasible solution will be a
solution that beats the current best solution. You can change this behaviour with a SearchLimit. See below.

170

Chapter 6. Local search: the job-shop problem

It will also complete the initial Assignment or the solution provided by the initial
DecisionBuilder.;
a Searchlimit specifying the stopping criteria each time we start searching a new
neighborhood;
an std::vector of LocalSearchFilters used to speed up the search by pruning
unfeasible (or undesirable) candidate solutions: instead of letting the solver nd out
if a candidate solution is feasible or not, you can help it by bypassing its checking
mechanism and telling it right away if a candidate solution is not feasible.
LocalSearchOperators are detailed in the next section and LocalSearchFilters in
section 6.8. We now detail these two basics ingredients that are the initial solution and the
LocalSearchPhaseParameters parameter.
The initial solution
To start the local search, we need an initial feasible solution. We can either give a starting
solution or we can ask the CP solver to nd one for us. To let the solver nd a solution for us, we
pass to it a DecisionBuilder. The rst solution discovered by this DecisionBuilder
will be taken as the initial solution.
There is a factory method for each one of the two options:
DecisionBuilder* Solver::MakeLocalSearchPhase(Assignment* assignment,
LocalSearchPhaseParameters* parameters)
DecisionBuilder* Solver::MakeLocalSearchPhase(
const std::vector<IntVar*>& vars,
DecisionBuilder* first_solution,
LocalSearchPhaseParameters* parameters)

In the le dummy_ls.cc, we use a gags ag FLAG_initial_phase to switch between


these two possibilities.
What are the variables involved in the local search procedure?
The local search only applies to the variables contained either in the Assignment or the
std::vector<IntVar*> of variables given to MakeLocalSearchPhase().

The LocalSearchPhaseParameters parameter


The LocalSearchPhaseParameters parameter holds the actual denition of the local
search phase.
It basically consists in:
a SolutionPool: as its name implies, this class is a pool of solutions. As usual,
SolutionPool is a pure virtual class that must be implemented. One such implementation is the DefaultSolutionPool that only keeps the current solution. You

171

6.5. Basic working of the solver: Local Search

dont have to provide one as it is constructed by default if you use the appropriate factory
method. If you want to keep intermediate solutions or want to modify these solutions
during the search, you might have to implement your own version. Four methods have to
be implemented:
void Initialize(Assignment* const assignment): This method
is called to initialize the SolutionPool with the initial Assignment.
void RegisterNewSolution(Assignment* const assignment):
This method is called when a new solution has been accepted by the local search
algorithm.
void GetNextSolution(Assignment* const assignment): This
method is called when the local search algorithm starts a new neighborhood.
assigment is the solution to start the new neighborhood search.
bool SyncNeeded(Assignment* const local_assignment): This
method checks if the current solution needs to be updated, i.e. the pool can oblige
the solver to start a new neighborhood search with the next solution given by the
pool (given by its GetNextSolution() method, see the Next() method of
the FindOneNeighbor DecisionBuilder class below).
A SolutionPool gives you complete control on the starting solution(s). Note that the
SolutionPool must take ownership of the Assignments it keeps33 .
a LocalSearchOperator: a LocalSearchOperator or a combination of
LocalSearchOperators explore the neighborhood of the current solution. We detail
them in the next section.
a DecisionBuilder: this complementary DecisionBuilder helps creating feasible solutions if your LocalSearchOperators only return partial solutions, i.e. solutions with unbounded variables. It also completes the initial solution if needed. If you
know that your candidate and the initial solutions are already feasible, you dont have to
provide this DecisionBuilder (set the corresponding pointer to NULL).
a SearchLimit: This SearchLimit limits the search of one neighborhood. The
most interesting statistic to limit is probably the number of found solutions:
SearchLimit * const limit = s.MakeSolutionsLimit(2);

This would limit the search to maximum two candidate solutions in the same neighborhood. By default, the CP solver stops the neighborhood search as soon as it nds a ltered
and feasible candidate solution. If you add an OptimizeVar to your model, once the
solver nds this good candidate solution, it changes the model to exclude solutions with
the same objective value. The second solution found can only be better than the rst one.
See section 3.9 to refresh your memory if needed. When the solver nds 2 solutions (or
when the whole neighborhood is explored), it stops and starts over again with the best
solution.
LocalSearchFilters: these lters speed up the search by bypassing the solver
checking mechanism if you know that the solution must be rejected (because it is not
33

Well, you could devise another way to keep track of the solutions and take care of their existence but anyhow,
you are responsible for these solutions.

172

Chapter 6. Local search: the job-shop problem

feasible, because it is not good enough, ...). If the lters accept a solution, the solver
still tests the feasibility of this solution. LocalSearchFilters are discussed in section 6.8.
Several factory methods are available to create a LocalSearchPhaseParameters parameter. At least you need to declare a LocalSearchOperator and a complementary
DecisionBuilder:
LocalSearchPhaseParameters * Solver::MakeLocalSearchPhaseParameters(
LocalSearchOperator *const ls_operator,
DecisionBuilder *const
complementary_decision_builder);

You can also pass all the above enumerated parameters :


LocalSearchPhaseParameters* Solver::MakeLocalSearchPhaseParameters(
SolutionPool* const pool,
LocalSearchOperator* const ls_operator,
DecisionBuilder* const
complementary_decision_builder,
SearchLimit* const limit,
const std::vector<LocalSearchFilter*>& filters);

The
LocalSearchOperator
will
nd
candidate
solutions
while
complementary_decision_builder DecisionBuilder will complete
candidate solutions if some of the variables are not assigned.

the
the

A handy way to create a DecisionBuilder to assist the local search operator(s) is to limit
one with MakeSolveOnce(). MakeSolveOnce is a DecisionBuilder that takes another DecisionBuilder db and SearchMonitors:
DecisionBuilder * const db = ...
SearchLimit* const limit = solver.MakeLimit(...);
DecisionBuilder * const complementary_decision_builder =
solver.MakeSolveOnce(db, limit);

The SolveOnce DecisionBuilder created by MakeSolveOnce() will collapse the


search tree described by the DecisionBuilder db and a set of SearchMonitors and
wrap it into a single point. The nested search stops after the rst solution is found. If there are
no solutions in this nested tree, then (the Next() method of) SolveOnce will fail.
If you know for sure that your LocalSearchOperator will return feasible solutions, you
dont have to provide a DecisionBuilder to assist: just submit NULL as argument for the
DecisionBuilder pointer.

6.5.3 The basic local search algorithm and the callback hooks for
the SearchMonitors
We feel compelled to use our warning box again:
We describe a simplied version of the Local Search algorithm.

173

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
LocalOptimum()

AcceptDelta(Assignment *delta,
Assignment *deltadelta)

AcceptNeighbor()
PeriodicCheck()

Descriptions
When a local optimum is reached. If true is
returned, the last solution is discarded and the
search proceeds to nd the next local optimum.
Handy when you implement a meta-heuristic with a
SearchMonitor.
When the LocalSearchOperator has produced
the next candidate solution given in the form of
delta and deltadelta. You can accept or reject
this new candidate solution.
After accepting a candidate solution during Local
Search.
Periodic call to check limits in long running search
procedures, like Local Search.

To ensure the communication between the local search and the global search, three utility functions are dened. These functions simply call their SearchMonitors counterparts, i.e. they
call the corresponding methods of the involved SearchMonitors:
bool LocalOptimumReached(): FalseExceptIfOneTrue.
bool AcceptDelta(): TrueExceptIfOneFalse.
void AcceptNeighbor(): Notication.
Before we delve into the core of the local search algorithm and the implementation of
the LocalSearch DecisionBuilders Next() method, we rst discuss the inner
workings of the FindOneNeighbor DecisionBuilder whose job is to nd the next
ltered and accepted candidate solution. This DecisionBuilder is used inside a
NestedSolveDecision that we study next. This Decision is returned by the Next()
method of the LocalSearch DecisionBuilder in the main loop of the local search algorithm. Finally, we address the LocalSearch DecisionBuilder class. In particular,
we study its initializing phase and its Next() method. We consider the case where an initial
DecisionBuilder constructs the initial solution.
SearchMonitors callbacks are indicated in the code by the comment:
// SEARCHMONITOR CALLBACK

The FindOneNeighbor DecisionBuilder


This DecisionBuilder tries to nd the next ltered and accepted candidate solution.
It tests (and sometimes completes) the candidate solutions given by the
LocalSearchOperator.

174

Chapter 6. Local search: the job-shop problem

We present its Next() method and discuss it after:


1

Decision* FindOneNeighbor::Next(Solver* const solver) {

2
3
4
5
6
7
8

// No neighbor (candidate solution) found


// only on the first call to Next().
if (!neighbor_found_) {
// SYNCHRONIZE ALL
...
}

9
10
11
12
13

// Another assignment is needed to apply the delta


Assignment* assignment_copy =
solver->MakeAssignment(reference_assignment_.get());
int counter = 0;

14
15
16
17
18
19
20
21

DecisionBuilder* restore =
solver->MakeRestoreAssignment(assignment_copy);
if (sub_decision_builder_) {
restore = solver->Compose(restore, sub_decision_builder_);
}
Assignment* delta = solver->MakeAssignment();
Assignment* deltadelta = solver->MakeAssignment();

22
23
24
25
26
27
28
29
30
31
32
33
34

// MAIN LOOP
while (true) {
delta->Clear();
deltadelta->Clear();
// SEARCHMONITOR CALLBACK
solver->TopPeriodicCheck();
if (++counter >= FLAGS_cp_local_search_sync_frequency &&
pool_->SyncNeeded(reference_assignment_.get())) {
// SYNCHRONIZE ALL
...
counter = 0;
}

35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54

if (!limit_->Check()
&& ls_operator_->MakeNextNeighbor(delta, deltadelta)) {
solver->neighbors_ += 1;
// SEARCHMONITOR CALLBACK
const bool meta_heuristics_filter =
AcceptDelta(solver->ParentSearch(), delta, deltadelta);
const bool move_filter = FilterAccept(delta, deltadelta);
if (meta_heuristics_filter && move_filter) {
solver->filtered_neighbors_ += 1;
assignment_copy->Copy(reference_assignment_.get());
assignment_copy->Copy(delta);
if (solver->SolveAndCommit(restore)) {
solver->accepted_neighbors_ += 1;
assignment_->Store();
neighbor_found_ = true;
return NULL;
}
}
} else {

175

6.5. Basic working of the solver: Local Search

55

if (neighbor_found_) {

56

// SEARCHMONITOR CALLBACK
AcceptNeighbor(solver->ParentSearch());
pool_->RegisterNewSolution(assignment_);
// SYNCHRONIZE ALL
...
} else {
break;
}

57
58
59
60
61
62
63

64

}
solver->Fail();
return NULL;

65
66
67
68

You might wonder why there are so many lines of code but there are a some subtleties to
consider.
The code of lines 5 to 8 is only called the rst time the Next() method is invoked and allow
to synchronize the Local Search machinery with the initial solution. In general, the words
SYNCHRONIZE ALL in the comments mean that we synchronize the Local Search Operators
and the Local Search Filters with a solution.
reference_assignment_ is an Assignment with the initial solution while
assignment_ is an Assignment with the current solution. On line 10, we copy
reference_assignment_ to the local assignment_copy Assignment to be able
to dene the deltas. counter counts the number candidate solutions. This counter is used
on line 29 to test if we shouldnt start again the Local Search with another solution.
On lines 15-19, we dene the restore DecisionBuilder that will allow us to keep the
newly found candidate solution.
We construct the delta and deltadelta on lines 20 and 21 and are now ready to enter the
main loop to nd the next solution.
On lines 25 and 26 we clear our deltas and on line 28 we allow for a periodic check: for
searches that last long, we allow the SearchMonitors to interfere and test if the search
needs to continue or not and/or must be adapted.
Lines 29-34 allow to change the starting solution and ask the solution
pool pool_ for a new solution via its GetNextSolution().
The
FLAGS_cp_local_search_sync_frequency value corresponds to the number
of attempts before the CP solver tries to synchronize the Local Search with a new solution.
On line 36 and 37, the SearchLimits applied to the search of one neighborhood are tested.
If the limits are not reached and if the LocalSearchOperator succeeds to nd a new
candidate solution, we enter the if statement on line 38. The LocalSearchOperators
MakeNextNeighbor() method is called to create the next candidate solution in deltas
format.
If you overwrite the MakeNextNeighbor() method, you need to manage the deltas:
you must take care of applying and reverting the deltas yourself if needed. You
can use the ApplyChanges() and RevertChanges() helper functions to do so.

176

Chapter 6. Local search: the job-shop problem

For instance, here is the implementation of the MakeNextNeighbor() method of the


IntVarLocalSearchOperator:
bool IntVarLocalSearchOperator::MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (!MakeOneNeighbor()) {
return false;
}
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}

ApplyChanges() actually lls the deltas after you use the helper methods
SetValue(), Activate() and the like to change the current candidate solution. Once
we enter the if statement on line 38, we have a new candidate solution and we update the solution counter accordingly. It is now time to test this new solution candidate. The rst test comes from the SearchMonitors in their AcceptDelta() methods. If only one SearchMonitor rejects this solution, it is rejected. In or-tools,
we implement (meta-)heuristics with SearchMonitors. See chapter 7 for more. The
AcceptDelta() function is the global utility function we mentioned above. Well meet
LocalOptimumReached() and AcceptNeighbor() a few lines below.
The second test is the ltering test on line 42.
FilterAccept() returns a
TrueExceptIfOneFalse. If both tests are successful, we enter the if statement on
line 44. If not, we simply generate another candidate solution. On lines 44 and 46, we
update the counter of filtered_neighbors_ and store the candidate solution in the
assignment_copy Assignment.
On line 47, we try (and if needed complete) the candidate. If we succeed, the current solution
and the counter accepted_neighbors_ are updated. The Next() method returns NULL
because the FindOneNeighbor DecisionBuilder has nished its job at this node of
the search tree. If we dont succeed, the solver fails on line 66.
The SolveAndCommit() method is similar to the Solve() method except that
SolveAndCommit will not backtrack all modications at the end of the search and this is
why you should:
Use the SolveAndCommit() method only in the Next() method of a
DecisionBuilder!
If the if test on line 36 and 37 fails, we enter the else part of the statement on line 55. This
means that either one SearchLimit was reached or that the neighborhood is exhausted. If a
solution (stored in assignment_) was found during the local search, we register it and synchronize the LocalSearchOperators and LocalSearchFilters with a new solution
provided by the solution pool pool_ on lines 58-60. We also notify the SearchMonitors
177

6.5. Basic working of the solver: Local Search

on line 57. If no solution was found, we simply break out of the while() loop on line 62
and make the CP solver fail on line 66.
The NestedSolveDecision Decision
The NestedSolveDecision is the Decision that the LocalSearchs Next()
method returns to nd the next solution. This Decision is basically a Decision wrapper around a nested solve with a given DecisionBuilder and SearchMonitors. It
doesnt do anything in its right branch (in its Refute() method) and calls Solve() or
SolveAndCommit() depending on a restore bool in its left branch (in its Apply()
method).
The NestedSolveDecision Decision can be in three states that are also the three states
of the Local Search:
Value
DECISION_FAILED
DECISION_PENDING
DECISION_FOUND

Meaning
The nested search phase failed, i.e.
Solve() or
SolveAndCommit() failed.
The nested search hasnt been called yet. The local search
is in this state when it balances the search tree.
The nested search phase succeeded and found a solution,
i.e. Solve() or SolveAndCommit() succeeded and
returned true.

The three states are dened in the NestedSolveDecision StateType enum.


We are now ready to assemble all the pieces of the puzzle together to understand the (simplied)
local search algorithm in or-tools.
The LocalSearch DecisionBuilder
We rst consider the initialization phase and then we discuss in details its Next() method.
Initialization

Consider the situation where we already have a LocalSearchPhaseParameters parameter set up and we let the CP solver construct the initial solution:
Solver s("Dummy LS");
...
std::vector<IntVar*> vars = ...
...
LocalSearchOperator * const ls_operator = ...
DecisionBuilder * const complementary_decision_builder = ...
...
LocalSearchPhaseParameters params =
s.MakeLocalSearchPhaseParameters(ls_operator,
complementary_decision_builder);

178

Chapter 6. Local search: the job-shop problem

The complementary_decision_builder DecisionBuilder will help us complete


the candidate solutions found by the LocalSearchOperator ls_operator. Our initial
solution will be constructed by the initial_solution DecisionBuilder (and completed by the complementary_decision_builder DecisionBuilder if needed).
Remember, that the solution chosen by the CP solver is the rst solution found by this
DecisionBuilder. We are now ready to create the DecisionBuilder for the local
search:
DecisionBuilder * const initial_solution = ...
...
DecisionBuilder * const ls = s.MakeLocalSearchPhase(vars,
initial_solution,
params);

We can now add as many monitors as we want and launch the solving process:
std::vector<SearchMonitor *> monitors;
...
s.Solve(ls, monitors);

Its interesting to see how this initial solution is constructed in the LocalSearch class. First,
we create an Assignment to store this initial solution:
Assignment * const initial_sol = s.MakeAssignment();

To store an Assignment found by the CP solver, we use the StoreAssignment


DecisionBuilder:
DecisionBuilder * store = solver->MakeStoreAssignment(initial_sol);

This DecisionBuilder simply stores the current solution in the initial_sol


Assignment:
DecisionBuilder * initial_solution_and_store = solver->Compose(
initial_solution,
complementary_decision_builder,
store);

initial_solution_and_store constructs
DecisionBuilder is used in a nested search:

this

initial

solution.

This

std::vector<SearchMonitor *> monitors;


monitors.push_back(limit);
NestedSolveDecision * initial_solution_decision =
new NestedSolveDecision(initial_solution_and_store,
false,
monitors);

where:
limit is the SearchLimit given to the local search algorithm;
the NestedSolveDecision constructors arguments are respectively:
a DecisionBuilder to construct the next solution;

179

6.5. Basic working of the solver: Local Search

a bool to indicate if we restore the last solution in case we cannot nd a solution;


an std::vector<SearchMonitor *>.
The Apply() method of a NestedSolveDecision calls SolveAndCommit():
solver->SolveAndCommit(initial_solution_and_store, monitors);

where the arguments respectively are:


a DecisionBuilder;
an std::vector<SearchMonitor *>.
The DecisionBuilder companion to StoreAssignment is RestoreAssignment
that installs an Assignment as the current solution:
Assignment * solution = ...
...
DecisionBuilder * current_sol = s.MakeRestoreAssignment(solution);
...
// do something fancy starting with current_sol
DecisionBuilder * fancy_db = s.Compose(current_sol, ...);
...
s.Solve(fancy_db,...);

This is exactly the DecisionBuilder used when you give an initial solution to the
CP solver. The initial_solution DecisionBuilder is simply replaced with a
RestoreAssignment DecisionBuilder taking your initial Assignment.
Now that we have developed the machinery to nd and test the initial solution, we are ready to
wrap the nested solve process into a NestedSolveDecision:
// Main DecisionBuilder to find candidate solutions one by one
DecisionBuilder* find_neighbors =
solver->RevAlloc(new FindOneNeighbor(assignment_,
pool_,
ls_operator_,
sub_decision_builder_,
limit_,
filters_));
NestedSolveDecision* decision = solver->RevAlloc(
new NestedSolveDecision(find_neighbors,
false)));

The boolean argument in the NestedSolveDecisions constructor indicates that we


dont want to backtrack. The decision Decision will thus call SolveAndCommit() in
its left branch.
The Next() method

The Next() method of the LocalSearch DecisionBuilder is in charge of controling


the Local Search. We present it rst and discuss it next:

180

Chapter 6. Local search: the job-shop problem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

Decision * Next(Solver * solver) {


...
const int state = decision->state();
switch (state) {
case NestedSolveDecision::DECISION_FAILED: {
// SEARCHMONITOR CALLBACK
if (!LocalOptimumReached(solver->ActiveSearch())) {
// Stop the current search
...
}
solver->Fail();
return NULL;
}
case NestedSolveDecision::DECISION_PENDING: {
// Stabilize search tree by balancing the current search tree.
// Statistics are updated even if this is not relevant to the
// global search
...
const int depth = solver->SearchDepth();
if (depth < kLocalSearchBalancedTreeDepth) {
return solver->balancing_decision();
} else if (depth > kLocalSearchBalancedTreeDepth) {
solver->Fail();
}
...
return decision;
}
case NestedSolveDecision::DECISION_FOUND: {
// Nothing important for us in this simplified version
...
return NULL;
}
default: {
LOG(ERROR) << "Unknown local search state";
return NULL;
}
}
return NULL;
}

The decision variable on line 3 is the NestedSolveDecision created with the


FindOneNeighbor DecisionBuilder. We switch between three cases depending
on the state of the nested search initiated by this Decision.
Line 5: case DECISION_FAILED: The nested solving process failed, meaning that
there are no solution left. We let the SearchMonitors decide if a local optimum has been reached and cannot be improved. LocalOptimum() returns a
FalseExceptIfOneTrue.
Line 14: case DECISION_PENDING: This is the most interesting case: we
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 dont do anything by de181

6.6. Local Search Operators

fault. Once the search tree height is over 32, the NestedSolveDecision Decision
enters in action and when the height of the three gets higher than 32, we make the CP
solver Fail() to backtrack on line 23 thus keeping the height of the tree bounded.
Line 28: case DECISION_FOUND: The nested search found a solution that is the current solution. The LocalSearchs Next() method has done its job at the current
node and nothing needs to be done.
Solve(), SolveAndCommit(), SolveOnce(), etc...: what are the differences?
This topic is so important that the whole section 13.3 is devoted to it. You already can
jump ahead and read this section if youre curious.

6.6 Local Search Operators


You can nd the code in the le tutorials/cplusplus/chap6/dummy_ls.cc.
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 ctive example consists in minimizing the sum of IntVars {0 , . . . , 1 } each with
domain [0, 1]. We add the ctive constraint 0 1 (and thus ask for 2):
min

0 ,..., 1

subject to:

0 + 1 + ... + 1
0 1.
{0, . . . , 1} for = 0 . . . 1.

Of course, we already know the optimal solution. Can we nd 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 modied part of the solution (an Assignment
called delta) is broadcast. You can also dene a second Assignment representing the
changes to the last candidate solution dened by the local search operator (an Assignment
called deltadelta).
The CP solver takes care of these deltas and other hassles for the most common cases34 .
The next gure shows the LS Operators hierarchy.
34

deltas and deltadeltas are explained in more details in section 6.8.

182

Chapter 6. Local search: the job-shop problem

LocalSearchOperator

IntVarLocalSearchOperator

SequenceVarLocalSearchOperator

PathOperator

These classes are declared in the header constraint_solver/constraint_solveri.h.


The PathOperator class is itself the base class of several other path specialized LS Operators. We will review them in subsection 9.7.3.
IntVarLocalSearchOperator is a specialization of LocalSearchOperator
built for an array of IntVars while SequenceVarLocalSearchOperator is a
specialization of LocalSearchOperator built for an array of SequenceVars35 .

6.6.2 Dening a custom LS operator


We will construct an LS Operator for an array of IntVars but the API for an array of
SequenceVars is similar36 .
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;
SetValue(int64 i, int64 value): sets the value of the i th variable to
value in the current Assignment and allows to construct a new feasible solution;
Size(): returns the size of the array of IntVars;
IntVar* Var(int64 index): returns the variable of given index.
To construct a new feasible solution, just redene MakeOneNeighbor(). What are the
issues you need to pay attention to? First, you have to be sure to visit the neighborhood, i.e. to
iterate among the (feasible) candidate solutions of this neighborhood. If you return the same
35

At the time of writing, there are no LocalSearchOperators dened for IntervalVars. See subsection XXX for a workaround.
36
For instance, the SetValue() method is replaced by the SetForwardSequence() and
SetBackwardSequence() methods.

183

6.6. Local Search Operators

solution(s) again and again or if you dont provide any solution, the solver will not detect it (in
the second case, the solver will enter an innite loop). You are responsible to scour correctly
the neighborhood. Second, you have to be sure the variables you want to change do exist (i.e.
beware of going out of bounds on arrays).
Now the good news is that you dont have to test for feasibility: its the job of the solver. You
are even allowed to assign out of domain values to the variables. Again, the solver will discard
such solutions (you can also lter these solutions out, see the section Filtering).
Without further delay, here is the code for our custom LSO:
class DecreaseOneVar: public IntVarLocalSearchOperator {
public:
DecreaseOneVar(const std::vector<IntVar*>& variables)
: IntVarLocalSearchOperator(variables.data(), variables.size()),
variable_index_(0) {}
virtual ~MoveOneVar() {}
protected:
// Make a neighbor assigning one variable to its target value.
virtual bool MakeOneNeighbor() {
if (variable_index_ == Size()) {
return false;
}
const int64 current_value = Value(variable_index_);
SetValue(variable_index_, current_value - 1);
variable_index_ = variable_index_ + 1;
return true;
}
private:
virtual void OnStart() {
variable_index_ = 0;
}
int64 variable_index_;
};

Our custom LS Operator simply takes one variable at a time and decrease its value by 1. The
neighborhood visited from a given solution [0 , 1 , . . . , 1 ] is made of the following solutions
(when feasible):
{[0 , 1 , . . . , 1 ], [0 1, 1 , . . . , 1 ], [0 , 1 1, . . . , 1 ], . . . , [0 , 1 , . . . , 1 1]}
The given initial solution is also part of the neighborhood.
We have rewritten the protected method MakeOneNeighbor() to construct the
next solutions. The variable variable_index_ indicates the current variable we are
decreasing in the current solution. As long as there are remaining variables to decrease, MakeNextNeighbor() returns true. Once we have decreased the last variable
(variable_index_ is then equal to Size()), it returns false.
The private method OnStart() that is used whenever we start again with a new feasible
solution, simply resets the variable index to 0 to be able to decrease the rst variable 0 by 1.
We use the LS Operator DecreaseOneVar in the function SimpleLS() that starts as follow:
184

Chapter 6. Local search: the job-shop problem

void SimpleLS(const int64 n, const bool init_phase) {


CHECK_GE(n, 2) << "size of problem (n) must be >= 2";
LOG(INFO) << "Simple LS " << (init_phase ? "with initial phase" :
"with initial solution") << std::endl;
Solver s("Simple LS");
vector<IntVar*> vars;
s.MakeIntVarArray(n, 0, n-1, &vars);
IntVar* const sum_var = s.MakeSum(vars)->Var();
OptimizeVar* const obj = s.MakeMinimize(sum_var, 1);
// unique constraint x_0 >= 1
s.AddConstraint(s.MakeGreaterOrEqual(vars[0], 1));
...

must be greater or equal to 2 as we ask for 0

1.

The OptimizeVar SearchMonitor is very important as it will give the direction to follow for the local search algorithm. Without it, the local search would walk randomly wihout
knowing where to go.
Next, based on the Boolean variable FLAG_initial_phase, we create
DecisionBuilder to nd an initial solution or we construct an initial Assignment:

// initial phase builder


DecisionBuilder * db = NULL;
// initial solution
Assignment * const initial_solution = s.MakeAssignment();
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);
}
}
}

As we assign the biggest value (ASSIGN_MAX_VALUE) to the rst unbound variables


(CHOOSE_FIRST_UNBOUND), the initial solution constructed by the DecisionBuilder
will be
[ 1, 1, . . . , 1].
To have some variation, we construct the following initial solution by hand:
[ 1, 2, 1, 2, . . . , {1 + ( + 1) mod 2}]

185

6.6. Local Search Operators


where the value for 1 is 2 if is even and 1 otherwise37 .
The search phase using the LS Operator is given by a... DecisionBuilder which shouldnt
surprise you by now:
// IntVarLocalSearchOperator
DecreaseOneVar one_var_ls(vars);
LocalSearchPhaseParameters* ls_params = NULL;
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);
}

Notice how the LS Operator is passed to the DecisionBuilder by means of the


LocalSearchPhaseParameters.
We collect the best and last solution:
SolutionCollector* const collector = s.MakeLastSolutionCollector();
collector->Add(vars);
collector->AddObjective(sum_var);

and log the search whenever a new feasible solution is found:


SearchMonitor* const log = s.MakeSearchLog(1000, obj);

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);

If we limit ourselves to 4 variables and construct an initial solution by hand:


./dummy_ls -n=4 -initial_phase=false

we obtain the following partial output:


Simple LS with initial solution
Start search, memory used = 15.09 MB
Root node processed (time = 0 ms, constraints = 2, memory used =
15.09 MB)
Solution #0 (objective value = 10, ...)
Solution #1 (objective value = 9, ...)
37

The modulo operator (mod) nds the remainder of the division of one (integer) number by another: For
instance, 11 mod 5 = 1 because 11 = 2 5 + 1. When you want to test a positive number for parity, you can
test mod 2. If mod 2 = 0 then is even, otherwise it is odd. In C++, the mod operator is %.

186

Chapter 6. Local search: the job-shop problem

Solution
Solution
Solution
Solution
Solution
Solution
Solution
Solution
Finished

#2 (objective value = 8, ...)


#3 (objective value = 7, ...)
#4 (objective value = 6, ...)
#5 (objective value = 5, ...)
#6 (objective value = 4, ...)
#7 (objective value = 3, ...)
#8 (objective value = 2, ...)
#9 (objective value = 1, ...)
search tree, ..., neighbors = 23, filtered neighbors = 23,
accepted neigbors = 9, ...)
End search (time = 1 ms, branches = 67, failures = 64, memory used =
15.13 MB, speed = 67000 branches/s)
Objective value = 1

As you can see, 10 solutions were generated with decreased objective values. Solution #0
is the initial solution given: [3, 2, 3, 2]. Then as expected, 9 neighborhoods were visited and
each time a better solution was chosen:
neighborhood 1 around [3, 2, 3, 2]: [2, 2, 3, 2] is immediately taken as it is a better solution
with value 9;
neighborhood 2 around [2, 2, 3, 2]: [1, 2, 3, 2] is a new better solution with value 8;
neighborhood 3 around [1, 2, 3, 2]: [0, 2, 3, 2] is rejected as infeasible, [1, 1, 3, 2] is a new better solution with value 7;
neighborhood 4 around [1, 1, 3, 2]: [0, 1, 3, 2] is rejected as infeasible, [1, 0, 3, 2] is a new better solution with value 6;
neighborhood 5 around [1, 0, 3, 2]: [0, 0, 3, 2], [0, 1, 3, 2] are rejected as infeasible,
[1, 0, 2, 2] is a new better solution with value 5;
neighborhood 6 around [1, 0, 2, 2]: [0, 1, 2, 2], [1, 1, 2, 2] are rejected as infeasible,
[1, 0, 1, 2] is a new better solution with value 4;
neighborhood 7 around [1, 0, 1, 2]: [0, 0, 1, 2], [1, 1, 1, 2] are rejected as infeasible,
[1, 0, 0, 2] is a new better solution with value 3;
neighborhood 8 around [1, 0, 0, 2]: [0, 0, 0, 2], [1, 1, 0, 2], [1, 0, 1, 2] are rejected as infeasible, [1, 0, 0, 1] is a new better solution with value 2;
neighborhood 9 around [1, 0, 0, 1]: [0, 0, 0, 1], [1, 1, 0, 1], [1, 0, 1, 1] are rejected as infeasible, [1, 0, 0, 0] is a new better solution with value 1;
At this point, the solver is able to recognize that there are no more possibilities. The two last
lines printed by the SearchLog summarize the local search:
Finished search tree, ..., neighbors = 23, filtered neighbors = 23,
accepted neighbors = 9, ...)
End search (time = 1 ms, branches = 67, failures = 64, memory used =
15.13 MB, speed = 67000 branches/s)

There were indeed 23 constructed candidate solutions among which 23 (ltered neighbors)
were accepted after ltering and 9 (accepted neighbors) were improving solutions.
If you take the last visited neighborhood (neighborhood 9), you might wonder if it was really
187

6.6. Local Search Operators


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 ltered those solutions
out and told the solver to disregard them. We didnt lter out any solution (and this is the reason
why the number of constructed neighbors is equal to the number of ltered neighbors). You
can learn more about ltering in the section Filtering.
If you want, you can try to start with the solution provided by the DecisionBuilder
([3, 3, 3, 3] when = 4) and see if you can gure out what the 29 constructed candidate solutions (neighbors) and 11 accepted solutions are.

6.6.3 Combining LS operators


Often, you want to combine several LocalSearchOperators. This can be done with the
ConcatenateOperators() method:
LocalSearchOperator* ConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops);

This creates a LocalSearchOperator which concatenates a vector of operators. Each operator from the vector is called sequentially. By default, when a candidate solution is accepted,
the neighborhood exploration restarts from the last active operator (the one which produced
this candidate solution).
This can be overriden by setting restart to true to force the exploration to start from the
rst operator in the vector:
LocalSearchOperator* Solver::ConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops, bool restart);

You can also use an evaluation callback to set the order in which the operators are explored
(the callback is called in LocalSearchOperator::Start()). The rst argument of the
callback is the index of the operator which produced the last move, the second argument is the
index of the operator to be evaluated. Ownership of the callback is taken by the solver.
Here is an example:
const int kPriorities = {10, 100, 10, 0};
int64 Evaluate(int active_operator, int current_operator) {
return kPriorities[current_operator];
}
LocalSearchOperator* concat =
solver.ConcatenateOperators(operators,
NewPermanentCallback(&Evaluate));

The elements of the operators vector will be sorted by increasing priority and explored in
that order (tie-breaks are handled by keeping the relative operator order in the vector). This
would result in the following order:
operators[3], operators[0], operators[2], operators[1].
Sometimes you dont know in what order to proceed. Then the following method might help
you:
188

Chapter 6. Local search: the job-shop problem

LocalSearchOperator* Solver::RandomConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops);

This LocalSearchOperator calls a random operator at each call to


MakeNextNeighbor(). You can provide the seed that is used to initialize the random number generator:
LocalSearchOperator* Solver::RandomConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops, int32 seed);

6.6.4 Interesting LS operators


Several existing LocalSearchOperators can be of great help. Combine these operators
with your own customized operators. PathOperators will be reviewed in subsection 9.7.3.
NeighborhoodLimit
This LocalSearchOperator creates a LocalSearchOperator that wraps another
LocalSearchOperator and limits the number of candidate solutions explored (i.e. calls to
MakeNextNeighbor() from the current solution (between two calls to Start()). When
this limit is reached, MakeNextNeighbor() returns false. The counter is cleared when
Start() is called.
Here is the factory method:
LocalSearchOperator* Solver::MakeNeighborhoodLimit(
LocalSearchOperator* const op,
int64 limit);

MoveTowardTargetLS
Creates a local search operator that tries to move the assignment of some variables toward a
target. The target is given as an Assignment. This operator generates candidate solutions
which only have one variable that belongs to the target Assignment set to its target value.
There are two factory methods to create a MoveTowardTargetLS operator:
LocalSearchOperator* Solver::MakeMoveTowardTargetOperator(
const Assignment& target);

and
LocalSearchOperator* Solver::MakeMoveTowardTargetOperator(
const std::vector<IntVar*>& variables,
const std::vector<int64>& target_values);

The target is here given by two std::vectors: a vector of variables and a vector of associated target values. The two vectors should be of the same length and the variables and values
are ordered in the same way.
189

6.7. The jobshop problem: and now with local search!

The variables are changed one after the other in the order given by the Assignment or the
vector of variables. When we restart from a new feasible solution, we dont start all over again
from the rst variable but keep changing variables from the last change.
DecrementValue and IncrementValue
These operators do exactly what their names say: they decrement and increment by 1 the value
of each variable one after the other.
To create them, use the generic factory method
LocalSearchOperator* Solver::MakeOperator(
const std::vector<IntVar*>& vars,
Solver::LocalSearchOperators op);

where op is an LocalSearchOperators enum. The values for DecrementValue and


IncrementValue are respectively Solver::DECREMENT and Solver::INCREMENT.
The variables are changed in the order given by the std::vector. Whenever we start to
explore a new neighborhood, the variables are changed from the beginning of the vector anew.
Large Neighborhood Search (LNS)
And last but not least, in or-tools, Large Neighborhood Search is implemented with
LocalSearchOperators but this is the topic of section ??.

6.7 The jobshop problem: and now with local search!


You can nd the code in the les jobshop_ls.h, jobshop_ls1.cc,
jobshop_ls2.cc
and
jobshop_ls3.cc
and
the
data
les
in first_example_jssp.txt and abz9.
We have seen in the previous section how to implement local search on our dummy example. This time, we apply local search on a real problem and present the real thing: the
MakeOneNeighbor() method, the delta and deltadelta Assignments and how to
implement incremental LocalSearchOperators.
To solve the job-shop problem, well dene two basic LocalSearchOperators. First,
well apply them separately and then well 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.

6.7.1 LocalSearchOperators: the real thing!


Until now, we only have overloaded the MakeOneNeighbor() method of
a LocalSearchOperator but as we have seen in sub-section 6.5.3, the real method

190

Chapter 6. Local search: the job-shop problem

called by the MakeOneNeighbor DecisionBuilder is MakeNextNeighbor().


Before we dissect MakeNextNeighbor(), we quickly explain again what the deltas are.
Deltas and DeltaDeltas
The idea behind the Deltas and DeltaDeltas is really simple: efciency. Only the modied part of the solution is broadcast:
Delta: the difference between the initial solution that denes 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()
The signature of the MakeNextNeighbor() method is:
bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta)

This method constructs the delta and deltadelta corresponding to the new candidate solution and returns true. If the neighborhood has been exhausted, i.e. the
LocalSearchOperator cannot nd another candidate solution, this method returns
false.
When you write your own MakeNextNeighbor() method, you have to provide the new
delta but you can skip the deltadelta if you prefer. This deltadelta can be convenient when you dene your lters and you can gain some efciency over the sole use of
deltas.
To help you construct these deltas, we provide an inner mechanism that constructs automatically these deltas when you use the following self-explanatory setters:
for IntVarLocalSearchOperators only:
SetValue(int64 index, int64 value);
for SequenceVarLocalSearchOperators only:
SetForwardSequence(int64 index, const std::vector<int>&
value);
SetBackwardSequence(int64 index, const
std::vector<int>& value);
for both:
Activate(int64 index);
Deactivate(int64 index).

191

6.7. The jobshop problem: and now with local search!

If you only use these methods to change the current solution, you then can automatically construct the deltas by calling the ApplyChanges() method and revert these changes by
calling the RevertChanges() method.
We recommend to use the following template to dene your MakeNextNeighbor()
method:
virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (NEIGHBORHOOD EXHAUSTED) {
return false;
}
// CONSTRUCT NEW CANDIDATE SOLUTION
...
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}

Currently, ApplyChanges() always returns true but this might change in the future and
then you might have to revert the changes, hence the while() loop.
We also provide several getters:
for IntVarLocalSearchOperators only:
int64 Value(int64 index);
IntVar* Var(int64 index);
int64 OldValue(int64 index);
for SequenceVarLocalSearchOperators only:
const std::vector<int>& Sequence(int64 index);
SequenceVar* Var(int64 index);
const std::vector<int>& OldSequence(int64 index);
for both:
bool IsIncremental();
bool Activated(int64 index);

192

Chapter 6. Local search: the job-shop problem

Why would I want


MakeOneNeighbor()?

to

use

MakeNextNeighbor()

instead

of

One reason is efciency: you skip one callback. But the real reason is that you might
need other methods than the ones that are provided to construct your candidate solution.
In this case, you have no other choice than to reimplement the MakeNextNeighbor()
method.

Incrementality
[TO BE WRITTEN]

6.7.2 The initial solution


We let the CP solver construct the initial solution for us.
What about reusing
the DecisionBuilder dened in section 6.2 and grab its rst 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);
// Initial solution for the Local Search.
Assignment* const first_solution = solver.MakeAssignment();
first_solution->Add(all_sequences);
first_solution->AddObjective(objective_var);
// Store the first solution.
DecisionBuilder* const store_db =
solver.MakeStoreAssignment(first_solution);
// 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);
LOG(INFO) << "Looking for the first solution";
const bool first_solution_found = solver.Solve(first_solution_phase);
if (first_solution_found) {
LOG(INFO) << "Solution found with makespan = "
<< first_solution->ObjectiveValue();
} else {
LOG(INFO) << "No initial solution found!";

193

6.7. The jobshop problem: and now with local search!

return;
}

If you have some troubles to follow, go back to section 6.2 to understand


the sequence_phase and obj_phase DecisionBuilders.
Here, we simply
add a StoreAssignment DecisionBuilder at the leaf of the search tree to collect
the solutions with the first_solution_phase DecisionBuilder. Our initial
solution will be stored in the first_solution Assignment. Next, we dene a rst
LocalSearchOperator.

6.7.3 Exchanging two IntervalVars on a SequenceVar


Youll nd the code in the le jobshop_ls1.cc and the SwapIntervals operator in the
le 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- operator.
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 rst IntervalVar variable to swap;
int current_second_: the index of the second IntervalVar variable to swap.
We proceed sequentially with the rst SequenceVar (current_var_ = 0) and exchange
the rst and second IntervalVars, then the rst 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 exhausted, i.e. it returns false when there are no more candidate to construct. Size() and
Var() are helper methods dened in the SequenceVarLocalSearchOperator class.
We start with current_var_, current_first_ and current_second_ all set to 0.
Pay attention to the fact that current_first_ and current_second_ are also updated
inside the if conditions.
194

Chapter 6. Local search: the job-shop problem

We are now ready to dene the OnStart() and MakeNextNeighbor() methods.


The OnStart() method is straightforward:
virtual void OnStart() {
current_var_ = 0;
current_first_ = 0;
current_second_ = 0;
}

For the MakeNextNeighbor() method, we use our template:


virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (!Increment()) {
return false;
}
std::vector<int> sequence = Sequence(current_var_);
const int tmp = sequence[current_first_];
sequence[current_first_] = sequence[current_second_];
sequence[current_second_] = tmp;
SetForwardSequence(current_var_, sequence);
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}

If
Increment()
returns
false,
we
have
exhausted
the
neighborhood and MakeNextNeighbor() must return false.
Sequence()
and
SetForwardSequence()
are
two
helper
methods
from
the
SequenceVarLocalSearchOperator class that allow us to use the
ApplyChanges() method to construct the deltas.
And thats it! Our LocalSearchOperator operator is completed. Lets test it!
First, we need our LocalSearchOperator:
LocalSearchOperator* const swap_operator =
solver.RevAlloc(new SwapIntervals(all_sequences.data(),
all_sequences.size()));

Then we need a complementary DecisionBuilder to construct feasible candidate solutions. We dont want to spent too much time on the completion of our solutions. We will use
the CHOOSE_RANDOM_RANK_FORWARD strategy:
DecisionBuilder* const random_sequence_phase =
solver.MakePhase(all_sequences,
Solver::CHOOSE_RANDOM_RANK_FORWARD);

195

6.7. The jobshop problem: and now with local search!

DecisionBuilder* const complementary_ls_db =


solver.MakeSolveOnce(solver.Compose(random_sequence_phase,
obj_phase));

If we run the program jobshop_ls1 with our instance problem (le


first_example_jssp.txt), we get the optimal solution. Always a good sign.
With the instance in abz9 however, we only get a solution with a cost of 1051 in 51,295
seconds:
Time (in s.)
51,295

Value
1051

Candidates
31172

Solutions
26

Not very satisfactory: 1051 is really far from the optimal value of 679. Lets try to generalize
our operator. Instead of just swapping two IntervalVars, well shufe an arbitrary number
of IntervalVars per SequenceVar in the next subsection.

6.7.4 Exchanging
an
arbitrary
number
IntervalVars on a SequenceVar

of

contiguous

Youll nd the code in the le jobshop_ls2.cc and the ShuffleIntervals operator


in the le jobshop_ls.
After having implemented the SwapIntervals operator, the only real difculty that remains
is to implement a permutation. This is not an easy task but well elude this difculty by only exchanging contiguous IntervalVars and using the std::next_permutation() function. You can nd 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
1
2
3
4
5
6

Permutations
012
021
102
120
201
210

We have generated the permutations of 0,1,2 with std::next_permutation(). There


are 3! = 6 permutations (the rst 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, etc38 .
38

This explanation is not rigorous but it is simple and you can ll the gaps. What happens if you start with 1 0
2? The std::next_permutation() function simply returns 1 2 0 (oops, there goes our rigour again!).

196

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 gags ag FLAGS_shuffle_length. First, we have to test if this length makes
sense but we also have to adapt it to each SequenceVar variable.
Without delay,
we present
LocalSearchOperator:

the

constructor

of

the

ShuffleIntervals

ShuffleIntervals(const SequenceVar* const* vars,


int size,
int max_length) :
SequenceVarLocalSearchOperator(vars, size),
max_length_(max_length),
current_var_(-1),
current_first_(-1),
current_length_(-1) {}

vars and size are just the array of SequenceVars and its size. max_length is the length
of the sequence of IntervalVars to shufe. Because you can have less IntervalVars
for a given SequenceVar, we have named it max_length.
The indices are very similar to the ones of the SwapIntervals operator:
current_var_: the index of the processed SequenceVar;
current_first_: the index of the rst IntervalVar variable to shufe;
current_length_: the length of the current sub-array of indices to shufe. It must
be smaller or equal to the number of IntervalVars in the SequenceVar.
Here is the code to increment the next permutation:
bool Increment() {
if (!std::next_permutation(current_permutation_.begin(),
current_permutation_.end())) {
// No permutation anymore -> update indices
if (++current_first_ >
Var(current_var_)->size() - current_length_) {
if (++current_var_ >= Size()) {
return false;
}
current_first_ = 0;
current_length_ = std::min(Var(current_var_)->size(),
max_length_);
current_permutation_.resize(current_length_);
}
// Reset first permutation in case we have to increase
// the permutation.
for (int i = 0; i < current_length_; ++i) {
current_permutation_[i] = i;
}
// Start with the next permutation, not the identity
// just constructed.
if(!std::next_permutation(current_permutation_.begin(),
If you give it 2 1 0, this function returns false but there is a side effect as the array will be ordered! Thus in
our case, well get 0 1 2!

197

6.7. The jobshop problem: and now with local search!

current_permutation_.end())) {
LOG(FATAL) << "Should never happen!";
}
}
return true;
}

Thanks to the std::next_permutation() function, this is a breeze! The OnStart()


method is again straightforward:
virtual void OnStart() {
current_var_ = 0;
current_first_ = 0;
current_length_ = std::min(Var(current_var_)->size(), max_length_);
current_permutation_.resize(current_length_);
for (int i = 0; i < current_length_; ++i) {
current_permutation_[i] = i;
}
}

We just have to pay attention to resize() the std::vector current_permutation_


of indices and we start with the same permutation: [0, 1, 2, 3, ...].
We again use our template for the MakeNextNeighbor() method:
virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (!Increment()) {
return false;
}
std::vector<int> sequence = Sequence(current_var_);
std::vector<int> sequence_backup(current_length_);
for (int i = 0; i < current_length_; ++i) {
sequence_backup[i] = sequence[i + current_first_];
}
for (int i = 0; i < current_length_; ++i) {
sequence[i + current_first_] =
sequence_backup[current_permutation_[i]];
}
SetForwardSequence(current_var_, sequence);
if (ApplyChanges(delta, deltadelta)) {
return true;
}
}
return false;
}

If Increment() returns false, we have exhausted the neighborhood and


MakeNextNeighbor() must return false. After the call to Increment(), we
simply copy the indices according to the new generated permutation and call the helper
method SetForwardSequence() to update the current SequenceVar variable.
ApplyChanges() constructs the deltas for us.
198

Chapter 6. Local search: the job-shop problem

File jobshop_ls2.cc is exactly the same as le jobshop_ls1.cc except that we use


the ShuffleIntervals operator instead of the SwapIntervals operator.
We
again
obtain
the
optimal
solution
on
our
instance
problem
(le
first_example_jssp.txt
whether
shuffle_length=2
or
shuffle_length=3). What about the abz9 instance? The next table summarize
some tests with different values for the suffle_length parameter:
suffle_length
2
3
4
5

Time (in s.)


12,301
21,312
170,087
584,173

Value
1016
1087
1034
1055

Candidates
4302
7505
70854
268478

Solutions
32
15
33
27

These results are typical for a local search operator. There certainly are several lessons to be
drawn from these results, but lets focus on one of the most basic and important ones. The
path taken to nd the local optimum is crucial. Even if the neighborhoods (theoretically) constructed with suffle_length set to 2 are all contained in the neighborhoods constructed
with suffle_length set to 3, we dont reach the same local optimum. This is very important to understand. The paths taken in both cases are different. The (practical) construction of
the neighbourhoods is dynamic and path-dependent. Good (meta-)heuristics are path-aware:
these heuristics take the path (and thus the history of the search) into account. Moreover, bigger neighbourhoods (shuffle_length = 3) arent necessarily better than smaller ones
(shuffle_length = 2). We obtain a better solution quicker with shuffle_length=2
than with suffle_length=3.
The best solution obtained so far has a value of 1016. Can we do better? Thats the topic of
next sub-section!

6.7.5 Can we do better?


Youll nd the code in the le jobshop_ls3.cc.
You should know by now that whenever we ask this question in this manual, the answer is yes. To nd a better solution, well rst investigate how important the initial solution is and then well enlarge our denition of a neighborhood by combining our two
LocalSearchOperators.
The initial solution
Local search is strongly dependent on the initial solution. Investing time in nding a good
solution is a good idea. Well use... local search to nd an initial solution to get the real local
search started! The idea is that maybe we can nd an even better solution in the vicinity of
this initial solution. We dont want to spend too much time to nd it though and well limit
ourselves to a custom-made SearchLimit. To dene this SearchLimit, we construct a
callback:
class LSInitialSolLimit : public ResultCallback<bool> {
public:

199

6.7. The jobshop problem: and now with local search!

LSInitialSolLimit(Solver * solver, int64 global_time_limit,


int solution_nbr_tolerance) :
solver_(solver), global_time_limit_(global_time_limit),
solution_nbr_tolerance_(solution_nbr_tolerance),
time_at_beginning_(solver_->wall_time()),
solutions_at_beginning_(solver_->solutions()),
solutions_since_last_check_(0) {}
// Returns true if limit is reached, false otherwise.
virtual bool Run() {
bool limit_reached = false;
// Test if time limit is reached.
if ((solver_->wall_time() - time_at_beginning_)
> global_time_limit_) {
limit_reached = true;
// Test if we continue despite time limit reached.
if (solver_->solutions() - solutions_since_last_check_
>= solution_nbr_tolerance_) {
// We continue because we produce enough new solutions.
limit_reached = false;
}
}
solutions_since_last_check_ = solver_->solutions();
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 rst local search that nds our initial solution, we use the same code as in the
le jobshop_ls2.cc (we call this rst solution first_solution).
To nd an initial solution, we use local search and start form the first_solution found.
We only use a ShuffleIntervals operator with a shufe length of 2. This time, we limit
this local search with our custom limit:
SearchLimit * initial_search_limit = solver.MakeCustomLimit(
new LSInitialSolLimit(&solver,
FLAGS_initial_time_limit_in_ms,
FLAGS_solutions_nbr_tolerance));

200

Chapter 6. Local search: the job-shop problem

FLAGS_initial_time_limit_in_ms and FLAGS_solutions_nbr_tolerance


are the two gags ags we use in the constructor of the callback LSInitialSolLimit
described above to limit the search.
The initial solution is stored in an Assigment initial_solution.
Now, we are ready to prepare the local search with our two LocalSearchOperators combined.
Combining the two LocalSearchOperators
Often, one LocalSearchOperator isnt enough to dene a good neighborhood. Finding
a good denition of a neighborhood is an art and is really difcult. One way to diversify a
neighborhood is to combine several basic LocalSearchOperators. Here, we combine
SwapIntervals and ShuffleIntervals:
std::vector<LocalSearchOperator*> operators;
LocalSearchOperator* const swap_operator =
solver.RevAlloc(new SwapIntervals(all_sequences.data(),
all_sequences.size()));
operators.push_back(swap_operator);
LocalSearchOperator* const shuffle_operator =
solver.RevAlloc(new ShuffleIntervals(all_sequences.data(),
all_sequences.size(),
FLAGS_shuffle_length));
operators.push_back(shuffle_operator);
LocalSearchOperator* const ls_concat =
solver.ConcatenateOperators(operators, true);

The
ConcatenateOperators()
method
takes
an
std::vector
of
LocalSearchOperator and a bool that indicates if we want to restart the operators one after the other in the order given by this vector once a solution has been found.
The rest of the code is similar to that in the le jobshop_ls2.cc.
Results
If we solve our problem instance (le first_example_jssp.txt), we still get the optimal
solution. No surprise here. What about the abz9 instance?
With our default value of
time_limit_in_ms = 0, thus no time limit;
shuffle_length = 4;
initial_time_limit_in_ms = 20000, thus a time of 20 seconds to nd an
initial solution with local search and the ShuffleIntervals operator with a shufe
length of 2 and;
solutions_nbr_tolerance = 1,
201

6.8. Filtering

we are not able to improve our best solution so far!


As we said, local search is very sensitive to the initial solution chosen. In the next table, we
start with different initial solutions:
Initial time limit
1,000
2,000
3,000
4,000
5,000
6,000
7,000
...
>= 13,000

Initial sol. obj.


1114
1103
1093
1089
1073
1057
1042
...
1016

Time
81,603
103,139
104,572
102,860
84,555
42,235
36,935
...
19,229

Value
983
936
931
931
931
1012
1012
...
1016

Candidates
49745
70944
70035
68359
63949
29957
26515
...
13017

Solutions
35
59
60
60
60
32
32
...
32

The rst column lists the times allowed to nd the initial solution with the
ShuffleIntervals operator (with its shufe length set to 2) and the second column collects the objective values of this initial solution. The more time given to the rst local search,
the better the objective values. The next four columns are the same as before.
You might think that starting from a better solution would give better results but it is no necessarily the case. Our best result, 931 is obtained when we start from solutions with an average
objective value. When we start with better solutions, like the one with an objective value of
1016, we completely miss the 931 solution! This 931 solution seems to be a local optimum
for our local search and it seems we can not escape it. In chapter 7, well see how some metaheuristics escape this local minimum. For now, we turn our attention to another preoccupation:
if you read the Candidates column and compare it with the Solutions column, you can see that
our algorithm produces lots of candidates and very few solutions. This is normal. Remember
that every time a candidate (a neighbor) is produced, the CP solver takes the time to verify if
this candidate is a feasible solution. This is costly. In the next section, well see a mechanism
to shortcut this verication and command the solver to disregard some candidates without the
need for the solver to test them explicitly.

6.8 Filtering
You can nd the code in the le dummy_ls_filtering.cc.
Our local search strategy of section 6.6 is not very efcient: we test lots of unfeasible
or undesirable candidate solutions. LocalSearchFilters allow to shortcut the solvers
solving and testing mechanism: we can tell the solver right away to skip a candidate solution.

6.8.1 LocalSearchFilters
LocalSearchFilters instruct the CP solver to skip (or not) the current
candidate solution.
You can nd the declaration and denition in the header
constraint_programming/constraint_solveri.h.

202

Chapter 6. Local search: the job-shop problem


There are basically two methods to implement39 :
virtual bool Accept(const Assignment* delta,
const Assignment* deltadelta) = 0;
virtual void Synchronize(const Assignment* assignment) = 0;

As you can see, these two methods are pure virtual methods and thus must be implemented.
The Accept() method returns true if you accept the current candidate solution to be
tested by the CP solver and false if you know you can skip this candidate solution. The
candidate solution is given in terms of delta and deltadelta. These are provided by
the MakeNextNeighbor() of the LocalSearchOperator. The Synchronize()
method, lets you synchronize the LocalSearchFilter with the current solution, which
allows you to reconstruct the candidate solutions given by the delta Assignment.
If your LocalSearchOperator is incremental, you must notice the CP solver by implementing the IsIncremental() method:
virtual bool IsIncremental() const { return true; }

By default, this method returns false.

6.8.2 Dening a custom LocalSearchFilter


We will lter the dummy example from the le dummy_ls.cc. You can nd the code in the
le 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. Lets help the poor solver and tell him right
away to discard such candidate solutions.
We are using IntVars and thus well inherit from IntVarLocalSearchFilter and
instead of implementing the Synchronize() method, well 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) {}

obj_ is an int64 to keep the objective value of the current solution.


Lets synchronize our lter with the objective value of the current solution:
virtual void OnSynchronize() {
obj_ = 0;
for (int i = 0; i < Size(); ++i) {
obj_ += Value(i);
}
}
39

For IntVar, the specialized IntVarLocalSearchFilter offers convenient methods and you should
rather implement the OnSynchronize() method that is called at the end of the Synchronize() method.

203

6.8. Filtering

Several helper methods are dened in the IntVarLocalSearchFilter class:


int64 Value(int index) const: returns the value of the th variable of the current solution. These values are automatically updated when Synchronize() is called;
IntVar* Var(int index) const:
std::vector;

returns the th variable given in the

bool FindIndex(const IntVar* const var, int64* index)


const: returns a bool to indicate if the th variable was found. If yes, you can
use the index variable;
int Size() const: returns the size of the std::vector of IntVars given to
the constructor of the IntVarLocalSearchFilter class.
To test a candidate solution, we use the delta, and sum the changed value of the objective
function:
virtual bool Accept(const Assignment* delta,
const Assignment* unused_deltadelta) {
const Assignment::IntContainer& solution_delta =
delta->IntVarContainer();
const int solution_delta_size = solution_delta.Size();
int64 new_obj = obj_;
for (int index = 0; index < solution_delta_size; ++index) {
int64 touched_var = -1;
FindIndex(solution_delta.Element(index).Var(), &touched_var);
const int64 old_value = Value(touched_var);
const int64 new_value = solution_delta.Element(index).Value();
new_obj += new_value - old_value;
}
return new_obj < obj_;
}

First, we acquire the IntContainer and its size. Each Assignment has containers
to keep its IntVars, IntervalVars and SequenceVars (more precisely pointers to).
To access those containers, use the corresponding Container() methods if you dont
want to change their content, use the corresponding Mutable...Container() method
if you want to change their content. For instance, to change the SequenceVars, use the
MutableSequenceVarContainer() method.
For the sake of efciency, Assignment contains a light version of the variables. For instance,
an ntVarContainer contains IntVarElements and the call to
FindIndex(solution_delta.Element(index).Var(), &touched_var);

simply returns the LocalSearchFilters index in touched_var of the corresponding


variable element with index index in the Assignment.
We only accept a candidate solution if its objective value is better that the one of the current
solution:
return new_obj < obj_;

In the DummyLS() method, we add the lter as follows:


204

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);

If we try again the dummy instance [3, 2, 3, 2]:


./dummy_ls_filtering -n=4 -initial_phase=false

we obtain:
..., neighbors = 23, filtered neighbors = 23,
accepted neighbors = 9, ...

which is exactly the same output without the ltering.


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, isnt? Our main purpose was to
learn how to write a custom LocalSearchFilter and we did it!
OK, youre not satised and neither are we. We know that 0
must be equal or greater than 0.

1 and that the other variables

Lets write a LocalSearchFilter that lters infeasible candidate solutions. We dont need
to provide an OnSyncronize() method. Here is our version of the Accept() method:
virtual bool Accept(const Assignment* delta,
const Assignment* deltadelta) {
const Assignment::IntContainer& solution_delta =
delta->IntVarContainer();
const int solution_delta_size = solution_delta.Size();
for (int index = 0; index < solution_delta_size; ++index) {
const IntVarElement& element = solution_delta.Element(index);
if (!element.Var()->Contains(element.Value())) {
return false;
}
}
return true;
}

Aha, you probably expected an ad hoc solution rather than the general solution above, didnt
you?40 .
We now obtain:
..., neighbors = 23, filtered neighbors = 9,
accepted neighbors = 9, ...
40

To be fair, this solution is not as general as it should be. We didnt take into account the fact that some
IntervalVar variables can be non active but for IntVars and SequenceVars it works well.

205

6.8. Filtering

Of course, we could have improved our LocalSearchOperator so that it doesnt produce


such infeasible solutions!

6.8.3 Interesting LocalSearchFilters


Two LocalSearchFilters have already been implemented in or-tools. There exist a
general version of the two LocalSearchFilters: ObjectiveFilter (and some subclasses) and VariableDomainFilter.
It is easy to add a VariableDomainFilter, simply use
LocalSearchFilter* Solver::MakeVariableDomainFilter();

The ObjectiveFilter is more interesting and exists in different avors depending on:
the type of move that is accepted based on the current objective value:
The different possibilities are given by the LocalSearchFilterBound enum:
GE: Move is accepted when the candidate objective value >= objective.Min;
LE: Move is accepted when the candidate objective value <= objective.Max;
EQ: Move is accepted when the current objective value is in the interval
objective.Min ... objective.Max.
the type of operation used in the objective function:
The different possibilities are given in the LocalSearchOperation enum and concern the variables given to the MakeLocalSearchObjectiveFilter() method:
SUM: The objective is the sum of the variables;
PROD: The objective is the product of the variables;
MAX: The objective is the max of the variables;
MIN: The objective is the min of the variables.
the callbacks used: we refer the curious reader to the code in the le
constraint_programming/local_search.cc for more details about different
available callbacks.
For all these versions, the factory method is MakeLocalSearchObjectiveFilter().
Again, we refer the reader to the code to see all available renements.

206

Chapter 6. Local search: the job-shop problem

6.9 Summary
6.9.1 What is missing?
6.9.2 To go further:
Examples:
Lab:

207

CHAPTER

SEVEN

META-HEURISTICS: SEVERAL PREVIOUS PROBLEMS

Overview:
Prerequisites:

Basic knowledge of C++.


Basic knowledge of Constraint Programming (see chapter 1).
Basic knowledge of the Constraint Programming Solver (see chapter 2).
Basic knowledge about how to dene an objective function (see section 3.3).
Section 5.3 on the inner working of the solver helps but is not mandatory.
For section 7.3: chapter 6 about local search and the job-shop problem.
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap7.


The les inside this directory are:

210

Chapter 7. Meta-heuristics: several previous problems

7.1 Meta-heuristics in or-tools


7.2 Search limits and SearchLimits
7.2.1 An example of a custom Searchlimit

7.3 Large neighborhood search (LNS): the job-shop


problem
7.3.1 What is Large Neighborhood Search?
7.3.2 Large Neighborhood Search in or-tools
7.3.3 Interesting LNS operators
SimpleLNS
RandomLNS

7.3.4 An heuristic to solve the job-shop problem


SequenceLns
Everything together

7.4 Restarting the search


7.4.1 Luby

7.5 Tabu search (TS)


7.5.1 The basic idea
7.5.2 The implementation
7.5.3 First results

7.6 Simulated annealing (SA)


7.6.1 The basic idea
7.6.2 The implementation

211

CHAPTER

EIGHT

CUSTOM CONSTRAINTS: THE


ALLDIFFERENT_EXCEPT_0 CONSTRAINT

Classes under scrutiny:


Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap4.


The les inside this directory are:

8.1 Basic working of the solver: constraints


8.1.1 Description

8.2 Consistency
8.3 The AllDifferent constraint
8.4 Changing dynamically the improvement step with a
SearchMonitor
8.5 Summary

Part III
Routing

CHAPTER

NINE

TRAVELLING SALESMAN PROBLEMS WITH


CONSTRAINTS: THE TSP WITH TIME WINDOWS

The third part of this manual deals with Routing Problems: we have a graph1 and seek to nd
a set of routes covering some or all nodes and/or edges/arcs while optimizing an objective
function along the routes2 (time, vehicle costs, etc.) and respecting certain constraints (number
of vehicles, goods to pickup and deliver, xed depots, capacities, clients to serve, time windows,
etc.).
To solve these problems, the or-tools offers a dedicated Constraint Programming sub-library:
the Routing Library (RL).
The next three chapters each deal with one of three broad categories of Routing Problems:
Chapter 9 deals with Node Routing Problems where nodes must to be visited and served.
Chapter 10 deals with Vehicle Routing Problems where vehicles serve clients along the
routes.
Chapter 11 deals with Arc Routing Problems where arcs/edges must be visited and served.
These three categories of problems share common properties but they all have their own
paradigms and scientic communities.
In this chapter, well discover the RL with what is probably the most studied problem in Operations Research: the Travelling Salesman Problem (TSP)3 .
We use the excellent C++ ePiX library4 to visualize TSP solutions in TSPLIB format and
TSPTW solutions in Lpez-Ibez-Blum and da Silva-Urrutia formats.
1
A graph = (, ) is a set of vertices (the set ) connected by edges (the set ). A directed edge is called
an arc. When we have capacities on the edges, we talk about a network.
2
The transportation metaphor is helpful to visualize the problems but the class of Routing Problems is much
broader. The Transportation Problem for instance is really an Assignment Problem. Networks can be of any type:
telephone networks (circuit switching), electronic data networks (such as the internet), VLSI (the design of chips),
etc.
3
We use the Canadian (and British, and South African, and...) spelling of the verb travelling but youll nd
much more scientic articles under the American spelling: traveling.
4
A
The ePiX library uses the TEX/LTEX engine to create beautiful graphics.

Overview:

We start this chapter by presenting in broad terms the different categories of Routing Problems and describe the Routing Library (RL) in a nutshell. Next, we introduce the Travelling
Salesman Problem (TSP) and the TSPLIB instances. To better understand the RL, we say a
few words about its inner working and the CP model we use. Because most of the Routing
Problems are intractable, we use Local Search. We explain our two phases approach in details
and show how to model the TSP in a few lines. Finally, we model and solve the TSP with Time
Windows.
Prerequisites:

Basic knowledge of C++.


Basic knowledge of Constraint Programming (see chapter 1).
Basic knowledge of the Constraint Programming Solver (see chapter 2).
Basic knowledge of Local Search (see chapter 6).
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap9.


The les inside this directory are:
tsp.h: This le contains the TSPData class that records the data for the TSP. This le
is used throughout the TSP examples.
tsplib.h:
Declarations
of
TSPLIBDistanceFunctions class.

TSPLIB

keywords

and

the

tsp_epix.h: This le provides the helper functions to visualize TSPLIB solutions


with the ePiX library.
tsplib_solution_to_epix.cc: A simple program to visualize solutions in
TSPLIB format with the ePiX library.
tsp_minimal.cc: A minimalist implementation of the TSP with the RL.
tsp.cc: A basic implementation of the TSP with the RL.
tsp_forbidden_arcs.cc: The TSP with forbidden arcs between some nodes.
tsptw.h: This le contains the TSPTWData class that records the data for the Travelling Salesman Problem with Time Windows. This le is used throughout the TSPTW
examples.
tsptw_epix.h: This le provides the helper functions to visualize TSPTW solutions
with the ePiX library.
tsptw.cc: A basic implementation of the TSPTW with the RL.

218

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

tsptw_ls.cc: A specialized implementation of the TSPTW with the RL.

9.1 A whole zoo of Routing Problems


This section is meant to make you aware that the classication of Routing Problems is intricate5 .
Actually, there is no real and widely adopted classication67 .
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 denition, 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 classication of Routing Problems when you
search for a specic 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

The 1/ /
Scheduling Problem
...
5

You can stop reading now if you want: this section involves neither Constraint Programming nor the or-tools
library.
6
From time to time, an article is published to propose a good classication but none has been adopted by the
community so far. See [Eksioglu2009] for instance.
7
Some people may actually disagree with the terms used in this manual.
8
Although Scheduling Problems and Routing Problems are not solved with the same techniques. See
[Prosser2003] for instance.

219

9.1. A whole zoo of Routing Problems

So what is a Routing Problem anyway?


Broadly speaking, a Routing Problem is a mathematical problem where you need to nd
routes in a graph (or more generally a network) respecting some visiting constraints. A
route is a path connecting a starting vertex and an ending vertex (both can coincide). Visiting constraints forbid or force to visit some or all nodes, edges and arcs. Often additional
constraints are required to model real problems.
Notice that what is known as the General Routing Problem in the scientic literature is a
combination of NRP and ARP: You have a graph or a network and you must nd one tour
covering/serving some required arcs/edges/nodes for a minimum cost, i.e. you only have
1 vehicle.
We now present the three broad categories of Routing Problems. All are Optimization Problems
where we try not only to nd a solution but a good solution or even a best solution. Most
problems minimize an objective function along the routes dened 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 scientic community but ARP specialists tend to believe that ARPs are intrinsically more
difcult 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.).

9.1.1 Node Routing Problems


Informal denition:
The term Node Routing Problem (NRP) is seldom used9 and mainly refers to Travelling Salesman 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
The Travelling Salesman Problem
9

ture!

220

Node Routing Problems might even describe problems unrelated to Routing Problems in the scientic litera-

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

The General Travelling Salesman Problem


The Cumulative Travelling Salesman Problem
The Sequential Ordering Problem
The Hamiltonian Cycle Problem
The Longest Path Problem
The Steiner Tree Problem
...
Authoritative source:
D. L. Applegate, R. E. Bixby, V. Chvatal, and W. J. Cook. The Traveling Salesman Problem:
A Computational Study, Princeton Series in Applied Mathematics, Princeton University Press,
606 pp., 2007.
The TSPTW:
The Travelling Salesman Problem with Time Windows is...
[insert epix graphic]

9.1.2 Vehicle Routing Problems


Informal denition:
Vehicle Routing Problems (VRPs) are concerned with a eet of (maybe heterogeneous) vehicles. The number of vehicles can be xed 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 respect 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 nd several routes
(1 per vehicle) that visit all clients and respect all given constraints.
Some problems
The Vehicle Routing Problem
The Capacitated Vehicle Routing Problem
The Pickup and Delivery Problem
The Vehicle Routing Problem with Time Windows
...

221

9.1. A whole zoo of Routing Problems

Authoritative source:
Golden, Bruce L.; Raghavan, S.; Wasil, Edward A. (Eds.). The Vehicle Routing Problem: Latest Advances and New Challenges. Springer, Series: Operations Research/Computer Science
Interfaces Series, Vol. 43, 2008, 589 p.
The CVRP:
The Capacitated Vehicle Routing Problem is...
[insert epix graphic]

9.1.3 Arc Routing Problems


Informal denition:
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
The Chinese Postman Problem
The Canadian Postman Problem
The Windy Postman Problem
The Hierarchical Postman Problem
The Rural Postman Problem
The Cumulative Chinese Postman Problem
The Route Inspection Problem
The Capacitated Arc Routing Problem
...
Authoritative source:
Dror, M. (Ed.). Arc Routing: Theory, Solutions and Applications. Kluwer Academic Publishers, Dordrecht, 2000.
The CCPP:
The Cumulative Chinese Postman Problem is ...
[insert epix graphic]
222

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

9.2 The Routing Library (RL) in a nutshell


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
The objectives of the RL are to
model and solve generic routing problems out of the box;
provide modelling and solving blocks that can easily be reused;
make simple models simple to model;
allow extensibility.
In short, we provide specialized primitives that you can assemble and customize to your needs.

9.2.2 Out of the box models


To be precise, the RL only uses one model to solve different Routing Problems. Its a one
ts all. This approach has its advantages and disadvantages. On one side, the model already
exists, has been tested and ne-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
difcult 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 Salesman Problem (and its variants, ATSP, TSPTW, ...) to multi-vehicles problems with dimension
constraints (capacities, time windows, ...) and various routing constraints (optional nodes, alternate 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.

9.2.3 On top of the CP library


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.

223

9.2. The Routing Library (RL) in a nutshell

9.2.4 Local Search


We are mainly using CP-based Local Search and Large Neighborhood Search using routingspecic 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).

9.2.5 Tuning the search


To tune and parametrize the search, use command-line gags. 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

To get the whole list of gags dened in the RL:


./my_beautiful_routing_algorithm --helpon=routing

The RL provides the handy SetCommandLineOption() method:


routing.SetCommandLineOption("routing_first_solution",
"PathCheapestArc");

This is equivalent to calling the program with the gag routing_first_solution set to
PathCheapestArc:
./my_beautiful_routing_algorithm
--routing_first_solution=PathCheapestArc

9.2.6 Dimensions
Often, real problems need to take into account some accumulated quantities along (the edges
and/or the nodes of) the routes. To model such quantities, the RL proposes the concept of
dimensions. A dimension is basically a set of variables that describe some quantities (given
by callbacks) accumulated along the routes. These variables are associated with each node of
the graph. You can add as many dimensions as you wish in an automated and easy fashion:
just call the appropriate AddDimension() method(s) and the RL creates and manages these
variables automatically.
You can add upper bounds (we develop this concept later) on a dimension and a capacity limits
per route/vehicle on accumulated quantities for a given dimension.
Examples of dimensions are weight or volume carried, distance and time.

9.2.7 Disjunctions
Nodes dont have to be visited, i.e. some nodes can be optional. For this, the RL uses the
struct Disjunction which is basically a set of nodes. In our model, we visit at most one
224

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

node in each Disjunction. If these sets are singletons, then you have optional nodes. You
can also force to visit at least one node in each or some of the Disjunctions.
Again, we have automated and simplied (and optimized!) the process to create these sets: just
call the appropriate AddDisjunction() method(s).

9.2.8 Routes/Vehicles are not mandatory


The same way that nodes dont have to be visited, vehicles/routes dont 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.

9.2.9 Heterogeneous eet of vehicles


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.
Only one model
We wrote several times that there is no universal solver11 for all the problems. This is of course
also true for the RL. We use a node-based model to solve quite a lot of different problems
but not all Routing Problems can be solved with the RL. In particular, common Arc Routing
Problems are probably best solved with a different model12 .
10

Or can you call them features of the RL?


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.
11

225

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, ...);

By the ANSI/ISO standard, we are guaranteed to be able to declare at least a maximum of


32767 nodes. Since the problems we try to solve are intractable, 32767 nodes are most of
the time enough14 .
Constraint Programming techniques - at the time of writing - are not competitive with state
of the art techniques (mostly Branch, Price and Cut with specialized heuristics to solve Linear Mixed Integer Programs) that can solve TSP with thousands of nodes to optimality. The
strength of Constraint Programming lies in its ability to handle side constraints well such as
time windows for instance.
You cannot visit a node twice
The way the model is coded (see section ??) doesnt 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.
The RL returns approximate solutions
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 wont have any
guarantee on the quality of the returned solution(s). You can force the RL to return proven
optimal solutions but the RL wasnt coded with exact solutions and procedures in mind.

9.3 The Travelling Salesman Problem (TSP)


You
can
nd
the
code
in
the
le
tsp.h,
tsp_epix.h
and tsplib_solution_to_epix.cc and the data in the les a280.tsp
and a280.opt.tour.
The Travelling Salesman Problem (TSP) is probably the most known and studied problem in Operations Research. In this section, we briey15 present this fascinating problem
13

And thus the number of vehicles too!


If your platform restricts you too much, you can always adapt the code!
15
Google TSP, Traveling Saleman Problem or Travelling Salesman Problem to nd lots of examples, explanations, applications, etc.
14

226

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

and the TSPLIB which stands for the TSP library and is a library of sample instances for
the TSP (and related problems) from various origins and of various types. To read TSPLIB
data, we have implemented our own TSPData class as none of the available source code are
compatible with our licence. Feel free to use it! Finally, we like to visualize what we are
doing. To do so, we use the excellent ePiX library through our TSPEpixData class.

9.3.1 The Problem


Given a graph = (, ) and pairwise distances between nodes, the TSP consists in nding
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 nd 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.
Do I really need a complete graph?
This question might come as a surprise to CP practitioners. Indeed, in CP you can use
any graph as input. Outside the CP paradigm, most algorithms solving the TSP ask for a
complete graph as input. The classical way to transform any (non complete) graph into a
complete graph is to replace each non existing edge (, ) by a well suited shortest path
edge between and . Worse, if you want to avoid certain arcs between nodes in a complete
graph, the classical way to achieve this is to set a very high cost/weight to the arcs to avoid.
In the RL, if you want to avoid arc (, ), you just remove from the domain of the variable
NextVar() of . See subection 9.6.3 for a detailed example.

16

The record at the time of writing is the pla85900 instance in Gerd Reinelts TSPLIB. This instance is a
VLSI application with 85 900 nodes. For many other instances with millions of nodes, solutions can be found that
are guaranteed to be within 1% of an optimal tour!
17
At least for now and if you try to solve them to optimality.

227

9.3. The Travelling Salesman Problem (TSP)

Symmetric or Asymmetric distances?


When we talk about a Travelling Salesman Problem, it is implicit that the distance between
two nodes and must be the same as the distance between and . This is not mandatory.
A distance in one direction could be larger than the distance in the other direction. For
instance, climbing a hill might cost more than descending it. When the distances are not
symmetric, i.e. d(, ) = d(, ), we talk about an Asymmetric 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 its open source!). You also might be interested in the 8th DIMACS Implementation
Challenge held in 2001 about the TSP.

9.3.2 Benchmark data


Several known benchmark data sources are available on the internet. One of the most known is
the TSPLIB page. Its a little bit outdated but it contains a lot of instances and their proven optimal solutions. Their TSPLIB format is the de facto standard format to encode TSP instances.
The TSPLIB format
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. Well 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 instance le

The TSPLIB not only deals with the TSP but also with related problems. We only detail one
type of TSP instance les. This is what the le a280.tsp18 looks like:
NAME : a280
COMMENT : drilling problem (Ludwig)
TYPE : TSP
DIMENSION: 280
EDGE_WEIGHT_TYPE : EUC_2D
NODE_COORD_SECTION
1 288 149
2 288 129
18

The le a280.tsp actually contains twice the same node (node 171 and 172 have the same coordinates)
but the name and the dimension have been kept. This is the only known defect in the TSPLIB.

228

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

3 270
4 256
5 256
6 246
...
EOF

133
141
157
157

Some of the attributes dont need any explanation. The TYPE keyword species the type of
data. We are only interested in:
TSP: Data for the symmetric TSP;
ATSP: Data for the asymmetric TSP and
TOUR: A collection of tours (see next subsection below).
DIMENSION is the number of nodes for the ATSP or TSP instances. EDGE_WEIGHT_TYPE
species how the edge weight are dened. In this case (EUC_2D), it is the Euclidean distance in
the plane. Several types of distances are considered. The NODE_COORD_SECTION keyword
starts the node coordinates section. Each line is made of three numbers:
Node_id x y
Node_id is a unique integer ( 1) node identier and (x,y) are Cartesian coordinates unless
otherwise stated. The coordinates dont have to be integers and can be any real numbers.
Not all instances have node coordinates.
There exist several other less obvious TSPLIB formats but we disregard them in this manual
(graphs can be given by different types of explicit matrices or by edge lists for example). Note
however that we take them into account in the code.
You might wonder how the depot is given. It is nowhere written where to start a tour. This is
normal because the TSP is not sensitive to the starting node: you can start a tour anywhere, the
total cost of the tour remains the same.
The solution le

Solution les are easier to deal with as they only contain tours. Every tour, called a sub-tour,
is a list of integers corresponding to the Node ids ended by -1.
This is what the le a280.opt.tour containing an optimal tour looks like:
NAME : ./TSPLIB/a280.tsp.opt.tour
TYPE : TOUR
DIMENSION : 280
TOUR_SECTION
1
2
242
243
...
279
3

229

9.3. The Travelling Salesman Problem (TSP)

280
-1

Since this le contains an optimal tour, there are no sub-tours and the list of integers contains
only one -1 at the end of the le.

9.3.3 The TSPData class


The TSPData class basically encapsulates a 2-dimensional matrix containing the distances
between all nodes. For efciency reasons, we use a 1-dimensional matrix with a smart pointer
dened in the header base/scoped_ptr.h:
private:
scoped_array<int64> matrix_;

To mimic the behaviour of a 2-dimensional matrix, we use:


int64 MatrixIndex(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const {
return (from * size_ + to).value();
}

Notice how we cast the RoutingModel::NodeIndex into an int64 by calling its


value() method.
The 1-dimensional matrix is made of the columns of the virtual 2-dimensional matrix placed
one after the other.
What is a smart pointer?
A smart pointer is a class that behaves like a pointer. Its main advantage is that it destroys
the object it points to when the smart pointer class is itself destroyeda . This behaviour
ensures that, no matter what happens (exceptions, wrong ownership of pointees, bad programming (yep!), etc.), the pointed object will be destroyed as soon as the pointer object
is out of scope and destroyed.
a

Several scenarii are possible. With reference counting, when more than one pointer refer to an object,
it is only when the last pointer referring to the object is destroyed that the the object itself is destroyed. If
you want to know more about this helpful technique, look up RAII (Resource Acquisition Is Initialization).

To read TSPLIB les


To read TSPLIB les, the TSPData class offers the
LoadTSPLIBFile(const std::string& filename);

method. It parses a le in TSPLIB format and loads the coordinates (if any) for further treatment. Note that the format is only partially checked: bad inputs might cause undened behaviour.

230

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

If during the parse phase an unknown keyword is encountered, the method exists and prints a
FATAL LOG message:
Unknown keyword: UNKNOWN

This method has been tested with almost all the les of the TSPLIB and should hopefully read
any correct TSPLIB format for the TSP.
To generate random TSP
To generate random TSP instances, the TSPData class provides the
RandomInitialize(const int size);

method. Several gflags parameters are available:


deterministic_random_seed: Use deterministic random seeds or not? true by
default;
use_symmetric_distances: Generate a symmetric TSP instance or not? true
by default;
min_distance: Minimum allowed distance between two nodes. 10 by default;
max_distance: Maximum allowed distance between two nodes. 100 by default.

9.3.4 Visualization with ePix


To visualize the solutions, we use the excellent ePiX library. The le 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);

To write a ePiX solution le, use the following methods:


void WriteSolutionFile(const
const
void WriteSolutionFile(const
const

Assignment * solution,
std::string & epix_filename);
std::string & tpslib_solution_filename,
std::string & epix_filename);

The rst method takes an Assignment while the second method reads the solution from a
TSPLIB solution le.
You can dene the width and height of the generated image:
DEFINE_int32(epix_width, 10, "Width of the pictures in cm.");
DEFINE_int32(epix_height, 10, "Height of the pictures in cm.");

Once the ePiX le is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp

231

9.4. The model behind the scenes: the main decision variables

Here is an example of a solution for the le a280.tsp:

You can also print the node labels with the ag:
DEFINE_bool(tsp_epix_labels, false, "Print labels or not?");

For your (and our!) convenience, we wrote the small program tsplib_solution_to_epix. Its
implementation is in the le tsplib_solution_to_epix.cc. To use it, invoke:
./tsplib_solution_to_epix TSPLIB_data_file TSPLIB_solution_file >
epix_file.xp

9.4 The model behind the scenes: the main decision


variables
We present the main decision variables of the model used in the RL. In section 14.11, we
describe the inner mechanisms of the RL in details. A Routing Problem is dened on a graph
(or a network). The nodes of the graph have unique NodeIndex identiers. Internally, we use
an auxiliary graph to model the Routing Problem. In RL jargon, the identiers 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).

232

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

9.4.1 The main idea: the node decision variables


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 nishes, it is even easier since we dont 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 identiers 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. Lets 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 difference between NodeIndex node identiers and int64 indices representing nodes in solutions.

9.4.2 The auxiliary graph21


To understand how the auxiliary graph is constructed, we need to consider a more general
Routing Problem than just a TSP with one vehicle. Well use a VRP with four vehicles/routes.
Lets take the original graph of the next gure:
19

Not every node, only the nodes that lead somewhere in the solution. Keep reading.
Remember that we dont allow a node to be visited more than once, i.e. only one vehicle can visit a node in
a solution.
21
This sub-section is a simplied version of the section The auxiliary graph from the chapter Under the hood.
20

233

9.4. The model behind the scenes: the main decision variables
1

1
0

5
3

1
0
1
0

Starting depot

1
0Ending depot
1
0

1
0

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 . In the RL, you must number your
original nodes from 0 to 1. If you dont follow this advice, you might get some surprises!
Always use NodeIndexes from 0 to 1 for your original graph!
There are nine nodes of which two are starting depots (1 and 3), one is an ending depot (7) and
one is a starting and ending depot (4). The NodeIndexes22 range from 0 to 8.
In this example, we take four vehicles/routes:
route 0: starts at 1 and ends at 4
route 1: starts at 3 and ends at 4
route 2: starts at 3 and ends at 7
route 3: starts at 4 and ends at 7
The auxiliary graph is obtained by keeping the transit nodes and adding a starting and ending
depot for each vehicle/route if needed like in the following gure:
1

5
6

1
04
1
0
1
0
1
0
1
0

1
70
1
0
1
0
1
0
1
0

Starting depot

1
0Ending depot
1
0

1
0

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 nally there are two added
copies of node 4 because two routes (routes 0 and 1) end at 4 and one route (route 3) starts from
4.
22

We should rather say NodeIndices but we pluralize the type name NodeIndex.
Note
also that the NodeIndex type lies inside the RoutingModel class, so we should rather use
RoutingModel::NodeIndex.

234

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

The way these nodes are numbered doesnt matter for the moment. For our example, the next
gure shows this numbering:
1

5
3

6
8

9
1
0
1
0 4
1
0
10 0
1
1
0
1
11 0
1
0
1
012
1
0
1
0

Starting depot

1
0Ending depot
1
0

1
0

Starting and ending depot


Transit node

Note that the int64 indices dont depend on a given solution but only on the given
graph/network and the depots.
What is an auxiliary graph?
An auxiliary graph is a graph constructed from the original graph. It helps to model a problem. In our case, the auxiliary graph allows us to model different routes. Well meet other
auxiliary graphs in the chapter Arc Routing Problems with constraints: the Cumulative
Chinese Postman Problem.

9.4.3 How to switch from NodeIndex to int64 and vice-versa?


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 shouldnt 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, youll understand that
RoutingModel::NodeIndex node = 12;

fails to compile. This is exactly the purpose of the IntType class23 .


If you need to translate an int64 index in a solution to the corresponding NodeIndex node
or vice-versa, use the following methods of the RoutingModel class:
NodeIndex IndexToNode(int64 index) const;
int64 NodeToIndex(NodeIndex node) const;
23

Have a look at base/int-type.h if you want to know more about the IntType class.

235

9.4. The model behind the scenes: the main decision variables

They are quicker and safer than a static_cast and ... give the correct results!
Try to avoid RoutingModel::NodeIndex::value() unless really necessary.

NodeIndexes and int64s dont necessarily coincide!


How can you nd the int64 index of a depot?
You shouldnt 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;

where vehicle is the number of the vehicle or route considered.


Never use NodeToIndex() on starting or ending nodes of a route.

9.4.4 How to follow a route?


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.

236

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

9.4.5 Not all int64 indices have a corresponding IntVar nexts_


variable
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
nodes in the next gure. 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 0
Var 2

0
Var 3

5
Var 6

Var 4
Path p0
4
Var 5
6

7
2
Path p1 3
8
NodeIndex : 0 . . . 8
Var (IntVar): 0 . . . 6
int64 : 0 . . . 8

Because nodes 5 and 8 are ending nodes, there is no nexts_ IntVar attached to them.
The solution depicted is:
Path 0 : 1 -> 0 -> 2 -> 3 -> 5
Path 1 : 7 -> 4 -> 6 -> 8
If we look at the internal int64 indices, we have:
Path 0 : 1 -> 0 -> 2 -> 3 -> 7
Path 1 : 6 -> 4 -> 5 -> 8
There are actually 9 int64 indices ranging from 0 to 8 because in this case there is no need to
duplicate a node. As you can see in the picture, there are only 7 nexts_ IntVar variables.
The following code:
LG << "Crash: " << Solution->Value(routing.NextVar(routing.End(0)));

compiles ne but triggers the feared


Segmentation fault

As you can see, there is no internal control on the int64 index you can give to methods. If
you want to know more about the way we internally number the indices, have a look at subsection 14.11.2. Notice also that the internal int64 index of the node with NodeIndex 6
is... 5 and the int64 index of the node with NodeIndex 7 is...6!

9.4.6 To summarize
Here is a little summary:
237

9.5. The model behind the scenes: overview

Types to represent nodes

What
True node Ids
Indices to follow
routes

Types
NodeIndex
int64

Comments
Unique for each original node from 0 to 1.
Not unique for each original node. Could be
bigger than 1 for the starting or ending node
of a route.

Internally, the RL uses int64 indices and duplicates some nodes if needed (the depots). The
main decision variables are IntVar only attached to internal nodes that lead somewhere. Each
variable has the whole range of int64 indices as domain24 .
To follow a route, use int64 indices. If you need to deal with the corresponding nodes, use
the NodeIndex IndexToNode(int64) method. The int64 index corresponding to the
rst node of route k is given by:
int64 first_node = routing.Start(k);

and the last node by:


int64 last_node = routing.End(k);

You can also test if an int64 index is the beginning or the ending of a route with the methods
bool IsStart(int64) and bool IsEnd(int64).
In a solution, to get the next int64 index next_node of a node given by an int64 index
current_node, use:
int64 next_node = solution->Value(routing.NextVar(current_node));

9.5 The model behind the scenes: overview


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 14.11, we
describe the inner mechanisms of the RL in details.
If you havent 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.

9.5.1 The RoutingModel class


All ingredients are dened 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

238

The CP solver does an initial propagation to quickly skim these domains.

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);

if there are several start/end depots:


// create multi depots
std::vector<std::pair<RoutingModel::NodeIndex,
RoutingModel::NodeIndex> > depots(2);
depots[0] = std::make_pair(1,5);
depots[1] = std::make_pair(7,1);
RoutingModel VRP(9, 2, depots);

Note that the space between the two ending > in:
std::vector<std::pair<RoutingModel::NodeIndex,
RoutingModel::NodeIndex> > depots(2);

is mandatory.

9.5.2 Variables
Basically, there are two type of variables:
Path variables: the main decision variables and additional variables to describe the different routes and
Dimension variables: these variables allow to add side constraints like time-windows,
capacities, etc. and denote some quantities (the dimensions) along the routes.
From now on in this section, we only use the internal int64 indices except if the indices are
explicitly of type NodeIndex. This is worth a warning:
For the rest of this section, we only use the internal int64 indices except if the
indices are explicitly of type RoutingModel::NodeIndex.

239

9.5. The model behind the scenes: 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.
Main decision variables

You can access the main variables with the method NextVar(int64):
IntVar* var = routing.NextVar(42);

var is a pointer to the IntVar corresponding to the node with the int64 42 index. In a
solution solution, the value of this variable gives the int64 index of the next node visited
after this node:
Assignment * const solution = routing.Solve();
...
int64 next_node = solution.Value(var);

Vehicles

Different routes/vehicles service different nodes. For each node i, VehicleVar(i) represents the IntVar* that represents the int index of the route/vehicle servicing node i in the
solution:
int route_number = solution->Value(routing.VehicleVar(i));

Taking a shortcut in the notation, we have that:


if NextVar(i) == j then VehicleVar(j) == VehicleVar(i).
That is, both nodes i and j are serviced by the same vehicle.
To grab the rst and last node (starting and ending depot) of a route/vehicle route_number,
you can use the Start() and End() methods that we discussed previously:
int64 starting_depot = routing.Start(route_number);
int64 ending_depot = routing.End(route_number);

240

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

Disjunctions and optional nodes

A node doesnt have to be visited. Nodes can be optional or part of a Disjunction, i.e. part
of a subset of nodes out of which at most one node can be visited in a solution.
ActiveVar(i) returns a boolean IntVar* (a IntVar variable with a {0, 1} domain)
indicating if the node i is visited or not in the solution. The way to describe a node that is not
visited is to make its NextVar(i) points to itself. Thus, and again with an abuse of notation,
we have:
ActiveVar(i) == (NextVar(i) != i).
Well discuss Disjunctions and optional nodes in details in section 11.4 when we will
transform a Cumulative Chinese Postman Problem (CCPP) into a Generalized TSP (GTSP). A
GTSP is similar to a TSP except that you have clusters of nodes you want to visit, i.e. you only
want to visit 1 node in each cluster.
Dimension variables
Dimension variables are used to accumulate quantities (or dimensions) along the routes. To denote a dimension, we use an std::string d. There are three types of dimension variables:
CumulVar(i, d): variables representing the quantity of dimension d when arriving
at the node i.
TransitVar(i, d): variables representing the quantity of dimension d added after
visiting the node i.
SlackVar(i, d): non negative slack variables such that (with the same abuse of
notation as above):
if NextVar(i) == j then CumulVar(j) = CumulVar(i) +
TransitVar(i) + SlackVar(i).
For a time dimension, you can think of waiting times.
You can add as many dimensions as you want25 .
The transit values can be constant, dened with callbacks, vectors or matrices. You can represent any quantities along routes with dimensions but not only. For instance, capacities and
time windows can be modelled with dimensions. Well play with dimensions at the end of
this chapter when well try to solve The Travelling Salesman Problem with Time Windows in
or-tools.

9.5.3 Constraints
In addition to the basics constraints that we discussed in the previous sub-section, the RL uses
constraints to avoid cycles, constraints to model the Disjunctions and pick-up and delivery
constraints.
25

Well, as many as your memory allows...

241

9.5. The model behind the scenes: overview

No cycle constraint
One of the most difcult constraint to model is a constraint to avoid cycles in the solutions.
For one tour, we dont want to revisit some nodes. Often, we get partial solutions like the one
depicted on gure (a):

(a)

(b)

It is often easy to obtain optimal solutions when we allow cycles (like in gure (a)) but difcult
to obtain a real solution (like in gure (b)), i.e. without cycles. Several constraints have been
proposed in the scientic literature, each with its cons and pros. Sometimes, we can avoid
this constraint by modelling the problem in such a way that only solutions without cycles can
be produced but then we have to deal with huge and often numerically (and theoretically26 )
unstable models.
In the RL, we use our dedicated NoCycle constraint (dened in
constraint_solver/constraints.cc) in combination with an AllDifferent
constraint on the NextVar() variables. The NoCycle constraint is implicitly added to the
model.
The NoCycle constructor has the following signature:
NoCycle(Solver* const s,
const IntVar* const* nexts,
int size,
const IntVar* const* active,
ResultCallback1<bool, int64>* sink_handler,
bool owner,
bool assume_paths);

We will not spend too much time on the different arguments. The nexts and active arrays
are what their names imply. The sink_handler is just a callback that indicates if a node is
a sink or not. Sinks represent the depots, i.e. the nodes where paths start and end.
The bool owner allows the solver to take ownership of the callback or not and the bool
assume_paths indicates if we deal with real paths or with a forest (paths dont necessarily
end) in the auxiliary graph.
The constraint essentially performs two actions:
26

242

For the specialists: for instance, primal and dual degenerate linear models.

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

forbid partial paths from looping back to themselves and


ensure each variable/node can be connected to a sink.
We refer the reader to subsection 14.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:

ActiveVar() = 1.
Disjunction

You might want to use optional Disjunctions, i.e. a group of nodes out of which at most
one node can be visited. This time, use:
void AddDisjunction(const std::vector<NodeIndex>& nodes,
int64 penalty);

This constraint is equivalent to:

ActiveVar() = 1

Disjunction

where p is a boolean variable corresponding to the Disjunction and the objective function
has an added (p * penalty) term. If none of the variables in the Disjunction is vis
ited ( Disjunction ActiveVar() = 0), p must be equal to one and the penalty is added to the
objective function.
To be optional, the penalty penalty attributed to the Disjunction must be non-negative
(
0), otherwise the RL uses a simple Disjunction, i.e. exactly one node in the
Disjunction will be visited in the solutions.
Pick-up and delivery constraints
These constraints ensure that two nodes belong to the same route. For instance, if nodes i and
j must be visited/delivered by the same vehicle, use:
void AddPickupAndDelivery(NodeIndex i, NodeIndex j);

Whenever you have an equality constraint linking the vehicle variables of two nodes, i.e. you
want to force the two nodes to be visited by the same vehicle, you should add (because it speeds
up the search process!) the PickupAndDelivery constraint:

243

9.5. The model behind the scenes: overview

Solver* const solver = routing.solver();


solver->AddConstraint(solver->MakeEquality(
routing.VehicleVar(routing.NodeToIndex(i)),
routing.VehicleVar(routing.NodeToIndex(j))));
routing.AddPickupAndDelivery(i, j);

This constraint is counter-intuitive in a least two ways:


1. It is not modelled by a real constraint: this pair of nodes is used to lter out solutions.
PathOperators take them into account in the Local Search and
2. It doesnt specify an order on the ordered pair (i,j) of nodes: node j could be visited
before node i.
The implementation of the PickupAndDelivery constraint in the RL is a little
counter-intuitive.

The CloseModel() method


Because we dont completely dene 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().

9.5.4 The objective function


The objective function is dened 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 xed cost of each route/vehicle;
the penalty costs for not visiting optional Disjunctions.
We detail each of these costs.
27

Actually, only an AllDifferent constraint on the NextVars is added in the constructor of the
RoutingModel class. This constraint reinforces the fact that you cannot visit a node twice.

244

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

The costs of the arcs


To set the cost of each arc, use a NodeEvaluator2 callback to return the cost of each (i,j)
arc:
void SetCost(NodeEvaluator2* evaluator);

NodeEvaluator2 is simply28 a typedef for a ResultCallback2<int64,


NodeIndex, NodeIndex>, i.e. a class that denes an int64 Run(NodeIndex i,
NodeIndex j) or method. If you already have a class that denes a distance method
on pairs of NodeIndexes, you can transform this class into a NodeEvaluator2 with
NewPermanentCallback().
First, the class that computes the distances:
class ComputeDistances {
...
int64 Distance(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const {
return ...;
}
...
;

Then, the use of a NodeEvaluator2 callback with NewPermanentCallback():


RoutingModel routing(....);
ComputeDistances my_distances_class(...);
routing.SetCost(NewPermanentCallback(&my_distances_class,
&ComputeDistances::Distance));

You can also use a function:


int64 distance(RoutingModel::NodeIndex i,
RoutingModel::NodeIndex j) {
return ...;
}

and use again NewPermanentCallback():


routing.SetCost(NewPermanentCallback(&distance));

NewPermanentCallback() is a (set of) function(s) that returns the appropriate


callback class made from its arguments.
Some template magic might be involved
too. ResultCallback2 and NewPermanentCallback() are dened in the header
base/callback.h. If you are curious about the callback mechanism and the use of
NewPermanentCallback(), read sub-section 14.3.3.
28

What follows is clearly C++ jargon. Basically, lets say that you need a method or a function
that returns the distances of the arcs. To pass it as argument to the SetCost() method, wrap it in a
NewPermanentCallback() call.

245

9.5. The model behind the scenes: overview

A xed cost for each of the existing routes


Routes/Vehicles dont all have to be used. It might cost less not to use a route/vehicle. To add
a xed 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.
A penalty cost for missed Disjunctions
We have already seen the penalty costs for optional Disjunctions above. The penalty cost
is only added to the objective function for a missed Disjunction: the solution doesnt
visit any node of the Disjunction. If the given penalty cost is negative for an optional
Disjunction, this Disjunction becomes mandatory and the penalty is set to zero. The
penalty cost can be zero for optional Disjunction and you can model optional nodes by
using singletons for each Disjunction.
Different types of vehicles
The cost for the arcs and the used routes/vehicles can be customized for each route/vehicle.
To customize the costs of the arcs, use:
void SetVehicleCost(int vehicle, NodeEvaluator2* evaluator);

where vehicle is the number of the route/vehicle.


To customize the xed costs of the routes/vehicles, use:
void SetVehicleFixedCost(int vehicle, int64 cost);

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();

This method does the following.


A bipartite graph is created with left nodes representing the nodes of the routing problem and
right nodes representing possible node successors. An arc between a left node l and a right
node r is created if r can be the node following l in a route (NextVar(l) = r). The cost of
the arc is the transit cost between l and r in the routing problem. Solving a Linear Assignment
Problem (minimum-cost perfect bipartite matching) returns a lower bound. Did you get it?
Lets draw a gure.

246

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
1

5
4
(a)

5
(b)

On the left (gure (a)), we have an original graph with two depots: a starting depot 1 and an
ending depot 5 and three transit nodes 2, 3 and 4. On the right (gure (b)), we have a bipartite
graph29 with the same number of left and right nodes. The cost on an arc (l,r) is the real
transit cost from l to r. The Linear Assignment Problem consists in nding a perfect matching
of minimum cost, i.e. a bijection along the arcs between the two sets of nodes of the bipartite
graph for a minimum cost. On gure (b), such an optimal solution is depicted in thick blue
dashed lines. As is the case here, this solution doesnt necessarily produce a (set of) closed
route(s) from a starting depot to an ending depot.
The routing model must be closed before calling this method.
Routing Problems with node disjunction constraints (including optional nodes) and
non-homogenous costs are not supported yet (the method returns 0 in these cases).
If your model is linear, you also can use the linear relaxation of your model. We will explore
these and other lower bounds in section 11.7 when well try to solve the Cumulative Chinese
Postman Problem.

9.5.5 Miscellaneous
We discuss here several improvements and conveniences of the RL.
Cache
[TO BE WRITTEN]
Light constraints
To speed up the search, it is sometimes better to only propagate on the bounds instead of the
whole domains for the basic constraints. These light constraints are checking constraints,
only triggered on WhenBound() events. They provide very little (or no) domain ltering.
Basically, these constraints ensure that the variables are respecting the equalities of the basic
constraints. They only perform bound reduction on the variables when these variables are
bound.
You can trigger the use of these light constraints with the following ag:
29

This bipartite graph is not really the one used by the CP solver but its close enough to get the idea.

247

9.6. The TSP in or-tools

DEFINE_bool(routing_use_light_propagation, false,
"Use constraints with light propagation in routing model.");

When false, the RL uses the regular constraints seen in the previous parts of this manual.
Try it, sometimes you can get a serious speed up. These light constraints are especially useful
in Local Search.
Locks
Often during the search, you nd what appears to be good sub-solutions, i.e. partial routes that
seem promising and that you want to keep xed for a while during the search. This can easily
be achieved by using locks.
A lock is simply an std::vector<int64> that represents a partial route. Using this lock
ensures that
NextVar(lock[i]) == lock[i+1]
is true in the current solution. We will use locks in section 11.6 when we will try to solve the
Cumulative Chinese Postman Problem.

9.6 The TSP in or-tools


You can nd the code in the les 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
les tsp_parameters.txt, a280.tsp and a280.opt.tour.
The RL is particularly well-suited to model a TSP. We start with a minimalistic implementation to show that a basic TSP can be coded in a few lines. Next, we develop a more
realistic approach to solve the TSP. Our instances can be randomly generated or read from
TSPLIB format les. Finally, we show how to avoid the use of a complete graph if the input
graph is not complete and compare the classical big M approach with a more appropriate
CP-based approach where the variables domains take the input graph into account.

9.6.1 Minimalistic implementation


You can nd the code in the le tutorials/cplusplus/chap9/tsp_minimal.cc.
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) {
...

248

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

return ...;
}
int main(int argc, char **argv) {
RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle
TSP.SetCost(NewPermanentCallback(MyCost));
const Assignment * solution = TSP.Solve();
// Solution inspection
if (solution != NULL) {
std::cout << "Cost: " << solution->ObjectiveValue() << std::endl;
for (int64 index = TSP.Start(0); !TSP.IsEnd(index);
index = solution->Value(TSP.NextVar(index))) {
std::cout << TSP.IndexToNode(index) << " ";
}
std::cout << std::endl;
} else {
std::cout << "No solution found" << std::endl;
}
return 0;
}

Given an appropriate cost function, a TSP can be modelled and solved in 3 lines:
RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle
TSP.SetCost(NewPermanentCallback(MyCost));
const Assignment * solution = TSP.Solve();

The cost function is given as a callback to the routing solver through its SetCost() method.
Other alternatives are possible and will be detailed in the next sections.

9.6.2 Basic implementation


You can nd the code in the le tutorials/cplusplus/chap9/tsp.cc.
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
We start by including the relevant headers:
#include <string>
#include <fstream>
#include "base/commandlineflags.h"
#include "constraint_solver/routing.h"
#include "base/join.h"

249

9.6. The TSP in or-tools

#include "tsp.h"
#include "tsp_epix.h"

base/join.h contains the StrCat() function that we use to concatenate strings. tsp.h
contains the denition and declaration of the TSPData class to read TSPLIB format instances
and write TSPLIB format solution les while tsp_epix.h contains the TSPEpixData
class to visualize TSP solutions. Under the hood, tsp.h includes the header tsplib.h that
gathers the keywords, distance functions and constants from the TSPLIB. You should consider
tsp.h and tsplib.h as one huge header le. tsp_epix.h is only needed if you want to
use the ePiX library to visualize TSP solutions. tsp_epix.h depends on tsp.h (and thus
tsplib.h).
Parameters
Several command line parameters are dened in the les tsp.h, tsplib.h, tsp_epix.h
and tsp.cc:
Files
tsp.h

Parameter
Description
deterministic_random_seed Use deterministic random
seeds or not?
use_symmetric_distances Generate a symmetric TSP
instance or not?
min_distance
Minimum allowed distance
between two nodes.
max_distance
Maximum allowed distance
between two nodes.
tsp_epix.h epix_width
Width of the pictures in cm.
epix_height
Height of the pictures in cm.
tsp.cc
tsp_size
Size of TSP instance. If 0,
must be read from a TSPLIB
le.
tsp_depot
The starting node of the tour.
tsp_data_file
Input le with TSPLIB data.
tsp_distance_matrix_file Output le with distance matrix.
tsp_width_size
Width size of elds in output
les.
tsp_solution_file
Output le with generated solution in TSPLIB format.
tsp_epix_file
ePiX solution le.
30
tsp_time_limit_in_ms
Time limit in ms, 0 means no
limit.
30

Default value
true
true
10
100
10
10
0

1
empty string
empty string
6
empty string
empty string
0

This ag is redundant with the routing_time_limit ag provided in routing.cc but we wanted to


underline the fact that this limit is given in milliseconds.

250

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

Command line parameters read from a le


When parameters start to pile up, writing them every time on the command line isnt very
practical. The gflags library provides the possibility to load the parameters from a text le.
For instance, a parameters le 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

You can read this le with the flagfile ag:


./tsp --flagfile=tsp_parameters.txt

which outputs the following le tsp_sol.txt on our system:


NAME : tsp_sol.txt
COMMENT : Automatically generated by TSPData (obj: 3948)
TYPE : TOUR
DIMENSION : 101
TOUR_SECTION
2
14
63
...
33
44
-1

The main function


Here is the main function:
int main(int argc, char **argv) {
std::string usage("...");
usage += argv[0];
usage += " -tsp_size=<size>\n\n";
usage += argv[0];
usage += " -tsp_data_file=<TSPLIB file>";
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 != "") {

251

9.6. The TSP in or-tools

tsp_data.LoadTSPLIBFile(FLAGS_tsp_data_file);
} else {
google::ShowUsageWithFlagsRestrict(argv[0], "tsp");
exit(-1);
}
operations_research::TSP(tsp_data);
return 0;
}

We start by writing the usage message that the user will see if she doesnt know what to
do. Next, we declare a TSPData object that will contain our TSP instance. As usual, all the
machinery is hidden in a function declared in the operations_research namespace:
TSP().
The TSP() function
We only detail the relevant parts of the TSP() function. First, we create the CP solver:
const int size = data.Size();
RoutingModel routing(size, 1);
routing.SetCost(NewPermanentCallback(&data, &TSPData::Distance));

The constructor of the RoutingModel class takes the number of nodes (size) and the number of vehicle (1) as parameters. The distance function is encoded in the TSPData object
given to the TSP() function.
Next, we dene some parameters:
// Disabling Large Neighborhood Search, comment out to activate it.
routing.SetCommandLineOption("routing_no_lns", "true");
if (FLAGS_tsp_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_tsp_time_limit_in_ms);
}

Because Large Neighborhood Search (LNS) can be quite slow, we deactivate it.
To dene the depot, we have to be careful as, internally, the CP solver starts counting the nodes
from 0 while in the TSPLIB format the counting starts from 1:
if (FLAGS_start_counting_at_1) {
CHECK_GT(FLAGS_tsp_depot, 0) << " Because we use the " <<
"TSPLIB convention, the depot id must be > 0";
}
RoutingModel::NodeIndex depot(FLAGS_start_counting_at_1 ?
FLAGS_tsp_depot -1 : FLAGS_tsp_depot);
routing.SetDepot(depot);

Notice that we also have to cast an int32 into a RoutingModel::NodeIndex.


Now that the instance and the parameters are accepted by the CP solver, we invoke its
Solve() method:
252

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

const Assignment* solution = routing.Solve();

Notice that the Solve() method returns a pointer to a const Assigment.


The inspection of the solution is done as usual:
if (solution != NULL) {
// test solution
if (!data.CheckSolution(routing, solution)) {
LOG(ERROR) << "Solution didnt pass the check test.";
} else {
LG << "Solution did pass the check test.";
}
// Solution cost.
LG << "Cost: " << solution->ObjectiveValue();
// Inspect solution.
string route;
const int route_nbr = 0;
for (int64 node = routing.Start(route_nbr);
!routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {
//LG << node;
route = StrCat(route, StrCat((FLAGS_start_counting_at_1 ?
routing.IndexToNode(node).value() + 1 :
routing.IndexToNode(node).value()), " -> "));
}
route = StrCat(route, (FLAGS_start_counting_at_1 ?
routing.IndexToNode(routing.End(route_nbr)).value() + 1 :
routing.IndexToNode(routing.End(route_nbr)).value()));
LG << route;
} else {
LG << "No solution found.";
}

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.

9.6.3 How to avoid some edges?


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 to these arcs. Arcs with such a large
distance will never be chosen31 . can be considered as innity.
In Constraint Programming, we can deal with forbidden arcs more elegantly: we simply remove
the forbidden values from the variable domains. Well use both techniques and compare them.
31

Actually, when permitted, an arc (, ) with a distance is often replaced by a shortest path and its
value is the length of the shortest path between and . One drawback is that you have to keep in memory the
shortest paths used (or recompute them) but it is often more efcient than using the large value.

253

9.6. The TSP in or-tools


First, we have to dene . We suppose that >>> max((, ) : , 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 ag M:
DEFINE_int64(M, kint64max, "Big m value to represent infinity");

We have also dened a ag to switch between the two techniques and a ag for the percentage
of arcs to forbid randomly in the le tsp_forbidden_arcs.cc:
DEFINE_bool(use_M, false, "Use big m or not?");
DEFINE_int32(percentage_forbidden_arcs, 20,
"Percentage of forbidden arcs");

The code in RandomForbidArcs() simply computes the number of arcs to forbid and
uniformly tries to forbid arcs one after the other:
void RandomForbidArcs(const int percentage_forbidden_arcs)
CHECK_GT(size_, 0) << "Instance non initialized yet!";

// Compute number of forbidden arcs


CHECK_GE(percentage_forbidden_arcs, 0)
<< "Percentage of forbidden arcs must be >= 0";
double percentage = percentage_forbidden_arcs;
if (percentage > FLAGS_percentage_forbidden_arcs_max) {
percentage = FLAGS_percentage_forbidden_arcs_max;
LG << "Percentage set to "
<< FLAGS_percentage_forbidden_arcs_max
<< " to avoid infinite loop with random numbers";
}
percentage /= 100;
// Dont count the principal diagonal
const int64 total_number_of_arcs = size_ * (size_ - 1) - size_;
const int64 number_of_forbidden_arcs =
(int64) total_number_of_arcs * percentage;
LG << "Forbid randomly " << number_of_forbidden_arcs
<< " arcs on " << total_number_of_arcs << " arcs.";
int64 number_forbidden_arcs_added = 0;
while (number_forbidden_arcs_added < number_of_forbidden_arcs) {
const int64 from = randomizer_.Uniform(size_ - 1);
const int64 to = randomizer_.Uniform(size_ - 1) + 1;
if (from == to) {continue;}
if (matrix_[MatrixIndex(from, to)] > FLAGS_M) {
matrix_[MatrixIndex(from, to)] = FLAGS_M;
VLOG(1) << "Arc (" << from << "," << to
32

Loosely speaking, the expression >>> max((, ) : , cities) means that is much much larger
that the largest distance between two cities.

254

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

<< ") has a larger value than M!";


++number_forbidden_arcs_added;
continue;
}
if (matrix_[MatrixIndex(from, to)] != FLAGS_M) {
matrix_[MatrixIndex(from, to)] = FLAGS_M;
++number_forbidden_arcs_added;
}
} // while(number_forbidden_arcs_added < number_of_forbidden_arcs)
}

Because our random number generator (as most random number generators) is not completely
random and uniform, we need to be sure to exit the while loop. This is why we introduce the
gag:
DEFINE_int32(percentage_forbidden_arcs_max, 94,
"Maximum percentage of arcs to forbid");

We bound the percentage of forbidden arcs by 94% by default.


[TO BE COMPLETED]

9.7 The two phases approach


You can nd the code in the le tsp_initial_solutions.cc.

255

9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

9.7.1 The initial solution


9.7.2 The PathOperator class
The TwoOpt PathOperator

9.7.3 Local Search PathOperators


TwoOpt
Relocate
OrOpt
Exchange
Cross
Inactive
SwapActive
ExtendedSwapActive
PathLNS
UnActiveLNS
How can I change the order of the LocalSearchOperators?

9.7.4 Filters
9.7.5 A Local Search heuristic for the TSP

9.8 The Travelling Salesman Problem with Time Windows (TSPTW)


You can nd the code in the le 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
les 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 -

256

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows
although it restricts the search tree33 - renders the problem even more difcult in practice!
Indeed, the beautiful symmetry of the TSP34 (any permutation of cities is a feasible solution)
is broken and even the search for feasible solutions is difcult [Savelsbergh1985].
We present the TSPTW and two instances formats: the Lpez-Ibez-Blum and the da SilvaUrrutia formats. As in the case of the TSP, we have implemented a class to read those instances:
the TSPTWData class. We also use the ePix library to visualize feasible solutions using the
TSPTWEpixData class.

9.8.1 The Travelling Salesman Problem with Time Windows


You might be surprised to learn that there is no common denition that is widely accepted
within the scientic community. The basic idea is to nd a tour that visits each node within
a time window but several variants exist.
We will use the denition given in Rodrigo Ferreira da Silva and Sebastin Urrutias 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 nding a minimum cost tour starting and ending at a given depot and visiting all customers. Each customer
has:
a service time : this is the time needed to service the customer;
a ready time (sometimes called release time): you cannot start to serve the customer
before her ready time and
a due time (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 dene a time window [ , ] within
which the client has to be served. You are allowed to visit the client before the ready time but
youll 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.
Lets illustrate a visit to a client . To do so, lets dene:
the arrival time : the time you arrive at the client and
the service start time : the time you start to service the client.
time spent at the client
service time i
ai
ti

time window [ai , bi ]

bi

si

time

In real application, the time spent at a client might be limited to the service. For instance, you
might wait in front of the clients ofce. Its common to consider that you start to service and
leave as soon as possible and this is our assumption in this chapter
33
34

All TSP solutions are not TSPTW solutions!


Notice how the depot is important for the TSPTW while it is not for the TSP.

257

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 , N, i.e. we impose that the bounds of the time
windows must be non negative integers. This also implies that the time windows and the
servicing times are nite.
The practical difculty of the TSPTW is such that only instances with about 100 nodes have
been solved to optimality36 and heuristics rarely challenge instances with more than 400 nodes.
The difculty of the problem not only depends on the number of nodes but also on the quality
of the time windows. Not many attempts can be found in the scientic literature about exact or
heuristic algorithms using CP to solve the TSPTW. Actually, not so many attempts have been
successful in solving this difcult problem in general. The scientic literature on this problem
is hence scarce.
We refer the interested reader to the two web pages cited in the next sub-section for some
relevant literature.

9.8.2 Benchmark data


There isnt a real standard. Basically, youll nd 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 Lpez-Ibez 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 Sebastin Urrutia also collected benchmark from different
sources in the literature. Their The TSPTW - Approaches & Additional Resources page contains about 100 instances.
Both pages provide best solutions and sum up the relevant literature.
The Lpez-Ibez-Blum format
We present the same instance proposed by Dumas et al. [Dumas1995] in both formats.
Here is the content of the le n20w20.001.txt (LIB_n20w20.001.txt in our directory
/tutorials/cplusplus/chap9/):
35

This condition doesnt hold in Rodrigo Ferreira da Silva and Sebastin Urrutias denition of a TSPTW. In
their article, they ask for (at least theoretically) , , R+ , i.e. non negative real numbers and
.
36
Instances with more than 100 nodes have been solved to optimality but no one - at least to the best of our
knowledge at the time of writing - can systematically solve to optimality instances with more than 40 nodes...

258

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

21
0 19 17 34 7 20 10 17 28 15 23 29 23 29 21 20 9 16 21 13 12
19 0 10 41 26 3 27 25 15 17 17 14 18 48 17 6 21 14 17 13 31
17 10 0 47 23 13 26 15 25 22 26 24 27 44 7 5 23 21 25 18 29
34 41 47 0 36 39 25 51 36 24 27 38 25 44 54 45 25 28 26 28 27
7 26 23 36 0 27 11 17 35 22 30 36 30 22 25 26 14 23 28 20 10
20 3 13 39 27 0 26 27 12 15 14 11 15 49 20 9 20 11 14 11 30
10 27 26 25 11 26 0 26 31 14 23 32 22 25 31 28 6 17 21 15 4
17 25 15 51 17 27 26 0 39 31 38 38 38 34 13 20 26 31 36 28 27
28 15 25 36 35 12 31 39 0 17 9 2 11 56 32 21 24 13 11 15 35
15 17 22 24 22 15 14 31 17 0 9 18 8 39 29 21 8 4 7 4 18
23 17 26 27 30 14 23 38 9 9 0 11 2 48 33 23 17 7 2 10 27
29 14 24 38 36 11 32 38 2 18 11 0 13 57 31 20 25 14 13 17 36
23 18 27 25 30 15 22 38 11 8 2 13 0 47 34 24 16 7 2 10 26
29 48 44 44 22 49 25 34 56 39 48 57 47 0 46 48 31 42 46 40 21
21 17 7 54 25 20 31 13 32 29 33 31 34 46 0 11 29 28 32 25 33
20 6 5 45 26 9 28 20 21 21 23 20 24 48 11 0 23 19 22 17 32
9 21 23 25 14 20 6 26 24 8 17 25 16 31 29 23 0 11 15 9 10
16 14 21 28 23 11 17 31 13 4 7 14 7 42 28 19 11 0 5 3 21
21 17 25 26 28 14 21 36 11 7 2 13 2 46 32 22 15 5 0 8 25
13 13 18 28 20 11 15 28 15 4 10 17 10 40 25 17 9 3 8 0 19
12 31 29 27 10 30 4 27 35 18 27 36 26 21 33 32 10 21 25 19 0
0
408
62
68
181
205
306
324
214
217
51
61
102
129
175
186
250
263
3
23
21
49
79
90
78
96
140
154
354
386
42
63
2
13
24
42
20
33
9
21
275
300

The rst line contains the number of nodes, including the depot. The n20w20.001 instance
has a depot and 20 nodes. The following 21 lines represent the distance matrix. This distance
typically represents the travel time between nodes and , plus the service time at node . The
distance matrix is not necessarily symmetrical. The last 21 lines represent the time windows
(earliest, latest) for each node, one per line. The rst node is the depot.
When then sum of service times is not 0, it is specied in a comment on the last line:

259

9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

# Sum of service times: 522

The da Silva-Urrutia format


We present exactly the same instance as above. Here is the le 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]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
999

16.00
22.00
12.00
47.00
11.00
25.00
22.00
0.00
37.00
31.00
38.00
36.00
38.00
4.00
5.00
16.00
25.00
31.00
36.00
28.00
20.00
0.00

23.00
4.00
6.00
38.00
29.00
5.00
31.00
16.00
3.00
19.00
12.00
1.00
14.00
50.00
4.00
3.00
25.00
15.00
14.00
16.00
35.00
0.00

0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00

0.00
62.00
181.00
306.00
214.00
51.00
102.00
175.00
250.00
3.00
21.00
79.00
78.00
140.00
354.00
42.00
2.00
24.00
20.00
9.00
275.00
0.00

408.00
68.00
205.00
324.00
217.00
61.00
129.00
186.00
263.00
23.00
49.00
90.00
96.00
154.00
386.00
63.00
13.00
42.00
33.00
21.00
300.00
0.00

0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00
0.00

Having seen the same instance, you dont need much complementary info to understand this
format. The rst line of data (CUST NO. 1) represents the depot and the last line marks the
end of the le. As you can see, the authors are not really optimistic about solving instances
with more than 999 nodes! We dont use the DEMAND column and we round down the numbers
of the last three columns.
You might think that the translation from this second format to the rst one is obvious. It is
not! See the remark on Travel-time Computation on the Jeffrey Ohlmann and Barrett Thomas
benchmark page. In the code, we dont try to match the data between the two formats, so you
might encounter different solutions.
The same instances in the da Silva-Urrutia and the Lpez-Ibez-Blum formats
might be slightly different.

Solutions
We use a simple format to record feasible solutions:
260

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

a rst line with a permutation of the nodes;


a second line with the objective value.
For our instance, here is an example of a feasible solution:
1 17 10 20 18 19 11 6 16 2 12 13 7 14 8 3 5 9 21 4 15
378

The objective value 378 is the sum of the costs of the arcs and not the time spent to travel
(which is 387 in this case).
A basic program check_tsptw_solutions.cc veries if a given solution is indeed feasible for a
given instance in Lpez-Ibez-Blum or da Silva-Urrutia formats:
./check_tsptw_solutions -tsptw_data_file=DSU_n20w20.001.txt
-tsptw_solution_file=n20w20.001.sol

This program checks if all the nodes have been serviced and if the solution is feasible:
bool IsFeasibleSolution() {
...
// for loop to test each node in the tour
for (...) {
// Test if we have to wait at client node
waiting_time = ReadyTime(node) - total_time;
if (waiting_time > 0) {
total_time = ReadyTime(node);
}
if (total_time + ServiceTime(node) > DueTime(node)) {
return false;
}
}
...
return true;
}

IsFeasibleSolution() returns true if the submitted solution is feasible and false


otherwise. To test this solution, we construct the tour node by node. Arriving at a node node
at time total_time in the for loop, we test two things:
First, if we have to wait.
We compute the waiting time waiting_time:
ReadyTime(node) returns the ready time of the node node and total_time is
the total time spent in the tour to reach the node node. If the ready time is greater
than total_time, waiting_time > 0 is true and we set total_time to
ReadyTime(node).
Second, if the due times are respected, i.e.:
is total_time + ServiceTime(node)

DueTime(node) true?

If not, the method returns false. If all the due times are respected, the method returns
true.
The output of the above command line is:

261

9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

TSPTW instance of type da Silva-Urrutia format


Solution is feasible!
Loaded obj value: 378, Computed obj value: 387
Total computed travel time: 391
TSPTW file DSU_n20w20.001.txt (n=21, min=2, max=59, sym? yes)
(!! n20w20.001 16.75 391 )

As you can see, the recorded objective value in the solution le is 378 while the value of the
computed objective value is 387. This is because the distance matrix computed is different
from the actual one really used to compute the objective value of the solution. We refer again
the reader to the remark on Travel-time Computation from Jeffrey Ohlmann and Barrett Thomas
cited above. If you use the right distance matrix as in the Lpez-Ibez-Blum format, you get:
TSPTW instance of type Lpez-Ibez-Blum format
Solution is feasible!
Loaded obj value: 378, Computed obj value: 378
Total computed travel time: 387
TSPTW file LIB_n20w20.001.txt (n=21, min=2, max=57, sym? yes)

Now both the given objective value and the computed one are equal. Note that the total travel
time is a bit longer: 387 for a total distance of 378.

9.8.3 The TSPTWData class


Youll nd the code in the le tsptw.h.
The TSPTWData class is modelled on the TSPData class. As in the case of the TSPLIB, we
number the nodes starting from one.
To read instance les
To read TSPTW instance les, the TSPTWData class offers the
LoadTSPTWFile(const std::string& filename);

method. It parses a le in Lpez-Ibez-Blum or da Silva-Urrutia format and - in the second


case - loads the coordinates and the service times for further treatment. Note that the instances
format is only partially checked: bad inputs might cause undened behaviour.
To test if the instance was successfully loaded, use:
bool IsInstanceLoaded() const;

Several specialized getters are available:


std::string Name() const: returns the instance name, here the lename of the
instance;
std::string InstanceDetails() const: returns a short description of the
instance;
int Size() const: returns the size of the instance;
262

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

int64 Horizon() const: returns the horizon of the instance, i.e. the maximal due
time;
int64 Distance(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const: returns the distance between the
two NodeIndexes;
RoutingModel::NodeIndex Depot() const: returns the depot. This the rst
node given in the instance and solutions les.
int64 ReadyTime(RoutingModel::NodeIndex i) const:
ready time of node i;

returns the

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 SilvaUrrutia format. In the Lpez-Ibez-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 accumulated 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);
}

To read solution les


To read solution les, use the
void LoadTSPTWSolutionFile(const std::string& filename);

method.
This way, you can load solution les and test them with the bool
IsFeasibleSolution() method briey seen above. Actually, you should enquire if the
solution is feasible before doing anything with it.
Three methods help you deal with the existence/feasibility of the solution:
bool IsSolutionLoaded() const;
bool IsSolution() const;
bool IsFeasibleSolution() const;

With IsSolutionLoaded() you can check that indeed a solution was loaded/read from a
le. IsSolution() tests if the solution contains once and only once all the nodes of the
graph while IsFeasibleSolution() tests if the loaded solution is feasible, i.e. if all due
times are respected.

263

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:
value and returns it;
int64 SolutionLoadedObjective() const:
stored in the instance le

computes the objective

returns the objective value

These methods are also available if the solution was obtained by the solver (in this
case, SolutionLoadedObjective() returns -1 and IsSolutionLoaded() returns
false).
The TSPTWData class doesnt generate random instances. We wrote a little program for this
purpose.

9.8.4 Random generation of instances


Youll nd the code in the le tsptw_generator.cc.
The TSPTW instance generator tsptw_generator is very basic. It generates an instance in
Lpez-Ibez-Blum or/and da Silva-Urrutia as follows:
it generates random points in the plane;
it generates a random tour;
it generates random service times and
it generates random time windows such that the random solution is feasible.
Several parameters (gags) are dened to control the output:
tsptw_name: The name of the instance;
tsptw_size: The number of clients including the depot;
tsptw_deterministic_random_seed: Use deterministic random seeds or not?
(default: true);
tsptw_time_window_min: Minimum window time length (default: 10);
tsptw_time_window_max: Maximum window time length (default: 30);
tsptw_service_time_min: Minimum service time length (default: 0);
tsptw_service_time_max: Maximum service time length (default: 10);
tsptw_x_max: Maximum x coordinate (default: 100);
tsptw_y_max: Maximum y coordinate (default: 100);
tsptw_LIB: Create a Lpez-Ibez-Blum format instance le or not? (default: true);
tsptw_DSU: Create a da Silva-Urrutia format instance le or not? (default: true);
264

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

By default, if the name of the instance is myInstance, tsptw_generator creates the three
les:
DSU_myInstance.txt;
LIB_myInstance.txt and
myInstance_init.sol.
myInstance_init.sol contains the random tour generated to create the instance. Files
with the same name are overwritten without mercy.

9.8.5 Visualization with ePix


To visualize the solutions, we rely again on the excellent ePiX library.
The le
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);

To write a ePiX solution le, use the following methods:


void WriteSolutionFile(const
const
void WriteSolutionFile(const
const

Assignment * solution,
std::string & epix_filename)
std::string & tpstw_solution_filename,
std::string & epix_filename);

The rst method takes an Assignment while the second method reads the solution from a
solution le.
You can dene the width and height of the generated image:
DEFINE_int32(epix_width, 10, "Width of the pictures in cm.");
DEFINE_int32(epix_height, 10, "Height of the pictures in cm.");

Once the ePiX le is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp

Here is an example of the solution in the le n20w20.001.sol:

265

9.9. The TSPTW in or-tools

The dot in red in the center represents the depot or rst node. The arrows indicate the direction
of the tour. Because of the time windows, the solution is no longer planar, i.e. the tour crosses
itself.
You can also print the node labels and the time windows with the ags:
DEFINE_bool(tsptw_epix_labels, false, "Print labels or not?");
DEFINE_bool(tsptw_epix_time_windows, false,
"Print time windows or not?");

For your (and our!) convenience, we wrote a small program tsptw_solution_to_epix. Its
implementation is in the le tsptw_solution_to_epix.cc. To use it, invoke:
./tsptw_solution_to_epix TSPTW_instance_file TSPTW_solution_file >
epix_file.xp

9.9 The TSPTW in or-tools


You can nd the code in the le 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
les 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.

9.9.1 Time windows as a Dimension


Youll nd the code in the le tsptw.cc.
Dimensions are quantities accumulated along the nodes in a routing solution and can be used
to model time windows. Remember the formula from last section:
Total travel time to node j = Total travel time to i +
Distance(i,j) + Service time at node i + Waiting time
at i.
266

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

This is perfect for a Dimension. If NextVar(i) == j then


CumulVar(j) = CumulVar(i) + TransitVar(i) +
SlackVar(i).
The correspondence is the following (NextVar(i) == j):
CumulVar(i): Total travel time to node i;
TransitVar(i): Distance(i,j) + Service time at node i;
SlackVar(i): Waiting time at i.
Lets write the corresponding code. First, we declare the routing solver:
RoutingModel routing(data.Size(), 1);
routing.SetDepot(data.Depot());
routing.SetCost(NewPermanentCallback(&data, &TSPTWData::Distance));

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);
}

We pass this callback to the routing solver:


routing.AddDimension(NewPermanentCallback(&data,
&TSPTWData::DistancePlusServiceTime),
data.Horizon(), data.Horizon(), true, "time");

The signature of AddDimension() is as follows:


void AddDimension(NodeEvaluator2* evaluator,
int64 slack_max,
int64 capacity,
bool fix_start_cumul_to_zero,
const string& name);

If NextVar(i) == j in a solution, then the TransitVar(i) variable is constrained to be


equal to evaluator(i,j). slack_max is an upper bound on the SlackVar() variables
and capacity is an upper bound on the CumulVar() variables. For both upper bounds,
we use the horizon. name is a string that permits to nd the variables corresponding to a
Dimension name:
IntVar* const cumul_var = routing.CumulVar(i, "time");

The astute reader will have noticed that there is a problem with the depot. Indeed, we want
to take the time to service the depot at the end of the tour, not the beginning. Fix the bool
fix_start_cumul_to_zero to true and the CumulVar() variable of the start node
of all vehicles will be set to 0.
To model the time windows of a node i, we simply bound the corresponding CumulVar(i)
variable:
267

9.9. The TSPTW in or-tools

for (RoutingModel::NodeIndex i(0); i < size; ++i) {


int64 index = routing.NodeToIndex(i);
IntVar* const cumul_var = routing.CumulVar(index, "time");
cumul_var->SetMin(data.ReadyTime(i));
cumul_var->SetMax(data.DueTime(i));
}

We use the basic search strategy and turn off the large neighborhood search that can slow down
the overall algorithm:
routing.set_first_solution_strategy(
RoutingModel::ROUTING_DEFAULT_STRATEGY);
routing.SetCommandLineOption("routing_no_lns", "true");

Lets test this TSPTW solver on the following generated instance in da Silva-Urrutia format
(le DSU_test.tsptw):
!!

test

CUST NO.
1
2
3
4
5
999

XCOORD.
72.00
59.00
99.00
69.00
42.00
0.00

YCOORD.
22.00
3.00
8.00
46.00
72.00
0.00

DEMAND
0.00
0.00
0.00
0.00
0.00
0.00

READY TIME
0.00
197.00
147.00
242.00
56.00
0.00

DUE DATE

SERVICE TIME

504.00
216.00
165.00
254.00
67.00
0.00

2.00
2.00
9.00
3.00
9.00
0.00

We invoke:
./tsptw -instance_file=DSU_test.tsptw -solution_file=test.sol

and we obtain:
1 5 3 2 4
252

Lets check this solution with


check_tsptw_solution -instance_file=DSU_test.tsptw
-solution_file=test.sol -log_level=1

The solution is feasible:


Actions:
travel
serve
travel
serve
travel
serve
travel

268

to
to
to
to

Nodes:

Releases:

Deadlines:

Services:

Durations:

Time:

4
4
2
2
1
1
3

56
56
147
147
197
197
242

67
67
165
165
216
216
254

9
9
9
9
2
2
3

58
9
86
9
40
2
44

58
67
153
162
202
204
248

Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

serve
3
travel to
0
serve
0
Solution is feasible!
Obj value = 252

242
0
0

254
504
504

3
2
2

3
24
2

251
275
277

If we solve the same instance but in Lpez-Ibez-Blum format (le LIB_test.tsptw):


5
0 25 39 27 67
25 0 49 47 80
32 42 0 51 95
26 46 57 0 46
60 73 95 40 0
0 504
197 216
147 165
242 254
56 67

we get the same solution but with a different objective value:


1 5 3 2 4
277

The reason is that the services times are added to the distances in this format.
check_tsptw_solution conrms this:
Actions:

Nodes:

travel to
serve
travel to
serve
travel to
serve
travel to
serve
travel to
serve
Solution is feasible!
Obj value = 277

4
4
2
2
1
1
3
3
0
0

Releases:

Deadlines:

Services:

Durations:

Time:

56
56
147
147
197
197
242
242
0
0

67
67
165
165
216
216
254
254
504
504

0
0
0
0
0
0
0
0
0
0

67
0
95
0
42
0
47
0
26
0

67
67
162
162
204
204
251
251
277
277

Real instances, like DSU_n20w20.001.txt, are out of reach for our basic tsptw. This is
mainly because nding a rst feasible solution is in itself a difcult problem. In the next subsection, well help the solver nding this rst feasible solution to start the local search.

9.9.2 A basic heuristic to nd an initial solution


[TO BE WRITTEN]
269

9.10. Summary

9.10 Summary
summary

270

CHAPTER

TEN

VEHICULE ROUTING PROBLEMS WITH


CONSTRAINTS: THE CAPACITATED VEHICLE
ROUTING PROBLEM

Overview:
Prerequisites:

Basic knowledge of C++.


Basic knowledge of Constraint Programming (see chapter 1).
Basic knowledge of the Constraint Programming Solver (see chapter 2).
Basic knowledge of Local Search (see chapter 6).
Basic knowledge of the Routing Library (see the chapter 9), especially:
section 9.2;
section 9.4;
section 9.7.
Files:

10.1 The Vehicle Routing Problem (VRP)


You can nd the code in the les tsplib_reader.h, cvrp_data_generator.h,
cvrp_data_generator.cc,
cvrp_data.h,
cvrp_solution.h,
cvrp_epix_data.h and cvrp_solution_to_epix.cc and the data in the
les A-n32-k5.vrp and opt-A-n32-k5.

10.2. The VRP in or-tools

10.1.1 The Problem


10.1.2 Benchmark data
The TSPLIB format for the CVRP
The instance le
The solution le

10.1.3 To read TSPLIB les


10.1.4 To generate a random CVRP: the CVRPDataGenerator
class
10.1.5 To hold and check a (C)VRP solution: the CVRPSolution
class
10.1.6 The CVRPData class: part I
10.1.7 Visualization with ePix

10.2 The VRP in or-tools


You can nd the code in the les tsplib_reader.h, cvrp_data_generator.h,
cvrp_data_generator.cc, cvrp_data.h, cvrp_data.h, cvrp_epix_data.h
and vrp_solution_to_epix.cc and the data in the les A-n32-k5.vrp.

10.2.1 How to force all vehicles to service cities?


10.2.2 The basic program
10.2.3 Some outputs

10.3 The Capacitated Vehicle Routing Problem (CVRP)


You can nd the code in the les cvrp_data.h and check_cvrp_solution.cc and
the data in the les A-n32-k5.vrp and opt-A-n32-k5.

272

Chapter 10. Vehicule Routing Problems with constraints: the capacitated


vehicle routing problem

10.3.1 The problem


10.3.2 The CVRPData class: part II

10.4 The CVRP in or-tools


You can nd the code in the les tsplib_reader.h, cvrp_data_generator.h,
cvrp_data_generator.cc,
cvrp_data.h,
cvrp_solution.h,
cvrp_epix_data.h and cvrp_solution_to_epix.cc and the data in the
les A-n32-k5.vrp and opt-A-n32-k5.

273

10.5. Multi-depots and vehicles

10.4.1 The demands as a Dimension


10.4.2 An initial solution
10.4.3 Different search strategies
10.4.4 What about individualizing the vehicles?

10.5 Multi-depots and vehicles


10.5.1 Problems with multi-depots
10.5.2 Multi-depots in practice
10.5.3 The VehicleVar() variables
10.5.4 VehicleClasses

10.6 Partial routes and Assigments


10.6.1 A little bit of vocabulary
10.6.2 Locks and the ApplyLocksToAllVehicles() method
10.6.3 Assignments and partial Assignments

10.7 Summary

274

CHAPTER

ELEVEN

ARC ROUTING PROBLEMS WITH CONSTRAINTS: THE


CUMULATIVE CHINESE POSTMAN PROBLEM

Overview:
Prerequisites:
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap11.


The les inside this directory are:

11.1. The Chinese Postman Problem (CPP)

11.1 The Chinese Postman Problem (CPP)


11.1.1 The Problem

11.2 The Cumulative Chinese Postman Problem (CCPP)


11.2.1 The Problem
11.2.2 Benchmark data
11.2.3 The CCPPData class
11.2.4 Visualization with ePix

11.3 A rst implementation for the CCPP


11.4 Disjunctions
11.4.1 Active and non active nodes

11.5 A second implementation for the CCPP


11.6 Partial routes and locks
11.7 Lower bounds
11.8 Summary

276

Part IV
Technicalities

CHAPTER

TWELVE

UTILITIES

12.1 Logging
[TO BE REREAD]
We provide very basic logging tools: macros replaced by some basic logging objects. They are
dened in the header base/logging.h.
LG or LOG(INFO) is always working. You can print messages to std:cerr like this
LG << "This is my important message with " << var << " pancakes.";

Of course, var must overwrite the << operator. The message is automatically followed by a
\n that adds a new line.
If you didnt change the value of the gags ag log_prefix to false, youll see the following message:
[20:47:47] my_file.cc:42: This is my important message with 3 pancakes.

Your message is prexed by the hour, the le name and the line number of the code source
where your message was dened. You can disable this prex by setting log_prefix to
false.
We provide different levels of logging:
First, depending on the severity:
INFO;
WARNING;
ERROR;
FATAL.
To use them, just write LOG(severity) as in:
LOG(FATAL) << "This message will kill you!";

For the moment, INFO, ERROR and WARNING are treated the same way. FATAL works
as expected and the program aborts (calls abort()) after printing the message.

12.2. Asserting

Second, depending on the debug or release mode. When debugging, you can use
DLOG(severity) with the same levels (and the same results). If NDEBUG is dened,
you are in release mode and DLOG(severity) doesnt do anything except for FATAL
where it becomes a LOG(ERROR).
Finally, you can also use VLOG(level) with different levels. The higher the level, the
more detailed the information. By default, the level is set to 0. You can change this by
setting the right level value to the gags ag log_level.
So, if FLAGS_log_level = 1 the following message is printed:
VLOG(1) << "He, he, you can see me!";

but not this one:


VLOG(2) << "This information is too detailed for you to see with
your log level...";

We rarely (understand never) go over level 4.


There is also a conditional logging: LOG_IF(severity, condition) and for debugging
DLOG_IF(severity, condition) that vanishes when NDEBUG is dened.
A little word of advice.
When logging is allowed, you create each time a logging object so this can be costly.
When logging is disallowed, you dont pay anything.

12.2 Asserting
We provide several assert-like macros in the header base/logging.h.
Remember that the variable NDEBUG (NO DEBUG) is dened by the standard. By default,
the assert debugging mechanism dened in assert.h or the C++ equivalent cassert is on.
You have to explicitly turn it off by dening the variable NDEBUG.
Two types of assert-like macros are provided:
Debug-only checking and
Always-on checking.
Debug-only macros are only triggered in DEBUG mode (i.e. when the variable NDEBUG is not
dened) and start with the letter D. In NON DEBUG mode (the variable NDEBUG is dened), the
code inside Debug-only macros vanishes. Always-on macros are always on duty. For instance,
DCHECK(x) is Debug-only while CHECK() is Always-on.
Here are the macros listed:

280

Chapter 12. Utilities


Name
(D)CHECK(x)
(D)CHECK_GE(x,y)
(D)CHECK_LT(x, y)
(D)CHECK_GT(x, y)
(D)CHECK_LE(x, y)
(D)CHECK_EQ(x, y)
(D)CHECK_NE(x, y)

Tests if
(x)
(x) >= (y)
(x) < (y)
(x) > (y)
(x) <= (y)
(x) == (y)
(x) != (y)

There is also the Always-on CHECK_NOTNULL(x) macro that tests if (x) != NULL.
are also dened. Note that these macros are always functional. If you prefer to use
safeguards that vanish in the release code, use their equivalent1 starting with a D:
DCHECK_LT(x, y), etc. and compile with the NDEBUG variable set to 1.
These macros are dened in the header logging.h:

12.3 Timing
We propose two timers: a basic timer (WallTimer) and a more advanced one
(CycleTimer). These two classes work under Windows, Linux and MacOS. The Solver
class uses by default a WallTimer internally.
Both timers are declared in the header base/timer.h.

12.3.1 Basic timer


This basic timer is dened by the WallTimer class. This class proposes the usual methods:
void Start()
void Stop()
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 seconds.
If you need even more precise timing, use the following method:
static int64 GetTimeInMicroSeconds()
1

There is no equivalent for CHECK_NOTNULL(x).

281

12.3. Timing

that returns the time in microseconds.


To measure the time, we query the system time and add or subtract the queried times.
Our timers measure the wall times.
To get the time in milliseconds, we use:
System
Linux
Windows
MacOS

Function
gettimeofday()
clock()
gettimeofday()

To get the time in microseconds, we use:


System
Linux
Windows
MacOS

Function
clock_gettime()
QueryPerformanceCounter() and QueryPerformanceFrequency()
mach_absolute_time() and mach_timebase_info()

What is the wall time?


The wall time is the real time that elapses from start to end of a program/task/process,
including the time due to system delays (other programs running at the same time, waiting
times for resources to become available, etc). In other words, it is the difference between
the time at which a task nishes and the time at which the task started.

12.3.2 Advanced timer


This timer is dened 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()
void Start()
void Stop()
int64 GetInUsec() const
int64 GetInMs() const
GetInUsec() returns the elapsed time in microseconds and GetInMs() converts this time
in milliseconds.

282

Chapter 12. Utilities

12.3.3 Integrated timer


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.
The Solvers integrated timer is never reset!
To query this timer:
Solver solver(...);
LG << solver.wall_time()
<< " ms elapsed since the creation of the solver";

12.4 Proling
12.5 Debugging
12.6 Serializing
12.7 Visualizing
12.8 Randomizing

283

CHAPTER

THIRTEEN

MODELING TRICKS

Overview:
Prerequisites:
Classes under scrutiny:
Files:

You can nd the code in the directory documentation/tutorials/cplusplus/chap13.


The les inside this directory are:

13.1. Efciency

13.1 Efciency
13.1.1 Keep variables ordered

13.2 False friends and counter-intuitive ideas


13.2.1 Accepted solutions vs feasible solutions
13.2.2 Solve() vs SolveAndCommit()
13.2.3 LocalOptimumReached() vs LocalOptimum()
13.2.4 DebugString() doesnt give the value of a solution
13.2.5 Solve() vs the StartSearch() - NextSolution() EndSearch() mechanism

13.3 What are my solving options?


13.3.1 The search mechanism
13.3.2 Global methods
Solve()
SolveAndCommit
MakeNestedOptimize()

13.3.3 DecisionBuilders
SolveOnce

13.3.4 Decisions
NestedSolveDecision

13.3.5 Summary

286

14.1. Main les and directories

CHAPTER

FOURTEEN

UNDER THE HOOD

14.1 Main les and directories


14.2 Naming conventions and programming idioms
14.2.1 Naming conventions
General naming conventions
Methods

14.2.2 Programming idioms


Factories
Caches
Callbacks
Visitors

14.3 Main classes, structures and typedefs


14.3.1 BaseObjects
14.3.2 PropagationBaseObjects
14.3.3 Callbacks
NewPermanentCallback()

14.4 The Trail struct


288

14.5 The Search class

Chapter 14. Under the hood

// Beginning of the search. virtual void EnterSearch();


// Restart the search. virtual void RestartSearch();
// End of the search. virtual void ExitSearch();
// Before calling DecisionBuilder::Next
sion(DecisionBuilder* const b);

virtual

void

BeginNextDeci-

// After calling DecisionBuilder::Next, along with the returned decision. virtual


void EndNextDecision(DecisionBuilder* const b, Decision* const d);
// Before applying the decision virtual void ApplyDecision(Decision* const d);
// Before refuting the Decision virtual void RefuteDecision(Decision* const d);
// Just after refuting or applying the decision, apply is true after Apply. // This
is called only if the Apply() or Refute() methods have not failed. virtual void
AfterDecision(Decision* const d, bool apply);
// Just when the failure occurs. virtual void BeginFail();
// After completing the backtrack. virtual void EndFail();
// Before the initial propagation. virtual void BeginInitialPropagation();
// After the initial propagation. virtual void EndInitialPropagation();
// This method is called when a solution is found. It asserts of the // solution is
valid. A value of false indicate that the solution // should be discarded. virtual bool
AcceptSolution();
// This method is called when a valid solution is found. If the // return value is true,
then search will resume after. If the result // is false, then search will stop there.
virtual bool AtSolution();
// When the search tree is nished. virtual void NoMoreSolutions();
// When a local optimum is reached. If true is returned, the last solution // is
discarded and the search proceeds with the next one. virtual bool LocalOptimum();
// virtual bool AcceptDelta(Assignment* delta, Assignment* deltadelta);
// After accepting a neighbor during local search. virtual void AcceptNeighbor();
Solver* solver() const
// Tells the solver to kill the current search. void FinishCurrentSearch();
// Tells the solver to restart the current search. void RestartCurrentSearch();
// Periodic call to check limits in long running methods. virtual void PeriodicCheck();
// Returns a percentage representing the propress of the search before // reaching
limits. virtual int ProgressPercent() { return kNoProgress; }
// Accepts the given model visitor. virtual void Accept(ModelVisitor* const visitor)
const;

289

14.9. Local Search (LS)

// Registers itself on the solver such that it gets notied of the search // and propagation events. virtual void Install();

14.9 Local Search (LS)


14.10 Meta-heuristics and SearchMonitors
14.10.1 The Metaheuristic class
14.10.2 Callbacks to implement

14.11 The Routing Library (RL)


You can nd the code in the le rl_auxiliary_graph.cc.
Each node has a unique identier of type RoutingModel::NodeIndex but we use
internally a unique index of type int64 (see section 9.4). The model is explained in broad
terms in section 9.5. All components are dened or accessible within the RoutingModel
class. To use this class, include the mandatory constraint_solver/routing.h
header.

14.11.1 Global constants


Some global constant basic paratemers of the model are:
Variables (pu/pr)
solver_ (pr)

Descriptions
CP Solver.

nodes_ (pr)
vehicles_ (pr)

Total number of nodes.


Total number of vehicles.

start_end_count_
(pr)
kUnassigned (pu)
kNoPenalty (pu)
RoutingModel::
kFirstNode (pu)
RoutingModel::
kInvalidNodeIndex
(pu)
Size() (pu)

Total number of different (starting and


ending) depots.
static const int = -1
static const int = -1
RoutingModel:: NodeIndex(0)

290

RoutingModel:: NodeIndex(-1)

Number of IntVar variables.

Queries
Solver* solver()
const
int nodes() const
int vehicles()
const
None
kUnassigned
kNoPenalty
RoutingModel::
kFirstNode
RoutingModel::
kInvalidNodeIndex
Size()

Chapter 14. Under the hood

(pu) stands for public and (pr) for private. The int64 Size() const method returns nodes_ + vehicles_ - start_end_count_, which is exactly the minimal number of variables needed to model the problem at hand with one variable per node (see next
subsection). kUnassigned is used for unassigned indices.

14.11.2 The auxiliary graph


You can nd the source code in the le rl_auxiliary_graph.cc.
The auxiliary graph is a graph constructed from the original graph. Lets examine the original
graph of the next gure:
1

5
3

1
0

1
0
1
0

Starting depot

1
0Ending depot
1
0

1
0

Starting and ending depot


Transit node

There are nine nodes, two of which are starting depots (1 and 3), one is an ending depot (7) and
one is a starting and ending depot (4). The NodeIndexes range from 0 to 8.
There are start_end_count_ = 4 distinct depots (nodes 1, 3, 4 and 7) and nodes_ start_end_count_ = 5 transit nodes (nodes 0, 2, 5, 6 and 8).
In this example, we take four vehicles/routes:
route 0: starts at 1 and ends at 4
route 1: starts at 3 and ends at 4
route 2: starts at 3 and ends at 7
route 3: starts at 4 and ends at 7
Here is the code:
std::vector<std::pair<RoutingModel::NodeIndex,
RoutingModel::NodeIndex> > depots(4);
depots[0] = std::make_pair(1,4);
depots[1] = std::make_pair(3,4);
depots[2] = std::make_pair(3,7);
depots[3] = std::make_pair(4,7);
RoutingModel VRP(9, 4, depots);

The auxiliary graph is obtained by keeping the transit nodes and adding a starting and ending
depot for each vehicle/route if needed as shown in the following gure:

291

14.11. The Routing Library (RL)

5
6

1
04
1
0
1
0
1
0
1
0

1
70
1
0
1
0
1
0
1
0

Starting depot

1
0Ending depot
1
0

1
0

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 nally there are two copies of
node 4 because two routes (routes 0 and 4) end at 4 and one route (route 3) starts from 4.
The number of variables is:
nodes_ + vehicles_ start_end_count_ = 9 + 4 4 = 9.
These nine variables correspond to all the nodes in the auxiliary graph leading somewhere, i.e.
starting depots and transit nodes in the auxiliary graph.
nexts_ variables
The main decision variables are IntVar* stored in an std::vector nexts_ and can be
accessed with the NextVar() method. The model uses one IntVar variable for each node
that can be linked to another node. If a node is the ending node of a route (and no route
starts from it), we dont use any NextVar() variable for that node. The minimal number of
nexts_ variables is:
nodes_ start_end_count_ + vehicles_
We need one variable for each node that is not a depot (nodes_ - start_end_count_)
and one variable for each vehicle (a starting depot: vehicles_).
Remember that the int64 Size() const method precisely returns this amount:
// Returns the number of next variables in the model.
int64 Size() const { return nodes_ + vehicles_ - start_end_count_; }

The domain of each IntVar is [0,Size() + vehicles_ - 1]. The end depots are
represented by the last vehicles_ indices.

292

Chapter 14. Under the hood

Numbering of the int64 indices


The SetStartEnd() method takes care of the numbering.
Nodes in the original graph that lead somewhere (starting depots and transit nodes)
are numbered from 0 to nodes_ + vehicles_ - start_end_count_ - 1
= Size() - 1. The end depots are numbered from Size() to Size() +
vehicles_ - 1.
The numbering corresponds to the order in which the original nodes
RoutingModel::NodeIndexes are given and the order the (start, end)
pairs of depots are given.
In total there are (Size() + vehicles_) int64 indices: one index for each transit
node and one index for each combination of depots and vehicles.
For our example, this numbering is as follows:
1

5
3

6
8

9
1
0
1
0 4
1
0
10 0
1
1
0
1
11 0
1
0
1
012
1
0
1
0

Starting depot

1
0Ending depot
1
0

1
0

Starting and ending depot


Transit node

If you set the FLAGS_log_level to 2 and skip the log prex:


./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

293

14.11. The Routing Library (RL)

Node
Node
Node
Node
Node
Node

index
index
index
index
index
index

3
4
5
6
7
8

->
->
->
->
->
->

Variable
Variable
Variable
Variable
Variable
Variable

index
index
index
index
index
index

3
4
5
6
-1
7

The variable indices are the int64 indices used internally in the RL. The Node Indexes
correspond to the unique NodeIndexes of each node in the original graph. Note that
NodeIndex 7 doesnt have a corresponding int64 index (-1 means exactly that) and that
NodeIndex 8 corresponds to int64 7 (not 8!).
Here is one possible solution:
1

5
6
3

1
04
1
0
1
0
1
0
1
0

1
70
1
0
1
0
1
0
1
0

Starting depot
1
0Ending depot
1
0
Starting and ending depot
Transit node

1
0

We output the routes, rst with the NodeIndexes and then with the internal int64 indices
with:
for (int p = 0; p < VRP.vehicles(); ++p) {
LG << "Route: " << p;
string route;
string index_route;
for (int64 index = VRP.Start(p); !VRP.IsEnd(index); index =
Solution->Value(VRP.NextVar(index))) {
route = StrCat(route,
StrCat(VRP.IndexToNode(index).value(), " -> "));
index_route = StrCat(index_route, StrCat(index, " -> "));
}
route = StrCat(route, VRP.IndexToNode(VRP.End(p)).value());
index_route = StrCat(index_route, VRP.End(p));
LG << route;
LG << index_route;
}

and get:
Route:
1 -> 0
1 -> 0
Route:
3 -> 5
3 -> 5
Route:

294

0
->
->
1
->
->
2

2 -> 4
2 -> 9
4
10

Chapter 14. Under the hood

3 -> 6
8 -> 6
Route:
4 -> 8
4 -> 7

->
->
3
->
->

7
11
7
12

Some remarks

NodeIndex and int64 indices dont necessarly match;


For each route, the starting int64 index is smaller than the ending int64 index;
All ending indices are equal or greater than Size(). Because there are vehicles_
ending int64 indices, this means that all int64 indices equal or greater than Size()
must correspond to end depots. The method IsEnd(int64) is thus simply:
bool IsEnd(int64 index) {
return index >= Size();
}

14.11.3 Variables
Path variables
Dimension variables

14.11.4 Constraints
NoCycle constraint

14.12 Summary

295

Part V
Apprendices

BIBLIOGRAPHY

[Williams2001] Williams, H.P. and Yan, H. Representations of the all_different Predicate of


Constraint Satisfaction in Integer Programming, INFORMS Journal on Computing, V.3, n.
2, pp 96-103, 2001.
[Gasarch2002] 23. (a) Gasarch. The P=?NP poll, SIGACT News 33 (2), pp 3447, 2002.
[Gasarch2012] 23. (a) Gasarch. The second P =?NP poll, SIGACT News 43(2), pp 53-77,
2012.
[Garey1979] Garey, M. R. and Johnson D. S. Computers and Intractability: A Guide to the
Theory of NP-Completeness, 1979, W. H. Freeman & Co, New York, NY, USA, pp 338.
[Freuder1997] E. C. Freuder. In Pursuit of the Holy Grail, Constraints, Kluwer Academic
Publishers, 2, pp. 57-61, 1997
[Abramson1997] D. Abramson and M. Randall. A Simulated Annealing code for General Integer Linear Programs, Annals of Operations Research, 86, pp. 3-24, 1997.
[Lopez-Ortiz2003] Alejandro Lopez-Ortiz, Claude-Guy Quimper, John Tromp and Peter Van
Beek. A fast and simple algorithm for bounds consistency of the all different constraint,
Proceedings of the 18th international joint conference on Articial intelligence, Acapulco,
Mexico, pp 245-250, 2003, Morgan Kaufmann Publishers Inc.
[Meyer-Papakonstantinou] Christophe Meyer and Periklis A. Papakonstantinou. On the complexity of constructing Golomb Rulers, Discrete Applied Mathematics, 57, pp 738748,
2009.
[Dimitromanolakis2002] Apostolos Dimitromanolakis. Analysis of the Golomb Ruler and the
Sidon Set Problems, and Determination of Large, Near-Optimal Golomb Rulers. Ph.D.
Thesis, Department of Electronic and Computer Engineering, Technical University of
Crete.
[GalinierEtAl] Philippe Galinier, Brigitte Jaumard, Rodrigo Morales and Gilles Pesant. A
Constraint-Based Approach to the Golomb Ruler Problem, XXX, 2007.
[SmithEtAl] Barbara M. Smith, Kostas Stergiou and Toby Walsh. Modelling the Golomb Ruler
Problem. Report 1999.12, School of computer studies, University of Leeds, 1999.
[Hoffman1969] Hoffman, Loessi and Moore. Constructions for the Solution of the m Queens
Problem, Mathematics Magazine, p. 66-72, 1969.

Bibliography

[Jordan2009] Jordan and Brett. A survey of known results and research areas for n-queens,
Discrete Mathematics, Volume 309, Issue 1, 2009, pp 1-31.
[Garey1976] Garey, M. R., Johnson, D. S. and Sethi, R., The complexity of owshop and
jobshop scheduling, Mathematics of Operations Research, volume 1, pp 117-129, 1976.
[Kis2002] Kis, T., On the complexity of non-preemptive shop scheduling with two jobs, Computing, volume 69, nbr 1, pp 37-49, 2002.
[Taillard1993] Taillard, E., 1993. Benchmarks for basic scheduling problems, European Journal of Operational Research, Elsevier, vol. 64(2), pages 278-285, January.
[Adams1988] J. Adams, E. Balas, D. Zawack, The shifting bottleneck procedure for job shop
scheduling. Management Science, 34, pp 391-401, 1988.
[Christodes1976] Christodes, Nicos. Worst-case analysis of a new heuristic for the travelling salesman problem, Technical Report, Carnegie Mellon University, 388, 1976.
[Eksioglu2009] B. Eksioglu, A. Volkan Vural, A. Reisman, The vehicle routing problem: A
taxonomic review, Computers & Industrial Engineering, Volume 57, Issue 4, November
2009, Pages 1472-1483.
[Prosser2003] J. C. Beck, P. Prosser and E. Selensky, Vehicle Routing and Job Shop Scheduling: Whats the difference?, Proc. of the 13th International Conference on Automated Planning and Scheduling, 2003, pages 267276.
[Savelsbergh1985] M.W.P. Savelsbergh. Local search in routing problems with time windows,
Annals of Operations Research 4, 285305, 1985.
[Ferreira2010] R. Ferreira da Silva and S. Urrutia. A General VNS heuristic for the traveling
salesman problem with time windows, Discrete Optimization, V.7, Issue 4, pp. 203-211,
2010.
[Dash2010] S. Dash, O. Gnlk, A. Lodi, and A. Tramontani. A Time Bucket Formulation for
the Traveling Salesman Problem with Time Windows, INFORMS Journal on Computing,
v24, pp 132-147, 2012 (published online before print on December 29, 2010).
[Dumas1995] Dumas, Y., Desrosiers, J., Gelinas, E., Solomon, M., An optimal algorithm for
the travelling salesman problem with time windows, Operations Research 43 (2) (1995)
367-371.

300

INDEX

Symbols

FATAL, 279

cp_model_stats, 58
cp_no_solve, 57
cp_print_model, 57
cp_show_constraints, 57
help, 45, 57
helpmatch=S, 45
helpon=FILE, 45
helpshort, 45

A
AddConstraint(), 38
Assignment, 40

C
constraint
AllDifferent, 32
cpviz, 92
cryptarithmetic
puzzles, 30

D
DebugString(), 56
DecisionBuilder, 38
DEFINE_bool, 44
DEFINE_double, 44
DEFINE_int32, 44
DEFINE_int64, 44
DEFINE_string, 44
DEFINE_uint64, 44
DLOG, 279
DLOG_IF, 280

E
EndSearch(), 40
ERROR, 279

F
factory method, 35

gags, 44
log levels, 280
log prex, 279
parameters read from a le, 250
replacement
(routing.SetCommandLineOption()),
224
shortcuts, 45
types, 44
Golomb Ruler
Problem, 49
Golomb ruler, 51

I
INFO, 279
IntExpr, 36
IntVar, 35

L
LG, 279
LOG(ERROR), 279
LOG(FATAL), 279
LOG(INFO), 279
LOG(WARNING), 279
LOG_IF, 280

M
MakeAllDifferent(), 38
MakeAllSolutionCollector(), 40
MakeBestValueSolutionCollector(), 40
MakeDifference(), 62
MakeEquality(), 38
MakeFirstSolutionCollector(), 40
MakeIntConst(), 60
MakeIntVar(), 35
MakeIntVarArray(), 55

Index

MakeLastSolutionCollector(), 40
MakeLessOrEqual(), 65
MakeMinimize, 55
MakeNonEquality(), 62
MakePhase(), 38
MakeProd(), 36
MakeScalProd(), 37
MakeSum(), 36
MakeTimeLimit(), 45

N
n-queens problem, 74
namespace
operations_research, 34
NewSearch(), 39
NextSolution(), 39

O
objective functions, 49, 50
OptimizeVar, 55

P
ParseCommandLineFlags(), 44
Problem
Cryptarithmetic puzzles, 30
Golomb Ruler, 49
puzzles
cryptarithmetic, 30

S
SearchLimit, 60
in Local Search, 172
specialized for time, 46
SearchMonitor
as SolutionCollector, 40
as Solvers parameters, 46
callbacks, 85
SetCommandLineOption(), 224
SolutionCollector, 40
AllSolutionCollector, 40
BestValueSolutionCollector, 40
FirstSolutionCollector, 40
LastSolutionCollector, 40
Solve(), 41
Solver
creation, 35
parameters, 45
SolverParameters, 45
302

SolverParameters(), 45
StringPrintf(), 55

T
time
wall_time(), 59

V
variables
IntVar, 35
VLOG, 280

W
wall_time()
time, 59
WARNING, 279

Вам также может понравиться