Files
ortools-clone/ortools/math_opt/samples/cpp/tsp.cc
Corentin Le Molgat c34026b101 Bump copyright to 2025
note: done using
```sh
git grep -l "2010-2024 Google" | xargs sed -i 's/2010-2024 Google/2010-2025 Google/'
```
2025-01-10 11:33:35 +01:00

365 lines
14 KiB
C++

// Copyright 2010-2025 Google LLC
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// A minimal TSP solver using MathOpt.
//
// In the Euclidean Traveling Salesperson Problem (TSP), you are given a list of
// n cities, each with an (x, y) coordinate, and you must find an order to visit
// the cities which minimizes the (Euclidean) travel distance.
//
// The MIP "cutset" formulation for the problem is as follows:
// * Data:
// n: An integer, the number of cities
// (x_i, y_i): a pair of floats for each i in N={0..n-1}, the location of
// each city
// d_ij for all (i, j) pairs of cities, the distance between city i and j
// (derived from the cities coordinates (x_i, y_i); this function is
// symmetric, i.e. d_ij = d_ji).
// * Decision variables:
// x_ij: A binary variable, indicates if the edge connecting i and j is
// used. Note that x_ij == x_ji, because the problem is symmetric. We
// only create variables for i < j, and have x_ji as an alias for
// x_ij.
// * MIP model:
// minimize sum_{i in N} sum_{j in N, j < i} d_ij * x_ij
// s.t. sum_{j in N, j != i} x_ij = 2 for all i in N
// sum_{i in S} sum_{j not in S} x_ij >= 2 for all S subset N
// |S| >= 3, |S| <= n - 3
// x_ij in {0, 1}
// The first set of constraints are called the degree constraints, and the
// second set of constraints are called the cutset constraints. There are
// exponentially many cutset, so we cannot add them all at the start of the
// solve. Instead, we will use a solver callback to view each integer solution
// and add any violated cutset constraints that exist.
//
// Note that, while there are exponentially many cutset constraints, we can
// quickly identify violated ones by exploiting that the solution is integer
// and the degree constraints are all already in the model and satisfied. As a
// result, the graph n nodes and edges when x_ij = 1 will be a degree two graph,
// so it will be a collection of cycles. If it is a single large cycle, then the
// solution is feasible, and if there multiple cycles, then taking the nodes of
// any cycle as S produces a violated cutset constraint.
//
// Note that this is a minimal TSP solution, more sophisticated MIP methods are
// possible.
#include <cmath>
#include <iostream>
#include <optional>
#include <ostream>
#include <string>
#include <utility>
#include <vector>
#include "absl/container/flat_hash_set.h"
#include "absl/flags/flag.h"
#include "absl/log/check.h"
#include "absl/random/random.h"
#include "absl/status/status.h"
#include "absl/status/statusor.h"
#include "absl/strings/str_cat.h"
#include "absl/strings/str_join.h"
#include "absl/types/span.h"
#include "ortools/base/helpers.h"
#include "ortools/base/init_google.h"
#include "ortools/base/logging.h"
#include "ortools/base/options.h"
#include "ortools/base/status_macros.h"
#include "ortools/math_opt/cpp/math_opt.h"
ABSL_FLAG(int, num_cities, 50, "Number of cities in random TSP.");
ABSL_FLAG(std::string, output, "",
"Write a svg of the solution here, or to standard out if empty.");
ABSL_FLAG(bool, test_instance, false,
"Solve the test TSP instead of a random instance.");
ABSL_FLAG(int, threads, 0,
"How many threads to solve with, or solver default if <= 0.");
ABSL_FLAG(bool, solve_logs, false,
"Have the solver print logs to standard out.");
ABSL_FLAG(operations_research::math_opt::SolverType, solver,
operations_research::math_opt::SolverType::kGscip,
"What underlying MIP solver to use (must support callbacks).");
namespace {
namespace math_opt = operations_research::math_opt;
using Cycle = std::vector<int>;
// Creates variables modeling the undirected edges for the TSP. For every (i, j)
// pair in [0,n) * [0, n), a variable is created only for j < i, but querying
// for the variable x_ij with j > i returns x_ji. Querying for x_ii (which does
// not exist) gives a CHECK failure.
//
// The Model object passed in to create EdgeVariables must outlive this.
class EdgeVariables {
public:
EdgeVariables(math_opt::Model& model, const int n) {
variables_.resize(n);
for (int i = 0; i < n; ++i) {
variables_[i].reserve(i);
for (int j = 0; j < i; ++j) {
variables_[i].push_back(
model.AddBinaryVariable(absl::StrCat("e_", i, "_", j)));
}
}
}
math_opt::Variable get(const int i, const int j) const {
CHECK_NE(i, j);
return i > j ? variables_[i][j] : variables_[j][i];
}
int num_cities() const { return static_cast<int>(variables_.size()); }
private:
std::vector<std::vector<math_opt::Variable>> variables_;
};
// Produces a random TSP problem where cities have random locations that are
// I.I.D Uniform [0, 1].
std::vector<std::pair<double, double>> RandomCities(int num_cities) {
absl::BitGen rand;
std::vector<std::pair<double, double>> cities;
for (int i = 0; i < num_cities; ++i) {
cities.push_back({absl::Uniform<double>(rand, 0.0, 1.0),
absl::Uniform<double>(rand, 0.0, 1.0)});
}
return cities;
}
std::vector<std::pair<double, double>> TestCities() {
return {{0, 0}, {0, 0.1}, {0.1, 0}, {0.1, 0.1},
{1, 0}, {1, 0.1}, {0.9, 0}, {0.9, 0.1}};
}
// Given an n city TSP instance, computes the n by n distance matrix using the
// Euclidean distance.
std::vector<std::vector<double>> DistanceMatrix(
absl::Span<const std::pair<double, double>> cities) {
const int num_cities = static_cast<int>(cities.size());
std::vector<std::vector<double>> distance_matrix(
num_cities, std::vector<double>(num_cities, 0.0));
for (int i = 0; i < num_cities; ++i) {
for (int j = 0; j < num_cities; ++j) {
if (i != j) {
const double dx = cities[i].first - cities[j].first;
const double dy = cities[i].second - cities[j].second;
distance_matrix[i][j] = std::sqrt(dx * dx + dy * dy);
}
}
}
return distance_matrix;
}
// Given the EdgeVariables and a var_values containing the value of each edge in
// a solution, returns an n by n boolean matrix of which edges are used (with
// false diagonal elements). It is assumed that var_values are approximately 0-1
// integer.
std::vector<std::vector<bool>> EdgeValues(
const EdgeVariables& edge_vars,
const math_opt::VariableMap<double>& var_values) {
const int n = edge_vars.num_cities();
std::vector<std::vector<bool>> edge_values(n, std::vector<bool>(n, false));
for (int i = 0; i < n; ++i) {
for (int j = 0; j < n; ++j) {
if (i != j) {
edge_values[i][j] = var_values.at(edge_vars.get(i, j)) > 0.5;
}
}
}
return edge_values;
}
// Given an n by n boolean matrix of edge values, returns a cycle decomposition.
// it is assumed that edge values respects the degree constraints (each row has
// only two true entries). Each cycle is represented as a list of cities with
// no repeats.
std::vector<Cycle> FindCycles(absl::Span<const std::vector<bool>> edge_values) {
// Algorithm: maintain a "visited" bit for each city indicating if we have
// formed a cycle containing this city. Consider the cities in order. When you
// find an unvisited city, start a new cycle beginning at this city. Then,
// build the cycle by finding an unvisited neighbor until no such neighbor
// exists (every city will have two neighbors, but eventually both will be
// visited). To find the "unvisited neighbor", we simply do a linear scan
// over the cities, checking both the adjacency matrix and the visited bit.
//
// Note that for this algorithm, in each cycle, the city with lowest index
// will be first, and the cycles will be sorted by their city of lowest index.
// This is an implementation detail and should not be relied upon.
const int n = static_cast<int>(edge_values.size());
std::vector<Cycle> result;
std::vector<bool> visited(n, false);
for (int i = 0; i < n; ++i) {
if (visited[i]) {
continue;
}
std::vector<int> cycle;
std::optional<int> next = i;
while (next.has_value()) {
cycle.push_back(*next);
visited[*next] = true;
int current = *next;
next = std::nullopt;
// Scan for an unvisited neighbor. We can start at i+1 since we know that
// everything from i back is visited.
for (int j = i + 1; j < n; ++j) {
if (!visited[j] && edge_values[current][j]) {
next = j;
break;
}
}
}
result.push_back(cycle);
}
return result;
}
// Returns the cutset constraint for the given set of nodes.
math_opt::BoundedLinearExpression CutsetConstraint(
absl::Span<const int> nodes, const EdgeVariables& edge_vars) {
const int n = edge_vars.num_cities();
const absl::flat_hash_set<int> node_set(nodes.begin(), nodes.end());
std::vector<int> not_in_set;
for (int i = 0; i < n; ++i) {
if (!node_set.contains(i)) {
not_in_set.push_back(i);
}
}
math_opt::LinearExpression cutset_edges;
for (const int in_set : nodes) {
for (const int out_of_set : not_in_set) {
cutset_edges += edge_vars.get(in_set, out_of_set);
}
}
return cutset_edges >= 2;
}
// Solves the TSP by returning the ordering of the cities that minimizes travel
// distance.
absl::StatusOr<Cycle> SolveTsp(
absl::Span<const std::pair<double, double>> cities,
const math_opt::SolverType solver) {
const int n = static_cast<int>(cities.size());
const std::vector<std::vector<double>> distance_matrix =
DistanceMatrix(cities);
CHECK_GE(n, 3);
math_opt::Model model("tsp");
const EdgeVariables edge_vars(model, n);
math_opt::LinearExpression edge_cost;
for (int i = 0; i < n; ++i) {
for (int j = i + 1; j < n; ++j) {
edge_cost += edge_vars.get(i, j) * distance_matrix[i][j];
}
}
model.Minimize(edge_cost);
// Add the degree constraints
for (int i = 0; i < n; ++i) {
math_opt::LinearExpression neighbors;
for (int j = 0; j < n; ++j) {
if (i != j) {
neighbors += edge_vars.get(i, j);
}
}
model.AddLinearConstraint(neighbors == 2, absl::StrCat("n_", i));
}
math_opt::SolveArguments args;
args.parameters.enable_output = absl::GetFlag(FLAGS_solve_logs);
const int threads = absl::GetFlag(FLAGS_threads);
if (threads > 0) {
args.parameters.threads = threads;
}
args.callback_registration.events.insert(
math_opt::CallbackEvent::kMipSolution);
args.callback_registration.add_lazy_constraints = true;
args.callback = [&edge_vars](const math_opt::CallbackData& cb_data) {
// At event CallbackEvent::kMipSolution, a solution is always present.
CHECK(cb_data.solution.has_value());
const std::vector<Cycle> cycles =
FindCycles(EdgeValues(edge_vars, *cb_data.solution));
math_opt::CallbackResult result;
if (cycles.size() > 1) {
for (const Cycle& cycle : cycles) {
result.AddLazyConstraint(CutsetConstraint(cycle, edge_vars));
}
}
return result;
};
ASSIGN_OR_RETURN(const math_opt::SolveResult result,
math_opt::Solve(model, solver, args));
RETURN_IF_ERROR(result.termination.EnsureIsOptimal());
std::cout << "Route length: " << result.objective_value() << std::endl;
const std::vector<Cycle> cycles =
FindCycles(EdgeValues(edge_vars, result.variable_values()));
CHECK_EQ(cycles.size(), 1);
CHECK_EQ(cycles[0].size(), n);
return cycles[0];
}
// Produces an SVG to draw a route for a TSP.
std::string RouteSvg(absl::Span<const std::pair<double, double>> cities,
const Cycle& cycle) {
constexpr int image_px = 1000;
constexpr int r = 5;
constexpr int image_plus_border = image_px + 2 * r;
std::vector<std::string> svg_lines;
svg_lines.push_back(absl::StrCat("<svg width=\"", image_plus_border,
"\" height=\"", image_plus_border, "\">"));
std::vector<std::string> polygon_coords;
for (const int city : cycle) {
const int x =
static_cast<int>(std::round(cities[city].first * image_px)) + r;
const int y =
static_cast<int>(std::round(cities[city].second * image_px)) + r;
svg_lines.push_back(absl::StrCat("<circle cx=\"", x, "\" cy=\"", y,
"\" r=\"", r, "\" fill=\"blue\" />"));
polygon_coords.push_back(absl::StrCat(x, ",", y));
}
std::string polygon_coords_string = absl::StrJoin(polygon_coords, " ");
svg_lines.push_back(
absl::StrCat("<polygon fill=\"none\" stroke=\"blue\" points=\"",
polygon_coords_string, "\" />"));
svg_lines.push_back("</svg>");
return absl::StrJoin(svg_lines, "\n");
}
absl::Status RealMain() {
std::vector<std::pair<double, double>> cities;
if (absl::GetFlag(FLAGS_test_instance)) {
cities = TestCities();
} else {
cities = RandomCities(absl::GetFlag(FLAGS_num_cities));
}
ASSIGN_OR_RETURN(const Cycle solution,
SolveTsp(cities, absl::GetFlag(FLAGS_solver)));
const std::string svg = RouteSvg(cities, solution);
if (absl::GetFlag(FLAGS_output).empty()) {
std::cout << svg << std::endl;
} else {
RETURN_IF_ERROR(
file::SetContents(absl::GetFlag(FLAGS_output), svg, file::Defaults()));
}
return absl::OkStatus();
}
} // namespace
int main(int argc, char** argv) {
InitGoogle(argv[0], &argc, &argv, true);
const absl::Status status = RealMain();
if (!status.ok()) {
LOG(QFATAL) << status;
}
return 0;
}