From b73a0c2a19d5f16fda1a6df4347978fdafdc469c Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Wed, 7 Aug 2024 16:57:30 +0200 Subject: [PATCH] routing: export from google3 --- ortools/constraint_solver/constraint_solver.h | 53 ++- .../constraint_solver/constraint_solveri.h | 7 +- ortools/constraint_solver/search.cc | 126 +++++++- ortools/routing/ils.cc | 88 +++-- ortools/routing/routing.cc | 306 ++++++++++++------ ortools/routing/routing.h | 84 +++-- ortools/routing/search.cc | 37 ++- 7 files changed, 509 insertions(+), 192 deletions(-) diff --git a/ortools/constraint_solver/constraint_solver.h b/ortools/constraint_solver/constraint_solver.h index ad6983c768b..8750b2242b5 100644 --- a/ortools/constraint_solver/constraint_solver.h +++ b/ortools/constraint_solver/constraint_solver.h @@ -140,6 +140,7 @@ class LocalSearchProfiler; class ModelCache; class ModelVisitor; class ObjectiveMonitor; +class BaseObjectiveMonitor; class OptimizeVar; class Pack; class ProfiledDecisionBuilder; @@ -2364,6 +2365,14 @@ class Solver { bool reset_penalties_on_new_best_solution = false); #endif + // Creates a composite objective monitor which alternates between objective + // monitors every time the search reaches a local optimum local optimium + // reached. This will stop if all monitors return false when LocalOptimium is + // called. + BaseObjectiveMonitor* MakeRoundRobinCompoundObjectiveMonitor( + std::vector monitors, + int num_max_local_optima_before_metaheuristic_switch); + /// This search monitor will restart the search periodically. /// At the iteration n, it will restart after scale_factor * Luby(n) failures /// where Luby is the Luby Strategy (i.e. 1 1 2 1 1 2 4 1 1 2 1 1 2 4 8...). @@ -4477,8 +4486,32 @@ class SolutionCollector : public SearchMonitor { #endif // SWIG }; -// Base objective monitor class. All metaheuristics derive from this. -class ObjectiveMonitor : public SearchMonitor { +// Base objective monitor class. All metaheuristics and metaheuristic combiners +// derive from this. +class BaseObjectiveMonitor : public SearchMonitor { + public: + explicit BaseObjectiveMonitor(Solver* solver) : SearchMonitor(solver) {} + ~BaseObjectiveMonitor() override {} +#ifndef SWIG + BaseObjectiveMonitor(const BaseObjectiveMonitor&) = delete; + BaseObjectiveMonitor& operator=(const BaseObjectiveMonitor&) = delete; +#endif // SWIG + virtual IntVar* ObjectiveVar(int index) const = 0; + virtual IntVar* MinimizationVar(int index) const = 0; + virtual int64_t Step(int index) const = 0; + virtual bool Maximize(int index) const = 0; + virtual int64_t BestValue(int index) const = 0; + virtual int Size() const = 0; + bool is_active() const { return is_active_; } + void set_active(bool is_active) { is_active_ = is_active; } + + private: + bool is_active_ = true; +}; + +// Base atomic objective monitor class. All non-composite metaheuristics derive +// from this. +class ObjectiveMonitor : public BaseObjectiveMonitor { public: ObjectiveMonitor(Solver* solver, const std::vector& maximize, std::vector vars, std::vector steps); @@ -4487,17 +4520,21 @@ class ObjectiveMonitor : public SearchMonitor { ObjectiveMonitor(const ObjectiveMonitor&) = delete; ObjectiveMonitor& operator=(const ObjectiveMonitor&) = delete; #endif // SWIG - IntVar* ObjectiveVar(int index) const { return objective_vars_[index]; } - IntVar* MinimizationVar(int index) const { return minimization_vars_[index]; } - int64_t Step(int index) const { return steps_[index]; } - bool Maximize(int index) const { + IntVar* ObjectiveVar(int index) const override { + return objective_vars_[index]; + } + IntVar* MinimizationVar(int index) const override { + return minimization_vars_[index]; + } + int64_t Step(int index) const override { return steps_[index]; } + bool Maximize(int index) const override { return ObjectiveVar(index) != MinimizationVar(index); } - int64_t BestValue(int index) const { + int64_t BestValue(int index) const override { return Maximize(index) ? CapOpp(BestInternalValue(index)) : BestInternalValue(index); } - int Size() const { return objective_vars_.size(); } + int Size() const override { return objective_vars_.size(); } void EnterSearch() override; bool AtSolution() override; bool AcceptDelta(Assignment* delta, Assignment* deltadelta) override; diff --git a/ortools/constraint_solver/constraint_solveri.h b/ortools/constraint_solver/constraint_solveri.h index 3e3e76f49f3..ff7911e5b73 100644 --- a/ortools/constraint_solver/constraint_solveri.h +++ b/ortools/constraint_solver/constraint_solveri.h @@ -587,10 +587,9 @@ class CallMethod2 : public Demon { } std::string DebugString() const override { - return absl::StrCat(absl::StrCat("CallMethod_", name_), - absl::StrCat("(", constraint_->DebugString()), - absl::StrCat(", ", ParameterDebugString(param1_)), - absl::StrCat(", ", ParameterDebugString(param2_), ")")); + return absl::StrCat("CallMethod_", name_, "(", constraint_->DebugString(), + ", ", ParameterDebugString(param1_), ", ", + ParameterDebugString(param2_), ")"); } private: diff --git a/ortools/constraint_solver/search.cc b/ortools/constraint_solver/search.cc index 51d8ef5c6dd..9be936d4585 100644 --- a/ortools/constraint_solver/search.cc +++ b/ortools/constraint_solver/search.cc @@ -2963,11 +2963,116 @@ SolutionCollector* Solver::MakeAllSolutionCollector() { // ---------- Objective Management ---------- +class RoundRobinCompoundObjectiveMonitor : public BaseObjectiveMonitor { + public: + RoundRobinCompoundObjectiveMonitor( + std::vector monitors, + int num_max_local_optima_before_metaheuristic_switch) + : BaseObjectiveMonitor(monitors[0]->solver()), + monitors_(std::move(monitors)), + enabled_monitors_(monitors_.size(), true), + local_optimum_limit_(num_max_local_optima_before_metaheuristic_switch) { + } + void EnterSearch() override { + active_monitor_ = 0; + num_local_optimum_ = 0; + enabled_monitors_.assign(monitors_.size(), true); + for (auto& monitor : monitors_) { + monitor->set_active(monitor == monitors_[active_monitor_]); + monitor->EnterSearch(); + } + } + void ApplyDecision(Decision* d) override { + monitors_[active_monitor_]->ApplyDecision(d); + } + void AcceptNeighbor() override { + monitors_[active_monitor_]->AcceptNeighbor(); + } + bool AtSolution() override { + bool ok = true; + for (auto& monitor : monitors_) { + ok &= monitor->AtSolution(); + } + return ok; + } + bool AcceptDelta(Assignment* delta, Assignment* deltadelta) override { + return monitors_[active_monitor_]->AcceptDelta(delta, deltadelta); + } + void BeginNextDecision(DecisionBuilder* db) override { + monitors_[active_monitor_]->BeginNextDecision(db); + } + void RefuteDecision(Decision* d) override { + monitors_[active_monitor_]->RefuteDecision(d); + } + bool AcceptSolution() override { + return monitors_[active_monitor_]->AcceptSolution(); + } + bool LocalOptimum() override { + const bool ok = monitors_[active_monitor_]->LocalOptimum(); + if (!ok) { + enabled_monitors_[active_monitor_] = false; + } + if (++num_local_optimum_ >= local_optimum_limit_ || !ok) { + monitors_[active_monitor_]->set_active(false); + int next_active_monitor = (active_monitor_ + 1) % monitors_.size(); + while (!enabled_monitors_[next_active_monitor]) { + if (next_active_monitor == active_monitor_) return false; + next_active_monitor = (active_monitor_ + 1) % monitors_.size(); + } + active_monitor_ = next_active_monitor; + monitors_[active_monitor_]->set_active(true); + num_local_optimum_ = 0; + VLOG(2) << "Switching to monitor " << active_monitor_ << " " + << monitors_[active_monitor_]->DebugString(); + } + return true; + } + IntVar* ObjectiveVar(int index) const override { + return monitors_[active_monitor_]->ObjectiveVar(index); + } + IntVar* MinimizationVar(int index) const override { + return monitors_[active_monitor_]->MinimizationVar(index); + } + int64_t Step(int index) const override { + return monitors_[active_monitor_]->Step(index); + } + bool Maximize(int index) const override { + return monitors_[active_monitor_]->Maximize(index); + } + int64_t BestValue(int index) const override { + return monitors_[active_monitor_]->BestValue(index); + } + int Size() const override { return monitors_[active_monitor_]->Size(); } + std::string DebugString() const override { + return monitors_[active_monitor_]->DebugString(); + } + void Accept(ModelVisitor* visitor) const override { + // TODO(user): properly implement this. + for (auto& monitor : monitors_) { + monitor->Accept(visitor); + } + } + + private: + const std::vector monitors_; + std::vector enabled_monitors_; + int active_monitor_ = 0; + int num_local_optimum_ = 0; + const int local_optimum_limit_; +}; + +BaseObjectiveMonitor* Solver::MakeRoundRobinCompoundObjectiveMonitor( + std::vector monitors, + int num_max_local_optima_before_metaheuristic_switch) { + return RevAlloc(new RoundRobinCompoundObjectiveMonitor( + std::move(monitors), num_max_local_optima_before_metaheuristic_switch)); +} + ObjectiveMonitor::ObjectiveMonitor(Solver* solver, const std::vector& maximize, std::vector vars, std::vector steps) - : SearchMonitor(solver), + : BaseObjectiveMonitor(solver), found_initial_solution_(false), objective_vars_(std::move(vars)), minimization_vars_(objective_vars_), @@ -3102,14 +3207,14 @@ void OptimizeVar::BeginNextDecision(DecisionBuilder*) { void OptimizeVar::ApplyBound() { if (found_initial_solution_) { MakeMinimizationVarsLessOrEqualWithSteps( - [this](int i) { return BestInternalValue(i); }); + [this](int i) { return CurrentInternalValue(i); }); } } void OptimizeVar::RefuteDecision(Decision*) { ApplyBound(); } bool OptimizeVar::AcceptSolution() { - if (!found_initial_solution_) { + if (!found_initial_solution_ || !is_active()) { return true; } else { // This code should never return false in sequential mode because @@ -3121,8 +3226,8 @@ bool OptimizeVar::AcceptSolution() { // accepted. if (!minimization_var->Bound()) return true; const int64_t value = minimization_var->Value(); - if (value == BestInternalValue(i)) continue; - return value < BestInternalValue(i); + if (value == CurrentInternalValue(i)) continue; + return value < CurrentInternalValue(i); } return false; } @@ -3304,6 +3409,7 @@ class TabuSearch : public Metaheuristic { const std::vector vars_; Assignment::IntContainer assignment_container_; + bool has_stored_assignment_ = false; std::vector last_values_; TabuList keep_tabu_list_; TabuList synced_keep_tabu_list_; @@ -3337,6 +3443,7 @@ void TabuSearch::EnterSearch() { Metaheuristic::EnterSearch(); solver()->SetUseFastLocalSearch(true); stamp_ = 0; + has_stored_assignment_ = false; } void TabuSearch::ApplyDecision(Decision* const d) { @@ -3415,7 +3522,7 @@ bool TabuSearch::AtSolution() { // New solution found: add new assignments to tabu lists; this is only // done after the first local optimum (stamp_ != 0) - if (0 != stamp_) { + if (0 != stamp_ && has_stored_assignment_) { for (int index = 0; index < vars_.size(); ++index) { IntVar* var = vars(index); const int64_t old_value = assignment_container_.Element(index).Value(); @@ -3431,6 +3538,7 @@ bool TabuSearch::AtSolution() { } } assignment_container_.Store(); + has_stored_assignment_ = true; return true; } @@ -3945,7 +4053,9 @@ void GuidedLocalSearch

::ApplyDecision(Decision* const d) { assignment_penalized_value_ = CapAdd(assignment_penalized_value_, penalty); } - penalized_objective_ = solver()->MakeSum(elements)->Var(); + solver()->SaveAndSetValue( + reinterpret_cast(&penalized_objective_), + reinterpret_cast(solver()->MakeSum(elements)->Var())); } penalized_values_.Commit(); old_penalized_value_ = assignment_penalized_value_; @@ -3983,7 +4093,7 @@ bool GuidedLocalSearch

::AtSolution() { if (!ObjectiveMonitor::AtSolution()) { return false; } - if (penalized_objective_ != nullptr) { + if (penalized_objective_ != nullptr && penalized_objective_->Bound()) { // If the value of the best solution has changed (aka a new best solution // has been found), triggering a reset on the penalties to start fresh. // The immediate consequence is a greedy dive towards a local minimum, diff --git a/ortools/routing/ils.cc b/ortools/routing/ils.cc index 9024c7d1e00..1de349c77c0 100644 --- a/ortools/routing/ils.cc +++ b/ortools/routing/ils.cc @@ -434,13 +434,18 @@ bool HasPerformedNodes(const RoutingModel& model, return false; } -// Returns a random performed customer for the given assignment and customer -// distribution. The procedure assumes the assignment has at least one -// performed customer. +// Returns a random performed visit for the given assignment. The procedure +// requires a distribution including all visits. Returns -1 if there are no +// performed visits. int64_t PickRandomPerformedVisit( const RoutingModel& model, const Assignment& assignment, std::mt19937& rnd, std::uniform_int_distribution& customer_dist) { - DCHECK(HasPerformedNodes(model, assignment)); + DCHECK_EQ(customer_dist.min(), 0); + DCHECK_EQ(customer_dist.max(), model.Size() - model.vehicles()); + + if (!HasPerformedNodes(model, assignment)) { + return -1; + } int64_t customer; do { @@ -654,42 +659,53 @@ CloseRoutesRemovalRuinProcedure::CloseRoutesRemovalRuinProcedure( neighbors_manager_(model->GetOrCreateNodeNeighborsByCostClass( {num_neighbors_for_route_selection, /*add_vehicle_starts_to_neighbors=*/false, + /*add_vehicle_ends_to_neighbors=*/false, /*only_sort_neighbors_for_partial_neighborhoods=*/false})), num_routes_(num_routes), rnd_(*rnd), - customer_dist_(0, model->Size() - 1), + customer_dist_(0, model->Size() - model->vehicles()), removed_routes_(model->vehicles()) {} std::function CloseRoutesRemovalRuinProcedure::Ruin( const Assignment* assignment) { + if (num_routes_ == 0) { + return [this, assignment](int64_t node) { + return assignment->Value(model_.NextVar(node)); + }; + } + + const int64_t seed_node = + PickRandomPerformedVisit(model_, *assignment, rnd_, customer_dist_); + if (seed_node == -1) { + return [this, assignment](int64_t node) { + return assignment->Value(model_.NextVar(node)); + }; + } + removed_routes_.SparseClearAll(); - if (num_routes_ > 0 && HasPerformedNodes(model_, *assignment)) { - const int64_t seed_node = - PickRandomPerformedVisit(model_, *assignment, rnd_, customer_dist_); - const int seed_route = assignment->Value(model_.VehicleVar(seed_node)); - DCHECK_GE(seed_route, 0); + const int seed_route = assignment->Value(model_.VehicleVar(seed_node)); + DCHECK_GE(seed_route, 0); - removed_routes_.Set(seed_route); + removed_routes_.Set(seed_route); - const RoutingCostClassIndex cost_class_index = - model_.GetCostClassIndexOfVehicle(seed_route); + const RoutingCostClassIndex cost_class_index = + model_.GetCostClassIndexOfVehicle(seed_route); - const std::vector& neighbors = - neighbors_manager_->GetNeighborsOfNodeForCostClass( - cost_class_index.value(), seed_node); + const std::vector& neighbors = + neighbors_manager_->GetOutgoingNeighborsOfNodeForCostClass( + cost_class_index.value(), seed_node); - for (int neighbor : neighbors) { - if (removed_routes_.NumberOfSetCallsWithDifferentArguments() == - num_routes_) { - break; - } - const int64_t route = assignment->Value(model_.VehicleVar(neighbor)); - if (route < 0 || removed_routes_[route]) { - continue; - } - removed_routes_.Set(route); + for (int neighbor : neighbors) { + if (removed_routes_.NumberOfSetCallsWithDifferentArguments() == + num_routes_) { + break; + } + const int64_t route = assignment->Value(model_.VehicleVar(neighbor)); + if (route < 0 || removed_routes_[route]) { + continue; } + removed_routes_.Set(route); } return [this, assignment](int64_t node) { @@ -712,23 +728,29 @@ RandomWalkRemovalRuinProcedure::RandomWalkRemovalRuinProcedure( neighbors_manager_(model->GetOrCreateNodeNeighborsByCostClass( {num_neighbors_for_route_selection, /*add_vehicle_starts_to_neighbors=*/false, + /*add_vehicle_ends_to_neighbors=*/false, /*only_sort_neighbors_for_partial_neighborhoods=*/false})), rnd_(*rnd), walk_length_(walk_length), - customer_dist_(0, model->Size() - 1) {} + customer_dist_(0, model->Size() - model->vehicles()) {} std::function RandomWalkRemovalRuinProcedure::Ruin( const Assignment* assignment) { - if (!HasPerformedNodes(model_, *assignment) || walk_length_ == 0) { - return [&model = model_, assignment](int64_t node) { - return assignment->Value(model.NextVar(node)); + if (walk_length_ == 0) { + return [this, assignment](int64_t node) { + return assignment->Value(model_.NextVar(node)); }; } - routing_solution_.Reset(assignment); - int64_t curr_node = PickRandomPerformedVisit(model_, *assignment, rnd_, customer_dist_); + if (curr_node == -1) { + return [this, assignment](int64_t node) { + return assignment->Value(model_.NextVar(node)); + }; + } + + routing_solution_.Reset(assignment); int walk_length = walk_length_; @@ -774,7 +796,7 @@ int64_t RandomWalkRemovalRuinProcedure::GetNextNodeToRemove( model_.GetCostClassIndexOfVehicle(curr_vehicle); const std::vector& neighbors = - neighbors_manager_->GetNeighborsOfNodeForCostClass( + neighbors_manager_->GetOutgoingNeighborsOfNodeForCostClass( cost_class_index.value(), node); int64_t same_route_closest_neighbor = -1; diff --git a/ortools/routing/routing.cc b/ortools/routing/routing.cc index 6a301419f3d..fd4098192af 100644 --- a/ortools/routing/routing.cc +++ b/ortools/routing/routing.cc @@ -238,165 +238,247 @@ SweepArranger* RoutingModel::sweep_arranger() const { } void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( - const RoutingModel& routing_model, const NodeNeighborsParameters& params) { + const NodeNeighborsParameters& params) { auto [num_neighbors, add_vehicle_starts_to_neighbors, + add_vehicle_ends_to_neighbors, only_sort_neighbors_for_partial_neighborhoods] = params; DCHECK_GE(num_neighbors, 0); // TODO(user): consider checking search limits. - const int size = routing_model.Size(); - const int num_non_start_end_nodes = size - routing_model.vehicles(); - const int size_with_vehicle_nodes = size + routing_model.vehicles(); + const int size = routing_model_.Size(); + const int num_non_start_end_nodes = size - routing_model_.vehicles(); + const int size_with_vehicle_nodes = size + routing_model_.vehicles(); const int max_num_neighbors = std::max(num_non_start_end_nodes - 1, 0); num_neighbors = std::min(max_num_neighbors, num_neighbors); - node_index_to_neighbors_by_cost_class_.clear(); - node_index_to_neighbor_indicator_by_cost_class_.clear(); + node_index_to_incoming_neighbors_by_cost_class_.clear(); + node_index_to_outgoing_neighbors_by_cost_class_.clear(); + node_index_to_outgoing_neighbor_indicator_by_cost_class_.clear(); + all_incoming_nodes_.clear(); + all_outgoing_nodes_.clear(); if (num_neighbors == max_num_neighbors && only_sort_neighbors_for_partial_neighborhoods) { - all_nodes_.reserve(size); - for (int node = 0; node < size; node++) { - if (add_vehicle_starts_to_neighbors || !routing_model.IsStart(node)) { - all_nodes_.push_back(node); + all_incoming_nodes_.reserve(size); + all_outgoing_nodes_.reserve(size); + for (int node = 0; node < size_with_vehicle_nodes; node++) { + const bool not_start = !routing_model_.IsStart(node); + const bool not_end = !routing_model_.IsEnd(node); + if (not_start && (not_end || add_vehicle_ends_to_neighbors)) { + all_outgoing_nodes_.push_back(node); + } + if (not_end && (not_start || add_vehicle_starts_to_neighbors)) { + all_incoming_nodes_.push_back(node); } } return; } - const int num_cost_classes = routing_model.GetCostClassesCount(); - node_index_to_neighbors_by_cost_class_.resize(num_cost_classes); - node_index_to_neighbor_indicator_by_cost_class_.resize(num_cost_classes); + const int num_cost_classes = routing_model_.GetCostClassesCount(); + node_index_to_incoming_neighbors_by_cost_class_.resize(num_cost_classes); + node_index_to_outgoing_neighbors_by_cost_class_.resize(num_cost_classes); + node_index_to_outgoing_neighbor_indicator_by_cost_class_.resize( + num_cost_classes); std::vector>> - node_index_to_costs_by_cost_class(num_cost_classes); + node_index_to_outgoing_costs_by_cost_class(num_cost_classes); for (int cc = 0; cc < num_cost_classes; cc++) { - node_index_to_neighbors_by_cost_class_[cc].resize(size_with_vehicle_nodes); - if (!routing_model.HasVehicleWithCostClassIndex( + if (!routing_model_.HasVehicleWithCostClassIndex( RoutingCostClassIndex(cc))) { continue; } - node_index_to_neighbor_indicator_by_cost_class_[cc].resize( + node_index_to_incoming_neighbors_by_cost_class_[cc].resize( size_with_vehicle_nodes); - node_index_to_costs_by_cost_class[cc].resize(size_with_vehicle_nodes); + node_index_to_outgoing_neighbors_by_cost_class_[cc].resize(size); + node_index_to_outgoing_neighbor_indicator_by_cost_class_[cc].resize(size); + node_index_to_outgoing_costs_by_cost_class[cc].resize(size); for (int node = 0; node < size_with_vehicle_nodes; node++) { - node_index_to_neighbors_by_cost_class_[cc][node].reserve( - num_neighbors + routing_model.vehicles()); - node_index_to_neighbor_indicator_by_cost_class_[cc][node].resize(size, - false); - node_index_to_costs_by_cost_class[cc][node].resize(size, -1); + node_index_to_incoming_neighbors_by_cost_class_[cc][node].reserve( + num_neighbors + routing_model_.vehicles()); + if (node < size) { + node_index_to_outgoing_neighbors_by_cost_class_[cc][node].reserve( + num_neighbors + routing_model_.vehicles()); + node_index_to_outgoing_neighbor_indicator_by_cost_class_[cc][node] + .resize(size_with_vehicle_nodes, false); + node_index_to_outgoing_costs_by_cost_class[cc][node].resize( + size_with_vehicle_nodes, -1); + } } } - std::vector neighbors; + std::vector outgoing_neighbors; for (int cost_class = 0; cost_class < num_cost_classes; cost_class++) { - if (!routing_model.HasVehicleWithCostClassIndex( + if (!routing_model_.HasVehicleWithCostClassIndex( RoutingCostClassIndex(cost_class))) { // No vehicle with this cost class, avoid unnecessary computations. continue; } - std::vector>& node_index_to_neighbors = - node_index_to_neighbors_by_cost_class_[cost_class]; - std::vector>& node_index_to_neighbor_indicator = - node_index_to_neighbor_indicator_by_cost_class_[cost_class]; - std::vector>& node_index_to_costs = - node_index_to_costs_by_cost_class[cost_class]; + std::vector>& node_index_to_incoming_neighbors = + node_index_to_incoming_neighbors_by_cost_class_[cost_class]; + std::vector>& node_index_to_outgoing_neighbors = + node_index_to_outgoing_neighbors_by_cost_class_[cost_class]; + std::vector>& node_index_to_outgoing_neighbor_indicator = + node_index_to_outgoing_neighbor_indicator_by_cost_class_[cost_class]; + std::vector>& node_index_to_outgoing_costs = + node_index_to_outgoing_costs_by_cost_class[cost_class]; for (int node_index = 0; node_index < size; ++node_index) { - if (routing_model.IsStart(node_index)) { + if (routing_model_.IsStart(node_index)) { // For vehicle start/ends, we consider all nodes (see below). continue; } // TODO(user): Use the model's IndexNeighborFinder when available. - neighbors.clear(); - neighbors.reserve(num_non_start_end_nodes); + outgoing_neighbors.clear(); + outgoing_neighbors.reserve(num_non_start_end_nodes); if (num_neighbors > 0) { - std::vector& costs = node_index_to_costs[node_index]; + std::vector& costs = node_index_to_outgoing_costs[node_index]; for (int after_node = 0; after_node < size; ++after_node) { - if (after_node != node_index && !routing_model.IsStart(after_node)) { - costs[after_node] = routing_model.GetArcCostForClass( + if (after_node != node_index && !routing_model_.IsStart(after_node)) { + costs[after_node] = routing_model_.GetArcCostForClass( node_index, after_node, cost_class); - neighbors.push_back(after_node); + outgoing_neighbors.push_back(after_node); } } // Get the 'num_neighbors' closest neighbors. - DCHECK_GE(neighbors.size(), num_neighbors); - std::nth_element( - neighbors.begin(), neighbors.begin() + num_neighbors - 1, - neighbors.end(), [&costs](int n1, int n2) { - return std::tie(costs[n1], n1) < std::tie(costs[n2], n2); - }); - neighbors.resize(num_neighbors); + DCHECK_GE(outgoing_neighbors.size(), num_neighbors); + std::nth_element(outgoing_neighbors.begin(), + outgoing_neighbors.begin() + num_neighbors - 1, + outgoing_neighbors.end(), [&costs](int n1, int n2) { + return std::tie(costs[n1], n1) < + std::tie(costs[n2], n2); + }); + outgoing_neighbors.resize(num_neighbors); } // Add neighborhoods. - for (int neighbor : neighbors) { - DCHECK(!routing_model.IsEnd(neighbor) && - !routing_model.IsStart(neighbor)); - if (node_index_to_neighbor_indicator[node_index][neighbor]) { - DCHECK(node_index_to_neighbor_indicator[neighbor][node_index]); - continue; - } - DCHECK(!node_index_to_neighbor_indicator[node_index][neighbor]); - node_index_to_neighbor_indicator[node_index][neighbor] = true; - node_index_to_neighbors[node_index].push_back(neighbor); - DCHECK(!node_index_to_neighbor_indicator[neighbor][node_index]); - node_index_to_neighbor_indicator[neighbor][node_index] = true; - node_index_to_neighbors[neighbor].push_back(node_index); + for (int outgoing_neighbor : outgoing_neighbors) { + DCHECK(!routing_model_.IsEnd(outgoing_neighbor) && + !routing_model_.IsStart(outgoing_neighbor)); + DCHECK(!node_index_to_outgoing_neighbor_indicator[node_index] + [outgoing_neighbor]); + node_index_to_outgoing_neighbor_indicator[node_index] + [outgoing_neighbor] = true; + node_index_to_outgoing_neighbors[node_index].push_back( + outgoing_neighbor); + // node_index is an incoming neighbor of outgoing_neighbor. + node_index_to_incoming_neighbors[outgoing_neighbor].push_back( + node_index); } + } + } - // Add all vehicle starts as neighbors to this node and vice-versa. - // TODO(user): Consider keeping vehicle start/ends out of neighbors, to - // prune arcs going from node to start for instance. - // TODO(user): Investigate whether we actually need to keep track of - // neighbors for vehicle ends. - // TODO(user): Investigate if vehicle ends should be considered as - // neighbors for every node too. - // TODO(user): In the current state of the function, vehicle starts - // aren't set as neighbors for vehicle ends. Investigate if that's WAI. - for (int vehicle = 0; vehicle < routing_model.vehicles(); vehicle++) { - const int vehicle_start = routing_model.Start(vehicle); - const int64_t cost_from_start = routing_model.GetArcCostForClass( - vehicle_start, node_index, cost_class); + // Add all vehicle start/ends as incoming/outgoing neighbors for all nodes. + for (int cost_class = 0; cost_class < num_cost_classes; cost_class++) { + if (!routing_model_.HasVehicleWithCostClassIndex( + RoutingCostClassIndex(cost_class))) { + // No vehicle with this cost class, avoid unnecessary computations. + continue; + } + std::vector>& node_index_to_incoming_neighbors = + node_index_to_incoming_neighbors_by_cost_class_[cost_class]; + std::vector>& node_index_to_outgoing_neighbors = + node_index_to_outgoing_neighbors_by_cost_class_[cost_class]; + std::vector>& node_index_to_outgoing_neighbor_indicator = + node_index_to_outgoing_neighbor_indicator_by_cost_class_[cost_class]; + std::vector>& node_index_to_outgoing_costs = + node_index_to_outgoing_costs_by_cost_class[cost_class]; + for (int vehicle = 0; vehicle < routing_model_.vehicles(); vehicle++) { + const int vehicle_start = routing_model_.Start(vehicle); + const int vehicle_end = routing_model_.End(vehicle); + + // Mark vehicle_start -> vehicle_end as a neighborhood arc. + DCHECK(!node_index_to_outgoing_neighbor_indicator[vehicle_start] + [vehicle_end]); + node_index_to_outgoing_neighbor_indicator[vehicle_start][vehicle_end] = + true; + if (add_vehicle_starts_to_neighbors) { + node_index_to_incoming_neighbors[vehicle_end].push_back(vehicle_start); + } + if (add_vehicle_ends_to_neighbors) { + node_index_to_outgoing_neighbors[vehicle_start].push_back(vehicle_end); + } + node_index_to_outgoing_costs[vehicle_start][vehicle_end] = + routing_model_.GetArcCostForClass(vehicle_start, vehicle_end, + cost_class); + + for (int node_index = 0; node_index < size; ++node_index) { + if (routing_model_.IsStart(node_index)) continue; + + // Mark vehicle_start -> node_index as a neighborhood arc. + DCHECK(!node_index_to_outgoing_neighbor_indicator[node_index] + [vehicle_start]); + DCHECK(!node_index_to_outgoing_neighbor_indicator[vehicle_start] + [node_index]); + node_index_to_outgoing_neighbor_indicator[vehicle_start][node_index] = + true; if (add_vehicle_starts_to_neighbors) { - DCHECK(!node_index_to_neighbor_indicator[node_index][vehicle_start]); - node_index_to_neighbor_indicator[node_index][vehicle_start] = true; - node_index_to_neighbors[node_index].push_back(vehicle_start); - node_index_to_costs[node_index][vehicle_start] = cost_from_start; + node_index_to_incoming_neighbors[node_index].push_back(vehicle_start); + } + node_index_to_outgoing_neighbors[vehicle_start].push_back(node_index); + node_index_to_outgoing_costs[vehicle_start][node_index] = + routing_model_.GetArcCostForClass(vehicle_start, node_index, + cost_class); + + // Mark node_index -> vehicle_end as a neighborhood arc. + DCHECK(!node_index_to_outgoing_neighbor_indicator[node_index] + [vehicle_end]); + node_index_to_outgoing_neighbor_indicator[node_index][vehicle_end] = + true; + node_index_to_incoming_neighbors[vehicle_end].push_back(node_index); + if (add_vehicle_ends_to_neighbors) { + node_index_to_outgoing_neighbors[node_index].push_back(vehicle_end); } - DCHECK(!node_index_to_neighbor_indicator[vehicle_start][node_index]); - node_index_to_neighbor_indicator[vehicle_start][node_index] = true; - node_index_to_neighbors[vehicle_start].push_back(node_index); - node_index_to_costs[vehicle_start][node_index] = cost_from_start; - - const int vehicle_end = routing_model.End(vehicle); - DCHECK(!node_index_to_neighbor_indicator[vehicle_end][node_index]); - node_index_to_neighbor_indicator[vehicle_end][node_index] = true; - node_index_to_neighbors[vehicle_end].push_back(node_index); - node_index_to_costs[vehicle_end][node_index] = - routing_model.GetArcCostForClass(node_index, vehicle_end, - cost_class); + node_index_to_outgoing_costs[node_index][vehicle_end] = + routing_model_.GetArcCostForClass(node_index, vehicle_end, + cost_class); } } } - // Sort the neighbors into node_index_to_neighbors_by_cost_class_ by cost. + // Sort the neighbors into + // node_index_to_{incoming,outgoing}_neighbors_by_cost_class_ by cost. for (int cost_class = 0; cost_class < num_cost_classes; cost_class++) { - if (!routing_model.HasVehicleWithCostClassIndex( + if (!routing_model_.HasVehicleWithCostClassIndex( RoutingCostClassIndex(cost_class))) { // No vehicle with this cost class. continue; } + const std::vector>& node_index_to_outgoing_costs = + node_index_to_outgoing_costs_by_cost_class[cost_class]; for (int node_index = 0; node_index < size_with_vehicle_nodes; ++node_index) { - auto& node_neighbors = - node_index_to_neighbors_by_cost_class_[cost_class][node_index]; - const std::vector& costs = - node_index_to_costs_by_cost_class[cost_class][node_index]; - absl::c_sort(node_neighbors, [&costs](int n1, int n2) { - DCHECK_GE(costs[n1], 0); - DCHECK_GE(costs[n2], 0); - return std::tie(costs[n1], n1) < std::tie(costs[n2], n2); - }); + std::vector& incoming_node_neighbors = + node_index_to_incoming_neighbors_by_cost_class_[cost_class] + [node_index]; + absl::c_sort( + incoming_node_neighbors, + [node_index, size, &node_index_to_outgoing_costs](int n1, int n2) { + DCHECK_GE(node_index_to_outgoing_costs[n1][node_index], 0); + DCHECK_GE(node_index_to_outgoing_costs[n2][node_index], 0); + DCHECK_LT(n1, size); + DCHECK_LT(n2, size); + return std::tie(node_index_to_outgoing_costs[n1][node_index], n1) < + std::tie(node_index_to_outgoing_costs[n2][node_index], n2); + }); // Check that there are no duplicate elements. - DCHECK(absl::c_adjacent_find(node_neighbors) == node_neighbors.end()); + DCHECK(absl::c_adjacent_find(incoming_node_neighbors) == + incoming_node_neighbors.end()); + + if (node_index < size) { + std::vector& outgoing_node_neighbors = + node_index_to_outgoing_neighbors_by_cost_class_[cost_class] + [node_index]; + const std::vector& outgoing_costs = + node_index_to_outgoing_costs[node_index]; + absl::c_sort(outgoing_node_neighbors, + [&outgoing_costs](int n1, int n2) { + DCHECK_GE(outgoing_costs[n1], 0); + DCHECK_GE(outgoing_costs[n2], 0); + return std::tie(outgoing_costs[n1], n1) < + std::tie(outgoing_costs[n2], n2); + }); + + // Check that there are no duplicate elements. + DCHECK(absl::c_adjacent_find(outgoing_node_neighbors) == + outgoing_node_neighbors.end()); + } } } } @@ -404,7 +486,7 @@ void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( const RoutingModel::NodeNeighborsByCostClass* RoutingModel::GetOrCreateNodeNeighborsByCostClass( double neighbors_ratio, int64_t min_neighbors, double& neighbors_ratio_used, - bool add_vehicle_starts_to_neighbors, + bool add_vehicle_starts_to_neighbors, bool add_vehicle_ends_to_neighbors, bool only_sort_neighbors_for_partial_neighborhoods) { const int64_t num_non_start_end_nodes = Size() - vehicles(); neighbors_ratio_used = neighbors_ratio; @@ -417,6 +499,7 @@ RoutingModel::GetOrCreateNodeNeighborsByCostClass( } return GetOrCreateNodeNeighborsByCostClass( {num_neighbors, add_vehicle_starts_to_neighbors, + add_vehicle_ends_to_neighbors, only_sort_neighbors_for_partial_neighborhoods}); } @@ -428,8 +511,9 @@ RoutingModel::GetOrCreateNodeNeighborsByCostClass( if (node_neighbors_by_cost_class != nullptr) { return node_neighbors_by_cost_class.get(); } - node_neighbors_by_cost_class = std::make_unique(); - node_neighbors_by_cost_class->ComputeNeighbors(*this, params); + node_neighbors_by_cost_class = + std::make_unique(this); + node_neighbors_by_cost_class->ComputeNeighbors(params); return node_neighbors_by_cost_class.get(); } @@ -4548,11 +4632,25 @@ void RoutingModel::CreateNeighborhoodOperators( GetOrCreateNodeNeighborsByCostClass( parameters.ls_operator_neighbors_ratio(), parameters.ls_operator_min_neighbors(), neighbors_ratio_used, - /*add_vehicle_starts_to_neighbors=*/false); + /*add_vehicle_starts_to_neighbors=*/false, + /*add_vehicle_ends_to_neighbors=*/false); + // TODO(user): Consider passing incoming/outgoing neighbors separately to + // LS operators. This would allow the Cross operator to use both incoming and + // outgoing neighbors, to exchange path starts and ends independently. const auto get_neighbors = [neighbors_by_cost_class, this]( int64_t node, int64_t start) -> const std::vector& { - return neighbors_by_cost_class->GetNeighborsOfNodeForCostClass( + if (IsEnd(node)) { + // HACK(user): As of CL/651760817, vehicle end nodes no longer have + // outgoing neighbors, which causes neighborhood operators that iterate + // on vehicle ends as seeds to fail. This is a temporary hack to consider + // the incoming neighbors for vehicle ends, before the proper fix which is + // to pass 2 separate callbacks for incoming and outgoing neighbors to + // neighborhood operators. + return neighbors_by_cost_class->GetIncomingNeighborsOfNodeForCostClass( + GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); + } + return neighbors_by_cost_class->GetOutgoingNeighborsOfNodeForCostClass( GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); }; diff --git a/ortools/routing/routing.h b/ortools/routing/routing.h index 516ff06ed1e..53b6735539f 100644 --- a/ortools/routing/routing.h +++ b/ortools/routing/routing.h @@ -1589,6 +1589,7 @@ class RoutingModel { struct NodeNeighborsParameters { int num_neighbors; bool add_vehicle_starts_to_neighbors = true; + bool add_vehicle_ends_to_neighbors = false; // In NodeNeighborsByCostClass, neighbors for each node are sorted by // increasing "cost" for each node. The following parameter determines if // neighbors are sorted based on distance only when the neighborhood is @@ -1600,6 +1601,8 @@ class RoutingModel { return num_neighbors == other.num_neighbors && add_vehicle_starts_to_neighbors == other.add_vehicle_starts_to_neighbors && + add_vehicle_ends_to_neighbors == + other.add_vehicle_ends_to_neighbors && only_sort_neighbors_for_partial_neighborhoods == other.only_sort_neighbors_for_partial_neighborhoods; } @@ -1607,41 +1610,81 @@ class RoutingModel { friend H AbslHashValue(H h, const NodeNeighborsParameters& params) { return H::combine(std::move(h), params.num_neighbors, params.add_vehicle_starts_to_neighbors, + params.add_vehicle_ends_to_neighbors, params.only_sort_neighbors_for_partial_neighborhoods); } }; class NodeNeighborsByCostClass { public: - NodeNeighborsByCostClass() = default; + explicit NodeNeighborsByCostClass(const RoutingModel* routing_model) + : routing_model_(*routing_model) {}; /// Computes num_neighbors neighbors of all nodes for every cost class in /// routing_model. - void ComputeNeighbors(const RoutingModel& routing_model, - const NodeNeighborsParameters& params); - /// Returns the neighbors of the given node for the given cost_class. - const std::vector& GetNeighborsOfNodeForCostClass( + void ComputeNeighbors(const NodeNeighborsParameters& params); + /// Returns the incoming neighbors of the given node for the given + /// cost_class, i.e. all 'neighbor' indices such that neighbor -> node_index + /// is a neighborhood arc for 'cost_class'. + const std::vector& GetIncomingNeighborsOfNodeForCostClass( int cost_class, int node_index) const { - return node_index_to_neighbors_by_cost_class_.empty() - ? all_nodes_ - : node_index_to_neighbors_by_cost_class_[cost_class] - [node_index]; + if (routing_model_.IsStart(node_index)) return empty_neighbors_; + + if (node_index_to_incoming_neighbors_by_cost_class_.empty()) { + return all_incoming_nodes_; + } + const std::vector>& node_index_to_incoming_neighbors = + node_index_to_incoming_neighbors_by_cost_class_[cost_class]; + if (node_index_to_incoming_neighbors.empty()) { + return empty_neighbors_; + } + return node_index_to_incoming_neighbors[node_index]; } - /// Returns whether or not node 'neighbor_index' is actually a neighbor of - /// node 'node_index' for the given cost_class. - bool IsNeighborOfNodeForCostClass(int cost_class, int node_index, - int neighbor_index) const { - return node_index_to_neighbor_indicator_by_cost_class_.empty() - ? true - : node_index_to_neighbor_indicator_by_cost_class_ - [cost_class][node_index][neighbor_index]; + + /// Returns the neighbors that are outgoing from 'node_index', i.e. + /// 'neighbor' indices such that node_index -> neighbor is a neighborhood + /// arc for 'cost_class'. + const std::vector& GetOutgoingNeighborsOfNodeForCostClass( + int cost_class, int node_index) const { + if (routing_model_.IsEnd(node_index)) return empty_neighbors_; + + if (node_index_to_outgoing_neighbors_by_cost_class_.empty()) { + return all_outgoing_nodes_; + } + const std::vector>& node_index_to_outgoing_neighbors = + node_index_to_outgoing_neighbors_by_cost_class_[cost_class]; + if (node_index_to_outgoing_neighbors.empty()) { + return empty_neighbors_; + } + return node_index_to_outgoing_neighbors[node_index]; + } + /// Returns true iff arc from_node -> to_node is a neighborhood arc for the + /// given cost_class, i.e. iff arc.to_node is an outgoing neighbor of + /// arc.from_node for 'cost_class'. + bool IsNeighborhoodArcForCostClass(int cost_class, int64_t from, + int64_t to) const { + if (node_index_to_outgoing_neighbor_indicator_by_cost_class_.empty()) { + return true; + } + if (routing_model_.IsEnd(from)) { + return false; + } + return node_index_to_outgoing_neighbor_indicator_by_cost_class_ + [cost_class][from][to]; } private: + const RoutingModel& routing_model_; + static constexpr std::vector empty_neighbors_ = {}; + + std::vector>> + node_index_to_incoming_neighbors_by_cost_class_; std::vector>> - node_index_to_neighbors_by_cost_class_; + node_index_to_outgoing_neighbors_by_cost_class_; std::vector>> - node_index_to_neighbor_indicator_by_cost_class_; - std::vector all_nodes_; + node_index_to_outgoing_neighbor_indicator_by_cost_class_; + + std::vector all_outgoing_nodes_; + std::vector all_incoming_nodes_; }; /// Returns neighbors of all nodes for every cost class. The result is cached @@ -1651,6 +1694,7 @@ class RoutingModel { const NodeNeighborsByCostClass* GetOrCreateNodeNeighborsByCostClass( double neighbors_ratio, int64_t min_neighbors, double& neighbors_ratio_used, bool add_vehicle_starts_to_neighbors = true, + bool add_vehicle_ends_to_neighbors = false, bool only_sort_neighbors_for_partial_neighborhoods = true); /// Returns parameters.num_neighbors neighbors of all nodes for every cost /// class. The result is cached and is computed once. diff --git a/ortools/routing/search.cc b/ortools/routing/search.cc index 3121ae34500..76e9298caf0 100644 --- a/ortools/routing/search.cc +++ b/ortools/routing/search.cc @@ -1632,8 +1632,8 @@ void GlobalCheapestInsertionFilteredHeuristic:: existing_insertion_positions; // Explore the neighborhood of the pickup. for (const int64_t pickup_insert_after : - node_index_to_neighbors_by_cost_class_->GetNeighborsOfNodeForCostClass( - cost_class, pickup)) { + node_index_to_neighbors_by_cost_class_ + ->GetIncomingNeighborsOfNodeForCostClass(cost_class, pickup)) { if (!Contains(pickup_insert_after)) { continue; } @@ -1663,8 +1663,8 @@ void GlobalCheapestInsertionFilteredHeuristic:: // Explore the neighborhood of the delivery. for (const int64_t delivery_insert_after : - node_index_to_neighbors_by_cost_class_->GetNeighborsOfNodeForCostClass( - cost_class, delivery)) { + node_index_to_neighbors_by_cost_class_ + ->GetIncomingNeighborsOfNodeForCostClass(cost_class, delivery)) { if (!Contains(delivery_insert_after)) { continue; } @@ -1796,8 +1796,8 @@ bool GlobalCheapestInsertionFilteredHeuristic::AddPairEntriesWithPickupAfter( model()->GetPickupAndDeliveryPairs(); DCHECK(pickup_to_entries->at(insert_after).empty()); for (const int64_t pickup : - node_index_to_neighbors_by_cost_class_->GetNeighborsOfNodeForCostClass( - cost_class, insert_after)) { + node_index_to_neighbors_by_cost_class_ + ->GetOutgoingNeighborsOfNodeForCostClass(cost_class, insert_after)) { if (StopSearchAndCleanup(priority_queue)) return false; if (Contains(pickup) || !model()->VehicleVar(pickup)->Contains(vehicle)) { continue; @@ -1845,8 +1845,8 @@ bool GlobalCheapestInsertionFilteredHeuristic::AddPairEntriesWithDeliveryAfter( const std::vector& pickup_delivery_pairs = model()->GetPickupAndDeliveryPairs(); for (const int64_t delivery : - node_index_to_neighbors_by_cost_class_->GetNeighborsOfNodeForCostClass( - cost_class, insert_after)) { + node_index_to_neighbors_by_cost_class_ + ->GetOutgoingNeighborsOfNodeForCostClass(cost_class, insert_after)) { if (StopSearchAndCleanup(priority_queue)) return false; if (Contains(delivery) || !model()->VehicleVar(delivery)->Contains(vehicle)) { @@ -2043,8 +2043,8 @@ void GlobalCheapestInsertionFilteredHeuristic:: return; } - // We're only considering the closest neighbors as insertion positions for - // the node. + // We're only considering the closest incoming neighbors as insertion + // positions for the node. const auto insert_on_vehicle_for_cost_class = [this, &vehicles, all_vehicles]( int v, int cost_class) { return (model()->GetCostClassIndexOfVehicle(v).value() == cost_class) && @@ -2053,8 +2053,8 @@ void GlobalCheapestInsertionFilteredHeuristic:: for (int cost_class = 0; cost_class < model()->GetCostClassesCount(); cost_class++) { for (const int64_t insert_after : - node_index_to_neighbors_by_cost_class_->GetNeighborsOfNodeForCostClass( - cost_class, node)) { + node_index_to_neighbors_by_cost_class_ + ->GetIncomingNeighborsOfNodeForCostClass(cost_class, node)) { if (!Contains(insert_after)) { continue; } @@ -2080,6 +2080,13 @@ bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterNodeInsertion( return false; } // Add new entries after "node" which has just been inserted. + // TODO(user): Also add node entries *before* the newly inserted node + // in the case where we have non-full neighborhoods. This will leverage the + // incoming neighbors of the newly inserted node, in case they're not also + // outgoing neighbors of 'insert_after'. + // NOTE: UpdateExistingNodeEntriesOnChain() could return the set of node + // indices that already have entries between insert_after and node, to avoid + // adding entries again for them when looking at incoming neighbors of node. if (!AddNodeEntriesAfter(nodes, vehicle, node, all_vehicles, queue)) { return false; } @@ -2107,8 +2114,8 @@ bool GlobalCheapestInsertionFilteredHeuristic::AddNodeEntriesAfter( // entries or if unperformed node insertions were present. queue->ClearInsertions(insert_after); const std::vector& neighbors = - node_index_to_neighbors_by_cost_class_->GetNeighborsOfNodeForCostClass( - cost_class, insert_after); + node_index_to_neighbors_by_cost_class_ + ->GetOutgoingNeighborsOfNodeForCostClass(cost_class, insert_after); if (neighbors.size() < nodes.NumberOfSetCallsWithDifferentArguments()) { // Iterate on the neighbors. for (int node : neighbors) { @@ -2122,7 +2129,7 @@ bool GlobalCheapestInsertionFilteredHeuristic::AddNodeEntriesAfter( for (int node : nodes.PositionsSetAtLeastOnce()) { if (StopSearch()) return false; if (!Contains(node) && - node_index_to_neighbors_by_cost_class_->IsNeighborOfNodeForCostClass( + node_index_to_neighbors_by_cost_class_->IsNeighborhoodArcForCostClass( cost_class, insert_after, node)) { AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue); }