From 1666cf41ab9ed64e3a0dbbd2dcf6e5c681254038 Mon Sep 17 00:00:00 2001 From: Laurent Perron Date: Tue, 9 Dec 2025 16:19:27 +0100 Subject: [PATCH] routing improvements --- ortools/constraint_solver/routing_ils.cc | 123 +++++++++++++++++++- ortools/constraint_solver/routing_ils.h | 6 + ortools/constraint_solver/routing_ils.proto | 32 ++++- 3 files changed, 158 insertions(+), 3 deletions(-) diff --git a/ortools/constraint_solver/routing_ils.cc b/ortools/constraint_solver/routing_ils.cc index ddfcebcf19..581d687594 100644 --- a/ortools/constraint_solver/routing_ils.cc +++ b/ortools/constraint_solver/routing_ils.cc @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -30,7 +31,6 @@ #include "absl/time/time.h" #include "absl/types/span.h" #include "google/protobuf/repeated_ptr_field.h" -#include "ortools/base/protoutil.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/routing.h" #include "ortools/constraint_solver/routing_ils.pb.h" @@ -468,6 +468,121 @@ class AllNodesPerformedAcceptanceCriterion const RoutingModel& model_; }; +// Returns the number of performed non-start/end nodes in the given assignment. +int CountPerformedNodes(const RoutingModel& model, + const Assignment& assignment) { + int count = 0; + for (int v = 0; v < model.vehicles(); ++v) { + int64_t current_node_index = model.Start(v); + while (true) { + current_node_index = assignment.Value(model.NextVar(current_node_index)); + if (model.IsEnd(current_node_index)) { + break; + } + count++; + } + } + return count; +} + +// Acceptance criterion in which a candidate assignment is accepted when it +// performs at least one more node than the reference assignment. +class MoreNodesPerformedAcceptanceCriterion + : public NeighborAcceptanceCriterion { + public: + explicit MoreNodesPerformedAcceptanceCriterion(const RoutingModel& model) + : model_(model) {} + + bool Accept([[maybe_unused]] const SearchState& search_state, + const Assignment* candidate, + const Assignment* reference) override { + return CountPerformedNodes(model_, *candidate) > + CountPerformedNodes(model_, *reference); + } + + private: + const RoutingModel& model_; +}; + +class AbsencesBasedAcceptanceCriterion : public NeighborAcceptanceCriterion { + public: + explicit AbsencesBasedAcceptanceCriterion( + const RoutingModel& model, bool remove_route_with_lowest_absences) + : model_(model), + remove_route_with_lowest_absences_(remove_route_with_lowest_absences), + absences_(model.Size(), 0) {} + + bool Accept([[maybe_unused]] const SearchState& search_state, + const Assignment* candidate, + const Assignment* reference) override { + int sum_candidate_absences = 0; + int sum_reference_absences = 0; + for (int node = 0; node < model_.Size(); ++node) { + if (model_.IsStart(node) || model_.IsEnd(node)) continue; + if (candidate->Value(model_.NextVar(node)) == node) { + sum_candidate_absences += absences_[node]; + } + if (reference->Value(model_.NextVar(node)) == node) { + sum_reference_absences += absences_[node]; + } + } + return sum_candidate_absences < sum_reference_absences; + } + + void OnIterationEnd(const Assignment* reference) override { + for (int node = 0; node < model_.Size(); ++node) { + if (model_.IsStart(node) || model_.IsEnd(node)) continue; + if (reference->Value(model_.NextVar(node)) == node) { + ++absences_[node]; + } + } + } + + void OnBestSolutionFound(Assignment* reference) override { + if (!remove_route_with_lowest_absences_) return; + + int candidate_route = -1; + int min_sum_absences = std::numeric_limits::max(); + + for (int route = 0; route < model_.vehicles(); ++route) { + if (model_.Next(*reference, model_.Start(route)) == model_.End(route)) + continue; + int sum_absences = 0; + for (int64_t node = reference->Value(model_.NextVar(model_.Start(route))); + node != model_.End(route); + node = reference->Value(model_.NextVar(node))) { + sum_absences += absences_[node]; + } + + if (sum_absences < min_sum_absences) { + candidate_route = route; + min_sum_absences = sum_absences; + } + } + + // Remove the route with the lowest sum of absences. + if (candidate_route != -1) { + // Set next pointers for inner nodes. + int64_t node = + reference->Value(model_.NextVar(model_.Start(candidate_route))); + while (node != model_.End(candidate_route)) { + const int64_t next_node = reference->Value(model_.NextVar(node)); + reference->SetValue(model_.NextVar(node), node); + reference->SetValue(model_.VehicleVar(node), -1); + node = next_node; + } + // Set next pointer for start node. + reference->SetValue(model_.NextVar(model_.Start(candidate_route)), + model_.End(candidate_route)); + } + } + + private: + const RoutingModel& model_; + bool remove_route_with_lowest_absences_; + std::vector absences_; +}; + // Returns whether the given assignment has at least one performed node. bool HasPerformedNodes(const RoutingModel& model, const Assignment& assignment) { @@ -1166,6 +1281,12 @@ std::unique_ptr MakeNeighborAcceptanceCriterion( rnd); case AcceptanceStrategy::kAllNodesPerformed: return std::make_unique(model); + case AcceptanceStrategy::kMoreNodesPerformed: + return std::make_unique(model); + case AcceptanceStrategy::kAbsencesBased: + return std::make_unique( + model, acceptance_strategy.absences_based() + .remove_route_with_lowest_absences()); default: LOG(DFATAL) << "Unsupported acceptance strategy."; return nullptr; diff --git a/ortools/constraint_solver/routing_ils.h b/ortools/constraint_solver/routing_ils.h index c783355c4f..8a59428783 100644 --- a/ortools/constraint_solver/routing_ils.h +++ b/ortools/constraint_solver/routing_ils.h @@ -265,6 +265,12 @@ class NeighborAcceptanceCriterion { virtual bool Accept(const SearchState& search_state, const Assignment* candidate, const Assignment* reference) = 0; + + // Called at the end of an ILS iteration. + virtual void OnIterationEnd([[maybe_unused]] const Assignment* reference) {} + + // Called when a new best solution found is found. + virtual void OnBestSolutionFound([[maybe_unused]] Assignment* reference) {} }; // Returns a neighbor acceptance criterion based on the given parameters. diff --git a/ortools/constraint_solver/routing_ils.proto b/ortools/constraint_solver/routing_ils.proto index 0a1369e007..f6ddd6e5a2 100644 --- a/ortools/constraint_solver/routing_ils.proto +++ b/ortools/constraint_solver/routing_ils.proto @@ -54,8 +54,6 @@ message RandomWalkRuinStrategy { // Ruin strategy based on the "Slack Induction by String Removals for Vehicle // Routing Problems" by Jan Christiaens and Greet Vanden Berghe, Transportation // Science 2020. -// Link to paper: -// https://kuleuven.limo.libis.be/discovery/fulldisplay?docid=lirias1988666&context=SearchWebhook&vid=32KUL_KUL:Lirias&lang=en&search_scope=lirias_profile&adaptor=SearchWebhook&tab=LIRIAS&query=any,contains,LIRIAS1988666&offset=0 // // Note that, in this implementation, the notion of "string" is replaced by // "sequence". @@ -296,12 +294,42 @@ message SimulatedAnnealingAcceptanceStrategy { // performed. message AllNodesPerformedAcceptanceStrategy {} +// Acceptance strategy in which a solution is accepted only if it performs at +// least one more node than the reference solution. +message MoreNodesPerformedAcceptanceStrategy {} + +// Acceptance strategy in which a solution is accepted only if it has less +// absences than the reference solution (see Slack Induction by String Removals +// for Vehicle Routing Problems" Christiaens and Vanden Berghe, Transportation +// Science 2020). +// +// In particular, for each node n, the number of solutions where n was not +// performed by any route is tracked by a counter absences[n]. A candidate is +// accepted if +// sum(absences[n]) < sum(absences[m]) +// with +// n in unperformed(candidate) +// m in unperformed(reference) +// +// The counter absences is increased after every ILS iteration for the +// unperformed nodes in the reference solution. In addition, when +// remove_route_with_lowest_absences is true and a new best found solution is +// found, the route with the lowest sum of absences is removed from the +// reference solution. +message AbsencesBasedAcceptanceStrategy { + // If true, when a new best solution is found, the route with the lowest sum + // of absences is removed from the reference solution. + optional bool remove_route_with_lowest_absences = 1; +} + // Determines when a candidate solution replaces another one. message AcceptanceStrategy { oneof strategy { GreedyDescentAcceptanceStrategy greedy_descent = 1; SimulatedAnnealingAcceptanceStrategy simulated_annealing = 2; AllNodesPerformedAcceptanceStrategy all_nodes_performed = 3; + MoreNodesPerformedAcceptanceStrategy more_nodes_performed = 4; + AbsencesBasedAcceptanceStrategy absences_based = 5; } }