Google OR-Tools v9.12
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 OR_TOOLS_GRAPH_CHRISTOFIDES_H_
27#define OR_TOOLS_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"
40#include "ortools/graph/graph.h"
44#include "ortools/linear_solver/linear_solver.pb.h"
46
47namespace operations_research {
48
49using ::util::CompleteGraph;
50
51template <typename CostType, typename ArcIndex = int64_t,
52 typename NodeIndex = int32_t,
53 typename CostFunction = std::function<CostType(NodeIndex, NodeIndex)>>
55 public:
56 enum class MatchingAlgorithm {
58#if defined(USE_CBC) || defined(USE_SCIP)
60#endif // defined(USE_CBC) || defined(USE_SCIP)
62 };
63 ChristofidesPathSolver(NodeIndex num_nodes, CostFunction costs);
64
65 // Sets the matching algorithm to use. A minimum weight perfect matching
66 // (MINIMUM_WEIGHT_MATCHING) guarantees the 3/2 upper bound to the optimal
67 // solution. A minimal weight perfect matching (MINIMAL_WEIGHT_MATCHING)
68 // finds a locally minimal weight matching which does not offer any bound
69 // guarantee but, as of 1/2017, is orders of magnitude faster than the
70 // minimum matching.
71 // By default, MINIMAL_WEIGHT_MATCHING is selected.
72 // TODO(user): Change the default when minimum matching gets faster.
74 matching_ = matching;
75 }
76
77 // Returns the cost of the approximate TSP tour.
78 CostType TravelingSalesmanCost();
79
80 // Returns the approximate TSP tour.
81 std::vector<NodeIndex> TravelingSalesmanPath();
82
83 // Runs the Christofides algorithm. Returns true if a solution was found,
84 // false otherwise.
85 bool Solve();
86
87 private:
88 // Safe addition operator to avoid overflows when possible.
89 template <typename T>
90 T SafeAdd(T a, T b) {
91 // TODO(user): use std::remove_cvref_t<T> once C++20 is available.
92 if constexpr (std::is_same_v<std::remove_cv_t<std::remove_reference_t<T>>,
93 int64_t> == true) {
94 return CapAdd(a, b);
95 } else {
96 return a + b;
97 }
98 }
99
100 // Matching algorithm to use.
101 MatchingAlgorithm matching_;
102
103 // The complete graph on the nodes of the problem.
104 CompleteGraph<NodeIndex, ArcIndex> graph_;
105
106 // Function returning the cost between nodes of the problem.
107 const CostFunction costs_;
108
109 // The cost of the computed TSP path.
110 CostType tsp_cost_;
111
112 // The path of the computed TSP,
113 std::vector<NodeIndex> tsp_path_;
114
115 // True if the TSP has been solved, false otherwise.
116 bool solved_;
117};
118
119// Computes a minimum weight perfect matching on an undirected graph.
120template <typename WeightFunctionType, typename GraphType>
121absl::StatusOr<std::vector<
122 std::pair<typename GraphType::NodeIndex, typename GraphType::NodeIndex>>>
123ComputeMinimumWeightMatching(const GraphType& graph,
124 const WeightFunctionType& weight) {
125 using ArcIndex = typename GraphType::ArcIndex;
126 using NodeIndex = typename GraphType::NodeIndex;
127 MinCostPerfectMatching matching(graph.num_nodes());
128 for (NodeIndex tail : graph.AllNodes()) {
129 for (const ArcIndex arc : graph.OutgoingArcs(tail)) {
130 const NodeIndex head = graph.Head(arc);
131 // Adding both arcs is redundant for MinCostPerfectMatching.
132 if (tail < head) {
133 matching.AddEdgeWithCost(tail, head, weight(arc));
134 }
135 }
136 }
137 MinCostPerfectMatching::Status status = matching.Solve();
138 if (status != MinCostPerfectMatching::OPTIMAL) {
139 return absl::InvalidArgumentError("Perfect matching failed");
140 }
141 std::vector<std::pair<NodeIndex, NodeIndex>> match;
142 for (NodeIndex tail : graph.AllNodes()) {
143 const NodeIndex head = matching.Match(tail);
144 if (tail < head) { // Both arcs are matched for a given edge, we keep one.
145 match.emplace_back(tail, head);
146 }
147 }
148 return match;
149}
150
151#if defined(USE_CBC) || defined(USE_SCIP)
152// Computes a minimum weight perfect matching on an undirected graph using a
153// Mixed Integer Programming model.
154// TODO(user): Handle infeasible cases if this algorithm is used outside of
155// Christofides.
156template <typename WeightFunctionType, typename GraphType>
157absl::StatusOr<std::vector<
158 std::pair<typename GraphType::NodeIndex, typename GraphType::NodeIndex>>>
160 const WeightFunctionType& weight) {
161 using ArcIndex = typename GraphType::ArcIndex;
162 using NodeIndex = typename GraphType::NodeIndex;
163 MPModelProto model;
164 model.set_maximize(false);
165 // The model is composed of Boolean decision variables to select matching arcs
166 // and constraints ensuring that each node appears in exactly one selected
167 // arc. The objective is to minimize the sum of the weights of selected arcs.
168 // It is assumed the graph is symmetrical.
169 std::vector<int> variable_indices(graph.num_arcs(), -1);
170 for (NodeIndex node : graph.AllNodes()) {
171 // Creating arc-selection Boolean variable.
172 for (const ArcIndex arc : graph.OutgoingArcs(node)) {
173 const NodeIndex head = graph.Head(arc);
174 if (node < head) {
175 variable_indices[arc] = model.variable_size();
176 MPVariableProto* const arc_var = model.add_variable();
177 arc_var->set_lower_bound(0);
178 arc_var->set_upper_bound(1);
179 arc_var->set_is_integer(true);
180 arc_var->set_objective_coefficient(weight(arc));
181 }
182 }
183 // Creating matching constraint:
184 // for all node i, sum(j) arc(i,j) + sum(j) arc(j,i) = 1
185 MPConstraintProto* const one_of_ct = model.add_constraint();
186 one_of_ct->set_lower_bound(1);
187 one_of_ct->set_upper_bound(1);
188 }
189 for (NodeIndex node : graph.AllNodes()) {
190 for (const ArcIndex arc : graph.OutgoingArcs(node)) {
191 const NodeIndex head = graph.Head(arc);
192 if (node < head) {
193 const int arc_var = variable_indices[arc];
194 DCHECK_GE(arc_var, 0);
195 MPConstraintProto* one_of_ct = model.mutable_constraint(node);
196 one_of_ct->add_var_index(arc_var);
197 one_of_ct->add_coefficient(1);
198 one_of_ct = model.mutable_constraint(head);
199 one_of_ct->add_var_index(arc_var);
200 one_of_ct->add_coefficient(1);
201 }
202 }
203 }
204#if defined(USE_SCIP)
205 MPSolver mp_solver("MatchingWithSCIP",
207#elif defined(USE_CBC)
208 MPSolver mp_solver("MatchingWithCBC",
210#endif
211 std::string error;
212 mp_solver.LoadModelFromProto(model, &error);
213 MPSolver::ResultStatus status = mp_solver.Solve();
214 if (status != MPSolver::OPTIMAL) {
215 return absl::InvalidArgumentError("MIP-based matching failed");
216 }
217 MPSolutionResponse response;
218 mp_solver.FillSolutionResponseProto(&response);
219 std::vector<std::pair<NodeIndex, NodeIndex>> matching;
220 for (ArcIndex arc = 0; arc < variable_indices.size(); ++arc) {
221 const int arc_var = variable_indices[arc];
222 if (arc_var >= 0 && response.variable_value(arc_var) > .9) {
223 DCHECK_GE(response.variable_value(arc_var), 1.0 - 1e-4);
224 matching.emplace_back(graph.Tail(arc), graph.Head(arc));
225 }
226 }
227 return matching;
228}
229#endif // defined(USE_CBC) || defined(USE_SCIP)
230
231template <typename CostType, typename ArcIndex, typename NodeIndex,
232 typename CostFunction>
234 ChristofidesPathSolver(NodeIndex num_nodes, CostFunction costs)
236 graph_(num_nodes),
237 costs_(std::move(costs)),
238 tsp_cost_(0),
239 solved_(false) {}
240
241template <typename CostType, typename ArcIndex, typename NodeIndex,
242 typename CostFunction>
243CostType ChristofidesPathSolver<CostType, ArcIndex, NodeIndex,
244 CostFunction>::TravelingSalesmanCost() {
245 if (!solved_) {
246 bool const ok = Solve();
247 DCHECK(ok);
248 }
249 return tsp_cost_;
250}
251
252template <typename CostType, typename ArcIndex, typename NodeIndex,
253 typename CostFunction>
254std::vector<NodeIndex> ChristofidesPathSolver<
255 CostType, ArcIndex, NodeIndex, CostFunction>::TravelingSalesmanPath() {
256 if (!solved_) {
257 const bool ok = Solve();
258 DCHECK(ok);
259 }
260 return tsp_path_;
261}
262
263template <typename CostType, typename ArcIndex, typename NodeIndex,
264 typename CostFunction>
266 CostFunction>::Solve() {
267 const NodeIndex num_nodes = graph_.num_nodes();
268 tsp_path_.clear();
269 tsp_cost_ = 0;
270 if (num_nodes == 1) {
271 tsp_path_ = {0, 0};
272 }
273 if (num_nodes <= 1) {
274 return true;
275 }
276 // Compute Minimum Spanning Tree.
277 const std::vector<ArcIndex> mst =
278 BuildPrimMinimumSpanningTree(graph_, [this](ArcIndex arc) {
279 return costs_(graph_.Tail(arc), graph_.Head(arc));
280 });
281 // Detect odd degree nodes.
282 std::vector<NodeIndex> degrees(num_nodes, 0);
283 for (ArcIndex arc : mst) {
284 degrees[graph_.Tail(arc)]++;
285 degrees[graph_.Head(arc)]++;
286 }
287 std::vector<NodeIndex> odd_degree_nodes;
288 for (int i = 0; i < degrees.size(); ++i) {
289 if (degrees[i] % 2 != 0) {
290 odd_degree_nodes.push_back(i);
291 }
292 }
293 // Find minimum-weight perfect matching on odd-degree-node complete graph.
294 // TODO(user): Make this code available as an independent algorithm.
295 const NodeIndex reduced_size = odd_degree_nodes.size();
296 DCHECK_NE(0, reduced_size);
297 CompleteGraph<NodeIndex, ArcIndex> reduced_graph(reduced_size);
298 std::vector<std::pair<NodeIndex, NodeIndex>> closure_arcs;
299 switch (matching_) {
301 auto result = ComputeMinimumWeightMatching(
302 reduced_graph, [this, &reduced_graph,
303 &odd_degree_nodes](CompleteGraph<>::ArcIndex arc) {
304 return costs_(odd_degree_nodes[reduced_graph.Tail(arc)],
305 odd_degree_nodes[reduced_graph.Head(arc)]);
306 });
307 if (!result.ok()) {
308 return false;
309 }
310 result->swap(closure_arcs);
311 break;
312 }
313#if defined(USE_CBC) || defined(USE_SCIP)
316 reduced_graph, [this, &reduced_graph,
317 &odd_degree_nodes](CompleteGraph<>::ArcIndex arc) {
318 return costs_(odd_degree_nodes[reduced_graph.Tail(arc)],
319 odd_degree_nodes[reduced_graph.Head(arc)]);
320 });
321 if (!result.ok()) {
322 return false;
323 }
324 result->swap(closure_arcs);
325 break;
326 }
327#endif // defined(USE_CBC) || defined(USE_SCIP)
329 // TODO(user): Cost caching was added and can gain up to 20% but
330 // increases memory usage; see if we can avoid caching.
331 std::vector<ArcIndex> ordered_arcs(reduced_graph.num_arcs());
332 std::vector<CostType> ordered_arc_costs(reduced_graph.num_arcs(), 0);
333 for (const ArcIndex arc : reduced_graph.AllForwardArcs()) {
334 ordered_arcs[arc] = arc;
335 ordered_arc_costs[arc] =
336 costs_(odd_degree_nodes[reduced_graph.Tail(arc)],
337 odd_degree_nodes[reduced_graph.Head(arc)]);
338 }
339 std::sort(ordered_arcs.begin(), ordered_arcs.end(),
340 [&ordered_arc_costs](ArcIndex arc_a, ArcIndex arc_b) {
341 return ordered_arc_costs[arc_a] < ordered_arc_costs[arc_b];
342 });
343 std::vector<bool> touched_nodes(reduced_size, false);
344 for (ArcIndex arc_index = 0; closure_arcs.size() * 2 < reduced_size;
345 ++arc_index) {
346 const ArcIndex arc = ordered_arcs[arc_index];
347 const NodeIndex tail = reduced_graph.Tail(arc);
348 const NodeIndex head = reduced_graph.Head(arc);
349 if (head != tail && !touched_nodes[tail] && !touched_nodes[head]) {
350 touched_nodes[tail] = true;
351 touched_nodes[head] = true;
352 closure_arcs.emplace_back(tail, head);
353 }
354 }
355 break;
356 }
357 }
358 // Build Eulerian path on minimum spanning tree + closing edges from matching
359 // and extract a solution to the Traveling Salesman from the path by skipping
360 // duplicate nodes.
362 num_nodes, closure_arcs.size() + mst.size());
363 for (ArcIndex arc : mst) {
364 egraph.AddArc(graph_.Tail(arc), graph_.Head(arc));
365 }
366 for (const auto arc : closure_arcs) {
367 egraph.AddArc(odd_degree_nodes[arc.first], odd_degree_nodes[arc.second]);
368 }
369 std::vector<bool> touched(num_nodes, false);
370 DCHECK(IsEulerianGraph(egraph));
371 for (const NodeIndex node : BuildEulerianTourFromNode(egraph, 0)) {
372 if (touched[node]) continue;
373 touched[node] = true;
374 tsp_cost_ = SafeAdd(tsp_cost_,
375 tsp_path_.empty() ? 0 : costs_(tsp_path_.back(), node));
376 tsp_path_.push_back(node);
377 }
378 tsp_cost_ =
379 SafeAdd(tsp_cost_, tsp_path_.empty() ? 0 : costs_(tsp_path_.back(), 0));
380 tsp_path_.push_back(0);
381 solved_ = true;
382 return true;
383}
384} // namespace operations_research
385
386#endif // OR_TOOLS_GRAPH_CHRISTOFIDES_H_
ChristofidesPathSolver(NodeIndex num_nodes, CostFunction costs)
void SetMatchingAlgorithm(MatchingAlgorithm matching)
std::vector< NodeIndex > TravelingSalesmanPath()
Returns the approximate TSP tour.
CostType TravelingSalesmanCost()
Returns the cost of the approximate TSP tour.
ResultStatus Solve()
Solves the problem using the default parameter values.
MPSolverResponseStatus LoadModelFromProto(const MPModelProto &input_model, std::string *error_message, bool clear_names=true)
--— Methods using protocol buffers --—
@ SCIP_MIXED_INTEGER_PROGRAMMING
Recommended default value for MIP problems.
void FillSolutionResponseProto(MPSolutionResponse *response) const
Encodes the current solution in a solution response protocol buffer.
void AddEdgeWithCost(int tail, int head, int64_t cost)
@ OPTIMAL
A perfect matching with min-cost has been found.
IntegerRange< ArcIndex > AllForwardArcs() const
Definition graph.h:975
ArcIndexType num_arcs() const
Returns the number of valid arcs in the graph.
Definition graph.h:220
NodeIndexType Tail(ArcIndexType arc) const
Definition graph.h:2277
NodeIndexType Head(ArcIndexType arc) const
Definition graph.h:2270
ArcIndexType AddArc(NodeIndexType tail, NodeIndexType head)
Definition graph.h:1565
In SWIG mode, we don't want anything besides these top-level includes.
bool IsEulerianGraph(const Graph &graph, bool assume_connectivity=true)
Returns true if a graph is Eulerian, aka all its nodes are of even degree.
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)
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)
Computes a minimum weight perfect matching on an undirected graph.
STL namespace.
trees with all degrees equal w the current value of degrees