Google OR-Tools v9.9
a fast and portable software suite for combinatorial optimization
Loading...
Searching...
No Matches
one_tree_lower_bound.h
Go to the documentation of this file.
1// Copyright 2010-2024 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// An implementation of the Held-Karp symmetric Traveling Salesman (TSP) lower
15// bound algorithm, inspired by "Estimating the Held-Karp lower bound for the
16// geometric TSP" by Christine L. Valenzuela and Antonia J. Jones, European
17// Journal of Operational Research, Volume 102, Issue 1, 1 October 1997,
18// Pages 157-175.
19//
20// The idea is to compute minimum 1-trees to evaluate a lower bound to the
21// corresponding TSP. A minimum 1-tree is a minimum spanning tree on all nodes
22// but one, to which are added the two shortest edges from the left-out node to
23// the nodes of the spanning tree. The sum of the cost of the edges of the
24// minimum 1-tree is a lower bound to the cost of the TSP.
25// In order to improve (increase) this lower bound, the idea is to add weights
26// to each nodes, weights which are added to the cost function used when
27// computing the 1-tree. If weight[i] is the weight of node i, the cost function
28// therefore becomes weighed_cost(i,j) = cost(i,j) + weight[i] + weight[j]. One
29// can see that w = weighed_cost(minimum 1-tree) - Sum(2 * weight[i])
30// = cost(minimum 1-tree) + Sum(weight[i] * (degree[i] - 2))
31// is a valid lower bound to the TSP:
32// 1) let T be the set of 1-trees on the nodes;
33// 2) let U be the set of tours on the nodes; U is a subset of T (tours are
34// 1-trees with all degrees equal to 2), therefore:
35// min(t in T) Cost(t) <= min(t in U) Cost(t)
36// and
37// min(t in T) WeighedCost(t) <= min(t in U) WeighedCost(t)
38// 3) weighed_cost(i,j) = cost(i,j) + weight[i] + weight[j], therefore:
39// for all t in T, WeighedCost(t) = Cost(t) + Sum(weight[i] * degree[i])
40// and
41// for all i in U, WeighedCost(t) = Cost(t) + Sum(weight[i] * 2)
42// 4) let t* in U s.t. WeighedCost(t*) = min(t in U) WeighedCost(t), therefore:
43// min(t in T) (Cost(t) + Sum(weight[i] * degree[i]))
44// <= Cost(t*) + Sum(weight[i] * 2)
45// and
46// min(t in T) (Cost(t) + Sum(weight[i] * (degree[i] - 2))) <= Cost(t*)
47// and
48// cost(minimum 1-tree) + Sum(weight[i] * (degree[i] - 2)) <= Cost(t*)
49// and
50// w <= Cost(t*)
51// 5) because t* is also the tour minimizing Cost(t) with t in U (weights do not
52// affect the optimality of a tour), Cost(t*) is the cost of the optimal
53// solution to the TSP and w is a lower bound to this cost.
54//
55// The best lower bound is the one for which weights maximize w. Intuitively as
56// degrees get closer to 2 the minimum 1-trees gets closer to a tour.
57//
58// At each iteration m, weights are therefore updated as follows:
59// weight(m+1)[i] = weight(m)[i] + step(m) * (degree(m)[i] - 2)
60// where degree(m)[i] is the degree of node i in the 1-tree at iteration i,
61// step(m) is a subgradient optimization step.
62//
63// This implementation uses two variants of Held-Karp's initial subgradient
64// optimization iterative estimation approach described in "The
65// traveling-salesman problem and minimum spanning trees: Part I and II", by
66// Michael Held and Richard M. Karp, Operations Research Vol. 18,
67// No. 6 (Nov. - Dec., 1970), pp. 1138-1162 and Mathematical Programming (1971).
68//
69// The first variant comes from Volgenant, T., and Jonker, R. (1982), "A branch
70// and bound algorithm for the symmetric traveling salesman problem based on the
71// 1-tree relaxation", European Journal of Operational Research. 9:83-89.".
72// It suggests using
73// step(m) = (1.0 * (m - 1) * (2 * M - 5) / (2 * (M - 1))) * step1
74// - (m - 2) * step1
75// + (0.5 * (m - 1) * (m - 2) / ((M - 1) * (M - 2))) * step1
76// where M is the maximum number of iterations and step1 is initially set to
77// L / (2 * number of nodes), where L is the un-weighed cost of the 1-tree;
78// step1 is updated each time a better w is found. The intuition is to have a
79// positive decreasing step which is equal to 0 after M iterations; Volgenant
80// and Jonker suggest that:
81// step(m) - 2 * step(m-1) + t(m-2) = constant,
82// step(M) = 0
83// and
84// step(1) - step(2) = 3 * (step(M-1) - step(M)).
85// The step(m) formula above derives from this recursive formulation.
86// This is the default algorithm used in this implementation.
87//
88// The second variant comes from Held, M., Wolfe, P., and Crowder, H. P. (1974),
89// "Validation of subgradient optimization", Mathematical Programming 6:62-88.
90// It derives from the original Held-Karp formulation:
91// step(m) = lambda(m) * (wlb - w(m)) / Sum((degree[i] - 2)^2),
92// where wlb is a lower bound to max(w(m)) and lambda(m) in [0, 2].
93// Help-Karp prove that
94// if w(m') > w(m) and 0 < step < 2 * (w(m') - w(m))/norm(degree(m) - 2)^2,
95// then weight(m+1) is closer to w' than w from which they derive the above
96// formula.
97// Held-Wolfe-Crowder show that using an overestimate UB is as effective as
98// using the underestimate wlb while UB is easier to compute. The resulting
99// formula is:
100// step(m) = lambda(m) * (UB - w(m)) / Sum((degree[i] - 2)^2),
101// where UB is an upper bound to the TSP (here computed with the Christofides
102// algorithm), and lambda(m) in [0, 2] initially set to 2. Held-Wolfe-Crowder
103// suggest running the algorithm for M = 2 * number of nodes iterations, then
104// dividing lambda and M by 2 until M is small enough (less than 2 in this
105// implementation).
106//
107// To speed up the computation, minimum spanning trees are actually computed on
108// a graph limited to the nearest neighbors of each node. Valenzuela-Jones 1997
109// experiments have shown that this does not harm the lower bound computation
110// significantly. At the end of the algorithm a last iteration is run on the
111// complete graph to ensure the bound is correct (the cost of a minimum 1-tree
112// on a partial graph is an upper bound to the one on a complete graph).
113//
114// Usage:
115// std::function<int64_t(int,int)> cost_function =...;
116// const double lower_bound =
117// ComputeOneTreeLowerBound(number_of_nodes, cost_function);
118// where number_of_nodes is the number of nodes in the TSP and cost_function
119// is a function returning the cost between two nodes.
120
121#ifndef OR_TOOLS_GRAPH_ONE_TREE_LOWER_BOUND_H_
122#define OR_TOOLS_GRAPH_ONE_TREE_LOWER_BOUND_H_
123
124#include <math.h>
125
126#include <cmath>
127#include <cstdint>
128#include <limits>
129#include <set>
130#include <utility>
131#include <vector>
132
133#include "absl/types/span.h"
134#include "ortools/base/types.h"
137
138namespace operations_research {
139
140// Implementation of algorithms computing Held-Karp bounds. They have to provide
141// the following methods:
142// - bool Next(): returns false when the algorithm must stop;
143// - double GetStep(): returns the current step computed by the algorithm;
144// - void OnOneTree(CostType one_tree_cost,
145// double w,
146// const std::vector<int>& degrees):
147// called each time a new minimum 1-tree is computed;
148// - one_tree_cost: the un-weighed cost of the 1-tree,
149// - w the current value of w,
150// - degrees: the degree of nodes in the 1-tree.
151// - OnNewWMax(CostType one_tree_cost): called when a better value of w is
152// found, one_tree_cost being the un-weighed cost of the corresponding
153// minimum 1-tree.
154
155// Implementation of the Volgenant Jonker algorithm (see the comments at the
156// head of the file for explanations).
157template <typename CostType>
158class VolgenantJonkerEvaluator {
159 public:
160 VolgenantJonkerEvaluator(int number_of_nodes, int max_iterations)
161 : step1_initialized_(false),
162 step1_(0),
163 iteration_(0),
164 max_iterations_(max_iterations > 0 ? max_iterations
165 : MaxIterations(number_of_nodes)),
166 number_of_nodes_(number_of_nodes) {}
167
168 bool Next() { return iteration_++ < max_iterations_; }
169
170 double GetStep() const {
171 return (1.0 * (iteration_ - 1) * (2 * max_iterations_ - 5) /
172 (2 * (max_iterations_ - 1))) *
173 step1_ -
174 (iteration_ - 2) * step1_ +
175 (0.5 * (iteration_ - 1) * (iteration_ - 2) /
176 ((max_iterations_ - 1) * (max_iterations_ - 2))) *
177 step1_;
178 }
179
180 void OnOneTree(CostType one_tree_cost, double w,
181 absl::Span<const int> degrees) {
182 if (!step1_initialized_) {
183 step1_initialized_ = true;
184 UpdateStep(one_tree_cost);
185 }
186 }
187
188 void OnNewWMax(CostType one_tree_cost) { UpdateStep(one_tree_cost); }
189
190 private:
191 // Automatic computation of the number of iterations based on empirical
192 // results given in Valenzuela-Jones 1997.
193 static int MaxIterations(int number_of_nodes) {
194 return static_cast<int>(28 * std::pow(number_of_nodes, 0.62));
195 }
196
197 void UpdateStep(CostType one_tree_cost) {
198 step1_ = one_tree_cost / (2 * number_of_nodes_);
199 }
200
201 bool step1_initialized_;
202 double step1_;
203 int iteration_;
204 const int max_iterations_;
205 const int number_of_nodes_;
206};
207
208// Implementation of the Held-Wolfe-Crowder algorithm (see the comments at the
209// head of the file for explanations).
210template <typename CostType, typename CostFunction>
211class HeldWolfeCrowderEvaluator {
212 public:
213 HeldWolfeCrowderEvaluator(int number_of_nodes, const CostFunction& cost)
214 : iteration_(0),
215 number_of_iterations_(2 * number_of_nodes),
216 upper_bound_(0),
217 lambda_(2.0),
218 step_(0) {
219 // TODO(user): Improve upper bound with some local search; tighter upper
220 // bounds lead to faster convergence.
221 ChristofidesPathSolver<CostType, int64_t, int, CostFunction> solver(
222 number_of_nodes, cost);
223 upper_bound_ = solver.TravelingSalesmanCost();
224 }
225
226 bool Next() {
227 const int min_iterations = 2;
228 if (iteration_ >= number_of_iterations_) {
229 number_of_iterations_ /= 2;
230 if (number_of_iterations_ < min_iterations) return false;
231 iteration_ = 0;
232 lambda_ /= 2;
233 } else {
234 ++iteration_;
235 }
236 return true;
237 }
238
239 double GetStep() const { return step_; }
240
241 void OnOneTree(CostType one_tree_cost, double w,
242 absl::Span<const int> degrees) {
243 double norm = 0;
244 for (int degree : degrees) {
245 const double delta = degree - 2;
246 norm += delta * delta;
247 }
248 step_ = lambda_ * (upper_bound_ - w) / norm;
249 }
250
251 void OnNewWMax(CostType one_tree_cost) {}
252
253 private:
254 int iteration_;
255 int number_of_iterations_;
256 CostType upper_bound_;
257 double lambda_;
258 double step_;
259};
260
261// Computes the nearest neighbors of each node for the given cost function.
262// The ith element of the returned vector contains the indices of the nearest
263// nodes to node i. Note that these indices contain the number_of_neighbors
264// nearest neighbors as well as all the nodes for which i is a nearest
265// neighbor.
266template <typename CostFunction>
267std::set<std::pair<int, int>> NearestNeighbors(int number_of_nodes,
268 int number_of_neighbors,
269 const CostFunction& cost) {
270 using CostType = decltype(cost(0, 0));
271 std::set<std::pair<int, int>> nearest;
272 for (int i = 0; i < number_of_nodes; ++i) {
273 std::vector<std::pair<CostType, int>> neighbors;
274 neighbors.reserve(number_of_nodes - 1);
275 for (int j = 0; j < number_of_nodes; ++j) {
276 if (i != j) {
277 neighbors.emplace_back(cost(i, j), j);
278 }
279 }
280 int size = neighbors.size();
281 if (number_of_neighbors < size) {
282 std::nth_element(neighbors.begin(),
283 neighbors.begin() + number_of_neighbors - 1,
284 neighbors.end());
285 size = number_of_neighbors;
286 }
287 for (int j = 0; j < size; ++j) {
288 nearest.insert({i, neighbors[j].second});
289 nearest.insert({neighbors[j].second, i});
290 }
291 }
292 return nearest;
293}
294
295// Let G be the complete graph on nodes in [0, number_of_nodes - 1]. Adds arcs
296// from the minimum spanning tree of G to the arcs set argument.
297template <typename CostFunction>
298void AddArcsFromMinimumSpanningTree(int number_of_nodes,
299 const CostFunction& cost,
300 std::set<std::pair<int, int>>* arcs) {
301 util::CompleteGraph<int, int> graph(number_of_nodes);
302 const std::vector<int> mst =
303 BuildPrimMinimumSpanningTree(graph, [&cost, &graph](int arc) {
304 return cost(graph.Tail(arc), graph.Head(arc));
305 });
306 for (int arc : mst) {
307 arcs->insert({graph.Tail(arc), graph.Head(arc)});
308 arcs->insert({graph.Head(arc), graph.Tail(arc)});
309 }
310}
311
312// Returns the index of the node in graph which minimizes cost(node, source)
313// with the constraint that accept(node) is true.
314template <typename CostFunction, typename GraphType, typename AcceptFunction>
315int GetNodeMinimizingEdgeCostToSource(const GraphType& graph, int source,
316 const CostFunction& cost,
317 AcceptFunction accept) {
318 int best_node = -1;
319 double best_edge_cost = 0;
320 for (const auto node : graph.AllNodes()) {
321 if (accept(node)) {
322 const double edge_cost = cost(node, source);
323 if (best_node == -1 || edge_cost < best_edge_cost) {
324 best_node = node;
325 best_edge_cost = edge_cost;
326 }
327 }
328 }
329 return best_node;
330}
331
332// Computes a 1-tree for the given graph, cost function and node weights.
333// Returns the degree of each node in the 1-tree and the un-weighed cost of the
334// 1-tree.
335template <typename CostFunction, typename GraphType, typename CostType>
336std::vector<int> ComputeOneTree(const GraphType& graph,
337 const CostFunction& cost,
338 const std::vector<double>& weights,
339 const std::vector<int>& sorted_arcs,
340 CostType* one_tree_cost) {
341 const auto weighed_cost = [&cost, &weights](int from, int to) {
342 return cost(from, to) + weights[from] + weights[to];
343 };
344 // Compute MST on graph.
345 std::vector<int> mst;
346 if (!sorted_arcs.empty()) {
347 mst = BuildKruskalMinimumSpanningTreeFromSortedArcs<GraphType>(graph,
348 sorted_arcs);
349 } else {
350 mst = BuildPrimMinimumSpanningTree<GraphType>(
351 graph, [&weighed_cost, &graph](int arc) {
352 return weighed_cost(graph.Tail(arc), graph.Head(arc));
353 });
354 }
355 std::vector<int> degrees(graph.num_nodes() + 1, 0);
356 *one_tree_cost = 0;
357 for (int arc : mst) {
358 degrees[graph.Head(arc)]++;
359 degrees[graph.Tail(arc)]++;
360 *one_tree_cost += cost(graph.Tail(arc), graph.Head(arc));
361 }
362 // Add 2 cheapest edges from the nodes in the graph to the extra node not in
363 // the graph.
364 const int extra_node = graph.num_nodes();
365 const auto update_one_tree = [extra_node, one_tree_cost, &degrees,
366 &cost](int node) {
367 *one_tree_cost += cost(node, extra_node);
368 degrees.back()++;
369 degrees[node]++;
370 };
371 const int node = GetNodeMinimizingEdgeCostToSource(
372 graph, extra_node, weighed_cost,
373 [extra_node](int n) { return n != extra_node; });
374 update_one_tree(node);
375 update_one_tree(GetNodeMinimizingEdgeCostToSource(
376 graph, extra_node, weighed_cost,
377 [extra_node, node](int n) { return n != extra_node && n != node; }));
378 return degrees;
379}
380
381// Computes the lower bound of a TSP using a given subgradient algorithm.
382template <typename CostFunction, typename Algorithm>
383double ComputeOneTreeLowerBoundWithAlgorithm(int number_of_nodes,
384 int nearest_neighbors,
385 const CostFunction& cost,
386 Algorithm* algorithm) {
387 if (number_of_nodes < 2) return 0;
388 if (number_of_nodes == 2) return cost(0, 1) + cost(1, 0);
389 using CostType = decltype(cost(0, 0));
390 auto nearest = NearestNeighbors(number_of_nodes - 1, nearest_neighbors, cost);
391 // Ensure nearest arcs result in a connected graph by adding arcs from the
392 // minimum spanning tree; this will add arcs which are likely to be "good"
393 // 1-tree arcs.
394 AddArcsFromMinimumSpanningTree(number_of_nodes - 1, cost, &nearest);
395 util::ListGraph<int, int> graph(number_of_nodes - 1, nearest.size());
396 for (const auto& arc : nearest) {
397 graph.AddArc(arc.first, arc.second);
398 }
399 std::vector<double> weights(number_of_nodes, 0);
400 std::vector<double> best_weights(number_of_nodes, 0);
401 double max_w = -std::numeric_limits<double>::infinity();
402 double w = 0;
403 // Iteratively compute lower bound using a partial graph.
404 while (algorithm->Next()) {
405 CostType one_tree_cost = 0;
406 const std::vector<int> degrees =
407 ComputeOneTree(graph, cost, weights, {}, &one_tree_cost);
408 algorithm->OnOneTree(one_tree_cost, w, degrees);
409 w = one_tree_cost;
410 for (int j = 0; j < number_of_nodes; ++j) {
411 w += weights[j] * (degrees[j] - 2);
412 }
413 if (w > max_w) {
414 max_w = w;
415 best_weights = weights;
416 algorithm->OnNewWMax(one_tree_cost);
417 }
418 const double step = algorithm->GetStep();
419 for (int j = 0; j < number_of_nodes; ++j) {
420 weights[j] += step * (degrees[j] - 2);
421 }
422 }
423 // Compute lower bound using the complete graph on the best weights. This is
424 // necessary as the MSTs computed on nearest neighbors is not guaranteed to
425 // lead to a lower bound.
426 util::CompleteGraph<int, int> complete_graph(number_of_nodes - 1);
427 CostType one_tree_cost = 0;
428 // TODO(user): We are not caching here since this would take O(n^2) memory;
429 // however the Kruskal algorithm will expand all arcs also consuming O(n^2)
430 // memory; investigate alternatives to expanding all arcs (Prim's algorithm).
431 const std::vector<int> degrees =
432 ComputeOneTree(complete_graph, cost, best_weights, {}, &one_tree_cost);
433 w = one_tree_cost;
434 for (int j = 0; j < number_of_nodes; ++j) {
435 w += best_weights[j] * (degrees[j] - 2);
436 }
437 return w;
438}
439
440// Parameters to configure the computation of the TSP lower bound.
441struct TravelingSalesmanLowerBoundParameters {
442 enum Algorithm {
443 VolgenantJonker,
444 HeldWolfeCrowder,
445 };
446 // Subgradient algorithm to use to compute the TSP lower bound.
447 Algorithm algorithm = VolgenantJonker;
448 // Number of iterations to use in the Volgenant-Jonker algorithm. Overrides
449 // automatic iteration computation if positive.
450 int volgenant_jonker_iterations = 0;
451 // Number of nearest neighbors to consider in the miminum spanning trees.
452 int nearest_neighbors = 40;
453};
454
455// Computes the lower bound of a TSP using given parameters.
456template <typename CostFunction>
457double ComputeOneTreeLowerBoundWithParameters(
458 int number_of_nodes, const CostFunction& cost,
459 const TravelingSalesmanLowerBoundParameters& parameters) {
460 using CostType = decltype(cost(0, 0));
461 switch (parameters.algorithm) {
462 case TravelingSalesmanLowerBoundParameters::VolgenantJonker: {
463 VolgenantJonkerEvaluator<CostType> algorithm(
464 number_of_nodes, parameters.volgenant_jonker_iterations);
465 return ComputeOneTreeLowerBoundWithAlgorithm(
466 number_of_nodes, parameters.nearest_neighbors, cost, &algorithm);
467 break;
468 }
469 case TravelingSalesmanLowerBoundParameters::HeldWolfeCrowder: {
470 HeldWolfeCrowderEvaluator<CostType, CostFunction> algorithm(
471 number_of_nodes, cost);
472 return ComputeOneTreeLowerBoundWithAlgorithm(
473 number_of_nodes, parameters.nearest_neighbors, cost, &algorithm);
474 }
475 default:
476 LOG(ERROR) << "Unsupported algorithm: " << parameters.algorithm;
477 return 0;
478 }
479}
480
481// Computes the lower bound of a TSP using default parameters (Volgenant-Jonker
482// algorithm, 200 iterations and 40 nearest neighbors) which have turned out to
483// give good results on the TSPLIB.
484template <typename CostFunction>
485double ComputeOneTreeLowerBound(int number_of_nodes, const CostFunction& cost) {
486 TravelingSalesmanLowerBoundParameters parameters;
487 return ComputeOneTreeLowerBoundWithParameters(number_of_nodes, cost,
488 parameters);
489}
490
491} // namespace operations_research
492
493#endif // OR_TOOLS_GRAPH_ONE_TREE_LOWER_BOUND_H_
SatParameters parameters
int arc
In SWIG mode, we don't want anything besides these top-level includes.
std::vector< typename Graph::ArcIndex > BuildPrimMinimumSpanningTree(const Graph &graph, const ArcValue &arc_value)
void OnOneTree(CostType one_tree_cost, double w, absl::Span< const int > degrees)
trees with all degrees equal w the current value of w
void OnNewWMax(CostType one_tree_cost)
bool Next()
double GetStep() const
trees with all degrees equal to
trees with all degrees equal w the current value of int max_iterations
trees with all degrees equal w the current value of degrees
int64_t delta
Definition resource.cc:1709