Google OR-Tools v9.15
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
christofides.h
Go to the documentation of this file.
1// Copyright 2010-2025 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// ChristofidesPathSolver computes an approximate solution to the Traveling
15// Salesman Problen using the Christofides algorithm (c.f.
16// https://en.wikipedia.org/wiki/Christofides_algorithm).
17// Note that the algorithm guarantees finding a solution within 3/2 of the
18// optimum when using minimum weight perfect matching in the matching phase.
19// The complexity of the algorithm is dominated by the complexity of the
20// matching algorithm: O(n^2 * log(n)) if minimal matching is used, or at least
21// O(n^3) or O(nmlog(n)) otherwise, depending on the implementation of the
22// perfect matching algorithm used, where n is the number of nodes and m is the
23// number of edges of the subgraph induced by odd-degree nodes of the minimum
24// spanning tree.
25
26#ifndef ORTOOLS_GRAPH_CHRISTOFIDES_H_
27#define ORTOOLS_GRAPH_CHRISTOFIDES_H_
28
29#include <cstdint>
30#include <functional>
31#include <string>
32#include <type_traits>
33#include <utility>
34#include <vector>
35
36#include "absl/status/status.h"
37#include "absl/status/statusor.h"
38#include "absl/strings/str_cat.h"
41#include "ortools/graph/graph.h"
47
48namespace operations_research {
49
50using ::util::CompleteGraph;
51
52// TODO(user): Only support integer weights/costs.
53template <typename CostType, typename ArcIndex = int64_t,
54 typename NodeIndex = int32_t,
55 typename CostFunction = std::function<CostType(NodeIndex, NodeIndex)>>
57 public:
58 enum class MatchingAlgorithm {
60#if defined(USE_CBC) || defined(USE_SCIP)
62#endif // defined(USE_CBC) || defined(USE_SCIP)
64 };
65 ChristofidesPathSolver(NodeIndex num_nodes, CostFunction costs);
66
67 // Sets the matching algorithm to use. A minimum weight perfect matching
68 // (MINIMUM_WEIGHT_MATCHING) guarantees the 3/2 upper bound to the optimal
69 // solution. A minimal weight perfect matching (MINIMAL_WEIGHT_MATCHING)
70 // finds a locally minimal weight matching which does not offer any bound
71 // guarantee but, as of 09/2025, can sometimes be faster on larger problems.
72 // By default, MINIMUM_WEIGHT_MATCHING is selected.
74 matching_ = matching;
75 }
76
77 // If the problem was not solved, solves it with the Christofides algorithm,
78 // and returns the cost of the approximate TSP tour.
79 absl::StatusOr<CostType> TravelingSalesmanCost();
80
81 // If the problem was not solved, solves it with the Christofides algorithm,
82 // and returns the approximate TSP tour.
83 absl::StatusOr<const std::vector<NodeIndex>&> TravelingSalesmanPath();
84
85 // If the problem was not solved, solves it with the Christofides algorithm.
86 absl::Status Solve();
87
88 private:
89 // Safe addition operator to avoid overflows when possible.
90 template <typename T>
91 T SafeAdd(T a, T b) {
92 // TODO(user): use std::remove_cvref_t<T> once C++20 is available.
93 if constexpr (std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
94 int64_t> == true) {
95 return CapAdd(a, b);
96 } else {
97 return a + b;
98 }
99 }
100
101 // Matching algorithm to use.
102 MatchingAlgorithm matching_;
103
104 // The complete graph on the nodes of the problem.
105 CompleteGraph<NodeIndex, ArcIndex> graph_;
106
107 // Function returning the cost between nodes of the problem.
108 const CostFunction costs_;
109
110 // The cost of the computed TSP path.
111 CostType tsp_cost_;
112
113 // The path of the computed TSP,
114 std::vector<NodeIndex> tsp_path_;
115
116 // True if the TSP has been solved, false otherwise.
117 bool solved_;
118};
119
120// Computes a minimum weight perfect matching on an undirected graph.
121template <typename CostType, typename WeightFunctionType, typename GraphType>
122absl::StatusOr<std::vector<
123 std::pair<typename GraphType::NodeIndex, typename GraphType::NodeIndex>>>
124ComputeMinimumWeightMatching(const GraphType& graph,
125 const WeightFunctionType& weight) {
126 using ArcIndex = typename GraphType::ArcIndex;
127 using NodeIndex = typename GraphType::NodeIndex;
128 if constexpr (!std::is_integral_v<CostType>) {
129 DLOG(WARNING) << "Weights are being cast to int64_t. This might result "
130 "in loss of precision or overflows.";
131 }
132 MinCostPerfectMatching matching(graph.num_nodes());
133 for (NodeIndex tail : graph.AllNodes()) {
134 for (const ArcIndex arc : graph.OutgoingArcs(tail)) {
135 const NodeIndex head = graph.Head(arc);
136 // Adding both arcs is redundant for MinCostPerfectMatching.
137 if (tail < head) {
138 matching.AddEdgeWithCost(tail, head, weight(arc));
139 }
140 }
141 }
142 MinCostPerfectMatching::Status status = matching.Solve();
143 if (status != MinCostPerfectMatching::OPTIMAL &&
145 return absl::InvalidArgumentError(
146 absl::StrCat("Perfect matching failed: ", status));
147 }
148 std::vector<std::pair<NodeIndex, NodeIndex>> match;
149 for (NodeIndex tail : graph.AllNodes()) {
150 const NodeIndex head = matching.Match(tail);
151 if (tail < head) { // Both arcs are matched for a given edge, we keep one.
152 match.emplace_back(tail, head);
153 }
154 }
155 return match;
156}
157
158#if defined(USE_CBC) || defined(USE_SCIP)
159// Computes a minimum weight perfect matching on an undirected graph using a
160// Mixed Integer Programming model.
161// TODO(user): Handle infeasible cases if this algorithm is used outside of
162// Christofides.
163template <typename WeightFunctionType, typename GraphType>
164absl::StatusOr<std::vector<
165 std::pair<typename GraphType::NodeIndex, typename GraphType::NodeIndex>>>
167 const WeightFunctionType& weight) {
168 using ArcIndex = typename GraphType::ArcIndex;
169 using NodeIndex = typename GraphType::NodeIndex;
170 MPModelProto model;
171 model.set_maximize(false);
172 // The model is composed of Boolean decision variables to select matching arcs
173 // and constraints ensuring that each node appears in exactly one selected
174 // arc. The objective is to minimize the sum of the weights of selected arcs.
175 // It is assumed the graph is symmetrical.
176 std::vector<int> variable_indices(graph.num_arcs(), -1);
177 for (NodeIndex node : graph.AllNodes()) {
178 // Creating arc-selection Boolean variable.
179 for (const ArcIndex arc : graph.OutgoingArcs(node)) {
180 const NodeIndex head = graph.Head(arc);
181 if (node < head) {
182 variable_indices[arc] = model.variable_size();
183 MPVariableProto* const arc_var = model.add_variable();
184 arc_var->set_lower_bound(0);
185 arc_var->set_upper_bound(1);
186 arc_var->set_is_integer(true);
187 arc_var->set_objective_coefficient(weight(arc));
188 }
189 }
190 // Creating matching constraint:
191 // for all node i, sum(j) arc(i,j) + sum(j) arc(j,i) = 1
192 MPConstraintProto* const one_of_ct = model.add_constraint();
193 one_of_ct->set_lower_bound(1);
194 one_of_ct->set_upper_bound(1);
195 }
196 for (NodeIndex node : graph.AllNodes()) {
197 for (const ArcIndex arc : graph.OutgoingArcs(node)) {
198 const NodeIndex head = graph.Head(arc);
199 if (node < head) {
200 const int arc_var = variable_indices[arc];
201 DCHECK_GE(arc_var, 0);
202 MPConstraintProto* one_of_ct = model.mutable_constraint(node);
203 one_of_ct->add_var_index(arc_var);
204 one_of_ct->add_coefficient(1);
205 one_of_ct = model.mutable_constraint(head);
206 one_of_ct->add_var_index(arc_var);
207 one_of_ct->add_coefficient(1);
208 }
209 }
210 }
211#if defined(USE_SCIP)
212 MPSolver mp_solver("MatchingWithSCIP",
214#elif defined(USE_CBC)
215 MPSolver mp_solver("MatchingWithCBC",
217#endif
218 std::string error;
219 mp_solver.LoadModelFromProto(model, &error);
220 MPSolver::ResultStatus status = mp_solver.Solve();
221 if (status != MPSolver::OPTIMAL) {
222 return absl::InvalidArgumentError("MIP-based matching failed");
223 }
224 MPSolutionResponse response;
225 mp_solver.FillSolutionResponseProto(&response);
226 std::vector<std::pair<NodeIndex, NodeIndex>> matching;
227 for (ArcIndex arc = 0; arc < variable_indices.size(); ++arc) {
228 const int arc_var = variable_indices[arc];
229 if (arc_var >= 0 && response.variable_value(arc_var) > .9) {
230 DCHECK_GE(response.variable_value(arc_var), 1.0 - 1e-4);
231 matching.emplace_back(graph.Tail(arc), graph.Head(arc));
232 }
233 }
234 return matching;
235}
236#endif // defined(USE_CBC) || defined(USE_SCIP)
237
238template <typename CostType, typename ArcIndex, typename NodeIndex,
239 typename CostFunction>
241 ChristofidesPathSolver(NodeIndex num_nodes, CostFunction costs)
242 : matching_(MatchingAlgorithm::MINIMUM_WEIGHT_MATCHING),
243 graph_(num_nodes),
244 costs_(std::move(costs)),
245 tsp_cost_(0),
246 solved_(false) {}
247
248template <typename CostType, typename ArcIndex, typename NodeIndex,
249 typename CostFunction>
250absl::StatusOr<CostType> ChristofidesPathSolver<
251 CostType, ArcIndex, NodeIndex, CostFunction>::TravelingSalesmanCost() {
252 if (!solved_) {
253 const absl::Status status = Solve();
254 if (!status.ok()) return status;
255 }
256 return tsp_cost_;
257}
258
259template <typename CostType, typename ArcIndex, typename NodeIndex,
260 typename CostFunction>
261absl::StatusOr<const std::vector<NodeIndex>&> ChristofidesPathSolver<
262 CostType, ArcIndex, NodeIndex, CostFunction>::TravelingSalesmanPath() {
263 if (!solved_) {
264 const absl::Status status = Solve();
265 if (!status.ok()) return status;
266 }
267 return tsp_path_;
268}
269
270template <typename CostType, typename ArcIndex, typename NodeIndex,
271 typename CostFunction>
272absl::Status
274 const NodeIndex num_nodes = graph_.num_nodes();
275 tsp_path_.clear();
276 tsp_cost_ = 0;
277 if (num_nodes == 1) {
278 tsp_path_ = {0, 0};
279 }
280 if (num_nodes <= 1) {
281 return absl::OkStatus();
282 }
283 // Compute Minimum Spanning Tree.
284 const std::vector<ArcIndex> mst =
285 BuildPrimMinimumSpanningTree(graph_, [this](ArcIndex arc) {
286 return costs_(graph_.Tail(arc), graph_.Head(arc));
287 });
288 // Detect odd degree nodes.
289 std::vector<NodeIndex> degrees(num_nodes, 0);
290 for (ArcIndex arc : mst) {
291 degrees[graph_.Tail(arc)]++;
292 degrees[graph_.Head(arc)]++;
293 }
294 std::vector<NodeIndex> odd_degree_nodes;
295 for (int i = 0; i < degrees.size(); ++i) {
296 if (degrees[i] % 2 != 0) {
297 odd_degree_nodes.push_back(i);
298 }
299 }
300 // Find minimum-weight perfect matching on odd-degree-node complete graph.
301 const NodeIndex reduced_size = odd_degree_nodes.size();
302 DCHECK_NE(0, reduced_size);
303 CompleteGraph<NodeIndex, ArcIndex> reduced_graph(reduced_size);
304 std::vector<std::pair<NodeIndex, NodeIndex>> closure_arcs;
305 switch (matching_) {
308 reduced_graph, [this, &reduced_graph,
309 &odd_degree_nodes](CompleteGraph<>::ArcIndex arc) {
310 return costs_(odd_degree_nodes[reduced_graph.Tail(arc)],
311 odd_degree_nodes[reduced_graph.Head(arc)]);
312 });
313 if (!result.ok()) {
314 return result.status();
315 }
316 result->swap(closure_arcs);
317 break;
318 }
319#if defined(USE_CBC) || defined(USE_SCIP)
322 reduced_graph, [this, &reduced_graph,
323 &odd_degree_nodes](CompleteGraph<>::ArcIndex arc) {
324 return costs_(odd_degree_nodes[reduced_graph.Tail(arc)],
325 odd_degree_nodes[reduced_graph.Head(arc)]);
326 });
327 if (!result.ok()) {
328 return result.status();
329 }
330 result->swap(closure_arcs);
331 break;
332 }
333#endif // defined(USE_CBC) || defined(USE_SCIP)
335 // TODO(user): Cost caching was added and can gain up to 20% but
336 // increases memory usage; see if we can avoid caching.
337 std::vector<ArcIndex> ordered_arcs(reduced_graph.num_arcs());
338 std::vector<CostType> ordered_arc_costs(reduced_graph.num_arcs(), 0);
339 for (const ArcIndex arc : reduced_graph.AllForwardArcs()) {
340 ordered_arcs[arc] = arc;
341 ordered_arc_costs[arc] =
342 costs_(odd_degree_nodes[reduced_graph.Tail(arc)],
343 odd_degree_nodes[reduced_graph.Head(arc)]);
344 }
345 absl::c_sort(ordered_arcs,
346 [&ordered_arc_costs](ArcIndex arc_a, ArcIndex arc_b) {
347 return ordered_arc_costs[arc_a] < ordered_arc_costs[arc_b];
348 });
349 std::vector<bool> touched_nodes(reduced_size, false);
350 for (ArcIndex arc_index = 0; closure_arcs.size() * 2 < reduced_size;
351 ++arc_index) {
352 const ArcIndex arc = ordered_arcs[arc_index];
353 const NodeIndex tail = reduced_graph.Tail(arc);
354 const NodeIndex head = reduced_graph.Head(arc);
355 if (head != tail && !touched_nodes[tail] && !touched_nodes[head]) {
356 touched_nodes[tail] = true;
357 touched_nodes[head] = true;
358 closure_arcs.emplace_back(tail, head);
359 }
360 }
361 break;
362 }
363 }
364 // Build Eulerian path on minimum spanning tree + closing edges from matching
365 // and extract a solution to the Traveling Salesman from the path by skipping
366 // duplicate nodes.
368 num_nodes, closure_arcs.size() + mst.size());
369 for (ArcIndex arc : mst) {
370 egraph.AddArc(graph_.Tail(arc), graph_.Head(arc));
371 }
372 for (const auto arc : closure_arcs) {
373 egraph.AddArc(odd_degree_nodes[arc.first], odd_degree_nodes[arc.second]);
374 }
375 std::vector<bool> touched(num_nodes, false);
376 DCHECK(IsEulerianGraph(egraph));
377 for (const NodeIndex node : BuildEulerianTourFromNode(egraph, 0)) {
378 if (touched[node]) continue;
379 touched[node] = true;
380 tsp_cost_ = SafeAdd(tsp_cost_,
381 tsp_path_.empty() ? 0 : costs_(tsp_path_.back(), node));
382 tsp_path_.push_back(node);
383 }
384 tsp_cost_ =
385 SafeAdd(tsp_cost_, tsp_path_.empty() ? 0 : costs_(tsp_path_.back(), 0));
386 tsp_path_.push_back(0);
387 solved_ = true;
388 return absl::OkStatus();
389}
390} // namespace operations_research
391
392#endif // ORTOOLS_GRAPH_CHRISTOFIDES_H_
ChristofidesPathSolver(NodeIndex num_nodes, CostFunction costs)
void SetMatchingAlgorithm(MatchingAlgorithm matching)
absl::StatusOr< CostType > TravelingSalesmanCost()
absl::StatusOr< const std::vector< NodeIndex > & > TravelingSalesmanPath()
::operations_research::MPConstraintProto *PROTOBUF_NONNULL add_constraint()
::operations_research::MPConstraintProto *PROTOBUF_NONNULL mutable_constraint(int index)
::operations_research::MPVariableProto *PROTOBUF_NONNULL add_variable()
ResultStatus Solve()
Solves the problem using the default parameter values.
MPSolverResponseStatus LoadModelFromProto(const MPModelProto &input_model, std::string *error_message, bool clear_names=true)
void FillSolutionResponseProto(MPSolutionResponse *response) const
Encodes the current solution in a solution response protocol buffer.
void AddEdgeWithCost(int tail, int head, int64_t cost)
IntegerRange< ArcIndex > AllForwardArcs() const
Definition graph.h:1154
ArcIndexType num_arcs() const
Definition graph.h:233
NodeIndexType Tail(ArcIndexType arc) const
Definition graph.h:1902
NodeIndexType Head(ArcIndexType arc) const
Definition graph.h:1895
ArcIndexType AddArc(NodeIndexType tail, NodeIndexType head)
Definition graph.h:1628
OR-Tools root namespace.
bool IsEulerianGraph(const Graph &graph, bool assume_connectivity=true)
int64_t CapAdd(int64_t x, int64_t y)
absl::StatusOr< std::vector< std::pair< typename GraphType::NodeIndex, typename GraphType::NodeIndex > > > ComputeMinimumWeightMatchingWithMIP(const GraphType &graph, const WeightFunctionType &weight)
std::vector< NodeIndex > BuildEulerianTourFromNode(const Graph &graph, NodeIndex root, bool assume_connectivity=true)
BlossomGraph::NodeIndex NodeIndex
std::vector< typename Graph::ArcIndex > BuildPrimMinimumSpanningTree(const Graph &graph, const ArcValue &arc_value)
absl::StatusOr< std::vector< std::pair< typename GraphType::NodeIndex, typename GraphType::NodeIndex > > > ComputeMinimumWeightMatching(const GraphType &graph, const WeightFunctionType &weight)
STL namespace.
trees with all degrees equal w the current value of degrees