OR-Tools  9.3
tsp.cc
Go to the documentation of this file.
1// Copyright 2010-2021 Google LLC
2// Licensed under the Apache License, Version 2.0 (the "License");
3// you may not use this file except in compliance with the License.
4// You may obtain a copy of the License at
5//
6// http://www.apache.org/licenses/LICENSE-2.0
7//
8// Unless required by applicable law or agreed to in writing, software
9// distributed under the License is distributed on an "AS IS" BASIS,
10// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11// See the License for the specific language governing permissions and
12// limitations under the License.
13
14// A minimal TSP solver using MathOpt.
15//
16// In the Euclidean Traveling Salesperson Problem (TSP), you are given a list of
17// n cities, each with an (x, y) coordinate, and you must find an order to visit
18// the cities in to minimize the (Euclidean) travel distance.
19//
20// The MIP "cutset" formulation for the problem is as follows:
21// * Data:
22// n: An integer, the number of cities
23// (x_i, y_i): a pair of floats for each i in 1..n, the location of each
24// city
25// d_ij for all (i, j) pairs of cities, the distance between city i and j.
26// * Decision variables:
27// x_ij: A binary variable, indicates if the edge connecting i and j is
28// used. Note that x_ij == x_ji, because the problem is symmetric. We
29// only create variables for i < j, and have x_ji as an alias for
30// x_ij.
31// * MIP model:
32// minimize sum_{i=1}^n sum_{j=1, j < i}^n d_ij * x_ij
33// s.t. sum_{j=1, j != i}^n x_ij = 2 for all i = 1..n
34// sum_{i in S} sum_{j not in S} x_ij >= 2 for all S subset {1,...,n}
35// |S| >= 3, |S| <= n - 3
36// x_ij in {0, 1}
37// The first set of constraints are called the degree constraints, and the
38// second set of constraints are called the cutset constraints. There are
39// exponentially many cutset, so we cannot add them all at the start of the
40// solve. Instead, we will use a solver callback to view each integer solution
41// and add any violated cutset constraints that exist.
42//
43// Note that, while there are exponentially many cutset constraints, we can
44// quickly identify violated ones by exploiting that the solution is integer
45// and the degree constraints are all already in the model and satisfied. As a
46// result, the graph n nodes and edges when x_ij = 1 will be a degree two graph,
47// so it will be a collection of cycles. If it is a single large cycle, then the
48// solution is feasible, and if there multiple cycles, then taking the nodes of
49// any cycle as S produces a violated cutset constraint.
50//
51// Note that this is a minimal TSP solution, more sophisticated MIP methods are
52// possible.
53
54#include <iostream>
55#include <optional>
56
57#include "absl/flags/flag.h"
58#include "absl/random/random.h"
59#include "absl/random/uniform_real_distribution.h"
60#include "absl/strings/str_cat.h"
61#include "absl/strings/str_join.h"
62#include "ortools/base/file.h"
69
70ABSL_FLAG(int, num_cities, 50, "Number of cities in random TSP.");
71ABSL_FLAG(std::string, output, "",
72 "Write a svg of the solution here, or to standard out if empty.");
73ABSL_FLAG(bool, test_instance, false,
74 "Solve the test TSP instead of a random instance.");
75ABSL_FLAG(int, threads, 0,
76 "How many threads to solve with, or solver default if <= 0.");
77
78namespace {
79
81using Cycle = std::vector<int>;
82
83// Creates variables modeling the undirected edges for the TSP. For every (i, j)
84// pair in [0,n) * [0, n), a variable is created only for j < i, but querying
85// for the variable x_ij with j > i returns x_ji. Querying for x_ii (which does
86// not exist) gives a CHECK failure.
87//
88// The Model object passed in to create EdgeVariables must outlive this.
89class EdgeVariables {
90 public:
91 EdgeVariables(math_opt::Model& model, const int n) {
92 variables_.resize(n);
93 for (int i = 0; i < n; ++i) {
94 variables_[i].reserve(i);
95 for (int j = 0; j < i; ++j) {
96 variables_[i].push_back(
97 model.AddBinaryVariable(absl::StrCat("e_", i, "_", j)));
98 }
99 }
100 }
101
102 math_opt::Variable get(const int i, const int j) const {
103 CHECK_NE(i, j);
104 return i > j ? variables_[i][j] : variables_[j][i];
105 }
106
107 int num_cities() const { return variables_.size(); }
108
109 private:
110 std::vector<std::vector<math_opt::Variable>> variables_;
111};
112
113// Produces a random TSP problem where cities have random locations that are
114// I.I.D Uniform [0, 1].
115std::vector<std::pair<double, double>> RandomCities(int num_cities) {
116 absl::BitGen rand;
117 std::vector<std::pair<double, double>> cities;
118 for (int i = 0; i < num_cities; ++i) {
119 cities.push_back({absl::Uniform<double>(rand, 0.0, 1.0),
120 absl::Uniform<double>(rand, 0.0, 1.0)});
121 }
122 return cities;
123}
124
125std::vector<std::pair<double, double>> TestCities() {
126 return {{0, 0}, {0, 0.1}, {0.1, 0}, {0.1, 0.1},
127 {1, 0}, {1, 0.1}, {0.9, 0}, {0.9, 0.1}};
128}
129
130// Given an n city TSP instance, computes the n by n distance matrix using the
131// Euclidean distance.
132std::vector<std::vector<double>> DistanceMatrix(
133 const std::vector<std::pair<double, double>>& cities) {
134 const int num_cities = cities.size();
135 std::vector<std::vector<double>> distance_matrix(
136 num_cities, std::vector<double>(num_cities, 0.0));
137 for (int i = 0; i < num_cities; ++i) {
138 for (int j = 0; j < num_cities; ++j) {
139 if (i != j) {
140 const double dx = cities[i].first - cities[j].first;
141 const double dy = cities[i].second - cities[j].second;
142 distance_matrix[i][j] = std::sqrt(dx * dx + dy * dy);
143 }
144 }
145 }
146 return distance_matrix;
147}
148
149// Given the EdgeVariables and a var_values containing the value of each edge in
150// a solution, returns an n by n boolean matrix of which edges are used (with
151// false diagonal elements). It is assumed that var_values are approximately 0-1
152// integer.
153std::vector<std::vector<bool>> EdgeValues(
154 const EdgeVariables& edge_vars,
155 const math_opt::VariableMap<double>& var_values) {
156 const int n = edge_vars.num_cities();
157 std::vector<std::vector<bool>> edge_values(n, std::vector<bool>(n, false));
158 for (int i = 0; i < n; ++i) {
159 for (int j = 0; j < n; ++j) {
160 if (i != j) {
161 edge_values[i][j] = var_values.at(edge_vars.get(i, j)) > 0.5;
162 }
163 }
164 }
165 return edge_values;
166}
167
168// Given an n by n boolean matrix of edge values, returns a cycle decomposition.
169// it is assumed that edge values respects the degree constraints (each row has
170// only two true entries). Each cycle is represented as a list of cities with
171// no repeats.
172std::vector<Cycle> FindCycles(
173 const std::vector<std::vector<bool>>& edge_values) {
174 // Algorithm: maintain a "visited" bit for each city indicating if we have
175 // formed a cycle containing this city. Consider the cities in order. When you
176 // find an unvisited city, start a new cycle beginning at this city. Then,
177 // build the cycle by finding an unvisited neighbor until no such neighbor
178 // exists (every city will have two neighbors, but eventually both will be
179 // visited). To find the "unvisited neighbor", we simply do a linear scan
180 // over the cities, checking both the adjacency matrix and the visited bit.
181 //
182 // Note that for this algorithm, in each cycle, the city with lowest index
183 // will be first, and the cycles will be sorted by their city of lowest index.
184 // This is an implementation detail and should not be relied upon.
185 const int n = edge_values.size();
186 std::vector<Cycle> result;
187 std::vector<bool> visited(n, false);
188 for (int i = 0; i < n; ++i) {
189 if (visited[i]) {
190 continue;
191 }
192 std::vector<int> cycle;
193 std::optional<int> next = i;
194 while (next.has_value()) {
195 cycle.push_back(*next);
196 visited[*next] = true;
197 int current = *next;
198 next = std::nullopt;
199 // Scan for an unvisited neighbor. We can start at i+1 since we know that
200 // everything from i back is visited.
201 for (int j = i + 1; j < n; ++j) {
202 if (!visited[j] && edge_values[current][j]) {
203 next = j;
204 break;
205 }
206 }
207 }
208 result.push_back(cycle);
209 }
210 return result;
211}
212
213// Given a cycle and an EdgeVariables, returns the cutset constraint for the set
214// of nodes in cycle.
215math_opt::BoundedLinearExpression CutsetConstraint(
216 const Cycle& cycle, const EdgeVariables& edge_vars) {
217 const int n = edge_vars.num_cities();
218 const absl::flat_hash_set<int> cycle_as_set(cycle.begin(), cycle.end());
219 std::vector<int> not_in_cycle;
220 for (int i = 0; i < n; ++i) {
221 if (!cycle_as_set.contains(i)) {
222 not_in_cycle.push_back(i);
223 }
224 }
225 math_opt::LinearExpression cutset_edges;
226 for (const int in_cycle : cycle) {
227 for (const int out_of_cycle : not_in_cycle) {
228 cutset_edges += edge_vars.get(in_cycle, out_of_cycle);
229 }
230 }
231 return cutset_edges >= 2;
232}
233
234// Solves the TSP by returning the ordering of the cities that minimizes travel
235// distance.
236absl::StatusOr<Cycle> SolveTsp(
237 const std::vector<std::pair<double, double>>& cities) {
238 const int n = cities.size();
239 const std::vector<std::vector<double>> distance_matrix =
240 DistanceMatrix(cities);
241 CHECK_GE(n, 3);
242 math_opt::Model model("tsp");
243 const EdgeVariables edge_vars(model, n);
245 for (int i = 0; i < n; ++i) {
246 for (int j = i + 1; j < n; ++j) {
247 edge_cost += edge_vars.get(i, j) * distance_matrix[i][j];
248 }
249 }
250 model.Minimize(edge_cost);
251
252 // Add the degree constraints
253 for (int i = 0; i < n; ++i) {
255 for (int j = 0; j < n; ++j) {
256 if (i != j) {
257 neighbors += edge_vars.get(i, j);
258 }
259 }
260 model.AddLinearConstraint(neighbors == 2, absl::StrCat("n_", i));
261 }
263 const int threads = absl::GetFlag(FLAGS_threads);
264 if (threads > 0) {
265 args.parameters.threads = threads;
266 }
267 args.callback_registration.events.insert(
268 math_opt::CallbackEvent::kMipSolution);
270 args.callback = [&edge_vars](const math_opt::CallbackData& cb_data) {
271 // At event CallbackEvent::kMipSolution, a solution is always present.
272 CHECK(cb_data.solution.has_value());
273 const std::vector<Cycle> cycles =
274 FindCycles(EdgeValues(edge_vars, *cb_data.solution));
276 if (cycles.size() > 1) {
277 for (const Cycle& cycle : cycles) {
278 result.AddLazyConstraint(CutsetConstraint(cycle, edge_vars));
279 }
280 }
281 return result;
282 };
284 math_opt::Solve(model, math_opt::SolverType::kGurobi, args));
285 if (result.termination.reason != math_opt::TerminationReason::kOptimal) {
287 << "Expected TSP solve terminate with reason optimal, found: "
288 << result.termination;
289 }
290 std::cout << "Route length: " << result.objective_value() << std::endl;
291 const std::vector<Cycle> cycles =
292 FindCycles(EdgeValues(edge_vars, result.variable_values()));
293 CHECK_EQ(cycles.size(), 1);
294 CHECK_EQ(cycles[0].size(), n);
295 return cycles[0];
296}
297
298// Produces an SVG to draw a route for a TSP.
299std::string RouteSvg(const std::vector<std::pair<double, double>>& cities,
300 const Cycle& cycle) {
301 constexpr int image_px = 1000;
302 constexpr int r = 5;
303 constexpr int image_plus_border = image_px + 2 * r;
304 std::vector<std::string> svg_lines;
305 svg_lines.push_back(absl::StrCat("<svg width=\"", image_plus_border,
306 "\" height=\"", image_plus_border, "\">"));
307 std::vector<std::string> polygon_coords;
308 for (const int city : cycle) {
309 const int x =
310 static_cast<int>(std::round(cities[city].first * image_px)) + r;
311 const int y =
312 static_cast<int>(std::round(cities[city].second * image_px)) + r;
313 svg_lines.push_back(absl::StrCat("<circle cx=\"", x, "\" cy=\"", y,
314 "\" r=\"", r, "\" fill=\"blue\" />"));
315 polygon_coords.push_back(absl::StrCat(x, ",", y));
316 }
317 std::string polygon_coords_string = absl::StrJoin(polygon_coords, " ");
318 svg_lines.push_back(
319 absl::StrCat("<polygon fill=\"none\" stroke=\"blue\" points=\"",
320 polygon_coords_string, "\" />"));
321 svg_lines.push_back("</svg>");
322 return absl::StrJoin(svg_lines, "\n");
323}
324
325void RealMain() {
326 std::vector<std::pair<double, double>> cities;
327 if (absl::GetFlag(FLAGS_test_instance)) {
328 cities = TestCities();
329 } else {
330 cities = RandomCities(absl::GetFlag(FLAGS_num_cities));
331 }
332 absl::StatusOr<Cycle> solution = SolveTsp(cities);
333 if (!solution.ok()) {
334 LOG(QFATAL) << solution.status();
335 }
336 const std::string svg = RouteSvg(cities, *solution);
337 if (absl::GetFlag(FLAGS_output).empty()) {
338 std::cout << svg << std::endl;
339 } else {
340 QCHECK_OK(
341 file::SetContents(absl::GetFlag(FLAGS_output), svg, file::Defaults()));
342 }
343}
344
345} // namespace
346
347int main(int argc, char** argv) {
348 InitGoogle(argv[0], &argc, &argv, true);
349 RealMain();
350 return 0;
351}
#define CHECK(condition)
Definition: base/logging.h:495
#define CHECK_EQ(val1, val2)
Definition: base/logging.h:703
#define CHECK_GE(val1, val2)
Definition: base/logging.h:707
#define QCHECK_OK
Definition: base/logging.h:45
#define CHECK_NE(val1, val2)
Definition: base/logging.h:704
#define LOG(severity)
Definition: base/logging.h:420
#define ASSIGN_OR_RETURN(lhs, rexpr)
const V & at(const K &k) const
Definition: id_map.h:480
Block * next
GRBmodel * model
void InitGoogle(const char *usage, int *argc, char ***argv, bool deprecated)
Definition: init_google.h:32
int Defaults()
Definition: base/file.h:120
absl::Status SetContents(const absl::string_view &filename, const absl::string_view &contents, int flags)
Definition: base/file.cc:196
absl::StatusOr< SolveResult > Solve(const Model &model, const SolverType solver_type, const SolveArguments &solve_args, const SolverInitArguments &init_args)
Definition: solve.cc:94
StatusBuilder InternalErrorBuilder()
absl::flat_hash_set< CallbackEvent > events
Definition: callback.h:161
void AddLazyConstraint(BoundedLinearExpression linear_constraint)
Definition: callback.h:243
const VariableMap< double > & variable_values() const
int main(int argc, char **argv)
Definition: tsp.cc:347
ABSL_FLAG(int, num_cities, 50, "Number of cities in random TSP.")