diff --git a/ortools/constraint_solver/BUILD.bazel b/ortools/constraint_solver/BUILD.bazel index d5aec3137c7..945561ef5fe 100644 --- a/ortools/constraint_solver/BUILD.bazel +++ b/ortools/constraint_solver/BUILD.bazel @@ -410,6 +410,7 @@ cc_library( "routing.h", "routing_constraints.h", "routing_decision_builders.h", + "routing_filter_committables.h", "routing_filters.h", "routing_ils.h", "routing_insertion_lns.h", diff --git a/ortools/constraint_solver/constraint_solveri.h b/ortools/constraint_solver/constraint_solveri.h index 866e902b9ff..ddb4c8a9c62 100644 --- a/ortools/constraint_solver/constraint_solveri.h +++ b/ortools/constraint_solver/constraint_solveri.h @@ -62,6 +62,7 @@ #include "absl/algorithm/container.h" #include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/strings/str_cat.h" #include "absl/strings/str_format.h" @@ -1347,6 +1348,157 @@ class ChangeValue : public IntVarLocalSearchOperator { int index_; }; +// Iterators on nodes used by Pathoperator to traverse the search space. + +class AlternativeNodeIterator { + public: + explicit AlternativeNodeIterator(bool use_sibling) + : use_sibling_(use_sibling) {} + ~AlternativeNodeIterator() {} + template + void Reset(const PathOperator& path_operator, int base_index_reference) { + index_ = 0; + DCHECK(path_operator.ConsiderAlternatives(base_index_reference)); + const int64_t base_node = path_operator.BaseNode(base_index_reference); + const int alternative_index = + use_sibling_ ? path_operator.GetSiblingAlternativeIndex(base_node) + : path_operator.GetAlternativeIndex(base_node); + alternative_set_ = + alternative_index >= 0 + ? absl::Span( + path_operator.alternative_sets_[alternative_index]) + : absl::Span(); + } + bool Next() { return ++index_ < alternative_set_.size(); } + int GetValue() const { + return (index_ >= alternative_set_.size()) ? -1 : alternative_set_[index_]; + } + + private: + const bool use_sibling_; + int index_ = 0; + absl::Span alternative_set_; +}; + +class NodeNeighborIterator { + public: + NodeNeighborIterator() {} + ~NodeNeighborIterator() {} + template + void Reset(const PathOperator& path_operator, int base_index_reference) { + using Span = absl::Span; + index_ = 0; + const int64_t base_node = path_operator.BaseNode(base_index_reference); + const int64_t start_node = path_operator.StartNode(base_index_reference); + const auto& get_incoming_neighbors = + path_operator.iteration_parameters_.get_incoming_neighbors; + incoming_neighbors_ = + path_operator.IsPathStart(base_node) || !get_incoming_neighbors + ? Span() + : Span(get_incoming_neighbors(base_node, start_node)); + const auto& get_outgoing_neighbors = + path_operator.iteration_parameters_.get_outgoing_neighbors; + outgoing_neighbors_ = + path_operator.IsPathEnd(base_node) || !get_outgoing_neighbors + ? Span() + : Span(get_outgoing_neighbors(base_node, start_node)); + } + bool Next() { + return ++index_ < incoming_neighbors_.size() + outgoing_neighbors_.size(); + } + int GetValue() const { + if (index_ < incoming_neighbors_.size()) { + return incoming_neighbors_[index_]; + } + const int index = index_ - incoming_neighbors_.size(); + return (index >= outgoing_neighbors_.size()) ? -1 + : outgoing_neighbors_[index]; + } + bool IsIncomingNeighbor() const { + return index_ < incoming_neighbors_.size(); + } + bool IsOutgoingNeighbor() const { + return index_ >= incoming_neighbors_.size(); + } + + private: + int index_ = 0; + absl::Span incoming_neighbors_; + absl::Span outgoing_neighbors_; +}; + +template +class BaseNodeIterators { + public: + BaseNodeIterators(const PathOperator* path_operator, int base_index_reference) + : path_operator_(*path_operator), + base_index_reference_(base_index_reference) {} + AlternativeNodeIterator* GetSiblingAlternativeIterator() const { + DCHECK(!alternatives_.empty()); + DCHECK(!finished_); + return alternatives_[0].get(); + } + AlternativeNodeIterator* GetAlternativeIterator() const { + DCHECK(!alternatives_.empty()); + DCHECK(!finished_); + return alternatives_[1].get(); + } + NodeNeighborIterator* GetNeighborIterator() const { + DCHECK(neighbors_); + DCHECK(!finished_); + return neighbors_.get(); + } + void Initialize() { + if (path_operator_.ConsiderAlternatives(base_index_reference_)) { + alternatives_.push_back(std::make_unique( + /*is_sibling=*/true)); + alternatives_.push_back(std::make_unique( + /*is_sibling=*/false)); + } + if (path_operator_.HasNeighbors()) { + neighbors_ = std::make_unique(); + } + } + void Reset(bool update_end_nodes = false) { + finished_ = false; + for (auto& alternative_iterator : alternatives_) { + alternative_iterator->Reset(path_operator_, base_index_reference_); + } + if (neighbors_) { + neighbors_->Reset(path_operator_, base_index_reference_); + if (update_end_nodes) neighbor_end_node_ = neighbors_->GetValue(); + } + } + bool Increment() { + DCHECK(!finished_); + for (auto& alternative_iterator : alternatives_) { + if (alternative_iterator->Next()) return true; + alternative_iterator->Reset(path_operator_, base_index_reference_); + } + if (neighbors_) { + if (neighbors_->Next()) return true; + neighbors_->Reset(path_operator_, base_index_reference_); + } + finished_ = true; + return false; + } + bool HasReachedEnd() const { + // TODO(user): Extend to other iterators. + if (!neighbors_) return true; + return neighbor_end_node_ == neighbors_->GetValue(); + } + + private: + const PathOperator& path_operator_; + int base_index_reference_ = -1; +#ifndef SWIG + std::vector> alternatives_; +#endif // SWIG + std::unique_ptr neighbors_; + int neighbor_end_node_ = -1; + bool finished_ = false; +}; + /// Base class of the local search operators dedicated to path modifications /// (a path is a set of nodes linked together by arcs). /// This family of neighborhoods supposes they are handling next variables @@ -1360,6 +1512,7 @@ class ChangeValue : public IntVarLocalSearchOperator { /// nodes which can be used to define a neighbor (through the BaseNode method) /// Subclasses only need to override MakeNeighbor to create neighbors using /// the services above (no direct manipulation of assignments). +template class PathOperator : public IntVarLocalSearchOperator { public: /// Set of parameters used to configure how the neighnorhood is traversed. @@ -1394,7 +1547,43 @@ class PathOperator : public IntVarLocalSearchOperator { /// Builds an instance of PathOperator from next and path variables. PathOperator(const std::vector& next_vars, const std::vector& path_vars, - IterationParameters iteration_parameters); + IterationParameters iteration_parameters) + : IntVarLocalSearchOperator(next_vars, true), + number_of_nexts_(next_vars.size()), + base_nodes_( + std::make_unique(iteration_parameters.number_of_base_nodes)), + end_nodes_( + std::make_unique(iteration_parameters.number_of_base_nodes)), + base_paths_( + std::make_unique(iteration_parameters.number_of_base_nodes)), + node_path_starts_(number_of_nexts_, -1), + node_path_ends_(number_of_nexts_, -1), + just_started_(false), + first_start_(true), + next_base_to_increment_(iteration_parameters.number_of_base_nodes), + iteration_parameters_(std::move(iteration_parameters)), + optimal_paths_enabled_(false), + active_paths_(number_of_nexts_), + alternative_index_(next_vars.size(), -1) { + DCHECK_GT(iteration_parameters_.number_of_base_nodes, 0); + for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { + base_node_iterators_.push_back(BaseNodeIterators(this, i)); + } + if constexpr (!ignore_path_vars) { + AddVars(path_vars); + } + path_basis_.push_back(0); + for (int i = 1; i < iteration_parameters_.number_of_base_nodes; ++i) { + if (!OnSamePathAsPreviousBase(i)) path_basis_.push_back(i); + } + if ((path_basis_.size() > 2) || + (!next_vars.empty() && !next_vars.back() + ->solver() + ->parameters() + .skip_locally_optimal_paths())) { + iteration_parameters_.skip_locally_optimal_paths = false; + } + } PathOperator( const std::vector& next_vars, const std::vector& path_vars, int number_of_base_nodes, @@ -1409,11 +1598,25 @@ class PathOperator : public IntVarLocalSearchOperator { std::move(get_outgoing_neighbors)}) {} ~PathOperator() override {} virtual bool MakeNeighbor() = 0; - void EnterSearch() override; - void Reset() override; + void EnterSearch() override { + first_start_ = true; + ResetIncrementalism(); + } + void Reset() override { + active_paths_.Clear(); + ResetIncrementalism(); + } // TODO(user): Make the following methods protected. - bool SkipUnchanged(int index) const override; + bool SkipUnchanged(int index) const override { + if constexpr (ignore_path_vars) return true; + if (index < number_of_nexts_) { + const int path_index = index + number_of_nexts_; + return Value(path_index) == OldValue(path_index); + } + const int next_index = index - number_of_nexts_; + return Value(next_index) == OldValue(next_index); + } /// Returns the node after node in the current delta. int64_t Next(int64_t node) const { @@ -1431,7 +1634,8 @@ class PathOperator : public IntVarLocalSearchOperator { /// Returns the index of the path to which node belongs in the current delta. /// Only returns a valid value if path variables are taken into account. int64_t Path(int64_t node) const { - return ignore_path_vars_ ? 0LL : Value(node + number_of_nexts_); + if constexpr (ignore_path_vars) return 0LL; + return Value(node + number_of_nexts_); } /// Number of next variables. @@ -1439,7 +1643,18 @@ class PathOperator : public IntVarLocalSearchOperator { protected: /// This method should not be overridden. Override MakeNeighbor() instead. - bool MakeOneNeighbor() override; + bool MakeOneNeighbor() override { + while (IncrementPosition()) { + // Need to revert changes here since MakeNeighbor might have returned + // false and have done changes in the previous iteration. + RevertChanges(true); + if (MakeNeighbor()) { + return true; + } + } + return false; + } + /// Called by OnStart() after initializing node information. Should be /// overridden instead of OnStart() to avoid calling PathOperator::OnStart /// explicitly. @@ -1452,29 +1667,15 @@ class PathOperator : public IntVarLocalSearchOperator { /// Returns the ith base node of the operator. int64_t BaseNode(int i) const { return base_nodes_[i]; } - /// Returns the alternative for the ith base node. - int BaseAlternative(int i) const { return base_alternatives_[i]; } /// Returns the alternative node for the ith base node. int64_t BaseAlternativeNode(int i) const { - if (!ConsiderAlternatives(i)) return BaseNode(i); - const int alternative_index = alternative_index_[BaseNode(i)]; - return alternative_index >= 0 - ? alternative_sets_[alternative_index][base_alternatives_[i]] - : BaseNode(i); - } - /// Returns the alternative for the sibling of the ith base node. - int BaseSiblingAlternative(int i) const { - return base_sibling_alternatives_[i]; + return GetNodeWithDefault(base_node_iterators_[i].GetAlternativeIterator(), + BaseNode(i)); } /// Returns the alternative node for the sibling of the ith base node. int64_t BaseSiblingAlternativeNode(int i) const { - if (!ConsiderAlternatives(i)) return BaseNode(i); - const int sibling_alternative_index = - GetSiblingAlternativeIndex(BaseNode(i)); - return sibling_alternative_index >= 0 - ? alternative_sets_[sibling_alternative_index] - [base_sibling_alternatives_[i]] - : BaseNode(i); + return GetNodeWithDefault( + base_node_iterators_[i].GetSiblingAlternativeIterator(), BaseNode(i)); } /// Returns the start node of the ith base node. int64_t StartNode(int i) const { return path_starts_[base_paths_[i]]; } @@ -1538,7 +1739,8 @@ class PathOperator : public IntVarLocalSearchOperator { } int64_t OldPath(int64_t node) const { - return ignore_path_vars_ ? 0LL : OldValue(node + number_of_nexts_); + if constexpr (ignore_path_vars) return 0LL; + return OldValue(node + number_of_nexts_); } int CurrentNodePathStart(int64_t node) const { @@ -1549,30 +1751,120 @@ class PathOperator : public IntVarLocalSearchOperator { /// Moves the chain starting after the node before_chain and ending at the /// node chain_end after the node destination - bool MoveChain(int64_t before_chain, int64_t chain_end, int64_t destination); + bool MoveChain(int64_t before_chain, int64_t chain_end, int64_t destination) { + if (destination == before_chain || destination == chain_end) return false; + DCHECK(CheckChainValidity(before_chain, chain_end, destination) && + !IsPathEnd(chain_end) && !IsPathEnd(destination)); + const int64_t destination_path = Path(destination); + const int64_t after_chain = Next(chain_end); + SetNext(chain_end, Next(destination), destination_path); + if constexpr (!ignore_path_vars) { + int current = destination; + int next = Next(before_chain); + while (current != chain_end) { + SetNext(current, next, destination_path); + current = next; + next = Next(next); + } + } else { + SetNext(destination, Next(before_chain), destination_path); + } + SetNext(before_chain, after_chain, Path(before_chain)); + return true; + } /// Reverses the chain starting after before_chain and ending before /// after_chain bool ReverseChain(int64_t before_chain, int64_t after_chain, - int64_t* chain_last); + int64_t* chain_last) { + if (CheckChainValidity(before_chain, after_chain, -1)) { + int64_t path = Path(before_chain); + int64_t current = Next(before_chain); + if (current == after_chain) { + return false; + } + int64_t current_next = Next(current); + SetNext(current, after_chain, path); + while (current_next != after_chain) { + const int64_t next = Next(current_next); + SetNext(current_next, current, path); + current = current_next; + current_next = next; + } + SetNext(before_chain, current, path); + *chain_last = current; + return true; + } + return false; + } + + /// Swaps the nodes node1 and node2. + bool SwapNodes(int64_t node1, int64_t node2) { + if (IsPathEnd(node1) || IsPathEnd(node2) || IsPathStart(node1) || + IsPathStart(node2)) { + return false; + } + if (node1 == node2) return false; + const int64_t prev_node1 = Prev(node1); + const bool ok = MoveChain(prev_node1, node1, Prev(node2)); + return MoveChain(Prev(node2), node2, prev_node1) || ok; + } /// Insert the inactive node after destination. - bool MakeActive(int64_t node, int64_t destination); + bool MakeActive(int64_t node, int64_t destination) { + if (IsPathEnd(destination)) return false; + const int64_t destination_path = Path(destination); + SetNext(node, Next(destination), destination_path); + SetNext(destination, node, destination_path); + return true; + } /// Makes the nodes on the chain starting after before_chain and ending at /// chain_end inactive. - bool MakeChainInactive(int64_t before_chain, int64_t chain_end); + bool MakeChainInactive(int64_t before_chain, int64_t chain_end) { + const int64_t kNoPath = -1; + if (CheckChainValidity(before_chain, chain_end, -1) && + !IsPathEnd(chain_end)) { + const int64_t after_chain = Next(chain_end); + int64_t current = Next(before_chain); + while (current != after_chain) { + const int64_t next = Next(current); + SetNext(current, current, kNoPath); + current = next; + } + SetNext(before_chain, after_chain, Path(before_chain)); + return true; + } + return false; + } + /// Replaces active by inactive in the current path, making active inactive. - bool SwapActiveAndInactive(int64_t active, int64_t inactive); + bool SwapActiveAndInactive(int64_t active, int64_t inactive) { + if (active == inactive) return false; + const int64_t prev = Prev(active); + return MakeChainInactive(prev, active) && MakeActive(inactive, prev); + } /// Swaps both chains, making nodes on active_chain inactive and inserting /// active_chain at the position where inactive_chain was. bool SwapActiveAndInactiveChains(absl::Span active_chain, - absl::Span inactive_chain); + absl::Span inactive_chain) { + if (active_chain.empty()) return false; + if (active_chain == inactive_chain) return false; + const int before_active_chain = Prev(active_chain.front()); + if (!MakeChainInactive(before_active_chain, active_chain.back())) { + return false; + } + for (auto it = inactive_chain.crbegin(); it != inactive_chain.crend(); + ++it) { + if (!MakeActive(*it, before_active_chain)) return false; + } + return true; + } /// Sets 'to' to be the node after 'from' on the given path. void SetNext(int64_t from, int64_t to, int64_t path) { DCHECK_LT(from, number_of_nexts_); SetValue(from, to); - if (!ignore_path_vars_) { + if constexpr (!ignore_path_vars) { DCHECK_LT(from + number_of_nexts_, Size()); SetValue(from + number_of_nexts_, path); } @@ -1636,23 +1928,37 @@ class PathOperator : public IntVarLocalSearchOperator { } /// Returns the index of the alternative set of the sibling of node. int GetSiblingAlternativeIndex(int node) const { - if (node >= alternative_index_.size()) return -1; - const int alternative = alternative_index_[node]; + const int alternative = GetAlternativeIndex(node); return alternative >= 0 ? sibling_alternative_[alternative] : -1; } + /// Returns the index of the alternative set of the node. + int GetAlternativeIndex(int node) const { + return (node >= alternative_index_.size()) ? -1 : alternative_index_[node]; + } /// Returns the active node in the alternative set of the sibling of the given /// node. int64_t GetActiveAlternativeSibling(int node) const { - if (node >= alternative_index_.size()) return -1; - const int alternative = alternative_index_[node]; - const int sibling_alternative = - alternative >= 0 ? sibling_alternative_[alternative] : -1; - return GetActiveInAlternativeSet(sibling_alternative); + return GetActiveInAlternativeSet(GetSiblingAlternativeIndex(node)); } /// Returns true if the chain is a valid path without cycles from before_chain /// to chain_end and does not contain exclude. + /// In particular, rejects a chain if chain_end is not strictly after + /// before_chain on the path. + /// Cycles are detected through chain length overflow. bool CheckChainValidity(int64_t before_chain, int64_t chain_end, - int64_t exclude) const; + int64_t exclude) const { + if (before_chain == chain_end || before_chain == exclude) return false; + int64_t current = before_chain; + int chain_size = 0; + while (current != chain_end) { + if (chain_size > number_of_nexts_) return false; + if (IsPathEnd(current)) return false; + current = Next(current); + ++chain_size; + if (current == exclude) return false; + } + return true; + } bool HasNeighbors() const { return iteration_parameters_.get_incoming_neighbors != nullptr || @@ -1667,61 +1973,360 @@ class PathOperator : public IntVarLocalSearchOperator { bool outgoing; }; Neighbor GetNeighborForBaseNode(int64_t base_index) const { - DCHECK(HasNeighbors()); - const int64_t node = BaseNode(base_index); - const int64_t start = StartNode(base_index); + auto* iterator = base_node_iterators_[base_index].GetNeighborIterator(); + return {.neighbor = iterator->GetValue(), + .outgoing = iterator->IsOutgoingNeighbor()}; + } - const int num_calls = calls_per_base_node_[base_index]; - const auto& get_incoming_neighbors = - iteration_parameters_.get_incoming_neighbors; - const std::vector& incoming_neighbors = - IsPathStart(node) || get_incoming_neighbors == nullptr - ? std::vector() - : get_incoming_neighbors(node, start); + const int number_of_nexts_; - if (num_calls < incoming_neighbors.size()) { - // Incoming neighbor. - DCHECK(!IsPathStart(node)); - return {.neighbor = incoming_neighbors[num_calls], .outgoing = false}; + private: + template + static int GetNodeWithDefault(const NodeIterator* node_iterator, + int default_value) { + const int node = node_iterator->GetValue(); + return node >= 0 ? node : default_value; } - // Outgoing neighbor. - if (IsPathEnd(node)) return {.neighbor = -1, .outgoing = true}; - const auto& get_outgoing_neighbors = - iteration_parameters_.get_outgoing_neighbors; - if (get_outgoing_neighbors == nullptr) { - DCHECK(IsPathStart(node)); - return {.neighbor = -1, .outgoing = true}; + void OnStart() override { + optimal_paths_enabled_ = false; + if (!iterators_initialized_) { + iterators_initialized_ = true; + for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { + base_node_iterators_[i].Initialize(); + } } - const int index = num_calls - incoming_neighbors.size(); - DCHECK_LT(index, get_outgoing_neighbors(node, start).size()); - return {.neighbor = get_outgoing_neighbors(node, start)[index], - .outgoing = true}; + InitializeBaseNodes(); + InitializeAlternatives(); + OnNodeInitialization(); } - const int number_of_nexts_; - const bool ignore_path_vars_; - - private: - void OnStart() override; /// Returns true if two nodes are on the same path in the current assignment. - bool OnSamePath(int64_t node1, int64_t node2) const; + bool OnSamePath(int64_t node1, int64_t node2) const { + if (IsInactive(node1) != IsInactive(node2)) { + return false; + } + for (int node = node1; !IsPathEnd(node); node = OldNext(node)) { + if (node == node2) { + return true; + } + } + for (int node = node2; !IsPathEnd(node); node = OldNext(node)) { + if (node == node1) { + return true; + } + } + return false; + } bool CheckEnds() const { - const int base_node_size = base_nodes_.size(); - for (int i = base_node_size - 1; i >= 0; --i) { - if (base_nodes_[i] != end_nodes_[i] || calls_per_base_node_[0] > 0) { + for (int i = iteration_parameters_.number_of_base_nodes - 1; i >= 0; --i) { + if (base_nodes_[i] != end_nodes_[i] || + !base_node_iterators_[i].HasReachedEnd()) { return true; } } return false; } - bool IncrementPosition(); - void InitializePathStarts(); - void InitializeInactives(); - void InitializeBaseNodes(); - void InitializeAlternatives(); - void Synchronize(); + bool IncrementPosition() { + const int base_node_size = iteration_parameters_.number_of_base_nodes; + + if (just_started_) { + just_started_ = false; + return true; + } + const int number_of_paths = path_starts_.size(); + // Finding next base node positions. + // Increment the position of inner base nodes first (higher index nodes); + // if a base node is at the end of a path, reposition it at the start + // of the path and increment the position of the preceding base node (this + // action is called a restart). + int last_restarted = base_node_size; + for (int i = base_node_size - 1; i >= 0; --i) { + if (base_nodes_[i] < number_of_nexts_ && i <= next_base_to_increment_) { + if (base_node_iterators_[i].Increment()) break; + base_nodes_[i] = OldNext(base_nodes_[i]); + base_node_iterators_[i].Reset(); + if (iteration_parameters_.accept_path_end_base || + !IsPathEnd(base_nodes_[i])) { + break; + } + } + base_nodes_[i] = StartNode(i); + base_node_iterators_[i].Reset(); + last_restarted = i; + } + next_base_to_increment_ = base_node_size; + // At the end of the loop, base nodes with indexes in + // [last_restarted, base_node_size[ have been restarted. + // Restarted base nodes are then repositioned by the virtual + // GetBaseNodeRestartPosition to reflect position constraints between + // base nodes (by default GetBaseNodeRestartPosition leaves the nodes + // at the start of the path). + // Base nodes are repositioned in ascending order to ensure that all + // base nodes "below" the node being repositioned have their final + // position. + for (int i = last_restarted; i < base_node_size; ++i) { + base_nodes_[i] = GetBaseNodeRestartPosition(i); + base_node_iterators_[i].Reset(); + } + if (last_restarted > 0) { + return CheckEnds(); + } + // If all base nodes have been restarted, base nodes are moved to new paths. + // First we mark the current paths as locally optimal if they have been + // completely explored. + if (optimal_paths_enabled_ && + iteration_parameters_.skip_locally_optimal_paths) { + if (path_basis_.size() > 1) { + for (int i = 1; i < path_basis_.size(); ++i) { + active_paths_.DeactivatePathPair(StartNode(path_basis_[i - 1]), + StartNode(path_basis_[i])); + } + } else { + active_paths_.DeactivatePathPair(StartNode(path_basis_[0]), + StartNode(path_basis_[0])); + } + } + std::vector current_starts(base_node_size); + for (int i = 0; i < base_node_size; ++i) { + current_starts[i] = StartNode(i); + } + // Exploration of next paths can lead to locally optimal paths since we are + // exploring them from scratch. + optimal_paths_enabled_ = true; + while (true) { + for (int i = base_node_size - 1; i >= 0; --i) { + const int next_path_index = base_paths_[i] + 1; + if (next_path_index < number_of_paths) { + base_paths_[i] = next_path_index; + base_nodes_[i] = path_starts_[next_path_index]; + base_node_iterators_[i].Reset(); + if (i == 0 || !OnSamePathAsPreviousBase(i)) { + break; + } + } else { + base_paths_[i] = 0; + base_nodes_[i] = path_starts_[0]; + base_node_iterators_[i].Reset(); + } + } + if (!iteration_parameters_.skip_locally_optimal_paths) return CheckEnds(); + // If the new paths have already been completely explored, we can + // skip them from now on. + if (path_basis_.size() > 1) { + for (int j = 1; j < path_basis_.size(); ++j) { + if (active_paths_.IsPathPairActive(StartNode(path_basis_[j - 1]), + StartNode(path_basis_[j]))) { + return CheckEnds(); + } + } + } else { + if (active_paths_.IsPathPairActive(StartNode(path_basis_[0]), + StartNode(path_basis_[0]))) { + return CheckEnds(); + } + } + // If we are back to paths we just iterated on or have reached the end + // of the neighborhood search space, we can stop. + if (!CheckEnds()) return false; + bool stop = true; + for (int i = 0; i < base_node_size; ++i) { + if (StartNode(i) != current_starts[i]) { + stop = false; + break; + } + } + if (stop) return false; + } + return CheckEnds(); + } + + void InitializePathStarts() { + // Detect nodes which do not have any possible predecessor in a path; these + // nodes are path starts. + int max_next = -1; + std::vector has_prevs(number_of_nexts_, false); + for (int i = 0; i < number_of_nexts_; ++i) { + const int next = OldNext(i); + if (next < number_of_nexts_) { + has_prevs[next] = true; + } + max_next = std::max(max_next, next); + } + // Update locally optimal paths. + if (iteration_parameters_.skip_locally_optimal_paths) { + active_paths_.Initialize( + /*is_start=*/[&has_prevs](int node) { return !has_prevs[node]; }); + for (int i = 0; i < number_of_nexts_; ++i) { + if (!has_prevs[i]) { + int current = i; + while (!IsPathEnd(current)) { + if ((OldNext(current) != PrevNext(current))) { + active_paths_.ActivatePath(i); + break; + } + current = OldNext(current); + } + } + } + } + // Create a list of path starts, dropping equivalent path starts of + // currently empty paths. + std::vector empty_found(number_of_nexts_, false); + std::vector new_path_starts; + for (int i = 0; i < number_of_nexts_; ++i) { + if (!has_prevs[i]) { + if (IsPathEnd(OldNext(i))) { + if (iteration_parameters_.start_empty_path_class != nullptr) { + if (empty_found[iteration_parameters_.start_empty_path_class(i)]) + continue; + empty_found[iteration_parameters_.start_empty_path_class(i)] = true; + } + } + new_path_starts.push_back(i); + } + } + if (!first_start_) { + // Synchronizing base_paths_ with base node positions. When the last move + // was performed a base node could have been moved to a new route in which + // case base_paths_ needs to be updated. This needs to be done on the path + // starts before we re-adjust base nodes for new path starts. + std::vector node_paths(max_next + 1, -1); + for (int i = 0; i < path_starts_.size(); ++i) { + int node = path_starts_[i]; + while (!IsPathEnd(node)) { + node_paths[node] = i; + node = OldNext(node); + } + node_paths[node] = i; + } + for (int j = 0; j < iteration_parameters_.number_of_base_nodes; ++j) { + if (IsInactive(base_nodes_[j]) || node_paths[base_nodes_[j]] == -1) { + // Base node was made inactive or was moved to a new path, reposition + // the base node to its restart position. + base_nodes_[j] = GetBaseNodeRestartPosition(j); + base_paths_[j] = node_paths[base_nodes_[j]]; + } else { + base_paths_[j] = node_paths[base_nodes_[j]]; + } + // Always restart from first alternative. + base_node_iterators_[j].Reset(); + } + // Re-adjust current base_nodes and base_paths to take into account new + // path starts (there could be fewer if a new path was made empty, or more + // if nodes were added to a formerly empty path). + int new_index = 0; + absl::flat_hash_set found_bases; + for (int i = 0; i < path_starts_.size(); ++i) { + int index = new_index; + // Note: old and new path starts are sorted by construction. + while (index < new_path_starts.size() && + new_path_starts[index] < path_starts_[i]) { + ++index; + } + const bool found = (index < new_path_starts.size() && + new_path_starts[index] == path_starts_[i]); + if (found) { + new_index = index; + } + for (int j = 0; j < iteration_parameters_.number_of_base_nodes; ++j) { + if (base_paths_[j] == i && !found_bases.contains(j)) { + found_bases.insert(j); + base_paths_[j] = new_index; + // If the current position of the base node is a removed empty path, + // readjusting it to the last visited path start. + if (!found) { + base_nodes_[j] = new_path_starts[new_index]; + } + } + } + } + } + path_starts_.swap(new_path_starts); + // For every base path, store the end corresponding to the path start. + // TODO(user): make this faster, maybe by pairing starts with ends. + path_ends_.clear(); + path_ends_.reserve(path_starts_.size()); + int64_t max_node_index = number_of_nexts_ - 1; + for (const int start_node : path_starts_) { + int64_t node = start_node; + while (!IsPathEnd(node)) node = OldNext(node); + path_ends_.push_back(node); + max_node_index = std::max(max_node_index, node); + } + node_path_starts_.assign(max_node_index + 1, -1); + node_path_ends_.assign(max_node_index + 1, -1); + for (int i = 0; i < path_starts_.size(); ++i) { + const int64_t start_node = path_starts_[i]; + const int64_t end_node = path_ends_[i]; + int64_t node = start_node; + while (!IsPathEnd(node)) { + node_path_starts_[node] = start_node; + node_path_ends_[node] = end_node; + node = OldNext(node); + } + node_path_starts_[node] = start_node; + node_path_ends_[node] = end_node; + } + } + void InitializeInactives() { + inactives_.clear(); + for (int i = 0; i < number_of_nexts_; ++i) { + inactives_.push_back(OldNext(i) == i); + } + } + void InitializeBaseNodes() { + // Inactive nodes must be detected before determining new path starts. + InitializeInactives(); + InitializePathStarts(); + if (first_start_ || InitPosition()) { + // Only do this once since the following starts will continue from the + // preceding position + for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { + base_paths_[i] = 0; + base_nodes_[i] = path_starts_[0]; + } + first_start_ = false; + } + for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { + // If base node has been made inactive, restart from path start. + int64_t base_node = base_nodes_[i]; + if (RestartAtPathStartOnSynchronize() || IsInactive(base_node)) { + base_node = path_starts_[base_paths_[i]]; + base_nodes_[i] = base_node; + } + end_nodes_[i] = base_node; + } + // Repair end_nodes_ in case some must be on the same path and are not + // anymore (due to other operators moving these nodes). + for (int i = 1; i < iteration_parameters_.number_of_base_nodes; ++i) { + if (OnSamePathAsPreviousBase(i) && + !OnSamePath(base_nodes_[i - 1], base_nodes_[i])) { + const int64_t base_node = base_nodes_[i - 1]; + base_nodes_[i] = base_node; + end_nodes_[i] = base_node; + base_paths_[i] = base_paths_[i - 1]; + } + } + for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { + base_node_iterators_[i].Reset(/*update_end_nodes=*/true); + } + just_started_ = true; + } + void InitializeAlternatives() { + active_in_alternative_set_.resize(alternative_sets_.size(), -1); + for (int i = 0; i < alternative_sets_.size(); ++i) { + const int64_t current_active = active_in_alternative_set_[i]; + if (current_active >= 0 && !IsInactive(current_active)) continue; + for (int64_t index : alternative_sets_[i]) { + if (!IsInactive(index)) { + active_in_alternative_set_[i] = index; + break; + } + } + } + } class ActivePaths { public: @@ -1767,14 +2372,15 @@ class PathOperator : public IntVarLocalSearchOperator { std::vector is_path_pair_active_; }; - std::vector base_nodes_; - std::vector base_alternatives_; - std::vector base_sibling_alternatives_; - std::vector end_nodes_; - std::vector base_paths_; + std::unique_ptr base_nodes_; + std::unique_ptr end_nodes_; + std::unique_ptr base_paths_; std::vector node_path_starts_; std::vector node_path_ends_; - std::vector calls_per_base_node_; + bool iterators_initialized_ = false; +#ifndef SWIG + std::vector> base_node_iterators_; +#endif // SWIG std::vector path_starts_; std::vector path_ends_; std::vector inactives_; @@ -1792,44 +2398,309 @@ class PathOperator : public IntVarLocalSearchOperator { std::vector alternative_index_; std::vector active_in_alternative_set_; std::vector sibling_alternative_; + + friend class BaseNodeIterators; + friend class AlternativeNodeIterator; + friend class NodeNeighborIterator; }; -/// Operator Factories. -template -LocalSearchOperator* MakeLocalSearchOperator( +#ifndef SWIG +/// ----- 2Opt ----- + +/// Reverses a sub-chain of a path. It is called 2Opt because it breaks +/// 2 arcs on the path; resulting paths are called 2-optimal. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5 +/// (where (1, 5) are first and last nodes of the path and can therefore not be +/// moved): +/// 1 -> 3 -> 2 -> 4 -> 5 +/// 1 -> 4 -> 3 -> 2 -> 5 +/// 1 -> 2 -> 4 -> 3 -> 5 + +LocalSearchOperator* MakeTwoOpt( Solver* solver, const std::vector& vars, const std::vector& secondary_vars, - std::function start_empty_path_class); + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors = + nullptr, + std::function&(int, int)> get_outgoing_neighbors = + nullptr); + +/// ----- Relocate ----- + +/// Moves a sub-chain of a path to another position; the specified chain length +/// is the fixed length of the chains being moved. When this length is 1 the +/// operator simply moves a node to another position. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5, for a chain length +/// of 2 (where (1, 5) are first and last nodes of the path and can +/// therefore not be moved): +/// 1 -> 4 -> 2 -> 3 -> 5 +/// 1 -> 3 -> 4 -> 2 -> 5 +/// +/// Using Relocate with chain lengths of 1, 2 and 3 together is equivalent to +/// the OrOpt operator on a path. The OrOpt operator is a limited version of +/// 3Opt (breaks 3 arcs on a path). -template -LocalSearchOperator* MakeLocalSearchOperatorWithArg( +LocalSearchOperator* MakeRelocate( Solver* solver, const std::vector& vars, const std::vector& secondary_vars, - std::function start_empty_path_class, ArgType arg); + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors = + nullptr, + std::function&(int, int)> get_outgoing_neighbors = + nullptr, + int64_t chain_length = 1LL, bool single_path = false, + const std::string& name = "Relocate"); + +/// ----- Exchange ----- + +/// Exchanges the positions of two nodes. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5 +/// (where (1, 5) are first and last nodes of the path and can therefore not +/// be moved): +/// 1 -> 3 -> 2 -> 4 -> 5 +/// 1 -> 4 -> 3 -> 2 -> 5 +/// 1 -> 2 -> 4 -> 3 -> 5 + +LocalSearchOperator* MakeExchange( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors = + nullptr, + std::function&(int, int)> get_outgoing_neighbors = + nullptr); + +/// ----- Cross ----- + +/// Cross echanges the starting chains of 2 paths, including exchanging the +/// whole paths. +/// First and last nodes are not moved. +/// Possible neighbors for the paths 1 -> 2 -> 3 -> 4 -> 5 and 6 -> 7 -> 8 +/// (where (1, 5) and (6, 8) are first and last nodes of the paths and can +/// therefore not be moved): +/// 1 -> 7 -> 3 -> 4 -> 5 6 -> 2 -> 8 +/// 1 -> 7 -> 4 -> 5 6 -> 2 -> 3 -> 8 +/// 1 -> 7 -> 5 6 -> 2 -> 3 -> 4 -> 8 + +LocalSearchOperator* MakeCross( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors = + nullptr, + std::function&(int, int)> get_outgoing_neighbors = + nullptr); -template -LocalSearchOperator* MakeLocalSearchOperatorWithNeighbors( +/// ----- MakeActive ----- + +/// MakeActive inserts an inactive node into a path. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 +/// and 4 are first and last nodes of the path) are: +/// 1 -> 5 -> 2 -> 3 -> 4 +/// 1 -> 2 -> 5 -> 3 -> 4 +/// 1 -> 2 -> 3 -> 5 -> 4 + +LocalSearchOperator* MakeActive( Solver* solver, const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, - std::function&(int, int)> get_incoming_neighbors, - std::function&(int, int)> get_outgoing_neighbors); - -/// Classes to which this template function can be applied to as of 04/2014. -/// Usage: LocalSearchOperator* op = MakeLocalSearchOperator(...); -/// class TwoOpt; -/// class Relocate; -/// class Exchange; -/// class Cross; -/// class MakeActiveOperator; -/// class MakeInactiveOperator; -/// class MakeChainInactiveOperator; -/// class SwapActiveOperator; -/// class SwapActiveChainOperator; -/// class ExtendedSwapActiveOperator; -/// class MakeActiveAndRelocate; -/// class RelocateAndMakeActiveOperator; -/// class RelocateAndMakeInactiveOperator; + std::function&(int, int)> get_incoming_neighbors = + nullptr, + std::function&(int, int)> get_outgoing_neighbors = + nullptr); + +/// ---- RelocateAndMakeActive ----- + +/// RelocateAndMakeActive relocates a node and replaces it by an inactive node. +/// The idea is to make room for inactive nodes. +/// Possible neighbor for paths 0 -> 4, 1 -> 2 -> 5 and 3 inactive is: +/// 0 -> 2 -> 4, 1 -> 3 -> 5. +/// TODO(user): Naming is close to MakeActiveAndRelocate but this one is +/// correct; rename MakeActiveAndRelocate if it is actually used. + +LocalSearchOperator* RelocateAndMakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +// ----- ExchangeAndMakeActive ----- + +// ExchangeAndMakeActive exchanges two nodes and inserts an inactive node. +// Possible neighbors for paths 0 -> 2 -> 4, 1 -> 3 -> 6 and 5 inactive are: +// 0 -> 3 -> 4, 1 -> 5 -> 2 -> 6 +// 0 -> 3 -> 5 -> 4, 1 -> 2 -> 6 +// 0 -> 5 -> 3 -> 4, 1 -> 2 -> 6 +// 0 -> 3 -> 4, 1 -> 2 -> 5 -> 6 +// +// Warning this operator creates a very large neighborhood, with O(m*n^3) +// neighbors (n: number of active nodes, m: number of non active nodes). +// It should be used with only a small number of non active nodes. +// TODO(user): Add support for neighbors which would make this operator more +// usable. + +LocalSearchOperator* ExchangeAndMakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +// ----- ExchangePathEndsAndMakeActive ----- + +// An operator which exchanges the first and last nodes of two paths and makes a +// node active. +// Possible neighbors for paths 0 -> 1 -> 2 -> 7, 6 -> 3 -> 4 -> 8 +// and 5 inactive are: +// 0 -> 5 -> 3 -> 4 -> 7, 6 -> 1 -> 2 -> 8 +// 0 -> 3 -> 4 -> 7, 6 -> 1 -> 5 -> 2 -> 8 +// 0 -> 3 -> 4 -> 7, 6 -> 1 -> 2 -> 5 -> 8 +// 0 -> 3 -> 5 -> 4 -> 7, 6 -> 1 -> 2 -> 8 +// 0 -> 3 -> 4 -> 5 -> 7, 6 -> 1 -> 2 -> 8 +// +// This neighborhood is an artificially reduced version of +// ExchangeAndMakeActiveOperator. It can still be used to opportunistically +// insert inactive nodes. + +LocalSearchOperator* ExchangePathStartEndsAndMakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- MakeActiveAndRelocate ----- + +/// MakeActiveAndRelocate makes a node active next to a node being relocated. +/// Possible neighbor for paths 0 -> 4, 1 -> 2 -> 5 and 3 inactive is: +/// 0 -> 3 -> 2 -> 4, 1 -> 5. + +LocalSearchOperator* MakeActiveAndRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- MakeInactive ----- + +/// MakeInactive makes path nodes inactive. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first +/// and last nodes of the path) are: +/// 1 -> 3 -> 4 & 2 inactive +/// 1 -> 2 -> 4 & 3 inactive + +LocalSearchOperator* MakeInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- RelocateAndMakeInactive ----- + +/// RelocateAndMakeInactive relocates a node to a new position and makes the +/// node which was at that position inactive. +/// Possible neighbors for paths 0 -> 2 -> 4, 1 -> 3 -> 5 are: +/// 0 -> 3 -> 4, 1 -> 5 & 2 inactive +/// 0 -> 4, 1 -> 2 -> 5 & 3 inactive + +LocalSearchOperator* RelocateAndMakeInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- MakeChainInactive ----- + +/// Operator which makes a "chain" of path nodes inactive. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first +/// and last nodes of the path) are: +/// 1 -> 3 -> 4 with 2 inactive +/// 1 -> 2 -> 4 with 3 inactive +/// 1 -> 4 with 2 and 3 inactive + +LocalSearchOperator* MakeChainInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- SwapActive ----- + +/// SwapActive replaces an active node by an inactive one. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 +/// and 4 are first and last nodes of the path) are: +/// 1 -> 5 -> 3 -> 4 & 2 inactive +/// 1 -> 2 -> 5 -> 4 & 3 inactive + +LocalSearchOperator* MakeSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- SwapActiveChain ----- + +LocalSearchOperator* MakeSwapActiveChain( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, int max_chain_size); + +/// ----- ExtendedSwapActive ----- + +/// ExtendedSwapActive makes an inactive node active and an active one +/// inactive. It is similar to SwapActiveOperator excepts that it tries to +/// insert the inactive node in all possible positions instead of just the +/// position of the node made inactive. +/// Possible neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 +/// and 4 are first and last nodes of the path) are: +/// 1 -> 5 -> 3 -> 4 & 2 inactive +/// 1 -> 3 -> 5 -> 4 & 2 inactive +/// 1 -> 5 -> 2 -> 4 & 3 inactive +/// 1 -> 2 -> 5 -> 4 & 3 inactive + +LocalSearchOperator* MakeExtendedSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class); + +/// ----- TSP-based operators ----- + +/// Sliding TSP operator +/// Uses an exact dynamic programming algorithm to solve the TSP corresponding +/// to path sub-chains. +/// For a subchain 1 -> 2 -> 3 -> 4 -> 5 -> 6, solves the TSP on nodes A, 2, 3, +/// 4, 5, where A is a merger of nodes 1 and 6 such that cost(A,i) = cost(1,i) +/// and cost(i,A) = cost(i,6). + +LocalSearchOperator* MakeTSPOpt(Solver* solver, + const std::vector& vars, + const std::vector& secondary_vars, + Solver::IndexEvaluator3 evaluator, + int chain_length); + +/// TSP-base lns. +/// Randomly merge consecutive nodes until n "meta"-nodes remain and solve the +/// corresponding TSP. This can be seen as a large neighborhood search operator +/// although decisions are taken with the operator. +/// This is an "unlimited" neighborhood which must be stopped by search limits. +/// To force diversification, the operator iteratively forces each node to serve +/// as base of a meta-node. + +LocalSearchOperator* MakeTSPLns(Solver* solver, + const std::vector& vars, + const std::vector& secondary_vars, + Solver::IndexEvaluator3 evaluator, + int tsp_size); + +/// ----- Lin-Kernighan ----- + +LocalSearchOperator* MakeLinKernighan( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + const Solver::IndexEvaluator3& evaluator, bool topt); + +/// ----- Path-based Large Neighborhood Search ----- + +/// Breaks "number_of_chunks" chains of "chunk_size" arcs, and deactivate all +/// inactive nodes if "unactive_fragments" is true. +/// As a special case, if chunk_size=0, then we break full paths. + +LocalSearchOperator* MakePathLns(Solver* solver, + const std::vector& vars, + const std::vector& secondary_vars, + int number_of_chunks, int chunk_size, + bool unactive_fragments); +#endif // SWIG #if !defined(SWIG) // After building a Directed Acyclic Graph, allows to generate sub-DAGs diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index 1e1933a654a..92d4cbca9df 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -58,10 +58,6 @@ ABSL_FLAG(int, cp_local_search_tsp_opt_size, 13, ABSL_FLAG(int, cp_local_search_tsp_lns_size, 10, "Size of TSPs solved in the TSPLns operator."); -ABSL_FLAG(bool, cp_use_empty_path_symmetry_breaker, true, - "If true, equivalent empty paths are removed from the neighborhood " - "of PathOperators"); - namespace operations_research { // Utility methods to ensure the communication between local search and the @@ -346,619 +342,32 @@ class DecrementValue : public ChangeValue { // ----- Path-based Operators ----- -PathOperator::PathOperator(const std::vector& next_vars, - const std::vector& path_vars, - IterationParameters iteration_parameters) - : IntVarLocalSearchOperator(next_vars, true), - number_of_nexts_(next_vars.size()), - ignore_path_vars_(path_vars.empty()), - base_nodes_(iteration_parameters.number_of_base_nodes), - base_alternatives_(iteration_parameters.number_of_base_nodes), - base_sibling_alternatives_(iteration_parameters.number_of_base_nodes), - end_nodes_(iteration_parameters.number_of_base_nodes), - base_paths_(iteration_parameters.number_of_base_nodes), - node_path_starts_(number_of_nexts_, -1), - node_path_ends_(number_of_nexts_, -1), - calls_per_base_node_(iteration_parameters.number_of_base_nodes, 0), - just_started_(false), - first_start_(true), - next_base_to_increment_(iteration_parameters.number_of_base_nodes), - iteration_parameters_(std::move(iteration_parameters)), - optimal_paths_enabled_(false), - active_paths_(number_of_nexts_), - alternative_index_(next_vars.size(), -1) { - DCHECK_GT(iteration_parameters_.number_of_base_nodes, 0); - if (!ignore_path_vars_) { - AddVars(path_vars); - } - path_basis_.push_back(0); - for (int i = 1; i < base_nodes_.size(); ++i) { - if (!OnSamePathAsPreviousBase(i)) path_basis_.push_back(i); - } - if ((path_basis_.size() > 2) || - (!next_vars.empty() && !next_vars.back() - ->solver() - ->parameters() - .skip_locally_optimal_paths())) { - iteration_parameters_.skip_locally_optimal_paths = false; - } -} - -void PathOperator::EnterSearch() { - first_start_ = true; - ResetIncrementalism(); -} - -void PathOperator::Reset() { - active_paths_.Clear(); - ResetIncrementalism(); -} - -void PathOperator::OnStart() { - optimal_paths_enabled_ = false; - InitializeBaseNodes(); - InitializeAlternatives(); - OnNodeInitialization(); -} - -bool PathOperator::MakeOneNeighbor() { - while (IncrementPosition()) { - // Need to revert changes here since MakeNeighbor might have returned false - // and have done changes in the previous iteration. - RevertChanges(true); - if (MakeNeighbor()) { - return true; - } - } - return false; -} - -bool PathOperator::SkipUnchanged(int index) const { - if (ignore_path_vars_) { - return true; - } - if (index < number_of_nexts_) { - int path_index = index + number_of_nexts_; - return Value(path_index) == OldValue(path_index); - } - int next_index = index - number_of_nexts_; - return Value(next_index) == OldValue(next_index); -} - -bool PathOperator::MoveChain(int64_t before_chain, int64_t chain_end, - int64_t destination) { - if (destination == before_chain || destination == chain_end) return false; - DCHECK(CheckChainValidity(before_chain, chain_end, destination) && - !IsPathEnd(chain_end) && !IsPathEnd(destination)); - const int64_t destination_path = Path(destination); - const int64_t after_chain = Next(chain_end); - SetNext(chain_end, Next(destination), destination_path); - if (!ignore_path_vars_) { - int current = destination; - int next = Next(before_chain); - while (current != chain_end) { - SetNext(current, next, destination_path); - current = next; - next = Next(next); - } - } else { - SetNext(destination, Next(before_chain), destination_path); - } - SetNext(before_chain, after_chain, Path(before_chain)); - return true; -} - -bool PathOperator::ReverseChain(int64_t before_chain, int64_t after_chain, - int64_t* chain_last) { - if (CheckChainValidity(before_chain, after_chain, -1)) { - int64_t path = Path(before_chain); - int64_t current = Next(before_chain); - if (current == after_chain) { - return false; - } - int64_t current_next = Next(current); - SetNext(current, after_chain, path); - while (current_next != after_chain) { - const int64_t next = Next(current_next); - SetNext(current_next, current, path); - current = current_next; - current_next = next; - } - SetNext(before_chain, current, path); - *chain_last = current; - return true; - } - return false; -} - -bool PathOperator::MakeActive(int64_t node, int64_t destination) { - if (!IsPathEnd(destination)) { - int64_t destination_path = Path(destination); - SetNext(node, Next(destination), destination_path); - SetNext(destination, node, destination_path); - return true; - } - return false; -} - -bool PathOperator::MakeChainInactive(int64_t before_chain, int64_t chain_end) { - const int64_t kNoPath = -1; - if (CheckChainValidity(before_chain, chain_end, -1) && - !IsPathEnd(chain_end)) { - const int64_t after_chain = Next(chain_end); - int64_t current = Next(before_chain); - while (current != after_chain) { - const int64_t next = Next(current); - SetNext(current, current, kNoPath); - current = next; - } - SetNext(before_chain, after_chain, Path(before_chain)); - return true; - } - return false; -} - -bool PathOperator::SwapActiveAndInactive(int64_t active, int64_t inactive) { - if (active == inactive) return false; - const int64_t prev = Prev(active); - return MakeChainInactive(prev, active) && MakeActive(inactive, prev); -} - -bool PathOperator::SwapActiveAndInactiveChains( - absl::Span active_chain, - absl::Span inactive_chain) { - if (active_chain.empty()) return false; - if (active_chain == inactive_chain) return false; - const int before_active_chain = Prev(active_chain.front()); - if (!MakeChainInactive(before_active_chain, active_chain.back())) { - return false; - } - for (auto it = inactive_chain.crbegin(); it != inactive_chain.crend(); ++it) { - if (!MakeActive(*it, before_active_chain)) return false; - } - return true; -} - -bool PathOperator::IncrementPosition() { - const int base_node_size = iteration_parameters_.number_of_base_nodes; - - if (just_started_) { - just_started_ = false; - return true; - } - const int number_of_paths = path_starts_.size(); - // Finding next base node positions. - // Increment the position of inner base nodes first (higher index nodes); - // if a base node is at the end of a path, reposition it at the start - // of the path and increment the position of the preceding base node (this - // action is called a restart). - int last_restarted = base_node_size; - for (int i = base_node_size - 1; i >= 0; --i) { - if (base_nodes_[i] < number_of_nexts_ && i <= next_base_to_increment_) { - if (ConsiderAlternatives(i)) { - // Iterate on sibling alternatives. - const int sibling_alternative_index = - GetSiblingAlternativeIndex(base_nodes_[i]); - if (sibling_alternative_index >= 0) { - if (base_sibling_alternatives_[i] < - alternative_sets_[sibling_alternative_index].size() - 1) { - ++base_sibling_alternatives_[i]; - break; - } - base_sibling_alternatives_[i] = 0; - } - // Iterate on base alternatives. - const int alternative_index = alternative_index_[base_nodes_[i]]; - if (alternative_index >= 0) { - if (base_alternatives_[i] < - alternative_sets_[alternative_index].size() - 1) { - ++base_alternatives_[i]; - break; - } - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - } - } - if (HasNeighbors()) { - const int64_t base_node = BaseNode(i); - const int64_t start_node = StartNode(i); - const int num_incoming_neighbors = - IsPathStart(base_node) || - iteration_parameters_.get_incoming_neighbors == nullptr - ? 0 - : iteration_parameters_ - .get_incoming_neighbors(base_node, start_node) - .size(); - const int num_outgoing_neighbors = - IsPathEnd(base_node) || - iteration_parameters_.get_outgoing_neighbors == nullptr - ? 0 - : iteration_parameters_ - .get_outgoing_neighbors(base_node, start_node) - .size(); - if (++calls_per_base_node_[i] < - num_incoming_neighbors + num_outgoing_neighbors) { - break; - } - } - calls_per_base_node_[i] = 0; - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - base_nodes_[i] = OldNext(base_nodes_[i]); - if (iteration_parameters_.accept_path_end_base || - !IsPathEnd(base_nodes_[i])) { - break; - } - } - calls_per_base_node_[i] = 0; - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - base_nodes_[i] = StartNode(i); - last_restarted = i; - } - next_base_to_increment_ = base_node_size; - // At the end of the loop, base nodes with indexes in - // [last_restarted, base_node_size[ have been restarted. - // Restarted base nodes are then repositioned by the virtual - // GetBaseNodeRestartPosition to reflect position constraints between - // base nodes (by default GetBaseNodeRestartPosition leaves the nodes - // at the start of the path). - // Base nodes are repositioned in ascending order to ensure that all - // base nodes "below" the node being repositioned have their final - // position. - for (int i = last_restarted; i < base_node_size; ++i) { - calls_per_base_node_[i] = 0; - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - base_nodes_[i] = GetBaseNodeRestartPosition(i); - } - if (last_restarted > 0) { - return CheckEnds(); - } - // If all base nodes have been restarted, base nodes are moved to new paths. - // First we mark the current paths as locally optimal if they have been - // completely explored. - if (optimal_paths_enabled_ && - iteration_parameters_.skip_locally_optimal_paths) { - if (path_basis_.size() > 1) { - for (int i = 1; i < path_basis_.size(); ++i) { - active_paths_.DeactivatePathPair(StartNode(path_basis_[i - 1]), - StartNode(path_basis_[i])); - } - } else { - active_paths_.DeactivatePathPair(StartNode(path_basis_[0]), - StartNode(path_basis_[0])); - } - } - std::vector current_starts(base_node_size); - for (int i = 0; i < base_node_size; ++i) { - current_starts[i] = StartNode(i); - } - // Exploration of next paths can lead to locally optimal paths since we are - // exploring them from scratch. - optimal_paths_enabled_ = true; - while (true) { - for (int i = base_node_size - 1; i >= 0; --i) { - const int next_path_index = base_paths_[i] + 1; - if (next_path_index < number_of_paths) { - base_paths_[i] = next_path_index; - calls_per_base_node_[i] = 0; - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - base_nodes_[i] = path_starts_[next_path_index]; - if (i == 0 || !OnSamePathAsPreviousBase(i)) { - break; - } - } else { - base_paths_[i] = 0; - calls_per_base_node_[i] = 0; - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - base_nodes_[i] = path_starts_[0]; - } - } - if (!iteration_parameters_.skip_locally_optimal_paths) return CheckEnds(); - // If the new paths have already been completely explored, we can - // skip them from now on. - if (path_basis_.size() > 1) { - for (int j = 1; j < path_basis_.size(); ++j) { - if (active_paths_.IsPathPairActive(StartNode(path_basis_[j - 1]), - StartNode(path_basis_[j]))) { - return CheckEnds(); - } - } - } else { - if (active_paths_.IsPathPairActive(StartNode(path_basis_[0]), - StartNode(path_basis_[0]))) { - return CheckEnds(); - } - } - // If we are back to paths we just iterated on or have reached the end - // of the neighborhood search space, we can stop. - if (!CheckEnds()) return false; - bool stop = true; - for (int i = 0; i < base_node_size; ++i) { - if (StartNode(i) != current_starts[i]) { - stop = false; - break; - } - } - if (stop) return false; - } - return CheckEnds(); -} - -void PathOperator::InitializePathStarts() { - // Detect nodes which do not have any possible predecessor in a path; these - // nodes are path starts. - int max_next = -1; - std::vector has_prevs(number_of_nexts_, false); - for (int i = 0; i < number_of_nexts_; ++i) { - const int next = OldNext(i); - if (next < number_of_nexts_) { - has_prevs[next] = true; - } - max_next = std::max(max_next, next); - } - // Update locally optimal paths. - if (iteration_parameters_.skip_locally_optimal_paths) { - active_paths_.Initialize( - /*is_start=*/[&has_prevs](int node) { return !has_prevs[node]; }); - for (int i = 0; i < number_of_nexts_; ++i) { - if (!has_prevs[i]) { - int current = i; - while (!IsPathEnd(current)) { - if ((OldNext(current) != PrevNext(current))) { - active_paths_.ActivatePath(i); - break; - } - current = OldNext(current); - } - } - } - } - // Create a list of path starts, dropping equivalent path starts of - // currently empty paths. - std::vector empty_found(number_of_nexts_, false); - std::vector new_path_starts; - const bool use_empty_path_symmetry_breaker = - absl::GetFlag(FLAGS_cp_use_empty_path_symmetry_breaker); - for (int i = 0; i < number_of_nexts_; ++i) { - if (!has_prevs[i]) { - if (use_empty_path_symmetry_breaker && IsPathEnd(OldNext(i))) { - if (iteration_parameters_.start_empty_path_class != nullptr) { - if (empty_found[iteration_parameters_.start_empty_path_class(i)]) - continue; - empty_found[iteration_parameters_.start_empty_path_class(i)] = true; - } - } - new_path_starts.push_back(i); - } - } - if (!first_start_) { - // Synchronizing base_paths_ with base node positions. When the last move - // was performed a base node could have been moved to a new route in which - // case base_paths_ needs to be updated. This needs to be done on the path - // starts before we re-adjust base nodes for new path starts. - std::vector node_paths(max_next + 1, -1); - for (int i = 0; i < path_starts_.size(); ++i) { - int node = path_starts_[i]; - while (!IsPathEnd(node)) { - node_paths[node] = i; - node = OldNext(node); - } - node_paths[node] = i; - } - for (int j = 0; j < iteration_parameters_.number_of_base_nodes; ++j) { - // Always restart from first alternative. - calls_per_base_node_[j] = 0; - base_alternatives_[j] = 0; - base_sibling_alternatives_[j] = 0; - if (IsInactive(base_nodes_[j]) || node_paths[base_nodes_[j]] == -1) { - // Base node was made inactive or was moved to a new path, reposition - // the base node to its restart position. - base_nodes_[j] = GetBaseNodeRestartPosition(j); - base_paths_[j] = node_paths[base_nodes_[j]]; - } else { - base_paths_[j] = node_paths[base_nodes_[j]]; - } - } - // Re-adjust current base_nodes and base_paths to take into account new - // path starts (there could be fewer if a new path was made empty, or more - // if nodes were added to a formerly empty path). - int new_index = 0; - absl::flat_hash_set found_bases; - for (int i = 0; i < path_starts_.size(); ++i) { - int index = new_index; - // Note: old and new path starts are sorted by construction. - while (index < new_path_starts.size() && - new_path_starts[index] < path_starts_[i]) { - ++index; - } - const bool found = (index < new_path_starts.size() && - new_path_starts[index] == path_starts_[i]); - if (found) { - new_index = index; - } - for (int j = 0; j < iteration_parameters_.number_of_base_nodes; ++j) { - if (base_paths_[j] == i && !found_bases.contains(j)) { - found_bases.insert(j); - base_paths_[j] = new_index; - // If the current position of the base node is a removed empty path, - // readjusting it to the last visited path start. - if (!found) { - base_nodes_[j] = new_path_starts[new_index]; - } - } - } - } - } - path_starts_.swap(new_path_starts); - // For every base path, store the end corresponding to the path start. - // TODO(user): make this faster, maybe by pairing starts with ends. - path_ends_.clear(); - path_ends_.reserve(path_starts_.size()); - int64_t max_node_index = number_of_nexts_ - 1; - for (const int start_node : path_starts_) { - int64_t node = start_node; - while (!IsPathEnd(node)) node = OldNext(node); - path_ends_.push_back(node); - max_node_index = std::max(max_node_index, node); - } - node_path_starts_.assign(max_node_index + 1, -1); - node_path_ends_.assign(max_node_index + 1, -1); - for (int i = 0; i < path_starts_.size(); ++i) { - const int64_t start_node = path_starts_[i]; - const int64_t end_node = path_ends_[i]; - int64_t node = start_node; - while (!IsPathEnd(node)) { - node_path_starts_[node] = start_node; - node_path_ends_[node] = end_node; - node = OldNext(node); - } - node_path_starts_[node] = start_node; - node_path_ends_[node] = end_node; - } -} - -void PathOperator::InitializeInactives() { - inactives_.clear(); - for (int i = 0; i < number_of_nexts_; ++i) { - inactives_.push_back(OldNext(i) == i); - } -} - -void PathOperator::InitializeBaseNodes() { - // Inactive nodes must be detected before determining new path starts. - InitializeInactives(); - InitializePathStarts(); - if (first_start_ || InitPosition()) { - // Only do this once since the following starts will continue from the - // preceding position - for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { - base_paths_[i] = 0; - base_nodes_[i] = path_starts_[0]; - } - first_start_ = false; - } - for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { - // If base node has been made inactive, restart from path start. - int64_t base_node = base_nodes_[i]; - if (RestartAtPathStartOnSynchronize() || IsInactive(base_node)) { - base_node = path_starts_[base_paths_[i]]; - base_nodes_[i] = base_node; - } - end_nodes_[i] = base_node; - } - // Repair end_nodes_ in case some must be on the same path and are not anymore - // (due to other operators moving these nodes). - for (int i = 1; i < iteration_parameters_.number_of_base_nodes; ++i) { - if (OnSamePathAsPreviousBase(i) && - !OnSamePath(base_nodes_[i - 1], base_nodes_[i])) { - const int64_t base_node = base_nodes_[i - 1]; - base_nodes_[i] = base_node; - end_nodes_[i] = base_node; - base_paths_[i] = base_paths_[i - 1]; - } - } - for (int i = 0; i < iteration_parameters_.number_of_base_nodes; ++i) { - base_alternatives_[i] = 0; - base_sibling_alternatives_[i] = 0; - calls_per_base_node_[i] = 0; - } - just_started_ = true; -} - -void PathOperator::InitializeAlternatives() { - active_in_alternative_set_.resize(alternative_sets_.size(), -1); - for (int i = 0; i < alternative_sets_.size(); ++i) { - const int64_t current_active = active_in_alternative_set_[i]; - if (current_active >= 0 && !IsInactive(current_active)) continue; - for (int64_t index : alternative_sets_[i]) { - if (!IsInactive(index)) { - active_in_alternative_set_[i] = index; - break; - } - } - } -} - -bool PathOperator::OnSamePath(int64_t node1, int64_t node2) const { - if (IsInactive(node1) != IsInactive(node2)) { - return false; - } - for (int node = node1; !IsPathEnd(node); node = OldNext(node)) { - if (node == node2) { - return true; - } - } - for (int node = node2; !IsPathEnd(node); node = OldNext(node)) { - if (node == node1) { - return true; - } - } - return false; -} - -// Rejects chain if chain_end is not after before_chain on the path or if -// the chain contains exclude. Given before_chain is the node before the -// chain, if before_chain and chain_end are the same the chain is rejected too. -// Also rejects cycles (cycle detection is detected through chain length -// overflow). -bool PathOperator::CheckChainValidity(int64_t before_chain, int64_t chain_end, - int64_t exclude) const { - if (before_chain == chain_end || before_chain == exclude) return false; - int64_t current = before_chain; - int chain_size = 0; - while (current != chain_end) { - if (chain_size > number_of_nexts_) { - return false; - } - if (IsPathEnd(current)) { - return false; - } - current = Next(current); - ++chain_size; - if (current == exclude) { - return false; - } - } - return true; -} +using NeighborAccessor = + std::function&(/*node=*/int, /*start_node=*/int)>; // ----- 2Opt ----- -// Reverses a sub-chain of a path. It is called 2Opt because it breaks -// 2 arcs on the path; resulting paths are called 2-optimal. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5 -// (where (1, 5) are first and last nodes of the path and can therefore not be -// moved): -// 1 -> 3 -> 2 -> 4 -> 5 -// 1 -> 4 -> 3 -> 2 -> 5 -// 1 -> 2 -> 4 -> 3 -> 5 - -using NeighborAccessor = - std::function&(/*node=*/int, /*start_node=*/int)>; -class TwoOpt : public PathOperator { +template +class TwoOpt : public PathOperator { public: TwoOpt(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, - NeighborAccessor get_incoming_neighbors = nullptr, - NeighborAccessor get_outgoing_neighbors = nullptr) - : PathOperator(vars, secondary_vars, - (get_incoming_neighbors == nullptr && - get_outgoing_neighbors == nullptr) - ? 2 - : 1, - /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/true, - std::move(start_empty_path_class), - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)), + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) + : PathOperator(vars, secondary_vars, + (get_incoming_neighbors == nullptr && + get_outgoing_neighbors == nullptr) + ? 2 + : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/true, + std::move(start_empty_path_class), + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)), last_base_(-1), last_(-1) {} - ~TwoOpt() override {} + ~TwoOpt() override = default; bool MakeNeighbor() override; bool IsIncremental() const override { return true; } @@ -971,7 +380,7 @@ class TwoOpt : public PathOperator { return true; } int64_t GetBaseNodeRestartPosition(int base_index) override { - return (base_index == 0) ? StartNode(0) : BaseNode(0); + return (base_index == 0) ? this->StartNode(0) : this->BaseNode(0); } private: @@ -981,41 +390,43 @@ class TwoOpt : public PathOperator { int64_t last_; }; -bool TwoOpt::MakeNeighbor() { - const int64_t node0 = BaseNode(0); +template +bool TwoOpt::MakeNeighbor() { + const int64_t node0 = this->BaseNode(0); int64_t before_chain = node0; int64_t after_chain = -1; - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); - if (neighbor < 0 || IsInactive(neighbor)) return false; - if (CurrentNodePathStart(node0) != CurrentNodePathStart(neighbor)) { + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); + if (neighbor < 0 || this->IsInactive(neighbor)) return false; + if (this->CurrentNodePathStart(node0) != + this->CurrentNodePathStart(neighbor)) { return false; } if (outgoing) { - if (IsPathEnd(neighbor)) return false; + if (this->IsPathEnd(neighbor)) return false; // Reverse the chain starting *after" node0 and ending with 'neighbor'. - after_chain = Next(neighbor); + after_chain = this->Next(neighbor); } else { - if (IsPathStart(neighbor)) return false; + if (this->IsPathStart(neighbor)) return false; // Reverse the chain starting with 'neighbor' and ending before node0. - before_chain = Prev(neighbor); + before_chain = this->Prev(neighbor); after_chain = node0; } } else { - DCHECK_EQ(StartNode(0), StartNode(1)); - after_chain = BaseNode(1); + DCHECK_EQ(this->StartNode(0), this->StartNode(1)); + after_chain = this->BaseNode(1); } // Incrementality is disabled with neighbors. - if (last_base_ != node0 || last_ == -1 || HasNeighbors()) { - RevertChanges(false); - if (IsPathEnd(node0)) { + if (last_base_ != node0 || last_ == -1 || this->HasNeighbors()) { + this->RevertChanges(false); + if (this->IsPathEnd(node0)) { last_ = -1; return false; } last_base_ = node0; - last_ = Next(before_chain); + last_ = this->Next(before_chain); int64_t chain_last; - if (ReverseChain(before_chain, after_chain, &chain_last) + if (this->ReverseChain(before_chain, after_chain, &chain_last) // Check there are more than one node in the chain (reversing a // single node is a NOP). && last_ != chain_last) { @@ -1025,35 +436,39 @@ bool TwoOpt::MakeNeighbor() { return false; } DCHECK_EQ(before_chain, node0); - const int64_t to_move = Next(last_); - DCHECK_EQ(Next(to_move), after_chain); - return MoveChain(last_, to_move, before_chain); + const int64_t to_move = this->Next(last_); + DCHECK_EQ(this->Next(to_move), after_chain); + return this->MoveChain(last_, to_move, before_chain); } -// ----- Relocate ----- +LocalSearchOperator* MakeTwoOpt( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new TwoOpt( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); + } + return solver->RevAlloc(new TwoOpt( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); +} -// Moves a sub-chain of a path to another position; the specified chain length -// is the fixed length of the chains being moved. When this length is 1 the -// operator simply moves a node to another position. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5, for a chain length -// of 2 (where (1, 5) are first and last nodes of the path and can -// therefore not be moved): -// 1 -> 4 -> 2 -> 3 -> 5 -// 1 -> 3 -> 4 -> 2 -> 5 -// -// Using Relocate with chain lengths of 1, 2 and 3 together is equivalent to -// the OrOpt operator on a path. The OrOpt operator is a limited version of -// 3Opt (breaks 3 arcs on a path). +// ----- Relocate ----- -class Relocate : public PathOperator { +template +class Relocate : public PathOperator { public: Relocate(const std::vector& vars, - const std::vector& secondary_vars, const std::string& name, + const std::vector& secondary_vars, absl::string_view name, std::function start_empty_path_class, NeighborAccessor get_incoming_neighbors, NeighborAccessor get_outgoing_neighbors, int64_t chain_length = 1LL, bool single_path = false) - : PathOperator( + : PathOperator( vars, secondary_vars, (get_incoming_neighbors == nullptr && get_outgoing_neighbors == nullptr) @@ -1065,30 +480,10 @@ class Relocate : public PathOperator { std::move(get_outgoing_neighbors)), chain_length_(chain_length), single_path_(single_path), - name_(name) { + name_(absl::StrCat(name, "<", chain_length, ">")) { CHECK_GT(chain_length_, 0); } - Relocate(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors, int64_t chain_length = 1LL, - bool single_path = false) - : Relocate(vars, secondary_vars, - absl::StrCat("Relocate<", chain_length, ">"), - std::move(start_empty_path_class), - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors), chain_length, single_path) { - } - Relocate(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - int64_t chain_length = 1LL, bool single_path = false) - : Relocate(vars, secondary_vars, - absl::StrCat("Relocate<", chain_length, ">"), - std::move(start_empty_path_class), nullptr, nullptr, - chain_length, single_path) {} - ~Relocate() override {} + ~Relocate() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return name_; } @@ -1106,141 +501,159 @@ class Relocate : public PathOperator { const std::string name_; }; -bool Relocate::MakeNeighbor() { +template +bool Relocate::MakeNeighbor() { const auto do_move = [this](int64_t before_chain, int64_t destination) { - DCHECK(!IsPathEnd(destination)); + DCHECK(!this->IsPathEnd(destination)); int64_t chain_end = before_chain; for (int i = 0; i < chain_length_; ++i) { - if (IsPathEnd(chain_end) || chain_end == destination) return false; - chain_end = Next(chain_end); + if (this->IsPathEnd(chain_end) || chain_end == destination) return false; + chain_end = this->Next(chain_end); } - return !IsPathEnd(chain_end) && - MoveChain(before_chain, chain_end, destination); + return !this->IsPathEnd(chain_end) && + this->MoveChain(before_chain, chain_end, destination); }; - const int64_t node0 = BaseNode(0); - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); - if (neighbor < 0 || IsInactive(neighbor)) return false; + const int64_t node0 = this->BaseNode(0); + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); + if (neighbor < 0 || this->IsInactive(neighbor)) return false; if (outgoing) { - return do_move(/*before_chain=*/Prev(neighbor), + return do_move(/*before_chain=*/this->Prev(neighbor), /*destination=*/node0); } DCHECK_EQ(chain_length_, 1); // TODO(user): Handle chain_length_ > 1 for incoming neighbors by going // backwards on the chain. NOTE: In this setting it makes sense to have path // ends as base nodes as we move the chain "before" the base node. - DCHECK(!IsPathStart(node0)) << "Path starts have no incoming neighbors."; - return do_move(/*before_chain=*/Prev(neighbor), - /*destination=*/Prev(node0)); + DCHECK(!this->IsPathStart(node0)) + << "Path starts have no incoming neighbors."; + return do_move(/*before_chain=*/this->Prev(neighbor), + /*destination=*/this->Prev(node0)); } - DCHECK(!single_path_ || StartNode(0) == StartNode(1)); - return do_move(/*before_chain=*/node0, /*destination=*/BaseNode(1)); + DCHECK(!single_path_ || this->StartNode(0) == this->StartNode(1)); + return do_move(/*before_chain=*/node0, /*destination=*/this->BaseNode(1)); } -// ----- Exchange ----- +LocalSearchOperator* MakeRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors, int64_t chain_length, + bool single_path, const std::string& name) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new Relocate( + vars, secondary_vars, name, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + chain_length, single_path)); + } + return solver->RevAlloc(new Relocate( + vars, secondary_vars, name, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + chain_length, single_path)); +} -// Exchanges the positions of two nodes. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 -> 5 -// (where (1, 5) are first and last nodes of the path and can therefore not -// be moved): -// 1 -> 3 -> 2 -> 4 -> 5 -// 1 -> 4 -> 3 -> 2 -> 5 -// 1 -> 2 -> 4 -> 3 -> 5 +// ----- Exchange ----- -class Exchange : public PathOperator { +template +class Exchange : public PathOperator { public: Exchange(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, - NeighborAccessor get_incoming_neighbors = nullptr, - NeighborAccessor get_outgoing_neighbors = nullptr) - : PathOperator(vars, secondary_vars, - (get_incoming_neighbors == nullptr && - get_outgoing_neighbors == nullptr) - ? 2 - : 1, - /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/false, - std::move(start_empty_path_class), - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)) {} - ~Exchange() override {} + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) + : PathOperator(vars, secondary_vars, + (get_incoming_neighbors == nullptr && + get_outgoing_neighbors == nullptr) + ? 2 + : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, + std::move(start_empty_path_class), + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)) {} + ~Exchange() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "Exchange"; } }; -bool Exchange::MakeNeighbor() { - const auto do_move = [this](int64_t node1, int64_t node2) { - if (IsPathEnd(node1) || IsPathEnd(node2) || IsPathStart(node1) || - IsPathStart(node2)) { - return false; - } - if (node1 == node2) return false; - const int64_t prev_node1 = Prev(node1); - const bool ok = MoveChain(prev_node1, node1, Prev(node2)); - return MoveChain(Prev(node2), node2, prev_node1) || ok; - }; - const int64_t node0 = BaseNode(0); - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); - if (neighbor < 0 || IsInactive(neighbor)) return false; +template +bool Exchange::MakeNeighbor() { + const int64_t node0 = this->BaseNode(0); + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); + if (neighbor < 0 || this->IsInactive(neighbor)) return false; if (outgoing) { // Exchange node0's next with 'neighbor'. - return do_move(Next(node0), neighbor); + return this->SwapNodes(this->Next(node0), neighbor); } - DCHECK(!IsPathStart(node0)) << "Path starts have no incoming neighbors."; + DCHECK(!this->IsPathStart(node0)) + << "Path starts have no incoming neighbors."; // Exchange node0's prev with 'neighbor'. - return do_move(Prev(node0), neighbor); + return this->SwapNodes(this->Prev(node0), neighbor); + } + return this->SwapNodes(this->Next(node0), this->Next(this->BaseNode(1))); +} + +LocalSearchOperator* MakeExchange( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new Exchange( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); } - return do_move(Next(node0), Next(BaseNode(1))); + return solver->RevAlloc(new Exchange( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); } // ----- Cross ----- -// Cross echanges the starting chains of 2 paths, including exchanging the -// whole paths. -// First and last nodes are not moved. -// Possible neighbors for the paths 1 -> 2 -> 3 -> 4 -> 5 and 6 -> 7 -> 8 -// (where (1, 5) and (6, 8) are first and last nodes of the paths and can -// therefore not be moved): -// 1 -> 7 -> 3 -> 4 -> 5 6 -> 2 -> 8 -// 1 -> 7 -> 4 -> 5 6 -> 2 -> 3 -> 8 -// 1 -> 7 -> 5 6 -> 2 -> 3 -> 4 -> 8 - -class Cross : public PathOperator { +template +class Cross : public PathOperator { public: Cross(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, - NeighborAccessor get_incoming_neighbors = nullptr, - NeighborAccessor get_outgoing_neighbors = nullptr) - : PathOperator( - vars, secondary_vars, get_outgoing_neighbors == nullptr ? 2 : 1, - /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/true, std::move(start_empty_path_class), - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)) {} - ~Cross() override {} + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) + : PathOperator(vars, secondary_vars, + (get_incoming_neighbors == nullptr && + get_outgoing_neighbors == nullptr) + ? 2 + : 1, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/true, + std::move(start_empty_path_class), + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)) {} + ~Cross() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "Cross"; } }; -bool Cross::MakeNeighbor() { - const int64_t start0 = StartNode(0); +template +bool Cross::MakeNeighbor() { + const int64_t start0 = this->StartNode(0); int64_t start1 = -1; - const int64_t node0 = BaseNode(0); + const int64_t node0 = this->BaseNode(0); int64_t node1 = -1; if (node0 == start0) return false; bool cross_path_starts = false; - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); if (neighbor < 0) return false; cross_path_starts = outgoing; - DCHECK(!IsPathStart(neighbor)); - if (IsInactive(neighbor)) return false; - start1 = CurrentNodePathStart(neighbor); + DCHECK(!this->IsPathStart(neighbor)); + if (this->IsInactive(neighbor)) return false; + start1 = this->CurrentNodePathStart(neighbor); // Tricky: In all cases we want to connect node0 to neighbor. If we are // crossing path starts, node0 is the end of a chain and neighbor is the // the first node after the other chain ending at node1, therefore @@ -1248,10 +661,10 @@ bool Cross::MakeNeighbor() { // If we are crossing path ends, node0 is the start of a chain and neighbor // is the last node before the other chain starting at node1, therefore // node1 = next(neighbor). - node1 = cross_path_starts ? Prev(neighbor) : Next(neighbor); + node1 = cross_path_starts ? this->Prev(neighbor) : this->Next(neighbor); } else { - start1 = StartNode(1); - node1 = BaseNode(1); + start1 = this->StartNode(1); + node1 = this->BaseNode(1); cross_path_starts = start0 < start1; } if (start1 == start0 || node1 == start1) return false; @@ -1260,44 +673,67 @@ bool Cross::MakeNeighbor() { if (cross_path_starts) { // Cross path starts. // If two paths are equivalent don't exchange the full paths. - if (PathClassFromStartNode(start0) == PathClassFromStartNode(start1) && - !IsPathEnd(node0) && IsPathEnd(Next(node0)) && !IsPathEnd(node1) && - IsPathEnd(Next(node1))) { + if (this->PathClassFromStartNode(start0) == + this->PathClassFromStartNode(start1) && + !this->IsPathEnd(node0) && this->IsPathEnd(this->Next(node0)) && + !this->IsPathEnd(node1) && this->IsPathEnd(this->Next(node1))) { return false; } - const int first1 = Next(start1); - if (!IsPathEnd(node0)) moved |= MoveChain(start0, node0, start1); - if (!IsPathEnd(node1)) moved |= MoveChain(Prev(first1), node1, start0); + const int first1 = this->Next(start1); + if (!this->IsPathEnd(node0)) + moved |= this->MoveChain(start0, node0, start1); + if (!this->IsPathEnd(node1)) + moved |= this->MoveChain(this->Prev(first1), node1, start0); } else { // Cross path ends. // If paths are equivalent, every end crossing has a corresponding start // crossing, we don't generate those symmetric neighbors. - if (PathClassFromStartNode(start0) == PathClassFromStartNode(start1) && - !HasNeighbors()) { + if (this->PathClassFromStartNode(start0) == + this->PathClassFromStartNode(start1) && + !this->HasNeighbors()) { return false; } // Never exchange full paths, equivalent or not. // Full path exchange is only performed when start0 < start1. - if (IsPathStart(Prev(node0)) && IsPathStart(Prev(node1)) && - !HasNeighbors()) { + if (this->IsPathStart(this->Prev(node0)) && + this->IsPathStart(this->Prev(node1)) && !this->HasNeighbors()) { return false; } - const int prev_end_node1 = Prev(CurrentNodePathEnd(node1)); - if (!IsPathEnd(node0)) { - moved |= MoveChain(Prev(node0), Prev(EndNode(0)), prev_end_node1); + const int prev_end_node1 = this->Prev(this->CurrentNodePathEnd(node1)); + if (!this->IsPathEnd(node0)) { + moved |= this->MoveChain(this->Prev(node0), this->Prev(this->EndNode(0)), + prev_end_node1); } - if (!IsPathEnd(node1)) { - moved |= MoveChain(Prev(node1), prev_end_node1, Prev(EndNode(0))); + if (!this->IsPathEnd(node1)) { + moved |= this->MoveChain(this->Prev(node1), prev_end_node1, + this->Prev(this->EndNode(0))); } } return moved; } +LocalSearchOperator* MakeCross( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new Cross( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); + } + return solver->RevAlloc(new Cross( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); +} + // ----- BaseInactiveNodeToPathOperator ----- // Base class of path operators which make inactive nodes active. -class BaseInactiveNodeToPathOperator : public PathOperator { +template +class BaseInactiveNodeToPathOperator : public PathOperator { public: BaseInactiveNodeToPathOperator( const std::vector& vars, @@ -1305,14 +741,15 @@ class BaseInactiveNodeToPathOperator : public PathOperator { std::function start_empty_path_class, NeighborAccessor get_incoming_neighbors = nullptr, NeighborAccessor get_outgoing_neighbors = nullptr) - : PathOperator(vars, secondary_vars, number_of_base_nodes, false, false, - std::move(start_empty_path_class), - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)), + : PathOperator(vars, secondary_vars, + number_of_base_nodes, false, false, + std::move(start_empty_path_class), + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)), inactive_node_(0) { // TODO(user): Activate skipping optimal paths. } - ~BaseInactiveNodeToPathOperator() override {} + ~BaseInactiveNodeToPathOperator() override = default; protected: bool MakeOneNeighbor() override; @@ -1324,20 +761,23 @@ class BaseInactiveNodeToPathOperator : public PathOperator { int inactive_node_; }; -void BaseInactiveNodeToPathOperator::OnNodeInitialization() { - for (int i = 0; i < Size(); ++i) { - if (IsInactive(i)) { +template +void BaseInactiveNodeToPathOperator::OnNodeInitialization() { + for (int i = 0; i < this->Size(); ++i) { + if (this->IsInactive(i)) { inactive_node_ = i; return; } } - inactive_node_ = Size(); + inactive_node_ = this->Size(); } -bool BaseInactiveNodeToPathOperator::MakeOneNeighbor() { - while (inactive_node_ < Size()) { - if (!IsInactive(inactive_node_) || !PathOperator::MakeOneNeighbor()) { - ResetPosition(); +template +bool BaseInactiveNodeToPathOperator::MakeOneNeighbor() { + while (inactive_node_ < this->Size()) { + if (!this->IsInactive(inactive_node_) || + !PathOperator::MakeOneNeighbor()) { + this->ResetPosition(); ++inactive_node_; } else { return true; @@ -1348,60 +788,67 @@ bool BaseInactiveNodeToPathOperator::MakeOneNeighbor() { // ----- MakeActiveOperator ----- -// MakeActiveOperator inserts an inactive node into a path. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 and -// 4 are first and last nodes of the path) are: -// 1 -> 5 -> 2 -> 3 -> 4 -// 1 -> 2 -> 5 -> 3 -> 4 -// 1 -> 2 -> 3 -> 5 -> 4 - -class MakeActiveOperator : public BaseInactiveNodeToPathOperator { +template +class MakeActiveOperator + : public BaseInactiveNodeToPathOperator { public: MakeActiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, - NeighborAccessor get_incoming_neighbors = nullptr, - NeighborAccessor get_outgoing_neighbors = nullptr) - : BaseInactiveNodeToPathOperator(vars, secondary_vars, 1, - std::move(start_empty_path_class), - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)) {} - ~MakeActiveOperator() override {} + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 1, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)) {} + ~MakeActiveOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "MakeActiveOperator"; } }; -bool MakeActiveOperator::MakeNeighbor() { +template +bool MakeActiveOperator::MakeNeighbor() { // TODO(user): Add support for neighbors of inactive nodes; would require // having a version without base nodes (probably not a PathOperator). - return MakeActive(GetInactiveNode(), BaseNode(0)); + return this->MakeActive(this->GetInactiveNode(), this->BaseNode(0)); +} + +LocalSearchOperator* MakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + NeighborAccessor get_incoming_neighbors, + NeighborAccessor get_outgoing_neighbors) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); + } + return solver->RevAlloc(new MakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); } // ---- RelocateAndMakeActiveOperator ----- -// RelocateAndMakeActiveOperator relocates a node and replaces it by an inactive -// node. -// The idea is to make room for inactive nodes. -// Possible neighbor for paths 0 -> 4, 1 -> 2 -> 5 and 3 inactive is: -// 0 -> 2 -> 4, 1 -> 3 -> 5. -// TODO(user): Naming is close to MakeActiveAndRelocate but this one is -// correct; rename MakeActiveAndRelocate if it is actually used. -class RelocateAndMakeActiveOperator : public BaseInactiveNodeToPathOperator { +template +class RelocateAndMakeActiveOperator + : public BaseInactiveNodeToPathOperator { public: RelocateAndMakeActiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : BaseInactiveNodeToPathOperator(vars, secondary_vars, 2, - std::move(start_empty_path_class)) {} - ~RelocateAndMakeActiveOperator() override {} + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 2, std::move(start_empty_path_class)) {} + ~RelocateAndMakeActiveOperator() override = default; bool MakeNeighbor() override { - const int64_t before_node_to_move = BaseNode(1); - const int64_t node = Next(before_node_to_move); - return !IsPathEnd(node) && - MoveChain(before_node_to_move, node, BaseNode(0)) && - MakeActive(GetInactiveNode(), before_node_to_move); + const int64_t before_node_to_move = this->BaseNode(1); + const int64_t node = this->Next(before_node_to_move); + return !this->IsPathEnd(node) && + this->MoveChain(before_node_to_move, node, this->BaseNode(0)) && + this->MakeActive(this->GetInactiveNode(), before_node_to_move); } std::string DebugString() const override { @@ -1409,20 +856,119 @@ class RelocateAndMakeActiveOperator : public BaseInactiveNodeToPathOperator { } }; -// ----- MakeActiveAndRelocate ----- +LocalSearchOperator* RelocateAndMakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new RelocateAndMakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new RelocateAndMakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} + +// ----- ExchangeAndMakeActiveOperator ----- + +template +class ExchangeAndMakeActiveOperator + : public BaseInactiveNodeToPathOperator { + public: + ExchangeAndMakeActiveOperator( + const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 3, std::move(start_empty_path_class)) {} + ~ExchangeAndMakeActiveOperator() override {} + bool MakeNeighbor() override { + return this->SwapNodes(this->Next(this->BaseNode(0)), + this->Next(this->BaseNode(1))) && + this->MakeActive(this->GetInactiveNode(), this->BaseNode(2)); + } + std::string DebugString() const override { + return "ExchangeAndMakeActiveOperator"; + } + + protected: + bool OnSamePathAsPreviousBase(int64_t base_index) override { + return base_index == 2; + } +}; + +LocalSearchOperator* ExchangeAndMakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new ExchangeAndMakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new ExchangeAndMakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} -// MakeActiveAndRelocate makes a node active next to a node being relocated. -// Possible neighbor for paths 0 -> 4, 1 -> 2 -> 5 and 3 inactive is: -// 0 -> 3 -> 2 -> 4, 1 -> 5. +// ----- ExchangePathEndsAndMakeActiveOperator ----- -class MakeActiveAndRelocate : public BaseInactiveNodeToPathOperator { +template +class ExchangePathStartEndsAndMakeActiveOperator + : public BaseInactiveNodeToPathOperator { public: - MakeActiveAndRelocate(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class) - : BaseInactiveNodeToPathOperator(vars, secondary_vars, 2, - std::move(start_empty_path_class)) {} - ~MakeActiveAndRelocate() override {} + ExchangePathStartEndsAndMakeActiveOperator( + const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 2, std::move(start_empty_path_class)) {} + ~ExchangePathStartEndsAndMakeActiveOperator() override = default; + int64_t GetBaseNodeRestartPosition(int base_index) override { + return (base_index == 1) ? this->Prev(this->EndNode(1)) + : this->StartNode(base_index); + } + bool MakeNeighbor() override { + const int64_t end_node0 = this->Prev(this->EndNode(0)); + const int64_t end_node1 = this->BaseNode(1); + if (end_node0 == end_node1 || end_node1 != this->Prev(this->EndNode(1))) { + return false; + } + const int64_t start_node0 = this->Next(this->StartNode(0)); + const int64_t start_node1 = this->Next(this->StartNode(1)); + DCHECK_NE(start_node0, start_node1); + return this->SwapNodes(end_node0, end_node1) && + this->SwapNodes(start_node0, start_node1) && + this->MakeActive(this->GetInactiveNode(), this->BaseNode(0)); + } + std::string DebugString() const override { + return "ExchangePathEndsAndMakeActiveOperator"; + } +}; + +LocalSearchOperator* ExchangePathStartEndsAndMakeActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc( + new ExchangePathStartEndsAndMakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new ExchangePathStartEndsAndMakeActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} + +// ----- MakeActiveAndRelocate ----- + +template +class MakeActiveAndRelocateOperator + : public BaseInactiveNodeToPathOperator { + public: + MakeActiveAndRelocateOperator( + const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 2, std::move(start_empty_path_class)) {} + ~MakeActiveAndRelocateOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { @@ -1430,66 +976,85 @@ class MakeActiveAndRelocate : public BaseInactiveNodeToPathOperator { } }; -bool MakeActiveAndRelocate::MakeNeighbor() { - const int64_t before_chain = BaseNode(1); - const int64_t chain_end = Next(before_chain); - const int64_t destination = BaseNode(0); - return !IsPathEnd(chain_end) && - MoveChain(before_chain, chain_end, destination) && - MakeActive(GetInactiveNode(), destination); +template +bool MakeActiveAndRelocateOperator::MakeNeighbor() { + const int64_t before_chain = this->BaseNode(1); + const int64_t chain_end = this->Next(before_chain); + const int64_t destination = this->BaseNode(0); + return !this->IsPathEnd(chain_end) && + this->MoveChain(before_chain, chain_end, destination) && + this->MakeActive(this->GetInactiveNode(), destination); } -// ----- MakeInactiveOperator ----- +LocalSearchOperator* MakeActiveAndRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakeActiveAndRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new MakeActiveAndRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} -// MakeInactiveOperator makes path nodes inactive. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first -// and last nodes of the path) are: -// 1 -> 3 -> 4 & 2 inactive -// 1 -> 2 -> 4 & 3 inactive +// ----- MakeInactiveOperator ----- -class MakeInactiveOperator : public PathOperator { +template +class MakeInactiveOperator : public PathOperator { public: MakeInactiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : PathOperator(vars, secondary_vars, 1, true, false, - std::move(start_empty_path_class), nullptr, nullptr) {} - ~MakeInactiveOperator() override {} + : PathOperator(vars, secondary_vars, 1, true, false, + std::move(start_empty_path_class), + nullptr, nullptr) {} + ~MakeInactiveOperator() override = default; bool MakeNeighbor() override { - const int64_t base = BaseNode(0); - return MakeChainInactive(base, Next(base)); + const int64_t base = this->BaseNode(0); + return this->MakeChainInactive(base, this->Next(base)); } std::string DebugString() const override { return "MakeInactiveOperator"; } }; -// ----- RelocateAndMakeInactiveOperator ----- +LocalSearchOperator* MakeInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakeInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new MakeInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} -// RelocateAndMakeInactiveOperator relocates a node to a new position and makes -// the node which was at that position inactive. -// Possible neighbors for paths 0 -> 2 -> 4, 1 -> 3 -> 5 are: -// 0 -> 3 -> 4, 1 -> 5 & 2 inactive -// 0 -> 4, 1 -> 2 -> 5 & 3 inactive +// ----- RelocateAndMakeInactiveOperator ----- -class RelocateAndMakeInactiveOperator : public PathOperator { +template +class RelocateAndMakeInactiveOperator : public PathOperator { public: RelocateAndMakeInactiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : PathOperator(vars, secondary_vars, 2, true, false, - std::move(start_empty_path_class), nullptr, nullptr) {} - ~RelocateAndMakeInactiveOperator() override {} + : PathOperator(vars, secondary_vars, 2, true, false, + std::move(start_empty_path_class), + nullptr, nullptr) {} + ~RelocateAndMakeInactiveOperator() override = default; bool MakeNeighbor() override { - const int64_t destination = BaseNode(1); - const int64_t before_to_move = BaseNode(0); - const int64_t node_to_inactivate = Next(destination); - if (node_to_inactivate == before_to_move || IsPathEnd(node_to_inactivate) || - !MakeChainInactive(destination, node_to_inactivate)) { + const int64_t destination = this->BaseNode(1); + const int64_t before_to_move = this->BaseNode(0); + const int64_t node_to_inactivate = this->Next(destination); + if (node_to_inactivate == before_to_move || + this->IsPathEnd(node_to_inactivate) || + !this->MakeChainInactive(destination, node_to_inactivate)) { return false; } - const int64_t node = Next(before_to_move); - return !IsPathEnd(node) && MoveChain(before_to_move, node, destination); + const int64_t node = this->Next(before_to_move); + return !this->IsPathEnd(node) && + this->MoveChain(before_to_move, node, destination); } std::string DebugString() const override { @@ -1497,35 +1062,42 @@ class RelocateAndMakeInactiveOperator : public PathOperator { } }; -// ----- MakeChainInactiveOperator ----- +LocalSearchOperator* RelocateAndMakeInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new RelocateAndMakeInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new RelocateAndMakeInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} -// Operator which makes a "chain" of path nodes inactive. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first -// and last nodes of the path) are: -// 1 -> 3 -> 4 with 2 inactive -// 1 -> 2 -> 4 with 3 inactive -// 1 -> 4 with 2 and 3 inactive +// ----- MakeChainInactiveOperator ----- -class MakeChainInactiveOperator : public PathOperator { +template +class MakeChainInactiveOperator : public PathOperator { public: MakeChainInactiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : PathOperator(vars, secondary_vars, 2, - /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/false, - std::move(start_empty_path_class), nullptr, nullptr) {} - ~MakeChainInactiveOperator() override {} + : PathOperator(vars, secondary_vars, 2, + /*skip_locally_optimal_paths=*/true, + /*accept_path_end_base=*/false, + std::move(start_empty_path_class), + nullptr, nullptr) {} + ~MakeChainInactiveOperator() override = default; bool MakeNeighbor() override { - const int64_t chain_end = BaseNode(1); - if (!IsPathEnd(chain_end) && chain_end != BaseNode(0) && - !Var(chain_end)->Contains(chain_end)) { + const int64_t chain_end = this->BaseNode(1); + if (!this->IsPathEnd(chain_end) && chain_end != this->BaseNode(0) && + !this->Var(chain_end)->Contains(chain_end)) { // Move to the next before_chain since an unskippable node has been // encountered. - SetNextBaseToIncrement(0); + this->SetNextBaseToIncrement(0); return false; } - return MakeChainInactive(BaseNode(0), chain_end); + return this->MakeChainInactive(this->BaseNode(0), chain_end); } std::string DebugString() const override { @@ -1541,52 +1113,75 @@ class MakeChainInactiveOperator : public PathOperator { int64_t GetBaseNodeRestartPosition(int base_index) override { // Base node 1 must be after base node 0. - return (base_index == 0) ? StartNode(base_index) : BaseNode(base_index - 1); + return (base_index == 0) ? this->StartNode(base_index) + : this->BaseNode(base_index - 1); } }; -// ----- SwapActiveOperator ----- +LocalSearchOperator* MakeChainInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakeChainInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new MakeChainInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); +} -// SwapActiveOperator replaces an active node by an inactive one. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 and -// 4 are first and last nodes of the path) are: -// 1 -> 5 -> 3 -> 4 & 2 inactive -// 1 -> 2 -> 5 -> 4 & 3 inactive +// ----- SwapActiveOperator ----- -class SwapActiveOperator : public BaseInactiveNodeToPathOperator { +template +class SwapActiveOperator + : public BaseInactiveNodeToPathOperator { public: SwapActiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : BaseInactiveNodeToPathOperator(vars, secondary_vars, 1, - std::move(start_empty_path_class)) {} - ~SwapActiveOperator() override {} - bool MakeNeighbor() override; + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 1, std::move(start_empty_path_class)) {} + ~SwapActiveOperator() override = default; + bool MakeNeighbor() override { + const int64_t base = this->BaseNode(0); + return this->MakeChainInactive(base, this->Next(base)) && + this->MakeActive(this->GetInactiveNode(), base); + } std::string DebugString() const override { return "SwapActiveOperator"; } }; -bool SwapActiveOperator::MakeNeighbor() { - const int64_t base = BaseNode(0); - return MakeChainInactive(base, Next(base)) && - MakeActive(GetInactiveNode(), base); +LocalSearchOperator* MakeSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new SwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); + } + return solver->RevAlloc(new SwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); } -class SwapActiveChainOperator : public BaseInactiveNodeToPathOperator { +// ----- SwapActiveChainOperator ----- + +template +class SwapActiveChainOperator + : public BaseInactiveNodeToPathOperator { public: SwapActiveChainOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, int max_chain_size) - : BaseInactiveNodeToPathOperator(vars, secondary_vars, 2, - std::move(start_empty_path_class)), + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 2, std::move(start_empty_path_class)), last_before_chain_(-1), last_chain_end_(-1), current_chain_size_(0), max_chain_size_(max_chain_size) { DCHECK_GE(max_chain_size_, 1); } - ~SwapActiveChainOperator() override {} + ~SwapActiveChainOperator() override = default; bool MakeNeighbor() override; bool IsIncremental() const override { return true; } @@ -1622,16 +1217,17 @@ class SwapActiveChainOperator : public BaseInactiveNodeToPathOperator { const int max_chain_size_; }; -bool SwapActiveChainOperator::MakeNeighbor() { - const int64_t before_chain = BaseNode(0); - const int64_t chain_end = BaseNode(1); +template +bool SwapActiveChainOperator::MakeNeighbor() { + const int64_t before_chain = this->BaseNode(0); + const int64_t chain_end = this->BaseNode(1); if (last_before_chain_ != before_chain || last_chain_end_ == -1) { - RevertChanges(/*change_was_incremental=*/false); + this->RevertChanges(/*change_was_incremental=*/false); last_before_chain_ = before_chain; - last_chain_end_ = GetInactiveNode(); - if (!IsPathEnd(chain_end) && before_chain != chain_end && - MakeChainInactive(before_chain, chain_end) && - MakeActive(GetInactiveNode(), before_chain)) { + last_chain_end_ = this->GetInactiveNode(); + if (!this->IsPathEnd(chain_end) && before_chain != chain_end && + this->MakeChainInactive(before_chain, chain_end) && + this->MakeActive(this->GetInactiveNode(), before_chain)) { ++current_chain_size_; return true; } else { @@ -1642,12 +1238,12 @@ bool SwapActiveChainOperator::MakeNeighbor() { } if (current_chain_size_ >= max_chain_size_) { // Move to the next before_chain. - SetNextBaseToIncrement(0); + this->SetNextBaseToIncrement(0); current_chain_size_ = 0; return false; } - if (!IsPathEnd(last_chain_end_) && - MakeChainInactive(last_chain_end_, Next(last_chain_end_))) { + if (!this->IsPathEnd(last_chain_end_) && + this->MakeChainInactive(last_chain_end_, this->Next(last_chain_end_))) { ++current_chain_size_; return true; } @@ -1656,59 +1252,67 @@ bool SwapActiveChainOperator::MakeNeighbor() { return false; } +LocalSearchOperator* MakeSwapActiveChain( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, int max_chain_size) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new SwapActiveChainOperator( + vars, secondary_vars, std::move(start_empty_path_class), + max_chain_size)); + } + return solver->RevAlloc(new SwapActiveChainOperator( + vars, secondary_vars, std::move(start_empty_path_class), max_chain_size)); +} + // ----- ExtendedSwapActiveOperator ----- -// ExtendedSwapActiveOperator makes an inactive node active and an active one -// inactive. It is similar to SwapActiveOperator excepts that it tries to -// insert the inactive node in all possible positions instead of just the -// position of the node made inactive. -// Possible neighbors for the path 1 -> 2 -> 3 -> 4 with 5 inactive (where 1 and -// 4 are first and last nodes of the path) are: -// 1 -> 5 -> 3 -> 4 & 2 inactive -// 1 -> 3 -> 5 -> 4 & 2 inactive -// 1 -> 5 -> 2 -> 4 & 3 inactive -// 1 -> 2 -> 5 -> 4 & 3 inactive - -class ExtendedSwapActiveOperator : public BaseInactiveNodeToPathOperator { +template +class ExtendedSwapActiveOperator + : public BaseInactiveNodeToPathOperator { public: ExtendedSwapActiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class) - : BaseInactiveNodeToPathOperator(vars, secondary_vars, 2, - std::move(start_empty_path_class)) {} - ~ExtendedSwapActiveOperator() override {} - bool MakeNeighbor() override; + : BaseInactiveNodeToPathOperator( + vars, secondary_vars, 2, std::move(start_empty_path_class)) {} + ~ExtendedSwapActiveOperator() override = default; + bool MakeNeighbor() override { + const int64_t base0 = this->BaseNode(0); + const int64_t base1 = this->BaseNode(1); + if (this->Next(base0) == base1) { + return false; + } + return this->MakeChainInactive(base0, this->Next(base0)) && + this->MakeActive(this->GetInactiveNode(), base1); + } std::string DebugString() const override { return "ExtendedSwapActiveOperator"; } }; -bool ExtendedSwapActiveOperator::MakeNeighbor() { - const int64_t base0 = BaseNode(0); - const int64_t base1 = BaseNode(1); - if (Next(base0) == base1) { - return false; +LocalSearchOperator* MakeExtendedSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new ExtendedSwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); } - return MakeChainInactive(base0, Next(base0)) && - MakeActive(GetInactiveNode(), base1); + return solver->RevAlloc(new ExtendedSwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class))); } // ----- TSP-based operators ----- -// Sliding TSP operator -// Uses an exact dynamic programming algorithm to solve the TSP corresponding -// to path sub-chains. -// For a subchain 1 -> 2 -> 3 -> 4 -> 5 -> 6, solves the TSP on nodes A, 2, 3, -// 4, 5, where A is a merger of nodes 1 and 6 such that cost(A,i) = cost(1,i) -// and cost(i,A) = cost(i,6). - -class TSPOpt : public PathOperator { +template +class TSPOpt : public PathOperator { public: TSPOpt(const std::vector& vars, const std::vector& secondary_vars, Solver::IndexEvaluator3 evaluator, int chain_length); - ~TSPOpt() override {} + ~TSPOpt() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "TSPOpt"; } @@ -1721,29 +1325,32 @@ class TSPOpt : public PathOperator { const int chain_length_; }; -TSPOpt::TSPOpt(const std::vector& vars, - const std::vector& secondary_vars, - Solver::IndexEvaluator3 evaluator, int chain_length) - : PathOperator(vars, secondary_vars, 1, true, false, nullptr, nullptr, - nullptr), +template +TSPOpt::TSPOpt(const std::vector& vars, + const std::vector& secondary_vars, + Solver::IndexEvaluator3 evaluator, + int chain_length) + : PathOperator(vars, secondary_vars, 1, true, false, + nullptr, nullptr, nullptr), hamiltonian_path_solver_(cost_), evaluator_(std::move(evaluator)), chain_length_(chain_length) {} -bool TSPOpt::MakeNeighbor() { +template +bool TSPOpt::MakeNeighbor() { std::vector nodes; - int64_t chain_end = BaseNode(0); + int64_t chain_end = this->BaseNode(0); for (int i = 0; i < chain_length_ + 1; ++i) { nodes.push_back(chain_end); - if (IsPathEnd(chain_end)) { + if (this->IsPathEnd(chain_end)) { break; } - chain_end = Next(chain_end); + chain_end = this->Next(chain_end); } if (nodes.size() <= 3) { return false; } - int64_t chain_path = Path(BaseNode(0)); + int64_t chain_path = this->Path(this->BaseNode(0)); const int size = nodes.size() - 1; cost_.resize(size); for (int i = 0; i < size; ++i) { @@ -1758,26 +1365,32 @@ bool TSPOpt::MakeNeighbor() { hamiltonian_path_solver_.TravelingSalesmanPath(&path); CHECK_EQ(size + 1, path.size()); for (int i = 0; i < size - 1; ++i) { - SetNext(nodes[path[i]], nodes[path[i + 1]], chain_path); + this->SetNext(nodes[path[i]], nodes[path[i + 1]], chain_path); } - SetNext(nodes[path[size - 1]], nodes[size], chain_path); + this->SetNext(nodes[path[size - 1]], nodes[size], chain_path); return true; } -// TSP-base lns -// Randomly merge consecutive nodes until n "meta"-nodes remain and solve the -// corresponding TSP. This can be seen as a large neighborhood search operator -// although decisions are taken with the operator. -// This is an "unlimited" neighborhood which must be stopped by search limits. -// To force diversification, the operator iteratively forces each node to serve -// as base of a meta-node. +LocalSearchOperator* MakeTSPOpt(Solver* solver, + const std::vector& vars, + const std::vector& secondary_vars, + Solver::IndexEvaluator3 evaluator, + int chain_length) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new TSPOpt( + vars, secondary_vars, std::move(evaluator), chain_length)); + } + return solver->RevAlloc(new TSPOpt( + vars, secondary_vars, std::move(evaluator), chain_length)); +} -class TSPLns : public PathOperator { +template +class TSPLns : public PathOperator { public: TSPLns(const std::vector& vars, const std::vector& secondary_vars, Solver::IndexEvaluator3 evaluator, int tsp_size); - ~TSPLns() override {} + ~TSPLns() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "TSPLns"; } @@ -1788,7 +1401,7 @@ class TSPLns : public PathOperator { private: void OnNodeInitialization() override { // NOTE: Avoid any computations if there are no vars added. - has_long_enough_paths_ = Size() != 0; + has_long_enough_paths_ = this->Size() != 0; } std::vector> cost_; @@ -1800,11 +1413,13 @@ class TSPLns : public PathOperator { bool has_long_enough_paths_; }; -TSPLns::TSPLns(const std::vector& vars, - const std::vector& secondary_vars, - Solver::IndexEvaluator3 evaluator, int tsp_size) - : PathOperator(vars, secondary_vars, 1, true, false, nullptr, nullptr, - nullptr), +template +TSPLns::TSPLns(const std::vector& vars, + const std::vector& secondary_vars, + Solver::IndexEvaluator3 evaluator, + int tsp_size) + : PathOperator(vars, secondary_vars, 1, true, false, + nullptr, nullptr, nullptr), hamiltonian_path_solver_(cost_), evaluator_(std::move(evaluator)), tsp_size_(tsp_size), @@ -1817,21 +1432,24 @@ TSPLns::TSPLns(const std::vector& vars, } } -bool TSPLns::MakeOneNeighbor() { +template +bool TSPLns::MakeOneNeighbor() { while (has_long_enough_paths_) { has_long_enough_paths_ = false; - if (PathOperator::MakeOneNeighbor()) { + if (PathOperator::MakeOneNeighbor()) { return true; } - Var(0)->solver()->TopPeriodicCheck(); + this->Var(0)->solver()->TopPeriodicCheck(); } return false; } -bool TSPLns::MakeNeighbor() { - const int64_t base_node = BaseNode(0); +template +bool TSPLns::MakeNeighbor() { + const int64_t base_node = this->BaseNode(0); std::vector nodes; - for (int64_t node = StartNode(0); !IsPathEnd(node); node = Next(node)) { + for (int64_t node = this->StartNode(0); !this->IsPathEnd(node); + node = this->Next(node)) { nodes.push_back(node); } if (nodes.size() <= tsp_size_) { @@ -1855,10 +1473,10 @@ bool TSPLns::MakeNeighbor() { std::vector breaks; std::vector meta_node_costs; int64_t cost = 0; - int64_t node = StartNode(0); - int64_t node_path = Path(node); - while (!IsPathEnd(node)) { - int64_t next = Next(node); + int64_t node = this->StartNode(0); + int64_t node_path = this->Path(node); + while (!this->IsPathEnd(node)) { + int64_t next = this->Next(node); if (breaks_set.contains(node)) { breaks.push_back(node); meta_node_costs.push_back(cost); @@ -1873,13 +1491,13 @@ bool TSPLns::MakeNeighbor() { // Setup TSP cost matrix CHECK_EQ(meta_node_costs.size(), tsp_size_); for (int i = 0; i < tsp_size_; ++i) { - cost_[i][0] = - CapAdd(meta_node_costs[i], - evaluator_(breaks[i], Next(breaks[tsp_size_ - 1]), node_path)); + cost_[i][0] = CapAdd( + meta_node_costs[i], + evaluator_(breaks[i], this->Next(breaks[tsp_size_ - 1]), node_path)); for (int j = 1; j < tsp_size_; ++j) { cost_[i][j] = CapAdd(meta_node_costs[i], - evaluator_(breaks[i], Next(breaks[j - 1]), node_path)); + evaluator_(breaks[i], this->Next(breaks[j - 1]), node_path)); } cost_[i][i] = 0; } @@ -1899,14 +1517,28 @@ bool TSPLns::MakeNeighbor() { } CHECK_EQ(0, path[path.size() - 1]); for (int i = 0; i < tsp_size_ - 1; ++i) { - SetNext(breaks[path[i]], OldNext(breaks[path[i + 1] - 1]), node_path); + this->SetNext(breaks[path[i]], this->OldNext(breaks[path[i + 1] - 1]), + node_path); } - SetNext(breaks[path[tsp_size_ - 1]], OldNext(breaks[tsp_size_ - 1]), - node_path); + this->SetNext(breaks[path[tsp_size_ - 1]], + this->OldNext(breaks[tsp_size_ - 1]), node_path); return true; } -// ----- Lin Kernighan ----- +LocalSearchOperator* MakeTSPLns(Solver* solver, + const std::vector& vars, + const std::vector& secondary_vars, + Solver::IndexEvaluator3 evaluator, + int tsp_size) { + if (secondary_vars.empty()) { + return solver->RevAlloc( + new TSPLns(vars, secondary_vars, std::move(evaluator), tsp_size)); + } + return solver->RevAlloc( + new TSPLns(vars, secondary_vars, std::move(evaluator), tsp_size)); +} + +// ----- Lin-Kernighan ----- // For each variable in vars, stores the 'size' pairs(i,j) with the smallest // value according to evaluator, where i is the index of the variable in vars @@ -1914,17 +1546,19 @@ bool TSPLns::MakeNeighbor() { // Note that the resulting pairs are sorted. // Works in O(size) per variable on average (same approach as qsort) +template class NearestNeighbors { public: NearestNeighbors(Solver::IndexEvaluator3 evaluator, - const PathOperator& path_operator, int size); + const PathOperator& path_operator, + int size); // This type is neither copyable nor movable. NearestNeighbors(const NearestNeighbors&) = delete; NearestNeighbors& operator=(const NearestNeighbors&) = delete; - virtual ~NearestNeighbors() {} - void Initialize(const std::vector& path); + virtual ~NearestNeighbors() = default; + void Initialize(absl::Span path); const std::vector& Neighbors(int index) const; virtual std::string DebugString() const { return "NearestNeighbors"; } @@ -1934,29 +1568,36 @@ class NearestNeighbors { std::vector> neighbors_; Solver::IndexEvaluator3 evaluator_; - const PathOperator& path_operator_; + const PathOperator& path_operator_; const int size_; }; -NearestNeighbors::NearestNeighbors(Solver::IndexEvaluator3 evaluator, - const PathOperator& path_operator, int size) +template +NearestNeighbors::NearestNeighbors( + Solver::IndexEvaluator3 evaluator, + const PathOperator& path_operator, int size) : neighbors_(path_operator.number_of_nexts()), evaluator_(std::move(evaluator)), path_operator_(path_operator), size_(size) {} -void NearestNeighbors::Initialize(const std::vector& path) { +template +void NearestNeighbors::Initialize( + absl::Span path) { for (int node : path) { if (node < path_operator_.number_of_nexts()) ComputeNearest(node, path); } } -const std::vector& NearestNeighbors::Neighbors(int index) const { +template +const std::vector& NearestNeighbors::Neighbors( + int index) const { return neighbors_[index]; } -void NearestNeighbors::ComputeNearest(int row, - absl::Span path_nodes) { +template +void NearestNeighbors::ComputeNearest( + int row, absl::Span path_nodes) { // Find size_ nearest neighbors for row of index 'row'. const int path = path_operator_.Path(row); const IntVar* var = path_operator_.Var(row); @@ -1980,12 +1621,13 @@ void NearestNeighbors::ComputeNearest(int row, std::sort(neighbors_[row].begin(), neighbors_[row].end()); } -class LinKernighan : public PathOperator { +template +class LinKernighan : public PathOperator { public: LinKernighan(const std::vector& vars, const std::vector& secondary_vars, const Solver::IndexEvaluator3& evaluator, bool topt); - ~LinKernighan() override; + ~LinKernighan() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "LinKernighan"; } @@ -1993,12 +1635,10 @@ class LinKernighan : public PathOperator { private: void OnNodeInitialization() override; - static const int kNeighbors; - bool GetBestOut(int64_t in_i, int64_t in_j, int64_t* out, int64_t* gain); Solver::IndexEvaluator3 const evaluator_; - NearestNeighbors neighbors_; + NearestNeighbors neighbors_; absl::flat_hash_set marked_; const bool topt_; std::vector old_path_starts_; @@ -2008,31 +1648,32 @@ class LinKernighan : public PathOperator { // followed by a series of 2opt moves. Return a neighbor for which the global // gain is positive. -LinKernighan::LinKernighan(const std::vector& vars, - const std::vector& secondary_vars, - const Solver::IndexEvaluator3& evaluator, bool topt) - : PathOperator(vars, secondary_vars, 1, true, false, nullptr, nullptr, - nullptr), +template +LinKernighan::LinKernighan( + const std::vector& vars, + const std::vector& secondary_vars, + const Solver::IndexEvaluator3& evaluator, bool topt) + : PathOperator(vars, secondary_vars, 1, true, false, + nullptr, nullptr, nullptr), evaluator_(evaluator), - neighbors_(evaluator, *this, kNeighbors), + neighbors_(evaluator, *this, /*size=*/5 + 1), topt_(topt) { - old_path_starts_.resize(number_of_nexts(), -1); + old_path_starts_.resize(this->number_of_nexts(), -1); } -LinKernighan::~LinKernighan() {} - -void LinKernighan::OnNodeInitialization() { +template +void LinKernighan::OnNodeInitialization() { absl::flat_hash_set touched_paths; - for (int i = 0; i < number_of_nexts(); ++i) { - if (IsPathStart(i)) { - for (int node = i; !IsPathEnd(node); node = Next(node)) { + for (int i = 0; i < this->number_of_nexts(); ++i) { + if (this->IsPathStart(i)) { + for (int node = i; !this->IsPathEnd(node); node = this->Next(node)) { if (i != old_path_starts_[node]) { touched_paths.insert(old_path_starts_[node]); touched_paths.insert(i); old_path_starts_[node] = i; } } - } else if (Next(i) == i) { + } else if (this->Next(i) == i) { touched_paths.insert(old_path_starts_[i]); old_path_starts_[i] = -1; } @@ -2041,22 +1682,23 @@ void LinKernighan::OnNodeInitialization() { if (touched_path_start == -1) continue; std::vector path; int node = touched_path_start; - while (!IsPathEnd(node)) { + while (!this->IsPathEnd(node)) { path.push_back(node); - node = Next(node); + node = this->Next(node); } path.push_back(node); neighbors_.Initialize(path); } } -bool LinKernighan::MakeNeighbor() { +template +bool LinKernighan::MakeNeighbor() { marked_.clear(); - int64_t node = BaseNode(0); - int64_t path = Path(node); + int64_t node = this->BaseNode(0); + int64_t path = this->Path(node); int64_t base = node; - int64_t next = Next(node); - if (IsPathEnd(next)) return false; + int64_t next = this->Next(node); + if (this->IsPathEnd(next)) return false; int64_t out = -1; int64_t gain = 0; marked_.insert(node); @@ -2065,38 +1707,41 @@ bool LinKernighan::MakeNeighbor() { marked_.insert(next); marked_.insert(out); const int64_t node1 = out; - if (IsPathEnd(node1)) return false; - const int64_t next1 = Next(node1); - if (IsPathEnd(next1)) return false; + if (this->IsPathEnd(node1)) return false; + const int64_t next1 = this->Next(node1); + if (this->IsPathEnd(next1)) return false; if (!GetBestOut(node1, next1, &out, &gain)) return false; marked_.insert(next1); marked_.insert(out); - if (!CheckChainValidity(out, node1, node) || !MoveChain(out, node1, node)) { + if (!this->CheckChainValidity(out, node1, node) || + !this->MoveChain(out, node1, node)) { return false; } - const int64_t next_out = Next(out); + const int64_t next_out = this->Next(out); const int64_t in_cost = evaluator_(node, next_out, path); const int64_t out_cost = evaluator_(out, next_out, path); if (CapAdd(CapSub(gain, in_cost), out_cost) > 0) return true; node = out; - if (IsPathEnd(node)) return false; + if (this->IsPathEnd(node)) return false; next = next_out; - if (IsPathEnd(next)) return false; + if (this->IsPathEnd(next)) return false; } // Try 2opts while (GetBestOut(node, next, &out, &gain)) { marked_.insert(next); marked_.insert(out); int64_t chain_last; - if (Next(base) == out || (!IsPathEnd(out) && Next(out) == base)) { + if (this->Next(base) == out || + (!this->IsPathEnd(out) && this->Next(out) == base)) { return false; } - const bool success = ReverseChain(base, out, &chain_last) || - ReverseChain(out, base, &chain_last); + const bool success = this->ReverseChain(base, out, &chain_last) || + this->ReverseChain(out, base, &chain_last); if (!success) { #ifndef NDEBUG LOG(ERROR) << "ReverseChain failed: " << base << " " << out; - for (int node = StartNode(0); !IsPathEnd(node); node = Next(node)) { + for (int node = this->StartNode(0); !this->IsPathEnd(node); + node = this->Next(node)) { LOG(ERROR) << "node: " << node; } LOG(ERROR) << "node: " << node; @@ -2109,27 +1754,26 @@ bool LinKernighan::MakeNeighbor() { return true; } node = out; - if (IsPathEnd(node)) { + if (this->IsPathEnd(node)) { return false; } next = chain_last; - if (IsPathEnd(next)) { + if (this->IsPathEnd(next)) { return false; } } return false; } -const int LinKernighan::kNeighbors = 5 + 1; - -bool LinKernighan::GetBestOut(int64_t in_i, int64_t in_j, int64_t* out, - int64_t* gain) { +template +bool LinKernighan::GetBestOut(int64_t in_i, int64_t in_j, + int64_t* out, int64_t* gain) { int64_t best_gain = std::numeric_limits::min(); - const int64_t path = Path(in_i); + const int64_t path = this->Path(in_i); const int64_t out_cost = evaluator_(in_i, in_j, path); const int64_t current_gain = CapAdd(*gain, out_cost); for (int next : neighbors_.Neighbors(in_j)) { - if (next != in_j && next != Next(in_j) && !marked_.contains(in_j) && + if (next != in_j && next != this->Next(in_j) && !marked_.contains(in_j) && !marked_.contains(next)) { const int64_t in_cost = evaluator_(in_j, next, path); const int64_t new_gain = CapSub(current_gain, in_cost); @@ -2143,25 +1787,34 @@ bool LinKernighan::GetBestOut(int64_t in_i, int64_t in_j, int64_t* out, return (best_gain > std::numeric_limits::min()); } -// ----- Path-based Large Neighborhood Search ----- +LocalSearchOperator* MakeLinKernighan( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + const Solver::IndexEvaluator3& evaluator, bool topt) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new LinKernighan(vars, secondary_vars, + std::move(evaluator), topt)); + } + return solver->RevAlloc(new LinKernighan(vars, secondary_vars, + std::move(evaluator), topt)); +} -// Breaks "number_of_chunks" chains of "chunk_size" arcs, and deactivate all -// inactive nodes if "unactive_fragments" is true. -// As a special case, if chunk_size=0, then we break full paths. +// ----- Path-based Large Neighborhood Search ----- -class PathLns : public PathOperator { +template +class PathLns : public PathOperator { public: PathLns(const std::vector& vars, const std::vector& secondary_vars, int number_of_chunks, int chunk_size, bool unactive_fragments) - : PathOperator(vars, secondary_vars, number_of_chunks, true, true, - nullptr, nullptr, nullptr), + : PathOperator(vars, secondary_vars, number_of_chunks, + true, true, nullptr, nullptr, nullptr), number_of_chunks_(number_of_chunks), chunk_size_(chunk_size), unactive_fragments_(unactive_fragments) { CHECK_GE(chunk_size_, 0); } - ~PathLns() override {} + ~PathLns() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "PathLns"; } @@ -2177,46 +1830,63 @@ class PathLns : public PathOperator { const bool unactive_fragments_; }; -bool PathLns::MakeNeighbor() { +template +bool PathLns::MakeNeighbor() { if (ChainsAreFullPaths()) { // Reject the current position as a neighbor if any of its base node // isn't at the start of a path. // TODO(user): make this more efficient. for (int i = 0; i < number_of_chunks_; ++i) { - if (BaseNode(i) != StartNode(i)) return false; + if (this->BaseNode(i) != this->StartNode(i)) return false; } } for (int i = 0; i < number_of_chunks_; ++i) { - DeactivateChain(BaseNode(i)); + DeactivateChain(this->BaseNode(i)); } DeactivateUnactives(); return true; } -void PathLns::DeactivateChain(int64_t node) { +template +void PathLns::DeactivateChain(int64_t node) { for (int i = 0, current = node; - (ChainsAreFullPaths() || i < chunk_size_) && !IsPathEnd(current); - ++i, current = Next(current)) { - Deactivate(current); - if (!ignore_path_vars_) { - Deactivate(number_of_nexts_ + current); + (ChainsAreFullPaths() || i < chunk_size_) && !this->IsPathEnd(current); + ++i, current = this->Next(current)) { + this->Deactivate(current); + if constexpr (!ignore_path_vars) { + this->Deactivate(this->number_of_nexts_ + current); } } } -void PathLns::DeactivateUnactives() { +template +void PathLns::DeactivateUnactives() { if (unactive_fragments_) { - for (int i = 0; i < Size(); ++i) { - if (IsInactive(i)) { - Deactivate(i); - if (!ignore_path_vars_) { - Deactivate(number_of_nexts_ + i); + for (int i = 0; i < this->Size(); ++i) { + if (this->IsInactive(i)) { + this->Deactivate(i); + if constexpr (!ignore_path_vars) { + this->Deactivate(this->number_of_nexts_ + i); } } } } } +LocalSearchOperator* MakePathLns(Solver* solver, + const std::vector& vars, + const std::vector& secondary_vars, + int number_of_chunks, int chunk_size, + bool unactive_fragments) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new PathLns(vars, secondary_vars, + number_of_chunks, chunk_size, + unactive_fragments)); + } + return solver->RevAlloc(new PathLns( + vars, secondary_vars, number_of_chunks, chunk_size, unactive_fragments)); +} + // ----- Limit the number of neighborhoods explored ----- class NeighborhoodLimit : public LocalSearchOperator { @@ -2679,96 +2349,8 @@ LocalSearchOperator* Solver::MultiArmedBanditConcatenateOperators( ops, memory_coefficient, exploration_coefficient, maximize)); } -// ----- Operator factory ----- - -template -LocalSearchOperator* MakeLocalSearchOperator( - Solver* solver, const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class) { - return solver->RevAlloc( - new T(vars, secondary_vars, std::move(start_empty_path_class), nullptr)); -} - -template -LocalSearchOperator* MakeLocalSearchOperatorWithArg( - Solver* solver, const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, ArgType arg) { - return solver->RevAlloc(new T( - vars, secondary_vars, std::move(start_empty_path_class), std::move(arg))); -} - -template -LocalSearchOperator* MakeLocalSearchOperatorWithNeighbors( - Solver* solver, const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors) { - return solver->RevAlloc(new T( - vars, secondary_vars, std::move(start_empty_path_class), - std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors))); -} - -#define MAKE_LOCAL_SEARCH_OPERATOR(OperatorClass) \ - template <> \ - LocalSearchOperator* MakeLocalSearchOperator( \ - Solver * solver, const std::vector& vars, \ - const std::vector& secondary_vars, \ - std::function start_empty_path_class) { \ - return solver->RevAlloc(new OperatorClass( \ - vars, secondary_vars, std::move(start_empty_path_class))); \ - } - -#define MAKE_LOCAL_SEARCH_OPERATOR_WITH_ARG(OperatorClass, ArgType) \ - template <> \ - LocalSearchOperator* MakeLocalSearchOperatorWithArg( \ - Solver * solver, const std::vector& vars, \ - const std::vector& secondary_vars, \ - std::function start_empty_path_class, ArgType arg) { \ - return solver->RevAlloc( \ - new OperatorClass(vars, secondary_vars, \ - std::move(start_empty_path_class), std::move(arg))); \ - } - -#define MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(OperatorClass) \ - template <> \ - LocalSearchOperator* MakeLocalSearchOperatorWithNeighbors( \ - Solver * solver, const std::vector& vars, \ - const std::vector& secondary_vars, \ - std::function start_empty_path_class, \ - NeighborAccessor get_incoming_neighbors, \ - NeighborAccessor get_outgoing_neighbors) { \ - return solver->RevAlloc(new OperatorClass( \ - vars, secondary_vars, std::move(start_empty_path_class), \ - std::move(get_incoming_neighbors), \ - std::move(get_outgoing_neighbors))); \ - } - -MAKE_LOCAL_SEARCH_OPERATOR(TwoOpt) -MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(TwoOpt) -MAKE_LOCAL_SEARCH_OPERATOR(Relocate) -MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(Relocate) -MAKE_LOCAL_SEARCH_OPERATOR(Exchange) -MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(Exchange) -MAKE_LOCAL_SEARCH_OPERATOR(Cross) -MAKE_LOCAL_SEARCH_OPERATOR_WITH_NEIGHBORS(Cross) -MAKE_LOCAL_SEARCH_OPERATOR(MakeActiveOperator) -MAKE_LOCAL_SEARCH_OPERATOR(MakeInactiveOperator) -MAKE_LOCAL_SEARCH_OPERATOR(MakeChainInactiveOperator) -MAKE_LOCAL_SEARCH_OPERATOR(SwapActiveOperator) -MAKE_LOCAL_SEARCH_OPERATOR_WITH_ARG(SwapActiveChainOperator, int) -MAKE_LOCAL_SEARCH_OPERATOR(ExtendedSwapActiveOperator) -MAKE_LOCAL_SEARCH_OPERATOR(MakeActiveAndRelocate) -MAKE_LOCAL_SEARCH_OPERATOR(RelocateAndMakeActiveOperator) -MAKE_LOCAL_SEARCH_OPERATOR(RelocateAndMakeInactiveOperator) - -#undef MAKE_LOCAL_SEARCH_OPERATOR - // TODO(user): Remove (parts of) the following methods as they are mostly -// redundant with the MakeLocalSearchOperatorWithNeighbors and -// MakeLocalSearchOperator functions. +// redundant with individual operator builder functions. LocalSearchOperator* Solver::MakeOperator( const std::vector& vars, Solver::LocalSearchOperators op, NeighborAccessor get_incoming_neighbors, @@ -2785,73 +2367,70 @@ LocalSearchOperator* Solver::MakeOperator( NeighborAccessor get_outgoing_neighbors) { switch (op) { case Solver::TWOOPT: { - return MakeLocalSearchOperatorWithNeighbors( - this, vars, secondary_vars, nullptr, - std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)); + return MakeTwoOpt(this, vars, secondary_vars, nullptr, + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)); } case Solver::OROPT: { std::vector operators; for (int i = 1; i < 4; ++i) { - operators.push_back( - RevAlloc(new Relocate(vars, secondary_vars, - /*name=*/absl::StrCat("OrOpt<", i, ">"), - /*start_empty_path_class=*/nullptr, - /*get_incoming_neighbors=*/nullptr, - /*get_outgoing_neighbors=*/nullptr, - /*chain_length=*/i, /*single_path=*/true))); + operators.push_back(MakeRelocate(this, vars, secondary_vars, + /*start_empty_path_class=*/nullptr, + /*get_incoming_neighbors=*/nullptr, + /*get_outgoing_neighbors=*/nullptr, + /*chain_length=*/i, + /*single_path=*/true, + /*name=*/"OrOpt")); } return ConcatenateOperators(operators); } case Solver::RELOCATE: { - return MakeLocalSearchOperatorWithNeighbors( - this, vars, secondary_vars, nullptr, + return MakeRelocate( + this, vars, secondary_vars, /*start_empty_path_class=*/nullptr, std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)); } case Solver::EXCHANGE: { - return MakeLocalSearchOperatorWithNeighbors( - this, vars, secondary_vars, nullptr, - std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)); + return MakeExchange(this, vars, secondary_vars, nullptr, + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)); } case Solver::CROSS: { - return MakeLocalSearchOperatorWithNeighbors( - this, vars, secondary_vars, nullptr, - std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)); + return MakeCross(this, vars, secondary_vars, nullptr, + std::move(get_incoming_neighbors), + std::move(get_outgoing_neighbors)); } case Solver::MAKEACTIVE: { - return MakeLocalSearchOperator( - this, vars, secondary_vars, nullptr); + return MakeActive(this, vars, secondary_vars, nullptr); } case Solver::MAKEINACTIVE: { - return MakeLocalSearchOperator( - this, vars, secondary_vars, nullptr); + return MakeInactive(this, vars, secondary_vars, nullptr); } case Solver::MAKECHAININACTIVE: { - return MakeLocalSearchOperator( - this, vars, secondary_vars, nullptr); + return MakeChainInactive(this, vars, secondary_vars, nullptr); } case Solver::SWAPACTIVE: { - return MakeLocalSearchOperator( - this, vars, secondary_vars, nullptr); + return MakeSwapActive(this, vars, secondary_vars, nullptr); } case Solver::SWAPACTIVECHAIN: { - return MakeLocalSearchOperatorWithArg( - this, vars, secondary_vars, nullptr, kint32max); + return MakeSwapActiveChain(this, vars, secondary_vars, nullptr, + kint32max); } case Solver::EXTENDEDSWAPACTIVE: { - return MakeLocalSearchOperator( - this, vars, secondary_vars, nullptr); + return MakeExtendedSwapActive(this, vars, secondary_vars, nullptr); } case Solver::PATHLNS: { - return RevAlloc(new PathLns(vars, secondary_vars, 2, 3, false)); + return MakePathLns(this, vars, secondary_vars, /*number_of_chunks=*/2, + /*chunk_size=*/3, /*unactive_fragments=*/false); } case Solver::FULLPATHLNS: { - return RevAlloc(new PathLns(vars, secondary_vars, - /*number_of_chunks=*/1, - /*chunk_size=*/0, - /*unactive_fragments=*/true)); + return MakePathLns(this, vars, secondary_vars, + /*number_of_chunks=*/1, + /*chunk_size=*/0, + /*unactive_fragments=*/true); } case Solver::UNACTIVELNS: { - return RevAlloc(new PathLns(vars, secondary_vars, 1, 6, true)); + return MakePathLns(this, vars, secondary_vars, /*number_of_chunks=*/1, + /*chunk_size=*/6, /*unactive_fragments=*/true); } case Solver::INCREMENT: { if (!secondary_vars.empty()) { @@ -2894,21 +2473,19 @@ LocalSearchOperator* Solver::MakeOperator( switch (op) { case Solver::LK: { std::vector operators; - operators.push_back(RevAlloc( - new LinKernighan(vars, secondary_vars, evaluator, /*topt=*/false))); - operators.push_back(RevAlloc( - new LinKernighan(vars, secondary_vars, evaluator, /*topt=*/true))); + operators.push_back(MakeLinKernighan(this, vars, secondary_vars, + evaluator, /*topt=*/false)); + operators.push_back(MakeLinKernighan(this, vars, secondary_vars, + evaluator, /*topt=*/true)); return ConcatenateOperators(operators); } case Solver::TSPOPT: { - return RevAlloc( - new TSPOpt(vars, secondary_vars, std::move(evaluator), - absl::GetFlag(FLAGS_cp_local_search_tsp_opt_size))); + return MakeTSPOpt(this, vars, secondary_vars, std::move(evaluator), + absl::GetFlag(FLAGS_cp_local_search_tsp_opt_size)); } case Solver::TSPLNS: { - return RevAlloc( - new TSPLns(vars, secondary_vars, std::move(evaluator), - absl::GetFlag(FLAGS_cp_local_search_tsp_lns_size))); + return MakeTSPLns(this, vars, secondary_vars, std::move(evaluator), + absl::GetFlag(FLAGS_cp_local_search_tsp_lns_size)); } default: LOG(FATAL) << "Unknown operator " << op; diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index 804c7ffa33a..ebacbcc1280 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -17,6 +17,7 @@ #include #include +#include #include #include #include @@ -95,22 +96,6 @@ using Graph = ::util::ListGraph; using CostValue = int64_t; } // namespace -namespace operations_research { -class Cross; -class Exchange; -class ExtendedSwapActiveOperator; -class LocalSearchPhaseParameters; -class MakeActiveAndRelocate; -class MakeActiveOperator; -class MakeChainInactiveOperator; -class MakeInactiveOperator; -class Relocate; -class RelocateAndMakeActiveOperator; -class SwapActiveOperator; -class SwapActiveChainOperator; -class TwoOpt; -} // namespace operations_research - // Trace settings namespace operations_research { @@ -2931,21 +2916,31 @@ void RoutingModel::CloseModelWithParameters( } // Dimension node precedences. - for (const RoutingDimension::NodePrecedence& node_precedence : + for (const auto [first_node, second_node, offset, performed_constraint] : dimension->GetNodePrecedences()) { - const int64_t first_node = node_precedence.first_node; - const int64_t second_node = node_precedence.second_node; IntExpr* const nodes_are_selected = solver_->MakeMin(active_[first_node], active_[second_node]); IntExpr* const cumul_difference = solver_->MakeDifference( dimension->CumulVar(second_node), dimension->CumulVar(first_node)); IntVar* const cumul_difference_is_ge_offset = - solver_->MakeIsGreaterOrEqualCstVar(cumul_difference, - node_precedence.offset); + solver_->MakeIsGreaterOrEqualCstVar(cumul_difference, offset); // Forces the implication: both nodes are active => cumul difference // constraint is active. solver_->AddConstraint(solver_->MakeLessOrEqual( nodes_are_selected->Var(), cumul_difference_is_ge_offset)); + using PerformedConstraint = + RoutingDimension::NodePrecedence::PerformedConstraint; + switch (performed_constraint) { + case PerformedConstraint::kFirstAndSecondIndependent: + break; + case PerformedConstraint::kSecondImpliesFirst: + solver_->AddConstraint(solver_->MakeGreaterOrEqual( + active_[first_node], active_[second_node])); + break; + case PerformedConstraint::kFirstAndSecondEqual: + solver_->AddConstraint( + solver_->MakeEquality(active_[first_node], active_[second_node])); + } } } @@ -3311,6 +3306,11 @@ const Assignment* RoutingModel::SolveFromAssignmentsWithParameters( break; } } + if (collect_assignments_->solution_count() == 0 && update_time_limits() && + hint_ != nullptr) { + solver_->Solve(solve_db_, monitors_); + run_secondary_ls(); + } } } @@ -4663,15 +4663,22 @@ RoutingModel::GetOrCreateFirstSolutionLargeNeighborhoodSearchLimit() { } LocalSearchOperator* RoutingModel::CreateInsertionOperator() { - LocalSearchOperator* insertion_operator = - CreateCPOperator(); + auto get_vehicle_vars = [this]() { + return CostsAreHomogeneousAcrossVehicles() ? std::vector() + : vehicle_vars_; + }; + LocalSearchOperator* insertion_operator = MakeActive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); if (!pickup_delivery_pairs_.empty()) { insertion_operator = solver_->ConcatenateOperators( - {CreatePairOperator(), insertion_operator}); + {MakePairActive(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, pickup_delivery_pairs_), + insertion_operator}); } if (!implicit_pickup_delivery_pairs_without_alternatives_.empty()) { insertion_operator = solver_->ConcatenateOperators( - {CreateOperator( + {MakePairActive(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, implicit_pickup_delivery_pairs_without_alternatives_), insertion_operator}); } @@ -4679,11 +4686,17 @@ LocalSearchOperator* RoutingModel::CreateInsertionOperator() { } LocalSearchOperator* RoutingModel::CreateMakeInactiveOperator() { - LocalSearchOperator* make_inactive_operator = - CreateCPOperator(); + auto get_vehicle_vars = [this]() { + return CostsAreHomogeneousAcrossVehicles() ? std::vector() + : vehicle_vars_; + }; + LocalSearchOperator* make_inactive_operator = MakeInactive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); if (!pickup_delivery_pairs_.empty()) { make_inactive_operator = solver_->ConcatenateOperators( - {CreatePairOperator(), + {MakePairInactive(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, + pickup_delivery_pairs_), make_inactive_operator}); } return make_inactive_operator; @@ -4704,20 +4717,24 @@ void RoutingModel::CreateNeighborhoodOperators( parameters.ls_operator_min_neighbors(), neighbors_ratio_used, /*add_vehicle_starts_to_neighbors=*/false, /*add_vehicle_ends_to_neighbors=*/false); - const auto get_incoming_neighbors = - [neighbors_by_cost_class, this]( - int64_t node, int64_t start) -> const std::vector& { + std::function&(int, int)> get_incoming_neighbors; + std::function&(int, int)> get_outgoing_neighbors; + if (neighbors_ratio_used != 1) { + get_incoming_neighbors = [neighbors_by_cost_class, this]( + int64_t node, + int64_t start) -> const std::vector& { DCHECK(!IsStart(node)); return neighbors_by_cost_class->GetIncomingNeighborsOfNodeForCostClass( GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); }; - const auto get_outgoing_neighbors = - [neighbors_by_cost_class, this]( - int64_t node, int64_t start) -> const std::vector& { + get_outgoing_neighbors = [neighbors_by_cost_class, this]( + int64_t node, + int64_t start) -> const std::vector& { DCHECK(!IsEnd(node)); return neighbors_by_cost_class->GetOutgoingNeighborsOfNodeForCostClass( GetCostClassIndexOfVehicle(VehicleIndex(start)).value(), node); }; + } local_search_operators_.clear(); local_search_operators_.resize(LOCAL_SEARCH_OPERATOR_COUNTER, nullptr); @@ -4753,29 +4770,43 @@ void RoutingModel::CreateNeighborhoodOperators( } } + auto get_vehicle_vars = [this]() { + return CostsAreHomogeneousAcrossVehicles() ? std::vector() + : vehicle_vars_; + }; + // Other operators defined in the CP solver. - local_search_operators_[RELOCATE] = - CreateOperatorWithNeighborsRatio( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors); - local_search_operators_[EXCHANGE] = - CreateOperatorWithNeighborsRatio( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors); - local_search_operators_[CROSS] = CreateOperatorWithNeighborsRatio( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors); - local_search_operators_[TWO_OPT] = CreateOperatorWithNeighborsRatio( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors); - local_search_operators_[RELOCATE_AND_MAKE_ACTIVE] = - CreateCPOperator(); - local_search_operators_[MAKE_ACTIVE_AND_RELOCATE] = - CreateCPOperator(); - local_search_operators_[MAKE_CHAIN_INACTIVE] = - CreateCPOperator(); - local_search_operators_[SWAP_ACTIVE] = CreateCPOperator(); - local_search_operators_[SWAP_ACTIVE_CHAIN] = - CreateCPOperatorWithArg( + local_search_operators_[RELOCATE] = MakeRelocate( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors); + local_search_operators_[EXCHANGE] = MakeExchange( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors); + local_search_operators_[CROSS] = MakeCross( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors); + local_search_operators_[TWO_OPT] = MakeTwoOpt( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors); + local_search_operators_[RELOCATE_AND_MAKE_ACTIVE] = RelocateAndMakeActive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); + local_search_operators_[MAKE_ACTIVE_AND_RELOCATE] = MakeActiveAndRelocate( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); + local_search_operators_[EXCHANGE_AND_MAKE_ACTIVE] = ExchangeAndMakeActive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); + local_search_operators_[EXCHANGE_PATH_START_ENDS_AND_MAKE_ACTIVE] = + ExchangePathStartEndsAndMakeActive(solver_.get(), nexts_, + get_vehicle_vars(), + vehicle_start_class_callback_); + local_search_operators_[MAKE_CHAIN_INACTIVE] = MakeChainInactive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); + local_search_operators_[SWAP_ACTIVE] = MakeSwapActive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); + local_search_operators_[SWAP_ACTIVE_CHAIN] = MakeSwapActiveChain( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, parameters.max_swap_active_chain_size()); - local_search_operators_[EXTENDED_SWAP_ACTIVE] = - CreateCPOperator(); + local_search_operators_[EXTENDED_SWAP_ACTIVE] = MakeExtendedSwapActive( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_); std::vector> alternative_sets(disjunctions_.size()); for (const RoutingModel::Disjunction& disjunction : disjunctions_) { // Only add disjunctions of cardinality 1 and of size > 1, as @@ -4787,59 +4818,63 @@ void RoutingModel::CreateNeighborhoodOperators( } } local_search_operators_[SHORTEST_PATH_SWAP_ACTIVE] = - CreateOperator( - alternative_sets, + MakeSwapActiveToShortestPath( + solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, alternative_sets, GetLocalSearchHomogeneousArcCostCallback(parameters)); // TODO(user): Consider having only one variant of 2Opt active. - local_search_operators_[SHORTEST_PATH_TWO_OPT] = - CreateOperator( - alternative_sets, - GetLocalSearchHomogeneousArcCostCallback(parameters)); + local_search_operators_[SHORTEST_PATH_TWO_OPT] = MakeTwoOptWithShortestPath( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + alternative_sets, GetLocalSearchHomogeneousArcCostCallback(parameters)); // Routing-specific operators. local_search_operators_[MAKE_ACTIVE] = CreateInsertionOperator(); local_search_operators_[MAKE_INACTIVE] = CreateMakeInactiveOperator(); local_search_operators_[RELOCATE_PAIR] = - CreatePairOperator(); + MakePairRelocate(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, pickup_delivery_pairs_); std::vector light_relocate_pair_operators; - light_relocate_pair_operators.push_back( - CreateOperatorWithNeighborsRatio( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors, - pickup_delivery_pairs_, [this](int64_t start) { + light_relocate_pair_operators.push_back(MakeLightPairRelocate( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_, + [this](int64_t start) { return vehicle_pickup_delivery_policy_[VehicleIndex(start)] == RoutingModel::PICKUP_AND_DELIVERY_LIFO; })); - light_relocate_pair_operators.push_back( - CreatePairOperator(neighbors_ratio_used, - get_incoming_neighbors, - get_outgoing_neighbors)); + light_relocate_pair_operators.push_back(MakeGroupPairAndRelocate( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_)); local_search_operators_[LIGHT_RELOCATE_PAIR] = solver_->ConcatenateOperators(light_relocate_pair_operators); local_search_operators_[EXCHANGE_PAIR] = solver_->ConcatenateOperators( - {CreatePairOperator(neighbors_ratio_used, - get_incoming_neighbors, - get_outgoing_neighbors), - solver_->RevAlloc(new SwapIndexPairOperator( - nexts_, - CostsAreHomogeneousAcrossVehicles() ? std::vector() - : vehicle_vars_, + {MakePairExchange(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, get_incoming_neighbors, + get_outgoing_neighbors, pickup_delivery_pairs_), + solver_->RevAlloc(new SwapIndexPairOperator(nexts_, get_vehicle_vars(), pickup_delivery_pairs_))}); - local_search_operators_[EXCHANGE_RELOCATE_PAIR] = - CreatePairOperator(); - local_search_operators_[RELOCATE_NEIGHBORS] = - CreateOperatorWithNeighborsRatio( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors, + local_search_operators_[EXCHANGE_RELOCATE_PAIR] = MakePairExchangeRelocate( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + pickup_delivery_pairs_); + local_search_operators_[RELOCATE_NEIGHBORS] = MakeRelocateNeighbors( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors, GetLocalSearchHomogeneousArcCostCallback(parameters)); local_search_operators_[NODE_PAIR_SWAP] = solver_->ConcatenateOperators( - {CreatePairOperator(), - CreatePairOperator>(), - CreatePairOperator>()}); - local_search_operators_[RELOCATE_SUBTRIP] = - CreatePairOperator( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors); - local_search_operators_[EXCHANGE_SUBTRIP] = - CreatePairOperator( - neighbors_ratio_used, get_incoming_neighbors, get_outgoing_neighbors); + {MakeIndexPairSwapActive(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, + pickup_delivery_pairs_), + MakePairNodeSwapActive(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, + pickup_delivery_pairs_), + MakePairNodeSwapActive(solver_.get(), nexts_, get_vehicle_vars(), + vehicle_start_class_callback_, + pickup_delivery_pairs_)}); + local_search_operators_[RELOCATE_SUBTRIP] = MakeRelocateSubtrip( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_); + local_search_operators_[EXCHANGE_SUBTRIP] = MakeExchangeSubtrip( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, + get_incoming_neighbors, get_outgoing_neighbors, pickup_delivery_pairs_); const auto arc_cost_for_path_start = [this, arc_cost_getter = GetLocalSearchArcCostCallback(parameters)]( @@ -4852,13 +4887,11 @@ void RoutingModel::CreateNeighborhoodOperators( : CapSub(arc_cost, GetFixedCostOfVehicle(vehicle)); }; local_search_operators_[RELOCATE_EXPENSIVE_CHAIN] = - solver_->RevAlloc(new RelocateExpensiveChain( - nexts_, - CostsAreHomogeneousAcrossVehicles() ? std::vector() - : vehicle_vars_, + MakeRelocateExpensiveChain( + solver_.get(), nexts_, get_vehicle_vars(), vehicle_start_class_callback_, parameters.relocate_expensive_chain_num_arcs_to_consider(), - arc_cost_for_path_start)); + arc_cost_for_path_start); // Insertion-based LNS neighborhoods. const auto make_global_cheapest_insertion_filtered_heuristic = @@ -4894,7 +4927,7 @@ void RoutingModel::CreateNeighborhoodOperators( GetOrCreateLocalSearchFilterManager( parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/false}), - bin_capacities_.get()); + /*use_first_solution_hint=*/false, bin_capacities_.get()); }; local_search_operators_[GLOBAL_CHEAPEST_INSERTION_CLOSE_NODES_LNS] = solver_->RevAlloc(new FilteredHeuristicCloseNodesLNSOperator( @@ -5019,6 +5052,11 @@ LocalSearchOperator* RoutingModel::GetNeighborhoodOperators( CP_ROUTING_PUSH_OPERATOR(MAKE_ACTIVE_AND_RELOCATE, relocate_and_make_active); + CP_ROUTING_PUSH_OPERATOR(EXCHANGE_AND_MAKE_ACTIVE, + exchange_and_make_active); + CP_ROUTING_PUSH_OPERATOR(EXCHANGE_PATH_START_ENDS_AND_MAKE_ACTIVE, + exchange_path_start_ends_and_make_active); + CP_ROUTING_PUSH_OPERATOR(SWAP_ACTIVE, swap_active); CP_ROUTING_PUSH_OPERATOR(SWAP_ACTIVE_CHAIN, swap_active_chain); CP_ROUTING_PUSH_OPERATOR(EXTENDED_SWAP_ACTIVE, extended_swap_active); @@ -5214,7 +5252,9 @@ RoutingModel::CreateLocalSearchFilters( // local search deltas and their domain will be checked by // VariableDomainFilter. if (CostsAreHomogeneousAcrossVehicles()) { - filter_events.push_back({MakeVehicleVarFilter(*this), kAccept, priority}); + filter_events.push_back( + {MakeVehicleVarFilter(*this, path_state_reference), kAccept, + priority}); } // Append filters, then overwrite preset priority to current priority. @@ -5233,10 +5273,11 @@ RoutingModel::CreateLocalSearchFilters( // called first. ++priority; if (!pickup_delivery_pairs_.empty()) { - filter_events.push_back( - {MakePickupDeliveryFilter(*this, pickup_delivery_pairs_, - vehicle_pickup_delivery_policy_), - kAccept, priority}); + LocalSearchFilter* filter = MakePickupDeliveryFilter( + *this, path_state_reference, pickup_delivery_pairs_, + vehicle_pickup_delivery_policy_); + filter_events.push_back({filter, kRelax, priority}); + filter_events.push_back({filter, kAccept, priority}); } if (options.filter_objective) { const int num_vehicles = vehicles(); @@ -5865,7 +5906,8 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/false}), - bin_capacities_.get(), optimize_on_insertion); + /*use_first_solution_hint=*/true, bin_capacities_.get(), + optimize_on_insertion); IntVarFilteredDecisionBuilder* const strong_lci = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( @@ -5878,7 +5920,8 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( GetOrCreateLocalSearchFilterManager(search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/true}), - bin_capacities_.get(), optimize_on_insertion); + /*use_first_solution_hint=*/true, bin_capacities_.get(), + optimize_on_insertion); first_solution_decision_builders_ [FirstSolutionStrategy::LOCAL_CHEAPEST_INSERTION] = solver_->Try( first_solution_filtered_decision_builders_ @@ -5902,7 +5945,8 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/false}), - bin_capacities_.get(), optimize_on_insertion); + /*use_first_solution_hint=*/true, bin_capacities_.get(), + optimize_on_insertion); IntVarFilteredDecisionBuilder* const strong_lcci = CreateIntVarFilteredDecisionBuilder< LocalCheapestInsertionFilteredHeuristic>( @@ -5912,7 +5956,8 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( GetOrCreateLocalSearchFilterManager(search_parameters, {/*filter_objective=*/true, /*filter_with_cp_solver=*/true}), - bin_capacities_.get(), optimize_on_insertion); + /*use_first_solution_hint=*/true, bin_capacities_.get(), + optimize_on_insertion); first_solution_decision_builders_ [FirstSolutionStrategy::LOCAL_CHEAPEST_COST_INSERTION] = solver_->Try( first_solution_filtered_decision_builders_ diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index 6ee0ce6690b..b79f0149aae 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -1320,6 +1320,13 @@ class OR_DLL RoutingModel { void SetFirstSolutionEvaluator(Solver::IndexEvaluator2 evaluator) { first_solution_evaluator_ = std::move(evaluator); } + /// Adds a hint to be used by first solution strategies. The hint assignment + /// must outlive the search. + /// As of 2024-12, only used by LOCAL_CHEAPEST_INSERTION and + /// LOCAL_CHEAPEST_COST_INSERTION. + void SetFirstSolutionHint(const Assignment* hint) { hint_ = hint; } + /// Returns the current hint assignment. + const Assignment* GetFirstSolutionHint() const { return hint_; } /// Adds a local search operator to the set of operators used to solve the /// vehicle routing problem. void AddLocalSearchOperator(LocalSearchOperator* ls_operator); @@ -2106,6 +2113,8 @@ class OR_DLL RoutingModel { MAKE_ACTIVE, RELOCATE_AND_MAKE_ACTIVE, MAKE_ACTIVE_AND_RELOCATE, + EXCHANGE_AND_MAKE_ACTIVE, + EXCHANGE_PATH_START_ENDS_AND_MAKE_ACTIVE, MAKE_INACTIVE, MAKE_CHAIN_INACTIVE, SWAP_ACTIVE, @@ -2349,132 +2358,6 @@ class OR_DLL RoutingModel { RegularLimit* GetOrCreateFirstSolutionLargeNeighborhoodSearchLimit(); LocalSearchOperator* CreateInsertionOperator(); LocalSearchOperator* CreateMakeInactiveOperator(); -#ifndef SWIG - template - LocalSearchOperator* CreateCPOperator(const T& operator_factory) { - return operator_factory(solver_.get(), nexts_, - CostsAreHomogeneousAcrossVehicles() - ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_); - } - template - LocalSearchOperator* CreateCPOperator() { - return CreateCPOperator(MakeLocalSearchOperator); - } - template - LocalSearchOperator* CreateCPOperatorWithArg(ArgType arg) { - return CreateCPOperatorWithArg(MakeLocalSearchOperatorWithArg, - std::move(arg)); - } - - using NeighborAccessor = - std::function&(/*node=*/int, /*start_node=*/int)>; - template - LocalSearchOperator* CreateCPOperatorWithNeighbors( - NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors) { - return CreateCPOperatorWithNeighbors( - MakeLocalSearchOperatorWithNeighbors, - std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)); - } - template - LocalSearchOperator* CreateOperatorWithNeighborsRatio( - int neighbors_ratio_used, NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors) { - return neighbors_ratio_used == 1 ? CreateCPOperator() - : CreateCPOperatorWithNeighbors( - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)); - } - template - LocalSearchOperator* CreateCPOperatorWithNeighbors( - const T& operator_factory, NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors) { - return operator_factory( - solver_.get(), nexts_, - CostsAreHomogeneousAcrossVehicles() ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_, std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors)); - } - template - LocalSearchOperator* CreateCPOperatorWithArg(const T& operator_factory, - ArgType arg) { - return operator_factory(solver_.get(), nexts_, - CostsAreHomogeneousAcrossVehicles() - ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_, std::move(arg)); - } - template - LocalSearchOperator* CreateOperator(const Arg& arg) { - return solver_->RevAlloc(new T(nexts_, - CostsAreHomogeneousAcrossVehicles() - ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_, arg)); - } - template - LocalSearchOperator* CreateOperatorWithNeighbors( - NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors, const Arg& arg) { - return solver_->RevAlloc( - new T(nexts_, - CostsAreHomogeneousAcrossVehicles() ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_, std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors), arg)); - } - template - LocalSearchOperator* CreateOperatorWithNeighborsRatio( - int neighbors_ratio_used, NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors, const Arg& arg) { - return neighbors_ratio_used == 1 - ? CreateOperator(arg) - : CreateOperatorWithNeighbors( - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors), arg); - } - template - LocalSearchOperator* CreateOperator(const Arg1& arg1, MoveableArg2 arg2) { - return solver_->RevAlloc( - new T(nexts_, - CostsAreHomogeneousAcrossVehicles() ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_, arg1, std::move(arg2))); - } - template - LocalSearchOperator* CreateOperatorWithNeighborsRatio( - int neighbors_ratio_used, NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors, const Arg1& arg1, - MoveableArg2 arg2) { - return neighbors_ratio_used == 1 - ? CreateOperator(arg1, std::move(arg2)) - : solver_->RevAlloc(new T(nexts_, - CostsAreHomogeneousAcrossVehicles() - ? std::vector() - : vehicle_vars_, - vehicle_start_class_callback_, - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors), - arg1, std::move(arg2))); - } - template - LocalSearchOperator* CreatePairOperator() { - return CreateOperator(pickup_delivery_pairs_); - } - template - LocalSearchOperator* CreatePairOperator( - int neighbors_ratio_used, NeighborAccessor get_incoming_neighbors, - NeighborAccessor get_outgoing_neighbors) { - return neighbors_ratio_used == 1 - ? CreateOperator(pickup_delivery_pairs_) - : CreateOperatorWithNeighbors( - std::move(get_incoming_neighbors), - std::move(get_outgoing_neighbors), pickup_delivery_pairs_); - } -#endif // SWIG void CreateNeighborhoodOperators(const RoutingSearchParameters& parameters); LocalSearchOperator* ConcatenateOperators( const RoutingSearchParameters& search_parameters, @@ -2753,6 +2636,7 @@ class OR_DLL RoutingModel { Solver::IndexEvaluator2 first_solution_evaluator_; FirstSolutionStrategy::Value automatic_first_solution_strategy_ = FirstSolutionStrategy::UNSET; + const Assignment* hint_ = nullptr; std::vector local_search_operators_; std::vector monitors_; std::vector secondary_ls_monitors_; @@ -3617,6 +3501,15 @@ class RoutingDimension { int64_t first_node; int64_t second_node; int64_t offset; + enum class PerformedConstraint { + // first_node and/or second_node can be unperformed. + kFirstAndSecondIndependent, + // if second_node is performed, first_node must be performed. + kSecondImpliesFirst, + // first_node is performed iff second_node is performed. + kFirstAndSecondEqual, + }; + PerformedConstraint performed_constraint; }; void AddNodePrecedence(NodePrecedence precedence) { @@ -3625,12 +3518,52 @@ class RoutingDimension { const std::vector& GetNodePrecedences() const { return node_precedences_; } -#endif // SWIG - + /// Returns the status of a precedence based on the performed constraint and + /// the performed status of the first and second node of a precedence. + enum class PrecedenceStatus { + kActive, + kInactive, + kInvalid, + }; + static PrecedenceStatus GetPrecedenceStatus( + bool first_unperformed, bool second_unperformed, + NodePrecedence::PerformedConstraint performed_constraint) { + switch (performed_constraint) { + case NodePrecedence::PerformedConstraint::kFirstAndSecondIndependent: + if (first_unperformed || second_unperformed) { + return PrecedenceStatus::kInactive; + } + break; + case NodePrecedence::PerformedConstraint::kSecondImpliesFirst: + if (first_unperformed) { + if (!second_unperformed) return PrecedenceStatus::kInvalid; + return PrecedenceStatus::kInactive; + } + if (second_unperformed) return PrecedenceStatus::kInactive; + break; + case NodePrecedence::PerformedConstraint::kFirstAndSecondEqual: + if (first_unperformed != second_unperformed) { + return PrecedenceStatus::kInvalid; + } + if (first_unperformed) return PrecedenceStatus::kInactive; + break; + } + return PrecedenceStatus::kActive; + } + void AddNodePrecedence( + int64_t first_node, int64_t second_node, int64_t offset, + NodePrecedence::PerformedConstraint performed_constraint = + NodePrecedence::PerformedConstraint::kFirstAndSecondIndependent) { + AddNodePrecedence({first_node, second_node, offset, performed_constraint}); + } +#else void AddNodePrecedence(int64_t first_node, int64_t second_node, int64_t offset) { - AddNodePrecedence({first_node, second_node, offset}); + AddNodePrecedence( + {first_node, second_node, offset, + NodePrecedence::PerformedConstraint::kFirstAndSecondIndependent}); } +#endif // SWIG int64_t GetSpanUpperBoundForVehicle(int vehicle) const { return vehicle_span_upper_bounds_[vehicle]; diff --git a/ortools/constraint_solver/routing_constraints.cc b/ortools/constraint_solver/routing_constraints.cc index cc6d0408847..656732a38d8 100644 --- a/ortools/constraint_solver/routing_constraints.cc +++ b/ortools/constraint_solver/routing_constraints.cc @@ -183,9 +183,11 @@ class ResourceAssignmentConstraint : public Constraint { const util_intops::StrongVector> ignored_resources_per_class(resource_group_.GetResourceClassesCount()); std::vector> assignment_costs(model_.vehicles()); + // TODO(user): Adjust the 'solve_duration_ratio' parameter. for (int v : resource_group_.GetVehiclesRequiringAResource()) { if (!ComputeVehicleToResourceClassAssignmentCosts( - v, resource_group_, ignored_resources_per_class, next, transit, + v, /*solve_duration_ratio=*/1.0, resource_group_, + ignored_resources_per_class, next, transit, /*optimize_vehicle_costs*/ false, model_.GetMutableLocalCumulLPOptimizer(dimension), model_.GetMutableLocalCumulMPOptimizer(dimension), diff --git a/ortools/constraint_solver/routing_decision_builders.cc b/ortools/constraint_solver/routing_decision_builders.cc index f101bfcc28c..956126dd3d3 100644 --- a/ortools/constraint_solver/routing_decision_builders.cc +++ b/ortools/constraint_solver/routing_decision_builders.cc @@ -17,11 +17,12 @@ #include #include #include -#include #include +#include #include #include +#include "absl/algorithm/container.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/types/span.h" @@ -184,6 +185,20 @@ void AppendRouteCumulAndBreakVarAndValues( vals.resize(new_num_values); } +namespace { +int GetVehicleRouteSize(const RoutingModel& model, int vehicle) { + int route_size = -1; + int64_t node = model.Start(vehicle); + while (node != model.End(vehicle)) { + route_size++; + DCHECK(model.NextVar(node)->Bound()); + node = model.NextVar(node)->Value(); + } + DCHECK_GE(route_size, 0); + return route_size; +} +} // namespace + class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { public: SetCumulsFromLocalDimensionCosts( @@ -237,14 +252,14 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { DCHECK(DimensionFixedTransitsEqualTransitEvaluators(dimension_)); cp_variables_.clear(); cp_values_.clear(); + vehicles_without_resource_assignment_.clear(); + vehicles_with_resource_assignment_.clear(); - std::vector vehicles_without_resource_assignment; - std::vector vehicles_with_resource_assignment; util_intops::StrongVector> used_resources_per_class; DetermineVehiclesRequiringResourceAssignment( - &vehicles_without_resource_assignment, - &vehicles_with_resource_assignment, &used_resources_per_class); + &vehicles_without_resource_assignment_, + &vehicles_with_resource_assignment_, &used_resources_per_class); const auto next = [&model = model_](int64_t n) { return model.NextVar(n)->Value(); @@ -252,39 +267,49 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { // First look at vehicles that do not need resource assignment (fewer/faster // computations). - for (int vehicle : vehicles_without_resource_assignment) { + // NOTE(user): If it ever becomes an issue, we can consider leaving more + // 'shares' for the resource assignment calls since they're more expensive. + int solve_duration_shares = vehicles_without_resource_assignment_.size() + + vehicles_with_resource_assignment_.size(); + for (int vehicle : vehicles_without_resource_assignment_) { solver->TopPeriodicCheck(); - std::vector cumul_values; - std::vector break_start_end_values; - if (!ComputeCumulAndBreakValuesForVehicle(vehicle, next, &cumul_values, - &break_start_end_values)) { + cumul_values_.clear(); + break_start_end_values_.clear(); + // TODO(user): Distinguish between FEASIBLE and OPTIMAL statuses to + // keep track of the FEASIBLE-only cases, and resolve the feasible-only + // cases with the remaining time (if any) after all routes have been + // scheduled with the initial 'solve_duration_ratio'. + if (!ComputeCumulAndBreakValuesForVehicle( + vehicle, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + next, &cumul_values_, &break_start_end_values_)) { return false; } - AppendRouteCumulAndBreakVarAndValues(dimension_, vehicle, cumul_values, - break_start_end_values, + solve_duration_shares--; + AppendRouteCumulAndBreakVarAndValues(dimension_, vehicle, cumul_values_, + break_start_end_values_, &cp_variables_, &cp_values_); } - if (vehicles_with_resource_assignment.empty()) { + if (vehicles_with_resource_assignment_.empty()) { return true; } // Do resource assignment for the vehicles requiring it and append the // corresponding var and values. - std::vector resource_indices; + resource_indices_.clear(); if (!ComputeVehicleResourceClassValuesAndIndices( - vehicles_with_resource_assignment, used_resources_per_class, next, - &resource_indices)) { + vehicles_with_resource_assignment_, used_resources_per_class, next, + &resource_indices_)) { return false; } - DCHECK_EQ(resource_indices.size(), model_.vehicles()); + DCHECK_EQ(resource_indices_.size(), model_.vehicles()); const int num_resource_classes = resource_group_->GetResourceClassesCount(); - for (int v : vehicles_with_resource_assignment) { + for (int v : vehicles_with_resource_assignment_) { DCHECK(next(model_.Start(v)) != model_.End(v) || model_.IsVehicleUsedWhenEmpty(v)); const auto& [unused, cumul_values, break_values] = vehicle_resource_class_values_[v]; - const int resource_index = resource_indices[v]; + const int resource_index = resource_indices_[v]; DCHECK_GE(resource_index, 0); DCHECK_EQ(cumul_values.size(), num_resource_classes); DCHECK_EQ(break_values.size(), num_resource_classes); @@ -298,61 +323,98 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { const std::vector& resource_vars = model_.ResourceVars(rg_index_); - DCHECK_EQ(resource_vars.size(), resource_indices.size()); + DCHECK_EQ(resource_vars.size(), resource_indices_.size()); cp_variables_.insert(cp_variables_.end(), resource_vars.begin(), resource_vars.end()); - cp_values_.insert(cp_values_.end(), resource_indices.begin(), - resource_indices.end()); + cp_values_.insert(cp_values_.end(), resource_indices_.begin(), + resource_indices_.end()); } return true; } - void DetermineVehiclesRequiringResourceAssignment( + inline void DetermineVehiclesRequiringResourceAssignment( std::vector* vehicles_without_resource_assignment, std::vector* vehicles_with_resource_assignment, util_intops::StrongVector>* used_resources_per_class) const { - vehicles_without_resource_assignment->clear(); - vehicles_with_resource_assignment->clear(); - used_resources_per_class->clear(); + DCHECK(vehicles_without_resource_assignment->empty()); + DCHECK(vehicles_with_resource_assignment->empty()); + DCHECK(used_resources_per_class->empty()); + struct VehicleInfo { + int vehicle_index; + int route_size; + bool requires_resource; +#if __cplusplus < 202002L + VehicleInfo(int vi, int rs, bool rr) + : vehicle_index(vi), route_size(rs), requires_resource(rr) {} +#endif + bool operator<(const VehicleInfo& other) const { + return std::tie(route_size, vehicle_index) < + std::tie(other.route_size, other.vehicle_index); + } + }; + + std::vector vehicle_info; + vehicle_info.reserve(model_.vehicles()); if (rg_index_ < 0) { - vehicles_without_resource_assignment->reserve(model_.vehicles()); for (int v = 0; v < model_.vehicles(); ++v) { - vehicles_without_resource_assignment->push_back(v); + const int route_size = GetVehicleRouteSize(model_, v); + vehicle_info.emplace_back(v, route_size, false); } + absl::c_sort(vehicle_info); + vehicles_without_resource_assignment->resize(model_.vehicles()); + absl::c_transform(vehicle_info, + vehicles_without_resource_assignment->begin(), + [](const VehicleInfo& v) { return v.vehicle_index; }); return; } + DCHECK_NE(resource_group_, nullptr); - const int num_vehicles_req_res = - resource_group_->GetVehiclesRequiringAResource().size(); - vehicles_without_resource_assignment->reserve(model_.vehicles() - - num_vehicles_req_res); - vehicles_with_resource_assignment->reserve(num_vehicles_req_res); used_resources_per_class->resize( resource_group_->GetResourceClassesCount()); + int num_vehicles_with_resource_assignment = 0; for (int v = 0; v < model_.vehicles(); ++v) { - if (!resource_group_->VehicleRequiresAResource(v)) { - vehicles_without_resource_assignment->push_back(v); - } else if (model_.NextVar(model_.Start(v))->Value() == model_.End(v) && + bool needs_resource = resource_group_->VehicleRequiresAResource(v); + if (needs_resource) { + if (model_.NextVar(model_.Start(v))->Value() == model_.End(v) && !model_.IsVehicleUsedWhenEmpty(v)) { // No resource assignment required for this unused vehicle. // TODO(user): Investigate if we should skip unused vehicles. - vehicles_without_resource_assignment->push_back(v); + needs_resource = false; } else if (model_.ResourceVar(v, rg_index_)->Bound()) { - vehicles_without_resource_assignment->push_back(v); + needs_resource = false; const int resource_idx = model_.ResourceVar(v, rg_index_)->Value(); DCHECK_GE(resource_idx, 0); used_resources_per_class ->at(resource_group_->GetResourceClassIndex(resource_idx)) .insert(resource_idx); } else { - vehicles_with_resource_assignment->push_back(v); + num_vehicles_with_resource_assignment++; + } + } + vehicle_info.emplace_back(v, GetVehicleRouteSize(model_, v), + needs_resource); + } + absl::c_sort(vehicle_info); + vehicles_with_resource_assignment->reserve( + num_vehicles_with_resource_assignment); + vehicles_without_resource_assignment->reserve( + model_.vehicles() - num_vehicles_with_resource_assignment); + for (const VehicleInfo& v_info : vehicle_info) { + if (v_info.requires_resource) { + vehicles_with_resource_assignment->push_back(v_info.vehicle_index); + } else { + vehicles_without_resource_assignment->push_back(v_info.vehicle_index); } } + DCHECK_EQ(vehicles_without_resource_assignment->size() + + vehicles_with_resource_assignment->size(), + model_.vehicles()); } bool ComputeCumulAndBreakValuesForVehicle( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, std::vector* cumul_values, std::vector* break_start_end_values) { cumul_values->clear(); @@ -378,34 +440,29 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { use_mp_optimizer ? mp_optimizer_ : lp_optimizer_; DCHECK_NE(optimizer, nullptr); DimensionSchedulingStatus status = - optimize_and_pack_ - ? optimizer->ComputePackedRouteCumuls( - vehicle, next_accessor, dimension_travel_info, resource, - cumul_values, break_start_end_values) + optimize_and_pack_ ? optimizer->ComputePackedRouteCumuls( + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values) : optimizer->ComputeRouteCumuls( - vehicle, next_accessor, dimension_travel_info, resource, - cumul_values, break_start_end_values); - if (status == DimensionSchedulingStatus::INFEASIBLE) { - return false; - } + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values); // If relaxation is not feasible, try the MP optimizer. if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { DCHECK(!use_mp_optimizer); DCHECK_NE(mp_optimizer_, nullptr); status = optimize_and_pack_ ? mp_optimizer_->ComputePackedRouteCumuls( - vehicle, next_accessor, dimension_travel_info, - resource, cumul_values, break_start_end_values) + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values) : mp_optimizer_->ComputeRouteCumuls( - vehicle, next_accessor, dimension_travel_info, - resource, cumul_values, break_start_end_values); - if (status == DimensionSchedulingStatus::INFEASIBLE) { - return false; + vehicle, solve_duration_ratio, next_accessor, + dimension_travel_info, resource, cumul_values, + break_start_end_values); } - } else { - DCHECK(status == DimensionSchedulingStatus::OPTIMAL); - } - return true; + return status != DimensionSchedulingStatus::INFEASIBLE; } bool ComputeVehicleResourceClassValuesAndIndices( @@ -418,17 +475,20 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { if (vehicles_to_assign.empty()) return true; DCHECK_NE(resource_group_, nullptr); + int solve_duration_shares = vehicles_to_assign.size(); for (int v : vehicles_to_assign) { DCHECK(resource_group_->VehicleRequiresAResource(v)); auto& [assignment_costs, cumul_values, break_values] = vehicle_resource_class_values_[v]; if (!ComputeVehicleToResourceClassAssignmentCosts( - v, *resource_group_, used_resources_per_class, next_accessor, + v, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + *resource_group_, used_resources_per_class, next_accessor, dimension_.transit_evaluator(v), /*optimize_vehicle_costs*/ true, lp_optimizer_, mp_optimizer_, &assignment_costs, &cumul_values, &break_values)) { return false; } + solve_duration_shares--; } return ComputeBestVehicleToResourceAssignment( @@ -467,6 +527,14 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { // - level 1: set remaining dimension values one by one. Rev decision_level_; DecisionBuilder* set_values_from_targets_ = nullptr; + // "Local" variables used by FillCPVariablesAndValues(). They can't be defined + // as true local variables, because the function may backtrack when a time + // limit is reached. + std::vector vehicles_without_resource_assignment_; + std::vector vehicles_with_resource_assignment_; + std::vector cumul_values_; + std::vector break_start_end_values_; + std::vector resource_indices_; }; } // namespace @@ -555,23 +623,17 @@ class SetCumulsFromGlobalDimensionCosts : public DecisionBuilder { model->GetDimensionResourceGroupIndices(dimension).empty() ? global_optimizer_ : global_mp_optimizer_; - const DimensionSchedulingStatus status = ComputeCumulBreakAndResourceValues( + DimensionSchedulingStatus status = ComputeCumulBreakAndResourceValues( optimizer, &cumul_values_, &break_start_end_values_, &resource_indices_per_group_); - - if (status == DimensionSchedulingStatus::INFEASIBLE) { - return false; - } else if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { + if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { // If relaxation is not feasible, try the MILP optimizer. - const DimensionSchedulingStatus mp_status = - ComputeCumulBreakAndResourceValues( + status = ComputeCumulBreakAndResourceValues( global_mp_optimizer_, &cumul_values_, &break_start_end_values_, &resource_indices_per_group_); - if (mp_status != DimensionSchedulingStatus::OPTIMAL) { - return false; } - } else { - DCHECK(status == DimensionSchedulingStatus::OPTIMAL); + if (status == DimensionSchedulingStatus::INFEASIBLE) { + return false; } // Concatenate cumul_values_, break_start_end_values_ and all // resource_indices_per_group_ into cp_values_. diff --git a/ortools/constraint_solver/routing_filter_committables.h b/ortools/constraint_solver/routing_filter_committables.h new file mode 100644 index 00000000000..6883f65b60e --- /dev/null +++ b/ortools/constraint_solver/routing_filter_committables.h @@ -0,0 +1,515 @@ +// 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. + +#ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTER_COMMITTABLES_H_ +#define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTER_COMMITTABLES_H_ + +#include +#include +#include +#include +#include + +#include "absl/log/check.h" +#include "absl/types/span.h" +#include "ortools/util/bitset.h" +#include "ortools/util/saturated_arithmetic.h" + +namespace operations_research { + +// A vector that allows to revert back to a previously committed state, +// get the set of changed indices, and get current and committed values. +template +class CommittableValue { + public: + explicit CommittableValue(const T& value) + : current_(value), committed_(value) {} + + const T& Get() const { return current_; } + const T& GetCommitted() const { return committed_; } + + void Set(const T& value) { current_ = value; } + + void SetAndCommit(const T& value) { + Set(value); + Commit(); + } + + void Revert() { current_ = committed_; } + + void Commit() { committed_ = current_; } + + private: + T current_; + T committed_; +}; + +template +class CommittableVector { + public: + // Makes a vector with initial elements all committed to value. + CommittableVector(size_t num_elements, const T& value) + : elements_(num_elements, {value, value}), changed_(num_elements) {} + + // Return the size of the vector. + size_t Size() const { return elements_.size(); } + + // Returns a copy of the value stored at index in the current state. + // Does not return a reference, because the class needs to know when elements + // are modified. + T Get(size_t index) const { + DCHECK_LT(index, elements_.size()); + return elements_[index].current; + } + + // Set the value stored at index in the current state to given value. + void Set(size_t index, const T& value) { + DCHECK_GE(index, 0); + DCHECK_LT(index, elements_.size()); + changed_.Set(index); + elements_[index].current = value; + } + + // Changes the values of the vector to those in the last Commit(). + void Revert() { + for (const size_t index : changed_.PositionsSetAtLeastOnce()) { + elements_[index].current = elements_[index].committed; + } + changed_.ClearAll(); + } + + // Makes the current state committed, clearing all changes. + void Commit() { + for (const size_t index : changed_.PositionsSetAtLeastOnce()) { + elements_[index].committed = elements_[index].current; + } + changed_.ClearAll(); + } + + // Sets all elements of this vector to given value, and commits to this state. + // Supposes that there are no changes since the last Commit() or Revert(). + void SetAllAndCommit(const T& value) { + DCHECK_EQ(0, changed_.NumberOfSetCallsWithDifferentArguments()); + elements_.assign(elements_.size(), {value, value}); + } + + // Returns a copy of the value stored at index in the last committed state. + T GetCommitted(size_t index) const { + DCHECK_LT(index, elements_.size()); + return elements_[index].committed; + } + + // Return true iff the value at index has been Set() since the last Commit() + // or Revert(), even if the current value is the same as the committed value. + bool HasChanged(size_t index) const { return changed_[index]; } + + // Returns the set of indices that have been Set() since the last Commit() or + // Revert(). + const std::vector& ChangedIndices() const { + return changed_.PositionsSetAtLeastOnce(); + } + + // TODO(user): NotifyReverted(), to tell the class that the changes + // have brought the vector back to the committed state. This allows O(1) + // Revert(), Commit() and empty changed indices. + + private: + struct VersionedElement { + T current; + T committed; + }; + // Holds current and committed versions of values of this vector. + std::vector elements_; + // Holds indices that were Set() since the last Commit() or Revert(). + SparseBitset changed_; +}; + +// This class allows to represent a state of dimension values for all paths of +// a vehicle routing problem. Values of interest for each path are: +// - nodes, +// - cumuls (min/max), +// - transit times, +// - sum of transit times since the beginning of the path, +// - span (min/max). +// +// This class can maintain two states at once: a committed state and a current +// state. The current state can be modified by first describing a path p to be +// modified with PushNode() and MakePathFromNewNodes(). Then the dimension +// values of this path can be modified with views returned by MutableXXX() +// methods. +// +// When a set of paths has been modified, the caller can decide to definitely +// change the committed state to the new state, or to revert to the committed +// state. +// +// Operations are meant to be efficient: +// - all path modifications, i.e. PushNode(), MakePathFromNewNodes(), +// MutableXXX(), MutableSpan() operations are O(1). +// - Revert() is O(num changed paths). +// - Commit() has two behaviors: +// - if there are less than max_num_committed_elements_ elements in the +// committed state, then Commit() is O(num changed paths). +// - otherwise, Commit() does a compaction of the committed state, in +// O(num_nodes + num_paths). +// The amortized cost of Commit(), when taking modifications into account, +// is O(size of changed paths), because all modifications pay at worst +// O(1) for its own compaction. +// +// Note that this class does not support the semantics associated with its +// fields names, for instance it does not make sure that cumul_min <= cumul_max. +// The field names are meant for readability for the user. +// However, path sizes are enforced: if a path has n nodes, then it has +// n fields for cumul min/max, n for transit_sums, and max(0, n-1) for transits. +class DimensionValues { + public: + DimensionValues(int num_paths, int num_nodes) + : range_of_path_(num_paths, {.begin = 0, .end = 0}), + committed_range_of_path_(num_paths, {.begin = 0, .end = 0}), + span_(num_paths, Interval::AllIntegers()), + committed_span_(num_paths, Interval::AllIntegers()), + vehicle_breaks_(num_paths), + committed_vehicle_breaks_(num_paths), + changed_paths_(num_paths), + max_num_committed_elements_(16 * num_nodes) { + nodes_.reserve(max_num_committed_elements_); + transit_.reserve(max_num_committed_elements_); + travel_.reserve(max_num_committed_elements_); + travel_sum_.reserve(max_num_committed_elements_); + cumul_.reserve(max_num_committed_elements_); + } + + struct Interval { + int64_t min; + int64_t max; + // Tests inequality between intervals. + bool operator!=(const Interval& other) const { + return min != other.min || max != other.max; + } + // Tests equality between intervals. + bool operator==(const Interval& other) const { + return min == other.min && max == other.max; + } + // Returns true iff the interval is empty. + bool IsEmpty() const { return min > max; } + // Increases the min to be at least lower_bound, + // returns true iff the interval is nonempty. + bool IncreaseMin(int64_t lower_bound) { + min = std::max(min, lower_bound); + return min <= max; + } + // Decreases the max to be at most upper_bound, + // returns true iff the interval is nonempty. + bool DecreaseMax(int64_t upper_bound) { + max = std::min(max, upper_bound); + return min <= max; + } + // Intersects this interval with the other, returns true iff the interval + // is nonempty. + bool IntersectWith(const Interval& other) { + min = std::max(min, other.min); + max = std::min(max, other.max); + return min <= max; + } + // A set addition, with intervals: adds other.min to the min, other.max to + // the max, with CapAdd(). + void Add(const Interval& other) { + DCHECK(!IsEmpty()); + DCHECK(!other.IsEmpty()); + min = CapAdd(min, other.min); + max = CapAdd(max, other.max); + } + // A set subtraction, with intervals: subtracts other.max from the min, + // other.min from the max, with CapSub(). + void Subtract(const Interval& other) { + DCHECK(!IsEmpty()); + DCHECK(!other.IsEmpty()); + min = CapSub(min, other.max); + max = CapSub(max, other.min); + } + // Returns an interval containing all integers: {kint64min, kint64max}. + static Interval AllIntegers() { + return {.min = std::numeric_limits::min(), + .max = std::numeric_limits::max()}; + } + }; + + struct VehicleBreak { + Interval start; + Interval end; + Interval duration; + Interval is_performed; + bool operator==(const VehicleBreak& other) const { + return start == other.start && end == other.end && + duration == other.duration && is_performed == other.is_performed; + } + }; + + // Adds a node to new nodes. + void PushNode(int node) { nodes_.push_back(node); } + + // Turns new nodes into a new path, allocating dimension values for it. + void MakePathFromNewNodes(int path) { + DCHECK_GE(path, 0); + DCHECK_LT(path, range_of_path_.size()); + DCHECK(!changed_paths_[path]); + range_of_path_[path] = {.begin = num_current_elements_, + .end = nodes_.size()}; + changed_paths_.Set(path); + // Allocate dimension values. We allocate n cells for all dimension values, + // even transits, so they can all be indexed by the same range_of_path. + transit_.resize(nodes_.size(), Interval::AllIntegers()); + travel_.resize(nodes_.size(), 0); + travel_sum_.resize(nodes_.size(), 0); + cumul_.resize(nodes_.size(), Interval::AllIntegers()); + num_current_elements_ = nodes_.size(); + span_[path] = Interval::AllIntegers(); + } + + // Resets all path to empty, in both committed and current state. + void Reset() { + const int num_paths = range_of_path_.size(); + range_of_path_.assign(num_paths, {.begin = 0, .end = 0}); + committed_range_of_path_.assign(num_paths, {.begin = 0, .end = 0}); + changed_paths_.SparseClearAll(); + num_current_elements_ = 0; + num_committed_elements_ = 0; + nodes_.clear(); + transit_.clear(); + travel_.clear(); + travel_sum_.clear(); + cumul_.clear(); + committed_span_.assign(num_paths, Interval::AllIntegers()); + } + + // Clears the changed state, make it point to the committed state. + void Revert() { + for (const int path : changed_paths_.PositionsSetAtLeastOnce()) { + range_of_path_[path] = committed_range_of_path_[path]; + } + changed_paths_.SparseClearAll(); + num_current_elements_ = num_committed_elements_; + nodes_.resize(num_current_elements_); + transit_.resize(num_current_elements_); + travel_.resize(num_current_elements_); + travel_sum_.resize(num_current_elements_); + cumul_.resize(num_current_elements_); + } + + // Makes the committed state point to the current state. + // If the state representation is too large, reclaims memory by compacting + // the committed state. + void Commit() { + for (const int path : changed_paths_.PositionsSetAtLeastOnce()) { + committed_range_of_path_[path] = range_of_path_[path]; + committed_span_[path] = span_[path]; + committed_vehicle_breaks_[path] = vehicle_breaks_[path]; + } + changed_paths_.SparseClearAll(); + num_committed_elements_ = num_current_elements_; + // If the committed data would take too much space, compact the data: + // copy committed data to the end of vectors, erase old data, refresh + // indexing (range_of_path_). + if (num_current_elements_ <= max_num_committed_elements_) return; + temp_nodes_.clear(); + temp_transit_.clear(); + temp_travel_.clear(); + temp_travel_sum_.clear(); + temp_cumul_.clear(); + for (int path = 0; path < range_of_path_.size(); ++path) { + if (committed_range_of_path_[path].Size() == 0) continue; + const size_t new_begin = temp_nodes_.size(); + const auto [begin, end] = committed_range_of_path_[path]; + temp_nodes_.insert(temp_nodes_.end(), nodes_.begin() + begin, + nodes_.begin() + end); + temp_transit_.insert(temp_transit_.end(), transit_.begin() + begin, + transit_.begin() + end); + temp_travel_.insert(temp_travel_.end(), travel_.begin() + begin, + travel_.begin() + end); + temp_travel_sum_.insert(temp_travel_sum_.end(), + travel_sum_.begin() + begin, + travel_sum_.begin() + end); + temp_cumul_.insert(temp_cumul_.end(), cumul_.begin() + begin, + cumul_.begin() + end); + committed_range_of_path_[path] = {.begin = new_begin, + .end = temp_nodes_.size()}; + } + std::swap(nodes_, temp_nodes_); + std::swap(transit_, temp_transit_); + std::swap(travel_, temp_travel_); + std::swap(travel_sum_, temp_travel_sum_); + std::swap(cumul_, temp_cumul_); + range_of_path_ = committed_range_of_path_; + num_committed_elements_ = nodes_.size(); + num_current_elements_ = nodes_.size(); + } + + // Returns a const view of the nodes of the path, in the committed state. + absl::Span CommittedNodes(int path) const { + const auto [begin, end] = committed_range_of_path_[path]; + return absl::MakeConstSpan(nodes_.data() + begin, nodes_.data() + end); + } + + // Returns a const view of the nodes of the path, in the current state. + absl::Span Nodes(int path) const { + const auto [begin, end] = range_of_path_[path]; + return absl::MakeConstSpan(nodes_.data() + begin, nodes_.data() + end); + } + + // Returns a const view of the transits of the path, in the current state. + absl::Span Transits(int path) const { + auto [begin, end] = range_of_path_[path]; + // When the path is not empty, #transits = #nodes - 1. + // When the path is empty, begin = end, return empty span. + if (begin < end) --end; + return absl::MakeConstSpan(transit_.data() + begin, transit_.data() + end); + } + + // Returns a mutable view of the transits of the path, in the current state. + absl::Span MutableTransits(int path) { + auto [begin, end] = range_of_path_[path]; + // When the path is not empty, #transits = #nodes - 1. + // When the path is empty, begin = end, return empty span. + if (begin < end) --end; + return absl::MakeSpan(transit_.data() + begin, transit_.data() + end); + } + + // Returns a const view of the travels of the path, in the current + // state. + absl::Span Travels(int path) const { + auto [begin, end] = range_of_path_[path]; + if (begin < end) --end; + return absl::MakeConstSpan(travel_.data() + begin, travel_.data() + end); + } + + // Returns a mutable view of the travels of the path, in the current + // state. + absl::Span MutableTravels(int path) { + auto [begin, end] = range_of_path_[path]; + if (begin < end) --end; + return absl::MakeSpan(travel_.data() + begin, travel_.data() + end); + } + + // Returns a const view of the travel sums of the path, in the current state. + absl::Span TravelSums(int path) const { + const auto [begin, end] = range_of_path_[path]; + return absl::MakeConstSpan(travel_sum_.data() + begin, + travel_sum_.data() + end); + } + + // Returns a mutable view of the travel sums of the path in the current state. + absl::Span MutableTravelSums(int path) { + const auto [begin, end] = range_of_path_[path]; + return absl::MakeSpan(travel_sum_.data() + begin, travel_sum_.data() + end); + } + + // Returns a const view of the cumul mins of the path, in the current state. + absl::Span Cumuls(int path) const { + const auto [begin, end] = range_of_path_[path]; + return absl::MakeConstSpan(cumul_.data() + begin, cumul_.data() + end); + } + + // Returns a mutable view of the cumul mins of the path, in the current state. + absl::Span MutableCumuls(int path) { + const auto [begin, end] = range_of_path_[path]; + return absl::MakeSpan(cumul_.data() + begin, cumul_.data() + end); + } + + // Returns the span interval of the path, in the current state. + Interval Span(int path) const { + return changed_paths_[path] ? span_[path] : committed_span_[path]; + } + // Returns a mutable view of the span of the path, in the current state. + // The path must have been changed since the last commit. + Interval& MutableSpan(int path) { + DCHECK(changed_paths_[path]); + return span_[path]; + } + + // Returns a const view of the vehicle breaks of the path, in the current + // state. + absl::Span VehicleBreaks(int path) const { + return absl::MakeConstSpan(changed_paths_[path] + ? vehicle_breaks_[path] + : committed_vehicle_breaks_[path]); + } + + // Returns a mutable vector of the vehicle breaks of the path, in the current + // state. The path must have been changed since the last commit. + std::vector& MutableVehicleBreaks(int path) { + DCHECK(changed_paths_[path]); + return vehicle_breaks_[path]; + } + + // Returns the number of nodes of the path, in the current state. + int NumNodes(int path) const { return range_of_path_[path].Size(); } + // Returns a const view of the set of paths changed, in the current state. + absl::Span ChangedPaths() const { + return absl::MakeConstSpan(changed_paths_.PositionsSetAtLeastOnce()); + } + // Returns whether the given path was changed, in the current state. + bool PathHasChanged(int path) const { return changed_paths_[path]; } + + private: + // These vectors hold the data of both committed and current states. + // The ranges below determine which indices are associated to each path and + // each state. It is up to the user to maintain the following invariants: + // If range_of_path_[p] == {.begin = b, .end = e}, then, in the current + // state: + // - nodes_[i] for i in [b, e) are the nodes of the path p. + // - cumul_[r] + transit_[r] == cumul_[r+1] for r in [b, e-1). + // - travel_[r] <= transit_[r].min for r in [b, e-1). + // - travel_sum_[r] == sum_{r' in [0, r')} travel_[r'], for r in [b+1, e) + // - cumul[b] + span_[p] == cumul[e-1]. + // + // The same invariants should hold for committed_range_of_path_ and the + // committed state. + std::vector nodes_; + std::vector transit_; + std::vector travel_; + std::vector travel_sum_; + std::vector cumul_; + // Temporary vectors used in Commit() during compaction. + std::vector temp_nodes_; + std::vector temp_transit_; + std::vector temp_travel_; + std::vector temp_travel_sum_; + std::vector temp_cumul_; + // A path has a range of indices in the committed state and another one in the + // current state. + struct Range { + size_t begin = 0; + size_t end = 0; + int Size() const { return end - begin; } + }; + std::vector range_of_path_; + std::vector committed_range_of_path_; + // Associates span to each path. + std::vector span_; + std::vector committed_span_; + // Associates vehicle breaks with each path. + std::vector> vehicle_breaks_; + std::vector> committed_vehicle_breaks_; + // Stores whether each path has been changed since last committed state. + SparseBitset changed_paths_; + // Threshold for the size of the committed vector. This is purely heuristic: + // it should be more than the number of nodes so compactions do not occur at + // each submit, but ranges should not be too far apart to avoid cache misses. + const size_t max_num_committed_elements_; + // This locates the start of new nodes. + size_t num_current_elements_ = 0; + size_t num_committed_elements_ = 0; +}; + +} // namespace operations_research + +#endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTER_COMMITTABLES_H_ diff --git a/ortools/constraint_solver/routing_filters.cc b/ortools/constraint_solver/routing_filters.cc index 7ab4577dbd7..ff26622f91f 100644 --- a/ortools/constraint_solver/routing_filters.cc +++ b/ortools/constraint_solver/routing_filters.cc @@ -485,13 +485,11 @@ BasePathFilter::BasePathFilter(const std::vector& nexts, touched_paths_(nexts.size()), touched_path_chain_start_ends_(nexts.size(), {kUnassigned, kUnassigned}), ranks_(next_domain_size, kUnassigned), - status_(BasePathFilter::UNKNOWN), lns_detected_(false) {} bool BasePathFilter::Accept(const Assignment* delta, const Assignment* /*deltadelta*/, int64_t objective_min, int64_t objective_max) { - if (IsDisabled()) return true; lns_detected_ = false; for (const int touched : delta_touched_) { new_nexts_[touched] = kUnassigned; @@ -618,11 +616,6 @@ void BasePathFilter::SynchronizeFullAssignment() { } void BasePathFilter::OnSynchronize(const Assignment* delta) { - if (status_ == BasePathFilter::UNKNOWN) { - status_ = - DisableFiltering() ? BasePathFilter::DISABLED : BasePathFilter::ENABLED; - } - if (IsDisabled()) return; new_synchronized_unperformed_nodes_.ClearAll(); if (delta == nullptr || delta->Empty() || absl::c_all_of(ranks_, [](int rank) { return rank == kUnassigned; })) { @@ -1114,7 +1107,7 @@ bool ChainCumulFilter::AcceptPath(int64_t path_start, int64_t chain_start, bool PropagateLightweightVehicleBreaks( int path, DimensionValues& dimension_values, - const std::vector>& interbreaks) { + absl::Span> interbreaks) { using Interval = DimensionValues::Interval; using VehicleBreak = DimensionValues::VehicleBreak; const int64_t total_travel = dimension_values.TravelSums(path).back(); @@ -1935,16 +1928,25 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, const int num_nodes = nodes.size(); for (int rank = 0; rank < num_nodes; ++rank) { const int node = nodes[rank]; - for (const auto& precedence : node_index_to_precedences_[node]) { - const int first = precedence.first_node; - const int second = precedence.second_node; - const int64_t offset = precedence.offset; - const auto [path1, rank1] = location_of_node_.Get(first); - const auto [path2, rank2] = location_of_node_.Get(second); - if (path1 == -1 || path2 == -1) continue; - DCHECK(node == first || node == second); - DCHECK_EQ(first, dimension_values_.Nodes(path1)[rank1]); - DCHECK_EQ(second, dimension_values_.Nodes(path2)[rank2]); + for (const auto [first_node, second_node, offset, + performed_constraint] : + node_index_to_precedences_[node]) { + const auto [path1, rank1] = location_of_node_.Get(first_node); + const auto [path2, rank2] = location_of_node_.Get(second_node); + if (path1 == -1 && !IsVarSynced(first_node)) continue; + if (path2 == -1 && !IsVarSynced(second_node)) continue; + switch (RoutingDimension::GetPrecedenceStatus( + path1 == -1, path2 == -1, performed_constraint)) { + case RoutingDimension::PrecedenceStatus::kActive: + break; + case RoutingDimension::PrecedenceStatus::kInactive: + continue; + case RoutingDimension::PrecedenceStatus::kInvalid: + return false; + } + DCHECK(node == first_node || node == second_node); + DCHECK_EQ(first_node, dimension_values_.Nodes(path1)[rank1]); + DCHECK_EQ(second_node, dimension_values_.Nodes(path2)[rank2]); // Check that cumul1 + offset <= cumul2 is feasible. if (CapAdd(dimension_values_.Cumuls(path1)[rank1].min, offset) > dimension_values_.Cumuls(path2)[rank2].max) @@ -2001,6 +2003,9 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, if (may_use_optimizers_ && lp_optimizer_ != nullptr && accepted_objective_value_ <= objective_max) { std::vector paths_requiring_mp_optimizer; + // TODO(user): Further optimize the LPs when we find feasible-only + // solutions with the original time shares, if there's time left in the end. + int solve_duration_shares = dimension_values_.ChangedPaths().size(); for (const int vehicle : dimension_values_.ChangedPaths()) { if (!FilterWithDimensionCumulOptimizerForVehicle(vehicle)) { continue; @@ -2008,13 +2013,17 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, int64_t path_cost_with_lp = 0; const DimensionSchedulingStatus status = lp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, /*resource=*/nullptr, + vehicle, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + path_accessor_, /*resource=*/nullptr, filter_objective_cost_ ? &path_cost_with_lp : nullptr); + solve_duration_shares--; if (status == DimensionSchedulingStatus::INFEASIBLE) { return false; } // Replace previous path cost with the LP optimizer cost. if (filter_objective_cost_ && + (status == DimensionSchedulingStatus::OPTIMAL || + status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) && path_cost_with_lp > cost_of_path_.Get(vehicle)) { CapSubFrom(cost_of_path_.Get(vehicle), &accepted_objective_value_); CapAddTo(path_cost_with_lp, &accepted_objective_value_); @@ -2033,15 +2042,20 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, DCHECK_LE(accepted_objective_value_, objective_max); + solve_duration_shares = paths_requiring_mp_optimizer.size(); for (const int vehicle : paths_requiring_mp_optimizer) { int64_t path_cost_with_mp = 0; - if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, path_accessor_, /*resource=*/nullptr, - filter_objective_cost_ ? &path_cost_with_mp : nullptr) == - DimensionSchedulingStatus::INFEASIBLE) { + const DimensionSchedulingStatus status = + mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( + vehicle, /*solve_duration_ratio=*/1.0 / solve_duration_shares, + path_accessor_, /*resource=*/nullptr, + filter_objective_cost_ ? &path_cost_with_mp : nullptr); + solve_duration_shares--; + if (status == DimensionSchedulingStatus::INFEASIBLE) { return false; } if (filter_objective_cost_ && + status == DimensionSchedulingStatus::OPTIMAL && path_cost_with_mp > cost_of_path_.Get(vehicle)) { CapSubFrom(cost_of_path_.Get(vehicle), &accepted_objective_value_); CapAddTo(path_cost_with_mp, &accepted_objective_value_); @@ -2352,272 +2366,298 @@ void AppendDimensionCumulFilters( namespace { // Filter for pickup/delivery precedences. -class PickupDeliveryFilter : public BasePathFilter { +class PickupDeliveryFilter : public LocalSearchFilter { public: - PickupDeliveryFilter(const std::vector& nexts, int next_domain_size, - const PathsMetadata& paths_metadata, - const std::vector& pairs, + PickupDeliveryFilter(const PathState* path_state, + absl::Span pairs, const std::vector& vehicle_policies); ~PickupDeliveryFilter() override = default; - bool AcceptPath(int64_t path_start, int64_t chain_start, - int64_t chain_end) override; + bool Accept(const Assignment* /*delta*/, const Assignment* /*deltadelta*/, + int64_t /*objective_min*/, int64_t /*objective_max*/) override; + + void Reset() override; + void Synchronize(const Assignment* /*assignment*/, + const Assignment* /*delta*/) override; std::string DebugString() const override { return "PickupDeliveryFilter"; } private: - bool AcceptPathDefault(int64_t path_start); - template - bool AcceptPathOrdered(int64_t path_start); - - std::vector pair_firsts_; - std::vector pair_seconds_; - const std::vector pairs_; - SparseBitset<> visited_; + template + bool AcceptPathDispatch(); + template + bool AcceptPathDefault(int path); + template + bool AcceptPathOrdered(int path); + void AssignAllVisitedPairsAndLoopNodes(); + + const PathState* const path_state_; + struct PairInfo { + // @TODO(user): Use default member initializers once we drop C++17 + // support on github. + bool is_paired : 1; + bool is_pickup : 1; + int pair_index : 30; + PairInfo() : is_paired(false), pair_index(-1) {} + PairInfo(bool is_paired, bool is_pickup, int pair_index) + : is_paired(is_paired), is_pickup(is_pickup), pair_index(pair_index) {} + }; + std::vector pair_info_of_node_; + struct PairStatus { + // @TODO(user): Use default member initializers once we drop C++17 + // support on github. + bool pickup : 1; + bool delivery : 1; + PairStatus() : pickup(false), delivery(false) {} + }; + CommittableVector assigned_status_of_pair_; + SparseBitset pair_is_open_; + CommittableValue num_assigned_pairs_; std::deque visited_deque_; const std::vector vehicle_policies_; }; PickupDeliveryFilter::PickupDeliveryFilter( - const std::vector& nexts, int next_domain_size, - const PathsMetadata& paths_metadata, - const std::vector& pairs, + const PathState* path_state, absl::Span pairs, const std::vector& vehicle_policies) - : BasePathFilter(nexts, next_domain_size, paths_metadata), - pair_firsts_(next_domain_size, kUnassigned), - pair_seconds_(next_domain_size, kUnassigned), - pairs_(pairs), - visited_(Size()), + : path_state_(path_state), + pair_info_of_node_(path_state->NumNodes()), + assigned_status_of_pair_(pairs.size(), {}), + pair_is_open_(pairs.size()), + num_assigned_pairs_(0), vehicle_policies_(vehicle_policies) { - for (int i = 0; i < pairs.size(); ++i) { - const auto& index_pair = pairs[i]; - for (int first : index_pair.pickup_alternatives) { - pair_firsts_[first] = i; + for (int pair_index = 0; pair_index < pairs.size(); ++pair_index) { + const auto& [pickups, deliveries] = pairs[pair_index]; + for (const int pickup : pickups) { + pair_info_of_node_[pickup] = + // @TODO(user): Use aggregate initialization once we drop C++17 + // support on github. + PairInfo{/*is_paired=*/true, /*is_pickup=*/true, + /*pair_index=*/pair_index}; } - for (int second : index_pair.delivery_alternatives) { - pair_seconds_[second] = i; + for (const int delivery : deliveries) { + pair_info_of_node_[delivery] = + // @TODO(user): Use aggregate initialization once we drop C++17 + // support on github. + PairInfo{/*is_paired=*/true, /*is_pickup=*/false, + /*pair_index=*/pair_index}; } } } -bool PickupDeliveryFilter::AcceptPath(int64_t path_start, - int64_t /*chain_start*/, - int64_t /*chain_end*/) { - switch (vehicle_policies_[GetPath(path_start)]) { - case RoutingModel::PICKUP_AND_DELIVERY_NO_ORDER: - return AcceptPathDefault(path_start); - case RoutingModel::PICKUP_AND_DELIVERY_LIFO: - return AcceptPathOrdered(path_start); - case RoutingModel::PICKUP_AND_DELIVERY_FIFO: - return AcceptPathOrdered(path_start); - default: - return true; - } -} - -bool PickupDeliveryFilter::AcceptPathDefault(int64_t path_start) { - visited_.ClearAll(); - int64_t node = path_start; - int64_t path_length = 1; - while (node < Size()) { - // Detect sub-cycles (path is longer than longest possible path). - if (path_length > Size()) { - return false; +void PickupDeliveryFilter::Reset() { + assigned_status_of_pair_.Revert(); + assigned_status_of_pair_.SetAllAndCommit({}); + num_assigned_pairs_.SetAndCommit(0); +} + +void PickupDeliveryFilter::AssignAllVisitedPairsAndLoopNodes() { + assigned_status_of_pair_.Revert(); + num_assigned_pairs_.Revert(); + int num_assigned_pairs = num_assigned_pairs_.Get(); + if (num_assigned_pairs == assigned_status_of_pair_.Size()) return; + // If node is a pickup or delivery, this sets the assigned_status_of_pair_ + // status to true, and returns true if the whole pair *became* assigned. + auto assign_node = [this](int node) -> bool { + const auto [is_paired, is_pickup, pair_index] = pair_info_of_node_[node]; + if (!is_paired) return false; + bool assigned_pair = false; + PairStatus assigned_status = assigned_status_of_pair_.Get(pair_index); + if (is_pickup && !assigned_status.pickup) { + assigned_pair = assigned_status.delivery; + assigned_status.pickup = true; + assigned_status_of_pair_.Set(pair_index, assigned_status); } - if (pair_firsts_[node] != kUnassigned) { - // Checking on pair firsts is not actually necessary (inconsistencies - // will get caught when checking pair seconds); doing it anyway to - // cut checks early. - for (int second : pairs_[pair_firsts_[node]].delivery_alternatives) { - if (visited_[second]) { - return false; + if (!is_pickup && !assigned_status.delivery) { + assigned_pair = assigned_status.pickup; + assigned_status.delivery = true; + assigned_status_of_pair_.Set(pair_index, assigned_status); } + return assigned_pair; + }; + for (const int path : path_state_->ChangedPaths()) { + for (const int node : path_state_->Nodes(path)) { + num_assigned_pairs += assign_node(node) ? 1 : 0; } } - if (pair_seconds_[node] != kUnassigned) { - bool found_first = false; - bool some_synced = false; - for (int first : pairs_[pair_seconds_[node]].pickup_alternatives) { - if (visited_[first]) { - found_first = true; - break; + for (const int loop : path_state_->ChangedLoops()) { + num_assigned_pairs += assign_node(loop) ? 1 : 0; } - if (IsVarSynced(first)) { - some_synced = true; + num_assigned_pairs_.Set(num_assigned_pairs); } + +void PickupDeliveryFilter::Synchronize(const Assignment* /*assignment*/, + const Assignment* /*delta*/) { + AssignAllVisitedPairsAndLoopNodes(); + assigned_status_of_pair_.Commit(); + num_assigned_pairs_.Commit(); } - if (!found_first && some_synced) { - return false; + +bool PickupDeliveryFilter::Accept(const Assignment* /*delta*/, + const Assignment* /*deltadelta*/, + int64_t /*objective_min*/, + int64_t /*objective_max*/) { + if (path_state_->IsInvalid()) return true; // Protect against CP-LNS. + AssignAllVisitedPairsAndLoopNodes(); + const bool check_assigned_pairs = + num_assigned_pairs_.Get() < assigned_status_of_pair_.Size(); + if (check_assigned_pairs) { + return AcceptPathDispatch(); + } else { + return AcceptPathDispatch(); } } - visited_.Set(node); - const int64_t next = GetNext(node); - if (next == kUnassigned) { - // LNS detected, return true since path was ok up to now. - return true; - } - node = next; - ++path_length; - } - for (const int64_t node : visited_.PositionsSetAtLeastOnce()) { - if (pair_firsts_[node] != kUnassigned) { - bool found_second = false; - bool some_synced = false; - for (int second : pairs_[pair_firsts_[node]].delivery_alternatives) { - if (visited_[second]) { - found_second = true; + +template +bool PickupDeliveryFilter::AcceptPathDispatch() { + for (const int path : path_state_->ChangedPaths()) { + switch (vehicle_policies_[path]) { + case RoutingModel::PICKUP_AND_DELIVERY_NO_ORDER: + if (!AcceptPathDefault(path)) return false; break; + case RoutingModel::PICKUP_AND_DELIVERY_LIFO: + if (!AcceptPathOrdered(path)) return false; + break; + case RoutingModel::PICKUP_AND_DELIVERY_FIFO: + if (!AcceptPathOrdered(path)) return false; + break; + default: + continue; } - if (IsVarSynced(second)) { - some_synced = true; } + return true; } - if (!found_second && some_synced) { - return false; + +template +bool PickupDeliveryFilter::AcceptPathDefault(int path) { + pair_is_open_.SparseClearAll(); + int num_opened_pairs = 0; + for (const int node : path_state_->Nodes(path)) { + const auto [is_paired, is_pickup, pair_index] = pair_info_of_node_[node]; + if (!is_paired) continue; + if constexpr (check_assigned_pairs) { + const PairStatus status = assigned_status_of_pair_.Get(pair_index); + if (!status.pickup || !status.delivery) continue; } + if (is_pickup) { + pair_is_open_.Set(pair_index); + ++num_opened_pairs; + } else { + if (!pair_is_open_[pair_index]) return false; + pair_is_open_.Clear(pair_index); + --num_opened_pairs; } } + // For all visited pickup/delivery where both sides are assigned, + // the whole pair must be visited. + if (num_opened_pairs > 0) return false; + pair_is_open_.NotifyAllClear(); return true; } -template -bool PickupDeliveryFilter::AcceptPathOrdered(int64_t path_start) { +template +bool PickupDeliveryFilter::AcceptPathOrdered(int path) { visited_deque_.clear(); - int64_t node = path_start; - int64_t path_length = 1; - while (node < Size()) { - // Detect sub-cycles (path is longer than longest possible path). - if (path_length > Size()) { - return false; + for (const int node : path_state_->Nodes(path)) { + const auto [is_paired, is_pickup, pair_index] = pair_info_of_node_[node]; + if (!is_paired) continue; + if constexpr (check_assigned_pairs) { + const PairStatus status = assigned_status_of_pair_.Get(pair_index); + if (!status.pickup || !status.delivery) continue; } - if (pair_firsts_[node] != kUnassigned) { - if (lifo) { - visited_deque_.push_back(node); + if (is_pickup) { + visited_deque_.emplace_back(pair_index); } else { - visited_deque_.push_front(node); - } - } - if (pair_seconds_[node] != kUnassigned) { - bool found_first = false; - bool some_synced = false; - for (int first : pairs_[pair_seconds_[node]].pickup_alternatives) { - if (!visited_deque_.empty() && visited_deque_.back() == first) { - found_first = true; - break; - } - if (IsVarSynced(first)) { - some_synced = true; - } - } - if (!found_first && some_synced) { - return false; - } else if (!visited_deque_.empty()) { + if (visited_deque_.empty()) return false; + if constexpr (lifo) { + const int last_pair_index = visited_deque_.back(); + if (last_pair_index != pair_index) return false; visited_deque_.pop_back(); + } else { + const int first_pair_index = visited_deque_.front(); + if (first_pair_index != pair_index) return false; + visited_deque_.pop_front(); } } - const int64_t next = GetNext(node); - if (next == kUnassigned) { - // LNS detected, return true since path was ok up to now. - return true; - } - node = next; - ++path_length; - } - while (!visited_deque_.empty()) { - for (int second : - pairs_[pair_firsts_[visited_deque_.back()]].delivery_alternatives) { - if (IsVarSynced(second)) { - return false; - } } - visited_deque_.pop_back(); - } - return true; + return visited_deque_.empty(); } } // namespace -IntVarLocalSearchFilter* MakePickupDeliveryFilter( - const RoutingModel& routing_model, +LocalSearchFilter* MakePickupDeliveryFilter( + const RoutingModel& routing_model, const PathState* path_state, const std::vector& pairs, const std::vector& vehicle_policies) { - return routing_model.solver()->RevAlloc(new PickupDeliveryFilter( - routing_model.Nexts(), routing_model.Size() + routing_model.vehicles(), - routing_model.GetPathsMetadata(), pairs, vehicle_policies)); + return routing_model.solver()->RevAlloc( + new PickupDeliveryFilter(path_state, pairs, vehicle_policies)); } namespace { // Vehicle variable filter -class VehicleVarFilter : public BasePathFilter { +class VehicleVarFilter : public LocalSearchFilter { public: - explicit VehicleVarFilter(const RoutingModel& routing_model); - ~VehicleVarFilter() override = default; - bool AcceptPath(int64_t path_start, int64_t chain_start, - int64_t chain_end) override; + VehicleVarFilter(const RoutingModel& routing_model, + const PathState* path_state); + bool Accept(const Assignment* delta, const Assignment* deltadelta, + int64_t objective_min, int64_t objective_max) override; + void Synchronize(const Assignment* /*assignment*/, + const Assignment* /*delta*/) override; std::string DebugString() const override { return "VehicleVariableFilter"; } private: - bool DisableFiltering() const override; - bool IsVehicleVariableConstrained(int index) const; + bool HasConstrainedVehicleVars() const; - std::vector start_to_vehicle_; + const PathState* path_state_; std::vector vehicle_vars_; - const int64_t unconstrained_vehicle_var_domain_size_; - SparseBitset touched_; + const int num_vehicles_; + bool is_disabled_; }; -VehicleVarFilter::VehicleVarFilter(const RoutingModel& routing_model) - : BasePathFilter(routing_model.Nexts(), - routing_model.Size() + routing_model.vehicles(), - routing_model.GetPathsMetadata()), +VehicleVarFilter::VehicleVarFilter(const RoutingModel& routing_model, + const PathState* path_state) + : path_state_(path_state), vehicle_vars_(routing_model.VehicleVars()), - unconstrained_vehicle_var_domain_size_(routing_model.vehicles()), - touched_(routing_model.Nexts().size()) { - start_to_vehicle_.resize(Size(), -1); - for (int i = 0; i < routing_model.vehicles(); ++i) { - start_to_vehicle_[routing_model.Start(i)] = i; + num_vehicles_(routing_model.vehicles()), + is_disabled_(!HasConstrainedVehicleVars()) {} + +bool VehicleVarFilter::HasConstrainedVehicleVars() const { + for (const IntVar* var : vehicle_vars_) { + const int unconstrained_size = num_vehicles_ + ((var->Min() >= 0) ? 0 : 1); + if (var->Size() != unconstrained_size) return true; } + return false; } -bool VehicleVarFilter::AcceptPath(int64_t path_start, int64_t chain_start, - int64_t chain_end) { - touched_.SparseClearAll(); - const int64_t vehicle = start_to_vehicle_[path_start]; - int64_t node = chain_start; - while (node != chain_end) { - if (touched_[node] || !vehicle_vars_[node]->Contains(vehicle)) { - return false; +void VehicleVarFilter::Synchronize(const Assignment* /*assignment*/, + const Assignment* /*delta*/) { + is_disabled_ = !HasConstrainedVehicleVars(); } - touched_.Set(node); - node = GetNext(node); + +bool VehicleVarFilter::Accept(const Assignment* /*delta*/, + const Assignment* /*deltadelta*/, + int64_t /*objective_min*/, + int64_t /*objective_max*/) { + if (is_disabled_) return true; + for (const int path : path_state_->ChangedPaths()) { + // First and last chain are committed on the vehicle, no need to check. + for (const PathState::Chain chain : + path_state_->Chains(path).DropFirstChain().DropLastChain()) { + for (const int node : chain) { + if (!vehicle_vars_[node]->Contains(path)) return false; } - return vehicle_vars_[node]->Contains(vehicle); } - -bool VehicleVarFilter::DisableFiltering() const { - for (int i = 0; i < vehicle_vars_.size(); ++i) { - if (IsVehicleVariableConstrained(i)) return false; } return true; } -bool VehicleVarFilter::IsVehicleVariableConstrained(int index) const { - const IntVar* const vehicle_var = vehicle_vars_[index]; - // If vehicle variable contains -1 (optional node), then we need to - // add it to the "unconstrained" domain. Impact we don't filter mandatory - // nodes made inactive here, but it is covered by other filters. - const int adjusted_unconstrained_vehicle_var_domain_size = - vehicle_var->Min() >= 0 ? unconstrained_vehicle_var_domain_size_ - : unconstrained_vehicle_var_domain_size_ + 1; - return vehicle_var->Size() != adjusted_unconstrained_vehicle_var_domain_size; -} - } // namespace -IntVarLocalSearchFilter* MakeVehicleVarFilter( - const RoutingModel& routing_model) { - return routing_model.solver()->RevAlloc(new VehicleVarFilter(routing_model)); +LocalSearchFilter* MakeVehicleVarFilter(const RoutingModel& routing_model, + const PathState* path_state) { + return routing_model.solver()->RevAlloc( + new VehicleVarFilter(routing_model, path_state)); } namespace { @@ -2742,31 +2782,43 @@ bool LPCumulFilter::Accept(const Assignment* delta, if (!filter_objective_cost_) { // No need to compute the cost of the LP, only verify its feasibility. delta_cost_without_transit_ = 0; - const DimensionSchedulingStatus status = lp_optimizer_.ComputeCumuls( + DimensionSchedulingStatus status = lp_optimizer_.ComputeCumuls( next_accessor, {}, nullptr, nullptr, nullptr); - if (status == DimensionSchedulingStatus::OPTIMAL) return true; - if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY && - mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr, - nullptr) == - DimensionSchedulingStatus::OPTIMAL) { - return true; + if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { + status = mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr, + nullptr); } - return false; + DCHECK(status != DimensionSchedulingStatus::FEASIBLE) + << "FEASIBLE without filtering objective cost should be OPTIMAL"; + return status == DimensionSchedulingStatus::OPTIMAL; } - const DimensionSchedulingStatus status = + DimensionSchedulingStatus status = lp_optimizer_.ComputeCumulCostWithoutFixedTransits( next_accessor, &delta_cost_without_transit_); - if (status == DimensionSchedulingStatus::INFEASIBLE) { - delta_cost_without_transit_ = std::numeric_limits::max(); - return false; + + if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY || + status == DimensionSchedulingStatus::FEASIBLE) { + const DimensionSchedulingStatus lp_status = status; + int64_t mp_cost; + status = mp_optimizer_.ComputeCumulCostWithoutFixedTransits(next_accessor, + &mp_cost); + if (lp_status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY && + status == DimensionSchedulingStatus::OPTIMAL) { + // TRICKY: If the MP is only feasible, the computed cost isn't a lower + // bound to the problem, so we keep the LP relaxation's lower bound + // found by Glop. + delta_cost_without_transit_ = mp_cost; + } else if (lp_status == DimensionSchedulingStatus::FEASIBLE && + status != DimensionSchedulingStatus::INFEASIBLE) { + // TRICKY: Since feasible costs are not lower bounds, we keep the lowest + // of the costs between the LP-feasible and CP-SAT (feasible or optimal). + delta_cost_without_transit_ = + std::min(delta_cost_without_transit_, mp_cost); + } } - if (delta_cost_without_transit_ > objective_max) return false; - if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY && - mp_optimizer_.ComputeCumulCostWithoutFixedTransits( - next_accessor, &delta_cost_without_transit_) != - DimensionSchedulingStatus::OPTIMAL) { + if (status == DimensionSchedulingStatus::INFEASIBLE) { delta_cost_without_transit_ = std::numeric_limits::max(); return false; } @@ -2796,22 +2848,17 @@ void LPCumulFilter::OnSynchronize(const Assignment* /*delta*/) { next_accessor, &synchronized_cost_without_transit_) : lp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr, nullptr); - if (status == DimensionSchedulingStatus::INFEASIBLE) { - // TODO(user): This should only happen if the LP solver times out. - // DCHECK the fail wasn't due to an infeasible model. - synchronized_cost_without_transit_ = 0; - } if (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY) { status = filter_objective_cost_ ? mp_optimizer_.ComputeCumulCostWithoutFixedTransits( next_accessor, &synchronized_cost_without_transit_) : mp_optimizer_.ComputeCumuls(next_accessor, {}, nullptr, nullptr, nullptr); - if (status != DimensionSchedulingStatus::OPTIMAL) { - // TODO(user): This should only happen if the MP solver times out. + } + if (status == DimensionSchedulingStatus::INFEASIBLE) { + // TODO(user): This should only happen if the LP/MIP solver times out. // DCHECK the fail wasn't due to an infeasible model. synchronized_cost_without_transit_ = 0; - } } } @@ -2987,9 +3034,10 @@ bool ResourceGroupAssignmentFilter::FinalizeAcceptPath( continue; } if (!ComputeVehicleToResourceClassAssignmentCosts( - vehicle, resource_group_, ignored_resources_per_class_, - next_accessor, dimension_.transit_evaluator(vehicle), - filter_objective_cost_, lp_optimizer_, mp_optimizer_, + vehicle, /*solve_duration_ratio=*/1.0, resource_group_, + ignored_resources_per_class_, next_accessor, + dimension_.transit_evaluator(vehicle), filter_objective_cost_, + lp_optimizer_, mp_optimizer_, &delta_vehicle_to_resource_class_assignment_costs_[vehicle], nullptr, nullptr)) { return false; @@ -3061,8 +3109,10 @@ void ResourceGroupAssignmentFilter::OnSynchronizePathFromStart(int64_t start) { // vehicle requiring resource assignment to keep track of whether or not a // given vehicle-to-resource-class assignment is possible by storing 0 or -1 // in vehicle_to_resource_class_assignment_costs_. + // TODO(user): Adjust the 'solve_duration_ratio' below. if (!ComputeVehicleToResourceClassAssignmentCosts( - v, resource_group_, ignored_resources_per_class_, next_accessor, + v, /*solve_duration_ratio=*/1.0, resource_group_, + ignored_resources_per_class_, next_accessor, dimension_.transit_evaluator(v), filter_objective_cost_, lp_optimizer_, mp_optimizer_, &vehicle_to_resource_class_assignment_costs_[v], nullptr, nullptr)) { @@ -3145,21 +3195,22 @@ ResourceGroupAssignmentFilter::ComputeRouteCumulCostWithoutResourceAssignment( int64_t route_cost = 0; const DimensionSchedulingStatus status = lp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, next_accessor, resource, + vehicle, /*solve_duration_ratio=*/1.0, next_accessor, resource, filter_objective_cost_ ? &route_cost : nullptr); switch (status) { case DimensionSchedulingStatus::INFEASIBLE: return -1; case DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY: if (mp_optimizer_->ComputeRouteCumulCostWithoutFixedTransits( - vehicle, next_accessor, resource, + vehicle, /*solve_duration_ratio=*/1.0, next_accessor, resource, filter_objective_cost_ ? &route_cost : nullptr) == DimensionSchedulingStatus::INFEASIBLE) { return -1; } break; default: - DCHECK(status == DimensionSchedulingStatus::OPTIMAL); + DCHECK(status == DimensionSchedulingStatus::OPTIMAL || + status == DimensionSchedulingStatus::FEASIBLE); } return route_cost; } @@ -3365,9 +3416,14 @@ PathState::PathState(int num_nodes, std::vector path_start, for (int p = 0; p < num_paths_; ++p) { path_start_end_.push_back({path_start[p], path_end[p]}); } + Reset(); +} + +void PathState::Reset() { + is_invalid_ = false; // Initial state is all unperformed: paths go from start to end directly. committed_index_.assign(num_nodes_, -1); - committed_paths_.assign(num_nodes_, -1); + committed_paths_.assign(num_nodes_, kUnassigned); committed_nodes_.assign(2 * num_paths_, -1); chains_.assign(num_paths_ + 1, {-1, -1}); // Reserve 1 more for sentinel. paths_.assign(num_paths_, {-1, -1}); @@ -3387,7 +3443,8 @@ PathState::PathState(int num_nodes, std::vector path_start, paths_[path] = {path, path + 1}; } chains_[num_paths_] = {0, 0}; // Sentinel. - // Nodes that are not starts or ends are loops. + // Nodes that are not starts or ends are not in any path, but they still need + // to be represented in the committed state. for (int node = 0; node < num_nodes_; ++node) { if (committed_index_[node] != -1) continue; // node is start or end. committed_index_[node] = committed_nodes_.size(); @@ -3420,7 +3477,9 @@ void PathState::ChangePath(int path, absl::Span chains) { void PathState::ChangeLoops(absl::Span new_loops) { for (const int loop : new_loops) { - if (Path(loop) == -1) continue; + // If the node was already a loop, do not add it. + // If it was not assigned, it becomes a loop. + if (Path(loop) == kLoop) continue; changed_loops_.push_back(loop); } } @@ -3476,10 +3535,10 @@ void PathState::IncrementalCommit() { const int node = committed_nodes_[i]; committed_index_[node] = i; } - // New loops stay in place: only change their path to -1, + // New loops stay in place: only change their path to kLoop, // committed_index_ does not change. for (const int loop : ChangedLoops()) { - committed_paths_[loop] = -1; + committed_paths_[loop] = kLoop; } // Committed part of the state is set up, erase incremental changes. Revert(); @@ -3509,7 +3568,9 @@ void PathState::FullCommit() { if (committed_index_[node] != kUnindexed) continue; committed_index_[node] = index++; committed_nodes_.push_back(node); - committed_paths_[node] = -1; + } + for (const int loop : ChangedLoops()) { + committed_paths_[loop] = kLoop; } // Committed part of the state is set up, erase incremental changes. Revert(); @@ -3556,9 +3617,6 @@ class PathStateFilter : public LocalSearchFilter { // Map IntVar* index to node, offset by the min index in nexts. std::vector variable_index_to_node_; int index_offset_; - // Used only in Reset(), class member status avoids reallocations. - std::vector node_is_assigned_; - std::vector loops_; // Used in CutChains(), class member status avoids reallocations. std::vector changed_paths_; @@ -3614,27 +3672,7 @@ void PathStateFilter::Relax(const Assignment* delta, const Assignment*) { CutChains(); } -void PathStateFilter::Reset() { - path_state_->Revert(); - // Set all paths of path state to empty start -> end paths, - // and all nonstart/nonend nodes to node -> node loops. - const int num_nodes = path_state_->NumNodes(); - node_is_assigned_.assign(num_nodes, false); - loops_.clear(); - const int num_paths = path_state_->NumPaths(); - for (int path = 0; path < num_paths; ++path) { - const auto [start_index, end_index] = path_state_->CommittedPathRange(path); - path_state_->ChangePath( - path, {{start_index, start_index + 1}, {end_index - 1, end_index}}); - node_is_assigned_[path_state_->Start(path)] = true; - node_is_assigned_[path_state_->End(path)] = true; - } - for (int node = 0; node < num_nodes; ++node) { - if (!node_is_assigned_[node]) loops_.push_back(node); - } - path_state_->ChangeLoops(loops_); - path_state_->Commit(); -} +void PathStateFilter::Reset() { path_state_->Reset(); } // The solver does not guarantee that a given Commit() corresponds to // the previous Relax() (or that there has been a call to Relax()), @@ -3666,14 +3704,14 @@ void PathStateFilter::CutChains() { const int next_index = path_state_->CommittedIndex(next); const int node_path = path_state_->Path(node); if (next != node && - (next_index != node_index + 1 || node_path == -1)) { // New arc. + (next_index != node_index + 1 || node_path < 0)) { // New arc. tail_head_indices_.push_back({node_index, next_index}); changed_arcs_[num_changed_arcs++] = {node, next}; - if (node_path != -1 && !path_has_changed_[node_path]) { + if (node_path >= 0 && !path_has_changed_[node_path]) { path_has_changed_[node_path] = true; changed_paths_.push_back(node_path); } - } else if (node == next && node_path != -1) { // New loop. + } else if (node == next && node_path != PathState::kLoop) { // New loop. changed_loops_.push_back(node); } } @@ -3923,7 +3961,7 @@ bool DimensionChecker::Check() const { const int last_index = index_[last_node]; const int chain_path = path_state_->Path(first_node); const int chain_path_class = - chain_path == -1 ? -1 : path_class_[chain_path]; + chain_path < 0 ? -1 : path_class_[chain_path]; // Use a RIQ if the chain size is large enough; // the optimal size was found with the associated benchmark in tests, // in particular BM_DimensionChecker. @@ -4511,7 +4549,7 @@ int64_t PathEnergyCostChecker::ComputePathCost(int64_t path) const { // Add force needed to go from chain.First() to chain.Last(). const int chain_path = path_state_->Path(chain.First()); const int chain_force_class = - chain_path == -1 ? -1 : force_class_[chain_path]; + chain_path < 0 ? -1 : force_class_[chain_path]; const bool force_is_cached = chain_force_class == path_force_class; if (force_is_cached && chain.NumNodes() >= 2) { const int first_index = force_rmq_index_of_node_[chain.First()]; @@ -4570,9 +4608,9 @@ int64_t PathEnergyCostChecker::ComputePathCost(int64_t path) const { // costly calls to evaluators. const int chain_path = path_state_->Path(chain.First()); const int chain_force_class = - chain_path == -1 ? -1 : force_class_[chain_path]; + chain_path < 0 ? -1 : force_class_[chain_path]; const int chain_distance_class = - chain_path == -1 ? -1 : distance_class_[chain_path]; + chain_path < 0 ? -1 : distance_class_[chain_path]; const bool force_is_cached = chain_force_class == path_force_class; const bool distance_is_cached = chain_distance_class == path_distance_class; diff --git a/ortools/constraint_solver/routing_filters.h b/ortools/constraint_solver/routing_filters.h index 779a84c2c4d..1a0f3f211f6 100644 --- a/ortools/constraint_solver/routing_filters.h +++ b/ortools/constraint_solver/routing_filters.h @@ -14,8 +14,6 @@ #ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTERS_H_ #define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_FILTERS_H_ -#include -#include #include #include #include @@ -23,500 +21,21 @@ #include #include -#include "absl/log/check.h" #include "absl/strings/string_view.h" #include "absl/types/span.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" #include "ortools/constraint_solver/routing.h" +#include "ortools/constraint_solver/routing_filter_committables.h" #include "ortools/constraint_solver/routing_lp_scheduling.h" #include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/constraint_solver/routing_types.h" #include "ortools/util/bitset.h" #include "ortools/util/range_minimum_query.h" -#include "ortools/util/saturated_arithmetic.h" namespace operations_research { -// A vector that allows to revert back to a previously committed state, -// get the set of changed indices, and get current and committed values. -template -class CommittableValue { - public: - explicit CommittableValue(const T& value) - : current_(value), committed_(value) {} - - const T& Get() const { return current_; } - const T& GetCommitted() const { return committed_; } - - void Set(const T& value) { current_ = value; } - - void SetAndCommit(const T& value) { - Set(value); - Commit(); - } - - void Revert() { current_ = committed_; } - - void Commit() { committed_ = current_; } - - private: - T current_; - T committed_; -}; - -template -class CommittableVector { - public: - // Makes a vector with initial elements all committed to value. - CommittableVector(size_t num_elements, const T& value) - : elements_(num_elements, {value, value}), changed_(num_elements) {} - - // Return the size of the vector. - size_t Size() const { return elements_.size(); } - - // Returns a copy of the value stored at index in the current state. - // Does not return a reference, because the class needs to know when elements - // are modified. - T Get(size_t index) const { - DCHECK_LT(index, elements_.size()); - return elements_[index].current; - } - - // Set the value stored at index in the current state to given value. - void Set(size_t index, const T& value) { - DCHECK_GE(index, 0); - DCHECK_LT(index, elements_.size()); - changed_.Set(index); - elements_[index].current = value; - } - - // Changes the values of the vector to those in the last Commit(). - void Revert() { - for (const size_t index : changed_.PositionsSetAtLeastOnce()) { - elements_[index].current = elements_[index].committed; - } - changed_.ClearAll(); - } - - // Makes the current state committed, clearing all changes. - void Commit() { - for (const size_t index : changed_.PositionsSetAtLeastOnce()) { - elements_[index].committed = elements_[index].current; - } - changed_.ClearAll(); - } - - // Sets all elements of this vector to given value, and commits to this state. - // Supposes that there are no changes since the last Commit() or Revert(). - void SetAllAndCommit(const T& value) { - DCHECK_EQ(0, changed_.NumberOfSetCallsWithDifferentArguments()); - elements_.assign(elements_.size(), {value, value}); - } - - // Returns a copy of the value stored at index in the last committed state. - T GetCommitted(size_t index) const { - DCHECK_LT(index, elements_.size()); - return elements_[index].committed; - } - - // Return true iff the value at index has been Set() since the last Commit() - // or Revert(), even if the current value is the same as the committed value. - bool HasChanged(size_t index) const { return changed_[index]; } - - // Returns the set of indices that have been Set() since the last Commit() or - // Revert(). - const std::vector& ChangedIndices() const { - return changed_.PositionsSetAtLeastOnce(); - } - - private: - struct VersionedElement { - T current; - T committed; - }; - // Holds current and committed versions of values of this vector. - std::vector elements_; - // Holds indices that were Set() since the last Commit() or Revert(). - SparseBitset changed_; -}; - -// This class allows to represent a state of dimension values for all paths of -// a vehicle routing problem. Values of interest for each path are: -// - nodes, -// - cumuls (min/max), -// - transit times, -// - sum of transit times since the beginning of the path, -// - span (min/max). -// -// This class can maintain two states at once: a committed state and a current -// state. The current state can be modified by first describing a path p to be -// modified with PushNode() and MakePathFromNewNodes(). Then the dimension -// values of this path can be modified with views returned by MutableXXX() -// methods. -// -// When a set of paths has been modified, the caller can decide to definitely -// change the committed state to the new state, or to revert to the committed -// state. -// -// Operations are meant to be efficient: -// - all path modifications, i.e. PushNode(), MakePathFromNewNodes(), -// MutableXXX(), MutableSpan() operations are O(1). -// - Revert() is O(num changed paths). -// - Commit() has two behaviors: -// - if there are less than max_num_committed_elements_ elements in the -// committed state, then Commit() is O(num changed paths). -// - otherwise, Commit() does a compaction of the committed state, in -// O(num_nodes + num_paths). -// The amortized cost of Commit(), when taking modifications into account, -// is O(size of changed paths), because all modifications pay at worst -// O(1) for its own compaction. -// -// Note that this class does not support the semantics associated with its -// fields names, for instance it does not make sure that cumul_min <= cumul_max. -// The field names are meant for readability for the user. -// However, path sizes are enforced: if a path has n nodes, then it has -// n fields for cumul min/max, n for transit_sums, and max(0, n-1) for transits. -class DimensionValues { - public: - DimensionValues(int num_paths, int num_nodes) - : range_of_path_(num_paths, {.begin = 0, .end = 0}), - committed_range_of_path_(num_paths, {.begin = 0, .end = 0}), - span_(num_paths, Interval::AllIntegers()), - committed_span_(num_paths, Interval::AllIntegers()), - vehicle_breaks_(num_paths), - committed_vehicle_breaks_(num_paths), - changed_paths_(num_paths), - max_num_committed_elements_(16 * num_nodes) { - nodes_.reserve(max_num_committed_elements_); - transit_.reserve(max_num_committed_elements_); - travel_.reserve(max_num_committed_elements_); - travel_sum_.reserve(max_num_committed_elements_); - cumul_.reserve(max_num_committed_elements_); - } - - struct Interval { - int64_t min; - int64_t max; - // Tests inequality between intervals. - bool operator!=(const Interval& other) const { - return min != other.min || max != other.max; - } - // Tests equality between intervals. - bool operator==(const Interval& other) const { - return min == other.min && max == other.max; - } - // Returns true iff the interval is empty. - bool IsEmpty() const { return min > max; } - // Increases the min to be at least lower_bound, - // returns true iff the interval is nonempty. - bool IncreaseMin(int64_t lower_bound) { - min = std::max(min, lower_bound); - return min <= max; - } - // Decreases the max to be at most upper_bound, - // returns true iff the interval is nonempty. - bool DecreaseMax(int64_t upper_bound) { - max = std::min(max, upper_bound); - return min <= max; - } - // Intersects this interval with the other, returns true iff the interval - // is nonempty. - bool IntersectWith(const Interval& other) { - min = std::max(min, other.min); - max = std::min(max, other.max); - return min <= max; - } - // A set addition, with intervals: adds other.min to the min, other.max to - // the max, with CapAdd(). - void Add(const Interval& other) { - DCHECK(!IsEmpty()); - DCHECK(!other.IsEmpty()); - min = CapAdd(min, other.min); - max = CapAdd(max, other.max); - } - // A set subtraction, with intervals: subtracts other.max from the min, - // other.min from the max, with CapSub(). - void Subtract(const Interval& other) { - DCHECK(!IsEmpty()); - DCHECK(!other.IsEmpty()); - min = CapSub(min, other.max); - max = CapSub(max, other.min); - } - // Returns an interval containing all integers: {kint64min, kint64max}. - static Interval AllIntegers() { - return {.min = kint64min, .max = kint64max}; - } - }; - - struct VehicleBreak { - Interval start; - Interval end; - Interval duration; - Interval is_performed; - bool operator==(const VehicleBreak& other) const { - return start == other.start && end == other.end && - duration == other.duration && is_performed == other.is_performed; - } - }; - - // Adds a node to new nodes. - void PushNode(int node) { nodes_.push_back(node); } - - // Turns new nodes into a new path, allocating dimension values for it. - void MakePathFromNewNodes(int path) { - DCHECK_GE(path, 0); - DCHECK_LT(path, range_of_path_.size()); - DCHECK(!changed_paths_[path]); - range_of_path_[path] = {.begin = num_current_elements_, - .end = nodes_.size()}; - changed_paths_.Set(path); - // Allocate dimension values. We allocate n cells for all dimension values, - // even transits, so they can all be indexed by the same range_of_path. - transit_.resize(nodes_.size(), Interval::AllIntegers()); - travel_.resize(nodes_.size(), 0); - travel_sum_.resize(nodes_.size(), 0); - cumul_.resize(nodes_.size(), Interval::AllIntegers()); - num_current_elements_ = nodes_.size(); - span_[path] = Interval::AllIntegers(); - } - - // Resets all path to empty, in both committed and current state. - void Reset() { - const int num_paths = range_of_path_.size(); - range_of_path_.assign(num_paths, {.begin = 0, .end = 0}); - committed_range_of_path_.assign(num_paths, {.begin = 0, .end = 0}); - changed_paths_.SparseClearAll(); - num_current_elements_ = 0; - num_committed_elements_ = 0; - nodes_.clear(); - transit_.clear(); - travel_.clear(); - travel_sum_.clear(); - cumul_.clear(); - committed_span_.assign(num_paths, Interval::AllIntegers()); - } - - // Clears the changed state, make it point to the committed state. - void Revert() { - for (const int path : changed_paths_.PositionsSetAtLeastOnce()) { - range_of_path_[path] = committed_range_of_path_[path]; - } - changed_paths_.SparseClearAll(); - num_current_elements_ = num_committed_elements_; - nodes_.resize(num_current_elements_); - transit_.resize(num_current_elements_); - travel_.resize(num_current_elements_); - travel_sum_.resize(num_current_elements_); - cumul_.resize(num_current_elements_); - } - - // Makes the committed state point to the current state. - // If the state representation is too large, reclaims memory by compacting - // the committed state. - void Commit() { - for (const int path : changed_paths_.PositionsSetAtLeastOnce()) { - committed_range_of_path_[path] = range_of_path_[path]; - committed_span_[path] = span_[path]; - committed_vehicle_breaks_[path] = vehicle_breaks_[path]; - } - changed_paths_.SparseClearAll(); - num_committed_elements_ = num_current_elements_; - // If the committed data would take too much space, compact the data: - // copy committed data to the end of vectors, erase old data, refresh - // indexing (range_of_path_). - if (num_current_elements_ <= max_num_committed_elements_) return; - temp_nodes_.clear(); - temp_transit_.clear(); - temp_travel_.clear(); - temp_travel_sum_.clear(); - temp_cumul_.clear(); - for (int path = 0; path < range_of_path_.size(); ++path) { - if (committed_range_of_path_[path].Size() == 0) continue; - const size_t new_begin = temp_nodes_.size(); - const auto [begin, end] = committed_range_of_path_[path]; - temp_nodes_.insert(temp_nodes_.end(), nodes_.begin() + begin, - nodes_.begin() + end); - temp_transit_.insert(temp_transit_.end(), transit_.begin() + begin, - transit_.begin() + end); - temp_travel_.insert(temp_travel_.end(), travel_.begin() + begin, - travel_.begin() + end); - temp_travel_sum_.insert(temp_travel_sum_.end(), - travel_sum_.begin() + begin, - travel_sum_.begin() + end); - temp_cumul_.insert(temp_cumul_.end(), cumul_.begin() + begin, - cumul_.begin() + end); - committed_range_of_path_[path] = {.begin = new_begin, - .end = temp_nodes_.size()}; - } - std::swap(nodes_, temp_nodes_); - std::swap(transit_, temp_transit_); - std::swap(travel_, temp_travel_); - std::swap(travel_sum_, temp_travel_sum_); - std::swap(cumul_, temp_cumul_); - range_of_path_ = committed_range_of_path_; - num_committed_elements_ = nodes_.size(); - num_current_elements_ = nodes_.size(); - } - - // Returns a const view of the nodes of the path, in the committed state. - absl::Span CommittedNodes(int path) const { - const auto [begin, end] = committed_range_of_path_[path]; - return absl::MakeConstSpan(nodes_.data() + begin, nodes_.data() + end); - } - - // Returns a const view of the nodes of the path, in the current state. - absl::Span Nodes(int path) const { - const auto [begin, end] = range_of_path_[path]; - return absl::MakeConstSpan(nodes_.data() + begin, nodes_.data() + end); - } - - // Returns a const view of the transits of the path, in the current state. - absl::Span Transits(int path) const { - auto [begin, end] = range_of_path_[path]; - // When the path is not empty, #transits = #nodes - 1. - // When the path is empty, begin = end, return empty span. - if (begin < end) --end; - return absl::MakeConstSpan(transit_.data() + begin, transit_.data() + end); - } - - // Returns a mutable view of the transits of the path, in the current state. - absl::Span MutableTransits(int path) { - auto [begin, end] = range_of_path_[path]; - // When the path is not empty, #transits = #nodes - 1. - // When the path is empty, begin = end, return empty span. - if (begin < end) --end; - return absl::MakeSpan(transit_.data() + begin, transit_.data() + end); - } - - // Returns a const view of the travels of the path, in the current - // state. - absl::Span Travels(int path) const { - auto [begin, end] = range_of_path_[path]; - if (begin < end) --end; - return absl::MakeConstSpan(travel_.data() + begin, travel_.data() + end); - } - - // Returns a mutable view of the travels of the path, in the current - // state. - absl::Span MutableTravels(int path) { - auto [begin, end] = range_of_path_[path]; - if (begin < end) --end; - return absl::MakeSpan(travel_.data() + begin, travel_.data() + end); - } - - // Returns a const view of the travel sums of the path, in the current state. - absl::Span TravelSums(int path) const { - const auto [begin, end] = range_of_path_[path]; - return absl::MakeConstSpan(travel_sum_.data() + begin, - travel_sum_.data() + end); - } - - // Returns a mutable view of the travel sums of the path in the current state. - absl::Span MutableTravelSums(int path) { - const auto [begin, end] = range_of_path_[path]; - return absl::MakeSpan(travel_sum_.data() + begin, travel_sum_.data() + end); - } - - // Returns a const view of the cumul mins of the path, in the current state. - absl::Span Cumuls(int path) const { - const auto [begin, end] = range_of_path_[path]; - return absl::MakeConstSpan(cumul_.data() + begin, cumul_.data() + end); - } - - // Returns a mutable view of the cumul mins of the path, in the current state. - absl::Span MutableCumuls(int path) { - const auto [begin, end] = range_of_path_[path]; - return absl::MakeSpan(cumul_.data() + begin, cumul_.data() + end); - } - - // Returns the span interval of the path, in the current state. - Interval Span(int path) const { - return changed_paths_[path] ? span_[path] : committed_span_[path]; - } - // Returns a mutable view of the span of the path, in the current state. - // The path must have been changed since the last commit. - Interval& MutableSpan(int path) { - DCHECK(changed_paths_[path]); - return span_[path]; - } - - // Returns a const view of the vehicle breaks of the path, in the current - // state. - absl::Span VehicleBreaks(int path) const { - return absl::MakeConstSpan(changed_paths_[path] - ? vehicle_breaks_[path] - : committed_vehicle_breaks_[path]); - } - - // Returns a mutable vector of the vehicle breaks of the path, in the current - // state. The path must have been changed since the last commit. - std::vector& MutableVehicleBreaks(int path) { - DCHECK(changed_paths_[path]); - return vehicle_breaks_[path]; - } - - // Returns the number of nodes of the path, in the current state. - int NumNodes(int path) const { return range_of_path_[path].Size(); } - // Returns a const view of the set of paths changed, in the current state. - absl::Span ChangedPaths() const { - return absl::MakeConstSpan(changed_paths_.PositionsSetAtLeastOnce()); - } - // Returns whether the given path was changed, in the current state. - bool PathHasChanged(int path) const { return changed_paths_[path]; } - - private: - // These vectors hold the data of both committed and current states. - // The ranges below determine which indices are associated to each path and - // each state. It is up to the user to maintain the following invariants: - // If range_of_path_[p] == {.begin = b, .end = e}, then, in the current - // state: - // - nodes_[i] for i in [b, e) are the nodes of the path p. - // - cumul_[r] + transit_[r] == cumul_[r+1] for r in [b, e-1). - // - travel_[r] <= transit_[r].min for r in [b, e-1). - // - travel_sum_[r] == sum_{r' in [0, r')} travel_[r'], for r in [b+1, e) - // - cumul[b] + span_[p] == cumul[e-1]. - // - // The same invariants should hold for committed_range_of_path_ and the - // committed state. - std::vector nodes_; - std::vector transit_; - std::vector travel_; - std::vector travel_sum_; - std::vector cumul_; - // Temporary vectors used in Commit() during compaction. - std::vector temp_nodes_; - std::vector temp_transit_; - std::vector temp_travel_; - std::vector temp_travel_sum_; - std::vector temp_cumul_; - // A path has a range of indices in the committed state and another one in the - // current state. - struct Range { - size_t begin = 0; - size_t end = 0; - int Size() const { return end - begin; } - }; - std::vector range_of_path_; - std::vector committed_range_of_path_; - // Associates span to each path. - std::vector span_; - std::vector committed_span_; - // Associates vehicle breaks with each path. - std::vector> vehicle_breaks_; - std::vector> committed_vehicle_breaks_; - // Stores whether each path has been changed since last committed state. - SparseBitset changed_paths_; - // Threshold for the size of the committed vector. This is purely heuristic: - // it should be more than the number of nodes so compactions do not occur at - // each submit, but ranges should not be too far apart to avoid cache misses. - const size_t max_num_committed_elements_; - // This locates the start of new nodes. - size_t num_current_elements_ = 0; - size_t num_committed_elements_ = 0; -}; - // Propagates vehicle break constraints in dimension_values. // This returns false if breaks cannot fit the path. // Otherwise, this returns true, and modifies the start cumul, end cumul and the @@ -524,7 +43,7 @@ class DimensionValues { // This applies light reasoning, and runs in O(#breaks * #interbreak rules). bool PropagateLightweightVehicleBreaks( int path, DimensionValues& dimension_values, - const std::vector>& interbreaks); + absl::Span> interbreaks); /// Returns a filter tracking route constraints. IntVarLocalSearchFilter* MakeRouteConstraintFilter( @@ -551,17 +70,6 @@ IntVarLocalSearchFilter* MakeVehicleAmortizedCostFilter( IntVarLocalSearchFilter* MakeTypeRegulationsFilter( const RoutingModel& routing_model); -/// Returns a filter enforcing pickup and delivery constraints for the given -/// pair of nodes and given policies. -IntVarLocalSearchFilter* MakePickupDeliveryFilter( - const RoutingModel& routing_model, - const std::vector& pairs, - const std::vector& vehicle_policies); - -/// Returns a filter checking that vehicle variable domains are respected. -IntVarLocalSearchFilter* MakeVehicleVarFilter( - const RoutingModel& routing_model); - /// Returns a filter handling dimension costs and constraints. IntVarLocalSearchFilter* MakePathCumulFilter(const RoutingDimension& dimension, bool propagate_own_objective_value, @@ -667,7 +175,10 @@ class PathState { // State-dependent accessors. - // Returns the committed path of a given node, -1 if it is a loop. + static constexpr int kUnassigned = -2; + static constexpr int kLoop = -1; + // Returns the committed path of a given node, kLoop if it is a loop, + // kUnassigned if it is not assigned, int Path(int node) const { return committed_paths_[node]; } // Returns the set of paths that actually changed, // i.e. that have more than one chain. @@ -706,6 +217,8 @@ class PathState { void Commit(); // Erase incremental changes. See class comment for details. void Revert(); + // Sets all paths to start->end, all other nodes to kUnassigned. + void Reset(); // LNS Operators may not fix variables, // in which case we mark the candidate invalid. @@ -859,6 +372,14 @@ class PathState::ChainRange { Iterator begin() const { return {begin_, first_node_}; } Iterator end() const { return {end_, first_node_}; } + ChainRange DropFirstChain() const { + return (begin_ == end_) ? *this : ChainRange(begin_ + 1, end_, first_node_); + } + + ChainRange DropLastChain() const { + return (begin_ == end_) ? *this : ChainRange(begin_, end_ - 1, first_node_); + } + private: const ChainBounds* const begin_; const ChainBounds* const end_; @@ -929,6 +450,17 @@ LocalSearchFilter* MakePathStateFilter(Solver* solver, std::unique_ptr path_state, const std::vector& nexts); +/// Returns a filter checking that vehicle variable domains are respected. +LocalSearchFilter* MakeVehicleVarFilter(const RoutingModel& routing_model, + const PathState* path_state); + +/// Returns a filter enforcing pickup and delivery constraints for the given +/// pair of nodes and given policies. +LocalSearchFilter* MakePickupDeliveryFilter( + const RoutingModel& routing_model, const PathState* path_state, + const std::vector& pairs, + const std::vector& vehicle_policies); + // This checker enforces dimension requirements. // A dimension requires that there is some valuation of // cumul and demand such that for all paths: @@ -1428,7 +960,6 @@ class BasePathFilter : public IntVarLocalSearchFilter { int64_t End(int i) const { return paths_metadata_.End(i); } int GetPath(int64_t node) const { return paths_metadata_.GetPath(node); } int Rank(int64_t node) const { return ranks_[node]; } - bool IsDisabled() const { return status_ == DISABLED; } const std::vector& GetTouchedPathStarts() const { return touched_paths_.PositionsSetAtLeastOnce(); } @@ -1440,9 +971,6 @@ class BasePathFilter : public IntVarLocalSearchFilter { bool lns_detected() const { return lns_detected_; } private: - enum Status { UNKNOWN, ENABLED, DISABLED }; - - virtual bool DisableFiltering() const { return false; } virtual void OnBeforeSynchronizePaths(bool) {} virtual void OnAfterSynchronizePaths() {} virtual void OnSynchronizePathFromStart(int64_t) {} @@ -1469,7 +997,6 @@ class BasePathFilter : public IntVarLocalSearchFilter { // clang-format on std::vector ranks_; - Status status_; bool lns_detected_; }; diff --git a/ortools/constraint_solver/routing_flow.cc b/ortools/constraint_solver/routing_flow.cc index 3099c4c46d0..a8219780657 100644 --- a/ortools/constraint_solver/routing_flow.cc +++ b/ortools/constraint_solver/routing_flow.cc @@ -287,7 +287,7 @@ bool RoutingModel::SolveMatchingModel( // TODO(user): if the result is RELAXED_OPTIMAL_ONLY, do a // second pass with an MP solver. if (optimizer.ComputeRouteCumulCostWithoutFixedTransits( - vehicle, + vehicle, /*solve_duration_ratio=*/1.0, [&nexts](int64_t node) { return nexts.find(node)->second; }, @@ -312,7 +312,7 @@ bool RoutingModel::SolveMatchingModel( // TODO(user): if the result is RELAXED_OPTIMAL_ONLY, do a // second pass with an MP solver. if (optimizer.ComputeRouteCumulCostWithoutFixedTransits( - vehicle, + vehicle, /*solve_duration_ratio=*/1.0, [&nexts](int64_t node) { return nexts.find(node)->second; }, diff --git a/ortools/constraint_solver/routing_insertion_lns.cc b/ortools/constraint_solver/routing_insertion_lns.cc index 2abae46a56d..3e286c1191e 100644 --- a/ortools/constraint_solver/routing_insertion_lns.cc +++ b/ortools/constraint_solver/routing_insertion_lns.cc @@ -133,16 +133,17 @@ bool FilteredHeuristicLocalSearchOperator::MakeChangesAndInsertNodes() { FilteredHeuristicPathLNSOperator::FilteredHeuristicPathLNSOperator( std::unique_ptr heuristic) : FilteredHeuristicLocalSearchOperator(std::move(heuristic)), + num_routes_(model_->vehicles()), current_route_(0), last_route_(0), just_started_(false) {} void FilteredHeuristicPathLNSOperator::OnStart() { // NOTE: We set last_route_ to current_route_ here to make sure all routes - // are scanned in IncrementCurrentRouteToNextNonEmpty(). + // are scanned in GetFirstNonEmptyRouteAfterCurrentRoute(). last_route_ = current_route_; - if (CurrentRouteIsEmpty()) { - IncrementCurrentRouteToNextNonEmpty(); + if (RouteIsEmpty(current_route_)) { + current_route_ = GetFirstNonEmptyRouteAfterCurrentRoute(); } just_started_ = true; } @@ -150,25 +151,27 @@ void FilteredHeuristicPathLNSOperator::OnStart() { bool FilteredHeuristicPathLNSOperator::IncrementPosition() { if (just_started_) { just_started_ = false; - return !CurrentRouteIsEmpty(); + // If current_route_ is empty or is the only non-empty route, then we don't + // create a new neighbor with this operator as it would result in running + // a first solution heuristic with all the nodes. + return !RouteIsEmpty(current_route_) && + GetFirstNonEmptyRouteAfterCurrentRoute() != last_route_; } - IncrementCurrentRouteToNextNonEmpty(); + current_route_ = GetFirstNonEmptyRouteAfterCurrentRoute(); return current_route_ != last_route_; } -bool FilteredHeuristicPathLNSOperator::CurrentRouteIsEmpty() const { - return model_->IsEnd(OldValue(model_->Start(current_route_))); +bool FilteredHeuristicPathLNSOperator::RouteIsEmpty(int route) const { + return model_->IsEnd(OldValue(model_->Start(route))); } -void FilteredHeuristicPathLNSOperator::IncrementCurrentRouteToNextNonEmpty() { - const int num_routes = model_->vehicles(); - do { - ++current_route_ %= num_routes; - if (current_route_ == last_route_) { - // All routes have been scanned. - return; +int FilteredHeuristicPathLNSOperator::GetFirstNonEmptyRouteAfterCurrentRoute() + const { + int route = GetNextRoute(current_route_); + while (route != last_route_ && RouteIsEmpty(route)) { + route = GetNextRoute(route); } - } while (CurrentRouteIsEmpty()); + return route; } std::function diff --git a/ortools/constraint_solver/routing_insertion_lns.h b/ortools/constraint_solver/routing_insertion_lns.h index cd062e5d4bb..00b90e93a6b 100644 --- a/ortools/constraint_solver/routing_insertion_lns.h +++ b/ortools/constraint_solver/routing_insertion_lns.h @@ -97,11 +97,15 @@ class FilteredHeuristicPathLNSOperator void OnStart() override; bool IncrementPosition() override; - bool CurrentRouteIsEmpty() const; - void IncrementCurrentRouteToNextNonEmpty(); + bool RouteIsEmpty(int route) const; + int GetNextRoute(int route) const { + return route + 1 < num_routes_ ? route + 1 : 0; + } + int GetFirstNonEmptyRouteAfterCurrentRoute() const; std::function SetupNextAccessorForNeighbor() override; + const int num_routes_; int current_route_; int last_route_; bool just_started_; diff --git a/ortools/constraint_solver/routing_lp_scheduling.cc b/ortools/constraint_solver/routing_lp_scheduling.cc index 811f56ec734..3cf189244a4 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.cc +++ b/ortools/constraint_solver/routing_lp_scheduling.cc @@ -27,6 +27,8 @@ #include #include "absl/algorithm/container.h" +#include "absl/container/flat_hash_map.h" +#include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" @@ -36,12 +38,12 @@ #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/mathutil.h" +#include "ortools/base/strong_vector.h" #include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/routing.h" #include "ortools/constraint_solver/routing_parameters.pb.h" #include "ortools/glop/parameters.pb.h" -#include "ortools/graph/ebert_graph.h" #include "ortools/graph/min_cost_flow.h" #include "ortools/port/proto_utils.h" #include "ortools/sat/cp_model.pb.h" @@ -209,12 +211,15 @@ LocalDimensionCumulOptimizer::LocalDimensionCumulOptimizer( } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCost( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, int64_t* optimal_cost) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); int64_t transit_cost = 0; const DimensionSchedulingStatus status = optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, + vehicle, solve_duration_ratio, next_accessor, /*dimension_travel_info=*/nullptr, /*resource=*/nullptr, /*optimize_vehicle_costs=*/optimal_cost != nullptr, @@ -230,11 +235,15 @@ DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCost( DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::ResourceGroup::Resource* resource, int64_t* optimal_cost_without_transits) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, /*dimension_travel_info=*/nullptr, resource, + vehicle, solve_duration_ratio, next_accessor, + /*dimension_travel_info=*/nullptr, resource, /*optimize_vehicle_costs=*/optimal_cost_without_transits != nullptr, solver_[vehicle].get(), /*cumul_values=*/nullptr, /*break_values=*/nullptr, optimal_cost_without_transits, nullptr); @@ -242,69 +251,85 @@ LocalDimensionCumulOptimizer::ComputeRouteCumulCostWithoutFixedTransits( std::vector LocalDimensionCumulOptimizer:: ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, absl::Span resources, absl::Span resource_indices, bool optimize_vehicle_costs, std::vector* optimal_costs_without_transits, std::vector>* optimal_cumuls, std::vector>* optimal_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResources( - vehicle, next_accessor, transit_accessor, nullptr, resources, - resource_indices, optimize_vehicle_costs, solver_[vehicle].get(), - optimal_cumuls, optimal_breaks, optimal_costs_without_transits, nullptr); + vehicle, solve_duration_ratio, next_accessor, transit_accessor, nullptr, + resources, resource_indices, optimize_vehicle_costs, + solver_[vehicle].get(), optimal_cumuls, optimal_breaks, + optimal_costs_without_transits, nullptr); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* optimal_cumuls, std::vector* optimal_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, dimension_travel_info, resource, - /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls, - optimal_breaks, /*cost_without_transit=*/nullptr, + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + resource, /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), + optimal_cumuls, optimal_breaks, /*cost_without_transit=*/nullptr, /*transit_cost=*/nullptr); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteCumulsAndCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, std::vector* optimal_cumuls, std::vector* optimal_breaks, int64_t* optimal_cost_without_transits) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeSingleRouteWithResource( - vehicle, next_accessor, dimension_travel_info, nullptr, - /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), optimal_cumuls, - optimal_breaks, optimal_cost_without_transits, nullptr); + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + nullptr, /*optimize_vehicle_costs=*/true, solver_[vehicle].get(), + optimal_cumuls, optimal_breaks, optimal_cost_without_transits, nullptr); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputeRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, absl::Span solution_cumul_values, absl::Span solution_break_values, int64_t* solution_cost, int64_t* cost_offset, bool reuse_previous_model_if_possible, bool clear_lp, absl::Duration* solve_duration) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); RoutingLinearSolverWrapper* solver = solver_[vehicle].get(); return optimizer_core_.ComputeSingleRouteSolutionCostWithoutFixedTransits( - vehicle, next_accessor, dimension_travel_info, solver, - solution_cumul_values, solution_break_values, solution_cost, cost_offset, - reuse_previous_model_if_possible, clear_lp, + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + solver, solution_cumul_values, solution_break_values, solution_cost, + cost_offset, reuse_previous_model_if_possible, clear_lp, /*clear_solution_constraints=*/true, solve_duration); } DimensionSchedulingStatus LocalDimensionCumulOptimizer::ComputePackedRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* packed_cumuls, std::vector* packed_breaks) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); return optimizer_core_.OptimizeAndPackSingleRoute( - vehicle, next_accessor, dimension_travel_info, resource, - solver_[vehicle].get(), packed_cumuls, packed_breaks); + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + resource, solver_[vehicle].get(), packed_cumuls, packed_breaks); } const int CumulBoundsPropagator::kNoParent = -2; @@ -424,18 +449,25 @@ bool CumulBoundsPropagator::InitializeArcsAndBounds( } } - for (const RoutingDimension::NodePrecedence& precedence : + for (const auto [first_node, second_node, offset, performed_constraint] : dimension_.GetNodePrecedences()) { - const int first_index = precedence.first_node; - const int second_index = precedence.second_node; - if (lower_bounds[PositiveNode(first_index)] == - std::numeric_limits::min() || - lower_bounds[PositiveNode(second_index)] == - std::numeric_limits::min()) { - // One of the nodes is unperformed, so the precedence rule doesn't apply. + const bool first_node_unperformed = + lower_bounds[PositiveNode(first_node)] == + std::numeric_limits::min(); + const bool second_node_unperformed = + lower_bounds[PositiveNode(second_node)] == + std::numeric_limits::min(); + switch (RoutingDimension::GetPrecedenceStatus(first_node_unperformed, + second_node_unperformed, + performed_constraint)) { + case RoutingDimension::PrecedenceStatus::kActive: + break; + case RoutingDimension::PrecedenceStatus::kInactive: continue; + case RoutingDimension::PrecedenceStatus::kInvalid: + return false; } - AddArcs(first_index, second_index, precedence.offset); + AddArcs(first_node, second_node, offset); } return true; @@ -575,7 +607,8 @@ DimensionCumulOptimizerCore::DimensionCumulOptimizerCore( DimensionSchedulingStatus DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, RoutingLinearSolverWrapper* solver, absl::Span solution_cumul_values, @@ -605,7 +638,7 @@ DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( if (model->CheckLimit()) { return DimensionSchedulingStatus::INFEASIBLE; } - solve_duration_value = model->RemainingTime(); + solve_duration_value = model->RemainingTime() * solve_duration_ratio; if (solve_duration != nullptr) *solve_duration = solve_duration_value; if (cost_offset != nullptr) *cost_offset = cost_offset_value; } else { @@ -614,7 +647,7 @@ DimensionCumulOptimizerCore::ComputeSingleRouteSolutionCostWithoutFixedTransits( cost_offset_value = *cost_offset; CHECK(solve_duration != nullptr) << "Cannot reuse model without the solve_duration"; - solve_duration_value = *solve_duration; + solve_duration_value = *solve_duration * solve_duration_ratio; } // Constrains the cumuls. @@ -686,7 +719,8 @@ void ClearIfNonNull(std::vector* v) { DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeSingleRouteWithResource( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver, @@ -706,9 +740,9 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResource( std::vector> break_values_vec; const std::vector statuses = DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( - vehicle, next_accessor, dimension_->transit_evaluator(vehicle), - dimension_travel_info, resources, resource_indices, - optimize_vehicle_costs, solver, + vehicle, solve_duration_ratio, next_accessor, + dimension_->transit_evaluator(vehicle), dimension_travel_info, + resources, resource_indices, optimize_vehicle_costs, solver, cumul_values != nullptr ? &cumul_values_vec : nullptr, break_values != nullptr ? &break_values_vec : nullptr, cost_without_transits != nullptr ? &costs_without_transits : nullptr, @@ -824,7 +858,8 @@ bool TightenStartEndVariableBoundsWithAssignedResources( std::vector DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, const RouteDimensionTravelInfo* dimension_travel_info, absl::Span resources, @@ -867,9 +902,9 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( // NOTE: When there are no resources to optimize for, we still solve the // optimization problem for the route (without any added resource constraint). - const int num_solves = - std::max(static_cast(1UL), - resource_indices.size()); + // TODO(user): Consider taking 'num_solves' into account to re-adjust the + // solve_duration_ratio to make sure all sub-problems are given enough time. + const int num_solves = resource_indices.empty() ? 1 : resource_indices.size(); if (costs_without_transits != nullptr) { costs_without_transits->assign(num_solves, -1); } @@ -900,7 +935,8 @@ DimensionCumulOptimizerCore::OptimizeSingleRouteWithResources( continue; } - statuses.push_back(solver->Solve(model->RemainingTime())); + statuses.push_back( + solver->Solve(model->RemainingTime() * solve_duration_ratio)); if (statuses.back() == DimensionSchedulingStatus::INFEASIBLE) { continue; } @@ -1039,21 +1075,19 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack( packing_parameters.set_use_preprocessing(true); solver->SetParameters(packing_parameters.SerializeAsString()); } - DimensionSchedulingStatus status = DimensionSchedulingStatus::OPTIMAL; - if (Optimize(next_accessor, dimension_travel_info_per_route, solver, + DimensionSchedulingStatus status = + Optimize(next_accessor, dimension_travel_info_per_route, solver, /*cumul_values=*/nullptr, /*break_values=*/nullptr, /*resource_indices_per_group=*/nullptr, &cost, /*transit_cost=*/nullptr, - /*clear_lp=*/false, /*optimize_resource_assignment=*/false) == - DimensionSchedulingStatus::INFEASIBLE) { - status = DimensionSchedulingStatus::INFEASIBLE; - } + /*clear_lp=*/false, /*optimize_resource_assignment=*/false); if (status != DimensionSchedulingStatus::INFEASIBLE) { std::vector vehicles(dimension()->model()->vehicles()); std::iota(vehicles.begin(), vehicles.end(), 0); // Subtle: Even if the status was RELAXED_OPTIMAL_ONLY we try to pack just // in case packing manages to make the solution completely feasible. - status = PackRoutes(std::move(vehicles), solver, packing_parameters); + status = PackRoutes(std::move(vehicles), /*solve_duration_ratio=*/1.0, + solver, packing_parameters); } if (!solver->IsCPSATSolver()) { solver->SetParameters(original_params.SerializeAsString()); @@ -1073,7 +1107,8 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack( DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, @@ -1086,15 +1121,20 @@ DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( packing_parameters.set_use_preprocessing(true); solver->SetParameters(packing_parameters.SerializeAsString()); } + // TODO(user): Since there are 3 separate solves for packing, divide the + // input solve_duration_ratio by 3 before passing to the below functions? Or + // maybe divide by 2 and let PackRoutes() divide by 2 itself (since the 2 + // solves in PackRoutes() should start with a very good first solution hint). DimensionSchedulingStatus status = OptimizeSingleRouteWithResource( - vehicle, next_accessor, dimension_travel_info, resource, - /*optimize_vehicle_costs=*/true, solver, + vehicle, solve_duration_ratio, next_accessor, dimension_travel_info, + resource, /*optimize_vehicle_costs=*/true, solver, /*cumul_values=*/nullptr, /*break_values=*/nullptr, /*cost_without_transit=*/nullptr, /*transit_cost=*/nullptr, /*clear_lp=*/false); if (status != DimensionSchedulingStatus::INFEASIBLE) { - status = PackRoutes({vehicle}, solver, packing_parameters); + status = + PackRoutes({vehicle}, solve_duration_ratio, solver, packing_parameters); } if (!solver->IsCPSATSolver()) { solver->SetParameters(original_params.SerializeAsString()); @@ -1114,7 +1154,8 @@ DimensionCumulOptimizerCore::OptimizeAndPackSingleRoute( } DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( - std::vector vehicles, RoutingLinearSolverWrapper* solver, + std::vector vehicles, double solve_duration_ratio, + RoutingLinearSolverWrapper* solver, const glop::GlopParameters& packing_parameters) { const RoutingModel* model = dimension_->model(); @@ -1138,15 +1179,16 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( } glop::GlopParameters current_params; - const auto retry_solving = [¤t_params, model, solver]() { + const auto retry_solving = [¤t_params, model, solver, + solve_duration_ratio]() { // NOTE: To bypass some cases of false negatives due to imprecisions, we try // running Glop with a different use_dual_simplex parameter when running // into an infeasible status. current_params.set_use_dual_simplex(!current_params.use_dual_simplex()); solver->SetParameters(current_params.SerializeAsString()); - return solver->Solve(model->RemainingTime()); + return solver->Solve(model->RemainingTime() * solve_duration_ratio); }; - if (solver->Solve(model->RemainingTime()) == + if (solver->Solve(model->RemainingTime() * solve_duration_ratio) == DimensionSchedulingStatus::INFEASIBLE) { if (solver->IsCPSATSolver()) { return DimensionSchedulingStatus::INFEASIBLE; @@ -1173,7 +1215,8 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::PackRoutes( index_to_cumul_variable_[model->Start(vehicle)], -1); } - DimensionSchedulingStatus status = solver->Solve(model->RemainingTime()); + DimensionSchedulingStatus status = + solver->Solve(model->RemainingTime() * solve_duration_ratio); if (!solver->IsCPSATSolver() && status == DimensionSchedulingStatus::INFEASIBLE) { status = retry_solving(); @@ -1416,6 +1459,7 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( // Precompute the slopes and y-intercept as they will be used to detect // convexities and in the constraints. + // This has size index_anchor_end - index_anchor_start. const std::vector slope_and_y_intercept = PiecewiseLinearFunctionToSlopeAndYIntercept( travel_function, index_anchor_start, index_anchor_end); @@ -1454,7 +1498,8 @@ bool DimensionCumulOptimizerCore::SetRouteTravelConstraints( seg > 0 ? travel_x_anchors[index_anchor_start + seg] : current_route_min_cumuls_[pos] + pre_travel_transit; int64_t end_of_seg = seg + 1; - while (end_of_seg < num_pwl_anchors - 1 && !convexities[end_of_seg]) { + const int num_convexities = convexities.size(); + while (end_of_seg < num_convexities && !convexities[end_of_seg]) { ++end_of_seg; } const int64_t higher_bound_interval = @@ -1880,8 +1925,9 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( if (bound_cost.bound < std::numeric_limits::max() && bound_cost.cost > 0) { const int span_violation = solver->CreateNewPositiveVariable(); - SET_DEBUG_VARIABLE_NAME(solver, span_violation, - "quadratic_span_violation"); + SET_DEBUG_VARIABLE_NAME( + solver, span_violation, + absl::StrFormat("quadratic_span_violation(%ld)", vehicle)); // end - start <= bound + span_violation const int violation = solver->CreateNewConstraint( std::numeric_limits::min(), bound_cost.bound); @@ -1890,6 +1936,9 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( solver->SetCoefficient(violation, span_violation, -1.0); // Add variable squared_span_violation, equal to span_violation². const int squared_span_violation = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME( + solver, squared_span_violation, + absl::StrFormat("squared_span_violation(%ld)", vehicle)); solver->AddProductConstraint(squared_span_violation, {span_violation, span_violation}); // Add squared_span_violation * cost to objective. @@ -2147,6 +2196,134 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( } } + for (const auto& [limit, min_break_duration] : + dimension_->GetBreakDistanceDurationOfVehicle(vehicle)) { + int64_t min_num_breaks = 0; + if (limit > 0) { + min_num_breaks = + std::max(0, CapSub(total_fixed_transit, 1) / limit); + } + if (CapSub(current_route_min_cumuls_.back(), + current_route_max_cumuls_.front()) > limit) { + min_num_breaks = std::max(min_num_breaks, 1); + } + if (num_breaks < min_num_breaks) return false; + if (min_num_breaks == 0) continue; + + // Adds an LP relaxation of interbreak constraints. + // For all 0 <= pl < pr < path_size, for k > 0, + // if sum_{p in [pl, pr)} fixed_transit[p] > k * limit, + // then sum_{p in [pl, pr)} slack[p] >= k * min_break_duration. + // + // Moreover, if end_min[pr] - start_max[pl] > limit, + // the sum_{p in [pl, pr)} slack[p] >= min_break_duration. + // + // We want to apply the constraints above, without the ones that are + // dominated: + // - do not add the same constraint for k' < k, keep the largest k. + // - do not add the constraint for both (pl', pr') and (pl, pr) + // if [pl', pr') is a subset of [pl, pr), keep the smallest interval. + // TODO(user): reduce the number of constraints further; + // for instance if the constraint holds for (k, pl, pr) and (k', pl', pr') + // with pr <= pr', then no need to add the constraint for (k+k', pl, pr'). + // + // We need fast access to sum_{p in [pl, pr)} fixed_transit[p]. + // This will be sum_transits[pr] - sum_transits[pl]. Note that + // sum_transits[0] = 0, sum_transits[path_size-1] = total_fixed_transit. + std::vector sum_transits(path_size); + { + sum_transits[0] = 0; + for (int pos = 1; pos < path_size; ++pos) { + sum_transits[pos] = sum_transits[pos - 1] + fixed_transit[pos - 1]; + } + } + // To add the slack sum constraints, we need slack sum variables. + // Those are created lazily in a sparse vector, then only those useful + // variables are linked to slack variables after slack sum constraints + // have been added. + std::vector slack_sum_vars(path_size, -1); + // Given a number of breaks k, an interval of path positions [pl, pr), + // returns true if the interbreak constraint triggers for k breaks. + // TODO(user): find tighter end_min/start_max conditions. + // Mind that a break may be longer than min_break_duration. + auto trigger = [&](int k, int pl, int pr) -> bool { + const int64_t r_pre_travel = pr < path_size - 1 ? pre_travel[pr] : 0; + const int64_t l_post_travel = pl >= 1 ? post_travel[pl - 1] : 0; + const int64_t extra_travel = CapAdd(r_pre_travel, l_post_travel); + if (k == 1) { + const int64_t span_lb = CapAdd(CapSub(current_route_min_cumuls_[pr], + current_route_max_cumuls_[pl]), + extra_travel); + if (span_lb > limit) return true; + } + return CapAdd(CapSub(sum_transits[pr], sum_transits[pl]), extra_travel) > + CapProd(k, limit); + }; + int min_sum_var_index = path_size; + int max_sum_var_index = -1; + for (int k = 1; k <= min_num_breaks; ++k) { + int pr = 0; + for (int pl = 0; pl < path_size - 1; ++pl) { + pr = std::max(pr, pl + 1); + // Increase pr until transit(pl, pr) > k * limit. + while (pr < path_size && !trigger(k, pl, pr)) ++pr; + if (pr == path_size) break; + // Reduce [pl, pr) from the left. + while (pl < pr && trigger(k, pl + 1, pr)) ++pl; + // If pl == pr, then post_travel[pl-l] + pre_travel[pl] > limit. + // This is infeasible, because breaks cannot interrupt visits. + if (pl == pr) return false; + // If k is the largest for this interval, add the constraint. + // The call trigger(k', pl, pr) may hold for both k and k+1 with an + // irreducible interval [pl, pr] when there is a transit > limit at + // the beginning and at the end of the sub-route. + if (k < min_num_breaks && trigger(k + 1, pl, pr)) continue; + // If the solver is CPSAT, experimentation showed that adding slack sum + // constraints made solves worse than doing nothing, and that adding + // these lightweight constraints works better. + if (solver->IsCPSATSolver()) { + // lp_cumuls[pl] + transit(pl, pr) + k * min_break_duration <= + // lp_cumuls[pr]. + solver->AddLinearConstraint( + CapAdd(CapSub(sum_transits[pr], sum_transits[pl]), + CapProd(k, min_break_duration)), + kint64max, {{lp_cumuls[pr], 1}, {lp_cumuls[pl], -1}}); + } else { + if (slack_sum_vars[pl] == -1) { + slack_sum_vars[pl] = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME(solver, slack_sum_vars[pl], + absl::StrFormat("slack_sum_vars(%ld)", pl)); + min_sum_var_index = std::min(min_sum_var_index, pl); + } + if (slack_sum_vars[pr] == -1) { + slack_sum_vars[pr] = solver->CreateNewPositiveVariable(); + SET_DEBUG_VARIABLE_NAME(solver, slack_sum_vars[pr], + absl::StrFormat("slack_sum_vars(%ld)", pr)); + max_sum_var_index = std::max(max_sum_var_index, pr); + } + // sum_slacks[pr] - sum_slacks[pl] >= k * min_break_duration. + solver->AddLinearConstraint( + CapProd(k, min_break_duration), kint64max, + {{slack_sum_vars[pr], 1}, {slack_sum_vars[pl], -1}}); + } + } + } + if (min_sum_var_index < max_sum_var_index) { + slack_sum_vars[min_sum_var_index] = solver->AddVariable(0, 0); + int prev_index = min_sum_var_index; + for (int pos = min_sum_var_index + 1; pos <= max_sum_var_index; ++pos) { + if (slack_sum_vars[pos] == -1) continue; + // slack_sum_var[pos] = + // slack_sum_var[prev_index] + sum_{p in [prev_index, pos)} slack[p]. + const int ct = solver->AddLinearConstraint( + 0, 0, {{slack_sum_vars[pos], 1}, {slack_sum_vars[prev_index], -1}}); + for (int p = prev_index; p < pos; ++p) { + solver->SetCoefficient(ct, lp_slacks[p], -1); + } + prev_index = pos; + } + } + } if (!solver->IsCPSATSolver()) return true; if (!dimension_->GetBreakDistanceDurationOfVehicle(vehicle).empty()) { // If there is an optional interval, the following model would be wrong. @@ -2266,7 +2443,7 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( } return true; -} +} // NOLINT(readability/fn_size) namespace { bool AllValuesContainedExcept(const IntVar& var, absl::Span values, @@ -2300,23 +2477,26 @@ bool DimensionCumulOptimizerCore::SetGlobalConstraints( } // Node precedence constraints, set when both nodes are visited. - for (const RoutingDimension::NodePrecedence& precedence : + for (const auto [first_node, second_node, offset, performed_constraint] : dimension_->GetNodePrecedences()) { - const int first_cumul_var = index_to_cumul_variable_[precedence.first_node]; - const int second_cumul_var = - index_to_cumul_variable_[precedence.second_node]; - if (first_cumul_var < 0 || second_cumul_var < 0) { - // At least one of the nodes is not on any route, skip this precedence - // constraint. + const int first_cumul_var = index_to_cumul_variable_[first_node]; + const int second_cumul_var = index_to_cumul_variable_[second_node]; + switch (RoutingDimension::GetPrecedenceStatus( + first_cumul_var < 0, second_cumul_var < 0, performed_constraint)) { + case RoutingDimension::PrecedenceStatus::kActive: + break; + case RoutingDimension::PrecedenceStatus::kInactive: continue; + case RoutingDimension::PrecedenceStatus::kInvalid: + return false; } DCHECK_NE(first_cumul_var, second_cumul_var) << "Dimension " << dimension_->name() - << " has a self-precedence on node " << precedence.first_node << "."; + << " has a self-precedence on node " << first_node << "."; // cumul[second_node] - cumul[first_node] >= offset. const int ct = solver->CreateNewConstraint( - precedence.offset, std::numeric_limits::max()); + offset, std::numeric_limits::max()); solver->SetCoefficient(ct, second_cumul_var, 1); solver->SetCoefficient(ct, first_cumul_var, -1); } @@ -2671,7 +2851,8 @@ void MoveValuesToIndicesFrom(std::vector* out_values, } // namespace bool ComputeVehicleToResourceClassAssignmentCosts( - int v, const RoutingModel::ResourceGroup& resource_group, + int v, double solve_duration_ratio, + const RoutingModel::ResourceGroup& resource_group, const util_intops::StrongVector>& ignored_resources_per_class, @@ -2682,6 +2863,8 @@ bool ComputeVehicleToResourceClassAssignmentCosts( std::vector* assignment_costs, std::vector>* cumul_values, std::vector>* break_values) { + DCHECK_GT(solve_duration_ratio, 0); + DCHECK_LE(solve_duration_ratio, 1); DCHECK(lp_optimizer != nullptr); DCHECK(mp_optimizer != nullptr); DCHECK_NE(assignment_costs, nullptr); @@ -2745,7 +2928,7 @@ bool ComputeVehicleToResourceClassAssignmentCosts( std::vector> considered_break_values; std::vector statuses = optimizer->ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - v, next_accessor, transit_accessor, resources, + v, solve_duration_ratio, next_accessor, transit_accessor, resources, considered_resource_indices, optimize_vehicle_costs, &considered_assignment_costs, cumul_values == nullptr ? nullptr : &considered_cumul_values, @@ -2796,8 +2979,8 @@ bool ComputeVehicleToResourceClassAssignmentCosts( std::vector> mp_cumul_values; std::vector> mp_break_values; mp_optimizer->ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - v, next_accessor, transit_accessor, resources, mp_resource_indices, - optimize_vehicle_costs, &mp_assignment_costs, + v, solve_duration_ratio, next_accessor, transit_accessor, resources, + mp_resource_indices, optimize_vehicle_costs, &mp_assignment_costs, cumul_values == nullptr ? nullptr : &mp_cumul_values, break_values == nullptr ? nullptr : &mp_break_values); if (!mp_resource_indices.empty() && mp_assignment_costs.empty()) { @@ -2912,6 +3095,7 @@ int64_t ComputeBestVehicleToResourceAssignment( /*reserve_num_nodes*/ 2 + num_vehicles + num_resource_classes, /*reserve_num_arcs*/ num_vehicles + num_vehicles * num_resource_classes + num_resource_classes); + using ArcIndex = SimpleMinCostFlow::ArcIndex; const int source_index = num_vehicles + num_resource_classes; const int sink_index = source_index + 1; const auto flow_rc_index = [num_vehicles](int rc) { diff --git a/ortools/constraint_solver/routing_lp_scheduling.h b/ortools/constraint_solver/routing_lp_scheduling.h index c3c8f597555..8208cbab3db 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.h +++ b/ortools/constraint_solver/routing_lp_scheduling.h @@ -161,6 +161,8 @@ enum class DimensionSchedulingStatus { // An optimal solution was found, however constraints which were relaxed were // violated. RELAXED_OPTIMAL_ONLY, + // Only a feasible solution was found, optimality was not proven. + FEASIBLE, // A solution could not be found. INFEASIBLE }; @@ -169,7 +171,7 @@ class RoutingLinearSolverWrapper { public: static const int kNoConstraint = -1; - virtual ~RoutingLinearSolverWrapper() {} + virtual ~RoutingLinearSolverWrapper() = default; virtual void Clear() = 0; virtual int CreateNewPositiveVariable() = 0; virtual void SetVariableName(int index, absl::string_view name) = 0; @@ -404,8 +406,9 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { linear_program_.NotifyThatColumnsAreClean(); VLOG(2) << linear_program_.Dump(); const glop::ProblemStatus status = lp_solver_.Solve(linear_program_); + const bool feasible_only = status == glop::ProblemStatus::PRIMAL_FEASIBLE; if (status != glop::ProblemStatus::OPTIMAL && - status != glop::ProblemStatus::IMPRECISE) { + status != glop::ProblemStatus::IMPRECISE && !feasible_only) { return DimensionSchedulingStatus::INFEASIBLE; } if (is_relaxation_) { @@ -424,6 +427,9 @@ class RoutingGlopWrapper : public RoutingLinearSolverWrapper { return DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY; } } + if (feasible_only && !linear_program_.objective_coefficients().empty()) { + return DimensionSchedulingStatus::FEASIBLE; + } return DimensionSchedulingStatus::OPTIMAL; } int64_t GetObjectiveValue() const override { @@ -627,13 +633,14 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper { }; model_.clear_solution_hint(); auto* hint = model_.mutable_solution_hint(); + if (hint_.vars_size() == model_.variables_size()) { + *hint = hint_; + } else { for (const auto& [node, cumul] : schedule_variables_) { if (schedule_hint_[node] == 0) continue; hint->add_vars(cumul); hint->add_values(schedule_hint_[node]); } - if (hint_.vars_size() == model_.variables_size()) { - *model_.mutable_solution_hint() = hint_; } sat::Model model; model.Add(sat::NewSatParameters(parameters_)); @@ -645,6 +652,10 @@ class RoutingCPSatWrapper : public RoutingLinearSolverWrapper { !model_.has_floating_point_objective())) { record_hint(); return DimensionSchedulingStatus::OPTIMAL; + } else if (response_.status() == sat::CpSolverStatus::FEASIBLE) { + // TODO(user): Consider storing "feasible" solutions in a separate + // cache we use as hint when the "optimal" cache is empty. + return DimensionSchedulingStatus::FEASIBLE; } return DimensionSchedulingStatus::INFEASIBLE; } @@ -695,7 +706,8 @@ class DimensionCumulOptimizerCore { // Finds an optimal (or just feasible) solution for the route for the given // resource. If the resource is null, it is simply ignored. DimensionSchedulingStatus OptimizeSingleRouteWithResource( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const Resource* resource, bool optimize_vehicle_costs, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, @@ -706,7 +718,8 @@ class DimensionCumulOptimizerCore { // same model as in OptimizeSingleRouteWithResource() with the addition of // constraints for cumuls and breaks. DimensionSchedulingStatus ComputeSingleRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, RoutingLinearSolverWrapper* solver, absl::Span solution_cumul_values, @@ -721,7 +734,8 @@ class DimensionCumulOptimizerCore { // If both 'resources' and 'resource_indices' are empty, computes the optimal // solution for the route itself (without added resource constraints). std::vector OptimizeSingleRouteWithResources( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, const RouteDimensionTravelInfo* dimension_travel_info, absl::Span resources, @@ -755,7 +769,8 @@ class DimensionCumulOptimizerCore { std::vector* break_values); DimensionSchedulingStatus OptimizeAndPackSingleRoute( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RouteDimensionTravelInfo* dimension_travel_info, const Resource* resource, RoutingLinearSolverWrapper* solver, std::vector* cumul_values, std::vector* break_values); @@ -830,7 +845,8 @@ class DimensionCumulOptimizerCore { // ends' cumuls, and then maximizes the starts' cumuls without increasing the // ends. DimensionSchedulingStatus PackRoutes( - std::vector vehicles, RoutingLinearSolverWrapper* solver, + std::vector vehicles, double solve_duration_ratio, + RoutingLinearSolverWrapper* solver, const glop::GlopParameters& packing_parameters); std::unique_ptr propagator_; @@ -886,19 +902,22 @@ class LocalDimensionCumulOptimizer { // and stores it in "optimal_cost" (if not null). // Returns true iff the route respects all constraints. DimensionSchedulingStatus ComputeRouteCumulCost( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, int64_t* optimal_cost); // Same as ComputeRouteCumulCost, but the cost computed does not contain // the part of the vehicle span cost due to fixed transits. DimensionSchedulingStatus ComputeRouteCumulCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::ResourceGroup::Resource* resource, int64_t* optimal_cost_without_transits); std::vector ComputeRouteCumulCostsForResourcesWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const std::function& transit_accessor, absl::Span resources, absl::Span resource_indices, bool optimize_vehicle_costs, @@ -912,7 +931,8 @@ class LocalDimensionCumulOptimizer { // (if not null), and optimal_breaks, and returns true. // Returns false if the route is not feasible. DimensionSchedulingStatus ComputeRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* optimal_cumuls, @@ -921,7 +941,8 @@ class LocalDimensionCumulOptimizer { // Simple combination of ComputeRouteCumulCostWithoutFixedTransits() and // ComputeRouteCumuls(). DimensionSchedulingStatus ComputeRouteCumulsAndCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, std::vector* optimal_cumuls, std::vector* optimal_breaks, @@ -930,7 +951,8 @@ class LocalDimensionCumulOptimizer { // If feasible, computes the cost of a given route performed by a vehicle // defined by its cumuls and breaks. DimensionSchedulingStatus ComputeRouteSolutionCostWithoutFixedTransits( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, absl::Span solution_cumul_values, absl::Span solution_break_values, int64_t* solution_cost, @@ -944,7 +966,8 @@ class LocalDimensionCumulOptimizer { // If 'resource' is non-null, the packed route must also respect its start/end // time window. DimensionSchedulingStatus ComputePackedRouteCumuls( - int vehicle, const std::function& next_accessor, + int vehicle, double solve_duration_ratio, + const std::function& next_accessor, const RoutingModel::RouteDimensionTravelInfo* dimension_travel_info, const RoutingModel::ResourceGroup::Resource* resource, std::vector* packed_cumuls, std::vector* packed_breaks); @@ -1045,7 +1068,8 @@ int64_t ComputeBestVehicleToResourceAssignment( // The cumul and break values corresponding to the assignment of each resource // are also set in cumul_values and break_values, if non-null. bool ComputeVehicleToResourceClassAssignmentCosts( - int v, const RoutingModel::ResourceGroup& resource_group, + int v, double solve_duration_ratio, + const RoutingModel::ResourceGroup& resource_group, const util_intops::StrongVector>& ignored_resources_per_class, diff --git a/ortools/constraint_solver/routing_neighborhoods.cc b/ortools/constraint_solver/routing_neighborhoods.cc index b3b80f86f40..584ffc25d99 100644 --- a/ortools/constraint_solver/routing_neighborhoods.cc +++ b/ortools/constraint_solver/routing_neighborhoods.cc @@ -32,14 +32,15 @@ namespace operations_research { using NeighborAccessor = std::function&(/*node=*/int, /*start_node=*/int)>; -MakeRelocateNeighborsOperator::MakeRelocateNeighborsOperator( +template +MakeRelocateNeighborsOperator::MakeRelocateNeighborsOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, NeighborAccessor get_incoming_neighbors, NeighborAccessor get_outgoing_neighbors, RoutingTransitCallback2 arc_evaluator) - : PathOperator( + : PathOperator( vars, secondary_vars, /*number_of_base_nodes=*/ get_incoming_neighbors == nullptr && get_outgoing_neighbors == nullptr @@ -50,14 +51,15 @@ MakeRelocateNeighborsOperator::MakeRelocateNeighborsOperator( std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)), arc_evaluator_(std::move(arc_evaluator)) {} -bool MakeRelocateNeighborsOperator::MakeNeighbor() { +template +bool MakeRelocateNeighborsOperator::MakeNeighbor() { const auto do_move = [this](int64_t before_chain, int64_t destination) { - int64_t chain_end = Next(before_chain); - if (IsPathEnd(chain_end)) return false; + int64_t chain_end = this->Next(before_chain); + if (this->IsPathEnd(chain_end)) return false; if (chain_end == destination) return false; const int64_t max_arc_value = arc_evaluator_(destination, chain_end); - int64_t next = Next(chain_end); - while (!IsPathEnd(next) && + int64_t next = this->Next(chain_end); + while (!this->IsPathEnd(next) && arc_evaluator_(chain_end, next) <= max_arc_value) { // We return false here to avoid symmetric moves. The rationale is that // if destination is part of the same group as the chain, we probably want @@ -67,39 +69,39 @@ bool MakeRelocateNeighborsOperator::MakeNeighbor() { // depending if we want to permutate nodes within the same chain. if (next == destination) return false; chain_end = next; - next = Next(chain_end); + next = this->Next(chain_end); } return MoveChainAndRepair(before_chain, chain_end, destination); }; - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); - if (neighbor < 0 || IsInactive(neighbor)) return false; + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); + if (neighbor < 0 || this->IsInactive(neighbor)) return false; if (!outgoing) { // TODO(user): Handle incoming neighbors by going backwards on the // chain. return false; } - return do_move(/*before_chain=*/Prev(neighbor), - /*destination=*/BaseNode(0)); + return do_move(/*before_chain=*/this->Prev(neighbor), + /*destination=*/this->BaseNode(0)); } else { - return do_move(/*before_chain=*/BaseNode(0), - /*destination=*/BaseNode(1)); + return do_move(/*before_chain=*/this->BaseNode(0), + /*destination=*/this->BaseNode(1)); } } -bool MakeRelocateNeighborsOperator::MoveChainAndRepair(int64_t before_chain, - int64_t chain_end, - int64_t destination) { - if (MoveChain(before_chain, chain_end, destination)) { - if (!IsPathStart(destination)) { - int64_t current = Prev(destination); +template +bool MakeRelocateNeighborsOperator::MoveChainAndRepair( + int64_t before_chain, int64_t chain_end, int64_t destination) { + if (this->MoveChain(before_chain, chain_end, destination)) { + if (!this->IsPathStart(destination)) { + int64_t current = this->Prev(destination); int64_t last = chain_end; if (current == last) { // chain was just before destination current = before_chain; } - while (last >= 0 && !IsPathStart(current) && current != last) { + while (last >= 0 && !this->IsPathStart(current) && current != last) { last = Reposition(current, last); - current = Prev(current); + current = this->Prev(current); } } return true; @@ -107,31 +109,62 @@ bool MakeRelocateNeighborsOperator::MoveChainAndRepair(int64_t before_chain, return false; } -int64_t MakeRelocateNeighborsOperator::Reposition(int64_t before_to_move, - int64_t up_to) { +template +int64_t MakeRelocateNeighborsOperator::Reposition( + int64_t before_to_move, int64_t up_to) { const int64_t kNoChange = -1; - const int64_t to_move = Next(before_to_move); - int64_t next = Next(to_move); - if (Var(to_move)->Contains(next)) { + const int64_t to_move = this->Next(before_to_move); + int64_t next = this->Next(to_move); + if (this->Var(to_move)->Contains(next)) { return kNoChange; } int64_t prev = next; - next = Next(next); + next = this->Next(next); while (prev != up_to) { - if (Var(prev)->Contains(to_move) && Var(to_move)->Contains(next)) { - MoveChain(before_to_move, to_move, prev); + if (this->Var(prev)->Contains(to_move) && + this->Var(to_move)->Contains(next)) { + this->MoveChain(before_to_move, to_move, prev); return up_to; } prev = next; - next = Next(next); + next = this->Next(next); } - if (Var(prev)->Contains(to_move)) { - MoveChain(before_to_move, to_move, prev); + if (this->Var(prev)->Contains(to_move)) { + this->MoveChain(before_to_move, to_move, prev); return to_move; } return kNoChange; } +LocalSearchOperator* MakeRelocateNeighbors( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + RoutingTransitCallback2 arc_evaluator) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakeRelocateNeighborsOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + std::move(arc_evaluator))); + } + return solver->RevAlloc(new MakeRelocateNeighborsOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + std::move(arc_evaluator))); +} + +LocalSearchOperator* MakeRelocateNeighbors( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + RoutingTransitCallback2 arc_evaluator) { + return MakeRelocateNeighbors(solver, vars, secondary_vars, + std::move(start_empty_path_class), nullptr, + nullptr, std::move(arc_evaluator)); +} + ShortestPathOnAlternatives::ShortestPathOnAlternatives( int num_nodes, std::vector> alternative_sets, RoutingTransitCallback2 arc_evaluator) @@ -218,30 +251,34 @@ absl::Span ShortestPathOnAlternatives::GetShortestPath( return path_; } -TwoOptWithShortestPathOperator::TwoOptWithShortestPathOperator( +template +TwoOptWithShortestPathOperator:: + TwoOptWithShortestPathOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, std::vector> alternative_sets, RoutingTransitCallback2 arc_evaluator) - : PathOperator(vars, secondary_vars, /*number_of_base_nodes=*/2, + : PathOperator( + vars, secondary_vars, /*number_of_base_nodes=*/2, /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/true, - std::move(start_empty_path_class), nullptr, nullptr), + /*accept_path_end_base=*/true, std::move(start_empty_path_class), + nullptr, nullptr), shortest_path_manager_(vars.size(), std::move(alternative_sets), std::move(arc_evaluator)) {} -bool TwoOptWithShortestPathOperator::MakeNeighbor() { - DCHECK_EQ(StartNode(0), StartNode(1)); - const int64_t before_chain = BaseNode(0); - if (IsPathEnd(before_chain)) { +template +bool TwoOptWithShortestPathOperator::MakeNeighbor() { + DCHECK_EQ(this->StartNode(0), this->StartNode(1)); + const int64_t before_chain = this->BaseNode(0); + if (this->IsPathEnd(before_chain)) { ResetChainStatus(); return false; } - const int64_t after_chain = BaseNode(1); + const int64_t after_chain = this->BaseNode(1); bool has_alternatives = false; if (before_chain != after_chain) { - const int64_t prev_after_chain = Prev(after_chain); + const int64_t prev_after_chain = this->Prev(after_chain); if (prev_after_chain != before_chain && chain_status_.start == before_chain && chain_status_.end == prev_after_chain) { @@ -251,8 +288,8 @@ bool TwoOptWithShortestPathOperator::MakeNeighbor() { } else { // Non-incremental computation of alternative presence. The chains are // small by definition. - for (int64_t node = Next(before_chain); node != after_chain; - node = Next(node)) { + for (int64_t node = this->Next(before_chain); node != after_chain; + node = this->Next(node)) { has_alternatives |= shortest_path_manager_.HasAlternatives(node); } } @@ -262,66 +299,106 @@ bool TwoOptWithShortestPathOperator::MakeNeighbor() { chain_status_.has_alternatives = has_alternatives; if (!has_alternatives) return false; int64_t chain_last; - if (!ReverseChain(before_chain, after_chain, &chain_last)) return false; + if (!this->ReverseChain(before_chain, after_chain, &chain_last)) return false; chain_.clear(); - for (int64_t next = Next(before_chain); next != after_chain; - next = Next(next)) { + for (int64_t next = this->Next(before_chain); next != after_chain; + next = this->Next(next)) { chain_.push_back(next); } // The neighbor is accepted if there were actual changes, either we reverted a // chain with more than one node, or alternatives were swapped. - return SwapActiveAndInactiveChains(chain_, - shortest_path_manager_.GetShortestPath( + return this->SwapActiveAndInactiveChains( + chain_, shortest_path_manager_.GetShortestPath( before_chain, after_chain, chain_)) || chain_.size() > 1; } -SwapActiveToShortestPathOperator::SwapActiveToShortestPathOperator( +LocalSearchOperator* MakeTwoOptWithShortestPath( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::vector> alternative_sets, + RoutingTransitCallback2 arc_evaluator) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new TwoOptWithShortestPathOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(alternative_sets), std::move(arc_evaluator))); + } + return solver->RevAlloc(new TwoOptWithShortestPathOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(alternative_sets), std::move(arc_evaluator))); +} + +template +SwapActiveToShortestPathOperator:: + SwapActiveToShortestPathOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, std::vector> alternative_sets, RoutingTransitCallback2 arc_evaluator) - : PathOperator(vars, secondary_vars, /*number_of_base_nodes=*/1, + : PathOperator( + vars, secondary_vars, /*number_of_base_nodes=*/1, /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/false, - std::move(start_empty_path_class), nullptr, nullptr), + /*accept_path_end_base=*/false, std::move(start_empty_path_class), + nullptr, nullptr), shortest_path_manager_(vars.size(), std::move(alternative_sets), std::move(arc_evaluator)) {} -bool SwapActiveToShortestPathOperator::MakeNeighbor() { - const int64_t before_chain = BaseNode(0); - if (!IsPathStart(before_chain) && +template +bool SwapActiveToShortestPathOperator::MakeNeighbor() { + const int64_t before_chain = this->BaseNode(0); + if (!this->IsPathStart(before_chain) && shortest_path_manager_.HasAlternatives(before_chain)) { return false; } - int64_t next = Next(before_chain); + int64_t next = this->Next(before_chain); chain_.clear(); - while (!IsPathEnd(next) && shortest_path_manager_.HasAlternatives(next)) { + while (!this->IsPathEnd(next) && + shortest_path_manager_.HasAlternatives(next)) { chain_.push_back(next); - next = Next(next); + next = this->Next(next); } - return SwapActiveAndInactiveChains( + return this->SwapActiveAndInactiveChains( chain_, shortest_path_manager_.GetShortestPath( /*source=*/before_chain, /*sink=*/next, chain_)); } -MakePairActiveOperator::MakePairActiveOperator( +LocalSearchOperator* MakeSwapActiveToShortestPath( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::vector> alternative_sets, + RoutingTransitCallback2 arc_evaluator) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new SwapActiveToShortestPathOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(alternative_sets), std::move(arc_evaluator))); + } + return solver->RevAlloc(new SwapActiveToShortestPathOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(alternative_sets), std::move(arc_evaluator))); +} + +template +MakePairActiveOperator::MakePairActiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 2, false, true, - std::move(start_empty_path_class), nullptr, nullptr), + : PathOperator(vars, secondary_vars, 2, false, true, + std::move(start_empty_path_class), nullptr, + nullptr), inactive_pair_(0), inactive_pair_first_index_(0), inactive_pair_second_index_(0), pairs_(pairs) {} -bool MakePairActiveOperator::MakeOneNeighbor() { +template +bool MakePairActiveOperator::MakeOneNeighbor() { while (inactive_pair_ < pairs_.size()) { - if (PathOperator::MakeOneNeighbor()) return true; - ResetPosition(); + if (PathOperator::MakeOneNeighbor()) return true; + this->ResetPosition(); const auto& [pickup_alternatives, delivery_alternatives] = pairs_[inactive_pair_]; if (inactive_pair_first_index_ < pickup_alternatives.size() - 1) { @@ -338,8 +415,9 @@ bool MakePairActiveOperator::MakeOneNeighbor() { return false; } -bool MakePairActiveOperator::MakeNeighbor() { - DCHECK_EQ(StartNode(0), StartNode(1)); +template +bool MakePairActiveOperator::MakeNeighbor() { + DCHECK_EQ(this->StartNode(0), this->StartNode(1)); // Inserting the second node of the pair before the first one which ensures // that the only solutions where both nodes are next to each other have the // first node before the second (the move is not symmetric and doing it this @@ -347,28 +425,34 @@ bool MakePairActiveOperator::MakeNeighbor() { // pair is not violated). const auto& [pickup_alternatives, delivery_alternatives] = pairs_[inactive_pair_]; - return MakeActive(delivery_alternatives[inactive_pair_second_index_], - BaseNode(1)) && - MakeActive(pickup_alternatives[inactive_pair_first_index_], - BaseNode(0)); + return this->MakeActive(delivery_alternatives[inactive_pair_second_index_], + this->BaseNode(1)) && + this->MakeActive(pickup_alternatives[inactive_pair_first_index_], + this->BaseNode(0)); } -int64_t MakePairActiveOperator::GetBaseNodeRestartPosition(int base_index) { +template +int64_t MakePairActiveOperator::GetBaseNodeRestartPosition( + int base_index) { // Base node 1 must be after base node 0 if they are both on the same path. - if (base_index == 0 || StartNode(base_index) != StartNode(base_index - 1)) { - return StartNode(base_index); + if (base_index == 0 || + this->StartNode(base_index) != this->StartNode(base_index - 1)) { + return this->StartNode(base_index); } else { - return BaseNode(base_index - 1); + return this->BaseNode(base_index - 1); } } -void MakePairActiveOperator::OnNodeInitialization() { +template +void MakePairActiveOperator::OnNodeInitialization() { inactive_pair_ = FindNextInactivePair(0); inactive_pair_first_index_ = 0; inactive_pair_second_index_ = 0; } -int MakePairActiveOperator::FindNextInactivePair(int pair_index) const { +template +int MakePairActiveOperator::FindNextInactivePair( + int pair_index) const { for (int index = pair_index; index < pairs_.size(); ++index) { if (!ContainsActiveNodes(pairs_[index].pickup_alternatives) && !ContainsActiveNodes(pairs_[index].delivery_alternatives)) { @@ -378,71 +462,107 @@ int MakePairActiveOperator::FindNextInactivePair(int pair_index) const { return pairs_.size(); } -bool MakePairActiveOperator::ContainsActiveNodes( +template +bool MakePairActiveOperator::ContainsActiveNodes( absl::Span nodes) const { for (int64_t node : nodes) { - if (!IsInactive(node)) return true; + if (!this->IsInactive(node)) return true; } return false; } -MakePairInactiveOperator::MakePairInactiveOperator( +LocalSearchOperator* MakePairActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakePairActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); + } + return solver->RevAlloc(new MakePairActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); +} + +template +MakePairInactiveOperator::MakePairInactiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 1, true, false, - std::move(start_empty_path_class), nullptr, nullptr) { - AddPairAlternativeSets(pairs); + : PathOperator(vars, secondary_vars, 1, true, false, + std::move(start_empty_path_class), nullptr, + nullptr) { + this->AddPairAlternativeSets(pairs); } -bool MakePairInactiveOperator::MakeNeighbor() { - const int64_t base = BaseNode(0); - const int64_t first_index = Next(base); - const int64_t second_index = GetActiveAlternativeSibling(first_index); +template +bool MakePairInactiveOperator::MakeNeighbor() { + const int64_t base = this->BaseNode(0); + const int64_t first_index = this->Next(base); + const int64_t second_index = this->GetActiveAlternativeSibling(first_index); if (second_index < 0) { return false; } - return MakeChainInactive(base, first_index) && - MakeChainInactive(Prev(second_index), second_index); + return this->MakeChainInactive(base, first_index) && + this->MakeChainInactive(this->Prev(second_index), second_index); +} + +LocalSearchOperator* MakePairInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new MakePairInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); + } + return solver->RevAlloc(new MakePairInactiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); } -PairRelocateOperator::PairRelocateOperator( +template +PairRelocateOperator::PairRelocateOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 3, true, false, - std::move(start_empty_path_class), nullptr, nullptr) { + : PathOperator(vars, secondary_vars, 3, true, false, + std::move(start_empty_path_class), nullptr, + nullptr) { // TODO(user): Add a version where a (first_node, second_node) pair are // added respectively after first_node_neighbor and second_node_neighbor. // This requires a complete restructuring of the code, since we would require // scanning neighbors for a non-base node (second_node is an active sibling // of first_node). - AddPairAlternativeSets(pairs); + this->AddPairAlternativeSets(pairs); } -bool PairRelocateOperator::MakeNeighbor() { - DCHECK_EQ(StartNode(1), StartNode(2)); - const int64_t first_pair_node = BaseNode(kPairFirstNode); - if (IsPathStart(first_pair_node)) { +template +bool PairRelocateOperator::MakeNeighbor() { + DCHECK_EQ(this->StartNode(1), this->StartNode(2)); + const int64_t first_pair_node = this->BaseNode(kPairFirstNode); + if (this->IsPathStart(first_pair_node)) { return false; } - int64_t first_prev = Prev(first_pair_node); - const int second_pair_node = GetActiveAlternativeSibling(first_pair_node); - if (second_pair_node < 0 || IsPathEnd(second_pair_node) || - IsPathStart(second_pair_node)) { + int64_t first_prev = this->Prev(first_pair_node); + const int second_pair_node = + this->GetActiveAlternativeSibling(first_pair_node); + if (second_pair_node < 0 || this->IsPathEnd(second_pair_node) || + this->IsPathStart(second_pair_node)) { return false; } - const int64_t second_prev = Prev(second_pair_node); + const int64_t second_prev = this->Prev(second_pair_node); - const int64_t first_node_destination = BaseNode(kPairFirstNodeDestination); + const int64_t first_node_destination = + this->BaseNode(kPairFirstNodeDestination); if (first_node_destination == second_pair_node) { // The second_pair_node -> first_pair_node link is forbidden. return false; } - const int64_t second_node_destination = BaseNode(kPairSecondNodeDestination); + const int64_t second_node_destination = + this->BaseNode(kPairSecondNodeDestination); if (second_prev == first_pair_node && first_node_destination == first_prev && second_node_destination == first_prev) { // If the current sequence is first_prev -> first_pair_node -> @@ -459,35 +579,52 @@ bool PairRelocateOperator::MakeNeighbor() { return false; } const bool moved_second_pair_node = - MoveChain(second_prev, second_pair_node, second_node_destination); + this->MoveChain(second_prev, second_pair_node, second_node_destination); // Explicitly calling Prev as second_pair_node might have been moved before // first_pair_node. - const bool moved_first_pair_node = - MoveChain(Prev(first_pair_node), first_pair_node, first_node_destination); + const bool moved_first_pair_node = this->MoveChain( + this->Prev(first_pair_node), first_pair_node, first_node_destination); // Swapping alternatives in. - SwapActiveAndInactive(second_pair_node, - BaseSiblingAlternativeNode(kPairFirstNode)); - SwapActiveAndInactive(first_pair_node, BaseAlternativeNode(kPairFirstNode)); + this->SwapActiveAndInactive(second_pair_node, + this->BaseSiblingAlternativeNode(kPairFirstNode)); + this->SwapActiveAndInactive(first_pair_node, + this->BaseAlternativeNode(kPairFirstNode)); return moved_first_pair_node || moved_second_pair_node; } -int64_t PairRelocateOperator::GetBaseNodeRestartPosition(int base_index) { +template +int64_t PairRelocateOperator::GetBaseNodeRestartPosition( + int base_index) { // Destination node of the second node of a pair must be after the // destination node of the first node of a pair. if (base_index == kPairSecondNodeDestination) { - return BaseNode(kPairFirstNodeDestination); + return this->BaseNode(kPairFirstNodeDestination); } else { - return StartNode(base_index); + return this->StartNode(base_index); + } +} + +LocalSearchOperator* MakePairRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new PairRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); } + return solver->RevAlloc(new PairRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); } -GroupPairAndRelocateOperator::GroupPairAndRelocateOperator( +template +GroupPairAndRelocateOperator::GroupPairAndRelocateOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, NeighborAccessor, NeighborAccessor get_outgoing_neighbors, const std::vector& pairs) - : PathOperator( + : PathOperator( vars, secondary_vars, /*number_of_base_nodes=*/ get_outgoing_neighbors == nullptr ? 2 : 1, @@ -495,64 +632,91 @@ GroupPairAndRelocateOperator::GroupPairAndRelocateOperator( /*accept_path_end_base=*/false, std::move(start_empty_path_class), nullptr, // We don't use incoming neighbors for this operator. std::move(get_outgoing_neighbors)) { - AddPairAlternativeSets(pairs); + this->AddPairAlternativeSets(pairs); } -bool GroupPairAndRelocateOperator::MakeNeighbor() { +template +bool GroupPairAndRelocateOperator::MakeNeighbor() { const auto do_move = [this](int64_t node, int64_t destination) { - if (IsPathEnd(node) || IsInactive(node)) return false; - const int64_t sibling = GetActiveAlternativeSibling(node); + if (this->IsPathEnd(node) || this->IsInactive(node)) return false; + const int64_t sibling = this->GetActiveAlternativeSibling(node); if (sibling == -1) return false; // Skip redundant cases. if (destination == node || destination == sibling) return false; - const bool ok = MoveChain(Prev(node), node, destination); - return MoveChain(Prev(sibling), sibling, node) || ok; + const bool ok = this->MoveChain(this->Prev(node), node, destination); + return this->MoveChain(this->Prev(sibling), sibling, node) || ok; }; - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); if (neighbor < 0) return false; DCHECK(outgoing); - return do_move(/*node=*/neighbor, /*destination=*/BaseNode(0)); + return do_move(/*node=*/neighbor, /*destination=*/this->BaseNode(0)); + } + return do_move(/*node=*/this->Next(this->BaseNode(0)), + /*destination=*/this->BaseNode(1)); +} + +LocalSearchOperator* MakeGroupPairAndRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new GroupPairAndRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); + } else { + return solver->RevAlloc(new GroupPairAndRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); } - return do_move(/*node=*/Next(BaseNode(0)), /*destination=*/BaseNode(1)); } -LightPairRelocateOperator::LightPairRelocateOperator( +LocalSearchOperator* MakeGroupPairAndRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + return MakeGroupPairAndRelocate(solver, vars, secondary_vars, + std::move(start_empty_path_class), nullptr, + nullptr, pairs); +} + +template +LightPairRelocateOperator::LightPairRelocateOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, NeighborAccessor, NeighborAccessor get_outgoing_neighbors, const std::vector& pairs, std::function force_lifo) - : PathOperator(vars, secondary_vars, + : PathOperator( + vars, secondary_vars, /*number_of_base_nodes=*/ get_outgoing_neighbors == nullptr ? 2 : 1, /*skip_locally_optimal_paths=*/true, - /*accept_path_end_base=*/false, - std::move(start_empty_path_class), + /*accept_path_end_base=*/false, std::move(start_empty_path_class), nullptr, // Incoming neighbors not used as of 09/2024. std::move(get_outgoing_neighbors)), force_lifo_(std::move(force_lifo)) { - AddPairAlternativeSets(pairs); + this->AddPairAlternativeSets(pairs); } -LightPairRelocateOperator::LightPairRelocateOperator( - const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - const std::vector& pairs, - std::function force_lifo) - : LightPairRelocateOperator(vars, secondary_vars, - std::move(start_empty_path_class), nullptr, - nullptr, pairs, std::move(force_lifo)) {} - -bool LightPairRelocateOperator::MakeNeighbor() { +template +bool LightPairRelocateOperator::MakeNeighbor() { const auto do_move = [this](int64_t node, int64_t destination, bool destination_is_lifo) { - if (IsPathStart(node) || IsPathEnd(node) || IsInactive(node)) return false; - const int64_t prev = Prev(node); - if (IsPathEnd(node)) return false; - const int64_t sibling = GetActiveAlternativeSibling(node); + if (this->IsPathStart(node) || this->IsPathEnd(node) || + this->IsInactive(node)) { + return false; + } + const int64_t prev = this->Prev(node); + if (this->IsPathEnd(node)) return false; + const int64_t sibling = this->GetActiveAlternativeSibling(node); if (sibling == -1 || destination == sibling) return false; // Note: MoveChain will return false if it is a no-op (moving the chain to @@ -566,60 +730,100 @@ bool LightPairRelocateOperator::MakeNeighbor() { // "after". // TODO(user): extend to relocating before the start of sub-tours (when // all pairs have been matched). - if (IsPathStart(destination)) { - const bool ok = MoveChain(prev, node, destination); + if (this->IsPathStart(destination)) { + const bool ok = this->MoveChain(prev, node, destination); const int64_t destination_sibling = - GetActiveAlternativeSibling(Next(node)); + this->GetActiveAlternativeSibling(this->Next(node)); if (destination_sibling == -1) { // Not inserting before a pair node: insert sibling after node. - return MoveChain(Prev(sibling), sibling, node) || ok; + return this->MoveChain(this->Prev(sibling), sibling, node) || ok; } else { // Depending on the lifo status of the path, insert sibling before or // after destination_sibling since node is being inserted before // next(destination). if (!destination_is_lifo) { - if (Prev(destination_sibling) == sibling) return ok; - return MoveChain(Prev(sibling), sibling, Prev(destination_sibling)) || + if (this->Prev(destination_sibling) == sibling) return ok; + return this->MoveChain(this->Prev(sibling), sibling, + this->Prev(destination_sibling)) || ok; } else { - return MoveChain(Prev(sibling), sibling, destination_sibling) || ok; + return this->MoveChain(this->Prev(sibling), sibling, + destination_sibling) || + ok; } } } // Relocating the first node of a pair "after" the first node of another // pair. const int64_t destination_sibling = - GetActiveAlternativeSibling(destination); + this->GetActiveAlternativeSibling(destination); if (destination_sibling == -1) return false; - const bool ok = MoveChain(prev, node, destination); + const bool ok = this->MoveChain(prev, node, destination); if (!destination_is_lifo) { - return MoveChain(Prev(sibling), sibling, destination_sibling) || ok; + return this->MoveChain(this->Prev(sibling), sibling, + destination_sibling) || + ok; } else { - if (Prev(destination_sibling) == sibling) return ok; - return MoveChain(Prev(sibling), sibling, Prev(destination_sibling)) || ok; + if (this->Prev(destination_sibling) == sibling) return ok; + return this->MoveChain(this->Prev(sibling), sibling, + this->Prev(destination_sibling)) || + ok; } }; - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); if (neighbor < 0) return false; // TODO(user): Add support for incoming neighbors. DCHECK(outgoing); // TODO(user): Add support for lifo for neighbor-based move. - return do_move(/*node=*/neighbor, /*destination=*/BaseNode(0), + return do_move(/*node=*/neighbor, /*destination=*/this->BaseNode(0), /*destination_is_lifo=*/false); } - return do_move(/*node=*/Next(BaseNode(0)), /*destination=*/BaseNode(1), - force_lifo_ != nullptr && force_lifo_(StartNode(1))); + return do_move(/*node=*/this->Next(this->BaseNode(0)), + /*destination=*/this->BaseNode(1), + force_lifo_ != nullptr && force_lifo_(this->StartNode(1))); +} + +LocalSearchOperator* MakeLightPairRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + const std::vector& pairs, + std::function force_lifo) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new LightPairRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs, std::move(force_lifo))); + } + return solver->RevAlloc(new LightPairRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs, std::move(force_lifo))); +} + +LocalSearchOperator* MakeLightPairRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs, + std::function force_lifo) { + return MakeLightPairRelocate(solver, vars, secondary_vars, + std::move(start_empty_path_class), nullptr, + nullptr, pairs, std::move(force_lifo)); } -PairExchangeOperator::PairExchangeOperator( +template +PairExchangeOperator::PairExchangeOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, NeighborAccessor get_incoming_neighbors, NeighborAccessor get_outgoing_neighbors, const std::vector& pairs) - : PathOperator( + : PathOperator( vars, secondary_vars, /*number_of_base_nodes=*/ get_incoming_neighbors == nullptr && get_outgoing_neighbors == nullptr @@ -629,28 +833,29 @@ PairExchangeOperator::PairExchangeOperator( /*accept_path_end_base=*/false, std::move(start_empty_path_class), std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors)) { - AddPairAlternativeSets(pairs); + this->AddPairAlternativeSets(pairs); } -bool PairExchangeOperator::MakeNeighbor() { - const int64_t node1 = BaseNode(0); +template +bool PairExchangeOperator::MakeNeighbor() { + const int64_t node1 = this->BaseNode(0); int64_t prev1, sibling1, sibling_prev1 = -1; if (!GetPreviousAndSibling(node1, &prev1, &sibling1, &sibling_prev1)) { return false; } int64_t node2 = -1; - if (!HasNeighbors()) { - node2 = BaseNode(1); + if (!this->HasNeighbors()) { + node2 = this->BaseNode(1); } else { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); - if (neighbor < 0 || IsInactive(neighbor)) return false; + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); + if (neighbor < 0 || this->IsInactive(neighbor)) return false; if (outgoing) { - if (IsPathStart(neighbor)) return false; - } else if (IsPathEnd(neighbor)) { + if (this->IsPathStart(neighbor)) return false; + } else if (this->IsPathEnd(neighbor)) { return false; } - node2 = outgoing ? Prev(neighbor) : Next(neighbor); - if (IsPathEnd(node2)) return false; + node2 = outgoing ? this->Prev(neighbor) : this->Next(neighbor); + if (this->IsPathEnd(node2)) return false; } int64_t prev2, sibling2, sibling_prev2 = -1; if (!GetPreviousAndSibling(node2, &prev2, &sibling2, &sibling_prev2)) { @@ -659,15 +864,16 @@ bool PairExchangeOperator::MakeNeighbor() { bool status = true; // Exchanging node1 and node2. if (node1 == prev2) { - status = MoveChain(prev2, node2, prev1); + status = this->MoveChain(prev2, node2, prev1); if (sibling_prev1 == node2) sibling_prev1 = node1; if (sibling_prev2 == node2) sibling_prev2 = node1; } else if (node2 == prev1) { - status = MoveChain(prev1, node1, prev2); + status = this->MoveChain(prev1, node1, prev2); if (sibling_prev1 == node1) sibling_prev1 = node2; if (sibling_prev2 == node1) sibling_prev2 = node2; } else { - status = MoveChain(prev1, node1, node2) && MoveChain(prev2, node2, prev1); + status = this->MoveChain(prev1, node1, node2) && + this->MoveChain(prev2, node2, prev1); if (sibling_prev1 == node1) { sibling_prev1 = node2; } else if (sibling_prev1 == node2) { @@ -682,54 +888,88 @@ bool PairExchangeOperator::MakeNeighbor() { if (!status) return false; // Exchanging sibling1 and sibling2. if (sibling1 == sibling_prev2) { - status = MoveChain(sibling_prev2, sibling2, sibling_prev1); + status = this->MoveChain(sibling_prev2, sibling2, sibling_prev1); } else if (sibling2 == sibling_prev1) { - status = MoveChain(sibling_prev1, sibling1, sibling_prev2); + status = this->MoveChain(sibling_prev1, sibling1, sibling_prev2); } else { - status = MoveChain(sibling_prev1, sibling1, sibling2) && - MoveChain(sibling_prev2, sibling2, sibling_prev1); + status = this->MoveChain(sibling_prev1, sibling1, sibling2) && + this->MoveChain(sibling_prev2, sibling2, sibling_prev1); } // Swapping alternatives in. - SwapActiveAndInactive(sibling1, BaseSiblingAlternativeNode(0)); - SwapActiveAndInactive(node1, BaseAlternativeNode(0)); - if (!HasNeighbors()) { + this->SwapActiveAndInactive(sibling1, this->BaseSiblingAlternativeNode(0)); + this->SwapActiveAndInactive(node1, this->BaseAlternativeNode(0)); + if (!this->HasNeighbors()) { // TODO(user): Support alternatives with neighbors. - SwapActiveAndInactive(sibling2, BaseSiblingAlternativeNode(1)); - SwapActiveAndInactive(node2, BaseAlternativeNode(1)); + this->SwapActiveAndInactive(sibling2, this->BaseSiblingAlternativeNode(1)); + this->SwapActiveAndInactive(node2, this->BaseAlternativeNode(1)); } return status; } -bool PairExchangeOperator::GetPreviousAndSibling( +template +bool PairExchangeOperator::GetPreviousAndSibling( int64_t node, int64_t* previous, int64_t* sibling, int64_t* sibling_previous) const { - if (IsPathStart(node)) return false; - *previous = Prev(node); - *sibling = GetActiveAlternativeSibling(node); - *sibling_previous = *sibling >= 0 ? Prev(*sibling) : -1; + if (this->IsPathStart(node)) return false; + *previous = this->Prev(node); + *sibling = this->GetActiveAlternativeSibling(node); + *sibling_previous = *sibling >= 0 ? this->Prev(*sibling) : -1; return *sibling_previous >= 0; } -PairExchangeRelocateOperator::PairExchangeRelocateOperator( +LocalSearchOperator* MakePairExchange( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new PairExchangeOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); + } + return solver->RevAlloc(new PairExchangeOperator( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); +} + +LocalSearchOperator* MakePairExchange( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + return MakePairExchange(solver, vars, secondary_vars, + std::move(start_empty_path_class), nullptr, nullptr, + pairs); +} + +template +PairExchangeRelocateOperator::PairExchangeRelocateOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 6, true, false, - std::move(start_empty_path_class), nullptr, nullptr) { - AddPairAlternativeSets(pairs); + : PathOperator(vars, secondary_vars, 6, true, false, + std::move(start_empty_path_class), nullptr, + nullptr) { + this->AddPairAlternativeSets(pairs); } -bool PairExchangeRelocateOperator::MakeNeighbor() { - DCHECK_EQ(StartNode(kSecondPairFirstNodeDestination), - StartNode(kSecondPairSecondNodeDestination)); - DCHECK_EQ(StartNode(kSecondPairFirstNode), - StartNode(kFirstPairFirstNodeDestination)); - DCHECK_EQ(StartNode(kSecondPairFirstNode), - StartNode(kFirstPairSecondNodeDestination)); - - if (StartNode(kFirstPairFirstNode) == StartNode(kSecondPairFirstNode)) { - SetNextBaseToIncrement(kSecondPairFirstNode); +template +bool PairExchangeRelocateOperator::MakeNeighbor() { + DCHECK_EQ(this->StartNode(kSecondPairFirstNodeDestination), + this->StartNode(kSecondPairSecondNodeDestination)); + DCHECK_EQ(this->StartNode(kSecondPairFirstNode), + this->StartNode(kFirstPairFirstNodeDestination)); + DCHECK_EQ(this->StartNode(kSecondPairFirstNode), + this->StartNode(kFirstPairSecondNodeDestination)); + + if (this->StartNode(kFirstPairFirstNode) == + this->StartNode(kSecondPairFirstNode)) { + this->SetNextBaseToIncrement(kSecondPairFirstNode); return false; } // Through this method, [X][Y] represent the variable for the @@ -737,65 +977,69 @@ bool PairExchangeRelocateOperator::MakeNeighbor() { int64_t nodes[2][2]; int64_t prev[2][2]; int64_t dest[2][2]; - nodes[0][0] = BaseNode(kFirstPairFirstNode); - nodes[1][0] = BaseNode(kSecondPairFirstNode); + nodes[0][0] = this->BaseNode(kFirstPairFirstNode); + nodes[1][0] = this->BaseNode(kSecondPairFirstNode); if (nodes[1][0] <= nodes[0][0]) { // Exchange is symmetric. - SetNextBaseToIncrement(kSecondPairFirstNode); + this->SetNextBaseToIncrement(kSecondPairFirstNode); return false; } - if (!GetPreviousAndSibling(nodes[0][0], &prev[0][0], &nodes[0][1], + if (!this->GetPreviousAndSibling(nodes[0][0], &prev[0][0], &nodes[0][1], &prev[0][1])) { - SetNextBaseToIncrement(kFirstPairFirstNode); + this->SetNextBaseToIncrement(kFirstPairFirstNode); return false; } - if (!GetPreviousAndSibling(nodes[1][0], &prev[1][0], &nodes[1][1], + if (!this->GetPreviousAndSibling(nodes[1][0], &prev[1][0], &nodes[1][1], &prev[1][1])) { - SetNextBaseToIncrement(kSecondPairFirstNode); + this->SetNextBaseToIncrement(kSecondPairFirstNode); return false; } - if (!LoadAndCheckDest(0, 0, kFirstPairFirstNodeDestination, nodes, dest)) { - SetNextBaseToIncrement(kFirstPairFirstNodeDestination); + if (!this->LoadAndCheckDest(0, 0, kFirstPairFirstNodeDestination, nodes, + dest)) { + this->SetNextBaseToIncrement(kFirstPairFirstNodeDestination); return false; } - if (!LoadAndCheckDest(0, 1, kFirstPairSecondNodeDestination, nodes, dest)) { - SetNextBaseToIncrement(kFirstPairSecondNodeDestination); + if (!this->LoadAndCheckDest(0, 1, kFirstPairSecondNodeDestination, nodes, + dest)) { + this->SetNextBaseToIncrement(kFirstPairSecondNodeDestination); return false; } - if (StartNode(kSecondPairFirstNodeDestination) != - StartNode(kFirstPairFirstNode) || - !LoadAndCheckDest(1, 0, kSecondPairFirstNodeDestination, nodes, dest)) { - SetNextBaseToIncrement(kSecondPairFirstNodeDestination); + if (this->StartNode(kSecondPairFirstNodeDestination) != + this->StartNode(kFirstPairFirstNode) || + !this->LoadAndCheckDest(1, 0, kSecondPairFirstNodeDestination, nodes, + dest)) { + this->SetNextBaseToIncrement(kSecondPairFirstNodeDestination); return false; } - if (!LoadAndCheckDest(1, 1, kSecondPairSecondNodeDestination, nodes, dest)) { - SetNextBaseToIncrement(kSecondPairSecondNodeDestination); + if (!this->LoadAndCheckDest(1, 1, kSecondPairSecondNodeDestination, nodes, + dest)) { + this->SetNextBaseToIncrement(kSecondPairSecondNodeDestination); return false; } - if (!MoveNode(0, 1, nodes, dest, prev)) { - SetNextBaseToIncrement(kFirstPairSecondNodeDestination); + if (!this->MoveNode(0, 1, nodes, dest, prev)) { + this->SetNextBaseToIncrement(kFirstPairSecondNodeDestination); return false; } - if (!MoveNode(0, 0, nodes, dest, prev)) { - SetNextBaseToIncrement(kFirstPairSecondNodeDestination); + if (!this->MoveNode(0, 0, nodes, dest, prev)) { + this->SetNextBaseToIncrement(kFirstPairSecondNodeDestination); return false; } - if (!MoveNode(1, 1, nodes, dest, prev)) { + if (!this->MoveNode(1, 1, nodes, dest, prev)) { return false; } - if (!MoveNode(1, 0, nodes, dest, prev)) { + if (!this->MoveNode(1, 0, nodes, dest, prev)) { return false; } return true; } -bool PairExchangeRelocateOperator::MoveNode(int pair, int node, - int64_t nodes[2][2], - int64_t dest[2][2], +template +bool PairExchangeRelocateOperator::MoveNode( + int pair, int node, int64_t nodes[2][2], int64_t dest[2][2], int64_t prev[2][2]) { - if (!MoveChain(prev[pair][node], nodes[pair][node], dest[pair][node])) { + if (!this->MoveChain(prev[pair][node], nodes[pair][node], dest[pair][node])) { return false; } // Update the other pair if needed. @@ -808,17 +1052,18 @@ bool PairExchangeRelocateOperator::MoveNode(int pair, int node, return true; } -bool PairExchangeRelocateOperator::LoadAndCheckDest(int pair, int node, - int64_t base_node, - int64_t nodes[2][2], +template +bool PairExchangeRelocateOperator::LoadAndCheckDest( + int pair, int node, int64_t base_node, int64_t nodes[2][2], int64_t dest[2][2]) const { - dest[pair][node] = BaseNode(base_node); + dest[pair][node] = this->BaseNode(base_node); // A destination cannot be a node that will be moved. return !(nodes[0][0] == dest[pair][node] || nodes[0][1] == dest[pair][node] || nodes[1][0] == dest[pair][node] || nodes[1][1] == dest[pair][node]); } -bool PairExchangeRelocateOperator::OnSamePathAsPreviousBase( +template +bool PairExchangeRelocateOperator::OnSamePathAsPreviousBase( int64_t base_index) { // Ensuring the destination of the first pair is on the route of the second. // pair. @@ -828,26 +1073,42 @@ bool PairExchangeRelocateOperator::OnSamePathAsPreviousBase( base_index == kSecondPairSecondNodeDestination; } -int64_t PairExchangeRelocateOperator::GetBaseNodeRestartPosition( +template +int64_t +PairExchangeRelocateOperator::GetBaseNodeRestartPosition( int base_index) { if (base_index == kFirstPairSecondNodeDestination || base_index == kSecondPairSecondNodeDestination) { - return BaseNode(base_index - 1); + return this->BaseNode(base_index - 1); } else { - return StartNode(base_index); + return this->StartNode(base_index); } } -bool PairExchangeRelocateOperator::GetPreviousAndSibling( +template +bool PairExchangeRelocateOperator::GetPreviousAndSibling( int64_t node, int64_t* previous, int64_t* sibling, int64_t* sibling_previous) const { - if (IsPathStart(node)) return false; - *previous = Prev(node); - *sibling = GetActiveAlternativeSibling(node); - *sibling_previous = *sibling >= 0 ? Prev(*sibling) : -1; + if (this->IsPathStart(node)) return false; + *previous = this->Prev(node); + *sibling = this->GetActiveAlternativeSibling(node); + *sibling_previous = *sibling >= 0 ? this->Prev(*sibling) : -1; return *sibling_previous >= 0; } +LocalSearchOperator* MakePairExchangeRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new PairExchangeRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); + } + return solver->RevAlloc(new PairExchangeRelocateOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); +} + SwapIndexPairOperator::SwapIndexPairOperator( const std::vector& vars, const std::vector& path_vars, const std::vector& pairs) @@ -968,23 +1229,26 @@ bool SwapIndexPairOperator::UpdateActiveNodes() { return false; } -IndexPairSwapActiveOperator::IndexPairSwapActiveOperator( +template +IndexPairSwapActiveOperator::IndexPairSwapActiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 1, true, false, - std::move(start_empty_path_class), nullptr, nullptr), + : PathOperator(vars, secondary_vars, 1, true, false, + std::move(start_empty_path_class), nullptr, + nullptr), inactive_node_(0) { - AddPairAlternativeSets(pairs); + this->AddPairAlternativeSets(pairs); } -bool IndexPairSwapActiveOperator::MakeNextNeighbor(Assignment* delta, - Assignment* deltadelta) { - while (inactive_node_ < Size()) { - if (!IsInactive(inactive_node_) || - !PathOperator::MakeNextNeighbor(delta, deltadelta)) { - ResetPosition(); +template +bool IndexPairSwapActiveOperator::MakeNextNeighbor( + Assignment* delta, Assignment* deltadelta) { + while (inactive_node_ < this->Size()) { + if (!this->IsInactive(inactive_node_) || + !PathOperator::MakeNextNeighbor(delta, deltadelta)) { + this->ResetPosition(); ++inactive_node_; } else { return true; @@ -993,36 +1257,54 @@ bool IndexPairSwapActiveOperator::MakeNextNeighbor(Assignment* delta, return false; } -bool IndexPairSwapActiveOperator::MakeNeighbor() { - const int64_t base = BaseNode(0); - const int64_t next = Next(base); - const int64_t other = GetActiveAlternativeSibling(next); +template +bool IndexPairSwapActiveOperator::MakeNeighbor() { + const int64_t base = this->BaseNode(0); + const int64_t next = this->Next(base); + const int64_t other = this->GetActiveAlternativeSibling(next); if (other != -1) { - return MakeChainInactive(Prev(other), other) && - MakeChainInactive(base, next) && MakeActive(inactive_node_, base); + return this->MakeChainInactive(this->Prev(other), other) && + this->MakeChainInactive(base, next) && + this->MakeActive(inactive_node_, base); } return false; } -void IndexPairSwapActiveOperator::OnNodeInitialization() { - PathOperator::OnNodeInitialization(); - for (int i = 0; i < Size(); ++i) { - if (IsInactive(i)) { +template +void IndexPairSwapActiveOperator::OnNodeInitialization() { + PathOperator::OnNodeInitialization(); + for (int i = 0; i < this->Size(); ++i) { + if (this->IsInactive(i)) { inactive_node_ = i; return; } } - inactive_node_ = Size(); + inactive_node_ = this->Size(); } -RelocateExpensiveChain::RelocateExpensiveChain( +LocalSearchOperator* MakeIndexPairSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new IndexPairSwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); + } + return solver->RevAlloc(new IndexPairSwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); +} + +template +RelocateExpensiveChain::RelocateExpensiveChain( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, int num_arcs_to_consider, std::function arc_cost_for_path_start) - : PathOperator(vars, secondary_vars, 1, false, false, - std::move(start_empty_path_class), nullptr, nullptr), + : PathOperator(vars, secondary_vars, 1, false, false, + std::move(start_empty_path_class), nullptr, + nullptr), num_arcs_to_consider_(num_arcs_to_consider), current_path_(0), current_expensive_arc_indices_({-1, -1}), @@ -1032,7 +1314,8 @@ RelocateExpensiveChain::RelocateExpensiveChain( DCHECK_GE(num_arcs_to_consider_, 2); } -bool RelocateExpensiveChain::MakeNeighbor() { +template +bool RelocateExpensiveChain::MakeNeighbor() { // TODO(user): Consider node neighbors? The operator would no longer be // a path operator though, because we would no longer have any base nodes. const int first_arc_index = current_expensive_arc_indices_.first; @@ -1046,20 +1329,24 @@ bool RelocateExpensiveChain::MakeNeighbor() { const std::pair& second_start_and_rank = most_expensive_arc_starts_and_ranks_[second_arc_index]; if (first_start_and_rank.second < second_start_and_rank.second) { - return CheckChainValidity(first_start_and_rank.first, - second_start_and_rank.first, BaseNode(0)) && - MoveChain(first_start_and_rank.first, second_start_and_rank.first, - BaseNode(0)); - } - return CheckChainValidity(second_start_and_rank.first, - first_start_and_rank.first, BaseNode(0)) && - MoveChain(second_start_and_rank.first, first_start_and_rank.first, - BaseNode(0)); + return this->CheckChainValidity(first_start_and_rank.first, + second_start_and_rank.first, + this->BaseNode(0)) && + this->MoveChain(first_start_and_rank.first, + second_start_and_rank.first, this->BaseNode(0)); + } + return this->CheckChainValidity(second_start_and_rank.first, + first_start_and_rank.first, + this->BaseNode(0)) && + this->MoveChain(second_start_and_rank.first, + first_start_and_rank.first, this->BaseNode(0)); } -bool RelocateExpensiveChain::MakeOneNeighbor() { + +template +bool RelocateExpensiveChain::MakeOneNeighbor() { while (has_non_empty_paths_to_explore_) { - if (!PathOperator::MakeOneNeighbor()) { - ResetPosition(); + if (!PathOperator::MakeOneNeighbor()) { + this->ResetPosition(); // Move on to the next expensive arcs on the same path. if (IncrementCurrentArcIndices()) { continue; @@ -1076,8 +1363,9 @@ bool RelocateExpensiveChain::MakeOneNeighbor() { return false; } -void RelocateExpensiveChain::OnNodeInitialization() { - if (current_path_ >= path_starts().size()) { +template +void RelocateExpensiveChain::OnNodeInitialization() { + if (current_path_ >= this->path_starts().size()) { // current_path_ was made empty by last move (and it was the last non-empty // path), restart from 0. current_path_ = 0; @@ -1086,14 +1374,16 @@ void RelocateExpensiveChain::OnNodeInitialization() { has_non_empty_paths_to_explore_ = FindMostExpensiveChainsOnRemainingPaths(); } -void RelocateExpensiveChain::IncrementCurrentPath() { - const int num_paths = path_starts().size(); +template +void RelocateExpensiveChain::IncrementCurrentPath() { + const int num_paths = this->path_starts().size(); if (++current_path_ == num_paths) { current_path_ = 0; } } -bool RelocateExpensiveChain::IncrementCurrentArcIndices() { +template +bool RelocateExpensiveChain::IncrementCurrentArcIndices() { int& second_index = current_expensive_arc_indices_.second; if (++second_index < most_expensive_arc_starts_and_ranks_.size()) { return true; @@ -1107,12 +1397,14 @@ bool RelocateExpensiveChain::IncrementCurrentArcIndices() { return false; } -bool RelocateExpensiveChain::FindMostExpensiveChainsOnRemainingPaths() { +template +bool RelocateExpensiveChain< + ignore_path_vars>::FindMostExpensiveChainsOnRemainingPaths() { do { if (FindMostExpensiveArcsOnRoute( - num_arcs_to_consider_, path_starts()[current_path_], - [this](int64_t i) { return OldNext(i); }, - [this](int64_t node) { return IsPathEnd(node); }, + num_arcs_to_consider_, this->path_starts()[current_path_], + [this](int64_t i) { return this->OldNext(i); }, + [this](int64_t node) { return this->IsPathEnd(node); }, arc_cost_for_path_start_, &most_expensive_arc_starts_and_ranks_, ¤t_expensive_arc_indices_)) { return true; @@ -1122,6 +1414,22 @@ bool RelocateExpensiveChain::FindMostExpensiveChainsOnRemainingPaths() { return false; } +LocalSearchOperator* MakeRelocateExpensiveChain( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + int num_arcs_to_consider, + std::function arc_cost_for_path_start) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new RelocateExpensiveChain( + vars, secondary_vars, std::move(start_empty_path_class), + num_arcs_to_consider, std::move(arc_cost_for_path_start))); + } + return solver->RevAlloc(new RelocateExpensiveChain( + vars, secondary_vars, std::move(start_empty_path_class), + num_arcs_to_consider, std::move(arc_cost_for_path_start))); +} + PickupAndDeliveryData::PickupAndDeliveryData( int num_nodes, absl::Span pairs) : is_pickup_node_(num_nodes, false), @@ -1139,13 +1447,14 @@ PickupAndDeliveryData::PickupAndDeliveryData( } } -RelocateSubtrip::RelocateSubtrip( +template +RelocateSubtrip::RelocateSubtrip( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, NeighborAccessor, NeighborAccessor get_outgoing_neighbors, absl::Span pairs) - : PathOperator( + : PathOperator( vars, secondary_vars, /*number_of_base_nodes=*/ get_outgoing_neighbors == nullptr ? 2 : 1, @@ -1153,25 +1462,28 @@ RelocateSubtrip::RelocateSubtrip( /*accept_path_end_base=*/false, std::move(start_empty_path_class), nullptr, // Incoming neighbors aren't supported as of 09/2024. std::move(get_outgoing_neighbors)), - pd_data_(number_of_nexts_, pairs) { + pd_data_(this->number_of_nexts_, pairs) { opened_pairs_set_.resize(pairs.size(), false); } -void RelocateSubtrip::SetPath(absl::Span path, int path_id) { +template +void RelocateSubtrip::SetPath(absl::Span path, + int path_id) { for (int i = 1; i < path.size(); ++i) { - SetNext(path[i - 1], path[i], path_id); + this->SetNext(path[i - 1], path[i], path_id); } } -bool RelocateSubtrip::RelocateSubTripFromPickup(const int64_t chain_first_node, - const int64_t insertion_node) { - if (IsPathEnd(insertion_node)) return false; - if (Prev(chain_first_node) == insertion_node) +template +bool RelocateSubtrip::RelocateSubTripFromPickup( + const int64_t chain_first_node, const int64_t insertion_node) { + if (this->IsPathEnd(insertion_node)) return false; + if (this->Prev(chain_first_node) == insertion_node) return false; // Skip null move. int num_opened_pairs = 0; // Split chain into subtrip and rejected nodes. - rejected_nodes_ = {Prev(chain_first_node)}; + rejected_nodes_ = {this->Prev(chain_first_node)}; subtrip_nodes_ = {insertion_node}; int current = chain_first_node; do { @@ -1193,29 +1505,30 @@ bool RelocateSubtrip::RelocateSubTripFromPickup(const int64_t chain_first_node, opened_pairs_set_[pair] = false; } } - current = Next(current); - } while (num_opened_pairs != 0 && !IsPathEnd(current)); + current = this->Next(current); + } while (num_opened_pairs != 0 && !this->IsPathEnd(current)); DCHECK_EQ(num_opened_pairs, 0); rejected_nodes_.push_back(current); - subtrip_nodes_.push_back(Next(insertion_node)); + subtrip_nodes_.push_back(this->Next(insertion_node)); // Set new paths. - SetPath(rejected_nodes_, Path(chain_first_node)); - SetPath(subtrip_nodes_, Path(insertion_node)); + SetPath(rejected_nodes_, this->Path(chain_first_node)); + SetPath(subtrip_nodes_, this->Path(insertion_node)); return true; } -bool RelocateSubtrip::RelocateSubTripFromDelivery( +template +bool RelocateSubtrip::RelocateSubTripFromDelivery( const int64_t chain_last_node, const int64_t insertion_node) { - if (IsPathEnd(insertion_node)) return false; + if (this->IsPathEnd(insertion_node)) return false; // opened_pairs_set_ should be all false. DCHECK(std::none_of(opened_pairs_set_.begin(), opened_pairs_set_.end(), [](bool value) { return value; })); int num_opened_pairs = 0; // Split chain into subtrip and rejected nodes. Store nodes in reverse order. - rejected_nodes_ = {Next(chain_last_node)}; - subtrip_nodes_ = {Next(insertion_node)}; + rejected_nodes_ = {this->Next(chain_last_node)}; + subtrip_nodes_ = {this->Next(insertion_node)}; int current = chain_last_node; do { if (current == insertion_node) { @@ -1235,8 +1548,8 @@ bool RelocateSubtrip::RelocateSubTripFromDelivery( opened_pairs_set_[pair] = false; } } - current = Prev(current); - } while (num_opened_pairs != 0 && !IsPathStart(current)); + current = this->Prev(current); + } while (num_opened_pairs != 0 && !this->IsPathStart(current)); DCHECK_EQ(num_opened_pairs, 0); if (current == insertion_node) return false; // Skip null move. rejected_nodes_.push_back(current); @@ -1248,14 +1561,15 @@ bool RelocateSubtrip::RelocateSubTripFromDelivery( std::reverse(subtrip_nodes_.begin(), subtrip_nodes_.end()); // Set new paths. - SetPath(rejected_nodes_, Path(chain_last_node)); - SetPath(subtrip_nodes_, Path(insertion_node)); + SetPath(rejected_nodes_, this->Path(chain_last_node)); + SetPath(subtrip_nodes_, this->Path(insertion_node)); return true; } -bool RelocateSubtrip::MakeNeighbor() { +template +bool RelocateSubtrip::MakeNeighbor() { const auto do_move = [this](int64_t node, int64_t insertion_node) { - if (IsInactive(node)) return false; + if (this->IsInactive(node)) return false; if (pd_data_.IsPickupNode(node)) { return RelocateSubTripFromPickup(node, insertion_node); } else if (pd_data_.IsDeliveryNode(node)) { @@ -1264,23 +1578,54 @@ bool RelocateSubtrip::MakeNeighbor() { return false; } }; - if (HasNeighbors()) { - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); + if (this->HasNeighbors()) { + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); if (neighbor < 0) return false; DCHECK(outgoing); - if (IsInactive(neighbor)) return false; - return do_move(/*node=*/neighbor, /*insertion_node=*/BaseNode(0)); + if (this->IsInactive(neighbor)) return false; + return do_move(/*node=*/neighbor, /*insertion_node=*/this->BaseNode(0)); } - return do_move(/*node=*/BaseNode(0), /*insertion_node=*/BaseNode(1)); + return do_move(/*node=*/this->BaseNode(0), + /*insertion_node=*/this->BaseNode(1)); } -ExchangeSubtrip::ExchangeSubtrip( +LocalSearchOperator* MakeRelocateSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + absl::Span pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new RelocateSubtrip( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); + } + return solver->RevAlloc(new RelocateSubtrip( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); +} + +LocalSearchOperator* MakeRelocateSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + absl::Span pairs) { + return MakeRelocateSubtrip(solver, vars, secondary_vars, + std::move(start_empty_path_class), nullptr, + nullptr, pairs); +} + +template +ExchangeSubtrip::ExchangeSubtrip( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, NeighborAccessor, NeighborAccessor get_outgoing_neighbors, absl::Span pairs) - : PathOperator( + : PathOperator( vars, secondary_vars, /*number_of_base_nodes=*/ get_outgoing_neighbors == nullptr ? 2 : 1, @@ -1288,13 +1633,15 @@ ExchangeSubtrip::ExchangeSubtrip( /*accept_path_end_base=*/false, std::move(start_empty_path_class), nullptr, // Incoming neighbors aren't supported as of 09/2024. std::move(get_outgoing_neighbors)), - pd_data_(number_of_nexts_, pairs) { + pd_data_(this->number_of_nexts_, pairs) { opened_pairs_set_.resize(pairs.size(), false); } -void ExchangeSubtrip::SetPath(absl::Span path, int path_id) { +template +void ExchangeSubtrip::SetPath(absl::Span path, + int path_id) { for (int i = 1; i < path.size(); ++i) { - SetNext(path[i - 1], path[i], path_id); + this->SetNext(path[i - 1], path[i], path_id); } } @@ -1304,29 +1651,31 @@ bool VectorContains(absl::Span values, int64_t target) { } } // namespace -bool ExchangeSubtrip::MakeNeighbor() { +template +bool ExchangeSubtrip::MakeNeighbor() { int64_t node0 = -1; int64_t node1 = -1; - if (HasNeighbors()) { - const int64_t node = BaseNode(0); - const auto [neighbor, outgoing] = GetNeighborForBaseNode(0); + if (this->HasNeighbors()) { + const int64_t node = this->BaseNode(0); + const auto [neighbor, outgoing] = this->GetNeighborForBaseNode(0); if (neighbor < 0) return false; DCHECK(outgoing); - if (IsInactive(neighbor)) return false; + if (this->IsInactive(neighbor)) return false; if (pd_data_.IsDeliveryNode(node) && - pd_data_.IsDeliveryNode(Prev(neighbor))) { + pd_data_.IsDeliveryNode(this->Prev(neighbor))) { node0 = node; - node1 = Prev(neighbor); - } else if (pd_data_.IsPickupNode(neighbor) && !IsPathEnd(Next(node)) && - pd_data_.IsPickupNode(Next(node))) { - node0 = Next(node); + node1 = this->Prev(neighbor); + } else if (pd_data_.IsPickupNode(neighbor) && + !this->IsPathEnd(this->Next(node)) && + pd_data_.IsPickupNode(this->Next(node))) { + node0 = this->Next(node); node1 = neighbor; } else { return false; } } else { - node0 = BaseNode(0); - node1 = BaseNode(1); + node0 = this->BaseNode(0); + node1 = this->BaseNode(1); } if (pd_data_.GetPairOfNode(node0) == -1) return false; @@ -1346,7 +1695,7 @@ bool ExchangeSubtrip::MakeNeighbor() { } // If paths intersect, skip the move. - if (HasNeighbors() || StartNode(0) == StartNode(1)) { + if (this->HasNeighbors() || this->StartNode(0) == this->StartNode(1)) { if (VectorContains(rejects0_, subtrip1_.front())) return false; if (VectorContains(rejects1_, subtrip0_.front())) return false; if (VectorContains(subtrip0_, subtrip1_.front())) return false; @@ -1354,10 +1703,10 @@ bool ExchangeSubtrip::MakeNeighbor() { } // Assemble the new paths. - path0_ = {Prev(subtrip0_.front())}; - path1_ = {Prev(subtrip1_.front())}; - const int64_t last0 = Next(subtrip0_.back()); - const int64_t last1 = Next(subtrip1_.back()); + path0_ = {this->Prev(subtrip0_.front())}; + path1_ = {this->Prev(subtrip1_.front())}; + const int64_t last0 = this->Next(subtrip0_.back()); + const int64_t last1 = this->Next(subtrip1_.back()); const bool concatenated01 = last0 == subtrip1_.front(); const bool concatenated10 = last1 == subtrip0_.front(); @@ -1382,14 +1731,15 @@ bool ExchangeSubtrip::MakeNeighbor() { // Change the paths. Since SetNext() modifies Path() values, // record path_id0 and path_id11 before calling SetPath(); - const int64_t path0_id = Path(node0); - const int64_t path1_id = Path(node1); + const int64_t path0_id = this->Path(node0); + const int64_t path1_id = this->Path(node1); SetPath(path0_, path0_id); SetPath(path1_, path1_id); return true; } -bool ExchangeSubtrip::ExtractChainsAndCheckCanonical( +template +bool ExchangeSubtrip::ExtractChainsAndCheckCanonical( int64_t base_node, std::vector* rejects, std::vector* subtrip) { const bool extracted = @@ -1404,8 +1754,9 @@ bool ExchangeSubtrip::ExtractChainsAndCheckCanonical( !rejects->empty(); } -bool ExchangeSubtrip::ExtractChainsFromPickup(int64_t base_node, - std::vector* rejects, +template +bool ExchangeSubtrip::ExtractChainsFromPickup( + int64_t base_node, std::vector* rejects, std::vector* subtrip) { DCHECK(pd_data_.IsPickupNode(base_node)); DCHECK(rejects->empty()); @@ -1429,13 +1780,14 @@ bool ExchangeSubtrip::ExtractChainsFromPickup(int64_t base_node, opened_pairs_set_[pair] = false; } } - current = Next(current); - } while (num_opened_pairs != 0 && !IsPathEnd(current)); + current = this->Next(current); + } while (num_opened_pairs != 0 && !this->IsPathEnd(current)); return num_opened_pairs == 0; } -bool ExchangeSubtrip::ExtractChainsFromDelivery(int64_t base_node, - std::vector* rejects, +template +bool ExchangeSubtrip::ExtractChainsFromDelivery( + int64_t base_node, std::vector* rejects, std::vector* subtrip) { DCHECK(pd_data_.IsDeliveryNode(base_node)); DCHECK(rejects->empty()); @@ -1459,12 +1811,41 @@ bool ExchangeSubtrip::ExtractChainsFromDelivery(int64_t base_node, opened_pairs_set_[pair] = false; } } - current = Prev(current); - } while (num_opened_pairs != 0 && !IsPathStart(current)); + current = this->Prev(current); + } while (num_opened_pairs != 0 && !this->IsPathStart(current)); if (num_opened_pairs != 0) return false; std::reverse(rejects->begin(), rejects->end()); std::reverse(subtrip->begin(), subtrip->end()); return true; } +LocalSearchOperator* MakeExchangeSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + absl::Span pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new ExchangeSubtrip( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); + } + return solver->RevAlloc(new ExchangeSubtrip( + vars, secondary_vars, std::move(start_empty_path_class), + std::move(get_incoming_neighbors), std::move(get_outgoing_neighbors), + pairs)); +} + +LocalSearchOperator* MakeExchangeSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + absl::Span pairs) { + return MakeExchangeSubtrip(solver, vars, secondary_vars, + std::move(start_empty_path_class), nullptr, + nullptr, pairs); +} + } // namespace operations_research diff --git a/ortools/constraint_solver/routing_neighborhoods.h b/ortools/constraint_solver/routing_neighborhoods.h index 55fc0dbf539..a387fed953d 100644 --- a/ortools/constraint_solver/routing_neighborhoods.h +++ b/ortools/constraint_solver/routing_neighborhoods.h @@ -14,6 +14,8 @@ #ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_NEIGHBORHOODS_H_ #define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_NEIGHBORHOODS_H_ +#include + #include #include #include @@ -50,7 +52,9 @@ namespace operations_research { /// This operator is extremely useful to move chains of nodes which are located /// at the same place (for instance nodes part of a same stop). // TODO(user): Consider merging with standard Relocate in local_search.cc. -class MakeRelocateNeighborsOperator : public PathOperator { + +template +class MakeRelocateNeighborsOperator : public PathOperator { public: MakeRelocateNeighborsOperator( const std::vector& vars, @@ -59,15 +63,7 @@ class MakeRelocateNeighborsOperator : public PathOperator { std::function&(int, int)> get_incoming_neighbors, std::function&(int, int)> get_outgoing_neighbors, RoutingTransitCallback2 arc_evaluator); - MakeRelocateNeighborsOperator( - const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - RoutingTransitCallback2 arc_evaluator) - : MakeRelocateNeighborsOperator( - vars, secondary_vars, std::move(start_empty_path_class), nullptr, - nullptr, std::move(arc_evaluator)) {} - ~MakeRelocateNeighborsOperator() override {} + ~MakeRelocateNeighborsOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "RelocateNeighbors"; } @@ -94,6 +90,20 @@ class MakeRelocateNeighborsOperator : public PathOperator { RoutingTransitCallback2 arc_evaluator_; }; +LocalSearchOperator* MakeRelocateNeighbors( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + RoutingTransitCallback2 arc_evaluator); + +LocalSearchOperator* MakeRelocateNeighbors( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + RoutingTransitCallback2 arc_evaluator); + // Class used to compute shortest paths on DAGs formed by chains of alternative // node sets. class ShortestPathOnAlternatives { @@ -121,7 +131,8 @@ class ShortestPathOnAlternatives { // Reverses a sub-chain of a path and then replaces it with the shortest path on // the DAG formed by the sequence of its node alternatives. This operator will // only reverse chains with at least a node with multiple alternatives. -class TwoOptWithShortestPathOperator : public PathOperator { +template +class TwoOptWithShortestPathOperator : public PathOperator { public: TwoOptWithShortestPathOperator( const std::vector& vars, @@ -139,7 +150,7 @@ class TwoOptWithShortestPathOperator : public PathOperator { return true; } int64_t GetBaseNodeRestartPosition(int base_index) override { - return (base_index == 0) ? StartNode(0) : BaseNode(0); + return (base_index == 0) ? this->StartNode(0) : this->BaseNode(0); } // Necessary to have proper information about alternatives of current path. bool RestartAtPathStartOnSynchronize() override { return true; } @@ -163,6 +174,13 @@ class TwoOptWithShortestPathOperator : public PathOperator { std::vector chain_; }; +LocalSearchOperator* MakeTwoOptWithShortestPath( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::vector> alternative_sets, + RoutingTransitCallback2 arc_evaluator); + // Swaps active nodes from node alternatives in sequence. Considers chains of // nodes with alternatives, builds a DAG from the chain, each "layer" of the DAG // being composed of the set of alternatives of the node at a given rank in the @@ -181,7 +199,8 @@ class TwoOptWithShortestPathOperator : public PathOperator { // Supposing the shortest path from 0 to 5 is 0, 2, 3, 5, the neighbor for the // chain will be: 0 -> 2 -> 3 -> 5. // TODO(user): Support vehicle-class-dependent arc_evaluators. -class SwapActiveToShortestPathOperator : public PathOperator { +template +class SwapActiveToShortestPathOperator : public PathOperator { public: SwapActiveToShortestPathOperator( const std::vector& vars, @@ -200,6 +219,13 @@ class SwapActiveToShortestPathOperator : public PathOperator { std::vector chain_; }; +LocalSearchOperator* MakeSwapActiveToShortestPath( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::vector> alternative_sets, + RoutingTransitCallback2 arc_evaluator); + /// Pair-based neighborhood operators, designed to move nodes by pairs (pairs /// are static and given). These neighborhoods are very useful for Pickup and /// Delivery problems where pickup and delivery nodes must remain on the same @@ -223,13 +249,14 @@ class SwapActiveToShortestPathOperator : public PathOperator { /// 1 -> [B] -> [A] -> 2 -> 3 /// 1 -> 2 -> [B] -> [A] -> 3 /// which can only be obtained by inserting A after B. -class MakePairActiveOperator : public PathOperator { +template +class MakePairActiveOperator : public PathOperator { public: MakePairActiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs); - ~MakePairActiveOperator() override {} + ~MakePairActiveOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "MakePairActive"; } @@ -258,8 +285,15 @@ class MakePairActiveOperator : public PathOperator { const std::vector pairs_; }; +LocalSearchOperator* MakePairActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// Operator which makes pairs of active nodes inactive. -class MakePairInactiveOperator : public PathOperator { +template +class MakePairInactiveOperator : public PathOperator { public: MakePairInactiveOperator(const std::vector& vars, const std::vector& secondary_vars, @@ -270,6 +304,12 @@ class MakePairInactiveOperator : public PathOperator { std::string DebugString() const override { return "MakePairInActive"; } }; +LocalSearchOperator* MakePairInactive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// Operator which moves a pair of nodes to another position where the first /// node of the pair must be before the second node on the same path. /// Possible neighbors for the path 1 -> A -> B -> 2 -> 3 (where (1, 3) are @@ -278,13 +318,14 @@ class MakePairInactiveOperator : public PathOperator { /// 1 -> [A] -> 2 -> [B] -> 3 /// 1 -> 2 -> [A] -> [B] -> 3 /// The pair can be moved to another path. -class PairRelocateOperator : public PathOperator { +template +class PairRelocateOperator : public PathOperator { public: PairRelocateOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs); - ~PairRelocateOperator() override {} + ~PairRelocateOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "PairRelocateOperator"; } @@ -308,9 +349,16 @@ class PairRelocateOperator : public PathOperator { static constexpr int kPairSecondNodeDestination = 2; }; +LocalSearchOperator* MakePairRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// Operator which moves a pair of nodes to another position where the first /// node of the pair is directly before the second node. -class GroupPairAndRelocateOperator : public PathOperator { +template +class GroupPairAndRelocateOperator : public PathOperator { public: GroupPairAndRelocateOperator( const std::vector& vars, @@ -319,20 +367,26 @@ class GroupPairAndRelocateOperator : public PathOperator { std::function&(int, int)> get_incoming_neighbors, std::function&(int, int)> get_outgoing_neighbors, const std::vector& pairs); - GroupPairAndRelocateOperator( - const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - const std::vector& pairs) - : GroupPairAndRelocateOperator(vars, secondary_vars, - std::move(start_empty_path_class), nullptr, - nullptr, pairs) {} - ~GroupPairAndRelocateOperator() override {} + ~GroupPairAndRelocateOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "GroupPairAndRelocate"; } }; +LocalSearchOperator* MakeGroupPairAndRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + const std::vector& pairs); + +LocalSearchOperator* MakeGroupPairAndRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// Operator which moves a pair of nodes to another position where the first /// node of the pair must be before the second node on the same path. /// The default behavior of the operator is to insert the first node after the @@ -344,7 +398,8 @@ class GroupPairAndRelocateOperator : public PathOperator { /// returns true then the LIFO behavior will be enforced, otherwise it's FIFO. // TODO(user): Add a version which inserts the first node before the other // pair's first node; there are many redundant neighbors if done blindly. -class LightPairRelocateOperator : public PathOperator { +template +class LightPairRelocateOperator : public PathOperator { public: LightPairRelocateOperator( const std::vector& vars, @@ -354,12 +409,7 @@ class LightPairRelocateOperator : public PathOperator { std::function&(int, int)> get_outgoing_neighbors, const std::vector& pairs, std::function force_lifo = nullptr); - LightPairRelocateOperator(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - const std::vector& pairs, - std::function force_lifo = nullptr); - ~LightPairRelocateOperator() override {} + ~LightPairRelocateOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { @@ -370,13 +420,30 @@ class LightPairRelocateOperator : public PathOperator { std::function force_lifo_; }; +LocalSearchOperator* MakeLightPairRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + const std::vector& pairs, + std::function force_lifo = nullptr); + +LocalSearchOperator* MakeLightPairRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs, + std::function force_lifo = nullptr); + /// Operator which exchanges the position of two pairs; for both pairs the first /// node of the pair must be before the second node on the same path. /// Possible neighbors for the paths 1 -> A -> B -> 2 -> 3 and 4 -> C -> D -> 5 /// (where (1, 3) and (4, 5) are first and last nodes of the paths and can /// therefore not be moved, and (A, B) and (C,D) are pairs of nodes): /// 1 -> [C] -> [D] -> 2 -> 3, 4 -> [A] -> [B] -> 5 -class PairExchangeOperator : public PathOperator { +template +class PairExchangeOperator : public PathOperator { public: PairExchangeOperator( const std::vector& vars, @@ -385,14 +452,7 @@ class PairExchangeOperator : public PathOperator { std::function&(int, int)> get_incoming_neighbors, std::function&(int, int)> get_outgoing_neighbors, const std::vector& pairs); - PairExchangeOperator(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - const std::vector& pairs) - : PairExchangeOperator(vars, secondary_vars, - std::move(start_empty_path_class), nullptr, - nullptr, pairs) {} - ~PairExchangeOperator() override {} + ~PairExchangeOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { return "PairExchangeOperator"; } @@ -406,6 +466,20 @@ class PairExchangeOperator : public PathOperator { int64_t* sibling_previous) const; }; +LocalSearchOperator* MakePairExchange( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + const std::vector& pairs); + +LocalSearchOperator* MakePairExchange( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// Operator which exchanges the paths of two pairs (path have to be different). /// Pairs are inserted in all possible positions in their new path with the /// constraint that the second node must be placed after the first. @@ -419,14 +493,15 @@ class PairExchangeOperator : public PathOperator { /// 1 -> C -> D -> 2 -> 3 4 -> A -> B -> 5 -> 6 /// 1 -> C -> 2 -> D -> 3 4 -> A -> 5 -> B -> 6 /// 1 -> 2 -> C -> D -> 3 4 -> 5 -> A -> B -> 6 -class PairExchangeRelocateOperator : public PathOperator { +template +class PairExchangeRelocateOperator : public PathOperator { public: PairExchangeRelocateOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs); - ~PairExchangeRelocateOperator() override {} + ~PairExchangeRelocateOperator() override = default; bool MakeNeighbor() override; std::string DebugString() const override { @@ -454,6 +529,12 @@ class PairExchangeRelocateOperator : public PathOperator { static constexpr int kSecondPairSecondNodeDestination = 5; }; +LocalSearchOperator* MakePairExchangeRelocate( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// Operator which iterates through each alternative of a set of pairs. If a /// pair has n and m alternatives, n.m alternatives will be explored. /// Possible neighbors for the path 1 -> A -> a -> 2 (where (1, 2) are first and @@ -469,7 +550,7 @@ class SwapIndexPairOperator : public IntVarLocalSearchOperator { SwapIndexPairOperator(const std::vector& vars, const std::vector& path_vars, const std::vector& pairs); - ~SwapIndexPairOperator() override {} + ~SwapIndexPairOperator() override = default; bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) override; void OnStart() override; @@ -502,14 +583,15 @@ class SwapIndexPairOperator : public IntVarLocalSearchOperator { /// Operator which inserts inactive nodes into a path and makes a pair of /// active nodes inactive. -class IndexPairSwapActiveOperator : public PathOperator { +template +class IndexPairSwapActiveOperator : public PathOperator { public: IndexPairSwapActiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs); - ~IndexPairSwapActiveOperator() override {} + ~IndexPairSwapActiveOperator() override = default; bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) override; bool MakeNeighbor() override; @@ -523,6 +605,12 @@ class IndexPairSwapActiveOperator : public PathOperator { int inactive_node_; }; +LocalSearchOperator* MakeIndexPairSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs); + /// RelocateExpensiveChain /// /// Operator which relocates the most expensive subchains (given a cost @@ -530,7 +618,8 @@ class IndexPairSwapActiveOperator : public PathOperator { /// /// The most expensive chain on a path is the one resulting from cutting the 2 /// most expensive arcs on this path. -class RelocateExpensiveChain : public PathOperator { +template +class RelocateExpensiveChain : public PathOperator { public: RelocateExpensiveChain(const std::vector& vars, const std::vector& secondary_vars, @@ -538,7 +627,8 @@ class RelocateExpensiveChain : public PathOperator { int num_arcs_to_consider, std::function arc_cost_for_path_start); - ~RelocateExpensiveChain() override {} + ~RelocateExpensiveChain() override = default; + bool MakeNeighbor() override; bool MakeOneNeighbor() override; @@ -569,6 +659,13 @@ class RelocateExpensiveChain : public PathOperator { bool has_non_empty_paths_to_explore_; }; +LocalSearchOperator* MakeRelocateExpensiveChain( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + int num_arcs_to_consider, + std::function arc_cost_for_path_start); + /// Operator which inserts pairs of inactive nodes into a path and makes an /// active node inactive. /// There are two versions: @@ -576,14 +673,14 @@ class RelocateExpensiveChain : public PathOperator { /// pair (with swap_first true); /// - one which makes inactive the node being replaced by the second node of the /// pair (with swap_first false). -template -class PairNodeSwapActiveOperator : public PathOperator { +template +class PairNodeSwapActiveOperator : public PathOperator { public: PairNodeSwapActiveOperator(const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs); - ~PairNodeSwapActiveOperator() override {} + ~PairNodeSwapActiveOperator() override = default; bool MakeNextNeighbor(Assignment* delta, Assignment* deltadelta) override; bool MakeNeighbor() override; @@ -614,33 +711,37 @@ class PairNodeSwapActiveOperator : public PathOperator { // ========================================================================== // Section: Implementations of the template classes declared above. -template -PairNodeSwapActiveOperator::PairNodeSwapActiveOperator( +template +PairNodeSwapActiveOperator:: + PairNodeSwapActiveOperator( const std::vector& vars, const std::vector& secondary_vars, std::function start_empty_path_class, const std::vector& pairs) - : PathOperator(vars, secondary_vars, 2, false, false, - std::move(start_empty_path_class), nullptr, nullptr), + : PathOperator(vars, secondary_vars, 2, false, false, + std::move(start_empty_path_class), nullptr, + nullptr), inactive_pair_(0), pairs_(pairs) {} -template -int64_t PairNodeSwapActiveOperator::GetBaseNodeRestartPosition( - int base_index) { +template +int64_t PairNodeSwapActiveOperator< + swap_first, ignore_path_vars>::GetBaseNodeRestartPosition(int base_index) { // Base node 1 must be after base node 0 if they are both on the same path. - if (base_index == 0 || StartNode(base_index) != StartNode(base_index - 1)) { - return StartNode(base_index); + if (base_index == 0 || + this->StartNode(base_index) != this->StartNode(base_index - 1)) { + return this->StartNode(base_index); } else { - return BaseNode(base_index - 1); + return this->BaseNode(base_index - 1); } } -template -void PairNodeSwapActiveOperator::OnNodeInitialization() { +template +void PairNodeSwapActiveOperator::OnNodeInitialization() { for (int i = 0; i < pairs_.size(); ++i) { - if (IsInactive(pairs_[i].pickup_alternatives[0]) && - IsInactive(pairs_[i].delivery_alternatives[0])) { + if (this->IsInactive(pairs_[i].pickup_alternatives[0]) && + this->IsInactive(pairs_[i].delivery_alternatives[0])) { inactive_pair_ = i; return; } @@ -648,14 +749,14 @@ void PairNodeSwapActiveOperator::OnNodeInitialization() { inactive_pair_ = pairs_.size(); } -template -bool PairNodeSwapActiveOperator::MakeNextNeighbor( +template +bool PairNodeSwapActiveOperator::MakeNextNeighbor( Assignment* delta, Assignment* deltadelta) { while (inactive_pair_ < pairs_.size()) { - if (!IsInactive(pairs_[inactive_pair_].pickup_alternatives[0]) || - !IsInactive(pairs_[inactive_pair_].delivery_alternatives[0]) || - !PathOperator::MakeNextNeighbor(delta, deltadelta)) { - ResetPosition(); + if (!this->IsInactive(pairs_[inactive_pair_].pickup_alternatives[0]) || + !this->IsInactive(pairs_[inactive_pair_].delivery_alternatives[0]) || + !PathOperator::MakeNextNeighbor(delta, deltadelta)) { + this->ResetPosition(); ++inactive_pair_; } else { return true; @@ -664,25 +765,39 @@ bool PairNodeSwapActiveOperator::MakeNextNeighbor( return false; } -template -bool PairNodeSwapActiveOperator::MakeNeighbor() { - const int64_t base = BaseNode(0); - if (IsPathEnd(base)) { +template +bool PairNodeSwapActiveOperator::MakeNeighbor() { + const int64_t base = this->BaseNode(0); + if (this->IsPathEnd(base)) { return false; } const int64_t pair_first = pairs_[inactive_pair_].pickup_alternatives[0]; const int64_t pair_second = pairs_[inactive_pair_].delivery_alternatives[0]; if (swap_first) { - return MakeActive(pair_second, BaseNode(1)) && - MakeActive(pair_first, base) && - MakeChainInactive(pair_first, Next(pair_first)); + return this->MakeActive(pair_second, this->BaseNode(1)) && + this->MakeActive(pair_first, base) && + this->MakeChainInactive(pair_first, this->Next(pair_first)); } else { - return MakeActive(pair_second, BaseNode(1)) && - MakeActive(pair_first, base) && - MakeChainInactive(pair_second, Next(pair_second)); + return this->MakeActive(pair_second, this->BaseNode(1)) && + this->MakeActive(pair_first, base) && + this->MakeChainInactive(pair_second, this->Next(pair_second)); } } +template +LocalSearchOperator* MakePairNodeSwapActive( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + const std::vector& pairs) { + if (secondary_vars.empty()) { + return solver->RevAlloc(new PairNodeSwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); + } + return solver->RevAlloc(new PairNodeSwapActiveOperator( + vars, secondary_vars, std::move(start_empty_path_class), pairs)); +} + /// A utility class to maintain pickup and delivery information of nodes. class PickupAndDeliveryData { public: @@ -718,7 +833,8 @@ class PickupAndDeliveryData { /// at base_node such that rejected nodes are only deliveries. If the base_node /// is a delivery, it selects the smallest subtrip ending at base_node such that /// rejected nodes are only pickups. -class RelocateSubtrip : public PathOperator { +template +class RelocateSubtrip : public PathOperator { public: RelocateSubtrip( const std::vector& vars, @@ -727,12 +843,6 @@ class RelocateSubtrip : public PathOperator { std::function&(int, int)> get_incoming_neighbors, std::function&(int, int)> get_outgoing_neighbors, absl::Span pairs); - RelocateSubtrip(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - absl::Span pairs) - : RelocateSubtrip(vars, secondary_vars, std::move(start_empty_path_class), - nullptr, nullptr, pairs) {} std::string DebugString() const override { return "RelocateSubtrip"; } bool MakeNeighbor() override; @@ -756,7 +866,22 @@ class RelocateSubtrip : public PathOperator { std::vector subtrip_nodes_; }; -class ExchangeSubtrip : public PathOperator { +LocalSearchOperator* MakeRelocateSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + absl::Span pairs); + +LocalSearchOperator* MakeRelocateSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + absl::Span pairs); + +template +class ExchangeSubtrip : public PathOperator { public: ExchangeSubtrip( const std::vector& vars, @@ -765,12 +890,6 @@ class ExchangeSubtrip : public PathOperator { std::function&(int, int)> get_incoming_neighbors, std::function&(int, int)> get_outgoing_neighbors, absl::Span pairs); - ExchangeSubtrip(const std::vector& vars, - const std::vector& secondary_vars, - std::function start_empty_path_class, - absl::Span pairs) - : ExchangeSubtrip(vars, secondary_vars, std::move(start_empty_path_class), - nullptr, nullptr, pairs) {} std::string DebugString() const override { return "ExchangeSubtrip"; } bool MakeNeighbor() override; @@ -816,6 +935,20 @@ class ExchangeSubtrip : public PathOperator { std::vector path1_; }; +LocalSearchOperator* MakeExchangeSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + std::function&(int, int)> get_incoming_neighbors, + std::function&(int, int)> get_outgoing_neighbors, + absl::Span pairs); + +LocalSearchOperator* MakeExchangeSubtrip( + Solver* solver, const std::vector& vars, + const std::vector& secondary_vars, + std::function start_empty_path_class, + absl::Span pairs); + } // namespace operations_research #endif // OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_NEIGHBORHOODS_H_ diff --git a/ortools/constraint_solver/routing_parameters.cc b/ortools/constraint_solver/routing_parameters.cc index 1f36b19438f..9d1126395a9 100644 --- a/ortools/constraint_solver/routing_parameters.cc +++ b/ortools/constraint_solver/routing_parameters.cc @@ -121,6 +121,8 @@ RoutingSearchParameters CreateDefaultRoutingSearchParameters() { o->set_use_tsp_opt(BOOL_FALSE); o->set_use_make_active(BOOL_TRUE); o->set_use_relocate_and_make_active(BOOL_FALSE); // costly if true by default + o->set_use_exchange_and_make_active(BOOL_FALSE); // very costly + o->set_use_exchange_path_start_ends_and_make_active(BOOL_FALSE); o->set_use_make_inactive(BOOL_TRUE); o->set_use_make_chain_inactive(BOOL_TRUE); o->set_use_swap_active(BOOL_TRUE); @@ -211,6 +213,8 @@ RoutingSearchParameters CreateDefaultSecondaryRoutingSearchParameters() { o->set_use_tsp_opt(BOOL_FALSE); o->set_use_make_active(BOOL_FALSE); o->set_use_relocate_and_make_active(BOOL_FALSE); + o->set_use_exchange_and_make_active(BOOL_FALSE); + o->set_use_exchange_path_start_ends_and_make_active(BOOL_FALSE); o->set_use_make_inactive(BOOL_FALSE); o->set_use_make_chain_inactive(BOOL_FALSE); o->set_use_swap_active(BOOL_FALSE); diff --git a/ortools/constraint_solver/routing_parameters.proto b/ortools/constraint_solver/routing_parameters.proto index d6e8d4c1344..320811e7666 100644 --- a/ortools/constraint_solver/routing_parameters.proto +++ b/ortools/constraint_solver/routing_parameters.proto @@ -150,6 +150,10 @@ message RoutingSearchParameters { SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS = 5; // Select nodes with the smallest distance to the closest vehicle. SORTING_PROPERTY_LOWEST_MIN_ARC_COST_TO_VEHICLE_START_ENDS = 6; + // Selects nodes that have a higher dimension usage on average, where the + // usage is determined as the ratio of node demand over vehicle capacity. + // Currently, this property only supports unary dimensions. + SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE = 7; } // The properties used to sort insertion entries in the local cheapest @@ -175,7 +179,7 @@ message RoutingSearchParameters { int32 first_solution_optimization_period = 59; // Local search neighborhood operators used to build a solutions neighborhood. - // Next ID: 37 + // Next ID: 39 message LocalSearchNeighborhoodOperators { // --- Inter-route operators --- // Operator which moves a single node to another position. @@ -339,6 +343,23 @@ message RoutingSearchParameters { // (where 1,2 and 5,6 are first and last nodes of paths) is: // 1 -> 4 -> 3 -> 5, 2 -> 6. OptionalBoolean use_relocate_and_make_active = 21; + // Operator which exchanges two nodes and inserts an inactive node. + // Possible neighbors for paths 0 -> 2 -> 4, 1 -> 3 -> 6 and 5 inactive are: + // 0 -> 3 -> 4, 1 -> 5 -> 2 -> 6 + // 0 -> 3 -> 5 -> 4, 1 -> 2 -> 6 + // 0 -> 5 -> 3 -> 4, 1 -> 2 -> 6 + // 0 -> 3 -> 4, 1 -> 2 -> 5 -> 6 + OptionalBoolean use_exchange_and_make_active = 37; + // Operator which exchanges the first and last nodes of two paths and makes + // a node active. + // Possible neighbors for paths 0 -> 1 -> 2 -> 7, 6 -> 3 -> 4 -> 8 + // and 5 inactive are: + // 0 -> 5 -> 3 -> 4 -> 7, 6 -> 1 -> 2 -> 8 + // 0 -> 3 -> 4 -> 7, 6 -> 1 -> 5 -> 2 -> 8 + // 0 -> 3 -> 4 -> 7, 6 -> 1 -> 2 -> 5 -> 8 + // 0 -> 3 -> 5 -> 4 -> 7, 6 -> 1 -> 2 -> 8 + // 0 -> 3 -> 4 -> 5 -> 7, 6 -> 1 -> 2 -> 8 + OptionalBoolean use_exchange_path_start_ends_and_make_active = 38; // Operator which makes path nodes inactive. // Possible neighbors for the path 1 -> 2 -> 3 -> 4 (where 1 and 4 are first // and last nodes of the path) are: diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/constraint_solver/routing_search.cc index 6cb5401d101..0b92c576c8b 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/constraint_solver/routing_search.cc @@ -50,6 +50,7 @@ #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" @@ -535,9 +536,11 @@ Assignment* RoutingFilteredHeuristic::BuildSolutionFromRoutes( return nullptr; } -std::optional IntVarFilteredHeuristic::Evaluate(bool commit) { +std::optional IntVarFilteredHeuristic::Evaluate( + bool commit, bool ignore_upper_bound, bool update_upper_bound) { ++number_of_decisions_; - const bool accept = FilterAccept(); + 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 @@ -547,9 +550,13 @@ std::optional IntVarFilteredHeuristic::Evaluate(bool commit) { // is committed, the upper bound is relaxed to make sure further // (cost-degrading) insertions will be accepted // (cf. SynchronizeFilters()). - DCHECK_LE(filter_manager_->GetAcceptedObjectiveValue(), + DCHECK(ignore_upper_bound || + filter_manager_->GetAcceptedObjectiveValue() <= objective_upper_bound_); - objective_upper_bound_ = filter_manager_->GetAcceptedObjectiveValue(); + objective_upper_bound = filter_manager_->GetAcceptedObjectiveValue(); + if (update_upper_bound) { + objective_upper_bound_ = objective_upper_bound; + } } if (commit) { const Assignment::IntContainer& delta_container = @@ -575,7 +582,7 @@ std::optional IntVarFilteredHeuristic::Evaluate(bool commit) { } delta_->Clear(); delta_indices_.clear(); - return accept ? std::optional{objective_upper_bound_} : std::nullopt; + return accept ? std::optional{objective_upper_bound} : std::nullopt; } void IntVarFilteredHeuristic::SynchronizeFilters() { @@ -584,12 +591,13 @@ void IntVarFilteredHeuristic::SynchronizeFilters() { objective_upper_bound_ = std::numeric_limits::max(); } -bool IntVarFilteredHeuristic::FilterAccept() { +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::min(), - objective_upper_bound_); + return filter_manager_->Accept( + monitor, delta_, empty_, std::numeric_limits::min(), + ignore_upper_bound ? std::numeric_limits::max() + : objective_upper_bound_); } // RoutingFilteredHeuristic @@ -876,15 +884,21 @@ void CheapestInsertionFilteredHeuristic::AppendInsertionPositionsAfter( 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); if (evaluator_ == nullptr) { InsertBetween(node_to_insert, insert_after, insert_before, vehicle); - std::optional insertion_cost = Evaluate(/*commit=*/false); + std::optional insertion_cost = + Evaluate(/*commit=*/false, /*ignore_upper_bound=*/hint_weight > 0, + /*update_upper_bound=*/hint_weight >= 0); if (insertion_cost.has_value()) { - node_insertions->push_back({insert_after, vehicle, *insertion_cost}); + node_insertions->push_back( + {insert_after, vehicle, -hint_weight, *insertion_cost}); } } else { node_insertions->push_back( - {insert_after, vehicle, + {insert_after, vehicle, -hint_weight, ignore_cost ? 0 : GetInsertionCostForNodeAtPosition(node_to_insert, insert_after, @@ -2492,7 +2506,8 @@ LocalCheapestInsertionFilteredHeuristic:: RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy, std::vector insertion_sorting_properties, - LocalSearchFilterManager* filter_manager, BinCapacities* bin_capacities, + LocalSearchFilterManager* filter_manager, bool use_first_solution_hint, + BinCapacities* bin_capacities, std::function&, std::vector*)> optimize_on_insertion) @@ -2501,6 +2516,7 @@ LocalCheapestInsertionFilteredHeuristic:: filter_manager), pair_insertion_strategy_(pair_insertion_strategy), insertion_sorting_properties_(std::move(insertion_sorting_properties)), + use_first_solution_hint_(use_first_solution_hint), bin_capacities_(bin_capacities), optimize_on_insertion_(std::move(optimize_on_insertion)) { DCHECK(!insertion_sorting_properties_.empty()); @@ -2510,6 +2526,20 @@ 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(); } @@ -2543,7 +2573,7 @@ bool LocalCheapestInsertionFilteredHeuristic::OptimizeOnInsertion( namespace { // Computes the cost from vehicle starts to pickups. std::vector> ComputeStartToPickupCosts( - const RoutingModel& model, const std::vector& pickups, + const RoutingModel& model, absl::Span pickups, const Bitset64& vehicle_set) { std::vector> pickup_costs(model.Size()); for (int64_t pickup : pickups) { @@ -2560,7 +2590,7 @@ std::vector> ComputeStartToPickupCosts( // Computes the cost from deliveries to vehicle ends. std::vector> ComputeDeliveryToEndCosts( - const RoutingModel& model, const std::vector& deliveries, + const RoutingModel& model, absl::Span deliveries, const Bitset64& vehicle_set) { std::vector> delivery_costs(model.Size()); for (int64_t delivery : deliveries) { @@ -2665,6 +2695,125 @@ std::optional GetAvgPickupDeliveryPairDistanceFromVehicles( : 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& 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& 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(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& 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(max_vehicle_capacity * + dimension_usage_sum); +} + } // namespace void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { @@ -2683,11 +2832,21 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { 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) { + 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 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) { @@ -2714,6 +2873,9 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { properties.push_back(neg_min_distance_to_vehicles); neg_min_distance_to_vehicles_appended = true; break; + case RoutingSearchParameters::SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE: + properties.push_back(CapOpp(avg_dimension_usage)); + break; default: LOG(DFATAL) << "Unknown RoutingSearchParameter::InsertionSortingProperty " @@ -2736,6 +2898,7 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { // 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 RoutingSearchParameters::InsertionSortingProperty property : insertion_sorting_properties_) { if (property == @@ -2745,13 +2908,25 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { RoutingSearchParameters:: SORTING_PROPERTY_LOWEST_AVG_ARC_COST_TO_VEHICLE_START_ENDS) { compute_avg_pickup_delivery_pair_distance_from_vehicles = true; - break; + } + if (property == + RoutingSearchParameters::SORTING_PROPERTY_HIGHEST_DIMENSION_USAGE) { + compute_avg_dimension_usage = true; } } Bitset64 vehicle_set(model.vehicles()); for (int v = 0; v < model.vehicles(); ++v) vehicle_set.Set(v); + const std::vector unary_dimensions = + compute_avg_dimension_usage ? model.GetUnaryDimensions() + : std::vector(); + + const int64_t max_dimension_capacity = + compute_avg_dimension_usage + ? GetMaxFiniteDimensionCapacity(model, unary_dimensions) + : 0; + // Iterating on pickup and delivery pairs. const std::vector& pairs = model.GetPickupAndDeliveryPairs(); @@ -2760,12 +2935,16 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { const auto& [pickups, deliveries] = pairs[pair_index]; int64_t num_allowed_vehicles = std::numeric_limits::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(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) { @@ -2774,6 +2953,8 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { static_cast(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 maybe_avg_pair_to_vehicle_cost = @@ -2788,10 +2969,17 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { continue; } + const int64_t avg_pair_dimension_usage = + compute_avg_dimension_usage + ? GetAvgPickupDeliveryPairUnaryDimensionUsage( + model, unary_dimensions, max_dimension_capacity, pair_index) + : 0; + absl::InlinedVector properties = get_insertion_properties( CapAdd(pickup_penalty, delivery_penalty), num_allowed_vehicles, maybe_avg_pair_to_vehicle_cost.value(), - GetNegMaxDistanceFromVehicles(model, pair_index)); + GetNegMaxDistanceFromVehicles(model, pair_index), hint_weight, + reversed_hint_weight, avg_pair_dimension_usage); insertion_order_.push_back({.properties = std::move(properties), .vehicle = 0, @@ -2813,9 +3001,16 @@ void LocalCheapestInsertionFilteredHeuristic::ComputeInsertionOrder() { 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 properties = get_insertion_properties( model.UnperformedPenalty(node), model.VehicleVar(node)->Size(), - sum_distance / vehicle_set.size(), CapOpp(min_distance)); + 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, @@ -2839,7 +3034,7 @@ bool LocalCheapestInsertionFilteredHeuristic::InsertPair( vehicle); // Capturing the state of the delta before it gets wiped by Evaluate. std::vector indices = delta_indices(); - if (Evaluate(/*commit=*/true).has_value()) { + if (Evaluate(/*commit=*/true, /*ignore_upper_bound=*/true).has_value()) { OptimizeOnInsertion(std::move(indices)); return true; } @@ -2888,8 +3083,9 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPair( std::vector sorted_pair_positions = ComputeEvaluatorSortedPairPositions(pickup, delivery); if (sorted_pair_positions.empty()) continue; - for (const auto [insert_pickup_after, insert_delivery_after, unused_value, - vehicle] : sorted_pair_positions) { + 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()) { @@ -2935,10 +3131,13 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour( 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); const int64_t cost = GetInsertionCostForNodeAtPosition( insertion.node, insertion.pred, succ, sequence.Vehicle()); CapAddTo(cost, &sequence_cost); @@ -2946,6 +3145,7 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour( previous_succ = succ; } sequence.Cost() = sequence_cost; + sequence.SetHintWeight(hint_weight); } if (bin_capacities == nullptr) return; for (InsertionSequence sequence : insertion_container_) { @@ -2967,15 +3167,21 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour( for (InsertionSequence sequence : insertion_container_) { 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); InsertBetween(insertion.node, insertion.pred, succ, sequence.Vehicle()); previous_node = insertion.node; previous_succ = succ; } - sequence.Cost() = Evaluate(/*commit=*/false).value_or(kint64max); + sequence.Cost() = + Evaluate(/*commit=*/false, /*ignore_upper_bound=*/hint_weight > 0) + .value_or(kint64max); + sequence.SetHintWeight(hint_weight); } }; @@ -3025,7 +3231,8 @@ void LocalCheapestInsertionFilteredHeuristic::InsertBestPairMultitour( previous_node = insertion.node; previous_succ = succ; } - if (Evaluate(/*commit=*/true).has_value()) { + if (Evaluate(/*commit=*/true, /*ignore_upper_bound=*/true) + .has_value()) { // Insertion succeeded. if (MustUpdateBinCapacities()) { bin_capacities_->AddItemToBin(pickup, vehicle); @@ -3131,7 +3338,8 @@ bool LocalCheapestInsertionFilteredHeuristic::BuildSolutionInternal() { Value(insertion.insert_after), insertion.vehicle); // Capturing the state of the delta before it gets wiped by Evaluate. std::vector indices = delta_indices(); - if (Evaluate(/*commit=*/true).has_value()) { + if (Evaluate(/*commit=*/true, /*ignore_upper_bound=*/true) + .has_value()) { if (MustUpdateBinCapacities()) { bin_capacities_->AddItemToBin(index, insertion.vehicle); } @@ -3224,16 +3432,21 @@ LocalCheapestInsertionFilteredHeuristic::ComputeEvaluatorSortedPairPositions( 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); if (evaluator_ == nullptr) { InsertBetween(pickup, insert_pickup_after, insert_pickup_before, vehicle); InsertBetween(delivery, insert_delivery_after, insert_delivery_before, vehicle); - std::optional insertion_cost = Evaluate(/*commit=*/false); + std::optional insertion_cost = Evaluate( + /*commit=*/false, /*ignore_upper_bound=*/hint_weight > 0); if (insertion_cost.has_value()) { sorted_pickup_delivery_insertions.push_back( - {insert_pickup_after, insert_delivery_after, *insertion_cost, - vehicle}); + {insert_pickup_after, insert_delivery_after, -hint_weight, + *insertion_cost, vehicle}); } } else { const int64_t pickup_cost = GetInsertionCostForNodeAtPosition( @@ -3250,8 +3463,8 @@ LocalCheapestInsertionFilteredHeuristic::ComputeEvaluatorSortedPairPositions( bin_capacities_->RemoveItemFromBin(pickup, vehicle); bin_capacities_->RemoveItemFromBin(delivery, vehicle); } - sorted_pickup_delivery_insertions.push_back({insert_pickup_after, - insert_delivery_after, + sorted_pickup_delivery_insertions.push_back( + {insert_pickup_after, insert_delivery_after, -hint_weight, total_cost, vehicle}); } insert_delivery_after = insert_delivery_before; @@ -4822,8 +5035,8 @@ class RouteConstructor { enum MergeStatus { FIRST_SECOND, SECOND_FIRST, NO_MERGE }; struct RouteSort { - bool operator()(const std::vector& route1, - const std::vector& route2) const { + bool operator()(absl::Span route1, + absl::Span route2) const { return (route1.size() < route2.size()); } } RouteComparator; diff --git a/ortools/constraint_solver/routing_search.h b/ortools/constraint_solver/routing_search.h index beec623ee8b..295bf7eca8f 100644 --- a/ortools/constraint_solver/routing_search.h +++ b/ortools/constraint_solver/routing_search.h @@ -250,7 +250,8 @@ class IntVarFilteredHeuristic { /// solution. /// In any case all modifications to the internal delta are cleared before /// returning. - std::optional Evaluate(bool commit); + std::optional Evaluate(bool commit, bool ignore_upper_bound = false, + bool update_upper_bound = true); /// Returns true if the search must be stopped. virtual bool StopSearch() { return false; } /// Modifies the current solution by setting the variable of index 'index' to @@ -295,7 +296,7 @@ class IntVarFilteredHeuristic { private: /// Checks if filters accept a given modification to the current solution /// (represented by delta). - bool FilterAccept(); + bool FilterAccept(bool ignore_upper_bound); Solver* solver_; std::vector vars_; @@ -375,11 +376,13 @@ class CheapestInsertionFilteredHeuristic : public RoutingFilteredHeuristic { struct NodeInsertion { int64_t insert_after; int vehicle; + int neg_hint_weight; int64_t value; bool operator<(const NodeInsertion& other) const { - return std::tie(value, insert_after, vehicle) < - std::tie(other.value, other.insert_after, other.vehicle); + return std::tie(neg_hint_weight, value, insert_after, vehicle) < + std::tie(other.neg_hint_weight, other.value, other.insert_after, + other.vehicle); } }; struct StartEndValue { @@ -473,8 +476,22 @@ class CheapestInsertionFilteredHeuristic : public RoutingFilteredHeuristic { /// if penalty callback is null or if the node cannot be unperformed. int64_t GetUnperformedValue(int64_t node_to_insert) const; + bool HasHintedNext(int node) const { + CHECK_LT(node, hint_next_values_.size()); + return hint_next_values_[node] != -1; + } + bool HasHintedPrev(int node) const { + CHECK_LT(node, hint_prev_values_.size()); + return hint_prev_values_[node] != -1; + } + bool IsHint(int node, int64_t next) const { + return node < hint_next_values_.size() && hint_next_values_[node] == next; + } + std::function evaluator_; std::function penalty_evaluator_; + std::vector hint_next_values_; + std::vector hint_prev_values_; }; /// Filter-based decision builder which builds a solution by inserting @@ -928,10 +945,12 @@ class InsertionSequenceContainer { size_t begin; size_t end; int vehicle; + int neg_hint_weight; int64_t cost; bool operator<(const InsertionBounds& other) const { - return std::tie(cost, vehicle, begin) < - std::tie(other.cost, other.vehicle, other.begin); + return std::tie(neg_hint_weight, cost, vehicle, begin) < + std::tie(other.neg_hint_weight, other.cost, other.vehicle, + other.begin); } size_t Size() const { return end - begin; } }; @@ -963,6 +982,10 @@ class InsertionSequenceContainer { int Vehicle() const { return bounds_->vehicle; } int64_t Cost() const { return bounds_->cost; } int64_t& Cost() { return bounds_->cost; } + void SetHintWeight(int hint_weight) { + bounds_->neg_hint_weight = -hint_weight; + } + int NegHintWeight() const { return bounds_->neg_hint_weight; } private: const Insertion* const data_; @@ -1093,13 +1116,15 @@ class InsertionSequenceGenerator { struct PickupDeliveryInsertion { int64_t insert_pickup_after; int64_t insert_delivery_after; + int neg_hint_weight; int64_t value; int vehicle; bool operator<(const PickupDeliveryInsertion& other) const { - return std::tie(value, insert_pickup_after, insert_delivery_after, - vehicle) < std::tie(other.value, other.insert_pickup_after, - other.insert_delivery_after, + return std::tie(neg_hint_weight, value, insert_pickup_after, + insert_delivery_after, vehicle) < + std::tie(other.neg_hint_weight, other.value, + other.insert_pickup_after, other.insert_delivery_after, other.vehicle); } }; @@ -1119,7 +1144,7 @@ class LocalCheapestInsertionFilteredHeuristic RoutingSearchParameters::PairInsertionStrategy pair_insertion_strategy, std::vector insertion_sorting_properties, - LocalSearchFilterManager* filter_manager, + LocalSearchFilterManager* filter_manager, bool use_first_solution_hint, BinCapacities* bin_capacities = nullptr, std::function&, std::vector*)> @@ -1186,6 +1211,8 @@ class LocalCheapestInsertionFilteredHeuristic InsertionSequenceContainer insertion_container_; InsertionSequenceGenerator insertion_generator_; + const bool use_first_solution_hint_; + BinCapacities* const bin_capacities_; std::function&,