or-tools open source library
User’s Manual Nikolaj van Omme Laurent Perron Vincent Furnon
June 21, 2013 (A4 version)
© Copyright 2012-2013, Google
Welcome to the or-tools user’s manual!
© Copyright 2012-2013, Google
License information This document is provided under the terms of the
Apac Ap ache he Li Licen cense se 2. 2.0 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 document available available on the Internet. This document should only be available 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 GOOGLE is a trademark of Google Inc. Linux is a registered registered trademark of Linus Torvald Torvald in the United States, other countries, or both. Java Java and all Java-based Java-based trademarks trademarks and logos are trademarks trademarks of Sun Microsystem Inc. in the United States, other countries, or both. Other companies, companies, products, or service names may be trademarks 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 with the help of our Constrain Constraintt Progra Programmi mming ng (CP) solver. solver. If you do know what operations difficult it is sometimes sometimes to find efficient, efficient, easy to use and open source code, research is and how difficult 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 always open to suggesti suggestions. ons. See the section section How to reach us? below below. If you have have comments about this manual or the documentation in general, see the section Do you have comments?.
What is or-tools? operations r esearch tools written in C++ at Google. The or-tools library is a set of o
The main tools are: • A Constraint Constraint Programming Programming solver. solver. • A simple and unified interface to several several linear programming and mixed integer programming solvers (GLPK, CLP, CBC and SCIP). • Knapsack Knapsack algorithms algorithms.. • Graph algorithms (shortest paths, min cost flow, flow, max flow, flow, linear sum assignment). • FlatZinc support.
1
If you are curious: Wikipedia curious: Wikipedia article on Operations research. research.
In short, the or-tools library is: • Open source and free Everyth Everything, ing, includin including g the exampl examples, es, the implem implementa entation tionss of 2 the algorithms, algorithms, the various various documentations documentations , is licenced under the Apache Apache License License 2.0 and is available for download. If you make substantial improvements to our code, please share it with the whole community. community. • Alive The library is actively maintained and updates and improvements are made on an almost daily basis. • Documented OK, we just started to write the documentation but there are already numerous examples written in C++, Python, Java and C#! • Portable Because it is made by Google, the code conforms strictly to the Google coding styles3 . The code is known to compile on: – gcc 4.4.x on ubuntu 10.04 and up (10.10, 11.04, 11.10 and 12.04). 12.04). – xcode >= 3.2.3 on Mac OS X Snow 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 W Wee try to make our code as easy to use as possible possible (especially inPython and C#). Of course, course, there is a (small) (small) learni learning ng curve to use our library 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 manual by yourself. 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 section 1.8.. 2
The source code and the scripts used to generate the documentation will be available soon. See for instance http://google-styleguid http://google-styleguide.googlecode.com/svn e.googlecode.com/svn/trunk/cppguide.xml /trunk/cppguide.xml for for the Google C++ Style Guide. 3
iv
What you will not learn in this document This document is by no means a tutorial on Operations Research nor on Contraint Programming. It is also NOT a reference manual (refer to the documentation the documentation hub to hub to find the reference the reference manual). 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 the reference manual and manual and find the right method, parameter, function, . . . or code code them yourselv yourselves! es! We don’t document document the non Constraint Programming Programming (CP) part of the library. 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 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 n or C# . This This could possib possibly ly change change in the future. future. The tutorial tutorial examples examples (see below) 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 chapter stands stands on its own. own. The best way way to read this this manual is to look look for a specific 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. knowledge. For each chapter chapter,, we list those prerequisites. prerequisites. This non-linear non-linear way of reading 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 (Basics) is an introduction on how to use the CP solver to solve small problems. problems. For real problems, 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, library, the third part is for you Routing ( ). Finally, some utilities and tricks are described in the last part (Technicalities ).
Targeted audience This This manual manual is writte written n with with two types types of readers readers in mind. First, First, someone someone who is not familfamiliar with Constraint Programming Programming nor is she a professional professional programmer programmer.. Second, an educated reader who masters Constraint Programming and is quite at ease without necessarily mastering one of the supported computer languages. 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? profiles? You tell us!
v
Conventions used in this manual All All the the code code is syste systema matic tical ally ly writt written en in monospac Functi tion on and and metho method’ d’ss monospace e font. Func names names are followed followed by parenthese parentheses. s. The method 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 necessary code or code lines and replace them by ". ". . . ".
In this this exam xample, ple, the the para param meter eterss of the the func functi tion on MakeBaseLine2() are strip stripped ped as are are the the cont conten entt of this this metho ethod d and and the the code code line liness that that foll follo ow the the defin definit itio ion n of this this func functi tion on.. The The purp purpos osee of this this exam exampl plee is to sho show that that the the code code is writ writte ten n insi inside de the namespace operations_research . All commands are issued from a Unix-like terminal: 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 complete examples on the documentation 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 .
Lab sessions Theory is good but useless without practice and experience. For each chapter, we provide exercises. Most of them are practical and consist in completing some C++ code. Even if you don’t (like to) code in C++, these lab sessions are helpful as we develop some concepts seen in the manual more in details. Exercises vary between simple and straightforward to sometimes really challenging. In the latter case, we mark these exercises as such. For all the exercises, we provide solutions. You can find (soon!) the exercises and their solutions on the documentation hub:
http://or-tools.googlecode.com/svn/trunk/documentation/ documentation_hub.html#lab_sessions or under the following directory of the or-tools library:
documentation/labs/C++
How to reach us? The whole project or-tools is hosted on Google code:
http://code.google.com/p/or-tools/ You can follow us on Google+:
https://plus.google.com/u/0/108010024297451468877/posts vii
and post your questions, suggestions, remarks, . . . to the or-tools discussion group:
http://groups.google.com/group/or-tools-discuss
How to reference this document? Use this simple reference: N. van Omme, L. Perron and V. Furnon, or-tools user’s manual, Google, 2013. Here is a bibtex entry:
@TECHREPORT{or-tools-user-manual, author = Nikolaj van Omme and Laurent Perron and Vincent Furnon, title = or-tools user’s manual, institution = Google, year = 2013 }
Do you have comments? If you have comments, suggestions, corrections, feedback, . . . , about this document or about the documentation of the or-tools library in general, please send them to
[email protected]. Thank you very much. Happy reading! The or-tools team
i
CONTENTS
Foreword
iii
I
Basics
1
1
Introduction to constraint programming 1.1 The 4-queens problem . . . . . . . . . . . . . . . . 1.2 What is constraint programming? . . . . . . . . . . 1.3 A little bit of theory . . . . . . . . . . . . . . . . . 1.4 Real examples . . . . . . . . . . . . . . . . . . . . 1.5 The three-stage method: describe, model and solve . 1.6 It’s always a matter of tradeoffs . . . . . . . . . . . 1.7 The Google or-tools library . . . . . . . . . . . . . 1.8 The content of the manual . . . . . . . . . . . . . .
2
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
First steps with or-tools: cryptarithmetic puzzles 2.1 Getting started . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.2 The cryptarithmetic puzzle problem and a first model . . . . . . . . 2.3 Anatomy of a basic C++ code . . . . . . . . . . . . . . . . . . . . 2.4 SolutionCollector s and Assignments to collect solutions 2.5 Parameters . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 2.6 Other supported languages . . . . . . . . . . . . . . . . . . . . . . 2.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . .
3
Using objectives in constraint programming: the Golomb ruler problem 3.1 Objective functions and how to compare search strategies . . . . . . . 3.2 The Golomb ruler problem and a first model . . . . . . . . . . . . . . 3.3 An implementation of the first model . . . . . . . . . . . . . . . . . . 3.4 What model did I pass to the solver? . . . . . . . . . . . . . . . . . . . 3.5 Some global statistics about the search and how to limit the search. . . 3.6 A second model and its implementation . . . . . . . . . . . . . . . . . 3.7 A third model and its implementation . . . . . . . . . . . . . . . . . . 3.8 How to tighten the model? . . . . . . . . . . . . . . . . . . . . . . . . 3.9 How does the solver optimize? . . . . . . . . . . . . . . . . . . . . . . 3.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
Reification
. . . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . .
. . . . . . .
. . . . . . . .
3 3 9 12 18 21 22 23 23
. . . . . . .
25 26 31 33 40 44 46 47
49 . . . . 50 . . . . 51 . . . . 54 . . . . 56 . . . . 59 . . . . 60 . . . . 64 . . . . 64 . . . . 68 . . . . 68 69
4.1
II 5
6
7
8
What is reification? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Customization Defining search primitives: the n-queens problem 5.1 The n-queens problem . . . . . . . . . . . . . . . . . . 5.2 Implementation of the basic model . . . . . . . . . . . 5.3 Basic working of the solver: the search algorithm . . . . 5.4 cpviz: how to visualize the search . . . . . . . . . . . . 5.5 Basic working of the solver: the phases . . . . . . . . . 5.6 Out of the box variables and values selection primitives 5.7 Customized search primitives . . . . . . . . . . . . . . 5.8 Breaking symmetries with SymmetryBreaker s . . . 5.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . .
69
71 . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . .
Local search: the job-shop problem 6.1 The job-shop problem, the disjunctive model and benchmark data 6.2 An implementation of the disjunctive model . . . . . . . . . . . 6.3 Scheduling in or-tools . . . . . . . . . . . . . . . . . . . . . . . 6.4 What is local search (LS)? . . . . . . . . . . . . . . . . . . . . . 6.5 Basic working of the solver: Local Search . . . . . . . . . . . . . 6.6 Local Search Operators . . . . . . . . . . . . . . . . . . . . . . 6.7 The jobshop problem: and now with local search!. . . . . . . . . 6.8 Filtering . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6.9 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Meta-heuristics: several previous problems 7.1 Meta-heuristics in or-tools . . . . . . . . . . . . . . . . . 7.2 Search limits and SearchLimit s . . . . . . . . . . . . 7.3 Large neighborhood search (LNS): the job-shop problem . 7.4 Restarting the search . . . . . . . . . . . . . . . . . . . . 7.5 Tabu search (TS) . . . . . . . . . . . . . . . . . . . . . . 7.6 Simulated annealing (SA) . . . . . . . . . . . . . . . . . 7.7 Guided local search (GLS) . . . . . . . . . . . . . . . . . 7.8 Variable Neigborhood Search (VNS) . . . . . . . . . . . 7.9 Default search . . . . . . . . . . . . . . . . . . . . . . . 7.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
Custom constraints: the alldifferent_except_0 constraint 8.1 Basic working of the solver: constraints . . . . . . . . . . . . . . . . . . 8.2 Consistency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8.3 The AllDifferent constraint . . . . . . . . . . . . . . . . . . . . . 8.4 Changing dynamically the improvement step with a SearchMonitor . 8.5 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . .
. . . . . . . . .
. . . . . . . . .
. . . . . . . . . .
. . . . .
. . . . . . . . .
73 74 77 81 92 114 123 126 126 132
. . . . . . . . .
133 135 143 148 161 167 182 190 202 207
. . . . . . . . . .
209 211 211 211 211 211 211 211 211 211 211
. . . . .
213 213 213 213 213 213
III 9
Routing
215
Travelling Salesman Problems with constraints: the TSP with time windows 9.1 A whole zoo of Routing Problems . . . . . . . . . . . . . . . . . . . . . . 9.2 The Routing Library (RL) in a nutshell . . . . . . . . . . . . . . . . . . . 9.3 The Travelling Salesman Problem (TSP) . . . . . . . . . . . . . . . . . . 9.4 The model behind the scenes: the main decision variables . . . . . . . . . 9.5 The model behind the scenes: overview . . . . . . . . . . . . . . . . . . . 9.6 The TSP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.7 The two phases approach . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.8 The Travelling Salesman Problem with Time Windows (TSPTW) . . . . . 9.9 The TSPTW in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . 9.10 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . .
. . . . . . . . . .
217 219 223 226 232 238 248 255 256 266 270
10 Vehicule Routing Problems with constraints: the capacitated vehicle routing problem 271 10.1 The Vehicle Routing Problem (VRP) . . . . . . . . . . . . . . . . . . . . . . 271 10.2 The VRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 272 10.3 The Capacitated Vehicle Routing Problem (CVRP) . . . . . . . . . . . . . . . 272 10.4 The CVRP in or-tools . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 273 10.5 Multi-depots and vehicles . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274 10.6 Partial routes and Assigments . . . . . . . . . . . . . . . . . . . . . . . . . . 274 10.7 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 274 11 Arc Routing Problems with constraints: the Cumulative Chinese Postman Problem 275 11.1 The Chinese Postman Problem (CPP) . . . . . . . . . . . . . . . . . . . . . . 276 11.2 The Cumulative Chinese Postman Problem (CCPP) . . . . . . . . . . . . . . . 276 11.3 A first implementation for the CCPP . . . . . . . . . . . . . . . . . . . . . . . 276 11.4 Disjunctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 11.5 A second implementation for the CCPP . . . . . . . . . . . . . . . . . . . . . 276 11.6 Partial routes and locks . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 11.7 Lower bounds . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276 11.8 Summary . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 276
IV
Technicalities
12 Utilities 12.1 Logging . . . . 12.2 Asserting . . . 12.3 Timing . . . . 12.4 Profiling . . . 12.5 Debugging . . 12.6 Serializing . . 12.7 Visualizing . . 12.8 Randomizing . 13 Modeling tricks
277 . . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
. . . . . . . .
279 279 280 281 283 283 283 283 283 285
13.1 Efficiency . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 286 13.2 False friends and counter-intuitive ideas . . . . . . . . . . . . . . . . . . . . . 286 13.3 What are my solving options? . . . . . . . . . . . . . . . . . . . . . . . . . . 286 14 Under the hood 14.1 Main files and directories . . . . . . . . . . . 14.2 Naming conventions and programming idioms 14.3 Main classes, structures and typedefs . . . . . 14.4 The Trail struct . . . . . . . . . . . . . . . 14.5 The Search class . . . . . . . . . . . . . . . 14.6 The Queue class . . . . . . . . . . . . . . . . 14.7 Variables and Assignments . . . . . . . . . 14.8 SearchMonitor s . . . . . . . . . . . . . . 14.9 Local Search (LS) . . . . . . . . . . . . . . . 14.10 Meta-heuristics and SearchMonitor s . . . 14.11 The Routing Library (RL) . . . . . . . . . . . 14.12 Summary . . . . . . . . . . . . . . . . . . . .
V
Apprendices
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
. . . . . . . . . . . .
287 288 288 288 288 288 288 288 288 290 290 290 295
297
Bibliography
299
Index
301
Part I Basics
CHAPTER
ONE
INTRODUCTION TO CONSTRAINT PROGRAMMING
In this chapter, we introduce Constraint Programming (CP) and the or-tools library and its core principles. We also present the content of this manual. Overview:
The chapter is divided in three parts. First, we introduce Constraint Programming by looking at a solving process done by our CP solver. Along the way, we will try to define Constraint Programming and show some practical problems where CP stands out. A little bit of theory will lay the foundations for the whole manual. Second, we introduce a simple strategy (the three-stage method ) that can help us when confronted with a problem to solve. This method will be applied repeatedly in this manual. Another recurrent idea in this manual is to be aware of tradeoffs. This idea is the key to successful optimization and well worth a whole section. Finally, we outline the general principles of the library and detail the content of this manual. Prerequisites:
• None. Being open minded, relaxed and prepared to enjoy the or-tools library helps though.
1.1 The 4-queens problem We present a well-known problem among Constraint Programming practitioners: the 4-queens problem. We shall encounter this problem again and generalize it in Chapter 5.
1.1.1 The problem The 4-queens problem 1 consists in placing four queens on a 4 x 4 chessboard so that no two queens can capture each other. That is, no two queens are allowed to be placed on the same row, the same column or the same diagonal. 1
See section 5.1 for a more precise definition of this problem.
1.1. The 4-queens problem
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 to a mathematical model with variables and constraints. Variables represent decisions and constraints restraint the variables of taking arbitrary values altogether. For instance, to model the 4-queens problem, we could use a binary variable 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. We also need to constraint the need for 4 queens. We could add the constraint:
�
xij = 4.
(i,j )∈ squares
This constraints ensure that we place 4 queens on the chessboard. In general, constraints only permit possible combinations of values of variables corresponding to real solutions2 . In the next section, we will see how the or-tools’ CP solver tries to solve this problem. More precisely, how the solver will try to solve the model we will develop and explain in sections 5.1 and 5.23 .
1.1.3 Propagation and search Constrainst Programming solvers are mainly based on two concepts4 : • propagation5 : variables can have different values but the solver must remove some of those values to keep all the variables values compatible with the model. In Constraint Programming, clever algorithms are devised to remove those values in an efficient manner. These algorithms propagate the current state of the solver and removes incompatible or undesirable values. 2
Things are a little bit more complex than that but let’s keep it simple for the moment. See subsection 1.3.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
• 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 grey crosses on the following Figure. One constraint tells the solver that there cannot be another queen on the same diagonal with a negative slope (the diagonals going down then right). The red crosses show this impossibility.
One constraint tells the solver that no two queens can be on the same row, hence the next red crosses.
After this first step, only the white squares are still available to place the three remaining queens. The process of excluding some squares is what is called propagation . The second step starts with the solver trying to place a second queen. It does so in the first available square from above 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 grey crosses in the next Figure. The propagation is as follow: The same negative diagonal constraint as in step 1 tells the solver that no queen can be on the negative diagonal of the second queen, hence the red cross. 6
You can find this search process detailed in sections 5.2 and 5.4.
5
1.1. The 4-queens problem
Another constraint for the diagonals with positive slopes (diagonals going up then right) tells the solver that no queen can be placed on the positive diagonal of second queen, hence the red cross.
Now, we have a failure as there is no possibility to place a third queen in the third column: there simply can not be a solution with this configuration. The solver has to backtrack! The solver decides to challenge its last decision to place the second queen in the third row from above and places it in the fourth row. The propagation is as follow: First, the square with the red cross is removed because of the positive diagonal constraint. This leaves only one possibility to place a queen in the fourth column.
The “no two queen on the same row” constraint removes one more square in the third column, leaving only one square to place the last remaining queen.
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 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 feasibility (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.4 for a description of some of its achievements). The problems CP deals (mostly) with are called Constraint Satisfaction Problems (CSP). Roughly, a CSP is a mathematical model with constraints and the goal is to 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 constraints (forcing them to be linear, use of big M for disjunctions, replacing one constraints by a bunch of linear constraints, relying on the direction of optimization (minimizing or maximizing), etc.). None of this happens in CP because constraints can be any constraints. They even 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 contraints is the AllDifferent constraint. This constraint 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 t 0 , t 1 and t 2 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 programming for instance:
ti −
2
∑ jλ ∑ λ ∑ λ j =0
2 j =0 2 i=0
ij ij ij
= 0 ∀i = 1 ∀i 1 ∀ j
To model the AllDifferent(t0 , . . . , tn−1 ) constraint8 with t i ∈ [0, n − 1] , we already need n 2 auxiliary variables λij :
λij =
{1
if ti takes value j 0 otherwise
and 3n linear equations! Of course if AllDifferent(t0 , t1 , t2 ) was being replaced by its linear integer programming translation 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 9 10
10
In some special cases, we are able to model the AllDifferent constraint in a more efficient manner. Propagation is done globally on all involved variables but the propagation is done constraint by constraint. Basically, you only need to be able to propagate (hopefully efficiently) your constraints.
Chapter 1. Introduction to constraint programming One of the curiosities of CP is its ability to deal with meta-constraints: constraints on constraints! Take for instance the Element constraint. Let [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 = x y . 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 heterogeneous 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 implemented in or-tools) is a systematic search algorithm: it systematically generates all possible assignments one by one11 , trying to extend a partial solution toward a complete solution. If it 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 16 2 = 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 itself 13 . 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 See the section Basic working of the solver: the search algorithm for more details. See next section for more. 13 In section Model, we will see a model with a search space of size 16 for the 4-queens problem. 14 Determining the exact (or even approximate) search space size is very often a (very) 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 12
11
1.3. A little bit of theory space, we want to visit solutions where x21 = 1 but also solutions where x 21 = 0. This choice is called branching . Most systematic search algorithms are called branch-and-something: • branch and bound; • branch and prune; • branch and cut; • branch and price; • ... In Constraint Programming, we use Branch and prune where pruning is another term for propagation. You can also combine different techniques. For instance branch, price and cut . CP for the MIP practitionersa
There are strong similarities between the two basic search algorithms used to solve an MIP and a CSP. MIP Branch and bound Bound:
• Relax constraints
• Propagate constraints
• Reduce gap
• Reduce variable domains
Goal: Optimality View: Objective oriented a
CSP Branch and prune Prune:
Goal: Feasibility View: Domain oriented
This is an aside for our MIP (Mix Integer Programming) colleagues. It’s full of jargon on purpose.
1.3 A little bit of theory [MUST BE REREAD] We give you in a nutshell the important ideas and the vocabulary we use throughout this manual. In section 1.3.1, we cover the basic vocabulary to talk about the problems we solve in CP. Section 1.3.4 introduces informally complexity theory. One of the difficulties of this theory is that there are a lot of technical details no to be missed. We have tried in this section to introduce you to important ideas without being drawn into too many details (some inescapable details are in the footnotes). Complexity theory is quite new (it really started in the ‘70s) and is not easy (and after reading this section, you’ll have more questions than answers). If you are allergic to theory, read the next two subsections but skip the rest. We are convinced - we took the time to write it, right? - that you would benefit from reading this section in its entirety but it is up to you.
12
Chapter 1. Introduction to constraint programming
1.3.1 Constraint Satisfaction Problems (CSP) and Constraint Optimization Problems (COP) We illustrate the different components of a Constraint Satisfaction Problem with the 4-queens problem we saw in section 1.1. A CSP consists of • a set of variables X = {x0 , . . . , xn−1 }. Ex.: For the 4-queens problem, we have a binary variable x ij indicating the presence or not of a queen on square (i, j):
X = { x00 , x01 , x02 , x03 , x10 , x11 , x12 , . . . , x33 } • for each variable xi , a finite set Di of possible values (its domain). Ex.: Each variable xij is a binary variable, thus
D00 = D 01 = . . . = D 33 = {0, 1}. • constraints that restrict the values the variables can take simultaneously. Ex.: Constraints that avoid that two queens are on the same row: row 0: row 1: row 2: row 3:
x00 + x01 + x02 + x03 x10 + x11 + x12 + x13 x20 + x21 + x22 + x23 x30 + x31 + x32 + x33
1 1 1 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 row. Later, in section5.2, we will provide another model with other variables and constraints. As we mentioned earlier, values don’t need to be integers and constraints don’t need to be algebraic equations or inequalities17 . If we want to optimize, i.e. to minimize or maximize an objective function, we talk about a Constraint Optimization Problem (COP). The objective function can be one of the variables of the problem or a function of some or all the variables. A feasible solution to a CSP or a COP is a feasible assignment: every variable has been assigned a value from its domain in such a way that all the constraints of the model are respected. The objective value of a feasible solution is the value of the objective function for this solution. An optimal solution to a COP is a feasible solution such that there are no other solutions with better objective values. Note that an optimal solution doesn’t need to exist nor is it unique. 17
Basically, the only requirement for a constraint in CP is its ability to be propagated.
13
1.3. A little bit of theory
1.3.2 Problems, instances and solutions 1.3.3 Two important ideas of the complexity theory for the hurried reader If you prefer not to read the next section, we have summarized its main ideas: • problems are divided in two categories18 : easy (P problems) and hard (NP-Hard or NP-Complete problems) problems. Hard problems are also called intractable19 and in general we only can find approximate solutions for such problems20 . Actually, the question of being able to find exact solutions to hard problems is still open (See the box “The ? P = N P question” below); • good solutions (vs. exact solutions) are called approximations and since the ‘90s a considerable effort was invested in designing a complexity theory of approximations . There is a whole zoo of complexity classes. Some problems can be approximated but without any guarantee on the quality of the solution, others can be approximated with as much precision as you desire but you have to pay the price for this precision: the more precision you want the slower your algorithm will be. For some problems it is hard to find approximations and for others, it is even impossible to find an approximation with any guarantee on its quality!
1.3.4 Complexity theory in a few lines Some problems such as the Travelling Salesman Problem (see chapter 9) are hard to solve21 : no one could ever come up with a very efficient algorithm to solve this problem. On the other hand, other problems, like the Chinese Postman Problem (see chapter 11), are solved very efficiently. In the ’70s, complexity experts were able to translate this fact into a beautifulcomplexity theory. Hard to solve problems are called intractable problems. When you cannot solve an intractable 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 them in a few lines. We willingly kept certain technical details out of the way. These technical details are important and actually without them, you can not construct a complexity theory. 18
Most problems of practical interest belong to either categories but these two categories don’t cover all problems. 19 Intractable problems are problems which in practice take too long to solve exactly, so there is a gap between the theoretical definition (NP-Hard problems) and the practical definition ( Intractable problems). 20 Technically, you could find an exact solution but you would not be able to prove that it is indeed an exact solution in general. 21 Roughly, we consider a problem to be hard to solve if we need a lot of time to solve it. Read on.
14
Chapter 1. Introduction to constraint programming Intractability One of the main difficulties complexity experts faced in the ‘70s was to come up with a theoretical definition of the complexity of problems not algorithms. Indeed, it is relatively easy22 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 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 absolute): 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 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. So problem A is as difficult as problem B (because if you know an efficient algorithm to solve problem A, you can solve problem B as efficiently) and we write B T A and say that problem B reduces efficiently to problem A or that τ is an efficient reduction23 . The search for an efficient algorithm is replaced by the search for an efficient reduction between instances of two problems to prove complexity. This main idea leads to a lot of technicalities: • how to measure the complexity of an algorithm? • what is an efficient transformation? • what are the requirements for such a reduction? • ... We don’t answer these interesting questions except the one on efficiency. We consider a reduction τ 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 22
Well, to a certain degree. You need to know what instances you consider, how these are encoded, what type of machines you use and so on. 23 The T in T is in honor of Alan Turing. Different types of efficient reductions exist.
15
1.3. A little bit of theory can be solved by a polynomial-time bounded algorithm2425 . 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 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 passes exactly once through each vertex, can be easily done. Problems for which it is easy to verify their solutions, i.e. for which this verification can be done in polynomial time, are said to be in the class N P 26 . P ⊂ NP 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 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 problem (and hereby proving that this problem belongs to the set of NP-Complete problems? This is done in two steps: 1. First, you have to know that the reduction is transitive. This means that if A T B and B T C then A T C . Thus, if you have one problem Z such that all problems Ai in NP are reducible to Z , i.e. Ai T 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 T X then Ai T Z T X a . The funny fact is that if X is in NP, then X T Z also. 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). Proving that the SAT problem is NP-Complete is a major achievement in the complexity theory (the proof is highly technical). a
If you want to prove that a problem 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! 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). 25 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. 26 The abbreviation N P refers to non-deterministic polynomial time, not to non-polynomial. 24
16
Chapter 1. Introduction to constraint programming
Finally, if a problem is as hard as an NP-Complete problem, it is called an NP-Hard problem. Optimization problems, whose decision version belong to NP-Complete, fall into this category. The next figure summarizes the relations between the complexity classes27 we have seen as most of the experts believe they stand, i.e. P̸ = NP. NP−Hard
NP−Complete
NP
P If P
=
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 ̸ = N P 61 83 P = N P 9 9 No idea 30 8 One possible outcome - mentioned by very few - is that this question could be... undecidable, i.e. there is no yes or no answerb ! a We agglomerated all other answers into a category No idea although the poll allowed people to fully 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 27
Be aware that there are many more complexity classes.
17
1.4. Real examples
Computers and Intractability: A Guide to the Theory of NP-Completeness from M. R. Garey and D. S. Johnson (see [Garey1979]28 ).
The practical aspect of intractability If you try to solve a problem that is proven to be NP-Hard, you know that it is probably an = NP ). At least, you know that no one could ever come with an intractable problem (if P ̸ efficient algorithm to solve it and that it is unlikely to happen soon. Thus, you can not solve exactly “big” instances of your problem. What can you do? Maybe there are special cases that can be solved in polynomial time? If you are not interested in these cases and your instances are too big to be solved exactly, even with parallel and/or decomposition algorithms, then there is only one thing to do: approximate your problem and/or the solutions. You could simplify your problem and/or be 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 constraints29 . In a nutshell, you soften a constraint by allowing this constraint to be violated. In a approximate solution where the constraint is violated, you penalize the objective function by a certain amount corresponding to the violation. The bigger the violation, the bigger the penalty. The idea is to 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 solution30 .
Approximation complexity [TO BE DONE]
1.4 Real examples Since the ‘90s, CP is used by small and major companies (including Google) around the world. It has become the technology of choice for some problems in scheduling, rostering, timetabling, and configuration. Here is a non-exhaustive list31 where CP has been used with success: • Production sequencing • Production scheduling • Satellite tasking • Maintenance planning 28
This book was written in 1979 and so misses the last developments of the complexity theory but it clearly explains the NP-Completeness theory and provides a long list of NP-Complete problems. 29 For MIP practitioners, this is equivalent to Lagrangian Relaxation. 30 In the case of optimization, a solution that isn’t that different means a solution that has a good objective value, preferably close to the optimum. 31 This list is much inspired from the excellent documentation provided by Helmut Simonis under the Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License.
18
Chapter 1. Introduction to constraint programming
• Transport • Stand allocation • Personnel assignment • Personnel requirement planning • Hardware design • Compilation • Financial applications • Placement • Industrial cutting • Air 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.4.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.
19
1.4. Real examples Most of the time, CP32 : • solves your problem within minutes; • only needs the push of a button (after setting some parameters and providing some data of course); • is very flexible and easily allows you to add or remove side constraints; • improve solutions found by hand by up to sometimes 30%. All of this can only happen if you 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 instance.
1.4.2 Can CP be compared to the holy grail of Operations Research? From time to time, people search for the holy grail of Computer Science. We could 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 Annealing33 to solve arbitrary combinatorial optimization problems34 .
Modelling languages ( AIMMS, AMPL, GAMS, Xpress-Mosel, etc) are yet another attempt at engineering this universal algorithm. You write your model in a common algebraic/symbolic language, often close to the mathematical language35 . It is then translated for an appropriate solver of your choice. Some modelling languages even let you write high level algorithms. One of the great advantages of modelling languages is the possibility to quickly prototype your algorithm and to try it online (and for free!) with the NEOS server36 . All these approaches don’t compare37 to dedicated algorithms tailored for a specific problem38 . Until now, all these attempts have been vain. That said, CP - because of its particularity of dealing with contraints localy39 - is probably the closest technique to the holy grail. Actually, we 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. 32
This is common knowledge in the field. You can learn more about Simulated Annealing (SA) in the section Simulated annealing (SA). 34 This implies that any problem can be translated into a combinatorial problem! 35 See Wikipedia Algebraic languages. 36 The NEOS server proposes several state-of-the-art solvers. As stated on its website: “Optimization problems are solved automatically with minimal input from the user. Users only need a definition of the optimization problem; all additional information required by the optimization solver is determined automatically.” 33
37
Luckily, otherwise we would be jobless! ?
Actually, this search for the holy grail is closely related to the famous P = NP question. If such algoritm exists, then most probably P = NP. See the section Intractability. 39 See the subsection The ease to model a problem. 38
20
Chapter 1. Introduction to constraint programming
1.5 The three-stage method: describe, model and solve We propose a recipe that belongs to the folklore. Like all recipes it is only a guide and should not be followed blindly40 . When it comes to research, everything is permitted and your imagination is the limit. As Feyerabend (Wikipedia) once said:
Anything goes In short, we propose to address a problem a in three stages: • describe • model • solve The three stages are inter-mingled and are not intended to be followed one after the other rigidly as each stage 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 quite naturally problems. For instance, if you need to ensure that some variables must hold different values, simply use the AllDifferent constraint on these variables. Reification allows you to express some constraints that are simply unavailable in other solving paradigms. Another strength of constraint programming is its malleability. You can add side constraints very easily and adapt your models accordingly making constraint programming ideal to prototype new ideas.
1.5.1 Describe This step is often overlooked but is one of most important part of the overall solving process. Indeed, a real problem is often too complex to be solved in its entirety: you have to discard some constraints, to simplify certain hypothesizes, take into account the time to solve the problem (for instance if you have to solve the problem everyday, your algorithm can not take one month to provide a solution). Do you really need to solve the problem exactly? Or can you approximate it? This step is really critical and need to be carefully planned and executed. Is this manual, we will focus on three questions: • What is the goal of the problem we try to solve? What kind of solutions are we exactly expected to provide? 40
If you are allergic to this “academic” approach, you probably will be happy to know that we only use this three-stage method in the first two partsc of this manual.
21
1.6. It’s always a matter of tradeoffs
• What are the decision variables? What are the variables whose values are crucial to solve the problem? • What are the constraints? Are our constraints suited to solve the problem at hand?
1.5.2 Model Again a difficult stage if not the most challenging part of the solving process. Modelling is more of an Art than anything else. With experience, you will be able to model more easily and use known and effective tricks. If you are a novice in Operations Research/Constraint Programming, pay attention to the proposed models in this manual as they involve a lot of knowledge and subtleties. Do not be discouraged if you do not understand them at first. This is perfectly normal. Take the time to read them several times until you master them. Beside, it could be our fault if you do not understand them: maybe we did not explain them well? When confronted with a new problem, you might not know what do to. We all face this situation. This is what research is all about!
1.5.3 Solve The reader should be aware that this stage 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 problem41 . When confronted to 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 disadvantages42 . Let’s say we are implementing our own constraint with our own propagation algorithm (this is exactly what we will do in chapter ?? ). We might develop a very clever 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! 41 42
22
At least, no one found one and with our actual knowledge, there is a strong suspicion that none exist. Of course, we are talking about clever options.
Chapter 1. Introduction to constraint programming
Be conscious of the tradeoffs and that what seems the best option at a time might actually not work that well no matter how clever the basic idea was. Ideas have to be tested and retested. This testing is an uncompromising way to take decisions but also allows to get a better insight of how and why an algorithm actually works (or fails). CP and the or-tools library allow us to develop very quickly prototypes we can test, improve, test, redesign, test, etc., you get the idea. The good optimization 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 1.7.1 Coding philosophy
1.8 The content of the manual 1.8.1 Part I: Basics 1.8.2 Part II: Customization 1.8.3 Part III: Routing 1.8.4 Part IV: Technicalities 1.8.5 Appendices
23
CHAPTER
TWO
FIRST STEPS WITH OR-TOOLS: CRYPTARITHMETIC PUZZLES
This chapter introduces the basics of the or-tools library. In particular, we show how to use the Constraint Programming Solver (CP Solver). It takes a while to get used to the logic of the library, but once you grasp the basics explained in this chapter, 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 SolutionCollector s 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.3.1 summarizes in two Figures all the required steps to write a basic program. Prerequisites:
• basic knowledge of C++. • basic knowledge of Constraint Programming (see chapter 1). Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap2 . The files inside this directory are: • cp_is_fun1.cc: Our first example: a simple cryptarithmetic puzzle to show the basics. • cp_is_fun2.cc: Use of SolutionCollector s 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
2.1 Getting started In case of conflict with the http://code.google.com/p/or-tools/wiki/AGettingStarted page, please disregard this section. Last updated: May 04, 2012
2.1.1 Getting the source Please visit http://code.google.com/p/or-tools/source/checkout to checkout the sources of ortools.
2.1.2 Content of the Archive Upon untarring the given operation_research tar file, you will get the following structure:
or-tools/ Files/directories
LICENSE-2.0.txt Makefile README bin/ dependencies/ examples/com/ examples/csharp/ examples/cpp/ examples/python/ examples/tests/ lib/ makefiles/ objs/ src/algorithms/ src/base/ src/com/ src/constraint_solver/ src/gen/ src/graph/ src/linear_solver/ src/util/ tools/
26
Descriptions Apache License Main Makefile This file Where all binary files will be created Where third_party code will be downloaded and installed Directory containing all java samples Directory containing C# examples and a visual studio 2010 solution to build them C++ examples Python examples Unit tests Where libraries and jar files will be created Directory that contains sub-makefiles Where C++ objs files will be stored A collection of OR algorithms (non graph related) Directory containing basic utilities Directory containing java and C# source code for the libraries The main directory for the constraint solver library The root directory for all generated code (java classes, protocol buffers, swig files) Standard OR graph algorithms The main directory for the linear solver wrapper library More utilities needed by various libraries Binaries and scripts needed by various platforms
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.1.3 Installation on unix platforms For linux users, please intall zlib-devel, bison, flex, autoconf, libtool, python-setuptools, and python-dev. The command on ubuntu is the following: sudo apt-get install bison flex python-setuptools python-dev autoconf \ libtool zlib-devel
The fedora command is: sudo yum install subversion bison flex python-setuptools python-dev \ autoconf libtool zlib-devel
If you wish to use glpk, please download glpk from http://ftp.gnu.org/gnu/glpk/glpk-4.47.tar.gz and put the archive in or-tools/dependencies/archives . If you have the license, please download ziboptsuite-2.1.1.tgz from http://zibopt.zib.de/download.shtml and put the archive in or-tools/dependencies/archives . If you wish to use .NET, you need to install mono. On linux platforms, just install the mono-devel package. You need a recent one (at least 2.8 I believe) to work correctly. If mono is not supported on your platform, you can install it using the procedure for Mac OS X. On Mac OS X, you need 64 bit support. Thus you need to build mono by hand. Copy the mono archive http://download.mono-project.com/sources/mono/mono-2.10.8.1.tar.gz to dependencies/archives . You can use dependencies/install/bin/gmcs to compile C# files and dependencies/install/bin/mono to run resulting .exe files. run: make third_party make install_python_modules
If you are on opensuse and maybe redhat, the make install_python_module will fail. One workaround is described on this page http://stackoverflow.com/questions/4495120/combine-user-with-prefix-error-with-setup-pyinstall. If you have root privilieges, you can replace the last line and install the python modules for all users with the following command: cd dependencies/sources/google-apputils sudo python2.7 setup.py install
It should create the Makefile.local automatically. Please note that the command: make clean_third_party
will clean all downloaded sources, all compiled dependencies, and Makefile.local . It is useful to get a clean state, or if you have added an archive in dependencies.archives.
27
2.1. Getting started
2.1.4 Installation on Windows Create the or-tools svn copy where you want to work. Install python from http://www.python.org/download/releases/2.7/ Install java JDK from http://www.oracle.com/technetwork/java/javase/downloads/jdk-7u2download-1377129.html You need to install python-setuptools for windows. http://pypi.python.org/pypi/setuptools#files .
Please fetch it from
If you wish to use glpk, please download glpk from http://ftp.gnu.org/gnu/glpk/glpk-4.47.tar.gz and put the archive in or-tools/dependencies/archives . Then you can download all dependencies and build them using: make third_party
then edit Makefile.local to point to the correct python and java installation. Afterwards, to use python, you need to install google-apputils: cd dependencies/sources/google-apputils c:\python27\python.exe setup.py install
Please note that the command: make clean_third_party
will clean all downloaded sources, all compiled dependencies, and Makefile.local . It is useful to get a clean state, or if you have added an archive in dependencies.archives.
2.1.5 Running tests You can check that everything is running correctly by running: make test
If everything is OK, it will run a selection of examples from all technologies inC++, python , java, and C#.
2.1.6 Compiling libraries and running examples Compiling libraries All build rules use make (gnu make), even on windows. A make.exe binary is provided in the tools sub-directory. You can query the list of targets just by typing make
28
Chapter 2. First steps with or-tools: cryptarithmetic puzzles You can then compile the library, examples and python, java, and .NET wrappings for the constraint solver, the linear solver wrappers, and the algorithms: make all
To compile in debug mode, please use make DEBUG=-g all
or make DEBUG="/Od /Zi" all
under windows. You can clean everything using: make clean
When everything is compiled, you will find under or-tools/bin and or-tools/lib: • Some static libraries (libcp.a, libutil.a and libbase.a, and more) • One binary per C++ example (e.g. nqueens) • C++ wrapping libraries (pywrapcp.so, linjniwrapconstraint_solver.so) • Java jars (com.google.ortools.constraintsolver.jar...) • C# assemblies
C++ examples You can execute C++ examples just by running then: ./bin/magic_square
Python examples For the python examples, as we have not installed the constraint_solver module, we need to use the following command: on windows: set PYTHONPATH=%PYTHONPATH%;
\src,
then c:\Python27\python.exe python/sample.py.
On unix: PYTHONPATH=src python/
As in
29
2.1. Getting started
PYTHONPATH=src python2.6 python/golomb8.py
There is a special target in the makefile to run python examples. The above example can be run with make rpy EX=golomb8
Java examples You can run java examples with the run_ makefile target as in: make run_RabbitsPheasants
There is a special target in the makefile to run java examples. The above example can be run with make rjava EX=RabbitsPheasants
.NET examples If you have .NET support compiled in, you can build .NET libraries with the command: make csharp. You can compile C# examples typing: make csharpexe.
To run a C# example, on windows, just type the name bin\csflow.exe
On unix, use the mono interpreter: mono bin/csflow.exe
There is a special target in the makefile to run C# examples. The above example can be run with make rcs EX=csflow
30
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.2 The cryptarithmetic puzzle problem and a first model Now that your system is up and running (if not, see section 2.1), let us solve a cryptarithmetic puzzle with the help of the or-tools library. In this section, we describe the problem and propose a first model to solve it. This model is by no means efficient but allows us a gentle introduction to the library. A better model is presented in the corresponding lab session.
2.2.1 Description of the problem A cryptarithmetic puzzle is a mathematical game where the digits of some numbers are represented by letters (or symbols). Each letter represents a unique digit. The goal is to 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.2.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 2
This the mathematical term to specify that the equation is true. Like the famous SEND + MORE = MONEY ... in base 10.
31
2.2. The cryptarithmetic puzzle problem and a first model
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:
=
T
2
·b + + I·b + U·b
P
·b +
E
+ + +
C
U
+
F
·b
· b3 +
R
· b2 +
S N
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 AllDifferent a 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. MakeAllDifferent() .
In or-tools, we use the method
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?”
32
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.3 Anatomy of a basic C++ code In this section, we code the model developed in section 2.2. You can find the code in the file tutorials/cplusplus/chap2/cp_is_fun1.cc . We quickly scan through the code and describe the basic constituents needed to solve the cryptarithmetic puzzle in C++. In the next chapters, we will cover some of them in more details.
2.3.1 At a glance
33
2.3. Anatomy of a basic C++ code
2.3.2 Headers To use the library, we need to include a few headers: #include "base/logging.h" #include "constraint_solver/constraint_solver.h"
The header logging.h is needed for some logging facilities and some assert-like macros. The header constraint_solver.h is the main entry point to the CP solver and must be included4 whenever you intend to use it.
2.3.3 The namespace operations_research The whole library is nested in the namespace operations_research . We follow the same convention in all our examples and code inside this namespace: namespace operations_research { IntVar* const MakeBaseLine2(...) {
... } ... void CPIsFun() { // Magic happens here!
} }
// namespace operations_research
MakeBaseLine2 , MakeBaseLine3 and MakeBaseLine4 are helper functions to create the model. We detail these functions later in section 2.3.7 but for the moment, 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. The main function does not lie inside the namespace operations_research , hence the use of the operations_research identifier to call the function CPIsFun() . 5
34
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
return 0;
}
2.3.4 The CP solver The CP solver is the main engine to solve a problem instance. It is also responsible for the creation of the model. It has a very rich Application Programming Interface (API) and provides a lots of functionalities. The CP solver is created as follows: Solver solver("CP is fun!");
The only argument of the constructor is an identification string. The Solver class has one additional constructor covered in section 2.5.
2.3.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
35
2.3. Anatomy of a basic C++ code (IntervalVar ), sequence variables (SequenceVar ) and variables to encapsulate objectives (OptimizeVar ).
2.3.6 Assert-like macros It is always a good idea to program defensively. We use several assert-like macros 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 program is aborted and the cause is printed: [23:51:34] examples/cp_is_fun1.cc:108: Check failed: (kBase) >= (letters.size()) Aborted
You can find more about the assert-like macros in section 12.2.
2.3.7 Constraints To create an integer linear constraint, we need to know how to multiply an integer variable with an integer constant and how to add two integer variables. We have seen that the solver creates a variable and only provides a pointer to that variable. The solver also provides factory methods to multiply an integer 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();
36
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
Is the call to Var() really necessary?
Yes! Var() not only transforms a constraint into a variable but also a stateless expression into a stateful and monotonic variable. Variables are stateful objects that provide a rich API. On the other hand, subclasses of BaseIntExpr represent range-only stateless objects. That is, MakeMin(MakeSum(A,B),a) is recomputed each time as MakeMin(A,a) + MakeMin(B,a) . Furthermore, sometimes the propagation on an expression is not complete. For instance, if A is an IntVar with domain [0..5], and B another IntVar with domain [0..5] then MakeSum(A, B) has domain [0, 10]. If we apply MakeMax(MakeSum(A, B), 4)) then we will deduce that both A and B will have domain [0..4]. In that case, the max of MakeMax(MakeSum(A, B),4) is 8 and not 4. To get back monotonicity, we need to cast the expression into a variable using the Var() method: MakeMax(MakeSum(A, B),4)->Var() . The resulting variable is stateful and monotonic. Never store a pointer to an IntExpr nor a BaseIntExpr in the code. The safe code should always call Var() on an expression built by the solver, and store the object as an IntVar*. To construct a sum, we use a combination of MakeSum() and MakeProd() factory methods: const int64 kBase = 10; IntVar* const c = solver.MakeInt(1, kBase - 1, "C"); IntVar* const p = ...;
... IntVar* const s = ...; IntVar* const term1 = solver.MakeSum(solver.MakeProd(c,kBase),p)->Var(); IntVar* const term2 = solver.MakeSum(solver.MakeProd(i,kBase),s)->Var();
There is no need to cast the result of MakeProd(c,kBbase) into an IntVar because MakeSum() takes two pointers to an IntExpr. The combination of MakeSum() and MakeProd() can quickly become tedious. We use helper functions to construct sums. For example, to construct the first term of our cryptarithmetic 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 IntVar s and an std::vector of
37
2.3. Anatomy of a basic C++ code
integer coefficients: IntVar* const var1 = solver.MakeInt(...); ... IntVar* const varN = solver.MakeInt(...); std::vector variables; variables.push_back(var1); ... variables.push_back(varN); std::vector 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 letters; letters.push_back(c); letters.push_back(p); ... letters.push_back(e); solver.AddConstraint(solver.MakeAllDifferent(letters));
38
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.3.8 The Decision Builder A DecisionBuilder is responsible for creating the actual search tree, i.e. it is responsible for the search. The solver provides a factory method MakePhase() that returns a pointer to the newly created DecisionBuilder object: DecisionBuilder* const db = solver.MakePhase(letters, Solver::CHOOSE_FIRST_UNBOUND, Solver::ASSIGN_MIN_VALUE);
The 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.3.9 The search and the solutions To prepare for a new search: DecisionBuilder* const db = ... solver.NewSearch(db);
To actually search for the next solution in the search tree, we call the method NextSolution() . It returns true if a solution was found and false otherwise: if (solver.NextSolution()) { // Do something with the current solution
} else { // The search is finished
}
We print out the found solution and check if it is valid6 : if (solver.NextSolution()) {
LOG(INFO) << "Solution found:"; LOG(INFO) << "C=" << c->Value() << "I=" << i->Value() << "F=" << f->Value() << "N=" << n->Value() << "R=" << r->Value()
<< << << << <<
" " " " "
" " " " "
<< "P=" << "S=" << "U=" << "T=" << "E="
<< << << << <<
p->Value() << s->Value() << u->Value() << t->Value() << e->Value();
" " " "
" " " "
// Is CP + IS + FUN = TRUE?
CHECK_EQ(p->Value() + s->Value() + n->Value() + kBase * (c->Value() + i->Value() + u->Value()) + kBase * kBase * f->Value(), e->Value() + kBase * u->Value() + 6
Actually and contrary to the intuition, NextSolution() 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.
39
2.4. SolutionCollector s and Assignment s to collect solutions
kBase * kBase * r->Value() + kBase * kBase * kBase * t->Value()); } else { LOG(INFO) << "Cannot solve problem."; } // if (solver.NextSolution())
The output is: $[23:51:34] examples/cp_is_fun1.cc:132: Solution found: $[23:51:34] examples/cp_is_fun1.cc:133: C=2 P=3 I=7 S=4 F=9 U=6 N=8 T=1 R=0 E=5
We check the validity of the solution after printing: if the solution is not valid, we can see what was found by the solver. To obtain all the solutions, NextSolution() can be called repeatedly: while (solver.NextSolution()) { // Do something with the current solution
} else { // The search is finished
}
2.3.10 The end of the search To 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.5.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.4 SolutionCollectors and Assignments to collect solutions The or-tools library let you collect and store the solutions of your searches with the help of SolutionCollector s 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 .
40
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
2.4.1 SolutionCollectors The SolutionCollector class is one of several specialized SearchMonitors classes. i.e. SolutionCollector inherits from SearchMonitors . SearchMonitors provides a set of callbacks to monitor all search events. We will learn more about them in the next chapter. To collect solutions, several SolutionCollector are available: • FirstSolutionCollector : to collect the 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 :
41
2.4. SolutionCollector s and Assignment s to collect solutions
first_solution->Add(c);
The method Add() simply adds the variable c to the SolutionCollector . The variable c is not tied to the solver, i.e. you will not be able to retrieve its value by c->Value() after a search with the method Solve(). To launch the search: solver.Solve(db,first_solution);
After the search, you can retrieve the value of c like this: first_solution->solution(0)->Value(c)
or through the shortcut: first_solution->Value(0,c)
In both cases, the index 0 denotes the 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 SolutionCollector s 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();
42
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
//
Add it to the SolutionCollector
all_solutions->Add(v1); ... DecisionBuilder* const db = ... ... solver.Solve(db, all_solutions); //
Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl; for (int index = 0; index < number_solutions; ++index) {
LOG(INFO) << "Solution found:"; LOG(INFO) << "v1=" << all_solutions->Value(index,v1); }
2.4.2 Assignments The or-tools library provides the class Assignment to store the solution (in parts or as a whole). The class Assignment has a rich API that allows you to retrieve not only the values of the variables in a solution but also additional information. You can also act on some of the variables for instance to disable them during a search. We will see this class in more details in chapter XXX. SolutionCollector* const all_solutions = solver.MakeAllSolutionCollector(); //
Add the interesting variables to the SolutionCollector
IntVar* v1 = solver.MakeSum(solver.MakeProd(c,kBase), p)->Var(); //
Add it to the SolutionCollector
all_solutions->Add(v1); ... DecisionBuilder* const db = ... ... solver.Solve(db, all_solutions); //
Retrieve the solutions
const int number_solutions = all_solutions->solution_count();
LOG(INFO) << "Number of solutions: " << number_solutions << std::endl; for (int index = 0; index < number_solutions; ++index) { Assignment* const solution = all_solutions->solution(index);
LOG(INFO) << "Solution found:"; LOG(INFO) << "v1=" << solution->Value(v1); }
In section 12.6, we’ll use it to serialize a solution.
43
2.5. Parameters
What is the difference between NewSearch() and Solve() ?
Depending on the search, Solve() is equivalent to either solver.NewSearch(); solver.NextSolution(); solver.EndSearch();
or solver.NewSearch(); while (solver.NextSolution()) {...}; solver.EndSearch();
With NewSearch() you can access the variables of the current solutions (no need for a SolutionCollector ). More importantly, you can interfere with the search.
2.5 Parameters This section is divided in two parts. First, we show you how to use Google’s command line flag library. Second, we explain how to pass parameters to the CP solver.
2.5.1 Google’s gflags The Google’s flags library is quite similar to other command flags libraries with the noticeable 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."); ... namespace operations_research { ...
44
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
and then parse the command line: int main(int argc, char **argv) {
google::ParseCommandLineFlags(&argc, &argv, true); operations_research::CPIsFun(); return 0; }
Note that argc and argv are passed as pointers so that ParseCommandLineFlags() is able to modify them. All 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_fun4 --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 thegflags documentation.
2.5.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: // Save profile in file
solver.ExportProfilingOverview("profile.txt");
45
2.6. Other supported languages We will see how to profile more in details in the section 12.4. The SolverParameters struct mainly deals with the internal usage of memory and is for advanced users. SearchMonitor s
Second, you can use SearchMonitor s. We have already seen how to use them to collect solutions in section 2.4. Suppose we want to limit the available time to solve a problem. To pass this parameter on the command line, we 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 chapter ?? .
2.6 Other supported languages Everything is coded in C++ and available through SWIG in Python, Java, and .NET (using mono on non windows platforms). What language you use is a matter of taste. If you main concern is 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:
46
Chapter 2. First steps with or-tools: cryptarithmetic puzzles
Naming convention for methods in or-tools
A C++ method named MYMethod() becomes: • MyMethod() in Python (pascal case); • myMethod() in Java (camel case); • MyMethod() in C# (pascal case). Methods with “_” (underscore) like objective_value() become ObjectiveValue() or objectiveValue() .
2.7 Summary summary
47
CHAPTER
THREE
USING OBJECTIVES IN CONSTRAINT PROGRAMMING: THE GOLOMB RULER PROBLEM
In this chapter, we are not only looking for a feasible solution but we want the best solution! Most of the time, the search is done in two steps. First, we find the best solution1 . Second, we prove 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 (preferably implicitly) the complete complete search tree. Overview:
We start by stating the Golomb Ruler Problem (GRP) and showing that this problem is difficult. 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: introduced: adding better bounds and breaking symmetries. symmetries. Finally Finally, we say a few words about the strategies used by the solver to optimize optimize an objective objective function. Prerequisites:
• Basic knowle knowledge dge of C++. C++. • Basic knowledge knowledge of Constraint Constraint Programming Programming (see chapter 1 chapter 1). ). • Basic knowledge knowledge of the Constraint Constraint Programming Programming Solver (see chapter chapter 2 2). ). Remarks:
• The sums used in this chapter to model the GRP are tricky tricky but you don’t need to master them. them. We do all the dirty dirty work for you. you. In fact, fact, you can complet completely ely skip them them if you wish. The basic ideas behind these sums are simple and are easy to follow follow. • We introduce two kinds of variables variables in our modelizations: modelizations: the marks of the ruler and the differences between the marks. 1
How How do we know know we have have a best best solution solution?? Only Only when we have prove proven n it to be so! The two two steps are intermingle intermingled. d. So why do we speak about two steps? steps? Because, 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 functions and how how to compare compare search search strategies
Classes under scrutiny: Files:
You can can find find the the code code in the the dire direct ctor ory y documentation/tutorials/cplusplus/chap3 . The files inside this directory directory are: • Makefile. • golomb1.cc: A first implementation implementation.. We show how to tell the solver to optimize optimize an n(n−1) objective function. We use the 2 differences as variables. • golomb2.cc: Same Same file file as golomb1.cc but with some global statistics about the search added so we can see how well or bad a model behave. • golomb3.cc: A second implementation. implementation. This time, we only use the marks as variables variables and introduce the quaternary inequality constraints. • golomb4.cc: We improve the second implementation by reintroducing the ferences variables.
n(n−1)
2
dif-
• golomb5.cc: In this third implementatio implementation, n, we replace the inequality constraints constraints by the more powerful globlal AllDifferent constraint. • golomb6.cc: The last impleme implementat ntation ion is a tighteni tightening ng of the model model used in the third third implem implement entatio ation. n. We add better better upper upper and lower lower bounds and break break a symmet symmetry ry in the search tree. In all the code odes, we use the sam same strategy tegy to select the next vari ariable to branch on (CHOOSE_FIRST_UNBOUND ) and the same strategy to assign it a value (ASSIGN_MIN_VALUE ). The The time timess we comp compar aree not not only only meas measur uree the the solv solvee proc proces esss but also the time needed to construct the model.
3.1 3. 1 Obje Object ctiv ive e fu func ncti tion ons s an and d ho how w to co comp mpar are e se sear arch ch strategies In this chapter, we want to construct a ruler with minimal length. Not any ruler, a Golomb ruler. The ruler ruler we seek has to obey certain certain constraint constraints, s, i.e. it has to be a feasible feasible solution solutionss of the models we develop to represent the Golomb Ruler Problem. Problem. The objective function to minimize is the length of the Golomb ruler. You can see the objective function function as a variab variable: le: you want to find out what what is the minimum minimum or maximu maximum m value this variable variable can hold for any given feasible solution. solution. Don’t Don’t worry if this is not all too clear. clear. There are numerous examples 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 fast fast2 . We will devise differ2
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.
50
Chapter Chapter 3. Using objective objectives s in constraint constraint programm programming: ing: the Golomb ruler problem ent models and search strategies strategies and compare them. To do so, we will look at the following statistics: • time: This our main criteria. The faster the better! • failures: How How many times times do we need to backtrack backtrack in the search search tree? Many Many failures failures might be an indication indication that there exist better search strategies. strategies. • branches: How many times do we need to branch? Faster Faster algorithms tend to visit fewer branches.
3.2 The Golomb Golomb ruler proble problem m and a first model 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 problem and propose a first model to solve it. This model is not very efficient efficient and we will develop better models in the next sections.
3.2.1 Desc Descripti ription on of the problem problem A Golo Golomb mb rule rulerr is a seque sequence nce of non-n non-neg egat ativ ivee inte intege gers rs such such that that ever every y diffe differen rence ce of two two inte integer gerss 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) sequence) is called the order of of the Golomb ruler. Figure 3.1 Figure 3.1 illustrates a Golomb ruler of order 4 and all its - distinct - differences. 0
2
7
2
5
11 4
7 9 11
Figure 3.1: A non optimal Golomb ruler of order 4. The Golomb ruler is { 0, 2, 7, 11} and its length is 11. Because Because we are interested interested in Golomb Golomb rulers with minimal length, we can fix the first mark to 0. Figur Figuree 3.2 3.2 illustrates illustrates an optimal Golomb ruler of order 4 and all its - distinct - differences. Its length, 6, is optimal: it is not possible to construct a Golomb ruler with 4 marks with a length G(4) = 6. More generally, for a Golomb ruler smaller than 6. We denote this optimal value by G(4) Problem (GRP) is to find, G(n) its optimal value. The Golomb Ruler Problem of order n, we denote by G( for a given order n, the smallest 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 invol involving ving hundreds hundreds of people! people!
51
3.2. The Golomb Golomb ruler proble problem m and a first model model 0
1
4
1
3
6 2
4 5 6
Figure 3.2: An optimal Golomb ruler of order 4. The next table compares the number of days, the number of participants on the Internet and the G(24), G(25) G(25) and G(26) G(26)34 5 . number of visited nodes in the search tree to find and prove G(24) Orders 24 25 26
Days 1,572 3,006 24
Participants 41,803 124,387 2754
Visited nodes 555,551,924,848,254,200 52,898,840,308,130,480,000 3,185,174,774,663,455
G (27) started The search for G(27) started on February 24, 2009 and at that time was expected expected to take... 7 6 years! Still think it is an easy problem? You too can participate: The participate: The OGR Project. Project. You can find all the known optimal Golomb rulers and more information on Wikipedia on Wikipedia.. Why Golomb Rulers?
Golom Golomb b ruler rulerss have have a wide wide varie variety ty of appli applicat catio ions, ns, inclu includin ding g radio radio astr astrono onomy my and infor informa ma-tion theory. In radio astronomy, when constrained to be lined up, telescopes collect more accurate accurate information information if they are placed on the marks of a Golomb Golomb ruler. ruler. In information information theory, theory, Golomb Golomb rulers are used for error detection and correction. correction.
3.2.2 How to solve solve the proble problem? m? We follow again the classical three-stage method described described in section 1.5 section 1.5:: describe, model and solve.
Describe What is the goal of the Golomb Ruler Problem? To find a minimal Golomb ruler for a given order n . Our objective objective function is the length of the ruler or the largest integer in the Golomb ruler sequence. What are the decision variables (unknowns)? We have at least two choices. We can either view the unknowns as the marks of the ruler (and retrieve all the differences from these variables) 3
http://stats.distributed.net/projects.php?project_id=24 http://stats.distributed.net/projects.php?project_id=25 5 http://stats.distributed.net/projects.php?project_id=26 6 Although Although it is strongly suspected suspected that the Golomb Ruler Problem Problem is a very difficult difficult problem, problem, the computacomputational complexity of this problem is unknown (see [Meyer-P [Meyer-Papakonstantinou] apakonstantinou]). ). 4
52
Chapter Chapter 3. Using objective objectives s in constraint constraint programm programming: ing: the Golomb ruler problem
or choose the unknowns to be the differences differences (and retrieve retrieve the marks). marks). Let’s Let’s try this second n(n−1) approach and use the efficient AllDifferent constraint. constraint. There are 2 such differences. What are the constraints? Using the differences as variables, we need to construct a Golomb ruler, ruler, i.e. the structure of the Golomb Golomb ruler has to be respected respected (see next section).
Model For each positive difference, difference, we have a decision decision variable. variable. We collect them in an array Y. Let’s Y [i]. Figu order the differences so that we know which difference is represented by Y [ Figure re 3.3 illustrates an ordered sequence of differences for a Golomb ruler of order 4. st
1
nd
2
3
rd
th
4
th
5 th
6
Figure 3.3: An ordered sequence of differences differences for the Golomb Golomb ruler of order 4.
Y [ ( 2−1) − 1] since the first index of an array We want to minimize the last difference in Y i.e. Y [ n = 4, we want to optimize Y [ Y [ 4(42−1) − 1] = Y [5] Y [5] which represents the is 0. When the order is n = 6th difference. Y [i], we will also use the more convenient notation Y i . difference. Instead of writing writing Y [ n n
Figure 3.4 illus illustr trate atess the struc structur tures es than than must must be respe respecte cted d for a Golom Golomb b rule rulerr of ororder 5. To impose the inner structure of the Golomb Ruler, we force Y 4 = Y 0 + Y 1 , Y 5 = Y 1 + Y 2 and so on as illustrated in Figure 3.4 Figure 3.4.. index
Y 0
Y 1
Y 2
Y 3 i = 2
Y 4 Y 5
i = 3 Y 6 i = 4
Y 7
Y 4 Y 5 Y 6
=
Y 7 Y 8
=
Y 9
=
Y 0
+
=
Y 1 Y 1
+
=
Y 0
+
=
Y 0
+
Y 1 Y 1
+
Y 1
+
+
Y 2 Y 2
+
Y 3
j=0 j = 1 j = 2
Y 2 Y 2
+
Y 3
j = 0 j = 1
Y 2
+
Y 3
j = 0
Y 8 Y 9
Figure 3.4: The inner structure of a Golomb ruler of order 5. An easy way to construct construct these equality constraints is to use an index index going from 4 to 97 , an index i to count the number of terms in a given equality and an index j to indicate the rank of the starting term in each equality: 7
Or more generally from the index of the first difference that is the sum of two differences in our sequence
(n − 1) to the index of the last difference
(
n(n−1)
2
) −1
.
53
3.3. An implement implementation ation of the first first model model
int index = n - 2; ++i) { for (int i = 2; i <= n - 1; ++i) ++j) j) { for (int j = 0; j < n-i; ++
++index; ++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 solve this model. model. A default default basic strateg strategy y will do for the moment. moment. The next next chapter chapter 5 is entirely entirely devoted devoted to the subject of search strategies. strategies.
3.3 An implement implementation ation of the the first model model In this section, we code the first model developed in section 3.2 section 3.2.. You can find the code in the file tutorials/cplusplus/chap3/golomb1.cc . We take the order order (the number number of marks) from the command line: DEFINE_int64(n, 5, "Numbe "Number r of mar marks. ks." ");
3.3.1 An upper upper bound bound 2n − 1. Several Several upper bounds exist on Golomb rulers. rulers. For instance, instance, we could take n 3 − 2n2 + 2n Indeed, it can be shown that the sequence Φ(a Φ(a) = na 2 + a
0 a < n.
2n − 1 (when a = a = n n − 1), we have forms a Golomb ruler. As its largest member is n3 − 2n2 + 2n an upper bound on the length of a Golomb ruler of order n : G(n) n3 − 2n2 + 2n 2 n − 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 conjecture holds for n < 65000 (see [Dimitromanolakis2002] (see [Dimitromanolakis2002]). ). This is perfect for our needs: CHECK_LT(n, 65000); 65000); int64 max = n * n - 1; const int64
54
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem
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 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 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 create an std::vector (or to append some IntVar*s to an existing std::vector).
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 function: OptimizeVar* const length = s.MakeMinimize(Y[num_vars - 1], 1);
and give the variable length to the Solve() method: 8
Remember the remark at the beginning of this chapter about the tricky sums!
55
3.4. What model did I pass to the solver?
s.Solve(db, collector, length);
In the section 3.9, we will explain how the solver optimizes and the meaning of the mysterious parameter 1 in ... = s.MakeMinimize(Y[num_vars - 1], 1);
3.4 What model did I pass to the solver? The model we developed to solve the cryptarithmetic puzzle in section 2.2 was quite simple. The 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 section, 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) Y_5(1..24) Y_6(1..24) Y_7(1..24)
== Var<(Y_1(1..24) + == Var<(Y_2(1..24) + == Var<(Y_3(1..24) + == Var<(Y_2(1..24) +
Y_0(1..24))>(2..48) Y_1(1..24))>(2..48) Y_2(1..24))>(2..48) Var<(Y_1(1..24) + Y_0(1..24))>(2..48))>(3..72) ...: Y_8(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) + Y_1(1..24))>(2..48))>(3..72) ...: Y_9(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) + Var<(Y_1(1..24) + Y_0(1..24))>(2..48))>(3..72))>(4..96)
These are exactly the constraints listed in Figure 3.4 page 53.
56
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem
3.4.2 Use the default 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 constraint_solver.cc , these are:
command
line
flags.
For
the
file
Flags from src/constraint_solver/constraint_solver.cc: -cp_export_file (Export model to file using CPModelProto.) type: string default: "" -cp_model_stats (use StatisticsModelVisitor on model before solving.) type: bool default: false -cp_name_cast_variables (Name variables casted from expressions) type: bool default: false -cp_name_variables (Force all variables to have names.) type: bool default: false -cp_no_solve (Force failure at the beginning of a search.) type: bool default: false -cp_print_model (use PrintModelVisitor on model before solving.) type: bool default: false -cp_profile_file (Export profiling overview to file.) type: string default: "" -cp_show_constraints (show all constraints added to the solver.) type: bool default: false -cp_trace_propagation (Trace propagation events (constraint and demon executions, variable modifications).) type: bool default: false -cp_trace_search (Trace search events) type: bool default: false -cp_verbose_fail (Verbose output when failing.) type: bool default: false
We are interested in the constraints. Invoking ./golomb1 --n=5 --cp_no_solve --cp_show_constraints
gives us: ...: BoundsAllDifferent(Y_0(1..24), Y_1(1..24), Y_2(1..24), Y_3(1..24), Y_4(1..24), Y_5(1..24), Y_6(1..24), Y_7(1..24), Y_8(1..24), Y_9(1..24))
This is the AllDifferent constraint on bounds where we see all the variables with their initial domains. Then: ...: cast((Y_1(1..24) + Y_0(1..24)), Var<(Y_1(1..24) + Y_0(1..24))> (2..48))
The cast to transform the sum Y 1 + Y 0 into an IntVar. And then:
57
3.4. What model did I pass to the solver?
...: ...: ...: ...:
Y_4(1..24) Y_5(1..24) Y_6(1..24) Y_7(1..24)
== Var<(Y_1(1..24) + == Var<(Y_2(1..24) + == Var<(Y_3(1..24) + == Var<(Y_2(1..24) +
Y_0(1..24))>(2..48) Y_1(1..24))>(2..48) Y_2(1..24))>(2..48) Var<(Y_1(1..24) + Y_0(1..24))>(2..48))>(3..72) ...: Y_8(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) + Y_1(1..24))>(2..48))>(3..72) ...: Y_9(1..24) == Var<(Y_3(1..24) + Var<(Y_2(1..24) + Var<(Y_1(1..24) + Y_0(1..24))>(2..48))>(3..72))>(4..96) ...: Forcing early failure ...: Check failed: (collector->solution_count()) == (1) Aborted
All this output was generated from the following line in constraint_solver.cc : LOG(INFO) << c->DebugString();
where c is a pointer to a Constraint. Invoking ./golomb1 --n=5 --cp_no_solve --cp_model_stats
we obtain some statistics about the model: ...: Model has: ...: - 17 constraints. ...: * 1 AllDifferent ...: * 6 Equal ...: * 10 CastExpressionIntoVariable ...: - 20 integer variables. ...: - 10 integer expressions. ...: * 10 Sum ...: - 10 expressions casted into variables. ...: - 0 interval variables. ...: - 0 sequence variables. ...: - 2 model extensions. ...: * 1 VariableGroup ...: * 1 Objective
Indeed, we have 1 AllDifferent constraint, 6 equality constraints and 10 IntVar variables. Where does the rest come from? To construct the equality constraints, we cast 10 times integer expressions into IntVar (remember the ...->Var() calls), hence the 10 integer expressions, the 10 supplementary IntVar variables and the 10 sums. The 2 model extensions are the objectiveOptimizeVar variable and the std::vector array of IntVar variables (VariableGroup ). Try the other flags!
58
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem
3.5 Some global statistics about the search and how to limit the search In section 3.1, we talked about some global statistics about the search. In this section we review them one by one. You can find the code in the file tutorials/cplusplus/chap3/golomb2.cc .
3.5.1 Time This is probably the most common statistic. There exist several timing libraries or tools to measure the duration of an algorithm. The or-tools library offers a basic but portable timer. This timer starts to measure the time from the creation of the solver. solver("TicTac") s;
//
Starts the timer of the solver.
If you need the elapsed time since the creation of the timer, just call wall_time(): const int64 elapsed_time = s.wall_time();
The time is given in milliseconds. If you only want to measure the time spent to solve the problem, just subtract times: const int64 time1 = s.wall_time();
s.Solve(...); const int64 time2 = s.wall_time(); LOG(INFO) << "The Solve method took " << (time2 - time1)/1000.0 << " seconds";
As its name implies, the time measured is the wall time, i.e. it is the difference between the time at which a task finishes and the time at which the task started and not the actual time spent by the computer to solve a problem. For instance, on our computer, the program in golomb1.cc for n = 9 takes Time: 4,773 seconds
59
3.6. A second model and its implementation
3.5.2 Failures 3.5.3 Branches 3.5.4 SearchLimits
3.6 A second model and its implementation n(n 1)
Our first model is really bad. One of the reasons is that we use too many variables: 2− differences. 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/golomb3.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 X(n + 1); X[0] = s.MakeIntConst(-1); // The solver doesn’t allow NULL pointers // X[1] = 0 X[1] = s.MakeIntConst(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 n 2 − 1 as an upper bound for the marks: // Upper bound on G(n), only valid for n <= 65 000
CHECK_LE(n, 65000); const int64 max = n * n - 1; ... for (int i = 2; i <= n; ++i) { X[i] = s.MakeIntVar(1, max, StringPrintf("X%03d", i)); }
This time we 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 constraints9 :
X [ j] − X [i]̸ = X [l] − X [k] 9
60
∀ 1 i,k,j,l n.
Quaternary constraints is just a fancy way to say that the constraints each involves four variables.
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem
= l . For instance, combination We don’t need all combinations of (i,j,k,l) with i̸ = k and j ̸ (3, 2, 6, 4) and combination (2, 3, 4, 6) would both give the same constraint. One way to avoid such redundancy is to impose an order on unique positive differences10 . Take again n = 4 and define the sequence of differences as in Figure 3.5. 1
st nd
2
3 4
rd
th th
5
6
th
Figure 3.5: Another ordered sequence of differences for the Golomb ruler of order 4. 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 st
1
nd
2
=
rd
3
=
th
4
=
5
=
th
th
6
=
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. 2
nd
rd
3
= 4
= = =
th th
5 th
6
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); 10
In section Breaking symmetries with constraints we’ll use another trick.
61
3.6. A second model and its implementation
CHECK_GE(i, 1); CHECK_GT(j, 1); if (j == n) { if (i == n - 1) { return false; } else {
*next_i = i + 1; *next_j = i + 2; } } else { *next_i = i; *next_j = j + 1; } return true;
}
If there is a next interval, the function next_interval() returns true, false otherwise. We can now construct our quaternary constraints11 : IntVar* diff1; IntVar* diff2; int k, l, next_k, next_l; for (int i = 1; i < n - 1; ++i) { for (int j = i + 1; j <= n; ++j) { k = i; l = j; diff1 = s.MakeDifference(X[j], X[i])->Var(); diff1->SetMin(1); while (next_interval(n, k, l, &next_k, &next_l)) { diff2 = s.MakeDifference(X[next_l], X[next_k])->Var(); diff2->SetMin(1); s.AddConstraint(s.MakeNonEquality(diff1, diff2)); k = next_k; l = next_l; } } }
Note that we set the minimum value of the difference to 1, diff1->SetMin(1) , to ensure that the differences are positive and 1. Note also that the method MakeDifference() 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 simplyname1 - name2 . Let’s compare the first and second implementation. The next table compares some global statistics about the search for G(9). 11
62
Remember again the remark at the beginning of this chapter about the tricky sums.
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem Statistics Time (s) Failures Branches Backtracks
Impl1 4,712 51 833 103 654 51 836
Impl2 48,317 75 587 151 169 75 590
If the 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/golomb4.cc . Let’s improve our second model by using variables to denote the differences and define variables Y[i][j] = X[j] - X[i]: std::vector >Y(n + 1, std::vector(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 Time (s) Failures Branches Backtracks
Impl1 4,712 51 833 103 654 51 836
Impl2 48,317 75 587 151 169 75 590
Impl2+ 1,984 53 516 107 025 53 519
Although we have more failures, more branches and we do backtrack more than in the first model, we were able to divide the time by 2! Can we do better? You bet!
63
3.7. A third model and its implementation
3.7 A third model and its implementation By using the same variables Y[i][j] = X[j] - X[i] in our improved version of our second model in the previous section, we were able to couple the effect of the propagations of the “quaternary constraints”. But these constraints lack a global vision of the propagation and this is were the global AllDifferent constraint comes into the picture. You can find the code in the file tutorials/cplusplus/chap3/golomb5.cc . Let’s replace the quaternary constraints by the AllDifferent constraint on the Y variables: std::vector Y; for (int i = 1; i <= n; ++i) { for (int j = i + 1; j <= n; ++j) { IntVar* const diff = s.MakeDifference(X[j], X[i])->Var(); Y.push_back(diff); diff->SetMin(1); } } s.AddConstraint(s.MakeAllDifferent(Y));
and compare our three implementations, again to compute G(9): Statistics Time (s) Failures Branches Backtracks
Impl1 4,712 51 833 103 654 51 836
Impl2 48,317 75 587 151 169 75 590
Impl2+ 1,984 53 516 107 025 53 519
Impl3 0,338 7 521 15 032 7 524
What an improvement! In the next section, we present two strategies that generally allow to tighten a given model and thus improve, sometimes dramatically, the search time.
3.8 How to tighten the model? Generally speaking, if we are able to reduce the size of the search tree (to tighten the model), we can speed up the search. We are talking about visiting (preferably implicitly) the whole search tree to be able to prove optimality (other techniques exist to find good nodes in the search tree). We present two12 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 techniques13 . You can find the code in the file tutorials/cplusplus/chap3/golomb6.cc . There exist other techniques. Later, in section XXX, we will see how over-constraining can improve the search. 13 This short explanation is certainly too simple to describe all the subtleties of search strategies. After all, modelling is an art! 12
64
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem
3.8.1 Breaking symmetries with constraints In the section 3.6, when we declared the variables X representing the marks of a Golomb ruler, we implicitly took for granted that X[1] < X[2] < ... < X[n] . That is exactly what we did when we imposed the differences to be positive: IntVar* diff1; IntVar* diff2; int k, l, next_k, next_l; for (int i = 1; i < n - 1; ++i) { for (int j = i + 1; j <= n; ++j) { k = i; l = j; diff1 = s.MakeDifference(X[j], X[i])->Var(); diff1->SetMin(1); while (next_interval(n, k, l, &next_k, &next_l)) { diff2 = s.MakeDifference(X[next_l], X[next_k])->Var(); diff2->SetMin(1); s.AddConstraint(s.MakeNonEquality(diff1, diff2)); k = next_k; l = next_l; } } }
In trying to avoid duplicating certain quaternary constraints, we actually declared implicitly to the solver that X[1] < X[2] < ... < X[n] . Hadn’t we done so, there was no way the solver could have guessed that the marks are in an increasing sequence14 . For the solver, the solution
X [1] = 0, X [2] = 1, X [3] = 4, X [4] = 6
(3.1)
X [1] = 4, X [2] = 1, X [3] = 6, X [4] = 0
(3.2)
and the solution
would have been two different solutions and we would explicitly have had to tell the solver not to generate the second one: for (int i = 1; i < n; ++i) {
s.AddConstraint(s.MakeLess(X[i],X[i+1])); }
Thanks to diff1->SetMin(1) and diff2->SetMin(1) and the two for loops, the ordered variables X [1], X [2], X [3], X [4] have only increasing values, i.e. if i j then X [i] 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. 14
Declaring variables in an std::vector doesn’t tell anything about their respective values!
65
3.8. How to tighten the model? 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 SymmetryBreaker s) so that it generates itself the constraints to break symmetries. These constraints are generated on the fly during the search!
3.8.2 Better bounds helps In all implementations, we used n 2 − 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 25 as these bounds can be obtained by projective and affine projections in the plane15 . 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 > Y(n + 1, std::vector(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: 15
These transformations were discovered in the beginning of the 20th century without any computer! See http://www.research.ibm.com/people/s/shearer/grtab.html.
66
Chapter 3. Using objectives in constraint programming: the Golomb ruler problem
Constraint * AllDifferent(Solver* s, const std::vector > & vars) { std::vector 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] 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] 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 Time (s) Failures Branches Backtracks
Impl1 4,712 51 833 103 654 51 836
Impl2 48,317 75 587 151 169 75 590
Impl2+ 1,984 53 516 107 025 53 519
Impl3 0,338 7 521 15 032 7 524
tightened Impl3 0,137 2288 4572 2291
The interested reader can find other dynamic bounds in [GalinierEtAl].
67
3.9. How does the solver optimize?
3.9 How does the solver optimize? 3.10 Summary Not a good approach. For example with our tightened implementation 3 we need 35.679 seconds to solve G(11). Further reading: see [SmithEtAl]
68
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?
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 (Decision s, DecisionBuilder s, ...) 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. 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 we have already met in chapter 1. Prerequisites:
• Basic knowledge of C++. • Basic knowledge of Constraint Programming and the n-queens problem (see chapter 1). • Basic knowledge of the Constraint Programming Solver (see chapter 2). • The willingness to roll up your sleeves and be prepared to look a little under the hood. Classes under scrutiny:
Decision , DecisionBuilder , DecisionVisitor , SearchMonitor , TreeMonitor . 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
Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap5 . The files inside this directory are: • Makefile. • nqueens_utilities.h : Contains two helper functions to test the number of solutions found and to print a solution. • nqueen1.cc: A first implementation of our basic model to find all solutions. • nqueen2.cc: The same implementation as in nqueen1.cc but this time to find only one solution. • nqueen3.cc: The same implementation as in nqueen2.cc but this time we use a TreeMonitor to visualize the search with cpviz. • nqueen4.cc: The same implementation as in nqueen3.cc but with some added statistics. • cpviz_nqueens4_basic.txt : cleaned output of ./nqueens4 --size=4 --cp_trace_search --cp_trace_propagation . • solver_benchmark.h : a basic SolverBenchmark class to benchmark different search strategies. • phases1.cc: we use the SolverBenchmark class to test different search strategies to find the next variables and values to branch on among the predefined choices in the IntVarStrategy and IntValueStrategy enum s. • nqueen5.cc: • nqueen6.cc: • nqueen7.cc:
5.1 The n-queens problem We have discussed the n-queens problem (and defined what a solution is) in chapter 1.
5.1.1 The n-queen problem in more details In the general n-queens problem , a set of n queens is to be placed on an n x n chessboard so that no two queens attack each other. Little is known that 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 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.
74
Chapter 5. Defining search primitives: the n-queens problem
al. proposed a simple algorithm to return a solution of the n-queens problem [Hoffman1969]. So we have to be careful when we talk about the n-queens problem. There are at least three different problems that people refer to when talking about the n-queens problem: • 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: unique: distinct:
1 1 1
2 0 0
3 0 0
4 1 2
5 2 10
6 1 4
7 6 40
8 12 92
9 46 352
10 92 724
11 341 2,680
12 1,787 14,200
13 9,233 73,712
14 45,752 365,596
Notice that there are more solutions for n = 5 than n = 6. What about the last three known values? Here there are: n: unique: distinct:
24 28,439,272,956,934 227,514,171,973,736
25 275,986,683,743,434 2,207,893,435,808,352
26 2,789,712,466,510,289 22,317,699,616,364,044
Quite impressive, isn’t it? It’s even more impressive when you know that these numbers were obtained by explicitly finding all these solutions! Is the n-queens problem only a “toy” problem?
While the n-queens problem is a wonderful problem to study backtracking systems and is intensively used in benchmarks to test these systems, there are real problems that can be modelled and solved as n-queens problems. For instance, it has been used for parallel memory storage schemes, VLSI testing, 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. 3 4
By solution, we mean feasible solution. These two problems are NP-Hard. See [Jordan2009].
75
5.1. The n-queens problem Describe What is the goal of the n-queens problem? We will focus on finding one or all solutions. Given a size n for the n × n chessboard, place n queens5 so that no two queens attack each other. What are the decision variables (unknowns)? We have different choices. One clever way to reduce the number of variables is to introduce only one variable for each queen. What are the constraints? No two queens can attack each other. This means to place 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 queen. Figure 5.1 illustrates the variables we will use to solve the n-queens problem in this chapter.
Figure 5.1: Variables to model the n-queens problem. The solution depicted is { 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.
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
It is not obvious that for every n , there exist at least a solution. In fact, for n = 2 and n = 3 there are no solution. Hoffman et al. proved that there are solutions for every n 4 in [Hoffman1969].
76
Chapter 5. Defining search primitives: the n-queens problem
5.2 Implementation of the basic model You can find the code in the file tutorials/cplusplus/chap5/nqueens1.cc . After the needed headers from the or-tools library: #include #include #include #include
"base/commandlineflags.h" "base/logging.h" "base/stringprintf.h" "constraint_solver/constraint_solver.h"
we add our header: #include "./nqueens_utilities.h"
The header nqueens_utilities.h contains some helper functions (see the subsection The helper functions below): among other CheckNumberOfSolutions() to check the known number of solutions (unique or distinct) of the n-queens problem and several functions to print the solutions recorded by a SolutionCollector . To be able to collect only unique solutions (up to a symmetry), we will use SymmetryBreaker s in section 5.8 page 126. A boolean gflag FLAGS_use_symmetry allows or disallows the use of SymmetryBreaker s. This flag is defined in the header ./nqueens_utilities.h and 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) << "Symmetries not yet implemented!"; } if (FLAGS_size != 0) { operations_research::NQueens(FLAGS_size); } else { for (int n = 1; n < 12; ++n) { operations_research::NQueens(n); } } return 0; }
We offer the possibility to print the first solution (flag print set to true) or all solutions (flag print_all set to true)6 . By default, the program doesn’t output any solution. 6
The code to print all the solutions is not shown here.
77
5.2. Implementation of the basic model
5.2.1 The model The model is 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 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.
Figure 5.2: A solution with no queen on the same row. Of course, this is not what we want. To forbid two queens to be on the same diagonal with slope +1 (diagonals that slope up-and-right), we could impose non-equality relations between our variables. For instance, to impose that the first queen represented by x 0 doesn’t attack any other queen on those diagonals, we can impose that
x0 − 1̸ = x 1, x0 − 2̸ = x 2 , x0 − 3̸ = x 3 , . . .
(5.1)
x0̸ = x 1 + 1, x0̸ = x 2 + 2, x0̸ = x 3 + 3, . . .
(5.2)
(5.1) is equivalent to
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̸ = x 2, x1 − 2̸ = x 3 , x1 − 3̸ = x 4 , . . .
(5.3)
x1̸ = x 2 + 1, x1̸ = x 3 + 2, x1̸ = x 4 + 3, . . .
(5.4)
or equivalently
78
Chapter 5. Defining search primitives: the n-queens problem In general, for queen xi , we impose that x̸ i = x j + j − i. Now, here comes the trick. If you add 1 to all members of (5.4), you get
x1 + 1̸ = x 2 + 2, x1 + 1̸ = x 3 + 3, x1 + 1̸ = x 4 + 4, . . . and more generally x̸ ̸ = x j + j i = x j + j − i becomes simply x i + i
(5.5)
∀ j : j > i7 .
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)
(5.7)
With a similar reasoning,
AllDifferent (x0 , x1 − 1, x2 − 2, x3 − 3, x4 − 4, . . .)
ensures that no two queens are on the same diagonal with slope−1 (diagonals that slope downand-right). We can thus add: std::vector 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 SolutionCollector s as usual: SolutionCollector* const solution_counter = s.MakeAllSolutionCollector(NULL); SolutionCollector* const collector = s.MakeFirstSolutionCollector(); collector->Add(queens); std::vector monitors; monitors.push_back(solution_counter); monitors.push_back(collector);
We keeps our basic search strategy: DecisionBuilder* const db = s.MakePhase(queens, Solver::CHOOSE_FIRST_UNBOUND, Solver::ASSIGN_MIN_VALUE); s.Solve(db, monitors);
// go!
In the next sections, we will test different DecisionBuilder s. 7
∀ j : j > i simply means that we consider all j greater than i.
79
5.2. Implementation of the basic model
5.2.2 The helper functions To test our model (and the solver!), we use the function CheckNumberOfSolutions() to check the number of known solutions, unique up to a symmetry when we use SymmetryBreaker s and otherwise distinct: void CheckNumberOfSolutions(int size, int num_solutions) { if (FLAGS_use_symmetry) { if (size - 1 < kKnownUniqueSolutions) {
CHECK_EQ(num_solutions, kNumUniqueSolutions[size - 1]); } else if (!FLAGS_cp_no_solve) { CHECK_GT(num_solutions, 0); } } else { if (size - 1 < kKnownSolutions) { CHECK_EQ(num_solutions, kNumSolutions[size - 1]); } else if (!FLAGS_cp_no_solve) { CHECK_GT(num_solutions, 0); } } return;
}
kNumUniqueSolutions[] and kNumSolutions[] are static arrays with the proven number of solutions. We restrict ourselves to testing the number of all distinct solutions up to kKnownSolutions = 18 and unique solutions up to kKnownUniqueSolutions = 19. By unique solution we mean a unique solution up to a symmetry (see the section Breaking symmetries with SymmetryBreakers for more). The print helper functions are all based on PrintSolution() : void PrintSolution(const int size, const std::vector& 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(collector->Value(solution_number, queens[i]));
std::cout << std::setw(2); if (pos == j) { std::cout << i; } else { std::cout << "."; } std::cout << " "; } std::cout << std::endl; } }
80
Chapter 5. Defining search primitives: the n-queens problem
return;
}
You might wonder why we cast the return value of collector->Value() into an int? The value() method returns an int64.
5.2.3 First results Because finding all solutions is hard, we expect the solver to face more and more difficulties 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 First solution All Solutions
10 0 0,055
11 0 0,259
12 0 1,309
13 0 7,059
14 0,003 40,762
To 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 First solution
25 0,048
26 0,392
27 0,521
28 3,239
29 1,601
30 63,08
31 14,277
It looks like our solver has some troubles to 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.
5.3 Basic working of the solver: the search algorithm Basically, the CP solver consists of three main components: • the main search algorithm that permits to traverse/construct the search tree and to call the callbacks at the right moments; • the Trail that is responsible for reversibility (when backtracking, you have to restore the previous states) and • the Queue where the propagation takes place thanks to the Demons. In this section, we only discuss the main search algorithm. We present a simplified version of the main search algorithm. Although far from being complete, it gathers all the necessary basic elements and allows you to understand when some of the callbacks of the SearchMonitor s are called.
81
5.3. Basic working of the solver: the search algorithm
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 14 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]̸ = 2 at another branch like in Figure 5.3.
xi
= 2
xi
=2
Figure 5.3: The search space is divided in two search sub-trees Each subspace is now smaller and we hope easier to solve. We continue this divide and conquer mechanism until we know that a subspace 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 = 2) in another, we obtain a binary search trees8 . This way of refuting this decision (x[i] ̸ 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.
8
82
Not to be confused with a binary search tree (BST) used to store ordered sets.
Chapter 5. Defining search primitives: the n-queens problem
What exactly is a search tree?
A search tree is more a concept than a real object. It is made of nodes but these nodes 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 available. In this section, we redefine some virtual functions of an abstract class. In section XXX, we will see another similar mechanism. An example will clarify this mechanism. Take a SearchMonitor class. If you want to implement your own search monitor, you inherit from SearchMonitor and you 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.8.3).
Phases The CP solver allows you to combine several searches, i.e. different types of sub-searches. You can search a subtree of the search tree differently from the rest of your search. This is called nested search while the whole search is called a top-level search. There are no limitations and you can nest as many searches as you like. You can also restart a (top level or nested) search.
83
5.3. Basic working of the solver: the search algorithm In or-tools, each time you use a new DecisionBuilder , we say you are in a new phase. This is where the name MakePhase comes from.
5.3.2 The basic idea The basic idea9 is very simple yet effective. A DecisionBuilder is responsible to return a Decision at a node. A decision would be for instance, 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]̸ = 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.4: 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 (see the box What is a preorder traversal of a binary tree?) of the binary search tree. What is a pre-order traversal of a binary tree?
The search tree depicted on the Figure The actual search tree of our search has its node numbered in the order given by a pre-order traversal. There are two other traversals: inorder and post-order . We invite the curious reader to google pre-order traversal of a tree to 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 SearchMonitor s 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: 1 2 3 4 5
bool Solver::Solve(DecisionBuilder* const db, SearchMonitor* const * monitors, int size) {
NewSearch(db, monitors, size); searches_.back()->set_created_by_solve(true); 9
84
// Overwrites default.
The real code deals with a lots of subtleties to implement different variants of the search algorithm.
Chapter 5. Defining search primitives: the n-queens problem
6 7 8 9 10
NextSolution(); const bool solution_found = searches_.back()->solution_counter() > 0; EndSearch(); return solution_found; }
searches_ is an std::vector of Search es 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 SearchMonitor s. 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 SearchMonitor s contain a set of callbacks called on search tree events, such as entering/exiting search, applying/refuting decisions, failing, accepting solutions... In this section, we present the callbacks of the SearchMonitor class10 listed in Table 5.1 and show you exactly when they are called in the search algorithm. We draw again your attention to the fact that the algorithm shown here is a 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 details11 . 10
There are a few more callbacks defined in a SearchMonitor . See XXX Of course, you can have a peak right now but some more background will probably help you understand this mechanism better. Beside, you don’t need to understand the inner mechanism to be able to use nested search! 11
85
5.3. Basic working of the solver: the search algorithm
Table 5.1: Basic search algorithm callbacks from the SearchMonitor class. Methods
Descriptions Beginning of the search. EnterSearch() End of the search. ExitSearch() BeginNextDecision(DecisionBuilder * Before calling DecisionBuilder::Next() .
const b) EndNextDecision(DecisionBuilder * const b, Decision * const d) ApplyDecision(Decision * const d) RefuteDecision(Decision * const d) AfterDecision(Decision * const d, bool apply)
BeginFail() EndFail() BeginInitialPropagation() EndInitialPropagation() AcceptSolution()
AtSolution()
NoMoreSolutions()
After calling DecisionBuilder::Next() , along with the returned decision. Before applying the Decision. Before refuting the Decision. Just after refuting or applying the Decision, apply is true after Apply(). This is called only if the Apply() or Refute() methods have not failed. Just when the failure occurs. After completing the backtrack. Before the initial propagation. After the initial propagation. This method is called when a solution is found. It asserts if the solution is valid. A value of false indicates that the solution should be discarded. This method is called when a valid solution is found. If the return value is true, then search will resume. If the result is false, then search will stop there. When the search tree has been visited.
To follow the main search algorithm, it is best to know in what states the solver can be. The enum SolverState enumerates the possibilities in the following table: Value
OUTSIDE_SEARCH IN_ROOT_NODE IN_SEARCH AT_SOLUTION NO_MORE_SOLUTIONS PROBLEM_INFEASIBLE
Meaning Before search, after search. Executing the root node. Executing the search code. After successful NextSolution() and before EndSearch(). After failed NextSolution() and before EndSearch() . After search, the model is infeasible.
NewSearch()
This is how the NewSearch() method might have looked in a 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. 1 2 3
void Solver::NewSearch(DecisionBuilder* const db, SearchMonitor* const * monitors, int size {
4 5 6
Search* const search = searches_.back(); state_ = OUTSIDE_SEARCH;
86
Chapter 5. Defining search primitives: the n-queens problem
7
13
// // // // // //
14
...
8 9 10 11 12
Init: Install Install Install Install Install
the main propagation monitor DemonProfiler if needed customer’s SearchMonitors DecisionBuilder’s SearchMonitors print trace if needed
15 16
search->EnterSearch();
// SEARCHMONITOR CALLBACK
17
// Set decision builder.
18 19
search->set_decision_builder(db);
state_ = IN_ROOT_NODE; search->BeginInitialPropagation();
20 21 22
// SEARCHMONITOR CALLBACK
23
try {
24
//
25
Initial constraint propagation
ProcessConstraints(); search->EndInitialPropagation(); ... state_ = IN_SEARCH; } catch (const FailException& e) { ... state_ = PROBLEM_INFEASIBLE; }
26 27 28 29 30 31 32 33
// SEARCHMONITOR CALLBACK
34 35
36
}
return;
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: 1 2 3
bool Solver::NextSolution() { Search* const search = searches_.back();
Decision* fd = NULL;//
failed decision
4
//
5 6
Take action following solver state
switch (state_) {
87
5.3. Basic working of the solver: the search algorithm
case PROBLEM_INFEASIBLE: return false; case NO_MORE_SOLUTIONS: return false; case AT_SOLUTION: {// We need to backtrack
7
8
9
10
11
// SEARCHMONITOR CALLBACK // BacktrackOneLevel() calls search->EndFail() if (BacktrackOneLevel(&fd)) {// No more solutions. search->NoMoreSolutions();// SEARCHMONITOR CALLBACKS
12 13 14
16
17
15
state_ = NO_MORE_SOLUTIONS; return false; } state_ = IN_SEARCH; break;
18 19
20
}
21
case OUTSIDE_SEARCH: {
22
23 24
state_ = IN_ROOT_NODE; search->BeginInitialPropagation();// SEARCHMONITOR CALLBACKS try { ProcessConstraints(); search->EndInitialPropagation();// SEARCHMONITOR CALLBACKS ... state_ = IN_SEARCH; } catch(const FailException& e) { ... state_ = PROBLEM_INFEASIBLE; return false; } break;
25
26 27 28
29 30 31 32
33
34
35
}
36
case IN_SEARCH: break;
37 38
39
}
40 41
DecisionBuilder* const db = search->decision_builder();
42
//
43 44 45
MAIN SEARCH LOOP TO FIND THE NEXT SOLUTION IF ANY
volatile bool finish = false; volatile bool result = false;
while (!finish) {//
46 47
Try to find next solution
try {
48
//
Explore right branch of the tree on backtrack if (fd != NULL) {// We have a right branch
49 50
... search->RefuteDecision(fd);// SEARCHMONITOR CALLBACK fd->Refute(this); search->AfterDecision(fd, false);// SEARCHMONITOR CALLBACK ... fd = NULL;
51
52 53
54 55 56
}
57 58
//
59 60
Decision* d = NULL; //
61
88
Explore left branches of the tree Go left as often as possible
Chapter 5. Defining search primitives: the n-queens problem
while (true) {// Trying to branch left
62
63
search->BeginNextDecision(db);// SEARCHMONITOR CALLBACK d = db->Next(this); search->EndNextDecision(db, d);// SEARCHMONITOR CALLBACK
64
65 66
//
67
if (d == fail_decision_) {
68
Dead-end? This is a shortcut
search->BeginFail();// SEARCHMONITOR CALLBACK // fail now instead of after 2 branches.
69
throw FailException();
70 71
}
72
//
73
if (d != NULL) {
74 75
76 77 78
79 80
search->ApplyDecision(d);// SEARCHMONITOR CALLBACK d->Apply(this); search->AfterDecision(d, true);// SEARCHMONITOR CALLBACK ... } else {// No Decision left, the DecisionBuilder has finished break; } }//
81
Explore next left branch of the tree
while (true)
82
// We can not go further left... test Solution // SEARCHMONITOR CALLBACK if (search->AcceptSolution()) {// Accept Solution // SEARCHMONITOR CALLBACK
83 84 85 86
if (!search->AtSolution() || !CurrentlyInSolve()) {
87
result = true; finish = true; } else { search->BeginFail();// SEARCHMONITOR CALLBACK throw FailException(); } } else { search->BeginFail();// SEARCHMONITOR CALLBACK throw FailException(); } } catch (const FailException& e) {
88 89 90 91 92 93 94 95 96 97 98
// // //
We must backtrack SEARCHMONITOR CALLBACK BacktrackOneLevel() calls search->EndFail() if (BacktrackOneLevel(&fd)) { // no more solutions. search->NoMoreSolutions();// SEARCHMONITOR CALLBACK
99 100 101 102
103 104 105
result = false; finish = true; }
106 107
}
108
}//
while (!finish)
109
//
110
Set solver current state
... state_ = ...;
114
return result;
115
}
111 112 113
89
5.3. Basic working of the solver: the search algorithm
Let’s dissect the algorithm. First of all, you might wonder where does the propagation take place? In a few words: Constraint s are responsible of attaching Demons to variables. These Demons are on their turn responsible for implementing the actual propagation. Whenever the domain of a variable changes, the corresponding Demons are triggered. In the main search algorithm, this happens twice: when we Apply() a Decision (line 75) and when we Refute() a Decision (line 53). Back to the algorithm. On line 2, the solver grabs the last search. Indeed, several searches can be nested and queued. The Search object is responsible of monitoring the search for one DecisionBuilder (one phase) and triggers the callbacks of the installed SearchMonitor s 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 defined12 • finish: becomes true when the search is over; • result: denotes if a feasible solution was indeed found or not. These two variables are declared volatile to allow their use between setjmp and longjmp , otherwise the compiler might optimize certain portions of code away. Basically, it tells the compiler that these variables can be changed from the outside. This main loop starts at line 47 and ends at line 108. The try - catch mechanism allows to easily explain the backtrack mechanism. Whenever we need to backtrack in the search, a FailException is thrown13 . If the Decision pointer fd is not NULL, this means that we have backtracked to the 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 thewhile loop (line 62-81). The DecisionBuilder produces its next Decision on line 64. If it detects that this branch is a dead-end, it is allowed to return a FailDecision which the solver tests at line 67. If the search tree is empty, the DecisionBuilder returns NULL. The solver tests this possibility on line 73. If the DecisionBuilder found a next Decision, it is applied on line 75. 12
These two variables play a role when we use nested searches, restart or finish a search but these possibilities are not shown here. 13 Did we already mention that the try - catch mechanism is not used in the production code? ;-)
90
Chapter 5. Defining search primitives: the n-queens problem 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 SearchMonitor s no matter what they return, i.e. you are guaranteed that all SearchMonitor s will be called. If one SearchMonitor has its method AcceptSolution() returning false, search->AcceptSolution() returns false. On the contrary, if only one SearchMonitor has its AtSolution() method returning true, search->AtSolution() returns true. The test on line 87 is a little bit complex: test = !search->AtSolution() || !CurrentlyInSolve()
Remember that AtSolution() returns true if we want to resume the search (i.e. if at least one SearchMonitor->AtSolution() returns true), false otherwise. CurrentlyInSolve() returns true if the solve process was called with the Solve() method and false if it was called with the NextSolution() method. Thus, test is true (and we stop the search in NextSolution() ) if all SearchMonitor s decided to stop the search (search->AtSolution() returns then false) or if at least one SearchMonitor decided to continue but the solve process was called by NextSolution() . Indeed, a user expects NextSolution() to stop whenever it encounters a feasible solution. Whenever a backtrack is necessary, a FailException is caught and the solver backtracks to the next available right branch if possible. Finally, the current state of the solver is set and the method NextSolution() returns if a solution has been found and accepted by all SearchMonitor s 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, contrary to intuition, a solution may not have all variables of the model bound. It is the responsibility of the DecisionBuilder to keep returning decisions until all variables are indeed bound. The most extreme counterexample is calling Solve() with a trivial DecisionBuilder whose Next() method always returns NULL. In this case, Solve() immediately returns true, since not assigning any variable to any value is a solution, unless the root node propagation discovers that the model is infeasible. EndSearch()
The EndSearch() method cleans the solver and if required, writes the profile of the search in a file. It also calls the ExitSearch() callbacks of all installed SearchMonitor s.
91
5.4. cpviz: how to visualize the search
Here is how it might have looked in a simplified version of the main search algorithm. 1 2 3 4 5 6 7 8 9 10 11
void Solver::EndSearch() { Search* const search = searches_.back();
... search->ExitSearch();// SEARCHMONITOR CALLBACK search->Clear(); state_ = OUTSIDE_SEARCH; if (!FLAGS_cp_profile_file.empty()) { LOG(INFO) << "Exporting profile to " << FLAGS_cp_profile_file; ExportProfilingOverview(FLAGS_cp_profile_file); } }
5.4 cpviz: how to visualize the search To get a better feeling of the way the CP solver explores the search tree, we will use the wonderful open-source visualization toolkit for 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 SearchMonitor s. 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 SearchMonitor s in your code: vector monitors; ... SearchMonitor* const cpviz = s.MakeTreeMonitor(vars, "tree.xml", "visualization.xml"); monitors.push_back(cpviz);
You need also a configuration file (named configuration.xml ) as this one:
92
Chapter 5. Defining search search primitives: the n-queens problem
xsi:noNamespaceSchemaLocation="configura xsi:noNamespaceSchemaLocat ion="configuration.xsd" tion.xsd" xmlns:xsi="h xmlns:xsi="http:// ttp:// www.w3.org/2001/XMLSchema-instance"> show="viz "viz" " type= type="lay "layout" out" disp display=" lay="expa expanded" nded" repe repeat=" at="all" all"
Basically, it tells cpviz to produce the graphic files for the search tree (show="tree" ) and the variables (show="viz" ) in the directory /tmp. If you are really lazy, we even provide a factory method which generates automatically a default configuration file: SearchMonitor* const cpviz = s.MakeTreeMonitor(vars, "configuration.xml", "configuration.xml" , "tree.xml", "tree.xml" , "visualization.xml"); "visualization.xml" );
Afte Afterr your your searc search h is finish finished ed AND AND you have have calle called d (imp (impli licit citle ley y or expl explic icitl itly) y)EndSearch() 14 , you can run cpviz to digest the XML files representing your search by entering the viz/bin directory directory and typing: java ie.ucc.c ie.ucc.cccc. ccc.viz.V viz.Viz iz configur configuratio ation.xml n.xml tree.xml tree.xml visualiz visualizatio ation.xml n.xml
on a comman command d line into a termin terminal al near you. This This will produce produce the follow following ing picture 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 = 4 and cpviz generates 8 files. n-queens problem with n =
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 constructed during the search — is not really suited for a graphical representation as it can quickly become very big (compare the tree above with the actual search tree that is represented below). To avoid huge 14
tree.xml and visualization.xml are are gene genera rate ted d in the the ExitSearch() call callba back ck of the the TreeMonitor class.
93
5.4. cpviz: cpviz: how to visual visualize ize the searc search h
trees, we have reduced reduced their sizes by contracting several several nodes. nodes. Except for the dummy root node, each node corresponds to a variable during the search and only left branches are given x[1] = 2) and the explicitly. The numbers along the branches denote the applied decisions (like x[1] numbers in the right corner above the variable 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; solutions; • red denote sub-trees without any feasible solutions; • blue denote intermediate intermediate try nodes (these only exist during the search).
5.4.2 Inter Interpret preting ing the graphical graphical results You can find the code in the file tutorials/cplusplus/chap5/nqueens4.cc . To bette betterr under understa stand nd the the output output of cpviz and to follo follow w the the searc search h with with preci precisi sion, on, let’ let’ss trace the search and the propagation of our program nqueens4: ./nqueen ./nqueens4 s4 --size --size= =4 --cp_tra --cp_trace_se ce_search arch --cp_tra --cp_trace_pr ce_propag opagation ation 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. Reca Recall ll that that we are solvin solving g the the proble problem m of findi finding ng all all disti distinct nct soluti solution onss of the the n-que n-queens ens problem with 4 quee queens ns.. Our Our sear search ch stra strate tegy gy is to choo choose se the the first first varia ariabl blee with with a non non empty domain with a least two elements (Solver::CHOOSE_FIRST_UNBOUND ). Once this this vari variabl ablee is chose chosen, n, we give give it the sm small alles estt possi possibl blee valu valuee conta containe ined d in its its domain domain (Solver::ASSIGN_MIN_VALUE ). We have 4 variables x0 , x1 , x2 and an d x3 introduced in that order. The 3 constraints are all AllDifferent constraints introduced in the following order: AllDifferent(x0 , x1 , x2 , x3 )
1 , x2 + 2, 2 , x3 + 3) AllDifferent(x0 , x1 + 1, 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 reconstruct the search tree: As you can see, at each node, the solver took a Decision: the the left bran branch ch to apply the Decision and the right branch to refute this Decision . The leaf nodes in red denote subtrees that are not worth exploring explicitly: we cannot find any feasible solution along these branches branches of the tree. tree. The leaf nodes nodes in green denote denote on the contrary contrary feasible feasible solutions. solutions. The
94
Chapter 5. Defining search search primitives: the n-queens problem node 0
x0
=0
x0
=0
node 1
node 4 x0
x1
=2
x1
=1 x0
=2
=1
node 6 node 2
node 3
node 5 x0
x0
=2
=2
node 8 node 7 x1
=0
x1
=0 node 10
node 9
Figure 5.5: The actual search tree of our search nodes are numbered in the order of creation and we can see that the search tree is traversed in pre-order by the solver. In the file nqeens4.cc, we have printed some statistics about the search: std:: std::cout cout std:: std::cout cout std:: std::cout cout std:: std::cout cout std:: std::cout cout
<< "Numbe "Number r of sol soluti utions ons: : " << num_solutions << std:: std::endl; endl; << "Failures "Failures: : " << s.failures() << std:: std::endl; endl; << "Branches "Branches: : " << s.branches() << std:: std::endl; endl; << "Backtrac "Backtracks: ks: " << s.fail_stamp() << std:: std::endl; endl; << "Stamps "Stamps: : " << s.stamp() << std:: std::endl; endl;
and with size = 4, we get as output: Number Number of soluti solutions ons: : 2 Failures Failures: : 6 Branches Branches: : 10 Backtrac Backtracks: ks: 9 Stamps Stamps: : 29
Let’s Let’s see if we can deduce these statistics statistics from the search tree. The three first statistics are easy to spot in the tree: Number Number of solutions solutions (2): There are indeed two distinct solutions denoted by the two green leafs. Failures (6): A failure occurs whenever the solver has to backtrack, whether it is because of a real failure (nodes 2 − 3 and 9 − 10) or a success (nodes 5 and 7). Indeed, when the solver finds a solution, it has to backtrack to find other solutio solutions. ns. The metho method d failures() returns the number of leaves of the
95
5.4. cpviz: cpviz: how to visual visualize ize the searc search h 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): Bec Becaus ausee of the way the the searc search h is coded coded,, the fail_stamp counter starts already at 2 before before any top level search. 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 backtrack 15 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. search. It is related related to the queue queue actions actions during during the search. search. The queue 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 counter is increased. increased. Other Other queue actions actions also increase increase this this counter counter. For 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 statistic reflects the amount of work needed by the solver during during the search. search. We refer refer the curious curious reader reader to the source source code for more more details.
Our cpviz output of the search tree How can we compare the real tree with our cpviz output? output? The trick trick is to observe observe the construction of the tree one node at a time. We construct the real tree node by node from the tree produced by cpviz. The left image is the cpviz output while the right image is the actual tree.
Step 0:
We start with a dummy node. This node is needed in our construction. construction. You’ll see in a moment why.
Figure 5.6: Contruction of the real search tree from the cpviz tree: step 0
15
96
Actually, the very last backtrack happens when the solver is deleted.
Chapter 5. Defining search search primitives: the n-queens problem
Step 1:
node 0
(a) cpviz
(b) Real search tree
Figure 5.7: Construction of the real search tree from the cpviz tree: step 1 Next, we start with the actual cpviz o ou utput, the dummy root the little number 0 ne next to anything.
root node. As you can see in our node doesn’t even have a name and this non existing name doesn’t mean
Step 2:
node 0
x0
=0
node 1
(a) cpviz
(b) Real search search tree
Figure 5.8: Construction of the real search tree from the cpviz tree: step 2 You can see in our cpviz output that the solver has applied the Decision x0 = 0 but that it couldn’t couldn’t realize realize if this was a good choice choice or not. The little little number 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: x 0 ∈ {0, 1, 2, 3} before being assigned the value 0 .
97
5.4. cpviz: cpviz: how to visual visualize ize the searc search h
Step 3:
node 0
x0 =
0
node 1
x1 =
2
node 2
(a) cpviz
(b) Real search search tree
Figure 5.9: Construction of the real search tree from the cpviz tree: step 3 After having applied the Decision x 0 = 0 at step 2, the solver now applies the Decision x1 = 2 which leads, after propagation, propagation, to a failure. failure.
Step 4:
node 0
x0 =
x0
0
=0
node 1
x1 =
2
node 2
(a) cpviz
node 4
x1
=2
node 3
(b) Real search tree
Figure 5.10: Construction of the real search tree from the cpviz tree: step 4 Our cpviz output now clearly warns that taking 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 x 1 = nowhere. We have to backtrack and to ̸ 2 after the branch x 0 = 0 is leading nowhere. refute the Decision x0 = 0. We have thus a new branch x0̸ = 0 in the real search tree.
98
Chapter 5. Defining search primitives: the n-queens problem
Step 5:
node 0
x0
x0
=0
=0
node 1
node 4 x0 =
x1
=2
x1
node 2
(a) cpviz
1
=2
node 3
node 5
(b) Real search tree
Figure 5.11: 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 =
x0
0
=0
node 1
node 4 x0 =
x1 =
2
x1
1 x0
=2
=1
node 6 node 2
node 3
node 5 x0 =
2
node 7
(a) cpviz
(b) Real search tree
Figure 5.12: Construction of the real search tree from the cpviz tree: step 6 We 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
99
5.4. cpviz: how to visualize the search
Step 7:
node 0
x0 =
x0
0
=0
node 1
node 4 x0 =
x1 =
2
x1
1 x0
=2
=1 node 6
node 2
node 3
node 5 x0
x0
=2
=2 node 8
node 7
(a) cpviz
(b) Real search tree
Figure 5.13: Construction of the real search tree from the cpviz tree: step 7 We add a tentative branch in the cpviz output. The branch before we applied the Decision x2 = 0 that lead to a feasible solution, so now we know that the solver is trying to refute that decision: x 2̸ = 0.
Step 8:
node 0
x0 =
x0
0
=0
node 1
node 4 x0 =
x1 =
2
x1
1 x0
=2
=1 node 6
node 2
node 3
node 5 x0 =
x0
2
=2 node 8
node 7 x1 =
0
x1
=0 node 10
node 9
(a) cpviz
(b) Real search tree
Figure 5.14: Construction of the real search tree from the cpviz tree: step 8 The 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̸ = 1 both fail.
100
Chapter 5. Defining search primitives: the n-queens problem 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: x 0 ∈ {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: x 0 ∈ {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: x 0 ∈ {0}, x1 ∈ {2}, x2 ∈ {1, 3}, x3 ∈ {1, 2}. The propagation is as follow:
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x2 : 3 101
5.4. cpviz: how to visualize the search
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: x 0 ∈ {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
102
Chapter 5. Defining search primitives: the n-queens problem
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 x 2 is empty. We need to backtrack to the root node and refute the Decision x0 = 0. node 4: x 0 ∈ {1, 2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We Decision x0 = 1 which complies with our search strategy.
apply
node 5: x 0 ∈ {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
103
5.4. cpviz: how to visualize the search
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
104
Chapter 5. Defining search primitives: the n-queens problem
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: x 0 ∈ {2, 3}, x1 ∈ {0, 1, 2, 3}, x2 ∈ {0, 1, 2, 3}, x3 ∈ {0, 1, 2, 3}. We Decision x0 = 2.
apply
the
node 7: x 0 ∈ {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 105
5.4. cpviz: how to visualize the search
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
106
Chapter 5. Defining search primitives: the n-queens problem
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: x 0 ∈ {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: x 0 ∈ {3}, x1 ∈ {0}, x2 ∈ {0, 2}, x3 ∈ {1, 2}. Propagation:
AllDifferent(x0 , x1 − 1, x2 − 2, x3 − 3) :
x3 : 2
107
5.4. cpviz: how to visualize the search
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 x 2 is empty, we have failure and have to backtrack to node 8 and refute Decision x1 = 0. node 10: x 0 ∈ {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 x 2 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
108
Chapter 5. Defining search primitives: the n-queens problem
nodes in the tree, we are constrained to display very little information about the propagation process. In short, if we 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 column 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.15: cpviz output of the propagation: step 0 Nothing happens as we add a dummy root node. Notice that the variables are numbered from 1 to 4.
109
5.4. cpviz: how to visualize the search
Step 1:
node 0
(a) cpviz tree
(b) Real search tree
(c) cpviz propagation
Figure 5.16: cpviz output of the propagation: step 1 The yellow rectangle tells us that the focus is on variable 1(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.17: cpviz output of the propagation: step 2 The red square indicates that the variable x 0 was fixed to 0 . The dark green squares show the propagation. The focus is on variable 2 (x1 ).
110
Chapter 5. Defining search primitives: the n-queens problem
Step 3:
node 0
x0 =
0
node 1
x1 =
2
node 2
(a) cpviz tree
(b) Real search tree
(c) cpviz propagation
Figure 5.18: cpviz output of the propagation: step 3 The red rectangle warns of a failure: there is no feasible solution with x0 = 0 and x1 = 2.
Step 4:
node 0
x0 =
x0
0
=0
node 1
x1 =
2
node 2
(a) cpviz tree
node 4
x1
=2
node 3
(b) Real search tree
(c) cpviz propagation
Figure 5.19: cpviz output of the propagation: step 4 There is not much information here: only that the last variable tried was x 1 and that we ended up with a failure.
111
5.4. cpviz: how to visualize the search
Step 5:
node 0
x0 =
x0
0
=0
node 1
node 4 x0 =
x1 =
2
x1
node 2
(a) cpviz tree
1
=2
node 3
node 5
(b) Real search tree
(c) cpviz propagation
Figure 5.20: cpviz output of the propagation: step 5 Solution found.
Step 6:
no e 0
x0 =
x0
0
=0
node 1
node 4 x0 =
x1 =
2
x1
=
1 x0
2
=1
node 6 node 2
node 3
node 5 x0 =
2
node 7
(a) cpviz tree
(b) Real search tree
(c) cpviz propagation
Figure 5.21: cpviz output of the propagation: step 6 Solution found.
112
Chapter 5. Defining search primitives: the n-queens problem
Step 7:
node 0
x0
x0
=0
=0
node 1
node 4 x0 =
x1
=2
x1
1 x0
=2
=1 node 6
node 2
node 3
node 5 x0 =
2
x0
=2 node 8
node 7
(a) cpviz tree
(b) Real search tree
(c) cpviz propagation
Figure 5.22: cpviz output of the propagation: step 7 End of propagation at node 8 and focus on variable x1 .
113
5.5. Basic working of the solver: the phases
Step 8:
node 0
x0 =
x0
0
=0
node 1
node 4 x0 =
x1 =
2
x1
1 x0
=2
=1 node 6
node 2
node 3
node 5 x0 =
x0
2
=2 node 8
node 7 x1 =
0
x1
=0 node 10
node 9
(a) cpviz tree
(b) Real search tree
(c) cpviz propagation
Figure 5.23: cpviz output of the propagation: step 8 Failure. The 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 tree16 . 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 DecisionBuilder s and Decisions for scheduling until the dedicated section 6.3.4 in the next chapter. To better understand how phases and DecisionBuilder s work, we will implement our own DecisionBuilder and Decision classes in section 5.7. In this section, we show you how to use these primitives and some very basic examples17 . 16
Well, sort of. Read on! DecisionBuilder s and Decision s are used internally and you cannot access them directly. To use them, invoke the corresponding factory methods. 17
114
Chapter 5. Defining search primitives: the n-queens problem
5.5.1 DecisionBuilders and phases DecisionBuilder s (combined with SearchMonitor s) 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 DecisionBuilder s left to deal with it. We use DecisionBuilder s in two scenarios18 : 1. The basic scenario is to divide the search sub-tree in two (preferably non overlapping) search sub-trees. To do so, the DecisionBuilder returns a (pointer to a) Decision through its Next() method. The Decision class tells the solver what to do on the left branch (through its Apply() method) and the right branch (through its Refute() method). Some available DecisionBuilder s 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 DecisionBuilder s 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. 18
One could argue that these two scenarios are not really mutually exclusive. Indeed, we divide the scenarios in two cases depending on whether the DecisionBuilder returns a Decision or not. Some DecisionBuilder s delegate the creation process of Decision s to other DecisionBuilder s.
115
5.5. Basic working of the solver: the phases Some examples of available DecisionBuilder s that do some stuff at a node without splitting the search sub-tree in two: • StoreAssignment and RestoreAssignment : respectively store and restore Assignment s during the search. • AddConstraintDecisionBuilder : search.
adds a Constraint during the
• ApplyBranchSelector : changes the way the branches are selected. For instance, the left branch can become the right branch and vice-versa. Have a look at the Solver::DecisionModification enum for more. • LocalSearch : applies local search operators to 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* const extras): to add some extra SearchMonitors at the beginning of the search. Please note there are no checks at this point for duplication. • virtual string DebugString() const : method to give a name to your object.
the usual DebugString()
• virtual void Accept(ModelVisitor * const visitor) const: usual Accept() method to let you visit the model and take appropriate actions.
the
5.5.2 Decisions and DecisionVisitors The Decision class together with the DecisionBuilder class implement the branching rules of the search, i.e. how to branch (or divide the search sub-tree) at a given node in the search tree. Although a DecisionBuilder could return several types of Decision s during a search, we recommend to stick to one Decision for a DecisionBuilder per phase.
DecisionVisitor s 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. Decision s
The Decision class is responsible to tell the solver what to do on left branches through its Apply() method:
116
Chapter 5. Defining search primitives: the n-queens problem
virtual void Apply(Solver* const s) = 0;
and the right branch through its Refute() method: virtual void Refute(Solver* const s) = 0;
These two pure virtual methods must be implemented in every Decision class. A Decision object is returned by a DecisionBuilder through its Next() method. Two more methods can be implemented: • virtual string DebugString() const : method.
the usual DebugString()
• virtual void Accept(DecisionVisitor * const visitor) const : accepts the given visitor. Several Decision classes are available. We enumerate the different strategies implemented by the available Decision classes dealing with IntVars in the next section. In the next subsection, we detail a basic example. AssignOneVariableValue as an example
An obvious choice for a Decision class for IntVars is probably AssignOneVariableValue . This class assigns a value to a variable in the left branch and forbids this assignment in the right branch. The constructor takes the variable to branch on and the value to assign to it: AssignOneVariableValue(IntVar* const v, int64 val) : var_(v), value_(val) { }
var_ and value_ are local private copies of the variable and the value. The Apply() and Refute() methods are straithforward: void Apply(Solver* const s) {
var_ ->SetValue(value_); } void Refute(Solver* const s) {
var_ ->RemoveValue(value_); }
DecisionVisitor s
DecisionVisitor s are attached to Decisions. The corresponding methods of the DecisionVisitor are triggered just before a Decision is applied19 . 19
In this case, the methods are triggered when Decision objects are created and these objects are created just before their Apply() method is called. See the subsection Visitors for more.
117
5.5. Basic working of the solver: the phases When dealing with IntVars, two possibilities can be audited: • when a variable will be assigned a value, implement the virtual void VisitSetVariableValue(IntVar* const var, int64 value);
method. • when a variable domain will be splitted in two by a given value, implement the virtual void VisitSplitVariableDomain(IntVar* const var,
int64 value, bool start_with_lower_half);
method. If start_with_lower_half is true, the decision to be applied is var 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 DecisionVisitor s.
5.5.3 Combining DecisionBuilders We propose two ways to combine DecisionBuilder s: • Compose() : combine sequential searches, i.e. DecisionBuilder s are used one after the other; • Try(): combine parallel searches, i.e. DecisionBuilder s are used in parallel. You can of course combine the two. Compose()
Creates a DecisionBuilder which sequentially composes DecisionBuilder s. Solver s(...); ... DecisionBuilder * const db1 = ...; DecisionBuilder * const db2 = ...; DecisionBuilder * const db = s.Compose(db1, db2);
At each leaf of the search tree corresponding to the DecisionBuilder db1 , the second DecisionBuilder db2 is called. The DecisionBuilder db search tree will be as follows:
118
Chapter 5. Defining search primitives: the n-queens problem
db1 search tree
db2
db2
db2
db2
search tree
search tree
search tree
search tree
db search tree db = s.Compose(db1, db2); This composition of DecisionBuilder s frequently happens in scheduling. For instance, in the section The DecisionBuilders where we try to solve a Job-Shop Problem, the solving process is done in two consecutive phases: first we rank the tasks for each machine, then we schedule each task at its earliest start time. To do so, we Compose() two DecisionBuilder s. You can Compose() more than two DecisionBuilder s. There are two more specific methods to Compose() three and even four DecisionBuilder s. And if that is not enough, use DecisionBuilder* Compose(const std::vector& dbs);
where you can Compose() as many DecisionBuilder s as you like! Try()
Creates a DecisionBuilder which tries DecisionBuilder s in parallel. Solver s(...); ... DecisionBuilder * const db1 = ...; DecisionBuilder * const db2 = ...; DecisionBuilder * const db = s.Try(db1, db2);
The DecisionBuilder db1 and the DecisionBuilder db2 are each called from the top of the search tree one after the other. The DecisionBuilder db search tree will be as follows:
119
5.5. Basic working of the solver: the phases
db1
db2
search tree
search tree
db search tree db = s.Try(db1, db2); This combination is handy to try a DecisionBuilder db1 which partially explores the search space. If it fails, you can use the DecisionBuilder db2 as a backup. As with Compose(), you can Try() up to four DecisionBuilder s and use DecisionBuilder* Try(const std::vector& dbs);
for more. Beware that Try(db1, db2, db3, db4) will give an unbalanced tree to the right, whereas Try(Try(db1, db2), Try(db3, db4)) will give a balanced tree.
5.5.4 Nested searches Nested searches are searches in sub-trees that are initiated from a particular node in the global search tree. Another way of looking at things is to say that nested searches collapse a search tree described by one or more DecisionBuilder s and sets of SearchMonitor s 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 SearchMonitor s 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 SearchMonitor s and if you want to use more than four SearchMonitor s, use DecisionBuilder* MakeSolveOnce(DecisionBuilder* const db, const std::vector& monitors);
120
Chapter 5. Defining search primitives: the n-queens problem
NestedOptimize
NestedOptimize is similar to SolveOnce except that it seeks for an optimal solution instead of just a feasible solution. If there are no solutions in this nested tree, it fails. The factory method is MakeNestedOptimize() . Again, you can use none or up to four SearchMonitor s and use the version with an std::vector: DecisionBuilder* MakeNestedOptimize(DecisionBuilder* const db, Assignment* const solution, bool maximize, int64 step, const std::vector& 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. For std::vector and std::vector, see section 6.3 in the next chapter. The MakePhase() method is overloaded with different arguments and we discuss most of them in this subsection.
The 2-steps approach Variables and values are chosen in two steps: 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& 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.
121
5.5. Basic working of the solver: the phases Callbacks to the rescue What if you want to use your own strategies? One way to do this is to develop your own Decisions and DecisionBuilder s. Another way is to provide callbacks to the MakePhase() method. These callbacks evaluate different variables and values you can assign to a chosen variable. The best choice is each time the one that minimizes the values returned (through the Run() method) by the callbacks. We will explore both ways in the section 5.7. There are two types of callbacks20 accepted by MakePhase(): typedef ResultCallback1 IndexEvaluator1; typedef ResultCallback2 IndexEvaluator2;
IndexEvaluator1 allows to evaluate the next variable to branch on by giving the index of this variable in the std::vector 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& vars, IndexEvaluator1* var_evaluator, IndexEvaluator2* val_eval);
You provide both evaluators. DecisionBuilder* MakePhase(const std::vector& vars, IntVarStrategy var_str, IndexEvaluator2* val_eval, IndexEvaluator1* tie_breaker);
You use a predefined IntVarStrategy strategy to find the next variable to branch on, provide 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& 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. 20
122
If you want to know more about callbacks, see the section Callbacks in the chapter Under the hood .
Chapter 5. Defining search primitives: the n-queens problem 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& vars, IndexEvaluator2* evaluator, EvaluatorStrategy str);
and DecisionBuilder* MakePhase(const std::vector& vars, IndexEvaluator2* evaluator, IndexEvaluator1* tie_breaker, EvaluatorStrategy str);
You might wonder what the EvaluatorStrategy strategy is. The selection is done by scanning every pair . 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 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 primitives To choose among the IntVar variables and the int64 values when branching, several variables and values selection primitives are available. As stated before (see the subsection The 2-steps approach in the previous section for more), the selection is done in two steps: • First, select the variable; • Second, select an available value for this variable. To construct the corresponding DecisionBuilder , use one of the MakePhase() factory methods. For instance: DecisionBuilder* MakePhase(const std::vector& vars, IntVarStrategy var_str, IntValueStrategy val_str);
123
5.6. Out of the box variables and values selection primitives
5.6.1 IntVarStrategy enum s 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 IntVar s 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 IntVar s 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.
124
Chapter 5. Defining search primitives: the n-queens problem Most of the strategies are self-explanatory except maybe for CHOOSE_PATH . This selection strategy is most convenient when you try to 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 5
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 variable 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 enum s to select the next value The IntValueStrategy enum describes the strategies available to select the next value(s) for the already chosen variable at each node during the search: INT_VALUE_DEFAULT The default behaviour is ASSIGN_MIN_VALUE . INT_VALUE_SIMPLE The simple selection is ASSIGN_MIN_VALUE . ASSIGN_MIN_VALUE Selects the minimum available value of the selected variable. ASSIGN_MAX_VALUE Selects the maximum available value of the selected variable. ASSIGN_RANDOM_VALUE Selects randomly one of the available values of the selected variable.
125
5.7. Customized search primitives
ASSIGN_CENTER_VALUE Selects the 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 SolverBenchmarkStats . 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 outperforms any other. 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.
5.7 Customized search primitives 5.7.1 The basic search strategy visualized 5.7.2 First try: start from the center 5.7.3 Second try: dynamic variable selection 5.7.4 DecisionBuilders and Decisions more in details BaseAssignVariables as an example
5.8 Breaking symmetries with SymmetryBreakers Now that we have seen the Decision and DecisionVisitor classes in details and that we are trying to solve the n-queens problem, how could we resist to introduce SymmetryBreaker s? Breaking symmetries of a model or a problem is a very effective technique to reduce the size of the search tree and, most of the time, it also permits to reduce - sometimes spectacularly - the search time. We have already seen this effectiveness when we introduced a constraint to avoid
126
Chapter 5. Defining search primitives: the n-queens problem mirror Golomb rulers in section 3.8.1 page 65. This time, we will use SymmetryBreaker s. As their name implies, their role is to break symmetries. In contrast to explicitly adding symmetry breaking constraints in the model before the solving process,SymmetryBreaker s 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.24 represents two symmetric solutions.
(a) Solution 1
(b) Solution 2
Figure 5.24: Two symmetric solutions for the 4-queens problem. These two solutions are symmetric along a vertical axis dividing the square in two equal parts. If we have x1 = 1 (or x1 = 0) during the search, we know that we don’t have to test a solution with x 2 = 1 (or x 2 = 0) as every solution with x 1 = 1 ( x1 = 0) has an equivalent symmetric solution with x2 = 1 ( x2 = 0). You can tell the CP solver not to visit the branch x 2 = c if during the search we already have tried to set x 1 = c. To do this, we use a SymmetryManager and a SymmetryBreaker . The SymmetryManager collects SymmetryBreaker s for a given problem. During the search, each Decision is visited by all the SymmetryBreaker s. If there is a match between the Decision and a SymmetryBreaker , the SymmetryManager will, upon refutation of that Decision issue a Constraint to forbid the symmetrical exploration of the search tree. As you might have guessed, SymmetryManager s are SearchMonitor s and SymmetryBreaker s are DecisionVisitor s.
5.8.2 SymmetryBreakers You can find the code in the file 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.25 illustrates the returned indices by the symmetric() method.
127
5.8. Breaking symmetries with SymmetryBreaker s
Figure 5.25: The indices returned by the symmetric() method.
128
Chapter 5. Defining search primitives: the n-queens problem
We also use two methods to do the translation between the indices and the variables. Given an IntVar * var, Index(var) returns the index of the variable corresponding to var: int Index(IntVar* const var) const { return FindWithDefault(indices_, var, -1);
}
Given an FindWithDefault() is defined in the header base/map-util.h . std::map 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 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& 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 vars_; std::map indices_; const int size_; };
Now, we can specialize it for each symmetry we want to break. How do we tell a SymmetryBreaker to notify the SymmetryManager to add a corresponding constraint upon refutation of a given Decision ? For the n-queens problem, we can use the AddIntegerVariableEqualValueClause() method of the SymmetryBreaker class. Given the assignation of a value to an IntVar, give this method
129
5.8. Breaking symmetries with SymmetryBreaker s the corresponding symmetric assignation. We call this corresponding assignment aclause. 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& 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 Decision s 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 RevAlloc Whenever you define your own subclass of BaseObject (and a SymmetryBreaker is a BaseObject ), it is good practice to register the given object as being reversible to the solver. That is, the solver will take ownership of the object and delete it when it backtracks out of the current state. To register an object as reversible, you invoke the RevAlloc() method of the solver: Solver s("nqueens"); ... NQueenSymmetry* const sy = s.RevAlloc(new SY(&s, queens));
130
Chapter 5. Defining search primitives: the n-queens problem RevAlloc() returns a pointer to the newly created and registered object. You can thus invoke this method with arguments in the constructor of the constructed object without having to keep a pointer to this object. The solver will now take care of your object. If you have an array of objects that are subclasses of BaseObject, IntVar, IntExpr and Constraint, you can register your array with RevAllocArray() . This method is also valid for arrays of ints, int64, uint64 and bool. The array must have been allocated with the new[] operator. If you take a look at the source code, you will see that the factories methods callRevAlloc() to pass ownership of their objects to the solver.
5.8.4 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; • 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 std::vector:
in
an
std::vector 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 SearchMonitor s:
131
5.9. Summary
std::vector monitors; ... monitors.push_back(symmetry_manager); ... DecisionBuilder* const db = s.MakePhase(...); ... s.Solve(db, monitors);
These seven SymmetryBreaker s 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.5 Results Let’s compare the time and the search trees again. [TO BE DONE]
5.9 Summary
132
CHAPTER
SIX
LOCAL SEARCH: THE JOB-SHOP PROBLEM
We enter here in a new world where we don’t try to solve a problem to optimality but seek a good solution. Remember from sub-section 1.3.4 that some problems1 are hard to solve. No matter how powerful our computers are2, we quickly hit a wall if we try to solve these problems to optimality. Do we give up? Of course not! If it is not possible to compute the best solutions, we can try to 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-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 IntervalVar s 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 + . . . + x n−1 where each variable has the same domain [0, n − 1]. To complicate things a little bit, we add the constraint x0 1. Once we understand how to use local search in or-tools, we use basic LocalSearchOperator s to solve the job-shop problem and compare the exact and approxActually, most interesting problems! But watch out for the next generations of computers: molecular (http://en.wikipedia.org/wiki/Molecular_computer) and computers based on quantum (http://en.wikipedia.org/wiki/Quantum_computer)! 1 2
computers mechanics
imate results. Finally, to speed up the local search algorithm, we use LocalSearchFilter s 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. Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap6 . The files inside this directory are: • Makefile. • jobshop.h : This file contains the JobShopData class that records the data for jobshop 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 instances 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 ortools. • jobshop_ls.h : two basic LocalSearchOperator s for the job-shop problem. • jobshop_ls1.cc : A basic implementation SwapIntervals LocalSearchOperator .
of
Local
Search
with
the
• jobshop_ls2.cc : A basic implementation ShuffleIntervals LocalSearchOperator .
of
Local
Search
with
the
• jobshop_ls3.cc : A basic implementation of Local Search with both the SwapIntervals and ShuffleIntervals LocalSearchOperator s. We use also local search to find an initial solution. • dummy_ls_filtering.cc : The basic example extended with filtering.
134
Chapter 6. Local search: the job-shop problem 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 but...). Content:
6.1 The job-shop problem, the disjunctive model and benchmark data 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 . We describe the job-shop problem, a first model and the benchmark data. The job-shop problem belongs to the intractable problems (∈ NP). Only few very special cases can be solved in polynomial time (see [Garey1976] and [Kis2002]). The 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 mij 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 a 02 which must be processed on machine m02 = 1 during p02 = 4 units of time and task a12 which must be processed on machine m02 = 2 during p02 = 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 a 00 on machine 0 must be processed before task a 10 on machine 1 that itself must be processed before task a 20 on machine 2. It is not mandatory but most of 3 4
Tasks are also called operations. We use a slightly different and we hope easier notation than the ones used by the scheduling community.
135
6.1. The job-shop problem, the disjunctive model and benchmark data the literature and benchmark data are concerned by problems where each job is made of m 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 C max. The makespan can be defined as
C max = 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
C max = 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 not only did you see that it is the longest job to process but its last task takes 4 units of time. Here is the Gantt chart of a possible schedule:
machine 0
job 0
machine 1
job 1
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:
machine 0
job 0
machine 1
job 1
machine 2
job 2 2
4
6
8
10
12
Its makespan is 11 units of time. How can we simply describe a schedule? Let us define 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
136
And a correspondence rule between those integers and the tasks.
Chapter 6. Local search: the job-shop problem are completely left shifted on the Gantt chart6 , we can 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)
s
0 e n i h c a m
(0,2)
(1,2)
e n i h c a m
1
(2,1)
(2,2) n e h i a c m
job 0
2
(1,4)
t job 1
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. 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! 6
137
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)
s
(1,2)
(0,2)
(2,2)
(2,1)
job 0 (1,4)
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)
s
(1,2)
(0,2)
(2,2)
(2,1)
job 0 (1,4)
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 first 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 reprensentation. We again rely on the The three-stage method: describe, model and solve. What are the decision variables? We use the variables t ij 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 t k 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.
138
Chapter 6. Local search: the job-shop problem
What are the constraints? In the disjunctive graph, we have two kind of edges to model a feasible schedule: • conjunctive arcs modelling the order in which each task of a job has to be processed:
∀(k, l) ∈ C such that k̸ = s and l̸ = t : tk + pk 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 = m l tk + pk tl or tl + pl tk These constraints are called disjunctive constraints. They forbid cycles in a clique corresponding to a machine9 . What is the objective function? The objective function (the makespan) C max doesn’t correspond to a variable of the model. We have to construct its value. Because we minimize the makespan, we can use a little trick. Let 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 : C max tk + pk . Here is the model10 :
mintk
C max
s.t.:
C max tk + pk tk + pk tl tk + pk tl or tl + pl tk 0
tk
∀ k ∈ S ∀ (k, l) ∈ C ∀ (k, l) ∈ D : m k = m l ∀ 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 t2 , t2 + p2 t3 and t3 + p3 t1 . Add these three inequalities and you obtain p1 + p2 + p3 0. This is impossible if one of the p i is greater than 0 as every p i 0. 10 It is not obvious that this model produces optimal solutions that are feasible schedules but it can be shown that it does.
139
6.1. The job-shop problem, the disjunctive model and benchmark data
6.1.4 The data and 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 format
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 20 15 6 14 5 21 8 13 4 11 1 11 14 35 13 20 1 35 5 31 0 13 3 26 6 14 9 17 7 38 0 30 4 35 2 40 10 35 6 30 14 23 8 29 ...
(Table 1, instance 9) 11 17 10 18 12 11 12 20 10 19 13 12 13 37 7 38 3 40
... ... ...
The 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 11
We copied the files abz9 and 20_5_01_ta001.txt in manual/tutorials/cplusplus/chap6 for your convenience.
140
the
directory
Chapter 6. Local search: the job-shop problem
83 3 89 58 56 2 923 15 11 49 31 20 3 513 71 99 15 68 85 ...
This format is made for 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
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 > 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(); if (!reader.loaded_successfully()) {
141
6.1. The job-shop problem, the disjunctive model and benchmark data
LOG(FATAL) << "Could not open job-shop file " << filename_; }
To parse the data 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& TasksOfJob(int job_id) const : returns a reference to the corresponding std::vector 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)
142
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 files first_example_jssp.txt and abz9.
the
data
in
the
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 DecisionBuilder s 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 upper bound on the starting time of the IntervalVar . The fourth argument is a bool that 12
The next section is entirely dedicated to scheduling in or-tools.
143
6.2. An implementation of the disjunctive model indicates if the IntervalVar can be unperformed or not. Unperformed IntervalVar s simply don’t exist anymore. This can happen when the IntervalVar is not consistent anymore. 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 > jobs_to_tasks(job_count); std::vector > 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& tasks = data.TasksOfJob(job_id); for (int task_index = 0; task_index < tasks.size(); ++task_index) { const JobShopData::Task& task = tasks[task_index];
CHECK_EQ(job_id, task.job_id); const string name = ... IntervalVar* const one_task = ... jobs_to_tasks[task.job_id].push_back(one_task); machines_to_tasks[task.machine_id].push_back(one_task); } }
We will create the SequenceVar variables later when we will add the disjunctive constraints.
6.2.2 The conjunctive constraints Recall that the conjunctive constraints ensure the sequence order of tasks inside a job is respected. If IntervalVar t1 is the task right before IntervalVar t2 in a job, we can add an IntervalBinaryRelation constraint with the right relation between the two IntervalVar s. 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 DisjunctiveConstraint s and a corresponding factory method: const std::string name = StringPrintf("Machine_%d", machine_id); DisjunctiveConstraint* const ct =
solver.MakeDisjunctiveConstraint(machines_to_tasks[machine_id], name);
144
Chapter 6. Local search: the job-shop problem A SequenceVar variable is a variable whose domain is a set of possible orderings of the IntervalVar s. It allows ordering tasks. You can only create13 SequenceVar s with the MakeSequenceVar() method of the DisjunctiveConstraint class: std::vector 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 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 DecisionBuilder s that are combined in a top-down fashion, i.e. one DecisionBuilder is applied and then when we reach a leaf in the search tree, the second DecisionBuilder kicks in. Since this chapter is about local search, we will use default search strategies for both phases. 13
The factory method Solver::MakeSequenceVar(...) has been removed from the API.
145
6.2. An implementation of the disjunctive model
First, we 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 SearchMonitor s: // Search log.
const int kLogFrequency = 1000000; SearchMonitor* const search_log =
solver.MakeSearchLog(kLogFrequency, objective_monitor); SearchLimit* limit = NULL; if (FLAGS_time_limit_in_ms > 0) { limit = solver.MakeTimeLimit(FLAGS_time_limit_in_ms); } SolutionCollector* const collector = solver.MakeLastSolutionCollector(); collector->Add(all_sequences); collector->AddObjective(objective_var);
and launch the search: // Search.
if (solver.Solve(main_phase,
search_log, objective_monitor, limit, collector)) { for (int m = 0; m < machine_count; ++m) { LOG(INFO) << "Objective value: " << collector->objective_value(0); SequenceVar* const seq = all_sequences[m]; LOG(INFO) << seq->name() << ": " << IntVectorToString(collector->ForwardSequence(0, seq), ", "); } }
146
Chapter 6. Local search: the job-shop problem collector->ForwardSequence(0, seq) is a shortcut to return the std::vector 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 & sequence = collector->ForwardSequence(0, seq); const int seq_size = sequence.size(); for (int i = 0; i < seq_size; ++i) { IntervalVar * t = seq->Interval(sequence[i]); s << "Job " << sequence[i] << " ("; s << collector->Value(0,t->StartExpr()->Var()); s << ","; s << collector->Value(0,t->EndExpr()->Var()); s << ") "; } s.flush(); LOG(INFO) << s.str(); }
The result for our instance is:
147
6.3. Scheduling in or-tools
...: Machine_0: Job 0 (0,3) ...: Machine_1: Job 2 (0,4) ...: Machine_2: Job 1 (5,6)
Job 1 (3,5) Job 0 (4,6) Job 0 (6,8)
Job 1 (6,10) Job 2 (8,11)
Let’s try the abz9 instance: Sol. nbr. 87 107
Obj. val. 1015 986
Branches 131 733 6 242 194
Time (s) 26,756 1088,487
After a little bit more than 18 minutes (1088,487 seconds), the CP solver 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 produce goods and services. The job-shop problem is a good example of such problems. Constraint programming has been proved successful in solving some scheduling problems with dedicated variables and strategies [ref]. In or-tools, the CP solver offers some variable types (IntervalVar s 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 SequenceVar s model sequences of tasks on one machine. Once you master these variables, you can use them in a variety of different contexts but for the moment keep in mind this modelling association. IntervalVar s
An IntervalVar variable represents an integer interval variable. It is often used in scheduling to represent a task because it has: • a starting time: s ; • a duration: d and
148
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 cannot use these methods. Well, if you do, nothing bad will happen but you will get gibberish as the IntervalVar is no longer updated. These methods have corresponding “safe” versions if you need them. 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()
StartRange
EndMin()
EndMax() EndRange
and you have the following setters and getters: • virtual int64 StartMin() const = 0; • virtual int64 StartMax() const = 0; • virtual void SetStartMin(int64 m) = 0; • virtual void SetStartMax(int64 m) = 0; • virtual void SetStartRange(int64 mi, int64 ma) = 0; • virtual int64 DurationMin() const = 0; • virtual int64 DurationMax() const = 0; • virtual void SetDurationMin(int64 m) = 0; • virtual void SetDurationMax(int64 m) = 0; • virtual void SetDurationRange(int64 mi, int64 ma) = 0; 14
The implementation optimizes different cases and thus doesn’t necessarily correspond to the figure. Read on.
149
6.3. Scheduling in or-tools • virtual int64 EndMin() const = 0; • virtual int64 EndMax () const = 0; • virtual void SetEndMin (int64 m) = 0; • virtual void SetEndMax (int64 m) = 0; • virtual void SetEndRange (int64 mi, int64 ma) = 0; As usual, the IntervalVar class is an abstract base class and several specialized sub-classes exist. For instance, we saw the FixedDurationPerformedIntervalVar class in the previous section (created with MakeFixedDurationIntervalVar() ). To create IntervalVar variables, use the factory methods provided by the solver. For instance: IntervalVar* Solver:MakeFixedInterval(int64 start, int64 duration, const string& name); IntervalVar* Solver::MakeFixedDurationIntervalVar(int64 start_min, int64 start_max, int64 duration, bool optional, const string& name); void Solver::MakeFixedDurationIntervalVarArray(int count,
int64 start_min, int64 start_max, int64 duration, bool optional, const string& name, std::vector* array);
The first factory method creates a FixedInterval : its starting time, duration and ending time are all fixed. MakeFixedDurationIntervalVar() and MakeFixedDurationIntervalVarArray() create respectively an IntervalVar and an std::vector with count elements. The start_min and start_max parameters give a range for the IntervalVar s 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 meaningless). An IntervalVar is automatically marked as unperformed when it is not consistent
150
Chapter 6. Local search: the job-shop problem
anymore (starting time greater than ending time, duration < 0...). You can get and set if an IntervalVar must, may or cannot be performed with the following methods: virtual bool MustBePerformed() const = 0; virtual bool MayBePerformed() const = 0; bool CannotBePerformed() const { return !MayBePerformed(); } bool IsPerformedBound() { return MustBePerformed() == MayBePerformed();
} virtual void SetPerformed( bool val) = 0;
As for the starting time, the ending time and the duration of an IntervalVar variable, its “performedness” is encapsulated in an IntExpr you can query with: IntExpr* PerformedExpr();
The corresponding IntExpr acts like a 0 − 1 IntervalVar15 . If its minimum value is 1 , the corresponding IntervalVar variables must be performed. If its maximal value is 0 , the corresponding IntervalVar is unperformed and if min = 0 and max = 1, the corresponding IntervalVar might be performed. The use of an IntExpr allows expressiveness and the use of sophisticated constraints. As we have seen, if the IntervalVar is unperformed , we cannot use StartExpr(), DurationExpr() and EndExpr(). You can however call their safe versions: • IntExpr* SafeStartExpr(int64 unperformed_value); • IntExpr* SafeDurationExpr(int64 unperformed_value); • IntExpr* SafeEndExpr(int64 unperformed_value) If the variable is performed, these expressions will return their exact values, otherwise they will return “obvious” values (see the 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 between its minimal value and -1 if the variable may be performed and -1 if the variable is unperformed. SequenceVar s
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 !
151
6.3. Scheduling in or-tools how the IntervalVars are ranked. You can conceptualize16 this class as depicted in the following figure:
Precedence matrix
0
1
2
3
0
0
0
0
1
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 IntervalVar s
Ranked IntervalVars are exactly that: already ranked variables in the sequence. IntervalVar s 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 figure illustrates this: 1
?
?
2
Ranked sequence
Array of IntervalVars 0 Not ranked yet
1 Ranked
2 Ranked
3 unperformed
IntervalVar variables 1 and 2 are ranked (and performed ) while IntervalVar variable 0 may be performed but is not performed yet and IntervalVar variable 3 is unperformed and thus 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 and the precedence matrix is given by a scoped_ptr . The actual class contains some more data structures to facilitate and optimize the propagation. 17 Thus, unranked variables are variables that may be performed . Yeah, three-states situations that evolves with time are nastier than a good old Manichean one.
152
Chapter 6. Local search: the job-shop problem Possible IntervalVar variables
Possible IntervalVar variables
to be ranked first
to be ranked last
42
1 Ranked sequence
86
R a n k F i r s t ( )
Ranked first
19
...
) ( t a s L k n a R
23
6
2
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().
153
6.3. Scheduling in or-tools
• IntervalVar * Interval(int index) const : Returns IntervalVar from the array of IntervalVars.
the
index
th
• IntVar* Next(int index) const : To each IntervalVar corresponds an associated IntVar that represents the “ranking” of the IntervalVar in the ranked sequence. The Next() method returns this IntVar variable for the index th IntervalVar in the array of IntervalVar s. For instance, if you want to know what is the next IntervalVar after the 3 IntervalVar in the sequence, use the following code:
rd
SequenceVar * seq = ...; ... IntVar * next_var = seq->Next(2); if (next_var->Bound()) { // OK, ranked LG << "The next IntervalVar after the 3rd IntervalVar in " << "the sequence is " << next_var->Value() - 1; }
As you can see, there is a difference of one between the returned value and the actual index of the IntervalVar in the array of IntervalVars variables. • int size() const: Returns the number of IntervalVar variables. • void FillSequence(...): a getter filling the three std::vector of first ranked, last ranked and unperformed variables: void FillSequence(std::vector* const rank_first, std::vector* const rank_lasts, std::vector* const unperformed) const;
The method first clears the three std::vector s 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* const possible_firsts, std::vector* 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 .
154
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 variables. • 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 variables. • void RankSequence(...): a setter acting on three std::vector of first, last and unperformed variables: void RankSequence(const std::vector& rank_firsts, const std::vector& rank_lasts, const std::vector& unperformed);
Ranks the IntervalVars in the given order. Again, the rank_firsts std::vector gives the IntervalVar s in order (rank_firsts[0] if the first ranked IntervalVar and so on) and the rank_lasts std::vector 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 will be marked as such and all IntervalVar variables in the rank_firsts and rank_lasts std::vector will be marked as performed .
6.3.2 Constraints on IntervalVars Most of the common constraints on IntervalVars are implemented in the library. IntervalUnaryRelation constraints
You can specify a temporal relation between an IntervalVar t and an integer d: • ENDS_AFTER: t ends after d, i.e. End(t) >= d; • ENDS_AT: t ends at d, i.e. End(t) == d; • ENDS_BEFORE : t ends before d, i.e. End(t) <= d; • STARTS_AFTER : t starts after d, i.e. Start(t) >= d; • STARTS_AT : t starts at d, i.e. Start(t) == d; • STARTS_BEFORE : t starts before d, i.e. Start(t) <= d ; • CROSS_DATE: STARTS_BEFORE and ENDS_AFTER at the same time, i.e. d is in t; • AVOID_DATE: STARTS_AFTER or ENDS_BEFORE , i.e. d is not in t.
155
6.3. Scheduling in or-tools The possibilities are enclosed in the UnaryIntervalRelation enum . The corresponding constraints are IntervalUnaryRelation constraints and the factory method is: Constraint* Solver::MakeIntervalVarRelation(IntervalVar* const t, Solver::UnaryIntervalRelation r, int64 d);
BinaryIntervalRelation constraints
You can specify a temporal relation between two IntervalVars t1 and t2: • ENDS_AFTER_END : t1 ends after t2 ends, i.e. End(t1) >= End(t2) ; • ENDS_AFTER_START : t1 ends after t2 starts, i.e. End(t1) >= Start(t2); • ENDS_AT_END : t1 ends at the end of t2, i.e. End(t1) == End(t2); • ENDS_AT_START : t1 ends at t2‘s start, i.e. End(t1) == Start(t2) ; • STARTS_AFTER_START : t1 starts after t2 starts, Start(t2) ;
i.e.
Start(t1) >=
• 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 ÌntervalVar‘ variables are temporally disjoint, i.e. they cannot be processed at the same time. To create such a constraint, use: solver = ... ... IntervalVar * const t1 = ... IntervalVar * const t2 = ... ... Constraint * ct = solver.MakeTemporalDisjunction(t1, t2);
Maybe you can relate the decision on what has to happen first to the value an IntVar takes:
156
Chapter 6. Local search: the job-shop problem ... IntVar * const decider = ... Constraint * ct = solver.MakeTemporalDisjunction(t1, t2, decider)
If decider takes the value 0 , then t1 has to happen before t2 , otherwise it is the contrary. This constraint works the other way around too: if t1 happens before t2 , then the IntVar decider is bound to 0 and else to a positive value (understand 1 in this case). DisjunctiveConstraint constraints
DisjunctiveConstraint constraints are like TemporalDisjunction constraints but for an unlimited number of IntervalVar variables. Think of the DisjunctiveConstraint as a kind of AllDifferent constraints but on IntervalVar s. 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 IntervalVar s will be disjoint. The DisjunctiveConstraint class itself is a pure abstract class. Subclasses must implement the following method: virtual SequenceVar* MakeSequenceVar() = 0;
This method creates a SequenceVar containing the “rankable”18 IntervalVar s given in the intervals std::vector.
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 implementation. This might change in the future.
CumulativeConstraint constraints
This constraint forces, for any integer t, the sum of the demands corresponding to an interval containing t to not exceed the given capacity. Intervals and demands should be vectors of equal size. Demands should only contain non-negative values. Zero values are supported, and the corresponding intervals are filtered out, as they neither impact nor are impacted by this constraint. Here is one factory method with a limited static capacity: You remember that unperformed IntervalVar s are “non existing”, don’t you? And yes, we know that the adjective “rankable” doesn’t exist... 18
157
6.3. Scheduling in or-tools
Constraint* MakeCumulative(const std::vector& intervals, const std::vector& demands, int64 capacity, const string& name);
If you need more flexibility, use the following factory method: Constraint* MakeCumulative(const std::vector& intervals, const std::vector& demands, IntVar* const capacity, const string& name);
Here the capacity is modelled by an IntVar. This variable is really a capacity in the sense that it is this variable that determines the capacity and it will not be adjusted to satisfy the CumulativeConstraint constraint.
6.3.3 Constraints on SequenceVars There are none for the time being. Nobody prevents you from implementing one though.
6.3.4 DecisionBuilders and Decisions for scheduling This sub-section is going to be very brief. Indeed, even if room has been made in the code to welcome several alternative strategies, at the moment of writing (revision r2502, January 11 th 2013) there is “only one real” strategy implemented to deal with IntervalVar s and SequenceVar s. The RankFirstIntervalVars DecisionBuilder for SequenceVar s and the SetTimesForward DecisionBuilder for IntervalVar s both try to rank the IntervalVar s one after the other starting with the first “available” ones. When we’ll implement different strategies, we will update the manual. 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 DecisionBuilder s 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. IntervalVar s
For IntervalVar variables, there is only one strategy implemented even if there are three entries in the IntervalStrategy enum : INTERVAL_DEFAULT = INTERVAL_SIMPLE = INTERVAL_SET_TIMES_FORWARD : The CP solver simply schedules the IntervalVar with the lowest starting time (StartMin() ) and in case of a tie, the IntervalVar with the lowest ending time (StartMax() ).
158
Chapter 6. Local search: the job-shop problem The DecisionBuilder class is the SetTimesForward class. It r eturns 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. You create the corresponding phase with the good old MakePhase factory method: DecisionBuilder * MakePhase ( const std::vector< IntervalVar * > &intervals, IntervalStrategy str);
SequenceVar s
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 IntervalVar s 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 IntervalVar s 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& 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. We are thus assured to visit the complete search tree... of solutions of rankedIntervalVar s if needed. After the ranking of IntervalVars, the schedule is still loose and any
159
6.3. Scheduling in or-tools IntervalVar may have been unnecessarily postponed. This is so important that we use our warning box: After the ranking of IntervalVars, the schedule is still loose and any IntervalVar may have been unnecessarily postponed If for instance, you are interested in the makespan, you might want to schedule each IntervalVar at its earliest start time. As we have seen in the previous section, this can be accomplished by minimizing the objective function corresponding to the ending times of all IntervalVar s: 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 DecisionBuilder s 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 whatever 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! Here is the output of an optimal solution found by the solver:
160
Chapter 6. Local search: the job-shop problem
Objective value: Machine_0: Job 1 Machine_1: Job 2 Machine_2: Job 1
13 (0,2) (3,7) (2,3)
Job 0 (2,5) Job 0 (7,9) Job 1 (9,13) Job 2 (7,10) Job 0 (10,12)
As you can see, the 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 following 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 guarantee on the quality of the found solution(s). We will discuss these three ingredients in details in a moment but before here are some examples of local search (meta-)heuristics20 : • Tabu Search | (62 100) 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. 19
161
6.4. What is local search (LS)?
• Hill Climbing | (54 300) • Scatter Search | (5 600) • Simulated Annealing | (474 000) • Beam Search | (12 700) • Particle Swarm Optimization | (74 500) • Greedy Descent | (263) • Gradient Search | (16 300) • Variable Neighbourhood Search | (1 620) • Guided Local Search | (2 020) • Genetic Algorithms | (530 000) • Ant Colony Optimization | (31 100) • Greedy Adaptive Search Procedure (GRASP) • ... and there are a lot more! Most of these methods are quite recent in Research Operations (from the eighties and later on). Most successful methods take into account their search history to guide the search. Even better - when well implemented - reactive methods21 learn and adapt themselves during the search. As you might have guessed from the long list of different local search (meta-) heuristics, there is no universal solving method22 . The more insight/knowledge of the structure of your 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). You’ll find a detailed example of this kind of relaxation and the use of local search in the lab exercises XXX where we will try to find a solution to the n-queens problem with local search. It might sound complicated but it really isn’t. 21
See Wikipedia Reactive search optimization or reactive-search.org. Google No Free Lunch Theorem in optimization to learn more about this. 23 Relaxing a constraint means that you remove this constraint or weaken it. For instance, you can replace x1 1 by x1 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! 22
162
Chapter 6. Local search: the job-shop problem
2. They improve locally this solution: This is the tricky part to understand. Improvements to the initial solution are done locally. This means that you need to 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 N x 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: z f
N x1 N x0
N x2
x0 I ni ti al s ol ut io n
x1
x3
L oc al m in imu m
x
x2 Global minimum
xi solution i neighborhood
N x
i
neighborhood of x i
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:
163
6.4. What is local search (LS)?
The local search procedure starts from an initial feasible solution x0 and searches the neighborhood N x 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 N x , the best solution found is x 2 . 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). 0
1
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 ∈ N x but x0 ̸ ∈ N x 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 solution of the neighborhood If you have constructed a successful candidate, make MakeNextNeighbor() returns true. When the whole neighborhood has been visited, make it returns false. 0
1
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 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 wath 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.
164
Chapter 6. Local search: the job-shop problem
– maximum number of solutions; – ...
• improvements criteria: – stop if no improvement for n number of steps/x time; – stop if gap between estimate of optimal solution and best solution obtained so far is smaller than x; – ...
These stopping criteria can be further divided in: • absolute: for instance, a global maximal number of iterations; • relative: for instance, the improvements are too small with respect to the time, the number of iterations, the number of solutions, ... . Most of the time, you combine some of these criteria together. You can also update them during the search. In or-tools, stopping criteria are implemented using specialized SearchMonitor s: SearchLimit s (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 metaprogramming 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.
165
6.4. What is local search (LS)?
• but...: Local search methods are strongly dependent on your knowledge of the problem and your ability to use this knowledge during the search. For instance, very often the initial solution plays a crucial role in the 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 neighborhood 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).
166
Chapter 6. Local search: the job-shop problem
What do we mean by a guarantee on the solution?
Several concepts of guarantee have been developed. We will not go into detailsa about the concept of guarantee but let’s give an example. In a now famous report[Christofides1976], Christofides proposed and analyzed a heuristic that is guaranteed to solve the metric Travelling Salesman Problemb within a 3/2 factor, i.e. no matter the instance, this heuristic will always return a solution whose cost is at most 3/2 times the cost of the optimal solution. This means that in the worst case, the returned solution costs 3/2 times the cost of the optimal solution. This is guaranteed! See Wikipedia Approximation Algorithm. a
If theory doesn’t scare you, have a look at the subsection Approximation complexity for more about approximation theory and quality guarantees. b The metric TSP is the classical TSP but on graphs that respect the triangle inequality, i.e. d(a, c) 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 28
Tabu search, simulated annealing, guided local search and the like were designed to overcome some shortcomings of local search methods. Depending on the problem and how they are implemented, these methods can also be seen as global search methods. 29 As all analogies, this one has certainly its limits!
167
6.5. Basic working of the solver: Local Search
Search. It’s good to keep them in memory for the rest of this section. We then overview the implementation and describe some of its main components. Finally, we detail the inner workings of the Local Search algorithm and indicate where the callbacks of the SearchMonitor s 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 NestedSolveDecision s (through its Next() method). These NestedSolveDecision s 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(){ SolveAndCommit(FindOneNeighbor(ls)); }
NestedSolveDecision::Refute(){}
ls is the LocalSearchOperator that constructs the candidate solutions. The search tree very quickly becomes completely unbalanced if we only keep 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. x 0 , x1 , x2 , . . . are solutions. As we have seen, the Local Search algorithm moves from one solution to another. It takes a starting solution x i and visit the neighborhood defined around x i to find the next solution x i+1 . By visiting the neighborhood, we mean constructing
168
Chapter 6. Local search: the job-shop problem and testing feasible solutions y 0 = x i , y1 , y2 , . . . of this neighborhood. We call these solutions candidate solutions. In the code, they are called neighbors. The LocalSearchOperator produces these candidates and the FindOneNeighbor DecisionBuilder filter these out to keep the interesting candidate solutions only. When a stopping criteria is met or the neighborhood has been exhausted, the current solution of the CP solver is the next starting solution. Let’s illustrate this:
N 1 x
N 0 x
Candidate solutions y1 y0 y2 x0
Initial solution
y3
y4
y5
x1
Current solution = starting solution for N
x1
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. • LocalSearchOperator s: This class is responsible of constructing the candidate solutions. • 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).
169
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 LocalSearchOperator s into one LocalSearchOperator ; – a complementary DecisionBuilder to instantiate unbound variables once an (incomplete) candidate solution has been defined by the LocalSearchOperator . 30
In the code, you are only allowed to use one LocalSearchOperator but you can combine several LocalSearchOperator s 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 . See below.
170
Chapter 6. Local search: the job-shop problem It will also complete the initial Assignment or the solution provided by the initial DecisionBuilder .; – a Searchlimit specifying the stopping criteria each time we start searching a new neighborhood; – an std::vector of LocalSearchFilters used to speed up the search by pruning unfeasible (or undesirable) candidate solutions: instead of letting the solver 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.
LocalSearchOperator s are detailed in the next section and LocalSearchFilter s in section 6.8. We now detail these two basics ingredients that are the initial solution and the LocalSearchPhaseParameters parameter. The initial solution To start the local search, we need an initial feasible solution. We can either give a starting solution or we can ask the CP solver to 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& 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 of variables given to MakeLocalSearchPhase() .
The LocalSearchPhaseParameters parameter The LocalSearchPhaseParameters parameter holds the actual definition of the local search phase. It basically consists in: • a SolutionPool : as its name implies, this class is a pool of solutions. As usual, SolutionPool is a pure virtual class that must be implemented. One such implementation is the DefaultSolutionPool that only keeps the current solution. You
171
6.5. Basic working of the solver: Local Search
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 Assignment s it keeps33 . • a LocalSearchOperator : a LocalSearchOperator or a combination of LocalSearchOperator s explore the neighborhood of the current solution. We detail them in the next section. • a DecisionBuilder : this complementary DecisionBuilder helps creating feasible solutions if your LocalSearchOperator s only return partial solutions, i.e. solutions with unbounded variables. It also completes the initial solution if needed. If you know that your candidate and the initial solutions are already feasible, you 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 neighborhood. 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 when the whole neighborhood is explored), it stops and starts over again with the best solution. • LocalSearchFilter s: 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 33
Well, you could devise another way to keep track of the solutions and take care of their existence but anyhow, you are responsible for these solutions.
172
Chapter 6. Local search: the job-shop problem
feasible, because it is not good enough, ...). If the filters accept a solution, the solver still tests the feasibility of this solution. LocalSearchFilter s are discussed in section 6.8. Several factory methods are available to create a LocalSearchPhaseParameters parameter. At least you need to declare a LocalSearchOperator and a complementary DecisionBuilder : LocalSearchPhaseParameters * Solver::MakeLocalSearchPhaseParameters( LocalSearchOperator *const ls_operator, DecisionBuilder *const complementary_decision_builder);
You can also pass all the above enumerated parameters : LocalSearchPhaseParameters* Solver::MakeLocalSearchPhaseParameters( SolutionPool* const pool, LocalSearchOperator* const ls_operator, DecisionBuilder* const complementary_decision_builder, SearchLimit* const limit, const std::vector& filters);
The LocalSearchOperator will find candidate solutions while complementary_decision_builder DecisionBuilder will complete candidate solutions if some of the variables are not assigned.
the the
A handy way to create a DecisionBuilder to assist the local search operator(s) is to limit one with MakeSolveOnce() . MakeSolveOnce is a DecisionBuilder that takes another DecisionBuilder db and SearchMonitor s: 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 SearchMonitor s 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. If you know for sure that your LocalSearchOperator will return feasible solutions, you don’t have to provide a DecisionBuilder to assist: just submit NULL as argument for the DecisionBuilder pointer.
6.5.3 The basic local search algorithm and the callback hooks for the SearchMonitors We feel compelled to use our warning box again: We describe a simplified version of the Local Search algorithm.
173
6.5. Basic working of the solver: Local Search If you want to know more, have a look at the section Local Search (LS) in the chapter Under the hood . In this subsection, we present the callbacks of the SearchMonitor listed in Table 6.1 and show you exactly when they are called in the search algorithm. Table 6.1: Local Search algorithm callbacks from the SearchMonitor class. Methods
LocalOptimum()
AcceptDelta(Assignment *delta, Assignment *deltadelta)
AcceptNeighbor() PeriodicCheck()
Descriptions When a local optimum is reached. If true is returned, the last solution is discarded and the search proceeds to find the next local optimum. Handy when you implement a meta-heuristic with a SearchMonitor . When the LocalSearchOperator has produced the next candidate solution given in the form of delta and deltadelta. You can accept or reject this new candidate solution. After accepting a candidate solution during Local Search. Periodic call to check limits in long running search procedures, like Local Search.
To ensure the communication between the local search and the global search, three utility functions are defined. These functions simply call their SearchMonitor ‘s counterparts, i.e. they call the corresponding methods of the involved SearchMonitor s: • 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 inner 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
The FindOneNeighbor DecisionBuilder This DecisionBuilder tries to find the next filtered and accepted candidate solution. It tests (and sometimes completes) the candidate solutions given by the LocalSearchOperator .
174
Chapter 6. Local search: the job-shop problem We present its Next() method and discuss it after: 1
Decision* FindOneNeighbor::Next(Solver* const solver) {
2
4
// //
5
if (!neighbor_found_) {
3
No neighbor (candidate solution) found only on the first call to Next().
6
//
7
...
SYNCHRONIZE ALL
}
8 9
// Another assignment is needed to apply the delta
10 11
Assignment* assignment_copy = solver->MakeAssignment(reference_assignment_.get()); int counter = 0;
12 13 14 15 16 17 18 19 20 21
DecisionBuilder* restore = solver->MakeRestoreAssignment(assignment_copy); if (sub_decision_builder_) { restore = solver->Compose(restore, sub_decision_builder_); } Assignment* delta = solver->MakeAssignment(); Assignment* deltadelta = solver->MakeAssignment();
22
//
23 24 25 26
MAIN LOOP
while (true) {
delta->Clear(); deltadelta->Clear(); //
27 28
29 30
solver->TopPeriodicCheck(); if (++counter >= FLAGS_cp_local_search_sync_frequency && pool_ ->SyncNeeded(reference_assignment_.get())) { //
31
SYNCHRONIZE ALL
... counter = 0;
32 33
SEARCHMONITOR CALLBACK
}
34 35
if (!limit_ ->Check()
36
&& ls_operator_ ->MakeNextNeighbor(delta, deltadelta)) { solver->neighbors_ += 1;
37 38
//
39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
SEARCHMONITOR CALLBACK
const bool meta_heuristics_filter =
AcceptDelta(solver->ParentSearch(), delta, deltadelta); const bool move_filter = FilterAccept(delta, deltadelta); if (meta_heuristics_filter && move_filter) { solver->filtered_neighbors_ += 1; assignment_copy->Copy(reference_assignment_.get()); assignment_copy->Copy(delta); if (solver->SolveAndCommit(restore)) { solver->accepted_neighbors_ += 1; assignment_ ->Store(); neighbor_found_ = true; return NULL; } } } else {
175
6.5. Basic working of the solver: Local Search
if (neighbor_found_) {
55
//
56
57 58
AcceptNeighbor(solver->ParentSearch()); pool_ ->RegisterNewSolution(assignment_);
59
//
60
...
SYNCHRONIZE ALL
} else { break; }
61
62
SEARCHMONITOR CALLBACK
63
}
64 65 66
67
68
}
} solver->Fail(); return NULL;
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 SearchMonitor s 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 MakeNextNeighbor() method is called to create the next candidate solution in deltas format. If you overwrite the MakeNextNeighbor() method, you need to manage the deltas: you must take care of applying and reverting the deltas yourself if needed. You can use the ApplyChanges() and RevertChanges() helper functions to do so.
176
Chapter 6. Local search: the job-shop problem For instance, here is the implementation of the MakeNextNeighbor() method of the IntVarLocalSearchOperator : bool IntVarLocalSearchOperator::MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) { CHECK_NOTNULL(delta); while (true) { RevertChanges(true); if (!MakeOneNeighbor()) { return false;
} if (ApplyChanges(delta, deltadelta)) { return true;
} } return false;
}
ApplyChanges() actually 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 update the solution counter accordingly. It is now time to test this new solution candidate. The first test comes from the SearchMonitor s in their AcceptDelta() methods. If only one SearchMonitor rejects this solution, it is rejected. In or-tools, we implement (meta-)heuristics with SearchMonitor s. 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 ! If the if test on line 36 and 37 fails, we enter the else part of the statement on line 55. This means that either one SearchLimit was reached or that the neighborhood is exhausted. If a solution (stored in assignment_ ) was found during the local search, we register it and synchronize the LocalSearchOperator s and LocalSearchFilter s with a new solution provided by the solution pool pool_ on lines 58-60. We also notify the SearchMonitor s
177
6.5. Basic working of the solver: Local Search on line 57. If no solution was found, we simply break out of the while() loop on line 62 and make the CP solver fail on line 66.
The NestedSolveDecision Decision The NestedSolveDecision is the Decision that the LocalSearch‘s Next() method returns to find the next solution. This Decision is basically a Decision wrapper around a nested solve with a given DecisionBuilder and SearchMonitor s. 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
DECISION_FAILED DECISION_PENDING DECISION_FOUND
Meaning The nested search phase failed, i.e. Solve() or SolveAndCommit() failed. The nested search hasn’t been called yet. The local search is in this state when it balances the search tree. The nested search phase succeeded and found a solution, i.e. Solve() or SolveAndCommit() succeeded and returned true.
The three states are 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 parameter set up and we let the CP solver construct the initial solution: Solver s("Dummy LS"); ... std::vector vars = ... ... LocalSearchOperator * const ls_operator = ... DecisionBuilder * const complementary_decision_builder = ... ... LocalSearchPhaseParameters params = s.MakeLocalSearchPhaseParameters(ls_operator, complementary_decision_builder);
178
Chapter 6. Local search: the job-shop problem The complementary_decision_builder DecisionBuilder will help us complete the candidate solutions found by the LocalSearchOperator ls_operator . Our initial solution will be constructed by the initial_solution DecisionBuilder (and completed by the complementary_decision_builder DecisionBuilder if needed). Remember, that the solution chosen by the CP solver is the 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 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 DecisionBuilder is used in a nested search:
this
initial
solution.
This
std::vector monitors; monitors.push_back(limit); NestedSolveDecision * initial_solution_decision = new NestedSolveDecision(initial_solution_and_store, false, monitors);
where: • limit is the SearchLimit given to the local search algorithm; • the NestedSolveDecision constructor’s arguments are respectively: – a DecisionBuilder to construct the next solution;
179
6.5. Basic working of the solver: Local Search – a bool to indicate if we restore the last solution in case we cannot find a solution; – an std::vector.
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. 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. 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:
180
Chapter 6. Local search: the job-shop problem
1 2 3 4 5
Decision * Next(Solver * solver) { ... const int state = decision->state(); switch (state) { case NestedSolveDecision::DECISION_FAILED: { // SEARCHMONITOR CALLBACK
6
if (!LocalOptimumReached(solver->ActiveSearch())) {
7 8
// Stop the current search
9
... } solver->Fail(); return NULL;
10 11
12
}
13
case NestedSolveDecision::DECISION_PENDING: {
14
17
// // //
18
...
15 16
const int depth = solver->SearchDepth(); if (depth < kLocalSearchBalancedTreeDepth) { return solver->balancing_decision(); } else if (depth > kLocalSearchBalancedTreeDepth) {
19 20
21 22
23
Stabilize search tree by balancing the current search tree. Statistics are updated even if this is not relevant to the global search
solver->Fail(); } ...
24 25
return decision;
26
}
27
case NestedSolveDecision::DECISION_FOUND: {
28 29
// Nothing important for us in this simplified version
30
... return NULL;
31
}
32
default: {
33 34
35
LOG(ERROR) << "Unknown local search state"; return NULL; }
36
}
37 38
39
}
return NULL;
The decision variable on line 3 is the NestedSolveDecision created with the FindOneNeighbor DecisionBuilder . We switch between three cases depending on the state of the nested search initiated by this Decision . • Line 5: case DECISION_FAILED: The nested solving process failed, meaning that there are no solution left. We let the SearchMonitor s decide if a local optimum has been reached and cannot be improved. LocalOptimum() returns a FalseExceptIfOneTrue . • Line 14: case DECISION_PENDING: This is the most interesting case: we try to keep the search tree balanced and force its height to be bounded. kLocalSearchBalancedTreeDepth is set to 32. So as long as the tree height is smaller than 32, the LocalSearch DecisionBuilder returns the same BalancingDecision on line 21. BalancingDecision s don’t do anything by de-
181
6.6. Local Search Operators fault. Once the search tree height is over 32, the NestedSolveDecision Decision enters in action and when the height of the three gets higher than 32, we make the CP solver Fail() to backtrack on line 23 thus keeping the height of the tree bounded. • Line 28: case DECISION_FOUND: The nested search found a solution that is the current solution. The 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 13.3 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 tutorials/cplusplus/chap6/dummy_ls.cc . We will use a dummy example throughout this section so we can solely focus on the basic ingredients provided by the or-tools library to do the local search. Our 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 ,...,xn−1
x0 + x1 + ... + 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 . The next figure shows the LS Operators hierarchy. 34
delta s and deltadelta s are explained in more details in section 6.8.
182
Chapter 6. Local search: the job-shop problem
LocalSearchOperator
IntVarLocalSearchOperator
SequenceVarLocalSearchOperator
PathOperator
These classes are declared in the header constraint_solver/constraint_solveri.h . The PathOperator class is itself the base class of several other path specialized LS Operators. We will review them in subsection 9.7.3.
IntVarLocalSearchOperator is a specialization of LocalSearchOperator built for an array of IntVars while SequenceVarLocalSearchOperator is a specialization of LocalSearchOperator built for an array of SequenceVars35 .
6.6.2 Defining a custom LS operator We will construct an LS Operator for an array of IntVars but the API for an array of SequenceVar s is similar36 . There are two methods to overwrite: • OnStart() : this private method is called each time the operator is synced with a new feasible solution; • MakeOneNeighbor() : this protected method creates a new feasible solution. As long as there are new solutions constructed it returns true, false otherwise. Some helper methods are provided: • int64 Value(int64 index) : returns the value in the current Assignment of the variable of given index; • int64 OldValue(int64 index) : returns the value in the last Assignment (the initial solution or the last accepted solution) of the variable of given index; • SetValue(int64 i, int64 value) : sets the value of the i th variable to value in the current Assignment and allows to construct a new feasible solution; • Size(): returns the size of the array of IntVars; • IntVar* Var(int64 index): returns the variable of given index. To construct a new feasible solution, just 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 35
At the time of writing, there are no LocalSearchOperator s defined for IntervalVar s. See subsection XXX for a workaround. 36 For instance, the SetValue() method is replaced by the SetForwardSequence() and SetBackwardSequence() methods.
183
6.6. Local Search Operators
solution(s) again and again or if you 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& 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 variable_index_ = variable_index_ + 1; return true;
- 1);
} 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. We have rewritten the protected method MakeOneNeighbor() to construct the next solutions. The variable variable_index_ indicates the current variable we are decreasing in the current solution. As long as there are remaining variables to decrease, MakeNextNeighbor() returns true. Once we have decreased the last variable (variable_index_ is then equal to Size()), it returns false. The private method OnStart() that is used whenever we start again with a new feasible solution, simply resets the variable index to 0 to be able to decrease the first variable x0 by 1. We use the LS Operator DecreaseOneVar in the function SimpleLS() that starts as follow:
184
Chapter 6. Local search: the job-shop problem
void SimpleLS(const int64 n, const bool init_phase) {
CHECK_GE(n, 2) << "size of problem (n) must be >= 2"; LOG(INFO) << "Simple LS " << (init_phase ? "with initial phase" : "with initial solution") << std::endl; Solver s("Simple LS"); vector 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 wihout knowing where to go. Next, based on the Boolean variable FLAG_initial_phase , we create DecisionBuilder to find an initial solution or we construct an initial Assignment:
a
// initial phase builder
DecisionBuilder * db = NULL; // initial solution
Assignment * const initial_solution = s.MakeAssignment(); if (init_phase) {
db = s.MakePhase(vars, Solver::CHOOSE_FIRST_UNBOUND, Solver::ASSIGN_MAX_VALUE); } else { initial_solution->Add(vars); for (int i = 0; i < n; ++i) { if (i % 2 == 0) { initial_solution->SetValue(vars[i], n - 1); } else { initial_solution->SetValue(vars[i], n - 2); } } }
As we assign the biggest value (ASSIGN_MAX_VALUE ) to the 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}]
185
6.6. Local Search Operators where the value for xn−1 is n − 2 if n is even and n − 1 otherwise37 . 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: ./dummy_ls -n=4 -initial_phase=false
we obtain the following partial output: Simple LS with initial solution Start search, memory used = 15.09 MB Root node processed (time = 0 ms, constraints = 2, memory used = 15.09 MB) Solution #0 (objective value = 10, ...) Solution #1 (objective value = 9, ...) 37
The modulo operator (mod) 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 %.
186
Chapter 6. Local search: the job-shop problem
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 better solution with value 7; neighborhood 4 around [1, 1, 3, 2]: [0, 1, 3, 2] is rejected as infeasible, [1, 0, 3, 2] is a new better solution with value 6; neighborhood 5 around [1, 0, 3, 2]: [0, 0, 3, 2], [0, −1, 3, 2] are [1, 0, 2, 2] is a new better solution with value 5;
rejected as infeasible,
neighborhood 6 around [1, 0, 2, 2]: [0, 1, 2, 2], [1, −1, 2, 2] are [1, 0, 1, 2] is a new better solution with value 4;
rejected as infeasible,
neighborhood 7 around [1, 0, 1, 2]: [0, 0, 1, 2], [1, −1, 1, 2] are [1, 0, 0, 2] is a new better solution with value 3;
rejected as infeasible,
neighborhood 8 around [1, 0, 0, 2]: [0, 0, 0, 2], [1, −1, 0, 2], [1, 0, −1, 2] are rejected as infeasible, [1, 0, 0, 1] is a new better solution with value 2; neighborhood 9 around [1, 0, 0, 1]: [0, 0, 0, 1], [1, −1, 0, 1], [1, 0, −1, 1] are rejected as infeasible, [1, 0, 0, 0] is a new better solution with value 1;
At this point, the solver is able to recognize that there are no more possibilities. The two last lines printed by the SearchLog summarize the local search: Finished search tree, ..., neighbors = 23, filtered neighbors = 23, accepted neighbors = 9, ...) End search (time = 1 ms, branches = 67, failures = 64, memory used = 15.13 MB, speed = 67000 branches/s)
There were indeed 23 constructed candidate solutions among which 23 (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
187
6.6. Local Local Search Search Operator Operators s
[0, 0, 0, 1], [1, [1, −1, 0, 1] and [1, [1, 0, −1, 1] and let the solver necessary to construct “solutions” [0, decide if they were interesting interesting or not. The answer is no. We could have filtered those solutions 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 neighbors is equal to the number of filtered neighbors). 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([3, tions (neighbors) (neighbors) and 11 accepted solutions are.
6.6.3 Comb Combining ining LS operator operators s Often, you want to combine several LocalSearchOperator s. This can be done with the ConcatenateOperators() method: LocalSearchOperator* ConcatenateOperators ConcatenateOperators( ( std::vector vector< & ops); const std::
This creates a LocalSearchOperator which concatenates concatenates a vector of operators. Each operator from the vector is called sequentially. By default, when a candidate solution is accepted, the neighborhood exploration restarts from the last active operator (the one which produced this candidate solution). This can be overriden by setting restart to true to force the exploration to start from the first operator in the vector: vector: LocalSearchOperator* Solver:: Solver::ConcatenateOperators( ConcatenateOperators( std::vector vector< & ops, bool restart); const std::
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. evaluated. Ownership Ownership of the callback is taken by the solver. solver. Here is an example: 10, , 100 100, , 10 10, , 0}; const int kPriorities = {10 int64 Evaluate( Evaluate(int active_operator, int current_ope current_operato rator) r) { return kPriorities[current_operator]; } LocalSearchOperator* concat = solver.ConcatenateOperators solver.ConcatenateOperators(operators, (operators, NewPermanentCallback(& NewPermanentCallback( &Evaluate));
The elements of the operators’ vector will be sorted by increasing priority and explored in that order (tie-breaks (tie-breaks are handled by keeping the relative operator operator order in the vector). vector). This would result in the following following order:
operators[3], operators[3], operators[0], operators[0], operators[2], operators[2], operators[1] operators[1] . Sometimes you don’t know in what order to proceed. Then the following method might help you:
188
Chapter Chapter 6. Local Local search: search: the job-shop job-shop problem problem
LocalSearchOperator* Solver:: Solver::RandomConcatenateOperators( RandomConcatenateOperators( std::vector vector< & ops); const std::
This LocalSearchOperator ca calls a random operator at each call to provid idee the the seed seed that that is used used to initi initial aliz izee the the ranranMakeNextNeighbor() . You can prov dom number generator: LocalSearchOperator* Solver:: Solver::RandomConcatenateOperators( RandomConcatenateOperators( std::vector vector< & ops, ops, int32 int32 seed); seed); const std::
6.6.4 Inter Interestin esting g LS oper operator ators s Several existing LocalSearchOperator s can be of great great help. Combine Combine these these operators operators with your own customized operators. PathOperator s will be reviewed reviewed in subsection subsection 9.7.3 9.7.3.. NeighborhoodLimit Neighborh oodLimit
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:: Solver::MakeNeighborhoodLimit( MakeNeighborhoodLimit( LocalSearchOperator* const op, int64 limit); limit);
MoveTowardTargetLS MoveTowar dTargetLS
Creates a local search operator that tries to move the assignment of some variables toward a target. target. The target target is given given as an Assignment. This operator operator generates candidate candidate solutions solutions which only have one variable that belongs to the target Assignment set to its target value. There are two factory methods to create a MoveTowardTargetLS operator: LocalSearchOperator* Solver:: Solver::MakeMoveTowardTargetOperator( MakeMoveTowardTargetOperator( Assignment& target); const Assignment&
and LocalSearchOperator* Solver:: Solver::MakeMoveTowardTargetOperator( MakeMoveTowardTargetOperator( std::vector vector< & variables, const std:: std::vector vector< & int64>& target_values); const std::
The target is here given by two std::vector s: a vector of variables and a vector of associated target values. The two vectors should be of the same length and the variables and values are ordered in the same way.
189
6.7. The jobshop jobshop problem problem:: and now with with local search! search! The variables are changed one after the other in the order given by the Assignment or the vector of variables. variables. When we restart from a new feasible feasible solution, we don’t start all over again from the first variable variable but keep changing variables variables from the last change. DecrementValue and IncrementValue
These operators operators do exactly exactly what their names say: they decrement decrement and increment increment by 1 the value of each variable variable one after the other. To create them, use the generic factory method LocalSearchOperator* Solver:: Solver::MakeOperator( MakeOperator( std::vector vector< & vars, const std:: Solver:: Solver::LocalSearchOp LocalSearchOperators erators 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 . Whene Whenever ver we we start to explore a new neighborhood, the variables are changed from the beginning of the vector anew.
Large Neighborhood Search (LNS) And last last but not not leas least, t, in or-tools, Larg Largee Neigh Neighbor borho hood od Sear Search ch is imple impleme ment nted ed with with LocalSearchOperator s but this is the topic of section ?? .
6.7 The jobshop jobshop problem: problem: and now with local local search! search! You can find the code in the files jobshop_ls.h , jobshop_ls1.cc , and jobshop_ls3.cc an and the data files jobshop_ls2.cc in first_example_jssp.txt and abz9. We have seen in the previous section how to implement local search on our dummy example. ample. This This time, we apply local local search on a real problem problem and present present the real thing: thing: the MakeOneNeighbor() method, the delta and deltadelta Assignment Assignment s and how to implement incremental LocalSearchOperator s. To solve the job-shop problem, we’ll define two basic LocalSearchOperator s. Firs First, t, we’ll apply them separately and then we’ll combine them to get better results. In doing so, we will discover discover that local search is very sensitive sensitive to the initial solution used to start it and that the search is path-dependent.
6.7.1 LocalSearchOperators: the real thing! Until now, we only have overloaded the MakeOneNeighbor() met metho hod d of a LocalSearchOperator but as we have seen in sub-section 6.5.3, 6.5.3, the real method
190
Chapter Chapter 6. Local Local search: search: the job-shop job-shop problem problem MakeOneNeighbor DecisionBuilder DecisionBuilder is MakeNextNeighbor() . call called ed by the the MakeOneNeighbor Before we dissect MakeNextNeighbor() , we quickly explain again what the deltas are. Delta s and DeltaDelta s
The idea behind the Deltas and DeltaDelta s is really simple: efficiency. Only the modified part of the solution is broadcast: • Delta: the difference difference between the initial solution that defines the neighborhood 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() MakeNextN eighbor()
The signature of the MakeNextNeighbor() method is: Assignment nt* deltadelta) bool MakeNextNeighbor(Assignment* delta, Assignme
This This method method construc constructs ts the delta and deltadelta corre correspo spondi nding ng to the new new cancandidate didate solution solution and returns returns true. If the the neig neighb hbor orho hood od has has been been exha exhaus uste ted, d, i.e. i.e. the the LocalSearchOperator cannot cannot find find anot another her candid candidat atee solut solutio ion, n, this this metho method d retur returns ns false. When you write your own MakeNextNeighbor() method, you have to provide the new delta but you can skip the deltadelta if you prefe preferr. This This deltadelta can be convenient 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 provide an inner mechanism that constructs automatautomatically these deltas when you use the following following self-explanatory self-explanatory setters: • for IntVarLocalSearchOperator s only:
SetValue( lue(int6 int64 4 index, index, int64 int64 value) value) ; – SetVa • for SequenceVarLocalSearchOperator s only: – SetForwardSequence SetForwardSequence(int64 (int64 index, const std::vector& std::vector& value) ;
SetBackwardSequence(int64 e(int64 index, const – SetBackwardSequenc std::vector& std::vector& value); • for both: both:
Activate(int64 nt64 index); – Activate(i Deactivate(int64 nt64 index). – Deactivate(i
191
6.7. The jobshop jobshop problem problem:: and now with with local search! search!
If you only use these methods to change the current solution, you then can automatically construct the deltas by calling the ApplyChanges() method and revert these changes by calling the RevertChanges() method. We recom recomme mend nd to use the the follo followi wing ng temp templa late te to defin definee your your MakeNextNeighbor() method: MakeNextNeighbor(Assignment* delta, virtual bool MakeNextNeighbor(Assignment Assignment* deltadelt deltadelta) a) { CHECK_NOTNULL(delta); true) ) { while (true RevertChanges(true RevertChanges(true); ); (NEIGHBORHOOD HOOD EXHAUSTE EXHAUSTED) D) { if (NEIGHBOR false; return false; } // CON CONSTR STRUCT UCT NEW CAN CANDID DIDATE ATE SOL SOLUTI UTION ON
... (ApplyChanges(del s(delta, ta, deltadelt deltadelta)) a)) { if (ApplyChange true; return true; } } false; 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 IntVarLocalSearchOperator s only: – int64 int64 Value(in Value(int64 t64 index) index) ;
Var(int64 index) index); – IntVar* Var(int64 int64 OldValue OldValue(int (int64 64 index) index) ; – int64 • for SequenceVarLocalSearchOperator s only:
const std::vec std::vector< tor int>& & Sequence Sequence(int (int64 64 index) index) ; – const – SequenceVar * Var(int64 Var(int64 index) index); – const const std::vec std::vector< tor int>& & OldSeque OldSequence( nce(int6 int64 4 index) index) ;
• for both: both:
bool IsIncremental() IsIncremental(); – bool bool Activate Activated(in d(int64 t64 index) index) ; – bool
192
Chapter Chapter 6. Local Local search: search: the job-shop job-shop problem problem
Why would I want MakeOneNeighbor() MakeOneNeighbor()?
to
use MakeNextNeighbor() in instead
of
One reason reason is efficien efficiency: cy: you skip one callback callback.. But the real reason reason is that you might need other methods than the ones that are provided to construct your candidate solution. In this case, you have no other choice than to reimplement the MakeNextNeighbor() method.
Incrementality [TO BE WRITTEN]
6.7.2 The initial initial solutio solution n We let let the CP sol solver con constru truct the initi itial solu olution ion for us. What abou bout reus eusing the DecisionBuilder defined in section 6.2 section 6.2 and and grab its first feasible solution? // Th This is de deci cisi sion on bu buil ilde der r wi will ll ra rank nk al all l ta task sks s on al all l ma mach chin ines es. .
DecisionBuilder* const sequence_phase = solver.MakePhase(all_sequen solver.MakePhase(all_sequences, ces, Solver:: Solver::SEQUENCE_DEFAULT); SEQUENCE_DEFAULT); // Afte After r th the e ra rank nkin ing g of ta task sks, s, th the e sc sche hedu dule le is st stil ill l lo loos ose e an and d an any y // ta task sk ca can n be po post stpo pone ned d at wi will ll. . Fi Fix x th the e ob obje ject ctiv ive e va vari riab able le to it its s // min minimu imum m val value. ue.
DecisionBuilder* const obj_phase = solver.MakePhase(objective_var, Solver:: Solver::CHOOSE_FIRST_UNBOUND, CHOOSE_FIRST_UNBOUND, Solver:: Solver::ASSIGN_MIN_VALUE); ASSIGN_MIN_VALUE); // In Init itia ial l so solu luti tion on fo for r th the e Lo Loca cal l Se Sear arch ch. .
Assignment* const first_solution = solver.MakeAssignment(); first_solution-> first_solution->Add(all_sequences); Add(all_sequences); first_solution-> first_solution->AddObjective(objective_var); AddObjective(objective_var); // St Stor ore e th the e fi firs rst t so solu luti tion on. .
DecisionBuilder* const store_db = solver.MakeStoreAssignment solver.MakeStoreAssignment(first_soluti (first_solution); on); // Th The e ma main in de deci cisi sion on bu buil ilde der r (r (ran anks ks al all l ta task sks, s, th then en fi fixe xes s th the e // objective_va objective_variable). riable).
DecisionBuilder* const first_solution_phase = solver.Compose(sequence_pha solver.Compose(sequence_phase, se, obj_phase, store_db); LOG(INFO) << "Looki "Looking ng for the fir first st sol soluti ution" on"; ; solver.Solve(first_solution_phase); n_phase); const bool first_solution_found = solver.Solve(first_solutio (first_solution_found) ) { if (first_solution_found LOG(INFO) << "Solu "Solutio tion n fou found nd wit with h mak makesp espan an = " << first_solution-> first_solution->ObjectiveValue(); ObjectiveValue(); } else { LOG(INFO) << "No ini initia tial l sol soluti ution on fou found! nd!" ";
193
6.7. The jobshop problem: and now with local search!
return;
}
If you have some troubles to follow, go back to section 6.2 to understand the sequence_phase and obj_phase DecisionBuilder s. 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 IntervalVar s on a SequenceVar is very common and the corresponding operator is often referred to as the 2-opt-, 2-exchange- or swap- operator. We implement a basic version that systematically exchanges all IntervalVar s for all SequenceVar s 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 exhausted, 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 . Pay attention to the fact that current_first_ and current_second_ are also updated inside the if conditions.
194
Chapter 6. Local search: the job-shop problem We are now ready to 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 sequence = Sequence(current_var_); const int tmp = sequence[current_first_]; sequence[current_first_] = sequence[current_second_]; sequence[current_second_] = tmp; SetForwardSequence(current_var_, sequence); if (ApplyChanges(delta, deltadelta)) { return true;
} } return false;
}
If Increment() returns false, we have exhausted the neighborhood and MakeNextNeighbor() must return false. Sequence() and SetForwardSequence() are two helper methods from the SequenceVarLocalSearchOperator class that allow us to use the ApplyChanges()‘‘ method to construct the ‘‘delta s. 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.data(), all_sequences.size()));
Then we need a complementary DecisionBuilder to construct feasible candidate solutions. 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, Solver::CHOOSE_RANDOM_RANK_FORWARD);
195
6.7. The jobshop problem: and now with local search!
DecisionBuilder* const complementary_ls_db = solver.MakeSolveOnce(solver.Compose(random_sequence_phase, obj_phase));
If we run the program jobshop_ls1 with our instance problem (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.) 51,295
Value 1051
Candidates 31172
Solutions 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 IntervalVars on a SequenceVar
of
contiguous
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 exchanging contiguous IntervalVars and using the std::next_permutation() function. You can find the declaration of this function in the header algorithm. Its customizable version reads like: template bool next_permutation (BidirectionalIterator first,
BidirectionalIterator last, Compare comp);
We accept the default values for the BidirectionalIterator and the Compare classes. It will rearrange the elements in the range [first,last) into the next lexicographically greater permutation. An example will clarify this jargon: No 1 2 3 4 5 6
Permutations 012 021 102 120 201 210
We have generated the permutations of 0,1,2 with std::next_permutation() . There are 3! = 6 permutations (the 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, etc38 . 38
This explanation is not rigorous but it is simple and you can fill the gaps. What happens if you start with 1 0 2? The std::next_permutation() function simply “returns” 1 2 0 (oops, there goes our rigour again!).
196
Chapter 6. Local search: the job-shop problem As usual with the std, the last element is not involved in the permutation. There is only one more detail we have to pay attention to. We ask the user to provide the length of the permutation with the 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 LocalSearchOperator :
the
constructor
of
the ShuffleIntervals
ShuffleIntervals(const SequenceVar* const* vars, int size, int max_length) : SequenceVarLocalSearchOperator(vars, size), max_length_(max_length), current_var_(-1), current_first_(-1), current_length_(-1) {}
vars and size are just the array of SequenceVar s and its size. max_length is the length of the sequence of IntervalVars to shuffle. Because you can have less IntervalVar s 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 IntervalVar s in the SequenceVar. Here is the code to increment the next permutation: bool Increment() { if (!std::next_permutation(current_permutation_.begin(),
current_permutation_.end())) { // No permutation anymore -> update indices
if (++current_first_ >
Var(current_var_)->size() - current_length_) { if (++current_var_ >= Size()) { return false; } current_first_ = 0; current_length_ = std::min(Var(current_var_)->size(), max_length_); current_permutation_.resize(current_length_); } // Reset first permutation in case we have to increase // the permutation.
for (int i = 0; i < current_length_; ++i) {
current_permutation_[i] = i; } // Start with the next permutation, not the identity // just constructed.
if(!std::next_permutation(current_permutation_.begin(), If you give it 2 1 0, this function returns false but there is a side effect as the array will be ordered! Thus in our case, we’ll get 0 1 2!
197
6.7. The jobshop problem: and now with local search!
current_permutation_.end())) { LOG(FATAL) << "Should never happen!"; } } return true;
}
Thanks to the std::next_permutation() function, this is a breeze! The OnStart() method is again straightforward: virtual void OnStart() {
current_var_ = 0; current_first_ = 0; current_length_ = std::min(Var(current_var_)->size(), max_length_); current_permutation_.resize(current_length_); for (int i = 0; i < current_length_; ++i) { current_permutation_[i] = i; } }
We just have to pay attention to resize() the std::vector current_permutation_ of indices and we start with the same permutation: [0, 1, 2, 3, ...] . We again use our template for the MakeNextNeighbor() method: virtual bool MakeNextNeighbor(Assignment* delta,
Assignment* deltadelta) { CHECK_NOTNULL(delta); while (true) { RevertChanges(true); if (!Increment()) { return false; } std::vector sequence = Sequence(current_var_); std::vector sequence_backup(current_length_); for (int i = 0; i < current_length_; ++i) { sequence_backup[i] = sequence[i + current_first_]; } for (int i = 0; i < current_length_; ++i) { sequence[i + current_first_] = sequence_backup[current_permutation_[i]]; } SetForwardSequence(current_var_, sequence); if (ApplyChanges(delta, deltadelta)) { return true; } } return false; }
If Increment() returns false, we have exhausted the neighborhood and MakeNextNeighbor() must return false. After the call to Increment(), we simply copy the indices according to the new generated permutation and call the helper method SetForwardSequence() to update the current SequenceVar variable. ApplyChanges() constructs the deltas for us.
198
Chapter 6. Local search: the job-shop problem File jobshop_ls2.cc is exactly the same as 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 2 3 4 5
Time (in s.) 12,301 21,312 170,087 584,173
Value 1016 1087 1034 1055
Candidates 4302 7505 70854 268478
Solutions 32 15 33 27
These results are typical for a local search operator. There certainly are several lessons to be drawn from these results, but 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) constructed 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 important to understand. The paths taken in both cases are different. The (practical) construction of the neighbourhoods is dynamic and path-dependent. Good (meta-)heuristics are path-aware: these heuristics take the path (and thus the history of the search) into account. Moreover, bigger neighbourhoods (shuffle_length = 3 ) 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 answer is yes. To find a better solution, we’ll first investigate how important the initial solution is and then we’ll enlarge our definition of a neighborhood by combining our two LocalSearchOperator s.
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: class LSInitialSolLimit : public ResultCallback< bool> { public:
199
6.7. The jobshop problem: and now with local search!
LSInitialSolLimit(Solver * solver, int64 global_time_limit, int solution_nbr_tolerance) : solver_(solver), global_time_limit_(global_time_limit), solution_nbr_tolerance_(solution_nbr_tolerance), time_at_beginning_(solver_ ->wall_time()), solutions_at_beginning_(solver_ ->solutions()), solutions_since_last_check_(0) {} //
Returns true if limit is reached, false otherwise.
virtual bool Run() { bool limit_reached = false; //
Test if time limit is reached.
if ((solver_ ->wall_time() - time_at_beginning_)
> global_time_limit_) { limit_reached = true; //
Test if we continue despite time limit reached.
if (solver_ ->solutions() - solutions_since_last_check_
>= solution_nbr_tolerance_) { //
We continue because we produce enough new solutions.
limit_reached = false; } } solutions_since_last_check_ = solver_ ->solutions(); return limit_reached;
} private:
Solver * solver_; int64 global_time_limit_; int solution_nbr_tolerance_; int64 time_at_beginning_; int solutions_at_beginning_; int solutions_since_last_check_; };
The main method in this callback is the virtual bool Run() method. This method returns true if our limit has been reached and false otherwise. The time limit in ms is given by global_time_limit . If the Search is still producing a certain amount solution_nbr_tolerance of solutions, we let the search continue. To initialize our 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, FLAGS_solutions_nbr_tolerance));
200
Chapter 6. Local search: the job-shop problem FLAGS_initial_time_limit_in_ms and FLAGS_solutions_nbr_tolerance are the two gflags flags we use in the constructor of the callback LSInitialSolLimit described above to limit the search. The initial solution is stored in an Assigment initial_solution . Now, we are ready to prepare the local search with our two LocalSearchOperator s combined.
Combining the two LocalSearchOperator s 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 LocalSearchOperator s. Here, we combine SwapIntervals and ShuffleIntervals : std::vector operators; LocalSearchOperator* const swap_operator = solver.RevAlloc(new SwapIntervals(all_sequences.data(), all_sequences.size())); operators.push_back(swap_operator); LocalSearchOperator* const shuffle_operator = solver.RevAlloc(new ShuffleIntervals(all_sequences.data(), all_sequences.size(), FLAGS_shuffle_length)); operators.push_back(shuffle_operator); LocalSearchOperator* const ls_concat = solver.ConcatenateOperators(operators, true);
The ConcatenateOperators() method takes an std::vector of LocalSearchOperator and a bool that indicates if we want to restart the operators one after the other in the order given by this vector once a solution has been found. The rest of the code is similar to that in the 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 ,
201
6.8. Filtering
we are not able to improve our best solution so far! As we said, local search is very sensitive to the initial solution chosen. In the next table, we start with different initial solutions: Initial time limit 1,000 2,000 3,000 4,000 5,000 6,000 7,000 ... >= 13,000
Initial sol. obj. 1114 1103 1093 1089 1073 1057 1042 ... 1016
Time 81,603 103,139 104,572 102,860 84,555 42,235 36,935 ... 19,229
Value 983 936 931 931 931 1012 1012 ... 1016
Candidates 49745 70944 70035 68359 63949 29957 26515 ... 13017
Solutions 35 59 60 60 60 32 32 ... 32
The 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 collects 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 necessarily the case. Our best result, 931 is obtained when we start from solutions with an average objective value. When we start with better solutions, like the one with an objective value of 1016, we completely miss the 931 solution! This 931 solution seems to be a local optimum for our local search and it seems we can not escape it. In chapter 7, we’ll see how some metaheuristics escape this local minimum. For now, we turn our attention to another preoccupation: if you read the Candidates column and compare it with the Solutions column, you can see that our algorithm produces lots of candidates and very few solutions. This is normal. Remember that every time a candidate (a neighbor) is produced, the CP solver takes the time to verify if this candidate is a feasible solution. This is costly. In the next section, 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. LocalSearchFilter s 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 LocalSearchFilter s 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 .
202
Chapter 6. Local search: the job-shop problem There are basically two methods to implement39 : virtual bool Accept(const Assignment* delta, const Assignment* deltadelta) = 0; virtual void Synchronize(const Assignment* assignment) = 0;
As you can see, these two methods are pure virtual methods and thus must be implemented. The Accept() method returns true if you accept the current candidate solution to be tested by the CP solver and false if you know you can skip this candidate solution. The candidate solution is given in terms of delta and deltadelta. These are provided by the MakeNextNeighbor() of the LocalSearchOperator . The Synchronize() method, lets you synchronize the LocalSearchFilter with the current solution, which allows you to reconstruct the candidate solutions given by the delta Assignment . If your LocalSearchOperator is incremental, you must notice the CP solver by implementing the IsIncremental() method: virtual bool IsIncremental() const { return true; }
By default, this method returns false.
6.8.2 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 poor 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& 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); } } 39
For IntVar , the specialized IntVarLocalSearchFilter offers convenient methods and you should rather implement the OnSynchronize() method that is called at the end of the Synchronize() method.
203
6.8. Filtering Several helper methods are defined in the IntVarLocalSearchFilter class: • int64 Value(int index) const: returns the value of the ith variable of the current solution. These values are automatically updated when Synchronize() is called; • IntVar* Var(int index) const: std::vector ;
returns the ith variable given in the
• 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 IntVarElement s 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:
204
Chapter 6. Local search: the job-shop problem ... LocalSearchFilter * const filter = s.RevAlloc( new ObjectiveValueFilter(vars)); std::vector 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 x 0 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?40 . We now obtain: ..., neighbors = 23, filtered neighbors = 9, accepted neighbors = 9, ... 40
To be fair, this solution is not as general as it should be. We didn’t take into account the fact that some IntervalVar variables can be non active but for IntVar s and SequenceVar s it works well.
205
6.8. Filtering Of course, we could have improved our LocalSearchOperator so that it doesn’t produce such infeasible solutions!
6.8.3 Interesting LocalSearchFilters Two LocalSearchFilter s have already been implemented in or-tools. There exist a general version of the two LocalSearchFilter s: ObjectiveFilter (and some subclasses) and VariableDomainFilter . It is easy to add a VariableDomainFilter , simply use LocalSearchFilter* Solver::MakeVariableDomainFilter();
The ObjectiveFilter is more interesting and exists in different 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 concern the variables given to the MakeLocalSearchObjectiveFilter() method: – SUM: The objective is the sum of the variables; – PROD: The objective is the product of the variables; – MAX: The objective is the max of the variables; – MIN: The objective is the min of the variables.
• the callbacks used: we refer the curious reader to the code in the 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.
206
Chapter 6. Local search: the job-shop problem
6.9 Summary 6.9.1 What is missing? 6.9.2 To go further: Examples: Lab:
207
CHAPTER
SEVEN
META-HEURISTICS: SEVERAL PREVIOUS PROBLEMS
Overview: Prerequisites:
• Basic knowledge of C++. • Basic knowledge of Constraint Programming (see chapter 1). • Basic knowledge of the Constraint Programming Solver (see chapter 2). • Basic knowledge about how to define an objective function (see section 3.3). • Section 5.3 on the inner working of the solver helps but is not mandatory. • For section 7.3: chapter 6 about local search and the job-shop problem. Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap7 . The files inside this directory are:
210
Chapter 7. Meta-heuristics: several previous problems
7.1 Meta-heuristics in or-tools 7.2 Search limits and SearchLimits 7.2.1 An example of a custom Searchlimit
7.3 Large neighborhood search (LNS): the job-shop problem 7.3.1 What is Large Neighborhood Search? 7.3.2 Large Neighborhood Search in or-tools 7.3.3 Interesting LNS operators SimpleLNS RandomLNS
7.3.4 An heuristic to solve the job-shop problem SequenceLns
Everything together
7.4 Restarting the search 7.4.1 Luby
7.5 Tabu search (TS) 7.5.1 The basic idea 7.5.2 The implementation 7.5.3 First results
7.6 Simulated annealing (SA) 7.6.1 The basic idea 7.6.2 The implementation
211
CHAPTER
EIGHT
CUSTOM CONSTRAINTS: THE ALLDIFFERENT_EXCEPT_0 CONSTRAINT
Classes under scrutiny: Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap4 . The files inside this directory are:
8.1 Basic working of the solver: constraints 8.1.1 Description
8.2 Consistency 8.3 The AllDifferent constraint 8.4 Changing dynamically the improvement step with a SearchMonitor
8.5 Summary
Part III Routing
CHAPTER
NINE
TRAVELLING SALESMAN PROBLEMS WITH CONSTRAINTS: THE TSP WITH TIME WINDOWS
The third part of this manual deals with Routing Problems: we have a graph1 and seek to 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 three chapters each deal with one of three broad categories of Routing Problems: • Chapter 9 deals with Node Routing Problems where nodes must to be visited and served. • Chapter 10 deals with Vehicle Routing Problems where vehicles serve clients along the routes. • Chapter 11 deals with Arc Routing Problems where arcs/edges must be visited and served. These three categories of problems share common properties but they all have their own paradigms and scientific communities. In this chapter, we’ll discover the RL with what is probably the most studied problem in Operations Research: the Travelling Salesman Problem (TSP)3 . We use the excellent C++ ePiX library4 to visualize TSP solutions in TSPLIB format and TSPTW solutions in López-Ibáñez-Blum and da Silva-Urrutia formats. 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. 1
Overview:
We start this chapter by presenting in broad terms the different categories of Routing Problems and describe the Routing Library (RL) in a nutshell. Next, we introduce the Travelling Salesman Problem (TSP) and the TSPLIB instances. To better understand the RL, we say a few words about its inner working and the CP model we use. Because most of the Routing Problems are intractable, we use Local Search. We explain our two phases approach in details and show how to model the TSP in a few lines. Finally, we model and solve the TSP with Time Windows. Prerequisites:
• Basic knowledge of C++. • Basic knowledge of Constraint Programming (see chapter 1). • Basic knowledge of the Constraint Programming Solver (see chapter 2). • Basic knowledge of Local Search (see chapter 6). Files:
You can 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 TSPLIBDistanceFunctions class.
TSPLIB
keywords
and
the
• 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 Travelling 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.
218
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows • tsptw_ls.cc : A specialized implementation of the TSPTW with the RL.
9.1 A whole zoo of Routing Problems This section is meant to make you aware that the 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 • The 1/s jk / • ...
∑ C
j Scheduling
Problem
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. 5
219
9.1. A whole zoo of Routing Problems
So what is a Routing Problem anyway?
Broadly speaking, a Routing Problem is a mathematical problem where you need to 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). Visiting constraints forbid or force to visit some or all nodes, edges and arcs. Often additional constraints are required to model real problems. Notice that what is known as the General Routing Problem in the 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 Salesman Problems (TSP)-like problems. In this manual, when we refer to NRP, we mean TSP-like problems, i.e. routing problems where nodes must be visited and served. We use it to refer to node-related Routing Problems and in contrast to arc-related Routing Problems. Most of the NRPs consider 1 vehicle of ∞ capacity, i.e. we seek one tour that covers all the required nodes.
Some problems • The Travelling Salesman Problem 9
Node Routing Problems might even describe problems unrelated to Routing Problems in the scientific litera-
ture!
220
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) vehicles. 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 respect some “time”-constraints (like the total duration of a route, time windows to serve clients, etc.). Clients are usually modelled by nodes and to solve a VRP, one seeks to 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 • ...
221
9.1. A whole zoo of Routing Problems Authoritative source: Golden, Bruce L.; Raghavan, S.; Wasil, Edward A. (Eds.). The Vehicle Routing Problem: Latest Advances and New Challenges . Springer, Series: Operations Research/Computer Science Interfaces Series, Vol. 43, 2008, 589 p.
The CVRP: The Capacitated Vehicle Routing Problem is... [insert epix graphic]
9.1.3 Arc Routing Problems Informal 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 Publishers, Dordrecht, 2000.
The CCPP: The Cumulative Chinese Postman Problem is ... [insert epix graphic]
222
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
9.2 The Routing Library (RL) in a nutshell The vehicle routing library lets one model and solve generic routing problems ranging from the Travelling Salesman Problem to more complex problems such as the Capacitated Vehicle Routing Problem with Time Windows. In this section, we present its main characteristics.
9.2.1 Objectives The objectives of the RL are to • model and solve generic routing problems out of the box; • provide modelling and solving blocks that can easily be reused; • make simple models simple to model; • allow extensibility. In short, we provide specialized primitives that you can assemble and customize to your needs.
9.2.2 Out of the box models To be precise, the RL only uses one model to solve different Routing Problems. 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 Salesman Problem (and its variants, ATSP, TSPTW, ...) to multi-vehicles problems with dimension constraints (capacities, time windows, ...) and various routing constraints (optional nodes, alternate nodes, ...). Have a look at subsections 9.2.6 and and 9.2.7 below to have an idea of the additional constraints you can use in this model.
9.2.3 On top of the CP library The RL is a layer above the CP Solver. Most of the internal cabling is hidden but can be accessed anytime. Everything is contained is one single class: the RoutingModel class. This class internaly uses an object of type Solver that can be accessed and queried: RoutingModel routing(...); Solver* const solver = routing.solver();
You can thus use the full power of the CP Solver and extend your models using the numerous available constraints. The RoutingModel class by itself only uses IntVars to model Routing Problems.
223
9.2. The Routing Library (RL) in a nutshell
9.2.4 Local Search We are mainly using CP-based Local Search and Large Neighborhood Search using routingspecific 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
224
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows node in each Disjunction . If these sets are singletons, then you have optional nodes. You can also force to visit at least one node in each or some of the Disjunctions. Again, we have automated and 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 . Or can you call them features of the RL? At least, to the best of our knowledge. See the subsection Can CP be compared to the holy grail of Operations Research? for more. 12 See the chapter on Arc Routing for a discussion about which Arc Routing Problems can be solved by the RL. 10 11
225
9.3. The Travelling Salesman Problem (TSP) Number of nodes The RoutingModel class has a limit on the maximum number of nodes it can handle13 . Indeed, its constructors take an regular int as the number of nodes it can model: RoutingModel(int nodes, ...);
By the ANSI/ISO standard, we are guaranteed to be able to declare at least a maximum of 32767 nodes. Since the problems we try to solve are intractable, 32767 nodes are most of the time enough14 . Constraint Programming techniques - at the time of writing - are not competitive with state of the art techniques (mostly Branch, Price and Cut with specialized heuristics to solve Linear Mixed Integer Programs) that can solve TSP with thousands of nodes to optimality. The strength of Constraint Programming lies in its ability to handle side constraints well such as time windows for instance.
You cannot visit a node twice The way the model is coded (see section ?? ) 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 problem in Operations Research. In this section, we briefly15 present this fascinating problem 13
And thus the number of vehicles too! If your platform restricts you too much, you can always adapt the code! 15 Google TSP, Traveling Saleman Problem or Travelling Salesman Problem to find lots of examples, explanations, applications, etc. 14
226
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows and the TSPLIB which stands for the TSP library and is a library of sample instances for the TSP (and related problems) from various origins and of various types. To read TSPLIB data, we have implemented our own TSPData class as none of the available source code are compatible with our licence. Feel free to use it! Finally, we like to visualize what we are doing. To do so, we use the excellent ePiX library through our TSPEpixData class.
9.3.1 The Problem Given a graph 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.
227
9.3. The Travelling Salesman Problem (TSP)
Symmetric or Asymmetric distances?
When we talk about a Travelling Salesman Problem, it is implicit that the distance between two nodes 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)̸ = 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 optimal solutions. Their TSPLIB format is the de facto standard format to encode TSP instances.
The TSPLIB format The TSPLIB format is explained in great details in the document TSPLIB95. Here is a small excerpt to understand the basics. Refer to the TSPLIB95 document for more. The complete TSPLIB collection of problems has been successfully solved to optimality with the Concorde code in 2005-2006. The convention in the TSPLIB is to number the nodes starting at 1. 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.tsp 18 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.
228
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
3 270 4 256 5 256 6 246 ... EOF
133 141 157 157
Some of the attributes 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
229
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 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 programming (yep!), etc.), the pointed object will be destroyed as soon as the pointer object is out of scope and destroyed. a
Several scenarii are possible. With reference counting, when more than one pointer refer to an object, it is only when the last pointer referring to the object is destroyed that the the object itself is destroyed. If you want to know more about this helpful technique, look up RAII (Resource Acquisition Is Initialization).
To read TSPLIB 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 treatment. Note that the format is only partially checked: bad inputs might cause undefined behaviour.
230
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
If during the parse phase an unknown keyword is encountered, the method exists and prints a FATAL LOG message: Unknown keyword: UNKNOWN
This method has been tested with almost all the 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
231
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 14.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).
232
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
9.4.1 The main idea: the node decision variables The model is node based: routes are paths linking nodes. For almost each node19 , we keep an IntVar* variable (stored internally in a private std::vector 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 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 difference between NodeIndex node identifiers and int64 indices representing nodes in solutions.
9.4.2 The auxiliary graph21 To understand how the auxiliary graph is constructed, we need to consider a more general Routing Problem than just a TSP with one vehicle. 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. 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 . 20
233
9.4. The model behind the scenes: the main decision variables 1
0
4
2
8
5 3
6
7
Starting depot Ending depot 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 NodeIndex es 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
4 8
5 3
6
7
Starting depot Ending depot 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. 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 . 22
234
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
4 7
10
5 3
9
2
6
11 12
8 Starting depot Ending depot 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. We’ll meet other auxiliary graphs in the chapter Arc Routing Problems with constraints: the Cumulative Chinese Postman Problem .
9.4.3 How to switch from NodeIndex to int64 and vice-versa? A NodeIndex behaves like a regular int but it is in fact an IntType. We use IntTypes to avoid annoying automatic castings between different integer types and to preserve a certain type-safety. A NodeIndex is a NodeIndex and 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; 23
Have a look at base/int-type.h if you want to know more about the IntType class.
235
9.4. The model behind the scenes: the main decision variables They are quicker and safer than a static_cast and ... give the correct results! Try to avoid RoutingModel::NodeIndex::value() unless really necessary.
NodeIndex es 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.
236
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
9.4.5 Not all int64 indices have a corresponding IntVar nexts_ variable Only internal nodes that can lead somewhere possess a decision variable. Only the nodes that are visited and the starting depots have a main decision IntVar variable. There are 9 original nodes in the next 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 1
Var 0
5
0 Var 2
Var 6
Var 3
2 Path
Var 4 Path p0 4 Var 5 6
7
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 subsection 14.11.2. Notice also that the internal int64 index of the node with NodeIndex 6 is... 5 and the int64 index of the node with NodeIndex 7 is...6!
9.4.6 To summarize Here is a little summary:
237
9.5. The model behind the scenes: overview
Types to represent nodes
What True node Ids Indices to follow routes
Types
NodeIndex int64
Comments Unique for each original node from 0 to n − 1. Not unique for each original node. Could be 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 scenes: overview In this section, we give an overview of the main basic components of our model. Most of these components will be detailed in this chapter and the next two chapters. In section 14.11, we describe the inner mechanisms of the RL in details. If you 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
238
The CP solver does an initial propagation to quickly skim these domains.
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
RoutingModel routing(...); Solver* const solver = routing.solver();
Most desirable features for an RL are directly accessible through the RoutingModel class though. The accessors (getters and setters) will be discussed throughout the third part of this manual. But it is good to know that, as a last resort, you have a complete access (read control) to the internals of the RL. Basically, two constructors are available depending on the number of depots: • if there is only one depot: //
42 nodes and 7 routes/vehicles
RoutingModel routing(42, 7); //
depot is node with NodeIndex 5
routing.SetDepot(5);
• if there are several start/end depots: //
create multi depots
std::vector > 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 > depots(2);
is mandatory.
9.5.2 Variables Basically, there are two type of variables: • Path variables: the main decision variables and additional variables to describe the different routes and • Dimension variables: these variables allow to add side constraints like time-windows, capacities, etc. and denote some quantities (the dimensions) along the routes. From now on in this section, we only use the internal int64 indices except if the indices are explicitly of type NodeIndex. This is worth a warning: For the rest of this section, we only use the internal int64 indices except if the indices are explicitly of type RoutingModel::NodeIndex .
239
9.5. The model behind the scenes: overview Path variables Path variables describe the different routes. There are three types of path variables that can be accessed with the following methods: • NextVar(i): the main decision variables. NextVar(i) == j is true if j is the node immediately reached from node i in the solution. • VehicleVar(i) : represents the vehicle/route index to which node i belongs in the solution. • ActiveVar(i) : a Boolean variable that indicates if a node i is visited or not in the solution. Main decision variables
You can access the main variables with the method NextVar(int64) : IntVar* var = routing.NextVar(42);
var is a pointer to the IntVar corresponding to the node with the int64 42 index. In a solution solution, the value of this variable gives the int64 index of the next node visited after this node: Assignment * const solution = routing.Solve(); ... int64 next_node = solution.Value(var);
Vehicles
Different routes/vehicles service different nodes. For each node i , VehicleVar(i) represents the IntVar* that represents the int index of the route/vehicle servicing node i in the solution: int route_number = solution->Value(routing.VehicleVar(i));
Taking a shortcut in the notation, we have that: if NextVar(i) == j then VehicleVar(j) == VehicleVar(i) . That is, both nodes i and j are serviced by the same vehicle. To grab the 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);
240
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) . We’ll discuss Disjunctions and optional nodes in details in section 11.4 when we will transform a Cumulative Chinese Postman Problem (CCPP) into a Generalized TSP (GTSP). A GTSP is similar to a TSP except that you have clusters of nodes you want to visit, i.e. you only want to visit 1 node in each cluster.
Dimension variables Dimension variables are used to accumulate quantities (or dimensions) along the routes. To denote a dimension, we use an std::string d . There are three types of dimension variables: • CumulVar(i, d): variables representing the quantity of dimension d when arriving at the node i. • TransitVar(i, d): variables representing the quantity of dimension d added after visiting the node i. • SlackVar(i, d): non negative slack variables such that (with the same abuse of notation as above): if NextVar(i) == j then CumulVar(j) = CumulVar(i) + TransitVar(i) + SlackVar(i) . For a time dimension, you can think of waiting times. You can add as many dimensions as you want25 . The transit values can be constant, defined with callbacks, vectors or matrices. You can represent any quantities along routes with dimensions but not only. For instance, capacities and time windows can be modelled with dimensions. 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 Disjunction s and pick-up and delivery constraints. 25
Well, as many as your memory allows...
241
9.5. The model behind the scenes: overview No cycle constraint One of the most 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 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: 26
242
For the specialists: for instance, primal and dual degenerate linear models.
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
• forbid partial paths from looping back to themselves and • ensure each variable/node can be connected to a sink. We refer the reader to subsection 14.11.4 for a detailed description of our internal NoCycle constraint. Disjunction constraints
Disjunction s 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& nodes);
where nodes represents the group of nodes. This constraint is equivalent to:
�
ActiveVar(i) = 1.
i∈Disjunction
You might want to use optional Disjunction s, i.e. a group of nodes out of which at most one node can be visited. This time, use: void AddDisjunction(const std::vector& nodes,
int64 penalty);
This constraint is equivalent to:
p +
�
ActiveVar(i) = 1
i∈Disjunction
where p is a boolean variable corresponding to the Disjunction and the objective function has an added (p * penalty) term. If none of the variables in the Disjunction is visited ( 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:
243
9.5. The model behind the scenes: overview
Solver* const solver = routing.solver(); solver->AddConstraint(solver->MakeEquality( routing.VehicleVar(routing.NodeToIndex(i)), routing.VehicleVar(routing.NodeToIndex(j)))); routing.AddPickupAndDelivery(i, j);
This constraint is counter-intuitive in a least two ways: 1. It is not modelled by a real constraint: this pair of nodes is used to filter out solutions. PathOperator s 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 Disjunction s. We detail each of these costs. 27
Actually, only an AllDifferent constraint on the NextVar s is added in the constructor of the RoutingModel class. This constraint reinforces the fact that you cannot visit a node twice.
244
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows The costs of the arcs To set the cost of each arc, use a NodeEvaluator2 callback to return the cost of each (i,j) arc: void SetCost(NodeEvaluator2* evaluator);
NodeEvaluator2 is simply28 a typedef for a ResultCallback2 , 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 NodeIndex es, 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 14.3.3. 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”.
245
9.5. The model behind the scenes: overview 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.
A penalty cost for missed Disjunction s 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.
246
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
2
3
1 5
4
(a)
1
1
2
2
3
3
4
4
5
5
(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 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. We will explore these and other lower bounds in section 11.7 when we’ll try to solve the Cumulative Chinese Postman Problem.
9.5.5 Miscellaneous We discuss here several improvements and conveniences of the RL.
Cache [TO BE WRITTEN]
Light constraints To speed up the search, it is sometimes better to only propagate on the bounds instead of the whole domains for the basic constraints. These “light” constraints are “checking” constraints, only triggered on WhenBound() events. They provide very little (or no) domain 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: 29
This bipartite graph is not really the one used by the CP solver but it’s close enough to get the idea.
247
9.6. The TSP in or-tools
DEFINE_bool(routing_use_light_propagation, false, "Use constraints with light propagation in routing model.");
When false, the RL uses the regular constraints seen in the previous parts of this manual. Try it, sometimes you can get a serious speed up. These light constraints are especially useful in Local Search.
Locks Often during the search, you 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. A lock is simply an std::vector that represents a partial route. Using this lock ensures that
NextVar(lock[i]) == lock[i+1] is true in the current solution. We will use locks in section 11.6 when we will try to solve the Cumulative Chinese Postman Problem.
9.6 The TSP in or-tools You can 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 implementation to show that a basic TSP can be coded in a few lines. Next, we develop a more realistic approach to solve the TSP. Our instances can be randomly generated or read from TSPLIB format 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 #include "constraint_solver/routing.h"
using operations_research; //
Cost function
int64 MyCost(RoutingModel::NodeIndex from, RoutingModel::NodeIndex to) { ...
248
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
return ...;
} int main(int argc, char **argv) {
RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle TSP.SetCost(NewPermanentCallback(MyCost)); const Assignment * solution = TSP.Solve(); //
Solution inspection
if (solution != NULL) {
std::cout << "Cost: " << solution->ObjectiveValue() << std::endl; for (int64 index = TSP.Start(0); !TSP.IsEnd(index); index = solution->Value(TSP.NextVar(index))) { std::cout << TSP.IndexToNode(index) << " "; } std::cout << std::endl; } else { std::cout << "No solution found" << std::endl; } return 0; }
Given an appropriate cost function, a TSP can be modelled and solved in 3 lines: RoutingModel TSP(42, 1);// 42 nodes, 1 vehicle TSP.SetCost(NewPermanentCallback(MyCost)); const Assignment * solution = TSP.Solve();
The cost function is given as a callback to the routing solver through its SetCost() method. Other alternatives are possible and will be detailed in the next sections.
9.6.2 Basic implementation You can 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 #include #include "base/commandlineflags.h" #include "constraint_solver/routing.h" #include "base/join.h"
249
9.6. The TSP in or-tools
#include "tsp.h" #include "tsp_epix.h"
base/join.h contains the StrCat() function that we use to concatenate strings. tsp.h contains the 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 ). Parameters Several command line parameters are defined in the files tsp.h, tsplib.h , tsp_epix.h and tsp.cc: Files
tsp.h
Parameter
Description
deterministic_random_seed Use deterministic random seeds or not? use_symmetric_distances Generate a symmetric TSP instance or not? Minimum allowed distance min_distance between two nodes. Maximum allowed distance max_distance between two nodes. Width of the pictures in cm. tsp_epix.h epix_width epix_height Height of the pictures in cm. Size of TSP instance. If 0, tsp.cc tsp_size must be read from a TSPLIB file. The starting node of the tour. tsp_depot tsp_data_file Input file with TSPLIB data. tsp_distance_matrix_file Output file with distance matrix. Width size of fields in output tsp_width_size files. Output file with generated sotsp_solution_file lution in TSPLIB format. tsp_epix_file ePiX solution file. 30 Time limit in ms, 0 means no tsp_time_limit_in_ms limit. 30
Default value
true true 10 100 10 10 0
1 empty string empty string 6 empty string empty string 0
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.
250
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows 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
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=\n\n"; usage += argv[0]; usage += " -tsp_data_file="; google::SetUsageMessage(usage); google::ParseCommandLineFlags(&argc, &argv, true); operations_research::TSPData tsp_data; if (FLAGS_tsp_size > 0) {
tsp_data.RandomInitialize(FLAGS_tsp_size); } else if (FLAGS_tsp_data_file != "") {
251
9.6. The TSP in or-tools
tsp_data.LoadTSPLIBFile(FLAGS_tsp_data_file); } else { google::ShowUsageWithFlagsRestrict(argv[0], "tsp"); exit(-1); } operations_research::TSP(tsp_data); return 0;
}
We start by writing the usage message that the user will see if she 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().
The TSP() function We only detail the relevant parts of the TSP() function. First, we create the CP solver: const int size = data.Size();
RoutingModel routing(size, 1); routing.SetCost(NewPermanentCallback(&data, &TSPData::Distance));
The constructor of the RoutingModel class takes the number of nodes (size) and the number of vehicle (1) as parameters. The distance function is encoded in the TSPData object given to the TSP() function. Next, we 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:
252
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
const Assignment* solution = routing.Solve();
Notice that the Solve() method returns a pointer to a const Assigment . The inspection of the solution is done as usual: if (solution != NULL) { // test solution
if (!data.CheckSolution(routing, solution)) {
LOG(ERROR) << "Solution didn’t pass the check test."; } else { LG << "Solution did pass the check test."; } // Solution cost.
LG << "Cost: " << solution->ObjectiveValue(); // Inspect solution.
string route; const int route_nbr = 0; for (int64 node = routing.Start(route_nbr); !routing.IsEnd(node); node = solution->Value(routing.NextVar(node))) { //LG << node;
route = StrCat(route, StrCat((FLAGS_start_counting_at_1 ? routing.IndexToNode(node).value() + 1 : routing.IndexToNode(node).value()), " - > ")); } route = StrCat(route, (FLAGS_start_counting_at_1 ? routing.IndexToNode(routing.End(route_nbr)).value() + 1 : routing.IndexToNode(routing.End(route_nbr)).value())); LG << route; } else { LG << "No solution found."; }
We use the method CheckSolution() of the TSPData class to ensure that the solution returned by the CP Solver is valid. This method only checks if every node has been used only once in the tour and if the objective cost matches the objective value of the tour.
9.6.3 How to avoid some edges? The classical way to deal with forbidden arcs between two cities when an algorithm expects a complete graph as input is to assign a large value 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. 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.
253
9.6. The TSP in or-tools 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 : 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 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.
254
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
<< ") has a larger value than M!"; ++number_forbidden_arcs_added; continue; } if (matrix_[MatrixIndex(from, to)] != FLAGS_M) {
matrix_[MatrixIndex(from, to)] = FLAGS_M; ++number_forbidden_arcs_added; } }
//
while(number_forbidden_arcs_added < number_of_forbidden_arcs)
}
Because our random number generator (as most random number generators) is not completely random and uniform, we need to be sure to exit the while loop. This is why we introduce the gflag: DEFINE_int32(percentage_forbidden_arcs_max, 94, "Maximum percentage of arcs to forbid");
We bound the percentage of forbidden arcs by 94% by default. [TO BE COMPLETED]
9.7 The two phases approach You can find the code in the file tsp_initial_solutions.cc .
255
9.8. The Travelling Salesman Problem with Time Windows (TSPTW)
9.7.1 The initial solution 9.7.2 The PathOperator class The TwoOpt PathOperator
9.7.3 Local Search PathOperators TwoOpt Relocate OrOpt Exchange Cross Inactive SwapActive ExtendedSwapActive PathLNS UnActiveLNS
How can I change the order of the LocalSearchOperator s?
9.7.4 Filters 9.7.5 A Local Search heuristic for the TSP
9.8 The Travelling Salesman Problem with Time Windows (TSPTW) You can 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 -
256
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 SilvaUrrutia formats. As in the case of the TSP, we have implemented a class to read those instances: the TSPTWData class. We also use the ePix library to visualize feasible solutions using the TSPTWEpixData class.
9.8.1 The Travelling Salesman Problem with Time Windows You might be surprised to learn that there is no common 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 minimum 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 a i (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 ti
time window [ ai, bi ]
bi
si
time
In real application, the time spent at a client might be limited to the service. For instance, you might wait in front of the 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 34
All TSP solutions are not TSPTW solutions! Notice how the depot is important for the TSPTW while it is not for the TSP.
257
9.8. The Travelling Salesman Problem with Time Windows (TSPTW) Some authors ([Dash2010] for instance) assign two costs on the edges: a travel cost and a travel time. While the travel times must respect the time windows constraints, the objective value is the sum of the travel costs on the edges. In this chapter, we only have one cost on the edges. The objective value and the real travel time are different: you might have to wait before servicing a client. Often, some conditions are applied to the time windows (in theory or practice). The only condition35 we will impose is that 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 contains 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) a i , bi , β i ∈ R+ , i.e. non negative real numbers and a i 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...
258
Chapter 9. Travelling Salesman Salesman Problems Problems with constraints: 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 4 2 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 typically represents represents the travel 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:
259
9.8. The Travelling Travelling Salesman Salesman Problem with Time Windows Windows (TSPTW)
# Su Sum m of se serv rvic ice e ti time mes: s: 52 522 2
The da Silva-Urrutia format We pres presen entt exact xactly ly the the same same insta instanc ncee as abov above. e. Here Here is the file file n20w20.001.txt (DSU_n20w20.001.txt in our directory /tutorials/cplusplus/chap9/ ): !! n20w20.001
16.75 391
CUST CUST NO. XCOORD XCOORD. . YCOORD YCOORD. . DEMAND DEMAND [READY [READY TIME] TIME] [DUE [DUE DATE] DATE] [SERVI [SERVICE CE TIME] TIME] 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 999
16.00 22.00 12.00 47.00 11.00 25.00 22.00 0.00 37.00 31.00 38.00 36.00 38.00 4.00 5.00 16.00 25.00 31.00 36.00 28.00 20.00 0.00
23.00 4.00 6.00 38.00 29.00 5.00 31.00 16.00 3.00 19.00 12.00 1.00 14.00 50.00 4.00 3.00 25.00 15.00 14.00 16.00 35.00 0.00
0.00 0.00 0 0. .00 0. 0.00 0. 0.00 0.00 0. 0.00 0 0. .00 0 0. .00 0.00 0.00 0.00 0.00 0. 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0. 0.00 0.00
0.00 62.00 181.00 306.00 214.00 51.00 102.00 175.00 250.00 3.00 21.00 79.00 78.00 140.00 354.00 42.00 2.00 24.00 20.00 9.00 275.00 0.00
408.00 68.00 205.00 324.00 217.00 61.00 129.00 186.00 263.00 23.00 49.00 90.00 96.00 154.00 386.00 63.00 13.00 42.00 33.00 21.00 300.00 0.00
0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00 0.00
Having seen the same instance, you don’t need much complementary info to understand this format. The first line of data (CUS UST T NO. 1) represents the depot and the last line marks the end of the file. As you can see, the authors authors are not really optimis optimistic tic about solving solving instances 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. obvious. It is not! See the remark on Travel-time Computation on the Jeffrey the Jeffrey Ohlmann and Barrett Thomas benchmark benchma rk page 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:
260
Chapter 9. Travelling Salesman Salesman Problems Problems with constraints: constraints: the TSP with time windows
• a first line with with a permutation permutation of the nodes; • a second line with with the objective objective value. 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 ./check_tsptw_solutions -tsptw_data_file -tsptw_data_file= =DSU_n20w20.001.txt -tsptw_solution_file= -tsptw_solution_file =n20w20.001.sol
This program checks if all the nodes have been serviced and if the solution is feasible: IsFeasibleSolution() { bool IsFeasibleSolution() ... //
for fo r loo oop p to test test eac each no nod de in the the to tour ur
for (. (... ..) ) { //
Tes Te st if we have have to wait wait at clie clien nt no node de
waiting_time = ReadyTime(node) - total_time; if (waiting_time > 0) { total_time = ReadyTime(node); } DueTime(node)) de)) { if (total_time + ServiceTime(node) > DueTime(no false; return false; } } ... true; 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:
waitingg time time waiting_time : • First, if we have to wait. We compute the waitin 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 ready time time is greate greaterr than total_time , waiti waiting_ ng_tim time e > 0 is true and we set total_time to ReadyTime(node) . • Second, Second, if the due times are respected, respected, i.e.: i.e.: is total_ti total_time me + ServiceT ServiceTime( ime(node node) ) DueTime(node) true? If not, the method returns false. If all the due times are respected, the method returns true. The output of the above command line is:
261
9.8. The Travelling Travelling Salesman Salesman Problem with Time Windows Windows (TSPTW)
TSPTW TSPTW instan instance ce of type da Silva-Urr Silva-Urrutia utia format format Solution Solution is feasible feasible! ! Loaded Loaded obj value: value: 378, 378, Comput Computed ed obj value: value: 387 Total Total computed computed travel travel time: time: 39 391 1 TSPTW TSPTW file DSU_n20w DSU_n20w20.00 20.001.tx 1.txt t (n=21, min min= =2, max max= =59 59, , sy sym? m? ye yes s) (!! n20w20 n20w20.00 .001 1 16.75 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 This is because because the distance distance matrix compute computed d is different different from the actual one really used to compute the objective value of the solution. We refer again the the read reader er to the the rema remark rk on Travel-time Computation from from Jeffr Jeffrey ey Ohlm Ohlmann ann and and Barr Barret ettt Thom Thomas as cited above. above. If you use the right distance matrix as in the López-Ibáñez-Blu López-Ibáñez-Blum m format, you get: TSPTW TSPTW instan instance ce of type López-Ibáñez-Blum López-Ibáñez-Blum format Solution Solution is feasible feasible! ! Loaded Loaded obj value: value: 378, 378, Comput Computed ed obj value: value: 378 Total Total computed computed travel travel time: time: 38 387 7 TSPTW TSPTW file LIB_n20w LIB_n20w20.00 20.001.tx 1.txt t (n=21, min min= =2, max max= =57 57, , sy sym? m? ye yes s)
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 378.
9.8.3 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:: std::string 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. treatment. Note that the instance’ instance’ss format is only partially partially checked: bad inputs might cause undefined behaviour behaviour.. To test if the instance instance was successfully successfully loaded, use: bool IsInstanceLoaded() const;
Several specialized getters are available:
std::stri string ng Name() Name() const const : returns the instance name, here the filename of the • std:: instance; • std::string returns a short short descripti description on of the std::string InstanceDetails() InstanceDetails() const : returns instance;
int Size() Size() co const nst: returns the size of the instance; • int 262
Chapter 9. Travelling Salesman Salesman Problems Problems with constraints: constraints: the TSP with time windows int64 Horizon( Horizon() ) const const : returns the horizon • int64 horizon of the instance, instance, i.e. the maximal due time; • int64 Distance(RoutingMo Distance(RoutingModel::Node del::NodeIndex Index from, RoutingM RoutingModel odel::No ::NodeIn deIndex dex to) const const : retu return rnss the the dist distan ance ce betw betwee een n the the two NodeIndexes; • RoutingModel::Node RoutingModel::NodeIndex Index Depot() const : returns the depot. This the first node given in the instance and solutions files.
int64 ReadyTim ReadyTime(Ro e(Routin utingMod gModel:: el::Node NodeInde Index x i) const const : • int64 ready time of node i;
returns the
• int64 return rnss the due due int64 DueTime( DueTime(Rout RoutingM ingModel odel::No ::NodeIn deIndex dex i) const const : retu time of node i
ServiceTime( ime(Rout RoutingM ingModel odel::No ::NodeIn deIndex dex i) const const : retur • int64 ServiceT returns ns the the service time of node i. The ServiceTime() method only makes sense when an instance is given in the da SilvaUrrutia 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 accumulated along the routes at each node. At a given node to, the accumulated time is the travel travel cost (from, om, to to) ) plus the time to service the node to . The TSPTWData class has a of the arc (fr special method to return this quantity: int64 CumulTime(RoutingModel CumulTime(RoutingModel:: ::Node NodeInde Index x from, from, RoutingModel:: RoutingModel::Node NodeInde Index x to) const { Distance(from, om, to) + ServiceTime(from); return Distance(fr }
To read solution files To read solution files, use the LoadTSPTWSolutionFile( (const std:: std::string string& & filename); void LoadTSPTWSolutionFile
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 anything with it. Three methods help you deal with the existence/feas existence/feasibility ibility of the solution: 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.
263
9.8. The Travelling Travelling Salesman Salesman Problem with Time Windows Windows (TSPTW)
Once you are sure that a solution is valid and feasible, you can query the loaded solution:
SolutionComputedTotalTravel talTravelTime() Time() const : comput • int64 SolutionComputedTo computes es the total travel travel time and returns returns it. The travel travel total total time time often often differ differss from the objecti objective ve value because of waiting times; SolutionComputedObjective() jective() const : • int64 SolutionComputedOb value value and returns it; • int64 SolutionLoadedObje SolutionLoadedObjective() ctive() const : stored in the instance file
comp comput utes es the the obje object ctiive
retu return rnss the the obje object ctiive value value
Thes Thesee meth method odss are are also also avai availa labl blee if the the solu soluti tion on was was obta obtain ined ed by the the solv solver er (in (in this this case, SolutionLoadedObjective() returns -1 and an d IsSolutionLoaded() returns false). The TSPTWData class doesn’t generate random instances. We wrote a little program for this purpose.
9.8.4 Rand Random om generation generation of instances instances You’ll find the code in the file tsptw_generator.cc . The TSPTW instance generator tsptw_generator is very very basic. basic. It generate generatess an instance instance in López-Ibáñez-Blum or/and da Silva-Urrutia as follows: • it gener generate atess n random points in the plane; • it generates generates a random random tour; tour; • it generates generates random service service times and and • it generates random time time windows windows such that the random solution is feasible. Several 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 Minimum window time length (default: (default: 10); • tsptw_time_window_max : Maximum Maximum window time length (default: (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 Silva-Urrutia format instance instance file or not? (default: (default: true);
264
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows By default, if the name of the instance is myInstance, tsptw_generator creates the three 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 :
265
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 Dimension s 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.
Dimension s are quantities accumulated along the nodes in a routing solution and can be used to model time windows. Remember the formula from last section: Total travel time to node j = Total travel time to i + Distance(i,j) + Service time at node i + Waiting time at i. 266
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows This is perfect for a Dimension. If NextVar(i) == j then
CumulVar(j) = CumulVar(i) + TransitVar(i) + SlackVar(i) . The correspondence is the following (NextVar(i) == j ): • CumulVar(i) : Total travel time to node i ; • TransitVar(i) : Distance(i,j) + Service time at node i ; • SlackVar(i) : Waiting time at i . 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:
267
9.9. The TSPTW in or-tools
for (RoutingModel::NodeIndex i(0); i < size; ++i) {
int64 index = routing.NodeToIndex(i); IntVar* const cumul_var = routing.CumulVar(index, "time"); cumul_var->SetMin(data.ReadyTime(i)); cumul_var->SetMax(data.DueTime(i)); }
We use the basic search strategy and turn off the large neighborhood search that can slow down the overall algorithm: routing.set_first_solution_strategy( RoutingModel::ROUTING_DEFAULT_STRATEGY); routing.SetCommandLineOption("routing_no_lns", "true");
Let’s test this TSPTW solver on the following generated instance in da Silva-Urrutia format (file DSU_test.tsptw ): !!
test
CUST NO. 1 2 3 4 5 999
XCOORD. 72.00 59.00 99.00 69.00 42.00 0.00
YCOORD. 22.00 3.00 8.00 46.00 72.00 0.00
DEMAND
READY TIME
0.00 0.00 0.00 0.00 0.00 0.00
0.00 197.00 147.00 242.00 56.00 0.00
DUE DATE
SERVICE TIME
504.00 216.00 165.00 254.00 67.00 0.00
2.00 2.00 9.00 3.00 9.00 0.00
We invoke: ./tsptw -instance_file=DSU_test.tsptw -solution_file=test.sol
and we obtain: 1 5 3 2 4 252
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: travel serve travel serve travel serve travel
268
to to to to
Nodes:
Releases:
Deadlines:
Services:
Durations:
Time:
4 4 2 2 1 1 3
56 56 147 147 197 197 242
67 67 165 165 216 216 254
9 9 9 9 2 2 3
58 9 86 9 40 2 44
58 67 153 162 202 204 248
Chapter 9. Travelling Salesman Problems with constraints: the TSP with time windows
serve 3 travel to 0 serve 0 Solution is feasible! Obj value = 252
242 0 0
254 504 504
3 2 2
3 24 2
251 275 277
If we solve the same instance but in 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:
travel to serve travel to serve travel to serve travel to serve travel to serve Solution is feasible! Obj value = 277
4 4 2 2 1 1 3 3 0 0
Releases:
Deadlines:
Services:
Durations:
Time:
56 56 147 147 197 197 242 242 0 0
67 67 165 165 216 216 254 254 504 504
0 0 0 0 0 0 0 0 0 0
67 0 95 0 42 0 47 0 26 0
67 67 162 162 204 204 251 251 277 277
Real instances, like DSU_n20w20.001.txt , are out of reach for our basic tsptw. This is mainly because finding a first feasible solution is in itself a difficult problem. In the next subsection, 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]
269
9.10. Summary
9.10 Summary summary
270
CHAPTER
TEN
VEHICULE ROUTING PROBLEMS WITH CONSTRAINTS: THE CAPACITATED VEHICLE ROUTING PROBLEM
Overview: Prerequisites:
• Basic knowledge of C++. • Basic knowledge of Constraint Programming (see chapter 1). • Basic knowledge of the Constraint Programming Solver (see chapter 2). • Basic knowledge of Local Search (see chapter 6). • Basic knowledge of the Routing Library (see the chapter 9), especially: – section 9.2; – section 9.4; – section 9.7. Files:
10.1 The Vehicle Routing Problem (VRP) You can 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 .
10.2. The VRP in or-tools
10.1.1 The Problem 10.1.2 Benchmark data The TSPLIB format for the CVRP The instance file The solution file
10.1.3 To read TSPLIB files 10.1.4 To generate a random CVRP: the CVRPDataGenerator class 10.1.5 To hold and check a (C)VRP solution: the CVRPSolution class 10.1.6 The CVRPData class: part I 10.1.7 Visualization with ePix
10.2 The VRP in or-tools You can 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 .
10.2.1 How to force all vehicles to service cities? 10.2.2 The basic program 10.2.3 Some outputs
10.3 The Capacitated Vehicle Routing Problem (CVRP) You can 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 .
272
Chapter 10. Vehicule Routing Problems with constraints: the capacitated vehicle routing problem
10.3.1 The problem 10.3.2 The CVRPData class: part II
10.4 The CVRP in or-tools You can 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 .
273
10.5. Multi-depots and vehicles
10.4.1 The demands as a Dimension 10.4.2 An initial solution 10.4.3 Different search strategies 10.4.4 What about individualizing the vehicles?
10.5 Multi-depots and vehicles 10.5.1 Problems with multi-depots 10.5.2 Multi-depots in practice 10.5.3 The VehicleVar() variables 10.5.4 VehicleClasses
10.6 Partial routes and Assigments 10.6.1 A little bit of vocabulary 10.6.2 Locks and the ApplyLocksToAllVehicles() method 10.6.3 Assignments and partial Assignments
10.7 Summary
274
CHAPTER
ELEVEN
ARC ROUTING PROBLEMS WITH CONSTRAINTS: THE CUMULATIVE CHINESE POSTMAN PROBLEM
Overview: Prerequisites: Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap11 . The files inside this directory are:
11.1. The Chinese Postman Problem (CPP)
11.1 The Chinese Postman Problem (CPP) 11.1.1 The Problem
11.2 The Cumulative Chinese Postman Problem (CCPP) 11.2.1 The Problem 11.2.2 Benchmark data 11.2.3 The CCPPData class 11.2.4 Visualization with ePix
11.3 A first implementation for the CCPP 11.4 Disjunctions 11.4.1 Active and non active nodes
11.5 A second implementation for the CCPP 11.6 Partial routes and locks 11.7 Lower bounds 11.8 Summary
276
Part IV Technicalities
CHAPTER
TWELVE
UTILITIES
12.1 Logging [TO BE REREAD] We provide very basic logging tools: macros replaced by some basic logging objects. They are defined in the header base/logging.h .
LG or LOG(INFO) is always working. You can print messages to std:cerr like this LG << "This is my important message with " << var << " pancakes.";
Of course, var must overwrite the << operator. The message is automatically followed by a \n that adds a new line. If you didn’t change the value of the gflags flag log_prefix to false, you’ll see the following message: [20:47:47] my_file.cc:42: This is my important message with 3 pancakes.
Your message is prefixed by the hour, the file name and the line number of the code source where your message was defined. You can disable this prefix by setting log_prefix to false. We provide different levels of logging: • First, depending on the severity: – INFO; – WARNING; – ERROR; – FATAL.
To use them, just write LOG(severity) as in: LOG(FATAL) << "This message will kill you!";
For the moment, INFO, ERROR and WARNING are treated the same way. FATAL works as expected and the program aborts (calls abort()) after printing the message.
12.2. Asserting
• Second, depending on the debug or release mode. When debugging, you can use DLOG(severity) with the same levels (and the same results). If NDEBUG is defined, you are in release mode and DLOG(severity) doesn’t do anything except for FATAL where it becomes a LOG(ERROR) . • Finally, you can also use VLOG(level) with different levels. The higher the level, the more detailed the information. By default, the level is set to 0. You can change this by setting the right level value to the gflags flag log_level . So, if FLAGS_log_level = 1 the following message is printed: VLOG(1) << "He, he, you can see me!";
but not this one: VLOG(2) << "This information is too detailed for you to see with your log level...";
We rarely (understand never) go over level 4. There is also a conditional logging: LOG_IF(severity, condition) and for debugging DLOG_IF(severity, condition) that vanishes when NDEBUG is defined. A little word of advice. When logging is allowed, you create each time a logging object so this can be costly. When logging is disallowed, you don’t pay anything.
12.2 Asserting We provide several assert-like macros in the header base/logging.h . Remember that the variable NDEBUG (“NO DEBUG”) is defined by the standard. By default, the assert debugging mechanism defined in assert.h or the C++ equivalent cassert is on. You have to explicitly turn it off by defining the variable NDEBUG. Two types of assert-like macros are provided: • Debug-only checking and • Always-on checking.
Debug-only macros are only triggered in DEBUG mode (i.e. when the variable NDEBUG is not defined) and start with the letter D. In NON DEBUG mode (the variable NDEBUG is defined), the code inside Debug-only macros vanishes. Always-on macros are always on duty. For instance, DCHECK(x) is Debug-only while CHECK() is Always-on. Here are the macros listed:
280
Chapter 12. Utilities Name
Tests if
(D)CHECK(x) (D)CHECK_GE(x,y) (D)CHECK_LT(x, y) (D)CHECK_GT(x, y) (D)CHECK_LE(x, y) (D)CHECK_EQ(x, y) (D)CHECK_NE(x, y)
(x) (x) (x) (x) (x) (x) (x)
>= (y) < (y) > (y) <= (y) == (y) != (y)
There is also the Always-on CHECK_NOTNULL(x) macro that tests if (x) != NULL. are also defined. Note that these macros are always functional. If you prefer to use safeguards that vanish in the release code, use their equivalent1 starting with a D : DCHECK_LT(x, y), etc. and compile with the NDEBUG variable set to 1. These macros are defined in the header logging.h:
12.3 Timing We propose two timers: a basic timer (WallTimer ) and a more advanced one (CycleTimer ). These two classes work under Windows, Linux and MacOS. The Solver class uses by default a WallTimer internally. Both timers are declared in the header base/timer.h .
12.3.1 Basic timer This basic timer is defined by the WallTimer class. This class proposes the usual methods: • void Start() • void Stop() • bool Reset() • void Restart() • bool IsRunning() const • int64 GetInMs() const • double Get() const
GetInMs() returns the elapsed time in milliseconds while Get() returns this time in seconds. If you need even more precise timing, use the following method: • static int64 GetTimeInMicroSeconds() 1
There is no equivalent for CHECK_NOTNULL(x) .
281
12.3. Timing that returns the time in microseconds. To measure the time, we query the system time and add or subtract the queried times. Our timers measure the wall times. To get the time in milliseconds, we use: System Linux Windows MacOS
Function
gettimeofday() clock() gettimeofday()
To get the time in microseconds, we use: System Linux Windows MacOS
Function
clock_gettime() QueryPerformanceCounter() and QueryPerformanceFrequency() mach_absolute_time() and mach_timebase_info()
What is the wall time?
The wall time is the real time that elapses from start to end of a program/task/process, including the time due to system delays (other programs running at the same time, waiting times for resources to become available, etc). In other words, it is the difference between the time at which a task finishes and the time at which the task started.
12.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() • void Start() • void Stop() • int64 GetInUsec() const • int64 GetInMs() const
GetInUsec() returns the elapsed time in microseconds and GetInMs() converts this time in milliseconds.
282
Chapter 12. Utilities
12.3.3 Integrated timer The Solver class comes with an integrated timer. By default, this timer is a WallTimer (We use a typedef ClockTimer for a WallTimer). This timer starts counting at the creation of the solver and is never reset. The 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";
12.4 Profiling 12.5 Debugging 12.6 Serializing 12.7 Visualizing 12.8 Randomizing
283
CHAPTER
THIRTEEN
MODELING TRICKS
Overview: Prerequisites: Classes under scrutiny: Files:
You can find the code in the directory documentation/tutorials/cplusplus/chap13 . The files inside this directory are:
13.1. Efficiency
13.1 Efficiency 13.1.1 Keep variables ordered
13.2 False friends and counter-intuitive ideas 13.2.1 Accepted solutions vs feasible solutions 13.2.2 Solve() vs SolveAndCommit() 13.2.3 LocalOptimumReached() vs LocalOptimum() 13.2.4 DebugString() doesn’t give the value of a solution 13.2.5 Solve() vs the StartSearch() - NextSolution() EndSearch() mechanism
13.3 What are my solving options? 13.3.1 The search mechanism 13.3.2 Global methods Solve() SolveAndCommit MakeNestedOptimize()
13.3.3 DecisionBuilders SolveOnce
13.3.4 Decisions NestedSolveDecision
13.3.5 Summary
286
14.1. Main files and directories
CHAPTER
FOURTEEN
UNDER THE HOOD
14.1 Main files and directories 14.2 Naming conventions and programming idioms 14.2.1 Naming conventions General naming conventions Methods
14.2.2 Programming idioms Factories Caches Callbacks Visitors
14.3 Main classes, structures and typedefs 14.3.1 BaseObjects 14.3.2 PropagationBaseObjects 14.3.3 Callbacks NewPermanentCallback()
14.4 The Trail struct 288
14.5 The Search class
Chapter 14. Under the hood
// Beginning of the search. virtual void EnterSearch(); // Restart the search. virtual void RestartSearch(); // End of the search. virtual void ExitSearch(); // Before calling DecisionBuilder::Next sion(DecisionBuilder* const b);
virtual
void
BeginNextDeci-
// After calling DecisionBuilder::Next, along with the returned decision. virtual void EndNextDecision(DecisionBuilder* const b, Decision* const d); // Before applying the decision virtual void ApplyDecision(Decision* const d); // Before refuting the Decision virtual void RefuteDecision(Decision* const d); // Just after refuting or applying the decision, apply is true after Apply. // This is called only if the Apply() or Refute() methods have not failed. virtual void AfterDecision(Decision* const d, bool apply); // Just when the failure occurs. virtual void BeginFail(); // After completing the backtrack. virtual void EndFail(); // Before the initial propagation. virtual void BeginInitialPropagation(); // After the initial propagation. virtual void EndInitialPropagation(); // This method is called when a solution is found. It asserts of the // solution is valid. A value of false indicate that the solution // should be discarded. virtual bool AcceptSolution(); // This method is called when a valid solution is found. If the // return value is true, then search will resume after. If the result // is false, then search will stop there. virtual bool AtSolution(); // When the search tree is 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 PeriodicCheck(); // Returns a percentage representing the propress of the search before // reaching limits. virtual int ProgressPercent() { return kNoProgress; } // Accepts the given model visitor. virtual void Accept(ModelVisitor* const visitor) const;
289
14.9. Local Search (LS)
// Registers itself on the solver such that it gets notified of the search // and propagation events. virtual void Install();
14.9 Local Search (LS) 14.10 Meta-heuristics and SearchMonitors 14.10.1 The Metaheuristic class 14.10.2 Callbacks to implement
14.11 The Routing Library (RL) You can 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.
14.11.1 Global constants Some global constant basic paratemers of the model are: Variables (pu/pr) solver_ (pr)
nodes_ (pr) vehicles_ (pr) start_end_count_ (pr) kUnassigned (pu) kNoPenalty (pu) RoutingModel:: kFirstNode (pu) RoutingModel:: kInvalidNodeIndex (pu) Size() (pu)
290
Descriptions CP Solver.
Queries
Solver* solver() const Total number of nodes. int nodes() const int vehicles() Total number of vehicles. const Total number of different (starting and None ending) depots. static const int = -1 kUnassigned static const int = -1 kNoPenalty RoutingModel:: RoutingModel:: NodeIndex(0) kFirstNode RoutingModel:: NodeIndex(-1) RoutingModel:: kInvalidNodeIndex Number of IntVar variables.
Size()
Chapter 14. Under the hood (pu) stands for public and (pr) for private. The int64 Size() const method returns nodes_ + vehicles_ - start_end_count_ , which is exactly the minimal number of variables needed to model the problem at hand with one variable per node (see next subsection). kUnassigned is used for unassigned indices.
14.11.2 The auxiliary graph You can 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
2
4 8
5 3
6
7
Starting depot Ending depot 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 > 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:
291
14.11. The Routing Library (RL)
1
0
2
4 8
5 3
6
7
Starting depot Ending depot 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.
292
Chapter 14. Under the hood
Numbering of the int64 indices
The SetStartEnd() method takes care of the numbering. Nodes in the original graph that lead somewhere (starting depots and transit nodes) are numbered from 0 to nodes_ + vehicles_ - start_end_count_ - 1 = Size() - 1. The end depots are numbered from Size() to Size() + vehicles_ - 1. The numbering corresponds to the order in which the original nodes RoutingModel::NodeIndex es 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
4 7
10
5 3
9
2
6
11 12
8 Starting depot Ending depot 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
293
14.11. The Routing Library (RL)
Node Node Node Node Node Node
index index index index index index
3 4 5 6 7 8
-> -> -> -> -> ->
Variable Variable Variable Variable Variable Variable
index index index index index index
3 4 5 6 -1 7
The variable indices are the int64 indices used internally in the RL. The Node Indexes correspond to the unique NodeIndex es 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
4 8
5 6
7
3 Starting depot Ending depot Starting and ending depot Transit node
We output the routes, first with the NodeIndex es and then with the internal int64 indices with: for (int p = 0; p < VRP.vehicles(); ++p) {
LG << "Route: " << p; string route; string index_route; for (int64 index = VRP.Start(p); !VRP.IsEnd(index); index = Solution->Value(VRP.NextVar(index))) { route = StrCat(route, StrCat(VRP.IndexToNode(index).value(), " - > ")); index_route = StrCat(index_route, StrCat(index, " - > ")); } route = StrCat(route, VRP.IndexToNode(VRP.End(p)).value()); index_route = StrCat(index_route, VRP.End(p)); LG << route; LG << index_route; }
and get: Route: 1 -> 0 1 -> 0 Route: 3 -> 5 3 -> 5 Route:
294
0 -> -> 1 -> -> 2
2 -> 4 2 -> 9 4 10
Chapter 14. Under the hood
3 -> 6 8 -> 6 Route: 4 -> 8 4 -> 7
-> -> 3 -> ->
7 11 7 12
Some remarks
• NodeIndex and int64 indices 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();
}
14.11.3 Variables Path variables Dimension variables
14.11.4 Constraints NoCycle constraint
14.12 Summary
295
Part V Apprendices
BIBLIOGRAPHY
[Williams2001] Williams, H.P. and Yan, H. Representations of the all_different Predicate of Constraint Satisfaction in Integer Programming , INFORMS Journal on Computing, V.3, n. 2, pp 96-103, 2001. [Gasarch2002] 23. (a) Gasarch. The P=?NP poll , SIGACT News 33 (2), pp 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. [Freuder1997] E. C. Freuder. In Pursuit of the Holy Grail , Constraints, Kluwer Academic Publishers, 2, pp. 57-61, 1997 [Abramson1997] D. Abramson and M. Randall. A Simulated Annealing code for General Integer Linear Programs, Annals of Operations Research, 86, pp. 3-24, 1997. [Lopez-Ortiz2003] Alejandro Lopez-Ortiz, Claude-Guy Quimper, John Tromp and Peter Van Beek. A fast and simple algorithm for bounds consistency of the all different constraint , Proceedings of the 18th international joint conference on 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 , XXX, 2007. [SmithEtAl] Barbara M. Smith, Kostas Stergiou and Toby Walsh. Modelling the Golomb Ruler Problem. Report 1999.12, School of computer studies, University of Leeds, 1999. [Hoffman1969] Hoffman, Loessi and Moore. Constructions for the Solution of the m Queens Problem, Mathematics Magazine, p. 66-72, 1969.
Bibliography [Jordan2009] Jordan and Brett. A survey of known results and research areas for n-queens , Discrete Mathematics, Volume 309, Issue 1, 2009, pp 1-31. [Garey1976] Garey, M. R., Johnson, D. S. and Sethi, R., The complexity of 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 , Computing, volume 69, nbr 1, pp 37-49, 2002. [Taillard1993] Taillard, E., 1993. Benchmarks for basic scheduling problems , European Journal of Operational Research, Elsevier, vol. 64(2), pages 278-285, January. [Adams1988] J. Adams, E. Balas, D. Zawack, The shifting bottleneck procedure for job shop scheduling. Management Science, 34, pp 391-401, 1988. [Christofides1976] Christofides, Nicos. Worst-case analysis of a new heuristic for the travelling salesman problem, Technical Report, Carnegie Mellon University, 388, 1976. [Eksioglu2009] B. Eksioglu, A. Volkan Vural, A. Reisman, The vehicle routing problem: A taxonomic review, Computers & Industrial Engineering, Volume 57, Issue 4, November 2009, Pages 1472-1483. [Prosser2003] J. C. Beck, P. Prosser and E. Selensky, Vehicle Routing and Job Shop Scheduling: What’s the difference?, Proc. of the 13th International Conference on Automated Planning 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.
300
INDEX
Symbols
FATAL, 279
–cp_model_stats, 58 –cp_no_solve, 57 –cp_print_model, 57 –cp_show_constraints, 57 –help, 45, 57 –helpmatch=S, 45 –helpon=FILE, 45 –helpshort, 45
G
A AddConstraint(), 38 Assignment, 40
C constraint AllDifferent, 32 cpviz, 92 cryptarithmetic puzzles, 30
D DebugString(), 56 DecisionBuilder, 38 DEFINE_bool, 44 DEFINE_double, 44 DEFINE_int32, 44 DEFINE_int64, 44 DEFINE_string, 44 DEFINE_uint64, 44 DLOG, 279 DLOG_IF, 280
E EndSearch(), 40 ERROR, 279
F factory method, 35
gflags, 44 log levels, 280 log prefix, 279 parameters read from a file, 250 replacement (routing.SetCommandLineOption()), 224 shortcuts, 45 types, 44 Golomb Ruler Problem, 49 Golomb ruler, 51
I INFO, 279 IntExpr, 36 IntVar, 35
L LG, 279 LOG(ERROR), 279 LOG(FATAL), 279 LOG(INFO), 279 LOG(WARNING), 279 LOG_IF, 280
M MakeAllDifferent(), 38 MakeAllSolutionCollector(), 40 MakeBestValueSolutionCollector(), 40 MakeDifference(), 62 MakeEquality(), 38 MakeFirstSolutionCollector(), 40 MakeIntConst(), 60 MakeIntVar(), 35 MakeIntVarArray(), 55