5788 lines
228 KiB
C++
5788 lines
228 KiB
C++
// Copyright 2010-2025 Google LLC
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
// Implementation of all classes related to routing and search.
|
|
// This includes decision builders, local search neighborhood operators
|
|
// and local search filters.
|
|
// TODO(user): Move all existing routing search code here.
|
|
|
|
#include "ortools/constraint_solver/routing_search.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdint>
|
|
#include <cstdlib>
|
|
#include <deque>
|
|
#include <functional>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <memory>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "absl/algorithm/container.h"
|
|
#include "absl/base/attributes.h"
|
|
#include "absl/container/btree_set.h"
|
|
#include "absl/container/flat_hash_map.h"
|
|
#include "absl/container/flat_hash_set.h"
|
|
#include "absl/container/inlined_vector.h"
|
|
#include "absl/flags/flag.h"
|
|
#include "absl/log/check.h"
|
|
#include "absl/log/die_if_null.h"
|
|
#include "absl/log/log.h"
|
|
#include "absl/strings/str_cat.h"
|
|
#include "absl/strings/string_view.h"
|
|
#include "absl/time/time.h"
|
|
#include "absl/types/span.h"
|
|
#include "ortools/base/adjustable_priority_queue.h"
|
|
#include "ortools/base/logging.h"
|
|
#include "ortools/base/map_util.h"
|
|
#include "ortools/base/mathutil.h"
|
|
#include "ortools/base/protoutil.h"
|
|
#include "ortools/base/stl_util.h"
|
|
#include "ortools/base/types.h"
|
|
#include "ortools/constraint_solver/constraint_solver.h"
|
|
#include "ortools/constraint_solver/constraint_solveri.h"
|
|
#include "ortools/graph/christofides.h"
|
|
#include "ortools/constraint_solver/routing.h"
|
|
#include "ortools/constraint_solver/routing_enums.pb.h"
|
|
#include "ortools/constraint_solver/routing_heuristic_parameters.pb.h"
|
|
#include "ortools/constraint_solver/routing_parameters.pb.h"
|
|
#include "ortools/constraint_solver/routing_parameters_utils.h"
|
|
#include "ortools/constraint_solver/routing_types.h"
|
|
#include "ortools/constraint_solver/routing_utils.h"
|
|
#include "ortools/util/bitset.h"
|
|
#include "ortools/util/range_query_function.h"
|
|
#include "ortools/util/saturated_arithmetic.h"
|
|
|
|
namespace operations_research {
|
|
class LocalSearchPhaseParameters;
|
|
} // namespace operations_research
|
|
|
|
ABSL_FLAG(bool, routing_shift_insertion_cost_by_penalty, true,
|
|
"Shift insertion costs by the penalty of the inserted node(s).");
|
|
|
|
ABSL_FLAG(int64_t, sweep_sectors, 1,
|
|
"The number of sectors the space is divided into before it is sweeped"
|
|
" by the ray.");
|
|
|
|
namespace operations_research {
|
|
|
|
namespace {
|
|
void ConvertAssignment(const RoutingModel* src_model, const Assignment* src,
|
|
const RoutingModel* dst_model, Assignment* dst) {
|
|
DCHECK_EQ(src_model->Nexts().size(), dst_model->Nexts().size());
|
|
DCHECK_NE(src, nullptr);
|
|
DCHECK_EQ(src_model->solver(), src->solver());
|
|
DCHECK_EQ(dst_model->solver(), dst->solver());
|
|
for (int i = 0; i < src_model->Nexts().size(); i++) {
|
|
IntVar* const dst_next_var = dst_model->NextVar(i);
|
|
if (!dst->Contains(dst_next_var)) {
|
|
dst->Add(dst_next_var);
|
|
}
|
|
dst->SetValue(dst_next_var, src->Value(src_model->NextVar(i)));
|
|
}
|
|
}
|
|
|
|
bool AreAssignmentsEquivalent(const RoutingModel& model,
|
|
const Assignment& assignment1,
|
|
const Assignment& assignment2) {
|
|
for (IntVar* next_var : model.Nexts()) {
|
|
if (assignment1.Value(next_var) != assignment2.Value(next_var)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
absl::Duration GetTimeLimit(const RoutingSearchParameters& search_parameters) {
|
|
return search_parameters.has_time_limit()
|
|
? util_time::DecodeGoogleApiProto(search_parameters.time_limit())
|
|
.value()
|
|
: absl::InfiniteDuration();
|
|
}
|
|
|
|
// TODO(user): Refactor this function with other versions living in
|
|
// routing.cc.
|
|
// TODO(user): Should we update solution limits as well?
|
|
bool UpdateTimeLimits(Solver* solver, int64_t start_time_ms,
|
|
absl::Duration time_limit,
|
|
RoutingSearchParameters& search_parameters) {
|
|
if (!search_parameters.has_time_limit()) return true;
|
|
const absl::Duration elapsed_time =
|
|
absl::Milliseconds(solver->wall_time() - start_time_ms);
|
|
const absl::Duration time_left =
|
|
std::min(time_limit - elapsed_time, GetTimeLimit(search_parameters));
|
|
if (time_left <= absl::ZeroDuration()) return false;
|
|
const absl::Status status = util_time::EncodeGoogleApiProto(
|
|
time_left, search_parameters.mutable_time_limit());
|
|
DCHECK_OK(status);
|
|
return true;
|
|
}
|
|
} // namespace
|
|
|
|
const Assignment* SolveWithAlternativeSolvers(
|
|
RoutingModel* primary_model,
|
|
const std::vector<RoutingModel*>& alternative_models,
|
|
const RoutingSearchParameters& parameters,
|
|
int max_non_improving_iterations) {
|
|
return SolveFromAssignmentWithAlternativeSolvers(
|
|
/*assignment=*/nullptr, primary_model, alternative_models, parameters,
|
|
max_non_improving_iterations);
|
|
}
|
|
|
|
const Assignment* SolveFromAssignmentWithAlternativeSolvers(
|
|
const Assignment* assignment, RoutingModel* primary_model,
|
|
const std::vector<RoutingModel*>& alternative_models,
|
|
const RoutingSearchParameters& parameters,
|
|
int max_non_improving_iterations) {
|
|
return SolveFromAssignmentWithAlternativeSolversAndParameters(
|
|
assignment, primary_model, parameters, alternative_models, {},
|
|
max_non_improving_iterations);
|
|
}
|
|
|
|
const Assignment* SolveFromAssignmentWithAlternativeSolversAndParameters(
|
|
const Assignment* assignment, RoutingModel* primary_model,
|
|
const RoutingSearchParameters& primary_parameters,
|
|
const std::vector<RoutingModel*>& alternative_models,
|
|
const std::vector<RoutingSearchParameters>& alternative_parameters,
|
|
int max_non_improving_iterations) {
|
|
const int64_t start_time_ms = primary_model->solver()->wall_time();
|
|
if (max_non_improving_iterations < 0) return nullptr;
|
|
// Shortcut if no alternative models will be explored.
|
|
if (max_non_improving_iterations == 0 || alternative_models.empty()) {
|
|
return primary_model->SolveFromAssignmentWithParameters(assignment,
|
|
primary_parameters);
|
|
}
|
|
RoutingSearchParameters mutable_search_parameters = primary_parameters;
|
|
// The first alternating phases are limited to greedy descent. The final pass
|
|
// at the end of this function will actually use the metaheuristic if needed.
|
|
// TODO(user): Add support for multiple metaheuristics.
|
|
const LocalSearchMetaheuristic_Value metaheuristic =
|
|
mutable_search_parameters.local_search_metaheuristic();
|
|
const bool run_metaheuristic_phase =
|
|
metaheuristic != LocalSearchMetaheuristic::GREEDY_DESCENT &&
|
|
metaheuristic != LocalSearchMetaheuristic::AUTOMATIC;
|
|
if (run_metaheuristic_phase) {
|
|
mutable_search_parameters.set_local_search_metaheuristic(
|
|
LocalSearchMetaheuristic::GREEDY_DESCENT);
|
|
}
|
|
std::vector<RoutingSearchParameters> mutable_alternative_parameters =
|
|
alternative_parameters;
|
|
DCHECK(mutable_alternative_parameters.empty() ||
|
|
mutable_alternative_parameters.size() == alternative_models.size());
|
|
for (RoutingSearchParameters& mutable_alternative_parameter :
|
|
mutable_alternative_parameters) {
|
|
if (!mutable_alternative_parameter.has_time_limit() &&
|
|
mutable_search_parameters.has_time_limit()) {
|
|
*mutable_alternative_parameter.mutable_time_limit() =
|
|
mutable_search_parameters.time_limit();
|
|
}
|
|
}
|
|
const absl::Duration time_limit = GetTimeLimit(mutable_search_parameters);
|
|
Assignment* best_assignment = primary_model->solver()->MakeAssignment();
|
|
std::vector<Assignment*> first_solutions;
|
|
first_solutions.reserve(alternative_models.size());
|
|
for (RoutingModel* model : alternative_models) {
|
|
first_solutions.push_back(model->solver()->MakeAssignment());
|
|
}
|
|
Assignment* primary_first_solution =
|
|
primary_model->solver()->MakeAssignment();
|
|
Assignment* current_solution = primary_model->solver()->MakeAssignment();
|
|
DCHECK(assignment == nullptr ||
|
|
assignment->solver() == primary_model->solver());
|
|
const Assignment* solution = primary_model->SolveFromAssignmentWithParameters(
|
|
assignment, mutable_search_parameters);
|
|
if (solution == nullptr) return nullptr;
|
|
best_assignment->Copy(solution);
|
|
int iteration = 0;
|
|
while (iteration < max_non_improving_iterations) {
|
|
if (best_assignment->ObjectiveValue() > solution->ObjectiveValue()) {
|
|
best_assignment->Copy(solution);
|
|
}
|
|
current_solution->Copy(solution);
|
|
for (int i = 0; i < alternative_models.size(); ++i) {
|
|
RoutingSearchParameters& mutable_alternative_parameter =
|
|
mutable_alternative_parameters.empty()
|
|
? mutable_search_parameters
|
|
: mutable_alternative_parameters[i];
|
|
if (!UpdateTimeLimits(primary_model->solver(), start_time_ms, time_limit,
|
|
mutable_alternative_parameter)) {
|
|
return best_assignment;
|
|
}
|
|
ConvertAssignment(primary_model, solution, alternative_models[i],
|
|
first_solutions[i]);
|
|
solution = alternative_models[i]->SolveFromAssignmentWithParameters(
|
|
first_solutions[i], mutable_alternative_parameter);
|
|
if (solution == nullptr) {
|
|
solution = first_solutions[i];
|
|
}
|
|
if (!UpdateTimeLimits(primary_model->solver(), start_time_ms, time_limit,
|
|
mutable_search_parameters)) {
|
|
return best_assignment;
|
|
}
|
|
ConvertAssignment(alternative_models[i], solution, primary_model,
|
|
primary_first_solution);
|
|
solution = primary_model->SolveFromAssignmentWithParameters(
|
|
primary_first_solution, mutable_search_parameters);
|
|
if (solution == nullptr) return best_assignment;
|
|
}
|
|
// No modifications done in this iteration, no need to continue.
|
|
if (AreAssignmentsEquivalent(*primary_model, *solution,
|
|
*current_solution)) {
|
|
break;
|
|
}
|
|
// We're back to the best assignment which means we will cycle if we
|
|
// continue the search.
|
|
if (AreAssignmentsEquivalent(*primary_model, *solution, *best_assignment)) {
|
|
break;
|
|
}
|
|
if (solution->ObjectiveValue() >= best_assignment->ObjectiveValue()) {
|
|
++iteration;
|
|
}
|
|
}
|
|
if (run_metaheuristic_phase &&
|
|
UpdateTimeLimits(primary_model->solver(), start_time_ms, time_limit,
|
|
mutable_search_parameters)) {
|
|
mutable_search_parameters.set_local_search_metaheuristic(metaheuristic);
|
|
return primary_model->SolveFromAssignmentWithParameters(
|
|
best_assignment, mutable_search_parameters);
|
|
}
|
|
return best_assignment;
|
|
}
|
|
|
|
// --- VehicleTypeCurator ---
|
|
|
|
void VehicleTypeCurator::Reset(const std::function<bool(int)>& store_vehicle) {
|
|
const std::vector<std::set<VehicleClassEntry>>& all_vehicle_classes_per_type =
|
|
vehicle_type_container_->sorted_vehicle_classes_per_type;
|
|
sorted_vehicle_classes_per_type_.resize(all_vehicle_classes_per_type.size());
|
|
const std::vector<std::deque<int>>& all_vehicles_per_class =
|
|
vehicle_type_container_->vehicles_per_vehicle_class;
|
|
vehicles_per_vehicle_class_.resize(all_vehicles_per_class.size());
|
|
|
|
for (int type = 0; type < all_vehicle_classes_per_type.size(); type++) {
|
|
std::set<VehicleClassEntry>& stored_class_entries =
|
|
sorted_vehicle_classes_per_type_[type];
|
|
stored_class_entries.clear();
|
|
for (VehicleClassEntry class_entry : all_vehicle_classes_per_type[type]) {
|
|
const int vehicle_class = class_entry.vehicle_class;
|
|
std::vector<int>& stored_vehicles =
|
|
vehicles_per_vehicle_class_[vehicle_class];
|
|
stored_vehicles.clear();
|
|
for (int vehicle : all_vehicles_per_class[vehicle_class]) {
|
|
if (store_vehicle(vehicle)) {
|
|
stored_vehicles.push_back(vehicle);
|
|
}
|
|
}
|
|
if (!stored_vehicles.empty()) {
|
|
stored_class_entries.insert(class_entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void VehicleTypeCurator::Update(
|
|
const std::function<bool(int)>& remove_vehicle) {
|
|
for (std::set<VehicleClassEntry>& class_entries :
|
|
sorted_vehicle_classes_per_type_) {
|
|
auto class_entry_it = class_entries.begin();
|
|
while (class_entry_it != class_entries.end()) {
|
|
const int vehicle_class = class_entry_it->vehicle_class;
|
|
std::vector<int>& vehicles = vehicles_per_vehicle_class_[vehicle_class];
|
|
vehicles.erase(std::remove_if(vehicles.begin(), vehicles.end(),
|
|
[&remove_vehicle](int vehicle) {
|
|
return remove_vehicle(vehicle);
|
|
}),
|
|
vehicles.end());
|
|
if (vehicles.empty()) {
|
|
class_entry_it = class_entries.erase(class_entry_it);
|
|
} else {
|
|
class_entry_it++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool VehicleTypeCurator::HasCompatibleVehicleOfType(
|
|
int type, const std::function<bool(int)>& vehicle_is_compatible) const {
|
|
for (const VehicleClassEntry& vehicle_class_entry :
|
|
sorted_vehicle_classes_per_type_[type]) {
|
|
for (int vehicle :
|
|
vehicles_per_vehicle_class_[vehicle_class_entry.vehicle_class]) {
|
|
if (vehicle_is_compatible(vehicle)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::pair<int, int> VehicleTypeCurator::GetCompatibleVehicleOfType(
|
|
int type, const std::function<bool(int)>& vehicle_is_compatible,
|
|
const std::function<bool(int)>& stop_and_return_vehicle) {
|
|
std::set<VehicleTypeCurator::VehicleClassEntry>& sorted_classes =
|
|
sorted_vehicle_classes_per_type_[type];
|
|
auto vehicle_class_it = sorted_classes.begin();
|
|
|
|
while (vehicle_class_it != sorted_classes.end()) {
|
|
const int vehicle_class = vehicle_class_it->vehicle_class;
|
|
std::vector<int>& vehicles = vehicles_per_vehicle_class_[vehicle_class];
|
|
DCHECK(!vehicles.empty());
|
|
|
|
for (auto vehicle_it = vehicles.begin(); vehicle_it != vehicles.end();
|
|
vehicle_it++) {
|
|
const int vehicle = *vehicle_it;
|
|
if (vehicle_is_compatible(vehicle)) {
|
|
vehicles.erase(vehicle_it);
|
|
if (vehicles.empty()) {
|
|
sorted_classes.erase(vehicle_class_it);
|
|
}
|
|
return {vehicle, -1};
|
|
}
|
|
if (stop_and_return_vehicle(vehicle)) {
|
|
return {-1, vehicle};
|
|
}
|
|
}
|
|
// If no compatible vehicle was found in this class, move on to the next
|
|
// vehicle class.
|
|
vehicle_class_it++;
|
|
}
|
|
// No compatible vehicle of the given type was found and the stopping
|
|
// condition wasn't met.
|
|
return {-1, -1};
|
|
}
|
|
|
|
// - Models with pickup/deliveries or node precedences are best handled by
|
|
// PARALLEL_CHEAPEST_INSERTION.
|
|
// - As of January 2018, models with single nodes and at least one node with
|
|
// only one allowed vehicle are better solved by PATH_MOST_CONSTRAINED_ARC.
|
|
// - In all other cases, PATH_CHEAPEST_ARC is used.
|
|
// TODO(user): Make this smarter.
|
|
FirstSolutionStrategy::Value AutomaticFirstSolutionStrategy(
|
|
bool has_pickup_deliveries, bool has_node_precedences,
|
|
bool has_single_vehicle_node) {
|
|
if (has_pickup_deliveries || has_node_precedences) {
|
|
return FirstSolutionStrategy::PARALLEL_CHEAPEST_INSERTION;
|
|
}
|
|
if (has_single_vehicle_node) {
|
|
return FirstSolutionStrategy::PATH_MOST_CONSTRAINED_ARC;
|
|
}
|
|
return FirstSolutionStrategy::PATH_CHEAPEST_ARC;
|
|
}
|
|
|
|
std::vector<int64_t> ComputeVehicleEndChainStarts(const RoutingModel& model) {
|
|
const int64_t size = model.Size();
|
|
const int num_vehicles = model.vehicles();
|
|
// Find the chains of nodes (when nodes have their "Next" value bound in the
|
|
// current solution, it forms a link in a chain). Eventually, starts[end]
|
|
// will contain the index of the first node of the chain ending at node 'end'
|
|
// and ends[start] will be the last node of the chain starting at node
|
|
// 'start'. Values of starts[node] and ends[node] for other nodes is used
|
|
// for intermediary computations and do not necessarily reflect actual chain
|
|
// starts and ends.
|
|
std::vector<int64_t> starts(size + num_vehicles, -1);
|
|
std::vector<int64_t> ends(size + num_vehicles, -1);
|
|
for (int node = 0; node < size + num_vehicles; ++node) {
|
|
// Each node starts as a singleton chain.
|
|
starts[node] = node;
|
|
ends[node] = node;
|
|
}
|
|
std::vector<bool> touched(size, false);
|
|
for (int node = 0; node < size; ++node) {
|
|
int current = node;
|
|
while (!model.IsEnd(current) && !touched[current]) {
|
|
touched[current] = true;
|
|
IntVar* const next_var = model.NextVar(current);
|
|
if (next_var->Bound()) {
|
|
current = next_var->Value();
|
|
}
|
|
}
|
|
// Merge the sub-chain starting from 'node' and ending at 'current' with
|
|
// the existing sub-chain starting at 'current'.
|
|
starts[ends[current]] = starts[node];
|
|
ends[starts[node]] = ends[current];
|
|
}
|
|
|
|
// Set the 'end_chain_starts' for every vehicle.
|
|
std::vector<int64_t> end_chain_starts(num_vehicles);
|
|
for (int vehicle = 0; vehicle < num_vehicles; ++vehicle) {
|
|
end_chain_starts[vehicle] = starts[model.End(vehicle)];
|
|
}
|
|
return end_chain_starts;
|
|
}
|
|
|
|
// --- First solution decision builder ---
|
|
|
|
// IntVarFilteredDecisionBuilder
|
|
|
|
IntVarFilteredDecisionBuilder::IntVarFilteredDecisionBuilder(
|
|
std::unique_ptr<IntVarFilteredHeuristic> heuristic)
|
|
: heuristic_(std::move(heuristic)) {}
|
|
|
|
Decision* IntVarFilteredDecisionBuilder::Next(Solver* solver) {
|
|
Assignment* const assignment = heuristic_->BuildSolution();
|
|
if (assignment != nullptr) {
|
|
VLOG(2) << "Number of decisions: " << heuristic_->number_of_decisions();
|
|
VLOG(2) << "Number of rejected decisions: "
|
|
<< heuristic_->number_of_rejects();
|
|
assignment->Restore();
|
|
} else {
|
|
solver->Fail();
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int64_t IntVarFilteredDecisionBuilder::number_of_decisions() const {
|
|
return heuristic_->number_of_decisions();
|
|
}
|
|
|
|
int64_t IntVarFilteredDecisionBuilder::number_of_rejects() const {
|
|
return heuristic_->number_of_rejects();
|
|
}
|
|
|
|
std::string IntVarFilteredDecisionBuilder::DebugString() const {
|
|
return absl::StrCat("IntVarFilteredDecisionBuilder(",
|
|
heuristic_->DebugString(), ")");
|
|
}
|
|
|
|
// --- First solution heuristics ---
|
|
|
|
// IntVarFilteredHeuristic
|
|
|
|
IntVarFilteredHeuristic::IntVarFilteredHeuristic(
|
|
Solver* solver, const std::vector<IntVar*>& vars,
|
|
const std::vector<IntVar*>& secondary_vars,
|
|
LocalSearchFilterManager* filter_manager)
|
|
: assignment_(solver->MakeAssignment()),
|
|
solver_(solver),
|
|
vars_(vars),
|
|
base_vars_size_(vars.size()),
|
|
delta_(solver->MakeAssignment()),
|
|
empty_(solver->MakeAssignment()),
|
|
filter_manager_(filter_manager),
|
|
objective_upper_bound_(std::numeric_limits<int64_t>::max()),
|
|
number_of_decisions_(0),
|
|
number_of_rejects_(0) {
|
|
if (!secondary_vars.empty()) {
|
|
vars_.insert(vars_.end(), secondary_vars.begin(), secondary_vars.end());
|
|
}
|
|
assignment_->MutableIntVarContainer()->Resize(vars_.size());
|
|
is_in_delta_.resize(vars_.size(), false);
|
|
delta_indices_.reserve(vars_.size());
|
|
}
|
|
|
|
void IntVarFilteredHeuristic::ResetSolution() {
|
|
number_of_decisions_ = 0;
|
|
number_of_rejects_ = 0;
|
|
// Wiping assignment when starting a new search.
|
|
assignment_->MutableIntVarContainer()->Clear();
|
|
delta_->MutableIntVarContainer()->Clear();
|
|
SynchronizeFilters();
|
|
assignment_->MutableIntVarContainer()->Resize(vars_.size());
|
|
}
|
|
|
|
Assignment* IntVarFilteredHeuristic::BuildSolution() {
|
|
// Initialize must be called before the state of the heuristic is changed, in
|
|
// particular before InitializeSolution() and BuildSolutionInternal().
|
|
Initialize();
|
|
if (!InitializeSolution()) {
|
|
return nullptr;
|
|
}
|
|
if (BuildSolutionInternal()) {
|
|
return assignment_;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Assignment* RoutingFilteredHeuristic::BuildSolutionFromRoutes(
|
|
const std::function<int64_t(int64_t)>& next_accessor) {
|
|
// Initialize must be called before the state of the heuristic is changed, in
|
|
// particular before InitializeSolution() and BuildSolutionInternal().
|
|
Initialize();
|
|
// NOTE(b/219043402): The filter manager must first be synchronized with a
|
|
// valid solution that properly connects route starts to route ends in order
|
|
// for future FilterAccept() calls to correctly detect infeasibilities.
|
|
if (!InitializeSolution()) {
|
|
return nullptr;
|
|
}
|
|
|
|
for (int v = 0; v < model_->vehicles(); v++) {
|
|
int64_t node = model_->Start(v);
|
|
while (!model_->IsEnd(node)) {
|
|
const int64_t next = next_accessor(node);
|
|
DCHECK_NE(next, node);
|
|
SetNext(node, next, v);
|
|
SetVehicleIndex(node, v);
|
|
node = next;
|
|
}
|
|
}
|
|
if (!Evaluate(/*commit=*/true).has_value()) {
|
|
ResetVehicleIndices();
|
|
return nullptr;
|
|
}
|
|
if (BuildSolutionInternal()) {
|
|
return assignment_;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
std::optional<int64_t> IntVarFilteredHeuristic::Evaluate(
|
|
bool commit, bool ignore_upper_bound, bool update_upper_bound) {
|
|
++number_of_decisions_;
|
|
const bool accept = FilterAccept(ignore_upper_bound);
|
|
int64_t objective_upper_bound = objective_upper_bound_;
|
|
if (accept) {
|
|
if (filter_manager_ != nullptr) {
|
|
// objective upper_bound_ is used to reduce the number of potential
|
|
// insertion candidates, specifically when filter_manager_ filters cost.
|
|
// Rationale: the best cost candidate will always be valid and will be
|
|
// inserted so no use accepting degrading ones. However when a candidate
|
|
// is committed, the upper bound is relaxed to make sure further
|
|
// (cost-degrading) insertions will be accepted
|
|
// (cf. SynchronizeFilters()).
|
|
DCHECK(ignore_upper_bound ||
|
|
filter_manager_->GetAcceptedObjectiveValue() <=
|
|
objective_upper_bound_);
|
|
objective_upper_bound = filter_manager_->GetAcceptedObjectiveValue();
|
|
if (update_upper_bound) {
|
|
objective_upper_bound_ = objective_upper_bound;
|
|
}
|
|
}
|
|
if (commit) {
|
|
const Assignment::IntContainer& delta_container =
|
|
delta_->IntVarContainer();
|
|
const int delta_size = delta_container.Size();
|
|
Assignment::IntContainer* const container =
|
|
assignment_->MutableIntVarContainer();
|
|
for (int i = 0; i < delta_size; ++i) {
|
|
const IntVarElement& delta_element = delta_container.Element(i);
|
|
IntVar* const var = delta_element.Var();
|
|
DCHECK_EQ(var, vars_[delta_indices_[i]]);
|
|
container->AddAtPosition(var, delta_indices_[i])
|
|
->SetValue(delta_element.Value());
|
|
}
|
|
SynchronizeFilters();
|
|
}
|
|
} else {
|
|
++number_of_rejects_;
|
|
}
|
|
// Reset is_in_delta to all false.
|
|
for (const int delta_index : delta_indices_) {
|
|
is_in_delta_[delta_index] = false;
|
|
}
|
|
delta_->Clear();
|
|
delta_indices_.clear();
|
|
return accept ? std::optional<int64_t>{objective_upper_bound} : std::nullopt;
|
|
}
|
|
|
|
void IntVarFilteredHeuristic::SynchronizeFilters() {
|
|
if (filter_manager_) filter_manager_->Synchronize(assignment_, delta_);
|
|
// Resetting the upper bound to allow cost-increasing insertions.
|
|
objective_upper_bound_ = std::numeric_limits<int64_t>::max();
|
|
}
|
|
|
|
bool IntVarFilteredHeuristic::FilterAccept(bool ignore_upper_bound) {
|
|
if (!filter_manager_) return true;
|
|
LocalSearchMonitor* const monitor = solver_->GetLocalSearchMonitor();
|
|
return filter_manager_->Accept(
|
|
monitor, delta_, empty_, std::numeric_limits<int64_t>::min(),
|
|
ignore_upper_bound ? std::numeric_limits<int64_t>::max()
|
|
: objective_upper_bound_);
|
|
}
|
|
|
|
// RoutingFilteredHeuristic
|
|
|
|
RoutingFilteredHeuristic::RoutingFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
LocalSearchFilterManager* filter_manager)
|
|
: IntVarFilteredHeuristic(model->solver(), model->Nexts(),
|
|
model->CostsAreHomogeneousAcrossVehicles()
|
|
? std::vector<IntVar*>()
|
|
: model->VehicleVars(),
|
|
filter_manager),
|
|
model_(model),
|
|
stop_search_(std::move(stop_search)) {}
|
|
|
|
bool RoutingFilteredHeuristic::InitializeSolution() {
|
|
ResetSolution();
|
|
ResetVehicleIndices();
|
|
|
|
// Start by adding partial start chains to current assignment.
|
|
start_chain_ends_.resize(model()->vehicles());
|
|
for (int vehicle = 0; vehicle < model()->vehicles(); ++vehicle) {
|
|
int64_t node = model()->Start(vehicle);
|
|
while (!model()->IsEnd(node) && Var(node)->Bound()) {
|
|
const int64_t next = Var(node)->Min();
|
|
SetNext(node, next, vehicle);
|
|
SetVehicleIndex(node, vehicle);
|
|
node = next;
|
|
}
|
|
start_chain_ends_[vehicle] = node;
|
|
}
|
|
|
|
end_chain_starts_ = ComputeVehicleEndChainStarts(*model_);
|
|
|
|
// Set each route to be the concatenation of the chain at its start and the
|
|
// chain at its end, without nodes in between.
|
|
for (int vehicle = 0; vehicle < model()->vehicles(); ++vehicle) {
|
|
int64_t node = start_chain_ends_[vehicle];
|
|
if (!model()->IsEnd(node)) {
|
|
int64_t next = end_chain_starts_[vehicle];
|
|
SetNext(node, next, vehicle);
|
|
SetVehicleIndex(node, vehicle);
|
|
node = next;
|
|
while (!model()->IsEnd(node)) {
|
|
next = Var(node)->Min();
|
|
SetNext(node, next, vehicle);
|
|
SetVehicleIndex(node, vehicle);
|
|
node = next;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Evaluate(/*commit=*/true).has_value()) {
|
|
ResetVehicleIndices();
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RoutingFilteredHeuristic::MakeDisjunctionNodesUnperformed(int64_t node) {
|
|
model()->ForEachNodeInDisjunctionWithMaxCardinalityFromIndex(
|
|
node, 1, [this, node](int alternate) {
|
|
if (node != alternate && !Contains(alternate)) {
|
|
SetNext(alternate, alternate, -1);
|
|
}
|
|
});
|
|
}
|
|
|
|
void RoutingFilteredHeuristic::AddUnassignedNodesToEmptyVehicles() {
|
|
// TODO(user): check that delta_ is empty.
|
|
SynchronizeFilters();
|
|
absl::btree_set<std::pair<int64_t, int>> empty_vehicles;
|
|
for (int vehicle = 0; vehicle < model_->vehicles(); ++vehicle) {
|
|
if (VehicleIsEmpty(vehicle)) {
|
|
empty_vehicles.insert({model()->GetFixedCostOfVehicle(vehicle), vehicle});
|
|
}
|
|
}
|
|
for (int index = 0; index < model_->Size(); ++index) {
|
|
if (StopSearch() || empty_vehicles.empty()) return;
|
|
DCHECK(!IsSecondaryVar(index));
|
|
if (Contains(index)) continue;
|
|
for (auto [cost, vehicle] : empty_vehicles) {
|
|
SetNext(model_->Start(vehicle), index, vehicle);
|
|
SetNext(index, model_->End(vehicle), vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
empty_vehicles.erase({cost, vehicle});
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool RoutingFilteredHeuristic::MakeUnassignedNodesUnperformed() {
|
|
// TODO(user): check that delta_ is empty.
|
|
SynchronizeFilters();
|
|
for (int index = 0; index < model_->Size(); ++index) {
|
|
DCHECK(!IsSecondaryVar(index));
|
|
if (!Contains(index)) {
|
|
SetNext(index, index, -1);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void RoutingFilteredHeuristic::MakePartiallyPerformedPairsUnperformed() {
|
|
const int num_nexts = model()->Nexts().size();
|
|
std::vector<bool> to_make_unperformed(num_nexts, false);
|
|
for (const auto& [pickups, deliveries] :
|
|
model()->GetPickupAndDeliveryPairs()) {
|
|
int64_t performed_pickup = -1;
|
|
for (int64_t pickup : pickups) {
|
|
if (Contains(pickup) && Value(pickup) != pickup) {
|
|
performed_pickup = pickup;
|
|
break;
|
|
}
|
|
}
|
|
int64_t performed_delivery = -1;
|
|
for (int64_t delivery : deliveries) {
|
|
if (Contains(delivery) && Value(delivery) != delivery) {
|
|
performed_delivery = delivery;
|
|
break;
|
|
}
|
|
}
|
|
if ((performed_pickup == -1) != (performed_delivery == -1)) {
|
|
if (performed_pickup != -1) {
|
|
to_make_unperformed[performed_pickup] = true;
|
|
}
|
|
if (performed_delivery != -1) {
|
|
to_make_unperformed[performed_delivery] = true;
|
|
}
|
|
}
|
|
}
|
|
for (int index = 0; index < num_nexts; ++index) {
|
|
if (to_make_unperformed[index] || !Contains(index)) continue;
|
|
const int vehicle =
|
|
HasSecondaryVars() ? Value(SecondaryVarIndex(index)) : 0;
|
|
int64_t next = Value(index);
|
|
while (next < num_nexts && to_make_unperformed[next]) {
|
|
const int64_t next_of_next = Value(next);
|
|
SetNext(index, next_of_next, vehicle);
|
|
SetNext(next, next, -1);
|
|
next = next_of_next;
|
|
}
|
|
}
|
|
}
|
|
|
|
// CheapestInsertionFilteredHeuristic
|
|
|
|
CheapestInsertionFilteredHeuristic::CheapestInsertionFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
std::function<int64_t(int64_t, int64_t, int64_t)> evaluator,
|
|
std::function<int64_t(int64_t)> penalty_evaluator,
|
|
LocalSearchFilterManager* filter_manager)
|
|
: RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager),
|
|
evaluator_(std::move(evaluator)),
|
|
evaluator_cache_(model->Nexts().size()),
|
|
penalty_evaluator_(std::move(penalty_evaluator)) {}
|
|
|
|
namespace {
|
|
void ProcessVehicleStartEndCosts(
|
|
const RoutingModel& model, int node,
|
|
const std::function<void(int64_t, int)>& process_cost,
|
|
const Bitset64<int>& vehicle_set, bool ignore_start = false,
|
|
bool ignore_end = false) {
|
|
const auto start_end_cost = [&model, ignore_start, ignore_end](int64_t node,
|
|
int v) {
|
|
const int64_t start_cost =
|
|
ignore_start ? 0 : model.GetArcCostForVehicle(model.Start(v), node, v);
|
|
const int64_t end_cost =
|
|
ignore_end ? 0 : model.GetArcCostForVehicle(model.End(v), node, v);
|
|
return CapAdd(start_cost, end_cost);
|
|
};
|
|
|
|
// Iterating over an IntVar domain is faster than calling Contains.
|
|
// Therefore we iterate on 'vehicles' only if it's smaller than the domain
|
|
// size of the VehicleVar.
|
|
const IntVar* const vehicle_var = model.VehicleVar(node);
|
|
if (vehicle_var->Size() < vehicle_set.size()) {
|
|
std::unique_ptr<IntVarIterator> it(vehicle_var->MakeDomainIterator(false));
|
|
for (const int v : InitAndGetValues(it.get())) {
|
|
if (v < 0 || !vehicle_set[v]) {
|
|
continue;
|
|
}
|
|
process_cost(start_end_cost(node, v), v);
|
|
}
|
|
} else {
|
|
for (const int v : vehicle_set) {
|
|
if (!vehicle_var->Contains(v)) continue;
|
|
process_cost(start_end_cost(node, v), v);
|
|
}
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
std::vector<std::vector<CheapestInsertionFilteredHeuristic::StartEndValue>>
|
|
CheapestInsertionFilteredHeuristic::ComputeStartEndDistanceForVehicles(
|
|
absl::Span<const int> vehicles) {
|
|
// TODO(user): consider checking search limits.
|
|
const RoutingModel& model = *this->model();
|
|
std::vector<std::vector<StartEndValue>> start_end_distances_per_node(
|
|
model.Size());
|
|
|
|
Bitset64<int> vehicle_set(model.vehicles());
|
|
for (int v : vehicles) vehicle_set.Set(v);
|
|
|
|
for (int node = 0; node < model.Size(); node++) {
|
|
if (Contains(node)) continue;
|
|
std::vector<StartEndValue>& start_end_distances =
|
|
start_end_distances_per_node[node];
|
|
start_end_distances.reserve(
|
|
std::min(model.VehicleVar(node)->Size(),
|
|
static_cast<uint64_t>(vehicles.size())));
|
|
|
|
ProcessVehicleStartEndCosts(
|
|
model, node,
|
|
[&start_end_distances](int64_t dist, int v) {
|
|
start_end_distances.push_back({dist, v});
|
|
},
|
|
vehicle_set);
|
|
|
|
// Sort the distances for the node to all start/ends of available vehicles
|
|
// in decreasing order.
|
|
absl::c_sort(start_end_distances,
|
|
[](const StartEndValue& first, const StartEndValue& second) {
|
|
return second < first;
|
|
});
|
|
}
|
|
return start_end_distances_per_node;
|
|
}
|
|
|
|
void CheapestInsertionFilteredHeuristic::AddSeedNodeToQueue(
|
|
int node, std::vector<StartEndValue>* start_end_distances, SeedQueue* sq) {
|
|
if (start_end_distances->empty()) {
|
|
return;
|
|
}
|
|
|
|
// Put the best StartEndValue for this node in the priority queue.
|
|
StartEndValue& start_end_value = start_end_distances->back();
|
|
if (sq->prioritize_farthest_nodes) {
|
|
start_end_value.distance = CapOpp(start_end_value.distance);
|
|
}
|
|
const int64_t num_allowed_vehicles = model()->VehicleVar(node)->Size();
|
|
const int64_t neg_penalty = CapOpp(model()->UnperformedPenalty(node));
|
|
sq->priority_queue.push({.properties = {num_allowed_vehicles, neg_penalty,
|
|
start_end_value.distance},
|
|
.vehicle = start_end_value.vehicle,
|
|
.is_node_index = true,
|
|
.index = node});
|
|
start_end_distances->pop_back();
|
|
}
|
|
|
|
void CheapestInsertionFilteredHeuristic::InitializeSeedQueue(
|
|
std::vector<std::vector<StartEndValue>>* start_end_distances_per_node,
|
|
SeedQueue* sq) {
|
|
const int num_nodes = model()->Size();
|
|
DCHECK_EQ(start_end_distances_per_node->size(), num_nodes);
|
|
|
|
for (int node = 0; node < num_nodes; node++) {
|
|
if (Contains(node)) continue;
|
|
AddSeedNodeToQueue(node, &start_end_distances_per_node->at(node), sq);
|
|
}
|
|
}
|
|
|
|
void CheapestInsertionFilteredHeuristic::InsertBetween(int64_t node,
|
|
int64_t predecessor,
|
|
int64_t successor,
|
|
int vehicle) {
|
|
SetValue(predecessor, node);
|
|
SetValue(node, successor);
|
|
MakeDisjunctionNodesUnperformed(node);
|
|
if (HasSecondaryVars() && vehicle != -1) {
|
|
SetValue(SecondaryVarIndex(predecessor), vehicle);
|
|
SetValue(SecondaryVarIndex(node), vehicle);
|
|
SetValue(SecondaryVarIndex(successor), vehicle);
|
|
}
|
|
}
|
|
|
|
int64_t
|
|
CheapestInsertionFilteredHeuristic::GetEvaluatorInsertionCostForNodeAtPosition(
|
|
int64_t node_to_insert, int64_t insert_after, int64_t insert_before,
|
|
int vehicle) const {
|
|
DCHECK(evaluator_ != nullptr);
|
|
EvaluatorCache& cache_entry = evaluator_cache_[insert_after];
|
|
if (cache_entry.node != insert_before || cache_entry.vehicle != vehicle) {
|
|
cache_entry = {.value = evaluator_(insert_after, insert_before, vehicle),
|
|
.node = insert_before,
|
|
.vehicle = vehicle};
|
|
}
|
|
return CapSub(CapAdd(evaluator_(insert_after, node_to_insert, vehicle),
|
|
evaluator_(node_to_insert, insert_before, vehicle)),
|
|
cache_entry.value);
|
|
}
|
|
|
|
std::optional<int64_t>
|
|
CheapestInsertionFilteredHeuristic::GetInsertionCostForNodeAtPosition(
|
|
int64_t node_to_insert, int64_t insert_after, int64_t insert_before,
|
|
int vehicle, int hint_weight) {
|
|
if (evaluator_ != nullptr) {
|
|
return GetEvaluatorInsertionCostForNodeAtPosition(
|
|
node_to_insert, insert_after, insert_before, vehicle);
|
|
}
|
|
InsertBetween(node_to_insert, insert_after, insert_before, vehicle);
|
|
return Evaluate(/*commit=*/false, /*ignore_upper_bound=*/hint_weight > 0,
|
|
/*update_upper_bound=*/hint_weight >= 0);
|
|
}
|
|
|
|
std::optional<int64_t>
|
|
CheapestInsertionFilteredHeuristic::GetInsertionCostForPairAtPositions(
|
|
int64_t pickup, int64_t pickup_insert_after, int64_t delivery,
|
|
int64_t delivery_insert_after, int vehicle, int hint_weight) {
|
|
DCHECK_GE(pickup_insert_after, 0);
|
|
const int64_t pickup_insert_before = Value(pickup_insert_after);
|
|
DCHECK_GE(delivery_insert_after, 0);
|
|
const int64_t delivery_insert_before = (delivery_insert_after == pickup)
|
|
? pickup_insert_before
|
|
: Value(delivery_insert_after);
|
|
if (evaluator_ != nullptr) {
|
|
const int64_t pickup_cost = GetEvaluatorInsertionCostForNodeAtPosition(
|
|
pickup, pickup_insert_after, pickup_insert_before, vehicle);
|
|
const int64_t delivery_cost = GetEvaluatorInsertionCostForNodeAtPosition(
|
|
delivery, delivery_insert_after, delivery_insert_before, vehicle);
|
|
return CapAdd(pickup_cost, delivery_cost);
|
|
}
|
|
|
|
InsertBetween(pickup, pickup_insert_after, pickup_insert_before, vehicle);
|
|
InsertBetween(delivery, delivery_insert_after, delivery_insert_before,
|
|
vehicle);
|
|
return Evaluate(/*commit=*/false,
|
|
/*ignore_upper_bound=*/hint_weight > 0,
|
|
/*update_upper_bound=*/hint_weight >= 0);
|
|
}
|
|
|
|
int64_t CheapestInsertionFilteredHeuristic::GetUnperformedValue(
|
|
int64_t node_to_insert) const {
|
|
if (penalty_evaluator_ != nullptr) {
|
|
return penalty_evaluator_(node_to_insert);
|
|
}
|
|
return std::numeric_limits<int64_t>::max();
|
|
}
|
|
|
|
// GlobalCheapestInsertionFilteredHeuristic
|
|
|
|
GlobalCheapestInsertionFilteredHeuristic::
|
|
GlobalCheapestInsertionFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
std::function<int64_t(int64_t, int64_t, int64_t)> evaluator,
|
|
std::function<int64_t(int64_t)> penalty_evaluator,
|
|
LocalSearchFilterManager* filter_manager,
|
|
GlobalCheapestInsertionParameters parameters)
|
|
: CheapestInsertionFilteredHeuristic(
|
|
model, std::move(stop_search), std::move(evaluator),
|
|
std::move(penalty_evaluator), filter_manager),
|
|
gci_params_(parameters),
|
|
node_index_to_vehicle_(model->Size(), -1),
|
|
node_index_to_neighbors_by_cost_class_(nullptr),
|
|
empty_vehicle_type_curator_(nullptr),
|
|
temp_inserted_nodes_(model->Size()) {
|
|
CHECK_GT(gci_params_.neighbors_ratio, 0);
|
|
CHECK_LE(gci_params_.neighbors_ratio, 1);
|
|
CHECK_GE(gci_params_.min_neighbors, 1);
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::CheckVehicleIndices() const {
|
|
std::vector<bool> node_is_visited(model()->Size(), false);
|
|
for (int v = 0; v < model()->vehicles(); v++) {
|
|
for (int node = model()->Start(v); !model()->IsEnd(node);
|
|
node = Value(node)) {
|
|
if (node_index_to_vehicle_[node] != v) {
|
|
return false;
|
|
}
|
|
node_is_visited[node] = true;
|
|
}
|
|
}
|
|
|
|
for (int node = 0; node < model()->Size(); node++) {
|
|
if (!node_is_visited[node] && node_index_to_vehicle_[node] != -1) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() {
|
|
// Get neighbors.
|
|
double neighbors_ratio_used = 1;
|
|
node_index_to_neighbors_by_cost_class_ =
|
|
model()->GetOrCreateNodeNeighborsByCostClass(gci_params_.neighbors_ratio,
|
|
gci_params_.min_neighbors,
|
|
neighbors_ratio_used);
|
|
if (neighbors_ratio_used == 1) {
|
|
gci_params_.use_neighbors_ratio_for_initialization = false;
|
|
}
|
|
|
|
if (empty_vehicle_type_curator_ == nullptr) {
|
|
empty_vehicle_type_curator_ = std::make_unique<VehicleTypeCurator>(
|
|
model()->GetVehicleTypeContainer());
|
|
}
|
|
// Store all empty vehicles in the empty_vehicle_type_curator_.
|
|
empty_vehicle_type_curator_->Reset(
|
|
[this](int vehicle) { return VehicleIsEmpty(vehicle); });
|
|
// Insert partially inserted pairs.
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
std::map<int64_t, std::vector<int>> pairs_to_insert_by_bucket;
|
|
absl::flat_hash_map<int, std::map<int64_t, std::vector<int>>>
|
|
vehicle_to_pair_nodes;
|
|
for (int index = 0; index < pickup_delivery_pairs.size(); index++) {
|
|
const PickupDeliveryPair& pickup_delivery_pair =
|
|
pickup_delivery_pairs[index];
|
|
const auto& [pickups, deliveries] = pickup_delivery_pair;
|
|
int pickup_vehicle = -1;
|
|
for (int64_t pickup : pickups) {
|
|
if (Contains(pickup)) {
|
|
pickup_vehicle = node_index_to_vehicle_[pickup];
|
|
break;
|
|
}
|
|
}
|
|
int delivery_vehicle = -1;
|
|
for (int64_t delivery : deliveries) {
|
|
if (Contains(delivery)) {
|
|
delivery_vehicle = node_index_to_vehicle_[delivery];
|
|
break;
|
|
}
|
|
}
|
|
if (pickup_vehicle < 0 && delivery_vehicle < 0) {
|
|
pairs_to_insert_by_bucket[GetBucketOfPair(pickup_delivery_pair)]
|
|
.push_back(index);
|
|
}
|
|
if (pickup_vehicle >= 0 && delivery_vehicle < 0) {
|
|
std::vector<int>& pair_nodes = vehicle_to_pair_nodes[pickup_vehicle][1];
|
|
for (int64_t delivery : deliveries) {
|
|
pair_nodes.push_back(delivery);
|
|
}
|
|
}
|
|
if (pickup_vehicle < 0 && delivery_vehicle >= 0) {
|
|
std::vector<int>& pair_nodes = vehicle_to_pair_nodes[delivery_vehicle][1];
|
|
for (int64_t pickup : pickups) {
|
|
pair_nodes.push_back(pickup);
|
|
}
|
|
}
|
|
}
|
|
|
|
const auto unperform_unassigned_and_check = [this]() {
|
|
return MakeUnassignedNodesUnperformed() &&
|
|
Evaluate(/*commit=*/true).has_value();
|
|
};
|
|
for (const auto& [vehicle, nodes] : vehicle_to_pair_nodes) {
|
|
if (!InsertNodesOnRoutes(nodes, {vehicle})) {
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
}
|
|
|
|
if (!InsertPairsAndNodesByRequirementTopologicalOrder()) {
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
if (!InsertPairsAndNodesByPrecedenceTopologicalOrder()) {
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
|
|
// TODO(user): Adapt the pair insertions to also support seed and
|
|
// sequential insertion.
|
|
if (!InsertPairs(pairs_to_insert_by_bucket)) {
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
std::map<int64_t, std::vector<int>> nodes_by_bucket;
|
|
for (int node = 0; node < model()->Size(); ++node) {
|
|
if (!Contains(node) && !model()->IsPickup(node) &&
|
|
!model()->IsDelivery(node)) {
|
|
nodes_by_bucket[GetBucketOfNode(node)].push_back(node);
|
|
}
|
|
}
|
|
InsertFarthestNodesAsSeeds();
|
|
if (gci_params_.is_sequential) {
|
|
if (!SequentialInsertNodes(nodes_by_bucket)) {
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
} else if (!InsertNodesOnRoutes(nodes_by_bucket, {})) {
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
DCHECK(CheckVehicleIndices());
|
|
return unperform_unassigned_and_check();
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::
|
|
InsertPairsAndNodesByRequirementTopologicalOrder() {
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
for (const std::vector<int>& types :
|
|
model()->GetTopologicallySortedVisitTypes()) {
|
|
for (int type : types) {
|
|
std::map<int64_t, std::vector<int>> pairs_to_insert_by_bucket;
|
|
for (int index : model()->GetPairIndicesOfType(type)) {
|
|
pairs_to_insert_by_bucket[GetBucketOfPair(pickup_delivery_pairs[index])]
|
|
.push_back(index);
|
|
}
|
|
if (!InsertPairs(pairs_to_insert_by_bucket)) return false;
|
|
std::map<int64_t, std::vector<int>> nodes_by_bucket;
|
|
for (int node : model()->GetSingleNodesOfType(type)) {
|
|
nodes_by_bucket[GetBucketOfNode(node)].push_back(node);
|
|
}
|
|
if (!InsertNodesOnRoutes(nodes_by_bucket, {})) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::
|
|
InsertPairsAndNodesByPrecedenceTopologicalOrder() {
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
for (const std::vector<std::vector<int>>& ordered_nodes :
|
|
model()->GetTopologicallySortedNodePrecedences()) {
|
|
for (const std::vector<int>& nodes : ordered_nodes) {
|
|
std::map<int64_t, std::vector<int>> pairs_to_insert_by_bucket;
|
|
for (int node : nodes) {
|
|
if (Contains(node)) continue;
|
|
if (!model()->IsPickup(node) && !model()->IsDelivery(node)) continue;
|
|
const std::optional<RoutingModel::PickupDeliveryPosition>
|
|
pickup_position = model()->GetPickupPosition(node);
|
|
if (pickup_position.has_value()) {
|
|
const int index = pickup_position->pd_pair_index;
|
|
pairs_to_insert_by_bucket[GetBucketOfPair(
|
|
pickup_delivery_pairs[index])]
|
|
.push_back(index);
|
|
}
|
|
const std::optional<RoutingModel::PickupDeliveryPosition>
|
|
delivery_position = model()->GetDeliveryPosition(node);
|
|
if (delivery_position.has_value()) {
|
|
const int index = delivery_position->pd_pair_index;
|
|
pairs_to_insert_by_bucket[GetBucketOfPair(
|
|
pickup_delivery_pairs[index])]
|
|
.push_back(index);
|
|
}
|
|
}
|
|
if (!InsertPairs(pairs_to_insert_by_bucket)) return false;
|
|
std::map<int64_t, std::vector<int>> nodes_by_bucket;
|
|
for (int node : nodes) {
|
|
if (Contains(node)) continue;
|
|
nodes_by_bucket[GetBucketOfNode(node)].push_back(node);
|
|
}
|
|
if (!InsertNodesOnRoutes(nodes_by_bucket, {})) return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::InsertPairs(
|
|
const std::map<int64_t, std::vector<int>>& pair_indices_by_bucket) {
|
|
AdjustablePriorityQueue<PairEntry> priority_queue;
|
|
std::vector<PairEntries> pickup_to_entries;
|
|
std::vector<PairEntries> delivery_to_entries;
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
auto pair_is_performed = [this, &pickup_delivery_pairs](int pair_index) {
|
|
const auto& [pickups, deliveries] = pickup_delivery_pairs[pair_index];
|
|
for (int64_t pickup : pickups) {
|
|
if (Contains(pickup)) return true;
|
|
}
|
|
for (int64_t delivery : deliveries) {
|
|
if (Contains(delivery)) return true;
|
|
}
|
|
return false;
|
|
};
|
|
absl::flat_hash_set<int> pair_indices_to_insert;
|
|
for (const auto& [bucket, pair_indices] : pair_indices_by_bucket) {
|
|
for (const int pair_index : pair_indices) {
|
|
if (!pair_is_performed(pair_index)) {
|
|
pair_indices_to_insert.insert(pair_index);
|
|
}
|
|
}
|
|
if (!InitializePairPositions(pair_indices_to_insert, &priority_queue,
|
|
&pickup_to_entries, &delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
while (!priority_queue.IsEmpty()) {
|
|
if (StopSearchAndCleanup(&priority_queue)) {
|
|
return false;
|
|
}
|
|
PairEntry* const entry = priority_queue.Top();
|
|
const int64_t pickup = entry->pickup_to_insert();
|
|
const int64_t delivery = entry->delivery_to_insert();
|
|
if (Contains(pickup) || Contains(delivery)) {
|
|
DeletePairEntry(entry, &priority_queue, &pickup_to_entries,
|
|
&delivery_to_entries);
|
|
continue;
|
|
}
|
|
|
|
const int entry_vehicle = entry->vehicle();
|
|
if (entry_vehicle == -1) {
|
|
// Pair is unperformed.
|
|
SetNext(pickup, pickup, -1);
|
|
SetNext(delivery, delivery, -1);
|
|
if (!Evaluate(/*commit=*/true).has_value()) {
|
|
DeletePairEntry(entry, &priority_queue, &pickup_to_entries,
|
|
&delivery_to_entries);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Pair is performed.
|
|
if (UseEmptyVehicleTypeCuratorForVehicle(entry_vehicle)) {
|
|
if (!InsertPairEntryUsingEmptyVehicleTypeCurator(
|
|
pair_indices_to_insert, entry, &priority_queue,
|
|
&pickup_to_entries, &delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
// The entry corresponded to an insertion on an empty vehicle, which was
|
|
// handled by the call to InsertPairEntryUsingEmptyVehicleTypeCurator().
|
|
continue;
|
|
}
|
|
|
|
const int64_t pickup_insert_after = entry->pickup_insert_after();
|
|
const int64_t pickup_insert_before = Value(pickup_insert_after);
|
|
InsertBetween(pickup, pickup_insert_after, pickup_insert_before);
|
|
|
|
const int64_t delivery_insert_after = entry->delivery_insert_after();
|
|
const int64_t delivery_insert_before = (delivery_insert_after == pickup)
|
|
? pickup_insert_before
|
|
: Value(delivery_insert_after);
|
|
InsertBetween(delivery, delivery_insert_after, delivery_insert_before);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
if (!UpdateAfterPairInsertion(
|
|
pair_indices_to_insert, entry_vehicle, pickup,
|
|
pickup_insert_after, delivery, delivery_insert_after,
|
|
&priority_queue, &pickup_to_entries, &delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
DeletePairEntry(entry, &priority_queue, &pickup_to_entries,
|
|
&delivery_to_entries);
|
|
}
|
|
}
|
|
// In case all pairs could not be inserted, pushing uninserted ones to the
|
|
// next bucket.
|
|
for (auto it = pair_indices_to_insert.begin(),
|
|
last = pair_indices_to_insert.end();
|
|
it != last;) {
|
|
if (pair_is_performed(*it)) {
|
|
pair_indices_to_insert.erase(it++);
|
|
} else {
|
|
++it;
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::
|
|
InsertPairEntryUsingEmptyVehicleTypeCurator(
|
|
const absl::flat_hash_set<int>& pair_indices,
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry* const pair_entry,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>*
|
|
priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_to_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_to_entries) {
|
|
const int entry_vehicle = pair_entry->vehicle();
|
|
DCHECK(UseEmptyVehicleTypeCuratorForVehicle(entry_vehicle));
|
|
|
|
// Trying to insert on an empty vehicle.
|
|
// As we only have one pair_entry per empty vehicle type, we try inserting on
|
|
// all vehicles of this type with the same fixed cost, as they all have the
|
|
// same insertion value.
|
|
const int64_t pickup = pair_entry->pickup_to_insert();
|
|
const int64_t delivery = pair_entry->delivery_to_insert();
|
|
const int64_t entry_fixed_cost =
|
|
model()->GetFixedCostOfVehicle(entry_vehicle);
|
|
auto vehicle_is_compatible = [this, entry_fixed_cost, pickup,
|
|
delivery](int vehicle) {
|
|
if (model()->GetFixedCostOfVehicle(vehicle) != entry_fixed_cost) {
|
|
return false;
|
|
}
|
|
// NOTE: Only empty vehicles should be in the vehicle_curator_.
|
|
DCHECK(VehicleIsEmpty(vehicle));
|
|
const int64_t end = model()->End(vehicle);
|
|
InsertBetween(pickup, model()->Start(vehicle), end, vehicle);
|
|
InsertBetween(delivery, pickup, end, vehicle);
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
};
|
|
// Since the vehicles of the same type are sorted by increasing fixed
|
|
// cost by the curator, we can stop as soon as a vehicle with a fixed cost
|
|
// higher than the entry_fixed_cost is found which is empty, and adapt the
|
|
// pair entry with this new vehicle.
|
|
auto stop_and_return_vehicle = [this, entry_fixed_cost](int vehicle) {
|
|
return model()->GetFixedCostOfVehicle(vehicle) > entry_fixed_cost;
|
|
};
|
|
const auto [compatible_vehicle, next_fixed_cost_empty_vehicle] =
|
|
empty_vehicle_type_curator_->GetCompatibleVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(entry_vehicle),
|
|
vehicle_is_compatible, stop_and_return_vehicle);
|
|
if (compatible_vehicle >= 0) {
|
|
// The pair was inserted on this vehicle.
|
|
const int64_t vehicle_start = model()->Start(compatible_vehicle);
|
|
const int num_previous_vehicle_entries =
|
|
pickup_to_entries->at(vehicle_start).size() +
|
|
delivery_to_entries->at(vehicle_start).size();
|
|
if (!UpdateAfterPairInsertion(
|
|
pair_indices, compatible_vehicle, pickup, vehicle_start, delivery,
|
|
pickup, priority_queue, pickup_to_entries, delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
if (compatible_vehicle != entry_vehicle) {
|
|
// The pair was inserted on another empty vehicle of the same type
|
|
// and same fixed cost as entry_vehicle.
|
|
// Since this vehicle is empty and has the same fixed cost as the
|
|
// entry_vehicle, it shouldn't be the representative of empty vehicles
|
|
// for any pickup/delivery in the priority queue.
|
|
DCHECK(
|
|
num_previous_vehicle_entries == 0 ||
|
|
model()->GetVehicleClassIndexOfVehicle(compatible_vehicle).value() !=
|
|
model()->GetVehicleClassIndexOfVehicle(entry_vehicle).value());
|
|
return true;
|
|
}
|
|
// The previously unused entry_vehicle is now used, so we use the next
|
|
// available vehicle of the same type to compute and store insertions on
|
|
// empty vehicles.
|
|
const int new_empty_vehicle =
|
|
empty_vehicle_type_curator_->GetLowestFixedCostVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(compatible_vehicle));
|
|
|
|
if (new_empty_vehicle >= 0) {
|
|
DCHECK(VehicleIsEmpty(new_empty_vehicle));
|
|
// Add node entries after this vehicle start for uninserted pairs which
|
|
// don't have entries on this empty vehicle.
|
|
// Clearing all existing entries before adding updated ones. Some could
|
|
// have been added when next_fixed_cost_empty_vehicle >= 0 (see the next
|
|
// branch).
|
|
const int64_t new_empty_vehicle_start = model()->Start(new_empty_vehicle);
|
|
const std::vector<PairEntry*> to_remove(
|
|
pickup_to_entries->at(new_empty_vehicle_start).begin(),
|
|
pickup_to_entries->at(new_empty_vehicle_start).end());
|
|
for (PairEntry* entry : to_remove) {
|
|
DeletePairEntry(entry, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
if (!AddPairEntriesWithPickupAfter(
|
|
pair_indices, new_empty_vehicle, new_empty_vehicle_start,
|
|
/*skip_entries_inserting_delivery_after=*/-1, priority_queue,
|
|
pickup_to_entries, delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (next_fixed_cost_empty_vehicle >= 0) {
|
|
// Could not insert on this vehicle or any other vehicle of the same type
|
|
// with the same fixed cost, but found an empty vehicle of this type with
|
|
// higher fixed cost.
|
|
DCHECK(VehicleIsEmpty(next_fixed_cost_empty_vehicle));
|
|
// Update the pair entry to correspond to an insertion on this
|
|
// next_fixed_cost_empty_vehicle instead of the previous entry_vehicle.
|
|
pair_entry->set_vehicle(next_fixed_cost_empty_vehicle);
|
|
pickup_to_entries->at(pair_entry->pickup_insert_after()).erase(pair_entry);
|
|
pair_entry->set_pickup_insert_after(
|
|
model()->Start(next_fixed_cost_empty_vehicle));
|
|
pickup_to_entries->at(pair_entry->pickup_insert_after()).insert(pair_entry);
|
|
DCHECK_EQ(pair_entry->delivery_insert_after(), pickup);
|
|
if (!UpdatePairEntry(pair_entry, priority_queue)) {
|
|
DeletePairEntry(pair_entry, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
} else {
|
|
DeletePairEntry(pair_entry, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
class GlobalCheapestInsertionFilteredHeuristic::NodeEntryQueue {
|
|
public:
|
|
struct Entry {
|
|
bool operator<(const Entry& other) const {
|
|
if (bucket != other.bucket) {
|
|
return bucket < other.bucket;
|
|
}
|
|
if (value != other.value) {
|
|
return value < other.value;
|
|
}
|
|
if ((vehicle == -1) ^ (other.vehicle == -1)) {
|
|
return other.vehicle == -1;
|
|
}
|
|
return std::tie(insert_after, node_to_insert, vehicle) <
|
|
std::tie(other.insert_after, other.node_to_insert, other.vehicle);
|
|
}
|
|
int64_t value;
|
|
int64_t node_to_insert;
|
|
int64_t insert_after;
|
|
int vehicle;
|
|
int bucket;
|
|
};
|
|
|
|
explicit NodeEntryQueue(int num_nodes)
|
|
: entries_(num_nodes), touched_entries_(num_nodes) {}
|
|
void Clear() {
|
|
priority_queue_.Clear();
|
|
for (Entries& entries : entries_) entries.Clear();
|
|
touched_entries_.ResetAllToFalse();
|
|
}
|
|
bool IsEmpty() const {
|
|
return priority_queue_.IsEmpty() &&
|
|
touched_entries_.NumberOfSetCallsWithDifferentArguments() == 0;
|
|
}
|
|
bool IsEmpty(int64_t insert_after) const {
|
|
return insert_after >= entries_.size() ||
|
|
entries_[insert_after].entries.empty();
|
|
}
|
|
Entry* Top() {
|
|
DCHECK(!IsEmpty());
|
|
for (int touched : touched_entries_.PositionsSetAtLeastOnce()) {
|
|
SortInsertions(&entries_[touched]);
|
|
}
|
|
touched_entries_.ResetAllToFalse();
|
|
DCHECK(!priority_queue_.IsEmpty());
|
|
Entries* entries = priority_queue_.Top();
|
|
DCHECK(!entries->entries.empty());
|
|
return entries->Top();
|
|
}
|
|
void Pop() {
|
|
if (IsEmpty()) return;
|
|
CHECK_EQ(touched_entries_.NumberOfSetCallsWithDifferentArguments(), 0);
|
|
Entries* top = priority_queue_.Top();
|
|
if (top->IncrementTop()) {
|
|
priority_queue_.NoteChangedPriority(top);
|
|
} else {
|
|
priority_queue_.Remove(top);
|
|
top->Clear();
|
|
}
|
|
}
|
|
void ClearInsertions(int64_t insert_after) {
|
|
if (IsEmpty(insert_after)) return;
|
|
Entries& entries = entries_[insert_after];
|
|
if (priority_queue_.Contains(&entries)) {
|
|
priority_queue_.Remove(&entries);
|
|
}
|
|
entries.Clear();
|
|
}
|
|
void PushInsertion(int64_t node, int64_t insert_after, int vehicle,
|
|
int bucket, int64_t value) {
|
|
entries_[insert_after].entries.push_back(
|
|
{value, node, insert_after, vehicle, bucket});
|
|
touched_entries_.Set(insert_after);
|
|
}
|
|
|
|
private:
|
|
struct Entries {
|
|
bool operator<(const Entries& other) const {
|
|
DCHECK(!entries.empty());
|
|
DCHECK(!other.entries.empty());
|
|
return other.entries[other.top] < entries[top];
|
|
}
|
|
void Clear() {
|
|
entries.clear();
|
|
top = 0;
|
|
heap_index = -1;
|
|
}
|
|
void SetHeapIndex(int index) { heap_index = index; }
|
|
int GetHeapIndex() const { return heap_index; }
|
|
bool IncrementTop() {
|
|
++top;
|
|
return top < entries.size();
|
|
}
|
|
Entry* Top() { return &entries[top]; }
|
|
|
|
std::vector<Entry> entries;
|
|
int top = 0;
|
|
int heap_index = -1;
|
|
};
|
|
|
|
void SortInsertions(Entries* entries) {
|
|
entries->top = 0;
|
|
if (entries->entries.empty()) return;
|
|
absl::c_sort(entries->entries);
|
|
if (!priority_queue_.Contains(entries)) {
|
|
priority_queue_.Add(entries);
|
|
} else {
|
|
priority_queue_.NoteChangedPriority(entries);
|
|
}
|
|
}
|
|
|
|
AdjustablePriorityQueue<Entries> priority_queue_;
|
|
std::vector<Entries> entries_;
|
|
SparseBitset<int> touched_entries_;
|
|
};
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::InsertNodesOnRoutes(
|
|
const std::map<int64_t, std::vector<int>>& nodes_by_bucket,
|
|
const absl::flat_hash_set<int>& vehicles) {
|
|
NodeEntryQueue queue(model()->Nexts().size());
|
|
SparseBitset<int> nodes_to_insert(model()->Size());
|
|
for (const auto& [bucket, nodes] : nodes_by_bucket) {
|
|
for (int node : nodes) {
|
|
nodes_to_insert.Set(node);
|
|
}
|
|
if (!InitializePositions(nodes_to_insert, vehicles, &queue)) {
|
|
return false;
|
|
}
|
|
// The following boolean indicates whether or not all vehicles are being
|
|
// considered for insertion of the nodes simultaneously.
|
|
// In the sequential version of the heuristic, as well as when inserting
|
|
// single pickup or deliveries from pickup/delivery pairs, this will be
|
|
// false. In the general parallel version of the heuristic, all_vehicles is
|
|
// true.
|
|
const bool all_vehicles =
|
|
vehicles.empty() || vehicles.size() == model()->vehicles();
|
|
|
|
while (!queue.IsEmpty()) {
|
|
const NodeEntryQueue::Entry* node_entry = queue.Top();
|
|
if (StopSearch()) return false;
|
|
const int64_t node_to_insert = node_entry->node_to_insert;
|
|
if (Contains(node_to_insert)) {
|
|
queue.Pop();
|
|
continue;
|
|
}
|
|
|
|
const int entry_vehicle = node_entry->vehicle;
|
|
if (entry_vehicle == -1) {
|
|
DCHECK(all_vehicles);
|
|
// Make node unperformed.
|
|
SetNext(node_to_insert, node_to_insert, -1);
|
|
if (!Evaluate(/*commit=*/true).has_value()) {
|
|
queue.Pop();
|
|
}
|
|
continue;
|
|
}
|
|
|
|
// Make node performed.
|
|
if (UseEmptyVehicleTypeCuratorForVehicle(entry_vehicle, all_vehicles)) {
|
|
DCHECK(all_vehicles);
|
|
if (!InsertNodeEntryUsingEmptyVehicleTypeCurator(
|
|
nodes_to_insert, all_vehicles, &queue)) {
|
|
return false;
|
|
}
|
|
continue;
|
|
}
|
|
|
|
const int64_t insert_after = node_entry->insert_after;
|
|
InsertBetween(node_to_insert, insert_after, Value(insert_after));
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
if (!UpdateAfterNodeInsertion(nodes_to_insert, entry_vehicle,
|
|
node_to_insert, insert_after,
|
|
all_vehicles, &queue)) {
|
|
return false;
|
|
}
|
|
} else {
|
|
queue.Pop();
|
|
}
|
|
}
|
|
// In case all nodes could not be inserted, pushing uninserted ones to the
|
|
// next bucket.
|
|
std::vector<int> non_inserted_nodes;
|
|
non_inserted_nodes.reserve(
|
|
nodes_to_insert.NumberOfSetCallsWithDifferentArguments());
|
|
for (int node : nodes_to_insert.PositionsSetAtLeastOnce()) {
|
|
if (!Contains(node)) non_inserted_nodes.push_back(node);
|
|
}
|
|
nodes_to_insert.ResetAllToFalse();
|
|
for (int node : non_inserted_nodes) {
|
|
nodes_to_insert.Set(node);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::
|
|
InsertNodeEntryUsingEmptyVehicleTypeCurator(const SparseBitset<int>& nodes,
|
|
bool all_vehicles,
|
|
NodeEntryQueue* queue) {
|
|
const NodeEntryQueue::Entry* node_entry = queue->Top();
|
|
const int entry_vehicle = node_entry->vehicle;
|
|
DCHECK(UseEmptyVehicleTypeCuratorForVehicle(entry_vehicle, all_vehicles));
|
|
DCHECK(evaluator_ != nullptr);
|
|
|
|
// Trying to insert on an empty vehicle, and all vehicles are being
|
|
// considered simultaneously.
|
|
// As we only have one node_entry per type, we try inserting on all vehicles
|
|
// of this type with the same fixed cost as they all have the same insertion
|
|
// value.
|
|
const int64_t node_to_insert = node_entry->node_to_insert;
|
|
const int bucket = node_entry->bucket;
|
|
const int64_t entry_fixed_cost =
|
|
model()->GetFixedCostOfVehicle(entry_vehicle);
|
|
auto vehicle_is_compatible = [this, entry_fixed_cost,
|
|
node_to_insert](int vehicle) {
|
|
if (model()->GetFixedCostOfVehicle(vehicle) != entry_fixed_cost) {
|
|
return false;
|
|
}
|
|
// NOTE: Only empty vehicles should be in the vehicle_curator_.
|
|
DCHECK(VehicleIsEmpty(vehicle));
|
|
InsertBetween(node_to_insert, model()->Start(vehicle),
|
|
model()->End(vehicle), vehicle);
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
};
|
|
// Since the vehicles of the same type are sorted by increasing fixed
|
|
// cost by the curator, we can stop as soon as an empty vehicle with a fixed
|
|
// cost higher than the entry_fixed_cost is found, and add new entries for
|
|
// this new vehicle.
|
|
auto stop_and_return_vehicle = [this, entry_fixed_cost](int vehicle) {
|
|
return model()->GetFixedCostOfVehicle(vehicle) > entry_fixed_cost;
|
|
};
|
|
const auto [compatible_vehicle, next_fixed_cost_empty_vehicle] =
|
|
empty_vehicle_type_curator_->GetCompatibleVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(entry_vehicle),
|
|
vehicle_is_compatible, stop_and_return_vehicle);
|
|
if (compatible_vehicle >= 0) {
|
|
// The node was inserted on this vehicle.
|
|
const int64_t compatible_start = model()->Start(compatible_vehicle);
|
|
const bool no_prior_entries_for_this_vehicle =
|
|
queue->IsEmpty(compatible_start);
|
|
if (!UpdateAfterNodeInsertion(nodes, compatible_vehicle, node_to_insert,
|
|
compatible_start, all_vehicles, queue)) {
|
|
return false;
|
|
}
|
|
if (compatible_vehicle != entry_vehicle) {
|
|
// The node was inserted on another empty vehicle of the same type
|
|
// and same fixed cost as entry_vehicle.
|
|
// Since this vehicle is empty and has the same fixed cost as the
|
|
// entry_vehicle, it shouldn't be the representative of empty vehicles
|
|
// for any node in the priority queue.
|
|
DCHECK(
|
|
no_prior_entries_for_this_vehicle ||
|
|
model()->GetVehicleClassIndexOfVehicle(compatible_vehicle).value() !=
|
|
model()->GetVehicleClassIndexOfVehicle(entry_vehicle).value());
|
|
return true;
|
|
}
|
|
// The previously unused entry_vehicle is now used, so we use the next
|
|
// available vehicle of the same type to compute and store insertions on
|
|
// empty vehicles.
|
|
const int new_empty_vehicle =
|
|
empty_vehicle_type_curator_->GetLowestFixedCostVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(compatible_vehicle));
|
|
|
|
if (new_empty_vehicle >= 0) {
|
|
DCHECK(VehicleIsEmpty(new_empty_vehicle));
|
|
// Add node entries after this vehicle start for uninserted nodes which
|
|
// don't have entries on this empty vehicle.
|
|
// Clearing all existing entries before adding updated ones. Some could
|
|
// have been added when next_fixed_cost_empty_vehicle >= 0 (see the next
|
|
// branch).
|
|
const int64_t new_empty_vehicle_start = model()->Start(new_empty_vehicle);
|
|
queue->ClearInsertions(new_empty_vehicle_start);
|
|
if (!AddNodeEntriesAfter(nodes, new_empty_vehicle,
|
|
new_empty_vehicle_start, all_vehicles, queue)) {
|
|
return false;
|
|
}
|
|
}
|
|
} else if (next_fixed_cost_empty_vehicle >= 0) {
|
|
// Could not insert on this vehicle or any other vehicle of the same
|
|
// type with the same fixed cost, but found an empty vehicle of this type
|
|
// with higher fixed cost.
|
|
DCHECK(VehicleIsEmpty(next_fixed_cost_empty_vehicle));
|
|
// Update the insertion entry to be on next_empty_vehicle instead of the
|
|
// previous entry_vehicle.
|
|
queue->Pop();
|
|
const int64_t insert_after = model()->Start(next_fixed_cost_empty_vehicle);
|
|
const int64_t insertion_cost = GetEvaluatorInsertionCostForNodeAtPosition(
|
|
node_to_insert, insert_after, Value(insert_after),
|
|
next_fixed_cost_empty_vehicle);
|
|
const int64_t penalty_shift =
|
|
absl::GetFlag(FLAGS_routing_shift_insertion_cost_by_penalty)
|
|
? GetUnperformedValue(node_to_insert)
|
|
: 0;
|
|
queue->PushInsertion(node_to_insert, insert_after,
|
|
next_fixed_cost_empty_vehicle, bucket,
|
|
CapSub(insertion_cost, penalty_shift));
|
|
} else {
|
|
queue->Pop();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::SequentialInsertNodes(
|
|
const std::map<int64_t, std::vector<int>>& nodes_by_bucket) {
|
|
std::vector<bool> is_vehicle_used;
|
|
absl::flat_hash_set<int> used_vehicles;
|
|
std::vector<int> unused_vehicles;
|
|
|
|
DetectUsedVehicles(&is_vehicle_used, &unused_vehicles, &used_vehicles);
|
|
if (!used_vehicles.empty() &&
|
|
!InsertNodesOnRoutes(nodes_by_bucket, used_vehicles)) {
|
|
return false;
|
|
}
|
|
|
|
std::vector<std::vector<StartEndValue>> start_end_distances_per_node =
|
|
ComputeStartEndDistanceForVehicles(unused_vehicles);
|
|
SeedQueue first_node_queue(/*prioritize_farthest_nodes=*/false);
|
|
InitializeSeedQueue(&start_end_distances_per_node, &first_node_queue);
|
|
|
|
int vehicle = InsertSeedNode(&start_end_distances_per_node, &first_node_queue,
|
|
&is_vehicle_used);
|
|
|
|
while (vehicle >= 0) {
|
|
if (!InsertNodesOnRoutes(nodes_by_bucket, {vehicle})) {
|
|
return false;
|
|
}
|
|
vehicle = InsertSeedNode(&start_end_distances_per_node, &first_node_queue,
|
|
&is_vehicle_used);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::DetectUsedVehicles(
|
|
std::vector<bool>* is_vehicle_used, std::vector<int>* unused_vehicles,
|
|
absl::flat_hash_set<int>* used_vehicles) {
|
|
is_vehicle_used->clear();
|
|
is_vehicle_used->resize(model()->vehicles());
|
|
|
|
used_vehicles->clear();
|
|
used_vehicles->reserve(model()->vehicles());
|
|
|
|
unused_vehicles->clear();
|
|
unused_vehicles->reserve(model()->vehicles());
|
|
|
|
for (int vehicle = 0; vehicle < model()->vehicles(); vehicle++) {
|
|
if (!VehicleIsEmpty(vehicle)) {
|
|
(*is_vehicle_used)[vehicle] = true;
|
|
used_vehicles->insert(vehicle);
|
|
} else {
|
|
(*is_vehicle_used)[vehicle] = false;
|
|
unused_vehicles->push_back(vehicle);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::IsCheapestClassRepresentative(
|
|
int vehicle) const {
|
|
if (evaluator_ == nullptr) {
|
|
// When no evaluator_ is given, filters are used to evaluate the cost and
|
|
// feasibility of inserting on each vehicle, so we don't use only one
|
|
// class representative for each class.
|
|
return true;
|
|
}
|
|
if (!VehicleIsEmpty(vehicle)) return true;
|
|
// We only consider the least expensive empty vehicle of each type for
|
|
// entries of the same vehicle class.
|
|
const int curator_vehicle =
|
|
empty_vehicle_type_curator_->GetLowestFixedCostVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(vehicle));
|
|
return curator_vehicle == vehicle ||
|
|
model()->GetVehicleClassIndexOfVehicle(curator_vehicle).value() !=
|
|
model()->GetVehicleClassIndexOfVehicle(vehicle).value();
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::InsertFarthestNodesAsSeeds() {
|
|
// TODO(user): consider checking search limits.
|
|
if (gci_params_.farthest_seeds_ratio <= 0) return;
|
|
// Insert at least 1 farthest Seed if the parameter is positive.
|
|
const int num_seeds = static_cast<int>(
|
|
std::ceil(gci_params_.farthest_seeds_ratio * model()->vehicles()));
|
|
|
|
std::vector<bool> is_vehicle_used;
|
|
absl::flat_hash_set<int> used_vehicles;
|
|
std::vector<int> unused_vehicles;
|
|
DetectUsedVehicles(&is_vehicle_used, &unused_vehicles, &used_vehicles);
|
|
std::vector<std::vector<StartEndValue>> start_end_distances_per_node =
|
|
ComputeStartEndDistanceForVehicles(unused_vehicles);
|
|
|
|
// Priority queue where the Seeds with a larger distance are given higher
|
|
// priority.
|
|
SeedQueue farthest_node_queue(/*prioritize_farthest_nodes=*/true);
|
|
InitializeSeedQueue(&start_end_distances_per_node, &farthest_node_queue);
|
|
|
|
int inserted_seeds = 0;
|
|
while (inserted_seeds++ < num_seeds) {
|
|
if (InsertSeedNode(&start_end_distances_per_node, &farthest_node_queue,
|
|
&is_vehicle_used) < 0) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
// NOTE: As we don't use the empty_vehicle_type_curator_ when inserting seed
|
|
// nodes on routes, some previously empty vehicles may now be used, so we
|
|
// update the curator accordingly to ensure it still only stores empty
|
|
// vehicles.
|
|
DCHECK(empty_vehicle_type_curator_ != nullptr);
|
|
empty_vehicle_type_curator_->Update(
|
|
[this](int vehicle) { return !VehicleIsEmpty(vehicle); });
|
|
}
|
|
|
|
int GlobalCheapestInsertionFilteredHeuristic::InsertSeedNode(
|
|
std::vector<std::vector<StartEndValue>>* start_end_distances_per_node,
|
|
SeedQueue* sq, std::vector<bool>* is_vehicle_used) {
|
|
auto& priority_queue = sq->priority_queue;
|
|
while (!priority_queue.empty()) {
|
|
if (StopSearch()) return -1;
|
|
const Seed& seed = priority_queue.top();
|
|
const int seed_node = seed.index;
|
|
DCHECK(seed.is_node_index);
|
|
const int seed_vehicle = seed.vehicle;
|
|
priority_queue.pop();
|
|
|
|
std::vector<StartEndValue>& other_start_end_values =
|
|
(*start_end_distances_per_node)[seed_node];
|
|
|
|
if (Contains(seed_node)) {
|
|
// The node is already inserted, it is therefore no longer considered as
|
|
// a potential seed.
|
|
other_start_end_values.clear();
|
|
continue;
|
|
}
|
|
if (!(*is_vehicle_used)[seed_vehicle]) {
|
|
// Try to insert this seed_node on this vehicle's route.
|
|
const int64_t start = model()->Start(seed_vehicle);
|
|
const int64_t end = model()->End(seed_vehicle);
|
|
DCHECK_EQ(Value(start), end);
|
|
InsertBetween(seed_node, start, end, seed_vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
(*is_vehicle_used)[seed_vehicle] = true;
|
|
other_start_end_values.clear();
|
|
SetVehicleIndex(seed_node, seed_vehicle);
|
|
return seed_vehicle;
|
|
}
|
|
}
|
|
// Either the vehicle is already used, or the Commit() wasn't successful.
|
|
// In both cases, we insert the next StartEndValue from
|
|
// start_end_distances_per_node[seed_node] in the priority queue.
|
|
AddSeedNodeToQueue(seed_node, &other_start_end_values, sq);
|
|
}
|
|
// No seed node was inserted.
|
|
return -1;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::InitializePairPositions(
|
|
const absl::flat_hash_set<int>& pair_indices,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_to_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_to_entries) {
|
|
priority_queue->Clear();
|
|
pickup_to_entries->clear();
|
|
pickup_to_entries->resize(model()->Size());
|
|
delivery_to_entries->clear();
|
|
delivery_to_entries->resize(model()->Size());
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
for (int index : pair_indices) {
|
|
const auto& [pickups, deliveries] = pickup_delivery_pairs[index];
|
|
for (int64_t pickup : pickups) {
|
|
if (Contains(pickup)) continue;
|
|
for (int64_t delivery : deliveries) {
|
|
if (Contains(delivery)) continue;
|
|
if (StopSearchAndCleanup(priority_queue)) return false;
|
|
// Add insertion entry making pair unperformed. When the pair is part
|
|
// of a disjunction we do not try to make any of its pairs unperformed
|
|
// as it requires having an entry with all pairs being unperformed.
|
|
// TODO(user): Adapt the code to make pair disjunctions unperformed.
|
|
if (gci_params_.add_unperformed_entries && pickups.size() == 1 &&
|
|
deliveries.size() == 1 &&
|
|
GetUnperformedValue(pickup) !=
|
|
std::numeric_limits<int64_t>::max() &&
|
|
GetUnperformedValue(delivery) !=
|
|
std::numeric_limits<int64_t>::max()) {
|
|
AddPairEntry(pickup, -1, delivery, -1, -1, priority_queue, nullptr,
|
|
nullptr);
|
|
}
|
|
// Add all other insertion entries with pair performed.
|
|
InitializeInsertionEntriesPerformingPair(
|
|
pickup, delivery, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::
|
|
InitializeInsertionEntriesPerformingPair(
|
|
int64_t pickup, int64_t delivery,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>*
|
|
priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_to_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_to_entries) {
|
|
if (!gci_params_.use_neighbors_ratio_for_initialization) {
|
|
std::unique_ptr<IntVarIterator> vehicle_it(
|
|
model()->VehicleVar(pickup)->MakeDomainIterator(false));
|
|
for (const int vehicle : InitAndGetValues(vehicle_it.get())) {
|
|
if (vehicle < 0 || !model()->VehicleVar(delivery)->Contains(vehicle)) {
|
|
continue;
|
|
}
|
|
if (!IsCheapestClassRepresentative(vehicle)) continue;
|
|
|
|
int64_t pickup_insert_after = model()->Start(vehicle);
|
|
while (!model()->IsEnd(pickup_insert_after)) {
|
|
int64_t delivery_insert_after = pickup;
|
|
while (!model()->IsEnd(delivery_insert_after)) {
|
|
AddPairEntry(pickup, pickup_insert_after, delivery,
|
|
delivery_insert_after, vehicle, priority_queue,
|
|
pickup_to_entries, delivery_to_entries);
|
|
delivery_insert_after = (delivery_insert_after == pickup)
|
|
? Value(pickup_insert_after)
|
|
: Value(delivery_insert_after);
|
|
}
|
|
pickup_insert_after = Value(pickup_insert_after);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We're only considering the closest neighbors as insertion positions for
|
|
// the pickup/delivery pair.
|
|
for (int cost_class = 0; cost_class < model()->GetCostClassesCount();
|
|
cost_class++) {
|
|
absl::flat_hash_set<std::pair<int64_t, int64_t>>
|
|
existing_insertion_positions;
|
|
// Explore the neighborhood of the pickup.
|
|
for (const int64_t pickup_insert_after :
|
|
node_index_to_neighbors_by_cost_class_
|
|
->GetIncomingNeighborsOfNodeForCostClass(cost_class, pickup)) {
|
|
if (!Contains(pickup_insert_after)) {
|
|
continue;
|
|
}
|
|
const int vehicle = node_index_to_vehicle_[pickup_insert_after];
|
|
if (vehicle < 0 ||
|
|
model()->GetCostClassIndexOfVehicle(vehicle).value() != cost_class) {
|
|
continue;
|
|
}
|
|
|
|
if (!IsCheapestClassRepresentative(vehicle)) continue;
|
|
|
|
if (!model()->VehicleVar(pickup)->Contains(vehicle) ||
|
|
!model()->VehicleVar(delivery)->Contains(vehicle)) {
|
|
continue;
|
|
}
|
|
int64_t delivery_insert_after = pickup;
|
|
while (!model()->IsEnd(delivery_insert_after)) {
|
|
const std::pair<int64_t, int64_t> insertion_position = {
|
|
pickup_insert_after, delivery_insert_after};
|
|
DCHECK(!existing_insertion_positions.contains(insertion_position));
|
|
existing_insertion_positions.insert(insertion_position);
|
|
|
|
AddPairEntry(pickup, pickup_insert_after, delivery,
|
|
delivery_insert_after, vehicle, priority_queue,
|
|
pickup_to_entries, delivery_to_entries);
|
|
delivery_insert_after = (delivery_insert_after == pickup)
|
|
? Value(pickup_insert_after)
|
|
: Value(delivery_insert_after);
|
|
}
|
|
}
|
|
|
|
// Explore the neighborhood of the delivery.
|
|
for (const int64_t delivery_insert_after :
|
|
node_index_to_neighbors_by_cost_class_
|
|
->GetIncomingNeighborsOfNodeForCostClass(cost_class, delivery)) {
|
|
if (!Contains(delivery_insert_after)) {
|
|
continue;
|
|
}
|
|
const int vehicle = node_index_to_vehicle_[delivery_insert_after];
|
|
if (vehicle < 0 ||
|
|
model()->GetCostClassIndexOfVehicle(vehicle).value() != cost_class) {
|
|
continue;
|
|
}
|
|
if (!model()->VehicleVar(pickup)->Contains(vehicle) ||
|
|
!model()->VehicleVar(delivery)->Contains(vehicle)) {
|
|
continue;
|
|
}
|
|
|
|
DCHECK(!VehicleIsEmpty(vehicle) ||
|
|
delivery_insert_after == model()->Start(vehicle));
|
|
|
|
int64_t pickup_insert_after = model()->Start(vehicle);
|
|
while (pickup_insert_after != delivery_insert_after) {
|
|
if (!existing_insertion_positions.contains(
|
|
std::make_pair(pickup_insert_after, delivery_insert_after))) {
|
|
AddPairEntry(pickup, pickup_insert_after, delivery,
|
|
delivery_insert_after, vehicle, priority_queue,
|
|
pickup_to_entries, delivery_to_entries);
|
|
}
|
|
pickup_insert_after = Value(pickup_insert_after);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterPairInsertion(
|
|
const absl::flat_hash_set<int>& pair_indices, int vehicle, int64_t pickup,
|
|
int64_t pickup_position, int64_t delivery, int64_t delivery_position,
|
|
AdjustablePriorityQueue<PairEntry>* priority_queue,
|
|
std::vector<PairEntries>* pickup_to_entries,
|
|
std::vector<PairEntries>* delivery_to_entries) {
|
|
// Clearing any entries created after the pickup; these entries are the ones
|
|
// where the delivery is to be inserted immediately after the pickup.
|
|
const std::vector<PairEntry*> to_remove(
|
|
delivery_to_entries->at(pickup).begin(),
|
|
delivery_to_entries->at(pickup).end());
|
|
for (PairEntry* pair_entry : to_remove) {
|
|
DeletePairEntry(pair_entry, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
DCHECK(pickup_to_entries->at(pickup).empty());
|
|
DCHECK(pickup_to_entries->at(delivery).empty());
|
|
DCHECK(delivery_to_entries->at(pickup).empty());
|
|
DCHECK(delivery_to_entries->at(delivery).empty());
|
|
// Update cost of existing entries after nodes which have new nexts
|
|
// (pickup_position and delivery_position).
|
|
if (!UpdateExistingPairEntriesAfter(pickup_position, priority_queue,
|
|
pickup_to_entries, delivery_to_entries) ||
|
|
!UpdateExistingPairEntriesAfter(delivery_position, priority_queue,
|
|
pickup_to_entries, delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
// Add new entries after nodes which have been inserted (pickup and delivery).
|
|
// We skip inserting deliveries after 'delivery' in the first call to make
|
|
// sure each pair is only inserted after ('pickup', 'delivery') once.
|
|
if (!AddPairEntriesAfter(pair_indices, vehicle, pickup,
|
|
/*skip_entries_inserting_delivery_after=*/delivery,
|
|
priority_queue, pickup_to_entries,
|
|
delivery_to_entries) ||
|
|
!AddPairEntriesAfter(pair_indices, vehicle, delivery,
|
|
/*skip_entries_inserting_delivery_after=*/-1,
|
|
priority_queue, pickup_to_entries,
|
|
delivery_to_entries)) {
|
|
return false;
|
|
}
|
|
SetVehicleIndex(pickup, vehicle);
|
|
SetVehicleIndex(delivery, vehicle);
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::UpdateExistingPairEntriesAfter(
|
|
int64_t insert_after,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_to_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_to_entries) {
|
|
DCHECK(!model()->IsEnd(insert_after));
|
|
// Remove entries at 'insert_after' with nodes which have already been
|
|
// inserted and update remaining entries.
|
|
std::vector<PairEntry*> to_remove;
|
|
for (const PairEntries* pair_entries :
|
|
{&pickup_to_entries->at(insert_after),
|
|
&delivery_to_entries->at(insert_after)}) {
|
|
if (StopSearchAndCleanup(priority_queue)) return false;
|
|
for (PairEntry* const pair_entry : *pair_entries) {
|
|
DCHECK(priority_queue->Contains(pair_entry));
|
|
if (Contains(pair_entry->pickup_to_insert()) ||
|
|
Contains(pair_entry->delivery_to_insert())) {
|
|
to_remove.push_back(pair_entry);
|
|
} else {
|
|
DCHECK(pickup_to_entries->at(pair_entry->pickup_insert_after())
|
|
.contains(pair_entry));
|
|
DCHECK(delivery_to_entries->at(pair_entry->delivery_insert_after())
|
|
.contains(pair_entry));
|
|
if (!UpdatePairEntry(pair_entry, priority_queue)) {
|
|
to_remove.push_back(pair_entry);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for (PairEntry* const pair_entry : to_remove) {
|
|
DeletePairEntry(pair_entry, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::AddPairEntriesWithPickupAfter(
|
|
const absl::flat_hash_set<int>& pair_indices, int vehicle,
|
|
int64_t insert_after, int64_t skip_entries_inserting_delivery_after,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_to_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_to_entries) {
|
|
const int cost_class = model()->GetCostClassIndexOfVehicle(vehicle).value();
|
|
const int64_t pickup_insert_before = Value(insert_after);
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
DCHECK(pickup_to_entries->at(insert_after).empty());
|
|
for (const int64_t pickup :
|
|
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;
|
|
}
|
|
if (const std::optional<RoutingModel::PickupDeliveryPosition> pickup_pos =
|
|
model()->GetPickupPosition(pickup);
|
|
pickup_pos.has_value()) {
|
|
const int pair_index = pickup_pos->pd_pair_index;
|
|
if (!pair_indices.contains(pair_index)) continue;
|
|
for (const int64_t delivery :
|
|
pickup_delivery_pairs[pair_index].delivery_alternatives) {
|
|
if (Contains(delivery) ||
|
|
!model()->VehicleVar(delivery)->Contains(vehicle)) {
|
|
continue;
|
|
}
|
|
int64_t delivery_insert_after = pickup;
|
|
while (!model()->IsEnd(delivery_insert_after)) {
|
|
if (delivery_insert_after != skip_entries_inserting_delivery_after) {
|
|
AddPairEntry(pickup, insert_after, delivery, delivery_insert_after,
|
|
vehicle, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
}
|
|
if (delivery_insert_after == pickup) {
|
|
delivery_insert_after = pickup_insert_before;
|
|
} else {
|
|
delivery_insert_after = Value(delivery_insert_after);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::AddPairEntriesWithDeliveryAfter(
|
|
const absl::flat_hash_set<int>& pair_indices, int vehicle,
|
|
int64_t insert_after,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_to_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_to_entries) {
|
|
const int cost_class = model()->GetCostClassIndexOfVehicle(vehicle).value();
|
|
const std::vector<PickupDeliveryPair>& pickup_delivery_pairs =
|
|
model()->GetPickupAndDeliveryPairs();
|
|
for (const int64_t delivery :
|
|
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)) {
|
|
continue;
|
|
}
|
|
if (const std::optional<RoutingModel::PickupDeliveryPosition> delivery_pos =
|
|
model()->GetDeliveryPosition(delivery);
|
|
delivery_pos.has_value()) {
|
|
const int pair_index = delivery_pos->pd_pair_index;
|
|
if (!pair_indices.contains(pair_index)) continue;
|
|
for (const int64_t pickup :
|
|
pickup_delivery_pairs[pair_index].pickup_alternatives) {
|
|
if (Contains(pickup) ||
|
|
!model()->VehicleVar(pickup)->Contains(vehicle)) {
|
|
continue;
|
|
}
|
|
int64_t pickup_insert_after = model()->Start(vehicle);
|
|
while (pickup_insert_after != insert_after) {
|
|
AddPairEntry(pickup, pickup_insert_after, delivery, insert_after,
|
|
vehicle, priority_queue, pickup_to_entries,
|
|
delivery_to_entries);
|
|
pickup_insert_after = Value(pickup_insert_after);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::DeletePairEntry(
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry* entry,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue,
|
|
std::vector<PairEntries>* pickup_to_entries,
|
|
std::vector<PairEntries>* delivery_to_entries) {
|
|
priority_queue->Remove(entry);
|
|
if (entry->pickup_insert_after() != -1) {
|
|
pickup_to_entries->at(entry->pickup_insert_after()).erase(entry);
|
|
}
|
|
if (entry->delivery_insert_after() != -1) {
|
|
delivery_to_entries->at(entry->delivery_insert_after()).erase(entry);
|
|
}
|
|
pair_entry_allocator_.FreeEntry(entry);
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::AddPairEntry(
|
|
int64_t pickup, int64_t pickup_insert_after, int64_t delivery,
|
|
int64_t delivery_insert_after, int vehicle,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
pickup_entries,
|
|
std::vector<GlobalCheapestInsertionFilteredHeuristic::PairEntries>*
|
|
delivery_entries) {
|
|
const IntVar* pickup_vehicle_var = model()->VehicleVar(pickup);
|
|
const IntVar* delivery_vehicle_var = model()->VehicleVar(delivery);
|
|
if (!pickup_vehicle_var->Contains(vehicle) ||
|
|
!delivery_vehicle_var->Contains(vehicle)) {
|
|
if (vehicle == -1 || !VehicleIsEmpty(vehicle)) return;
|
|
// We need to check there is not an equivalent empty vehicle the pair
|
|
// could fit on.
|
|
const auto vehicle_is_compatible = [pickup_vehicle_var,
|
|
delivery_vehicle_var](int vehicle) {
|
|
return pickup_vehicle_var->Contains(vehicle) &&
|
|
delivery_vehicle_var->Contains(vehicle);
|
|
};
|
|
if (!empty_vehicle_type_curator_->HasCompatibleVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(vehicle),
|
|
vehicle_is_compatible)) {
|
|
return;
|
|
}
|
|
}
|
|
const int num_allowed_vehicles =
|
|
std::min(pickup_vehicle_var->Size(), delivery_vehicle_var->Size());
|
|
const int64_t pair_penalty =
|
|
CapAdd(GetUnperformedValue(pickup), GetUnperformedValue(delivery));
|
|
const int64_t penalty_shift =
|
|
absl::GetFlag(FLAGS_routing_shift_insertion_cost_by_penalty)
|
|
? pair_penalty
|
|
: 0;
|
|
if (pickup_insert_after == -1) {
|
|
DCHECK_EQ(delivery_insert_after, -1);
|
|
DCHECK_EQ(vehicle, -1);
|
|
PairEntry* pair_entry = pair_entry_allocator_.NewEntry(
|
|
pickup, -1, delivery, -1, -1, num_allowed_vehicles);
|
|
pair_entry->set_value(CapSub(pair_penalty, penalty_shift));
|
|
priority_queue->Add(pair_entry);
|
|
return;
|
|
}
|
|
const std::optional<int64_t> insertion_cost =
|
|
GetInsertionCostForPairAtPositions(pickup, pickup_insert_after, delivery,
|
|
delivery_insert_after, vehicle);
|
|
|
|
if (!insertion_cost.has_value()) return;
|
|
|
|
PairEntry* const pair_entry = pair_entry_allocator_.NewEntry(
|
|
pickup, pickup_insert_after, delivery, delivery_insert_after, vehicle,
|
|
num_allowed_vehicles);
|
|
pair_entry->set_value(CapSub(*insertion_cost, penalty_shift));
|
|
|
|
// Add entry to priority_queue and pickup_/delivery_entries.
|
|
DCHECK(!priority_queue->Contains(pair_entry));
|
|
pickup_entries->at(pickup_insert_after).insert(pair_entry);
|
|
delivery_entries->at(delivery_insert_after).insert(pair_entry);
|
|
priority_queue->Add(pair_entry);
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::UpdatePairEntry(
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry* const pair_entry,
|
|
AdjustablePriorityQueue<
|
|
GlobalCheapestInsertionFilteredHeuristic::PairEntry>* priority_queue) {
|
|
const int64_t pickup = pair_entry->pickup_to_insert();
|
|
const int64_t delivery = pair_entry->delivery_to_insert();
|
|
const std::optional<int64_t> insertion_cost =
|
|
GetInsertionCostForPairAtPositions(
|
|
pickup, pair_entry->pickup_insert_after(), delivery,
|
|
pair_entry->delivery_insert_after(), pair_entry->vehicle());
|
|
const int64_t penalty_shift =
|
|
absl::GetFlag(FLAGS_routing_shift_insertion_cost_by_penalty) &&
|
|
insertion_cost.has_value()
|
|
? CapAdd(GetUnperformedValue(pickup), GetUnperformedValue(delivery))
|
|
: 0;
|
|
|
|
if (!insertion_cost.has_value()) {
|
|
return false;
|
|
}
|
|
|
|
pair_entry->set_value(CapSub(*insertion_cost, penalty_shift));
|
|
|
|
// Update the priority_queue.
|
|
DCHECK(priority_queue->Contains(pair_entry));
|
|
priority_queue->NoteChangedPriority(pair_entry);
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::InitializePositions(
|
|
const SparseBitset<int>& nodes, const absl::flat_hash_set<int>& vehicles,
|
|
NodeEntryQueue* queue) {
|
|
queue->Clear();
|
|
|
|
const int num_vehicles =
|
|
vehicles.empty() ? model()->vehicles() : vehicles.size();
|
|
const bool all_vehicles = (num_vehicles == model()->vehicles());
|
|
|
|
for (int node : nodes.PositionsSetAtLeastOnce()) {
|
|
if (Contains(node)) continue;
|
|
|
|
if (StopSearch()) return false;
|
|
// Add insertion entry making node unperformed.
|
|
if (gci_params_.add_unperformed_entries &&
|
|
GetUnperformedValue(node) != std::numeric_limits<int64_t>::max()) {
|
|
AddNodeEntry(node, node, -1, all_vehicles, queue);
|
|
}
|
|
// Add all insertion entries making node performed.
|
|
InitializeInsertionEntriesPerformingNode(node, vehicles, queue);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::
|
|
InitializeInsertionEntriesPerformingNode(
|
|
int64_t node, const absl::flat_hash_set<int>& vehicles,
|
|
NodeEntryQueue* queue) {
|
|
const int num_vehicles =
|
|
vehicles.empty() ? model()->vehicles() : vehicles.size();
|
|
const bool all_vehicles = (num_vehicles == model()->vehicles());
|
|
|
|
if (!gci_params_.use_neighbors_ratio_for_initialization) {
|
|
auto vehicles_it = vehicles.begin();
|
|
// TODO(user): Ideally we'd want to iterate on vehicle var values:
|
|
// std::unique_ptr<IntVarIterator>
|
|
// it(model()->VehicleVar(node)->MakeDomainIterator(false));
|
|
// for (const int64_t v : InitAndGetValues(it)) {
|
|
// Requires taking vehicles into account.
|
|
for (int v = 0; v < num_vehicles; v++) {
|
|
const int vehicle = vehicles.empty() ? v : *vehicles_it++;
|
|
if (!model()->VehicleVar(node)->Contains(vehicle)) continue;
|
|
if (all_vehicles && !IsCheapestClassRepresentative(vehicle)) continue;
|
|
|
|
int64_t insert_after = model()->Start(vehicle);
|
|
while (insert_after != model()->End(vehicle)) {
|
|
AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue);
|
|
insert_after = Value(insert_after);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// We're only considering the closest incoming neighbors as insertion
|
|
// positions for the node.
|
|
for (int cost_class = 0; cost_class < model()->GetCostClassesCount();
|
|
cost_class++) {
|
|
for (const int64_t insert_after :
|
|
node_index_to_neighbors_by_cost_class_
|
|
->GetIncomingNeighborsOfNodeForCostClass(cost_class, node)) {
|
|
if (!Contains(insert_after)) {
|
|
continue;
|
|
}
|
|
const int vehicle = node_index_to_vehicle_[insert_after];
|
|
if (vehicle == -1 ||
|
|
model()->GetCostClassIndexOfVehicle(vehicle).value() != cost_class) {
|
|
continue;
|
|
}
|
|
if (all_vehicles) {
|
|
if (!IsCheapestClassRepresentative(vehicle)) continue;
|
|
} else if (!vehicles.contains(vehicle)) {
|
|
continue;
|
|
}
|
|
AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::UpdateAfterNodeInsertion(
|
|
const SparseBitset<int>& nodes, int vehicle, int64_t node,
|
|
int64_t insert_after, bool all_vehicles, NodeEntryQueue* queue) {
|
|
// Update cost of existing entries after "insert_after" which now have new
|
|
// nexts.
|
|
if (!AddNodeEntriesAfter(nodes, vehicle, insert_after, all_vehicles, queue)) {
|
|
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'.
|
|
if (!AddNodeEntriesAfter(nodes, vehicle, node, all_vehicles, queue)) {
|
|
return false;
|
|
}
|
|
SetVehicleIndex(node, vehicle);
|
|
return true;
|
|
}
|
|
|
|
bool GlobalCheapestInsertionFilteredHeuristic::AddNodeEntriesAfter(
|
|
const SparseBitset<int>& nodes, int vehicle, int64_t insert_after,
|
|
bool all_vehicles, NodeEntryQueue* queue) {
|
|
temp_inserted_nodes_.ResetAllToFalse();
|
|
const int cost_class = model()->GetCostClassIndexOfVehicle(vehicle).value();
|
|
// Remove existing entries at 'insert_after', needed either when updating
|
|
// entries or if unperformed node insertions were present.
|
|
queue->ClearInsertions(insert_after);
|
|
|
|
const auto add_node_entries_for_neighbors =
|
|
[this, &nodes, &queue, insert_after, vehicle, all_vehicles](
|
|
absl::Span<const int> neighbors,
|
|
const std::function<bool(int64_t)>& is_neighbor) {
|
|
if (neighbors.size() < nodes.NumberOfSetCallsWithDifferentArguments()) {
|
|
// Iterate on the neighbors of 'node'.
|
|
for (int node : neighbors) {
|
|
if (StopSearch()) return false;
|
|
if (!Contains(node) && nodes[node] && !temp_inserted_nodes_[node]) {
|
|
temp_inserted_nodes_.Set(node);
|
|
AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue);
|
|
}
|
|
}
|
|
} else {
|
|
// Iterate on the nodes to insert.
|
|
for (int node : nodes.PositionsSetAtLeastOnce()) {
|
|
if (StopSearch()) return false;
|
|
if (!Contains(node) && is_neighbor(node) &&
|
|
!temp_inserted_nodes_[node]) {
|
|
temp_inserted_nodes_.Set(node);
|
|
AddNodeEntry(node, insert_after, vehicle, all_vehicles, queue);
|
|
}
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
if (!add_node_entries_for_neighbors(
|
|
node_index_to_neighbors_by_cost_class_
|
|
->GetOutgoingNeighborsOfNodeForCostClass(cost_class,
|
|
insert_after),
|
|
[this, insert_after, cost_class](int64_t node) {
|
|
return node_index_to_neighbors_by_cost_class_
|
|
->IsNeighborhoodArcForCostClass(cost_class, insert_after, node);
|
|
})) {
|
|
return false;
|
|
}
|
|
|
|
if (!node_index_to_neighbors_by_cost_class_->IsFullNeighborhood()) {
|
|
// When we have a non-full neighborhood, we also add entries for incoming
|
|
// neighbors of the successor of 'insert_after'.
|
|
const int64_t insert_before = Value(insert_after);
|
|
if (model()->IsEnd(insert_before)) {
|
|
// We don't explore incoming neighbors of an end node.
|
|
return true;
|
|
}
|
|
return add_node_entries_for_neighbors(
|
|
node_index_to_neighbors_by_cost_class_
|
|
->GetIncomingNeighborsOfNodeForCostClass(cost_class, insert_before),
|
|
[this, insert_before, cost_class](int64_t node) {
|
|
return node_index_to_neighbors_by_cost_class_
|
|
->IsNeighborhoodArcForCostClass(cost_class, node, insert_before);
|
|
});
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void GlobalCheapestInsertionFilteredHeuristic::AddNodeEntry(
|
|
int64_t node, int64_t insert_after, int vehicle, bool all_vehicles,
|
|
NodeEntryQueue* queue) {
|
|
const IntVar* const vehicle_var = model()->VehicleVar(node);
|
|
if (!vehicle_var->Contains(vehicle)) {
|
|
if (!UseEmptyVehicleTypeCuratorForVehicle(vehicle, all_vehicles)) return;
|
|
// We need to check there is not an equivalent empty vehicle the node
|
|
// could fit on.
|
|
const auto vehicle_is_compatible = [vehicle_var](int vehicle) {
|
|
return vehicle_var->Contains(vehicle);
|
|
};
|
|
if (!empty_vehicle_type_curator_->HasCompatibleVehicleOfType(
|
|
empty_vehicle_type_curator_->Type(vehicle),
|
|
vehicle_is_compatible)) {
|
|
return;
|
|
}
|
|
}
|
|
const int num_allowed_vehicles = vehicle_var->Size();
|
|
const int64_t node_penalty = GetUnperformedValue(node);
|
|
const int64_t penalty_shift =
|
|
absl::GetFlag(FLAGS_routing_shift_insertion_cost_by_penalty)
|
|
? node_penalty
|
|
: 0;
|
|
if (vehicle == -1) {
|
|
DCHECK_EQ(node, insert_after);
|
|
if (!all_vehicles) {
|
|
// NOTE: In the case where we're not considering all routes
|
|
// simultaneously, we don't add insertion entries making nodes
|
|
// unperformed.
|
|
return;
|
|
}
|
|
queue->PushInsertion(node, node, -1, num_allowed_vehicles,
|
|
CapSub(node_penalty, penalty_shift));
|
|
return;
|
|
}
|
|
|
|
const std::optional<int64_t> insertion_cost =
|
|
GetInsertionCostForNodeAtPosition(node, insert_after, Value(insert_after),
|
|
vehicle);
|
|
if (!insertion_cost.has_value()) return;
|
|
if (!all_vehicles && *insertion_cost > node_penalty) {
|
|
// NOTE: When all vehicles aren't considered for insertion, we don't
|
|
// add entries making nodes unperformed, so we don't add insertions
|
|
// which cost more than the node penalty either.
|
|
return;
|
|
}
|
|
queue->PushInsertion(node, insert_after, vehicle, num_allowed_vehicles,
|
|
CapSub(*insertion_cost, penalty_shift));
|
|
}
|
|
|
|
void InsertionSequenceGenerator::AppendPickupDeliveryMultitourInsertions(
|
|
int pickup, int delivery, int vehicle, absl::Span<const int> path,
|
|
const std::vector<bool>& path_node_is_pickup,
|
|
const std::vector<bool>& path_node_is_delivery,
|
|
InsertionSequenceContainer& insertions) {
|
|
const int num_nodes = path.size();
|
|
DCHECK_GE(num_nodes, 2);
|
|
const int kNoPrevIncrease = -1;
|
|
const int kNoNextDecrease = num_nodes;
|
|
{
|
|
prev_decrease_.resize(num_nodes - 1);
|
|
prev_increase_.resize(num_nodes - 1);
|
|
int prev_decrease = 0;
|
|
int prev_increase = kNoPrevIncrease;
|
|
for (int pos = 0; pos < num_nodes - 1; ++pos) {
|
|
if (path_node_is_delivery[pos]) prev_decrease = pos;
|
|
prev_decrease_[pos] = prev_decrease;
|
|
if (path_node_is_pickup[pos]) prev_increase = pos;
|
|
prev_increase_[pos] = prev_increase;
|
|
}
|
|
}
|
|
{
|
|
next_decrease_.resize(num_nodes - 1);
|
|
next_increase_.resize(num_nodes - 1);
|
|
int next_increase = num_nodes - 1;
|
|
int next_decrease = kNoNextDecrease;
|
|
for (int pos = num_nodes - 2; pos >= 0; --pos) {
|
|
next_decrease_[pos] = next_decrease;
|
|
if (path_node_is_delivery[pos]) next_decrease = pos;
|
|
next_increase_[pos] = next_increase;
|
|
if (path_node_is_pickup[pos]) next_increase = pos;
|
|
}
|
|
}
|
|
|
|
auto append = [pickup, delivery, vehicle, num_nodes, path, &insertions](
|
|
int pickup_pos, int delivery_pos) {
|
|
if (pickup_pos < 0 || num_nodes - 1 <= pickup_pos) return;
|
|
if (delivery_pos < 0 || num_nodes - 1 <= delivery_pos) return;
|
|
const int delivery_pred =
|
|
pickup_pos == delivery_pos ? pickup : path[delivery_pos];
|
|
insertions.AddInsertionSequence(
|
|
vehicle, {{.pred = path[pickup_pos], .node = pickup},
|
|
{.pred = delivery_pred, .node = delivery}});
|
|
};
|
|
|
|
// Find insertion positions for the input pair, pickup P and delivery D.
|
|
for (int pos = 0; pos < num_nodes - 1; ++pos) {
|
|
const bool is_after_decrease = prev_increase_[pos] < prev_decrease_[pos];
|
|
const bool is_before_increase = next_increase_[pos] < next_decrease_[pos];
|
|
if (is_after_decrease) {
|
|
append(prev_increase_[pos], pos);
|
|
if (is_before_increase) { // Upwards inflexion: vehicle is empty.
|
|
append(pos, next_increase_[pos] - 1);
|
|
append(pos, next_decrease_[pos] - 1);
|
|
// Avoids duplicate insertions. If next_increase_[pos] - 1 == pos:
|
|
// - append(pos, pos) is append(pos, next_increase_[pos] - 1)
|
|
// - because is_after_decrease,
|
|
// with pos' = prev_decrease_[pos],
|
|
// next_increase_[pos'] == next_increase_[pos], so that
|
|
// append(prev_decrease_[pos], pos) is
|
|
// append(pos', next_increase_[pos'] - 1).
|
|
if (next_increase_[pos] - 1 != pos) {
|
|
append(pos, pos);
|
|
if (prev_decrease_[pos] != pos) append(prev_decrease_[pos], pos);
|
|
}
|
|
}
|
|
} else {
|
|
append(pos, next_decrease_[pos] - 1);
|
|
if (!is_before_increase && next_decrease_[pos] - 1 != pos) {
|
|
// Downwards inflexion: vehicle is at its max.
|
|
// Avoids duplicate insertions, when next_decrease_[pos] - 1 == pos:
|
|
// - append(pos, pos) is append(pos, next_decrease_[pos] - 1)
|
|
// - because is_before_increase, with pos' = prev_increase_[pos],
|
|
// next_decrease_[pos'] == next_decrease_[pos], so that
|
|
// append(prev_increase_[pos], pos) is
|
|
// append(pos', next_decrease_[pos'] - 1).
|
|
append(pos, pos);
|
|
if (prev_increase_[pos] != pos) append(prev_increase_[pos], pos);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// LocalCheapestInsertionFilteredHeuristic
|
|
// TODO(user): Add support for penalty costs.
|
|
LocalCheapestInsertionFilteredHeuristic::
|
|
LocalCheapestInsertionFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
std::function<int64_t(int64_t, int64_t, int64_t)> evaluator,
|
|
LocalCheapestInsertionParameters lci_params,
|
|
LocalSearchFilterManager* filter_manager, bool use_first_solution_hint,
|
|
BinCapacities* bin_capacities,
|
|
std::function<bool(const std::vector<RoutingModel::VariableValuePair>&,
|
|
std::vector<RoutingModel::VariableValuePair>*)>
|
|
optimize_on_insertion)
|
|
: CheapestInsertionFilteredHeuristic(model, std::move(stop_search),
|
|
std::move(evaluator), nullptr,
|
|
filter_manager),
|
|
pair_insertion_strategy_(lci_params.pickup_delivery_strategy()),
|
|
insertion_sorting_properties_(GetLocalCheapestInsertionSortingProperties(
|
|
lci_params.insertion_sorting_properties())),
|
|
use_first_solution_hint_(use_first_solution_hint),
|
|
bin_capacities_(bin_capacities),
|
|
optimize_on_insertion_(std::move(optimize_on_insertion)),
|
|
use_random_insertion_order_(
|
|
!insertion_sorting_properties_.empty() &&
|
|
insertion_sorting_properties_.front() ==
|
|
LocalCheapestInsertionParameters::SORTING_PROPERTY_RANDOM) {
|
|
DCHECK(!insertion_sorting_properties_.empty());
|
|
}
|
|
|
|
void LocalCheapestInsertionFilteredHeuristic::Initialize() {
|
|
// NOTE(user): Keeping the code in a separate function as opposed to
|
|
// inlining here, to allow for future additions to this function.
|
|
synchronize_insertion_optimizer_ = true;
|
|
hint_next_values_.assign(model()->Size(), -1);
|
|
hint_prev_values_.assign(model()->Size() + model()->vehicles(), -1);
|
|
const Assignment* hint = model()->GetFirstSolutionHint();
|
|
if (hint != nullptr && use_first_solution_hint_) {
|
|
const Assignment::IntContainer& container = hint->IntVarContainer();
|
|
for (int i = 0; i < model()->Nexts().size(); ++i) {
|
|
const IntVarElement* element =
|
|
container.ElementPtrOrNull(model()->NextVar(i));
|
|
if (element != nullptr && element->Bound()) {
|
|
hint_next_values_[i] = element->Value();
|
|
hint_prev_values_[element->Value()] = i;
|
|
}
|
|
}
|
|
}
|
|
ComputeInsertionOrder();
|
|
}
|
|
|
|
bool LocalCheapestInsertionFilteredHeuristic::OptimizeOnInsertion(
|
|
std::vector<int> delta_indices) {
|
|
if (optimize_on_insertion_ == nullptr) return false;
|
|
std::vector<RoutingModel::VariableValuePair> in_state;
|
|
if (synchronize_insertion_optimizer_) {
|
|
for (int i = 0; i < model()->Nexts().size(); ++i) {
|
|
if (Contains(i)) {
|
|
in_state.push_back({i, Value(i)});
|
|
}
|
|
}
|
|
synchronize_insertion_optimizer_ = false;
|
|
} else {
|
|
for (int index : delta_indices) {
|
|
in_state.push_back({index, Value(index)});
|
|
}
|
|
}
|
|
std::vector<RoutingModel::VariableValuePair> out_state;
|
|
optimize_on_insertion_(in_state, &out_state);
|
|
if (out_state.empty()) return false;
|
|
for (const auto& [var, value] : out_state) {
|
|
if (Contains(var)) {
|
|
SetValue(var, value);
|
|
}
|
|
}
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
}
|
|
|
|
namespace {
|
|
// Computes the cost from vehicle starts to pickups.
|
|
std::vector<std::vector<int64_t>> ComputeStartToPickupCosts(
|
|
const RoutingModel& model, absl::Span<const int64_t> pickups,
|
|
const Bitset64<int>& vehicle_set) {
|
|
std::vector<std::vector<int64_t>> pickup_costs(model.Size());
|
|
for (int64_t pickup : pickups) {
|
|
std::vector<int64_t>& cost_from_start = pickup_costs[pickup];
|
|
cost_from_start.resize(model.vehicles(), -1);
|
|
|
|
ProcessVehicleStartEndCosts(
|
|
model, pickup,
|
|
[&cost_from_start](int64_t cost, int v) { cost_from_start[v] = cost; },
|
|
vehicle_set, /*ignore_start=*/false, /*ignore_end=*/true);
|
|
}
|
|
return pickup_costs;
|
|
}
|
|
|
|
// Computes the cost from deliveries to vehicle ends.
|
|
std::vector<std::vector<int64_t>> ComputeDeliveryToEndCosts(
|
|
const RoutingModel& model, absl::Span<const int64_t> deliveries,
|
|
const Bitset64<int>& vehicle_set) {
|
|
std::vector<std::vector<int64_t>> delivery_costs(model.Size());
|
|
for (int64_t delivery : deliveries) {
|
|
std::vector<int64_t>& cost_to_end = delivery_costs[delivery];
|
|
cost_to_end.resize(model.vehicles(), -1);
|
|
|
|
ProcessVehicleStartEndCosts(
|
|
model, delivery,
|
|
[&cost_to_end](int64_t cost, int v) { cost_to_end[v] = cost; },
|
|
vehicle_set, /*ignore_start=*/true, /*ignore_end=*/false);
|
|
}
|
|
return delivery_costs;
|
|
}
|
|
|
|
// Returns the opposite of the maximum cost between all pickup/delivery nodes of
|
|
// the given pair from their "closest" vehicle.
|
|
// For a given pickup/delivery, the cost from the closest vehicle is defined as
|
|
// min_{v in vehicles}(ArcCost(start[v]->pickup) + ArcCost(delivery->end[v])).
|
|
int64_t GetNegMaxDistanceFromVehicles(const RoutingModel& model,
|
|
int pair_index) {
|
|
const auto& [pickups, deliveries] =
|
|
model.GetPickupAndDeliveryPairs()[pair_index];
|
|
|
|
Bitset64<int> vehicle_set(model.vehicles());
|
|
for (int v = 0; v < model.vehicles(); ++v) vehicle_set.Set(v);
|
|
|
|
// Precompute the cost from vehicle starts to every pickup in the pair.
|
|
const std::vector<std::vector<int64_t>> pickup_costs =
|
|
ComputeStartToPickupCosts(model, pickups, vehicle_set);
|
|
|
|
// Precompute the cost from every delivery in the pair to vehicle ends.
|
|
const std::vector<std::vector<int64_t>> delivery_costs =
|
|
ComputeDeliveryToEndCosts(model, deliveries, vehicle_set);
|
|
|
|
int64_t max_pair_distance = 0;
|
|
for (int64_t pickup : pickups) {
|
|
const std::vector<int64_t>& cost_from_start = pickup_costs[pickup];
|
|
for (int64_t delivery : deliveries) {
|
|
const std::vector<int64_t>& cost_to_end = delivery_costs[delivery];
|
|
int64_t closest_vehicle_distance = std::numeric_limits<int64_t>::max();
|
|
for (int v = 0; v < model.vehicles(); v++) {
|
|
if (cost_from_start[v] < 0 || cost_to_end[v] < 0) {
|
|
// Vehicle not in the pickup and/or delivery's vehicle var domain.
|
|
continue;
|
|
}
|
|
closest_vehicle_distance =
|
|
std::min(closest_vehicle_distance,
|
|
CapAdd(cost_from_start[v], cost_to_end[v]));
|
|
}
|
|
max_pair_distance = std::max(max_pair_distance, closest_vehicle_distance);
|
|
}
|
|
}
|
|
return CapOpp(max_pair_distance);
|
|
}
|
|
|
|
// Returns the average distance between all pickup/delivery nodes of the given
|
|
// pair from all vehicles where they can be serverd. Returns an empty optional
|
|
// if no vehicle can serve the pair. For a given pickup/delivery, the distance
|
|
// from a vehicle v is defined as ArcCost(start[v]->pickup) +
|
|
// ArcCost(delivery->end[v]).
|
|
std::optional<int64_t> GetAvgPickupDeliveryPairDistanceFromVehicles(
|
|
const RoutingModel& model, int pair_index,
|
|
const Bitset64<int>& vehicle_set) {
|
|
const auto& [pickups, deliveries] =
|
|
model.GetPickupAndDeliveryPairs()[pair_index];
|
|
|
|
// Precompute the cost from vehicle starts to every pickup in the pair.
|
|
const std::vector<std::vector<int64_t>> pickup_costs =
|
|
ComputeStartToPickupCosts(model, pickups, vehicle_set);
|
|
|
|
// Precompute the cost from every delivery in the pair to vehicle ends.
|
|
const std::vector<std::vector<int64_t>> delivery_costs =
|
|
ComputeDeliveryToEndCosts(model, deliveries, vehicle_set);
|
|
|
|
int64_t avg_pair_distance_sum = 0;
|
|
int valid_pairs = 0;
|
|
for (int64_t pickup : pickups) {
|
|
const std::vector<int64_t>& cost_from_start = pickup_costs[pickup];
|
|
for (int64_t delivery : deliveries) {
|
|
const std::vector<int64_t>& cost_to_end = delivery_costs[delivery];
|
|
int64_t distance_from_vehicle_sum = 0;
|
|
int valid_vehicles = 0;
|
|
for (int v = 0; v < model.vehicles(); v++) {
|
|
if (cost_from_start[v] < 0 || cost_to_end[v] < 0) {
|
|
// Vehicle not in the pickup and/or delivery's vehicle var domain.
|
|
continue;
|
|
}
|
|
distance_from_vehicle_sum =
|
|
CapAdd(distance_from_vehicle_sum,
|
|
CapAdd(cost_from_start[v], cost_to_end[v]));
|
|
++valid_vehicles;
|
|
}
|
|
if (valid_vehicles > 0) {
|
|
avg_pair_distance_sum = CapAdd(
|
|
distance_from_vehicle_sum / valid_vehicles, avg_pair_distance_sum);
|
|
++valid_pairs;
|
|
}
|
|
}
|
|
}
|
|
return valid_pairs > 0
|
|
? std::make_optional<int64_t>(avg_pair_distance_sum / valid_pairs)
|
|
: std::nullopt;
|
|
}
|
|
|
|
// Returns the maximum vehicle capacity for the given dimensions. Vehicles with
|
|
// infinite capacity are ignored.
|
|
int64_t GetMaxFiniteDimensionCapacity(
|
|
const RoutingModel& model,
|
|
const std::vector<RoutingDimension*>& dimensions) {
|
|
int64_t max_capacity = 0;
|
|
for (const RoutingDimension* dimension : dimensions) {
|
|
for (int vehicle = 0; vehicle < model.vehicles(); ++vehicle) {
|
|
const int64_t capacity = dimension->vehicle_capacities()[vehicle];
|
|
if (capacity == kint64max) continue;
|
|
max_capacity = std::max(max_capacity, capacity);
|
|
}
|
|
}
|
|
return max_capacity;
|
|
}
|
|
|
|
// Returns the average dimension usage of the given node scaled according to the
|
|
// given max capacity.
|
|
int64_t GetAvgNodeUnaryDimensionUsage(
|
|
const RoutingModel& model, const std::vector<RoutingDimension*>& dimensions,
|
|
int64_t max_vehicle_capacity, int64_t node) {
|
|
if (dimensions.empty() || max_vehicle_capacity == 0) {
|
|
return 0;
|
|
}
|
|
|
|
double dimension_usage_sum = 0;
|
|
for (const RoutingDimension* dimension : dimensions) {
|
|
// TODO(user): extend to non unary dimensions.
|
|
DCHECK(dimension->IsUnary());
|
|
double dimension_usage_per_vehicle_sum = 0;
|
|
int valid_vehicles = 0;
|
|
|
|
// TODO(user): limit computations to vehicles allowed for this node.
|
|
for (int vehicle = 0; vehicle < model.vehicles(); ++vehicle) {
|
|
const int64_t capacity = dimension->vehicle_capacities()[vehicle];
|
|
if (capacity == 0) continue;
|
|
DCHECK_GE(capacity, 0);
|
|
valid_vehicles++;
|
|
// If the capacity is infinite, bypass the "noise" added to the dimension
|
|
// usage.
|
|
if (capacity == kint64max) continue;
|
|
const RoutingModel::TransitCallback1& transit_evaluator =
|
|
dimension->GetUnaryTransitEvaluator(vehicle);
|
|
dimension_usage_per_vehicle_sum +=
|
|
1.0 * std::abs(transit_evaluator(node)) / capacity;
|
|
}
|
|
|
|
if (valid_vehicles > 0) {
|
|
// Cap the dimension usage to 1.0 in case some node demand is greater than
|
|
// the vehicle capacity.
|
|
dimension_usage_sum +=
|
|
std::min(1.0, dimension_usage_per_vehicle_sum / valid_vehicles);
|
|
}
|
|
}
|
|
|
|
// We multiply the computed dimension_usage_sum by the max_vehicle_capacity to
|
|
// have a larger scale for rounding to int64_t.
|
|
return MathUtil::SafeRound<int64_t>(max_vehicle_capacity *
|
|
dimension_usage_sum);
|
|
}
|
|
|
|
// Returns the average dimension usage of a pickup and delivery pair scaled
|
|
// according to the given max capacity.
|
|
int64_t GetAvgPickupDeliveryPairUnaryDimensionUsage(
|
|
const RoutingModel& model, const std::vector<RoutingDimension*>& dimensions,
|
|
int64_t max_vehicle_capacity, int pair_index) {
|
|
if (dimensions.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
const auto& [pickups, deliveries] =
|
|
model.GetPickupAndDeliveryPairs()[pair_index];
|
|
if (pickups.empty() || deliveries.empty()) {
|
|
return 0;
|
|
}
|
|
|
|
double dimension_usage_sum = 0;
|
|
for (const RoutingDimension* dimension : dimensions) {
|
|
double dimension_usage_per_vehicle_sum = 0;
|
|
int valid_vehicles = 0;
|
|
// TODO(user): limit computations to nodes allowed for this vehicle.
|
|
for (int vehicle = 0; vehicle < model.vehicles(); ++vehicle) {
|
|
const int64_t capacity = dimension->vehicle_capacities()[vehicle];
|
|
if (capacity == 0) continue;
|
|
valid_vehicles++;
|
|
// If the capacity is infinite, bypass the "noise" added to the dimension
|
|
// usage.
|
|
if (capacity == kint64max) continue;
|
|
const RoutingModel::TransitCallback1& transit_evaluator =
|
|
dimension->GetUnaryTransitEvaluator(vehicle);
|
|
|
|
double pickup_sum = 0;
|
|
for (int64_t pickup : pickups) {
|
|
pickup_sum += 1.0 * std::abs(transit_evaluator(pickup)) / capacity;
|
|
}
|
|
const double avg_pickup_usage = pickup_sum / pickups.size();
|
|
double delivery_sum = 0;
|
|
for (int64_t delivery : deliveries) {
|
|
delivery_sum += 1.0 * std::abs(transit_evaluator(delivery)) / capacity;
|
|
}
|
|
const double avg_delivery_usage = delivery_sum / deliveries.size();
|
|
dimension_usage_per_vehicle_sum +=
|
|
std::max(avg_pickup_usage, avg_delivery_usage);
|
|
}
|
|
|
|
if (valid_vehicles > 0) {
|
|
// Cap the dimension usage to 1.0 in case some node demand is greater than
|
|
// the vehicle capacity.
|
|
dimension_usage_sum +=
|
|
std::min(1.0, dimension_usage_per_vehicle_sum / valid_vehicles);
|
|
}
|
|
}
|
|
|
|
// We multiply the computed dimension_usage_sum by the max_vehicle_capacity to
|
|
// have a larger scale for rounding to int64_t.
|
|
return MathUtil::SafeRound<int64_t>(max_vehicle_capacity *
|
|
dimension_usage_sum);
|
|
}
|
|
} // namespace
|
|
|
|
void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() {
|
|
if (use_random_insertion_order_) {
|
|
if (insertion_order_.empty()) {
|
|
const RoutingModel& model = *this->model();
|
|
insertion_order_.reserve(model.Size() +
|
|
model.GetPickupAndDeliveryPairs().size());
|
|
for (int pair_index = 0;
|
|
pair_index < model.GetPickupAndDeliveryPairs().size();
|
|
++pair_index) {
|
|
insertion_order_.push_back(
|
|
{.is_node_index = false, .index = pair_index});
|
|
}
|
|
for (int node = 0; node < model.Size(); ++node) {
|
|
if (model.IsStart(node) || model.IsEnd(node)) continue;
|
|
insertion_order_.push_back({.is_node_index = true, .index = node});
|
|
}
|
|
}
|
|
// We disregard all other properties and sort nodes/pairs in a random order.
|
|
absl::c_shuffle(insertion_order_, rnd_);
|
|
return;
|
|
}
|
|
|
|
if (!insertion_order_.empty()) return;
|
|
|
|
// We consider pairs and single nodes simultaneously, to make sure the most
|
|
// critical ones (fewer allowed vehicles and high penalties) get inserted
|
|
// first.
|
|
// TODO(user): Explore other metrics to evaluate criticality, more directly
|
|
// mixing penalties and allowed vehicles (such as a ratio between the two).
|
|
|
|
const RoutingModel& model = *this->model();
|
|
insertion_order_.reserve(model.Size() +
|
|
model.GetPickupAndDeliveryPairs().size());
|
|
|
|
auto get_insertion_properties =
|
|
[this](int64_t penalty, int64_t num_allowed_vehicles,
|
|
int64_t avg_distance_to_vehicle,
|
|
int64_t neg_min_distance_to_vehicles, int hint_weight,
|
|
int reversed_hint_weight, int64_t avg_dimension_usage) {
|
|
DCHECK_NE(0, num_allowed_vehicles);
|
|
absl::InlinedVector<int64_t, 8> properties;
|
|
properties.reserve(insertion_sorting_properties_.size());
|
|
|
|
// Always consider hints first. We favor nodes with hints over nodes
|
|
// with reversed hints.
|
|
// TODO(user): Figure out a way to insert hinted nodes in a logical
|
|
// order. We could try toposorting hinted nodes.
|
|
properties.push_back(-hint_weight);
|
|
properties.push_back(-reversed_hint_weight);
|
|
|
|
bool neg_min_distance_to_vehicles_appended = false;
|
|
for (const int property : insertion_sorting_properties_) {
|
|
switch (property) {
|
|
case LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_ALLOWED_VEHICLES:
|
|
properties.push_back(num_allowed_vehicles);
|
|
break;
|
|
case LocalCheapestInsertionParameters::SORTING_PROPERTY_PENALTY:
|
|
properties.push_back(CapOpp(penalty));
|
|
break;
|
|
case LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_PENALTY_OVER_ALLOWED_VEHICLES_RATIO:
|
|
properties.push_back(CapOpp(penalty / num_allowed_vehicles));
|
|
break;
|
|
case LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS:
|
|
properties.push_back(CapOpp(avg_distance_to_vehicle));
|
|
break;
|
|
case LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS:
|
|
properties.push_back(avg_distance_to_vehicle);
|
|
break;
|
|
case LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS:
|
|
properties.push_back(neg_min_distance_to_vehicles);
|
|
neg_min_distance_to_vehicles_appended = true;
|
|
break;
|
|
case LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE:
|
|
properties.push_back(CapOpp(avg_dimension_usage));
|
|
break;
|
|
default:
|
|
LOG(DFATAL)
|
|
<< "Unknown RoutingSearchParameter::InsertionSortingProperty "
|
|
"used!";
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Historically the negative max distance to vehicles has always been
|
|
// considered to be the last property in the hierarchy defining how
|
|
// nodes are sorted for the LCI heuristic, so we add it here iff it
|
|
// wasn't added before
|
|
if (!neg_min_distance_to_vehicles_appended) {
|
|
properties.push_back(neg_min_distance_to_vehicles);
|
|
}
|
|
|
|
return properties;
|
|
};
|
|
|
|
// Identify whether the selected properties require a more expensive
|
|
// preprocessing.
|
|
bool compute_avg_pickup_delivery_pair_distance_from_vehicles = false;
|
|
bool compute_avg_dimension_usage = false;
|
|
for (const LocalCheapestInsertionParameters::InsertionSortingProperty
|
|
property : insertion_sorting_properties_) {
|
|
if (property ==
|
|
LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_HIGHEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS ||
|
|
property ==
|
|
LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS) {
|
|
compute_avg_pickup_delivery_pair_distance_from_vehicles = true;
|
|
}
|
|
if (property == LocalCheapestInsertionParameters::
|
|
SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE) {
|
|
compute_avg_dimension_usage = true;
|
|
}
|
|
}
|
|
|
|
Bitset64<int> vehicle_set(model.vehicles());
|
|
for (int v = 0; v < model.vehicles(); ++v) vehicle_set.Set(v);
|
|
|
|
const std::vector<RoutingDimension*> unary_dimensions =
|
|
compute_avg_dimension_usage ? model.GetUnaryDimensions()
|
|
: std::vector<RoutingDimension*>();
|
|
|
|
const int64_t max_dimension_capacity =
|
|
compute_avg_dimension_usage
|
|
? GetMaxFiniteDimensionCapacity(model, unary_dimensions)
|
|
: 0;
|
|
|
|
// Iterating on pickup and delivery pairs.
|
|
const std::vector<PickupDeliveryPair>& pairs =
|
|
model.GetPickupAndDeliveryPairs();
|
|
|
|
for (int pair_index = 0; pair_index < pairs.size(); ++pair_index) {
|
|
const auto& [pickups, deliveries] = pairs[pair_index];
|
|
int64_t num_allowed_vehicles = std::numeric_limits<int64_t>::max();
|
|
int64_t pickup_penalty = 0;
|
|
int hint_weight = 0;
|
|
int reversed_hint_weight = 0;
|
|
for (int64_t pickup : pickups) {
|
|
num_allowed_vehicles =
|
|
std::min(num_allowed_vehicles,
|
|
static_cast<int64_t>(model.VehicleVar(pickup)->Size()));
|
|
pickup_penalty =
|
|
std::max(pickup_penalty, model.UnperformedPenalty(pickup));
|
|
hint_weight += HasHintedNext(pickup);
|
|
reversed_hint_weight += HasHintedPrev(pickup);
|
|
}
|
|
int64_t delivery_penalty = 0;
|
|
for (int64_t delivery : deliveries) {
|
|
num_allowed_vehicles =
|
|
std::min(num_allowed_vehicles,
|
|
static_cast<int64_t>(model.VehicleVar(delivery)->Size()));
|
|
delivery_penalty =
|
|
std::max(delivery_penalty, model.UnperformedPenalty(delivery));
|
|
hint_weight += HasHintedNext(delivery);
|
|
reversed_hint_weight += HasHintedPrev(delivery);
|
|
}
|
|
|
|
const std::optional<int64_t> maybe_avg_pair_to_vehicle_cost =
|
|
compute_avg_pickup_delivery_pair_distance_from_vehicles
|
|
? GetAvgPickupDeliveryPairDistanceFromVehicles(model, pair_index,
|
|
vehicle_set)
|
|
: std::make_optional<int64_t>(0);
|
|
|
|
if (!maybe_avg_pair_to_vehicle_cost.has_value()) {
|
|
// There is no vehicle that can serve the pickup/delivery nodes in the
|
|
// current pair index.
|
|
continue;
|
|
}
|
|
|
|
const int64_t avg_pair_dimension_usage =
|
|
compute_avg_dimension_usage
|
|
? GetAvgPickupDeliveryPairUnaryDimensionUsage(
|
|
model, unary_dimensions, max_dimension_capacity, pair_index)
|
|
: 0;
|
|
|
|
absl::InlinedVector<int64_t, 8> properties = get_insertion_properties(
|
|
CapAdd(pickup_penalty, delivery_penalty), num_allowed_vehicles,
|
|
maybe_avg_pair_to_vehicle_cost.value(),
|
|
GetNegMaxDistanceFromVehicles(model, pair_index), hint_weight,
|
|
reversed_hint_weight, avg_pair_dimension_usage);
|
|
|
|
insertion_order_.push_back({.properties = std::move(properties),
|
|
.vehicle = 0,
|
|
.is_node_index = false,
|
|
.index = pair_index});
|
|
}
|
|
|
|
for (int node = 0; node < model.Size(); ++node) {
|
|
if (model.IsStart(node) || model.IsEnd(node)) continue;
|
|
|
|
int64_t min_distance = std::numeric_limits<int64_t>::max();
|
|
int64_t sum_distance = 0;
|
|
ProcessVehicleStartEndCosts(
|
|
model, node,
|
|
[&min_distance, &sum_distance](int64_t dist, int) {
|
|
min_distance = std::min(min_distance, dist);
|
|
sum_distance = CapAdd(dist, sum_distance);
|
|
},
|
|
vehicle_set);
|
|
DCHECK_GT(vehicle_set.size(), 0);
|
|
|
|
const int64_t avg_dimension_usage =
|
|
compute_avg_dimension_usage
|
|
? GetAvgNodeUnaryDimensionUsage(model, unary_dimensions,
|
|
max_dimension_capacity, node)
|
|
: 0;
|
|
|
|
absl::InlinedVector<int64_t, 8> properties = get_insertion_properties(
|
|
model.UnperformedPenalty(node), model.VehicleVar(node)->Size(),
|
|
sum_distance / vehicle_set.size(), CapOpp(min_distance),
|
|
HasHintedNext(node), HasHintedPrev(node), avg_dimension_usage);
|
|
insertion_order_.push_back({.properties = std::move(properties),
|
|
.vehicle = 0,
|
|
.is_node_index = true,
|
|
.index = node});
|
|
}
|
|
|
|
absl::c_sort(insertion_order_, std::greater<Seed>());
|
|
absl::c_reverse(insertion_order_);
|
|
}
|
|
|
|
bool LocalCheapestInsertionFilteredHeuristic::InsertPair(
|
|
int64_t pickup, int64_t insert_pickup_after, int64_t delivery,
|
|
int64_t insert_delivery_after, int vehicle) {
|
|
const int64_t insert_pickup_before = Value(insert_pickup_after);
|
|
InsertBetween(pickup, insert_pickup_after, insert_pickup_before, vehicle);
|
|
DCHECK_NE(insert_delivery_after, insert_pickup_after);
|
|
const int64_t insert_delivery_before = (insert_delivery_after == pickup)
|
|
? insert_pickup_before
|
|
: Value(insert_delivery_after);
|
|
InsertBetween(delivery, insert_delivery_after, insert_delivery_before,
|
|
vehicle);
|
|
// Capturing the state of the delta before it gets wiped by Evaluate.
|
|
std::vector<int> indices = delta_indices();
|
|
if (Evaluate(/*commit=*/true, /*ignore_upper_bound=*/true).has_value()) {
|
|
OptimizeOnInsertion(std::move(indices));
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void LocalCheapestInsertionFilteredHeuristic::InsertBestPickupThenDelivery(
|
|
const PickupDeliveryPair& index_pair) {
|
|
for (int pickup : index_pair.pickup_alternatives) {
|
|
std::vector<NodeInsertion> pickup_insertions =
|
|
ComputeEvaluatorSortedPositions(pickup);
|
|
for (int delivery : index_pair.delivery_alternatives) {
|
|
if (StopSearch()) return;
|
|
for (const NodeInsertion& pickup_insertion : pickup_insertions) {
|
|
const int vehicle = pickup_insertion.vehicle;
|
|
if (!model()->VehicleVar(delivery)->Contains(vehicle)) continue;
|
|
if (MustUpdateBinCapacities() &&
|
|
!bin_capacities_->CheckAdditionsFeasibility({pickup, delivery},
|
|
vehicle)) {
|
|
continue;
|
|
}
|
|
for (const NodeInsertion& delivery_insertion :
|
|
ComputeEvaluatorSortedPositionsOnRouteAfter(
|
|
delivery, pickup, Value(pickup_insertion.insert_after),
|
|
vehicle)) {
|
|
if (InsertPair(pickup, pickup_insertion.insert_after, delivery,
|
|
delivery_insertion.insert_after, vehicle)) {
|
|
if (MustUpdateBinCapacities()) {
|
|
bin_capacities_->AddItemToBin(pickup, vehicle);
|
|
bin_capacities_->AddItemToBin(delivery, vehicle);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (StopSearch()) return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LocalCheapestInsertionFilteredHeuristic::InsertBestPair(
|
|
const PickupDeliveryPair& pair) {
|
|
for (int pickup : pair.pickup_alternatives) {
|
|
for (int delivery : pair.delivery_alternatives) {
|
|
if (StopSearch()) return;
|
|
std::vector<PickupDeliveryInsertion> sorted_pair_positions =
|
|
ComputeEvaluatorSortedPairPositions(pickup, delivery);
|
|
if (sorted_pair_positions.empty()) continue;
|
|
for (const auto [insert_pickup_after, insert_delivery_after,
|
|
unused_hint_weight, unused_value, vehicle] :
|
|
sorted_pair_positions) {
|
|
if (InsertPair(pickup, insert_pickup_after, delivery,
|
|
insert_delivery_after, vehicle)) {
|
|
if (MustUpdateBinCapacities()) {
|
|
bin_capacities_->AddItemToBin(pickup, vehicle);
|
|
bin_capacities_->AddItemToBin(delivery, vehicle);
|
|
}
|
|
return;
|
|
}
|
|
if (StopSearch()) return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour(
|
|
const PickupDeliveryPair& pair) {
|
|
using InsertionSequence = InsertionSequenceContainer::InsertionSequence;
|
|
using Insertion = InsertionSequenceContainer::Insertion;
|
|
|
|
std::vector<int> path;
|
|
std::vector<bool> path_node_is_pickup;
|
|
std::vector<bool> path_node_is_delivery;
|
|
// Fills path with all nodes visited by vehicle, including start/end.
|
|
auto fill_path = [&path, &path_node_is_pickup, &path_node_is_delivery,
|
|
this](int vehicle) {
|
|
path.clear();
|
|
path_node_is_pickup.clear();
|
|
path_node_is_delivery.clear();
|
|
const int start = model()->Start(vehicle);
|
|
const int end = model()->End(vehicle);
|
|
for (int node = start; node != end; node = Value(node)) {
|
|
path.push_back(node);
|
|
path_node_is_pickup.push_back(model()->IsPickup(node));
|
|
path_node_is_delivery.push_back(model()->IsDelivery(node));
|
|
}
|
|
path.push_back(end);
|
|
};
|
|
|
|
// Fills value field of all insertions, kint64max if unevaluable.
|
|
auto price_insertion_sequences = [this]() {
|
|
for (InsertionSequence sequence : insertion_container_) {
|
|
int64_t sequence_cost = 0;
|
|
int previous_node = -1;
|
|
int previous_succ = -1;
|
|
int hint_weight = 0;
|
|
for (const Insertion& insertion : sequence) {
|
|
const int succ = previous_node == insertion.pred
|
|
? previous_succ
|
|
: Value(insertion.pred);
|
|
hint_weight += IsHint(insertion.pred, insertion.node);
|
|
hint_weight += IsHint(insertion.node, succ);
|
|
if (evaluator_ == nullptr) {
|
|
InsertBetween(insertion.node, insertion.pred, succ,
|
|
sequence.Vehicle());
|
|
} else {
|
|
const auto cost = GetEvaluatorInsertionCostForNodeAtPosition(
|
|
insertion.node, insertion.pred, succ, sequence.Vehicle());
|
|
CapAddTo(cost, &sequence_cost);
|
|
}
|
|
previous_node = insertion.node;
|
|
previous_succ = succ;
|
|
}
|
|
if (evaluator_ == nullptr) {
|
|
sequence_cost =
|
|
Evaluate(/*commit=*/false, /*ignore_upper_bound=*/hint_weight > 0)
|
|
.value_or(kint64max);
|
|
}
|
|
sequence.Cost() = sequence_cost;
|
|
sequence.SetHintWeight(hint_weight);
|
|
}
|
|
if (bin_capacities_ == nullptr || evaluator_ == nullptr) return;
|
|
for (InsertionSequence sequence : insertion_container_) {
|
|
const int64_t old_cost = bin_capacities_->TotalCost();
|
|
for (const Insertion& insertion : sequence) {
|
|
bin_capacities_->AddItemToBin(insertion.node, sequence.Vehicle());
|
|
}
|
|
const int64_t new_cost = bin_capacities_->TotalCost();
|
|
const int64_t delta_cost = CapSub(new_cost, old_cost);
|
|
CapAddTo(delta_cost, &sequence.Cost());
|
|
for (const Insertion& insertion : sequence) {
|
|
bin_capacities_->RemoveItemFromBin(insertion.node, sequence.Vehicle());
|
|
}
|
|
}
|
|
};
|
|
|
|
for (int pickup : pair.pickup_alternatives) {
|
|
const IntVar* pickup_vehicle_var = model()->VehicleVar(pickup);
|
|
if (StopSearch()) return;
|
|
for (int delivery : pair.delivery_alternatives) {
|
|
const IntVar* delivery_vehicle_var = model()->VehicleVar(delivery);
|
|
insertion_container_.Clear();
|
|
std::unique_ptr<IntVarIterator> pickup_vehicles(
|
|
pickup_vehicle_var->MakeDomainIterator(false));
|
|
for (const int vehicle : InitAndGetValues(pickup_vehicles.get())) {
|
|
if (vehicle == -1) continue;
|
|
if (!delivery_vehicle_var->Contains(vehicle)) continue;
|
|
if (MustUpdateBinCapacities() &&
|
|
!bin_capacities_->CheckAdditionsFeasibility({pickup, delivery},
|
|
vehicle)) {
|
|
continue;
|
|
}
|
|
fill_path(vehicle);
|
|
insertion_generator_.AppendPickupDeliveryMultitourInsertions(
|
|
pickup, delivery, vehicle, path, path_node_is_pickup,
|
|
path_node_is_delivery, insertion_container_);
|
|
}
|
|
if (StopSearch()) return;
|
|
price_insertion_sequences();
|
|
if (StopSearch()) return;
|
|
insertion_container_.RemoveIf(
|
|
[](const InsertionSequence& sequence) -> bool {
|
|
return sequence.Cost() == kint64max;
|
|
});
|
|
insertion_container_.Sort();
|
|
for (InsertionSequence sequence : insertion_container_) {
|
|
if (StopSearch()) return;
|
|
int previous_node = -1;
|
|
int previous_succ = -1;
|
|
const int vehicle = sequence.Vehicle();
|
|
for (const Insertion& insertion : sequence) {
|
|
const int succ = previous_node == insertion.pred
|
|
? previous_succ
|
|
: Value(insertion.pred);
|
|
InsertBetween(insertion.node, insertion.pred, succ, vehicle);
|
|
previous_node = insertion.node;
|
|
previous_succ = succ;
|
|
}
|
|
if (Evaluate(/*commit=*/true, /*ignore_upper_bound=*/true)
|
|
.has_value()) {
|
|
// Insertion succeeded.
|
|
if (MustUpdateBinCapacities()) {
|
|
bin_capacities_->AddItemToBin(pickup, vehicle);
|
|
bin_capacities_->AddItemToBin(delivery, vehicle);
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
void SetFalseForAllAlternatives(const PickupDeliveryPair& pair,
|
|
std::vector<bool>* data) {
|
|
for (const int64_t pickup : pair.pickup_alternatives) {
|
|
data->at(pickup) = false;
|
|
}
|
|
for (const int64_t delivery : pair.delivery_alternatives) {
|
|
data->at(delivery) = false;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
bool LocalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() {
|
|
const RoutingModel& model = *this->model();
|
|
|
|
// Fill vehicle bins with nodes that are already inserted.
|
|
if (MustUpdateBinCapacities()) {
|
|
bin_capacities_->ClearItems();
|
|
for (int vehicle = 0; vehicle < model.vehicles(); ++vehicle) {
|
|
const int start = Value(model.Start(vehicle));
|
|
for (int node = start; !model.IsEnd(node); node = Value(node)) {
|
|
bin_capacities_->AddItemToBin(node, vehicle);
|
|
}
|
|
}
|
|
}
|
|
|
|
const std::vector<PickupDeliveryPair>& pairs =
|
|
model.GetPickupAndDeliveryPairs();
|
|
std::vector<bool> ignore_pair_index(pairs.size(), false);
|
|
std::vector<bool> insert_as_single_node(model.Size(), true);
|
|
for (int pair_index = 0; pair_index < pairs.size(); ++pair_index) {
|
|
const auto& [pickups, deliveries] = pairs[pair_index];
|
|
bool pickup_contained = false;
|
|
for (int64_t pickup : pickups) {
|
|
if (Contains(pickup)) {
|
|
pickup_contained = true;
|
|
break;
|
|
}
|
|
}
|
|
bool delivery_contained = false;
|
|
for (int64_t delivery : deliveries) {
|
|
if (Contains(delivery)) {
|
|
delivery_contained = true;
|
|
break;
|
|
}
|
|
}
|
|
ignore_pair_index[pair_index] = pickup_contained || delivery_contained;
|
|
if (pickup_contained == delivery_contained) {
|
|
// Either both pickup and delivery are already inserted for this pair, or
|
|
// neither are inserted and should be considered as pair.
|
|
// In both cases, the nodes in the pickup/delivery alternatives shouldn't
|
|
// be considered for insertion as single nodes.
|
|
SetFalseForAllAlternatives(pairs[pair_index], &insert_as_single_node);
|
|
}
|
|
}
|
|
|
|
for (const Seed& seed : insertion_order_) {
|
|
const int index = seed.index;
|
|
if (!seed.is_node_index) {
|
|
if (ignore_pair_index[index]) continue;
|
|
|
|
const auto& pair = pairs[index];
|
|
switch (pair_insertion_strategy_) {
|
|
case LocalCheapestInsertionParameters::AUTOMATIC:
|
|
case LocalCheapestInsertionParameters::BEST_PICKUP_DELIVERY_PAIR:
|
|
InsertBestPair(pair);
|
|
break;
|
|
case LocalCheapestInsertionParameters::BEST_PICKUP_THEN_BEST_DELIVERY:
|
|
InsertBestPickupThenDelivery(pair);
|
|
break;
|
|
case LocalCheapestInsertionParameters::
|
|
BEST_PICKUP_DELIVERY_PAIR_MULTITOUR:
|
|
InsertBestPairMultitour(pair);
|
|
break;
|
|
default:
|
|
LOG(ERROR) << "Unknown pair insertion strategy value.";
|
|
break;
|
|
}
|
|
if (StopSearch()) {
|
|
return MakeUnassignedNodesUnperformed() && Evaluate(true).has_value();
|
|
}
|
|
} else {
|
|
if (Contains(index) || !insert_as_single_node[index]) {
|
|
continue;
|
|
}
|
|
for (const NodeInsertion& insertion :
|
|
ComputeEvaluatorSortedPositions(index)) {
|
|
if (StopSearch()) {
|
|
return MakeUnassignedNodesUnperformed() && Evaluate(true).has_value();
|
|
}
|
|
InsertBetween(index, insertion.insert_after,
|
|
Value(insertion.insert_after), insertion.vehicle);
|
|
// Capturing the state of the delta before it gets wiped by Evaluate.
|
|
std::vector<int> indices = delta_indices();
|
|
if (Evaluate(/*commit=*/true, /*ignore_upper_bound=*/true)
|
|
.has_value()) {
|
|
if (MustUpdateBinCapacities()) {
|
|
bin_capacities_->AddItemToBin(index, insertion.vehicle);
|
|
}
|
|
OptimizeOnInsertion(std::move(indices));
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return MakeUnassignedNodesUnperformed() && Evaluate(true).has_value();
|
|
}
|
|
|
|
void LocalCheapestInsertionFilteredHeuristic::AppendInsertionPositionsAfter(
|
|
int64_t node_to_insert, int64_t start, int64_t next_after_start,
|
|
int vehicle, std::vector<NodeInsertion>* node_insertions) {
|
|
DCHECK(node_insertions != nullptr);
|
|
int64_t insert_after = start;
|
|
if (!model()->VehicleVar(node_to_insert)->Contains(vehicle)) return;
|
|
while (!model()->IsEnd(insert_after)) {
|
|
const int64_t insert_before =
|
|
(insert_after == start) ? next_after_start : Value(insert_after);
|
|
const int hint_weight = IsHint(insert_after, node_to_insert) +
|
|
IsHint(node_to_insert, insert_before) -
|
|
IsHint(insert_after, insert_before);
|
|
const auto insertion_cost = GetInsertionCostForNodeAtPosition(
|
|
node_to_insert, insert_after, insert_before, vehicle, hint_weight);
|
|
if (insertion_cost.has_value()) {
|
|
node_insertions->push_back(
|
|
{insert_after, vehicle, -hint_weight, *insertion_cost});
|
|
}
|
|
insert_after = insert_before;
|
|
}
|
|
}
|
|
|
|
std::vector<LocalCheapestInsertionFilteredHeuristic::NodeInsertion>
|
|
LocalCheapestInsertionFilteredHeuristic::ComputeEvaluatorSortedPositions(
|
|
int64_t node) {
|
|
DCHECK(!Contains(node));
|
|
const int size = model()->Size();
|
|
if (node >= size) return {};
|
|
std::vector<NodeInsertion> sorted_insertions;
|
|
const IntVar* vehicle_var = model()->VehicleVar(node);
|
|
std::unique_ptr<IntVarIterator> node_vehicles(
|
|
vehicle_var->MakeDomainIterator(false));
|
|
for (const int vehicle : InitAndGetValues(node_vehicles.get())) {
|
|
if (vehicle == -1) continue;
|
|
if (MustUpdateBinCapacities() &&
|
|
!bin_capacities_->CheckAdditionFeasibility(node, vehicle)) {
|
|
continue;
|
|
}
|
|
const int64_t start = model()->Start(vehicle);
|
|
const size_t old_num_insertions = sorted_insertions.size();
|
|
AppendInsertionPositionsAfter(node, start, Value(start), vehicle,
|
|
&sorted_insertions);
|
|
if (MustUpdateBinCapacities() && evaluator_) {
|
|
// Compute cost incurred from soft capacities.
|
|
const int64_t old_cost = bin_capacities_->TotalCost();
|
|
bin_capacities_->AddItemToBin(node, vehicle);
|
|
const int64_t new_cost = bin_capacities_->TotalCost();
|
|
bin_capacities_->RemoveItemFromBin(node, vehicle);
|
|
const int64_t delta_cost = CapSub(new_cost, old_cost);
|
|
// Add soft cost to new insertions.
|
|
for (size_t i = old_num_insertions; i < sorted_insertions.size(); ++i) {
|
|
CapAddTo(delta_cost, &sorted_insertions[i].value);
|
|
}
|
|
}
|
|
}
|
|
absl::c_sort(sorted_insertions);
|
|
return sorted_insertions;
|
|
}
|
|
|
|
std::vector<LocalCheapestInsertionFilteredHeuristic::NodeInsertion>
|
|
LocalCheapestInsertionFilteredHeuristic::
|
|
ComputeEvaluatorSortedPositionsOnRouteAfter(int64_t node, int64_t start,
|
|
int64_t next_after_start,
|
|
int vehicle) {
|
|
DCHECK(!Contains(node));
|
|
const int size = model()->Size();
|
|
if (node >= size) return {};
|
|
std::vector<NodeInsertion> sorted_insertions;
|
|
AppendInsertionPositionsAfter(node, start, next_after_start, vehicle,
|
|
&sorted_insertions);
|
|
absl::c_sort(sorted_insertions);
|
|
return sorted_insertions;
|
|
}
|
|
|
|
std::vector<PickupDeliveryInsertion>
|
|
LocalCheapestInsertionFilteredHeuristic::ComputeEvaluatorSortedPairPositions(
|
|
int pickup, int delivery) {
|
|
std::vector<PickupDeliveryInsertion> sorted_pickup_delivery_insertions;
|
|
const int size = model()->Size();
|
|
DCHECK_LT(pickup, size);
|
|
DCHECK_LT(delivery, size);
|
|
const IntVar* pickup_vehicle_var = model()->VehicleVar(pickup);
|
|
const IntVar* delivery_vehicle_var = model()->VehicleVar(delivery);
|
|
std::unique_ptr<IntVarIterator> pickup_vehicles(
|
|
pickup_vehicle_var->MakeDomainIterator(false));
|
|
for (const int vehicle : InitAndGetValues(pickup_vehicles.get())) {
|
|
if (vehicle == -1) continue;
|
|
if (!delivery_vehicle_var->Contains(vehicle)) continue;
|
|
if (MustUpdateBinCapacities() &&
|
|
!bin_capacities_->CheckAdditionsFeasibility({pickup, delivery},
|
|
vehicle)) {
|
|
continue;
|
|
}
|
|
int64_t insert_pickup_after = model()->Start(vehicle);
|
|
while (!model()->IsEnd(insert_pickup_after)) {
|
|
const int64_t insert_pickup_before = Value(insert_pickup_after);
|
|
int64_t insert_delivery_after = pickup;
|
|
while (!model()->IsEnd(insert_delivery_after)) {
|
|
if (StopSearch()) return {};
|
|
const int64_t insert_delivery_before =
|
|
insert_delivery_after == pickup ? insert_pickup_before
|
|
: Value(insert_delivery_after);
|
|
const int hint_weight = IsHint(insert_pickup_after, pickup) +
|
|
IsHint(insert_delivery_after, delivery) +
|
|
IsHint(pickup, insert_pickup_before) +
|
|
IsHint(delivery, insert_delivery_before);
|
|
const auto insertion_cost = GetInsertionCostForPairAtPositions(
|
|
pickup, insert_pickup_after, delivery, insert_delivery_after,
|
|
vehicle, hint_weight);
|
|
if (insertion_cost.has_value()) {
|
|
int64_t total_cost = *insertion_cost;
|
|
if (evaluator_ && MustUpdateBinCapacities()) {
|
|
const int64_t old_cost = bin_capacities_->TotalCost();
|
|
bin_capacities_->AddItemToBin(pickup, vehicle);
|
|
bin_capacities_->AddItemToBin(delivery, vehicle);
|
|
const int64_t new_cost = bin_capacities_->TotalCost();
|
|
CapAddTo(CapSub(new_cost, old_cost), &total_cost);
|
|
bin_capacities_->RemoveItemFromBin(pickup, vehicle);
|
|
bin_capacities_->RemoveItemFromBin(delivery, vehicle);
|
|
}
|
|
sorted_pickup_delivery_insertions.push_back(
|
|
{insert_pickup_after, insert_delivery_after, -hint_weight,
|
|
total_cost, vehicle});
|
|
}
|
|
insert_delivery_after = insert_delivery_before;
|
|
}
|
|
insert_pickup_after = insert_pickup_before;
|
|
}
|
|
}
|
|
absl::c_sort(sorted_pickup_delivery_insertions);
|
|
return sorted_pickup_delivery_insertions;
|
|
}
|
|
|
|
// CheapestAdditionFilteredHeuristic
|
|
|
|
CheapestAdditionFilteredHeuristic::CheapestAdditionFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
LocalSearchFilterManager* filter_manager)
|
|
: RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager) {}
|
|
|
|
bool CheapestAdditionFilteredHeuristic::BuildSolutionInternal() {
|
|
const int kUnassigned = -1;
|
|
const int num_nexts = model()->Nexts().size();
|
|
std::vector<std::vector<int64_t>> deliveries(num_nexts);
|
|
std::vector<std::vector<int64_t>> pickups(num_nexts);
|
|
for (const auto& [pickup_alternatives, delivery_alternatives] :
|
|
model()->GetPickupAndDeliveryPairs()) {
|
|
for (int pickup : pickup_alternatives) {
|
|
for (int delivery : delivery_alternatives) {
|
|
deliveries[pickup].push_back(delivery);
|
|
pickups[delivery].push_back(pickup);
|
|
}
|
|
}
|
|
}
|
|
// To mimic the behavior of PathSelector (cf. search.cc), iterating on
|
|
// routes with partial route at their start first then on routes with largest
|
|
// index.
|
|
std::vector<int> sorted_vehicles(model()->vehicles(), 0);
|
|
for (int vehicle = 0; vehicle < model()->vehicles(); ++vehicle) {
|
|
sorted_vehicles[vehicle] = vehicle;
|
|
}
|
|
absl::c_sort(sorted_vehicles,
|
|
PartialRoutesAndLargeVehicleIndicesFirst(*this));
|
|
// Neighbors of the node currently being extended.
|
|
for (const int vehicle : sorted_vehicles) {
|
|
int64_t last_node = GetStartChainEnd(vehicle);
|
|
bool extend_route = true;
|
|
// Extend the route of the current vehicle while it's possible. We can
|
|
// iterate more than once if pickup and delivery pairs have been inserted
|
|
// in the last iteration (see comment below); the new iteration will try to
|
|
// extend the route after the last delivery on the route.
|
|
while (extend_route) {
|
|
extend_route = false;
|
|
bool found = true;
|
|
int64_t index = last_node;
|
|
int64_t end = GetEndChainStart(vehicle);
|
|
// Extend the route until either the end node of the vehicle is reached
|
|
// or no node or node pair can be added. Deliveries in pickup and
|
|
// delivery pairs are added at the same time as pickups, at the end of the
|
|
// route, in reverse order of the pickups. Deliveries are never added
|
|
// alone.
|
|
while (found && !model()->IsEnd(index)) {
|
|
found = false;
|
|
std::vector<int64_t> neighbors;
|
|
if (index < model()->Nexts().size()) {
|
|
std::unique_ptr<IntVarIterator> it(
|
|
model()->Nexts()[index]->MakeDomainIterator(false));
|
|
auto next_values = InitAndGetValues(it.get());
|
|
neighbors = GetPossibleNextsFromIterator(index, next_values.begin(),
|
|
next_values.end());
|
|
}
|
|
for (int i = 0; !found && i < neighbors.size(); ++i) {
|
|
int64_t next = -1;
|
|
switch (i) {
|
|
case 0:
|
|
next = FindTopSuccessor(index, neighbors);
|
|
break;
|
|
case 1:
|
|
SortSuccessors(index, &neighbors);
|
|
ABSL_FALLTHROUGH_INTENDED;
|
|
default:
|
|
next = neighbors[i];
|
|
}
|
|
if (model()->IsEnd(next) && next != end) {
|
|
continue;
|
|
}
|
|
// Only add a delivery if one of its pickups has been added already.
|
|
if (!model()->IsEnd(next) && !pickups[next].empty()) {
|
|
bool contains_pickups = false;
|
|
for (int64_t pickup : pickups[next]) {
|
|
if (Contains(pickup)) {
|
|
contains_pickups = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!contains_pickups) {
|
|
continue;
|
|
}
|
|
}
|
|
std::vector<int64_t> next_deliveries;
|
|
if (next < deliveries.size()) {
|
|
next_deliveries = GetPossibleNextsFromIterator(
|
|
next, deliveries[next].begin(), deliveries[next].end());
|
|
}
|
|
if (next_deliveries.empty()) next_deliveries = {kUnassigned};
|
|
for (int j = 0; !found && j < next_deliveries.size(); ++j) {
|
|
if (StopSearch()) return false;
|
|
int delivery = -1;
|
|
switch (j) {
|
|
case 0:
|
|
delivery = FindTopSuccessor(next, next_deliveries);
|
|
break;
|
|
case 1:
|
|
SortSuccessors(next, &next_deliveries);
|
|
ABSL_FALLTHROUGH_INTENDED;
|
|
default:
|
|
delivery = next_deliveries[j];
|
|
}
|
|
// Insert "next" after "index", and before "end" if it is not the
|
|
// end already.
|
|
SetNext(index, next, vehicle);
|
|
if (!model()->IsEnd(next)) {
|
|
SetNext(next, end, vehicle);
|
|
MakeDisjunctionNodesUnperformed(next);
|
|
if (delivery != kUnassigned) {
|
|
SetNext(next, delivery, vehicle);
|
|
SetNext(delivery, end, vehicle);
|
|
MakeDisjunctionNodesUnperformed(delivery);
|
|
}
|
|
}
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
index = next;
|
|
found = true;
|
|
if (delivery != kUnassigned) {
|
|
if (model()->IsEnd(end) && last_node != delivery) {
|
|
last_node = delivery;
|
|
extend_route = true;
|
|
}
|
|
end = delivery;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MakeUnassignedNodesUnperformed();
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
}
|
|
|
|
bool CheapestAdditionFilteredHeuristic::
|
|
PartialRoutesAndLargeVehicleIndicesFirst::operator()(int vehicle1,
|
|
int vehicle2) const {
|
|
const bool has_partial_route1 = (builder_.model()->Start(vehicle1) !=
|
|
builder_.GetStartChainEnd(vehicle1));
|
|
const bool has_partial_route2 = (builder_.model()->Start(vehicle2) !=
|
|
builder_.GetStartChainEnd(vehicle2));
|
|
if (has_partial_route1 == has_partial_route2) {
|
|
return vehicle2 < vehicle1;
|
|
} else {
|
|
return has_partial_route2 < has_partial_route1;
|
|
}
|
|
}
|
|
|
|
// EvaluatorCheapestAdditionFilteredHeuristic
|
|
|
|
EvaluatorCheapestAdditionFilteredHeuristic::
|
|
EvaluatorCheapestAdditionFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
std::function<int64_t(int64_t, int64_t)> evaluator,
|
|
LocalSearchFilterManager* filter_manager)
|
|
: CheapestAdditionFilteredHeuristic(model, std::move(stop_search),
|
|
filter_manager),
|
|
evaluator_(std::move(evaluator)) {}
|
|
|
|
int64_t EvaluatorCheapestAdditionFilteredHeuristic::FindTopSuccessor(
|
|
int64_t node, const std::vector<int64_t>& successors) {
|
|
int64_t best_evaluation = std::numeric_limits<int64_t>::max();
|
|
int64_t best_successor = -1;
|
|
for (int64_t successor : successors) {
|
|
const int64_t evaluation = (successor >= 0)
|
|
? evaluator_(node, successor)
|
|
: std::numeric_limits<int64_t>::max();
|
|
if (evaluation < best_evaluation ||
|
|
(evaluation == best_evaluation && successor > best_successor)) {
|
|
best_evaluation = evaluation;
|
|
best_successor = successor;
|
|
}
|
|
}
|
|
return best_successor;
|
|
}
|
|
|
|
void EvaluatorCheapestAdditionFilteredHeuristic::SortSuccessors(
|
|
int64_t node, std::vector<int64_t>* successors) {
|
|
std::vector<std::pair<int64_t, int64_t>> values;
|
|
values.reserve(successors->size());
|
|
for (int64_t successor : *successors) {
|
|
// Tie-breaking on largest node index to mimic the behavior of
|
|
// CheapestValueSelector (search.cc).
|
|
values.push_back({evaluator_(node, successor), successor});
|
|
}
|
|
absl::c_sort(values, [](const std::pair<int64_t, int64_t>& s1,
|
|
const std::pair<int64_t, int64_t>& s2) {
|
|
return s1.first < s2.first ||
|
|
(s1.first == s2.first && s1.second > s2.second);
|
|
});
|
|
successors->clear();
|
|
for (auto value : values) {
|
|
successors->push_back(value.second);
|
|
}
|
|
}
|
|
|
|
// ComparatorCheapestAdditionFilteredHeuristic
|
|
|
|
ComparatorCheapestAdditionFilteredHeuristic::
|
|
ComparatorCheapestAdditionFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
Solver::VariableValueComparator comparator,
|
|
LocalSearchFilterManager* filter_manager)
|
|
: CheapestAdditionFilteredHeuristic(model, std::move(stop_search),
|
|
filter_manager),
|
|
comparator_(std::move(comparator)) {}
|
|
|
|
int64_t ComparatorCheapestAdditionFilteredHeuristic::FindTopSuccessor(
|
|
int64_t node, const std::vector<int64_t>& successors) {
|
|
return *absl::c_min_element(
|
|
successors, [this, node](int successor1, int successor2) {
|
|
return comparator_(node, successor1, successor2);
|
|
});
|
|
}
|
|
|
|
void ComparatorCheapestAdditionFilteredHeuristic::SortSuccessors(
|
|
int64_t node, std::vector<int64_t>* successors) {
|
|
absl::c_sort(*successors, [this, node](int successor1, int successor2) {
|
|
return comparator_(node, successor1, successor2);
|
|
});
|
|
}
|
|
|
|
// Class storing and allowing access to the savings according to the number of
|
|
// vehicle types.
|
|
// The savings are stored and sorted in sorted_savings_per_vehicle_type_.
|
|
// Furthermore, when there is more than one vehicle type, the savings for a same
|
|
// before-->after arc are sorted in costs_and_savings_per_arc_[arc] by
|
|
// increasing cost(s-->before-->after-->e), where s and e are the start and end
|
|
// of the route, in order to make sure the arc is served by the route with the
|
|
// closest depot (start/end) possible.
|
|
// When there is only one vehicle "type" (i.e. all vehicles have the same
|
|
// start/end and cost class), each arc has a single saving value associated to
|
|
// it, so we ignore this last step to avoid unnecessary computations, and only
|
|
// work with sorted_savings_per_vehicle_type_[0].
|
|
// In case of multiple vehicle types, the best savings for each arc, i.e. the
|
|
// savings corresponding to the closest vehicle type, are inserted and sorted in
|
|
// sorted_savings_.
|
|
//
|
|
// This class also handles skipped Savings:
|
|
// The vectors skipped_savings_starting/ending_at_ contain all the Savings that
|
|
// weren't added to the model, which we want to consider for later:
|
|
// 1) When a Saving before-->after with both nodes uncontained cannot be used to
|
|
// start a new route (no more available vehicles or could not commit on any
|
|
// of those available).
|
|
// 2) When only one of the nodes of the Saving is contained but on a different
|
|
// vehicle type.
|
|
// In these cases, the Update() method is called with update_best_saving = true,
|
|
// which in turn calls SkipSavingForArc() (within
|
|
// UpdateNextAndSkippedSavingsForArcWithType()) to mark the Saving for this arc
|
|
// (with the correct type in the second case) as "skipped", by storing it in
|
|
// skipped_savings_starting_at_[before] and skipped_savings_ending_at_[after].
|
|
//
|
|
// UpdateNextAndSkippedSavingsForArcWithType() also updates the next_savings_
|
|
// vector, which stores the savings to go through once we've iterated through
|
|
// all sorted_savings_.
|
|
// In the first case above, where neither nodes are contained, we skip the
|
|
// current Saving (current_saving_), and add the next best Saving for this arc
|
|
// to next_savings_ (in case this skipped Saving is never considered).
|
|
// In the second case with a specific type, we search for the Saving with the
|
|
// correct type for this arc, and add it to both next_savings_ and the skipped
|
|
// Savings.
|
|
//
|
|
// The skipped Savings are then re-considered when one of their ends gets
|
|
// inserted:
|
|
// When another Saving other_node-->before (or after-->other_node) gets
|
|
// inserted, all skipped Savings in skipped_savings_starting_at_[before] (or
|
|
// skipped_savings_ending_at_[after]) are once again considered by calling
|
|
// ReinjectSkippedSavingsStartingAt() (or ReinjectSkippedSavingsEndingAt()).
|
|
// Then, when calling GetSaving(), we iterate through the reinjected Savings in
|
|
// order of insertion in the vectors while there are reinjected savings.
|
|
template <typename Saving>
|
|
class SavingsFilteredHeuristic::SavingsContainer {
|
|
public:
|
|
explicit SavingsContainer(const SavingsFilteredHeuristic* savings_db,
|
|
int vehicle_types)
|
|
: savings_db_(savings_db),
|
|
index_in_sorted_savings_(0),
|
|
vehicle_types_(vehicle_types),
|
|
single_vehicle_type_(vehicle_types == 1),
|
|
using_incoming_reinjected_saving_(false),
|
|
sorted_(false),
|
|
to_update_(true) {}
|
|
|
|
void InitializeContainer(int64_t size, int64_t saving_neighbors) {
|
|
sorted_savings_per_vehicle_type_.clear();
|
|
sorted_savings_per_vehicle_type_.resize(vehicle_types_);
|
|
for (std::vector<Saving>& savings : sorted_savings_per_vehicle_type_) {
|
|
savings.reserve(size * saving_neighbors);
|
|
}
|
|
|
|
sorted_savings_.clear();
|
|
costs_and_savings_per_arc_.clear();
|
|
arc_indices_per_before_node_.clear();
|
|
|
|
if (!single_vehicle_type_) {
|
|
costs_and_savings_per_arc_.reserve(size * saving_neighbors);
|
|
arc_indices_per_before_node_.resize(size);
|
|
for (int before_node = 0; before_node < size; before_node++) {
|
|
arc_indices_per_before_node_[before_node].reserve(saving_neighbors);
|
|
}
|
|
}
|
|
skipped_savings_starting_at_.clear();
|
|
skipped_savings_starting_at_.resize(size);
|
|
skipped_savings_ending_at_.clear();
|
|
skipped_savings_ending_at_.resize(size);
|
|
incoming_reinjected_savings_ = nullptr;
|
|
outgoing_reinjected_savings_ = nullptr;
|
|
incoming_new_reinjected_savings_ = nullptr;
|
|
outgoing_new_reinjected_savings_ = nullptr;
|
|
}
|
|
|
|
void AddNewSaving(const Saving& saving, int64_t total_cost,
|
|
int64_t before_node, int64_t after_node, int vehicle_type) {
|
|
CHECK(!sorted_savings_per_vehicle_type_.empty())
|
|
<< "Container not initialized!";
|
|
sorted_savings_per_vehicle_type_[vehicle_type].push_back(saving);
|
|
UpdateArcIndicesCostsAndSavings(before_node, after_node,
|
|
{total_cost, saving});
|
|
}
|
|
|
|
void Sort() {
|
|
CHECK(!sorted_) << "Container already sorted!";
|
|
|
|
for (std::vector<Saving>& savings : sorted_savings_per_vehicle_type_) {
|
|
absl::c_sort(savings);
|
|
}
|
|
|
|
if (single_vehicle_type_) {
|
|
const auto& savings = sorted_savings_per_vehicle_type_[0];
|
|
sorted_savings_.resize(savings.size());
|
|
absl::c_transform(savings, sorted_savings_.begin(),
|
|
[](const Saving& saving) {
|
|
return SavingAndArc({saving, /*arc_index*/ -1});
|
|
});
|
|
} else {
|
|
// For each arc, sort the savings by decreasing total cost
|
|
// start-->a-->b-->end.
|
|
// The best saving for each arc is therefore the last of its vector.
|
|
sorted_savings_.reserve(vehicle_types_ *
|
|
costs_and_savings_per_arc_.size());
|
|
|
|
for (int arc_index = 0; arc_index < costs_and_savings_per_arc_.size();
|
|
arc_index++) {
|
|
std::vector<std::pair<int64_t, Saving>>& costs_and_savings =
|
|
costs_and_savings_per_arc_[arc_index];
|
|
DCHECK(!costs_and_savings.empty());
|
|
|
|
absl::c_sort(
|
|
costs_and_savings,
|
|
[](const std::pair<int64_t, Saving>& cs1,
|
|
const std::pair<int64_t, Saving>& cs2) { return cs1 > cs2; });
|
|
|
|
// Insert all Savings for this arc with the lowest cost into
|
|
// sorted_savings_.
|
|
// TODO(user): Also do this when reiterating on next_savings_.
|
|
const int64_t cost = costs_and_savings.back().first;
|
|
while (!costs_and_savings.empty() &&
|
|
costs_and_savings.back().first == cost) {
|
|
sorted_savings_.push_back(
|
|
{costs_and_savings.back().second, arc_index});
|
|
costs_and_savings.pop_back();
|
|
}
|
|
}
|
|
absl::c_sort(sorted_savings_);
|
|
next_saving_type_and_index_for_arc_.clear();
|
|
next_saving_type_and_index_for_arc_.resize(
|
|
costs_and_savings_per_arc_.size(), {-1, -1});
|
|
}
|
|
sorted_ = true;
|
|
index_in_sorted_savings_ = 0;
|
|
to_update_ = false;
|
|
}
|
|
|
|
bool HasSaving() {
|
|
return index_in_sorted_savings_ < sorted_savings_.size() ||
|
|
HasReinjectedSavings();
|
|
}
|
|
|
|
Saving GetSaving() {
|
|
CHECK(sorted_) << "Calling GetSaving() before Sort() !";
|
|
CHECK(!to_update_)
|
|
<< "Update() should be called between two calls to GetSaving() !";
|
|
|
|
to_update_ = true;
|
|
|
|
if (HasReinjectedSavings()) {
|
|
if (incoming_reinjected_savings_ != nullptr &&
|
|
outgoing_reinjected_savings_ != nullptr) {
|
|
// Get the best Saving among the two.
|
|
SavingAndArc& incoming_saving = incoming_reinjected_savings_->front();
|
|
SavingAndArc& outgoing_saving = outgoing_reinjected_savings_->front();
|
|
if (incoming_saving < outgoing_saving) {
|
|
current_saving_ = incoming_saving;
|
|
using_incoming_reinjected_saving_ = true;
|
|
} else {
|
|
current_saving_ = outgoing_saving;
|
|
using_incoming_reinjected_saving_ = false;
|
|
}
|
|
} else {
|
|
if (incoming_reinjected_savings_ != nullptr) {
|
|
current_saving_ = incoming_reinjected_savings_->front();
|
|
using_incoming_reinjected_saving_ = true;
|
|
}
|
|
if (outgoing_reinjected_savings_ != nullptr) {
|
|
current_saving_ = outgoing_reinjected_savings_->front();
|
|
using_incoming_reinjected_saving_ = false;
|
|
}
|
|
}
|
|
} else {
|
|
current_saving_ = sorted_savings_[index_in_sorted_savings_];
|
|
}
|
|
return current_saving_.saving;
|
|
}
|
|
|
|
void Update(bool update_best_saving, int type = -1) {
|
|
CHECK(to_update_) << "Container already up to date!";
|
|
if (update_best_saving) {
|
|
const int64_t arc_index = current_saving_.arc_index;
|
|
UpdateNextAndSkippedSavingsForArcWithType(arc_index, type);
|
|
}
|
|
if (!HasReinjectedSavings()) {
|
|
index_in_sorted_savings_++;
|
|
|
|
if (index_in_sorted_savings_ == sorted_savings_.size()) {
|
|
sorted_savings_.swap(next_savings_);
|
|
gtl::STLClearObject(&next_savings_);
|
|
index_in_sorted_savings_ = 0;
|
|
|
|
absl::c_sort(sorted_savings_);
|
|
next_saving_type_and_index_for_arc_.clear();
|
|
next_saving_type_and_index_for_arc_.resize(
|
|
costs_and_savings_per_arc_.size(), {-1, -1});
|
|
}
|
|
}
|
|
UpdateReinjectedSavings();
|
|
to_update_ = false;
|
|
}
|
|
|
|
void UpdateWithType(int type) {
|
|
CHECK(!single_vehicle_type_);
|
|
Update(/*update_best_saving*/ true, type);
|
|
}
|
|
|
|
const std::vector<Saving>& GetSortedSavingsForVehicleType(int type) {
|
|
CHECK(sorted_) << "Savings not sorted yet!";
|
|
CHECK_LT(type, vehicle_types_);
|
|
return sorted_savings_per_vehicle_type_[type];
|
|
}
|
|
|
|
void ReinjectSkippedSavingsStartingAt(int64_t node) {
|
|
CHECK(outgoing_new_reinjected_savings_ == nullptr);
|
|
outgoing_new_reinjected_savings_ = &(skipped_savings_starting_at_[node]);
|
|
}
|
|
|
|
void ReinjectSkippedSavingsEndingAt(int64_t node) {
|
|
CHECK(incoming_new_reinjected_savings_ == nullptr);
|
|
incoming_new_reinjected_savings_ = &(skipped_savings_ending_at_[node]);
|
|
}
|
|
|
|
private:
|
|
struct SavingAndArc {
|
|
Saving saving;
|
|
int64_t arc_index;
|
|
|
|
bool operator<(const SavingAndArc& other) const {
|
|
return std::tie(saving, arc_index) <
|
|
std::tie(other.saving, other.arc_index);
|
|
}
|
|
};
|
|
|
|
// Skips the Saving for the arc before_node-->after_node, by adding it to the
|
|
// skipped_savings_ vector of the nodes, if they're uncontained.
|
|
void SkipSavingForArc(const SavingAndArc& saving_and_arc) {
|
|
const Saving& saving = saving_and_arc.saving;
|
|
const int64_t before_node = saving.before_node;
|
|
const int64_t after_node = saving.after_node;
|
|
if (!savings_db_->Contains(before_node)) {
|
|
skipped_savings_starting_at_[before_node].push_back(saving_and_arc);
|
|
}
|
|
if (!savings_db_->Contains(after_node)) {
|
|
skipped_savings_ending_at_[after_node].push_back(saving_and_arc);
|
|
}
|
|
}
|
|
|
|
// Called within Update() when update_best_saving is true, this method updates
|
|
// the next_savings_ and skipped savings vectors for a given arc_index and
|
|
// vehicle type.
|
|
// When a Saving with the right type has already been added to next_savings_
|
|
// for this arc, no action is needed on next_savings_.
|
|
// Otherwise, if such a Saving exists, GetNextSavingForArcWithType() will find
|
|
// and assign it to next_saving, which is then used to update next_savings_.
|
|
// Finally, the right Saving is skipped for this arc: if looking for a
|
|
// specific type (i.e. type != -1), next_saving (which has the correct type)
|
|
// is skipped, otherwise the current_saving_ is.
|
|
void UpdateNextAndSkippedSavingsForArcWithType(int64_t arc_index, int type) {
|
|
if (single_vehicle_type_) {
|
|
// No next Saving, skip the current Saving.
|
|
CHECK_EQ(type, -1);
|
|
SkipSavingForArc(current_saving_);
|
|
return;
|
|
}
|
|
CHECK_GE(arc_index, 0);
|
|
auto& type_and_index = next_saving_type_and_index_for_arc_[arc_index];
|
|
const int previous_index = type_and_index.second;
|
|
const int previous_type = type_and_index.first;
|
|
bool next_saving_added = false;
|
|
Saving next_saving;
|
|
|
|
if (previous_index >= 0) {
|
|
// Next Saving already added for this arc.
|
|
DCHECK_GE(previous_type, 0);
|
|
if (type == -1 || previous_type == type) {
|
|
// Not looking for a specific type, or correct type already in
|
|
// next_savings_.
|
|
next_saving_added = true;
|
|
next_saving = next_savings_[previous_index].saving;
|
|
}
|
|
}
|
|
|
|
if (!next_saving_added &&
|
|
GetNextSavingForArcWithType(arc_index, type, &next_saving)) {
|
|
type_and_index.first = next_saving.vehicle_type;
|
|
if (previous_index >= 0) {
|
|
// Update the previous saving.
|
|
next_savings_[previous_index] = {next_saving, arc_index};
|
|
} else {
|
|
// Insert the new next Saving for this arc.
|
|
type_and_index.second = next_savings_.size();
|
|
next_savings_.push_back({next_saving, arc_index});
|
|
}
|
|
next_saving_added = true;
|
|
}
|
|
|
|
// Skip the Saving based on the vehicle type.
|
|
if (type == -1) {
|
|
// Skip the current Saving.
|
|
SkipSavingForArc(current_saving_);
|
|
} else {
|
|
// Skip the Saving with the correct type, already added to next_savings_
|
|
// if it was found.
|
|
if (next_saving_added) {
|
|
SkipSavingForArc({next_saving, arc_index});
|
|
}
|
|
}
|
|
}
|
|
|
|
void UpdateReinjectedSavings() {
|
|
UpdateGivenReinjectedSavings(incoming_new_reinjected_savings_,
|
|
&incoming_reinjected_savings_,
|
|
using_incoming_reinjected_saving_);
|
|
UpdateGivenReinjectedSavings(outgoing_new_reinjected_savings_,
|
|
&outgoing_reinjected_savings_,
|
|
!using_incoming_reinjected_saving_);
|
|
incoming_new_reinjected_savings_ = nullptr;
|
|
outgoing_new_reinjected_savings_ = nullptr;
|
|
}
|
|
|
|
void UpdateGivenReinjectedSavings(
|
|
std::deque<SavingAndArc>* new_reinjected_savings,
|
|
std::deque<SavingAndArc>** reinjected_savings,
|
|
bool using_reinjected_savings) {
|
|
if (new_reinjected_savings == nullptr) {
|
|
// No new reinjected savings, update the previous ones if needed.
|
|
if (*reinjected_savings != nullptr && using_reinjected_savings) {
|
|
CHECK(!(*reinjected_savings)->empty());
|
|
(*reinjected_savings)->pop_front();
|
|
if ((*reinjected_savings)->empty()) {
|
|
*reinjected_savings = nullptr;
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
// New savings reinjected.
|
|
// Forget about the previous reinjected savings and add the new ones if
|
|
// there are any.
|
|
if (*reinjected_savings != nullptr) {
|
|
(*reinjected_savings)->clear();
|
|
}
|
|
*reinjected_savings = nullptr;
|
|
if (!new_reinjected_savings->empty()) {
|
|
*reinjected_savings = new_reinjected_savings;
|
|
}
|
|
}
|
|
|
|
bool HasReinjectedSavings() {
|
|
return outgoing_reinjected_savings_ != nullptr ||
|
|
incoming_reinjected_savings_ != nullptr;
|
|
}
|
|
|
|
void UpdateArcIndicesCostsAndSavings(
|
|
int64_t before_node, int64_t after_node,
|
|
const std::pair<int64_t, Saving>& cost_and_saving) {
|
|
if (single_vehicle_type_) {
|
|
return;
|
|
}
|
|
absl::flat_hash_map<int, int>& arc_indices =
|
|
arc_indices_per_before_node_[before_node];
|
|
const auto& arc_inserted = arc_indices.insert(
|
|
std::make_pair(after_node, costs_and_savings_per_arc_.size()));
|
|
const int index = arc_inserted.first->second;
|
|
if (arc_inserted.second) {
|
|
costs_and_savings_per_arc_.push_back({cost_and_saving});
|
|
} else {
|
|
DCHECK_LT(index, costs_and_savings_per_arc_.size());
|
|
costs_and_savings_per_arc_[index].push_back(cost_and_saving);
|
|
}
|
|
}
|
|
|
|
bool GetNextSavingForArcWithType(int64_t arc_index, int type,
|
|
Saving* next_saving) {
|
|
std::vector<std::pair<int64_t, Saving>>& costs_and_savings =
|
|
costs_and_savings_per_arc_[arc_index];
|
|
|
|
bool found_saving = false;
|
|
while (!costs_and_savings.empty() && !found_saving) {
|
|
const Saving& saving = costs_and_savings.back().second;
|
|
if (type == -1 || saving.vehicle_type == type) {
|
|
*next_saving = saving;
|
|
found_saving = true;
|
|
}
|
|
costs_and_savings.pop_back();
|
|
}
|
|
return found_saving;
|
|
}
|
|
|
|
const SavingsFilteredHeuristic* const savings_db_;
|
|
int64_t index_in_sorted_savings_;
|
|
std::vector<std::vector<Saving>> sorted_savings_per_vehicle_type_;
|
|
std::vector<SavingAndArc> sorted_savings_;
|
|
std::vector<SavingAndArc> next_savings_;
|
|
std::vector<std::pair</*type*/ int, /*index*/ int>>
|
|
next_saving_type_and_index_for_arc_;
|
|
SavingAndArc current_saving_;
|
|
std::vector<std::vector<std::pair</*cost*/ int64_t, Saving>>>
|
|
costs_and_savings_per_arc_;
|
|
std::vector<absl::flat_hash_map</*after_node*/ int, /*arc_index*/ int>>
|
|
arc_indices_per_before_node_;
|
|
std::vector<std::deque<SavingAndArc>> skipped_savings_starting_at_;
|
|
std::vector<std::deque<SavingAndArc>> skipped_savings_ending_at_;
|
|
std::deque<SavingAndArc>* outgoing_reinjected_savings_;
|
|
std::deque<SavingAndArc>* incoming_reinjected_savings_;
|
|
std::deque<SavingAndArc>* outgoing_new_reinjected_savings_;
|
|
std::deque<SavingAndArc>* incoming_new_reinjected_savings_;
|
|
const int vehicle_types_;
|
|
const bool single_vehicle_type_;
|
|
bool using_incoming_reinjected_saving_;
|
|
bool sorted_;
|
|
bool to_update_;
|
|
};
|
|
|
|
// SavingsFilteredHeuristic
|
|
|
|
SavingsFilteredHeuristic::SavingsFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
SavingsParameters parameters, LocalSearchFilterManager* filter_manager)
|
|
: RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager),
|
|
vehicle_type_curator_(nullptr),
|
|
savings_params_(std::move(parameters)) {
|
|
DCHECK_GT(savings_params_.neighbors_ratio(), 0);
|
|
DCHECK_LE(savings_params_.neighbors_ratio(), 1);
|
|
DCHECK_GT(savings_params_.max_memory_usage_bytes(), 0);
|
|
DCHECK_GT(savings_params_.arc_coefficient(), 0);
|
|
}
|
|
|
|
SavingsFilteredHeuristic::~SavingsFilteredHeuristic() = default;
|
|
|
|
bool SavingsFilteredHeuristic::BuildSolutionInternal() {
|
|
if (vehicle_type_curator_ == nullptr) {
|
|
vehicle_type_curator_ = std::make_unique<VehicleTypeCurator>(
|
|
model()->GetVehicleTypeContainer());
|
|
}
|
|
// Only store empty vehicles in the vehicle_type_curator_.
|
|
vehicle_type_curator_->Reset(
|
|
[this](int vehicle) { return VehicleIsEmpty(vehicle); });
|
|
if (!ComputeSavings()) return false;
|
|
BuildRoutesFromSavings();
|
|
// Free all the space used to store the Savings in the container.
|
|
savings_container_.reset();
|
|
AddUnassignedNodesToEmptyVehicles();
|
|
MakeUnassignedNodesUnperformed();
|
|
if (!Evaluate(/*commit=*/true).has_value()) return false;
|
|
MakePartiallyPerformedPairsUnperformed();
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
}
|
|
|
|
int SavingsFilteredHeuristic::StartNewRouteWithBestVehicleOfType(
|
|
int type, int64_t before_node, int64_t after_node) {
|
|
auto vehicle_is_compatible = [this, before_node, after_node](int vehicle) {
|
|
if (!model()->VehicleVar(before_node)->Contains(vehicle) ||
|
|
!model()->VehicleVar(after_node)->Contains(vehicle)) {
|
|
return false;
|
|
}
|
|
// Try to commit the arc on this vehicle.
|
|
DCHECK(VehicleIsEmpty(vehicle));
|
|
SetNext(model()->Start(vehicle), before_node, vehicle);
|
|
SetNext(before_node, after_node, vehicle);
|
|
SetNext(after_node, model()->End(vehicle), vehicle);
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
};
|
|
|
|
return vehicle_type_curator_
|
|
->GetCompatibleVehicleOfType(
|
|
type, vehicle_is_compatible,
|
|
/*stop_and_return_vehicle*/ [](int) { return false; })
|
|
.first;
|
|
}
|
|
|
|
void SavingsFilteredHeuristic::AddSymmetricArcsToAdjacencyLists(
|
|
std::vector<std::vector<int64_t>>* adjacency_lists) {
|
|
for (int64_t node = 0; node < adjacency_lists->size(); node++) {
|
|
for (int64_t neighbor : (*adjacency_lists)[node]) {
|
|
if (model()->IsStart(neighbor) || model()->IsEnd(neighbor)) {
|
|
continue;
|
|
}
|
|
(*adjacency_lists)[neighbor].push_back(node);
|
|
}
|
|
}
|
|
absl::c_transform(*adjacency_lists, adjacency_lists->begin(),
|
|
[](std::vector<int64_t> vec) {
|
|
absl::c_sort(vec);
|
|
vec.erase(std::unique(vec.begin(), vec.end()), vec.end());
|
|
return vec;
|
|
});
|
|
}
|
|
|
|
// Computes the savings related to each pair of non-start and non-end nodes.
|
|
// The savings value for an arc a-->b for a vehicle starting at node s and
|
|
// ending at node e is:
|
|
// saving = cost(s-->a-->e) + cost(s-->b-->e) - cost(s-->a-->b-->e), i.e.
|
|
// saving = cost(a-->e) + cost(s-->b) - cost(a-->b)
|
|
// The saving value also considers a coefficient for the cost of the arc
|
|
// a-->b, which results in:
|
|
// saving = cost(a-->e) + cost(s-->b) - [arc_coefficient_ * cost(a-->b)]
|
|
// The higher this saving value, the better the arc.
|
|
// Here, the value stored for the savings is -saving, which are therefore
|
|
// considered in decreasing order.
|
|
bool SavingsFilteredHeuristic::ComputeSavings() {
|
|
const int num_vehicle_types = vehicle_type_curator_->NumTypes();
|
|
const int size = model()->Size();
|
|
|
|
std::vector<int64_t> uncontained_non_start_end_nodes;
|
|
uncontained_non_start_end_nodes.reserve(size);
|
|
for (int node = 0; node < size; node++) {
|
|
if (!model()->IsStart(node) && !model()->IsEnd(node) && !Contains(node)) {
|
|
uncontained_non_start_end_nodes.push_back(node);
|
|
}
|
|
}
|
|
|
|
const int64_t saving_neighbors =
|
|
std::min(MaxNumNeighborsPerNode(num_vehicle_types),
|
|
static_cast<int64_t>(uncontained_non_start_end_nodes.size()));
|
|
|
|
savings_container_ =
|
|
std::make_unique<SavingsContainer<Saving>>(this, num_vehicle_types);
|
|
savings_container_->InitializeContainer(size, saving_neighbors);
|
|
if (StopSearch()) return false;
|
|
std::vector<std::vector<int64_t>> adjacency_lists(size);
|
|
|
|
for (int type = 0; type < num_vehicle_types; ++type) {
|
|
const int vehicle =
|
|
vehicle_type_curator_->GetLowestFixedCostVehicleOfType(type);
|
|
if (vehicle < 0) continue;
|
|
|
|
const int64_t cost_class =
|
|
model()->GetCostClassIndexOfVehicle(vehicle).value();
|
|
const int64_t start = model()->Start(vehicle);
|
|
const int64_t end = model()->End(vehicle);
|
|
const int64_t fixed_cost = model()->GetFixedCostOfVehicle(vehicle);
|
|
|
|
// Compute the neighbors for each non-start/end node not already inserted in
|
|
// the model.
|
|
for (int before_node : uncontained_non_start_end_nodes) {
|
|
std::vector<std::pair</*cost*/ int64_t, /*node*/ int64_t>>
|
|
costed_after_nodes;
|
|
costed_after_nodes.reserve(uncontained_non_start_end_nodes.size());
|
|
if (StopSearch()) return false;
|
|
for (int after_node : uncontained_non_start_end_nodes) {
|
|
if (after_node != before_node) {
|
|
costed_after_nodes.push_back(std::make_pair(
|
|
model()->GetArcCostForClass(before_node, after_node, cost_class),
|
|
after_node));
|
|
}
|
|
}
|
|
if (saving_neighbors < costed_after_nodes.size()) {
|
|
std::nth_element(costed_after_nodes.begin(),
|
|
costed_after_nodes.begin() + saving_neighbors,
|
|
costed_after_nodes.end());
|
|
costed_after_nodes.resize(saving_neighbors);
|
|
}
|
|
adjacency_lists[before_node].resize(costed_after_nodes.size());
|
|
std::transform(costed_after_nodes.begin(), costed_after_nodes.end(),
|
|
adjacency_lists[before_node].begin(),
|
|
[](std::pair<int64_t, int64_t> cost_and_node) {
|
|
return cost_and_node.second;
|
|
});
|
|
}
|
|
if (savings_params_.add_reverse_arcs()) {
|
|
AddSymmetricArcsToAdjacencyLists(&adjacency_lists);
|
|
}
|
|
if (StopSearch()) return false;
|
|
|
|
// Build the savings for this vehicle type given the adjacency_lists.
|
|
for (int before_node : uncontained_non_start_end_nodes) {
|
|
const int64_t before_to_end_cost =
|
|
model()->GetArcCostForClass(before_node, end, cost_class);
|
|
const int64_t start_to_before_cost =
|
|
CapSub(model()->GetArcCostForClass(start, before_node, cost_class),
|
|
fixed_cost);
|
|
if (StopSearch()) return false;
|
|
for (int64_t after_node : adjacency_lists[before_node]) {
|
|
if (model()->IsStart(after_node) || model()->IsEnd(after_node) ||
|
|
before_node == after_node || Contains(after_node)) {
|
|
continue;
|
|
}
|
|
const int64_t arc_cost =
|
|
model()->GetArcCostForClass(before_node, after_node, cost_class);
|
|
const int64_t start_to_after_cost =
|
|
CapSub(model()->GetArcCostForClass(start, after_node, cost_class),
|
|
fixed_cost);
|
|
const int64_t after_to_end_cost =
|
|
model()->GetArcCostForClass(after_node, end, cost_class);
|
|
|
|
const double weighted_arc_cost_fp =
|
|
savings_params_.arc_coefficient() * arc_cost;
|
|
const int64_t weighted_arc_cost =
|
|
weighted_arc_cost_fp < std::numeric_limits<int64_t>::max()
|
|
? static_cast<int64_t>(weighted_arc_cost_fp)
|
|
: std::numeric_limits<int64_t>::max();
|
|
const int64_t saving_value = CapSub(
|
|
CapAdd(before_to_end_cost, start_to_after_cost), weighted_arc_cost);
|
|
|
|
const Saving saving =
|
|
BuildSaving(-saving_value, type, before_node, after_node);
|
|
|
|
const int64_t total_cost =
|
|
CapAdd(CapAdd(start_to_before_cost, arc_cost), after_to_end_cost);
|
|
|
|
savings_container_->AddNewSaving(saving, total_cost, before_node,
|
|
after_node, type);
|
|
}
|
|
}
|
|
}
|
|
savings_container_->Sort();
|
|
return !StopSearch();
|
|
}
|
|
|
|
int64_t SavingsFilteredHeuristic::MaxNumNeighborsPerNode(
|
|
int num_vehicle_types) const {
|
|
const int64_t size = model()->Size();
|
|
|
|
const int64_t num_neighbors_with_ratio =
|
|
std::max(1.0, size * savings_params_.neighbors_ratio());
|
|
|
|
// A single Saving takes 2*8 bytes of memory.
|
|
// max_memory_usage_in_savings_unit = num_savings * multiplicative_factor,
|
|
// Where multiplicative_factor is the memory taken (in Savings unit) for each
|
|
// computed Saving.
|
|
const double max_memory_usage_in_savings_unit =
|
|
savings_params_.max_memory_usage_bytes() / 16;
|
|
|
|
// In the SavingsContainer, for each Saving, the Savings are stored:
|
|
// - Once in "sorted_savings_per_vehicle_type", and (at most) once in
|
|
// "sorted_savings_" --> factor 2
|
|
// - If num_vehicle_types > 1, they're also stored by arc_index in
|
|
// "costs_and_savings_per_arc", along with their int64_t cost --> factor 1.5
|
|
//
|
|
// On top of that,
|
|
// - In the sequential version, the Saving* are also stored by in-coming and
|
|
// outgoing node (in in/out_savings_ptr), adding another 2*8 bytes per
|
|
// Saving --> factor 1.
|
|
// - In the parallel version, skipped Savings are also stored in
|
|
// skipped_savings_starting/ending_at_, resulting in a maximum added factor
|
|
// of 2 for each Saving.
|
|
// These extra factors are given by ExtraSavingsMemoryMultiplicativeFactor().
|
|
double multiplicative_factor = 2.0 + ExtraSavingsMemoryMultiplicativeFactor();
|
|
if (num_vehicle_types > 1) {
|
|
multiplicative_factor += 1.5;
|
|
}
|
|
const double num_savings =
|
|
max_memory_usage_in_savings_unit / multiplicative_factor;
|
|
const int64_t num_neighbors_with_memory_restriction =
|
|
std::max(1.0, num_savings / (num_vehicle_types * size));
|
|
|
|
return std::min(num_neighbors_with_ratio,
|
|
num_neighbors_with_memory_restriction);
|
|
}
|
|
|
|
// SequentialSavingsFilteredHeuristic
|
|
|
|
void SequentialSavingsFilteredHeuristic::BuildRoutesFromSavings() {
|
|
const int vehicle_types = vehicle_type_curator_->NumTypes();
|
|
DCHECK_GT(vehicle_types, 0);
|
|
const int size = model()->Size();
|
|
// Store savings for each incoming and outgoing node and by vehicle type. This
|
|
// is necessary to quickly extend partial chains without scanning all savings.
|
|
std::vector<std::vector<const Saving*>> in_savings_ptr(size * vehicle_types);
|
|
std::vector<std::vector<const Saving*>> out_savings_ptr(size * vehicle_types);
|
|
for (int type = 0; type < vehicle_types; type++) {
|
|
const int vehicle_type_offset = type * size;
|
|
const std::vector<Saving>& sorted_savings_for_type =
|
|
savings_container_->GetSortedSavingsForVehicleType(type);
|
|
for (const Saving& saving : sorted_savings_for_type) {
|
|
DCHECK_EQ(saving.vehicle_type, type);
|
|
const int before_node = saving.before_node;
|
|
in_savings_ptr[vehicle_type_offset + before_node].push_back(&saving);
|
|
const int after_node = saving.after_node;
|
|
out_savings_ptr[vehicle_type_offset + after_node].push_back(&saving);
|
|
}
|
|
}
|
|
|
|
// Build routes from savings.
|
|
while (savings_container_->HasSaving()) {
|
|
if (StopSearch()) return;
|
|
// First find the best saving to start a new route.
|
|
const Saving saving = savings_container_->GetSaving();
|
|
int before_node = saving.before_node;
|
|
int after_node = saving.after_node;
|
|
const bool nodes_contained = Contains(before_node) || Contains(after_node);
|
|
|
|
if (nodes_contained) {
|
|
savings_container_->Update(false);
|
|
continue;
|
|
}
|
|
|
|
// Find the right vehicle to start the route with this Saving.
|
|
const int type = saving.vehicle_type;
|
|
const int vehicle =
|
|
StartNewRouteWithBestVehicleOfType(type, before_node, after_node);
|
|
if (vehicle < 0) {
|
|
savings_container_->Update(true);
|
|
continue;
|
|
}
|
|
|
|
const int64_t start = model()->Start(vehicle);
|
|
const int64_t end = model()->End(vehicle);
|
|
// Then extend the route from both ends of the partial route.
|
|
int in_index = 0;
|
|
int out_index = 0;
|
|
const int saving_offset = type * size;
|
|
|
|
while (in_index < in_savings_ptr[saving_offset + after_node].size() ||
|
|
out_index < out_savings_ptr[saving_offset + before_node].size()) {
|
|
if (StopSearch()) return;
|
|
// First determine how to extend the route.
|
|
int before_before_node = -1;
|
|
int after_after_node = -1;
|
|
if (in_index < in_savings_ptr[saving_offset + after_node].size()) {
|
|
const Saving& in_saving =
|
|
*(in_savings_ptr[saving_offset + after_node][in_index]);
|
|
if (out_index < out_savings_ptr[saving_offset + before_node].size()) {
|
|
const Saving& out_saving =
|
|
*(out_savings_ptr[saving_offset + before_node][out_index]);
|
|
if (in_saving.saving < out_saving.saving) {
|
|
after_after_node = in_saving.after_node;
|
|
} else {
|
|
before_before_node = out_saving.before_node;
|
|
}
|
|
} else {
|
|
after_after_node = in_saving.after_node;
|
|
}
|
|
} else {
|
|
before_before_node =
|
|
out_savings_ptr[saving_offset + before_node][out_index]
|
|
->before_node;
|
|
}
|
|
// Extend the route
|
|
if (after_after_node != -1) {
|
|
DCHECK_EQ(before_before_node, -1);
|
|
++in_index;
|
|
// Extending after after_node
|
|
if (!Contains(after_after_node)) {
|
|
SetNext(after_node, after_after_node, vehicle);
|
|
SetNext(after_after_node, end, vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
in_index = 0;
|
|
after_node = after_after_node;
|
|
}
|
|
}
|
|
} else {
|
|
// Extending before before_node
|
|
CHECK_GE(before_before_node, 0);
|
|
++out_index;
|
|
if (!Contains(before_before_node)) {
|
|
SetNext(start, before_before_node, vehicle);
|
|
SetNext(before_before_node, before_node, vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
out_index = 0;
|
|
before_node = before_before_node;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
savings_container_->Update(false);
|
|
}
|
|
}
|
|
|
|
// ParallelSavingsFilteredHeuristic
|
|
|
|
void ParallelSavingsFilteredHeuristic::BuildRoutesFromSavings() {
|
|
// Initialize the vehicles of the first/last non start/end nodes served by
|
|
// each route.
|
|
const int64_t size = model()->Size();
|
|
const int vehicles = model()->vehicles();
|
|
|
|
first_node_on_route_.resize(vehicles, -1);
|
|
last_node_on_route_.resize(vehicles, -1);
|
|
vehicle_of_first_or_last_node_.resize(size, -1);
|
|
|
|
for (int vehicle = 0; vehicle < vehicles; vehicle++) {
|
|
const int64_t start = model()->Start(vehicle);
|
|
const int64_t end = model()->End(vehicle);
|
|
if (!Contains(start)) {
|
|
continue;
|
|
}
|
|
int64_t node = Value(start);
|
|
if (node != end) {
|
|
vehicle_of_first_or_last_node_[node] = vehicle;
|
|
first_node_on_route_[vehicle] = node;
|
|
|
|
int64_t next = Value(node);
|
|
while (next != end) {
|
|
node = next;
|
|
next = Value(node);
|
|
}
|
|
vehicle_of_first_or_last_node_[node] = vehicle;
|
|
last_node_on_route_[vehicle] = node;
|
|
}
|
|
}
|
|
|
|
while (savings_container_->HasSaving()) {
|
|
if (StopSearch()) return;
|
|
const Saving saving = savings_container_->GetSaving();
|
|
const int64_t before_node = saving.before_node;
|
|
const int64_t after_node = saving.after_node;
|
|
const int type = saving.vehicle_type;
|
|
|
|
if (!Contains(before_node) && !Contains(after_node)) {
|
|
// Neither nodes are contained, start a new route.
|
|
bool committed = false;
|
|
|
|
const int vehicle =
|
|
StartNewRouteWithBestVehicleOfType(type, before_node, after_node);
|
|
|
|
if (vehicle >= 0) {
|
|
committed = true;
|
|
// Store before_node and after_node as first and last nodes of the route
|
|
vehicle_of_first_or_last_node_[before_node] = vehicle;
|
|
vehicle_of_first_or_last_node_[after_node] = vehicle;
|
|
first_node_on_route_[vehicle] = before_node;
|
|
last_node_on_route_[vehicle] = after_node;
|
|
savings_container_->ReinjectSkippedSavingsStartingAt(after_node);
|
|
savings_container_->ReinjectSkippedSavingsEndingAt(before_node);
|
|
}
|
|
savings_container_->Update(!committed);
|
|
continue;
|
|
}
|
|
|
|
if (Contains(before_node) && Contains(after_node)) {
|
|
// Merge the two routes if before_node is last and after_node first of its
|
|
// route, the two nodes aren't already on the same route, and the vehicle
|
|
// types are compatible.
|
|
const int v1 = vehicle_of_first_or_last_node_[before_node];
|
|
const int64_t last_node = v1 == -1 ? -1 : last_node_on_route_[v1];
|
|
|
|
const int v2 = vehicle_of_first_or_last_node_[after_node];
|
|
const int64_t first_node = v2 == -1 ? -1 : first_node_on_route_[v2];
|
|
|
|
if (before_node == last_node && after_node == first_node && v1 != v2 &&
|
|
vehicle_type_curator_->Type(v1) == vehicle_type_curator_->Type(v2)) {
|
|
CHECK_EQ(Value(before_node), model()->End(v1));
|
|
CHECK_EQ(Value(model()->Start(v2)), after_node);
|
|
|
|
// We try merging the two routes.
|
|
// TODO(user): Try to use skipped savings to start new routes when
|
|
// a vehicle becomes available after a merge (not trivial because it can
|
|
// result in an infinite loop).
|
|
MergeRoutes(v1, v2, before_node, after_node);
|
|
}
|
|
}
|
|
|
|
if (Contains(before_node) && !Contains(after_node)) {
|
|
const int vehicle = vehicle_of_first_or_last_node_[before_node];
|
|
const int64_t last_node =
|
|
vehicle == -1 ? -1 : last_node_on_route_[vehicle];
|
|
|
|
if (before_node == last_node) {
|
|
const int64_t end = model()->End(vehicle);
|
|
CHECK_EQ(Value(before_node), end);
|
|
|
|
const int route_type = vehicle_type_curator_->Type(vehicle);
|
|
if (type != route_type) {
|
|
// The saving doesn't correspond to the type of the vehicle serving
|
|
// before_node. We update the container with the correct type.
|
|
savings_container_->UpdateWithType(route_type);
|
|
continue;
|
|
}
|
|
|
|
// Try adding after_node on route of before_node.
|
|
SetNext(before_node, after_node, vehicle);
|
|
SetNext(after_node, end, vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
if (first_node_on_route_[vehicle] != before_node) {
|
|
// before_node is no longer the start or end of its route
|
|
DCHECK_NE(Value(model()->Start(vehicle)), before_node);
|
|
vehicle_of_first_or_last_node_[before_node] = -1;
|
|
}
|
|
vehicle_of_first_or_last_node_[after_node] = vehicle;
|
|
last_node_on_route_[vehicle] = after_node;
|
|
savings_container_->ReinjectSkippedSavingsStartingAt(after_node);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!Contains(before_node) && Contains(after_node)) {
|
|
const int vehicle = vehicle_of_first_or_last_node_[after_node];
|
|
const int64_t first_node =
|
|
vehicle == -1 ? -1 : first_node_on_route_[vehicle];
|
|
|
|
if (after_node == first_node) {
|
|
const int64_t start = model()->Start(vehicle);
|
|
CHECK_EQ(Value(start), after_node);
|
|
|
|
const int route_type = vehicle_type_curator_->Type(vehicle);
|
|
if (type != route_type) {
|
|
// The saving doesn't correspond to the type of the vehicle serving
|
|
// after_node. We update the container with the correct type.
|
|
savings_container_->UpdateWithType(route_type);
|
|
continue;
|
|
}
|
|
|
|
// Try adding before_node on route of after_node.
|
|
SetNext(before_node, after_node, vehicle);
|
|
SetNext(start, before_node, vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
if (last_node_on_route_[vehicle] != after_node) {
|
|
// after_node is no longer the start or end of its route
|
|
DCHECK_NE(Value(after_node), model()->End(vehicle));
|
|
vehicle_of_first_or_last_node_[after_node] = -1;
|
|
}
|
|
vehicle_of_first_or_last_node_[before_node] = vehicle;
|
|
first_node_on_route_[vehicle] = before_node;
|
|
savings_container_->ReinjectSkippedSavingsEndingAt(before_node);
|
|
}
|
|
}
|
|
}
|
|
savings_container_->Update(/*update_best_saving*/ false);
|
|
}
|
|
}
|
|
|
|
void ParallelSavingsFilteredHeuristic::MergeRoutes(int first_vehicle,
|
|
int second_vehicle,
|
|
int64_t before_node,
|
|
int64_t after_node) {
|
|
if (StopSearch()) return;
|
|
const int64_t new_first_node = first_node_on_route_[first_vehicle];
|
|
DCHECK_EQ(vehicle_of_first_or_last_node_[new_first_node], first_vehicle);
|
|
CHECK_EQ(Value(model()->Start(first_vehicle)), new_first_node);
|
|
const int64_t new_last_node = last_node_on_route_[second_vehicle];
|
|
DCHECK_EQ(vehicle_of_first_or_last_node_[new_last_node], second_vehicle);
|
|
CHECK_EQ(Value(new_last_node), model()->End(second_vehicle));
|
|
|
|
// Select the vehicle with lower fixed cost to merge the routes.
|
|
int used_vehicle = first_vehicle;
|
|
int unused_vehicle = second_vehicle;
|
|
if (model()->GetFixedCostOfVehicle(first_vehicle) >
|
|
model()->GetFixedCostOfVehicle(second_vehicle)) {
|
|
used_vehicle = second_vehicle;
|
|
unused_vehicle = first_vehicle;
|
|
}
|
|
|
|
SetNext(before_node, after_node, used_vehicle);
|
|
SetNext(model()->Start(unused_vehicle), model()->End(unused_vehicle),
|
|
unused_vehicle);
|
|
if (used_vehicle == first_vehicle) {
|
|
SetNext(new_last_node, model()->End(used_vehicle), used_vehicle);
|
|
} else {
|
|
SetNext(model()->Start(used_vehicle), new_first_node, used_vehicle);
|
|
}
|
|
bool committed = Evaluate(/*commit=*/true).has_value();
|
|
if (!committed &&
|
|
model()->GetVehicleClassIndexOfVehicle(first_vehicle).value() !=
|
|
model()->GetVehicleClassIndexOfVehicle(second_vehicle).value()) {
|
|
// Try committing on other vehicle instead.
|
|
std::swap(used_vehicle, unused_vehicle);
|
|
SetNext(before_node, after_node, used_vehicle);
|
|
SetNext(model()->Start(unused_vehicle), model()->End(unused_vehicle),
|
|
unused_vehicle);
|
|
if (used_vehicle == first_vehicle) {
|
|
SetNext(new_last_node, model()->End(used_vehicle), used_vehicle);
|
|
} else {
|
|
SetNext(model()->Start(used_vehicle), new_first_node, used_vehicle);
|
|
}
|
|
committed = Evaluate(/*commit=*/true).has_value();
|
|
}
|
|
if (committed) {
|
|
// Make unused_vehicle available
|
|
vehicle_type_curator_->ReinjectVehicleOfClass(
|
|
unused_vehicle,
|
|
model()->GetVehicleClassIndexOfVehicle(unused_vehicle).value(),
|
|
model()->GetFixedCostOfVehicle(unused_vehicle));
|
|
|
|
// Update the first and last nodes on vehicles.
|
|
first_node_on_route_[unused_vehicle] = -1;
|
|
last_node_on_route_[unused_vehicle] = -1;
|
|
vehicle_of_first_or_last_node_[before_node] = -1;
|
|
vehicle_of_first_or_last_node_[after_node] = -1;
|
|
first_node_on_route_[used_vehicle] = new_first_node;
|
|
last_node_on_route_[used_vehicle] = new_last_node;
|
|
vehicle_of_first_or_last_node_[new_last_node] = used_vehicle;
|
|
vehicle_of_first_or_last_node_[new_first_node] = used_vehicle;
|
|
}
|
|
}
|
|
|
|
// ChristofidesFilteredHeuristic
|
|
|
|
ChristofidesFilteredHeuristic::ChristofidesFilteredHeuristic(
|
|
RoutingModel* model, std::function<bool()> stop_search,
|
|
LocalSearchFilterManager* filter_manager, bool use_minimum_matching)
|
|
: RoutingFilteredHeuristic(model, std::move(stop_search), filter_manager),
|
|
use_minimum_matching_(use_minimum_matching) {}
|
|
|
|
// TODO(user): Support pickup & delivery.
|
|
bool ChristofidesFilteredHeuristic::BuildSolutionInternal() {
|
|
const int size = model()->Size() - model()->vehicles() + 1;
|
|
// Node indices for Christofides solver.
|
|
// 0: start/end node
|
|
// >0: non start/end nodes
|
|
// TODO(user): Add robustness to fixed arcs by collapsing them into meta-
|
|
// nodes.
|
|
std::vector<int> indices(1, 0);
|
|
for (int i = 1; i < size; ++i) {
|
|
if (!model()->IsStart(i) && !model()->IsEnd(i)) {
|
|
indices.push_back(i);
|
|
}
|
|
}
|
|
const int num_cost_classes = model()->GetCostClassesCount();
|
|
std::vector<std::vector<int>> path_per_cost_class(num_cost_classes);
|
|
std::vector<bool> class_covered(num_cost_classes, false);
|
|
for (int vehicle = 0; vehicle < model()->vehicles(); ++vehicle) {
|
|
const int64_t cost_class =
|
|
model()->GetCostClassIndexOfVehicle(vehicle).value();
|
|
if (!class_covered[cost_class]) {
|
|
class_covered[cost_class] = true;
|
|
const int64_t start = model()->Start(vehicle);
|
|
const int64_t end = model()->End(vehicle);
|
|
auto cost = [this, &indices, start, end, cost_class](int from, int to) {
|
|
DCHECK_LT(from, indices.size());
|
|
DCHECK_LT(to, indices.size());
|
|
const int from_index = (from == 0) ? start : indices[from];
|
|
const int to_index = (to == 0) ? end : indices[to];
|
|
const int64_t cost =
|
|
model()->GetArcCostForClass(from_index, to_index, cost_class);
|
|
// To avoid overflow issues, capping costs at kint64max/2, the maximum
|
|
// value supported by MinCostPerfectMatching.
|
|
// TODO(user): Investigate if ChristofidesPathSolver should not
|
|
// return a status to bail out fast in case of problem.
|
|
return std::min(cost, std::numeric_limits<int64_t>::max() / 2);
|
|
};
|
|
using Cost = decltype(cost);
|
|
ChristofidesPathSolver<int64_t, int64_t, int, Cost> christofides_solver(
|
|
indices.size(), cost);
|
|
if (use_minimum_matching_) {
|
|
christofides_solver.SetMatchingAlgorithm(
|
|
ChristofidesPathSolver<int64_t, int64_t, int, Cost>::
|
|
MatchingAlgorithm::MINIMUM_WEIGHT_MATCHING);
|
|
}
|
|
if (christofides_solver.Solve()) {
|
|
path_per_cost_class[cost_class] =
|
|
christofides_solver.TravelingSalesmanPath();
|
|
}
|
|
}
|
|
}
|
|
// TODO(user): Investigate if sorting paths per cost improves solutions.
|
|
for (int vehicle = 0; vehicle < model()->vehicles(); ++vehicle) {
|
|
const int64_t cost_class =
|
|
model()->GetCostClassIndexOfVehicle(vehicle).value();
|
|
const std::vector<int>& path = path_per_cost_class[cost_class];
|
|
if (path.empty()) continue;
|
|
DCHECK_EQ(0, path[0]);
|
|
DCHECK_EQ(0, path.back());
|
|
// Extend route from start.
|
|
int prev = GetStartChainEnd(vehicle);
|
|
const int end = model()->End(vehicle);
|
|
for (int i = 1; i < path.size() - 1 && prev != end; ++i) {
|
|
if (StopSearch()) return false;
|
|
int next = indices[path[i]];
|
|
if (!Contains(next)) {
|
|
SetNext(prev, next, vehicle);
|
|
SetNext(next, end, vehicle);
|
|
if (Evaluate(/*commit=*/true).has_value()) {
|
|
prev = next;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
MakeUnassignedNodesUnperformed();
|
|
return Evaluate(/*commit=*/true).has_value();
|
|
}
|
|
|
|
// Sweep heuristic
|
|
// TODO(user): Clean up to match other first solution strategies.
|
|
|
|
namespace {
|
|
struct SweepIndex {
|
|
SweepIndex(const int64_t index, const double angle, const double distance)
|
|
: index(index), angle(angle), distance(distance) {}
|
|
~SweepIndex() = default;
|
|
|
|
int64_t index;
|
|
double angle;
|
|
double distance;
|
|
};
|
|
|
|
struct SweepIndexSortAngle {
|
|
bool operator()(const SweepIndex& node1, const SweepIndex& node2) const {
|
|
return (node1.angle < node2.angle);
|
|
}
|
|
} SweepIndexAngleComparator;
|
|
|
|
struct SweepIndexSortDistance {
|
|
bool operator()(const SweepIndex& node1, const SweepIndex& node2) const {
|
|
return (node1.distance < node2.distance);
|
|
}
|
|
} SweepIndexDistanceComparator;
|
|
} // namespace
|
|
|
|
SweepArranger::SweepArranger(
|
|
absl::Span<const std::pair<int64_t, int64_t>> points)
|
|
: coordinates_(2 * points.size(), 0), sectors_(1) {
|
|
for (int64_t i = 0; i < points.size(); ++i) {
|
|
coordinates_[2 * i] = points[i].first;
|
|
coordinates_[2 * i + 1] = points[i].second;
|
|
}
|
|
}
|
|
|
|
// Splits the space of the indices into sectors and sorts the indices of each
|
|
// sector with ascending angle from the depot.
|
|
void SweepArranger::ArrangeIndices(std::vector<int64_t>* indices) {
|
|
const double pi_rad = 3.14159265;
|
|
// Suppose that the center is at x0, y0.
|
|
const int x0 = coordinates_[0];
|
|
const int y0 = coordinates_[1];
|
|
|
|
std::vector<SweepIndex> sweep_indices;
|
|
for (int64_t index = 0; index < static_cast<int>(coordinates_.size()) / 2;
|
|
++index) {
|
|
const int x = coordinates_[2 * index];
|
|
const int y = coordinates_[2 * index + 1];
|
|
const double x_delta = x - x0;
|
|
const double y_delta = y - y0;
|
|
double square_distance = x_delta * x_delta + y_delta * y_delta;
|
|
double angle = square_distance == 0 ? 0 : std::atan2(y_delta, x_delta);
|
|
angle = angle >= 0 ? angle : 2 * pi_rad + angle;
|
|
sweep_indices.push_back(SweepIndex(index, angle, square_distance));
|
|
}
|
|
absl::c_sort(sweep_indices, SweepIndexDistanceComparator);
|
|
|
|
const int size = static_cast<int>(sweep_indices.size()) / sectors_;
|
|
for (int sector = 0; sector < sectors_; ++sector) {
|
|
std::vector<SweepIndex> cluster;
|
|
std::vector<SweepIndex>::iterator begin =
|
|
sweep_indices.begin() + sector * size;
|
|
std::vector<SweepIndex>::iterator end =
|
|
sector == sectors_ - 1 ? sweep_indices.end()
|
|
: sweep_indices.begin() + (sector + 1) * size;
|
|
std::sort(begin, end, SweepIndexAngleComparator);
|
|
}
|
|
for (const SweepIndex& sweep_index : sweep_indices) {
|
|
indices->push_back(sweep_index.index);
|
|
}
|
|
}
|
|
|
|
namespace {
|
|
|
|
struct Link {
|
|
Link(std::pair<int, int> link, double value, int vehicle_class,
|
|
int64_t start_depot, int64_t end_depot)
|
|
: link(link),
|
|
value(value),
|
|
vehicle_class(vehicle_class),
|
|
start_depot(start_depot),
|
|
end_depot(end_depot) {}
|
|
~Link() = default;
|
|
|
|
std::pair<int, int> link;
|
|
int64_t value;
|
|
int vehicle_class;
|
|
int64_t start_depot;
|
|
int64_t end_depot;
|
|
};
|
|
|
|
// The RouteConstructor creates the routes of a VRP instance subject to its
|
|
// constraints by iterating on a list of arcs appearing in descending order
|
|
// of priority.
|
|
// TODO(user): Use the dimension class in this class.
|
|
// TODO(user): Add support for vehicle-dependent dimension transits.
|
|
class RouteConstructor {
|
|
public:
|
|
RouteConstructor(Assignment* const assignment, RoutingModel* const model,
|
|
bool check_assignment, int64_t num_indices,
|
|
const std::vector<Link>& links_list)
|
|
: assignment_(assignment),
|
|
model_(model),
|
|
check_assignment_(check_assignment),
|
|
solver_(model_->solver()),
|
|
num_indices_(num_indices),
|
|
links_list_(links_list),
|
|
nexts_(model_->Nexts()),
|
|
in_route_(num_indices_, -1),
|
|
final_routes_(),
|
|
index_to_chain_index_(num_indices, -1),
|
|
index_to_vehicle_class_index_(num_indices, -1) {
|
|
{
|
|
const std::vector<std::string> dimension_names =
|
|
model_->GetAllDimensionNames();
|
|
dimensions_.assign(dimension_names.size(), nullptr);
|
|
for (int i = 0; i < dimension_names.size(); ++i) {
|
|
dimensions_[i] = &model_->GetDimensionOrDie(dimension_names[i]);
|
|
}
|
|
}
|
|
cumuls_.resize(dimensions_.size());
|
|
for (std::vector<int64_t>& cumuls : cumuls_) {
|
|
cumuls.resize(num_indices_);
|
|
}
|
|
new_possible_cumuls_.resize(dimensions_.size());
|
|
}
|
|
|
|
~RouteConstructor() = default;
|
|
|
|
void Construct() {
|
|
model_->solver()->TopPeriodicCheck();
|
|
// Initial State: Each order is served by its own vehicle.
|
|
for (int index = 0; index < num_indices_; ++index) {
|
|
if (!model_->IsStart(index) && !model_->IsEnd(index)) {
|
|
routes_.push_back({index});
|
|
in_route_[index] = routes_.size() - 1;
|
|
}
|
|
}
|
|
|
|
for (const Link& link : links_list_) {
|
|
model_->solver()->TopPeriodicCheck();
|
|
const int index1 = link.link.first;
|
|
const int index2 = link.link.second;
|
|
const int vehicle_class = link.vehicle_class;
|
|
const int64_t start_depot = link.start_depot;
|
|
const int64_t end_depot = link.end_depot;
|
|
|
|
// Initialisation of cumuls_ if the indices are encountered for first time
|
|
if (index_to_vehicle_class_index_[index1] < 0) {
|
|
for (int dimension_index = 0; dimension_index < dimensions_.size();
|
|
++dimension_index) {
|
|
cumuls_[dimension_index][index1] =
|
|
std::max(dimensions_[dimension_index]->GetTransitValue(
|
|
start_depot, index1, 0),
|
|
dimensions_[dimension_index]->CumulVar(index1)->Min());
|
|
}
|
|
}
|
|
if (index_to_vehicle_class_index_[index2] < 0) {
|
|
for (int dimension_index = 0; dimension_index < dimensions_.size();
|
|
++dimension_index) {
|
|
cumuls_[dimension_index][index2] =
|
|
std::max(dimensions_[dimension_index]->GetTransitValue(
|
|
start_depot, index2, 0),
|
|
dimensions_[dimension_index]->CumulVar(index2)->Min());
|
|
}
|
|
}
|
|
|
|
const int route_index1 = in_route_[index1];
|
|
const int route_index2 = in_route_[index2];
|
|
const bool merge =
|
|
route_index1 >= 0 && route_index2 >= 0 &&
|
|
FeasibleMerge(routes_[route_index1], routes_[route_index2], index1,
|
|
index2, route_index1, route_index2, vehicle_class,
|
|
start_depot, end_depot);
|
|
if (Merge(merge, route_index1, route_index2)) {
|
|
index_to_vehicle_class_index_[index1] = vehicle_class;
|
|
index_to_vehicle_class_index_[index2] = vehicle_class;
|
|
}
|
|
}
|
|
|
|
model_->solver()->TopPeriodicCheck();
|
|
// Beyond this point not checking limits anymore as the rest of the code is
|
|
// linear and that given we managed to build a solution would be ludicrous
|
|
// to drop it now.
|
|
for (int chain_index = 0; chain_index < chains_.size(); ++chain_index) {
|
|
if (!deleted_chains_.contains(chain_index)) {
|
|
final_chains_.push_back(chains_[chain_index]);
|
|
}
|
|
}
|
|
absl::c_sort(final_chains_, ChainComparator);
|
|
for (int route_index = 0; route_index < routes_.size(); ++route_index) {
|
|
if (!deleted_routes_.contains(route_index)) {
|
|
final_routes_.push_back(routes_[route_index]);
|
|
}
|
|
}
|
|
absl::c_sort(final_routes_, RouteComparator);
|
|
|
|
const int extra_vehicles = std::max(
|
|
0, static_cast<int>(final_chains_.size()) - model_->vehicles());
|
|
// Bind the Start and End of each chain
|
|
int chain_index = 0;
|
|
for (chain_index = extra_vehicles; chain_index < final_chains_.size();
|
|
++chain_index) {
|
|
if (chain_index - extra_vehicles >= model_->vehicles()) {
|
|
break;
|
|
}
|
|
const int start = final_chains_[chain_index].head;
|
|
const int end = final_chains_[chain_index].tail;
|
|
assignment_->Add(
|
|
model_->NextVar(model_->Start(chain_index - extra_vehicles)));
|
|
assignment_->SetValue(
|
|
model_->NextVar(model_->Start(chain_index - extra_vehicles)), start);
|
|
assignment_->Add(nexts_[end]);
|
|
assignment_->SetValue(nexts_[end],
|
|
model_->End(chain_index - extra_vehicles));
|
|
}
|
|
|
|
// Create the single order routes
|
|
for (int route_index = 0; route_index < final_routes_.size();
|
|
++route_index) {
|
|
if (chain_index - extra_vehicles >= model_->vehicles()) {
|
|
break;
|
|
}
|
|
DCHECK_LT(route_index, final_routes_.size());
|
|
const int head = final_routes_[route_index].front();
|
|
const int tail = final_routes_[route_index].back();
|
|
if (head == tail && head < model_->Size()) {
|
|
assignment_->Add(
|
|
model_->NextVar(model_->Start(chain_index - extra_vehicles)));
|
|
assignment_->SetValue(
|
|
model_->NextVar(model_->Start(chain_index - extra_vehicles)), head);
|
|
assignment_->Add(nexts_[tail]);
|
|
assignment_->SetValue(nexts_[tail],
|
|
model_->End(chain_index - extra_vehicles));
|
|
++chain_index;
|
|
}
|
|
}
|
|
|
|
// Unperformed
|
|
for (int index = 0; index < model_->Size(); ++index) {
|
|
IntVar* const next = nexts_[index];
|
|
if (!assignment_->Contains(next)) {
|
|
assignment_->Add(next);
|
|
if (next->Contains(index)) {
|
|
assignment_->SetValue(next, index);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private:
|
|
enum MergeStatus { FIRST_SECOND, SECOND_FIRST, NO_MERGE };
|
|
|
|
struct RouteSort {
|
|
bool operator()(absl::Span<const int> route1,
|
|
absl::Span<const int> route2) const {
|
|
return (route1.size() < route2.size());
|
|
}
|
|
} RouteComparator;
|
|
|
|
struct Chain {
|
|
int head;
|
|
int tail;
|
|
int nodes;
|
|
};
|
|
|
|
struct ChainSort {
|
|
bool operator()(const Chain& chain1, const Chain& chain2) const {
|
|
return (chain1.nodes < chain2.nodes);
|
|
}
|
|
} ChainComparator;
|
|
|
|
bool Head(int node) const {
|
|
return (node == routes_[in_route_[node]].front());
|
|
}
|
|
|
|
bool Tail(int node) const {
|
|
return (node == routes_[in_route_[node]].back());
|
|
}
|
|
|
|
bool FeasibleRoute(const std::vector<int>& route, int64_t route_cumul,
|
|
int dimension_index) {
|
|
const RoutingDimension& dimension = *dimensions_[dimension_index];
|
|
std::vector<int>::const_iterator it = route.begin();
|
|
int64_t cumul = route_cumul;
|
|
while (it != route.end()) {
|
|
const int previous = *it;
|
|
const int64_t cumul_previous = cumul;
|
|
gtl::InsertOrDie(&(new_possible_cumuls_[dimension_index]), previous,
|
|
cumul_previous);
|
|
++it;
|
|
if (it == route.end()) {
|
|
return true;
|
|
}
|
|
const int next = *it;
|
|
int64_t available_from_previous =
|
|
cumul_previous + dimension.GetTransitValue(previous, next, 0);
|
|
int64_t available_cumul_next =
|
|
std::max(cumuls_[dimension_index][next], available_from_previous);
|
|
|
|
const int64_t slack = available_cumul_next - available_from_previous;
|
|
if (slack > dimension.SlackVar(previous)->Max()) {
|
|
available_cumul_next =
|
|
available_from_previous + dimension.SlackVar(previous)->Max();
|
|
}
|
|
|
|
if (available_cumul_next > dimension.CumulVar(next)->Max()) {
|
|
return false;
|
|
}
|
|
if (available_cumul_next <= cumuls_[dimension_index][next]) {
|
|
return true;
|
|
}
|
|
cumul = available_cumul_next;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CheckRouteConnection(absl::Span<const int> route1,
|
|
const std::vector<int>& route2, int dimension_index,
|
|
int64_t /*start_depot*/, int64_t end_depot) {
|
|
const int tail1 = route1.back();
|
|
const int head2 = route2.front();
|
|
const int tail2 = route2.back();
|
|
const RoutingDimension& dimension = *dimensions_[dimension_index];
|
|
int non_depot_node = -1;
|
|
for (int node = 0; node < num_indices_; ++node) {
|
|
if (!model_->IsStart(node) && !model_->IsEnd(node)) {
|
|
non_depot_node = node;
|
|
break;
|
|
}
|
|
}
|
|
CHECK_GE(non_depot_node, 0);
|
|
const int64_t depot_threshold =
|
|
std::max(dimension.SlackVar(non_depot_node)->Max(),
|
|
dimension.CumulVar(non_depot_node)->Max());
|
|
|
|
int64_t available_from_tail1 = cumuls_[dimension_index][tail1] +
|
|
dimension.GetTransitValue(tail1, head2, 0);
|
|
int64_t new_available_cumul_head2 =
|
|
std::max(cumuls_[dimension_index][head2], available_from_tail1);
|
|
|
|
const int64_t slack = new_available_cumul_head2 - available_from_tail1;
|
|
if (slack > dimension.SlackVar(tail1)->Max()) {
|
|
new_available_cumul_head2 =
|
|
available_from_tail1 + dimension.SlackVar(tail1)->Max();
|
|
}
|
|
|
|
bool feasible_route = true;
|
|
if (new_available_cumul_head2 > dimension.CumulVar(head2)->Max()) {
|
|
return false;
|
|
}
|
|
if (new_available_cumul_head2 <= cumuls_[dimension_index][head2]) {
|
|
return true;
|
|
}
|
|
|
|
feasible_route =
|
|
FeasibleRoute(route2, new_available_cumul_head2, dimension_index);
|
|
const int64_t new_possible_cumul_tail2 =
|
|
new_possible_cumuls_[dimension_index].contains(tail2)
|
|
? new_possible_cumuls_[dimension_index][tail2]
|
|
: cumuls_[dimension_index][tail2];
|
|
|
|
if (!feasible_route || (new_possible_cumul_tail2 +
|
|
dimension.GetTransitValue(tail2, end_depot, 0) >
|
|
depot_threshold)) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool FeasibleMerge(absl::Span<const int> route1,
|
|
const std::vector<int>& route2, int node1, int node2,
|
|
int route_index1, int route_index2, int vehicle_class,
|
|
int64_t start_depot, int64_t end_depot) {
|
|
if ((route_index1 == route_index2) || !(Tail(node1) && Head(node2))) {
|
|
return false;
|
|
}
|
|
|
|
// Vehicle Class Check
|
|
if (!((index_to_vehicle_class_index_[node1] == -1 &&
|
|
index_to_vehicle_class_index_[node2] == -1) ||
|
|
(index_to_vehicle_class_index_[node1] == vehicle_class &&
|
|
index_to_vehicle_class_index_[node2] == -1) ||
|
|
(index_to_vehicle_class_index_[node1] == -1 &&
|
|
index_to_vehicle_class_index_[node2] == vehicle_class) ||
|
|
(index_to_vehicle_class_index_[node1] == vehicle_class &&
|
|
index_to_vehicle_class_index_[node2] == vehicle_class))) {
|
|
return false;
|
|
}
|
|
|
|
// Check Route1 -> Route2 connection for every dimension
|
|
bool merge = true;
|
|
for (int dimension_index = 0; dimension_index < dimensions_.size();
|
|
++dimension_index) {
|
|
new_possible_cumuls_[dimension_index].clear();
|
|
merge = merge && CheckRouteConnection(route1, route2, dimension_index,
|
|
start_depot, end_depot);
|
|
if (!merge) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool CheckTempAssignment(Assignment* const temp_assignment,
|
|
int new_chain_index, int old_chain_index, int head1,
|
|
int tail1, int head2, int tail2) {
|
|
// TODO(user): If the chain index is greater than the number of vehicles,
|
|
// use another vehicle instead.
|
|
if (new_chain_index >= model_->vehicles()) return false;
|
|
const int start = head1;
|
|
temp_assignment->Add(model_->NextVar(model_->Start(new_chain_index)));
|
|
temp_assignment->SetValue(model_->NextVar(model_->Start(new_chain_index)),
|
|
start);
|
|
temp_assignment->Add(nexts_[tail1]);
|
|
temp_assignment->SetValue(nexts_[tail1], head2);
|
|
temp_assignment->Add(nexts_[tail2]);
|
|
temp_assignment->SetValue(nexts_[tail2], model_->End(new_chain_index));
|
|
for (int chain_index = 0; chain_index < chains_.size(); ++chain_index) {
|
|
if ((chain_index != new_chain_index) &&
|
|
(chain_index != old_chain_index) &&
|
|
(!deleted_chains_.contains(chain_index))) {
|
|
const int start = chains_[chain_index].head;
|
|
const int end = chains_[chain_index].tail;
|
|
temp_assignment->Add(model_->NextVar(model_->Start(chain_index)));
|
|
temp_assignment->SetValue(model_->NextVar(model_->Start(chain_index)),
|
|
start);
|
|
temp_assignment->Add(nexts_[end]);
|
|
temp_assignment->SetValue(nexts_[end], model_->End(chain_index));
|
|
}
|
|
}
|
|
return solver_->Solve(solver_->MakeRestoreAssignment(temp_assignment));
|
|
}
|
|
|
|
bool UpdateAssignment(absl::Span<const int> route1,
|
|
absl::Span<const int> route2) {
|
|
bool feasible = true;
|
|
const int head1 = route1.front();
|
|
const int tail1 = route1.back();
|
|
const int head2 = route2.front();
|
|
const int tail2 = route2.back();
|
|
const int chain_index1 = index_to_chain_index_[head1];
|
|
const int chain_index2 = index_to_chain_index_[head2];
|
|
if (chain_index1 < 0 && chain_index2 < 0) {
|
|
const int chain_index = chains_.size();
|
|
if (check_assignment_) {
|
|
Assignment* const temp_assignment =
|
|
solver_->MakeAssignment(assignment_);
|
|
feasible = CheckTempAssignment(temp_assignment, chain_index, -1, head1,
|
|
tail1, head2, tail2);
|
|
}
|
|
if (feasible) {
|
|
Chain chain;
|
|
chain.head = head1;
|
|
chain.tail = tail2;
|
|
chain.nodes = 2;
|
|
index_to_chain_index_[head1] = chain_index;
|
|
index_to_chain_index_[tail2] = chain_index;
|
|
chains_.push_back(chain);
|
|
}
|
|
} else if (chain_index1 >= 0 && chain_index2 < 0) {
|
|
if (check_assignment_) {
|
|
Assignment* const temp_assignment =
|
|
solver_->MakeAssignment(assignment_);
|
|
feasible =
|
|
CheckTempAssignment(temp_assignment, chain_index1, chain_index2,
|
|
head1, tail1, head2, tail2);
|
|
}
|
|
if (feasible) {
|
|
index_to_chain_index_[tail2] = chain_index1;
|
|
chains_[chain_index1].head = head1;
|
|
chains_[chain_index1].tail = tail2;
|
|
++chains_[chain_index1].nodes;
|
|
}
|
|
} else if (chain_index1 < 0 && chain_index2 >= 0) {
|
|
if (check_assignment_) {
|
|
Assignment* const temp_assignment =
|
|
solver_->MakeAssignment(assignment_);
|
|
feasible =
|
|
CheckTempAssignment(temp_assignment, chain_index2, chain_index1,
|
|
head1, tail1, head2, tail2);
|
|
}
|
|
if (feasible) {
|
|
index_to_chain_index_[head1] = chain_index2;
|
|
chains_[chain_index2].head = head1;
|
|
chains_[chain_index2].tail = tail2;
|
|
++chains_[chain_index2].nodes;
|
|
}
|
|
} else {
|
|
if (check_assignment_) {
|
|
Assignment* const temp_assignment =
|
|
solver_->MakeAssignment(assignment_);
|
|
feasible =
|
|
CheckTempAssignment(temp_assignment, chain_index1, chain_index2,
|
|
head1, tail1, head2, tail2);
|
|
}
|
|
if (feasible) {
|
|
index_to_chain_index_[tail2] = chain_index1;
|
|
chains_[chain_index1].head = head1;
|
|
chains_[chain_index1].tail = tail2;
|
|
chains_[chain_index1].nodes += chains_[chain_index2].nodes;
|
|
deleted_chains_.insert(chain_index2);
|
|
}
|
|
}
|
|
if (feasible) {
|
|
assignment_->Add(nexts_[tail1]);
|
|
assignment_->SetValue(nexts_[tail1], head2);
|
|
}
|
|
return feasible;
|
|
}
|
|
|
|
bool Merge(bool merge, int index1, int index2) {
|
|
if (merge) {
|
|
if (UpdateAssignment(routes_[index1], routes_[index2])) {
|
|
// Connection Route1 -> Route2
|
|
for (const int node : routes_[index2]) {
|
|
in_route_[node] = index1;
|
|
routes_[index1].push_back(node);
|
|
}
|
|
for (int dimension_index = 0; dimension_index < dimensions_.size();
|
|
++dimension_index) {
|
|
for (const std::pair<int, int64_t> new_possible_cumul :
|
|
new_possible_cumuls_[dimension_index]) {
|
|
cumuls_[dimension_index][new_possible_cumul.first] =
|
|
new_possible_cumul.second;
|
|
}
|
|
}
|
|
deleted_routes_.insert(index2);
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
Assignment* const assignment_;
|
|
RoutingModel* const model_;
|
|
const bool check_assignment_;
|
|
Solver* const solver_;
|
|
const int64_t num_indices_;
|
|
const std::vector<Link> links_list_;
|
|
std::vector<IntVar*> nexts_;
|
|
std::vector<const RoutingDimension*> dimensions_; // Not owned.
|
|
std::vector<std::vector<int64_t>> cumuls_;
|
|
std::vector<absl::flat_hash_map<int, int64_t>> new_possible_cumuls_;
|
|
std::vector<std::vector<int>> routes_;
|
|
std::vector<int> in_route_;
|
|
absl::flat_hash_set<int> deleted_routes_;
|
|
std::vector<std::vector<int>> final_routes_;
|
|
std::vector<Chain> chains_;
|
|
absl::flat_hash_set<int> deleted_chains_;
|
|
std::vector<Chain> final_chains_;
|
|
std::vector<int> index_to_chain_index_;
|
|
std::vector<int> index_to_vehicle_class_index_;
|
|
};
|
|
|
|
// Decision Builder building a first solution based on Sweep heuristic for
|
|
// Vehicle Routing Problem.
|
|
// Suitable only when distance is considered as the cost.
|
|
class SweepBuilder : public DecisionBuilder {
|
|
public:
|
|
SweepBuilder(RoutingModel* const model, bool check_assignment)
|
|
: model_(model), check_assignment_(check_assignment) {}
|
|
~SweepBuilder() override = default;
|
|
|
|
Decision* Next(Solver* const solver) override {
|
|
// Setup the model of the instance for the Sweep Algorithm
|
|
ModelSetup();
|
|
|
|
// Build the assignment routes for the model
|
|
Assignment* const assignment = solver->MakeAssignment();
|
|
route_constructor_ = std::make_unique<RouteConstructor>(
|
|
assignment, model_, check_assignment_, num_indices_, links_);
|
|
// This call might cause backtracking if the search limit is reached.
|
|
route_constructor_->Construct();
|
|
route_constructor_.reset(nullptr);
|
|
// This call might cause backtracking if the solution is not feasible.
|
|
assignment->Restore();
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
private:
|
|
void ModelSetup() {
|
|
const int depot = model_->GetDepot();
|
|
num_indices_ = model_->Size() + model_->vehicles();
|
|
if (absl::GetFlag(FLAGS_sweep_sectors) > 0 &&
|
|
absl::GetFlag(FLAGS_sweep_sectors) < num_indices_) {
|
|
model_->sweep_arranger()->SetSectors(absl::GetFlag(FLAGS_sweep_sectors));
|
|
}
|
|
std::vector<int64_t> indices;
|
|
model_->sweep_arranger()->ArrangeIndices(&indices);
|
|
for (int i = 0; i < indices.size() - 1; ++i) {
|
|
const int64_t first = indices[i];
|
|
const int64_t second = indices[i + 1];
|
|
if ((model_->IsStart(first) || !model_->IsEnd(first)) &&
|
|
(model_->IsStart(second) || !model_->IsEnd(second))) {
|
|
if (first != depot && second != depot) {
|
|
links_.push_back(
|
|
Link(std::make_pair(first, second), 0, 0, depot, depot));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RoutingModel* const model_;
|
|
std::unique_ptr<RouteConstructor> route_constructor_;
|
|
const bool check_assignment_;
|
|
int64_t num_indices_;
|
|
std::vector<Link> links_;
|
|
};
|
|
} // namespace
|
|
|
|
DecisionBuilder* MakeSweepDecisionBuilder(RoutingModel* model,
|
|
bool check_assignment) {
|
|
return model->solver()->RevAlloc(new SweepBuilder(model, check_assignment));
|
|
}
|
|
|
|
// AllUnperformed
|
|
|
|
namespace {
|
|
// Decision builder to build a solution with all nodes inactive. It does no
|
|
// branching and may fail if some nodes cannot be made inactive.
|
|
|
|
class AllUnperformed : public DecisionBuilder {
|
|
public:
|
|
// Does not take ownership of model.
|
|
explicit AllUnperformed(RoutingModel* const model) : model_(model) {}
|
|
~AllUnperformed() override = default;
|
|
Decision* Next(Solver* const /*solver*/) override {
|
|
// Solver::(Un)FreezeQueue is private, passing through the public API
|
|
// on PropagationBaseObject.
|
|
model_->CostVar()->FreezeQueue();
|
|
for (int i = 0; i < model_->Size(); ++i) {
|
|
if (!model_->IsStart(i)) {
|
|
model_->ActiveVar(i)->SetValue(0);
|
|
}
|
|
}
|
|
model_->CostVar()->UnfreezeQueue();
|
|
return nullptr;
|
|
}
|
|
|
|
private:
|
|
RoutingModel* const model_;
|
|
};
|
|
} // namespace
|
|
|
|
DecisionBuilder* MakeAllUnperformed(RoutingModel* model) {
|
|
return model->solver()->RevAlloc(new AllUnperformed(model));
|
|
}
|
|
|
|
namespace {
|
|
// The description is in routing.h:MakeGuidedSlackFinalizer
|
|
class GuidedSlackFinalizer : public DecisionBuilder {
|
|
public:
|
|
GuidedSlackFinalizer(const RoutingDimension* dimension, RoutingModel* model,
|
|
std::function<int64_t(int64_t)> initializer);
|
|
|
|
// This type is neither copyable nor movable.
|
|
GuidedSlackFinalizer(const GuidedSlackFinalizer&) = delete;
|
|
GuidedSlackFinalizer& operator=(const GuidedSlackFinalizer&) = delete;
|
|
|
|
Decision* Next(Solver* solver) override;
|
|
|
|
private:
|
|
int64_t SelectValue(int64_t index);
|
|
int64_t ChooseVariable();
|
|
|
|
const RoutingDimension* const dimension_;
|
|
RoutingModel* const model_;
|
|
const std::function<int64_t(int64_t)> initializer_;
|
|
RevArray<bool> is_initialized_;
|
|
std::vector<int64_t> initial_values_;
|
|
Rev<int64_t> current_index_;
|
|
Rev<int64_t> current_route_;
|
|
RevArray<int64_t> last_delta_used_;
|
|
};
|
|
|
|
GuidedSlackFinalizer::GuidedSlackFinalizer(
|
|
const RoutingDimension* dimension, RoutingModel* model,
|
|
std::function<int64_t(int64_t)> initializer)
|
|
: dimension_(ABSL_DIE_IF_NULL(dimension)),
|
|
model_(ABSL_DIE_IF_NULL(model)),
|
|
initializer_(std::move(initializer)),
|
|
is_initialized_(dimension->slacks().size(), false),
|
|
initial_values_(dimension->slacks().size(),
|
|
std::numeric_limits<int64_t>::min()),
|
|
current_index_(model_->Start(0)),
|
|
current_route_(0),
|
|
last_delta_used_(dimension->slacks().size(), 0) {}
|
|
|
|
Decision* GuidedSlackFinalizer::Next(Solver* solver) {
|
|
CHECK_EQ(solver, model_->solver());
|
|
const int node_idx = ChooseVariable();
|
|
CHECK(node_idx == -1 ||
|
|
(node_idx >= 0 && node_idx < dimension_->slacks().size()));
|
|
if (node_idx != -1) {
|
|
if (!is_initialized_[node_idx]) {
|
|
initial_values_[node_idx] = initializer_(node_idx);
|
|
is_initialized_.SetValue(solver, node_idx, true);
|
|
}
|
|
const int64_t value = SelectValue(node_idx);
|
|
IntVar* const slack_variable = dimension_->SlackVar(node_idx);
|
|
return solver->MakeAssignVariableValue(slack_variable, value);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
int64_t GuidedSlackFinalizer::SelectValue(int64_t index) {
|
|
const IntVar* const slack_variable = dimension_->SlackVar(index);
|
|
const int64_t center = initial_values_[index];
|
|
const int64_t max_delta =
|
|
std::max(center - slack_variable->Min(), slack_variable->Max() - center) +
|
|
1;
|
|
int64_t delta = last_delta_used_[index];
|
|
|
|
// The sequence of deltas is 0, 1, -1, 2, -2 ...
|
|
// Only the values inside the domain of variable are returned.
|
|
while (std::abs(delta) < max_delta &&
|
|
!slack_variable->Contains(center + delta)) {
|
|
if (delta > 0) {
|
|
delta = -delta;
|
|
} else {
|
|
delta = -delta + 1;
|
|
}
|
|
}
|
|
last_delta_used_.SetValue(model_->solver(), index, delta);
|
|
return center + delta;
|
|
}
|
|
|
|
int64_t GuidedSlackFinalizer::ChooseVariable() {
|
|
int64_t int_current_node = current_index_.Value();
|
|
int64_t int_current_route = current_route_.Value();
|
|
|
|
while (int_current_route < model_->vehicles()) {
|
|
while (!model_->IsEnd(int_current_node) &&
|
|
dimension_->SlackVar(int_current_node)->Bound()) {
|
|
int_current_node = model_->NextVar(int_current_node)->Value();
|
|
}
|
|
if (!model_->IsEnd(int_current_node)) {
|
|
break;
|
|
}
|
|
int_current_route += 1;
|
|
if (int_current_route < model_->vehicles()) {
|
|
int_current_node = model_->Start(int_current_route);
|
|
}
|
|
}
|
|
|
|
CHECK(int_current_route == model_->vehicles() ||
|
|
!dimension_->SlackVar(int_current_node)->Bound());
|
|
current_index_.SetValue(model_->solver(), int_current_node);
|
|
current_route_.SetValue(model_->solver(), int_current_route);
|
|
if (int_current_route < model_->vehicles()) {
|
|
return int_current_node;
|
|
} else {
|
|
return -1;
|
|
}
|
|
}
|
|
} // namespace
|
|
|
|
DecisionBuilder* RoutingModel::MakeGuidedSlackFinalizer(
|
|
const RoutingDimension* dimension,
|
|
std::function<int64_t(int64_t)> initializer) {
|
|
return solver_->RevAlloc(
|
|
new GuidedSlackFinalizer(dimension, this, std::move(initializer)));
|
|
}
|
|
|
|
int64_t RoutingDimension::ShortestTransitionSlack(int64_t node) const {
|
|
CHECK_EQ(base_dimension_, this);
|
|
CHECK(!model_->IsEnd(node));
|
|
// Recall that the model is cumul[i+1] = cumul[i] + transit[i] + slack[i]. Our
|
|
// aim is to find a value for slack[i] such that cumul[i+1] + transit[i+1] is
|
|
// minimized.
|
|
const int64_t next = model_->NextVar(node)->Value();
|
|
if (model_->IsEnd(next)) {
|
|
return SlackVar(node)->Min();
|
|
}
|
|
const int64_t next_next = model_->NextVar(next)->Value();
|
|
const int64_t serving_vehicle = model_->VehicleVar(node)->Value();
|
|
CHECK_EQ(serving_vehicle, model_->VehicleVar(next)->Value());
|
|
const RoutingModel::StateDependentTransit transit_from_next =
|
|
model_->StateDependentTransitCallback(
|
|
state_dependent_class_evaluators_
|
|
[state_dependent_vehicle_to_class_[serving_vehicle]])(next,
|
|
next_next);
|
|
// We have that transit[i+1] is a function of cumul[i+1].
|
|
const int64_t next_cumul_min = CumulVar(next)->Min();
|
|
const int64_t next_cumul_max = CumulVar(next)->Max();
|
|
const int64_t optimal_next_cumul =
|
|
transit_from_next.transit_plus_identity->RangeMinArgument(
|
|
next_cumul_min, next_cumul_max + 1);
|
|
// A few checks to make sure we're on the same page.
|
|
DCHECK_LE(next_cumul_min, optimal_next_cumul);
|
|
DCHECK_LE(optimal_next_cumul, next_cumul_max);
|
|
// optimal_next_cumul = cumul + transit + optimal_slack, so
|
|
// optimal_slack = optimal_next_cumul - cumul - transit.
|
|
// In the current implementation TransitVar(i) = transit[i] + slack[i], so we
|
|
// have to find the transit from the evaluators.
|
|
const int64_t current_cumul = CumulVar(node)->Value();
|
|
const int64_t current_state_independent_transit = model_->TransitCallback(
|
|
class_evaluators_[vehicle_to_class_[serving_vehicle]])(node, next);
|
|
const int64_t current_state_dependent_transit =
|
|
model_
|
|
->StateDependentTransitCallback(
|
|
state_dependent_class_evaluators_
|
|
[state_dependent_vehicle_to_class_[serving_vehicle]])(node,
|
|
next)
|
|
.transit->Query(current_cumul);
|
|
const int64_t optimal_slack = optimal_next_cumul - current_cumul -
|
|
current_state_independent_transit -
|
|
current_state_dependent_transit;
|
|
CHECK_LE(SlackVar(node)->Min(), optimal_slack);
|
|
CHECK_LE(optimal_slack, SlackVar(node)->Max());
|
|
return optimal_slack;
|
|
}
|
|
|
|
namespace {
|
|
class GreedyDescentLSOperator : public LocalSearchOperator {
|
|
public:
|
|
explicit GreedyDescentLSOperator(std::vector<IntVar*> variables);
|
|
|
|
// This type is neither copyable nor movable.
|
|
GreedyDescentLSOperator(const GreedyDescentLSOperator&) = delete;
|
|
GreedyDescentLSOperator& operator=(const GreedyDescentLSOperator&) = delete;
|
|
|
|
bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) override;
|
|
void Start(const Assignment* assignment) override;
|
|
|
|
private:
|
|
int64_t FindMaxDistanceToDomain(const Assignment* assignment);
|
|
|
|
const std::vector<IntVar*> variables_;
|
|
const Assignment* center_;
|
|
int64_t current_step_;
|
|
// The deltas are returned in this order:
|
|
// (current_step_, 0, ... 0), (-current_step_, 0, ... 0),
|
|
// (0, current_step_, ... 0), (0, -current_step_, ... 0),
|
|
// ...
|
|
// (0, ... 0, current_step_), (0, ... 0, -current_step_).
|
|
// current_direction_ keeps track what was the last returned delta.
|
|
int64_t current_direction_;
|
|
};
|
|
|
|
GreedyDescentLSOperator::GreedyDescentLSOperator(std::vector<IntVar*> variables)
|
|
: variables_(std::move(variables)),
|
|
center_(nullptr),
|
|
current_step_(0),
|
|
current_direction_(0) {}
|
|
|
|
bool GreedyDescentLSOperator::MakeNextNeighbor(Assignment* delta,
|
|
Assignment* /*deltadelta*/) {
|
|
static const int64_t sings[] = {1, -1};
|
|
for (; 1 <= current_step_; current_step_ /= 2) {
|
|
for (; current_direction_ < 2 * variables_.size();) {
|
|
const int64_t variable_idx = current_direction_ / 2;
|
|
IntVar* const variable = variables_[variable_idx];
|
|
const int64_t sign_index = current_direction_ % 2;
|
|
const int64_t sign = sings[sign_index];
|
|
const int64_t offset = sign * current_step_;
|
|
const int64_t new_value = center_->Value(variable) + offset;
|
|
++current_direction_;
|
|
if (variable->Contains(new_value)) {
|
|
delta->Add(variable);
|
|
delta->SetValue(variable, new_value);
|
|
return true;
|
|
}
|
|
}
|
|
current_direction_ = 0;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void GreedyDescentLSOperator::Start(const Assignment* assignment) {
|
|
CHECK(assignment != nullptr);
|
|
current_step_ = FindMaxDistanceToDomain(assignment);
|
|
center_ = assignment;
|
|
}
|
|
|
|
int64_t GreedyDescentLSOperator::FindMaxDistanceToDomain(
|
|
const Assignment* assignment) {
|
|
int64_t result = std::numeric_limits<int64_t>::min();
|
|
for (const IntVar* const var : variables_) {
|
|
result = std::max(result, std::abs(var->Max() - assignment->Value(var)));
|
|
result = std::max(result, std::abs(var->Min() - assignment->Value(var)));
|
|
}
|
|
return result;
|
|
}
|
|
} // namespace
|
|
|
|
std::unique_ptr<LocalSearchOperator> RoutingModel::MakeGreedyDescentLSOperator(
|
|
std::vector<IntVar*> variables) {
|
|
return std::unique_ptr<LocalSearchOperator>(
|
|
new GreedyDescentLSOperator(std::move(variables)));
|
|
}
|
|
|
|
DecisionBuilder* RoutingModel::MakeSelfDependentDimensionFinalizer(
|
|
const RoutingDimension* dimension) {
|
|
CHECK(dimension != nullptr);
|
|
CHECK(dimension->base_dimension() == dimension);
|
|
DecisionBuilder* const guided_finalizer =
|
|
MakeGuidedSlackFinalizer(dimension, [dimension](int64_t index) {
|
|
return dimension->ShortestTransitionSlack(index);
|
|
});
|
|
DecisionBuilder* const slacks_finalizer =
|
|
solver_->MakeSolveOnce(guided_finalizer);
|
|
std::vector<IntVar*> start_cumuls(vehicles_, nullptr);
|
|
for (int64_t vehicle_idx = 0; vehicle_idx < vehicles_; ++vehicle_idx) {
|
|
start_cumuls[vehicle_idx] = dimension->CumulVar(Start(vehicle_idx));
|
|
}
|
|
LocalSearchOperator* const hill_climber =
|
|
solver_->RevAlloc(new GreedyDescentLSOperator(start_cumuls));
|
|
LocalSearchPhaseParameters* const parameters =
|
|
solver_->MakeLocalSearchPhaseParameters(CostVar(), hill_climber,
|
|
slacks_finalizer);
|
|
Assignment* const first_solution = solver_->MakeAssignment();
|
|
first_solution->Add(start_cumuls);
|
|
for (IntVar* const cumul : start_cumuls) {
|
|
first_solution->SetValue(cumul, cumul->Min());
|
|
}
|
|
DecisionBuilder* const finalizer =
|
|
solver_->MakeLocalSearchPhase(first_solution, parameters);
|
|
return finalizer;
|
|
}
|
|
} // namespace operations_research
|