Sie sind auf Seite 1von 417

or-tools open source library

User’s Manual
Nikolaj van Omme
Laurent Perron
Vincent Furnon

February 10, 2015 © Copyright 2012-2014, Google


(A4 version)
Welcome to the or-tools user’s manual!

© Copyright 2012-2014, Google

License information

This document is provided under the terms of the


Apache License 2.0
You can find 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 find other useful sources of
documentation about the or-tools library.

All the cliparts used in this manual are public domain except the oxygen-style mimetypes icons
from the open icon library licensed under the GNU Lesser General Public License version 3.

Trademarks

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, Håkan Kjellerstrand, Louis-Martin Rousseau, Thomas Carton de Wiart
FOREWORD

We are glad to welcome you to the or-tools user’s manual. In this foreword, we try to answer
most common questions a newcomer could have when discovering this manual or the library
for the first time.

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

What is or-tools?

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

1
If you are curious: Wikipedia article on Operations research.
In short, the or-tools library is:
• Open source and free Everything, including the examples, the implementations of
the algorithms, the various documentations2 , is licenced under the Apache License
2.0 and is available for download. If you make substantial improvements to our code,
please share it with the whole community.
• Alive The library is actively maintained and updates and improvements are made on an
almost daily basis.
• Documented OK, we just started to write the documentation but there are already nu-
merous examples written in C++, Python, Java and C#!
• Portable Because it is made by Google, the code conforms strictly to the Google coding
styles3 . The code is known to compile on:
– gcc 4.4.x on ubuntu 10.04 and up (10.10, 11.04, 11.10 and 12.04).
– xcode >= 3.2.3 on Mac OS X Snow Leopard and Mac OS X Lion (gcc 4.2.1).
– Microsoft Visual Studio 10.
Both 32 bit and 64 bit architectures are supported, although the code is optimized to run
in 64 bit mode.
• Efficient All we can say is that we use it internally at Google.
• Accessible Everything is coded in C++ but is available through SWIG in Python, Java,
and .NET (using Mono on non-Windows platforms).
• User-friendly We try to make our code as easy to use as possible (especially in Python
and C#). Of course, there is a (small) learning curve to use our library but once you
master several basic concepts, it is quite straightforward to code with the or-tools library.
• Tested We use it internally at Google since a few years and the community of users is
growing.

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

iv
What you will not learn in this document

This document is by no means a tutorial on Operations Research nor on Contraint Program-


ming. It is also NOT a reference manual (refer to the documentation hub to find 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 shouldn’t
have any trouble scanning the reference manual and find the right method, parameter, function,
. . . or code them yourselves!
We don’t document the non Constraint Programming (CP) part of the library. If you have any
questions about the non-CP part of the library, don’t hesitate to ask them on the mailing list.
See the section How to reach us? below.
We don’t 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 specific
answer, use the index or the table of contents to find a reference to that information. If you are
missing some requirements to understand a section, you can always backtrack on prerequisite
knowledge. For each chapter, we list those prerequisites. This non-linear way of reading is
probably the most efficient and rewarding one!

That said, the manual is kept short so that you can read it in its entirety. The first part (Basics)
is an introduction on how to use the CP solver to solve small problems. For real problems, you
need to customize your search and this is explained in the second part (Customization). If you
are interested in the routing part of the library, the third part is for you (Routing). Finally, some
utilities and tricks are described in the last part (Technicalities).

Targeted audience

This manual is written with two types of readers in mind. First, someone who is not famil-
iar with Constraint Programming nor is she a professional programmer. Second, an educated
reader who masters Constraint Programming and is quite at ease without necessarily mastering
one of the supported computer languages.
From time to time, we refer to scientific articles: you don’t need to read and understand them
to follow the manual.
Did we succeed to write for such different profiles? You tell us!

v
Conventions used in this manual

All the code is systematically written in monospace font. Function and method’s
names are followed by parentheses. The method MakeSomething() and the parameter
something are two beautiful examples of this convention.

To draw your attention on important matters, we use a box with a danger warning sign.

You have been warned!

To explain some details that would break the flow of the text, we use a shadowed box.

This is an explanation that would break the flow 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 definition 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 find complete examples on the documentation hub:

vi
http://or-tools.googlecode.com/svn/trunk/documentation/
documentation_hub.html#tutorial_examples
or under the following directory of the or-tools library:

documentation/tutorials/C++

If you prefer to code in Python, Java or C#, we have translated all the examples in your
favourite language. You can find the complete examples on the documentation hub or under
the directories:

documentation/tutorials/Python
documentation/tutorials/Java
documentation/tutorials/Csharp.

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
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 user’s manual, Google, 2014.
Here is a bibtex entry:
@TECHREPORT{or-tools-user-manual,
author = Nikolaj van Omme and Laurent Perron and Vincent
Furnon,
title = or-tools user’s manual,
institution = Google,
year = 2014
}

vii
Do you have comments?

If you have comments, suggestions, corrections, feedback, . . . , about this document or about the
documentation of the or-tools library in general, please send them to ortools.doc@gmail.com.
Thank you very much.

Happy reading!

The or-tools team

i
CONTENTS

Foreword iii

I Basics 1
1 Introduction to constraint programming 3
1.1 The 4-Queens Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3
1.2 What is constraint programming? . . . . . . . . . . . . . . . . . . . . . . . . 9
1.3 Real examples . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12
1.4 A little bit of theory . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 15
1.5 The three-stage method: describe, model and solve . . . . . . . . . . . . . . . 22
1.6 It’s always a matter of tradeoffs . . . . . . . . . . . . . . . . . . . . . . . . . 24
1.7 The Google or-tools library . . . . . . . . . . . . . . . . . . . . . . . . . . . 25
1.8 The content of the manual . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26
1.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 29

2 First steps with or-tools: cryptarithmetic puzzles 31


2.1 Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.2 Running the tutorial examples . . . . . . . . . . . . . . . . . . . . . . . . . . 32
2.3 The cryptarithmetic puzzle problem and a first model . . . . . . . . . . . . . . 34
2.4 Anatomy of a basic C++ code . . . . . . . . . . . . . . . . . . . . . . . . . . 36
2.5 SolutionCollectors and Assignments to collect solutions . . . . . . 43
2.6 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 47
2.7 Other supported languages . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49
2.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

3 Using objectives in constraint programming: the Golomb Ruler Problem 51


3.1 Objective functions and how to compare search strategies . . . . . . . . . . . 52
3.2 The Golomb ruler problem and a first model . . . . . . . . . . . . . . . . . . 53
3.3 An implementation of the first model . . . . . . . . . . . . . . . . . . . . . . 56
3.4 What model did I pass to the solver? . . . . . . . . . . . . . . . . . . . . . . . 58
3.5 Some global statistics about the search and how to limit the search . . . . . . . 61
3.6 A second model and its implementation . . . . . . . . . . . . . . . . . . . . . 64
3.7 A third model and its implementation . . . . . . . . . . . . . . . . . . . . . . 67
3.8 How to tighten the model? . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68
3.9 How does the solver optimize? . . . . . . . . . . . . . . . . . . . . . . . . . . 71
3.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 72

4 Reification 73
4.1 What is reification? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

II Customization 75
5 Defining search primitives: the n-Queens Problem 77
5.1 The n-Queens Problem . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78
5.2 Implementation of a basic model . . . . . . . . . . . . . . . . . . . . . . . . 81
5.3 Basic working of the solver: the search algorithm . . . . . . . . . . . . . . . . 86
5.4 cpviz: how to visualize the search . . . . . . . . . . . . . . . . . . . . . . . . 97
5.5 Basic working of the solver: the phases . . . . . . . . . . . . . . . . . . . . . 119
5.6 Out of the box variables and values selection primitives . . . . . . . . . . . . 128
5.7 Customized search primitives: DecisionBuilders and Decisions . . . 132
5.8 Breaking symmetries with SymmetryBreakers . . . . . . . . . . . . . . . 141
5.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 147

6 Local Search: the Job-Shop Problem 149


6.1 The Job-Shop Problem, the disjunctive model and benchmark data . . . . . . . 151
6.2 An implementation of the disjunctive model . . . . . . . . . . . . . . . . . . 159
6.3 Scheduling in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 164
6.4 What is Local Search (LS)? . . . . . . . . . . . . . . . . . . . . . . . . . . . 177
6.5 Basic working of the solver: Local Search . . . . . . . . . . . . . . . . . . . . 183
6.6 Local Search Operators . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 198
6.7 The Job-Shop Problem: and now with Local Search! . . . . . . . . . . . . . . 206
6.8 Filtering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 218
6.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 222

7 Meta-heuristics: several previous problems 225


7.1 Search limits and SearchLimits . . . . . . . . . . . . . . . . . . . . . . . 228
7.2 Restarting the search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 231
7.3 Meta-heuristics in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . 233
7.4 Tabu Search (TS) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 239
7.5 Simulated Annealing (SA) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 248
7.6 Guided Local Search (GLS) . . . . . . . . . . . . . . . . . . . . . . . . . . . 254
7.7 Large neighborhood search (LNS): the Job-Shop Problem . . . . . . . . . . . 269
7.8 Default search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 278
7.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 282

8 Custom constraints: the alldifferent_except_0 constraint 285


8.1 The alldifferent_except_0 constraint . . . . . . . . . . . . . . . . . 285
8.2 Basic working of the solver: constraints . . . . . . . . . . . . . . . . . . . . . 286
8.3 Consistency in a nutshell . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286
8.4 A basic Constraint example: the XXX Constraint . . . . . . . . . . . 286
8.5 First approach: model the constraint . . . . . . . . . . . . . . . . . . . . . . . 288
8.6 The AllDifferent constraint in more details . . . . . . . . . . . . . . . . 288
8.7 Second approach: a custom Constraint . . . . . . . . . . . . . . . . . . . 288
8.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 288

III Routing 289


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

10 Vehicule Routing Problems with constraints: the capacitated vehicle routing prob-
lem 345
10.1 The Vehicle Routing Problem (VRP) . . . . . . . . . . . . . . . . . . . . . . 346
10.2 The VRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 353
10.3 The Capacitated Vehicle Routing Problem (CVRP) . . . . . . . . . . . . . . . 358
10.4 The CVRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 360
10.5 Multi-depots and vehicles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 366
10.6 Partial routes and Assigments . . . . . . . . . . . . . . . . . . . . . . . . . . 369
10.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 375

IV Technicalities 377
11 Utilities 379
11.1 Logging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
11.2 Asserting . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
11.3 Timing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 379
11.4 Profiling . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 381
11.5 Debugging . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.6 Serializing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.7 Visualizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.8 Randomizing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382
11.9 Using FlatZinc . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 382

12 Modeling tricks 383


12.1 Different types of variables . . . . . . . . . . . . . . . . . . . . . . . . . . . 383
12.2 Efficiency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
12.3 False friends and counter-intuitive ideas . . . . . . . . . . . . . . . . . . . . . 385
12.4 What are my solving options? . . . . . . . . . . . . . . . . . . . . . . . . . . 385
12.5 Customized search primitives . . . . . . . . . . . . . . . . . . . . . . . . . . 385
12.6 Local Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 385
12.7 Changing dynamically the improvement step with a SearchMonitor . . . . 385
12.8 Parallelizing with the or-tools library . . . . . . . . . . . . . . . . . . . . . . 385

13 Under the hood 387


13.1 Main files and directories . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
13.2 Naming conventions and programming idioms . . . . . . . . . . . . . . . . . 388
13.3 Main classes, structures and typedefs . . . . . . . . . . . . . . . . . . . . . . 388
13.4 The Trail struct . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
13.5 The Search class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
13.6 The Queue class . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
13.7 Variables and Assignments . . . . . . . . . . . . . . . . . . . . . . . . . . 388
13.8 SearchMonitors . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 388
13.9 Local Search (LS) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
13.10 Meta-heuristics and SearchMonitors . . . . . . . . . . . . . . . . . . . . 390
13.11 The Routing Library (RL) . . . . . . . . . . . . . . . . . . . . . . . . . . . . 390
13.12 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 395

V Appendices 397
Bibliography 399

Index 403
Part I

Basics
CHAPTER

ONE

INTRODUCTION TO CONSTRAINT
PROGRAMMING

In this chapter, we introduce Constraint Programming (CP) and the or-tools library and its core
principles. We also present the content of this manual.

Overview:

This chapter is divided in three parts. First, we introduce Constraint Programming by looking at
an example of the solving process of our CP solver in section 1.1. We give a brief definition of
Constraint Programming in section 1.2 and show some practical problems where CP stands out
in section 1.3. Second, we lay some practical and theoretical foundations for the whole manual.
A little bit of theory in section 1.4 provides some theoretical backgrounds with interesting
practical implications. If you don’t know how to tackle a problem, we introduce a (very)
simple strategy (the three-stage method in section 1.5) that can help you when confronted with
a problem to solve. This method will be applied repeatedly in this manual. Another recurrent
idea in this manual is to be aware of tradeoffs. This idea is the key to successful optimization
and well worth the whole section 1.6. Finally, we outline the general principles of the library
in section 1.7 and detail the content of this manual in section 1.8.

Prerequisites:

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

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. The 4-Queens Problem

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.
The following figure illustrates a solution to the 4-Queens Problem: none of the 4 queens can
capture each other.

Although this particular problem isn’t very impressive, keep in mind that you can generalize it
to n × n chessboards with n > 4.

1.1.2 A mathematical translation of the problem

In Constraint Programming we translate a real problem into a mathematical model with vari-
ables and constraints. Variables represent decisions and constraints restrain the variables from
taking arbitrary values. For instance, to model the 4-Queens Problem, we could use a binary
variable xij that indicates if a queen is present on the given (i, j) square (xij = 1) or not
(xij = 0). The first index i denotes the ith row and the second index j the j th column. We need
several constraints to model that no two queens can capture each other. To limit the number of
queens to 4, we can add the following constraint:
X
xij = 4.
(i,j)∈ squares

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

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
1
See section 5.1 for a more precise definition of this problem.
2
Things are a little bit more complex than that but let’s keep it simple for the moment. See subsection 1.4.2 for
more.
3
We don’t 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 filtering, pruning or consistency techniques.

4
Chapter 1. Introduction to constraint programming

those values to keep all the variables values compatible with the model. In Constraint
Programming, clever algorithms are devised to remove those values in an efficient man-
ner. These algorithms propagate the current state of the solver and removes incompatible
or undesirable values.
• backtracking: from time to time, the solver is stuck because it tried to assign some
values to some variables that are just not possible (or desirable) because they don’t respect
the constraints. The solver must then challenge its previous choices and try other values.
This is called backtracking. Backtracking also occurs when the solver finds a solution
but continues the search and tries to find another solution.
To better understand Constraint Programming, let’s have a look at a real solving process6 . In
the following Figures, crosses represent the action of removing values from variables’ domain.
Each step in the solving process is separated from the following one by an horizontal line.

The solver starts by placing the first queen in the upper left corner. Because of the model we
gave to the solver, it knows that there cannot be any other queen in the same column, hence the
gray crosses on the following Figure. One constraint tells the solver that there cannot be another
queen on the same diagonal with a negative slope (the diagonals going down then right). The
red crosses show this impossibility.

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

After this first step, only the white squares are still available to place the three re-
maining queens. The process of excluding some squares is what is called propagation.

The second step starts with the solver trying to place a second queen. It does so in the first
available square from the top in the second column. As in the first step, the solver knows that
no other queen can be placed in a column where it just placed a queen, hence the new gray
crosses in the next Figure.
6
You can find this search process detailed in sections 5.2 and 5.4.

5
1.1. The 4-Queens Problem

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.

Another constraint for the diagonals with positive slopes (diagonals going up then right) tells
the solver that no queen can be placed on the positive diagonal of second queen, hence the red
cross.

Now, we have a failure as there is no possibility to place a third queen in the third column:
there simply can not be a solution with this configuration. The solver has to backtrack!

The solver decides to challenge its last decision to place the second queen in the third row from
the top and places it in the fourth row.
The propagation is as follow:
First, the square with the red cross is removed because of the positive diagonal constraint. This
leaves only one possibility to place a queen in the fourth column.

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

6
Chapter 1. Introduction to constraint programming

This is of course not possible and the negative diagonal constraint tells the solver
that no queen can be on a negative diagonal from the fourth queen. Since there
is one, the solver concludes that there is a failure. It has to backtrack again!

First, it tries to challenge its last choice for the second queen but it detects that there are no
more other choices. The solver has to challenge its first choice to place the first queen in the
first row and places the first queen in the first column second row.
The propagation can now take place:
Two values are taken away because of the negative diagonal constraint:

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 squares marked by red crosses:

7
1.1. The 4-Queens Problem

The positive diagonal constraint kicks in and forbids the red square leaving no choice to place
a third queen in the third column first row.

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

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

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

8
Chapter 1. Introduction to constraint programming

1.2 What is constraint programming?

Constraint Programming (CP) is an emergent field in operations research7 . It is based on feasi-


bility (i.e. finding a feasible solution) rather than optimization (i.e. finding 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 scientific 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.3
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 find a feasible solution i.e. to assign values to the variables of
the model such that every constraint is satisfied. One of the most well-known such problem
is the Boolean SATisfiability Problem (boolean SAT). (See Wikipedia Constraint satisfaction
problem and Boolean satisfiability 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 field and we use some jargon. Don’t be
afraid if you don’t understand those asides and just read on.

Constraint Programming does optimization too!


When faced with an optimization problem, CP first finds a feasible solution x0 with an
objective value of z(x0 ). It then adds the constraint z(x) < z(x0 ) and tries to find a
feasible solution for this enhanced model.
The same trick is applied again and again until the addition of constraint z(x) < z(xi ) for
a feasible solution xi renders the model incoherent, i.e. there is no feasible solution for
this model. The last feasible solution xi 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 field has its origins in a number of fields including Artificial intelligence, Programming Languages,
Symbolic Computing, Computational Logic, etc. The first articles related to CP are generally dated from the
seventies but CP really started in the eighties. As with every new field, names, origins, etc. are not settled
and different people sometimes take different avenues. We carefully tried to use commonly accepted names,
techniques, etc.

9
1.2. What is constraint programming?

The ease to model a problem

If you are used to (non-)linear programming, you know how difficult it is to model some con-
straints (forcing them to be linear, use of big M for disjunctions, replacing one constraint by a
bunch of linear constraints, relying on the direction of optimization (minimizing or maximiz-
ing), etc.). None of this happens in CP because constraints can be any constraints. They even
don’t have to be numerical and can deal with symbolic variables! This allows to model your
problems in a very natural fashion.
One of the most well-known global constraints is the AllDifferent constraint. This con-
straint ensures that the variables have different values in a feasible solution. For instance
AllDifferent(t0 , t1 , t2 ) forces the three variables t0 , t1 and t2 to have different values. Say
that t0 , t1 and t2 can take the integer values in [0, 2].
Compare

AllDifferent(t0 , t1 , t2 )

to the classical way (see [Williams2001]) of translating this constraint in linear integer pro-
gramming for instance:

ti − 2j=0 jλij = 0
P
∀i
P2
λij = 1 ∀i
Pj=0
2
i=0 λij 6 1 ∀j

To model the AllDifferent(t0 , . . . , tn−1 ) constraint8 with ti ∈ [0, n − 1], we already need n2
auxiliary variables λij :

1 if ti takes value j
λij =
0 otherwise
and 3n linear equations!
Of course if AllDifferent(t0 , t1 , t2 ) was being replaced by its linear integer programming trans-
lation for instance, it would only be syntactic sugar but it is not. Specialized and efficient
propagation algorithms were (and are still!) developed to ensure t0 , t1 and t2 keep different
values during the search.
Numerous specialized and general global constraints exist. The Global Constraint Catalog
references 354 global constraints at the time of writing.
Because CP deals locally9 with each constraints, adding constraints, even on the fly (i.e. during
the search), is not a problem. This makes CP a perfect framework to prototype and test ideas:
you can change the model without changing (too much) your search strategy/algorithm.

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 efficient manner.
9
Propagation is done globally on all involved variables but the propagation is done constraint by constraint.
10
Basically, you only need to be able to propagate (hopefully efficiently) your constraints.

10
Chapter 1. Introduction to constraint programming

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

1.2.2 The search

Propagation is not enough to find a feasible solution most of the time. The solver needs to test
partial or complete assignments of the variables. The basic search algorithm (and the one im-
plemented in or-tools) is a systematic search algorithm: it systematically generates all possible
assignments one by one11 , trying to extend a partial solution toward a complete solution. If it
finds an impossibility along the way, it backtracks and reconsiders the last assignment (or last
assignments) as we have seen in the previous section.
There exist numerous refinements (some implemented in or-tools too) to this basic version.
The assignment possibilities define the search space12 . In our 4-queens example, the search
space is defined by all possible assignments for the 16 variables xij . For each of them, we have
2 possibilities: 0 or 1. Thus in total, we have 162 = 256 possibilities. This is the size of the
search space. It’s important to understand that the search space is defined by the variables and
their domain (i.e. the model) and not by the problem itself13 . Actually, it is also defined by the
constraints you added to the model because those constraints reduce the possibilities and thus
the search space14 .
The search algorithm visits systematically the whole search space. The art of optimization is
to model a problem such that the search space is not too big and such that the search algorithm
visits only interesting portions of the search space quickly15 .
When the solver has done its propagation and has not found a solution, it has to assign a value
to a variable16 . Say variable x21 . Because we don’t want to miss any portion of the search
11
See the section Basic working of the solver: the search algorithm for more details.
12
See next section for more.
13
In section Model, we will see a model with a search space of size 16 for the 4-queens problem.
14
Determining the exact (or even approximate) search space size is very often a (very) difficult problem by
itself.
15
Most of the time, we want good solutions quite rapidly. It might be more interesting to have a huge search
space but that we can easily visit than a smaller search space that is more difficult to scan. See the section It’s
always a matter of tradeoffs.
16
Or a bunch of variables. Or it can just restrict the values some variables can take. Or a combination of both
but let’s keep it simple for the moment: the solver assigns a value to one variable at a time.

11
1.3. Real examples

space, we want to visit solutions where x21 = 1 but also solutions where x21 = 0. This choice
is called branching. Most systematic search algorithms are called branch-and-something:
• branch and bound;
• branch and prune;
• branch and cut;
• branch and price;
• ...
In Constraint Programming, we use Branch and prune where pruning is another term for prop-
agation. You can also combine different techniques. For instance branch, price and cut.

CP for the MIP practitionersa


There are strong similarities between the two basic search algorithms used to solve an
MIP and a CSP.

MIP CSP
Branch and bound Branch and prune
Bound: Prune:
• Relax constraints • Propagate constraints

• Reduce gap • Reduce variable domains

Goal: Optimality Goal: Feasibility


View: Objective oriented View: Domain oriented
a
This is an aside for our MIP (Mix Integer Programming) colleagues. It’s full of jargon on purpose.

1.3 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 configuration.
Here is a non-exhaustive list17 where CP has been used with success:
• Production sequencing
• Production scheduling
• Satellite tasking
• Maintenance planning
17
This list is much inspired from the excellent documentation provided by Helmut Simonis under the Creative
Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.

12
Chapter 1. Introduction to constraint programming

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

1.3.1 Success stories

We could list hundreds of success stories were CP made a - sometimes huge - difference but
we don’t want to advertise any company. You’ll find plenty on the web. Let us just advertise
CP as a very efficient and convenient tool to solve industrial problems.

13
1.3. Real examples

Most of the time, CP18 :


• solves your problem within minutes;
• only needs the push of a button (after setting some parameters and providing some data
of course);
• is very flexible and easily allows you to add or remove side constraints;
• improve solutions found by hand by the experts by up to sometimes 30%.
All of this can only happen if you find 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 in-
stance.

1.3.2 Can CP be compared to the holy grail of Operations Re-


search?

From time to time, people search for the holy grail of Computer Science. We could define
it as the pursuit to solving arbitrary combinatorial optimization problems with one universal
algorithm. As E. Freuder (see [Freuder1997]) states it:
The user states the problem, the computer solves it.
For instance, David Abramson and Marcus Randall in their 1997 article (see [Abramson1997])
tried to apply Simulated Annealing19 to solve arbitrary combinatorial optimization problems20 .
Modeling languages ( AIMMS, AMPL, GAMS, Xpress-Mosel, etc) are yet another attempt at
engineering this universal algorithm. You write your model in a common algebraic/symbolic
language, often close to the mathematical language21 . It is then translated for an appropriate
solver of your choice. Some modeling languages even let you write high level algorithms.
One of the great advantages of modeling languages is the possibility to quickly prototype your
algorithm and to try it online (and for free!) with the NEOS server22 .
All these approaches don’t compare23 to dedicated algorithms tailored for a specific problem24 .
Until now, all these attempts have been vain. That said, CP - because of its particularity of
dealing with constraints locally25 - is probably the closest technique to the holy grail. Actually,
we didn’t cite E. Freuder fully (see [Freuder1997]):
Constraint Programming represents one of the closest approaches computer science has yet
made to the Holy Grail of programming: the user states the problem, the computer solves it.
18
This is common knowledge in the field.
19
You can learn more about Simulated Annealing (SA) in the section Simulated Annealing (SA).
20
This implies that any problem can be translated into a combinatorial problem!
21
See Wikipedia Algebraic languages.
22
The NEOS server proposes several state-of-the-art solvers. As stated on its website: “Optimization problems
are solved automatically with minimal input from the user. Users only need a definition of the optimization
problem; all additional information required by the optimization solver is determined automatically.”
23
Luckily, otherwise we would be jobless! ,
24 ?
Actually, this search for the holy grail is closely related to the famous P = NP question. If such algorithm
exists, then most probably P = NP. See the section Intractability.
25
See the subsection The ease to model a problem.

14
Chapter 1. Introduction to constraint programming

1.4 A little bit of theory

Here you will find some important ideas and the vocabulary we use throughout this manual.
As is often the case with theory, if you don’t have some first-hand experience with practice
(in this case mathematical optimization), you might find it difficult to follow the theoretical
abstraction and technicalities. To help you get mixed up, experts often mix terms and use
(wrongly formulated) shortcuts when they describe their theory. Optimization is certainly no
exception. We’ll try to be as clear as possible and use specific examples to illustrate the most
important concepts. We also try not to oversimplify too much but from time to time, we do
to give you a a simple formulation to a complex theory. In subsection 1.4.1, we cover the
basic vocabulary used to describe the problems we solve in CP. Section 1.4.4 is an informal
introduction to complexity theory26 . One of the difficulties of this theory is that there are lots
of technical details no to be missed. We introduce you to important ideas without being drawn
into too many details (some unavoidable details can be found in the footnotes).
Complexity theory is relatively new (it really started in the ’70s) and is not easy (and after
reading this section, you’ll probably have more questions than answers). If you are allergic to
theory, skip subsection 1.4.4. We are convinced - we took the time to write it, right? - that you
would benefit from reading this section in its entirety but it is up to you. You might want to
read the important practical implications of this complexity theory in subsection 1.4.4 though.

1.4.1 Constraint Satisfaction Problems (CSP) and Constraint Opti-


mization Problems (COP)

We illustrate the different components of a Constraint Satisfaction Problem with the 4-Queens
Problem we saw in section 1.1. A Constraint Satisfaction Problem (CSP) consists of
• a set of variables X = {x0 , . . . , xn−1 }.
For the 4-Queens Problem, we have a binary variable xij indicating the presence or not
of a queen on square (i, j):

X = {x00 , x01 , x02 , x03 , x10 , x11 , x12 , . . . , x33 }

• a domain for each variable xi , i.e. a finite set Di of possible values.


For the 4-Queens Problem, each variable xij is a binary variable, thus

D00 = D01 = . . . = D33 = {0, 1}.

• a set of constraints that restrict the values the variables can take simultaneously.
For the 4-Queens Problem, we have a set of constraints that forbid two queens (or more)
26
We talk here about Time-complexity theory, i.e. we are concerned with the time we need to solve problems.
There exist other complexity theories, for instance the Space-complexity theory where we are concerned with the
memory space needed to solve problems.

15
1.4. A little bit of theory

to share the same row:


row 0: x00 + x01 + x02 + x03 6 1
row 1: x10 + x11 + x12 + x13 6 1
row 2: x20 + x21 + x22 + x23 6 1
row 3: x30 + x31 + x32 + x33 6 1

Indeed, these constraints ensure that for each row i at most one variable xi0 , xi1 , xi2 or
xi3 could take the value 1. Actually, we could replace the inequalities by equalities
because we know that every feasible solution must have a queen on each row27 . Later, in
section 5.2, we will provide another model with different variables and constraints.
As we mentioned earlier, values don’t need to be integers and constraints don’t need to be
algebraic equations or inequalities28 .
If we want to optimize, i.e. to minimize or maximize an objective function, we talk about a
Constraint Optimization Problem (COP). The objective function can be one of the variables
of the problem or a function of some or all the variables. Most of the problems used in this
manual fall into this category. In this manual, we’ll discuss among others:
• Cryptarithmetic Puzzles (CSP)
• The Golomb Ruler Problem (COP)
• The n-Queens Problem (CSP)
• The Job-Shop Problem (COP)
• The Travelling Salesman Problems with time windows (COP)
• The Capacitated Vehicle Routing Problem (COP)

1.4.2 Problems, instances and solutions

We will not go into details about what a mathematical problem exactly is. As an example,
we met the n-Queens Problem in section 1.1. We would like to take the time to differenti-
ate mathematical problems from real problems though. Let’s take an example. Let’s say you
are a company and want to give your users the ability to view streets from the comfort of their
browsers as if they were physically present in remote locations. To provide such a service, you
might want to scour the globe with special cars and other mobile means to take some pictures.
This is a real problem from real life. How do you provide such a service? Mathematical opti-
mization to the rescue! Subdivide this real (and challenging!) problem into smaller ones and
translate them into (solvable) mathematical problems:
• Taking pictures? All and well but how do you actually do this? How many pictures
do you need to take? At what angle? How do you morph them together and minimize
distortion? Etc....
27
We have to cautious here about the exact definition of the n-Queens Problem. The version we talk about is
a CSP where we know that we have to place n queens on an n x n chessboard so that no two queens attack each
other.
28
Basically, the only requirement for a constraint in CP is its ability to be propagated. See chapter Custom
constraints: the alldifferent_except_0 constraint.

16
Chapter 1. Introduction to constraint programming

• How do you scour the streets of a city? At what speed? How close to pictured objects?
With how many cars? At what time of the day? After covering a street, where should the
car head next? What about turns? Etc....
• How do you keep such information on the servers? How do you match the pictures with
the correct locations? How do you respond to a user request such that she gets the most
relevant information as quickly as possible? Etc....
• ...
Believe it or not, you can translate those questions into mathematical problems. And these
(some of them at least) mathematical problems can be (and are!) solved with tools like the
or-tools library.
Mathematical problems are a translation of such real problems into mathematical terms. We’ll
see many of them in this manual. Interestingly enough, the or-tools CP solver doesn’t have a
Model class on its own as the model is constructed in/by the solver itself but you have some
classes that relate to the mathematical model the solver is trying to solve. Among them, you
have a ModelVisitor class that lets you visit the model and act upon it.
These mathematical problems are sort of theoretical templates. What you want to solve with a
computer are specific versions of a problem. For instance, the n-Queens problem with n = 37
or n = 6484, or a Travelling Salesman Problem on a particular graph representing the city of
New York. We call these practical materialization of a mathematical problem instances. You
try to find mathematical theoretical methods to solve mathematical problems and solve specific
instances on your computer29 . When you ask the CP solver to print a model, you are in fact
asking for a specific instance.
A solution of an instance is an assignment of the variables: each variable is assigned a value of
its domain. This doesn’t mean that a solution solves your problem: the assignment doesn’t need
to respect the constraints! A feasible solution to a CSP or a COP is a feasible assignment: ev-
ery variable has been assigned a value from its domain in such a way that all the constraints of
the model are satisfied. The or-tools CP solver uses the class Assignment to represent a so-
lution. As its mathematical counterpart, it can represent a valid (i.e. a feasible) solution or not.
When we have a feasible solution, we’ll talk about a Solution (SolutionCollector
and SolutionPool classes or the NextSolution() method for instance) in or-tools al-
though there is no Solution class. The objective value of a feasible solution is the value of
the objective function for this solution. In or-tools, the objective function is modeled by the
OptimizeVar class, i.e. it is a variable. An optimal solution to a COP is a feasible solution
such that there are no other solutions with better objective values. Note that an optimal solu-
tion doesn’t need to exist nor is it unique in general. For instance, we’ll see that the n-Queens
Problem or the Golomb Ruler Problem both possess several optimal solutions.
Let us emphasize that the or-tools CP-solver deals only with discrete and finite variables, i.e.
the values a variable can take are elements of a finite set. If you want to solve a continuous
problem, you need to discretize it, i.e. model your problem with variables over a finite domain.
29
Of course, things are never that simple. The difference between a mathematical problem and an instance
is not always that clear. You might want to solve generically the n-Queens Problem with a meta-algorithm for
instance, or the n-Queens Problem could be viewed as an instance of a broader category of problems.

17
1.4. A little bit of theory

1.4.3 Complexity theory for the hurried reader

If you prefer not to read the next section (or if you want a good preparation to read it!), we have
summarized its main ideas:
• problems are divided in two categories30 : easy (P problems) and hard (NP-Hard or
NP-Complete problems) problems. Hard problems are also called intractable31 and in
general we only can find approximate solutions for such problems32 . Actually, the ques-
tion of being able to find exact solutions to hard problems is still open (See the box “The
?
P = N P question” below);

1.4.4 Complexity theory in a few lines

Some problems such as the Travelling Salesman Problem (see chapter 9) are hard to solve33 :
no one could ever come up with an efficient algorithm to solve this problem. On the other
hand, other problems, like the n-Queens Problem (see chapter 5), are solved very efficiently34 .
In the ’70s, complexity experts were able to translate this fact into a beautiful complexity theory.

Hard to solve problems are called intractable problems. When you cannot solve an in-
tractable problem to optimality, you can try to find good solutions or/and approximate the
problem. In the ‘90s, complexity experts continued their investigation on the complexity of
solving problems and developed what is now known as the approximation complexity theory.
Both theories are quite new, very interesting and ... not easy to understand. We try the tour
the force to introduce the basics of the general complexity theory in a few lines. We willingly
kept certain technical details out of the way. These technical details are important and actually
without them, you can not construct a complexity theory.

Intractability

One of the main difficulties complexity experts faced in the ‘70s was to come up with a theo-
retical definition of the complexity of problems not algorithms. Indeed, it is relatively easy35
to define a complexity measure of algorithms but how would you define the complexity of a
problem? If you have an efficient algorithm to solve a problem, you could say that the problem
belongs to the set of easy problems but what about difficult problems? The fact that we don’t
know an efficient algorithm to solve these doesn’t mean these problems are really difficult.
Someone could come up one day with an efficient algorithm. The solution the experts came up
30
Stated like this, it sounds pretty obvious but this complexity theory is really subtle and full of beautiful (and
useful) results. Actually, most problems of practical interest belong to either categories but these two categories
don’t cover all problems.
31
Intractable problems are problems which in practice take too long to solve exactly, so there is a gap between
the theoretical definition (NP-Hard problems) and the practical definition (Intractable problems).
32
Technically, you could find an exact solution but you would not be able to prove that it is indeed an exact
solution in general.
33
Roughly, we consider a problem to be hard to solve if we need a lot of time to solve it. Read on.
34
The Travelling Salesman Problem is said to be NP-hard while (a version of) the n-Queens Problem is said to
be in P.
35
Well, to a certain degree. You need to know what instances you consider, how these are encoded, what type
of machines you use and so on.

18
Chapter 1. Introduction to constraint programming

with was to build equivalence classes between problems and define the complexity of a problem
with respect to the complexity of other problems (so the notion of complexity is relative not
absolute36 ): a problem A is as hard as a problem B if there exists an efficient transformation τ
that maps every instance b of problem B into an instance τ (b) = a of problem A such that if
you solve a, you solve b.
A B

τ
τ (b) = a b

Indeed, if there exists an efficient algorithm to solve problem A, you can also solve efficiently
problem B: transform an instance b into into an instance τ (b) = a of problem A and solve it
with the efficient algorithm known to solve problem A. We write B 6T A and say that problem
B reduces efficiently to problem A and that τ is an efficient reduction37 . The search for an
efficient algorithm is replaced by the search for an efficient reduction between instances of two
problems to prove “equivalent” complexities.
This main idea leads to a lot of technicalities:
• how to exactly measure the complexity of an algorithm?
• what is an efficient transformation/reduction?
• what are the requirements for such a reduction?
• ...
We don’t answer these interesting questions except the one on efficiency. We consider a re-
duction τ efficient if there exist a polynomial-time bounded algorithm (this refers to the first
question...) that can transform any instance b of problem B into an instance a of problem A
such that the solutions correspond. This also means that we consider an algorithm efficient if it
is polynomially time-bounded (otherwise the efficiency of the reduction would be useless).
The class of problems that can be efficiently solved is called P , i.e. the class of problems that
can be solved by a polynomial-time bounded algorithm3839 .
Some problems are difficult to solve but once you have an answer, it is quite straightforward
to verify that a given solution is indeed the solution of the problem. One such problem is the
36
Well, for... hard problems.
37
The T in 6T is in honor of Alan Turing. Different types of efficient reductions exist.
38
For technical reasons, we don’t compare problems but languages and only consider decision problems, i.e.
problems that have a yes/no answer. The Subset Sum Problem is such a problem. Given a finite set of integers,
is there a non-empty subset whose sum is zero? The answer is yes or no. By extension, we say an optimization
problem is in P , if its equivalent decision problem is in P . For instance, the Chinese Postman Problem (CPP) is
an optimization problem where one wants to find a minimal route traversing all edges of a graph. The equivalent
decision problem is ” Is it possible to find a feasible route with cost less or equal to k ? ” where k is a given
integer. By extension, we will say that the CPP is in P (we should rather say that the CPP is in P − optimization).
39
This discussion is really about theoretical difficulties of problems. Some problems that are theoretically easy
(such as solving a Linear System or a Linear Program) are difficult in practice and conversely, some problems that
are theoretically difficult, such as the Knapsack Problem are routinely solved on big instances.

19
1.4. A little bit of theory

Hamiltonian Path Problem (HPP). Given a graph, is there a path that visits each vertex exactly
once? Finding such a path is difficult but verifying that a given path is indeed an Hamiltonian
path, i.e. that it passes exactly once through each vertex, can be easily done. Problems for
which solutions are easy to verify, i.e. for which this verification can be done in polynomial
time, are said to be in the class N P 40 . P ⊂ N P because if you can find a solution in polynomial
time, you can also verify this solution in polynomial time (just construct it). Whether we have
equality or not between these two sets is one of the major unsolved theoretical questions in
?
Operations Research (see the box “The P = N P question” below).
Not all problems in NP seem to be of equal complexity. Some problems, such as the HPP are
as hard as any problem in NP. Remember our classification of the complexity of problems?
This means that every problem in NP can be transformed in polynomial time into the HPP. The
hardest problems of NP form the class of NP-Complete problems.

How can you prove that all problems in NP are reducible to a single problem?
Wait a minute. There is an infinite number of problems in NP, many of which are unknown
to us. So, how is it possible to prove that all problems in NP can be reduced to a single
problem?
This can done in two steps:
1. First, notice that the reduction is transitive. This means that if A 6T B and B 6T C
then A 6T C. Thus, if you have one problem Z such that all problems Ai in NP are
reducible to Z, i.e. Ai 6T Z, then to prove that all problems Ai in NP reduce to a
problem X, you just need to prove that Z reduces to X. Indeed, if Z 6T X then
Ai 6T Z 6T X a .
This only works if you can find such problem Z. Well, such problem has been found,
see next point.
The funny fact is that if X is in NP, then X 6T Z also. Such problems are called
NP-Complete and we just showed that if you can solve one problem in NP-Complete
efficiently, you can solve all the problems in NP efficiently!
2. Several researchers (like for example Cook and Levin in the early ‘70s, see
Wikipedia on the Cook-Levin Theorem), were able to prove that all problems in
NP are reducible in polynomial time to the Boolean Satisfiability Problem (SAT)
and this problem is of course in NP.
Proving that the SAT problem is NP-Complete is a major achievement in the com-
plexity theory (all existing proves are highly technical).
a
If you want to prove that a problem Y is NP-Hard (see below), take a problem that is NP-Complete,
like the HPP, and reduce it to your problem. This might sound easy but it is not!

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 figure summarizes the relations between the complexity classes41 we have seen as
most of the experts believe they stand, i.e. P 6= NP.
40
The abbreviation N P refers to non-deterministic polynomial time, not to non-polynomial.
41
Be aware that there are many more complexity classes.

20
Chapter 1. Introduction to constraint programming

NP−Hard

NP−Complete

NP

If P 6= NP

?
The P = N P question
The P versus NP problem is a major unsolved problem in Computer Science. Informally, it
asks whether every problem whose solution can be quickly verified by a computer (∈ NP)
can also be quickly solved by a computer (∈ P). It is one of the seven Millennium Prize
Problems selected by the Clay Mathematics Institute. The offered prize to the first team to
solve this question is $1,000,000!
In 2002 and 2012, W. I. Gasarch (see [Gasarch2002] and [Gasarch2012]) conducted a poll
?
and asked his colleagues what they thought about the P = N P question. Here are the
results:
Outcomea % %
(2002) (2012)
P 6= N P 61 83
P = NP 9 9
No idea 30 8
One possible outcome - mentioned by very few - is that this question could be... undecid-
able, i.e. there is no yes or no answerb !
a
We agglomerated all other answers into a category No idea although the poll allowed people to fully ex-
press themselves (some answered “I don’t care” for instance). The first poll (2002) involved 100 researchers
while the second one involved 152 researchers.
b
See Undecidable problem on Wikipedia.

If you are interested in this fascinating subject, we recommend that you read the classical book
Computers and Intractability: A Guide to the Theory of NP-Completeness from M. R. Garey
and D. S. Johnson (see [Garey1979]42 ).
42
This book was written in 1979 and so misses the last developments of the complexity theory but it clearly
explains the NP-Completeness theory and provides a long list of NP-Complete problems.

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

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 P 6= N P ). At least, you know that no one could ever come with an
efficient algorithm to solve it and that it is unlikely to happen soon. Thus, you are not able to
solve exactly “big” instances of your problem. What can you do?
Maybe there are special cases that can be solved in polynomial time? If you are not interested
in those cases and your instances are too big to be solved exactly, even with parallel and/or
decomposition algorithms, then there is only one thing to do: approximate your problem and/or
the solutions.
You could simplify your problem and/or be satisfied with an approximation, i.e. a solution that
is not exact nor optimal. One way to do this in CP is to relax the model by softening some
constraints43 . In a nutshell, you soften a constraint by allowing this constraint to be violated.
In a approximate solution where the constraint is violated, you penalize the objective function
by a certain amount corresponding to the violation. The bigger the violation, the bigger the
penalty. The idea is to find a solution that doesn’t violate too much the soft constraints in the
hope that such approximate solution isn’t that different from an exact or optimal solution44 .
Another possible research avenue is to use (meta-)heuristics: algorithms that hopefully return
good or even near-optimal solutions. Some of these algorithms give a guarantee on the quality
of the produced solutions, some don’t and you just hope for the best. You can also monitor
the quality of the solutions by trying to close the optimality gap as much as possible. Given an
instance, compute a lower bound α and an upper bound β on the optimal value z ∗ . So you know
that z ∗ ∈ [α, β]. Closing the optimality gap is trying to shrink this interval by producing better
upper and lower bounds. If you manage to produce solutions with objective values belonging
to this interval, you know how close (or not) these values are from the optimal value of your
instance.
As with exact optimization, an approximation complexity theory emerged. It started in the
‘90 and is now a mature domain that improves greatly our comprehension of what we can or
can not (theorically) compute. There is a whole zoo of complexity classes. Some problems
can be approximated but without any guarantee on the quality of the solutions, others can
be approximated with as much precision as you desire but you have to pay the price for this
precision: the more precision you want the slower your algorithms will be. For some problems
it is hard to find approximations and for others, it is even impossible to find an approximation
with any guarantee whatsoever on its quality!

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 blindly45 . When it comes to research, everything is permitted and your imagi-
nation is the limit.
43
For MIP practitioners, this is equivalent to Lagrangian Relaxation.
44
In the case of optimization, a solution that isn’t that different means a solution that has a good objective value,
preferably close to the optimum.
45
If you are allergic to this “academic” approach, you probably will be happy to know that we only use this
three-stage method in the first two parts of this manual.

22
Chapter 1. Introduction to constraint programming

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 influence 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 problems quite natu-
rally. For instance, if you need to ensure that some variables must hold different values, simply
use the AllDifferent constraint on these variables. Reification allows you to express some
constraints that are simply unavailable in other solving paradigms. Another strength of con-
straint 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 prob-
lem (for instance if you have to solve the problem everyday, your algorithm can not take one
month to provide a solution). Do you really need to solve the problem exactly? Or can you
approximate it?
This step is really critical and need to be carefully planned and executed.
Is this manual, we will focus on three questions:
• What is the goal of the problem we try to solve? What kind of solutions are we exactly
expected to provide?
• What are the decision variables? What are the variables whose values are crucial to
solve the problem?
• What are the constraints? Are our constraints suited to solve the problem at hand?

23
1.6. It’s always a matter of tradeoffs

1.5.2 Model

Again a difficult stage if not the most challenging part of the solving process. Modeling is
more of an art than anything else. With experience, you will be able to model more easily
and use known and effective tricks. If you are a novice in Operations Research/Constraint
Programming, pay attention to the proposed models in this manual as they involve a lot of
knowledge and subtleties. Do not be discouraged if you do not understand them at first. This
is perfectly normal. Take the time to read them several times until you master them.
When confronted with a new problem, you might not know what do to. We all face this situa-
tion. This is what research is all about!

1.5.3 Solve

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

1.6 It’s always a matter of tradeoffs

There is no universal algorithm or paradigm to solve every problem46 .


When confronted with a real problem to solve, we first 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
reflects the real problem and the need to keep the model simple enough to be able to solve
it efficiently. When developing algorithms, we are always making decisions among several
options, each with its advantages and disadvantages47 . Let’s say we are implementing our own
constraint with our own propagation algorithm (this is exactly what we will do in chapter 8).
We might develop a very clever filtering 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 filter the domains might be prohibitive. Maybe another - less clever - algorithm that
filters less variables would fit better and allow an overall quicker search because for instance the
search tree could be visited more quickly. Second, the clever filtering algorithm could disregard
some variables that other filtering algorithms or branching schemes are based on, i.e. the clever
algorithm is not so clever when it works in collaboration with others!
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).
46
At least, no one found one and with our actual knowledge, there is a strong suspicion that none exist (see
section 1.4.4).
47
Of course, we are talking about clever options.

24
Chapter 1. Introduction to constraint programming

CP and the or-tools library allow us to develop very quickly prototypes we can test, improve,
test, redesign, test, etc., you get the idea.

The good optimization researcher’s motto:


It’s 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:

It’s 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

The Google or-tools open source library was publicly released in September 2010. Since then
it has evolved to be a huge project containing more than 65K lines of code written in C++48 !
The constraint programming part - including the important vehicle routing engine - has the
lion’s share with approximatively 55K lines of code. That’s right, the or-tools library is really
a constraint programming library. More specifically, a finite-domain constraint programming
library49 .
Contrary to other constraint programming libraries, our goal is not to provide a complete set of
algorithms, constraints, etc. We use our code internally at Google and we adapt it to our needs.
From time to time we open source some parts and share it with the world. As our goal is to
solve specific (and sometimes really challenging problems), we use a generic and lightweight
approach. We believe that specialized problems need specialized code. Our library aims at
simplicity because simple code, when well-written, is efficient and can be easily adapted to
solve complex problems. As an example of this lightweight approach, we don’t really offer a
matrix API. If you look at the results of the 2014 MiniZinc challenge, it seems our approach is
not too bad50 .
That said, our library is quite robust and can be used to solve a trove of problems. If you are
missing a known constraint programming algorithm, ask us on the OR Tools Discussion group,
we might have what you are looking for in store or even surprise you and implement it if we
see a good fit for our needs.
48
Beside C++, you can use the library through SWIG in Python, Java, and .NET (using mono on non
Windows platforms).
49
Most constraint programming libraries and frameworks use finite domains. Actually, if you use a computer,
you must - in a way or another - use a finite domain.
50
Yes, our constraint solver can read FlatZinc models. Lear more about how to use the FlatZinc API in sec-
tion 11.9.

25
1.8. The content of the manual

1.8 The content of the manual

The manual is divided in four parts:


• Part I: Basics: a gentle introduction to the basic working of the library.
• Part II: Customization: real problems need customized search algorithms and this is
what the second part is all about. We explain the basic inner working of the solver and
its customization.
• Part III: Routing: we provide a general routing solver on top of our Constraint Program-
ming solver that can already solve numerous node and vehicle problems out of the box.
Its rich API provides a good basis to develop specialized routing algorithms including
for some arc routing problems.
• Part IV: Technicalities: we detail non-basic but useful techniques of the CP solver and
its inner working.
The appendices consist of a bibliography and an index.
Each chapter in the three first parts is illustrated by one typical problem except chapter 7 on
meta-heuristics where we try to solve previously seen problems. Each problem is explained
from scratch so you can follow even if you’ve never heard about them.

1.8.1 Part I: Basics

Chapter 2: First steps with or-tools: cryptarithmetic puzzles: We start by helping you down-
load and install the or-tools library. Be careful to know exactly what third-party libraries
you want to use with or-tools. We then use the very basic functionalities of the CP
solver. We’ll encounter the Solver class and use the integer variables IntVar. The
model used in this chapter is very simple and we’ll add basic algebraic equalities with the
help of MakeSum(), MakeProd(), MakeEquality() and AddConstraint().
The AllDifferent constraint will make its first apparence too. More importantly,
we’ll use a DecisionBuilder to define the search phase and launch the search with
NextSolution(). To conduct the search, we use SearchMonitors and collect
solutions with SolutionCollectors and Assigments. Finally, we’ll say a few
words about the way to pass read-only parameters to the solver and about the other avail-
able programming languages in or-tools (Python, Java and C#). Although this chapter
is a gentle introduction to the basic use of the library, it also focuses on some basic but
important manipulations needed to get things right. Don’t miss them!
Chapter 3: Using objectives in constraint programming: the Golomb Ruler Problem: In
this chapter, we not only look for a feasible solution but for an optimal solution, i.e.
a solution that optimizes an objective function. To solve the Golomb Ruler Problem,
we’ll try five different models and compare them two by two. To have an intuition of
the models passed to the solver and the progress of the search, we show you how to
inspect the model you constructed and how to collect some statistics about the search.
Several flags are available to tune the search, collect statistics, etc. We present some of
them and how to trigger them. To limit the search in some way, use SearchLimits.
As SearchLimits use custom made functions or methods, this will be our first (but

26
Chapter 1. Introduction to constraint programming

certainly not last) encounter with callbacks and functors. Two very useful techniques
to tighten a model are introduced: adding better bounds and breaking symmetries.
Finally, we explain how our CP solver optimizes while it basically “only” finds feasible
solutions.
Chapter 4: Reification: [TO BE WRITTEN]

1.8.2 Part II: Customization

Chapter 5: Defining search primitives: the n-Queens Problem: The or-tools CP solver is
quite flexible and comes with several tools (Decisions, DecisionBuilders, ...)
that we call search primitives. Some are predefined and can be used right out of the
box while others can be customized thanks to callbacks. You can also combine dif-
ferent search strategies. SearchMonitors allow you to guide the search thanks to
callbacks. DecisionBuilders and Decisions define the search tree. We explain
their mechanisms and how they are embedded in the main search algorithm of the CP
solver. We also show where exactly in this main search algorithm most of the callbacks
of the SearchMonitors are triggered. The presented algorithm is a simplified version
of the real algorithm but you’ll get a pretty clear idea of the real algorithm. To better
understand all these tools, we use the wonderful cpviz library to visualize the search tree
and the variable propagations. The basic branching in the search tree is done by selecting
variables, then selecting values these variables can or can not hold. We list the avail-
able branching strategies. Once you master all these basic search concepts, we show you
how to customize them, i.e. how to create your own search primitives. This chapter is
difficult but essential to understand the basic working of the CP solver. To reward your
efforts and struggles to master this chapter, we end it with some cool stuff about how
to break symmetries during the search (on the fly!) using SymmetryManagers and
SymmetryBreakers.
Chapter 6: Local Search: the Job-Shop Problem: Scheduling is one of the fields where con-
straint programming has been applied with great success. It is thus not surprising that
the CP community has developed specific tools to solve scheduling problems. In this
chapter, we introduce the ones that have been implemented in or-tools. To address dif-
ficult problems - like the job-shop problem - we make use of (meta-)heuristics. Local
search is a general framework to seek a better solution starting from an initial solu-
tion. We explain what local search is and show how it’s done in or-tools. We present
a simplified version of our local search algorithm but, again, you’ll have a pretty clear
idea of the real algorithm and where exactly the callbacks of the SearchMonitors
are triggered. LocalSearchOperators are the main actors: they are in charge to
find candidate solutions given an initial solution. We show how to construct your own
customized LocalSearchOperators and present the most interesting ones that are
already implemented in or-tools. The CP solver verifies the feasibility of all constructed
candidate solutions but if you know how to quickly disregard some candidate solutions
(because you know they are infeasible or not desirable), you can help the CP solver by
creating your own LocalSearchFilters. We’ll show you how and also present a
list of available LocalSearchFilers that you might want to use.
Chapter 7: Meta-heuristics: several previous problems: [TO BE WRITTEN]

27
1.8. The content of the manual

Chapter 8: Custom constraints: the alldifferent_except_0 constraint: [TO BE WRITTEN]

1.8.3 Part III: Routing

Chapter 9: Travelling Salesman Problems with constraints: the TSP with time windows:
This chapter is our first encounter with the Routing Library (RL) and what better problem
than the Travelling Salesman Problem (TSP) to introduce it? We overview the library
and the problems it can solve. We then delve into the specifics of the mathematical
model we use to represent all these problems: first the variables, then the constraints.
In particular, we’ll see the auxiliary graph that we use to model multiple depots. Every
calculation is done on the auxiliary graph and you just have to translate the solutions
back to your original nodes. We show you how to switch between our auxiliary graph
and your original graph. To solve the Routing Problems, we use Local Search. Several
specialized PathOperators are implemented and we show you how to create your
customized versions. We try to solve the TSPLIB instances. You can add “quantities”
along the arcs. This is done by adding Dimensions. The quantities can represent
goods, people, volumes, ... but also distances and times. We model time windows with
Dimensions for instance.
Chapter 10: Vehicule Routing Problems with constraints: the capacitated vehicle routing problem:
[TO BE WRITTEN]

1.8.4 Part IV: Technicalities

Chapter 11: Utilities: This chapter is about supplementary tools you can use to enhance your
work-flow with the or-tools library. We’ll cover:
• Logging:
• Asserting:
• Timing:
• Profiling:
• Debugging:
• Serializing:
• Visualizing:
• Randomizing:
• Using FlatZinc:
Chapter 12: Modeling tricks: [TO BE WRITTEN]
Chapter 13: Under the hood: [TO BE WRITTEN]

28
Chapter 1. Introduction to constraint programming

1.8.5 Appendices

In this last part of the manual, you’ll find a bibliography and an index.

1.9 Summary

In this introductory chapter, we covered quite a lot of material. We introduced a mathematical


problem, the 4-Queens Problem, and saw in details how the solver manages to find a solution to
this problem. We then presented a brief overview of constraint programming and its strengths
and weaknesses. The or-tools library essentially uses constraint programming. We also gave
you an overview of the beautiful complexity theory. And finally, we proposed a school book
approach to solve a real problem if you are stuck.

29
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, you’re good to go and you
should be able to find your way through the numerous examples provided with the library.

Overview:

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

Prerequisites:

• basic knowledge of C++.


• basic knowledge of Constraint Programming (see chapter 1).

Classes under scrutiny:

IntVar, AllDifferent, DecisionBuilder, SolutionCollector,


Assignment, SearchMonitor.

Files:

The files used in this chapter are:


• cp_is_fun1.cc: Our first example: a simple cryptarithmetic puzzle to show the basics.
2.1. Getting started

• cp_is_fun2.cc: Use of SolutionCollectors to collect some or all solutions.


• cp_is_fun3.cc: Use of the Google gflags library to parse command line parameters.
• cp_is_fun4.cc: Use of read-only solver parameters.

2.1 Getting started

You’ll find all what is needed to compile the or-tools library and its third-party libraries in the
wiki page Getting Started.
This wiki page is always up to date with the most current instructions on how to install the li-
brary, whether from binaries or when compiling the source code. You will also find instructions
on how to run the examples.
Pay attention to the fact that this wiki page (and thus the installation procedure) changes reg-
ularly and that you might have to reinstall the library and its third-party dependencies from
scratch from time to time. If you use the variables defined in our makefiles, and your code
doesn’t compile or cannot be linked after an update or reinstallation of the or-tools library, you
might have to adapt your own makefiles accordingly.

2.2 Running the tutorial examples

The tutorial examples are the examples provided with this manual. You can find them in doc-
umentation/tutorials/cplusplus . For the moment, we only provide a C++ version. For each
chapter in this manual (except for the first chapter and the last part of the manual), we provide
tutorial examples and a makefile to use them.

2.2.1 C++ examples

Compiling and linking

You can compile and link the tutorial examples by invoking:


make tutorials OR_TOOLS_TOP=/directory/to/the/Google/or-tools/directory

where /directory/to/the/Google/or-tools/directory is the absolute or rela-


tive directory to the or-tools library. On my disk, this is:
/home/username/.../or-tools-read-only

You can also fill in this variable inside the Makefile.


Instead of compiling (and linking) all tutorial examples at once, you can select a specific ex-
ample by invoking its target, like so:
• Windows:

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

make cp_is_fun1.exe OR_TOOLS_TOP=/directory/to/the/Google/...

• MacOS and Unix:


make cp_is_fun1 OR_TOOLS_TOP=/directory/to/the/Google/...

where cp_is_fun1 is the target for the example cp_is_fun1.

Executing

You can execute C++ examples just by running them:


• Windows:
cp_is_fun1

• MacOS and Unix:


./cp_is_fun1

Cleaning

To delete all generated files:


make local_clean

Don’t use
make clean

as you will erase the generated files for the whole library!

Don’t use make clean to delete the tutorial examples!

33
2.3. The cryptarithmetic puzzle problem and a first model

2.3 The cryptarithmetic puzzle problem and a first


model

Now that your system is up and running (if not, see section 2.1), let us solve a cryptarithmetic
puzzle with the help of the or-tools library. In this section, we describe the problem and propose
a first model to solve it. This model is by no means efficient but allows us a gentle introduction
to the library.

2.3.1 Description of the problem

A cryptarithmetic puzzle is a mathematical game where the digits of some numbers are rep-
resented by letters (or symbols). Each letter represents a unique digit. The goal is to find the
digits such that a given mathematical equation is verified1 .
Here is an example:
C P
+ I S
+ F U N
---------
= T R U E
One solution is C=2 P=3 I=7 S=4 F=9 U=6 N=8 T=1 R=0 E=5 because
2 3
+ 7 4
+ 9 6 8
---------
= 1 0 6 5

Ideally, a good cryptarithmetic puzzle must have only one solution2 . We derogate from this
tradition. The above example has multiple solutions. We use it to show you how to collect all
solutions of a problem.

2.3.2 How to solve the problem?

We follow the classical three-stage method described in section 1.5.

Describe

The first stage is to describe the problem, preferably in natural language. What is the goal of
the puzzle? To replace letters by digits such that the sum CP+IS+FUN=TRUE is verified.
What are the unknowns (decision variables)? The digits that the letters represent. In other
words, for each letter we have one decision variable that can take any digit as value.
1
This the mathematical term to specify that the equation is true.
2
Like the famous SEND + MORE = MONEY ... in base 10.

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

What are the constraints? The obvious constraint is the sum that has to be verified. But there
are other - implicit - constraints. First, two different letters represent two different digits. This
implies that all the variables must have different values in a feasible solution. Second, it is
implicit that the first digit of a number can not be 0. Letters C, I, F and T can thus not represent
0. Third, there are 10 letters, so we need at least 10 different digits. The traditional decimal
base is sufficient but let’s be more general and allow for a bigger base. We will use a constant
kBase. The fact that we need at least 10 digits is not really a CP constraint. After all, the base
is not a variable but a given integer that is chosen once and for all for the whole program3 .

Model

For each letter, we have a decision variable (we keep the same letters to name the variables).
Given a base b, digits range from 0 to b-1. Remember that variables corresponding to C, I, F
and T should be different from 0. Thus C, I, F and T have [1, b − 1] as domain and P, S, U, N,
R and E have [0, b − 1] as domain. Another possibility is to keep the same domain [0, b − 1] for
all variables and force C, I, F and T to be different from 0 by adding inequalities. However,
restraining the domain to [1, b − 1] is more efficient.
To model the sum constraint in any base b, we add the linear equation:
+ C·b + P
+ I·b + S
+ F · b2 + U·b + N
= T · b3 + R · b2 + U·b + E
The global constraint AllDifferent springs to mind to model that variables must all have
different values:
AllDifferent(C,P,I,S,F,U,N,T,R,E)

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 satisfies 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. In or-tools, we use the method
MakeAllDifferent().

Solve

At this stage of our discovery of the library, we will not try to find a good search strategy
to solve this model. A default basic strategy will do for the moment. Chapter ?? is entirely
devoted to the subject of search strategies.
3
We could have chosen the base as a variable. For instance, to consider such a question as: “What are the
bases for which this puzzle has less than x solutions?”

35
2.4. Anatomy of a basic C++ code

2.4 Anatomy of a basic C++ code

In this section, we code the model developed in section 2.3. We quickly scan through the code
and describe the basic constituents needed to solve a cryptarithmetic puzzle in C++. In the next
chapters, we will cover some of them in more details.

2.4.1 At a glance

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

2.4.2 Headers

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.4.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.4.7 but for the moment, let’s 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.
5
The main function does not lie inside the namespace operations_research, hence the use of the
operations_research identifier to call the function CPIsFun().

37
2.4. Anatomy of a basic C++ code

return 0;
}

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

2.4.5 Variables

To create the model, we first need to create the decision variables:


const int64 kBase = 10;
IntVar* const c = solver.MakeIntVar(1, kBase - 1, "C");
IntVar* const p = solver.MakeIntVar(0, kBase - 1, "P");
...
IntVar* const e = 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 [i, j] =
{i, i+1, . . . , j −1, j} and has a name name. It returns a pointer to an IntVar. The declaration
IntVar* const c may seem a little be complicated at first. It is easier to understand if read
from right to left: c is a constant pointer to an IntVar. We can modify the object pointed by
c but this pointer, because it is constant, always refers to the same object.

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 undefined
behavioura !
a
It is possible to bypass the undefined behaviour but you don’t know what the solver needs to do,
so keep your hands off of the object pointers! ;-)

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

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

(IntervalVar), sequence variables (SequenceVar) and variables to encapsulate objec-


tives (OptimizeVar).

2.4.6 Assert-like macros

It is always a good idea to program defensively. We use several assert-like macros defined in
the header logging.h to assert some expressions. We know that the base has to be greater
than or equal to 10, so we add a check for this:
// Check if we have enough digits
CHECK_GE(kBase, letters.size());

CHECK_GE(x,y) is a macro that checks if condition (x) >= (y) is true. If not, the pro-
gram is aborted and the cause is printed:
[23:51:34] examples/cp_is_fun1.cc:108: Check failed:
(kBase) >= (letters.size())
Aborted

You can find more about the assert-like macros in section 11.2.

2.4.7 Constraints

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

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

39
2.4. Anatomy of a basic C++ code

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 com-
plete. 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 first term of our cryptarith-
metic puzzle "kBase c + p", we call MakeBaseLine2():
IntVar* const term1 = MakeBaseLine2(&solver, c, p, kBase);

The function MakeBaseLine2() is defined 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

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

integer coefficients:
IntVar* const var1 = solver.MakeInt(...);
...
IntVar* const varN = solver.MakeInt(...);

std::vector<IntVar*> variables;
variables.push_back(var1);
...
variables.push_back(varN);

std::vector<int64> coefficients(N);
// fill vector with coefficients
...

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

41
2.4. Anatomy of a basic C++ code

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

2.4.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() << " " << "P=" << p->Value() << " "
<< "I=" << i->Value() << " " << "S=" << s->Value() << " "
<< "F=" << f->Value() << " " << "U=" << u->Value() << " "
<< "N=" << n->Value() << " " << "T=" << t->Value() << " "
<< "R=" << r->Value() << " " << "E=" << 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() doesn’t return a feasible solution per se. It all
depends of the involved DecisionBuilder. The solver considers any leaf of the search tree as a solution if it
doesn’t fail (i.e. if it is accepted by several control mechanisms). See the section Basic working of the solver: the
search algorithm for more details.

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

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.4.10 The end of the search

To finish the search, invoke:


solver.EndSearch();

This method ensures that the solver is ready for a new search and if you asked for a profile file,
this file is saved. You can find more about the profile file in section 2.6.2. What happens if
you forget to end the search and didn’t ask for a profile file? If you don’t ask the solver to start
a new search, nothing bad will happen. It is just better practice to finish the search with the
method EndSearch().
See also What is the difference between NewSearch() and Solve()?.

2.5 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 find the code in the file tutorials/chap2/cplusplus/cp_is_fun2.cc.

43
2.5. SolutionCollectors and Assignments to collect solutions

2.5.1 SolutionCollectors

The SolutionCollector class is one of several specialized SearchMonitors classes.


i.e. SolutionCollector inherits from SearchMonitors. SearchMonitors pro-
vides 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 first 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. Let’s say you would like to know what the value of variable c is in the
first solution found. First, you create a SolutionCollector:
FirstSolutionCollector* const first_solution =
solver.MakeFirstSolutionCollector();

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

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

first_solution->Add(c);

The method Add() simply adds the variable c to the SolutionCollector. The
variable c is not tied to the solver, i.e. you will not be able to retrieve its value by
c->Value() after a search with the method Solve().
To launch the search:
solver.Solve(db,first_solution);

After the search, you can retrieve the value of c like this:
first_solution->solution(0)->Value(c)

or through the shortcut:


first_solution->Value(0,c)

In both cases, the index 0 denotes the first solution found. If you find it odd to specify the
index of the first solution with a FirstSolutionCollector, don’t forget that the API is
intended for generic SolutionCollectors including the AllSolutionCollector.
Let’s use the AllSolutionCollector to store and retrieve the values of the 72 solutions:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the variables to the SolutionCollector
all_solutions->Add(letters);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);

// 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, let’s say you are interested to
know the value of the expression kBase * c + p. Just construct a corresponding variable
and add it to the SolutionCollector:
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the interesting variables to the SolutionCollector
all_solutions->Add(c);
all_solutions->Add(p);
// Create the variable kBase * c + p
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var();

45
2.5. SolutionCollectors and Assignments to collect solutions

// 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.5.2 Assignments

The or-tools library provides the class Assignment to store the solution (in parts or as a
whole). The class Assignment has a rich API that allows you to retrieve not only the values
of the variables in a solution but also additional information. You can also act on some of the
variables for instance to disable them during a search.
SolutionCollector* const all_solutions =
solver.MakeAllSolutionCollector();
// Add the interesting variables to the SolutionCollector
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var();
// Add it to the SolutionCollector
all_solutions->Add(v1);
...
DecisionBuilder* const db = ...
...
solver.Solve(db, all_solutions);

// 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 11.6, we’ll use it to serialize a solution.

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

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.6 Parameters

You’ll find the code in the file tutorials/chap2/cplusplus/cp_is_fun3.cc.


This section is divided in two parts. First, we show you how to use Google’s command line
flag library. Second, we explain how to pass parameters to the CP solver.

2.6.1 Google’s gflags

The Google’s flags library is quite similar to other command line flags libraries with the notice-
able difference that the flag definitions may be scattered in different files.
To define a flag, we use the corresponding macro. Google’s flags library supports six types:
• DEFINE_bool: Boolean
• DEFINE_int32: 32-bit integer
• DEFINE_int64: 64-bit integer
• DEFINE_uint64: unsigned 64-bit integer
• DEFINE_double: double
• DEFINE_string: C++ string
Each of them takes the same three arguments: the name of the flag, its default value, and
a help string. In file tutorials/cplusplus/chap2/cp_is_fun3.cc, we parse the
base value on the command line. We first include the corresponding header and define the flag
“base“ in the global namespace:
...
#include "base/commandlineflags.h"
...
DEFINE_int64(base, 10, "Base used to solve the problem.");
...

47
2.6. Parameters

namespace operations_research {
...

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 defined flags are accessible as normal variables with the prefix FLAGS_ prepended:
const int64 kBase = FLAGS_base;

To change the base with a command line argument:


./cp_is_fun3 --base=12

If you want to know what the purpose of a flag is, just type one of the special flags on the
command line:
• --help: prints all the flags
• --helpshort: prints all the flags defined in the same file as main()
• --helpon=FILE: prints all the flags defined in file FILE
• --helpmatch=S: prints all the flags defined in the files *S*.*
For other features and to learn more about this library, we refer you to the gflags documentation.

2.6.2 CP Solver‘s parameters

You’ll find the code in the file 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:

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

// Save profile in file


solver.ExportProfilingOverview("profile.txt");

We will see how to profile more in details in the section 11.4. The SolverParameters
struct mainly deals with the internal usage of memory and is for advanced users.

SearchMonitors

Second, you can use SearchMonitors. We have already seen how to use them to collect
solutions in section 2.5. Suppose we want to limit the available time to solve a problem. To
pass this parameter on the command line, we define a time_limit variable:
DEFINE_int64(time_limit, 10000, "Time limit in milliseconds");

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 section 7.8.

2.7 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 efficiency and your problem
is really difficult, we advise you to use C++ for meanly two reasons:
• C++ is faster than Python, Java or C# and even more importantly
• you can tweak the library to your needs without worrying about SWIG.
That said, you might not notice differences in time executions between the different languages.
We have tried to add syntactic sugar when possible, particularly in Python and C#. If you
aim for the ease of use to, for instance, prototype, Python or C# are our preferred languages.
Most methods are available in all four flavors with the following naming convention:

49
2.8. Summary

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() become
ObjectiveValue() or objectiveValue().

2.8 Summary

This chapter helped you make your first steps with the CP solver. We used the Cryptarithmetic
Puzzle Problem to illustrate the basic working of the solver. Basically, we saw how:
• a mathematical model is designed (variables and constraints through factory methods);
• the solver simplifies memory management (the solver takes ownership of (almost) all
objects and deletes them when it doesn’t need them anymore);
• the search is defined (through one (or more) DecisionBuilders);
• the solving process is launched (NewSearch, NextSolution() and
EndSearch() or Solve());
• solution values are collected (through SolutionCollectors);
• all or the best solution is collected (again through SolutionCollectors);
• solutions are stored (through SolutionCollectors and Assigments);
• parameters are passed to the solver (use of Google’s gflags on the command line, with
a SolverParameters struct given to a solver and through SearchMonitors that
guides the search).
These are the building blocks to use the CP solver. All these concepts were just touched and
will be detailed with care in the next chapters.

50
CHAPTER

THREE

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 find the best solution1 . Second, we
prove that this solution is indeed the best (or as good as any other feasible solution in case there
are multiple optimal solutions) by scouring (preferably implicitly) the complete search tree.

Overview:

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

Prerequisites:

• 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 don’t need to master
them. We do all the dirty work for you. In fact, you can completely skip them if you
wish. The basic ideas behind these sums are simple and are easy to follow.
1
How do we know we have a best solution? Only when we have proven it to be so! The two steps are
intermingled. So why do we speak about two steps? Because, most of the time, it is easy to find a best (good)
solution (heuristics, good search strategies in the search tree, ...). The time-consuming part of the search consist
in disregarding/visiting the rest of the search tree.
3.1. Objective functions and how to compare search strategies

• We introduce two kinds of variables in our modelizations: the marks of the ruler and the
differences between the marks.
• When written, this chapter improved each algorithm one section after the other. Since
then, the compiler has been partly rewritten and the algorithms now have different effi-
ciencies. You can read more about their updated efficiencies in the section First results
of the section Default search.

Classes under scrutiny:

AllDifferent, OptimizeVar, SearchLimit.

Files:

The files used in this chapter are:


• golomb1.cc: A first implementation. We show how to tell the solver to optimize an
objective function. We use the n(n−1)
2
differences as variables.
• golomb2.cc: Same file as golomb1.cc with some global indicators about the search
and the use of DebugString().
• golomb3.cc: Same file as golomb2.cc with some global statistics and the use of
SearchLimits.
• golomb4.cc: A second implementation. This time, we only use the marks as variables
and introduce the quaternary inequality constraints.
n(n−1)
• golomb5.cc: We improve the second implementation by reintroducing the 2
differ-
ences variables.
• golomb6.cc: In this third implementation, we replace the inequality constraints by the
more powerful globlal AllDifferent constraint.
• golomb7.cc: The last implementation is a tightening of the model used in the third im-
plementation. 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.

52
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

The objective function to minimize is the length of the Golomb ruler. You can see the objective
function as a variable: you want to find out what is the minimum or maximum value this
variable can hold for any given feasible solution.
Don’t worry if this is not all too clear. There are numerous examples in this manual and you
will quickly learn these concepts without even realizing it.
We search for the smallest Golomb ruler but we also want to do it fast2 . We will devise different
models and compare them. To do so, we will look at the following statistics:
• time: This our main criteria. The faster the better!
• failures: How many times do we need to backtrack in the search tree? Many failures
might be an indication that there exist better search strategies and/or models.
• branches: How many times do we need to branch? Faster algorithms tend to visit fewer
branches and better models tend to have smaller search trees.
Later, in chapter 5, we will devise different search strategies and compare them using the same
statistics.

3.2 The Golomb ruler problem and a first model

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

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 2 7 11
2 5 4
7
9
11

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


2
To be honest, if you really want to solve the Golomb Ruler Problem, you shouldn’t use CP as, until now, no
one found how to use CP in an efficient manner to solve this difficult problem.

53
3.2. The Golomb ruler problem and a first model

The Golomb ruler is {0, 2, 7, 11} and its length is 11. Because we are interested in Golomb
rulers with minimal length, we can fix the first mark to 0. Figure 3.2 illustrates an optimal
Golomb ruler of order 4 and all its - distinct - differences.
0 1 4 6
1 3 2
4
5
6

Figure 3.2: An optimal Golomb ruler of order 4.

Its length, 6, is optimal: it is not possible to construct a Golomb ruler with 4 marks with a length
smaller than 6. We denote this optimal value by G(4) = 6. More generally, for a Golomb ruler
of order n, we denote by G(n) its optimal value. The Golomb Ruler Problem (GRP) is to find,
for a given order n, the smallest Golomb ruler with n marks.
You might be surprised to learn that the largest order for which the experts have found an
optimal Golomb ruler so far is... 26. And it was a huge hunt involving hundreds of people!
The next table compares the number of days, the number of participants on the Internet and the
number of visited nodes in the search tree to find and prove G(24), G(25) and G(26)345 .
Orders Days Participants Visited nodes
24 1,572 41,803 555,551,924,848,254,200
25 3,006 124,387 52,898,840,308,130,480,000
26 24 2754 3,185,174,774,663,455
The search for G(27) started on February 24, 2009 and at that time was expected to take... 7
years! Still think it is an easy6 problem? You too can participate: The OGR Project.
You can find all the known optimal Golomb rulers and more information on Wikipedia.

Why Golomb Rulers?


Golomb rulers have a wide variety of applications, including radio astronomy and informa-
tion 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.
3
http://stats.distributed.net/projects.php?project_id=24
4
http://stats.distributed.net/projects.php?project_id=25
5
http://stats.distributed.net/projects.php?project_id=26
6
Although it is strongly suspected that the Golomb Ruler Problem is a very difficult problem, the computa-
tional complexity of this problem is unknown (see [Meyer-Papakonstantinou]).

54
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

Describe

What is the goal of the Golomb Ruler Problem? To find a minimal Golomb ruler for a given
order n. Our objective function is the length of the ruler or the largest integer in the Golomb
ruler sequence.
What are the decision variables (unknowns)? We have at least two choices. We can either view
the unknowns as the marks of the ruler (and retrieve all the differences from these variables)
or choose the unknowns to be the differences (and retrieve the marks). Let’s try this second
approach and use the efficient AllDifferent constraint. There are n(n−1)
2
such differences.
What are the constraints? Using the differences as variables, we need to construct a Golomb
ruler, i.e. the structure of the Golomb ruler has to be respected (see next section).

Model

For each positive difference, we have a decision variable. We collect them in an array Y. Let’s
order the differences so that we know which difference is represented by Y [i]. Figure 3.3
illustrates an ordered sequence of differences for a Golomb ruler of order 4.

1st
2nd
3rd
th
4
5th
6th

Figure 3.3: An ordered sequence of differences for the Golomb ruler of order 4.

We want to minimize the last difference in Y i.e. Y [ n(n−1)


2
− 1] since the first index of an array
4(4−1)
is 0. When the order is n = 4, we want to optimize Y [ 2 − 1] = Y [5] which represents the
6th difference. Instead of writing Y [i], we will also use the more convenient notation Yi .

Figure 3.4 illustrates the structures than must be respected for a Golomb ruler of or-
der 5. To impose the inner structure of the Golomb Ruler, we force Y4 = Y0 + Y1 , Y5 = Y1 + Y2
and so on as illustrated in Figure 3.4.
index
Y0 Y1 Y2 Y3
i=2 Y4 = Y0 + Y1 j=0
Y4 Y5 = Y1 + Y2 j=1
Y6 = Y2 + Y3 j=2
Y5
i=3 Y7 = Y0 + Y1 + Y2 j=0
Y6 Y8 = Y1 + Y2 + Y3 j=1
i=4 Y9 = Y0 + Y1 + Y2 + Y3 j=0
Y7
Y8

Y9

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

55
3.3. An implementation of the first model

An easy way to construct these equality constraints is to use an index index going from 4 to 97 ,
an index i to count the number of terms in a given equality and an index j to indicate the rank
of the starting term in each equality:
int index = n - 2;
for (int i = 2; i <= n - 1; ++i) {
for (int j = 0; j < n-i; ++j) {
++index;
Y[index] = Y[j] + ... + Y[j + i - 1];
}
}

Solve

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

3.3 An implementation of the first model

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

3.3.1 An upper bound

Several upper bounds exist on Golomb rulers. For instance, we could take n3 − 2n2 + 2n − 1.
Indeed, it can be shown that the sequence
Φ(a) = na2 + a 0 6 a < n.
forms a Golomb ruler. As its largest member is n3 − 2n2 + 2n − 1 (when a = n − 1), we have
an upper bound on the length of a Golomb ruler of order n:
G(n) 6 n3 − 2n2 + 2n − 1.
Most bounds are really bad and this one isn’t an exception. The great mathematician Paul Erdös
conjectured that
G(n) < n2 .
This conjecture hasn’t been proved yet but computational evidence has shown that the conjec-
ture holds for n < 65000 (see [Dimitromanolakis2002]).
This is perfect for our needs:
7
Or more generally from the index ofthe first difference
 that is the sum of two differences in our sequence
n(n−1)
(n − 1) to the index of the last difference 2 −1 .

56
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

CHECK_LT(n, 65000);
const int64 max = n * n - 1;

3.3.2 The first model

We can now define our variables but instead of creating single instances of IntVars like this:
const int64 num_vars = (n*(n - 1))/2;
std::vector<IntVar*> Y(num_vars);
for (int i = 0; i < num_vars; ++i) {
Y[i] = s.MakeIntVar(1, max, StringPrintf("Y%03d", i));
}

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 don’t provide the same result! MakeIntVarArray()
appends num_vars IntVar* to the std::vector with names Y_i where i
goes from 0 to num_vars - 1. It is a convenient shortcut to quickly cre-
ate an std::vector<IntVar*> (or to append some IntVar*s to an existing
std::vector<IntVar*>).
StringPrintf() (shown in the first example) is a helper function declared in the header
base/stringprintf.h that mimics the C function printf().
We use the AllDifferent constraint to ensure that the differences (in Y) are distinct:
s.AddConstraint(s.MakeAllDifferent(Y));

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

How do we tell the solver to optimize? Use an OptimizeVar to declare the objective func-
tion:
8
Remember the remark at the beginning of this chapter about the tricky sums!

57
3.4. What model did I pass to the solver?

OptimizeVar* const length = s.MakeMinimize(Y[num_vars - 1], 1);

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


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.3 was quite simple.
The first model proposed to solve the Golumb Ruler Problem in the two previous sections is
more complex. We suppose our model is theoretically correct. How do we know we gave the
right model to the solver, i.e. how do we know that our implementation is correct? In this sec-
tion, we present two tools to debug the model we passed to the solver: the DebugString()
method and via the default command line flags of the CP solver.

3.4.1 Inspect objects with DebugString()

You can find the code in the file 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.
Let’s 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) == Var<(Y_1(1..24) +
Y_0(1..24))>(2..48)
...: Y_5(1..24) == Var<(Y_2(1..24) +
Y_1(1..24))>(2..48)
...: Y_6(1..24) == Var<(Y_3(1..24) +
Y_2(1..24))>(2..48)
...: Y_7(1..24) == Var<(Y_2(1..24) +
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)

58
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

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

3.4.2 Use the default flags

By default, the CP solver is able to return some information about the model. If you try
./golomb1 --help

in the terminal, you get all possible command line flags. For the file
constraint_solver.cc, these are:
Flags from src/constraint_solver/constraint_solver.cc:
-cp_export_file (Export model to file using CPModelProto.)
type: string default: ""
-cp_model_stats (use StatisticsModelVisitor on model before solving.)
type: bool default: false
-cp_name_cast_variables (Name variables casted from expressions)
type: bool default: false
-cp_name_variables (Force all variables to have names.)
type: bool default: false
-cp_no_solve (Force failure at the beginning of a search.)
type: bool default: false
-cp_print_model (use PrintModelVisitor on model before solving.)
type: bool default: false
-cp_profile_file (Export profiling overview to file.)
type: string default: ""
-cp_show_constraints (show all constraints added to the solver.)
type: bool default: false
-cp_trace_propagation (Trace propagation events (constraint and demon
executions, variable modifications).)
type: bool default: false
-cp_trace_search (Trace search events)
type: bool default: false
-cp_verbose_fail (Verbose output when failing.)
type: bool default: false

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:

59
3.4. What model did I pass to the solver?

...: 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 Y1 + Y0 into an IntVar.


And then:
...: Y_4(1..24) == Var<(Y_1(1..24) +
Y_0(1..24))>(2..48)
...: Y_5(1..24) == Var<(Y_2(1..24) +
Y_1(1..24))>(2..48)
...: Y_6(1..24) == Var<(Y_3(1..24) +
Y_2(1..24))>(2..48)
...: Y_7(1..24) == Var<(Y_2(1..24) +
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 vari-


ables. Where does the rest come from?
To construct the equality constraints, we cast 10 times integer expressions into IntVar (re-
member 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 flags!

60
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 find the code in the file tutorials/cplusplus/chap3/golomb3.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 portable timer. This
timer starts to measure the time from the creation of the solver.
solver("TicTac") s; // Starts the timer of the solver.

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

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

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

3.5.2 Failures

A failure occurs whenever the solver has to backtrack whether it is because of a real failure,
a success or because the search is restarted or continued. There are two main statistics about
failures that you can retrieve like so:
Solver s("Global statistics");
LOG(INFO) << "Failures: " << s.failures() << std::endl;
LOG(INFO) << "Fail stamps: " << s.fail_stamp() << std::endl;

failures() returns the number of leaves in the search tree. This is exactly the number of
time the solver has to backtrack whenever it doesn’t find a solution or it does find a solution.

61
3.5. Some global statistics about the search and how to limit the search

Indeed, whenever a solution is found, the corresponding branch of the tree doesn’t need to grow
any further and the solver needs to backtrack to other branches to find other solutions.
fail_stamp() adds the failures counted by failures() with some additional internal
ones. These failures are specific to our algorithm and are not really important at this stage.

3.5.3 Branches

This one is easy as branches() returns exactly the number of branches in the search tree:
Solver s("I count my branches");
LOG(INFO) << "Branches: " << s.branches() << std::endl;

3.5.4 SearchLimits

When you try to solve a difficult problem, it might happen that the solver runs for a long time
without finding a solution or a good enough solution. In such cases, you might want to limit
the behaviour of the solver. Some other statistics can be bounded during the search.
The class SearchLimit permits to limit
• the time;
• the number of visited branches;
• the number of failures;
• the number of solutions found;
• another stopping criteria you define yourself.
You can limit these statistics for the whole search or for each sub-search where the solver tries
to find the next feasible solution.
If you are only interested in limiting one of these statistics, individual methods are provided:
SearchLimit* Solver::MakeTimeLimit(int64 time);
SearchLimit* Solver::MakeBranchesLimit(int64 branches);
SearchLimit* Solver::MakeFailuresLimit(int64 failures);
SearchLimit* Solver::MakeSolutionsLimit(int64 solutions);

These methods only apply to the individual sub-searches and each time a sub-search is started,
counters are set to 0 again.
As SearchLimits are SearchMonitors, you can pass them as arguments to the solver’s
NewSearch() or Solve() methods:
Solver s("Don’t want to fail too much");
...
SearchLimit * const fail_limit = s.MakeFailuresLimit(3);
DecisionBuilder * const db = s.MakePhase(...);
...
s.Solve(db, fail_limit, ...);

62
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

If you want to limit some statistics for the whole search, you can use the following method:
SearchLimit* Solver::MakeLimit(int64 time,
int64 branches,
int64 failures,
int64 solutions,
bool smart_time_check,
bool cumulative);

Setting smart_time_check to true entails that, for a certain number of calls, the real
time limit will not be tested9 . This is done to avoid the costly call to wall_time().
If you want the statistics to be tested globally, set cumulative to true. If you don’t
want to impose a limit say on the number of failures, pass the maximum number of failures
(kint64max). These limits are tested 10 in SearchMonitors BeginNextDecision()
and RefuteDecision() methods. You’ll learn about these two methods (and a bunch of
others) in section 5.3.3. These are the logical places to test for the number of branches, failures
and solutions but it is good to know if you want to implement your own custom limit.
SearchLimits can be combined:
Solver s("I combine my limits");
...
SearchLimit * const limit1 = s.MakeLimit(...);
SearchLimit * const limit2 = s.MakeLimit(...);
SearchLimit * const ORLimit = s.MakeLimit(limit1, limit2);

The ORLimit is reached when either of the underlying SearchLimit limit1 or limit2
is reached.
You also can implement your own SearchLimit with a callback:
SearchLimit* MakeCustomLimit(ResultCallback<bool>* limiter);

The ResultCallback<bool> is a functor that implements the Run() method returning a


bool. Returning true means the limit is reached. See section 13.3.3 for more on callbacks
and functors.

If the Run() method returns true at a leaf of the search tree, the corresponding
solution will be rejected!

In subsection 6.7.5 about solving the job-shop problem with local search, we will con-
struct our own SearchLimit. SearchLimits can also be updated during the search using
the following method:
void Solver::UpdateLimits(int64 time,
int64 branches,
int64 failures,
int64 solutions,
SearchLimit* limit);
9
See the method RegularLimit::TimeDelta() in file search.cc if you want to know the details.
10
To be precise, some limits are tested at some other places of the search algorithm depending on the type of
search.

63
3.6. A second model and its implementation

3.6 A second model and its implementation

Our first model is really bad. One of the reasons is that we use too many variables: n(n−1)
2
dif-
ferences. What happens if we only consider the n marks as variables instead of the differences?

3.6.1 Variables

You can find the code in the file tutorials/cplusplus/chap3/golomb4.cc. Before


we dive into the code, let’s be practical and ease our life a bit. One of the difficulties of the code
in golomb1.cc is that we use the first element of the array Y. There is no need to do so. In
golomb3.cc, we use X[1] as the first mark (and not X[0]). In the same vain, we redefine
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 doesn’t 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 doesn’t allow NULL pointers, we have to assign a value to X[0]. The first
mark X[1] is 0. We use again n2 − 1 as an upper bound for the marks:
// Upper bound on G(n), only valid for n <= 65 000
CHECK_LE(n, 65000);
const int64 max = n * n - 1;
...
for (int i = 2; i <= n; ++i) {
X[i] = s.MakeIntVar(1, max, StringPrintf("X%03d", i));
}

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

3.6.2 Constraints

To express that all the differences between all pairs of marks must be distinct, we use the
quaternary constraints11 :

X[j] − X[i] 6= X[l] − X[k] ∀ 1 6 i, k, j, l 6 n.

We don’t need all combinations of (i, j, k, l) with i 6= k and j 6= l. For instance, combination
(3, 2, 6, 4) and combination (2, 3, 4, 6) would both give the same constraint. One way to avoid
such redundancy is to impose an order on unique positive differences12 . Take again n = 4 and
define the sequence of differences as in Figure 3.5.
With this order defined on the differences, we can easily generate all the quaternary constraints.
Take the first difference and impose it to be different from the second difference, then to be
different from the third difference and so on as suggested in Figure 3.6. Take the second
11
Quaternary constraints is just a fancy way to say that the constraints each involves four variables.
12
In section Breaking symmetries with constraints we’ll use another trick.

64
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

1st
2nd
3rd
4th
5th
6th

Figure 3.5: Another ordered sequence of differences for the Golomb ruler of order 4.

1st 6= 2nd
6 = 3rd
6 = 4th
6 = 5th
6 = 6th

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 6= 3rd
6= 4th
6= 5th
6= 6th

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

We define a helper function that, given a difference (i, j) corresponding to an interval X[j] −
X[i] computes the next difference in the sequence:
bool next_interval(const int n, const int i, const int j, int* next_i,
int* next_j) {
CHECK_LT(i, n);
CHECK_LE(j, n);
CHECK_GE(i, 1);
CHECK_GT(j, 1);

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

65
3.6. A second model and its implementation

return true;
}

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

Note that we set the minimum value of the difference to 1, diff1->SetMin(1), to ensure
that the differences are positive and > 1. Note also that the method MakeDifference()
doesn’t allow us to give a name to the new variable, which is normal as this new variable is the
difference of two existing variables. Its name is simply name1 - name2.
Let’s compare the first and second implementation. The next table compares some global
statistics about the search for G(9).
Statistics Impl1 Impl2
Time (s) 4,712 48,317
Failures 51 833 75 587
Branches 103 654 151 169
Backtracks 51 836 75 590
If the first model was bad, what can we say about this one? What went wrong? The quaternary
constraints... These constraints are all disparate and thus don’t allow efficient propagation.

3.6.3 An improved version

You can find the code in the file tutorials/cplusplus/chap3/golomb5.cc. Let’s


improve our second model by using variables to denote the differences and define variables
Y[i][j] = X[j] - X[i]:
13
Remember again the remark at the beginning of this chapter about the tricky sums.

66
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

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 G(9):
Statistics Impl1 Impl2 Impl2+
Time (s) 4,712 48,317 1,984
Failures 51 833 75 587 53 516
Branches 103 654 151 169 107 025
Backtracks 51 836 75 590 53 519
Although we have more failures, more branches and we do backtrack more than in the first
model, we were able to divide the time by 2! Can we do better? You bet!

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 find the code in the file tutorials/cplusplus/chap3/golomb6.cc.


Let’s 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);
}

67
3.8. How to tighten the model?

}
s.AddConstraint(s.MakeAllDifferent(Y));

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


Statistics Impl1 Impl2 Impl2+ Impl3
Time (s) 4,712 48,317 1,984 0,338
Failures 51 833 75 587 53 516 7 521
Branches 103 654 151 169 107 025 15 032
Backtracks 51 836 75 590 53 519 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 find good nodes in the
search tree). We present two14 such techniques here. Breaking symmetries allows to disregard
entire subtrees in the search tree that wouldn’t bring any new information to the search
while bounding reduces the variable domains and thus reduces the number of branching and
augments the efficiency of the propagation techniques15 .

You can find the code in the file tutorials/cplusplus/chap3/golomb7.cc.

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));
14
There exist other techniques. Later, we will see how over-constraining can improve the search.
15
This short explanation is certainly too simple to describe all the subtleties of search strategies. After all,
modelling is an art!

68
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

k = next_k;
l = next_l;
}
}
}

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


the solver that X[1] < X[2] < ... < X[n]. Hadn’t we done so, there was no way the
solver could have guessed that the marks are in an increasing sequence16 . For the solver, the
solution

X[1] = 0, X[2] = 1, X[3] = 4, X[4] = 6 (3.1)

and the solution

X[1] = 4, X[2] = 1, X[3] = 6, X[4] = 0 (3.2)

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

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

0 1 4 6

6 5 2 0

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 first one but not the second one, just add X[2]-X[1] < X[n] - X[n-1]:
s.AddConstraint(s.MakeLess(s.MakeDifference(X[2],X[1])->Var(),
s.MakeDifference(X[n],X[n-1])->Var()));

Later on, in section 5.8, we will see how to provide some rules to the solver (by implementing
SymmetryBreakers) so that it generates itself the constraints to break symmetries. These
constraints are generated on the fly during the search!
16
Declaring variables in an std::vector doesn’t tell anything about their respective values!

69
3.8. How to tighten the model?

3.8.2 Better bounds helps

In all implementations, we used n2 − 1 as an upper bound on G(n). In the case of the Golomb
Ruler Problem, finding good upper bounds is a false problem. Very efficient techniques exist
to find optimal or near optimal upper bounds. If we use those bounds, we reduce dramatically
the domains of the variables. We can actually use G(n) as an upper bound for n 6 25 as these
bounds can be obtained by projective and affine projections in the plane17 .
The search can also benefit from lower bounds. Every difference must in itself be a Golomb
ruler. Thus Y[i][j] can be bounded by below by the corresponding optimal Golomb ruler.
In this section, we use a 2-dimensional array to collect the differences: Y[i][j] = X[j]
- X[i]:
std::vector<std::vector<IntVar *> > Y(n + 1,
std::vector<IntVar *>(n + 1));
for (int i = 1; i < n; ++i) {
for (int j = i + 1; j <= n; ++j) {
Y[i][j] = s.MakeDifference(X[j], X[i])->Var();
if ((i > 1) || (j < n)) {
Y[i][j]->SetMin(kG[j-i +1]); // Lower bound G(j - 1 + 1)
} else {
Y[i][j]->SetMin(kG[j-i] + 1); // Lower bound on Y[1][n] (i=1,j=n)
}
}
}

where kG[n] is G(n).


The AllDifferent constraint doesn’t take a 2-dimensional array as parameter but it is easy
to create one by flattening the array:
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 don’t change during the search. Dynamic bounds are even
better as they improve during the search and tighten the domains even more.
For instance, note that

Y [1][2] + Y [2][3] + ... + Y [i][j] + ... + Y [n − 1][n] = X[n]


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

70
Chapter 3. Using objectives in constraint programming: the Golomb Ruler
Problem

so

Y [i][j] = X[n] − {Y [1][2] + Y [2][3] + ... + Y [i − 1][i] + Y [j][j + 1] + ... + Y [n − 1][n]}

The differences on the right hand side of this expression are a set of different integers and there
are n − 1 − j + i of them. If we minimize the sum of these consecutive differences, we actually
maximize the right hand side, i.e. we bound Y [i][j] from above:

Y [i][j] 6 X[n] − (n − 1 − j + i)(n − j + i)/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));
}
}

Let’s compare our tightened third implementation with the rest, again to compute G(9):
Statistics Impl1 Impl2 Impl2+ Impl3 tightened Impl3
Time (s) 4,712 48,317 1,984 0,338 0,137
Failures 51 833 75 587 53 516 7 521 2288
Branches 103 654 151 169 107 025 15 032 4572
Backtracks 51 836 75 590 53 519 7 524 2291
The interested reader can find other dynamic bounds in [GalinierEtAl].

3.9 How does the solver optimize?

The main idea is simple. When you define an objective function, as in


tutorials/cplusplus/chap3/golom1.cc:
OptimizeVar* const length = s.MakeMinimize(Y[num_vars - 1], 1);

you basically tell the solver three things:


1. to minimize the objective function;
2. the objective function is defined by the variable Y[num_vars - 1] and
3. the improvement step is 1.
The last instruction ensures that each time the solver finds a feasible solution with a value z, it
will attempt to find a better solution with value z − 1 and so on until it cannot find a feasible
solution with a give value α. The optimal value is thus in this case α + 1.
You can give any (positive) integer value as an improvement step to the solver:
OptimizeVar* const obj = s.MakeMaximize(obj_var, 42);

71
3.10. Summary

but beware that by default, there isn’t any smart algorithm implemented in or-tools to change
this improvement step during the search. If the solver finds a solution with an objective value of
237, it will try next to find a solution with an objective value of 19518 . If it finds one, it will try
to find another solution with an objective value of 153 but if doesn’t find any feasible solution
with a value of 195, it will stop the search and return the “best” solution with a value of 237!
If you use an improvement step of 1, you can be sure to reach an optimal solution if you provide
the solver with enough time and memory but you could also devise your own algorithm to
change this improvement step during the search.

3.10 Summary

In this chapter, we used some statistics to compare different algorithms:


• time: This our main criteria. The faster the better!
• failures: How many times do we need to backtrack in the search tree? Many failures
might be an indication that there exist better search strategies and/or models.
• branches: How many times do we need to branch? Faster algorithms tend to visit fewer
branches and better models tend to have smaller search trees.
To compare these statistics, we introduced the Golomb Ruler Problem and presented 3 different
models of this problem. Different models are solved differently by the solver, and choosing a
good model can sometimes drastically speed up the search process. We saw how to test if the
model given to the solver is indeed our intended model with the help of the DebugString()
method and the use of some command line flags. We also saw how to break symmetries with
constraints to make the search tree smaller and gain important improvements in the overall
speed of the search algorithm. Finally, we saw how the Constraint Programming solver does to
optimize a problem by adding some additional constraints every time it finds a feasible solution.
A final word about the Golomb Ruler Problem. Despite its simplicity and inherent symmetry,
no one ever came with a clever algorithm. The best algorithms are not based on Constraint
Programming. For instance, we need 35.679 seconds to solve G(11) with our best tight-
ened implementation of our third model. This doesn’t compare favorably with the state of the
art (a few milliseconds on the same computer). Interested readers are invited to further read
[SmithEtAl] to learn more about some modeling tricks for this beautiful but difficult problem.

18
It took us a while but we are pretty sure that 237 − 42 = 195.

72
CHAPTER

FOUR

REIFICATION

Overview:

Overview...

Prerequisites:

Classes under scrutiny:

Files:

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


The files inside this directory are:

4.1 What is reification?

This chapter will be the last to be written.


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 flexible and comes with several tools (Decisions,
DecisionBuilders, ...) that we call search primitives. Some are predefined and can be
used right out of the box while others can be customized thanks to callbacks. You can also
combine different search strategies. If needed, you can define your own search primitive classes
by inheriting from base classes.
To efficiently use your tools, you need to know them a little and this chapter introduces you in
a gentle manner to the inner working of the solver. The covered material is enough for you to
understand how you can customize your search primitives without being drowned in the often
tedious details of the implementation1 . To illustrate the customization of the search, we try to
solve the n-Queen Problem that we have introduced in chapter 1.

Overview:

We first discuss the n-Queen Problem and present a basic model to solve it. To go one step
further and devise a search algorithm to solve this problem, we present a little bit the inner
working of the solver. Specifically, we show how it traverses and constructs the search tree.
We even can visualize the search thanks to the wonderful cpviz framework that we introduce
next. Equipped with this knowledge and visualization capacity, we can better understand out
of the box and customized primitives and apply them to solve the n-Queen Problem. We end
this chapter with a relatively advanced feature: SymmetryBreakers that allow to break
symmetries during the search (on the fly).

Prerequisites:

• Basic knowledge of C++.


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

• 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, DecisionVisitor, SearchMonitor,


TreeMonitor.

Files:

The files used in this chapter are:


• nqueens_utilities.h: Contains helper functions to test the number of solutions found and
to print a solution.
• nqueens1.cc: A first implementation of our basic model to find all solutions.
• nqueens2.cc: The same implementation as in nqueen1.cc but this time to find only
one solution.
• nqueens3.cc: The same implementation as in nqueen2.cc but this time we use a
TreeMonitor to visualize the search with the program cpviz.
• nqueens4.cc: The same implementation as in nqueen3.cc but with some added statis-
tics.
• 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 find the next variables and values to branch on among the predefined choices in the
IntVarStrategy and IntValueStrategy enums.
• nqueens5.cc: we create our own customized search strategies to find the next variables
and values to branch on with the help of callbacks.
• nqueens6.cc: We create our own customized search strategies to find the next variables
and values to branch on by defining our own DecisionBuilder class.
• nqueens7.cc: We use SymmetryBreakers to speed up the search.
• nqueens8.cc: We combine both approaches from nqueens6.cc and nqueens7.cc.

5.1 The n-Queens Problem

We have discussed the 4-Queens Problem (and defined its solutions) in chapter 1. We will now
generalize this problem to an arbitrary number n.

78
Chapter 5. Defining search primitives: the n-Queens Problem

5.1.1 The n-Queens 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 finding one solution for every n is... quite easy2 . Indeed, there exist
polynomial-time algorithms that compute a solution given a size n. For instance, Hoffman et
al. proposed a simple algorithm to return a solution of the n-queens problem [Hoffman1969].
This construction shows that there exist a solution with n queens for a n × n board (whenever
n > 4).
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:
• finding one solution3 ,
• counting the number of solutions and
• finding (explicitly) all these solutions.
While the first problem is easy, the two others are difficult4 .
As with the Golomb Rulers Problem, the experts could only find the number of all the solutions
for small values. The biggest number of queens for which we know precisely the number
of solutions is n = 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.

n: 1 2 3 4 5 6 7 8 9 10 11 12 13 14
unique: 1 0 0 1 2 1 6 12 46 92 341 1,787 9,233 45,752
distinct: 1 0 0 2 10 4 40 92 352 724 2,680 14,200 73,712 365,596
Notice that there are more solutions for n = 5 than n = 6. What about the last three known
values? Here there are:
n: 24 25 26
unique: 28,439,272,956,934 275,986,683,743,434 2,789,712,466,510,289
distinct: 227,514,171,973,736 2,207,893,435,808,352 22,317,699,616,364,044
Quite impressive, isn’t it? It’s even more impressive when you know that these numbers were
obtained by explicitly finding all these solutions!
You can learn much more about this problem and the best available techniques5 from the cur-
rent world record (n = 26) holder: the Queens@TUD team from the Technische Universität
Dresden: http://queens.inf.tu-dresden.de/?l=en&n=0.
2
In computer science jargon, we say that the problem of finding one solution for the n-Queens Problem is in
P . Actually, it’s the decision version of this problem but to keep it simple, let’s say that finding one solution is
straightforward and easy and shouldn’t take too long.
3
By solution, we mean feasible solution.
4
These two problems are NP-Hard. See [Jordan2009].
5
This time, backtracking and thus Constraint Programming are among the most efficient techniques. However,
to compute all the solutions for n = 26, there is no way a general purpose CP solver can compete with specialized
bitwise representations of the problem and massively parallel specialized hardware!

79
5.1. The n-Queens Problem

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 paral-
lel memory storage schemes, VLSI testing, traffic 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.

Describe

What is the goal of the n-Queens Problem? We will focus on finding one or all solutions. Given
a size n for the n × n chessboard, place n queens6 so that no two queens attack each other.
What are the decision variables (unknowns)? We have different choices. One clever way to
reduce the number of variables is to introduce only one variable for each queen. As we know
that we can have at maximum one queen per column (or row), we can use one variable per
column (or row) to denote the presence of a queen or not.
What are the constraints? No two queens can attack each other. This means to place n queens
on the chessboard such that no two queens are placed on the same row, the same column or the
same diagonal.

Model

We know that no two queens can be placed on the same column and that we have as much
queens as columns. We will use one variable to place one queen on each column. The value of
the variable will denote the row of the corresponding queen7 . Figure 5.1 illustrates the variables
we will use to solve the n-Queens Problem in this chapter.
The solution depicted is {x0 = 2, x1 = 0, x2 = 3, x3 = 1}. The fact that the queens cannot
be on the same column is directly encoded into the model without needing a constraint. The
domains of the variables ([0, n−1]) also ensure that every column will be populated by a queen.
We have to ensure that the variables cannot take the same value. This is easily done with
AllDifferent(x0 , . . . , xn−1 ). We have to ensure that no two queens can be on the same
diagonal. It would be nice to have the variables on the diagonals so that we could use again the
AllDifferent constraint. Actually, we know when two queens are on the same diagonal.
We’ll use a known trick to model this constraint in the next section.
6
In fact, for n = 2 and n = 3 there are no solution and we have seen that for every n > 4, there exist at least
a solution by the construction given in [Hoffman1969].
7
We start counting at 0, right?

80
Chapter 5. Defining search primitives: the n-Queens Problem

Figure 5.1: Variables to model the n-Queens Problem.

Solve

This time we will... test some search strategies. We will not devise a good search strategy
because we don’t know yet what possibilities are implemented in the CP solver. We will test
different search strategies and see what works and why.

5.2 Implementation of a basic model

You can find the code in the file tutorials/cplusplus/chap5/nqueens1.cc.

We have introduced the ideas of a basic model in the previous section. We complete
this model and implement it in this section.

5.2.1 The model completed

The model is defined in the NQueens() function. The beginning of the function shouldn’t
surprise you:
void NQueens(int size) {
CHECK_GE(size, 1);
Solver s("nqueens");

// model
std::vector<IntVar*> queens;
for (int i = 0; i < size; ++i) {
queens.push_back(s.MakeIntVar(0, size - 1,
StringPrintf("queen%04d", i)));
}
s.AddConstraint(s.MakeAllDifferent(queens));
...

This AllDifferent(x0 , . . . , xn−1 ) basically ensures no two queens remain on the same row
but we could have a solution like the one depicted on the Figure 5.2.
Of course, this is not what we want. To forbid two queens to be on the same diagonal with
slope +1 (diagonals that slope up-and-right), we could impose non-equality relations between

81
5.2. Implementation of a basic model

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

our variables. For instance, to impose that the first queen represented by x0 doesn’t attack any
other queen on those diagonals, we can impose that

x0 − 1 6= x1 , x0 − 2 6= x2 , x0 − 3 6= x3 , . . . (5.1)

(5.1) is equivalent to

x0 6= x1 + 1, x0 6= x2 + 2, x0 6= x3 + 3, . . . (5.2)

Take the second queen x1 . We only have to look for the queens to her right. To impose that x1
doesn’t attack any queen x2 , x3 , . . . on a diagonal with slope +1, we can add

x1 − 1 6= x2 , x1 − 2 6= x3 , x1 − 3 6= x4 , . . . (5.3)

or equivalently

x1 6= x2 + 1, x1 6= x3 + 2, x1 6= x4 + 3, . . . (5.4)

In general, for queen xi , we impose that xi 6= xj + j − i. Now, here comes the trick. If you add
1 to all members of (5.4), you get

x1 + 1 6= x2 + 2, x1 + 1 6= x3 + 3, x1 + 1 6= x4 + 4, . . . (5.5)

and more generally xi 6= xj + j − i becomes simply xi + i 6= xj + j ∀ j : j > i8 .


This means that we can restrict ourselves to inequalities only involving xi + i terms. Each
of these terms must be different from all others. Doesn’t this ring a bell? Yep, this is the
AllDifferent constraint:

AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3, x4 + 4, . . .) (5.6)

With a similar reasoning,

AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3, x4 − 4, . . .) (5.7)

ensures that no two queens are on the same diagonal with slope −1 (diagonals that slope down-
and-right).
We can thus add:

8
∀ j : j > i simply means that we consider all j greater than i.

82
Chapter 5. Defining search primitives: the n-Queens Problem

std::vector<IntVar*> vars(size);
for (int i = 0; i < size; ++i) {
vars[i] = s.MakeSum(queens[i], i)->Var();
}
s.AddConstraint(s.MakeAllDifferent(vars));
for (int i = 0; i < size; ++i) {
vars[i] = s.MakeSum(queens[i], -i)->Var();
}
s.AddConstraint(s.MakeAllDifferent(vars));

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

We keep 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. To help us print and test
solutions for the n-Queens Problem, we will use two helper functions defined next.

5.2.2 The helper functions

You can find the code in the file tutorials/cplusplus/chap5/nqueens_utilities.h.

The header nqueens_utilities.h contains some helper functions: 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 141. 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);
}

83
5.2. Implementation of a basic model

} else {
if (size - 1 < kKnownSolutions) {
CHECK_EQ(num_solutions, kNumSolutions[size - 1]);
} else if (!FLAGS_cp_no_solve) {
CHECK_GT(num_solutions, 0);
}
}

return;
}

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

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 The whole program bundled

To compile our NQueens() function, we need some headers.


After the needed headers from the or-tools library:

84
Chapter 5. Defining search primitives: the n-Queens Problem

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

we add our header:


#include "nqueens_utilities.h"

A boolean gflag FLAGS_use_symmetry allows or disallows the use of


SymmetryBreakers. This flag is defined in the header nqueens_utilities.h.
To be able to use it in our main file, we need to declare it:
DECLARE_bool(use_symmetry);

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

We offer the possibility to print the first solution (flag print set to true) or all solutions (flag
print_all set to true). By default, the program doesn’t output any solution.

5.2.4 First results

You can find the code in the file tutorials/cplusplus/chap5/nqueens2.cc.

Because finding all solutions is hard, we expect the solver to face more and more diffi-
culties as the size n grows but what about the easy problem of finding only one solution?
In the file nqueens2.cc, we stop the search as soon as a solution has been found.
The following Table collects the results of our experiment with the same DecisionBuilder
and same model as above. The results are given in seconds.
Problem 10 11 12 13 14
First solution 0 0 0 0 0,003
All Solutions 0,055 0,259 1,309 7,059 40,762

85
5.3. Basic working of the solver: the search algorithm

To find all solutions, the solver shows a typical exponential behaviour for intractable problems.
The sizes are too small to conclude anything about the problem of finding one solution. In the
next Table, we try bigger sizes. The results are again in seconds.
Problem 25 26 27 28 29 30 31
First solution 0,048 0,392 0,521 3,239 1,601 63,08 14,277
It looks like our solver has some troubles to find one solution. This is perfectly normal because
we didn’t use a specific search strategy. In the rest of this chapter, we will try other search
strategies and compare them. We will also customize our strategies, i.e. define strategies of our
own but before we do so, we need to learn a little bit about the basic working of the solver.
Wikipedia9 has a very nice animation of our search strategy (CHOOSE_FIRST_UNBOUND
and ASSIGN_MIN_VALUE) to find the first feasible solution:
https://en.wikipedia.org/wiki/Eight_queens_puzzle#Animation_of_the_recursive_solution
Compare the output of:
./nqueens1 -size=8 -print

with the solution of the animation in figure 5.3.

(a) Output of nqueens1 (b) Wikipedia animation

Figure 5.3: The first solution obtained with our default search strategy.

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.
9
https://en.wikipedia.org/wiki/Eight_queens_puzzle#Animation_of_the_recursive_solution

86
Chapter 5. Defining search primitives: the n-Queens Problem

In this section, we only discuss the main search algorithm.


We present a simplified version of the main search algorithm. Although far from being com-
plete, it gathers all the necessary basic elements and allows you to understand when some of
the callbacks of the SearchMonitors are called.

We describe a simplified 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 13 or the source code itself.

5.3.1 Basic definitions

Let’s agree on some wording we will use throughout this chapter and the rest of the manual.

Search trees

A search tree represents the search space that the search algorithm will, implicitly or explicitly,
traverse or explore. Each node of the tree corresponds to a state of the search. Take an array
of variables x[] and a valid index i. At one node in the search tree, we divide the search space
in two exclusive search subspaces by imposing x[i] = 2 at one branch and x[i] 6= 2 at another
branch like in Figure 5.4.

xi = 2 xi 6= 2

Figure 5.4: 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 doesn’t contain a feasible solution or if we find all
feasible solutions of a subtree. The first node is called the root node and represent the complete
search space.
When we divide the search space by applying a decision (x[i] = 2) in one branch and by
refuting this decision (x[i] 6= 2) in another, we obtain a binary search trees10 . This way of
dividing the search tree in two is basically the algorithm used by the CP solver to explore a
search tree.
The divide mechanism can be more complex. For instance by dividing a subspace in more
than two subspaces. The subspaces don’t need to be mutually exclusive, you can have different
numbers of them at each node, etc.
10
Not to be confused with a binary search tree (BST) used to store ordered sets.

87
5.3. Basic working of the solver: the search algorithm

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
don’t 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 don’t 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 redefine
those functions. There are several techniques available11 . In this section, we redefine some
virtual functions of an abstract class.
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 redefine 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. Don’t forget to delete your SearchMonitor after use. You
can also use a smart pointer or even better, let the solver take ownership of the object with the
RevAlloc() method (see subsection 5.7.1).
11
In design pattern jargon, we talk about the Template method design pattern. See
http://en.wikipedia.org/wiki/Template_method_pattern for more. And by the way, the name template has
nothing to do with C++ templates.

88
Chapter 5. Defining search primitives: the n-Queens Problem

Phases

The CP solver allows you to combine several searches, i.e. different types of sub-searches. You
can search a subtree of the search tree differently from the rest of your search. This is called
nested search while the whole search is called a top-level search. There are no limitations and
you can nest as many searches as you like. You can also restart a (top level or nested) search.
In or-tools, each time you use a new DecisionBuilder, we say you are in a new phase.
This is where the name MakePhase comes from.

5.3.2 The basic idea

The basic idea12 is very simple yet effective. A DecisionBuilder is responsible to return
a Decision at a node. A decision would be for instance, x[4] = 3. We divide the sub search
tree at this node by applying this decision (left branch: x[4] = 3) and by refuting this decision
(right branch: x[4] 6= 3).
At the current node, the DecisionBuilder of the current search returns a Decision.
The Decision class basically tells the solver what to do going left (Apply()) or right
(Refute()) as illustrated on the next figure.
Decision

Apply() Refute()

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

From the root node, we follow the left branch whenever possible and backtrack to the first
available right branch when needed. When you see a search tree produced by the CP solver,
you can easily track the search by following a preorder traversal of the binary search tree.

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: in-
order and post-order. We invite the curious reader to google pre-order traversal of a tree
to find more. There are a number of applets showing the different traversals.

There are basically two ways to ask the CP solver to find a solution (or solutions) as we
have seen in chapter 2. Either you configure SearchMonitors and you call the Solver‘s
Solve() method, either you use the finer grained NewSearch() - NextSolution() -
EndSearch() mechanism. In the first case, you are not allowed to interfere with the search
process while in the second case you can act every time a solution is found. Solve() is
implemented with this second mechanism:
12
The real code deals with a lots of subtleties to implement different variants of the search algorithm.

89
5.3. Basic working of the solver: the search algorithm

1 bool Solver::Solve(DecisionBuilder* const db,


2 SearchMonitor* const * monitors,
3 int size) {
4 NewSearch(db, monitors, size);
5 searches_.back()->set_created_by_solve(true); // Overwrites default.
6 NextSolution();
7 const bool solution_found = searches_.back()->solution_counter() > 0;
8 EndSearch();
9 return solution_found;
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 first solution found (if any).
You can overwrite this behaviour by implementing the AtSolution() callback of the
SearchMonitor class. If this method returns true, the search continues, otherwise the
solver ends it.

5.3.3 The basic search algorithm and the callback hooks for the
SearchMonitors

SearchMonitors contain a set of callbacks called on search tree events, such as enter-
ing/exiting search, applying/refuting decisions, failing, accepting solutions... In this section,
we present the callbacks of the SearchMonitor class13 listed in Table 5.1 and show you
exactly when they are called in the search algorithm.
We draw again your attention to the fact that the algorithm shown here is a simplified version
of the search algorithm. In particular, we don’t show how the nested searches and the restart of
a search are implemented. We find this so important that we reuse our warning box:

We describe a simplified version of the main loop of the search algorithm.

We use exceptions in our simplified version while the actual implementation uses the more
efficient (and cryptic) setjmp - longjmp mechanism.
We describe briefly what nested searches are in the section Nested searches but you will have
to wait until the chapter Under the hood and the section Nested searches to learn the juicy
13
There are a few more callbacks defined in a SearchMonitor. See 13.8.1

90
Chapter 5. Defining search primitives: the n-Queens Problem

Table 5.1: Basic search algorithm callbacks from the SearchMonitor class.
Methods Descriptions
EnterSearch() Beginning of the search.
ExitSearch() End of the search.
BeginNextDecision(DecisionBuilder* Before calling DecisionBuilder::Next().
const b)
EndNextDecision(DecisionBuilder* After calling DecisionBuilder::Next(),
const b, Decision* const d) along with the returned decision.
ApplyDecision(Decision* const d) Before applying the Decision.
RefuteDecision(Decision* const d) Before refuting the Decision.
AfterDecision(Decision* const d, Just after refuting or applying the Decision,
bool apply) apply is true after Apply(). This is called only
if the Apply() or Refute() methods have not
failed.
BeginFail() Just when the failure occurs.
EndFail() After completing the backtrack.
BeginInitialPropagation() Before the initial propagation.
EndInitialPropagation() After the initial propagation.
AcceptSolution() This method is called when a solution is found. It
asserts if the solution is valid. A value of false in-
dicates that the solution should be discarded.
AtSolution() This method is called when a valid solution is found.
If the return value is true, then search will resume.
If the result is false, then search will stop there.
NoMoreSolutions() When the search tree has been visited.

details14 .
To follow the main search algorithm, it is best to know in what states the solver can be. The
enum SolverState enumerates the possibilities in the following table:

Value Meaning
OUTSIDE_SEARCH Before search, after search.
IN_ROOT_NODE Executing the root node.
IN_SEARCH Executing the search code.
AT_SOLUTION After successful NextSolution() and before EndSearch().
NO_MORE_SOLUTIONS After failed NextSolution() and before EndSearch().
PROBLEM_INFEASIBLE After search, the model is infeasible.

NewSearch()

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

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

91
5.3. Basic working of the solver: the search algorithm

1 void Solver::NewSearch(DecisionBuilder* const db,


2 SearchMonitor* const * monitors,
3 int size {
4

5 Search* const search = searches_.back();


6 state_ = OUTSIDE_SEARCH;
7

8 // Init:
9 // Install the main propagation monitor
10 // Install DemonProfiler if needed
11 // Install customer’s SearchMonitors
12 // Install DecisionBuilder’s SearchMonitors
13 // Install print trace if needed
14 ...
15

16 search->EnterSearch(); // SEARCHMONITOR CALLBACK


17

18 // Set decision builder.


19 search->set_decision_builder(db);
20

21 state_ = IN_ROOT_NODE;
22 search->BeginInitialPropagation(); // SEARCHMONITOR CALLBACK
23

24 try {
25 // Initial constraint propagation
26 ProcessConstraints();
27 search->EndInitialPropagation(); // SEARCHMONITOR CALLBACK
28 ...
29 state_ = IN_SEARCH;
30 } catch (const FailException& e) {
31 ...
32 state_ = PROBLEM_INFEASIBLE;
33 }
34

35 return;
36 }

The initialization part consists in installing the backtracking and propagation mechanisms, the
monitors and the print trace if needed. If everything goes smoothly, the solver is in state
IN_SEARCH.

NextSolution()

The NextSolution() method returns true if if finds the next solution, false otherwise.
Notice that the statistics are not reset whatsoever from one call of NextSolution() to the
next one.
We present and discuss this algorithm below. SearchMonitor‘s callbacks are indicated by
the comment:
// SEARCHMONITOR CALLBACK

Here is how it might have looked in a simplified version of the main search algorithm:

92
Chapter 5. Defining search primitives: the n-Queens Problem

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

5 // Take action following solver state


6 switch (state_) {
7 case PROBLEM_INFEASIBLE:
8 return false;
9 case NO_MORE_SOLUTIONS:
10 return false;
11 case AT_SOLUTION: {// We need to backtrack
12 // SEARCHMONITOR CALLBACK
13 // BacktrackOneLevel() calls search->EndFail()
14 if (BacktrackOneLevel(&fd)) {// No more solutions.
15 search->NoMoreSolutions();// SEARCHMONITOR CALLBACKS
16 state_ = NO_MORE_SOLUTIONS;
17 return false;
18 }
19 state_ = IN_SEARCH;
20 break;
21 }
22 case OUTSIDE_SEARCH: {
23 state_ = IN_ROOT_NODE;
24 search->BeginInitialPropagation();// SEARCHMONITOR CALLBACKS
25 try {
26 ProcessConstraints();
27 search->EndInitialPropagation();// SEARCHMONITOR CALLBACKS
28 ...
29 state_ = IN_SEARCH;
30 } catch(const FailException& e) {
31 ...
32 state_ = PROBLEM_INFEASIBLE;
33 return false;
34 }
35 break;
36 }
37 case IN_SEARCH:
38 break;
39 }
40

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


42

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


44 volatile bool finish = false;
45 volatile bool result = false;
46

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


48 try {
49 // Explore right branch of the tree on backtrack
50 if (fd != NULL) {// We have a right branch
51 ...
52 search->RefuteDecision(fd);// SEARCHMONITOR CALLBACK
53 fd->Refute(this);
54 search->AfterDecision(fd, false);// SEARCHMONITOR CALLBACK
55 ...

93
5.3. Basic working of the solver: the search algorithm

56 fd = NULL;
57 }
58

59 // Explore left branches of the tree


60 Decision* d = NULL;
61 // Go left as often as possible
62 while (true) {// Trying to branch left
63 search->BeginNextDecision(db);// SEARCHMONITOR CALLBACK
64 d = db->Next(this);
65 search->EndNextDecision(db, d);// SEARCHMONITOR CALLBACK
66 // Dead-end? This is a shortcut
67 if (d == fail_decision_) {
68 search->BeginFail();// SEARCHMONITOR CALLBACK
69 // fail now instead of after 2 branches.
70 throw FailException();
71 }
72 // Explore next left branch of the tree
73 if (d != NULL) {
74 search->ApplyDecision(d);// SEARCHMONITOR CALLBACK
75 d->Apply(this);
76 search->AfterDecision(d, true);// SEARCHMONITOR CALLBACK
77 ...
78 } else {// No Decision left, the DecisionBuilder has finished
79 break;
80 }
81 }// while (true)
82

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


84 // SEARCHMONITOR CALLBACK
85 if (search->AcceptSolution()) {// Accept Solution
86 // SEARCHMONITOR CALLBACK
87 if (!search->AtSolution() || !CurrentlyInSolve()) {
88 result = true;
89 finish = true;
90 } else {
91 search->BeginFail();// SEARCHMONITOR CALLBACK
92 throw FailException();
93 }
94 } else {
95 search->BeginFail();// SEARCHMONITOR CALLBACK
96 throw FailException();
97 }
98 } catch (const FailException& e) {
99 // We must backtrack
100 // SEARCHMONITOR CALLBACK
101 // BacktrackOneLevel() calls search->EndFail()
102 if (BacktrackOneLevel(&fd)) { // no more solutions.
103 search->NoMoreSolutions();// SEARCHMONITOR CALLBACK
104 result = false;
105 finish = true;
106 }
107 }
108 }// while (!finish)
109

110 // Set solver current state

94
Chapter 5. Defining search primitives: the n-Queens Problem

111 ...
112 state_ = ...;
113

114 return result;


115 }

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

95
5.3. Basic working of the solver: the search algorithm

branch is a dead-end, it is allowed to return a FailDecision which the solver tests at line
67.
If the search tree is empty, the DecisionBuilder returns NULL. The solver tests this pos-
sibility on line 73. If the DecisionBuilder found a next Decision, it is applied on line
75.
Whenever the solver cannot find a next left branch to explore, it exits the while(true) loop.
We are now ready to test if we have found a feasible solution at the leaf of a left branch. This
test is done one line 85. The method AcceptSolution() decides if the solution is feasible
or not. After finding a feasible solution, the method AtSolution() decides if we continue
or stop the search.
You might recognize these two methods as callbacks of a SearchMonitor. These
two methods call the corresponding methods of all installed SearchMonitors no
matter what they return, i.e. you are guaranteed that all SearchMonitors will
be called. If one SearchMonitor has its method AcceptSolution() re-
turning false, search->AcceptSolution() returns false. On the con-
trary, if only one SearchMonitor has its AtSolution() method returning true,
search->AtSolution() returns true.
The test on line 87 is a little bit complex:
test = !search->AtSolution() || !CurrentlyInSolve()

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

96
Chapter 5. Defining search primitives: the n-Queens Problem

EndSearch()

The EndSearch() method cleans the solver and if required, writes the profile of the search
in a file. It also calls the ExitSearch() callbacks of all installed SearchMonitors.
Here is how it might have looked in a simplified version of the main search algorithm.
1 void Solver::EndSearch() {
2 Search* const search = searches_.back();
3 ...
4 search->ExitSearch();// SEARCHMONITOR CALLBACK
5 search->Clear();
6 state_ = OUTSIDE_SEARCH;
7 if (!FLAGS_cp_profile_file.empty()) {
8 LOG(INFO) << "Exporting profile to " << FLAGS_cp_profile_file;
9 ExportProfilingOverview(FLAGS_cp_profile_file);
10 }
11 }

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 finite domain constraint programming cpviz.
Here is a description from their website of what this toolkit provides:
It provides visualization for search trees, variables and global
constraints through a post-mortem analysis of trace logs.

The important trick to understand is that the visualization is only available after the search is
done.
Please find all necessary information and tools at:
http://sourceforge.net/projects/cpviz/

5.4.1 TreeMonitors to provide the cpviz input

You can find the code in the file tutorials/cplusplus/chap5/nqueens3.cc.

To monitor the search, we use SearchMonitors. To produce the files needed by


cpviz to visualize the search, we use a specialized SearchMonitor: the TreeMonitor
class. cpviz needs two files as input: tree.xml and visualization.xml.
To produce these two files, 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);

97
5.4. cpviz: how to visualize the search

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


<?xml version="1.0" encoding="UTF-8"?>
<configuration version="1.0" directory="/tmp"
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 files for the search tree (show="tree") and
the variables (show="viz") in the directory /tmp17 .
If you are really lazy, we even provide a factory method which generates automatically a default
configuration file:
SearchMonitor* const cpviz = s.MakeTreeMonitor(vars,
"configuration.xml",
"tree.xml",
"visualization.xml");

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

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

cpviz produces the construction of the search tree, step by step. In our case we try to solve the
n-Queens Problem with n = 4 and cpviz generates 8 files.
17
/tmp is a temporary directory in linux. Change this directory accordingly in you are working under another
OS.
18
tree.xml and visualization.xml are generated in the ExitSearch() callback of the
TreeMonitor class.

98
Chapter 5. Defining search primitives: the n-Queens Problem

This is probably not what you expected. First of all, this is not a binary tree and there seems to
be an extra dummy root node. A binary tree — which is what is exactly constructed during the
search — is not really suited for a graphical representation as it can quickly become very big
(compare the tree above with the actual search tree that is represented below). To avoid huge
trees, we have reduced their sizes by contracting several nodes. Except for the dummy root
node, each node corresponds to a variable during the search and only left branches are given
explicitly. The numbers along the branches denote the applied decisions (like x[1] = 2) and the
numbers in the right corner above the variable names of the nodes are the number of values left
in the domain of the corresponding variable just before the decision was taken. Nodes coloured
in
• green denote feasible solutions;
• red denote sub-trees without any feasible solutions;
• blue denote intermediate try nodes (these only exist during the search).

5.4.2 Interpreting the graphical results

You can find the code in the file tutorials/cplusplus/chap5/nqueens4.cc.

To better understand the output of cpviz and to follow the search with precision, let’s
trace the search and the propagation of our program nqueens4:
./nqueens4 --size=4 --cp_trace_search --cp_trace_propagation 2>
cpviz_nqueens4_basic.txt

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


We will transcribe the information contained in the file 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 finding all distinct solutions of the n-Queens
Problem with 4 queens. Our search strategy is to choose the first 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 x0 , x1 , x2 and x3 introduced in that
order. The 3 constraints are all AllDifferent constraints introduced in the following order:

AllDifferent(x0 , x1 , x2 , x3 )
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3)
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3)

The search tree

By reading the file cpviz_nqueens4_basic.txt, we can retrace the search and recon-
struct the search tree:

99
5.4. cpviz: how to visualize the search

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2

node 8
node 7
x1 = 0 x1 6= 0

node 10
node 9

Figure 5.6: The actual search tree of our search

As you can see, at each node, the solver took a Decision: the left branch to apply the
Decision and the right branch to refute this Decision. The leaf nodes in red denote sub-
trees that are not worth exploring explicitly: we cannot find any feasible solution along these
branches of the tree. The leaf nodes in green denote on the contrary feasible solutions. The
nodes are numbered in the order of creation and we can see that the search tree is traversed in
pre-order by the solver.
In the file nqeens4.cc, we have printed some statistics about the search:
std::cout << "Number of solutions: " << num_solutions << std::endl;
std::cout << "Failures: " << s.failures() << std::endl;
std::cout << "Branches: " << s.branches() << std::endl;
std::cout << "Backtracks: " << s.fail_stamp() << std::endl;
std::cout << "Stamps: " << s.stamp() << std::endl;

and with size = 4, we get as output:


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

Let’s see if we can deduce these statistics from the search tree. The three first statistics are easy
to spot in the tree:
Number of solutions (2): There are indeed two distinct solutions denoted by the
two green leafs.

100
Chapter 5. Defining search primitives: the n-Queens Problem

Failures (6): A failure occurs whenever the solver has to backtrack, whether it is
because of a real failure (nodes 2 − 3 and 9 − 10) or a success (nodes 5 and
7). Indeed, when the solver finds a solution, it has to backtrack to find other
solutions. The method failures() returns the number of leaves of the
search tree. In our case, 6.
Branches (10): Number of branches in the tree, indeed 10.
The two last statistics are more difficult to understand by only looking at the search
tree.
Backtracks (9): Because of the way the search is coded, the fail_stamp
counter starts already at 2 before any top level search. There are 6 failures
(one for each node, see Failures above) and this brings the counter to 8. To
end the search, a last backtrack19 is necessary to reach the root node and undo
the search which brings the counter to 9.
Stamps (29): This statistic is more an internal statistic than a real indicator of the
search. It is related to the queue actions during the search. The queue is
responsible for the propagation which occurs when one or more variables
domains change. Every time the propagation process is triggered, the stamp
counter is increased. Other queue actions also increase this counter. For
instance, when the queue is frozen. For a simple search, this statistic is more
or less equivalent to the length of a pre-order traversal of the search tree (20
in our case). This statistic reflects the amount of work needed by the solver
during the search. We refer the curious reader to the source code for more
details.

Our cpviz output of the search tree

How can we compare the real tree with our cpviz output? The trick is to observe the con-
struction of the tree one node at a time. We construct the real tree node by node from the tree
produced by cpviz. The left image is the cpviz output while the right image is the actual tree.

Step 0:

We start with a dummy node. This node is needed in our construction. You’ll see in a moment
why.

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

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

101
5.4. cpviz: how to visualize the search

Step 1:

node 0

(a) cpviz (b) Real search tree

Figure 5.8: Construction of the real search tree from the cpviz tree: step 1

Next, we start with the actual root node. As you can see in our
cpviz output, the dummy root node doesn’t even have a name and
the little number 0 next to this non existing name doesn’t mean
anything.

Step 2:

node 0

x0 = 0

node 1

(a) cpviz (b) Real search tree

Figure 5.9: Construction of the real search tree from the cpviz tree: step 2

You can see in our cpviz output that the solver has applied the Decision x0 = 0 but that
it couldn’t realize if this was a good choice or not. The little number 4 next to the variable
name x0 means that before the decision was applied, the number of values in its domain was 4.
Indeed: x0 ∈ {0, 1, 2, 3} before being assigned the value 0.

102
Chapter 5. Defining search primitives: the n-Queens Problem

Step 3:

node 0

x0 = 0

node 1

x1 = 2

node 2

(a) cpviz (b) Real search tree

Figure 5.10: Construction of the real search tree from the cpviz tree: step 3

After having applied the Decision x0 = 0 at step 2, the solver now applies the Decision
x1 = 2 which leads, after propagation, to a failure.

Step 4:

node 0

x0 = 0 x0 6= 0

node 1 node 4

x1 = 2 x1 6= 2

node 2 node 3

(a) cpviz (b) Real search tree

Figure 5.11: Construction of the real search tree from the cpviz tree: step 4

Our cpviz output now clearly warns that taking x0 = 0 does not lead to a feasible solution.
This can only mean that the solver tried also to refute the Decision x1 = 2. So we know that
the branch x1 6= 2 after the branch x0 = 0 is leading nowhere. We have to backtrack and to
refute the Decision x0 = 0. We have thus a new branch x0 6= 0 in the real search tree.

103
5.4. cpviz: how to visualize the search

Step 5:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2

node 2 node 3 node 5

(a) cpviz (b) Real search tree

Figure 5.12: Construction of the real search tree from the cpviz tree: step 5

We find a feasible solution when x0 = 1. Thus we add the branch x0 = 1 and indicate success.

Step 6:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 = 2

node 7

(a) cpviz (b) Real search tree

Figure 5.13: Construction of the real search tree from the cpviz tree: step 6

We find a second feasible solution when x0 = 2. Before we can proceed by applying


Decision x0 = 2, we first have to refute the Decision x0 = 1

104
Chapter 5. Defining search primitives: the n-Queens Problem

Step 7:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2

node 8
node 7

(a) cpviz (b) Real search tree

Figure 5.14: Construction of the real search tree from the cpviz tree: step 7

We add a tentative branch in the cpviz output. The branch before we applied the Decision
x2 = 0 that lead to a feasible solution, so now we know that the solver is trying to refute that
decision: x2 6= 0.

Step 8:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2

node 8
node 7
x1 = 0 x1 6= 0

node 10
node 9

(a) cpviz (b) Real search tree

Figure 5.15: Construction of the real search tree from the cpviz tree: step 8

The final step is the branch x1 = 0 that leads to a failure. This means that when we apply and
refute x1 = 0, we get a failure. Thus we know that x0 = 1 and x0 6= 1 both fail.

105
5.4. cpviz: how to visualize the search

Propagation

To better understand the search, let’s have a look at the propagation in details. First, we look at
the real propagation, then at our cpviz output.
You can find an animated version of the propagation here.
We start at the root node with
node 0: x0 ∈ {0, 1, 2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We apply the
Decision x0 = 0 which corresponds to our search strategy.

node 1: x0 ∈ {0}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3} The propagation is


done in the following order.

AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x1 : 1 , x2 : 2 , x3 : 3

x0 ∈ {0}, x1 ∈ {0, 2, 3}, x2 ∈ {0, 1, 3}, x3 ∈ {0, 1, 2}

AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 0 , x2 : 0 , x3 : 0

x0 ∈ {0}, x1 ∈ {2, 3}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. No more propagation is possible. We
then apply the Decision x1 = 2

node 2: x0 ∈ {0}, x1 ∈ {2}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. The propagation is as follow:

AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x2 : 3

106
Chapter 5. Defining search primitives: the n-Queens Problem

x0 ∈ {0}, x1 ∈ {2}, x2 ∈ {1}, x3 ∈ {1, 2}.

AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x2 : 1

x0 ∈ {0}, x1 ∈ {2}, x2 ∈ ∅, x3 ∈ {1, 2}. We have a failure as the domain of x2 is empty.


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

node 3: x0 ∈ {0}, x1 ∈ {3}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. x1 is fixed to 3 because we removed
the value 2 of its domain (refuting the Decision x1 = 2).
Propagation:

AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x3 : 1

x0 ∈ {0}, x1 ∈ {3}, x2 ∈ {1, 3}, x3 ∈ {2}.

AllDifferent(x0 , x1 , x2 , x3 ) :
x2 : 3

107
5.4. cpviz: how to visualize the search

x0 ∈ {0}, x1 ∈ {3}, x2 ∈ {1}, x3 ∈ {2}.


This is of course not possible and the following propagation detects this impossibility:
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x2 : 1
x0 ∈ {0}, x1 ∈ {3}, x2 ∈ ∅, x3 ∈ {2}. We have again a failure as the domain of x2 is
empty. We need to backtrack to the root node and refute the Decision x0 = 0.

node 4: x0 ∈ {1, 2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We apply
Decision x0 = 1 which complies with our search strategy.

node 5: x0 ∈ {1}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. Propagation:


AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x1 : 2 , x2 : 3

x0 ∈ {1}, x1 ∈ {0, 1, 3}, x2 ∈ {0, 1, 2}, x3 ∈ {0, 1, 2, 3}.


AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x1 : 0

108
Chapter 5. Defining search primitives: the n-Queens Problem

x0 ∈ {1}, x1 ∈ {1, 3}, x2 ∈ {0, 1, 2}, x3 ∈ {0, 1, 2, 3}.

AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 1 , x2 : 1 , x3 : 1

x0 ∈ {1}, x1 ∈ {3}, x2 ∈ {0, 2}, x3 ∈ {0, 2, 3}.

AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x2 : 2

x0 ∈ {1}, x1 ∈ {3}, x2 ∈ {0}, x3 ∈ {0, 2, 3}.

AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 3

x0 ∈ {1}, x1 ∈ {3}, x2 ∈ {0}, x3 ∈ {0, 2}.

AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 0

109
5.4. cpviz: how to visualize the search

x0 ∈ {1}, x1 ∈ {3}, x2 ∈ {0}, x3 ∈ {2}.


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

node 6: x0 ∈ {2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We apply the
Decision x0 = 2.

node 7: x0 ∈ {2}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. Propagation:

AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x1 : 3

x0 ∈ {2}, x1 ∈ {0, 1, 2}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}.

AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x1 : 1 , x2 : 0

x0 ∈ {2}, x1 ∈ {0, 2}, x2 ∈ {1, 2, 3}, x3 ∈ {0, 1, 2, 3}.

AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 2 , x2 : 2 , x3 : 2

110
Chapter 5. Defining search primitives: the n-Queens Problem

x0 ∈ {2}, x1 ∈ {0}, x2 ∈ {1, 3}, x3 ∈ {0, 1, 3}.


AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x2 : 1

x0 ∈ {2}, x1 ∈ {0}, x2 ∈ {3}, x3 ∈ {0, 1, 3}.


AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 0

x0 ∈ {2}, x1 ∈ {0}, x2 ∈ {3}, x3 ∈ {1, 3}.


AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 3

111
5.4. cpviz: how to visualize the search

x0 ∈ {2}, x1 ∈ {0}, x2 ∈ {3}, x3 ∈ {1} and we have a second distinct solution! We


backtrack to node 6 and refute Decision x0 = 2.

node 8: x0 ∈ {3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. x0 is fixed because
there is only one value left in its domain.
Propagation:
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x1 : 2 , x2 : 1 , x3 : 0

x0 ∈ {3}, x1 ∈ {0, 1, 3}, x2 ∈ {0, 2, 3}, x3 ∈ {1, 2, 3}.


AllDifferent(x0 , x1 , x2 , x3 ) :
x1 : 3 , x2 : 3 , x3 : 3

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

node 9: x0 ∈ {3}, x1 ∈ {0}, x2 ∈ {0, 2}, x3 ∈ {1, 2}. Propagation:


AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x3 : 2

112
Chapter 5. Defining search primitives: the n-Queens Problem

x0 ∈ {3}, x1 ∈ {0}, x2 ∈ {0, 2}, x3 ∈ {1}.


AllDifferent(x0 , x1 , x2 , x3 ) :
x3 : 0

x0 ∈ {3}, x1 ∈ {0}, x2 ∈ {2}, x3 ∈ {1} which is impossible as the next propagation


shows:
AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x2 : 2
x0 ∈ {3}, x1 ∈ {0}, x2 ∈ ∅, x3 ∈ {1}. As the domain of x2 is empty, we have failure
and have to backtrack to node 8 and refute Decision x1 = 0.

node 10: x0 ∈ {3}, x1 ∈ {1}, x2 ∈ {0, 2}, x3 ∈ {1, 2}. Propagation:


AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x2 : 2

x0 ∈ {3}, x1 ∈ {0}, x2 ∈ {0}, x3 ∈ {1, 2}.


AllDifferent(x0 , x1 + 1, x2 + 2, x3 + 3) :
x2 : 0
x0 ∈ {3}, x1 ∈ {0}, x2 ∈ ∅, x3 ∈ {1, 2}. The empty domain for x2 indicates a failure
and we have to backtrack... to the root node as we have exhausted the search tree. The
search is thus finished 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

113
5.4. cpviz: how to visualize the search

nodes in the tree, we are constrained to display very little information about the propagation
process. In short, if we find
• a try node, we display the final propagation at this node;
• a solution, we display the solution;
• a failure, we display the first failure encountered and the values of the assigned variables.
We also display what variable we focus on next.
Let’s go again through the 9 steps. We display in the left col-
umn our cpviz tree output, in the middle column the actual search
tree and in the right column our cpviz output of the propagation.

Step 0:

(a) cpviz (b) cpviz propagation’s output

Figure 5.16: cpviz output of the propagation: step 0

Nothing happens as we add a dummy root node. Notice that the variables are numbered from
1 to 4.

114
Chapter 5. Defining search primitives: the n-Queens Problem

Step 1:

node 0

(a) cpviz tree (b) Real search tree (c) cpviz propagation

Figure 5.17: cpviz output of the propagation: step 1

The yellow rectangle tells us that the focus is on variable 1(x0 ), which means that at the next
step a value will be assigned to this variable.

Step 2:

node 0

x0 = 0

node 1

(a) cpviz tree (b) Real search tree (c) cpviz propagation

Figure 5.18: cpviz output of the propagation: step 2

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

115
5.4. cpviz: how to visualize the search

Step 3:

node 0

x0 = 0

node 1

x1 = 2

node 2

(a) cpviz tree (b) Real search tree (c) cpviz propagation

Figure 5.19: cpviz output of the propagation: step 3

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

Step 4:

node 0

x0 = 0 x0 6= 0

node 1 node 4

x1 = 2 x1 6= 2

node 2 node 3

(a) cpviz tree (b) Real search tree (c) cpviz propagation

Figure 5.20: cpviz output of the propagation: step 4

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

116
Chapter 5. Defining search primitives: the n-Queens Problem

Step 5:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2

node 2 node 3 node 5

(a) cpviz tree (b) Real search tree (c) cpviz propagation

Figure 5.21: cpviz output of the propagation: step 5

Solution found.

Step 6:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 = 2

node 7

(a) cpviz tree (b) Real search tree (c) cpviz propagation

Figure 5.22: cpviz output of the propagation: step 6

Solution found.

117
5.4. cpviz: how to visualize the search

Step 7:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2

node 8
node 7

(a) cpviz tree (b) Real search tree

(c) cpviz propagation

Figure 5.23: cpviz output of the propagation: step 7

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

118
Chapter 5. Defining search primitives: the n-Queens Problem

Step 8:

node 0

x0 = 0 x0 6= 0

node 1 node 4
x0 = 1
x1 = 2 x1 6= 2 x0 6= 1

node 6
node 2 node 3 node 5
x0 6= 2
x0 = 2

node 8
node 7
x1 = 0 x1 6= 0

node 10
node 9

(a) cpviz tree (b) Real search tree

(c) cpviz propagation

Figure 5.24: cpviz output of the propagation: step 8

Failure. The first failure was when x1 = 0.

5.5 Basic working of the solver: the phases

A phase corresponds to a type of (sub)search in the search tree20 . You can have several
phases/searches in your quest to find a feasible or optimal solution. In or-tools, a phase is
constructed by and corresponds to a DecisionBuilder. We postpone the discussion on
the DecisionBuilders and Decisions for scheduling until the dedicated section 6.3.4
in the next chapter.

To better understand how phases and DecisionBuilders work, we will implement our own
DecisionBuilder and Decision classes in section 5.7. In this section, we show you how
to use these primitives and some very basic examples21 .
20
Well, sort of. Read on!
21
DecisionBuilders and Decisions are used internally and you cannot access them directly. To use
them, invoke the corresponding factory methods.

119
5.5. Basic working of the solver: the phases

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 finished 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 scenarios22 :
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. It’s
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 doesn’t 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 finished its work at the current node.
22
One could argue that these two scenarios are not really mutually exclusive. Indeed, we divide the sce-
narios in two cases depending on whether the DecisionBuilder returns a Decision or not. Some
DecisionBuilders delegate the creation process of Decisions to other DecisionBuilders.

120
Chapter 5. Defining search primitives: the n-Queens Problem

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: adds a Constraint during the
search.
• ApplyBranchSelector: changes the way the branches are selected. For in-
stance, 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 find a solution.
• SolveOnce: stops the search as soon as it finds 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: the usual DebugString()
method to give a name to your object.
• virtual void Accept(ModelVisitor* const visitor) const: the
usual Accept() method to let you visit the model and take appropriate actions.

5.5.2 Decisions and DecisionVisitors

The Decision class together with the DecisionBuilder class implement the branch-
ing rules of the search, i.e. how to branch (or divide the search sub-tree) at a given node in
the search tree. Although a DecisionBuilder could return several types of Decisions
during a search, we recommend to stick to one Decision for a DecisionBuilder per
phase.
DecisionVisitors is a class whose methods are triggered just before a Decision is
applied. Your are notified of the concrete decision that will be applied and are thus able to take
action.

Decisions

The Decision class is responsible to tell the solver what to do on left branches through its
Apply() method:

121
5.5. Basic working of the solver: the phases

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: the usual DebugString()
method.
• virtual void Accept(DecisionVisitor* const visitor) const:
accepts the given visitor.
Several Decision classes are available. We enumerate the different strategies implemented
by the available Decision classes dealing with IntVars in the next section. In the next
subsection, we detail a basic example.

AssignOneVariableValue as an example

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 applied23 .
23
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.

122
Chapter 5. Defining search primitives: the n-Queens Problem

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 6 value

otherwise it is

var > value

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:

123
5.5. Basic working of the solver: the phases

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 pro-
cess is done in two consecutive phases: first we rank the tasks for each machine, then we sched-
ule 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 specific
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:

124
Chapter 5. Defining search primitives: the n-Queens Problem

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 bal-
anced 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 first 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);

125
5.5. Basic working of the solver: the phases

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 sec-
tion 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: first a variable is chosen and only then is a value
chosen to be assigned to this variable.
The basic version of the MakePhase() method is:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IntVarStrategy var_str,
IntValueStrategy val_str);

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

126
Chapter 5. Defining search primitives: the n-Queens Problem

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 as-
sign to a chosen variable. The best choice is each time the one that minimizes the values
returned (through the Run() method) by the callbacks. We will explore both ways in the
section 5.7. There are two types of callbacks24 accepted by MakePhase():
typedef ResultCallback1<int64, int64> IndexEvaluator1;
typedef ResultCallback2<int64, int64, int64> IndexEvaluator2;

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 (first 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 predefined IntVarStrategy strategy to find the next variable to branch on, pro-
vide your own callback IndexEvaluator2 to find the next value to give to this variable and
an evaluator IndexEvaluator1 to break any tie between different values.
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator1* var_evaluator,
IntValueStrategy val_str);

This time, you provide an evaluator IndexEvaluator1 to find the next variable but rely on
a predefined IntValueStrategy strategy to find the next value.
Several other combinations are provided.
24
If you want to know more about callbacks, see the section Callbacks in the chapter Under the hood.

127
5.6. Out of the box variables and values selection primitives

When the 2-steps approach isn’t enough

Sometimes this 2-step approach isn’t satisfactory. You may want to test all combinations of
variables/values. We provide two versions of the MakePhase() method just to do that:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator2* evaluator,
EvaluatorStrategy str);

and
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IndexEvaluator2* evaluator,
IndexEvaluator1* tie_breaker,
EvaluatorStrategy str);

You might wonder what the EvaluatorStrategy strategy is. The selection is done by
scanning every pair <variable, possible value>. The next selected pair is the best among all
possibilities, i.e. the pair with the smallest evaluation given by the IndexEvaluator2. This
approach is costly and therefore we offer two options given by the EvaluatorStrategy
enum:
• CHOOSE_STATIC_GLOBAL_BEST: Static evaluation: Pairs are compared at the first
call of the selector, and results are cached. Next calls to the selector use the previous
computation, and are thus not up-to-date, e.g. some <variable, value> pairs may not be
possible due to propagation since the first call.
• CHOOSE_DYNAMIC_GLOBAL_BEST: Dynamic evaluation: Pairs are compared each
time a variable is selected. That way all pairs are relevant and evaluation is accurate. This
strategy runs in O(number-of-pairs) at each variable selection, versus O(1) in the static
version.

5.6 Out of the box variables and values selection prim-


itives

To choose among the IntVar variables and the int64 values when branching, several vari-
ables and values selection primitives are available. As stated before (see the subsection The
2-steps approach in the previous section for more), the selection is done in two steps:
• First, select the variable;
• Second, select an available value for this variable.
To construct the corresponding DecisionBuilder, use one of the MakePhase() factory
methods. For instance:
DecisionBuilder* MakePhase(const std::vector<IntVar*>& vars,
IntVarStrategy var_str,
IntValueStrategy val_str);

128
Chapter 5. Defining search primitives: the n-Queens Problem

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 first unbound variable. Variables are considered in
the order of the vector of IntVars used to create the selector.
CHOOSE_RANDOM Randomly select one of the remaining unbound variables.
CHOOSE_MIN_SIZE_LOWEST_MIN Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the lowest min value. In case of tie, the first one is selected, first
being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE_HIGHEST_MIN Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the highest min value. In case of tie, the first one is selected,
first being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE_LOWEST_MAX Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the lowest max value. In case of tie, the first one is selected, first
being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE_HIGHEST_MAX Among unbound variables, selects the variable with
the smallest size, i.e. the smallest number of possible values. In case of tie, the selected
variables is the one with the highest max value. In case of tie, the first one is selected,
first being defined by the order in the vector of IntVars used to create the selector.
CHOOSE_LOWEST_MIN Among unbound variables, selects the variable with the smallest
minimal value. In case of tie, the first one is selected, first being defined by the order
in the vector of IntVars used to create the selector.
CHOOSE_HIGHEST_MAX Among unbound variables, selects the variable with the highest
maximal value. In case of tie, the first one is selected, first being defined by the order in
the vector of IntVars used to create the selector.
CHOOSE_MIN_SIZE Among unbound variables, selects the variable with the smallest size.
In case of tie, the first one is selected, first being defined by the order in the vector of
IntVars used to create the selector.
CHOOSE_MAX_SIZE Among unbound variables, selects the variable with the highest size.
In case of tie, the first one is selected, first being defined by the order in the vector of
IntVars used to create the selector.
CHOOSE_MAX_REGRET Among unbound variables, selects the variable with the biggest gap
between the first and the second values of the domain.
CHOOSE_PATH Selects the next unbound variable on a path, the path being defined by the
variables: vars[i] corresponds to the index of the next variable following variable i.

129
5.6. Out of the box variables and values selection primitives

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

1 2

0 4
We have

vars = [−, 0, 3, 1, −, −]

where ” − ” corresponds to a variable that wasn’t 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 vari-
able points.
2. If no such path is found, try to find 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 first unbound variable.
We will encounter paths again in third part of this manual, when we’ll 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 vari-
able.

130
Chapter 5. Defining search primitives: the n-Queens Problem

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

5.6.3 Results

You can find the code in the files 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 the SolverBenchmarkStats class. You can
find both classes in the solver_benchmark.h header.
In phases1.cc, we test different combinations of the above strategies to find the variables
and the values to branch on. You can try it for yourself and see that basically no predefined
strategy really outperforms any other.
The program writes a report text file with the best statistics about the search of the CP Solver
to solve the n-Queens Problem given a specified size. The text files have report_n.txt as
name with n the size considered.
We summarize the results when we run the program with sizes 5, 6, 8 and 10 in the following
table:

n Best wall time: Best branches:


5 ChooseFirstUnbound - AssignMin RandomSelector -
AssignCenter
6 ChooseFirstUnbound - AssignMin MinSizeLowestMinSelector -
AssignMin
8 MinSizeHighestMinSelector - MinSizeHighestMinSelector -
AssignCenter AssignMin
10 MinSizeHighestMaxSelector - MinSizeLowestMinSelector -
AssignCenter AssignMin
The most fun (and most efficient) way to use or-tools is to define your own selection strategies
and search primitives. This is the subject of the next section.

131
5.7. Customized search primitives: DecisionBuilders and Decisions

5.7 Customized search primitives: DecisionBuilders


and Decisions

The search tree is traversed in a linear fashion: you go down left to assign values until it
is no longer possible and you backtrack whenever necessary and go right25 . This means
that you cannot jump from one branch of the tree to another26 . But what you can do how-
ever is define the tree thanks to combinations of DecisionBuilders and Decisions27 .
To compare our customized search primitives with the basic search strategy used until
now (CHOOSE_FIRST_UNBOUND and ASSIGN_MIN_VALUE), we’ll gather some statistics.
Then we’ll define two customized search strategies using two different mechanisms: in the first
one, we’ll use callbacks and in the second one, we’ll define our own DecisionBuilder
class. In neither cases we’ll use rocket science: the goal is to give you a taste of what you
can do and how you can do it. Finally, we’ll say a few words about which search primitives to
customize and why.
Some customizable search primitives are declared in the header
constraint_solver/constraint_solveri.h (notice the i at the
end).
Before we go on, let’s pause and take a second to discuss the inner mechanism used by the CP
Solver to keep track of some objects.

5.7.1 BaseObject and RevAlloc

We have seen that most objects can be created with a factory method (the Make...() meth-
ods). This allows the CP solver to keep track of the created objects and delete them when
they are no longer required. When backtracking, some objects are no longer needed and the
CP Solver knows exactly when and what to delete. The BaseObject class is the root of all
reversibly28 allocated objects.
Whenever you define your own subclass of BaseObject (Decisions,
DecisionBuilders and for instance the SymmetryBreakers we’ll see in the next
section are BaseObjects), it is good practice to register the given object as being reversible
to the solver. That is, the solver will take ownership of the object and delete it when it back-
tracks out of the current state. To register an object as reversible, you invoke the RevAlloc()
method29 of the solver. For instance, the Next() method of the BaseAssignVariables
DecisionBuilder is implemented as follow:
25
We keep our visualization of our binary search tree, with going left to assign some values to variables and
going right to do the contrary, i.e. to avoid the assignment of these values to the same variables. Of course, there is
no left nor right in the tree. The basic idea is to partition the sub-tree at this point in the search into two sub-trees.
26
Well, this is not totally exact. The search tree is conceptual and depending on the view you have of the tree,
you can visit it in different ways but let us keep it simple for the moment. We have one search tree and the CP
Solver use a pre-order traversal to traverse this tree. Reread the section Basic working of the solver: the search
algorithm if needed.
27
And other primitives like SymmetryBreakers, subject of section 5.8.
28
This is in reference to the backtracking mechanism of the CP Solver.
29
Or one of its sibling methods.

132
Chapter 5. Defining search primitives: the n-Queens Problem

Decision* BaseAssignVariables::Next(Solver* const s) {


int64 id = 0;
IntVar* const var = selector_->SelectVariable(s, &id);
if (nullptr != var) {
const int64 value = selector_->SelectValue(var, id);
switch (mode_) {
case ASSIGN:
return s->RevAlloc(new AssignOneVariableValue(var, value));
...
}
}
return nullptr;
}

RevAlloc() returns a pointer to the newly created and registered object, in this case the
returned AssignOneVariableValue object. You can thus invoke this method with ar-
guments in the constructor of the constructed object without having to keep a pointer to this
object.
The solver will now take care of your object. If you have an array of objects that are subclasses
of BaseObject, IntVar, IntExpr and Constraint, you can register your array with
RevAllocArray(). This method is also valid for arrays of ints, int64, uint64 and
bool. The array must have been allocated with the new[] operator.
If you take a look at the source code, you will see that the factories methods call RevAlloc()
to pass ownership of their objects to the solver.
Note that if no variable can be selected, the Next() method returns a nullptr which is the
way to tell the CP solver that this DecisionBuilder has done its job.

5.7.2 The basic search strategy visualized

Let’s have a look at our default strategy:


• We first choose the first unbound variable: CHOOSE_FIRST_UNBOUND.
• Then we assign it the smallest available value: ASSIGN_MIN_VALUE.
The complete search tree for n = 4 looks nice:

There are indeed two feasible solutions:


============================
size: 4
The Solve method took 0.001 seconds

133
5.7. Customized search primitives: DecisionBuilders and Decisions

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

The complete search tree for n = 5 looks even better:

The solver seems to find the 10 feasible solutions quite quickly:


============================
size: 5
The Solve method took 0.07 seconds
Number of solutions: 10
Failures: 14
Branches: 26
Backtracks: 17
Stamps: 61

But then, n = 4 and n = 5 are really small numbers. And indeed, with n = 6, things start to
look really ugly:

That’s a real nightmare! The statistics collected during the search confirm this:
============================
size: 6
The Solve method took 0.005 seconds
Number of solutions: 4
Failures: 36
Branches: 70
Backtracks: 39
Stamps: 149

To only find 4 solutions, we need 70 branches and 39 backtracks!


This trend is confirmed if we look at larger values of n:

134
Chapter 5. Defining search primitives: the n-Queens Problem

n 7 8 9 10 11 12
Time (s) 0,014 0,052 0,25 0,899 4,236 21,773
Number of sol. 40 92 352 724 2680 14200
Failures 110 396 1546 6079 27246 131006
Branches 218 790 3090 12156 54490 262010
Backtracks 113 399 1549 6082 27249 131009
Stamps 445 1583 6189 24321 108989 524029
We clearly see the exponential pattern of intractable problems30 .
We have seen in the previous section that the other implemented search strategies didn’t seem
to do better. Can we do better? Let’s try!

How did we produce these wonderful images?


All the images of search trees were produced with cpviz and nqueens4. See the section
cpviz: how to visualize the search for more details on how to obtain them.

5.7.3 First try: start from the center (and use callbacks)

You can find the code in the file tutorials/cplusplus/chap5/nqueens5.cc.

Our default search strategy (CHOOSE_FIRST_UNBOUND and ASSIGN_MIN_VALUE)


starts with the variables from the left and then slowly assigns variables more in the center and
finishes with the variables at the other end, i.e. it will first try to assign some value to x0 , then
x1 , and so on until xn−1 for an n-Queens Problem.
When propagating the constraints from the side first, we don’t exclude as much values as when
we propagate from the center. This is illustrated on the next figure: In the left figure, the queen

(a) Queen in the corner (b) Queen in the middle

Figure 5.25: 9 forbidden variable-value combinations on the left but 11 on the right.

in the left upper corner forbids 9 variable-value combinations, while the queen on the right in
the middle forbids 11 variable-value combinations.
Maybe it would be worth trying to first select the variables from the middle? To do so, let’s
use a callback and define a simple way to evaluate the middleness of a variable index. We use
30
This is not a proof of course. Maybe another search strategy would yield a better algorithm but we do know
that this problem is intractable.

135
5.7. Customized search primitives: DecisionBuilders and Decisions

a simple callback IndexEvaluator1 that allows to evaluate the next variable to branch on
by giving the index of this variable in the std::vector<IntVar*> for unbounded variables:
typedef ResultCallback1<int64, int64> IndexEvaluator1;

As stated in 5.5.5, the smallest value returned by this callback will give the index of the chosen
variable to branch on. Define b n2 c to be the middle of a n × n chessboard. An easy way to
measure the distance of the index i of a variable to the middle of the chessboard is:
n
|b c − i|
2
where |X| denotes the absolute value of X, i.e. X if X > 0 or −X if X < 0. Translated into
code, we have the next callback:
class MiddleVariableIndexSelector : public IndexEvaluator1 {
public:
MiddleVariableIndexSelector(const int64 n): n_(n),
middle_var_index_((n-1)/2) {}
~MiddleVariableIndexSelector() {}
int64 Run(int64 index) {
return abs(middle_var_index_ - index);
}

private:
const int64 n_;
const int64 middle_var_index_;
};

To assign a value, we can use the predefined ASSIGN_CENTER_VALUE strategy that selects
the first available value that is the closest to the center of the domain of the selected variable.
Our code becomes:
MiddleVariableIndexSelector * index_evaluator =
new MiddleVariableIndexSelector(size);

DecisionBuilder* const db = s.MakePhase(queens,


index_evaluator,
Solver::ASSIGN_CENTER_VALUE);

s.Solve(db, monitors); // go!

In the next table, we compare this strategy to our default strategy:

136
Chapter 5. Defining search primitives: the n-Queens Problem

n Statistics Default strategy New strategy


7 Failures 110 98
Branches 218 194
8 Failures 396 368
Branches 790 734
9 Failures 1546 1272
Branches 3090 2542
10 Failures 6079 4950
Branches 12156 9898
11 Failures 27246 20441
Branches 54490 40880
12 Failures 131006 97335
Branches 262010 194668
Our new strategy is better and - even if this is not shown - quicker. That said,
is it really that clever? In the next table we compare our IndexEvaluator1
MiddleVariableIndexSelector combined with the ASSIGN_MIN_VALUE assign
value strategy:
n Statistics New strategy New strategy with ASSIGN_MIN_VALUE
7 Failures 98 97
Branches 194 192
8 Failures 368 363
Branches 734 724
9 Failures 1272 1254
Branches 2542 2506
10 Failures 4950 4857
Branches 9898 9712
11 Failures 20441 20072
Branches 40880 40142
12 Failures 97335 95396
Branches 194668 190784
How come that the value assigning strategy ASSIGN_MIN_VALUE does (slightly) better than
the ASSIGN_CENTER_VALUE? At first, assigning the smallest eligible value doesn’t seem
a good idea but you have to keep the big picture in mind: we are looking for all solutions.
Despite the fact that looking from the center seems to accelerate the search when looking for
one solution, it is counter-productive in some cases. Always test your ideas as the search is
always a matter of tradeoffs.
Another thing that should bug you is that the IndexEvaluator1 is called on all unbounded
variables for evaluation while we already know what the best unbounded variable is, no need
to go over all of them. Maybe we can use a shortcut and why not use our knowledge to select
the unbounded variables and assign them values? This is what we’ll try next.

137
5.7. Customized search primitives: DecisionBuilders and Decisions

5.7.4 Second try: dynamic variable selection (and define our own
DecisionBuilder class)

You can find the code in the files tutorials/cplusplus/chap5/nqueens6.cc.

In this sub-section, we will implement our own DecisionBuilder class. The idea
is to give you a glimpse of what can be done, not to solve the n-Queens Problem. The idea
proposed here can be further developed but this would lead us too far.
We continue with our first intuition to assign variables “from the center” as in our first try but
this time we select the variables dynamically. We could have constructed our own customized
Decision class but because we only assign one variable at a time, we can reuse the already
implemented AssignVariableValue Decision class. As usual, this class is not directly
available and we use the corresponding factory method MakeAssignVariableValue().
To mimic this implementation choice, we also use the anonymous namespace31 :
namespace {
... // code here is inaccessible in another unit
}

and use a factory method for our customized DecisionBuilder class. This is just to show
you why you cannot (and should not!) use some classes directly32 .
We name our customized DecisionBuilder NQueensDecisionBuilder:
class NQueensDecisionBuilder : public DecisionBuilder {
public:
NQueensDecisionBuilder(const int size,
const std::vector<IntVar*>& vars):
size_(size), vars_(vars), middle_var_index_((size-1)/2) {
CHECK_EQ(vars_.size(), size_);
}
~NQueensDecisionBuilder() {}

...
private:
const int size_;
const std::vector<IntVar*> vars_;
const int middle_var_index_;
};

and here is our factory method:


DecisionBuilder* MakeNQueensDecisionBuilder(Solver* const s,
const int size,
const std::vector<IntVar*>& vars) {
return s->RevAlloc(new NQueensDecisionBuilder(size, vars));
}

As you see, we construct a middle index for our variables in the constructor and assign this
value to middle_var_index_ as we did in our first try.
31
Although anonymous namespaces are no longer required in C++11.
32
You can access the class definitions in the same file though.

138
Chapter 5. Defining search primitives: the n-Queens Problem

We again use a two-stages approach: we first select a variable to branch on and then we select
a value from its domain to assign it in the Apply() section of the search tree. Remem-
ber that this is exactly what a DecisionBuilder does: at each node in the search tree,
it returns a Decision that is applied on one branch (Apply()) and refuted on the other
branch (Refute()). We select the variable that has the smallest domain, i.e. the smallest
number of values still available to assign to this variable. We could have chosen one of the
CHOOSE_MIN_SIZE_XXX variable selection strategies except that in the case of a tie, we
explicitly want to select the variable that is the “most in the middle”. To select this variable, we
use the following method:
IntVar* SelectVar(Solver* const s) {
IntVar* selected_var = nullptr;
int64 id = -1;
int64 min_domain_size = kint64max;

// go left on the chessboard


for (int64 i = middle_var_index_; i >= 0; --i) {
IntVar* const var = vars_[i];
if (!var->Bound() && var->Size() < min_domain_size) {
selected_var = var;
id = i;
min_domain_size = var->Size();
}
}

// go right on the chessboard


for (int64 i = middle_var_index_ + 1; i < size_; ++i) {
IntVar* const var = vars_[i];
if (!var->Bound() && var->Size() < min_domain_size) {
selected_var = var;
id = i;
min_domain_size = var->Size();
}
}

if (id == -1) {
return nullptr;
} else {
return selected_var;
}
}

This method simply returns (a pointer to) the selected IntVar variable. Of interest are the
methods Bound() that returns true if the variable is bounded, i.e. if it is assigned a value
and Size() that returns the size of the current domain of a variable. If we can not find
any variable, we return a nullptr. The idea behind this selection strategy is to choose a
variable that can make the solver fail as soon as possible. Indeed, if a queen doesn’t have many
possibilities, it’s probably because there is not enough room for her. This is strategy is called
the first fail principle.
Once a variable is selected, we select a value to be assigned to it. Different scenarii are possible.
Let’s try to select the row with the least compatible columns, i.e. the row that has the least
number of possibilities to be occupied by a queen. The idea is that we want to choose the row

139
5.7. Customized search primitives: DecisionBuilders and Decisions

that has the least possibilities to welcome a queen because this might lead to the best choice for
a queen on her column. Indeed, other choices might even reduce the number of possibilities
on this row. This strategy, the one that will most probably lead to a solution, is called the best
success principle.
We devise a simple method to count the number of incompatibilities for one row:
int64 count_number_of_row_incompatibilities(int64 row) {
int64 count = 0;
for (int64 i = 0; i < size_; ++i) {
if (!vars_[i]->Contains(row)) {
++count;
}
}
return count;
}

Given a row row, we count the impossibilities, i.e. the number of queens that cannot be placed
on this row. Note the use of the method Contains() that returns if a given value is in the
domain or not. The corresponding SelectValue() method is straightforward and we don’t
discuss it here. We now turn to the main method of the DecisionBuilder: its Next()
method. It is deceivingly simple:
Decision* Next(Solver* const s) {
IntVar* const var = SelectVar(s);
if (nullptr != var) {
const int64 value = SelectValue(var);
return s->MakeAssignVariableValue(var, value);
}

return nullptr;
}

If we find a variable and a value, we return an AssignVariableValue Decision that


will assign the value to the variable in its Apply() method and remove this value for this
variable in its Refute() method.
How does this simple strategy perform? In the next table, we compare the number of failures
and branches for our best first try strategy and this one:
n Statistics first try with ASSIGN_MIN_VALUE Second try
7 Failures 97 92
Branches 192 182
8 Failures 363 328
Branches 724 654
9 Failures 1254 1216
Branches 2506 2430
10 Failures 4857 4500
Branches 9712 8998
11 Failures 20072 17847
Branches 40142 35692
12 Failures 95396 86102
Branches 190784 172202

140
Chapter 5. Defining search primitives: the n-Queens Problem

So, yes, we did better by using some knowledge of the problem to prune the search tree but to
really tackle this problem, we need a deeper understanding of... symmetries. This is the subject
of the next section.
Don’t hesitate to have a look at the code. The routing library in particular has very specialized
implementations of DecisionBuilders and Decisions. In particular, have a look at the
different IntVarFilteredDecisionBuilders and you may want to implement your
versions only if you add lots of knowledge of the problem at hand. Otherwise, you can reuse
the already implemented (and more general and tested) versions.

5.7.5 How to customize search primitives?

Basically, think about an algorithm and implement whatever you need. To customize search
primitives, you can, in order of complexity:
• combine existing ones;
• redefine callbacks;
• implement your own version of some search primitive classes;
• mix all of the above.
Often, there is more than one way to do it. Choose the simplest way first to prototype your
algorithm. Combining existing chunks of code shouldn’t ask too much coding effort and will
tell you right away if your idea has some potential or not. Being able to rapidly construct an
algorithm in a few lines of code is one of the strength of the or-tools library. Once your idea
is implemented, profile your code to see if you really need an improved version or not. Maybe
the existing code is too general and you should adapt it to your specific problem with the help
of callbacks. Again, profile your code. Last situation, maybe there isn’t any piece of code to
do what you want. In this case, you’ll need to implement your own version. The library was
conceived with reuse and extension in mind. If you find yourself stuck, maybe it is because you
are not using/redefining the right search primitives? In section 12.5.1, we discuss what search
primitives to customize. If you think your piece of code might be general enough and could be
used by others, ask on the mailing list if we are not interested in implementing it ourselves or
if by any chance we don’t already have some code that we can open source.
Last but not least, if you have produced a piece of code of general interest, share it with the
community.

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

141
5.8. Breaking symmetries with SymmetryBreakers

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

5.8.1 The basic idea

The basic idea is quite simple. Consider again the 4-Queens Problem. Figure 5.26 represents
two symmetric solutions.

(a) Solution 1 (b) Solution 2

Figure 5.26: 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 x1 = 1 (or x1 = 0) during the search, we know that we don’t have to test a solution
with x2 = 1 (or x2 = 0) as every solution with x1 = 1 (x1 = 0) has an equivalent symmetric
solution with x2 = 1 (x2 = 0).
You can tell the CP solver not to visit the branch x2 = c if during the search we already have
tried to set x1 = c. To do this, we use a SymmetryManager and a SymmetryBreaker.
The SymmetryManager collects SymmetryBreakers for a given problem. During the
search, each Decision is visited by all the SymmetryBreakers. If there is a match be-
tween the Decision and a SymmetryBreaker, the SymmetryManager will, upon refu-
tation of that Decision issue a Constraint to forbid the symmetrical exploration of the
search tree. As you might have guessed, SymmetryManagers are SearchMonitors and
SymmetryBreakers are DecisionVisitors.

5.8.2 SymmetryBreakers

You can find the code in the files tutorials/cplusplus/chap5/nqueens7.cc.

Let’s create a SymmetryBreaker for the vertical axial symmetry. Because the square has
lots of symmetries, we introduce a helper method to find the symmetric indices of the variables
and the symmetric values for a given variable:
int symmetric(int index) const { return size_ - 1 - index}

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

142
Chapter 5. Defining search primitives: the n-Queens Problem

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

143
5.8. Breaking symmetries with SymmetryBreakers

We also use two methods to do the translation between the indices and the variables. Given an
IntVar * var, Index(var) returns the index of the variable corresponding to var:
int Index(IntVar* const var) const {
return FindWithDefault(indices_, var, -1);
}

FindWithDefault() is defined in the header constraint_solver/constraint_solveri.h.


Given an std::map<IntVar*, int> like indices_, it returns the corresponding int
if it finds the IntVar *. If it doesn’t find 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 cor-
responding constraint upon refutation of a given Decision? For the n-Queens Prob-
lem, we can use the AddIntegerVariableEqualValueClause() method of the
SymmetryBreaker class. Given the assignation of a value to an IntVar, give this method

144
Chapter 5. Defining search primitives: the n-Queens Problem

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

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 defines two other clauses:
• AddIntegerVariableGreaterOrEqualValueClause(IntVar* const
var, int64 value) and
• AddIntegerVariableLessOrEqualValueClause(IntVar* const
var, int64 value).
Their names are quite explicit and tell you what their purpose is. These methods would fit
perfectly within a VisitSplitVariableDomain() call for instance.

5.8.3 The SymmetryManager

Because the n-Queens Problem is defined on a square, we have a lots of symmetries we can
avoid:
• Vertical axis symmetry: we already defined the SY class;
• Horizontal axis symmetry: class SX;
• First diagonal symmetry: class SD1;
• Second diagonal symmetry: class SD2;

145
5.8. Breaking symmetries with SymmetryBreakers

• 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 objects in an
std::vector<SymmetryBreaker*>:
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:


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 find only unique solutions up to a symmetry.

5.8.4 Results

Let’s compare33 some statistics of this algorithm with our best try from previous section:
33
Don’t forget to use the use_symmetry flag!

146
Chapter 5. Defining search primitives: the n-Queens Problem

n Statistics Second try With SymmetryBreakers


7 Failures 92 24
Branches 82 44
8 Failures 328 71
Branches 654 139
9 Failures 1216 272
Branches 2430 541
10 Failures 4500 1074
Branches 8998 2142
11 Failures 17847 4845
Branches 35692 9686
12 Failures 86102 23159
Branches 172202 46312
Of course, the comparison is a little biased as in the case of the SymmetryBreakers we
don’t try to count or reproduce all the solutions. We are only looking at unique solution up to a
symmetry. With a little bit more work, you could retrieve all the solutions and the work to do so
is minimal compared to the traversal of the search tree. So our method would still be efficient.

5.9 Summary

This chapter is the first one of the second part of this manual and we have switched into high
gear. The first part was about the basics of the solver, now we entered the core part: how to
customize the CP solver to our needs. This cannot be done without a basic understanding of the
solver and this chapter provided you with enough insights into the main solving algorithm. In
particular, we saw some search primitives and how to customize them. We also saw how they
are called in the main search algorithm.
Our illustrative problem for this chapter is the n-Queens Problem. We have only proposed a
basic model as our focus was to customize search primitives, not devise a good model. To
better compare search strategies, we saw how to use the cpviz library to visualize the search
tree and the propagations of the constraints.
Search primitives are blocks used by the CP solver to construct the search tree and conduct the
search. Among them, we saw in details:
• SearchMonitors to control and monitor the search;
• DecisionBuilders and Decisions to create the search tree.
To customize search primitives, you can, in order of complexity:
• combine existing ones;
• redefine callbacks;
• implement your own version of some search primitive classes;
• mix all of the above.
For each case, we have seen an example.

147
5.9. Summary

Finally, we continued our discussion of symmetries in the search tree and saw a new way to skip
them on the fly with the help of SymmetryBreakers. By devising better search primitives
and algorithms, we went from a few seconds to only a few milli-seconds to solve the n-Queens
Problem for n = 7..12.
You might be curious, as we are, to combine our best search strategy and the
SymmetryBreakers. This is done in the next sub-section.

5.9.1 Combining both approaches

You can find the code in the file tutorials/cplusplus/chap5/nqueens8.cc.

We have improved our search strategy and search time little by little. We started by a
simple approach (file nqueens1.cc and algorithm A in the next table) and ended up with
our best algorithm discussed in section Second try: dynamic variable selection (and define
our own DecisionBuilder class). We then saw a completely different approach and used
SymmetryBreakers with a default search algorithm (file nqueens7.cc and algorithm B
in the next table). What about combining both approaches (file nqueens8.cc and algorithm
C in the next table)?
n Statistics Algorithm A Algorithm B Algorithm C
7 Failures 92 24 24
Branches 82 44 41
8 Failures 328 71 54
Branches 654 139 100
9 Failures 1216 272 186
Branches 2430 541 367
10 Failures 4500 1074 642
Branches 8998 2142 1275
11 Failures 17847 4845 2465
Branches 35692 9686 4924
12 Failures 86102 23159 11770
Branches 172202 46312 23511
To give you an idea of the progress we made, the following table gives you the time needed to
solve the 14-Queens Problem with the three algorithms:
Algorithms Time (s)
A 21,582
B 6,096
C 2.945
Combining both approaches did result in a clear cut gain but it is not always the case. Some-
times, two good ideas don’t mix that well.

148
CHAPTER

SIX

LOCAL SEARCH: THE JOB-SHOP


PROBLEM

We enter here in a new world where we don’t try to solve a problem to optimality but seek a
good solution. Remember from sub-section 1.4.4 that some problems1 are hard to solve. No
matter how powerful our computers are2 , we quickly hit a wall if we try to solve these problems
to optimality. Do we give up? Of course not! If it is not possible to compute the best solutions,
we can try to find very good solutions. Enter the fascinating world of (meta-)heuristics and
Local Search.

Throughout this chapter, we will use the Job-Shop Problem as an illustrative example.
The Job-Shop problem is a typical difficult scheduling problem. Don’t worry if you don’t
know anything about scheduling or the Job-Shop Problem, we explain this problem in details.
Scheduling is one of the fields where constraint programming has been applied with great
success. It is thus not surprising that the CP community has developed specific tools to solve
scheduling problems. In this chapter, we introduce the ones that have been implemented in
or-tools.

Overview:

We start by describing the Job-Shop Problem, the disjunctive model to represent it, two formats
to encode Job-Shop Problem instances (JSSP and Taillard) and our first exact results. We
next make a short stop to describe the specific primitives implemented in or-tools to solve
scheduling problems. For instance, instead of using IntVar variables, we use the dedicated
IntervalVars and SequenceVars.
After these preliminaries, we present Local Search and how it is implemented in the or-tools
library. Beside the Job-Shop Problem, we use a dummy problem to watch the inner mechanisms
of Local Search in or-tools in action:
We minimize x0 + x1 + . . . + xn−1 where each variable has the same domain
[0, n − 1]. To complicate things a little bit, we add the constraint x0 > 1.
1
Actually, most interesting problems!
2
But watch out for the next generations of computers: molecular computers
(http://en.wikipedia.org/wiki/Molecular_computer) and computers based on quantum mechanics
(http://en.wikipedia.org/wiki/Quantum_computer)!
Once we understand how to use Local Search in or-tools, we use basic
LocalSearchOperators to solve the Job-Shop Problem and compare the ex-
act and approximate results. Finally, to speed up the Local Search algorithm, we use
LocalSearchFilters for the dummy problem.

Prerequisites:

• 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 define an objective function (see section 3.3).
• Section 5.3 on the inner working of the solver helps but is not mandatory.

Classes under scrutiny:

IntervalVar, SequenceVar, LocalSearch, LocalSearchOperator,


LocalSearchFilter.

Files:

The files used in this chapter are:


• jobshop.h: This file contains the JobShopData class that records the data for Job-Shop
Problem instances. This file is used throughout all the Job-Shop examples.
• report_jobshopdata.cc: a simple program to report the content of job-shop problem in-
stances in JSSP or Taillard’s formats.
• abz9: a job-shop problem instance in JSSP format.
• 20_5_01_ta001.txt: a job-shop problem instance in Taillard’s format.
• first_example_jssp.txt: our first 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 or-tools.
• 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.

150
Chapter 6. Local Search: the Job-Shop Problem

• jobshop_ls3.cc: A basic implementation of Local Search with both the


SwapIntervals and ShuffleIntervals LocalSearchOperators. We
use also Local Search to find an initial solution.
• dummy_ls_filtering.cc: The basic example extended with filtering.
The files 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).

6.1 The Job-Shop Problem, the disjunctive model and


benchmark data

We describe the Job-Shop Problem, a first model and the benchmark data. The Job-Shop Prob-
lem belongs to the intractable problems (∈ NP). Only few very special cases can be solved in
polynomial time (see [Garey1976] and [Kis2002]). The definition of this fascinating problem
is not that complicated but you probably will need some extra attention if this is your first
encounter with it. Once you grasp its definition, the next subsections should flow easily.

6.1.1 Description of the problem

In the classical Job-Shop Problem there are n jobs that must be processed on m machines.
Each job consists of a sequence of different tasks3 . Each task needs to be processed during an
uninterrupted period of time on a given machine.
We use4 aij to denote the ith task of job j.
Given a set J of jobs, a set M of machines and a set T of tasks, we denote by τj the number of
tasks for a given job j ∈ J. To each task aij corresponds an ordered pair (mij , pij ): the task aij
needs to be processed on machine mij ∈ M for a period of pij units of time.
Here is an example with m = 3 machines and n = 3 jobs. We count jobs, machines and tasks
starting from 0.
• job 0 = [(0, 3), (1, 2), (2, 2)]
• job 1 = [(0, 2), (2, 1), (1, 4)]
• job 2 = [(1, 4), (2, 3)]
In this example, job 2 consists of τ2 = 2 tasks: task a02 which must be processed on machine
m02 = 1 during p02 = 4 units of time and task a12 which must be processed on machine
m12 = 2 during p12 = 3 units of time.
To have a Job-Shop Problem, the tasks must be processed in the order given by the sequence:
for job 0 this means that task a00 on machine 0 must be processed before task a10 on machine
1 that itself must be processed before task a20 on machine 2. It is not mandatory but most of
the literature and benchmark data are concerned by problems where each job is made of m
3
Tasks are also called operations.
4
We use a slightly different and we hope easier notation than the ones used by the scheduling community.

151
6.1. The Job-Shop Problem, the disjunctive model and benchmark data

tasks and each task in a job must be processed on a different machine, i.e. each job needs to be
processed exactly once on each machine.
We seek a schedule (solution) that minimizes the makespan (duration) of the whole process.
The makespan is the duration between the start of the first task (across all machines) and the
completion of the last task (again across all machines). The classical notation for the makespan
is Cmax .
Let’s define tij as the starting time of the processing of task aij .
The makespan can be defined as

Cmax = max{tij + pij }


tij

or equivalently as the maximum time needed among all jobs to be completely processed. Recall
that τj denotes the number of tasks for job j and that we count starting from 0. tτj −1,j denotes
thus the starting time of the last task of job j and we have

Cmax = max {tτj −1,j + pτj −1,j }


tτj −1,j

Let’s try to find a schedule for our example. Suppose you want to favour job 1 because you did
see that all jobs have the same processing time (7) and that job 1 has its last task requiring 4
units of time. Here is the Gantt chart of a possible schedule:

11111
00000
0000000
1111111
00000
11111
0000000
1111111 0110
machine 0 11111
00000
0000000
1111111 1010
machine 1111111
000000
job 0

000 11111
111 00000
000000000
111111111
00000
11111
000000000
111111111 00000
11111
00000
11111 1010
000000
111111
000 11111
00000 11111 0110
job 1
000
111
111 00000
00000
11111
00000
11111
machine 2 job 2
2 4 6 8 10 12

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

11111111111
00000000000 01
0000000
1111111
00000001111
machine 0 1111111
0000
0000
1111 1010
000000
111111
job 0

000000
111111
00000
11111
11 00000
11111
000000000
111111111
00
00000
11111
00000
11111
011111
111111111
00000000 101
010
00000
11111 0110
machine 1 job 1
00
11
00
1100000
00000
11111
00000
11111
machine 2 job 2
2 4 6 8 10 12

Its makespan is 11 units of time.


How can we simply describe a schedule? We defined tij as the starting time of task aij . A
feasible schedule can then be defined as a set5 of non negative integers {tij } such that the
definition of a Job-Shop Problem is respected. If we only consider schedules where all tasks
5
And a correspondence rule between those integers and the tasks.

152
Chapter 6. Local Search: the Job-Shop Problem

are completely left shifted on the Gantt chart6 , we can define a feasible schedule by giving the
sequence of jobs processed on each machine.
The first schedule can be described by:
• Machine 0: job 1, job 0
• Machine 1: job 2, job 1, job 0
• Machine 2: job 1, job 2, job 0
and the second optimal one by
• Machine 0: job 0, job 1
• Machine 1: job 2, job 0, job 1
• Machine 2: job 1, job 0, job 2
The Gantt chart offers a nice visualization of schedules but it doesn’t really give any insight
into the problem7 . The disjunctive graph allows a better understanding of the structure of the
problem.

6.1.2 The disjunctive graph

Figure 6.1 represents the disjunctive graph of our example. The graph is G = (V, C ∪D) where

(0,3) (1,2) (2,2)


job 0
machine 0

e2
hin
ac
(0,2) (2,1) (1,4)
e1

m
n

s t job 1
chi
ma

job 2
(1,4) (2,3)

Figure 6.1: A disjunctive graph.

• V is the set of vertices corresponding to the tasks. Two fictive vertices s and t are added
to represent the start and end times. Each vertex has a weight corresponding to the
processing time of the task it represents. Vertices s and t have weight 0.
• C are the conjunctive arcs between the ith and (i + 1)th tasks of a job. We also add
conjunctive arcs from s to the first task of every job and from the last task of every job to
t. These arcs are plain in figure 6.1.
• D are the disjunctive arcs between task to be processed on the same machine. These arcs
are dotted or dashed in figure 6.1.
6
A rigorous definition of schedules where all tasks are completely left shifted on the Gantt chart is beyond the
scope of this manual. In scheduling jargon, such schedules are called semi-active schedules.
7
Except if you see the disjunctive graph in the Gantt chart!

153
6.1. The Job-Shop Problem, the disjunctive model and benchmark data

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

(0,2) (2,1) (1,4)


s t job 1

job 2
(1,4) (2,3)

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

(0,2) (2,1) (1,4)


s t job 1

job 2
(1,4) (2,3)

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


We can now define the Job-Shop Problem as a graph problem: find a complete orientation of
the edges of a disjunctive graph such that the resulting directed graph is acyclic and the longest
weighted path from s to t is minimized. We will use this representation of the problem to
design our model.

6.1.3 The disjunctive model

This model is a straightforward translation of the definition of a Job-Shop Problem and its
disjunctive graph representation.
We again rely on the The three-stage method: describe, model and solve. What are the decision
variables? We use the variables tij to store the starting time of task i of job j. We could use
two fictive variables corresponding to the fictive vertices s and t but this is not necessary.
To simplify the notation, we will use the notation tk where k denotes a vertex (a task) of the
disjunctive graph. We use the same simplified notation for the processing times (p) and the
machine ids (m).
8
An acyclic graph is a graph without cycle. It can be shown that a complete directed acyclic graph induces a
total order on its vertices, i.e. a complete directed acyclic graph lets you order all its vertices unequivocally.

154
Chapter 6. Local Search: the Job-Shop Problem

What are the constraints? In the disjunctive graph, we have two kind of edges to model a
feasible schedule:
• conjunctive arcs modelling the order in which each task of a job has to be processed:

∀(k, l) ∈ C such that k 6= s and l 6= t :


tk + pk 6 tl

These constraints are called conjunctive constraints.


• disjunctive edges modelling the order in which tasks have to be processed on a single
machine:

∀(k, l) ∈ D such that mk = ml


tk + pk 6 tl or tl + pl 6 tk

These constraints are called disjunctive constraints. They forbid cycles in a clique corre-
sponding to a machine9 .
What is the objective function? The objective function (the makespan) Cmax doesn’t corre-
spond to a variable of the model. We have to construct its value. Because we minimize the
makespan, we can use a little trick. Let S be the set of all end tasks of all jobs. In our example,
S = {a20 (2, 2), a21 (1, 4), a12 (2, 3)}. The makespan must be greater than the overall time it
takes to process these tasks:

∀k ∈ S :
Cmax > tk + pk .

Here is the model10 :


mintk Cmax
s.t.:
Cmax > tk + pk ∀k ∈ S
tk + pk 6 tl ∀ (k, l) ∈ C
tk + pk 6 tl or tl + pl 6 tk ∀ (k, l) ∈ D : mk = ml
tk > 0 ∀ k ∈ V \ {s, t}

We will implement and solve this model in the next section but first we need to read and process
the data representing instances of Job-Shop Problems.
9
Here is why. Consider the following situation
t1
t2

t3
We have t1 +p1 6 t2 , t2 +p2 6 t3 and t3 +p3 6 t1 . Add these three inequalities and you obtain p1 +p2 +p3 6 0.
This is impossible if one of the pi is greater than 0 as every pi > 0.
10
It is not obvious that this model produces optimal solutions that are feasible schedules but it can be shown
that it does.

155
6.1. The Job-Shop Problem, the disjunctive model and benchmark data

6.1.4 The data and file formats

To collect the data, we use two different file formats: JSSP and professor Taillard’s format.
In the directory data/jobshop, you can find data files for the Job-Shop Problem11 . The file
jobshop.h lets you read both formats and store the data into a JobshopData class. We
will use this class throughout this chapter.

JSSP stands for Job Shop Scheduling Problem. Let’s consider the beginning of file abz9:
+++++++++++++++++++++++++++++

instance abz9

+++++++++++++++++++++++++++++
Adams, Balas, and Zawack 15 x 20 instance (Table 1, instance 9)
20 15
6 14 5 21 8 13 4 11 1 11 14 35 13 20 11 17 10 18 12 11 ...
1 35 5 31 0 13 3 26 6 14 9 17 7 38 12 20 10 19 13 12 ...
0 30 4 35 2 40 10 35 6 30 14 23 8 29 13 37 7 38 3 40 ...
...

The first 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 (mij , pij ) corresponds to a task. For this first job, the first task needs 14 units of time
on machine 6, the second task needs 21 units of time on machine 5 and so on.
As is often the case, there is a one to one correspondence between the tasks and the machines.

Taillard’s format

Let’s consider the beginning of file 20_5_01_ta001.txt:


20
5
873654221
0
468
54 79 16 66 58
1
325
83 3 89 58 56
2
923
11
We copied the files abz9 and 20_5_01_ta001.txt in the directory
manual/tutorials/cplusplus/chap6 for your convenience.

156
Chapter 6. Local Search: the Job-Shop Problem

15 11 49 31 20
3
513
71 99 15 68 85
...

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

0 is the id or index of the first job. The next number is not important for the job-shop problem.
The numbers in the last line correspond to processing times. We use the trick to assign these
times to machines 0, 1, 2 and so on. So job 0 is actually

[(0, 54), (1, 79), (2, 16), (3, 66), (4, 58)]

Because of this trick, one can not easily define our problem instance above in this format and
we don’t attempt to do it.
You can find anything you ever wanted to know and more about this format in [Taillard1993].

JobshopData

You can find the code in the files jobshop.h and report_jobshopdata.cc and the
data in the files abz9, 20_5_01_ta001.txt and first_example_jssp.txt.

The JobshopData class is a simple container for job-shop problem instances. It is defined
in the file jobshop.h. Basically, it wraps an std::vector<std::vector<Task> >
container where Task is a struct defined as follows:
struct Task {
Task(int j, int m, int d) : job_id(j), machine_id(m), duration(d) {}
int job_id;
int machine_id;
int duration;
};

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

157
6.1. The Job-Shop Problem, the disjunctive model and benchmark data

if (!reader.loaded_successfully()) {
LOG(FATAL) << "Could not open job-shop file " << filename_;
}

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

Just for fun, we have written the data file corresponding to our example above in JSSP format
in the file first_example_jssp.txt:
+++++++++++++++++++++++++++++

instance tutorial_first_jobshop_example

+++++++++++++++++++++++++++++
Simple instance of a job-shop problem in JSSP format
to illustrate the working of the or-tools library
3 3
0 3 1 2 2 2
0 2 2 1 1 4
1 4 2 3

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)

158
Chapter 6. Local Search: the Job-Shop Problem

The file report_jobshopdata.cc contains a simple program to test the content of data
files for the Job-Shop Problem.

6.2 An implementation of the disjunctive model

You can find the code in the file jobshop.cc and the data in the
files first_example_jssp.txt and abz9.

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

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

The first two arguments of MakeFixedDurationIntervalVar() are a lower and an up-


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

159
6.2. An implementation of the disjunctive model

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


simply don’t exist anymore. This can happen when the IntervalVar is not consistent any-
more. By setting the argument to false, we don’t 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 re-
spected. If IntervalVar t1 is the task right before IntervalVar t2 in a job, we
can add an IntervalBinaryRelation constraint with the right relation between the two
IntervalVars. In this case, the relation is STARTS_AFTER_END:
Constraint* const prec =
solver.MakeIntervalVarRelation(t2, Solver::STARTS_AFTER_END, t1);

In the next section, we will examine other possibilities and also temporal relations between an
IntervalVar t and an integer d representing time.

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

160
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: first we rank the tasks for
each machine, then we schedule each task at its earliest start time. This is done
with two DecisionBuilders that are combined in a top-down fashion, i.e. one
DecisionBuilder is applied and then when we reach a leaf in its search tree, the second
DecisionBuilder kicks in. Since this chapter is about local search, we will use default
search strategies for both phases.
13
The factory method Solver::MakeSequenceVar(...) has been removed from the API.

161
6.2. An implementation of the disjunctive model

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


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

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

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

6.2.6 The search and first 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), ", ");
}
}

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

163
6.3. Scheduling in or-tools

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


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

Let’s try the abz9 instance:


Sol. nbr. Obj. val. Branches Time (s)
87 1015 131 733 26,756
107 986 6 242 194 1088,487
After a little bit more than 18 minutes (1088,487 seconds), the CP solver finds 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 find (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 pro-
duce goods and services. The Job-Shop problem is a good example of such problems.
Constraint programming has been proved successful in solving some Scheduling Problems
with dedicated variables and strategies [Philippe2001]. In or-tools, the CP solver offers some
variable types (IntervalVars and SequenceVars) and roughly one specialized search
strategy with some variants. This part of the CP solver is not quite as developed as the rest of
the library and expect more to come. We summarize most of the or-tools features dealing with
scheduling in this section.

This part of the CP Solver is not quite settled yet. In case of doubt, check the code.

6.3.1 Variables

Two new types of variables are added to our arsenal: IntervalVars model tasks and
SequenceVars model sequences of tasks on one machine. Once you master these vari-
ables, you can use them in a variety of different contexts but for the moment keep in mind this
modelling association.

IntervalVars

An IntervalVar variable represents an integer interval variable. It is often used in schedul-


ing to represent a task because it has:
• a starting time: s;
• a duration: d and

164
Chapter 6. Local Search: the Job-Shop Problem

• an ending time: e.
s, d and e are IntVar expressions based on the ranges these items can have. You can retrieve
these expressions with the following methods:
• IntExpr* StartExpr();
• IntExpr* DurationExpr();
• IntExpr* EndExpr();
If the corresponding IntervalVar variable is unperformed (see next sub-section), you can-
not use these methods. Well, if you do, nothing bad will happen but you will get gibberish as
the IntervalVar is no longer updated. These methods have corresponding “safe” versions
if you need them (see next sub-section).
Don’t use
• IntExpr* StartExpr();
• IntExpr* DurationExpr();
• IntExpr* EndExpr();
if the corresponding IntervalVar variable is unperformed!

The IntervalVar can be virtually conceptualized14 as in the next figure:


DurationMax()

DurationRange

DurationMin()

StartMin() StartMax() EndMin() EndMax()

StartRange 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 doesn’t necessarily correspond to the figure. Read on.

165
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 in-
stance:
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 first factory method creates a FixedInterval: its starting time, dura-
tion and ending time are all fixed. MakeFixedDurationIntervalVar() and
MakeFixedDurationIntervalVarArray() create respectively an IntervalVar
and an std::vector<IntervalVar*> with count elements. The start_min and
start_max parameters give a range for the IntervalVars to start. The duration is fixed
and equal to duration for all the variables. The optional bool indicates if the variables
can be unperformed or not. When an array is created, the name of its elements are simply
name with their position in the array (0, 1, ..., count − 1) appended, like so:
name0, name1, name2, ... .

Several other factory methods are defined in the file 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 meaning-
less). An IntervalVar is automatically marked as unperformed when it is not consistent

166
Chapter 6. Local Search: the Job-Shop Problem

anymore (starting time greater than ending time, duration < 0...). You can get and set if an
IntervalVar must, may or cannot be performed with the following methods:
virtual bool MustBePerformed() const = 0;
virtual bool MayBePerformed() const = 0;
bool CannotBePerformed() const { return !MayBePerformed(); }
bool IsPerformedBound() {
return MustBePerformed() == MayBePerformed();
}
virtual void SetPerformed(bool val) = 0;

As for the starting time, the ending time and the duration of an IntervalVar variable, its
“performedness” is encapsulated in an IntExpr you can query with:
IntExpr* PerformedExpr();

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 file 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 be-
tween 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 refined 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!

167
6.3. Scheduling in or-tools

how the IntervalVars are ranked. You can conceptualize16 this class as depicted in the
following figure:
0 1 2 3
0 0 0 0 1
Precedence matrix 1 1 0 0 1
2 1 1 0 1
3 0 0 0 0

2 1 0 3
Current assignment

Array of IntervalVars
0 1 2 3
where the precedence matrix mat is such that mat(i,j) = 1 if i is ranked before j.
The IntervalVar are often given by their indices in the array of IntervalVars.

Ranked IntervalVars

Ranked IntervalVars are exactly that: already ranked variables in the sequence.
IntervalVars can be ranked at the beginning or at the end of the sequence in the
SequenceVar variable. unperformed IntervalVar can not be ranked17 . The next fig-
ure illustrates this:
1 ? ? 2
Ranked sequence

Array of IntervalVars
0 1 2 3
Not ranked yet Ranked Ranked unperformed

IntervalVar variables 1 and 2 are ranked (and performed) while IntervalVar variable
0 may be performed but is not performed yet and IntervalVar variable 3 is unperformed
and thus doesn’t exist anymore.
To rank the IntervalVar variables, we say that we rank them first or last. First and last
IntervalVar variables must be understood with respect to the unranked variables:
16
This looks very much like the actual implementation. The array is a scoped_array<IntervalVar*>
and the precedence matrix is given by a scoped_ptr<RevBitMatrix>. The actual class contains some more
data structures to facilitate and optimize the propagation.
17
Thus, unranked variables are variables that may be performed. Yeah, three-states situations that evolves with
time are nastier than a good old Manichean one.

168
Chapter 6. Local Search: the Job-Shop Problem

Possible IntervalVar variables Possible IntervalVar variables


to be ranked first to be ranked last

42 19

()
Ra

st
n

La
kF
1 86

nk
23 6 2

irs

Ra
t
...

()
Ranked sequence

Ranked first Ranked last

• to rank first an IntervalVar variable means that this variable will be ranked before
all unranked variables and
• to rank last an IntervalVar variable means that this variable will be ranked after all
unranked variables.

Public methods

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

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

169
6.3. Scheduling in or-tools

th
• IntervalVar* Interval(int index) const: Returns the index
IntervalVar from the array of IntervalVars.
• IntVar* Next(int index) const: To each IntervalVar corresponds an
associated IntVar that represents the “ranking” of the IntervalVar in the
ranked sequence. The Next() method returns this IntVar variable for the in-
dex th IntervalVar in the array of IntervalVars.
rd
For instance, if you want to know what is the next IntervalVar after the 3
IntervalVar in the sequence, use the following code:
SequenceVar * seq = ...;
...
IntVar * next_var = seq->Next(2);
if (next_var->Bound()) { // OK, ranked
LG << "The next IntervalVar after the 3rd IntervalVar in " <<
"the sequence is " << next_var->Value() - 1;
}

As you can see, there is a difference of one between the returned value and the
actual index of the IntervalVar in the array of IntervalVars variables.
• int size() const: Returns the number of IntervalVar variables.
• void FillSequence(...): a getter filling the three std::vector<int> of
first ranked, last ranked and unperformed variables:
void FillSequence(std::vector<int>* const rank_first,
std::vector<int>* const rank_lasts,
std::vector<int>* const unperformed) const;

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

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

170
Chapter 6. Local Search: the Job-Shop Problem

• void RankNotFirst(int index): Indicates that the index th IntervalVar


variable will not be ranked first among all currently unranked IntervalVar vari-
ables.
• void RankLast(int index): Ranks the index th IntervalVar variable first
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 first among all currently unranked IntervalVar vari-
ables.
• void RankSequence(...): a setter acting on three std::vector<int> of
first, 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 first 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.

171
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 t2‘s start, i.e. End(t1) == Start(t2);
• STARTS_AFTER_START: t1 starts after t2 starts, i.e. Start(t1) >=
Start(t2);
• STARTS_AFTER_END: t1 starts after t2 ends, i.e. Start(t1) >= End(t2);
• STARTS_AT_END: t1 starts at t2‘s end, i.e. Start(t1) == End(t2);
• STARTS_AT_START: t1 starts when t2 starts, i.e. Start(t1) == Start(t2);
• STAYS_IN_SYNC: STARTS_AT_START and ENDS_AT_END combined together.
These possibilities are enclosed in the BinaryIntervalRelation enum and the factory
method is:
Constraint* Solver::MakeIntervalVarRelation(IntervalVar* const t1,
Solver::BinaryIntervalRelation r,
IntervalVar* const t2)

TemporalDisjunction constraints

TemporalDisjunction constraints ensure that two IntervalVar variables are tempo-


rally 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 first to the value an IntVar takes:
...
IntVar * const decider = ...
Constraint * ct = solver.MakeTemporalDisjunction(t1, t2, decider)

172
Chapter 6. Local Search: the Job-Shop Problem

If decider takes the value 0, then t1 has to happen before t2, otherwise it is the contrary.
This constraint works the other way around too: if t1 happens before t2, then the IntVar
decider is bound to 0 and else to a positive value (understand 1 in this case).

DisjunctiveConstraint constraints

DisjunctiveConstraint constraints are like TemporalDisjunction con-


straints 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 imple-
ment the following method:
virtual SequenceVar* MakeSequenceVar() = 0;

This method creates a SequenceVar containing the “rankable”18 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 find 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 imple-
mentation. 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 corre-
sponding intervals are filtered out, as they neither impact nor are impacted by this constraint.
Here is one factory method with a limited static capacity:
Constraint* MakeCumulative(const std::vector<IntervalVar*>& intervals,
const std::vector<int64>& demands,
18
You remember that unperformed IntervalVars are “non existing”, don’t you? And yes, we know that the
adjective “rankable” doesn’t exist...

173
6.3. Scheduling in or-tools

int64 capacity,
const string& name);

If you need more flexibility, 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 r3804, Decem-
ber 18 th 2014) there is “only one real” strategy implemented to deal with IntervalVars
and SequenceVars. The RankFirstIntervalVars DecisionBuilder for
SequenceVars and the SetTimesForward DecisionBuilder for IntervalVars
both try to rank the IntervalVars one after the other starting with the first “available” ones.
If you’re curious about the implementation details, we refer you to the code (mainly to the file
constraint_solver/sched_search.cc).
If you need specialized DecisionBuilders and Decisions, you now know the inner
working of the CP solver well enough to construct ones to suit your needs. Although nothing
prevents you from creating tools that mix IntVars, IntervalVars and SequenceVars,
we strongly advice you to keep different types of variables separated and combine different
phases together instead.

IntervalVars

For IntervalVar variables, there are two strategies implemented even if there are four en-
tries in the IntervalStrategy enum:
• INTERVAL_DEFAULT = INTERVAL_SIMPLE = INTERVAL_SET_TIMES_FORWARD:
The CP solver simply schedules the IntervalVar with the lowest starting time
(StartMin()) and in case of a tie, the IntervalVar with the lowest ending
time (StartMax()).
• INTERVAL_SET_TIMES_BACKWARD: The CP Solver simply schedules the
IntervalVar with the highest ending time (EndMax()) and in case of a
tie, the IntervalVar with the highest starting time (StartMin()).

174
Chapter 6. Local Search: the Job-Shop Problem

For the first strategy, the DecisionBuilder class is the SetTimesForward class. It
returns a ScheduleOrPostpone Decision in its Next() method. This Decision
fixes the starting time of the IntervalVar to its minimum starting time (StartMin()) in
its Apply() method and, in its Refute() method, delays the execution of the corresponding
task by 1 unit of time, i.e. the IntervalVar cannot be scheduled before StartMin() +
1.
The second strategy is quite similar and the equivalent DecisionBuilder class is the
SetTimesBackward class.
You create the corresponding phase with the good old MakePhase factory method:
DecisionBuilder * MakePhase (
const std::vector< IntervalVar * > &intervals,
IntervalStrategy str);

SequenceVars

For SequenceVar variables, there are basically two ways of choosing the next
SequenceVar to rank its IntervalVars:
• SEQUENCE_DEFAULT = SEQUENCE_SIMPLE = CHOOSE_MIN_SLACK_RANK_FORWARD:
The CP solver chooses the SequenceVar which has the fewest opportunities
of manoeuvre, i.e. the SequenceVar for which the horizon range (hmax -
hmin, see the HorizonRange() method above) is the closest to the total
maximum duration of the IntervalVars that may be performed (dmax in the
DurationRange() method above). In other words, we define the slack to be

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 first.
• CHOOSE_RANDOM_RANK_FORWARD: Among the SequenceVars for which there
are still IntervalVars to rank, the CP solver chooses one randomly. Then it
randomly chooses a rankable IntervalVar and ranks it first.
SEQUENCE_DEFAULT, SEQUENCE_SIMPLE, CHOOSE_MIN_SLACK_RANK_FORWARD
and CHOOSE_RANDOM_RANK_FORWARD are given in the SequenceStrategy enum.
To create these search strategies, use the following factory method:
DecisionBuilder* Solver::MakePhase(
const std::vector<SequenceVar*>& sequences,
SequenceStrategy str);

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


Its Next() method returns a RankFirst Decision that ranks first the selected
IntervalVar in its Apply() method and doesn’t rank it first in its Refute() method.

175
6.3. Scheduling in or-tools

We are thus assured to visit the complete search tree... of solutions of ranked IntervalVars
if needed. After the ranking of IntervalVars, the schedule is still loose and any
IntervalVar may have been unnecessarily postponed. This is so important that we use
our warning box:

After the ranking of IntervalVars, the schedule is still loose and any
IntervalVar may have been unnecessarily postponed

If for instance, you are interested in the makespan, you might want to schedule each
IntervalVar at its earliest start time. As we have seen in the previous section, this can
be accomplished by minimizing the objective function corresponding to the ending times of all
IntervalVars:
IntVar * objective_var = ...
...
DecisionBuilder* const sequence_phase = solver.MakePhase(
all_sequences,
Solver::SEQUENCE_DEFAULT);
...
DecisionBuilder* const obj_phase = solver.MakePhase(objective_var,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);

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 specific temporal constraints, you can use a data structure specialized
for scheduling: the DependencyGraph. It is meant to store simple temporal constraints and
to propagate efficiently on the nodes of this temporal graph. One node in this graph corresponds
to an IntervalVar variable. You can build constraints on the start or the ending time of the
IntervalVar nodes.
Consider again our first example (first_example_jssp.txt) and let’s say that for what-
ever reason we want to impose that the first task of job 2 must start at least after one unit of
time after the first task of job 1. We could add this constraint in different ways but let’s use the
DependencyGraph:
solver = ...
...
DependencyGraph * graph = solver.Graph();
graph->AddStartsAfterEndWithDelay(jobs_to_tasks[2][0],
jobs_to_tasks[1][0], 1);

That’s it!

176
Chapter 6. Local Search: the Job-Shop Problem

Here is the output of an optimal solution found by the solver:


Objective value: 13
Machine_0: Job 1 (0,2) Job 0 (2,5)
Machine_1: Job 2 (3,7) Job 0 (7,9) Job 1 (9,13)
Machine_2: Job 1 (2,3) Job 2 (7,10) Job 0 (10,12)

As you can see, the first task of job 2 starts at 3 units of time and the first task of job 1 ends at
2 units of time.
Other methods of the DependencyGraph include:
• AddStartsAtEndWithDelay()
• AddStartsAfterStartWithDelay()
• AddStartsAtStartWithDelay()
The DependencyGraph and the DependencyGraphNode classes are declared in the
constraint_solver/constraint_solveri.h header.

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 difficult problems. We start this section
by describing what Local Search is and what Local Search methods have in common. Then we
discuss their efficiency and compare them with global methods.
Some paragraphs are quite dense, so don’t be scared if you don’t “get it all” after the first
reading. With time and practice, the use of Local Search methods will become a second nature.

6.4.1 The basic ingredients

Local Search is a whole bunch of families of (meta-)heuristics19 that roughly share the follow-
ing ingredients:
1. They start with a solution (feasible or not);
2. They improve locally this solution;
3. They finish the search when reaching a stopping criterion but usually without any guar-
antee on the quality of the found solution(s).
We will discuss these three ingredients in details in a moment but before here are some exam-
ples of Local Search (meta-)heuristics20 :
19
If the (subtle) difference between meta-heuristics and heuristics escapes you, read the box What is it with the
word meta?.
20
The numbers are the number of results obtained on Google Scholar on August 5, 2012. There isn’t much
we can say about those numbers but we though it would be fun to show them. The search for “GRASP” or
“Greedy Adaptive Search Procedure” didn’t return any meaningful results. The methods in bold are implemented
in or-tools.

177
6.4. What is Local Search (LS)?

• Tabu Search | (62 100)


• 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 specific
problem you gather, the better you can shape your algorithm to solve efficiently your problem.
Let’s 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 find one for you with a DecisionBuilder that you provide the Local Search
algorithm with.
What if your problem is to find 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). It might sound complicated but it really isn’t.
2. They improve locally this solution:
21
See Wikipedia Reactive search optimization or reactive-search.org.
22
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
x1 6 1 by x1 6 2. This last constraint is weaker than the first one because it allows more solutions to the
problem. Of course, it is preferable to weaken constraints in a meaningful way!

178
Chapter 6. Local Search: the Job-Shop Problem

This is the tricky part to understand. Improvements to the initial solution are done locally.
This means that you need to define a neighborhood (explicitly or implicitly) for a given
solution and a way to explore this neighborhood. Two solutions can be close (i.e. they
belong to the same neighborhood) or very far apart depending on the definition of a
neighborhood.
The idea is to (partially or completely) explore a neighborhood around an initial solution,
find a good (or the best) solution in this neighborhood and start all over again until a
stopping criterion is met.
Let’s denote by Nx the neighborhood of a solution x.
In its very basic form, we could formulate Local Search like this:

Often, steps 1. and 2. are done simultaneously. This is the case in or-tools.
The following figure illustrates this process:

111111
000000
000000
111111
Nx0
N
111111111111
000000000000
x1

Nx2

x0 x1 x3 x2 x

Initial solution Local minimum Global minimum

111
000 x
000 N
111
i solution i
neighborhood
xi neighborhood of xi

This figure depicts a function f to minimize. Don’t be fooled by its 2-dimensionality.


The x-axis represents solutions in a multi-dimensional space. The z-axis represents a
1-dimensional space with the values of the objective function f .
Let’s zoom in on the neighborhoods and found solutions:

179
6.4. What is Local Search (LS)?

The Local Search procedure starts from an initial feasible solution x0 and searches the
neighborhood Nx0 of this solution. The “best” solution found is x1 . The Local Search
procedure starts over again but with x1 as starting solution. In the neighborhood Nx1 , the
best solution found is x2 . The procedure continues on and on until stopping criteria are
met. Let’s say that one of these criteria is met and the search ends with x3 . You can see
that while the method moves towards the local optima, it misses it and completely misses
the global optimum! This is why the method is called local search: it probably will find
a local optimum (or come close to) but it is unable to find a global optimum (except by
chance).
If we had continued the search, chances are that our procedure would have iterated around
the local optimum. In this case, we say that the Local Search algorithm is trapped by a
local optimum. Some LS methods - like Tabu Search - were developed to escape such
local optimum but again there is no guarantee whatsoever that they can succeed.
The figure above is very instructive. For instance, you can see that neighborhoods
don’t have to be of equal size or centred around a variable xi . You can also see
that the relationship “being in the neighborhood of” is not necessarily symmetric:
x1 ∈ Nx0 but x0 6∈ Nx1 24 ! In or-tools, you define a neighborhood by implementing
the MakeNextNeighbor() callback method 25 from a LocalSearchOperator:
every time this method is called internally by the solver, it constructs one so-
lution of the neighborhood If you have constructed a successful candidate, make
MakeNextNeighbor() returns true. When the whole neighborhood has been vis-
ited, make it returns false.
3. They finish the search when reaching a stopping criterion but usually without any
guarantee on the quality of the found solution(s):
Common stopping criteria include:
• time limits:
– for the whole solving process or
– for some parts of the solving process.
• maximum number of steps/iterations:
– maximum number of branches;
– maximum number of failures;
24
To be fair, we have to mention that most LS methods require this relation to be symmetric as a desirable
feature. If this relation is symmetric, we would be to be able to retrace our steps in case of a false start or to
explore other possibilities. On the figure, you might think about going left to explore what is past the z − axis.
25
Well almost. The MakeNextNeighbor() callback is really low level and we have alleviate the task by
offering other higher level callbacks. See section 6.6 for more details.

180
Chapter 6. Local Search: the Job-Shop Problem

– 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 specific problem, you have to find out what parts
of solution you can combine and how you can combine them. A meta-heuristic gives you
guidelines to construct your algorithm.
It’s a recipe on how to write a recipe. You have one level of indirection like in meta-
programming where you write code to generate code.
a
See Wikipedia meta for the meaning of the word.

6.4.2 Is Local Search efficient?

In two words: yes but...26


Let’s dissect this terse answer:
• yes:
To really answer this question, you need to know what exactly you mean by “efficient”.
If you’re 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.

181
6.4. What is Local Search (LS)?

• but...:
Local search methods are strongly dependent on your knowledge of the problem and
your ability to use this knowledge during the search. For instance, very often the initial
solution plays a crucial role in the efficiency of the Local Search. You might start from a
solution that is too far from a global (or local) optimum or worse you start from a solution
from which it is impossible to reach a global (or even local) optimum with your neighbor-
hood definition. Several techniques have been proposed to tackle these annoyances. One
of them is to restart the search with different initial solutions. Another is to change the
definition of a neighborhood during the search like in Variable Neighbourhood Search
(VNS).
LS is a tradeoff between efficiency and the fact that LS doesn’t try to find a global optimum,
i.e. in other words you are willing to give up the idea of finding a global optimum for the
satisfaction to quickly find a (hopefully good) local optimum.

A certain blindness
LS methods are most of the time really blind when they search. Often you hear the analogy
between LS methods and descending a hilla to find the lowest point in a valley (when we
minimize a function). It would be more appropriate to compare LS methods with going
down a valley flooded by mist: you don’t see very far in what direction to go to continue
downhill. Sometimes you don’t see anything at all and you don’t even know if you are
allowed to set a foot in front of you!
a
If you’ve never heard this metaphor, skip this paragraph and don’t bother.

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 don’t have a clue of what we are doing and we just
hope for the best.
Most of the time, we face two non satisfactory situations:
• a good guarantee is expensive to compute (sometimes as expensive as finding a good
solution or even more!);
• a guarantee that isn’t very expensive to compute but that is close to being useless.
In either cases, it is not worth computing this guarantee27 .
Not having a theoretical guarantee on the quality of a solution doesn’t mean that the solution
found is not a good solution (it might even be the best solution), just that we don’t know how
good (or bad) this solution is!
27
Not to mention that some classes of problems are mathematically proven to have no possible guarantee on
their solution at all! (or only if P = NP).

182
Chapter 6. Local Search: the Job-Shop Problem

What do we mean by a guarantee on the solution?


Several concepts of guarantee have been developed. We will not go into details about the
concept of guarantee but let’s give an example. In a now famous report [Christofides1976],
Christofides proposed and analyzed a heuristic that is guaranteed to solve the metric Trav-
elling Salesman Problema 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
The metric TSP is the classical TSP but on graphs that respect the triangle inequality, i.e. d(a, c) 6
d(a, b) + d(b, c) where a, b and c are nodes of the graph and d() 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) efficient local methods that (hopefully) lead to local optima.
Could we have the best of these two worlds?
You’ve guessed it: we use global methods to find portions of the search space that might contain
good or even optimal solutions and we try to find those with Local Search methods. As always,
there is a tradeoff between the two.
To take again an analogy29 , looking for a good solution this way is a bit like trying to find crude
oil (or nowadays tar sands and the like): you send engineers, geologists, etc. to some places on
earth to prospect (global method). If they find a promising spot, you send a team to drill and
find out (local method).

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
Search. It’s good to keep them in memory for the rest of this section. We then overview the im-
plementation and describe some of its main components. Finally, we detail the inner workings
28
Tabu Search, Simulated Annealing, Guided Local Search and the like were designed to overcome some
shortcomings of Local Search methods. Depending on the problem and how they are implemented, these methods
can also be seen as Global Search methods.
29
As all analogies, this one has certainly its limits!

183
6.5. Basic working of the solver: Local Search

of the Local Search algorithm and indicate where the callbacks of the SearchMonitors are
called.
We present a simplified version of the Local Search algorithm. Yes, this is well worth a warning
box!

We describe a simplified 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 don’t do anything in
their right branches). As its name implies, the FindOneNeighbor DecisionBuilder
tries to find 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! Let’s summarize all this in the next picture:
LocalSearch::Next()

NestedSolveDecision

NestedSolveDecision::Apply(){ NestedSolveDecision::Refute(){}
SolveAndCommit(FindOneNeighbor(ls));
}

ls is the LocalSearchOperator that constructs the candidate solutions. The search tree
very quickly becomes completely unbalanced if we only keep finding solutions in the left
branches. We’ll see a balancing mechanism that involves one BalancingDecision at the
end of this section.
Speaking about candidate solutions, let’s agree on some wordings. The next figure presents the
beginning of a Local Search. x0 is the initial solution. In or-tools, this solution is given by an
Assignment or a DecisionBuilder that the LocalSearch class uses to construct this
initial solution. x0 , x1 , x2 , . . . are solutions. As we have seen, the Local Search algorithm moves
from one solution to another. It takes a starting solution xi and visit the neighborhood defined
around xi to find the next solution xi+1 . By visiting the neighborhood, we mean constructing
and testing feasible solutions y0 = xi , y1 , y2 , . . . of this neighborhood. We call these solutions
candidate solutions. In the code, they are called neighbors. The LocalSearchOperator

184
Chapter 6. Local Search: the Job-Shop Problem

produces these candidates and the FindOneNeighbor DecisionBuilder filter these out
to keep the interesting candidate solutions only. When a stopping criteria is met or the neigh-
borhood has been exhausted, the current solution of the CP solver is the next starting solution.
Let’s illustrate this:

111111
000000
111111111111
000000000000
000000
111111
Nx0
Nx1

Candidate solutions

y1 y0 y2 y3 y4 y5
x0 x1
Initial solution Current solution = starting solution for Nx1

The code consistently use the term neighbor to denote what we call a candidate solution in this
manual. We prefer to emphasize the fact that this neighbor solution is in fact a feasible solution
that the CP solver tests and accepts or rejects.

In this manual, we use the term candidate solution for what is consistently called a
neighbor in the code.

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 define the
current Local Search.
• LocalSearchOperators: This class is responsible of constructing the candidate so-
lutions.
• FindOneNeighbor: This DecisionBuilder filters the candidate solutions given
by the LocalSearchOperator and only constructs filtered 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 filter allows to immediately skip (discard) a candidate
solution. It is used by FindOneNeighbor to filter the candidate solutions.
We will not discuss the filtering mechanism here (see the dedicated section Filtering).

185
6.5. Basic working of the solver: Local Search

6.5.2 Overview of the Local Search Mechanism in or-tools

The next figure 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.
Let’s describe some pieces of the or-tools mechanism for Local Search:
• initial solution: we need a feasible solution to start with. You can either pass an
Assignment or a DecisionBuilder to the LocalSearch‘s constructor.
• LocalSearchPhaseParameters: the LocalSearchPhaseParameters parameter
holds the actual definition of the Local Search phase:
– a SolutionPool that keep solution(s);
– a LocalSearchOperator used to explore the neighborhood of the current
solution. You can combine several LocalSearchOperators into one
LocalSearchOperator;
30
In the code, you are only allowed to use one LocalSearchOperator but you can combine several
LocalSearchOperators in one LocalSearchOperator. This is a common pattern in the code.
31
MakeOneNeighbor() is a convenient method. The real method to create a new candidate is
MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) but you have to deal
with the low level delta and deltadelta. We discuss these details in the section LocalSearchOperators:
the real thing!.
32
By default, the solver accepts the first feasible solution and repeats the search starting with this new solution.
The idea is that if you combine the Local Search with an ObjectiveVar, the next feasible solution will be
a solution that beats the current best solution. You can change this behaviour with a SearchLimit. Read
on. The LocalSearch class is also deeply coupled to the Metaheuristic class or more generally to a
SearchMonitor. This is the subject of next chapter.

186
Chapter 6. Local Search: the Job-Shop Problem

– a complementary DecisionBuilder to instantiate unbound variables once an (in-


complete) candidate solution has been defined by the LocalSearchOperator.
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 find 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 (or not
desirable).
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

You can find the code in the file dummy_ls.cc.

To start the Local Search, we need an initial feasible solution. We can either give a
starting solution or we can ask the CP solver to find one for us. To let the solver find a
solution for us, we pass to it a DecisionBuilder. The first solution discovered by this
DecisionBuilder will be taken as the initial solution.
There is a factory method for each one of the two options:
DecisionBuilder* Solver::MakeLocalSearchPhase(Assignment* assignment,
LocalSearchPhaseParameters* parameters)

DecisionBuilder* Solver::MakeLocalSearchPhase(
const std::vector<IntVar*>& vars,
DecisionBuilder* first_solution,
LocalSearchPhaseParameters* parameters)

In the file dummy_ls.cc, we use a gflags flag 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 definition of the Local


Search phase.

187
6.5. Basic working of the solver: Local Search

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 imple-
mentation is the DefaultSolutionPool that only keeps the current solution. You
don’t 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 fea-
sible solutions if your LocalSearchOperators only return partial solutions, i.e. so-
lutions 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 don’t 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 neighbor-
hood. By default, the CP solver stops the neighborhood search as soon as it finds a filtered
and feasible candidate solution. If you add an OptimizeVar to your model, once the
solver finds this good candidate solution, it changes the model to exclude solutions with
the same objective value. The second solution found can only be better than the first one.
See section 3.9 to refresh your memory if needed. When the solver finds 2 solutions (or
33
Well, you could devise another way to keep track of the solutions and take care of their existence but anyhow,
you are responsible for these solutions.

188
Chapter 6. Local Search: the Job-Shop Problem

when the whole neighborhood is explored), it stops and starts over again with the best
solution.
• LocalSearchFilters: these filters speed up the search by bypassing the solver
checking mechanism if you know that the solution must be rejected (because it is not
feasible, because it is not good enough, ...). If the filters accept a solution, the solver
still tests the feasibility of this solution. LocalSearchFilters are discussed in sec-
tion 6.8.
Several factory methods are available to create a LocalSearchPhaseParameters pa-
rameter. At least you need to declare a LocalSearchOperator and a complementary
DecisionBuilder:
LocalSearchPhaseParameters * Solver::MakeLocalSearchPhaseParameters(
LocalSearchOperator *const ls_operator,
DecisionBuilder *const
complementary_decision_builder);

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 find candidate solutions while the


complementary_decision_builder DecisionBuilder will complete the
candidate solutions if some of the variables are not assigned.
A handy way to create a DecisionBuilder to assist the Local Search operator(s) is to limit
one with MakeSolveOnce(). MakeSolveOnce returns 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 first solution is found. If there are
no solutions in this nested tree, then (the Next() method of) SolveOnce will fail.

6.5.3 The basic Local Search algorithm and the callback hooks for
the SearchMonitors

We feel compelled to use our warning box again:

189
6.5. Basic working of the solver: Local Search

We describe a simplified version of the Local Search algorithm.

If you want to know more, have a look at the section Local Search (LS) in the chapter Under
the hood.

In this subsection, we present the callbacks of the SearchMonitor listed in Table 6.1
and show you exactly when they are called in the search algorithm.

Table 6.1: Local Search algorithm callbacks from the SearchMonitor class.
Methods Descriptions
LocalOptimum() When a local optimum is reached. If true is
returned, the last solution is discarded and the
search proceeds to find the next local optimum.
Handy when you implement a meta-heuristic with a
SearchMonitor.
AcceptDelta(Assignment *delta, When the LocalSearchOperator has produced
Assignment *deltadelta) the next candidate solution given in the form of
delta and deltadelta. You can accept or reject
this new candidate solution.
AcceptNeighbor() After accepting a candidate solution during Local
Search.
PeriodicCheck() Periodic call to check limits in long running search
procedures, like Local Search.

To ensure the communication between the Local Search and the Global Search, three utility
functions are defined. These functions simply call their SearchMonitor‘s counterparts, i.e.
they call the corresponding methods of the involved SearchMonitors:
• bool LocalOptimumReached(): FalseExceptIfOneTrue.
• bool AcceptDelta(): TrueExceptIfOneFalse.
• void AcceptNeighbor(): Notification.
Before we delve into the core of the Local Search algorithm and the implementation
of the LocalSearch DecisionBuilder‘s Next() method, we first discuss the in-
ner workings of the FindOneNeighbor DecisionBuilder whose job is to find the
next filtered and accepted candidate solution. This DecisionBuilder is used inside a
NestedSolveDecision that we study next. This Decision is returned by the Next()
method of the LocalSearch DecisionBuilder in the main loop of the Local Search
algorithm. Finally, we address the LocalSearch DecisionBuilder class. In particular,
we study its initializing phase and its Next() method. We consider the case where an initial
DecisionBuilder constructs the initial solution.
SearchMonitor‘s callbacks are indicated in the code by the comment:
// SEARCHMONITOR CALLBACK

190
Chapter 6. Local Search: the Job-Shop Problem

The FindOneNeighbor DecisionBuilder

This DecisionBuilder tries to find the next filtered and accepted candidate so-
lution. It tests (and sometimes completes) the candidate solutions given by the
LocalSearchOperator.
We present its Next() method and discuss it after:
1 Decision* FindOneNeighbor::Next(Solver* const solver) {
2

3 // No neighbor (candidate solution) found


4 // only on the first call to Next().
5 if (!neighbor_found_) {
6 // SYNCHRONIZE ALL
7 ...
8 }
9

10 // Another assignment is needed to apply the delta


11 Assignment* assignment_copy =
12 solver->MakeAssignment(reference_assignment_.get());
13 int counter = 0;
14

15 DecisionBuilder* restore =
16 solver->MakeRestoreAssignment(assignment_copy);
17 if (sub_decision_builder_) {
18 restore = solver->Compose(restore, sub_decision_builder_);
19 }
20 Assignment* delta = solver->MakeAssignment();
21 Assignment* deltadelta = solver->MakeAssignment();
22

23 // MAIN LOOP
24 while (true) {
25 delta->Clear();
26 deltadelta->Clear();
27 // SEARCHMONITOR CALLBACK
28 solver->TopPeriodicCheck();
29 if (++counter >= FLAGS_cp_local_search_sync_frequency &&
30 pool_->SyncNeeded(reference_assignment_.get())) {
31 // SYNCHRONIZE ALL
32 ...
33 counter = 0;
34 }
35

36 if (!limit_->Check()
37 && ls_operator_->MakeNextNeighbor(delta, deltadelta)) {
38 solver->neighbors_ += 1;
39 // SEARCHMONITOR CALLBACK
40 const bool meta_heuristics_filter =
41 AcceptDelta(solver->ParentSearch(), delta, deltadelta);
42 const bool move_filter = FilterAccept(delta, deltadelta);
43 if (meta_heuristics_filter && move_filter) {
44 solver->filtered_neighbors_ += 1;
45 assignment_copy->Copy(reference_assignment_.get());
46 assignment_copy->Copy(delta);
47 if (solver->SolveAndCommit(restore)) {

191
6.5. Basic working of the solver: Local Search

48 solver->accepted_neighbors_ += 1;
49 assignment_->Store();
50 neighbor_found_ = true;
51 return NULL;
52 }
53 }
54 } else {
55 if (neighbor_found_) {
56 // SEARCHMONITOR CALLBACK
57 AcceptNeighbor(solver->ParentSearch());
58 pool_->RegisterNewSolution(assignment_);
59 // SYNCHRONIZE ALL
60 ...
61 } else {
62 break;
63 }
64 }
65 }
66 solver->Fail();
67 return NULL;
68 }

You might wonder why there are so many lines of code but there are a some subtleties to
consider.
The code of lines 5 to 8 is only called the first time the Next() method is invoked and allow
to synchronize the Local Search machinery with the initial solution. In general, the words
SYNCHRONIZE ALL in the comments mean that we synchronize the Local Search Operators
and the Local Search Filters with a solution.
reference_assignment_ is an Assignment with the initial solution while
assignment_ is an Assignment with the current solution. On line 10, we copy
reference_assignment_ to the local assignment_copy Assignment to be able
to define the deltas. counter counts the number candidate solutions. This counter is used
on line 29 to test if we shouldn’t start again the Local Search with another solution.
On lines 15-19, we define the restore DecisionBuilder that will allow us to keep the
newly found candidate solution.
We construct the delta and deltadelta on lines 20 and 21 and are now ready to enter the
main loop to find the next solution.
On lines 25 and 26 we clear our deltas and on line 28 we allow for a periodic check: for
searches that last long, we allow the SearchMonitors to interfere and test if the search
needs to continue or not and/or must be adapted.
Lines 29-34 allow to change the starting solution and ask the solution
pool pool_ for a new solution via its GetNextSolution(). The
FLAGS_cp_local_search_sync_frequency value corresponds to the number
of attempts before the CP solver tries to synchronize the Local Search with a new solution.
On line 36 and 37, the SearchLimits applied to the search of one neighborhood are tested.
If the limits are not reached and if the LocalSearchOperator succeeds to find a new
candidate solution, we enter the if statement on line 38. The LocalSearchOperator‘s

192
Chapter 6. Local Search: the Job-Shop Problem

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.
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 fills the deltas after you use the helper methods
SetValue(), Activate() and the like to change the current candidate solution. Once
we enter the if statement on line 38, we have a new candidate solution and we up-
date the solution counter accordingly. It is now time to test this new solution candi-
date. The first test comes from the SearchMonitors in their AcceptDelta() meth-
ods. If only one SearchMonitor rejects this solution, it is rejected. In or-tools,
we implement (meta-)heuristics with SearchMonitors. See chapter 7 for more. The
AcceptDelta() function is the global utility function we mentioned above. We’ll meet
LocalOptimumReached() and AcceptNeighbor() a few lines below.
The second test is the filtering test on line 42. FilterAccept() returns a
TrueExceptIfOneFalse. If both tests are successful, we enter the if statement on
line 44. If not, we simply generate another candidate solution. On lines 44 and 46, we
update the counter of filtered_neighbors_ and store the candidate solution in the
assignment_copy Assignment.
On line 47, we try (and if needed complete) the candidate. If we succeed, the current solution
and the counter accepted_neighbors_ are updated. The Next() method returns NULL
because the FindOneNeighbor DecisionBuilder has finished its job at this node of
the search tree. If we don’t succeed, the solver fails on line 66.
The SolveAndCommit() method is similar to the Solve() method except that
SolveAndCommit will not backtrack all modifications at the end of the search and this is
why you should:

Use the SolveAndCommit() method only in the Next() method of a


DecisionBuilder!

193
6.5. Basic working of the solver: Local Search

If the if test on line 36 and 37 fails, we enter the else part of the statement on line 55. This
means that either one SearchLimit was reached or that the neighborhood is exhausted. If a
solution (stored in assignment_) was found during the Local Search, we register it and syn-
chronize the LocalSearchOperators and LocalSearchFilters with a new solution
provided by the solution pool pool_ on lines 58-60. We also notify the SearchMonitors
on line 57. If no solution was found, we simply break out of the while() loop on line 62
and make the CP solver fail on line 66.

The NestedSolveDecision Decision

The NestedSolveDecision is the Decision that the LocalSearch‘s Next()


method returns to find the next solution. This Decision is basically a Decision wrap-
per around a nested solve with a given DecisionBuilder and SearchMonitors. It
doesn’t 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 Meaning
DECISION_FAILED The nested search phase failed, i.e. Solve() or
SolveAndCommit() failed.
DECISION_PENDING The nested search hasn’t been called yet. The Local Search
is in this state when it balances the search tree.
DECISION_FOUND The nested search phase succeeded and found a solution,
i.e. Solve() or SolveAndCommit() succeeded and
returned true.
The three states are defined in the NestedSolveDecision StateType enum.
We are now ready to assemble all the pieces of the puzzle together to understand the (simplified)
Local Search algorithm in or-tools.

The LocalSearch DecisionBuilder

We first consider the initialization phase and then we discuss in details its Next() method.

Initialization

Consider the situation where we already have a LocalSearchPhaseParameters param-


eter 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 = ...

194
Chapter 6. Local Search: the Job-Shop Problem

...
LocalSearchPhaseParameters params =
s.MakeLocalSearchPhaseParameters(ls_operator,
complementary_decision_builder);

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 com-
pleted by the complementary_decision_builder DecisionBuilder if needed).
Remember, that the solution chosen by the CP solver is the first 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);

It’s interesting to see how this initial solution is constructed in the LocalSearch class. First,
we create an Assignment to store this initial solution:
Assignment * const initial_sol = s.MakeAssignment();

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 this initial solution. This


DecisionBuilder is used in a nested search:
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;

195
6.5. Basic working of the solver: Local Search

• the NestedSolveDecision constructor’s arguments are respectively:


– a DecisionBuilder to construct the next solution;
– a bool to indicate if we restore the last solution in case we cannot find 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 find and test the initial solution, we are ready to
wrap the nested solve process into a NestedSolveDecision:
// Main DecisionBuilder to find candidate solutions one by one
DecisionBuilder* find_neighbors =
solver->RevAlloc(new FindOneNeighbor(assignment_,
pool_,
ls_operator_,
sub_decision_builder_,
limit_,
filters_));
NestedSolveDecision* decision = solver->RevAlloc(
new NestedSolveDecision(find_neighbors,
false)));

The boolean argument in the NestedSolveDecision‘s constructor indicates that we


don’t want to backtrack. The decision Decision will thus call SolveAndCommit() in
its left branch.

196
Chapter 6. Local Search: the Job-Shop Problem

The Next() method

The Next() method of the LocalSearch DecisionBuilder is in charge of controling


the Local Search. We present it first and discuss it next:
1 Decision * Next(Solver * solver) {
2 ...
3 const int state = decision->state();
4 switch (state) {
5 case NestedSolveDecision::DECISION_FAILED: {
6 // SEARCHMONITOR CALLBACK: LocalOptimum()
7 if (!LocalOptimumReached(solver->ActiveSearch())) {
8 // Stop the current search
9 ...
10 }
11 solver->Fail();
12 return NULL;
13 }
14 case NestedSolveDecision::DECISION_PENDING: {
15 // Stabilize search tree by balancing the current search tree.
16 // Statistics are updated even if this is not relevant to the
17 // global search
18 ...
19 const int depth = solver->SearchDepth();
20 if (depth < kLocalSearchBalancedTreeDepth) {
21 return solver->balancing_decision();
22 } else if (depth > kLocalSearchBalancedTreeDepth) {
23 solver->Fail();
24 }
25 ...
26 return decision;
27 }
28 case NestedSolveDecision::DECISION_FOUND: {
29 // Nothing important for us in this simplified version
30 ...
31 return NULL;
32 }
33 default: {
34 LOG(ERROR) << "Unknown local search state";
35 return NULL;
36 }
37 }
38 return NULL;
39 }

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 op-
timum has been reached and cannot be improved. LocalOptimum() returns a
FalseExceptIfOneTrue.
• Line 14: case DECISION_PENDING: This is the most interesting case: we

197
6.6. Local Search Operators

try to keep the search tree balanced and force its height to be bounded.
kLocalSearchBalancedTreeDepth is set to 32. So as long as the tree
height is smaller than 32, the LocalSearch DecisionBuilder returns the same
BalancingDecision on line 21. BalancingDecisions don’t do anything by de-
fault. Once the search tree height is over 32, the NestedSolveDecision Decision
enters in action and when the height of the three gets higher than 32, we make the CP
solver Fail() to backtrack on line 23 thus keeping the height of the tree bounded.
• Line 28: case DECISION_FOUND: The nested search found a solution that is the cur-
rent solution. The LocalSearch‘s Next() method has done its job at the current
node and nothing needs to be done.

Solve(), SolveAndCommit(), SolveOnce(), etc...: what are the differences?


This topic is so important that the whole section 12.4 is devoted to it. You already can
jump ahead and read this section if you’re curious.

6.6 Local Search Operators

You can find the code in the file 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 fictive example consists in minimizing the sum of n IntVars {x0 , . . . , xn−1 } each with
domain [0, n − 1]. We add the fictive constraint x0 > 1 (and thus ask for n > 2):

min x0 + x1 + ... + xn−1


x0 ,...,xn−1

subject to: x0 > 1.


xi ∈ {0, . . . , n − 1} for i = 0 . . . n − 1.

Of course, we already know the optimal solution. Can we find it by Local Search?

6.6.1 LocalSearchOperators

The base class for all Local Search operators is LocalSearchOperator. The behaviour
of this class is similar to that of an iterator. The operator is synchronized with a feasible
solution (an Assignment that gives the current values of the variables). This is done in the
Start() method. Then one can iterate over the candidate solutions (the neighbors) using the
MakeNextNeighbor() method. Only the modified part of the solution (an Assignment
called delta) is broadcast. You can also define a second Assignment representing the
changes to the last candidate solution defined by the Local Search operator (an Assignment
called deltadelta).
The CP solver takes care of these deltas and other hassles for the most common cases34 .
34
deltas and deltadeltas are explained in more details in section 6.8.

198
Chapter 6. Local Search: the Job-Shop Problem

The next figure shows the LS Operators hierarchy.

LocalSearchOperator

VarLocalSearchOperator

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 Opera-
tors. We will review them in subsection 9.7.3 35 .

IntVarLocalSearchOperator is a specialization36 of LocalSearchOperator


built for an array of IntVars while SequenceVarLocalSearchOperator is a
specialization of LocalSearchOperator built for an array of SequenceVars37 .

6.6.2 Defining a custom LS operator

We will construct an LS Operator for an array of IntVars but the API for an array of
SequenceVars is similar38 .
There are two methods to overwrite:
• OnStart(): this private method is called each time the operator is synced with a
new feasible solution;
• MakeOneNeighbor(): this protected method creates a new feasible solution. As
long as there are new solutions constructed it returns true, false otherwise.
Some helper methods are provided:
• int64 Value(int64 index): returns the value in the current Assignment of
the variable of given index;
• int64 OldValue(int64 index): returns the value in the last Assignment (the
initial solution or the last accepted solution) of the variable of given index;
35
The PathOperator classes have some particularities. For instance, they use a special customized
MakeNeighbor() method instead of the MakeOneNeighbor() method.
36
The VarLocalSearchOperator class is the real base operator class for operators manipulating vari-
ables but its existence for our understanding of the LocalSearchOperators is irrelevant here.
37
At the time of writing, December 18 th , 2014, there are no LocalSearchOperators defined for
IntervalVars.
38
For instance, the SetValue() method is replaced by the SetForwardSequence() and
SetBackwardSequence() methods.

199
6.6. Local Search Operators

• 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 redefine 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
solution(s) again and again or if you don’t provide any solution, the solver will not detect it (in
the second case, the solver will enter an infinite 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 don’t have to test for feasibility: it’s 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 filter 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 [x0 , x1 , . . . , xn−1 ] is made of the following solutions
(when feasible):

{[x0 , x1 , . . . , xn−1 ], [x0 − 1, x1 , . . . , xn−1 ], [x0 , x1 − 1, . . . , xn−1 ], . . . , [x0 , x1 , . . . , xn−1 − 1]}

The given initial solution is also part of the neighborhood.

200
Chapter 6. Local Search: the Job-Shop Problem

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 de-
crease, 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 first variable x0 by 1.
We use the LS Operator DecreaseOneVar in the function SimpleLS() that starts as fol-
low:
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));
...

n must be greater or equal to 2 as we ask for x0 > 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 without
knowing where to go.
Next, based on the Boolean variable FLAG_initial_phase, we create a
DecisionBuilder to find 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);
}
}
}

201
6.6. Local Search Operators

As we assign the biggest value (ASSIGN_MAX_VALUE) to the first unbound variables


(CHOOSE_FIRST_UNBOUND), the initial solution constructed by the DecisionBuilder
will be

[n − 1, n − 1, . . . , n − 1].

To have some variation, we construct the following initial solution by hand:

[n − 1, n − 2, n − 1, n − 2, . . . , n − {1 + (n + 1) mod 2}]

where the value for xn−1 is n − 2 if n is even and n − 1 otherwise39 .


The search phase using the LS Operator is given by a... DecisionBuilder which shouldn’t
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:

39
The modulo operator (mod) finds the remainder of the division of one (integer) number by another: For
instance, 11 mod 5 = 1 because 11 = 2 × 5 + 1. When you want to test a positive number n for parity, you can
test n mod 2. If n mod 2 = 0 then n is even, otherwise it is odd. In C++, the mod operator is %.

202
Chapter 6. Local Search: the Job-Shop Problem

./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, ...)
Solution #2 (objective value = 8, ...)
Solution #3 (objective value = 7, ...)
Solution #4 (objective value = 6, ...)
Solution #5 (objective value = 5, ...)
Solution #6 (objective value = 4, ...)
Solution #7 (objective value = 3, ...)
Solution #8 (objective value = 2, ...)
Solution #9 (objective value = 1, ...)
Finished 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 bet-
ter solution with value 7;
neighborhood 4 around [1, 1, 3, 2]: [0, 1, 3, 2] is rejected as infeasible, [1, 0, 3, 2] is a new bet-
ter solution with value 6;
neighborhood 5 around [1, 0, 3, 2]: [0, 0, 3, 2], [0, −1, 3, 2] are rejected as infeasible,
[1, 0, 2, 2] is a new better solution with value 5;
neighborhood 6 around [1, 0, 2, 2]: [0, 1, 2, 2], [1, −1, 2, 2] are rejected as infeasible,
[1, 0, 1, 2] is a new better solution with value 4;
neighborhood 7 around [1, 0, 1, 2]: [0, 0, 1, 2], [1, −1, 1, 2] are rejected as infeasible,
[1, 0, 0, 2] is a new better solution with value 3;
neighborhood 8 around [1, 0, 0, 2]: [0, 0, 0, 2], [1, −1, 0, 2], [1, 0, −1, 2] are rejected as infea-
sible, [1, 0, 0, 1] is a new better solution with value 2;
neighborhood 9 around [1, 0, 0, 1]: [0, 0, 0, 1], [1, −1, 0, 1], [1, 0, −1, 1] are rejected as infea-
sible, [1, 0, 0, 0] is a new better solution with value 1;

203
6.6. Local Search Operators

At this point, the solver is able to recognize that there are no more possibilities. The two last
lines printed by the SearchLog summarize the Local Search:
Finished search tree, ..., neighbors = 23, filtered neighbors = 23,
accepted neighbors = 9, ...)
End search (time = 1 ms, branches = 67, failures = 64, memory used =
15.13 MB, speed = 67000 branches/s)

There were indeed 23 constructed candidate solutions among which 23 (filtered neighbors)
were accepted after filtering and 9 (accepted neighbors) were improving solutions.
If you take the last visited neighborhood (neighborhood 9), you might wonder if it was really
necessary to construct “solutions” [0, 0, 0, 1], [1, −1, 0, 1] and [1, 0, −1, 1] and let the solver
decide if they were interesting or not. The answer is no. We could have filtered those solutions
out and told the solver to disregard them. We didn’t filter out any solution (and this is the reason
why the number of constructed neighbors is equal to the number of filtered neighbors). You
can learn more about filtering in the section Filtering.
If you want, you can try to start with the solution provided by the DecisionBuilder
([3, 3, 3, 3] when n = 4) and see if you can figure out what the 29 constructed candidate solu-
tions (neighbors) and 11 accepted solutions are.

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


erator 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
first 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 first argument of the
callback is the index of the operator which produced the last move, the second argument is the
index of the operator to be evaluated. Ownership of the callback is taken by the solver.
Here is an example:
const int kPriorities = {10, 100, 10, 0};
int64 Evaluate(int active_operator, int current_operator) {
return kPriorities[current_operator];
}

204
Chapter 6. Local Search: the Job-Shop Problem

LocalSearchOperator* concat =
solver.ConcatenateOperators(operators,
NewPermanentCallback(&Evaluate));

The elements of the operators’ vector will be sorted by increasing priority and explored in
that order (tie-breaks are handled by keeping the relative operator order in the vector). This
would result in the following order:
operators[3], operators[0], operators[2], operators[1].
Sometimes you don’t know in what order to proceed. Then the following method might help
you:
LocalSearchOperator* Solver::RandomConcatenateOperators(
const std::vector<LocalSearchOperator*>& ops);

This LocalSearchOperator calls a random operator at each call to


MakeNextNeighbor(). You can provide the seed that is used to initialize the ran-
dom 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:

205
6.7. The Job-Shop Problem: and now with Local Search!

LocalSearchOperator* Solver::MakeMoveTowardTargetOperator(
const Assignment& target);

and
LocalSearchOperator* Solver::MakeMoveTowardTargetOperator(
const std::vector<IntVar*>& variables,
const std::vector<int64>& target_values);

The target is here given by two std::vectors: a vector of variables and a vector of associ-
ated target values. The two vectors should be of the same length and the variables and values
are ordered in the same way.
The variables are changed one after the other in the order given by the Assignment or the
vector of variables. When we restart from a new feasible solution, we don’t start all over again
from the first variable but keep changing variables from the last change.

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

6.7 The Job-Shop Problem: and now with Local Search!

You can find the code in the files jobshop_ls.h, jobshop_ls1.cc,


jobshop_ls2.cc and jobshop_ls3.cc and the data files
in first_example_jssp.txt and abz9.

We have seen in the previous section how to implement Local Search on our dummy

206
Chapter 6. Local Search: the Job-Shop Problem

example. This time, we apply Local Search on a real problem and present the real thing: the
MakeNextNeighbor() method, the delta and deltadelta Assignments40 .
To solve the Job-Shop Problem, we’ll define two basic LocalSearchOperators. First,
we’ll apply them separately and then we’ll combine them to get better results. In doing so, we
will discover that Local Search is very sensitive to the initial solution used to start it and that
the search is path-dependent.

6.7.1 LocalSearchOperators: the real thing!

Until now, we only have redefined the MakeOneNeighbor() 41 method of


a LocalSearchOperator but as we have seen in sub-section 6.5.3, the real method
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: efficiency. Only the modi-
fied part of the solution is broadcast:
• Delta: the difference between the initial solution that defines the neighborhood and the
current candidate solution.
• DeltaDelta: the difference between the current candidate solution and the previous
candidate solution.
Delta and DeltaDelta are just Assignments only containing the changes.

MakeNextNeighbor()

The signature of the MakeNextNeighbor() method is:


bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta)

This method constructs the delta and deltadelta corresponding to the new can-
didate solution and returns true. If the neighborhood has been exhausted, i.e. the
LocalSearchOperator cannot find another candidate solution, this method returns
false.
When you write your own MakeNextNeighbor() method, you have to provide the new
delta but you can skip the deltadelta if you prefer. This deltadelta can be con-
venient when you define your filters and you can gain some efficiency over the sole use of
deltas.
To help you construct these deltas, we provide an inner mechanism that constructs automat-
ically these deltas when you use the following self-explanatory setters:
40
But not how to implement incremental LocalSearchOperators. This is a very advanced feature.
41
Remember our footnote about the PathOperator classes? They use a special customized
MakeNeighbor() method instead of the MakeOneNeighbor() method.

207
6.7. The Job-Shop Problem: and now with Local Search!

• 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).
If you only use these methods to change the current solution, you then can automatically con-
struct the deltas by calling the ApplyChanges() method and revert these changes by
calling the RevertChanges() method.
We recommend to use the following template to define your MakeNextNeighbor()
method:
virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (NEIGHBORHOOD EXHAUSTED) { // CONDITION TO BE DEFINED
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);

208
Chapter 6. Local Search: the Job-Shop Problem

– SequenceVar* Var(int64 index);


– const std::vector<int>& OldSequence(int64 index);
• for both:
– bool IsIncremental();
– bool Activated(int64 index);

Why would I want to use MakeNextNeighbor() instead of


MakeOneNeighbor()?
One reason is efficiency: 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.

6.7.2 The initial solution

We let the CP solver construct the initial solution for us. What about reusing
the DecisionBuilder defined in section 6.2 and grab its first feasible solution?
// This decision builder will rank all tasks on all machines.
DecisionBuilder* const sequence_phase =
solver.MakePhase(all_sequences, Solver::SEQUENCE_DEFAULT);

// After the ranking of tasks, the schedule is still loose and any
// task can be postponed at will. Fix the objective variable to its
// minimum value.
DecisionBuilder* const obj_phase =
solver.MakePhase(objective_var,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MIN_VALUE);

// 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 = "

209
6.7. The Job-Shop Problem: and now with Local Search!

<< first_solution->ObjectiveValue();
} else {
LOG(INFO) << "No initial solution found!";
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 define a first
LocalSearchOperator.

6.7.3 Exchanging two IntervalVars on a SequenceVar

You’ll find the code in the file jobshop_ls1.cc and the SwapIntervals operator in the
file jobshop_ls.
The idea of exchanging two IntervalVars on a SequenceVar is very common and the
corresponding operator is often referred to as the 2-opt-, 2-exchange- or swap- opera-
tor.
We implement a basic version that systematically exchanges all IntervalVars for all
SequenceVars one after the other in the order given by the std::vectors. We use three
indices:
• int current_var_: the index of the processed SequenceVar;
• int current_first_: the index of the first IntervalVar variable to swap;
• int current_second_: the index of the second IntervalVar variable to swap.
We proceed sequentially with the first SequenceVar (current_var_ = 0) and exchange
the first and second IntervalVars, then the first and the third IntervalVars and so on
until exhaustion of all possibilities. Here is the code that increments these indices to create
each candidate solution:
bool Increment() {
const SequenceVar* const var = Var(current_var_);
if (++current_second_ >= var->size()) {
if (++current_first_ >= var->size() - 1) {
current_var_++;
current_first_ = 0;
}
current_second_ = current_first_ + 1;
}
return current_var_ < Size();
}

This Increment() method returns a bool that indicates when the neighborhood is ex-
hausted, i.e. it returns false when there are no more candidate to construct. Size() and
Var() are helper methods defined in the SequenceVarLocalSearchOperator class.
We start with current_var_, current_first_ and current_second_ all set to 0.

210
Chapter 6. Local Search: the Job-Shop Problem

Pay attention to the fact that current_first_ and current_second_ are also updated
inside the if conditions.
We are now ready to define the OnStart() and MakeNextNeighbor() methods.
The OnStart() method is straightforward:
virtual void OnStart() {
current_var_ = 0;
current_first_ = 0;
current_second_ = 0;
}

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


hood 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 that’s it! Our LocalSearchOperator operator is completed. Let’s test it!
First, we need our LocalSearchOperator:
LocalSearchOperator* const swap_operator =
solver.RevAlloc(new SwapIntervals(all_sequences));

Then we need a complementary DecisionBuilder to construct feasible candidate solu-


tions. We don’t 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,

211
6.7. The Job-Shop Problem: and now with Local Search!

Solver::CHOOSE_RANDOM_RANK_FORWARD);

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 (file


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.) Value Candidates Solutions
51,295 1051 31172 26
Not very satisfactory: 1051 is really far from the optimal value of 679. Let’s try to generalize
our operator. Instead of just swapping two IntervalVars, we’ll shuffle an arbitrary number
of IntervalVars per SequenceVar in the next subsection.

6.7.4 Exchanging an arbitrary number of contiguous


IntervalVars on a SequenceVar

You’ll find the code in the file jobshop_ls2.cc and the ShuffleIntervals operator
in the file jobshop_ls.
After having implemented the SwapIntervals operator, the only real difficulty that remains
is to implement a permutation. This is not an easy task but we’ll elude this difficulty by only ex-
changing contiguous IntervalVars and using the std::next_permutation() func-
tion. You can find the declaration of this function in the header algorithm. Its customizable
version reads like:
template <class BidirectionalIterator, class Compare>
bool next_permutation (BidirectionalIterator first,
BidirectionalIterator last, Compare comp);

We accept the default values for the BidirectionalIterator and the Compare classes.
It will rearrange the elements in the range [first,last) into the next lexicographically
greater permutation. An example will clarify this jargon:
No Permutations
1 012
2 021
3 102
4 120
5 201
6 210
We have generated the permutations of 0,1,2 with std::next_permutation(). There
are 3! = 6 permutations (the first permutation is given to std::next_permutation()
and is not generated by it) and you can see that the permutations are ordered by value, i.e. 0 1
2 is smaller than 0 2 1 that itself is smaller than 1 0 2, etc42 .
42
This explanation is not rigorous but it is simple and you can fill the gaps. What happens if you start with 1 0

212
Chapter 6. Local Search: the Job-Shop Problem

As usual with the std, the last element is not involved in the permutation. There is only one
more detail we have to pay attention to. We ask the user to provide the length of the permutation
with the gflags flag FLAGS_shuffle_length. First, we have to test if this length makes
sense but we also have to adapt it to each SequenceVar variable.
Without delay, we present the constructor of the ShuffleIntervals
LocalSearchOperator:
ShuffleIntervals(const std::vector<operations_research::SequenceVar*>& \
vars,
int max_length) :
SequenceVarLocalSearchOperator(vars, size),
max_length_(max_length),
current_var_(-1),
current_first_(-1),
current_length_(-1) {}

vars is the array of SequenceVars. max_length is the length of the sequence


of IntervalVars to shuffle. 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 first IntervalVar variable to shuffle;
• current_length_: the length of the current sub-array of indices to shuffle. 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.

2? The std::next_permutation() function simply “returns” 1 2 0 (oops, there goes our rigour again!).
If you give it 2 1 0, this function returns false but there is a side effect as the array will be ordered! Thus in
our case, we’ll get 0 1 2!

213
6.7. The Job-Shop Problem: and now with Local Search!

if(!std::next_permutation(current_permutation_.begin(),
current_permutation_.end())) {
LOG(FATAL) << "Should never happen!";
}
}
return true;
}

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.

214
Chapter 6. Local Search: the Job-Shop Problem

ApplyChanges() constructs the deltas for us.


File jobshop_ls2.cc is exactly the same as file 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
(file 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 Time (in s.) Value Candidates Solutions
2 12,301 1016 4302 32
3 21,312 1087 7505 15
4 170,087 1034 70854 33
5 584,173 1055 268478 27
These results are typical for a Local Search operator. There certainly are several lessons to be
drawn from these results, but let’s focus on one of the most basic and important ones. The
path taken to find the local optimum is crucial. Even if the neighborhoods (theoretically) con-
structed with suffle_length set to 2 are all contained in the neighborhoods constructed
with suffle_length set to 3, we don’t reach the same local optimum. This is very impor-
tant 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, big-
ger neighbourhoods (shuffle_length = 3) aren’t 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? That’s the topic of
next sub-section!

6.7.5 Can we do better?

You’ll find the code in the file jobshop_ls3.cc.


You should know by now that whenever we ask this question in this manual, the an-
swer is yes. To find a better solution, we’ll first investigate how important the initial so-
lution is and then we’ll enlarge our definition of a neighborhood by combining our two
LocalSearchOperators.

The initial solution

Local search is strongly dependent on the initial solution. Investing time in finding a good
solution is a good idea. We’ll use... Local Search to find an initial solution to get the real Local
Search started! The idea is that maybe we can find an even better solution in the vicinity of
this initial solution. We don’t want to spend too much time to find it though and we’ll limit
ourselves to a custom-made SearchLimit. To define this SearchLimit, we construct a
callback:

215
6.7. The Job-Shop Problem: and now with Local Search!

class LSInitialSolLimit : public ResultCallback<bool> {


public:
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 first Local Search that finds our initial solution, we use the same code as in the
file jobshop_ls2.cc (we call this first solution first_solution).
To find an initial solution, we use Local Search and start form the first_solution found.
We only use a ShuffleIntervals operator with a shuffle length of 2. This time, we limit
this Local Search with our custom limit:
SearchLimit * initial_search_limit = solver.MakeCustomLimit(
new LSInitialSolLimit(&solver,
FLAGS_initial_time_limit_in_ms,

216
Chapter 6. Local Search: the Job-Shop Problem

FLAGS_solutions_nbr_tolerance));

FLAGS_initial_time_limit_in_ms and FLAGS_solutions_nbr_tolerance


are the two gflags flags we use in the constructor of the callback LSInitialSolLimit
described above to limit the search.
The initial solution is stored in an Assignment initial_solution.
Now, we are ready to prepare the Local Search with our two LocalSearchOperators
combined.

Combining the two LocalSearchOperators

Often, one LocalSearchOperator isn’t enough to define a good neighborhood. Finding


a good definition of a neighborhood is an art and is really difficult. 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));
operators.push_back(swap_operator);

LocalSearchOperator* const shuffle_operator =


solver.RevAlloc(new ShuffleIntervals(all_sequences,
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 opera-
tors 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 file jobshop_ls2.cc.

Results

If we solve our problem instance (file first_example_jssp.txt), we still get the optimal
solution. No surprise here. What about the abz9 instance?
With our default value of
• time_limit_in_ms = 0, thus no time limit;
• shuffle_length = 4;
• initial_time_limit_in_ms = 20000, thus a time of 20 seconds to find an
initial solution with Local Search and the ShuffleIntervals operator with a shuffle
length of 2 and;
• solutions_nbr_tolerance = 1,

217
6.8. Filtering

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 Initial sol. obj. Time Value Candidates Solutions
1,000 1114 81,603 983 49745 35
2,000 1103 103,139 936 70944 59
3,000 1093 104,572 931 70035 60
4,000 1089 102,860 931 68359 60
5,000 1073 84,555 931 63949 60
6,000 1057 42,235 1012 29957 32
7,000 1042 36,935 1012 26515 32
... ... ... ... ... ...
>= 13,000 1016 19,229 1016 13017 32
The first column lists the times allowed to find the initial solution with the
ShuffleIntervals operator (with its shuffle length set to 2) and the second column col-
lects the objective values of this initial solution. The more time given to the first 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 neces-
sarily 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, we’ll see how some meta-
heuristics 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, we’ll see a mechanism
to shortcut this verification and command the solver to disregard some candidates without the
need for the solver to test them explicitly.

6.8 Filtering

You can find the code in the file dummy_ls_filtering.cc.

Our local search strategy of section 6.6 is not very efficient: we test lots of unfeasible
or undesirable candidate solutions. LocalSearchFilters allow to shortcut the solver’s
solving and testing mechanism: we can tell the solver right away to skip a candidate solution.

6.8.1 LocalSearchFilters

LocalSearchFilters instruct the CP solver to skip (or not) the current


candidate solution. You can find the declaration and definition in the header
constraint_programming/constraint_solveri.h.

218
Chapter 6. Local Search: the Job-Shop Problem

There are basically two methods to implement43 :


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 imple-
menting the IsIncremental() method:
virtual bool IsIncremental() const { return true; }

By default, this method returns false.

6.8.2 Defining a custom LocalSearchFilter

We will filter the dummy example from the file dummy_ls.cc. You can find the code in the
file dummy_ls_filtering.cc.
Because we use an OptimizeVar SearchMonitor, we know that each time a feasible
solution is found, the CP solver gladly adds a new constraint to prevent other solutions with the
same objective value from being feasible. Thus, candidate solutions with the same or higher
objective value will be rejected by the CP solver. Let’s help the busy solver and tell him right
away to discard such candidate solutions.
We are using IntVars and thus we’ll inherit from IntVarLocalSearchFilter and
instead of implementing the Synchronize() method, we’ll implement the specialized
OnSynchronize() method.
The constructor of the ObjectiveValueFilter class is straightforward:
ObjectiveValueFilter(const std::vector<IntVar*>& vars) :
IntVarLocalSearchFilter(vars.data(), vars.size()), obj_(0) {}

obj_ is an int64 to keep the objective value of the current solution.


Let’s synchronize our filter with the objective value of the current solution:
virtual void OnSynchronize() {
obj_ = 0;
for (int i = 0; i < Size(); ++i) {
obj_ += Value(i);
}
}
43
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.

219
6.8. Filtering

Several helper methods are defined in the IntVarLocalSearchFilter class:


• int64 Value(int index) const: returns the value of the ith variable of the cur-
rent solution. These values are automatically updated when Synchronize() is called;
• IntVar* Var(int index) const: returns the ith variable given in the
std::vector;
• bool FindIndex(const IntVar* const var, int64* index)
const: returns a bool to indicate if the ith 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 don’t
want to change their content, use the corresponding Mutable...Container() method
if you want to change their content. For instance, to change the SequenceVars, use the
MutableSequenceVarContainer() method.
For the sake of efficiency, Assignment contains a light version of the variables. For instance,
an ÌntVarContainer contains IntVarElements and the call to
FindIndex(solution_delta.Element(index).Var(), &touched_var);

simply returns the LocalSearchFilter‘s 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 filter as follows:

220
Chapter 6. Local Search: the Job-Shop Problem

...
LocalSearchFilter * const filter = s.RevAlloc(
new ObjectiveValueFilter(vars));
std::vector<LocalSearchFilter*> filters;
filters.push_back(filter);
...
ls_params = s.MakeLocalSearchPhaseParameters(..., filters);

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 filtering. Of course! Our
LocalSearchOperator systematically produces candidate solutions with a smaller
objective value than the current solution (the same value minus one)! Does it mean that we
have worked for nothing? Well, this is a dummy example, isn’t? Our main purpose was to
learn how to write a custom LocalSearchFilter and we did it!
OK, you’re not satisfied and neither are we. We know that x0 > 1 and that the other variables
must be equal or greater than 0.
Let’s write a LocalSearchFilter that filters infeasible candidate solutions. We don’t need
to provide an OnSyncronize() method. Here is our version of the Accept() method:
virtual bool Accept(const Assignment* delta,
const Assignment* deltadelta) {
const Assignment::IntContainer& solution_delta =
delta->IntVarContainer();
const int solution_delta_size = solution_delta.Size();

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, didn’t
you?44 .
We now obtain:
..., neighbors = 23, filtered neighbors = 9,
accepted neighbors = 9, ...
44
To be fair, this solution is not as general as it should be. We didn’t take into account the fact that some
IntervalVar variables can be non active but for IntVars and SequenceVars it works well.

221
6.9. Summary

Of course, we could have improved our LocalSearchOperator so that it doesn’t produce


such infeasible solutions!

6.8.3 Interesting LocalSearchFilters

There exist some general LocalSearchFilters in or-tools: ObjectiveFilter (and


some sub-classes) and VariableDomainFilter.
It is easy to add a VariableDomainFilter, simply use
LocalSearchFilter* Solver::MakeVariableDomainFilter();

As its name implies, it rejects assignments to values outside the domain of the variables.
The ObjectiveFilter is more interesting and exists in different flavors depending on:
• the type of move that is accepted based on the current objective value:
The different possibilities are given by the LocalSearchFilterBound enum:
– GE: Move is accepted when the candidate objective value >= objective.Min;
– LE: Move is accepted when the candidate objective value <= objective.Max;
– EQ: Move is accepted when the current objective value is in the interval
objective.Min ... objective.Max.
• the type of operation used in the objective function:
The different possibilities are given in the LocalSearchOperation enum and con-
cern the variables given to the MakeLocalSearchObjectiveFilter() method:
– SUM: The objective is the sum of the variables;
– PROD: The objective is the product of the variables;
– MAX: The objective is the max of the variables;
– MIN: The objective is the min of the variables.
• the callbacks used: we refer the curious reader to the code in the file
constraint_programming/local_search.cc for more details about different
available callbacks.
For all these versions, the factory method is MakeLocalSearchObjectiveFilter().
Again, we refer the reader to the code to see all available refinements.

6.9 Summary

Again, another dense chapter. In this chapter, we saw basically 4 things:


• what Local Search is and how it behaves practically;
• how Local Search is implemented in or-tools;

222
Chapter 6. Local Search: the Job-Shop Problem

• what kind of customized classes are used to tackle Scheduling Problems in or-tools;
• the Job-Shop Problem as an illustrative example.
More precisely, we saw a description of the Job-Shop Problem and how to model it with the
disjunctive model. This allowed us to introduce the IntervalVars and SequenceVars
variables that are typically used in Scheduling. We also combined two DecisionBuilders
in a top-down fashion. After describing general features of Local Search, we have
seen its general implementation in or-tools and in particular where the callbacks of the
SearchMonitors are called in the Local Search algorithm. Finally, we saw how by imple-
menting LocalSearchOperators and LocalSearchFilters we can define our neigh-
borhoods for the Local Search algorithm.
There are some interesting examples in the directory examples/cpp and the reader is en-
couraged to read them carefully.

223
CHAPTER

SEVEN

META-HEURISTICS: SEVERAL
PREVIOUS PROBLEMS

A meta-heuristic is a canvas or framework to construct a heuristic adapted to your specific


problem. Dozens of meta-heuristics have been invented. Let’s set aside the unproductive debate
about what meta-heuristic is best and who invented it and let’s dive right into the topic. Meta-
heuristics:
• are quite new: the first ones were designed in the 1960s;
• are not very well understood: why are some efficient for some problems but not for
others?;
• are somehow all interrelated: you can express one meta-heuristic with another;
• have their efficiency heavily depending on the quality of the code as well as the knowl-
edge of the problem.
• are often based on very simple ideas.
One could write books on meta-heuristics and indeed lots of books, articles and reports have
been written. There are even scientific communities that only swear by this meta-heuristic
and each meta-heuristic comes with its own vocabulary1 . In this manual, we only scratch the
1
In order to “sell” your (meta-)heuristic to the scientific community, it is also good to give it a snappy name.
We don’t resist to name a few:
• artificial bee colony algorithm,
• honey-bee mating optimization,
• intelligent water drops,
• firefly algorithm,
• monkey search,
• league championship algorithm,
• cuckoo search,
• virus optimization algorithm,
• galaxy-based search algorithm,
• ...
and our favorite: the imperialist competitive algorithm.
surface of this fascinating subject.
Many meta-heuristics are based on Local Search2 : they start with an initial solution and im-
prove it little by little. From now on and for the rest of this chapter, we only talk about meta-
heuristics using Local Search.

The whole chapter is about meta-heuristics using Local Search.

Among them, we present three well-known meta-heuristics:


• Tabu Search: one of the most efficient meta-heuristic on the market!
• Simulated Annealing: one of the first available meta-heuristic.
• Guided Local Search: well suited for some problems like Routing Problems.
These three meta-heuristics are implemented in or-tools and are used in the routing library
(through flags).
In or-tools, we implement meta-heuristics with SearchMonitors guiding Local Search (and
thus we also use the LocalSearch, LocalSearchOperator, LocalSearchFilter
classes). This is quite “natural” as SearchMonitors allow to... monitor the search and the
implemented meta-heuristics are based on Local Search.
When you implement a (meta)-heuristic, you have to take decisions. Our implementations are
one possibility among others. We would like to warn the reader on an important choice we
have made in our implementation: we only use meta-heuristic when we have reached a local
optimum, not before!

Our meta-heuristics only kick in when we already have reached a local optimum
with the Local Search.

A last word of advice. In this chapter, we decided to show you the code in full details. Real
code, especially in optimization, is complicated and has to deal with lots of subtleties. While
we try to explain most of the intricate details (but not all), you only see the end results. The
code we show has been polished and is used in production (and changes sometimes!). Some
parts have a long history of trial and error and there is no way we can explain all the details. We
cannot emphasize enough that you’ll learn how to use the or-tools by actually coding with it,
not only reading existing code. That said, the exposed code gives you good examples on what
can be done and how the makers of the or-tools library decided to do it. Beside, you’ll have a
pretty good idea of what actually our meta-heuristic implementations do and how to adapt the
code to solve your problems.

Overview:

Before diving in the heart of the matter, we address two ways to improve the search. First,
because our meta-heuristics never stop, we detail how SearchLimits can be used to stop
2
Local Search and meta-heuristics are so intricate that it is difficult to separate the two concepts. This is also
the case in the ortools library. The LocalSearch class was devised with the Metaheuristic class in mind
and vice-versa. Reread the section 6.4 if needed. It also describes what a meta-heuristic is.

226
Chapter 7. Meta-heuristics: several previous problems

a search. Second, we discuss restarting a search. Recent research shows that restarting a
search might improve the overall search. Next, we detail our Metaheuristic class and
our three specialized TabuSearch, SimulatedAnnealing and GuidedLocalSearch
implementations. To finish this chapter, we present another meta-heuristic, Large
Neighborhood Search3 , that is not implemented with a SearchMonitor but with a
LocalSearchOperator. Next we answer the question “What do you do when you have
no idea of a (good) search strategy?”. Answer? You throw in lots of heuristic and random be-
havior in your top-level search. In or-tools, we implemented the DefaultIntegerSearch
DecisionBuilder to do just that.

Prerequisites:

• Basic knowledge of C++.


• Basic knowledge of Constraint Programming (see chapter 1).
• Basic knowledge of the Constraint Programming Solver (see chapter 2).
• Section 5.3 on the inner working of the solver helps but is not mandatory.
• Chapter 6 about Local Search as this chapter heavily depends on Local Search. Most of
our examples involve the Job-Shop Problem.

Classes under scrutiny:

SearchLimit, SearchMonitor, Metaheuristic, TabuSearch,


SimulatedAnnealing, GuidedLocalSearch, LocalSearchOperator and
DefaultIntegerSearch.

Files:

The files used in this chapter are:


• limits.h: This header file contains several search limits. This file is used throughout most
of the examples of this chapter.
• jobshop_ts1.cc: The same as file jobshop_ls1.cc from the previous chapter but with
Tabu Search added.
• jobshop_ts2.cc: The same as file jobshop_ls3.cc from the previous chapter but with
Tabu Search added.
• jobshop_sa1.cc: The same as file jobshop_ls1.cc from the previous chapter but
with Simulated Annealing added.
• jobshop_sa2.cc: The same as file jobshop_ls3.cc from the previous chapter but
with Simulated Annealing added.
3
Large Neighborhood Search (LNS) can be seen as a meta-heuristic (same for Local Search) and is in a way
an extension of Local Search. In or-tools, we implement LNS with special LocalSearchOperators.

227
7.1. Search limits and SearchLimits

• dummy_lns.cc: The basic example solved with Large Neighborhood Search.


• jobshop_lns.h: a basic SequenceLns LocalSearchOperator to solve the Job-
Shop Problem with Large Neighborhood Search.
• jobshop_lns.cc: A basic implementation of Large Neighborhood Search with the
SequenceLns LocalSearchOperator to solve the Job-Shop Problem.
• jobshop_heuristic.cc: We use all the previous ingredients to solve approximately the
Job-Shop Problem.
• golomb_default_search1.cc: We solve the Golomb Ruler Problem with Default Search.
• golomb_default_search2.cc: Same as golomb_default_search1.cc but with cus-
tomized DefaultPhaseParameters parameters.

7.1 Search limits and SearchLimits

You can find the code in the file limits.h.

To control the search, we also need a way to stop the search when some criteria are
met. By default, our meta-heuristics never stop. This can easily be done with the help of a
SearchLimit.

Our meta-heuristic search never stops. Add a SearchLimit to limit the search.

We will use the SearchLimits defined in limits.h throughout the whole chapter.

7.1.1 An example of a custom Searchlimit:


NoImprovementLimit

We have already met SearchLimits in sub-section 3.5.4 and even used callbacks to define
our SearchLimit in sub-section 6.7.5 where we disregarded a given time limit if the search
produced enough solutions between checks. We go a little bit more into details here. It is not
always possible to create a CustomLimit with an appropriate callback to meet a desired end
search criteria. For instance, let’s say we would like to stop the search after a certain number
of solutions have been found but without any improvements in the objective value. To do so,
we need to have access to the objective value every time a new solution has been found. The
Solver class doesn’t provide any method to access its current objective value. What we need
is a custom SearchMonitor. Or more precisely, a custom SearchLimit.
SearchLimits are specialized SearchMonitors to end a Search. The SearchLimit
class itself is a pure virtual base class for all SearchLimits. Its only constructor is:
explicit SearchLimit(Solver* const s);

Several methods must be defined in order to have a valid SearchLimit:

228
Chapter 7. Meta-heuristics: several previous problems

• bool Check(): check the status of the limit. A return value of true indicates
that we have indeed crossed the limit. In that case, this method will not be called
again and the remaining search will be discarded. This method is called in the
BeginNextDecision() and RefuteDecision() callbacks.
• void Init(): called when the search limit is initialized in the EnterSearch()
callback.
• void Copy(const SearchLimit* const limit): copies a limit. Warning:
this leads to a direct (no check) down-casting of limit, so one needs to be sure both
SearchLimits are of the same type.
• SearchLimit* MakeClone() const: allocates a clone of the limit.
OK, let’s get our hands dirty and code! You can find the code in the file limits.h.
Our class inherits publicly from SearchLimit:
class NoImprovementLimit : public SearchLimit {
...
};

We’ll consider both minimizing and maximazing depending on a bool in the constructor4 :
class NoImprovementLimit : public SearchLimit {
public:
NoImprovementLimit(Solver * const solver,
IntVar * const objective_var,
const int solution_nbr_tolerance,
const bool minimize = true);

solution_nbr_tolerance represents the number of solutions allowed without any im-


provements in the objective value. We keep a reference to the objective variable given by the
objective_var argument.
To be able to retrieve the current solution objective value, we keep a copy of the current solution
in a prototype_ variable:
std::unique_ptr<Assignment> prototype_ = new Assignment(solver);
prototype_->AddObjective(objective_var);

and we add the objective variable to it.


The most interesting method is not the Check() method that only returns a Boolean
limit_reached_ but the AtSolution() method that computes this Boolean. Remem-
ber that the AtSolution() method is called whenever a new solution has been found. Here
is the code:
1 virtual bool AtSolution() {
2 ++nbr_solutions_with_no_better_obj_;
3

4 prototype_->Store();
5

4
We don’t follow the code convention of using a maximize bool but the fully attentive reader noticed it,
didn’t she?

229
7.1. Search limits and SearchLimits

6 const IntVar* objective = prototype_->Objective();


7

8 if (minimize_ && objective->Min() < best_result_) {


9 best_result_ = objective->Min();
10 nbr_solutions_with_no_better_obj_ = 0;
11 } else if (!minimize_ && objective->Max() > best_result_) {
12 best_result_ = objective->Max();
13 nbr_solutions_with_no_better_obj_ = 0;
14 }
15

16 if (nbr_solutions_with_no_better_obj_ > solution_nbr_tolerance_) {


17 limit_reached_ = true;
18 }
19 return true;
20 }

For each solution, we increase the counter nbr_solutions_with_no_better_obj_ at


line 2. We reset this counter to 0 in lines 8 to 14 if the current solution has a better objective
value than the best known so far. To do this we store the current solution in our prototype_
Assignment on line 4. We will use the NoImprovementLimit class in the next sections.
Beware of the warning formulated in section 12.3.2 about composite objects.

7.1.2 A callback to catch the CTRL-C interrupt

The CatchCTRLBreakLimit class is only available in linux for the moment.

CTRL-C (the Ctrl key in combination with the C key) sends the SIGINT signal which will
interrupt the application except if we catch this signal and exit peacefully. Because meta-
heuristics can take a long time before even producing a solution or find a better solution, we
have implemented a CatchCTRLBreakLimit class that allows the CP Solver to fail peace-
fully instead of abruptly interrupting the search process. The code involved is beyond the scope
of this manual (if you are curious, have a look at the file limits.h). As usual, we have de-
fined a factory method:
SearchLimit * MakeCatchCTRLBreakLimit(Solver * const solver);

that you can use to create a new CatchCTRLBreakLimit object.


Be aware that in linux, the SIGINT signal is caught if you include limits.h and that if
you don’t use this SearchLimit you will not be able to stop your current search by pressing
CRTL-C.
In linux, don’t include the file limits.h if you don’t use
CatchCTRLBreakLimit and plan to press CRTL-C to stop the solving
process.

230
Chapter 7. Meta-heuristics: several previous problems

7.2 Restarting the search

Restarting the search - especially after having gathered some information - can dramatically
improve the search5 (see [Gomes1998] for instance). It is well known that the first decisions
taken at the beginning of a search can have an enormous impact on the search tree size and the
time required to visit this tree in part or in whole.
The CP solver provides two restart strategies:
• constant restart and
• Luby restart.
Both restart strategies are implemented by SearchMonitors. We detail both strategies in
the next sub-sections. Some information is collected during a search and is kept to guide a
restarted search. NoGoods (when used) are among them and we discuss them below6 .

7.2.1 Constant Restart

As its name implies, constant restart restarts the search periodically. The factory method
SearchMonitor* Solver::MakeConstantRestart(int frequency);

creates a ConstantRestart SearchMonitor. The frequency parameter indicates


how many failures are allowed before the CP Solver restarts the search.

7.2.2 Luby

In a now well-known article [Luby1993], Luby et al. describe an optimal (universal) strategy
S univer = (t1 , t2 , t3 , . . .) to restart a search (an algorithm) that has a certain randomness in it
but that is guaranteed to converge to a solution given enough time. ti is called a run length: a
number of iterations in the algorithm. To apply this strategy, you first let the algorithm run for
t1 iterations, than restart it and let it run for t2 iterations and so on.
We will not go into the (rather technical) details of this article. For the curious reader, here is
this optimal strategy:
S univer = (1, 1, 2, 1, 1, 2, 4, 1, 1, 2, 1, 1, 2, 4, 8, . . .)
or more formally:
(
2k−1 , if i = 2k − 1
ti =
ti−2k−1 +1 , if 2k−1 6 i < 2k − 1.
with i, k = 1, 2, . . .. Notice that all the run lengths ti are power of 2 and that each sequence is
repeated before a new power of 2 is tried.
The factory method of the Solver class is the following:
5
For both heuristic and exact searches.
6
What else is kept during a restart of the search? Essentially, impact-based searches also keep their estimations.
See section 7.8.1 in this chapter for more about impact-based searches.

231
7.2. Restarting the search

SearchMonitor* Solver::MakeLubyRestart(int scale_factor);

It creates a LubyRestart SearchMonitor. In our implementation, the solver will restart


the search after scale_factor * Luby(n) failures where n = 1, 2, 3, ... de-
notes each iteration when the solver restarts and Luby() returns the above sequence.

7.2.3 NoGoods

This is an advanced feature that hasn’t been tested as thoroughly as the rest of the code.

NoGoods haven’t been tested as thoroughly as the rest of the code.

The basic idea of a nogood is simple: keeping a list of contradictory variable “assignments”.
These contradictory variable assignments can be stated before or/and during the search. The
idea is to use these nogoods in the search to - hopefully - cut short some branches for the
search tree without having to rediscover these contradictory assignments again and again. This
is especially useful when restarting the search.
An example will clarify our discussion. Let’s say that during the search, a procedure finds that
the following variable assignments lead to a failure:
x1 = 2, x8 = 4 and x3 6= −2.
The list of the 3 variable “assignments” is a nogood. Whenever we have these three conditions,
we know for sure that they lead to a failure. This information could lead to some refinements in
the search. In our (basic) implementation of nogoods, we only use this information to propagate
the nogood7 .
Each clause (such as x1 = 2 and x3 6= 3) are held in an abstract NoGoodTerm class. At the
time of writing (7 th of January 2015, r3832), only the clauses xi = a and xj 6= b have been
implemented, both in the specialized IntegerVariableNoGoodTerm class. A Boolean
assign variable distinguishes both cases. The NoGood class is basically a container for such
clauses. Its only (private) variable is:
std::vector<NoGoodTerm*> terms_;

The NoGood class has two methods to add these two types of clauses:
void NoGood::AddIntegerVariableEqualValueTerm(IntVar* const var,
int64 value) {
terms_.push_back(new IntegerVariableNoGoodTerm(var, value, true));
}

and
void NoGood::AddIntegerVariableNotEqualValueTerm(IntVar* const var,
int64 value) {
terms_.push_back(new IntegerVariableNoGoodTerm(var, value, false));
}
7
This process is really basic: if there is only one undecided clause (term) and all the other clauses are verified,
then the opposite of this undecided clause is added to the solver.

232
Chapter 7. Meta-heuristics: several previous problems

The assign variable (third parameter of the IntegerVariableNoGoodTerm construc-


tor) is set to true in AddIntegerVariableEqualValueTerm and to false in
AddIntegerVariableNotEqualValueTerm.
To store several NoGoods we use a specialized class derived from the NoGoodManager class.
At the time of writing, one such class exists:
class NaiveNoGoodManager : public NoGoodManager {...};

with the corresponding factory method:


NoGoodManager* Solver::MakeNoGoodManager() {
return RevAlloc(new NaiveNoGoodManager(this));
}

As its name implies, this class doesn’t do much more than collect and propagate the nogoods
but it can be used as a base example to develop your own NoGoodManager.
Its only (private) variable is a list of nogoods:
std::vector<NoGood*> nogoods_;

To see the NoGoods in action, we refer the reader to the code written for the default search
in the file default_search.cc, especially the RestartMonitor SearchMonitor
that uses a NoGoodManager. A (basic) mechanism is implemented to create and collect
NoGoods obtained from diving heuristics8 . Once such heuristic finds a contradictory variable
assignment, a NoGood is created and the search might or might not be restarted.

7.3 Meta-heuristics in or-tools

To help you (and us) in the building of meta-heuristics, the or-tools library offers an abstract
Metaheuristic which your specialized class can inherit from. We deal with optimization
problems: we either minimize or maximize an objective function. The way meta-heuristics are
implemented in or-tools is to let the meta-heuristic itself decide what to do when the objective
value changes: it can add a new constraint on the objective value and/or the next solution to be
found. This means that you don’t use the OptimizeVar class to encapsulate your objective
function9 but instead use the Metaheuristic class to add complementary constraints on
your objective.

Don’t use the OptimizeVar class to encapsulate your objective function when
using a Metaheuristic class.

The Metaheuristic class inherits from the SearchMonitor class. SearchMonitors


allows you to monitor/control the search. If you don’t remember anything about the basic
8
A diving heuristic is an heuristic that dives into the search tree, assigning values one after the other to
variables.
9
Well, if you do, nothing bad will happen. It’s just unnecessary work for the CP Solver. Both OptimizeVar
and Metaheuristic classes are SearchMonitors. The Metaheuristic class was implemented to
be used without the OptimizeVar class. This is the case for the three implemented Metaheuristics:
TabuSearch, SimulatedAnnealing and GuidedLocalSearch.

233
7.3. Meta-heuristics in or-tools

search algorithm and the SearchMonitor class, we invite you to quickly (re)read section 5.3
about the basic working of the search algorithm and especially section 6.5 about the Local
Search algorithm. We do sketch what the methods of the SearchMonitor class do in this
chapter thought. We reiterate our warning concerning the start of our meta-heuristics:

Our meta-heuristics only kick in when we have already reached a local optimum
with the Local Search.

This is a choice in our implementations of the meta-heuristics. See the box Why wait for a local
optimum to start a meta-heuristic? below for more.
One last thing before digging further into this fascinating subject. Because details matter very
much in the case of meta-heuristic implementations, we present the full code of all the classes
discussed in this and the next three sections.

7.3.1 The basic idea

The basic idea behind meta-heuristics is to enhance the Local Search. In particular, most meta-
heuristics share (at least) the three following main features:
• avoid being trapped in local optimums;
• intensify the search in a promising neighborhood (intensification) and
• keep better solutions met during the search (aspiration criterion).
We discuss these three features more in details next.

The local optimum trap

Let’s reexamine the figure used in the previous chapter to illustrate Local Search:

x0 x1 x3 x2 x

Initial solution Local minimum Global minimum

With a given LocalSearchOperator and starting at x0 , we might find solutions x1 , x2 , x3


and even probably the local minimum depicted above. In practice, no matter what starting point
you take close to the local optimum, your Local Search will probably guide you towards this
local optimum. This is because at each of these points, locally and with the limited information
your Local Search has, it will move towards this local optimum. This is called being trapped
in a local optimum. Meta-heuristics devise clever ways to get out of this local optimum trap
but to do so, meta-heuristics must accept worse solutions.

234
Chapter 7. Meta-heuristics: several previous problems

Intensification vs diversification

Avoiding being trapped in a local optimum can be seen as a special case of diversification of
the search: the algorithm explores different parts of the search space in the hope of finding
promising neighborhoods that a basic Local Search algorithm would not explore on its own.
The opposite is also desirable. Once a promising neighborhood has been found, it is probably
worth exploring it in more details. This is called intensification.
Often the intensification and diversification mechanisms are conflicting and the meta-heuristic
oscillates between these two phases in the search.

The aspiration criterion

In practice, we often see the following scenario. A LocalSearchOperator could have


found a better candidate solution in a neighborhood if it were operating without restric-
tions by the meta-heuristics That’s a really annoying situation: you want to help your
LocalSearchOperator by restricting it a little bit and then it turns out that without your
guidelines, it would have been able to find a better solution This situation is so common that
most meta-heuristics use an aspiration criterion: a criterion to accept a solution even if it
doesn’t follow the meta-heuristic guidelines. Most of the time this criterion is based on the
quality of the found solution: a solution is accepted no matter what if it is better than any other
solution encountered so far.

7.3.2 The Metaheuristic class

The base Metaheuristic class is quite simple. It only implements three basic virtual meth-
ods:
• virtual void EnterSearch();
• virtual bool AtSolution() and
• virtual void RefuteDecision(Decision* const d).
It also defines five basic variables:
protected:
IntVar* const objective_;
int64 step_;
int64 current_;
int64 best_;
bool maximize_;

The names of the variables are self-explanatory. maximize_ is set to true when maximizing
and false when minimizing.
We reproduce the implementation of the three virtual methods integrally. Not only are they
pedagogically interesting, it is also good to know them to avoid reinventing the wheel.

235
7.3. Meta-heuristics in or-tools

EnterSearch()

EnterSearch is called at the beginning of the search and is the perfect place to initialize our
variables:
virtual void Metaheuristic::EnterSearch() {
if (maximize_) {
best_ = objective_->Min();
current_ = kint64min;
} else {
best_ = objective_->Max();
current_ = kint64max;
}
}

The other variables are set in the constructor. The current_ variable holds the value of the
current solution and is used to bound the objective value. We start with a very high value to
allow the meta-heuristic to find solutions that have worst objective values than the best solution
encountered so far.

AtSolution()

The AtSolution() method is called whenever a valid solution is found. If the return value
is true, then the search will resume, otherwise the search will stop there. This is the perfect
place to update the current and best solution:
virtual bool Metaheuristic::AtSolution() {
current_ = objective_->Value();
if (maximize_) {
best_ = std::max(current_, best_);
} else {
best_ = std::min(current_, best_);
}
return true;
}

By default, we don’t want the Metaheuristic class to halt the search so we return true.

RefuteDecision()

RefuteDecision(Decision* const d) is called just before refuting the correspond-


ing Decision. Remember how, at each node of the search tree, the search algorithm visits
two search sub-trees: the first one when applying the Decision and the second one when
refuting this Decision. This second visit happens (essentially) when the search algorithm
backtracks and on its way back we test if our meta-heuristic can still beat the best solution or
not:
virtual void Metaheuristic::RefuteDecision(Decision* d) {
if (maximize_) {
if (objective_->Max() < best_ + step_) {
solver()->Fail();

236
Chapter 7. Meta-heuristics: several previous problems

}
} else if (objective_->Min() > best_ - step_) {
solver()->Fail();
}
}

If our meta-heuristic is not able to beat the best solution, we make the search fail at this node
and let the solver continue the search at another node. We don’t take any action before a
Decision is applied by default because each meta-heuristic has its own way of dealing with
an applied Decision. You’ll probably need to overwrite the ApplyDecision() method
for each meta-heuristic.

Do I really need to inherit from Metaheurstic to create my specialized meta-


heuristic?
Using a SearchMonitor is probably the way to go if you want to implement a meta-
heuristic. And if you decide to inherit from a SearchMonitor, why not benefit from the
added value of the Metaheuristic class? More generally, isn’t there another way to
construct a (meta-)heuristic? Large Neighborhood Searcha is implemented by a BaseLNS
class that is a IntVarLocalSearchOperator class for instance. It is all a question
of choice and... trade-offs.
a
You can read more about LNS in section 7.7.

7.3.3 Interesting callbacks from the SearchMonitor class

We briefly review some of the callbacks of the SearchMonitor class that we might want
to implement when designing a new meta-heuristic. We have already seen a basic use for
EnterSearch(), AtSolution() and RefuteDecision() above. We probably need
to extend/specialize these basic implementations but for now, we discuss other callbacks of the
SearchMonitor class that can or must be used to implement a meta-heuristic. Don’t forget
that we use Local Search.

ApplyDecision()

This is the counter-part of RefuteDecision() and must be implemented. This callback


is called just before applying the Decision and you use it to control the direction of the
Local Search, i.e. to control where to go in the neighborhood to find the next solution. Every
meta-heuristic has its own search mechanism and this is the main method to implement.
To search for better solutions, you might want to implement something along these lines:
// Go downhill to the next local optimum
if (maximize_) {
const int64 bound = (current_ > kint64min) ?
current_ + step_ : current_;
s->AddConstraint(s->MakeGreaterOrEqual(objective_, bound));
} else {

237
7.3. Meta-heuristics in or-tools

const int64 bound = (current_ < kint64max) ?


current_ - step_ : current_;
s->AddConstraint(s->MakeLessOrEqual(objective_, bound));
}

LocalOptimum()

LocalOptimum() is called whenever a “local optimum is reached”: but it is up to you to


decide in your SearchMonitor/Metaheuristic class if this is the case or not. Most
probably you would want to agree if a solution has been found. You know that a nested Local
Search has been exhausted and that probably a local best solution has been found. If your
meta-heuristic continues the search as ours do, this is the place to launch your meta-heuristic
and restart another Local Search.

Why wait for a local optimum to start a meta-heuristic?


Let’s try to explain our practical approach in a few lines. First of all, we do accept any
improving solution found by a LocalSearchOperator even if the move leading to
that solution isn’t permitted by the meta-heuristic. For instance, in our implementation of
the Tabu Search, we allow the LocalSearchOperator to transgress any tabu movesa .
To allow this, we implemented the tabu moves as constraints that can be violated in case
a better solution is found. Thus as long as a LocalSearchOperator finds improving
solutions, the use of a meta-heuristic will not make any difference in our implementa-
tions. Our point of view is to let a LocalSearchOperator do its job as long as it
can improve the solutions it finds. Once it cannot improve a solution, i.e. once it scours
the neighborhood without any success, we have... a local optimum. And that’s precisely
when our meta-heuristics kick in: we would like to get out of this local minimum trap
and visit other neighborhoods. This also means that you should use our meta-heuristics
with LocalSearchOperators that are not too clever and don’t visit a huge number of
neighbors (candidate solutions). For instance, LocalSearchOperators that include
lots of randomness should probably best be avoided.
a
See next section about the Tabu Search if you are not acquainted with this meta-heuristic.

AcceptNeighbor()

This method is called whenever a neighbor (what we called a candidate solution in the previous
chapter) has been selected (accepted and filtered) in one neighborhood. This is probably one
iteration of your algorithm and the perfect time to update your search strategy depending on
the new candidate solution.

7.3.4 Implemented meta-heuristics in or-tools

The following figure illustrates the hierarchy situation between the different implemented
Metaheuristic classes:

238
Chapter 7. Meta-heuristics: several previous problems

SearchMonitor

Metaheuristic

TabuSearch SimulatedAnnealing GuidedLocalSearch

Tabu Search, Simulated Annealing and Guided Local Search are three well-known meta-
heuristics.
In the following sections, we’ll have a detailed look at each of these classes and apply them to
solve one or several problems we have already encountered10 . Meta-heuristic are more a gen-
eral canvas within which much liberty is offered for a concrete and specialized implementation.
Our implementation is only one among many.
These three meta-heuristics can be used within the Routing Library (RL) by switching a com-
mand line flag:
- routing_guided_local_search
- routing_simulated_annealing
- routing_tabu_search

All three flags are set to false by default. You can only use one meta-heuristic at a time.

7.4 Tabu Search (TS)

This meta-heuristic was invented in the eighties and has been quite successful to produce very
good solutions for most problems. Well implemented, it can be very efficient. We describe
here our generic and therefor simple implementation. We’ll again develop and discuss the
whole code. Not only will you know exactly what to expect from our implementation but it
can also serve as an example for your own implementation of a meta-heuristic with the or-tools
library.

7.4.1 The basic idea11

The basic idea is to avoid being trapped in a local optimum by making some features of a
solution tabu: we don’t want to produce any solution with these features for a certain period
of time. This period of time is called a tenure. If we choose these features well, not only do
we have the guarantee that we will not reproduce the local optimum again (because it has these
features) but we might get out of the vicinity of this local optimum and explore more promising
neighborhoods. This is called the diversification phase: we seek to find solutions with different
features than previously obtained. Once you find a good solution, you might want to explore
10
We will not see an example for the GLS meta-heuristic because our implementation is tailored to solve
Routing Problems.
11
We only give a you a very basic idea barely enough to understand our implementation. Actually, some Tabu
Search experts might even not agree with our presentation of the main idea.

239
7.4. Tabu Search (TS)

solutions “close” to it. If you manage to find what features is important for a solution, you
might want to keep them to explore similar solutions. This is called the intensification phase:
we seek to find solutions with similar (or the same) features to a (or several) given solution(s).
We keep two lists: one for forbidden features and one for features to keep in the next solutions.
The Tabu Search oscillates between these two phases: diversification to avoid being trapped in
local optima and also explore other parts of the search space and intensification to search more
in details a promising neighborhood.
We only have scratched the surface of the Tabu Search meta-heuristic. If you want to know
more about TS, the classical book by Glover and Laguna [Glover1997] is still a good reference.
Another good reference is [Gendreau2005]. To have an up to date account on this topic, search
the Internet: there are plenty of documents about TS.

7.4.2 Our implementation

Our implementation only deals with IntVar variables. Because our implementation is quite
generic and can be used with any LocalSearchOperator and any problem, the features
of a solution we consider are the given IntVar variables and their values. In intensification
mode, we will keep certain variables fixed, i.e. bounded to a certain value and in diversification
mode, we will forbid some variables to take some values. Actually, we mix both modes: we
use two tenures: keep_tenure and forbid_tenure. We also keep two lists of variables:
keep_tabu_list_ and forbid_tabu_list_: variables in the keep_tabu_list_
must keep their values and variables in the forbid_tabu_list_ can not use the corre-
sponding values. To see what variables are selected and added to those lists, you have to look
at the code below. They will be kept in the lists for a certain tenure. The tenure can be seen
as a certain amount of time or a certain number of iterations. In our case, we consider each
time a new neighbor (candidate solution) is selected by the Local Search algorithm. We keep a
variable stamp_ that counts the number of iterations. Because we only start the Tabu Search
after the first optimum is reached, this variable is at 0 until the first local optimum is reached.
After that, every time a candidate solution is selected (including the starting solution when the
Local Search starts again after finding a local optimum), this variable is incremented.
Our aspiration criterion is simple: a solution is accepted no matter what if it is better than
any other solution encountered so far. Because of this aspiration criterion, our TabuSearch
Metaheuristic only add constraints. No variable has its domain skimmed or its value
fixed. These additional constraints always allow for a better solution to be accepted. To do so,
we use a Boolean IntVar aspiration variable and a Boolean IntVar tabu variable:
one of the two has to be equal to 1.

Some helpers

To know what a given variable should keep as value or not and how long, we define a simple
VarValue struct:
struct VarValue {
VarValue(IntVar* const var, int64 value, int64 stamp)
: var_(var), value_(value), stamp_(stamp) {}
IntVar* const var_;

240
Chapter 7. Meta-heuristics: several previous problems

const int64 value_;


const int64 stamp_;
};

Our lists will be declared as:


typedef std::list<VarValue> TabuList;

The std::list can be used as a FIFO list, i.e. a queue12 . You can add an element at
the beginning of the queue/list with its push_front() method and retrieve an element at
the end of the queue/list with its pop_back() method. To update a list with respect to the
time/iterations/tenures, we chop off the tail of the list whenever an element is outdated:
void AgeList(int64 tenure, TabuList* list) {
while (!list->empty() && list->back().stamp_ < stamp_ - tenure) {
list->pop_back();
}
}

The real method called is AgeLists() because it updates both lists and increment the
stamp_ variable.
void AgeLists() {
AgeList(keep_tenure_, &keep_tabu_list_);
AgeList(forbid_tenure_, &forbid_tabu_list_);
++stamp_;
}

The constructor and the variables

Let’s start with the (private) variables of the TabuSearch class:


const std::vector<IntVar*> vars_;
Assignment assignment_;
int64 last_;
TabuList keep_tabu_list_;
int64 keep_tenure_;
TabuList forbid_tabu_list_;
int64 forbid_tenure_;
double tabu_factor_;
int64 stamp_;
bool found_initial_solution_;

vars_ are the variables to use in both Tabu lists. Because of the genericity of our implementa-
tion, these variables must be used (changed) by the LocalSearchOperator13 . We keep an
Assignment with the last found solution and store its objective value in the last_ variable.
With the variables inherited from the Metaheuristic class, this will allow us to play with
the last_, current_ and best_ values.
12
FIFO stands for First In, First Out. Basically, a queue is a data structure that allows you to add elements and
retrieve them in their order of appearance.
13
Variables that are not changed by the LocalSearchOperator will not enter any Tabu list.

241
7.4. Tabu Search (TS)

Both Tabu lists and corresponding tenures have quite explicit names: keep_tabu_list_
and keep_tenure_ for the to be kept features list and forbid_tabu_list_ and
forbid_tenure_ for the forbidden features list.
The tabu_factor_ variable is a percentage of the number of variables that are following the
Tabu guidelines. If equal to 1.0, it means that all variables must follow the Tabu guidelines or
in other words, no violation of the Tabu criteria is allowed (except when the aspiration criterion
kicks in). If equal to 0.0, no variable must follow the Tabu guidelines or in other words, the
Tabu criteria can be fully violated.
stamp_ is a counter with the number of iterations. Until the Tabu Search is really launched,
this counter stays at 0. Finally, found_initial_solution_ is a Boolean that indicates if
an initial solution has been found or not.
The constructor is quite straightforward:
TabuSearch(Solver* const s,
bool maximize,
IntVar* objective,
int64 step,
const std::vector<IntVar*>& vars,
int64 keep_tenure,
int64 forbid_tenure,
double tabu_factor)

step denotes the decrease/increase of the objective value sought after each found feasible
solution. Because it is not dynamically changed, you probably want to keep this variable at 1.
This value is kept in the inherited variable step_.
Now that we have met the variables and helpers, we can discuss the callbacks of the
TabuSearch class. We ordered them below in a way to gradually have a big picture of
our algorithmic implementation, not in the order they are called.

LocalOptimum()

The LocalOptimum() method is called whenever a nested Local Search is finished. If


one SearchMonitor returns true in its LocalOptimum callback, the Local Search is
restarted and the search continues.
bool LocalOptimum() {
AgeLists();
if (maximize_) {
current_ = kint64min;
} else {
current_ = kint64max;
}
return found_initial_solution_;
}

This method launches the Tabu Search. Indeed, by calling AgeLists(), the stamp_ vari-
able is incremented and all the code in the if (0 != stamp_) condition (see below) can
be executed. As the Local Search will be relaunched, this method also updates the current_

242
Chapter 7. Meta-heuristics: several previous problems

variable. Finally, if an initial solution has been found, the method returns True to continue the
Local Search. If no solution has been previously found, the search is aborted.

AcceptNeighbor()

The AcceptNeighbor() method is called whenever a candidate solution is selected to be


the next solution in the Local Search algorithm14 . More precisely, it is called in the Next()
method of the FindOneNeighbor DecisionBuilder. This is called an iteration in the
Local Search.
void TabuSearch::AcceptNeighbor() {
if (0 != stamp_) {
AgeLists();
}
}

We age both lists and increment the stamp_ variable to acknowledge the iteration if we have
already obtained a local optimum in the Local Search.

AtSolution()

The AtSolution() method is called whenever a solution is found and accepted in the Local
Search. It called in the NextSolution() method of the current search of the CP Solver.
1 bool AtSolution() {
2 if (!Metaheuristic::AtSolution()) {
3 return false;
4 }
5 found_initial_solution_ = true;
6 last_ = current_;
7

8 if (0 != stamp_) {
9 for (int i = 0; i < vars_.size(); ++i) {
10 IntVar* const var = vars_[i];
11 const int64 old_value = assignment_.Value(var);
12 const int64 new_value = var->Value();
13 if (old_value != new_value) {
14 VarValue keep_value(var, new_value, stamp_);
15 keep_tabu_list_.push_front(keep_value);
16 VarValue forbid_value(var, old_value, stamp_);
17 forbid_tabu_list_.push_front(forbid_value);
18 }
19 }
20 }
21 assignment_.Store();
22

23 return true;
24 }

On lines 2 to 6, we update the current_, last_ and best_ values. We also set
found_initial_solution_ since we have a solution and hence an initial solution. On
14
I.e. filtered and accepted, see section 6.5.3 if needed.

243
7.4. Tabu Search (TS)

line 21 we store the current solution. This allows us to keep the values of the variables and
compare them with a new solution as we do in the lines 8 to 20. These lines are only pro-
cessed if we have reached our first local optimum (if (stamp_ != 0) {...}). If this
is the case, we consider among the variables given to the constructor, those that have changed
since last solution (if (old_value != new_value) {}). We force those variables to
keep their new values for keep_tenure iterations and forbid them to take their old value for
forbid_tenure iterations. Depending on the value of forbid_tenure, you can forbid
a variable to take several values.
Because we want the search to resume, we return true.

ApplyDecision()

The ApplyDecision() method is called when a Decision is about to be applied. This is


the place to add the constraints.
1 void TabuSearch::ApplyDecision(Decision* const d) {
2 Solver* const s = solver();
3 if (d == s->balancing_decision()) {
4 return;
5 }
6

7 IntVar* aspiration = s->MakeBoolVar();


8 if (maximize_) {
9 s->AddConstraint(
10 s->MakeIsGreaterOrEqualCstCt(objective_, best_ + step_,
11 aspiration));
12 } else {
13 s->AddConstraint(
14 s->MakeIsLessOrEqualCstCt(objective_, best_ - step_,
15 aspiration));
16 }
17

18 std::vector<IntVar*> tabu_vars;
19 for (const VarValue& vv : keep_tabu_list_) {
20 IntVar* tabu_var = s->MakeBoolVar();
21 Constraint* keep_cst =
22 s->MakeIsEqualCstCt(vv.var_, vv.value_, tabu_var);
23 s->AddConstraint(keep_cst);
24 tabu_vars.push_back(tabu_var);
25 }
26 for (const VarValue& vv : forbid_tabu_list_) {
27 IntVar* tabu_var = s->MakeBoolVar();
28 Constraint* forbid_cst =
29 s->MakeIsDifferentCstCt(vv.var_, vv.value_, tabu_var);
30 s->AddConstraint(forbid_cst);
31 tabu_vars.push_back(tabu_var);
32 }
33 if (tabu_vars.size() > 0) {
34 IntVar* tabu = s->MakeBoolVar();
35 s->AddConstraint(s->MakeIsGreaterOrEqualCstCt(
36 s->MakeSum(tabu_vars)->Var(),
37 tabu_vars.size() * tabu_factor_,

244
Chapter 7. Meta-heuristics: several previous problems

38 tabu));
39 s->AddConstraint(s->MakeGreaterOrEqual(s->MakeSum(aspiration, tabu),
40 1LL));
41 }
42

43 if (maximize_) {
44 const int64 bound = (current_ > kint64min) ?
45 current_ + step_ : current_;
46 s->AddConstraint(s->MakeGreaterOrEqual(objective_, bound));
47 } else {
48 const int64 bound = (current_ < kint64max) ?
49 current_ - step_ : current_;
50 s->AddConstraint(s->MakeLessOrEqual(objective_, bound));
51 }
52

53 if (found_initial_solution_) {
54 s->AddConstraint(s->MakeNonEquality(objective_, last_));
55 }
56 }

After quickly screening the content of this method, you might be curious about why we didn’t
encapsulate any code in a if (0 != stamp_) {...}. The reason is simple, we need to
be able to “revert” to “any” solution at any time if the aspiration criteria is used. At the same
time (lines 43 to 51), we force the solver to produce better solutions as an OptimizeVar
SearchMonitor would15 . We also add (lines 53 to 55) a constraint to forbid obtaining
solutions with the same values and thus avoid plateaus (i.e. solutions with the exact same
objective value one after the other). Let’s discuss the other lines.
Lines 3 to 5 are technical. Whenever we have the unique BalancingDecision, we know
that we shouldn’t do anything and let it pass. Lines 7 to 16 is about our aspiration criterion:
we accept a neighbor (candidate solution) if it improves the best solution found so far. These
constraints allow to accept solutions that will be found in the future and are considered Tabu.
Lines 18 to 41 are dedicated to add the corresponding constraints for the Tabu Search mech-
anism. First (lines 19 to 25) to store some features then (lines 26 to 32) to forbid some other
features. Each time, a BoolVar variable corresponding to the added equality (lines 21 to
23) or inequality (lines 28 to 30) is created and added to the std::vector<IntVar*>
tabu_vars list. This list is then used (lines 35 to 38) to control how many of these variables
(tabu_vars.size() * tabu_factor_) should really follow or not the Tabu criteria.
Finally, line 39 adds a constraint to balance the Tabu and aspiration criteria.

7.4.3 First results

You can find the code in the files jobshop.h, jobshop_ls.h, limits.h,
jobshop_tabu1.cc and jobshop_tabu2.cc and the data in the file abz9.

The code found in the file jobshop_ts1.cc is very similar to the code in
jobshop_ls1.cc from the previous chapter. We only use the SwapIntervals
15
Although the OptimizeVar SearchMonitor does not add constraints.

245
7.4. Tabu Search (TS)

LocalSearchOperator in the Local Search to solve the Job-Shop Problem16 because we


want to quickly reach a Local Optimum and compare both Local Searches with and without
Tabu Search.
As in jobshop_ls1.cc, we don’t devise any specialized search strategy and we use the
basic Solver::CHOOSE_RANDOM_RANK_FORWARD strategy to rank all tasks and then
the basic Solver::CHOOSE_FIRST_UNBOUND with Solver::ASSIGN_MIN_VALUE
to schedule each task at its earliest start time.
One main difference with the code in jobshop_ls1.cc is that we don’t use an
OptimizeVar SearchOperator but rely on the TabuSearch Metaheuristic to
minimize the objective_var IntVar variable.
What are the variables we will use with the TabuSearch class? As it only accepts (point-
ers to) IntVars, we will use the associated IntVar that represents the “ranking” of the
IntervalVars in the ranked sequence17 . This is exactly what the Next() method of the
SequenceVar class returns:
std::vector<IntVar*> tabu_vars;
for (int seq = 0; seq < all_sequences.size(); ++seq) {
SequenceVar * seq_var = all_sequences[seq];
for (int interval = 0; interval < seq_var->size(); ++interval ) {
IntVar * next = seq_var->Next(interval);
tabu_vars.push_back(next);
}
}

To create a TabuSearch instance, there is the following factory method:


SearchMonitor* Solver::MakeTabuSearch(bool maximize, IntVar* const v,
int64 step, const std::vector<IntVar*>& vars,
int64 keep_tenure, int64 forbid_tenure,
double tabu_factor) {
return RevAlloc(new TabuSearch(this, maximize, v, step, vars,
keep_tenure, forbid_tenure,
tabu_factor));
}

We use it like so:


SearchMonitor * tabu_search = solver.MakeTabuSearch(false,
objective_var,
1,
tabu_vars,
FLAGS_keep_tenure,
FLAGS_forbid_tenure,
FLAGS_tabu_factor);

Let’s compare the results of jobshop_ls1.cc and jobshop_ts1.cc for different pa-
rameters of the Tabu Search on the problem defined in file abz9. But first, recall that the Local
16
If you don’t remember anything about the Job-Shop Problem, don’t panic. You can read about it in the
previous chapter and if you prefer you can completely skip the problem definition and our implementation to
solve it (but not the use of LocalSearchOperators as our implementation only works with Local Search).
We only discuss here the use of the TabuSearch meta-heuristic.
17
You might want to reread the section 6.3.1 about variables used for scheduling in ortools.

246
Chapter 7. Meta-heuristics: several previous problems

Search in jobshop_ls1.cc, found a Local Optimum (26 th solution, after 35 seconds18 )


with a value of 1051 (and the Local Search stops there). For the file abz9, we have 300 Tabu
variables.
We now use the Tabu Search to try to improve this result. Whenever the Local Search will
reach this local optimum, the Tabu Search will kick in and hopefully get us out of this local
optimum trap. We use the NoImprovementLimit SearchLimit with the number of
solutions accepted without any improvement in the objective value set to 30 and infinite time.
The next table compares some results obtained with different Tabu parameters to solve the
problem in file abz9.
K F TF #Sol Time(s) #Best value Time (s)
10 5 1.0 123 202,235 92 948 145,135
10 10 1.0 123 199,682 93 948 143,416
5 10 1.0 157 294,816 127 987 249,965
15 10 1.0 123 289,651 93 948 237,480
10 0 1.0 123 154,212 92 948 119,058
10 30 1.0 77 199,009 46 1025 62,842
10 30 0.6 61 90,186 30 1049 40,498
10 10 0.6 61 106,086 30 1049 40,574
10 10 0.2 61 92,421 30 1049 40,576
10 10 0.0 57 87,766 26 1051 35,517
The three first columns show the following Tabu parameters: K represents the keep_tenure
value, F the forbid_tenure and TF the tabu_factor. The next two columns give re-
spectively the the total number of solution generated (#Sol) and total time (Time(s) in seconds)
that the corresponding algorithm took (when stopped by our SearchLimit). The last three
columns are about the best solution found: its number (#Best), value (value) and the number of
seconds needed to find it (Time (s)).
Some preliminary comments. First of all, the Tabu Search improves the search in most cases,
a good sign. The only case (last line) where it doesn’t improve the search is when we use a
tabu_factor of 0.0: no Tabu criterion needs to be fulfilled. The Tabu Search relaunches
the Local Search without any improvement and the NoImprovementLimit kicks in after
30 more solutions. Second, the Tabu Search is quite robust. The first five lines give similar
results for similar parameters. It seems that keeping some features of a solution is more
important than forbidding some. Third, softening the Tabu criterion (tabu_factor < 1)
doesn’t seem to work in this particular case.

Can we do better than our best Local Search described in jobshop_ls3.cc? The
best objective value was 931. You can read all about in section 6.7.5.

We added a Tabu Search method in file jobshop_ts2.cc to complete the code in


jobshop_ls3.cc19 . With the default parameters and the following Tabu parameters
keep_tenure=10, forbid_tenure=5 and tabu_factor=1.0, we obtain an objec-
18
You might be surprised by these 35 seconds if you compare them to the 51 seconds needed in the previous
chapter. Of course, we used another computer.
19
We use an OptimizeVar in file jobshop_ts2.cc because the initial solution is found by Local Search.
This OptimizeVar variable is not used in the Tabu Search.

247
7.5. Simulated Annealing (SA)

tive value of 924 (in 201,604 seconds). Much better than 931 but still very far from the optimal
value of 679. And we had roughly to work twice as hard to go from 931 to 924. This is typical.
To get even better results, one must implement specialized algorithms.

7.5 Simulated Annealing (SA)

In 1983, the world of combinatorial optimization was literally shattered by a paper of Kirk-
patrick et al. [Kirkpatrick1983] in which it was shown that a new heuristic approach called
Simulated Annealing could converge to an optimal solution of a combinatorial problem, albeit
in infinite computing time20 . This was really unheard of: an heuristic that could converge, if you
give it the time, to an optimal solution! This new approach spurred the interest of the research
community and opened a new domain of what is now known as meta-heuristic algorithms.
We first present the basic idea of SA, keeping a eye on our basic implementation that we present
in details next. As with Tabu Search, we present some first results on the Job-Shop Problem.

7.5.1 The basic idea

We describe the meta-heuristic as it is generally presented. Our implementation is a little bit


different.
The basic idea of the SA algorithm is to decide if a given neighbor xtest (candidate solution) in
the neighborhood Nx of a solution x should be accepted or not. This is done probabilistically: a
solution may or may not be accepted given a certain probability P (...). This probability evolves
with time and becomes lower and lower such that at the beginning of the algorithm a large part
of the search space can be reached and slowly but surely the probability converges toward 0
and the algorithm focuses on one (final) neighborhood. We say that the system moves toward
states of lower energy.
These probabilities P (...) (usually) depend on three factors:
• energy levels e = Energy(x) of the current solution x and etest = Energy(xtest ) of the
solution xtest ;
• the temperature t = Temperature(k, ...) of the system at a given iteration k.
and we write P (e, etest , t).
The algorithm stops when reaching an energy level that is low enough or if the Temperature(...)
reaches 0 (or any other desired condition). To be more precise, let’s look at a pseudo-code of
the general SA algorithm:
20
This is a theoretical result. In practice, the SA algorithm converges very slowly and visiting the complete
search tree would probably take less time!

248
Chapter 7. Meta-heuristics: several previous problems

Three functions are used:


• Energy(): measures the energy of the whole system: the smaller the better. Think about
the objective function in a minimization problem for instance. We seek to minimize the
energy level of the system.
• Temperature(): to mimic the annealing in metallurgy, temperature should decrease with
time and the system sees its energy level decrease. This function should decrease with
each iteration.
• Random(): this function returns a probability. i.e. a value between 0,0 and 1,0. It follows
a certain probabilistic law and any law can be used. Of course, the choice of the law
influences the algorithm.

Initialization

The initialization is quite simple: start with an initial solution (in or-tools: a local optimum) x0
and compute its energy level e = Energy(x0 ). The initial temperature plays an important role in
the algorithm: higher means that the algorithm (theoretically) has more chances of converging
toward a global optimum [Granville1994]. However, it also means that the algorithm will take
much longer to converge.

Stopping criteria

The usual stopping criteria can be used. However, often, a limited number of iterations
are allowed as we need the T emperature() to decrease towards 0 to compute the transi-
tion/acceptance probabilities P (e, etest , t) and let the algorithm converge.

249
7.5. Simulated Annealing (SA)

Acceptance probabilities

The algorithm accepts or rejects a new solution xtest depending on the computed acceptance
probabilities P (e, etest , t). When the temperature decreases towards 0 with time, this probability
should converge towards 0 when e < etest , i.e. the algorithm only accepts new states if their
energy level is lower than the energy level of the current (last) state. The classical aspiration
criteria is to accept any solution that is better, i.e. P (e, etest , t) = 1 if e > etest . For the algorithm
to be able to escape a local optimum, it must be able to move towards solution of higher energy,
i.e. P (e, etest , t) > 0 even if e < etest .
The way these probabilities are computed is really problem dependent and is a key operation
for a good convergence of the algorithm.

States or iterations?
Each meta-heuristic has its own vocabulary and SA is no exception. Broadly speak-
ing, a state corresponds to an accepted solution x by the SA algorithm while each
test of a neighbor (candidate solution) is considered as an iteration k. Typically, the
Temperature(...) depends on the current iteration and we speak about the energy
level of a state (even if we compute this energy level at each iteration).

Why is it called Simulated Annealing?


The name comes from annealing in metallurgy, a technique involving heating and con-
trolled cooling of a material to increase the size of its crystals and reduce their defects.
SA’s algorithm decreases slowly but surely the probability of accepting worse solutions as
time passes.

7.5.2 Our implementation21

Our implementation is a simplified version of the SA algorithm.

Our basic implementation of the SA algorithm differs slightly from the classical
implementation.

The constructor and the variables

Let’s start with the (private) variables of the SimulatedAnnealing class:


const int64 temperature0_;
int64 iteration_;
ACMRandom rand_;
bool found_initial_solution_;
21
We don’t re-detail the utility of each SearchMonitor callback. Read previous section on the Tabu Search
to refresh your memory if needed.

250
Chapter 7. Meta-heuristics: several previous problems

Most variables are self-explanatory. The ACMRandom rand_ variable is our random generator
as we need some randomness to generate the probabilities at each iteration. An iteration is
obtained each time we choose a new neighbor (candidate) solution.
The constructor is quite straightforward:
SimulatedAnnealing(Solver* const s, bool maximize,
IntVar* objective, int64 step,
int64 initial_temperature)
: Metaheuristic(s, maximize, objective, step),
temperature0_(initial_temperature),
iteration_(0),
rand_(654),
found_initial_solution_(false) {}

The temperature schedule

To compute the temperature for each iteration, we use the following (private) method:
float Temperature() const {
if (iteration_ > 0) {
return (1.0 * temperature0_) / iteration_; // Cauchy annealing
} else {
return 0.;
}
}

LocalOptimum()

As you know by now, this is the callback that triggers the meta-heuristic and we set our iteration
counter to 1 when this method is called for the first time.
bool LocalOptimum() {
if (maximize_) {
current_ = kint64min;
} else {
current_ = kint64max;
}
++iteration_;
return found_initial_solution_ && Temperature() > 0;
}

You might be surprised by the redundant test of Temperature() but our code might change
in the future and we need the temperature to be greater than 0. This is called defensive pro-
gramming.

AcceptNeighbor()

As in the Tabu Search, we increase our iteration counter for each accepted solution:

251
7.5. Simulated Annealing (SA)

void AcceptNeighbor() {
if (iteration_ > 0) {
++iteration_;
}
}

This is only done if we already have reached a local optimum.

AtSolution()

We simply return true to resume the search:


bool AtSolution() {
if (!Metaheuristic::AtSolution()) {
return false;
}
found_initial_solution_ = true;
return true;
}

In our basic version, the temperature only depends on the iteration.

ApplyDecision()

This is again the place to add some constraints to manage the search. We show the code first
and discuss it after:
1 void ApplyDecision(Decision* const d) {
2 Solver* const s = solver();
3 if (d == s->balancing_decision()) {
4 return;
5 }
6

7 const int64 energy_bound = Temperature() * log2(rand_.RndFloat());


8

9 if (maximize_) {
10 const int64 bound =
11 (current_ > kint64min) ? current_ + step_ + energy_bound :
12 current_;
13 s->AddConstraint(s->MakeGreaterOrEqual(objective_, bound));
14 } else {
15 const int64 bound =
16 (current_ < kint64max) ? current_ - step_ - energy_bound :
17 current_;
18 s->AddConstraint(s->MakeLessOrEqual(objective_, bound));
19 }
20 }

Code on lines 3 to 5 is to avoid any BalancingDecision. On line 7, we compute an energy


bound: this bound converges toward 0 with the temperature converging towards 0. Lines 9 to 19
compute a new bound for the objective value. Let’s focus on the minimization case. The energy
bound is always negative (rand_.RndFloat() returns a float between 0.0 (excluded)

252
Chapter 7. Meta-heuristics: several previous problems

and 1.0), so the bound is always positive or 0. The higher this bound, the more likely the Local
Search can escape a local optimum by accepting worse solutions. With time, as the temperature
decreases, this bound converges towards the classical bound current_ - step_ which is
the bound used in a regular Local Search: we only go downhill.

Is SA really efficient?
Among the three meta-heuristic we describe in this manual (Tabu Search, Guided Local
Search and SA), SA is certainly the most basic one. Simulated annealing can be seen as a
random walk on a search graph (See [Michiels2007] for instance). This means that a basic
version of SA is unlikely to give good results. As we said above, SA was one of the first
meta-heuristic on the market. Again, the implementation details are very important and a
good SA implementation can beat a sloppy Tabu Search for instance. Moreover, SA is, as
all meta-heuristics are, a canvas that can be further explored and transformed.

7.5.3 First results

You can find the code in the files jobshop.h, jobshop_ls.h, limits.h,
jobshop_sa1.cc and jobshop_sa2.cc and the data in the file abz9.

As with the Tabu Search, we’ll try to improve the search coded in jobshop_ls1.cc
from the previous chapter. You can find the code in the file jobshop_sa1.cc. We only use
the SwapIntervals LocalSearchOperator in the Local Search to solve the Job-Shop
Problem because we want to quickly reach a Local Optimum and compare both Local Searches
with and without Simulated Annealing. We also use the same SearchLimits to stop the
search.
Again, we don’t use an OptimizeVar variable for the objective function as we let the
SimulatedAnnealing Metaheuristic do its job.
The factory method is the following:
SearchMonitor* Solver::MakeSimulatedAnnealing(bool maximize,
IntVar* const v,
int64 step,
int64 initial_temperature) {
return RevAlloc(
new SimulatedAnnealing(this, maximize, v, step, initial_temperature));
}

and we use it like so:


SearchMonitor * simulated_annealing = solver.MakeSimulatedAnnealing(
false,
objective_var,
1,
FLAGS_initial_temperature);

We don’t post any result as this version is too basic to improve on the local optimum found in
the previous chapter. You can see that the search tries to escape this local optimum but without

253
7.6. Guided Local Search (GLS)

luck: the Local Search is really trapped even when we start with a very high temperature. We
can see that the efficiency of the algorithm also depends on the LocalSearchOperators
used. In this case, this operator simply cycles between a set of solutions.
Will we have better luck if we use the two LocalSearchOperators as in file
jobshop_ls3.cc? Let’s try. The code is in the file jobshop_sa2.cc22 . We don’t
present any code here as the changes are similar to what was done in file jobshop_ts2.cc
to add Tabu Search.
With different initial temperatures, we obtain the same result: a better value of 1016 (coming
from a local optimum of 1051). Not bad but still far from the optimal value 679. The reason
is again that our LocalSearchOperators cycle through some solutions and don’t visit the
whole search space.

7.6 Guided Local Search (GLS)

Guided Local Search is another successful meta-heuristic that emerged in the ‘90. It has been
successfully applied to a large number of difficult problems and has been particularly successful
in Routing Problems.
Our Guided Local Search implementation is especially tailored for the Routing Library. It uses
a callback to a cost function that takes two int64 indices corresponding to 2 nodes23 , i.e. the
cost of traversing an arc. If you can successfully translate the cost of using two variables i
and j one after the other in your objective function for your specific problem, then you can
use our implementation out of the box. Otherwise, you’ll have to create your own version.
We hope that after reading this section you’ll have a better idea on how you can do it. The
last sub-section gives you some hints if you want to adapt our implementation to solve your
problem.

Our Guided Local Search implementation is especially tailored for the Routing Li-
brary

Along the way, we’ll give you enough information to fully understand (almost) all the code and
understand the Routing Library (RL) conventions24 .
Among the three implemented meta-heuristics implemented in or-tools, GLS has certainly the
most refined and efficient (and thus complicated) implementation.

7.6.1 The basic idea

The GLS is a penalty-based method that sits on top of a Local Search. Its originality and
efficiency stems from the way it penalizes some features of a solution along the search. We
22
We use an OptimizeVar in file jobshop_sa2.cc because the initial solution is found by Local Search.
The OptimizeVar variable is not used in the Simulated Annealing.
23
There is also a version with 3 indices i, j and k where the cost function returns the cost of traversing an arc
(i, j) with a vehicle k, i.e. the cost of traversing an arc depends on the type of vehicles used. Read on.
24
See the sections 9.5 and 13.11 to understand the juicy details. We omit these details here as they are not
important for the understanding of the GLS algorithm.

254
Chapter 7. Meta-heuristics: several previous problems

assume minimization.

The augmented objective function

Denote by Ii the following indicator function:


(
1 if solution x has feature i
Ii (x) =
0 otherwise.

The GLS meta-heuristic penalizes some features of a local optimum. Let pi be a penalty at-
tached to a feature i and f denote the original objective function. The GLS meta-heuristic uses
the following augmented objective function g:
X
g(x) = f (x) + λ (Ii (x) · pi )
i

The idea is to let the Local Search find solutions with this new augmented objective function.
λ is called the penalty factor and can be used to tune the search to find similar solutions (a low
λ value, intensification) or completely different solutions (a high λ value, diversification).

The penalties and their modifications

Penalties usually start with a 0 value and are incremented by 1 with each local optimum. The
originality and efficiency of the GLS is that a feature is only penalized if its utility is large
enough. The idea is to penalize costly features but not penalize them too much if they often
show up. The utility function for a feature i in a solution x is defined as follows:

ci (x)
ui (x) = Ii (x) .
1 + pi

where ci () denotes the cost associated with feature i in solution x. If a feature i is not present
in a solution x, its utility for this solution is 0 (Ii (x) = 0). Otherwise, the utility is proportional
to the cost ci (x) of this feature in the solution x but tends to disappear whenever this feature
i is often penalized. A feature that shows up regularly in local optima might be part of a good
solution.

7.6.2 Our implementation

Our implementation is at the same time specific for Routing Problems but also generic for any
Routing Problem. The chosen features of a solution is the fact that an arc (i,j) is traversed
or not for this solution. So, we will speak of a (i,j) Arc feature (and talk about cij , uij and
pij ).
Our implementation is practically following the basic GLS guidelines by the book.
Let’s denote by dij the cost of traversing an arc (i, j) in a given solution. In our case, this is
given by the cost of the objective function for that arc and we have cij = dij . This cost can
depend on the type of vehicle used if we use different types of vehicles.

255
7.6. Guided Local Search (GLS)

Our augmented objective function is given by


X X
g(x) = dij (x) + λ (Iij (x) · pij · cij (x))
(i,j) (i.j)

Within the Routing Library, the penalty factor λ is given by the gflags command line flag
routing_guided_local_search_lambda_coefficient and is set to the value 0,1
by default.

7.6.3 GuidedLocalSearchPenalties

Penalties are stored in a GuidedLocalSearchPenalties class. This class is abstract and


two implementations exist depending on the data structure to store the penalties:
• GuidedLocalSearchPenaltiesTable: for dense GLS penalties using a matrix
std::vector<std::vector<int64> > and
• GuidedLocalSearchPenaltiesMap: for sparse GLS penalties using a
hash_map25 .
By default, the dense version is used but you can switch to the sparse version by setting the
cp_use_sparse_gls_penalties flag to true on the command line.
Here is the skeleton of the abstract class:
class GuidedLocalSearchPenalties {
public:
virtual ~GuidedLocalSearchPenalties() {}
virtual bool HasValues() const = 0;
virtual void Increment(const Arc& arc) = 0;
virtual int64 Value(const Arc& arc) const = 0;
virtual void Reset() = 0;
};

An Arc is simply a (from,to) pair:


typedef std::pair<int64, int64> Arc;

7.6.4 The abstract GuidedLocalSearch class

The GuidedLocalSearch class is a pure abstract base class. Two specialized implementa-
tions exist:
• BinaryGuidedLocalSearch (2-indices version): when all vehicles have the same
cost to traverse any arc (i, j) and
• TernaryGuidedLocalSearch (3-indices version): when the cost of traversing an
arc (i, j) also depends on the type of vehicle k.
We discuss these two classes in details later on.
25
The hash_map data structure is compiler dependent but it is exactly what its name says: a hash map.

256
Chapter 7. Meta-heuristics: several previous problems

To compare two Arcs

To compare two arcs, we use the following comparator:


struct Comparator {
bool operator()(const std::pair<Arc, double>& i,
const std::pair<Arc, double>& j) {
return i.second > j.second;
}
};

This struct is called a functor (or function object) and is basically a function call encap-
sulated in a class (or a struct). This is done by overloading the function call operator
(operator()) of the class (or struct)26 .
Notice that we compare the double values attached to each Arcs in the (Arc, double)
pairs. We’ll use this Comparator struct to compare utilities attached to Arcs.

The variables and the constructor

Let’s start with the (protected) variables of the GuidedLocalSearch class:


IntVar* penalized_objective_;
Assignment assignment_;
int64 assignment_penalized_value_;
int64 old_penalized_value_;
const std::vector<IntVar*> vars_;
hash_map<const IntVar*, int64> indices_;
const double penalty_factor_;
std::unique_ptr<GuidedLocalSearchPenalties> penalties_;
std::unique_ptr<int64[]> current_penalized_values_;
std::unique_ptr<int64[]> delta_cache_;
bool incremental_;

We cover the most interesting variables.


The penalized_objective_ P IntVar represents the penalized part of the penal-
ized objective function: λ (i.j) (Iij (x) · pij · cij (x)). When there are no penalties, the
pointer penalized_objective_ is set to nullptr. Actually, the expression of
penalized_objective_ is a little bit more complicated than that because of our choice
of added constraints. See the ApplyDecision() method below for more details.
We keep the current solution in assignment_ as usual.
assignment_penalized_value_
P is the value of the expression
λ (i.j) (Iij (x) · pij · cij (x)) for the current solution. old_penalized_value_ is
used to update the penalized value incrementally in the AcceptDelta method.
vars_ is an std::vector with our node variables.
26
This is a very common idiom in C++. Not only does it allow to construct more robust code
(you can use functions and/or classes) and use the STL (Standard Template Library) but it also al-
lows you to use states (variables) and most compilers can do some tricks to speed up the code. See
http://en.wikipedia.org/wiki/Function_object#In_C_and_C.2B.2B for more.

257
7.6. Guided Local Search (GLS)

indices_ is a hash_map to quickly find the index of a variable given as IntVar*.


penalty_factor is the penalty factor λ.
The penalties computed during the search are stored in a
GuidedLocalSearchPenalties object pointed to by the penalties_ variable:
for an Arc arc, penalties_->Value(arc) returns its current penalty.
Finally, the three last variables are used to update the penalized costs incrementally in the
AcceptDelta() method. We’ll discuss this method in details below.
The constructor is quite straightforward:
GuidedLocalSearch(Solver* const s, IntVar* objective, bool maximize,
int64 step, const std::vector<IntVar*>& vars,
double penalty_factor);

where step is the usual step used to force the objective function to improve.

The pure virtual methods and the helpers

The pure virtual methods that must be defined in a specialized GuidedLocalSearch class
are:
virtual int64 AssignmentElementPenalty(const Assignment& assignment,
int index) = 0;
virtual int64 AssignmentPenalty(const Assignment& assignment, int index,
int64 next) = 0;
virtual bool EvaluateElementValue(const Assignment::IntContainer&
container,
int64 index, int* container_index,
int64* penalty) = 0;
virtual IntExpr* MakeElementPenalty(int index) = 0;

The used of 2 indices (in the signature of AssignmentPenalty) indicates that our
GuidedLocalSearch class is really tailored to deal with arcs. The best way to understand
what these methods are supposed to do is to study their implementations in details.
• AssignmentElementPenalty() returns the penalized value associated to the arc
leaving node i in a given solution assignment. This penalized value is (for minimiza-
tion) equal to λ · pij (x) · cij (x) for a given solution x.
We need to do do a little incursion in the Routing Library (RL) before we can go on.
The RL (Routing Library) encodes the traversing of an arc (i, j) in a solution with
vars_[i] = j, i.e. from node i go to node j where vars_[i] denotes the IntVar
variable corresponding to node i and vars_ is an std::vector of such variables.
Back to AssignmentElementPenalty.
Here is the implementation of this method for the BinaryGuidedLocalSearch
class:
int64 AssignmentElementPenalty(
const Assignment& assignment, int index) {

258
Chapter 7. Meta-heuristics: several previous problems

return PenalizedValue(index, assignment.Value(vars_[index]));


}

where the PenalizedValue(int64 i, int64 j) helper method computes the


penalized value for a given arc (i, j):
1 int64 PenalizedValue(int64 i, int64 j) {
2 const Arc arc(i, j);
3 const int64 penalty = penalties_->Value(arc);
4 if (penalty != 0) {
5 const int64 penalized_value =
6 penalty_factor_ * penalty * objective_function_->Run(i, j);
7 if (maximize_) {
8 return -penalized_value;
9 } else {
10 return penalized_value;
11 }
12 } else {
13 return 0;
14 }
15 }

The test if (penalty != 0) on line 4 is simply to avoid costly


objective_function_->Run(i, j) calls.
• AssignmentPenalty() returns the cost of traversing an arc (i, j) in a given solution
assignment. It is the cost cij for a solution to have arc (feature) (i, j). i is given
by the index of the IntVar variable corresponding to node i and next is the node
index corresponding to node j. For the BinaryGuidedLocalSearch, this method
is defined as:
int64 AssignmentPenalty(const Assignment& assignment,
int index, int64 next) {
return objective_function_->Run(index, next);
}

This cost is the same for all vehicles. In the case of the
TernaryGuidedLocalSearch class, we need to take the type of vehicle
traversing the arc (i, j) into account. We added a reference to a given Assignment
assignment to induce from this solution assignment what the type of vehicle
traversing arc (i, j) is. The type of vehicle traversing from node i is given by the
secondary_vars_[i] variable:
int64 AssignmentPenalty(const Assignment& assignment,
int index, int64 next) {
return objective_function_->Run(index, next,
assignment.Value(secondary_vars_[index]));
}

• EvaluateElementValue() evaluates the penalized value of a given arc (i, j).


It does so by using a shortcut to Assignment::IntContainers instead of
Assignments and IntVarElements instead of IntVars for efficiency. It also tests
if a node is part of a solution. In the Routing Library, one can disable a node, i.e. make
this node disappear as it never existed. If the node is not disabled, i.e. active, the penal-

259
7.6. Guided Local Search (GLS)

ized value is stored in a variable pointed to by penalty and the method returns true,
otherwise it returns false.
Here is the implementation for the BinaryGuidedLocalSearch class:
bool EvaluateElementValue(
const Assignment::IntContainer& container,
int64 index,
int* container_index,
int64* penalty) {
const IntVarElement& element = container.Element(*container_index);
if (element.Activated()) {
*penalty = PenalizedValue(index, element.Value());
return true;
}
return false;
}

The EvaluateElementValue() method is only used in the Evaluate() helper


of the GuidedLocalSearch:
1 int64 Evaluate(const Assignment* delta,
2 int64 current_penalty,
3 const int64* const out_values,
4 bool cache_delta_values) {
5 int64 penalty = current_penalty;
6 const Assignment::IntContainer& container =
7 delta->IntVarContainer();
8 const int size = container.Size();
9 for (int i = 0; i < size; ++i) {
10 const IntVarElement& new_element = container.Element(i);
11 IntVar* var = new_element.Var();
12 int64 index = -1;
13 if (FindCopy(indices_, var, &index)) {
14 penalty -= out_values[index];
15 int64 new_penalty = 0;
16 if (EvaluateElementValue(container,
17 index,
18 &i,
19 &new_penalty)) {
20 penalty += new_penalty;
21 if (cache_delta_values) {
22 delta_cache_[index] = new_penalty;
23 }
24 }
25 }
26 }
27 return penalty;
28 }

This method updates the penalty of the whole solution given by a delta Assignment
and is only called in AcceptDelta(). Recall that this delta is the difference be-
tween the last accepted solution xi of the Local Search and the candidate solution we are
currently testing. We will not go into all the details. Just notice how the penalized value
(variable penalty) is updated on lines 14 and 20.

260
Chapter 7. Meta-heuristics: several previous problems

• MakeElementPenalty() returns an IntExpr (pointer) to an Element expression


(pointer) that can be casted to an IntVar (pointer). We use these variables to compute
the penalized part of the augmented objective function in such a way that we can add
constraints with this expression.
For the BinaryGuidedLocalSearch the Element variable is computed as fol-
lows:
IntExpr* MakeElementPenalty(int index) {
return solver()->MakeElement(
NewPermanentCallback(this,
&BinaryGuidedLocalSearch::PenalizedValue,
static_cast<int64>(index)),
vars_[index]);
}

In MakeElementPenalty(), NewPermanentCallback() with its sec-


ond parameter static_cast<int64>(index) sets the first parameter of
PenalizedValue() to index, i.e. we use a callback that returns the cost associated
to have an arc outgoing from node i in a solution. The generated expression ensures that
we compute the right penalized value for a given solution.
Let’s now review the implemented SearchMonitor callbacks for the
GuidedLocalSearch class. The chosen order of presentation is pedagogical. Remember
that the code is generic and is used for the 2- and 3-indices versions.

EnterSearch()

This is where you initialize your code before a search is launched.


void EnterSearch() {
Metaheuristic::EnterSearch();
penalized_objective_ = nullptr;
assignment_penalized_value_ = 0;
old_penalized_value_ = 0;
...
penalties_->Reset();
}

This is a basic initialization. Of particular interest, notice how we set


penalized_objective_ to nullptr. We do this each time there are no penalties
and later we can test if (penalized_objective_ != nullptr).

LocalOptimum()

The LocalOptimum() method is called whenever a nested Local Search has finished. If
one SearchMonitor returns true in its LocalOptimum() callback, the Local Search is
restarted and the search continues. In this method, we penalize the features of the local optimum
solution according to their utility. Recall that the feature used here is whether the solution

261
7.6. Guided Local Search (GLS)

traverses an arc (i, j) or not. We use the utility function described earlier:
( c (x)
ij
if arc (i, j) is used;
uij (x) = 1+pij
0 otherwise.

and penalize the most expensive used arcs (i, j) according to their utility.
Let’s recall the way the RL (Routing Library) encodes the traversing of an arc (i, j) in a solution
with vars_[i] = j, i.e. from node i go to node j where vars_[i] denotes the IntVar
variable corresponding to node i. If no arc is traversed from node i (for instance node i is an
arrival depot or is not visited at all in a solution), RL’s convention is to set vars_[i] = i.
Because we only update the penalties in this callback, notice that the GLS is only triggered
after a local optimum has been found.
We are now ready to read the code:
1 bool LocalOptimum() {
2 std::vector<std::pair<Arc, double> > utility(vars_.size());
3 for (int i = 0; i < vars_.size(); ++i) {
4 if (!assignment_.Bound(vars_[i])) {
5 // Never synced with a solution, problem infeasible.
6 return false;
7 }
8 const int64 var_value = assignment_.Value(vars_[i]);
9 const int64 value =
10 (var_value != i) ? AssignmentPenalty(assignment_, i, var_value) : 0;
11 const Arc arc(i, var_value);
12 const int64 penalty = penalties_->Value(arc);
13 utility[i] = std::pair<Arc, double>(arc, value / (penalty + 1.0));
14 }
15 Comparator comparator;
16 std::stable_sort(utility.begin(), utility.end(), comparator);
17 int64 utility_value = utility[0].second;
18 penalties_->Increment(utility[0].first);
19 for (int i = 1; i < utility.size() &&
20 utility_value == utility[i].second; ++i) {
21 penalties_->Increment(utility[i].first);
22 }
23 if (maximize_) {
24 current_ = kint64min;
25 } else {
26 current_ = kint64max;
27 }
28 return true;
29 }

The method is divided in 3 sections: lines 2 to 14 to compute the utilities, lines 15 to 22 to


penalize the arcs according to their utilities and finally lines 23 to 28 to reset the value of the
current_ variable that we use to bound our solutions in the Local Search.
In the first section (lines 2 to 14), we compute the utilities as follow. The utility of each
variable vars_[i] is stored in the std::vector<std::pair<Arc, double> >
utility array. As you can read, we have to test if the solution if feasible, i.e. if each
of its variable is bounded or not. This is done on lines 4 to 7. For an arc (i, j) ((i,

262
Chapter 7. Meta-heuristics: several previous problems

var_value)), we compute its cost value: 0 if the arc is not traversed in the solution or
AssignmentPenalty(assignment_, i, var_value) otherwise, i.e. the cost to
c
traverse arc (i, j) in the solution. On line 13, the utility pijij+1 is computed for the outgoing arc
(i, j).
In the second section (lines 15 to 22), we only penalize arcs with the highest utility. First, we
sort the utilities in descending order with the help of our Comparator in lines 15 and 16. On
lines 17 and 18, we penalize the arc with the highest utility. The for loop on lines 19 to 22, pe-
nalize only the arcs with the same utility (utility_value == utility[i].second).
The third section (lines 23 to 28) is by now no surprise. We reset the value of the current_
variable such that we can bound the solutions in the Local Search by a higher value than for
instance the value of the best solution: this allows the meta-heuristic to escape local optima.

AtSolution()

The AtSolution() method is called whenever a solution is found and accepted in the Local
Search
bool AtSolution() {
if (!Metaheuristic::AtSolution()) {
return false;
}
if (penalized_objective_ != nullptr) { // no move has been found
current_ += penalized_objective_->Value();
}
assignment_.Store();
return true;
}

We update the best solution (Metaheuristic::AtSolution()) and the augmented ob-


jective function g. This is done as follow: first we update the current_ variable with the
current P
objective value (again in Metaheuristic::AtSolution()) and then we add the “penalized
part” λ (i.j) (Iij (x) · pij · cij (x)) from penalized_objective_->Value(). We also
store the current solution.

ApplyDecision()

The ApplyDecision() method is called when a Decision is about to be applied. This is


the place to add the constraints.
1 void ApplyDecision(Decision* const d) {
2 if (d == solver()->balancing_decision()) {
3 return;
4 }
5 std::vector<IntVar*> elements;
6 assignment_penalized_value_ = 0;
7 if (penalties_->HasValues()) {
8 for (int i = 0; i < vars_.size(); ++i) {
9 IntExpr* expr = MakeElementPenalty(i);
10 elements.push_back(expr->Var());

263
7.6. Guided Local Search (GLS)

11 const int64 penalty = AssignmentElementPenalty(assignment_, i);


12 current_penalized_values_[i] = penalty;
13 delta_cache_[i] = penalty;
14 assignment_penalized_value_ += penalty;
15 }
16 old_penalized_value_ = assignment_penalized_value_;
17 incremental_ = false;
18 penalized_objective_ = solver()->MakeSum(elements)->Var();
19 if (maximize_) {
20 IntExpr* min_pen_exp =
21 solver()->MakeDifference(current_ + step_, penalized_objective_);
22 IntVar* min_exp =
23 solver()->MakeMin(min_pen_exp, best_ + step_)->Var();
24 solver()->AddConstraint(
25 solver()->MakeGreaterOrEqual(objective_, min_exp));
26 } else {
27 IntExpr* max_pen_exp =
28 solver()->MakeDifference(current_ - step_, penalized_objective_);
29 IntVar* max_exp =
30 solver()->MakeMax(max_pen_exp, best_ - step_)->Var();
31 solver()->AddConstraint(solver()
32 ->MakeLessOrEqual(objective_, max_exp));
33 }
34 } else {
35 penalized_objective_ = nullptr;
36 if (maximize_) {
37 const int64 bound =
38 (current_ > kint64min) ? current_ + step_ : current_;
39 objective_->SetMin(bound);
40 } else {
41 const int64 bound =
42 (current_ < kint64max) ? current_ - step_ : current_;
43 objective_->SetMax(bound);
44 }
45 }
46 }

Basically, this method adds the following constraint:


• when minimizing:
objective <= Max(current penalized cost - penalized_objective - step, best so-
lution cost - step)
• when maximizing:
objective >= Min(current penalized cost - penalized_objective + step, best so-
lution cost + step)
where “current penalized cost” is the augmented objective function value g(x) of the current
solution x and “penalized_objective” - despite its name - corresponds to the penalized part of
the augmented objective function but expressed as an IntExpr.
Let’s dig into the code. As usual, we have to disregard the BalancingDecision on lines
2 to 4. Then we test if we have penalties on line 7. If not (lines 34 to 45), we simply add -
in case of minimization - the constraint objective <= current_ - step_ but we do

264
Chapter 7. Meta-heuristics: several previous problems

it like an ObjectiveVar by modifying the upper bound on the domain of the objective
variable. This avoids one more constraint and is perfectly in line with our aspiration criterion
to accept better solution.
The test penalties_->HasValues() on line 7 is true if there is at least one arc with a
positive penalty.
If there is one or more penalties, we enter the code on the lines 8 to 32. For each arc (i, j) ((i,
assignment_.Value(vars_[i]))) , we create an Element expression corresponding
to the Element constraint for the corresponding penalty on line 9. All these Element ex-
pressions are collected into a sum stored in the variable penalized_objective_ on line
18. Lines 11 to 14 compute and store the penalized part of the augmented objective function
individually for each node. We skip lines 16 and 17 as they update variables to use with the
deltas. Finally, we add the constraint mentioned right after the code in lines 19 to 33. Notice
that the part “objective <= current penalized cost - penalized_objective - step” of this constraint
for the current solution reduces to “objective <= objective - step” and that the second part allows
us to accept better solutions (aspiration criterion).

AcceptDelta()

This meta-heuristic is coded efficiently and uses the delta and deltadelta of the
LocalSearchOperators. A quick reminder:
• delta: the difference between the initial solution that defines the neighborhood and the
current candidate solution.
• deltadelta: the difference between the current candidate solution and the previous
candidate solution. We say that the LocalSearchOperator is incremental.
The AcceptDelta() method of a SearchMonitor can accept or refuse a candidate
solution. It is filtering the solutions and the result of this callback in the main Local
Search algorithm (see the sub-section The basic Local Search algorithm and the callback
hooks for the SearchMonitors) is stored in a variable that has a very interesting name:
meta_heuristics_filter.
The AcceptDelta() callback from the GuidedLocalSearch class computes the penal-
ized value corresponding to the deltas and modifies their objective bound accordingly.
1 bool AcceptDelta(Assignment* delta, Assignment* deltadelta) {
2 if ((delta != nullptr || deltadelta != nullptr) &&
3 penalties_->HasValues()) {
4 int64 penalty = 0;
5 if (!deltadelta->Empty()) {
6 if (!incremental_) {
7 penalty = Evaluate(delta,
8 assignment_penalized_value_,
9 current_penalized_values_.get(),
10 true);
11 } else {
12 penalty = Evaluate(deltadelta,
13 old_penalized_value_,
14 delta_cache_.get(),
15 true);

265
7.6. Guided Local Search (GLS)

16 }
17 incremental_ = true;
18 } else {
19 if (incremental_) {
20 for (int i = 0; i < vars_.size(); ++i) {
21 delta_cache_[i] = current_penalized_values_[i];
22 }
23 old_penalized_value_ = assignment_penalized_value_;
24 }
25 incremental_ = false;
26 penalty = Evaluate(delta,
27 assignment_penalized_value_,
28 current_penalized_values_.get(),
29 false);
30 }
31 old_penalized_value_ = penalty;
32 if (!delta->HasObjective()) {
33 delta->AddObjective(objective_);
34 }
35 if (delta->Objective() == objective_) {
36 if (maximize_) {
37 delta->SetObjectiveMin(
38 std::max(std::min(current_ + step_ - penalty, best_ + step_),
39 delta->ObjectiveMin()));
40 } else {
41 delta->SetObjectiveMax(
42 std::min(std::max(current_ - step_ - penalty, best_ - step_),
43 delta->ObjectiveMax()));
44 }
45 }
46 }
47 return true;
48 }

This method returns true on line 47 as it accepts every delta. The whole update can only be
applied if at least a delta is present and if penalties exist. This is precisely the test on lines 2
and 3. The code on lines 4 to 31 updates the penalized value of the candidate solution. The code
is a little bit intricate because it has to be generic: we test the presence of the deltadelta and
delta data structures and update the incremental_ parameter accordingly. When then use
the best (aka most efficient) method to update this penalized value with a call to Evaluate().
On lines 35 to 45, we update the bound of delta: this can speed up the process to accept or
reject this candidate solution.

7.6.5 The real classes

GuidedLocalSearch classes come in two flavors:


• BinaryGuidedLocalSearch:
• TernaryGuidedLocalSearch:

266
Chapter 7. Meta-heuristics: several previous problems

BinaryGuidedLocalSearch

The BinaryGuidedLocalSearch class is used for Routing Problems where the traversing
of an edge doesn’t depend on the type of vehicles, i.e. the cost is the same for all vehicles.
Here is the constructor:
BinaryGuidedLocalSearch::BinaryGuidedLocalSearch(
Solver* const solver,
IntVar* const objective,
Solver::IndexEvaluator2* objective_function,
bool maximize,
int64 step,
const std::vector<IntVar*>& vars,
double penalty_factor)
: GuidedLocalSearch(solver,
objective,
maximize,
step,
vars,
penalty_factor),
objective_function_(objective_function) {
objective_function_->CheckIsRepeatable();
}

The variables vars are the main variables corresponding to the nodes. The objective function
is a callback that takes two int64 and returns an int64. Basically, it’s the cost of traversing
the arc (i, j).
The corresponding factory method is:
SearchMonitor* Solver::MakeGuidedLocalSearch(
bool maximize,
IntVar* const objective,
ResultCallback2<int64, int64, int64>* objective_function,
int64 step,
const std::vector<IntVar*>& vars,
double penalty_factor) {
return RevAlloc(new BinaryGuidedLocalSearch(this,
objective,
objective_function,
maximize,
step,
vars,
penalty_factor));
}

TernaryGuidedLocalSearch

This version was especially made to deal with heterogeneous costs for different vehicles in the
Routing Library: the cost of an arc also depends on the vehicle used. At the initialization of the
Routing Solver, the GuidedLocalSearch meta-heuristic is created as follow:

267
7.6. Guided Local Search (GLS)

...
switch (metaheuristic) {
case ROUTING_GUIDED_LOCAL_SEARCH:
if (CostsAreHomogeneousAcrossVehicles()) {
optimize = solver_->MakeGuidedLocalSearch(
false, cost_,
NewPermanentCallback(this, &RoutingModel::GetHomogeneousCost),
FLAGS_routing_optimization_step, nexts_,
FLAGS_routing_guided_local_search_lambda_coefficient);
} else {
optimize = solver_->MakeGuidedLocalSearch(
false, cost_,
NewPermanentCallback(this, &RoutingModel::GetArcCostForVehicle),
FLAGS_routing_optimization_step, nexts_, vehicle_vars_,
FLAGS_routing_guided_local_search_lambda_coefficient);
}
break;
...
}

If the costs are the same for all vehicles, we use the int64
RoutingModel::GetHomogeneousCost(int64 i, int64 j) costs. This
method takes two int64: the index of the first node i and the index of the second node j. If
on the contrary, the costs depend on the vehicle traversing an arc (i, j), we use the int64
RoutingModel::GetArcCostForVehicle(int64 i, int64 j, int64 k)
costs: the third int64 k corresponds to the index of the vehicle type used to traverse the arc
(i, j).
The corresponding factory method is:
SearchMonitor* Solver::MakeGuidedLocalSearch(
bool maximize,
IntVar* const objective,
ResultCallback3<int64, int64, int64, int64>* objective_function,
int64 step,
const std::vector<IntVar*>& vars,
const std::vector<IntVar*>& secondary_vars,
double penalty_factor) {
return RevAlloc(new TernaryGuidedLocalSearch(this,
objective,
objective_function,
maximize,
step,
vars,
secondary_vars,
penalty_factor));
}

The secondary secondary_vars variables are simply the variables corresponding to the
vehicles.

268
Chapter 7. Meta-heuristics: several previous problems

7.6.6 Guidelines to write your own GLS

GLS is a good meta-heuristic and it might be worth to give it a try to solve your problem.
As we have seen, our implementation of the GLS is heavily optimized: not only do we use
GLS filtering (AcceptDelta()) but the implementation
P is especially tailored for Routing
Problems and objective functions of the form (i,j) cij . What if you have a problem that
doesn’t fit into this canvas? Create your own version of the GLS!
We give you some hints on how to do that in this sub-section.
First, you have to change the call to a 2-indices or 3-indices callbacks to compute the objective
function value.
Second, if you look carefully at the code of the abstract GuidedLocalSearch class, you’ll
find that the only method that really depends on 2 indices is the AssignmentPenalty()
method. This method is only used in the LocalOptimum() callback.
Third, you have to adapt all 2- and 3-indices data structures such as for instance the
GuidedLocalSearchPenalties classes.
Finally, you have to decide if you need GLS filtering or not.
All in all, the GuidedLocalSearch, BinaryGuidedLocalSearch,
TernaryGuidedLocalSearch and GuidedLocalSearchPenalties,
GuidedLocalSearchPenaltiesTable, GuidedLocalSearchPenaltiesMap
classes give you a good example on how to implement your own GLS.

7.7 Large neighborhood search (LNS): the Job-Shop


Problem

We have seen in the previous chapter that one of the difficulties of Local Search is to define the
right notion of neighborhood:
• too small and you might get stuck in a local optimum;
• too big and you might loose precious time exploring huge neighbourhoods without any
guarantee to find a good solution.
Could we combine advantages of both approaches? Visit huge neighborhoods but only paying
the cost needed to visit small neighborhoods? This is what Very Large-Scale Neighbourhood
(VLSN)27 methods try to achieve. The basic idea is to create large neighborhoods but to only
(heuristically) visit the more interesting parts of it.
Large Neighbourhood Search (LNS) is one of those VLN methods and is especially well
suited to be combined with Constraint Programming.
27
Very Large-Scale Neighbourhood methods are more defined by the fact that the neighborhoods considered
are growing exponentially in the size of the input than the way these neighborhoods are explored. But if you want
to explore these huge neighborhoods efficiently, you must do so heuristically, hence our shortcut in the “definition”
of Very Large-Scale Neighbourhood methods.

269
7.7. Large neighborhood search (LNS): the Job-Shop Problem

7.7.1 What is Large Neighborhood Search?

The Large Neighborhood Search (LNS) meta-heuristic was proposed by Shaw in 1998
[Shaw1998]. The neighborhood of a solution is defined implicitly by a destroy and a repair
methods. A destroy method destroys part of the current solution while a repair method re-
builds the destroyed solution. Typically, the destroy method contains some randomness such
that different parts of the current solution are destroyed and... different parts of the search tree
visited! This means that the neighborhoods can be seen as larger than in “classical” Local
Search, hence the name.
In its very basic form, we could formulate large neighborhood search like this:

Often, steps 1. and 2. are done simultaneously. This is the case in or-tools.
It looks very much like Local Search, the only difference is the way the neighborhoods are
constructed.
As always, the definition of the destroy and repair methods is a matter of trade-off.
An important concept is the degree of destruction: if only a small part of a solution is destructed,
the LNS misses its purpose and merely becomes a “classical” Local Search method acting on
small neighborhoods. If a very large part (or the entirety) of the solution is destructed, then the
reconstruction process consists in repeated (full) optimizations from scratch.
Various scenarios are possible for the repair method ranging from reconstructing optimally the
destructed (partial) solution or using weak but very quick heuristics to reconstruct it. In the
first case, you obtain the best possible completed solution but it is often costly, in the second
case you obtain a probably bad solution but very quickly. Most probably, you’ll want to use an
intermediate scenario: devise an heuristic that reconstruct quite quickly not too bad solutions.
When Large Neighborhood Search is used in combination with Constraint Programming, we
often use the term fix for the destroy method and optimize for the repair method. Indeed, the
destruction is done by freeing some variables and thus fixing the remaining ones to their current
values and the repairing consists in optimizing this solution while keeping the fixed variables
to their current values.

7.7.2 Large Neighborhood Search in or-tools

You can find the code in the files dummy_lns.cc.

Large Neighborhood Search is implemented with LocalSearchOperators in


or-tools. For IntVars, there is a specialized BaseLNS class that inherits from

270
Chapter 7. Meta-heuristics: several previous problems

IntVarLocalSearchOperator. For IntervalVars and SequenceVars, you


can inherit from the corresponding LocalSearchOperators. We’ll use the BaseLNS
class in this sub-section and inherit from SequenceVarLocalSearchOperator when
we’ll try to solve the Job-Shop Problem below.
Our basic example from previous chapter is to minimize the sum of n IntVars {x0 , . . . , xn−1 }
each with domain [0, n − 1]. We add the fictive constraint x0 > 1 (and thus ask for n > 2):

min x0 + x1 + ... + xn−1


x0 ,...,xn−1

subject to: x0 > 1.


xi ∈ {0, . . . , n − 1} for i = 0 . . . n − 1.

For IntVars, you can use the BaseLNS class. In this LocalSearchOperator, we have
redefined the OnStart() and MakeOneNeighbor() methods like this:
LocalSearchOperator BaseLNS
OnStart() InitFragments()
MakeOneNeighbor() NextFragment()
A Fragment is just an std::vector<int> containing the indices of the IntVars to “de-
stroy”, i.e. to free. The other IntVars keep their current values. The complementary
DecisionBuilder given to the LocalSearchOperator will repair the current solu-
tion. The signature of the NextFragment() is as follow:
virtual bool NextFragment(std::vector<int>* fragment) = 0;

This method is a pure virtual method and must be defined. To free some variables, you
fill the fragment vector with the corresponding indices. This method returns true if
their are still candidates solutions in the neighborhood, false otherwise (exactly like the
MakeOneNeighbor() method).
Let’s use a basic LNS to solve our basic problem. We’ll free one variable at a time in the order
given by the std::vector of IntVars. First, we initialize the index of the first variable in
InitFragments():
virtual void InitFragments() { index_ = 0; }

where index_ is a private int indicating the current index of the variable we are about to
destroy.
The NextFragment() method is straightforward:
virtual bool NextFragment(std::vector<int>* fragment) {
const int size = Size();
if (index_ < size) {
fragment->push_back(index_);
++index_;
return true;
} else {
return false;
}
}

271
7.7. Large neighborhood search (LNS): the Job-Shop Problem

This time, let’s repair optimally the destroyed solution. The NestedOptimize
DecisionBuilder is exactly what we need as a complementary DecisionBuilder.
It will collapse a search tree described by a DecisionBuilder db and a set of monitors
and wrap it into a single point.
There exist several factory methods to construct such a NestedOptimize
DecisionBuilder but all need an Assignment to store the optimal solution found:
Assignment * const optimal_candidate_solution = s.MakeAssignment();
optimal_candidate_solution->Add(vars);
optimal_candidate_solution->AddObjective(sum_var);

The factory method we will use look like this:


DecisionBuilder* MakeNestedOptimize(DecisionBuilder* const db,
Assignment* const solution,
bool maximize,
int64 step);

where db is the DecisionBuilder used to optimize, solution stores the optimal solu-
tion found (if any), maximize is a bool indicating if we maximize or minimize and step is
the classical step used to optimize.
For our basic example, we use a basic DecisionBuilder to optimize:
DecisionBuilder * optimal_complementary_db = s.MakeNestedOptimize(
s.MakePhase(vars,
Solver::CHOOSE_FIRST_UNBOUND,
Solver::ASSIGN_MAX_VALUE),
optimal_candidate_solution,
false,
1);

We then construct our LNS operator:


OneVarLns one_var_lns(vars);

and wrap the Local Search:


LocalSearchPhaseParameters* ls_params
= s.MakeLocalSearchPhaseParameters(&one_var_lns,
optimal_complementary_db,
limit);
DecisionBuilder* ls = s.MakeLocalSearchPhase(initial_solution,
ls_params);

where limit is a SearchLimit and initial_solution is our initial solution. When


n=4, this initial solution is [3, 2, 3, 2].
The simplified output of dummy_lns is:
Simple Large Neighborhood Search with initial solution

Start search, memory used = 15.21 MB


Root node processed (time = 0 ms, constraints = 2, memory used =
15.21 MB)

272
Chapter 7. Meta-heuristics: several previous problems

Solution #0 (objective value = 10, ...)


Solution #1 (objective value = 8, ...)
Solution #2 (objective value = 6, ...)
Solution #3 (objective value = 3, ...)
Solution #4 (objective value = 1, ...)
Finished search tree, ..., neighbors = 10, filtered neighbors = 10,
accepted neigbors = 4, ...)
End search (time = 1 ms, branches = 58, failures = 57, memory used =
15.21 MB, speed = 58000 branches/s)
Objective value = 1

5 solutions were generated with decreased objective values. Solution #0 is the initial solu-
tion given: [3, 2, 3, 2]. For the next 4 solutions, the NestedOptimize DecisionBuilder
did its job and optimized the partial solution:
neighborhood 1 around [3, 2, 3, 2]: [−, 2, 3, 2] is immediately taken as the complementary
DecisionBuilder transforms it into the optimal (for this DecisionBuilder) so-
lution [1, 2, 3, 2] with an objective value of 8.
neighborhood 2 around [1, 2, 3, 2]: [−, 2, 3, 2] is rejected as the optimal solution [1, 2, 3, 2]
doesn’t have a better objective value than 8. [1, −, 3, 2] is immediately accepted as the
optimal solution constructed is [1, 0, 3, 2] with an objective value of 6.
neighborhood 3 around [1, 0, 3, 2]: [−, 0, 3, 2] and [1, −, 3, 2] are rejected and [1, 0, −, 2] is
accepted as the optimal solution constructed is [1, 0, 0, 2] with an objective value of 3.
neighborhood 4 around [1, 0, 0, 2]: [−, 0, 0, 2], [1, −, 0, 2] and [1, 0, −, 2] are rejected while
[1, 0, 0, −] is accepted as the optimal solution constructed [1, 0, 0, 0] has an objective
value of 1.
The two last lines printed by the SearchLog summarize the local search:
Finished search tree, ..., neighbors = 10, filtered neighbors = 10,
accepted neigbors = 4, ...)
End search (time = 1 ms, branches = 58, failures = 57, memory used =
15.21 MB, speed = 58000 branches/s)
Objective value = 1

There were indeed 10 constructed candidate solutions among which 10 (filtered neighbors)
were accepted after filtering (there is none!) and 4 (accepted neighbors) were improving solu-
tions.
For this basic example, repairing optimally led to the optimal solution but this is not necessarily
the case.

7.7.3 Interesting LNS operators

At the moment of writing (28 th of February 2015, rev 3845), there are only a few specialized
LNS operators. All concern IntVars:
• There are two basic LNS operators:

273
7.7. Large neighborhood search (LNS): the Job-Shop Problem

– SimpleLNS: Frees a number of contiguous variables (à la mod(m)) in the


std::vector<IntVar*>;
– RandomLNS: Frees a number m of randomly chosen variables from the
std::vector<IntVar*>.
• Some PathOperators. We’ll see PathOperators more in details in the section
Local Search PathOperators.

SimpleLNS

The SimpleLNS LocalSearchOperator frees a number of contiguous variables. Its


NextFragment() method reads:
bool NextFragment(std::vector<int>* fragment) {
const int size = Size();
if (index_ < size) {
for (int i = index_; i < index_ + number_of_variables_; ++i) {
fragment->push_back(i % size);
}
++index_;
return true;
} else {
return false;
}
}

The factory method to create this LocalSearchOperator is MakeOperator():


LocalSearchOperator* Solver::MakeOperator(
const std::vector<IntVar*>& vars,
Solver::LocalSearchOperators op)

where LocalSearchOperators is an enum describing different


LocalSearchOperators. To create a SimpleLNS, we use Solver::SIMPLELNS:
LocalSearchOperator * simple_lns =
solver.MakeOperator(vars,
Solver::SIMPLELNS);

By default, the variable number_of_variables_ in NextFragment() will be set to


1 and thus SimpleLNS destroys one variable at a time. Unfortunately, SimpleLNS is not
accessible directly. If you want to destroy more than 1 variable, you’ll have to implement your
own LocalSearchOperator.

RandomLNS

The RandomLNS LocalSearchOperator destroys randomly some variables. Its


NextFragment() method reads:
bool NextFragment(std::vector<int>* fragment) {
for (int i = 0; i < number_of_variables_; ++i) {

274
Chapter 7. Meta-heuristics: several previous problems

fragment->push_back(rand_.Uniform(Size()));
}
return true;
}

number_of_variables_ represents the number of variables to destroy. As you can see,


this method always returns true. This means that the neighborhood is never exhausted.
rand_ is an object of type ACMRandom which is an ACM minimal standard random num-
ber generator (see section 11.8 for more). rand_.Uniform(Size()) returns a random
number between 0 and Size() - 1. It might happen that the same variable is chosen more
than once. There are 2 factory methods to create RandomLNS LocalSearchOperators:
LocalSearchOperator* Solver::MakeRandomLNSOperator(
const std::vector<IntVar*>& vars,
int number_of_variables);
LocalSearchOperator* Solver::MakeRandomLNSOperator(
const std::vector<IntVar*>& vars,
int number_of_variables,
int32 seed);

7.7.4 A heuristic to solve the job-shop problem

You can find the code in the files jobshop_ls.h, jobshop_lns.h, jobshop_lns.cc
and jobshop_heuristic.cc and the data file in abz9.

We present a basic Large Neighborhood Search operator SequenceLns to solve the


Job-Shop Problem in the file jobshop_lns.cc. In the file jobshop_heuristic.cc,
we mix all LocalSearchOperators we have seen in the previous chapter Local Search:
the Job-Shop Problem and the SequenceLns operator.

SequenceLns

We define a basic LNS operator: SequenceLNS. This operator destroys current_length


IntervalVars randomly in the middle of each SequenceVar as depicted on the next
picture:

Ranked sequences
...

Ranked first Ranked last


current_length

To allow for some diversity, from time to time this operator destroys completely two
SequenceVars.
For SequenceVars, there are no specialized LNS operators. We thus inherit from
SequenceVarLocalSearchOperator:

275
7.7. Large neighborhood search (LNS): the Job-Shop Problem

class SequenceLns : public SequenceVarLocalSearchOperator {


public:
SequenceLns(const SequenceVar* const* vars,
int size,
int seed,
int max_length)
: SequenceVarLocalSearchOperator(vars, size),
random_(seed),
max_length_(max_length) {}

random_ is again an object of type ACMRandom and max_length is the maximal num-
ber of IntervalVars to destroy in each SequenceVar. It’s a upper bound because the
SequenceVar could contain less IntervalVars.
We use again our template for the MakeNextNeighbor() method:
virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) {
CHECK_NOTNULL(delta);
while (true) {
RevertChanges(true);
if (random_.Uniform(2) == 0) {
FreeTimeWindow();
} else {
FreeTwoResources();
}
if (ApplyChanges(delta, deltadelta)) {
VLOG(1) << "Delta = " << delta->DebugString();
return true;
}
}
return false;
}

FreeTwoResources() simply destroys two random SequenceVars:


void FreeTwoResources() {
std::vector<int> free_sequence;
SetForwardSequence(random_.Uniform(Size()), free_sequence);
SetForwardSequence(random_.Uniform(Size()), free_sequence);
}

FreeTimeWindow() is more interesting:


void FreeTimeWindow() {
for (int i = 0; i < Size(); ++i) {
std::vector<int> sequence = Sequence(i);
const int current_length =
std::min(static_cast<int>(sequence.size()), max_length_);
const int start_position =
random_.Uniform(sequence.size() - current_length);
std::vector<int> forward;
for (int j = 0; j < start_position; ++j) {
forward.push_back(sequence[j]);
}

276
Chapter 7. Meta-heuristics: several previous problems

std::vector<int> backward;
for (int j = sequence.size() - 1;
j >= start_position + current_length;
--j) {
backward.push_back(sequence[j]);
}
SetForwardSequence(i, forward);
SetBackwardSequence(i, backward);
}
}

We use the SequenceLNS in the file jobshop_lns.cc to solve the Job-Shop Problem.
Four parameters are defined through gflags flags:
• time_limit_in_ms: Time limit in ms, 0 means no limit;
• sub_sequence_length: The sub sequence length for the ShuffleIntervals
LS operator;
• lns_seed: The seed for the LNS random search;
• lns_limit: maximal number of candidate solutions to consider for each neighborhood
search in the LNS.
When we try to solve the abz9 instance with our default parameters, we quickly find this
solution
Solution #190 (objective value = 802, ..., time = 10612 ms, ...,
neighbors = 1884, ..., accepted neighbors = 190, ...)

After only 10 seconds, we obtain a feasible solution with an objective value of 802. Much better
than what we obtained in the previous chapter (the best value was 931)! Large Neighborhood
Search (and its randomness) widens the scope of the neighborhood definition and allows to
search a bigger portion of the search space but still it doesn’t avoid the local trap. jobshop_lns
seems to get stuck with this solution. In the next sub-section, we use the Local Search operators
defined in the previous chapter and the SequenceLNS operator together.

Everything together

In the file jobshop_heuristic.cc, we mix the three LocalSearchOperators we


have previously defined:
• SwapIntervals and ShuffleIntervals defined in the previous chapter and
• SequenceLNS.
As in jobshop_ls3.cc, we use Local Search to find an initial solution. We let the program
run for 18 minutes. The best solution found had an objective value of 745 and 717056 candidate
solutions were tested! Maybe jobshop_heuristic would have found a better solution after a
while but there is no guarantee whatsoever. We didn’t tune the algorithm, i.e. we didn’t try to
understand and fix its parameters (all the gflags flags) to their optimal values (if any) and we
only used the abz9 instance to test it.

277
7.8. Default search

Since we discussed the code in file jobshop_ls3.cc, the CP Solver has evolved and has
been improved in general. Unfortunately, for this precise code the Solver seems to be stuck in
a local optimum with a of value of 809. This might change in the future.

7.8 Default search

What do you do if you face a problem that doesn’t inspire you? Or that is too complicated
to devise a customized search strategy? Use a default search! Several search strategies were
devised to tackle any problem. To do so, they use generic methods that can be used with-
out too much specific knowledge of the problem at hand. How can they do that? Simple.
They use the model you provide and test some hypotheses to devise a dynamic search strat-
egy. This concept is rather advanced but you can easily use our DefaultIntegerSearch
DecisionBuilder. As its name implies, this DecisionBuilder only deals with
IntVars.

7.8.1 Impact based search strategies

Several general-purpose strategies for Constraint Programming are based on the concept of
impacts. While the basic ideas are quite simple, the implementation details are cumbersome
and must be analyzed with great details.

The basic idea28

Roughly speaking, an impact measures the search space reduction of a basic variable assign-
ment xi = v. When a variable is assigned, the constraints are propagated in a way or another
and, hopefully, the domains of the other variables shrink and the overall search space dimin-
ishes.
In the section Second try: dynamic variable selection (and define our own DecisionBuilder
class), we have encountered and implemented the first fail principle:
To succeed, try first where you are most likely to fail,
and the best success principle:
To find a solution, keep your options as large as possible.
Both principles are popular among CP experts. In practice one chooses first the variables that
are the most constrained or that have the smallest domains (first fail principle) and then, once a
variable has been chosen, choose a value that maximizes the number of possibilities for future
assignments in the hope that if a solution exists which such assignment you will find it (best
success principle).
In the section Second try: dynamic variable selection (and define our own DecisionBuilder
class), we used these two principles: first we choose the queens that had the smallest domains
28
A side note for MIP practitioners: there are strong similarities between impacts and pseudo-costs. Indeed
impacts were devised with pseudo costs and general branching schemes from MIP in mind. See [refalo2004] for
more.

278
Chapter 7. Meta-heuristics: several previous problems

starting from the center, and then we placed these queens in the best way to keep the most
options open for the other queens choosing the row with the least compatible columns, again
starting from the center.
Impact-based searches try to replicate exactly that: balancing these those principles. Most of
the time, this is done dynamically, i.e. impacts (or rather estimates of impacts) are evaluated
at each node of the search tree. It is also efficient to take some time before the search starts
to evaluate good variable candidates or to restart the search with the knowledge obtained from
the previous search(es). The idea here is to construct a search tree that is as small (efficient) as
possible.
Other ingredients can also be added to the mix.

Definition of an impact

For a nice introduction to the concept of impacts, we refer the reader to [refalo2004]. We use
the same notation as in this article.
Consider the number of all possible combinations of values for the variables as an estimation
P of the size of the search tree:

P = |Dx1 | × |Dx2 | × . . . × |Dxn |

If we look at this product before (Pbef ore ) and after (Paf ter ) an assignment xi = a we have an
estimation of the importance of this assignment for reducing the search space:
Paf ter
I(xi = a) = 1 −
Pbef ore

This reduction rate is called the impact of the assignment xi = a.


The higher the impact for an assignment, the smaller the size of the search tree. At one extreme,
if the assignment violates the model, we have I(xi = a) = 1. On the other hand, if the
assignment xi = a didn’t reduce too much the domains of the other variables, I(xi = a) ≈ 0.
Now we need a measure of the impact of a variable (not just the impact of the assignment of
this variable for one value). Not only must this measure be able to compare different variables at
a node of the search tree, but it also must be able to be computed easily. Several possibilities are
available and do indeed exist. They are based on I(xi = a) for several values a ∈ Dxi . We refer
the interested reader to the existing literature and the code for the different implementations that
we use in the or-tools library.

7.8.2 DefaultPhaseParameters

The DefaultPhaseParameters struct allows to customize a


DefaultIntegerSearch DecisionBuilder. It holds the following variables:

279
7.8. Default search

Variable Default value


var_selection_schema CHOOSE_MAX_SUM_IMPACT
value_selection_schema SELECT_MIN_IMPACT
initialization_splits kDefaultNumberOfSplits
run_all_heuristics true
heuristic_period kDefaultHeuristicPeriod
heuristic_num_failures_limit kDefaultHeuristicNumFailuresLimit
persistent_impact true
random_seed kDefaultSeed
restart_log_size kDefaultRestartLogSize
display_level NORMAL
use_no_goods kDefaultUseNoGoods
decision_builder nullptr
lns kDefaultLnsControl
We discuss briefly some of these variables and refer the reader to the code for a deeper com-
prehension of our implementation of the DefaultIntegerSearch DecisionBuilder.
• var_selection_schema: This parameter describes how the next variable to instan-
tiate will be chosen. Its type is the following enum:
enum VariableSelection {
CHOOSE_MAX_SUM_IMPACT = 0,
CHOOSE_MAX_AVERAGE_IMPACT = 1,
CHOOSE_MAX_VALUE_IMPACT = 2,
};

As you can see, we try to maximize the impact for the selected variable, following the
first fail principle.
• value_selection_schema: This parameter describes which value to select for a
given variable. Its type is the following enum:
enum ValueSelection {
SELECT_MIN_IMPACT = 0,
SELECT_MAX_IMPACT = 1,
};

This time, we propose both the minimization or maximization of the impact. By default,
we try to minimize it, following the best success principle.
• run_all_heuristics (bool): The default phase will run heuristics periodically.
This Boolean parameter indicates if we should run all heuristics, or a randomly selected
one. Check the file default_search.cc to see the different heuristics chosen to
assign variables and values. Most of them are a combination between specific search
strategies and randomness.
• heuristic_period (int): The distance in nodes between each run of the heuris-
tics. A negative or null value means that no heuristic is run.
• heuristic_num_failures_limit (int): The failure limit for each heuristic
that we run.

280
Chapter 7. Meta-heuristics: several previous problems

• persistent_impact (bool): Whether to keep the impact from the first search
for other searches or to recompute the impact for each new search.
• random_seed (int): Seed used to initialize the random part in some heuristics.
• decision_builder (DecisionBuilder*): When defined, this overrides the
default impact based DecisionBuilder.

7.8.3 First results

You can find the code in the files golomb_default_search1.cc


and golomb_default_search2.cc.

We use the Golomb Ruler Problem from the chapter Using objectives in constraint pro-
gramming: the Golomb Ruler Problem to illustrate the use of the default search phase. No
need to remember the Golomb Ruler Problem, we just want to compare our default strategy
(CHOOSE_FIRST_UNBOUND then ASSIGN_MIN_VALUE) with the default phase search.
We take exactly the same model in both cases (see golomb7.cc).
There are two factory methods you can use to define a DefaultIntegerSearch
DecisionBuilder:
DecisionBuilder* Solver::MakeDefaultPhase(
const std::vector<IntVar*>& vars) {
DefaultPhaseParameters parameters;
return MakeDefaultPhase(vars, parameters);
}

DecisionBuilder* Solver::MakeDefaultPhase(
const std::vector<IntVar*>& vars,
const DefaultPhaseParameters& parameters) {
return RevAlloc(new DefaultIntegerSearch(this, vars, parameters));
}

The first one uses the DefaultPhaseParameters struct with its default values, the
second one accepts a customized DefaultPhaseParameters struct.
Let’s try the default DefaultPhaseParameters (file
golomb_default_search1.cc) and the Default Search to solve the Golomb Ruler
Problem with n=9. Let’s compare our new result with the results of the chapter Using
objectives in constraint programming: the Golomb Ruler Problem in the next Table29 :
Impl1 Impl2 Impl2+ Impl3 Impl3+ Default Search
1,513 0,79 0,812 0,163 0,059 1,378
Times are given in seconds.
We need to tweak a little bit our DefaultPhaseParameters struct if we
want to have a chance of beating the implementations Impl2 to Impl3+ (file
golomb_default_search2.cc):
29
If you compare the results with the ones written in section 3.7, you’ll see that not only did we change of
computer but that the library has evolved since we wrote chapter 3.

281
7.9. Summary

DefaultPhaseParameters parameters;
parameters.var_selection_schema =
DefaultPhaseParameters::CHOOSE_MAX_VALUE_IMPACT;
parameters.value_selection_schema =
DefaultPhaseParameters::SELECT_MAX_IMPACT;
parameters.heuristic_period = -1;
parameters.restart_log_size = -5;
parameters.use_no_goods = false;

With these parameters30 we get:


Default Search Default Search with customized parameters
1,378 0,066
Not bad for an algorithm that doesn’t know anything about the problem31 ! As n grows, we
can see the difference between algorithm Impl3+ (golomb7.cc) and our customized Default
Search (golomb_default_search2.cc):
n Impl3+ Default Search with customized parameters
9 0.059 0,066
10 0,379 0,32
11 14,543 19,935
12 65,674 76,156

7.9 Summary

All the meta-heuristics seen in this chapter sits on top of a Local Search. In or-tools, we made
the choice to only trigger a meta-heuristic after a local optimum has been reached.
We have seen in details the three meta-heuristics implemented in or-tools with a
Metaheuristic class:
• Tabu Search;
• Simulated Annealing and
• Guided Local Search.
For each of them, we have seen our implementation in full details.
We have also seen how we implemented Large Neighborhood Search with specialized
LocalSearchOperators.
Because our meta-heuristics never stop, we have seen how to implement customized
SearchLimits that stop the search when a specific stopping criterion is reached. When you
don’t know what do to do, we have seen that you can use default search strategies that evolve dy-
namically. In particular, we have seen the DefaultIntegerSearch DecisionBuilder
and how restarting a search might help.
30
These parameters were obtained after some trials.
31
Once again, until now, no-one could ever come with a clever algorithm using only Constraint Programming.

282
Chapter 7. Meta-heuristics: several previous problems

7.9.1 A little word on the efficiency of meta-heuristics

As we have seen in this chapter, our three meta-heuristics aren’t a silver bullet and this is
normal: our implementations are too generic. To solve a problem efficiently, you really need
to use the structure of the problem to help the solver as much as possible. We have also seen
that the quality of the LocalSearchOperators involved is crucial: the meta-heuristics can
only guide the Local Search. If the Local Search limits itself to visit only a small part of the
search space, the meta-heuristics are helpless.
The strength of meta-heuristics based on Local Search comes from learning while searching.
First of all, most meta-heuristics depend on some parameters. Finding the right parameters to
solve a specific instance is in itself a hard problem. Second, these parameters can be changed
dynamically during the search and the most efficient searches evolve dynamically. Think about
intensification versus diversification. Tabu Search learns about what features to forbid or keep
and Guided Local Search penalizes some features (and thus rewards others). Learning which
parameters to favor or not happens during the search and we talk about reactive search. One
could say that escaping a local optimum is only a (although a very important) byproduct of this
learning process: the meta-heuristic “understands” it is stuck and seeks to move forward past
this local optimum, taking the history of the search into account.

283
CHAPTER

EIGHT

CUSTOM CONSTRAINTS: THE


ALLDIFFERENT_EXCEPT_0
CONSTRAINT

The global alldifferent_except_0 constraint is exactly as its name says: an


alldifferent constraint except for the variables that have a 0 value. Although this con-
straint is already implemented in or-tools1 , we will spend the whole chapter dissecting this
constraint and learn how to implement our own customized version.

Classes under scrutiny:

Files:

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


The files inside this directory are:

8.1 The alldifferent_except_0 constraint

8.1.1 Definition

8.1.2 The implemented AllDifferentExcept

Constraint* Solver::MakeAllDifferentExcept(const std::vector<IntVar*>&


vars,
int64 escape_value) {
int escape_candidates = 0;
for (int i = 0; i < vars.size(); ++i) {
escape_candidates += (vars[i]->Contains(escape_value));
}
if (escape_candidates <= 1) {
1
The implemented constraint is a little bit more general and doesn’t consider 0 as a special value. We’ll discuss
this constraint in section 8.1.
8.2. Basic working of the solver: constraints

return MakeAllDifferent(vars);
} else {
return RevAlloc(new AllDifferentExcept(this, vars, escape_value));
}
}

8.1.3 Use

8.1.4 A first example: The Partial Latin Square Extension Problem


(PLSE)

8.2 Basic working of the solver: constraints

8.2.1 BaseObject

8.2.2 Demons

8.2.3 The BaseObject and PropagationBaseObject classes

PropagationBaseObject

8.2.4 The propagation queue

8.2.5 The Constraint class

The Post() method

The InitialPropagate() method

8.2.6 Propagation in action: the PropagationMonitor class

8.3 Consistency in a nutshell

8.4 A basic Constraint example: the XXX


Constraint

8.4.1 The AllDifferentExcept constraint more in details

class AllDifferentExcept : public Constraint {


public:
AllDifferentExcept(Solver* const s, std::vector<IntVar*> vars, int64 escape_value)

286
Chapter 8. Custom constraints: the alldifferent_except_0 constraint

: Constraint(s), vars_(vars), escape_value_(escape_value) {}

virtual ~AllDifferentExcept() {}

virtual void Post() {


for (int i = 0; i < vars_.size(); ++i) {
IntVar* const var = vars_[i];
Demon* const d = MakeConstraintDemon1(
solver(), this, &AllDifferentExcept::Propagate, "Propagate", i);
var->WhenBound(d);
}
}

virtual void InitialPropagate() {


for (int i = 0; i < vars_.size(); ++i) {
if (vars_[i]->Bound()) {
Propagate(i);
}
}
}

void Propagate(int index) {


const int64 val = vars_[index]->Value();
if (val != escape_value_) {
for (int j = 0; j < vars_.size(); ++j) {
if (index != j) {
vars_[j]->RemoveValue(val);
}
}
}
}

virtual std::string DebugString() const {


return StringPrintf("AllDifferentExcept([%s], %" GG_LL_FORMAT "d",
JoinDebugStringPtr(vars_, ", ").c_str(), escape_value_);
}

virtual void Accept(ModelVisitor* const visitor) const {


visitor->BeginVisitConstraint(ModelVisitor::kAllDifferent, this);
visitor->VisitIntegerVariableArrayArgument(ModelVisitor::kVarsArgument,
vars_);
visitor->VisitIntegerArgument(ModelVisitor::kValueArgument, escape_value_);
visitor->EndVisitConstraint(ModelVisitor::kAllDifferent, this);
}

private:
std::vector<IntVar*> vars_;
const int64 escape_value_;
};

basic_constraint_example

287
8.5. First approach: model the constraint

8.5 First approach: model the constraint

8.5.1 First model

8.6 The AllDifferent constraint in more details

8.7 Second approach: a custom Constraint

8.7.1 Well

alldifferent_except_0

8.8 Summary

288
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 find
a set of routes covering some or all nodes and/or edges/arcs while optimizing an objective
function along the routes2 (time, vehicle costs, etc.) and respecting certain constraints (number
of vehicles, goods to pickup and deliver, fixed depots, capacities, clients to serve, time windows,
etc.).
To solve these problems, the or-tools offers a dedicated Constraint Programming sub-library:
the Routing Library (RL).
The next two chapters each deal with one of two broad categories of Routing Problems:
• Chapter 9 deals with Node Routing Problems where nodes must to be visited and served.
• Chapter 10 deals with Vehicle Routing Problems where vehicles serve clients along the
routes.
• Chapter ?? deals with Arc Routing Problems where arcs/edges must be visited and served.
These three categories of problems share common properties but they all have their own
paradigms and scientific communities.
In this chapter, we’ll discover the RL with what is probably the most studied problem in Oper-
ations Research: the Travelling Salesman Problem (TSP)3 .
We use the excellent C++ ePiX library4 to visualize TSP solutions in TSPLIB format and
TSPTW solutions in López-Ibáñez-Blum and da Silva-Urrutia formats.
1
A graph G = (V, E) is a set of vertices (the set V ) connected by edges (the set E). A directed edge is called
an arc. When we have capacities on the edges, we talk about a network.
2
The transportation metaphor is helpful to visualize the problems but the class of Routing Problems is much
broader. The Transportation Problem for instance is really an Assignment Problem. Networks can be of any type:
telephone networks (circuit switching), electronic data networks (such as the internet), VLSI (the design of chips),
etc.
3
We use the Canadian (and British, and South African, and...) spelling of the verb travelling but you’ll find
much more scientific articles under the American spelling: traveling.
4
The ePiX library uses the TEX/LATEX engine to create beautiful graphics.
Overview:

We start this chapter by presenting in broad terms the different categories of Routing Prob-
lems and describe the Routing Library (RL) in a nutshell. Next, we introduce the Travelling
Salesman Problem (TSP) and the TSPLIB instances. To better understand the RL, we say a
few words about its inner working and the CP model we use. Because most of the Routing
Problems are intractable, we use Local Search. We explain our two phases approach in details
and show how to model the TSP in a few lines. Finally, we model and solve the TSP with Time
Windows.

Prerequisites:

• 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 find the code in the directory documentation/tutorials/cplusplus/chap9.


The files inside this directory are:
• tsp.h: This file contains the TSPData class that records the data for the TSP. This file
is used throughout the TSP examples.
• tsplib.h: Declarations of TSPLIB keywords and the
TSPLIBDistanceFunctions class.
• tsp_epix.h: This file 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 file contains the TSPTWData class that records the data for the Trav-
elling Salesman Problem with Time Windows. This file is used throughout the TSPTW
examples.
• tsptw_epix.h: This file provides the helper functions to visualize TSPTW solutions
with the ePiX library.
• tsptw.cc: A basic implementation of the TSPTW with the RL.

292
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 classification of Routing Problems is intricate5 .
Actually, there is no real and widely adopted classification67 .
All the Routing Problems are somewhat related to each others and to Scheduling Problems8 .
We can roughly divide Routing Problems in three broad - and often overlapping - categories:
• Node Routing Problems (NRP)
• Vehicle Routing Problems (VRP)
• Arc Routing Problems (ARP)
For each category, we give an informal definition, list some known mathematical problems,
refer an authoritative source and present quickly the examples we detail in each chapter of part
III.
Be aware of the complexity of the classification of Routing Problems when you
search for a specific routing problem.

Most problems have variants and sometimes are known under different names. For instance,
the Cumulative Travelling Salesman Problem is also known as:
• The Travelling Salesman Problem with cumulative costs
• The Travelling Repairman Problem
• The Deliveryman Problem
• The Minimum Latency Problem
P
• The 1/sjk / Cj Scheduling Problem
• ...

5
You can stop reading now if you want: this section involves neither Constraint Programming nor the or-tools
library.
6
From time to time, an article is published to propose a good classification but none has been adopted by the
community so far. See [Eksioglu2009] for instance.
7
Some people may actually disagree with the terms used in this manual.
8
Although Scheduling Problems and Routing Problems are not solved with the same techniques. See
[Prosser2003] for instance.

293
9.1. A whole zoo of Routing Problems

So what is a Routing Problem anyway?


Broadly speaking, a Routing Problem is a mathematical problem where you need to find
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). Visit-
ing 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 scientific literature is a
combination of NRP and ARP: You have a graph or a network and you must find 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 find a solution but a good solution or even a best solution. Most
problems minimize an objective function along the routes defined in the solution. Typically,
the objective function is the sum of the weights of the edges/arcs/nodes the solution is made of
and a cost for each of the vehicles when more than one is involved.
One main difference between Arc Routing Problems and Node Routing Problems is that basic
ARPs (like the Chinese Postman Problem on undirected and directed graphs) are easy problems
while basic NRPs (like the Metric Travelling Salesman Problem) are intractable. But add some
basic constraints and/or consider mixed graphs and the ARPs too become intractable. More
often than not, the size of ARPs we are able to solve are an order of magnitude smaller than
the size of the corresponding NRPs we are able to solve. This can be partly explained by
the fact that NRPs received (and still receive) more attention than their equivalent ARPs from
the scientific community but ARP specialists tend to believe that ARPs are intrinsically more
difficult than NRPs.
VRPs are often used to model real transportation problems where goods/services/people are
moved from one point to another and as such must respect lots of side constraints (capacities,
delivery times, etc.).

9.1.1 Node Routing Problems

Informal definition:

The term Node Routing Problem (NRP) is seldom used9 and mainly refers to Travelling Sales-
man Problems (TSP)-like problems. In this manual, when we refer to NRP, we mean TSP-like
problems, i.e. routing problems where nodes must be visited and served. We use it to refer to
node-related Routing Problems and in contrast to arc-related Routing Problems. Most of the
NRPs consider 1 vehicle of ∞ capacity, i.e. we seek one tour that covers all the required nodes.

Some problems

• The Travelling Salesman Problem


9
Node Routing Problems might even describe problems unrelated to Routing Problems in the scientific litera-
ture!

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

Vehicle Routing Problems (VRPs) are concerned with a fleet of (maybe heterogeneous) vehi-
cles. The number of vehicles can be fixed in advance or be a variable of the problem. Generally,
a vehicle has a certain capacity (number of people, number of tons of goods, etc.) and must re-
spect some “time”-constraints (like the total duration of a route, time windows to serve clients,
etc.). Clients are usually modelled by nodes and to solve a VRP, one seeks to find several routes
(1 per vehicle) that visit all clients and respect all given constraints.

Some problems

• The Vehicle Routing Problem


• The Capacitated Vehicle Routing Problem
• The Pickup and Delivery Problem
• The Vehicle Routing Problem with Time Windows
• ...

295
9.1. A whole zoo of Routing Problems

Authoritative source:

Golden, Bruce L.; Raghavan, S.; Wasil, Edward A. (Eds.). The Vehicle Routing Problem: Lat-
est Advances and New Challenges. Springer, Series: Operations Research/Computer Science
Interfaces Series, Vol. 43, 2008, 589 p.

The CVRP:

The Capacitated Vehicle Routing Problem is...


[insert epix graphic]

9.1.3 Arc Routing Problems

Informal definition:

In Arc Routing Problems, we visit and serve edges and/or arcs. Most of the problems consider
1 vehicle of ∞ capacity, i.e. we seek one tour that covers all the required edges and/or arcs.

Some problems

• 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 Publish-
ers, Dordrecht, 2000.

The CCPP:

The Cumulative Chinese Postman Problem is ...


[insert epix graphic]

296
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. It’s a one
fits all. This approach has its advantages and disadvantages. On one side, the model already
exists, has been tested and fine-tuned by our team and you can reuse it to solve several Routing
Problems (meaning the learning curve is low). On the other side, if you need to solve a very
difficult Routing Problem, you probably would like to build one specialized model yourself.
Our RL can then serve as an inspiration.
The RL lets you model a wide range of vehicle routing problems from the Travelling Sales-
man Problem (and its variants, ATSP, TSPTW, ...) to multi-vehicles problems with dimension
constraints (capacities, time windows, ...) and various routing constraints (optional nodes, al-
ternate nodes, ...). Have a look at subsections 9.2.6 and and 9.2.7 below to have an idea of the
additional constraints you can use in this model.

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.

297
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 routing-
specific neighborhoods. Implementations of Tabu Search (TS), Simulated Annealing (SA) and
Guided Local Search (GLS) are available too and have proven to give good results (especially
GLS).

9.2.5 Tuning the search

To tune and parametrize the search, use command-line gflags. For instance, you might want to
use Tabu Search and limit the allowed solving time to 3 minutes:
./my_beautiful_routing_algorithm --routing_tabu_search=true
--routing_time_limit=180000

To get the whole list of gflags defined 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 gflag routing_first_solution set to
PathCheapestArc:
./my_beautiful_routing_algorithm
--routing_first_solution=PathCheapestArc

9.2.6 Dimensions

Often, real problems need to take into account some accumulated quantities along (the edges
and/or the nodes of) the routes. To model such quantities, the RL proposes the concept of
dimensions. A dimension is basically a set of variables that describe some quantities (given
by callbacks) accumulated along the routes. These variables are associated with each node of
the graph. You can add as many dimensions as you wish in an automated and easy fashion:
just call the appropriate AddDimension() method(s) and the RL creates and manages these
variables automatically.
You can add upper bounds (we develop this concept later) on a dimension and a capacity limits
per route/vehicle on accumulated quantities for a given dimension.
Examples of dimensions are weight or volume carried, distance and time.

9.2.7 Disjunctions

Nodes don’t have to be visited, i.e. some nodes can be optional. For this, the RL uses the
struct Disjunction which is basically a set of nodes. In our model, we visit at most one

298
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

node in each Disjunction. If these sets are singletons, then you have optional nodes. You
can also force to visit at least one node in each or some of the Disjunctions.
Again, we have automated and simplified (and optimized!) the process to create these sets: just
call the appropriate AddDisjunction() method(s).

9.2.8 Routes/Vehicles are not mandatory

The same way that nodes don’t have to be visited, vehicles/routes don’t have to be used, i.e.
some vehicles/routes can be optional. You might want to minimize the number of vehicles
needed as part of your problem.

9.2.9 Heterogeneous fleet 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?
11
At least, to the best of our knowledge. See the subsection Can CP be compared to the holy grail of Operations
Research? for more.
12
See the chapter on Arc Routing for a discussion about which Arc Routing Problems can be solved by the RL.

299
9.3. The Travelling Salesman Problem (TSP)

Number of nodes

The RoutingModel class has a limit on the maximum number of nodes it can handle13 .
Indeed, its constructors take an regular int as the number of nodes it can model:
RoutingModel(int nodes, ...);

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 Lin-
ear 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 ??) doesn’t allow you to visit a node more than once.
You can have several vehicles at one depot though.

A depot is a depot

This means you can only start from a depot and/or arrive to a depot, not transit through a depot.

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 won’t have any
guarantee on the quality of the returned solution(s). You can force the RL to return proven
optimal solutions but the RL wasn’t coded with exact solutions and procedures in mind.

9.3 The Travelling Salesman Problem (TSP)

You can find the code in the file tsp.h, tsp_epix.h


and tsplib_solution_to_epix.cc and the data in the files a280.tsp
and a280.opt.tour.

The Travelling Salesman Problem (TSP) is probably the most known and studied prob-
lem in Operations Research. In this section, we briefly15 present this fascinating problem
13
And thus the number of vehicles too!
14
If your platform restricts you too much, you can always adapt the code!
15
Google TSP, Traveling Saleman Problem or Travelling Salesman Problem to find lots of examples, explana-
tions, applications, etc.

300
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

and the TSPLIB which stands for the TSP library and is a library of sample instances for
the TSP (and related problems) from various origins and of various types. To read TSPLIB
data, we have implemented our own TSPData class as none of the available source code are
compatible with our licence. Feel free to use it! Finally, we like to visualize what we are
doing. To do so, we use the excellent ePiX library through our TSPEpixData class.

9.3.1 The Problem

Given a graph G = (V, E) and pairwise distances between nodes, the TSP consists in finding
the shortest possible path that visits each node exactly once and returns to the starting node.
You can think about a salesman that must visit several cities and come back to his hometown,
hence the name the problem.
The cost we want to minimize is the sum of the distances along the path. Although there is
a special vertex called the depot from which the tour starts and ends, we are really concerned
with the overall cost of the tour, i.e. the we could start and end the tour at every node without
changing the objective cost of the tour.
Below you can find a picture of a solution of the TSP with 280 cities (a280) in the section
Visualization with ePix.
The best algorithms can now routinely solve TSP instances with then thousands of nodes to
optimality16 .
These instances are out of scope of the Constraint Programming paradigm17 . CP shines when
you consider complicated side constraints like the addition of time windows: each customer
(represented by a node) has to be serviced inside a given time interval.

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 (i, j) by a well suited shortest path
edge between i and j. 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 (i, j), you just remove j from the domain of the variable
NextVar(i) of i. See subection 9.6.3 for a detailed example.

16
The record at the time of writing is the pla85900 instance in Gerd Reinelt’s TSPLIB. This instance is a
VLSI application with 85 900 nodes. For many other instances with millions of nodes, solutions can be found that
are guaranteed to be within 1% of an optimal tour!
17
At least for now and if you try to solve them to optimality.

301
9.3. The Travelling Salesman Problem (TSP)

Symmetric or Asymmetric distances?


When we talk about a Travelling Salesman Problem, it is implicit that the distance between
two nodes i and j must be the same as the distance between j and i. 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(i, j) 6= d(j, i), 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 it’s 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. It’s a little bit outdated but it contains a lot of instances and their proven op-
timal solutions. Their TSPLIB format is the de facto standard format to encode TSP instances.

The TSPLIB format

The TSPLIB format is explained in great details in the document TSPLIB95. Here is a small
excerpt to understand the basics. Refer to the TSPLIB95 document for more. The complete
TSPLIB collection of problems has been successfully solved to optimality with the Concorde
code in 2005-2006.
The convention in the TSPLIB is to number the nodes starting at 1. We’ll adopt this convention
here too. The Routing Library (RL) on the contrary starts numbering its nodes at 0.

Nodes are numbered from 1 to n in the TSPLIB and we keep this convention in this
chapter.

The instance file

The TSPLIB not only deals with the TSP but also with related problems. We only detail one
type of TSP instance files. This is what the file a280.tsp18 looks like:
NAME : a280
COMMENT : drilling problem (Ludwig)
TYPE : TSP
DIMENSION: 280
EDGE_WEIGHT_TYPE : EUC_2D
NODE_COORD_SECTION
1 288 149
2 288 129
18
The file a280.tsp actually contains twice the same node (node 171 and 172 have the same coordinates)
but the name and the dimension have been kept. This is the only known defect in the TSPLIB.

302
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

3 270 133
4 256 141
5 256 157
6 246 157
...
EOF

Some of the attributes don’t need any explanation. The TYPE keyword specifies the type of
data. We are only interested in:
• TSP: Data for the symmetric TSP;
• ATSP: Data for the asymmetric TSP and
• TOUR: A collection of tours (see next subsection below).
DIMENSION is the number of nodes for the ATSP or TSP instances. EDGE_WEIGHT_TYPE
specifies how the edge weight are defined. In this case (EUC_2D), it is the Euclidean distance in
the plane. Several types of distances are considered. The NODE_COORD_SECTION keyword
starts the node coordinates section. Each line is made of three numbers:
Node_id x y
Node_id is a unique integer (> 1) node identifier and (x,y) are Cartesian coordinates unless
otherwise stated. The coordinates don’t have to be integers and can be any real numbers.
Not all instances have node coordinates.
There exist several other less obvious TSPLIB formats but we disregard them in this manual
(graphs can be given by different types of explicit matrices or by edge lists for example). Note
however that we take them into account in the code.
You might wonder how the depot is given. It is nowhere written where to start a tour. This is
normal because the TSP is not sensitive to the starting node: you can start a tour anywhere, the
total cost of the tour remains the same.

The solution file

Solution files are easier to deal with as they only contain tours. Every tour, called a sub-tour,
is a list of integers corresponding to the Node ids ended by -1.
This is what the file a280.opt.tour containing an optimal tour looks like:
NAME : ./TSPLIB/a280.tsp.opt.tour
TYPE : TOUR
DIMENSION : 280
TOUR_SECTION
1
2
242
243
...
279
3

303
9.3. The Travelling Salesman Problem (TSP)

280
-1

Since this file contains an optimal tour, there are no sub-tours and the list of integers contains
only one -1 at the end of the file.

9.3.3 The TSPData class

The TSPData class basically encapsulates a 2-dimensional matrix containing the distances
between all nodes. For efficiency reasons, we use a 1-dimensional matrix with a smart pointer
defined in the header base/scoped_ptr.h:
private:
scoped_array<int64> matrix_;

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. It’s 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 pro-
gramming (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 files

To read TSPLIB files, the TSPData class offers the


LoadTSPLIBFile(const std::string& filename);

method. It parses a file in TSPLIB format and loads the coordinates (if any) for further treat-
ment. Note that the format is only partially checked: bad inputs might cause undefined be-
haviour.

304
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

If during the parse phase an unknown keyword is encountered, the method exists and prints a
FATAL LOG message:
Unknown keyword: UNKNOWN

This method has been tested with almost all the files of the TSPLIB and should hopefully read
any correct TSPLIB format for the TSP.

To 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 file tsp_epix.h contains
the TSPEpixData class. A TSPEpixData object is related to a RoutingModel and a
TSPData. Its unique constructor signature is
TSPEpixData(const RoutingModel & routing, const TSPData & data);

To write a ePiX solution file, use the following methods:


void WriteSolutionFile(const Assignment * solution,
const std::string & epix_filename);
void WriteSolutionFile(const std::string & tpslib_solution_filename,
const std::string & epix_filename);

The first method takes an Assignment while the second method reads the solution from a
TSPLIB solution file.
You can define the width and height of the generated image:
DEFINE_int32(epix_width, 10, "Width of the pictures in cm.");
DEFINE_int32(epix_height, 10, "Height of the pictures in cm.");

Once the ePiX file is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp

305
9.4. The model behind the scenes: the main decision variables

Here is an example of a solution for the file a280.tsp:

You can also print the node labels with the flag:
DEFINE_bool(tsp_epix_labels, false, "Print labels or not?");

For your (and our!) convenience, we wrote the small program tsplib_solution_to_epix. Its
implementation is in the file tsplib_solution_to_epix.cc. To use it, invoke:
./tsplib_solution_to_epix TSPLIB_data_file TSPLIB_solution_file >
epix_file.xp

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 13.11, we
describe the inner mechanisms of the RL in details. A Routing Problem is defined on a graph
(or a network). The nodes of the graph have unique NodeIndex identifiers. Internally, we use
an auxiliary graph to model the Routing Problem. In RL jargon, the identifiers of the nodes of
this auxiliary graph are called int64 indices. Be careful not to mix them up. To distinguish
one from the other, we use two non-compatible types: NodeIndex and int64.
A node of the original graph can be:
• a transit node;
• a starting depot;
• an ending depot;
• a starting and an ending depot.
A depot cannot be an transit node and a transit node can only be visited by at most one vehicle
in a solution. The number of vehicles can be arbitrary (within the limit of an int).

306
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

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 finishes, it is even easier since we don’t need any variable at all because the
route stops at this depot and there is no need to know where to go next. The situation is a little
bit messier if for instance we have two vehicles starting from the same depot. One variable will
not do. In the RL, we deal with this situation by duplicating this depot and give each node its
own IntVar* variable in the std::vector<IntVar*> nexts_.
Internally, we use int64 indices to label the nodes and their duplicates. These int64 indices
are the identifiers of the nodes of an auxiliary graph we present in the next sub-section.
The domains of the IntVar nexts_ variables consist of these int64 indices. Let’s say we
have a solution solution and a RoutingModel object routing. In the following code:
int64 current_node = ...
int64 next_node_index = solution.Value(routing.NextVar(current_node));

next_node_index is the int64 index of the node following immediately the int64
current_node in the Assignment solution.
Before we present the main decision variables of our model, we need to understand the dif-
ference between NodeIndex node identifiers and int64 indices representing nodes in solu-
tions.

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. We’ll use a VRP with four vehicles/routes.
Let’s take the original graph of the next figure:
19
Not every node, only the nodes that lead somewhere in the solution. Keep reading.
20
Remember that we don’t allow a node to be visited more than once, i.e. only one vehicle can visit a node in
a solution.
21
This sub-section is a simplified version of the section The auxiliary graph from the chapter Under the hood.

307
9.4. The model behind the scenes: the main decision variables

1 0 4
1
0
2
8
5
3 6 7
1
0
0
1
Starting depot
1
0
0Ending depot
1
1
0 Starting and ending depot
Transit node

You can of course number (or name) the nodes of the original graph any way you like. For
instance, in the TSPLIB, nodes are numbered from 1 to n. In the RL, you must number your
original nodes from 0 to n − 1. If you don’t follow this advice, you might get some surprises!

Always use NodeIndexes from 0 to n − 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 figure:
1 0 2 1
0
0
14
0
1 8
5 0
1
0
1
3 6 71
0
1
00
1
01
10

Starting depot
1
0
0Ending depot
1
1
0 Starting and ending depot
Transit node

Node 1 is not duplicated because there is only one route (route 0) that starts from 1. Node 3 is
duplicated once because there are two routes (routes 1 and 2) that start from 3. Node 7 has been
duplicated once because two routes (routes 2 and 3) end at 7 and finally there are two added
copies of node 4 because two routes (routes 0 and 1) end at 4 and one route (route 3) starts from
4.
22
We should rather say NodeIndices but we pluralize the type name NodeIndex. Note
also that the NodeIndex type lies inside the RoutingModel class, so we should rather use
RoutingModel::NodeIndex.

308
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

The way these nodes are numbered doesn’t matter for the moment. For our example, the next
figure shows this numbering:

1 0 2 1
0
9
0
1
0 4
1 7
5 10 1
0
0
1
3 6 11 1
0
0
1
0
1
01
1 0
12
8
Starting depot
1Ending depot
0
0
1
1
0 Starting and ending depot
Transit node

Note that the int64 indices don’t depend on a given solution but only on the given
graph/network and the depots.

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.

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 shouldn’t be compatible with anything else.
A value() method allows the cast thought:
RoutingModel::NodeIndex node(12);
// the next statement fails to compile
int64 myint = node;
// this is permitted
int64 myint = node.value();

Behind the scene, a static_cast is triggered. If you are following, you’ll understand that
RoutingModel::NodeIndex node = 12;

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;

They are quicker and safer than a static_cast and ... give the correct results!
23
Have a look at base/int-type.h if you want to know more about the IntType class.

309
9.4. The model behind the scenes: the main decision variables

Try to avoid RoutingModel::NodeIndex::value() unless really necessary.

NodeIndexes and int64s don’t necessarily coincide!

How can you find the int64 index of a depot? You shouldn’t use the method
NodeToIndex() to determine the int64 index of a starting or ending node in a route.
Use instead
int64 Start(int vehicle) const;
int64 End(int vehicle) const;

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.

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

310
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

nodes in the next figure. They have a NodeIndex ranging from 0 to 8. There are 2 starting
depots (1 and 7) and 2 ending depot (5 and 8). Route 0 starts at 1 and ends at 5 while route 1
starts at 7 and ends at 8.
Var 1 Var 4
1 Path p0
Var 0 4
5
0 Var 5
Var 6 6
Var 2
Var 3
2 7
Path p1 3 8
NodeIndex : 0 . . . 8
Var (IntVar): 0 . . . 6
int64 : 0 . . . 8
Because nodes 5 and 8 are ending nodes, there is no nexts_ IntVar attached to them.
The solution depicted is:
• Path p0 : 1 -> 0 -> 2 -> 3 -> 5
• Path p1 : 7 -> 4 -> 6 -> 8
If we look at the internal int64 indices, we have:
• Path p0 : 1 -> 0 -> 2 -> 3 -> 7
• Path p1 : 6 -> 4 -> 5 -> 8
There are actually 9 int64 indices ranging from 0 to 8 because in this case there is no need to
duplicate a node. As you can see in the picture, there are only 7 nexts_ IntVar variables.
The following code:
LG << "Crash: " << Solution->Value(routing.NextVar(routing.End(0)));

compiles fine 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 sub-
section 13.11.2. Notice also that the internal int64 index of the node with NodeIndex 6
is... 5 and the int64 index of the node with NodeIndex 7 is...6!

9.4.6 To summarize

Here is a little summary:

311
9.5. The model behind the scene: overview

Types to represent nodes

What Types Comments


True node Ids NodeIndex Unique for each original node from 0 to n − 1.
Indices to follow int64 Not unique for each original node. Could be
routes bigger than n − 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
first 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 scene: 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 13.11, we
describe the inner mechanisms of the RL in details.

If you haven’t already read the section 9.4 about the main decision variables and the
auxiliary graph, we strongly recommend that you do so before reading this section.

9.5.1 The RoutingModel class

All ingredients are defined within the RoutingModel class. This class is declared in the
header constraint_solver/routing.h.
As already mentionned, the RL is a layer above the CP Solver and the internal cabling is
accessible through the underlying solver:
24
The CP solver does an initial propagation to quickly skim these domains.

312
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

RoutingModel routing(...);
Solver* const solver = routing.solver();

Most desirable features for an RL are directly accessible through the RoutingModel class
though. The accessors (getters and setters) will be discussed throughout the third part of this
manual. But it is good to know that, as a last resort, you have a complete access (read control)
to the internals of the RL.
Basically, two constructors are available depending on the number of depots:
• if there is only one depot:
// 42 nodes and 7 routes/vehicles
RoutingModel routing(42, 7);
// depot is node with NodeIndex 5
routing.SetDepot(5);

• 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 dif-
ferent 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.

313
9.5. The model behind the scene: overview

Path variables

Path variables describe the different routes. There are three types of path variables that can be
accessed with the following methods:
• NextVar(i): the main decision variables. NextVar(i) == j is true if j is the
node immediately reached from node i in the solution.
• VehicleVar(i): represents the vehicle/route index to which node i belongs in the
solution.
• ActiveVar(i): a Boolean variable that indicates if a node i is visited or not in the
solution.

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) repre-
sents the IntVar* that represents the int index of the route/vehicle servicing node i in the
solution:
int route_number = solution->Value(routing.VehicleVar(i));

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

314
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

Disjunctions and optional nodes

A node doesn’t have to be visited. Nodes can be optional or part of a Disjunction, i.e. part
of a subset of nodes out of which at most one node can be visited in a solution.
ActiveVar(i) returns a boolean IntVar* (a IntVar variable with a {0, 1} domain)
indicating if the node i is visited or not in the solution. The way to describe a node that is not
visited is to make its NextVar(i) points to itself. Thus, and again with an abuse of notation,
we have:
ActiveVar(i) == (NextVar(i) != i).

Dimension variables

Dimension variables are used to accumulate quantities (or dimensions) along the routes. To de-
note a dimension, we use an std::string d. There are three types of dimension variables:
• CumulVar(i, d): variables representing the quantity of dimension d when arriving
at the node i.
• TransitVar(i, d): variables representing the quantity of dimension d added after
visiting the node i.
• SlackVar(i, d): non negative slack variables such that (with the same abuse of
notation as above):
if NextVar(i) == j then CumulVar(j) = CumulVar(i) +
TransitVar(i) + SlackVar(i).
For a time dimension, you can think of waiting times.
You can add as many dimensions as you want25 .
The transit values can be constant, defined with callbacks, vectors or matrices. You can rep-
resent any quantities along routes with dimensions but not only. For instance, capacities and
time windows can be modelled with dimensions. We’ll play with dimensions at the end of
this chapter when we’ll try to solve The Travelling Salesman Problem with Time Windows in
or-tools.

9.5.3 Constraints

In addition to the basics constraints that we discussed in the previous sub-section, the RL uses
constraints to avoid cycles, constraints to model the Disjunctions and pick-up and delivery
constraints.

No cycle constraint

One of the most difficult constraint to model is a constraint to avoid cycles in the solutions.
For one tour, we don’t want to revisit some nodes. Often, we get partial solutions like the one
25
Well, as many as your memory allows...

315
9.5. The model behind the scene: overview

depicted on figure (a):

(a) (b)

It is often easy to obtain optimal solutions when we allow cycles (like in figure (a)) but difficult
to obtain a real solution (like in figure (b)), i.e. without cycles. Several constraints have been
proposed in the scientific literature, each with its cons and pros. Sometimes, we can avoid
this constraint by modelling the problem in such a way that only solutions without cycles can
be produced but then we have to deal with huge and often numerically (and theoretically26 )
unstable models.
In the RL, we use our dedicated NoCycle constraint (defined in
constraint_solver/constraints.cc) in combination with an AllDifferent
constraint on the NextVar() variables. The NoCycle constraint is implicitly added to the
model.
The NoCycle constructor has the following signature:
NoCycle(Solver* const s,
const IntVar* const* nexts,
int size,
const IntVar* const* active,
ResultCallback1<bool, int64>* sink_handler,
bool owner,
bool assume_paths);

We will not spend too much time on the different arguments. The nexts and active arrays
are what their names imply. The sink_handler is just a callback that indicates if a node is
a sink or not. Sinks represent the depots, i.e. the nodes where paths start and end.
The bool owner allows the solver to take ownership of the callback or not and the bool
assume_paths indicates if we deal with real paths or with a forest (paths don’t necessarily
end) in the auxiliary graph.
The constraint essentially performs two actions:
• forbid partial paths from looping back to themselves and
• ensure each variable/node can be connected to a sink.
26
For the specialists: for instance, primal and dual degenerate linear models.

316
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

We refer the reader to subsection 13.11.4 for a detailed description of our internal NoCycle
constraint.

Disjunction constraints

Disjunctions on a group of nodes allow to visit at most one of the nodes in this group. If
you want to visit exactly one node in a Disjunction, use:
void AddDisjunction(const std::vector<NodeIndex>& nodes);

where nodes represents the group of nodes. This constraint is equivalent to:
X
ActiveVar(i) = 1.
i∈Disjunction

You might want to use optional Disjunctions, i.e. a group of nodes out of which at most
one node can be visited. This time, use:
void AddDisjunction(const std::vector<NodeIndex>& nodes,
int64 penalty);

This constraint is equivalent to:


X
p + ActiveVar(i) = 1
i∈Disjunction

where p is a boolean variable corresponding to the Disjunction and the objective function
has anPadded (p * penalty) term. If none of the variables in the Disjunction is vis-
ited ( i∈Disjunction ActiveVar(i) = 0), p must be equal to one and the penalty is added to the
objective function.
To be optional, the penalty penalty attributed to the Disjunction must be non-negative
(> 0), otherwise the RL uses a simple Disjunction, i.e. exactly one node in the
Disjunction will be visited in the solutions.

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:
Solver* const solver = routing.solver();
solver->AddConstraint(solver->MakeEquality(
routing.VehicleVar(routing.NodeToIndex(i)),
routing.VehicleVar(routing.NodeToIndex(j))));
routing.AddPickupAndDelivery(i, j);

317
9.5. The model behind the scene: overview

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 filter out solutions.
PathOperators take them into account in the Local Search and
2. It doesn’t 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 don’t completely define the model when we construct the RoutingModel class,
most of the (implicit or explicit) constraints27 and the objective function are added in a special
CloseModel() method. This method is automatically called before a call to Solve() but
if you want to inspect the model before, you need to call this method explicitly. This method is
also automatically called when you deal with Assignments. In particular, it is called by
• ReadAssignment();
• RestoreAssignment() and
• ReadAssignmentFromRoutes().

9.5.4 The objective function

The objective function is defined by an IntVar. To get access to it, call CostVar():
IntVar* const obj = routing.CostVar();

The RL solver tries to minimize this obj variable. The value of the objective function is the
sum of:
• the costs of the arcs in each path;
• a fixed cost of each route/vehicle;
• the penalty costs for not visiting optional Disjunctions.
We detail each of these costs.

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);
27
Actually, only an AllDifferent constraint on the NextVars is added in the constructor of the
RoutingModel class. This constraint reinforces the fact that you cannot visit a node twice.

318
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

NodeEvaluator2 is simply28 a typedef for a ResultCallback2<int64,


NodeIndex, NodeIndex>, i.e. a class that defines an int64 Run(NodeIndex i,
NodeIndex j) or method. If you already have a class that defines 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 defined in the header
base/callback.h. If you are curious about the callback mechanism and the use of
NewPermanentCallback(), read sub-section 13.3.3.

A fixed cost for each of the existing routes

Routes/Vehicles don’t all have to be used. It might cost less not to use a route/vehicle. To add
a fixed cost for each route/vehicle, use:
void SetRouteFixedCost(int64 cost);

This int64 cost will only be added for each route that contains at least one visited node, i.e.
a different node than the start and end nodes of the route.
28
What follows is clearly C++ jargon. Basically, let’s say that you need a method or a function
that returns the distances of the arcs. To pass it as argument to the SetCost() method, wrap it in a
NewPermanentCallback() “call”.

319
9.5. The model behind the scene: overview

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 doesn’t
visit any node of the Disjunction. If the given penalty cost is negative for an optional
Disjunction, this Disjunction becomes mandatory and the penalty is set to zero. The
penalty cost can be zero for optional Disjunction and you can model optional nodes by
using singletons for each Disjunction.

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 fixed 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?
Let’s draw a figure.
1 1
2
2 2

3 1 3 3
5
4 4

4 5 5

(a) (b)

On the left (figure (a)), we have an original graph with two depots: a starting depot 1 and an
ending depot 5 and three transit nodes 2, 3 and 4. On the right (figure (b)), we have a bipartite

320
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

graph29 with the same number of left and right nodes. The cost on an arc (l,r) is the real
transit cost from l to r. The Linear Assignment Problem consists in finding a perfect matching
of minimum cost, i.e. a bijection along the arcs between the two sets of nodes of the bipartite
graph for a minimum cost. On figure (b), such an optimal solution is depicted in thick blue
dashed lines. As is the case here, this solution doesn’t necessarily produce a (set of) closed
route(s) from a starting depot to an ending depot.
The routing model must be closed before calling this method.

Routing Problems with node disjunction constraints (including optional nodes) and
non-homogenous costs are not supported yet (the method returns 0 in these cases).

If your model is linear, you also can use the linear relaxation of your model.

9.5.5 Miscellaneous

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 filtering.
Basically, these constraints ensure that the variables are respecting the equalities of the basic
constraints. They only perform bound reduction on the variables when these variables are
bound.
You can trigger the use of these light constraints with the following flag:
DEFINE_bool(routing_use_light_propagation, false,
"Use constraints with light propagation in routing model.");

When false, the RL uses the regular constraints seen in the previous parts of this manual.
Try it, sometimes you can get a serious speed up. These light constraints are especially useful
in Local Search.

Locks

Often during the search, you find what appears to be good sub-solutions, i.e. partial routes that
seem promising and that you want to keep fixed for a while during the search. This can easily
be achieved by using locks.
29
This bipartite graph is not really the one used by the CP solver but it’s close enough to get the idea.

321
9.6. The TSP in or-tools

A lock is simply an std::vector<int64> that represents a partial route. Using this lock
ensures that
NextVar(lock[i]) == lock[i+1]
is true in the current solution. We will use locks in section ?? when we will try to solve the
Cumulative Chinese Postman Problem.

9.6 The TSP in or-tools

You can find the code in the files tsp.h, tsp_epix.h, tsp_minimal.cc, tsp.cc,
tsplib_solution_to_epix.cc and tsp_forbidden_arcs.cc and the data in the
files tsp_parameters.txt, a280.tsp and a280.opt.tour.

The RL is particularly well-suited to model a TSP. We start with a minimalistic imple-


mentation 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 files. 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 find the code in the file 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) {
...
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) << " ";

322
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

}
std::cout << std::endl;
} else {
std::cout << "No solution found" << std::endl;
}
return 0;
}

Given an appropriate cost function, a TSP can be modelled and solved in 3 lines:
RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle
TSP.SetCost(NewPermanentCallback(MyCost));

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 find the code in the file 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"

#include "tsp.h"
#include "tsp_epix.h"

base/join.h contains the StrCat() function that we use to concatenate strings. tsp.h
contains the definition and declaration of the TSPData class to read TSPLIB format instances
and write TSPLIB format solution files while tsp_epix.h contains the TSPEpixData
class to visualize TSP solutions. Under the hood, tsp.h includes the header tsplib.h that
gathers the keywords, distance functions and constants from the TSPLIB. You should consider
tsp.h and tsplib.h as one huge header file. tsp_epix.h is only needed if you want to
use the ePiX library to visualize TSP solutions. tsp_epix.h depends on tsp.h (and thus
tsplib.h).

323
9.6. The TSP in or-tools

Parameters

Several command line parameters are defined in the files tsp.h, tsplib.h, tsp_epix.h
and tsp.cc:

Files Parameter Description Default value


tsp.h deterministic_random_seed Use deterministic random true
seeds or not?
use_symmetric_distances Generate a symmetric TSP true
instance or not?
min_distance Minimum allowed distance 10
between two nodes.
max_distance Maximum allowed distance 100
between two nodes.
tsp_epix.h epix_width Width of the pictures in cm. 10
epix_height Height of the pictures in cm. 10
tsp.cc tsp_size Size of TSP instance. If 0, 0
must be read from a TSPLIB
file.
tsp_depot The starting node of the tour. 1
tsp_data_file Input file with TSPLIB data. empty string
tsp_distance_matrix_file Output file with distance ma- empty string
trix.
tsp_width_size Width size of fields in output 6
files.
tsp_solution_file Output file with generated so- empty string
lution in TSPLIB format.
tsp_epix_file ePiX solution file. empty string
30
tsp_time_limit_in_ms Time limit in ms, 0 means no 0
limit.

Command line parameters read from a file

When parameters start to pile up, writing them every time on the command line isn’t very
practical. The gflags library provides the possibility to load the parameters from a text file.
For instance, a parameters file tsp_parameters.txt for our TSPData class might look
like this:
--tsp_depot=2
--deterministic_random_seed=true
--use_symmetric_distances=true
--min_distance=23
--max_distance=748
--tsp_initial_heuristic=PathCheapestArc
--tsp_size=101
--tsp_solution_file=tsp_sol.txt
30
This flag is redundant with the routing_time_limit flag provided in routing.cc but we wanted to
underline the fact that this limit is given in milliseconds.

324
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

You can read this file with the flagfile flag:


./tsp --flagfile=tsp_parameters.txt

which outputs the following file 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 != "") {
tsp_data.LoadTSPLIBFile(FLAGS_tsp_data_file);
} else {
google::ShowUsageWithFlagsRestrict(argv[0], "tsp");
exit(-1);
}

operations_research::TSP(tsp_data);

return 0;
}

We start by writing the usage message that the user will see if she doesn’t know what to
do. Next, we declare a TSPData object that will contain our TSP instance. As usual, all the
machinery is hidden in a function declared in the operations_research namespace:
TSP().

325
9.6. The TSP in or-tools

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 num-
ber of vehicle (1) as parameters. The distance function is encoded in the TSPData object
given to the TSP() function.
Next, we define some parameters:
// Disabling Large Neighborhood Search, comment out to activate it.
routing.SetCommandLineOption("routing_no_lns", "true");

if (FLAGS_tsp_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_tsp_time_limit_in_ms);
}

Because Large Neighborhood Search (LNS) can be quite slow, we deactivate it.
To define the depot, we have to be careful as, internally, the CP solver starts counting the nodes
from 0 while in the TSPLIB format the counting starts from 1:
if (FLAGS_start_counting_at_1) {
CHECK_GT(FLAGS_tsp_depot, 0) << " Because we use the " <<
"TSPLIB convention, the depot id must be > 0";
}
RoutingModel::NodeIndex depot(FLAGS_start_counting_at_1 ?
FLAGS_tsp_depot -1 : FLAGS_tsp_depot);
routing.SetDepot(depot);

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:
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 didn’t pass the check test.";
} else {
LG << "Solution did pass the check test.";
}
// Solution cost.
LG << "Cost: " << solution->ObjectiveValue();
// Inspect solution.

326
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

string route;
const int route_nbr = 0;
for (int64 node = routing.Start(route_nbr);
!routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {

//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 M to these arcs. Arcs with such a large
distance will never be chosen31 . M can be considered as infinity.
In Constraint Programming, we can deal with forbidden arcs more elegantly: we simply remove
the forbidden values from the variable domains. We’ll use both techniques and compare them.
First, we have to define M . We suppose that M >>> max(d(x, y) : x, y ∈ cities)32 and we
take the largest allowed value kint64max.
We have implemented a RandomForbidArcs() method in the TSPData class to randomly
forbid a percentage of arcs:
void RandomForbidArcs(const int percentage_forbidden_arcs);

This method alters the existing distance matrix and replaces the distance of forbidden arcs by
the flag M:
DEFINE_int64(M, kint64max, "Big m value to represent infinity");

We have also defined a flag to switch between the two techniques and a flag for the percentage
of arcs to forbid randomly in the file tsp_forbidden_arcs.cc:
31
Actually, when permitted, an arc (i, j) with a distance M is often replaced by a shortest path i → j and its
value is the length of the shortest path between i and j. One drawback is that you have to keep in memory the
shortest paths used (or recompute them) but it is often more efficient than using the large M value.
32
Loosely speaking, the expression M >>> max(d(x, y) : x, y ∈ cities) means that M is much much larger
that the largest distance between two cities.

327
9.6. The TSP in or-tools

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;

// Don’t 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
<< ") 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
gflag:
DEFINE_int32(percentage_forbidden_arcs_max, 94,
"Maximum percentage of arcs to forbid");

328
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

We bound the percentage of forbidden arcs by 94% by default.


[TO BE COMPLETED]

9.7 The two phases approach

You can find the code in the file tsp_initial_solutions.cc.

329
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

9.7.1 The initial solution

9.7.2 The PathOperator class

MakeNeighbor() instead of MakeOneNeighbor()

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


dows (TSPTW)

You can find the code in the file tsp.h, tsp_epix.h, tsp_minimal.cc, tsp.cc,
tsplib_solution_to_epix.cc and tsp_forbidden_arcs.cc and the data in the
files tsp_parameters.txt, a280.tsp and a280.opt.tour.

The Travelling Salesman Problem with Time Windows is similar to the TSP except that
cities (or clients) must be visited within a given time window. This added time constraint -

330
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

although it restricts the search tree33 - renders the problem even more difficult in practice!
Indeed, the beautiful symmetry of the TSP34 (any permutation of cities is a feasible solution)
is broken and even the search for feasible solutions is difficult [Savelsbergh1985].
We present the TSPTW and two instances formats: the López-Ibáñez-Blum and the da Silva-
Urrutia formats. As in the case of the TSP, we have implemented a class to read those instances:
the TSPTWData class. We also use the ePix library to visualize feasible solutions using the
TSPTWEpixData class.

9.8.1 The Travelling Salesman Problem with Time Windows

You might be surprised to learn that there is no common definition that is widely accepted
within the scientific community. The basic idea is to find a “tour” that visits each node within
a time window but several variants exist.
We will use the definition given in Rodrigo Ferreira da Silva and Sebastián Urrutia’s 2010
article [Ferreira2010]. Instead of visiting cities as in the TSP, we visit and service customers.
The Travelling Salesman Problem with Time Windows (TSPTW) consists in finding a mini-
mum cost tour starting and ending at a given depot and visiting all customers. Each customer i
has:
• a service time βi : this is the time needed to service the customer;
• a ready time ai (sometimes called release time): you cannot start to serve the customer
before her ready time and
• a due time bi (sometimes called deadline): you must serve the client before her due time.
You only can (and must) visit each client once. The costs on the arcs represent the travel times
(and sometimes also the service times). The total cost of a tour is the sum of the costs on the
arcs used in the tour. The ready and due times of a client i define a time window [ai , bi ] within
which the client has to be served. You are allowed to visit the client before the ready time but
you’ll have to wait until the ready time before you can service her. Due times must be respected
and tours that fail to serve clients before their due time are considered infeasible.
Let’s illustrate a visit to a client i. To do so, let’s define:
• the arrival time ti : the time you arrive at the client and
• the service start time si : the time you start to service the client.
time spent at the client

service time βi

ai time window [ai , bi ] bi


ti 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 client’s office. It’s common to consider that you start to service and
leave as soon as possible and this is our assumption in this chapter
33
All TSP solutions are not TSPTW solutions!
34
Notice how the depot is important for the TSPTW while it is not for the TSP.

331
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

Some authors ([Dash2010] for instance) assign two costs on the edges: a travel cost and a
travel time. While the travel times must respect the time windows constraints, the objective
value is the sum of the travel costs on the edges. In this chapter, we only have one cost on the
edges. The objective value and the real travel time are different: you might have to wait before
servicing a client.
Often, some conditions are applied to the time windows (in theory or practice). The only
condition35 we will impose is that ai , bi ∈ N, i.e. we impose that the bounds of the time
windows must be non negative integers. This also implies that the time windows and the
servicing times are finite.
The practical difficulty of the TSPTW is such that only instances with about 100 nodes have
been solved to optimality36 and heuristics rarely challenge instances with more than 400 nodes.
The difficulty of the problem not only depends on the number of nodes but also on the “quality”
of the time windows. Not many attempts can be found in the scientific literature about exact or
heuristic algorithms using CP to solve the TSPTW. Actually, not so many attempts have been
successful in solving this difficult problem in general. The scientific literature on this problem
is hence scarce.
We refer the interested reader to the two web pages cited in the next sub-section for some
relevant literature.

9.8.2 Benchmark data

There isn’t a real standard. Basically, you’ll find two types of formats and their variants. We
refer you to two web pages because their respective authors took great care in formatting all
the instances uniformly.
Manuel López-Ibáñez and Christian Blum have collected benchmark instances from different
sources in the literature. Their Benchmark Instances for the TSPTW page contains about 300
instances.
Rodrigo Ferreira da Silva and Sebastián Urrutia also collected benchmark from different
sources in the literature. Their The TSPTW - Approaches & Additional Resources page con-
tains about 100 instances.
Both pages provide best solutions and sum up the relevant literature.

The López-Ibáñez-Blum format

We present the same instance proposed by Dumas et al. [Dumas1995] in both formats.
Here is the content of the file n20w20.001.txt (LIB_n20w20.001.txt in our directory
/tutorials/cplusplus/chap9/):
35
This condition doesn’t hold in Rodrigo Ferreira da Silva and Sebastián Urrutia’s definition of a TSPTW. In
their article, they ask for (at least theoretically) ai , bi , βi ∈ R+ , i.e. non negative real numbers and ai 6 bi .
36
Instances with more than 100 nodes have been solved to optimality but no one - at least to the best of our
knowledge at the time of writing - can systematically solve to optimality instances with more than 40 nodes...

332
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

21
0 19 17 34 7 20 10 17 28 15 23 29 23 29 21 20 9 16 21 13 12
19 0 10 41 26 3 27 25 15 17 17 14 18 48 17 6 21 14 17 13 31
17 10 0 47 23 13 26 15 25 22 26 24 27 44 7 5 23 21 25 18 29
34 41 47 0 36 39 25 51 36 24 27 38 25 44 54 45 25 28 26 28 27
7 26 23 36 0 27 11 17 35 22 30 36 30 22 25 26 14 23 28 20 10
20 3 13 39 27 0 26 27 12 15 14 11 15 49 20 9 20 11 14 11 30
10 27 26 25 11 26 0 26 31 14 23 32 22 25 31 28 6 17 21 15 4
17 25 15 51 17 27 26 0 39 31 38 38 38 34 13 20 26 31 36 28 27
28 15 25 36 35 12 31 39 0 17 9 2 11 56 32 21 24 13 11 15 35
15 17 22 24 22 15 14 31 17 0 9 18 8 39 29 21 8 4 7 4 18
23 17 26 27 30 14 23 38 9 9 0 11 2 48 33 23 17 7 2 10 27
29 14 24 38 36 11 32 38 2 18 11 0 13 57 31 20 25 14 13 17 36
23 18 27 25 30 15 22 38 11 8 2 13 0 47 34 24 16 7 2 10 26
29 48 44 44 22 49 25 34 56 39 48 57 47 0 46 48 31 42 46 40 21
21 17 7 54 25 20 31 13 32 29 33 31 34 46 0 11 29 28 32 25 33
20 6 5 45 26 9 28 20 21 21 23 20 24 48 11 0 23 19 22 17 32
9 21 23 25 14 20 6 26 24 8 17 25 16 31 29 23 0 11 15 9 10
16 14 21 28 23 11 17 31 13 4 7 14 7 42 28 19 11 0 5 3 21
21 17 25 26 28 14 21 36 11 7 2 13 2 46 32 22 15 5 0 8 25
13 13 18 28 20 11 15 28 15 4 10 17 10 40 25 17 9 3 8 0 19
12 31 29 27 10 30 4 27 35 18 27 36 26 21 33 32 10 21 25 19 0
0 408
62 68
181 205
306 324
214 217
51 61
102 129
175 186
250 263
3 23
21 49
79 90
78 96
140 154
354 386
42 63
2 13
24 42
20 33
9 21
275 300

The first line contains the number of nodes, including the depot. The n20w20.001 instance
has a depot and 20 nodes. The following 21 lines represent the distance matrix. This distance
typically represents the travel time between nodes i and j, plus the service time at node i. The
distance matrix is not necessarily symmetrical. The last 21 lines represent the time windows
(earliest, latest) for each node, one per line. The first node is the depot.
When then sum of service times is not 0, it is specified in a comment on the last line:

333
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

# Sum of service times: 522

The da Silva-Urrutia format

We present exactly the same instance as above. Here is the file n20w20.001.txt
(DSU_n20w20.001.txt in our directory /tutorials/cplusplus/chap9/):
!! n20w20.001 16.75 391

CUST NO. XCOORD. YCOORD. DEMAND [READY TIME] [DUE DATE] [SERVICE TIME]

1 16.00 23.00 0.00 0.00 408.00 0.00


2 22.00 4.00 0.00 62.00 68.00 0.00
3 12.00 6.00 0.00 181.00 205.00 0.00
4 47.00 38.00 0.00 306.00 324.00 0.00
5 11.00 29.00 0.00 214.00 217.00 0.00
6 25.00 5.00 0.00 51.00 61.00 0.00
7 22.00 31.00 0.00 102.00 129.00 0.00
8 0.00 16.00 0.00 175.00 186.00 0.00
9 37.00 3.00 0.00 250.00 263.00 0.00
10 31.00 19.00 0.00 3.00 23.00 0.00
11 38.00 12.00 0.00 21.00 49.00 0.00
12 36.00 1.00 0.00 79.00 90.00 0.00
13 38.00 14.00 0.00 78.00 96.00 0.00
14 4.00 50.00 0.00 140.00 154.00 0.00
15 5.00 4.00 0.00 354.00 386.00 0.00
16 16.00 3.00 0.00 42.00 63.00 0.00
17 25.00 25.00 0.00 2.00 13.00 0.00
18 31.00 15.00 0.00 24.00 42.00 0.00
19 36.00 14.00 0.00 20.00 33.00 0.00
20 28.00 16.00 0.00 9.00 21.00 0.00
21 20.00 35.00 0.00 275.00 300.00 0.00
999 0.00 0.00 0.00 0.00 0.00 0.00

Having seen the same instance, you don’t need much complementary info to understand this
format. The first line of data (CUST NO. 1) represents the depot and the last line marks the
end of the file. As you can see, the authors are not really optimistic about solving instances
with more than 999 nodes! We don’t use the DEMAND column and we round down the numbers
of the last three columns.
You might think that the translation from this second format to the first one is obvious. It is
not! See the remark on Travel-time Computation on the Jeffrey Ohlmann and Barrett Thomas
benchmark page. In the code, we don’t try to match the data between the two formats, so you
might encounter different solutions.

The same instances in the da Silva-Urrutia and the López-Ibáñez-Blum formats


might be slightly different.

Solutions

We use a simple format to record feasible solutions:

334
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

• a first 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 verifies if a given solution is indeed feasible for a
given instance in López-Ibáñez-Blum or da Silva-Urrutia formats:
./check_tsptw_solutions -tsptw_data_file=DSU_n20w20.001.txt
-tsptw_solution_file=n20w20.001.sol

This program checks if all the nodes have been serviced and if the solution is feasible:
bool IsFeasibleSolution() {
...
// for loop to test each node in the tour
for (...) {
// Test if we have to wait at client node
waiting_time = ReadyTime(node) - total_time;
if (waiting_time > 0) {
total_time = ReadyTime(node);
}
if (total_time + ServiceTime(node) > DueTime(node)) {
return false;
}
}
...
return true;
}

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

335
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 file is 378 while the value of the
computed objective value is 387. This is because the distance matrix computed is different
from the actual one really used to compute the objective value of the solution. We refer again
the reader to the remark on Travel-time Computation from Jeffrey Ohlmann and Barrett Thomas
cited above. If you use the right distance matrix as in the López-Ibáñez-Blum format, you get:
TSPTW instance of type López-Ibáñez-Blum format
Solution is feasible!
Loaded obj value: 378, Computed obj value: 378
Total computed travel time: 387
TSPTW file LIB_n20w20.001.txt (n=21, min=2, max=57, sym? yes)

Now both the given objective value and the computed one are equal. Note that the total travel
time is a bit longer: 387 for a total distance of 378.

9.8.3 The TSPTWData class

You’ll find the code in the file 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 files

To read TSPTW instance files, the TSPTWData class offers the


LoadTSPTWFile(const std::string& filename);

method. It parses a file in López-Ibáñez-Blum or da Silva-Urrutia format and - in the second


case - loads the coordinates and the service times for further treatment. Note that the instance’s
format is only partially checked: bad inputs might cause undefined 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 filename of the
instance;
• std::string InstanceDetails() const: returns a short description of the
instance;
• int Size() const: returns the size of the instance;

336
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

• int64 Horizon() const: returns the horizon of the instance, i.e. the maximal due
time;
• int64 Distance(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const: returns the distance between the
two NodeIndexes;
• RoutingModel::NodeIndex Depot() const: returns the depot. This the first
node given in the instance and solutions files.
• int64 ReadyTime(RoutingModel::NodeIndex i) const: returns the
ready time of node i;
• int64 DueTime(RoutingModel::NodeIndex i) const: returns the due
time of node i
• int64 ServiceTime(RoutingModel::NodeIndex i) const: returns the
service time of node i.
The ServiceTime() method only makes sense when an instance is given in the da Silva-
Urrutia format. In the López-Ibáñez-Blum format, the service times are added to the arc costs
in the “distance” matrix and the ServiceTime() method returns 0.
To model the time windows in the RT, we use Dimensions, i.e. quantities that are accumu-
lated along the routes at each node. At a given node to, the accumulated time is the travel cost
of the arc (from, to) plus the time to service the node to. The TSPTWData class has a
special method to return this quantity:
int64 CumulTime(RoutingModel::NodeIndex from,
RoutingModel::NodeIndex to) const {
return Distance(from, to) + ServiceTime(from);
}

To read solution files

To read solution files, use the


void LoadTSPTWSolutionFile(const std::string& filename);

method. This way, you can load solution files and test them with the bool
IsFeasibleSolution() method briefly seen above. Actually, you should enquire if the
solution is feasible before doing anything with it.
Three methods help you deal with the existence/feasibility of the solution:
bool IsSolutionLoaded() const;
bool IsSolution() const;
bool IsFeasibleSolution() const;

With IsSolutionLoaded() you can check that indeed a solution was loaded/read from a
file. IsSolution() tests if the solution contains once and only once all the nodes of the
graph while IsFeasibleSolution() tests if the loaded solution is feasible, i.e. if all due
times are respected.

337
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)

Once you are sure that a solution is valid and feasible, you can query the loaded solution:
• int64 SolutionComputedTotalTravelTime() const: computes the total
travel time and returns it. The travel total time often differs from the objective value
because of waiting times;
• int64 SolutionComputedObjective() const: computes the objective
value and returns it;
• int64 SolutionLoadedObjective() const: returns the objective value
stored in the instance file
These methods are also available if the solution was obtained by the solver (in this
case, SolutionLoadedObjective() returns -1 and IsSolutionLoaded() returns
false).
The TSPTWData class doesn’t generate random instances. We wrote a little program for this
purpose.

9.8.4 Random generation of instances

You’ll find the code in the file tsptw_generator.cc.


The TSPTW instance generator tsptw_generator is very basic. It generates an instance in
López-Ibáñez-Blum or/and da Silva-Urrutia as follows:
• it generates n 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 (gflags) are defined 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 López-Ibáñez-Blum format instance file or not? (default: true);
• tsptw_DSU: Create a da Silva-Urrutia format instance file or not? (default: true);

338
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

By default, if the name of the instance is myInstance, tsptw_generator creates the three
files:
• DSU_myInstance.txt;
• LIB_myInstance.txt and
• myInstance_init.sol.
myInstance_init.sol contains the random tour generated to create the instance. Files
with the same name are overwritten without mercy.

9.8.5 Visualization with ePix

To visualize the solutions, we rely again on the excellent ePiX library. The file
tsptw_epix.h contains the TSPTWEpixData class. This class is similar to the
TSPEpixData class. Its unique constructor reads:
RoutingModel routing(...);
...
TSPTWData data(...);
...
TSPTWEpixData(const RoutingModel& routing,
const TSPTWData& data);

To write a ePiX solution file, use the following methods:


void WriteSolutionFile(const Assignment * solution,
const std::string & epix_filename)
void WriteSolutionFile(const std::string & tpstw_solution_filename,
const std::string & epix_filename);

The first method takes an Assignment while the second method reads the solution from a
solution file.
You can define the width and height of the generated image:
DEFINE_int32(epix_width, 10, "Width of the pictures in cm.");
DEFINE_int32(epix_height, 10, "Height of the pictures in cm.");

Once the ePiX file is written, you must evoke the ePiX elaps script:
./elaps -pdf epix_file.xp

Here is an example of the solution in the file n20w20.001.sol:

339
9.9. The TSPTW in or-tools

The dot in red in the center represents the depot or first node. The arrows indicate the direction
of the tour. Because of the time windows, the solution is no longer planar, i.e. the tour crosses
itself.
You can also print the node labels and the time windows with the flags:
DEFINE_bool(tsptw_epix_labels, false, "Print labels or not?");
DEFINE_bool(tsptw_epix_time_windows, false,
"Print time windows or not?");

For your (and our!) convenience, we wrote a small program tsptw_solution_to_epix. Its
implementation is in the file tsptw_solution_to_epix.cc. To use it, invoke:
./tsptw_solution_to_epix TSPTW_instance_file TSPTW_solution_file >
epix_file.xp

9.9 The TSPTW in or-tools

You can find the code in the file tsp.h, tsp_epix.h, tsp_minimal.cc, tsp.cc,
tsplib_solution_to_epix.cc and tsp_forbidden_arcs.cc and the data in the
files tsp_parameters.txt, a280.tsp and a280.opt.tour.

In this section, we try to solve the TSPTW Problem. First, we use Dimensions to
model the time windows and the default routing strategy. Then, we use a basic heuristic to
create a starting solution for the Local Search.

9.9.1 Time windows as a Dimension

You’ll find the code in the file 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.

340
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.
Let’s 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 find 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:

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

Let’s test this TSPTW solver on the following generated instance in da Silva-Urrutia format
(file DSU_test.tsptw):

!! test

CUST NO. XCOORD. YCOORD. DEMAND READY TIME DUE DATE SERVICE TIME

1 72.00 22.00 0.00 0.00 504.00 2.00


2 59.00 3.00 0.00 197.00 216.00 2.00
3 99.00 8.00 0.00 147.00 165.00 9.00
4 69.00 46.00 0.00 242.00 254.00 3.00
5 42.00 72.00 0.00 56.00 67.00 9.00
999 0.00 0.00 0.00 0.00 0.00 0.00

We invoke:
./tsptw -instance_file=DSU_test.tsptw -solution_file=test.sol

and we obtain:
1 5 3 2 4
252

Let’s check this solution with


check_tsptw_solution -instance_file=DSU_test.tsptw
-solution_file=test.sol -log_level=1

The solution is feasible:

Actions: Nodes: Releases: Deadlines: Services: Durations: Time:

travel to 4 56 67 9 58 58
serve 4 56 67 9 9 67
travel to 2 147 165 9 86 153
serve 2 147 165 9 9 162
travel to 1 197 216 2 40 202
serve 1 197 216 2 2 204
travel to 3 242 254 3 44 248

342
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time
windows

serve 3 242 254 3 3 251


travel to 0 0 504 2 24 275
serve 0 0 504 2 2 277
Solution is feasible!
Obj value = 252

If we solve the same instance but in López-Ibáñez-Blum format (file 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 confirms this:

Actions: Nodes: Releases: Deadlines: Services: Durations: Time:

travel to 4 56 67 0 67 67
serve 4 56 67 0 0 67
travel to 2 147 165 0 95 162
serve 2 147 165 0 0 162
travel to 1 197 216 0 42 204
serve 1 197 216 0 0 204
travel to 3 242 254 0 47 251
serve 3 242 254 0 0 251
travel to 0 0 504 0 26 277
serve 0 0 504 0 0 277
Solution is feasible!
Obj value = 277

Real instances, like DSU_n20w20.001.txt, are out of reach for our basic tsptw. This is
mainly because finding a first feasible solution is in itself a difficult problem. In the next sub-
section, we’ll help the solver finding this first feasible solution to start the local search.

9.9.2 A basic heuristic to find an initial solution

[TO BE WRITTEN]

343
9.10. Summary

9.10 Summary
summary

344
CHAPTER

TEN

VEHICULE ROUTING PROBLEMS


WITH CONSTRAINTS: THE
CAPACITATED VEHICLE ROUTING
PROBLEM

The Vehicle Routing Problem (VRP) is a Routing Problem where you seek to service a number
of customers with a fleet of (homogeneous or heterogeneous) vehicles starting from one depot.
The basic idea is to service clients - represented by nodes in a graph - by the vehicles. Lots
of theoric and industrial problems can be modelled as a VRP. The problem’s origins can be
traced back to the fifties (see [Dantzig1959] for instance). It includes the TSP (a VRP with one
vehicle) as a special case, and is, as such, a computationally complex problem.
We again use the excellent C++ ePiX library1 to visualize VRP and CVRP solutions in TSPLIB
format.

Overview:

We first introduce the VRP and the TSPLIB instances for the Capacitated Routing Vehicle
Problem. The TPSLIB instance format is the de facto format to represent CVRP instances in
the scientific community. We then present a basic program to solve the bare VRP. To do so, we
show how to interact directly with the underlying CP solver. Next, the CVRP is introduced and
explained. Capacities are modelled with Dimensions. Finally, we discuss the multi-depots
variant of the VRP in general and how to fix some parts of the routes while letting the CP solver
assign the other clients to vehicles, i.e. how to complete the partial solution.

Prerequisites:

• Basic knowledge of C++.


• Basic knowledge of Constraint Programming (see chapter 1).
• Basic knowledge of the Constraint Programming Solver (see chapter 2).
1
The ePiX library uses the TEX/LATEX engine to create beautiful graphics.
10.1. The Vehicle Routing Problem (VRP)

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

You can find the code in the directory documentation/tutorials/cplusplus/chap10.


The files in this directory are:
• cvrp_data_generator.h: Contains the CVRPDataGenerator class that gener-
ates random CVRP instances.
• cvrp_data_generator.cc: Generates random CVRP instances.
• cvrp_data.h: Contains the CVRPData class used in this chapter to encode a CVRP
instance.
• cvrp_solution.h: Povides the CVRPSolution used to represent CVRP solutions.
• cvrp_epix_data.h: Provides the helper functions to visualize CVRP solutions with
the ePiX library.
• vrp_solution_to_epix.cc: Permits the visualization of a VRP solution with the
help of the ePiX library.
• vrp.cc: A basic implementation to solve the VRP.
• check_vrp_solution.cc: Checks if a VRP solution is feasible.
• cvrp_basic.cc: A basic implementation to solve the CVRP.
• cvrp_solution_to_epix.cc: Permits the visualization of a CVRP solution with
the help of the ePiX library.
• check_cvrp_solution.cc: Checks if a CVRP solution is feasible.
• vrp_IO.cc: Simple program to apply and test the IO mechanism of the RL with a
multi-VRP.
• vrp_locks.cc: Simple program to apply and test locks in a multi-VRP.

10.1 The Vehicle Routing Problem (VRP)

You can find the code in the files 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
files A-n32-k5.vrp and opt-A-n32-k5.

346
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

In this section, we briefly present one of the basic versions of the Vehicle Routing Problem.
Most of the time, there are additional constraints2 . This basic version is very little studied in
the scientific literature and in this manual we use it as a gentle introduction to the CVRP.
The usual format to encode CVRP instances is from the TSPLIB. There is no TSPLIB for-
mat for the basic VRP, so we simply read CVRP and forget about the demands of the CVRP
instances to solve a basic version of the VRP.
We can reuse our TSPLIBReader class as it also manages to read CVRP instances. We
use again the excellent ePiX library through our CVRPEpixData class to visualize CVRP
instances and their solutions.
We didn’t program specialized VRP classes. Instead we’ll use and present more general CVRP
classes.

10.1.1 The Problem

Given a graph G = (V, E) and pairwise distances between nodes, the VRP consists in finding
one or several routes to visit each node once. Each tour starts and ends at the same depot. The
cost of a tour is given by the sum of the distances along the route and the total cost of a feasible
solution for the VRP is the sum of the costs of all tours.
This “definition” remains vague enough to encompass the most known variants of the VRP.
Indeed, not only does the VRP exist in different flavors (capacitated, multi-depots, with time-
windows, with pick-up and delivery, ...) but several slightly different definitions exist in the
literature. In particular, some instances fix - in a way or another - the number of vehicles to be
used and a total travelled distance limit is imposed on the vehicles.
In this manual, we will use the definition given by Gilbert Laporte in [Laporte1992]. In this
article, a VRP is designed in such a way that
• there is only one depot and each vehicle route starts and ends at this depot;
• each node - except the depot - has to be visited (or serviced) exactly once by exactly one
vehicle;
• the fleet of vehicles is homogeneous, i.e. the cost to travel an arc is the same for all the
vehicles;
• the number of vehicles can be fixed, bounded or free;
• the distances between two nodes don’t need to be equal;
• the objective function is the sum of the arcs traversed by all vehicles that service at least
one node with no additional cost for each used vehicle.
• some side constraints are satisfied.
The last point is important. Indeed, without side constraints and if the graph obeys the triangle
inequality (i.e. d(x, z) 6 d(x, y) + d(y, z)), then there is no need to use more than one vehicle.
2
This basic version of the VRP is better known under the name mTSP or the Multiple TSP. We refer the reader
to [Bektas2006] for more.

347
10.1. The Vehicle Routing Problem (VRP)

For instance, the solution with two vehicles in the next figure (left) costs more than the same
solution where only one vehicle follows the two routes one after the other (right):

z
x
y

As d(x, z) 6 d(x, y) + d(y, z), the shortcut to go immediately from x to z without passing by
the depot y costs less.
The most common side constraints include:
• capacity restrictions: a non-negative weight (or demand) di is attached to each city i
(except the depot) and the sum of weights of any vehicle route may not exceed the vehicle
capacity. Capacity-constrained VRP are referred to as CVRP and will be studied in this
chapter.
• maximum number of cities that can be visited: the number of cities on any route is
bounded above by q. This is a special case of CVRP with di = 1 for all i except the
depot and capacity equal to q for all vehicles.
• total time (or distance) restrictions: each route has its length bounded by a certain amount
T of time that cannot be exceeded by each vehicle. Such VRP are often denoted as DVRP
or distance-constrained VRP.
• time-windows: each city must be serviced within a time-window [ai , bi ] and waiting times
are allowed.
• precedence relations between pair of cities: city j cannot be visited before city i. Among
such problems are the VRPPD: the Vehicle Routing Problems with Pickup and Delivery.
A number of goods need to be moved from certain pickup locations to other delivery
locations.
And the list goes on.
For our basic version of the VRP, all vehicles must be used. This version of the VRP is better
known as the mTSP3 . Some problems can be coined as mTSP and we refer again the reader to
[Bektas2006] to find some examples.
Below you’ll find a picture of a solution of a VRP with 32 cities and 5 vehicles (A-n32-k5)
in the sub-section Visualization with ePix.

10.1.2 Benchmark data

Several known benchmark data sources are available on the internet. We refer you to three:
• The VRPLIB page: http://www.or.deis.unibo.it/research_pages/ORinstances/VRPLIB/VRPLIB.html
and
3
Not to be confused with the k-TSP where only k nodes/cities must be visited/serviced.

348
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

• The NEO (Network and Emerging Optimization) page: http://neo.lcc.uma.es/vrp/vrp-


instances/.
• The SYMPHONY VRP data page maintained by T. Ralphs:
http://branchandcut.org/VRP/data/index.htm.
Their instances are encoded in the TSPLIB format. We refer the reader to the sub-section The
TSPLIB format for an introduction to this format.

The TSPLIB format for the CVRP

The VRP in the TSPLIB format are only CVRP, i.e. capacitated problems. We will ignore the
demands on the nodes to solve our basic VRP. Don’t forget the TSPLIB convention to number
the nodes starting at 1.

Nodes are numbered from 1 to n in the TSPLIB and we keep this convention in this
chapter.

The instance file

Here is the file A-n32-k5.vrp:


NAME : A-n32-k5
COMMENT : (Augerat et al, Min no of trucks: 5, Optimal value: 784)
TYPE : CVRP
DIMENSION : 32
EDGE_WEIGHT_TYPE : EUC_2D
CAPACITY : 100
NODE_COORD_SECTION
1 82 76
2 96 44
3 50 5
4 49 8
...
DEMAND_SECTION
1 0
2 19
3 21
4 6
...
DEPOT_SECTION
1
-1
EOF

The type is CVRP and the capacity of the vehicles is specified after the keyword CAPACITY.
The demands on the node are specified in a DEMAND_SECTION section. The TSPLIB format
requires the depot to be listed in the the DEMAND_SECTION section and have a demand of 0.
Note that there is no specification whatsoever on the number of vehicles.
If you use other instances, be careful that they fully comply to the TSPLIB format (or change
the code).

349
10.1. The Vehicle Routing Problem (VRP)

The solution file

While there exists a TSPLIB format for the solutions of (C)VRP, it is seldom used. We’ll follow
the trend and use the most commonly adopted format.
This is what the file opt-A-n32-k5 containing an optimal solution for the CVRP
A-n32-k5 instance above looks like:
Route #1: 21 31 19 17 13 7 26
Route #2: 12 1 16 30
Route #3: 27 24
Route #4: 29 18 8 9 22 15 10 25 5 20
Route #5: 14 28 11 4 23 3 2 6
cost 784

Routes are numbered starting form 1 while the nodes in the solution file are numbered starting
from... 0! Also, note that the depot is not listed.

Nodes are numbered from 0 in the solution files!

By default, we’ll use the same convention. A bool


numbering_solution_nodes_from_zero flag allows to switch between a num-
bering starting from 0 or 1 in the solution file.

10.1.3 To read TSPLIB files

Our good old TSPLIBReader class defined in the


header routing_common/tsplib_reader.h (see 9.3.2) comes again to the res-
cue. It was designed to also deal with CVRP. Simply provide a filename to the constructor:

TSPLIBReader tsp_data_reader(instance_file);

10.1.4 To generate a random CVRP: the CVRPDataGenerator


class

We have defined a basic CVRPDataGenerator class to generate CVRP random instances.


Each generated instance has at least one CVRP feasible solution. An instance is created by first
creating a random CVRP solution, i.e. several feasible tours are created. We then associate
demands such that the capacity of each vehicle is respected. A CVRPDataGenerator needs
an instance name and the total number of nodes, including the depot:
CVRPDataGenerator cvrp_data_generator(instance_name, instance_size);

A little program crvp_data_generator generates random instances of CVRP. You can invoke
it as follows:

350
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

./cvrp_data_generator -instance_name=my_instance -instance_size=40


> my_instance_file.vrp

Several parameters scattered in different files are available as gflags:


Flags Types Default Description
instance_name std::string “” Name of the instance.
instance_size int32 0 Number of nodes, in-
cluding the depot.
instance_filename std::string “” Filename to save the
CVRP instance in
TSPLIB format.
depot int32 1 Depot of the CVRP in-
stance. Must be greater
or equal to 1.
distance_file std::string “” Matrix distance file.
deterministic_random_seed bool true Use deterministic ran-
dom seeds or not?
number_vehicles int32 2 Number of vehicles.
capacity int64 100 Capacity of all vehicles.
allow_zero_capacity bool true Allow node with zero
capacity?
width_size int32 6 Width size of fields in
output.
x_max int32 100 Maximum x coordi-
nate.
y_max int32 100 Maximum y coordi-
nate.

10.1.5 To hold and check a (C)VRP solution: the CVRPSolution


class

To represent a (C)VRP solution, we have defined the CVRPSolution class. Two constructors
are available:
CVRPSolution(const CVRPData & data, std::string filename);
CVRPSolution(const CVRPData & data,
const RoutingModel * routing,
const Assignment * sol);

Two methods verify the feasibility of the solution:


• bool CVRPSolution::IsSolution() const: tests if all nodes are serviced
once and only once, i.e. if the solution is a feasible VRP solution and
• bool IsFeasibleSolution() const: tests also if the capacities of the vehicles
are respected, i.e. if the solution is a feasible CVRP solution.
The CVRPSolution class provides iterators to run through the solution. For instance, the
ComputeObjectiveValue() method - that computes the objective value of the solution -

351
10.1. The Vehicle Routing Problem (VRP)

is written as follows:
int64 CVRPSolution::ComputeObjectiveValue() const {
int64 obj = 0;
RoutingModel::NodeIndex from_node, to_node;

for (const_vehicle_iterator v_iter = vehicle_begin();


v_iter != vehicle_end(); ++v_iter) {
from_node = depot_;
for (const_node_iterator n_iter = node_begin(v_iter);
n_iter != node_end(v_iter); ++n_iter ) {
to_node = *n_iter;
obj += data_.Distance(from_node, to_node);
from_node = to_node;
}
// Last arc
obj += data_.Distance(to_node, depot_);
}

return obj;
}

Because this method is constant and doesn’t change the solution, it uses constant iterators. The
CVRPSolution class also provides the following non constant iterators:
• vehicle_iterator and
• node_iterator.

10.1.6 The CVRPData class: part I

Because there is no TPSLIB format to encode VRP, we don’t provide a VRPData class. In-
stead, we use the more general CVRPData class and disregard the demands. We provide two
ways to create a CVRPData object: you can read a TSPLIB file or randomly generate an
instance.
As usual, you need to give a TSPLIBReader to the CVRPData constructor:
CVRPData cvrp_data(tsp_data_reader);

or the CVRPDataGenerator to the CVRPData constructor:


CVRPData cvrp_data(cvrp_data_generator);

Basically, the CVRPData class contains the distance matrix, the nodes coordinates (if any) and
the clients demands.

10.1.7 Visualization with ePix

You can visualize a (C)VRP instance and/or a solution with the CVRPEpixData class. First,
link it to a CVRPData object:

352
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

CVRPData cvrp_data(...);
CVRPEpixData epix_data(cvrp_data);

and then invoke its Print...() or Write...() methods:


void PrintInstance(std::ostream & out) const;
void WriteInstance(const std::string & filename) const;
void PrintSolution(std::ostream & out,
const CVRPSolution & sol) const;
void WriteSolution(const std::string & filename,
const CVRPSolution & sol) const;

For your (and our!) convenience, we have written the small program cvrp_solution_to_epix
to visualize a CVRP solution. To create a pdf image of the opt-A-n32-k5 solution, invoke
it as follows:
./cvrp_solution_to_epix -instance_file=A-n32-k5.vrp
-solution_file=opt-A-n32-k5 > opt-A-n32-k5.xp

then process the xp file with elaps:


elaps -pdf opt-A-n32-k5.xp

to obtain the following image:

The same flags as for the program tsp_solution_to_epix can be applied. See sub-section 9.3.4.

10.2 The VRP in or-tools

You can find the code in the files 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 files A-n32-k5.vrp.

In this section, we look at a simple implementation of a program to solve the basic


VRP. Particularly of interest is how we force each vehicle to service at least one city and how
we traverse a solution of the VRP.
We don’t devise specialized search strategies and use the default strategy.

353
10.2. The VRP in or-tools

10.2.1 How to force all vehicles to service cities?

One way to force all vehicles to service at least one city is to forbid each vehicle to return
immediately to its ending depot. In this fashion, the vehicle will have to service at least one
city. To acheive this, we simply remove the end depots from the domain of the NextVar()
variables corresponding to begin depots.
To obtain the NextVar() corresponding to begin depots is easy:
IntVar* const start_var =
routing.NextVar(routing.Start(vehicle_nbr));

To obtain the int64 indices corresponding to end depots is not more complicated. We detail
the auxiliary graph in the sub-section The auxiliary graph. The internal numbering of its nodes
is done such that all end depots are numbered last, having int64 indices from Size() to
Size() + vehicles() non-inclusive where Size() returns the number of NextVar()
variables and vehicles() the number of vehicles.
To remove the int64 indices, we need to have a complete model with all variables defined.
To complete the model, we must invoke the CloseModel() method:
RoutingModel routing(...);
...
routing.CloseModel();

Now, we can remove the undesired int64 indices:


// Forbidding empty routes
for (int vehicle_nbr = 0; vehicle_nbr < FLAGS_number_vehicles;
++vehicle_nbr) {
IntVar* const start_var =
routing.NextVar(routing.Start(vehicle_nbr));
for (int64 node_index = routing.Size();
node_index < routing.Size() + routing.vehicles();
++node_index) {
start_var->RemoveValue(node_index);
}
}

10.2.2 The basic program

You’ll find the code in the file vrp.cc.


The main method is classic:
int main(int argc, char **argv) {
...
operations_research::TSPLIBReader
tsplib_reader(FLAGS_instance_file);
operations_research::CVRPData cvrp_data(tsplib_reader);
operations_research::VRP_solver(cvrp_data);

354
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

return 0;
}

Several flags are defined:


Flags Types Default Description
instance_file std::string “” Input file with TSPLIB
data.
depot int32 1 Depot of the CVRP in-
stance. Must be greater
or equal to 1.
solution_file std::string “” Output file with gen-
erated solution in
(C)VRP format.
number_vehicles int32 2 Number of vehicles.
time_limit_in_ms int32 0 Time limit in ms. 0
means no limit.
The VRPSolver() function is in charge of solving a basic VRP.
void VRPSolver (const CVRPData & data) {

const int size = data.Size();

RoutingModel routing(size, FLAGS_number_vehicles);


routing.SetCost(NewPermanentCallback(&data, &CVRPData::Distance));

if (FLAGS_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_time_limit_in_ms);
}

// Setting depot
CHECK_GT(FLAGS_depot, 0) << " Because we use the"
<< " TSPLIB convention, the depot id must be > 0";
RoutingModel::NodeIndex depot(FLAGS_depot -1);
routing.SetDepot(depot);

routing.CloseModel();

...

We must invoke the CloseModel() to finalize the model for our instance. We need the
complete model to be able to interact with it.
We continue the inspection of the VRP_solver() function:
void VRPSolver (const CVRPData & data) {

...
// Forbidding empty routes
// See above.

// SOLVE
const Assignment* solution = routing.Solve();

355
10.2. The VRP in or-tools

...

You could inspect the solution as usual, only taking into account that there are more than one
vehicle:
if (solution != NULL) {

// Solution cost.
LG << "Obj value: " << solution->ObjectiveValue();
// Inspect solution.
std::string route;
for (int vehicle_nbr = 0; vehicle_nbr < FLAGS_number_vehicles;
++vehicle_nbr) {
route = "";
for (int64 node = routing.Start(vehicle_nbr);
!routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {
route = StrCat(route,
StrCat(routing.IndexToNode(node).value() + 1 , " -> "));
}
route = StrCat(route,
routing.IndexToNode(routing.End(vehicle_nbr)).value() + 1 );
LG << "Route #" << vehicle_nbr + 1 << std::endl
<< route << std::endl;
}

} else {
LG << "No solution found.";
}

In VRPSolver(), we prefer to use the CVRPSolution class:


void VRPSolver (const CVRPData & data) {
...
CVRPSolution cvrp_sol(data, &routing, solution);
cvrp_sol.SetName(StrCat("Solution for instance ", data.Name(),
" computed by vrp.cc"));
// test solution
if (!cvrp_sol.IsSolution()) {
LOG(ERROR) << "Solution is NOT feasible!";
} else {
LG << "Solution is feasible and has an obj value of "
<< cvrp_sol.ComputeObjectiveValue();
// SAVE SOLUTION IN CVRP FORMAT
if (FLAGS_solution_file != "") {
cvrp_sol.Write(FLAGS_solution_file);
}
}
}

10.2.3 Some outputs

Let’s see if our trick works. We invoke the vrp program with the flag number_vehicles
equal to 2, 3, 4 and 5. For instance:

356
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

./vrp -instance_file=A-n32-k5.vrp -number_vehicles=2


-solution_file=A-n32-k5-k2.sol

The solution is:


Route #1: 26 16 12 1 7 13 21 31 19 17 2 3 23 6 14 24 27 29 18 8 28
4 11 9 22 15 10 25 5 20
Route #2: 30
cost 545

There are indeed two routes. Don’t forget that this solution doesn’t con-
tain the depot and that the nodes are numbered from 0 (Remember the
numbering_solution_nodes_from_zero flag?).
Here are different outputs obtained with the ePiX library, with number_vehicles equal to
2, 3, 4 and 5:

357
10.3. The Capacitated Vehicle Routing Problem (CVRP)

Without a specialized search strategy, we obtain solutions that are not very interesting. When
we’ll solve real CVRPs, we’ll devise specialized search strategies.

10.3 The Capacitated Vehicle Routing Problem (CVRP)

You can find the code in the files cvrp_data.h and check_cvrp_solution.cc and
the data in the files A-n32-k5.vrp and opt-A-n32-k5.

We don’t have much to say about the CVRP as much has been said in the section The
Vehicle Routing Problem (VRP). The side constraint we add here to the VRP is a capacity
constraint: each vehicle must respect its capacity on the route it traverses, i.e. the sum of the
node capacities of all the nodes serviced by one vehicle must be smaller than the vehicule
capacity - for all vehicles.

10.3.1 The problem

The Capacitated Vehicle Routing Problem (CVRP) is one class of Vehicle Routing Problems
where the vehicles have a certain capacity they can not exceed. A client can only be serviced
by one vehicle. The depot is the same for all vehicles and doesn’t need to be serviced.
In this version, all vehicles have the same capacity C that cannot be exceeded on a tour and
clients (nodes) have demands. A client i has a demand di that has to be met when a vehicle
services this client. Think of demands as quantities of goods that the vehicle picks up when
visiting clients: the total among of goods/demands picked up along the route cannot exceed its
capacity.
The program check_cvrp_solution allows you to check if indeed the capacities of the vehicles
are respected along the routes. For instance, invoking:
./check_cvrp_solution -log_level=1 -instance_file=A-n32-k5.vrp
-solution_file=opt-A-n32-k5

prints:
Route 1 with capacity 100
Servicing node 22 with demand 12 (capacity left: 88)
Servicing node 32 with demand 0 (capacity left: 88)

358
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

Servicing node 20 with demand 24 (capacity left: 64)


Servicing node 18 with demand 19 (capacity left: 45)
Servicing node 14 with demand 16 (capacity left: 29)
Servicing node 8 with demand 16 (capacity left: 13)
Servicing node 27 with demand 2 (capacity left: 11)
Route 2 with capacity 100
Servicing node 13 with demand 21 (capacity left: 79)
Servicing node 2 with demand 19 (capacity left: 60)
Servicing node 17 with demand 18 (capacity left: 42)
Servicing node 31 with demand 14 (capacity left: 28)
Route 3 with capacity 100
Servicing node 28 with demand 20 (capacity left: 80)
Servicing node 25 with demand 24 (capacity left: 56)
Route 4 with capacity 100
Servicing node 30 with demand 2 (capacity left: 98)
Servicing node 19 with demand 1 (capacity left: 97)
Servicing node 9 with demand 6 (capacity left: 91)
Servicing node 10 with demand 16 (capacity left: 75)
Servicing node 23 with demand 4 (capacity left: 71)
Servicing node 16 with demand 22 (capacity left: 49)
Servicing node 11 with demand 8 (capacity left: 41)
Servicing node 26 with demand 24 (capacity left: 17)
Servicing node 6 with demand 7 (capacity left: 10)
Servicing node 21 with demand 8 (capacity left: 2)
Route 5 with capacity 100
Servicing node 15 with demand 3 (capacity left: 97)
Servicing node 29 with demand 15 (capacity left: 82)
Servicing node 12 with demand 14 (capacity left: 68)
Servicing node 5 with demand 19 (capacity left: 49)
Servicing node 24 with demand 8 (capacity left: 41)
Servicing node 4 with demand 6 (capacity left: 35)
Servicing node 3 with demand 21 (capacity left: 14)
Servicing node 7 with demand 12 (capacity left: 2)
Solution is feasible!
Obj value = 784

As you can see, each vehicle has its capacity respected. In case you wonder, we use the real
node identifiers (from the original graph) in this output.
In the next section, we show you how to customize each vehicle (its usage cost, its capacity, its
edge traversal cost).
Exact methods can solve instances with 100 clients (see [Roberti2012]) but even heuristics
are limited to solve instances with more or less 1200 clients (see [Groër2011]). Most effi-
cient heuristics are a combination of integer programing with local search (see [Toth2008] or
[Groër2011]).

Several problems modelled as CVRP


[TO BE WRITTEN]

359
10.4. The CVRP in or-tools

10.3.2 The CVRPData class: part II

To play with instances and see how solutions can change when you change some parts of the
instances, the CVRPData class provides some setters:
void SetDepot(RoutingModel::NodeIndex d);
void SetDemand(const RoutingModel::NodeIndex i, int64 demand);
void SetCapacity(int64 capacity);

You even can change the distance between two nodes i and j:
CVRPData cvrp_data(...);
...
RoutingModel::NodeIndex i = ...;
RoutingModel::NodeIndex j = ...;
int64 new_distance = ...;
cvrp_data.SetDistance(i, j) = new_distance;

Note that the call to SetDistance() is different because it returns an lvalue (left value)4 .
You also have the corresponding getters:
RoutingModel::NodeIndex Depot() const;
int64 Demand(const RoutingModel::NodeIndex i) const;
int64 TotalDemand() const;
int64 Capacity() const;
int64 Distance(RoutingModel::NodeIndex i,
RoutingModel::NodeIndex j) const;

The TotalDemand() method returns the sum of all demands for all clients.

10.4 The CVRP in or-tools

You can find the code in the files 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
files A-n32-k5.vrp and opt-A-n32-k5.

To solve a CVRP with or-tools, we’ll use our homemade CVRP classes (CVRPData,
CVRPSolution and CVRPEpixData). The main difficulty that remains is how to model
the demands in or-tools. Simple: with Dimensions. We will also detail how to provide an
initial solution, how to tweak the search strategy and finally how to deal with an heterogenous
fleet of vehicles.

10.4.1 The demands as a Dimension

You’ll find the code in the file cvrp_basic.cc.


4
You don’t need to know the details of what exactly an lvalue is in C++. It’s enough to know that you can
use an lvalue in an assignment (to the left of the equal sign).

360
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

The accumulation of demands along the routes makes the Dimension variables perfect candi-
dates to model demands. We suggest you to read the sub-section Time windows as a Dimension
to refresh your memory if needed. The situation is a little easier here as the demands only de-
pend on the client and not the arcs that the vehicles traverse to reach these clients.
As usual, the solving process is encapsulated in a void CVRPBasicSolver(const
CVRPData & data) function inside the operations_research namespace called
from the main function:
int main(int argc, char **argv) {
...
operations_research::TSPLIBReader tsplib_reader(instance_file);
operations_research::CVRPData cvrp_data(tsplib_reader);
operations_research::CVRPBasicSolver(cvrp_data);
}

The creation of the routing model is quite known by now:


void CVRPBasicSolver (const CVRPData & data) {

const int size = data.Size();


const int64 capacity = data.Capacity();

CHECK_GT(FLAGS_number_vehicles, 1)
<< "We need at least two vehicles!";
// Little check to see if we have enough vehicles
CHECK_GT(capacity, data.TotalDemand()/FLAGS_number_vehicles)
<< "No enough vehicles to cover all the demands";
...

This quick check is handy: no need to find a feasible solution when none exists. The distances
and the depot are passed to the solver in the usual way:
void CVRPBasicSolver (const CVRPData & data) {
...
RoutingModel routing(size, FLAGS_number_vehicles);
routing.SetCost(NewPermanentCallback(&data, &CVRPData::Distance));

if (FLAGS_time_limit_in_ms > 0) {
routing.UpdateTimeLimit(FLAGS_time_limit_in_ms);
}

// Setting depot
CHECK_GT(FLAGS_depot, 0) << " Because we use the"
<< " TSPLIB convention, the depot id must be > 0";
RoutingModel::NodeIndex depot(FLAGS_depot -1);
routing.SetDepot(depot);
...

To add the client demands and the capacity constraints, we can use the
AddVectorDimension() method. To use this method, we need a demands array
with the int64 demands such that demands[i] corresponds to the demand of client i.
void CVRPBasicSolver (const CVRPData & data) {
...

361
10.4. The CVRP in or-tools

std::vector<int64> demands(size);
for (RoutingModel::NodeIndex i(RoutingModel::kFirstNode);
i < size; ++i) {
demands[i.value()] = data.Demand(i);
}
...

The API requires a C-array:


void AddVectorDimension(const int64* values,
int64 capacity,
bool fix_start_cumul_to_zero,
const string& name);

Because the C++ language guarantees that the values in an std::vector are contiguous, we
can pass the address of its first element:
void CVRPBasicSolver (const CVRPData & data) {
...
routing.AddVectorDimension(&demands[0], capacity, true, "Demand");
...

The bool argument indicates if the demand of the depot is set to demands[0] (when
false) or to 0 (when true) . As this demand is 0 for CVRP, this argument doesn’t really
matter and is, hence, set to true.
Now, come the solving process and the inspection if any solution is found:
void CVRPBasicSolver (const CVRPData & data) {
...
const Assignment* solution = routing.Solve();
if (solution != NULL) {
CVRPSolution cvrp_sol(data, &routing, solution);
cvrp_sol.SetName(StrCat("Solution for instance ", data.Name(),
" computed by vrp.cc"));
// test solution
if (!cvrp_sol.IsFeasibleSolution()) {
LOG(ERROR) << "Solution is NOT feasible!";
} else {
LG << "Solution is feasible and has an obj value of "
<< cvrp_sol.ComputeObjectiveValue();
// SAVE SOLUTION IN CVRP FORMAT
if (FLAGS_solution_file != "") {
cvrp_sol.Write(FLAGS_solution_file);
} else {
cvrp_sol.Print(std::cout);
}
}
} else {
LG << "No solution found.";
}
}

Let’s test the program:

362
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

./cvrp_basic -instance_file=A-n32-k5.vrp -number_vehicles=5

The output is:


Using first solution strategy: DefaultStrategy
Using metaheuristic: GreedyDescent
Solution is feasible and has an obj value of 849
Route #1: 22 9 11 4 6 7 16
Route #2: 20 5 25 10 15 29 27
Route #3: 13 2 3 23 28 8 18 14 24
Route #4: 26 17 19 31 21 1 12
Route #5: 30
cost 849

It is quite far from the optimal solution opt-A-n32-k5 with an objective value of 784. Using
GreedyDescent is not very clever but first, before we change the search strategy, let’s give
a hand to the solver and allow for the introduction of a known initial solution to start the local
search.

10.4.2 An initial solution

You’ll find the code in the file cvrp_basic.cc.


First, let’s define a gflags to hold the name of the file containing a good starting solution:
DEFINE_string(initial_solution_file, "",
"Input file with a valid feasible solution.");

To read this solution, we use our CVRPSolution class. To transform a solution to


an Assignment, the RoutingModel class proposes several methods. We’ll use its
RoutesToAssignment() method:
bool RoutesToAssignment(const std::vector<
std::vector<NodeIndex> >& routes,
bool ignore_inactive_nodes,
bool close_routes,
Assignment* const assignment) const;

The routes are lists of nodes traversed by the vehicles. The indices of the outer
std::vector in routes correspond to the vehicles identifiers, the inner std::vector
contains the nodes on the routes for the given vehicles. The inner std::vectors must not
contain the start and end nodes, as these are determined by the RoutingModel class itself.
This is exactly what the Routes() method of the CVRPSolution returns.
With ignore_inactive_nodes set to false, this method will fail in case some of the
nodes in the routes are deactivated; when set to true, these nodes will be skipped.
If close_routes is set to true, the routes are closed; otherwise they are kept open.
The RoutesToAssignment method sets the NextVar() variables of the Assigment to
the corresponding values contained in the std::vector<...> routes. You don’t need
to add manually these variables in the Assignment: if they are missing, the method adds
them automatically. The method returns true if the routes are successfully loaded. However,

363
10.4. The CVRP in or-tools

such assignment might still not be a valid solution to the routing problem. This is due to more
complex constraints that are not tested. To verify that the solution is indeed feasible for your
model, call the CP solver CheckSolution() method.
One last thing, you cannot call the RoutesToAssignment() if the routing model is not
closed beforehand.
Time for some code:
void CVRPBasicSolver (const CVRPData & data) {
...
routing.CloseModel();

// Use initial solution if provided


Assignment * initial_sol = NULL;
if (FLAGS_initial_solution_file != "") {
initial_sol = routing.solver()->MakeAssignment();
CVRPSolution cvrp_init_sol(data, FLAGS_initial_solution_file);
routing.RoutesToAssignment(cvrp_init_sol.Routes(),
true,
true,
initial_sol);

if (routing.solver()->CheckAssignment(initial_sol)) {
CVRPSolution temp_sol(data, &routing, initial_sol);
LG << "Initial solution provided is feasible with obj = "
<< temp_sol.ComputeObjectiveValue();
} else {
LG << "Initial solution provided is NOT feasible... exit!";
return;
}
}

const Assignment* solution = routing.Solve(initial_sol);


...

A few comments are in order here. If an initial file is provided, we create the initial_sol
Assignment with the solver’s MakeAssignment() method. Remember that this cre-
ates an hollow shell to contain some variables that you have to add yourself. We don’t need
to do this here as the RoutesToAssignment() method will do it for you but only for
the NextVar() variables. We check the feasibility of the initial solution by calling the
CheckAssignment() method of the CP solver. The CheckAssignment() method cre-
ates a new Search and propagates the initial constraints of the model with the given solution.
It returns true if the solver didn’t fail which means that the given solution is feasible.
We previously have seen that to compute the objective value of a solution, you somehow
need to give this solution to the solver and let it solve the model. One way is to use a
SolutionCollector, another is to use DecisionBuilders: a StoreAssignment
and a RestoreAssignment with an Assignment to which you have attached the objec-
tive variable. However, this is not needed here since the CVRPSolution class computes an
objective value from an Assignment with assigned NextVar() variables. This is precisely
the role of the temp_sol object.

364
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

Finally, the Solve() method takes into account this initial solution. Only the main
NextVar() variables are needed. This initial solution is reconstructed and tested by the
CP Routing solver. If initial_sol is NULL then the solving process is started from scratch
and the CP Routing solver tries to find an initial solution for the local search procedure.
We will see more in details the different methods provided by the RoutingModel class to
switch from routes to Assignment and vice-versa in the section Assignments and partial
Assignments.

10.4.3 Different search strategies

[TO BE WRITTEN ONCE SEARCHLIMITS WITH RESPECT TO LOCAL SEARCH ARE


DEFINED]

10.4.4 What about customizing the vehicles?

Until now, we considered an homogeneous fleet of vehicles: all vehicles are exactly the same.
What happens if you have (very) different types of vehicles? The RL allows you to customize
each class of vehicles.
A different cost might be assigned to each type of vehicles. This can be done by the
SetVehicleFixedCost() method:
void SetVehicleFixedCost(int vehicle, int64 cost);

The cost of using a certain type of vehicles can be higher or lower than others. If a vehicle is
used, i.e. this vehicle serves at least one node, this cost is added to the objective function.
Different types of vehicles have different capacities? No problem. This is allowed in the RL:
void AddDimensionWithVehicleCapacity(NodeEvaluator2* evaluator,
int64 slack_max,
VehicleEvaluator* vehicle_capacity,
bool fix_start_cumul_to_zero,
const string& name);

AddDimensionWithVehicleCapacity() works exactly like AddDimension() ex-


cept a VehicleEvaluator callback is used to return the capacities for each vehicle. A
VehicleEvaluator is simply a ResultCallback1<int64, int64> and you need
to implement its int64 Run(int64 vehicle) method to return the capacity of vehicle
number vehicle.
You can even set different costs to traverse the arcs of the graph:
void SetVehicleCost(int vehicle, NodeEvaluator2* evaluator);

365
10.5. Multi-depots and vehicles

10.5 Multi-depots and vehicles

Some instances have different depots. This is not a problem for the RL. You can have as many
depots as you want (within the limit of an int). These depots can be starting, ending or
starting and ending depots. Each problem in the RL is modelled with routes: each route starts
at a depot, finishes at a depot and is serviced by one vehicle. The auxiliary graph used internally
is constructed in such a way that every vehicle has its own starting and ending depots.
Each route and vehicle are in a one to one correspondence in the RL. They are represented by
VehicleVar() variables and divided among several VehicleClasses.

10.5.1 Problems with multi-depots

You can find the source code in the file chap13/rl_auxiliary_graph.cc.


We only consider problems where the starting and ending depots are known for each vehicle.
If you have to deal with a problem where this is not the case, there exists a bunch of modeling
tricks where you can add fictive nodes.
To create the RoutingModel with multi-depots, simply pass to the con-
structor an std::vector<std::pair<RoutingModel::NodexIndex,
RoutingModel::NodexIndex> > with the list of pairs of starting and ending depots in
the original graph, one pair for each vehicle. The index of the pairs in the std::vector
corresponds to the index of the vehicles.
Let’s implement the code for the instance used in the sub-section The auxiliary graph3.
1 0 4
1
0
2
8
5
3 6 7
1
0
0
1
Starting depot
1
0
0Ending depot
1
1
0 Starting and ending depot
Transit node

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

366
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

depots[1] = std::make_pair(3,4);
depots[2] = std::make_pair(3,7);
depots[3] = std::make_pair(4,7);

RoutingModel VRP(9, 4, depots);

The number of vehicles (4) and the length of the std::vector with the pairs of depots
(depots.length()) must be equal5 .

10.5.2 Multi-depots in practice

There are several restrictions on the depots and practical facts about the RL model that are
worth mentioning.
All vehicles are not necessarily used in a solution but if a vehicle is used it respects its starting
and ending depots. When a vehicle is not used, the NextVar() variable corresponding to the
starting depot of this vehicle points to the ending depot of this vehicle, i.e. if you have:
int vehicle = ...;
IntVar * start_var = routing.NextVar(routing.Start(vehicle)).
Assignment * solution = routing.Solve(...);

and the vehicle vehicle is not used in this solution, then


routing.IsEnd(solution.Value(start_var));

returns true6 .
The method IsVehicleUsed() of the RoutingModel class tests exactly this.
As mentioned earlier, a depot cannot be a transit node: you can only start, finish or start and
finish a tour at a depot.

A depot cannot be a transit node.

10.5.3 The VehicleVar() variables

In the RL, there is a one to one correspondence between vehicles and routes. You proba-
bly noticed that we interchangeably used the terms route and vehicle in this manual. When
you declare v vehicles/routes in your model, the RL solver creates a model with v vehi-
cles/routes numbered from 0 to vehicles() - 1. These vehicles/routes are divided in
different VehicleClasses (see next sub-section).
The VehicleVar(int64 i) method returns the IntVar* corresponding to the node with
int64 index i: this variable indicates which vehicle services node i, i.e. if node i is serviced
by vehicle vehicle_number in a solution (with the same abuse of notation as before):
5
If not, you trigger an assert().
6
Remember that there are no NextVar() variables for end depots.

367
10.5. Multi-depots and vehicles

VehicleVar(i) == vehicle_number.
You can grab all VehicleVar() variables at once with:
const std::vector<IntVar*>& VehicleVars() const;

For a vehicle vehicle_number, the following two conditions are satisfied:


routing.VehicleVar(routing.Start(vehicle_number)) ==
vehicle_number
and
routing.VehicleVar(routing.End(vehicle_number)) ==
vehicle_number.
On the same route, all nodes are serviced by the same vehicle, i.e.:
If NextVar(i) == j then VehicleVar(i) == VehicleVar(j)
If a node i is not active, i.e. not serviced by a vehicle, VehicleVar(i) is set to -1 but
don’t rely on this to test if a node is active or not. Each node i has a corresponding BoolVar
that indicates if the node is active or not. ActiveVar(i) returns (a pointer to) this variable.
Internally, the real criterion used is to test if NextVar(i) points to itself or not. i.e. a node i
is active if
NextVar(i) != i
and inactive if
NextVar(i) == i.
Depots are always active and thus can not be part of a Disjunction. This is worth remem-
bering:

Depots are always active and thus can not be part of a Disjunction.

10.5.4 VehicleClasses

For efficiency reasons, vehicles/routes are divided in several VehicleClasses depending on


the starting and ending depot(s) and the cost to use the vehicle/route7 . The VehicleClass
is a simple struct based on these three parameters. Its constructor method signature is:
VehicleClass(RoutingModel::NodeIndex start_node,
RoutingModel::NodeIndex end_node,
const int64 cost);

This struct provides an bool Equal(const VehicleClass& vehicle1,


const VehicleClass& vehicle2) method to compare two VehicleClasses. The
following method returns all the different VehicleClasses used in the model:
7
This cost can be set by SetRouteFixedCost(int64 cost) if all vehicles have the same cost or
SetVehicleFixedCost(int vehicle, int64 cost) to set individual costs.

368
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

void GetVehicleClasses(std::vector<VehicleClass>* vehicle_classes)


const;

10.6 Partial routes and Assigments

You can find the code in the file vrp_locks.cc.

Sometimes, while searching for a good solution, you find some promising partial routes
or you may already know that some routes or partial routes should be part of a solution.
Wouldn’t it be nifty to be able to fix some parts of the solution and let the CP routing solver
assign the rest of the solution? Dream no more: this possibility is integrated in the RL and we
detail it in this section.

10.6.1 A little bit of terminology

Before we go on, let’s agree on the terminology. Our routing problems are modelled with a
graph G = (V, E ∪ A) with V the set of all vertices, E the set of edges and A the set of arcs.
Here are key terms that we will use throughout the rest of part III:
paths: We use path in the common graph theoretic sense. A path is a sequence of adjacent8
edges and/or arcs with the possibility to traverse these edges and/or arcs one after the
other9 . The first and last vertices belong to the path and are called respectively start end
end vertices.
cycles: A cycle is a path such that the start and end vertices are the same.
simple paths: A simple path is a path that doesn’t intersect itself.
simple cycles: A simple cycle is a simple path such that the start and end vertices coincide.
routes: A route is a simple path or simple cycle that connects a starting depot and an ending
depot and that is traversed by only one vehicle.
empty routes: An empty route is a pair of starting and ending depots that are assigned to the
same vehicle.
partial routes: A partial route is a simple path that is traversed by only one vehicle. The idea
is to name “parts” of contiguous edges/arcs that could be extended - in both incoming and
outgoing directions - to form a route. A route can be considered as a partial route only if
the starting and ending depots are not the same. This partial route cannot be extended at
both its end depot vertices but we still call it a partial route.
8
Two edges are adjacent if they share a common vertex.
9
We don’t distinguish between paths with only edges (paths), only arcs (directed paths) or containing edges
and arcs (mixed paths). In the same vein, we don’t distinguish between cycles with only edges (cycles), only arcs
(circuits) or containing edges and arcs (mixed cycles).

369
10.6. Partial routes and Assigments

10.6.2 Locks and the ApplyLocksToAllVehicles() method

You can find the source code in the file vrp_locks.cc.


A lock is what we call internally an std::vector<RoutingModel::NodeIndex> that
represents a partial route. Locks can be fixed (we prefer to say applied) before the search.
Basically, this means that given a lock p corresponding to a vehicle v (again with the same
abuse of notation):
NextVar(p[i]) == p[i+1] for all i and i+1 in p
and
VehicleVar(p[i]) == v for all i in p.
To apply the locks, use the ApplyLocksToAllVehicles() method:
std::vector<std::pair<RoutingModel::NodeIndex,
RoutingModel::NodeIndex> > depots(4);
// Internal depots are 1, 3, 4 and 7
// thus with the TSPLIB convention in this manual,
// the real depots are 2, 4, 5 and 8
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 routing(29, 4, depots); // 29 nodes, 4 vehicles
...
routing.CloseModel();

// Constructing partial routes


std::vector<std::vector<RoutingModel::NodeIndex> > p(3);
// first partial route
p[0].push_back(RoutingModel::NodeIndex(0));
p[0].push_back(RoutingModel::NodeIndex(2));
...
p[0].push_back(RoutingModel::NodeIndex(26));
p[0].push_back(RoutingModel::NodeIndex(7));
// second partial route
p[1].push_back(RoutingModel::NodeIndex(23));
p[1].push_back(RoutingModel::NodeIndex(18));
...
p[1].push_back(RoutingModel::NodeIndex(13));
...
if (!routing.ApplyLocksToAllVehicles(p, FLAGS_close_routes)) {
LOG(FATAL) << "Unable to apply locks...";
}

Here are a few remarks about the ApplyLocksToAllVehicles() method:


• You can only call ApplyLocksToAllVehicles() if the model is closed (or you’ll
trigger an assert()).
• Partial routes are attached to the corresponding starting depots. For instance, p[1][0]
is attached to the depot of the second route/vehicle. This means that partial routes con-

370
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

structed with the ApplyLocksToAllVehicles() method all start at a given depot.


• The bool FLAGS_close_routes indicates if the routes are closed or not. If set to
true, all the given partial routes are closed (i.e. the last vertex of each lock is connected
to the corresponding end depot) and all the remaining transit vertices are deactivated.
If set to false, the partial routes are not closed and the remaining vertices are not
deactivated (already deactivated vertices remain deactivated).
• You can only use transit nodes and each transit node can only be in one lock (no depot
allowed in the locks).
• You can add empty routes by adding an empty vector to the corresponding vehi-
cle/route. In our example, route p[2] is empty and can thus be completed by the CP
routing solver (if FLAGS_close_routes is set to false). The remaining routes
that were not defined in p are closed (i.e. NextVar(routing.Start(v)) ==
routing.End(v) for all v >= p.size()).
• You can get the corresponding Assignment with the PreAssignment() method:
const Assignment* const solution_from_locks =
routing.PreAssignment();

• Finally, ApplyLocksToAllVehicles() returns true if the all the locks could be


applied and false otherwise.

Pay close attention to all the remarks before using the


ApplyLocksToAllVehicles() method.

Now, let’s solve this instance:


const Assignment* solution = routing.Solve();

and inspect the solution:


if (solution != NULL) {
// Solution cost.
LG << "Obj value: " << solution->ObjectiveValue();
// Inspect solution.
std::string route;
for (int vehicle_nbr = 0; vehicle_nbr < 4; ++vehicle_nbr) {
route = "";
for (int64 node = routing.Start(vehicle_nbr);
!routing.IsEnd(node);
node = solution->Value(routing.NextVar(node))) {
route = StrCat(route,
StrCat(routing.IndexToNode(node).value() + 1,
" -> "));
}
route = StrCat(route,
routing.IndexToNode(
routing.End(vehicle_nbr)).value() + 1 );
LG << "Route #" << vehicle_nbr + 1 << std::endl
<< route << std::endl;
}
} else {

371
10.6. Partial routes and Assigments

LG << "No solution found.";


}

Let’s refresh our memory about the data instance before we look at the results.
The routes depots are:
• route 1: 2 and 5;
• route 2: 4 and 5;
• route 3: 4 and 8;
• route 4: 5 and 8.
The defined locks are:
• p[0]: 1 -> 3 -> 18 -> 27 -> 22;
• p[1]: 24 -> 19 -> 16 -> 14;
• p[2]:
The fact that we only applied locks for the 3 first routes while the model has 4 routes means
that the fourth route will not be used in the search.
If you set FLAGS_close_routes to true, you’ll get a partial solution that is not feasible
and the following expected result:
No solution found.

If you set FLAGS_close_routes to false, the partial solution made up by the locks is
completed by the CP routing solver:
Obj value: 804
Route #1
2 -> 1 -> 3 -> 18 -> 27 -> 22 -> 26 -> 5

Route #2
4 -> 24 -> 19 -> 16 -> 14 -> 17 -> 21 -> 25 -> 29 -> 5

Route #3
4 -> 6 -> 7 -> 9 -> 10 -> 11 -> 12 -> 13 -> 15 -> 20 -> 23 -> 28 -> 8

Route #4
5 -> 8

If you find the ApplyLocksToAllVehicles() method too restrictive for your needs, you
can always construct a partial Assignment and pass it to the CP routing solver as we will do
in the next sub-section.

372
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

Locks and online problems


Locks can be applied when you have an idea of partial routes that should be fixed in a
solution for one reason or the other. A type of problems where you often apply locks
is worth mentioning: the so-called online or dynamical problems. These problems are
dynamic in the sense that the instances gradually change over time: some parts of these
instances change with time or are only revealed over time.
For a VRP, you may think of actual drivers that are trapped in congested areas: you must
then adapt the routes to follow (to reroute) (or be prepared for some unpleasant conse-
quences). You don’t want/have the time or the resources to recompute a solution from
scratch. The partial routes not affected by congestion might still be optimal after all.
Problems where the instances are completely known - like all the problems presented in
this manual - are then coined as offline problems by contrast.

10.6.3 Assignments and partial Assignments

You can find the source code in the file vrp_IO.cc.


The RL provides several helper methods to write and read Assignments.
1. First, you have the shortcut methods:
bool WriteAssignment(const string& file_name) const;
Assignment* ReadAssignment(const string& file_name);

The first method writes the current solution to a file and the second method loads the
Assignment contained in the file as the current solution. The format used is the pro-
tocol buffer from Google10 . These two methods are shortcuts. WriteAssignment()
takes the current solution and invokes its Save() method while ReadAssignment()
invokes the Load() method of an Assignment and restores this Assignment as the
current solution with the RestoreAssignment DecisionBuilder.
To test if everything went fine, use WriteAssignment() and
ReadAssignment(). The former returns true if the Assignment was suc-
cessfully saved and false otherwise. The latter returns NULL if it could not load the
Assignment contained in the file as the current solution.
If you already have an Assignment at hand, you can restore it as the current solution
with
Assignment* RestoreAssignment(const Assignment& solution);

Again, if solution is not valid, RestoreAssignment() returns NULL.


As usual with the RestoreAssignment DecisionBuilder, you don’t
need to provide a complete Assignment. If needed, the CP solver will
complete the solution. For the RL, this DecisionBuilder will be the
10
This format is a multi-platform compatible binary format for serializing structured data. See their website or
the section Serializing where we reveal everything you always wanted to know about serializing with the or-tools
library.

373
10.6. Partial routes and Assigments

classical default DecisionBuilder on the NextVar() variables with the


CHOOSE_FIRST_UNBOUND and ASSIGN_MIN_VALUE strategies.
You can also add extra interesting IntVar variables to the routing Assignment:
void AddToAssignment(IntVar* const var);

2. Second and more interestingly, the RL provides meth-


ods to translate an Assignment into and from an
std::vector<std::vector<RoutingModel::NodeIndex> >. The
vector is a little bit peculiar as it doesn’t hold the starting and ending depots:
RoutingModel routing(); // as above
...
const Assignment* solution = routing.Solve();
...
std::vector<std::vector<RoutingModel::NodeIndex> > sol;
routing.AssignmentToRoutes(*solution, &sol);

In the file vrp_IO.cc, we print the vector and for the instance above, we obtain:
Solution saved into an std::vector of size 4
Route #1 with starting depot 2 and ending depot 5
1 -> 3 -> 18 -> 27 -> 22 -> 26

Route #2 with starting depot 4 and ending depot 5


24 -> 19 -> 16 -> 14 -> 17 -> 21 -> 25 -> 29

Route #3 with starting depot 4 and ending depot 8


6 -> 7 -> 9 -> 10 -> 11 -> 12 -> 13 -> 15 -> 20 -> 23 -> 28

Route #4 with starting depot 5 and ending depot 8

As you can see, no depot is saved into this std::vector. This is exactly the kind of
std::vector that you need to pass to RoutesToAssignment():
Assignment* const restored_sol =
routing.ReadAssignmentFromRoutes(sol, false);

This method restores the solution contained in the vector as the current solution.
In contrast to the RoutesToAssignment() method, the solution passed to
ReadAssignmentFromRoutes() must be a complete solution, i.e. all NextVar()
mandatory variables must be assigned.
We also remind the reader that in contrast to all other loading methods presented here,
RoutesToAssignment() doesn’t reconstruct a feasible solution and deals only with
NextVar() variables. If your model has many complicated side constraints (like
Dimensions with slack variables), the CP routing solver might need some time to recon-
struct a feasible solution from the NextVar() variables.

374
Chapter 10. Vehicule Routing Problems with constraints: the capacitated
vehicle routing problem

Partial Assignments and the RL


Partial Assignments in the Routing Library have nothing special and you can use partial
Assignments in a similar way with the CP solver. The RL provides several handy helper
methods that you can use in your own code. Aside from the defensive testings, these
methods are only several lines long.

10.7 Summary

375
Part IV

Technicalities
CHAPTER

ELEVEN

UTILITIES

Classes under scrutiny:

11.1 Logging

11.1.1 Logging based on severity

11.1.2 Logging in DEBUG mode only

11.1.3 Customized logging levels

11.1.4 Conditional logging

11.1.5 The logging classes

11.2 Asserting

11.3 Timing

We propose two timers: a basic timer (WallTimer) and a more advanced one
(CycleTimer). These two classes work under Windows, Linux and MacOS. The Solver
class uses by default a WallTimer internally.
Both timers are declared in the header base/timer.h.

11.3.1 Basic timer

This basic timer is defined by the WallTimer class. This class proposes the usual methods:
• void Start()
• void Stop()
11.3. Timing

• bool Reset()
• void Restart()
• bool IsRunning() const
• int64 GetInMs() const
• double Get() const
GetInMs() returns the elapsed time in milliseconds while Get() returns this time in sec-
onds.
If you need even more precise timing, use the following method:
• static int64 GetTimeInMicroSeconds()
that returns the time in microseconds.
To measure the time, we query the system time and add or subtract the queried times.

Our timers measure the wall times.

To get the time in milliseconds, we use:


System Function
Linux gettimeofday()
Windows clock()
MacOS gettimeofday()
To get the time in microseconds, we use:

System Function
Linux clock_gettime()
Windows QueryPerformanceCounter() and QueryPerformanceFrequency()
MacOS 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 finishes and the time at which the task started.

11.3.2 Advanced timer

This timer is defined by the CycleTimer class. Actually, the CycleTimer class uses... the
WallTimer class internally. More precisely, the CycleTimer class is based on the static
int64 GetTimeInMicroSeconds() method of the WallTimer class.
Its methods are
• void Reset()

380
Chapter 11. Utilities

• void Start()
• void Stop()
• int64 GetInUsec() const
• int64 GetInMs() const
GetInUsec() returns the elapsed time in microseconds and GetInMs() converts this time
in milliseconds.

11.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 Solver‘s integrated timer is never reset!

To query this timer:


Solver solver(...);
LG << solver.wall_time()
<< " ms elapsed since the creation of the solver";

11.4 Profiling

// This struct holds all parameters for the Solver object. // SolverParameters is only used by the
Solver constructor to define solving // parameters such as the trail compression or the profile
level. // Note this is for advanced users only. struct SolverParameters {
enum ProfileLevel { NO_PROFILING, NORMAL_PROFILING };
enum TraceLevel { NO_TRACE, NORMAL_TRACE }
// Support for profiling propagation. LIGHT supports only a reduced // version of the
summary. COMPLETE supports the full version of the // summary, as well as the csv
export. ProfileLevel profile_level;
// Support for full trace of propagation. TraceLevel trace_level;
DEFINE_bool(cp_trace_propagation, false, “Trace propagation events (constraint and de-
mon executions,” ” variable modifications).”);
DEFINE_bool(cp_trace_search, false, “Trace search events”); DE-
FINE_bool(cp_show_constraints, false,
“show all constraints added to the solver.”);
DEFINE_bool(cp_print_model, false, “use PrintModelVisitor on model before solving.”);

381
11.5. Debugging

DEFINE_bool(cp_model_stats, false, “use StatisticsModelVisitor on model before solv-


ing.”);
DEFINE_string(cp_export_file, “”, “Export model to file using CPModel-
Proto.”); DEFINE_bool(cp_no_solve, false, “Force failure at the beginning of
a search.”); DEFINE_string(cp_profile_file, “”, “Export profiling overview to
file.”); DEFINE_bool(cp_verbose_fail, false, “Verbose output when failing.”); DE-
FINE_bool(cp_name_variables, false, “Force all variables to have names.”); DE-
FINE_bool(cp_name_cast_variables, false,
“Name variables casted from expressions”);

11.5 Debugging

11.5.1 The DebugString() method

Naming variables

The convenient operator<<

11.6 Serializing

11.7 Visualizing

11.7.1 Visualizing the model

DebugString for small parts

ModelVisitors for the whole model

11.7.2 Visualizing the search

gflags‘s command line flags

The TreeMonitor class

11.8 Randomizing

11.9 Using FlatZinc

382
CHAPTER

TWELVE

MODELING TRICKS

Overview:

Prerequisites:

Classes under scrutiny:

Files:

You can find the code in the directory documentation/tutorials/cplusplus/chap13.


The files inside this directory are:

12.1 Different types of variables

The or-tools library offers different types of variables:


12.1. Different types of variables

384
Chapter 12. Modeling tricks

12.1.1 What if my domain is finite but non-contiguous?

Domains with non integer values

Domains with non contiguous integer values

12.1.2 What if my domain is non-finite?

12.2 Efficiency

12.2.1 Keep variables ordered

12.2.2 Know the order of constraint propagation

12.3 False friends and counter-intuitive ideas

12.3.1 Accepted solutions vs feasible solutions

12.3.2 Composite objects

12.3.3 Solve() vs SolveAndCommit()

12.3.4 LocalOptimumReached() vs LocalOptimum()

12.3.5 DebugString() doesn’t give the value of a solution

12.3.6 Solve() vs the StartSearch() - NextSolution() -


EndSearch() mechanism

12.4 What are my solving options?

12.4.1 The search mechanism

12.4.2 Global methods

Solve()

SolveAndCommit

MakeNestedOptimize()

12.4.3 DecisionBuilders
385
SolveOnce
13.1. Main files and directories

CHAPTER

THIRTEEN

UNDER THE HOOD

13.1 Main files and directories

13.2 Naming conventions and programming idioms

13.2.1 Naming conventions

General naming conventions

Methods

13.2.2 Programming idioms

Factories

Caches

Callbacks

Visitors

Listeners

13.3 Main classes, structures and typedefs

13.3.1 BaseObjects

13.3.2 PropagationBaseObjects

13.3.3 Callbacks

NewPermanentCallback()

388
13.4 The Trail struct
Chapter 13. 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 virtual void BeginNextDeci-
sion(DecisionBuilder* const b);
// 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 finished. 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 Periodic-
Check();
// 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;

389
13.9. Local Search (LS)

// Registers itself on the solver such that it gets notified of the search // and propa-
gation events. virtual void Install();

13.9 Local Search (LS)

13.10 Meta-heuristics and SearchMonitors

13.10.1 The Metaheuristic class

13.10.2 Callbacks to implement

13.11 The Routing Library (RL)

You can find the code in the file rl_auxiliary_graph.cc.

Each node has a unique identifier 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 defined or accessible within the RoutingModel
class. To use this class, include the mandatory constraint_solver/routing.h
header.

13.11.1 Global constants

Some global constant basic paratemers of the model are:

Variables (pu/pr) Descriptions Queries


solver_ (pr) CP Solver. Solver* solver()
const
nodes_ (pr) Total number of nodes. int nodes() const
vehicles_ (pr) Total number of vehicles. int vehicles()
const
start_end_count_ Total number of different (starting and None
(pr) ending) depots.
kUnassigned (pu) static const int = -1 kUnassigned
kNoPenalty (pu) static const int = -1 kNoPenalty
RoutingModel:: RoutingModel:: NodeIndex(0) RoutingModel::
kFirstNode (pu) kFirstNode
RoutingModel:: RoutingModel:: NodeIndex(-1) RoutingModel::
kInvalidNodeIndex kInvalidNodeIndex
(pu)
Size() (pu) Number of IntVar variables. Size()

390
Chapter 13. Under the hood

(pu) stands for public and (pr) for private. The int64 Size() const method re-
turns nodes_ + vehicles_ - start_end_count_, which is exactly the minimal num-
ber of variables needed to model the problem at hand with one variable per node (see next
subsection). kUnassigned is used for unassigned indices.

13.11.2 The auxiliary graph

You can find the source code in the file rl_auxiliary_graph.cc.


The auxiliary graph is a graph constructed from the original graph. Let’s examine the original
graph of the next figure:
1 0 4
1
0
2
8
5
3 6 7
1
0
0
1
Starting depot
1
0
0Ending depot
1
01 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 figure:

391
13.11. The Routing Library (RL)

1 0 2 1
0
0
14
0
1 8
5 0
1
0
1
3 6 71
0
1
00
1
01
10

Starting depot
1Ending depot
0
0
1
1
0 Starting and ending depot
Transit node
Node 1 is not duplicated because there is only one route (route 0) that starts from 1. Node 3
is duplicated once because there are two routes (routes 1 and 2) that start from 3. Node 7 is
duplicated once because two routes (routes 2 and 3) end at 7 and finally there are two copies of
node 4 because two routes (routes 0 and 4) end at 4 and one route (route 3) starts from 4.
The number of variables is:

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 don’t 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.

392
Chapter 13. 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 0 2 1
0
9
0
1
0 4
1 7
5 10 1
0
0
1
3 6 11 1
0
0
1
0
1
01
1 0
12
8
Starting depot
1
0
0Ending depot
1
01 Starting and ending depot
Transit node
If you set the FLAGS_log_level to 2 and skip the log prefix:
./rl_auxiliary_graph --log_level=2 --log_prefix=false

you get:
Number of nodes: 9
Number of vehicles: 4
Variable index 0 -> Node index 0
Variable index 1 -> Node index 1
Variable index 2 -> Node index 2
Variable index 3 -> Node index 3
Variable index 4 -> Node index 4
Variable index 5 -> Node index 5
Variable index 6 -> Node index 6
Variable index 7 -> Node index 8
Variable index 8 -> Node index 3
Variable index 9 -> Node index 4
Variable index 10 -> Node index 4
Variable index 11 -> Node index 7
Variable index 12 -> Node index 7
Node index 0 -> Variable index 0
Node index 1 -> Variable index 1
Node index 2 -> Variable index 2

393
13.11. The Routing Library (RL)

Node index 3 -> Variable index 3


Node index 4 -> Variable index 4
Node index 5 -> Variable index 5
Node index 6 -> Variable index 6
Node index 7 -> Variable index -1
Node index 8 -> Variable index 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 doesn’t have a corresponding int64 index (-1 means exactly that) and that
NodeIndex 8 corresponds to int64 7 (not 8!).
Here is one possible solution:
1 0 2 1
04
0
1
0
1 8
5 0
1
0
1
6 71
0
1
00
1
3 01
10

Starting depot
0
1
0Ending depot
1
01Starting and ending depot
Transit node
We output the routes, first with the NodeIndexes and then with the internal int64 indices
with:
for (int p = 0; p < VRP.vehicles(); ++p) {
LG << "Route: " << p;
string route;
string index_route;
for (int64 index = VRP.Start(p); !VRP.IsEnd(index); index =
Solution->Value(VRP.NextVar(index))) {
route = StrCat(route,
StrCat(VRP.IndexToNode(index).value(), " -> "));
index_route = StrCat(index_route, StrCat(index, " -> "));
}
route = StrCat(route, VRP.IndexToNode(VRP.End(p)).value());
index_route = StrCat(index_route, VRP.End(p));
LG << route;
LG << index_route;
}

and get:
Route: 0
1 -> 0 -> 2 -> 4
1 -> 0 -> 2 -> 9
Route: 1
3 -> 5 -> 4
3 -> 5 -> 10
Route: 2

394
Chapter 13. Under the hood

3 -> 6 -> 7
8 -> 6 -> 11
Route: 3
4 -> 8 -> 7
4 -> 7 -> 12

Some remarks

• NodeIndex and int64 indices don’t 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();
}

13.11.3 Variables

Path variables

Dimension variables

13.11.4 Constraints

NoCycle constraint

13.12 Summary

395
Part V

Appendices
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.
[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 In-
teger Linear Programs, Annals of Operations Research, 86, pp. 3-24, 1997.
[Gasarch2002] 23. (a) Gasarch. The P=?NP poll, SIGACT News 33 (2), pp 34–47, 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.
[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 Artificial intelligence, Acapulco,
Mexico, pp 245-250, 2003, Morgan Kaufmann Publishers Inc.
[Meyer-Papakonstantinou] Christophe Meyer and Periklis A. Papakonstantinou. On the com-
plexity of constructing Golomb Rulers, Discrete Applied Mathematics, 57, pp 738–748,
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, 3rd International Workshop
on the Integration of AI and OR Techniques in Constraint Programming for Combinatorial
Optimization Problems (CPAIOR01), 2001.
[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.
Bibliography

[Hoffman1969] Hoffman, Loessi and Moore. Constructions for the Solution of the m Queens
Problem, Mathematics Magazine, p. 66-72, 1969.
[Jordan2009] Jordan and Brett. A survey of known results and research areas for n-queens,
Discrete Mathematics, Volume 309, Issue 1, 2009, pp 1-31.
[Garey1976] Garey, M. R., Johnson, D. S. and Sethi, R., The complexity of flowshop and
jobshop scheduling, Mathematics of Operations Research, volume 1, pp 117-129, 1976.
[Kis2002] Kis, T., On the complexity of non-preemptive shop scheduling with two jobs, Com-
puting, volume 69, nbr 1, pp 37-49, 2002.
[Taillard1993] Taillard, E., 1993. Benchmarks for basic scheduling problems, European Jour-
nal of Operational Research, Elsevier, vol. 64(2), pages 278-285, January.
[Adams1988] J. Adams, E. Balas, D. Zawack, The shifting bottleneck procedure for job shop
scheduling. Management Science, 34, pp 391-401, 1988.
[Philippe2001] 2. Philippe, C. Le Pape, and W. Nuijten. Constraint-based scheduling: ap-
plying constraint programming to scheduling problems. Vol. 39. Springer, 2001.
[Christofides1976] Christofides, Nicos. Worst-case analysis of a new heuristic for the travel-
ling salesman problem, Technical Report, Carnegie Mellon University, 388, 1976.
[Luby1993] 13. Luby, A. Sinclair and D. Zuckerman, Optimal speedup of Las Vegas algo-
rithms, Information Processing Letters, Volume 47, Issue 4, 1993, pp 173-180.
[Gomes1998] C. P. Gomes, B. Selman, and H. Kautz, Boosting Combinatorial Search Through
Randomization, presented at National Conference on Artificial Intelligence (AAAI), Madi-
son, WI, 1998.
[Glover1997] 6. Glover and M. Laguna. Tabu Search, Kluwer Academic Publishers, 1997.
[Gendreau2005] M. Gendreau and J.-Y. Potvin. Tabu search. In E. K. Burke and G. Kendall,
editors, Search Methodologies: Introductory Tutorials in Optimization and Decision Sup-
port Techniques. Springer-Verlag, 2005
[Kirkpatrick1983] S. Kirkpatrick, C. D. Gelatt Jr., M. P. Vecchi. Optimization by Simulated
Annealing. Science. New Series, Vol. 220, No. 4598, 1983, pp. 671-680.
[Granville1994] 22. Granville, M. Krivanek, J.-P. Rasson. Simulated annealing: A proof of
convergence. IEEE Transactions on Pattern Analysis and Machine Intelligence 16 (6),
pp 652–656, 1994.
[Michiels2007] W. Michiels, E. Aarts and J. Korst, Chapter 8: Asymptotic Convergence of
Simulated Annealing in Theoretical Aspects of Local Search, Monographs in Theoretical
Computer Science, Springer, 2007
[Shaw1998] P. Shaw. Using constraint programming and local search methods to solve vehicle
routing problems, Fourth International Conference on Principles and Practice of Constraint
Programming, v 1520, Lecture Notes in Computer Science, pp 417–431, 1998.
[refalo2004] P. Refalo, Impact-Based Search Strategies for Constraint Programming in Prin-
ciples and Practice of Constraint Programming – CP 2004, Lecture Notes in Computer
Science, Springer 2004, pp 557-571.

400
Bibliography

[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 Schedul-
ing: What’s the difference?, Proc. of the 13th International Conference on Automated Plan-
ning and Scheduling, 2003, pages 267–276.
[Savelsbergh1985] M.W.P. Savelsbergh. Local search in routing problems with time windows,
Annals of Operations Research 4, 285–305, 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. Günlük, 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.
[Dantzig1959] 7. (a) Dantzig and J. H. Ramser. The Truck Dispatching Problem, Manage-
ment Science v. 6, pp 80-91, 1959.
[Bektas2006] T. Bektas. The multiple traveling salesman problem: an overview of formula-
tions and solution procedures, Omega, Elsevier, v. 34(3), pp 209-219, 2006.
[Laporte1992] G. Laporte. The vehicle routing problem: An overview of exact and approximate
algorithms, European Journal of Operational Research, v. 59(3), pp 345-358, 1992.
[Roberti2012] R. Roberti. Exact algorithms for different classes of vehicle routing problems,
PhD Thesis, University of Bologna, 2012.
[Groër2011] C. Groër, B. Golden and E Wasil. A Parallel Algorithm for the Vehicle Routing
Problem, INFORMS Journal on Computing, v. 23(2), pp. 315-330, 2011.
[Toth2008] P. Toth and A. Tramontani. An integer linear programming local search for ca-
pacitated vehicle routing problems, in The Vehicle Routing Problem: Latest Advances and
New Challenges, Springer US, pp. 275-295, 2008.

401
INDEX

Symbols domain, 15
–cp_model_stats, 60
–cp_no_solve, 59
E
–cp_print_model, 59 efficient reduction, 18
–cp_show_constraints, 59 empty route, 369
–help, 48, 59 EndSearch(), 43
–helpmatch=S, 48 F
–helpon=FILE, 48
factory method, 38
–helpshort, 48
feasible solution, 17
A first fail principle, 139
AddConstraint(), 41 G
Assignment, 43
gflags, 47
B parameters read from a file, 324
replacement (rout-
best success principle, 139
ing.SetCommandLineOption()),
C 298
shortcuts, 48
clause, 144
types, 47
complexity theory, 18
Golomb Ruler
constraint, 15
Problem, 51
AllDifferent, 35
Golomb ruler, 53
Constraint Optimization Problem, 16
graph
Constraint Satisfaction Problem, 15
cycle, 369
cpviz, 97
empty route, 369
cryptarithmetic
partial route, 369
puzzles, 33
path, 369
cycle, 369
route, 369
D simple cycle, 369
simple path, 369
DebugString(), 58
DecisionBuilder, 41 I
DEFINE_bool, 47
instance of a problem, 17
DEFINE_double, 47
IntExpr, 39
DEFINE_int32, 47
intractable problems, 18
DEFINE_int64, 47
IntVar, 38
DEFINE_string, 47
DEFINE_uint64, 47
Index

M R
MakeAllDifferent(), 41 real problem, 16
MakeAllSolutionCollector(), 43 route, 369
MakeBestValueSolutionCollector(), 43
MakeDifference(), 66 S
MakeEquality(), 41 SearchLimit, 62
MakeFirstSolutionCollector(), 43 in Local Search, 188
MakeIntConst(), 64 specialized for time, 49
MakeIntVar(), 38 SearchMonitor
MakeIntVarArray(), 57 as SolutionCollector, 43
MakeLastSolutionCollector(), 43 as Solver’s parameters, 49
MakeLessOrEqual(), 69 callbacks, 90
MakeMinimize, 57 SetCommandLineOption(), 298
MakeNonEquality(), 66 simple cycle, 369
MakePhase(), 41 simple path, 369
MakeProd(), 39 solution, 17
MakeScalProd(), 40 SolutionCollector, 43
MakeSum(), 39 AllSolutionCollector, 43
MakeTimeLimit(), 48 BestValueSolutionCollector, 43
mathematical problem, 16 FirstSolutionCollector, 43
LastSolutionCollector, 43
N Solve(), 44
n-queens problem, 78 Solver
namespace creation, 38
operations_research, 37 parameters, 48
NewSearch(), 42 SolverParameters, 48
NextSolution(), 42 SolverParameters(), 48
NP, 18 StringPrintf(), 57
NP-Complete, 18
NP-Hard, 18 T
time
O wall_time(), 61
objective function, 16
objective functions, 51, 52 V
objective solution, 17 variable, 15
OptimizeVar, 57 variables
IntVar, 38
P
P, 18 W
ParseCommandLineFlags(), 47 wall_time()
partial route, 369 time, 61
path, 369
Problem
Cryptarithmetic puzzles, 33
Golomb Ruler, 51
puzzles
cryptarithmetic, 33

404

Das könnte Ihnen auch gefallen