From 906804220f67f133101ebb769ddde686a2223eff Mon Sep 17 00:00:00 2001 From: Corentin Le Molgat Date: Mon, 3 Jun 2024 17:13:58 +0200 Subject: [PATCH] constraint_solver: Export from google3 --- .../constraint_solver/constraint_solveri.h | 503 +------- ortools/constraint_solver/element.cc | 24 +- .../constraint_solver/graph_constraints.cc | 3 +- ortools/constraint_solver/local_search.cc | 803 +----------- ortools/constraint_solver/resource.cc | 2 +- ortools/constraint_solver/routing.cc | 296 +++-- ortools/constraint_solver/routing.h | 101 +- ortools/constraint_solver/routing_breaks.cc | 2 +- .../constraint_solver/routing_constraints.cc | 8 +- .../routing_decision_builders.cc | 7 +- ortools/constraint_solver/routing_filters.cc | 1142 ++++++++++++++++- ortools/constraint_solver/routing_filters.h | 529 ++++++++ ortools/constraint_solver/routing_ils.cc | 259 +++- ortools/constraint_solver/routing_ils.h | 84 +- .../routing_insertion_lns.cc | 41 +- .../routing_lp_scheduling.cc | 35 +- .../constraint_solver/routing_lp_scheduling.h | 9 +- .../routing_neighborhoods.cc | 5 +- ortools/constraint_solver/routing_search.cc | 17 +- ortools/constraint_solver/routing_search.h | 4 +- ortools/constraint_solver/routing_types.h | 1 - ortools/constraint_solver/sched_search.cc | 3 +- ortools/constraint_solver/search.cc | 159 ++- ortools/constraint_solver/visitor.cc | 4 +- 24 files changed, 2490 insertions(+), 1551 deletions(-) diff --git a/ortools/constraint_solver/constraint_solveri.h b/ortools/constraint_solver/constraint_solveri.h index 323595486fa..04adc7c9a44 100644 --- a/ortools/constraint_solver/constraint_solveri.h +++ b/ortools/constraint_solver/constraint_solveri.h @@ -54,7 +54,6 @@ #include #include -#include #include #include #include @@ -63,7 +62,10 @@ #include "absl/algorithm/container.h" #include "absl/container/flat_hash_map.h" +#include "absl/log/check.h" #include "absl/strings/str_cat.h" +#include "absl/strings/str_format.h" +#include "absl/time/time.h" #include "ortools/base/logging.h" #include "ortools/base/strong_int.h" #include "ortools/base/strong_vector.h" @@ -1813,6 +1815,11 @@ class LocalSearchState { // domain identified by domain_id: only Relax, Tighten and Min/Max read // operations are available. Variable MakeVariable(VariableDomainId domain_id); + // Makes a variable from an interval without going through a domain_id. + // Can be used when no direct manipulation of the domain is needed. + Variable MakeVariableWithRelaxedDomain(int64_t min, int64_t max); + // Makes a variable with no state, this is meant as a placeholder. + static Variable DummyVariable(); void Commit(); void Revert(); bool StateIsFeasible() const { @@ -2001,7 +2008,8 @@ class LocalSearchState { bool constraint_is_trailed_ = false; }; // Used to identify constraints and hold ownership. - util_intops::StrongVector> constraints_; + util_intops::StrongVector> + constraints_; }; // A LocalSearchState Variable can only be created by a LocalSearchState, @@ -2011,20 +2019,34 @@ class LocalSearchState { // to ensure that variable users will not misuse the state. class LocalSearchState::Variable { public: - int64_t Min() const { return state_->VariableDomainMin(domain_id_); } - int64_t Max() const { return state_->VariableDomainMax(domain_id_); } - bool SetMin(int64_t new_min) { + int64_t Min() const { + DCHECK(Exists()); + return state_->VariableDomainMin(domain_id_); + } + int64_t Max() const { + DCHECK(Exists()); + return state_->VariableDomainMax(domain_id_); + } + // Sets variable's minimum to max(Min(), new_min) and propagates the change. + // Returns true iff the variable domain is nonempty and propagation succeeded. + bool SetMin(int64_t new_min) const { + if (!Exists()) return true; return state_->TightenVariableDomainMin(domain_id_, new_min) && state_->PropagateTighten(domain_id_); } - bool SetMax(int64_t new_max) { + // Sets variable's maximum to min(Max(), new_max) and propagates the change. + // Returns true iff the variable domain is nonempty and propagation succeeded. + bool SetMax(int64_t new_max) const { + if (!Exists()) return true; return state_->TightenVariableDomainMax(domain_id_, new_max) && state_->PropagateTighten(domain_id_); } - void Relax() { + void Relax() const { + if (state_ == nullptr) return; state_->RelaxVariableDomain(domain_id_); state_->PropagateRelax(domain_id_); } + bool Exists() const { return state_ != nullptr; } private: // Only LocalSearchState can construct LocalSearchVariables. @@ -2033,8 +2055,8 @@ class LocalSearchState::Variable { Variable(LocalSearchState* state, VariableDomainId domain_id) : state_(state), domain_id_(domain_id) {} - LocalSearchState* const state_; - const VariableDomainId domain_id_; + LocalSearchState* state_; + VariableDomainId domain_id_; }; #endif // !defined(SWIG) @@ -2396,9 +2418,11 @@ class SearchLog : public SearchMonitor { std::function display_callback_; const bool display_on_new_solutions_only_; int nsol_; - int64_t tick_; + absl::Duration tick_; std::vector objective_min_; std::vector objective_max_; + std::vector last_objective_value_; + absl::Duration last_objective_timestamp_; int min_right_depth_; int max_depth_; int sliding_min_depth_; @@ -3335,465 +3359,6 @@ inline int64_t PosIntDivDown(int64_t e, int64_t v) { std::vector ToInt64Vector(const std::vector& input); -#if !defined(SWIG) -// A PathState represents a set of paths and changes made on it. -// -// More accurately, let us define P_{num_nodes, starts, ends}-graphs the set of -// directed graphs with nodes [0, num_nodes) whose connected components are -// paths from starts[i] to ends[i] (for the same i) and loops. -// Let us fix num_nodes, starts and ends, so we call these P-graphs. -// -// A P-graph can be described by the sequence of nodes of each of its paths, -// and its set of loops. To describe a change made on a given P-graph G0 that -// yields another P-graph G1, we choose to describe G1 in terms of G0. When -// the difference between G0 and G1 is small, as is almost always the case in a -// local search setting, the description is compact, allowing for incremental -// filters to be efficient. -// -// In order to describe G1 in terms of G0 succintly, we describe each path of -// G1 as a sequence of chains of G0. A chain of G0 is either a nonempty sequence -// of consecutive nodes of a path of G0, or a node that was a loop in G0. -// For instance, a path that was not modified from G0 to G1 has one chain, -// the sequence of all nodes in the path. Typically, local search operators -// modify one or two paths, and the resulting paths can described as sequences -// of two to four chains of G0. Paths that were modified are listed explicitly, -// allowing to iterate only on changed paths. -// The loops of G1 are described more implicitly: the loops of G1 not in G0 -// are listed explicitly, but those in both G1 and G0 are not listed. -// -// A PathState object can be in two states: committed or changed. -// At construction, the object is committed, G0. -// To enter a changed state G1, one can pass modifications with ChangePath() and -// ChangeLoops(). For reasons of efficiency, a chain is described as a range of -// node indices in the representation of the committed graph G0. To that effect, -// the nodes of a path of G0 are guaranteed to have consecutive indices. -// -// Filters can then browse the change efficiently using ChangedPaths(), -// Chains(), Nodes() and ChangedLoops(). -// -// Then Commit() or Revert() can be called: Commit() sets the changed state G1 -// as the new committed state, Revert() erases all changes. -class PathState { - public: - // A Chain allows to iterate on all nodes of a chain, and access some data: - // first node, last node, number of nodes in the chain. - // Chain is a range, its iterator ChainNodeIterator, its value type int. - // Chains are returned by PathChainIterator's operator*(). - class Chain; - // A ChainRange allows to iterate on all chains of a path. - // ChainRange is a range, its iterator Chain*, its value type Chain. - class ChainRange; - // A NodeRange allows to iterate on all nodes of a path. - // NodeRange is a range, its iterator PathNodeIterator, its value type int. - class NodeRange; - - struct ChainBounds { - ChainBounds() {} - ChainBounds(int begin_index, int end_index) - : begin_index(begin_index), end_index(end_index) {} - int begin_index; - int end_index; - }; - int CommittedIndex(int node) const { return committed_index_[node]; } - ChainBounds CommittedPathRange(int path) const { return chains_[path]; } - - // Path constructor: path_start and path_end must be disjoint, - // their values in [0, num_nodes). - PathState(int num_nodes, std::vector path_start, - std::vector path_end); - - // Instance-constant accessors. - - // Returns the number of nodes in the underlying graph. - int NumNodes() const { return num_nodes_; } - // Returns the number of paths (empty paths included). - int NumPaths() const { return num_paths_; } - // Returns the start of a path. - int Start(int path) const { return path_start_end_[path].start; } - // Returns the end of a path. - int End(int path) const { return path_start_end_[path].end; } - - // State-dependent accessors. - - // Returns the committed path of a given node, -1 if it is a loop. - 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. - const std::vector& ChangedPaths() const { return changed_paths_; } - // Returns the set of loops that were added by the change. - const std::vector& ChangedLoops() const { return changed_loops_; } - // Returns the current range of chains of path. - ChainRange Chains(int path) const; - // Returns the current range of nodes of path. - NodeRange Nodes(int path) const; - - // State modifiers. - - // Changes the path to the given sequence of chains of the committed state. - // Chains are described by semi-open intervals. No optimization is made in - // case two consecutive chains are actually already consecutive in the - // committed state: they are not merged into one chain, and Chains(path) will - // report the two chains. - void ChangePath(int path, const std::vector& chains); - // Same as above, but the initializer_list interface avoids the need to pass - // a vector. - void ChangePath(int path, const std::initializer_list& chains) { - changed_paths_.push_back(path); - const int path_begin_index = chains_.size(); - chains_.insert(chains_.end(), chains.begin(), chains.end()); - const int path_end_index = chains_.size(); - paths_[path] = {path_begin_index, path_end_index}; - // Always add sentinel, in case this is the last path change. - chains_.emplace_back(0, 0); - } - - // Describes the nodes that are newly loops in this change. - void ChangeLoops(const std::vector& new_loops); - - // Set the current state G1 as committed. See class comment for details. - void Commit(); - // Erase incremental changes. See class comment for details. - void Revert(); - - // LNS Operators may not fix variables, - // in which case we mark the candidate invalid. - void SetInvalid() { is_invalid_ = true; } - bool IsInvalid() const { return is_invalid_; } - - private: - // Most structs below are named pairs of ints, for typing purposes. - - // Start and end are stored together to optimize (likely) simultaneous access. - struct PathStartEnd { - PathStartEnd(int start, int end) : start(start), end(end) {} - int start; - int end; - }; - // Paths are ranges of chains, which are ranges of committed nodes, see below. - struct PathBounds { - int begin_index; - int end_index; - }; - - // Copies nodes in chains of path at the end of nodes, - // and sets those nodes' path member to value path. - void CopyNewPathAtEndOfNodes(int path); - // Commits paths in O(#{changed paths' nodes}) time, - // increasing this object's space usage by O(|changed path nodes|). - void IncrementalCommit(); - // Commits paths in O(num_nodes + num_paths) time, - // reducing this object's space usage to O(num_nodes + num_paths). - void FullCommit(); - - // Instance-constant data. - const int num_nodes_; - const int num_paths_; - std::vector path_start_end_; - - // Representation of the committed and changed paths. - // A path is a range of chains, which is a range of nodes. - // Ranges are represented internally by indices in vectors: - // ChainBounds are indices in committed_nodes_. PathBounds are indices in - // chains_. When committed (after construction, Revert() or Commit()): - // - path ranges are [path, path+1): they have one chain. - // - chain ranges don't overlap, chains_ has an empty sentinel at the end. - // The sentinel allows the Nodes() iterator to maintain its current pointer - // to committed nodes on NodeRange::operator++(). - // - committed_nodes_ contains all nodes, both paths and loops. - // Actually, old duplicates will likely appear, - // the current version of a node is at the index given by - // committed_index_[node]. A Commit() can add nodes at the end of - // committed_nodes_ in a space/time tradeoff, but if committed_nodes_' size - // is above num_nodes_threshold_, Commit() must reclaim useless duplicates' - // space by rewriting the path/chain/nodes structure. - // When changed (after ChangePaths() and ChangeLoops()), - // the structure is updated accordingly: - // - path ranges that were changed have nonoverlapping values [begin, end) - // where begin is >= num_paths_ + 1, i.e. new chains are stored after - // the committed state. - // - additional chain ranges are stored after the committed chains and its - // sentinel to represent the new chains resulting from the changes. - // Those chains do not overlap with one another or with committed chains. - // - committed_nodes_ are not modified, and still represent the committed - // paths. committed_index_ is not modified either. - std::vector committed_nodes_; - // Maps nodes to their path in the latest committed state. - std::vector committed_paths_; - // Maps nodes to their index in the latest committed state. - std::vector committed_index_; - const int num_nodes_threshold_; - std::vector chains_; - std::vector paths_; - - // Incremental information. - std::vector changed_paths_; - std::vector changed_loops_; - - // See IsInvalid() and SetInvalid(). - bool is_invalid_ = false; -}; - -// A Chain is a range of committed nodes. -class PathState::Chain { - public: - class Iterator { - public: - Iterator& operator++() { - ++current_node_; - return *this; - } - int operator*() const { return *current_node_; } - bool operator!=(Iterator other) const { - return current_node_ != other.current_node_; - } - - private: - // Only a Chain can construct its iterator. - friend class PathState::Chain; - explicit Iterator(const int* node) : current_node_(node) {} - const int* current_node_; - }; - - // Chains hold CommittedNode* values, a Chain may be invalidated - // if the underlying vector is modified. - Chain(const int* begin_node, const int* end_node) - : begin_(begin_node), end_(end_node) {} - - int NumNodes() const { return end_ - begin_; } - int First() const { return *begin_; } - int Last() const { return *(end_ - 1); } - Iterator begin() const { return Iterator(begin_); } - Iterator end() const { return Iterator(end_); } - - Chain WithoutFirstNode() const { return Chain(begin_ + 1, end_); } - - private: - const int* const begin_; - const int* const end_; -}; - -// A ChainRange is a range of Chains, committed or not. -class PathState::ChainRange { - public: - class Iterator { - public: - Iterator& operator++() { - ++current_chain_; - return *this; - } - Chain operator*() const { - return {first_node_ + current_chain_->begin_index, - first_node_ + current_chain_->end_index}; - } - bool operator!=(Iterator other) const { - return current_chain_ != other.current_chain_; - } - - private: - // Only a ChainRange can construct its Iterator. - friend class ChainRange; - Iterator(const ChainBounds* chain, const int* const first_node) - : current_chain_(chain), first_node_(first_node) {} - const ChainBounds* current_chain_; - const int* const first_node_; - }; - - // ChainRanges hold ChainBounds* and CommittedNode*, - // a ChainRange may be invalidated if on of the underlying vector is modified. - ChainRange(const ChainBounds* const begin_chain, - const ChainBounds* const end_chain, const int* const first_node) - : begin_(begin_chain), end_(end_chain), first_node_(first_node) {} - - Iterator begin() const { return {begin_, first_node_}; } - Iterator end() const { return {end_, first_node_}; } - - private: - const ChainBounds* const begin_; - const ChainBounds* const end_; - const int* const first_node_; -}; - -// A NodeRange allows to iterate on all nodes of a path, -// by a two-level iteration on ChainBounds* and CommittedNode* of a PathState. -class PathState::NodeRange { - public: - class Iterator { - public: - Iterator& operator++() { - ++current_node_; - if (current_node_ == end_node_) { - ++current_chain_; - // Note: dereferencing bounds is valid because there is a sentinel - // value at the end of PathState::chains_ to that intent. - const ChainBounds bounds = *current_chain_; - current_node_ = first_node_ + bounds.begin_index; - end_node_ = first_node_ + bounds.end_index; - } - return *this; - } - int operator*() const { return *current_node_; } - bool operator!=(Iterator other) const { - return current_chain_ != other.current_chain_; - } - - private: - // Only a NodeRange can construct its Iterator. - friend class NodeRange; - Iterator(const ChainBounds* current_chain, const int* const first_node) - : current_node_(first_node + current_chain->begin_index), - end_node_(first_node + current_chain->end_index), - current_chain_(current_chain), - first_node_(first_node) {} - const int* current_node_; - const int* end_node_; - const ChainBounds* current_chain_; - const int* const first_node_; - }; - - // NodeRanges hold ChainBounds* and int* (first committed node), - // a NodeRange may be invalidated if on of the underlying vector is modified. - NodeRange(const ChainBounds* begin_chain, const ChainBounds* end_chain, - const int* first_node) - : begin_chain_(begin_chain), - end_chain_(end_chain), - first_node_(first_node) {} - Iterator begin() const { return {begin_chain_, first_node_}; } - // Note: there is a sentinel value at the end of PathState::chains_, - // so dereferencing chain_range_.end()->begin_ is always valid. - Iterator end() const { return {end_chain_, first_node_}; } - - private: - const ChainBounds* begin_chain_; - const ChainBounds* end_chain_; - const int* const first_node_; -}; - -// This checker enforces dimension requirements. -// A dimension requires that there is some valuation of -// cumul and demand such that for all paths: -// - cumul[A] is in interval node_capacity[A] -// - if arc A -> B is on a path of path_class p, -// then cumul[A] + demand[p](A, B) = cumul[B]. -// - if A is on a path of class p, then -// cumul[A] must be inside interval path_capacity[path]. -class DimensionChecker { - public: - struct Interval { - int64_t min; - int64_t max; - }; - - struct ExtendedInterval { - int64_t min; - int64_t max; - int64_t num_negative_infinity; - int64_t num_positive_infinity; - }; - - // TODO(user): the addition of kMinRangeSizeForRIQ slowed down Check(). - // See if using a template parameter makes it faster. - DimensionChecker(const PathState* path_state, - std::vector path_capacity, - std::vector path_class, - std::vector> - demand_per_path_class, - std::vector node_capacity, - int min_range_size_for_riq = kOptimalMinRangeSizeForRIQ); - - // Given the change made in PathState, checks that the dimension - // constraint is still feasible. - bool Check() const; - - // Commits to the changes made in PathState, - // must be called before PathState::Commit(). - void Commit(); - - static constexpr int kOptimalMinRangeSizeForRIQ = 4; - - private: - inline void UpdateCumulUsingChainRIQ(int first_index, int last_index, - const ExtendedInterval& path_capacity, - ExtendedInterval& cumul) const; - - // Commits to the current solution and rebuilds structures from scratch. - void FullCommit(); - // Commits to the current solution and only build structures for paths that - // changed, using additional space to do so in a time-memory tradeoff. - void IncrementalCommit(); - // Adds sums of given path to the bottom layer of the Range Intersection Query - // structure, updates index_ and previous_nontrivial_index_. - void AppendPathDemandsToSums(int path); - // Updates the Range Intersection Query structure from its bottom layer, - // with [begin_index, end_index) the range of the change, - // which must be at the end of the bottom layer. - // Supposes that requests overlapping the range will be inside the range, - // to avoid updating all layers. - void UpdateRIQStructure(int begin_index, int end_index); - - const PathState* const path_state_; - const std::vector path_capacity_; - const std::vector path_class_; - const std::vector> - demand_per_path_class_; - std::vector cached_demand_; - const std::vector node_capacity_; - - // Precomputed data. - // Maps nodes to their pre-computed data, except for isolated nodes, - // which do not have precomputed data. - // Only valid for nodes that are in some path in the committed state. - std::vector index_; - // Range intersection query in , with n = #nodes. - // Let node be in a path, i = index_[node], start the start of node's path. - // Let l such that index_[start] <= i - 2**l. - // - riq_[l][i].tsum_at_lst contains the sum of demands from start to node. - // - riq_[l][i].tsum_at_fst contains the sum of demands from start to the - // node at i - 2**l. - // - riq_[l][i].tightest_tsum contains the intersection of - // riq_[0][j].tsum_at_lst for all j in (i - 2**l, i]. - // - riq_[0][i].cumuls_to_lst and riq_[0][i].cumuls_to_fst contain - // the node's capacity. - // - riq_[l][i].cumuls_to_lst is the intersection, for j in (i - 2**l, i], of - // riq_[0][j].cumuls_to_lst + sum_{k in [j, i)} demand(k, k+1) - // - riq_[l][i].cumuls_to_fst is the intersection, for j in (i - 2**l, i], of - // riq_[0][j].cumuls_to_fst - sum_{k in (i-2**l, j)} demand(k, k+1) - struct RIQNode { - ExtendedInterval cumuls_to_fst; - ExtendedInterval tightest_tsum; - ExtendedInterval cumuls_to_lst; - ExtendedInterval tsum_at_fst; - ExtendedInterval tsum_at_lst; - }; - std::vector> riq_; - // The incremental branch of Commit() may waste space in the layers of the - // RIQ structure. This is the upper limit of a layer's size. - const int maximum_riq_layer_size_; - // Range queries are used on a chain only if the range is larger than this. - const int min_range_size_for_riq_; -}; - -// Make a filter that takes ownership of a PathState and synchronizes it with -// solver events. The solver represents a graph with array of variables 'nexts'. -// Solver events are embodied by Assignment* deltas, that are translated to node -// changes during Relax(), committed during Synchronize(), and reverted on -// Revert(). -LocalSearchFilter* MakePathStateFilter(Solver* solver, - std::unique_ptr path_state, - const std::vector& nexts); - -// Make a filter that translates solver events to the input checker's interface. -// Since DimensionChecker has a PathState, the filter returned by this -// must be synchronized to the corresponding PathStateFilter: -// - Relax() must be called after the PathStateFilter's. -// - Accept() must be called after. -// - Synchronize() must be called before. -// - Revert() must be called before. -LocalSearchFilter* MakeDimensionFilter( - Solver* solver, std::unique_ptr checker, - const std::string& dimension_name); - -#endif // !defined(SWIG) - } // namespace operations_research #endif // OR_TOOLS_CONSTRAINT_SOLVER_CONSTRAINT_SOLVERI_H_ diff --git a/ortools/constraint_solver/element.cc b/ortools/constraint_solver/element.cc index b77a0dd1354..5d3fbea76f9 100644 --- a/ortools/constraint_solver/element.cc +++ b/ortools/constraint_solver/element.cc @@ -407,18 +407,18 @@ RangeMinimumQueryExprElement::RangeMinimumQueryExprElement( } int64_t RangeMinimumQueryExprElement::Min() const { - return min_rmq_.GetMinimumFromRange(IndexMin(), IndexMax() + 1); + return min_rmq_.RangeMinimum(IndexMin(), IndexMax() + 1); } int64_t RangeMinimumQueryExprElement::Max() const { - return max_rmq_.GetMinimumFromRange(IndexMin(), IndexMax() + 1); + return max_rmq_.RangeMinimum(IndexMin(), IndexMax() + 1); } void RangeMinimumQueryExprElement::Range(int64_t* mi, int64_t* ma) { const int64_t range_min = IndexMin(); const int64_t range_max = IndexMax() + 1; - *mi = min_rmq_.GetMinimumFromRange(range_min, range_max); - *ma = max_rmq_.GetMinimumFromRange(range_min, range_max); + *mi = min_rmq_.RangeMinimum(range_min, range_max); + *ma = max_rmq_.RangeMinimum(range_min, range_max); } #define UPDATE_RMQ_BASE_ELEMENT_INDEX_BOUNDS(test) \ @@ -870,16 +870,14 @@ IntExpr* Solver::MakeMonotonicElement(Solver::IndexEvaluator1 values, bool increasing, IntVar* const index) { CHECK_EQ(this, index->solver()); if (increasing) { - return RegisterIntExpr( - RevAlloc(new IncreasingIntExprFunctionElement(this, values, index))); + return RegisterIntExpr(RevAlloc( + new IncreasingIntExprFunctionElement(this, std::move(values), index))); } else { - // You need to pass by copy such that opposite_value does not include a - // dandling reference when leaving this scope. - Solver::IndexEvaluator1 opposite_values = [values](int64_t i) { - return -values(i); - }; - return RegisterIntExpr(MakeOpposite(RevAlloc( - new IncreasingIntExprFunctionElement(this, opposite_values, index)))); + return RegisterIntExpr( + MakeOpposite(RevAlloc(new IncreasingIntExprFunctionElement( + this, + [values = std::move(values)](int64_t i) { return -values(i); }, + index)))); } } diff --git a/ortools/constraint_solver/graph_constraints.cc b/ortools/constraint_solver/graph_constraints.cc index d5ab3d72dcd..349f2550ff4 100644 --- a/ortools/constraint_solver/graph_constraints.cc +++ b/ortools/constraint_solver/graph_constraints.cc @@ -627,7 +627,8 @@ Constraint* Solver::MakeNoCycle(const std::vector& nexts, const int64_t size = nexts.size(); sink_handler = [size](int64_t index) { return index >= size; }; } - return RevAlloc(new NoCycle(this, nexts, active, sink_handler, assume_paths)); + return RevAlloc( + new NoCycle(this, nexts, active, std::move(sink_handler), assume_paths)); } Constraint* Solver::MakeNoCycle(const std::vector& nexts, diff --git a/ortools/constraint_solver/local_search.cc b/ortools/constraint_solver/local_search.cc index d783bd875d8..2d11813cdd2 100644 --- a/ortools/constraint_solver/local_search.cc +++ b/ortools/constraint_solver/local_search.cc @@ -36,13 +36,13 @@ #include "absl/strings/str_format.h" #include "absl/strings/string_view.h" #include "absl/time/time.h" -#include "absl/types/span.h" #include "ortools/base/iterator_adaptors.h" #include "ortools/base/logging.h" #include "ortools/base/map_util.h" #include "ortools/base/strong_int.h" #include "ortools/base/strong_vector.h" #include "ortools/base/timer.h" +#include "ortools/base/types.h" #include "ortools/constraint_solver/constraint_solver.h" #include "ortools/constraint_solver/constraint_solveri.h" #include "ortools/graph/hamiltonian_path.h" @@ -2116,8 +2116,8 @@ void CompoundOperator::Start(const Assignment* assignment) { start_assignment_ = assignment; started_.ClearAll(); if (!operators_.empty()) { - OperatorComparator comparator(evaluator_, operator_indices_[index_]); - std::sort(operator_indices_.begin(), operator_indices_.end(), comparator); + std::sort(operator_indices_.begin(), operator_indices_.end(), + OperatorComparator(evaluator_, operator_indices_[index_])); index_ = 0; } } @@ -2154,8 +2154,6 @@ int64_t CompoundOperatorNoRestart(int size, int active_index, return (operator_index < active_index) ? size + operator_index - active_index : operator_index - active_index; } - -int64_t CompoundOperatorRestart(int, int) { return 0; } } // namespace LocalSearchOperator* Solver::ConcatenateOperators( @@ -2166,8 +2164,8 @@ LocalSearchOperator* Solver::ConcatenateOperators( LocalSearchOperator* Solver::ConcatenateOperators( const std::vector& ops, bool restart) { if (restart) { - std::function eval = CompoundOperatorRestart; - return ConcatenateOperators(ops, eval); + return ConcatenateOperators( + ops, std::function([](int, int) { return 0; })); } const int size = ops.size(); return ConcatenateOperators(ops, [size](int i, int j) { @@ -2488,7 +2486,8 @@ MAKE_LOCAL_SEARCH_OPERATOR(RelocateAndMakeInactiveOperator) LocalSearchOperator* Solver::MakeOperator( const std::vector& vars, Solver::LocalSearchOperators op, std::function&(int, int)> get_neighbors) { - return MakeOperator(vars, std::vector(), op, get_neighbors); + return MakeOperator(vars, std::vector(), op, + std::move(get_neighbors)); } LocalSearchOperator* Solver::MakeOperator( @@ -2651,760 +2650,6 @@ LocalSearchFilter* Solver::MakeRejectFilter() { return RevAlloc(new RejectFilter()); } -PathState::PathState(int num_nodes, std::vector path_start, - std::vector path_end) - : num_nodes_(num_nodes), - num_paths_(path_start.size()), - num_nodes_threshold_(std::max(16, 4 * num_nodes_)) // Arbitrary value. -{ - DCHECK_EQ(path_start.size(), num_paths_); - DCHECK_EQ(path_end.size(), num_paths_); - for (int p = 0; p < num_paths_; ++p) { - path_start_end_.push_back({path_start[p], path_end[p]}); - } - // 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_nodes_.assign(2 * num_paths_, -1); - chains_.assign(num_paths_ + 1, {-1, -1}); // Reserve 1 more for sentinel. - paths_.assign(num_paths_, {-1, -1}); - for (int path = 0; path < num_paths_; ++path) { - const int index = 2 * path; - const auto& [start, end] = path_start_end_[path]; - committed_index_[start] = index; - committed_index_[end] = index + 1; - - committed_nodes_[index] = start; - committed_nodes_[index + 1] = end; - - committed_paths_[start] = path; - committed_paths_[end] = path; - - chains_[path] = {index, index + 2}; - paths_[path] = {path, path + 1}; - } - chains_[num_paths_] = {0, 0}; // Sentinel. - // Nodes that are not starts or ends are loops. - 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(); - committed_nodes_.push_back(node); - } -} - -PathState::ChainRange PathState::Chains(int path) const { - const PathBounds bounds = paths_[path]; - return PathState::ChainRange(chains_.data() + bounds.begin_index, - chains_.data() + bounds.end_index, - committed_nodes_.data()); -} - -PathState::NodeRange PathState::Nodes(int path) const { - const PathBounds bounds = paths_[path]; - return PathState::NodeRange(chains_.data() + bounds.begin_index, - chains_.data() + bounds.end_index, - committed_nodes_.data()); -} - -void PathState::ChangePath(int path, const std::vector& chains) { - changed_paths_.push_back(path); - const int path_begin_index = chains_.size(); - chains_.insert(chains_.end(), chains.begin(), chains.end()); - const int path_end_index = chains_.size(); - paths_[path] = {path_begin_index, path_end_index}; - chains_.emplace_back(0, 0); // Sentinel. -} - -void PathState::ChangeLoops(const std::vector& new_loops) { - for (const int loop : new_loops) { - if (Path(loop) == -1) continue; - changed_loops_.push_back(loop); - } -} - -void PathState::Commit() { - DCHECK(!IsInvalid()); - if (committed_nodes_.size() < num_nodes_threshold_) { - IncrementalCommit(); - } else { - FullCommit(); - } -} - -void PathState::Revert() { - is_invalid_ = false; - chains_.resize(num_paths_ + 1); // One per path + sentinel. - for (const int path : changed_paths_) { - paths_[path] = {path, path + 1}; - } - changed_paths_.clear(); - changed_loops_.clear(); -} - -void PathState::CopyNewPathAtEndOfNodes(int path) { - // Copy path's nodes, chain by chain. - const PathBounds path_bounds = paths_[path]; - for (int i = path_bounds.begin_index; i < path_bounds.end_index; ++i) { - const ChainBounds chain_bounds = chains_[i]; - committed_nodes_.insert(committed_nodes_.end(), - committed_nodes_.data() + chain_bounds.begin_index, - committed_nodes_.data() + chain_bounds.end_index); - if (committed_paths_[committed_nodes_.back()] == path) continue; - for (int i = chain_bounds.begin_index; i < chain_bounds.end_index; ++i) { - const int node = committed_nodes_[i]; - committed_paths_[node] = path; - } - } -} - -// TODO(user): Instead of copying paths at the end systematically, -// reuse some of the memory when possible. -void PathState::IncrementalCommit() { - const int new_nodes_begin = committed_nodes_.size(); - for (const int path : ChangedPaths()) { - const int chain_begin = committed_nodes_.size(); - CopyNewPathAtEndOfNodes(path); - const int chain_end = committed_nodes_.size(); - chains_[path] = {chain_begin, chain_end}; - } - // Re-index all copied nodes. - const int new_nodes_end = committed_nodes_.size(); - for (int i = new_nodes_begin; i < new_nodes_end; ++i) { - const int node = committed_nodes_[i]; - committed_index_[node] = i; - } - // New loops stay in place: only change their path to -1, - // committed_index_ does not change. - for (const int loop : ChangedLoops()) { - committed_paths_[loop] = -1; - } - // Committed part of the state is set up, erase incremental changes. - Revert(); -} - -void PathState::FullCommit() { - // Copy all paths at the end of committed_nodes_, - // then remove all old committed_nodes_. - const int old_num_nodes = committed_nodes_.size(); - for (int path = 0; path < num_paths_; ++path) { - const int new_path_begin = committed_nodes_.size() - old_num_nodes; - CopyNewPathAtEndOfNodes(path); - const int new_path_end = committed_nodes_.size() - old_num_nodes; - chains_[path] = {new_path_begin, new_path_end}; - } - committed_nodes_.erase(committed_nodes_.begin(), - committed_nodes_.begin() + old_num_nodes); - - // Reindex path nodes, then loop nodes. - constexpr int kUnindexed = -1; - committed_index_.assign(num_nodes_, kUnindexed); - int index = 0; - for (const int node : committed_nodes_) { - committed_index_[node] = index++; - } - for (int node = 0; node < num_nodes_; ++node) { - if (committed_index_[node] != kUnindexed) continue; - committed_index_[node] = index++; - committed_nodes_.push_back(node); - committed_paths_[node] = -1; - } - // Committed part of the state is set up, erase incremental changes. - Revert(); -} - -namespace { - -class PathStateFilter : public LocalSearchFilter { - public: - std::string DebugString() const override { return "PathStateFilter"; } - PathStateFilter(std::unique_ptr path_state, - const std::vector& nexts); - void Relax(const Assignment* delta, const Assignment* deltadelta) override; - bool Accept(const Assignment*, const Assignment*, int64_t, int64_t) override { - return true; - } - void Synchronize(const Assignment*, const Assignment*) override{}; - void Commit(const Assignment* assignment, const Assignment* delta) override; - void Revert() override; - void Reset() override; - - private: - // Used in arc to chain translation, see below. - struct TailHeadIndices { - int tail_index; - int head_index; - }; - struct IndexArc { - int index; - int arc; - bool operator<(const IndexArc& other) const { return index < other.index; } - }; - - // Translate changed_arcs_ to chains, pass to underlying PathState. - void CutChains(); - // From changed_paths_ and changed_arcs_, fill chains_ and paths_. - // Selection-based algorithm in O(n^2), to use for small change sets. - void MakeChainsFromChangedPathsAndArcsWithSelectionAlgorithm(); - // From changed_paths_ and changed_arcs_, fill chains_ and paths_. - // Generic algorithm in O(sort(n)+n), to use for larger change sets. - void MakeChainsFromChangedPathsAndArcsWithGenericAlgorithm(); - - const std::unique_ptr path_state_; - // 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_; - std::vector path_has_changed_; - std::vector> changed_arcs_; - std::vector changed_loops_; - std::vector tail_head_indices_; - std::vector arcs_by_tail_index_; - std::vector arcs_by_head_index_; - std::vector next_arc_; - std::vector path_chains_; -}; - -PathStateFilter::PathStateFilter(std::unique_ptr path_state, - const std::vector& nexts) - : path_state_(std::move(path_state)) { - { - int min_index = std::numeric_limits::max(); - int max_index = std::numeric_limits::min(); - for (const IntVar* next : nexts) { - const int index = next->index(); - min_index = std::min(min_index, index); - max_index = std::max(max_index, index); - } - variable_index_to_node_.resize(max_index - min_index + 1, -1); - index_offset_ = min_index; - } - - for (int node = 0; node < nexts.size(); ++node) { - const int index = nexts[node]->index() - index_offset_; - variable_index_to_node_[index] = node; - } - path_has_changed_.assign(path_state_->NumPaths(), false); -} - -void PathStateFilter::Relax(const Assignment* delta, const Assignment*) { - path_state_->Revert(); - changed_arcs_.clear(); - for (const IntVarElement& var_value : delta->IntVarContainer().elements()) { - if (var_value.Var() == nullptr) continue; - const int index = var_value.Var()->index() - index_offset_; - if (index < 0 || variable_index_to_node_.size() <= index) continue; - const int node = variable_index_to_node_[index]; - if (node == -1) continue; - if (var_value.Bound()) { - changed_arcs_.emplace_back(node, var_value.Value()); - } else { - path_state_->Revert(); - path_state_->SetInvalid(); - return; - } - } - 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(); -} - -// The solver does not guarantee that a given Commit() corresponds to -// the previous Relax() (or that there has been a call to Relax()), -// so we replay the full change call sequence. -void PathStateFilter::Commit(const Assignment* assignment, - const Assignment* delta) { - path_state_->Revert(); - if (delta == nullptr || delta->Empty()) { - Relax(assignment, nullptr); - } else { - Relax(delta, nullptr); - } - path_state_->Commit(); -} - -void PathStateFilter::Revert() { path_state_->Revert(); } - -void PathStateFilter::CutChains() { - // Filter out unchanged arcs from changed_arcs_, - // translate changed arcs to changed arc indices. - // Fill changed_paths_ while we hold node_path. - for (const int path : changed_paths_) path_has_changed_[path] = false; - changed_paths_.clear(); - tail_head_indices_.clear(); - changed_loops_.clear(); - int num_changed_arcs = 0; - for (const auto [node, next] : changed_arcs_) { - const int node_index = path_state_->CommittedIndex(node); - 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. - 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]) { - path_has_changed_[node_path] = true; - changed_paths_.push_back(node_path); - } - } else if (node == next && node_path != -1) { // New loop. - changed_loops_.push_back(node); - } - } - changed_arcs_.resize(num_changed_arcs); - - path_state_->ChangeLoops(changed_loops_); - if (tail_head_indices_.size() + changed_paths_.size() <= 8) { - MakeChainsFromChangedPathsAndArcsWithSelectionAlgorithm(); - } else { - MakeChainsFromChangedPathsAndArcsWithGenericAlgorithm(); - } -} - -void PathStateFilter:: - MakeChainsFromChangedPathsAndArcsWithSelectionAlgorithm() { - int num_visited_changed_arcs = 0; - const int num_changed_arcs = tail_head_indices_.size(); - // For every path, find all its chains. - for (const int path : changed_paths_) { - path_chains_.clear(); - const auto [start_index, end_index] = path_state_->CommittedPathRange(path); - int current_index = start_index; - while (true) { - // Look for smallest non-visited tail_index that is no smaller than - // current_index. - int selected_arc = -1; - int selected_tail_index = std::numeric_limits::max(); - for (int i = num_visited_changed_arcs; i < num_changed_arcs; ++i) { - const int tail_index = tail_head_indices_[i].tail_index; - if (current_index <= tail_index && tail_index < selected_tail_index) { - selected_arc = i; - selected_tail_index = tail_index; - } - } - // If there is no such tail index, or more generally if the next chain - // would be cut by end of path, - // stack {current_index, end_index + 1} in chains_, and go to next path. - // Otherwise, stack {current_index, tail_index+1} in chains_, - // set current_index = head_index, set pair to visited. - if (start_index <= current_index && current_index < end_index && - end_index <= selected_tail_index) { - path_chains_.emplace_back(current_index, end_index); - break; - } else { - path_chains_.emplace_back(current_index, selected_tail_index + 1); - current_index = tail_head_indices_[selected_arc].head_index; - std::swap(tail_head_indices_[num_visited_changed_arcs], - tail_head_indices_[selected_arc]); - ++num_visited_changed_arcs; - } - } - path_state_->ChangePath(path, path_chains_); - } -} - -void PathStateFilter::MakeChainsFromChangedPathsAndArcsWithGenericAlgorithm() { - // TRICKY: For each changed path, we want to generate a sequence of chains - // that represents the path in the changed state. - // First, notice that if we add a fake end->start arc for each changed path, - // then all chains will be from the head of an arc to the tail of an arc. - // A way to generate the changed chains and paths would be, for each path, - // to start from a fake arc's head (the path start), go down the path until - // the tail of an arc, and go to the next arc until we return on the fake arc, - // enqueuing the [head, tail] chains as we go. - // In turn, to do that, we need to know which arc to go to. - // If we sort all heads and tails by index in two separate arrays, - // the head_index and tail_index at the same rank are such that - // [head_index, tail_index] is a chain. Moreover, the arc that must be visited - // after head_index's arc is tail_index's arc. - - // Add a fake end->start arc for each path. - for (const int path : changed_paths_) { - const auto [start_index, end_index] = path_state_->CommittedPathRange(path); - tail_head_indices_.push_back({end_index - 1, start_index}); - } - - // Generate pairs (tail_index, arc) and (head_index, arc) for all arcs, - // sort those pairs by index. - const int num_arc_indices = tail_head_indices_.size(); - arcs_by_tail_index_.resize(num_arc_indices); - arcs_by_head_index_.resize(num_arc_indices); - for (int i = 0; i < num_arc_indices; ++i) { - arcs_by_tail_index_[i] = {tail_head_indices_[i].tail_index, i}; - arcs_by_head_index_[i] = {tail_head_indices_[i].head_index, i}; - } - std::sort(arcs_by_tail_index_.begin(), arcs_by_tail_index_.end()); - std::sort(arcs_by_head_index_.begin(), arcs_by_head_index_.end()); - // Generate the map from arc to next arc in path. - next_arc_.resize(num_arc_indices); - for (int i = 0; i < num_arc_indices; ++i) { - next_arc_[arcs_by_head_index_[i].arc] = arcs_by_tail_index_[i].arc; - } - - // Generate chains: for every changed path, start from its fake arc, - // jump to next_arc_ until going back to fake arc, - // enqueuing chains as we go. - const int first_fake_arc = num_arc_indices - changed_paths_.size(); - for (int fake_arc = first_fake_arc; fake_arc < num_arc_indices; ++fake_arc) { - path_chains_.clear(); - int32_t arc = fake_arc; - do { - const int chain_begin = tail_head_indices_[arc].head_index; - arc = next_arc_[arc]; - const int chain_end = tail_head_indices_[arc].tail_index + 1; - path_chains_.emplace_back(chain_begin, chain_end); - } while (arc != fake_arc); - const int path = changed_paths_[fake_arc - first_fake_arc]; - path_state_->ChangePath(path, path_chains_); - } -} - -} // namespace - -LocalSearchFilter* MakePathStateFilter(Solver* solver, - std::unique_ptr path_state, - const std::vector& nexts) { - PathStateFilter* filter = new PathStateFilter(std::move(path_state), nexts); - return solver->RevAlloc(filter); -} - -namespace { -using EInterval = DimensionChecker::ExtendedInterval; - -constexpr int64_t kint64min = std::numeric_limits::min(); -constexpr int64_t kint64max = std::numeric_limits::max(); - -EInterval operator&(const EInterval& i1, const EInterval& i2) { - return {std::max(i1.num_negative_infinity == 0 ? i1.min : kint64min, - i2.num_negative_infinity == 0 ? i2.min : kint64min), - std::min(i1.num_positive_infinity == 0 ? i1.max : kint64max, - i2.num_positive_infinity == 0 ? i2.max : kint64max), - std::min(i1.num_negative_infinity, i2.num_negative_infinity), - std::min(i1.num_positive_infinity, i2.num_positive_infinity)}; -} - -EInterval operator&=(EInterval& i1, const EInterval& i2) { - i1 = i1 & i2; - return i1; -} - -bool IsEmpty(const EInterval& interval) { - const int64_t minimum_value = - interval.num_negative_infinity == 0 ? interval.min : kint64min; - const int64_t maximum_value = - interval.num_positive_infinity == 0 ? interval.max : kint64max; - return minimum_value > maximum_value; -} - -EInterval operator+(const EInterval& i1, const EInterval& i2) { - return {CapAdd(i1.min, i2.min), CapAdd(i1.max, i2.max), - i1.num_negative_infinity + i2.num_negative_infinity, - i1.num_positive_infinity + i2.num_positive_infinity}; -} - -EInterval& operator+=(EInterval& i1, const EInterval& i2) { - i1 = i1 + i2; - return i1; -} - -EInterval operator-(const EInterval& i1, const EInterval& i2) { - return {CapSub(i1.min, i2.max), CapSub(i1.max, i2.min), - i1.num_negative_infinity + i2.num_positive_infinity, - i1.num_positive_infinity + i2.num_negative_infinity}; -} - -// Return the interval delta such that from + delta = to. -// Note that the result is not the same as "to + (-from)". -EInterval Delta(const EInterval& from, const EInterval& to) { - return {CapSub(to.min, from.min), CapSub(to.max, from.max), - to.num_negative_infinity - from.num_negative_infinity, - to.num_positive_infinity - from.num_positive_infinity}; -} - -EInterval ToExtendedInterval(DimensionChecker::Interval interval) { - const bool is_neg_infinity = interval.min == kint64min; - const bool is_pos_infinity = interval.max == kint64max; - return {is_neg_infinity ? 0 : interval.min, - is_pos_infinity ? 0 : interval.max, is_neg_infinity ? 1 : 0, - is_pos_infinity ? 1 : 0}; -} - -std::vector ToExtendedIntervals( - absl::Span intervals) { - std::vector extended_intervals; - extended_intervals.reserve(intervals.size()); - for (const auto& interval : intervals) { - extended_intervals.push_back(ToExtendedInterval(interval)); - } - return extended_intervals; -} -} // namespace - -DimensionChecker::DimensionChecker( - const PathState* path_state, std::vector path_capacity, - std::vector path_class, - std::vector> - demand_per_path_class, - std::vector node_capacity, int min_range_size_for_riq) - : path_state_(path_state), - path_capacity_(ToExtendedIntervals(path_capacity)), - path_class_(std::move(path_class)), - demand_per_path_class_(std::move(demand_per_path_class)), - node_capacity_(ToExtendedIntervals(node_capacity)), - index_(path_state_->NumNodes(), 0), - maximum_riq_layer_size_(std::max( - 16, 4 * path_state_->NumNodes())), // 16 and 4 are arbitrary. - min_range_size_for_riq_(min_range_size_for_riq) { - const int num_nodes = path_state_->NumNodes(); - cached_demand_.resize(num_nodes); - const int num_paths = path_state_->NumPaths(); - DCHECK_EQ(num_paths, path_capacity_.size()); - DCHECK_EQ(num_paths, path_class_.size()); - const int maximum_riq_exponent = MostSignificantBitPosition32(num_nodes); - riq_.resize(maximum_riq_exponent + 1); - FullCommit(); -} - -bool DimensionChecker::Check() const { - if (path_state_->IsInvalid()) return true; - for (const int path : path_state_->ChangedPaths()) { - const EInterval path_capacity = path_capacity_[path]; - const int path_class = path_class_[path]; - // Loop invariant: except for the first chain, cumul represents the cumul - // state of the last node of the previous chain, and it is nonempty. - int prev_node = path_state_->Start(path); - EInterval cumul = node_capacity_[prev_node] & path_capacity; - if (IsEmpty(cumul)) return false; - - for (const auto chain : path_state_->Chains(path)) { - const int first_node = chain.First(); - const int last_node = chain.Last(); - - if (prev_node != first_node) { - // Bring cumul state from last node of previous chain to first node of - // current chain. - const EInterval demand = ToExtendedInterval( - demand_per_path_class_[path_class](prev_node, first_node)); - cumul += demand; - cumul &= path_capacity; - cumul &= node_capacity_[first_node]; - if (IsEmpty(cumul)) return false; - prev_node = first_node; - } - - // Bring cumul state from first node to last node of the current chain. - const int first_index = index_[first_node]; - 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]; - // 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. - const bool chain_is_cached = chain_path_class == path_class; - if (last_index - first_index > min_range_size_for_riq_ && - chain_is_cached) { - UpdateCumulUsingChainRIQ(first_index, last_index, path_capacity, cumul); - if (IsEmpty(cumul)) return false; - prev_node = chain.Last(); - } else { - for (const int node : chain.WithoutFirstNode()) { - const EInterval demand = - chain_is_cached - ? cached_demand_[prev_node] - : ToExtendedInterval( - demand_per_path_class_[path_class](prev_node, node)); - cumul += demand; - cumul &= node_capacity_[node]; - cumul &= path_capacity; - if (IsEmpty(cumul)) return false; - prev_node = node; - } - } - } - } - return true; -} - -void DimensionChecker::Commit() { - const int current_layer_size = riq_[0].size(); - int change_size = path_state_->ChangedPaths().size(); - for (const int path : path_state_->ChangedPaths()) { - for (const auto chain : path_state_->Chains(path)) { - change_size += chain.NumNodes(); - } - } - if (current_layer_size + change_size <= maximum_riq_layer_size_) { - IncrementalCommit(); - } else { - FullCommit(); - } -} - -void DimensionChecker::IncrementalCommit() { - for (const int path : path_state_->ChangedPaths()) { - const int begin_index = riq_[0].size(); - AppendPathDemandsToSums(path); - UpdateRIQStructure(begin_index, riq_[0].size()); - } -} - -void DimensionChecker::FullCommit() { - // Clear all structures. - for (auto& layer : riq_) layer.clear(); - // Append all paths. - const int num_paths = path_state_->NumPaths(); - for (int path = 0; path < num_paths; ++path) { - const int begin_index = riq_[0].size(); - AppendPathDemandsToSums(path); - UpdateRIQStructure(begin_index, riq_[0].size()); - } -} - -void DimensionChecker::AppendPathDemandsToSums(int path) { - // Value of forwards_demand_sums_riq_ at node_index must be the sum - // of all demands of nodes from start of path to node. - const int path_class = path_class_[path]; - EInterval demand_sum = {0, 0, 0, 0}; - int prev = path_state_->Start(path); - int index = riq_[0].size(); - for (const int node : path_state_->Nodes(path)) { - // Transition to current node. - const EInterval demand = - prev == node ? EInterval{0, 0, 0, 0} - : ToExtendedInterval( - demand_per_path_class_[path_class](prev, node)); - demand_sum += demand; - cached_demand_[prev] = demand; - prev = node; - // Store all data of current node. - index_[node] = index++; - riq_[0].push_back({.cumuls_to_fst = node_capacity_[node], - .tightest_tsum = demand_sum, - .cumuls_to_lst = node_capacity_[node], - .tsum_at_fst = demand_sum, - .tsum_at_lst = demand_sum}); - } - cached_demand_[path_state_->End(path)] = {0, 0, 0, 0}; -} - -void DimensionChecker::UpdateRIQStructure(int begin_index, int end_index) { - // The max layer is the one used by Range Intersection Query functions on - // (begin_index, end_index - 1). - const int max_layer = - MostSignificantBitPosition32(end_index - begin_index - 1); - for (int layer = 1, half_window = 1; layer <= max_layer; - ++layer, half_window *= 2) { - riq_[layer].resize(end_index); - for (int i = begin_index + 2 * half_window - 1; i < end_index; ++i) { - // The window covered by riq_[layer][i] goes from - // first = i - 2 * half_window + 1 to last = i, inclusive. - // Values are computed from two half-windows of the layer below, - // the F-window = (i - 2 * half_window, i - half_window], and - // the L-window - (i - half_window, i]. - const RIQNode& fw = riq_[layer - 1][i - half_window]; - const RIQNode& lw = riq_[layer - 1][i]; - const EInterval lst_to_lst = Delta(fw.tsum_at_lst, lw.tsum_at_lst); - const EInterval fst_to_fst = Delta(fw.tsum_at_fst, lw.tsum_at_fst); - - riq_[layer][i] = { - .cumuls_to_fst = fw.cumuls_to_fst & lw.cumuls_to_fst - fst_to_fst, - .tightest_tsum = fw.tightest_tsum & lw.tightest_tsum, - .cumuls_to_lst = fw.cumuls_to_lst + lst_to_lst & lw.cumuls_to_lst, - .tsum_at_fst = fw.tsum_at_fst, - .tsum_at_lst = lw.tsum_at_lst}; - } - } -} - -// The RIQ schema decomposes the request into two windows: -// - the F window covers indices [first_index, first_index + window) -// - the L window covers indices (last_index - window, last_index] -// The decomposition uses the first and last nodes of these windows. -void DimensionChecker::UpdateCumulUsingChainRIQ( - int first_index, int last_index, const ExtendedInterval& path_capacity, - ExtendedInterval& cumul) const { - DCHECK_LE(0, first_index); - DCHECK_LT(first_index, last_index); - DCHECK_LT(last_index, riq_[0].size()); - const int layer = MostSignificantBitPosition32(last_index - first_index); - const int window = 1 << layer; - const RIQNode& fw = riq_[layer][first_index + window - 1]; - const RIQNode& lw = riq_[layer][last_index]; - - // Compute the set of cumul values that can reach the last node. - cumul &= fw.cumuls_to_fst; - cumul &= lw.cumuls_to_fst - Delta(fw.tsum_at_fst, lw.tsum_at_fst); - cumul &= path_capacity - - Delta(fw.tsum_at_fst, fw.tightest_tsum & lw.tightest_tsum); - - // We need to check for emptiness before widening the interval with transit. - if (IsEmpty(cumul)) return; - - // Transit to last node. - cumul += Delta(fw.tsum_at_fst, lw.tsum_at_lst); - - // Compute the set of cumul values that are reached from first node. - cumul &= fw.cumuls_to_lst + Delta(fw.tsum_at_lst, lw.tsum_at_lst); - cumul &= lw.cumuls_to_lst; -} - -namespace { - -class DimensionFilter : public LocalSearchFilter { - public: - std::string DebugString() const override { return name_; } - DimensionFilter(std::unique_ptr checker, - absl::string_view dimension_name) - : checker_(std::move(checker)), - name_(absl::StrCat("DimensionFilter(", dimension_name, ")")) {} - - bool Accept(const Assignment*, const Assignment*, int64_t, int64_t) override { - return checker_->Check(); - } - - void Synchronize(const Assignment*, const Assignment*) override { - checker_->Commit(); - } - - private: - std::unique_ptr checker_; - const std::string name_; -}; - -} // namespace - -LocalSearchFilter* MakeDimensionFilter( - Solver* solver, std::unique_ptr checker, - const std::string& dimension_name) { - DimensionFilter* filter = - new DimensionFilter(std::move(checker), dimension_name); - return solver->RevAlloc(filter); -} - namespace { // ----- Variable domain filter ----- // Rejects assignments to values outside the domain of variables @@ -3880,6 +3125,16 @@ LocalSearchState::Variable LocalSearchState::MakeVariable( return {this, domain_id}; } +LocalSearchState::Variable LocalSearchState::MakeVariableWithRelaxedDomain( + int64_t min, int64_t max) { + const VariableDomainId domain_id = AddVariableDomain(min, max); + return {this, domain_id}; +} + +LocalSearchState::Variable LocalSearchState::DummyVariable() { + return {nullptr, VariableDomainId(-1)}; +} + void LocalSearchState::RelaxVariableDomain(VariableDomainId domain_id) { DCHECK(state_domains_are_all_nonempty_); if (!state_has_relaxed_domains_) { @@ -4121,14 +3376,14 @@ LocalSearchState::WeightedSum::WeightedSum( } else { invariants_.wsum_maxs = CapAdd(invariants_.wsum_maxs, CapProd(weight, max)); - } + } } else { if (min == kint64min) { ++invariants_.num_pos_inf; } else { invariants_.wsum_maxs = CapAdd(invariants_.wsum_maxs, CapProd(weight, min)); - } + } if (max == kint64max) { ++invariants_.num_neg_inf; } else { @@ -4154,7 +3409,7 @@ LocalSearchState::VariableDomain LocalSearchState::WeightedSum::Propagate( const int64_t new_min = state_->VariableDomainMin(input.domain); const int64_t new_max = state_->VariableDomainMax(input.domain); const int64_t weight = input.weight; - if (weight > 0) { + if (weight > 0) { if (input.min != new_min) { // Remove contribution of last known min. if (input.min == kint64min) { @@ -4176,7 +3431,7 @@ LocalSearchState::VariableDomain LocalSearchState::WeightedSum::Propagate( // Remove contribution of last known max. if (input.max == kint64max) { --invariants_.num_pos_inf; - } else { + } else { invariants_.wsum_maxs = CapSub(invariants_.wsum_maxs, CapProd(weight, input.max)); } @@ -4204,9 +3459,9 @@ LocalSearchState::VariableDomain LocalSearchState::WeightedSum::Propagate( } else { invariants_.wsum_maxs = CapAdd(invariants_.wsum_maxs, CapProd(weight, new_min)); - } + } input.min = new_min; - } + } if (input.max != new_max) { // Remove contribution of last known max. if (input.max == kint64max) { @@ -4214,14 +3469,14 @@ LocalSearchState::VariableDomain LocalSearchState::WeightedSum::Propagate( } else { invariants_.wsum_mins = CapSub(invariants_.wsum_mins, CapProd(weight, input.max)); - } + } // Add contribution of new max. if (new_max == kint64max) { ++invariants_.num_neg_inf; } else { invariants_.wsum_mins = CapAdd(invariants_.wsum_mins, CapProd(weight, new_max)); - } + } input.max = new_max; } } @@ -4261,8 +3516,8 @@ bool LocalSearchState::PropagateTighten(VariableDomainId domain_id) { const auto [output_min, output_max] = constraint->Propagate(input_index); if (output_min != kint64min && !TightenVariableDomainMin(constraint->Output(), output_min)) { - return false; - } + return false; + } if (output_max != kint64max && !TightenVariableDomainMax(constraint->Output(), output_max)) { return false; @@ -4363,8 +3618,8 @@ class LocalSearchProfiler : public LocalSearchMonitor { local_search_operator_statistics->set_duration_seconds(duration_seconds); }); ParseLocalSearchFilterStatistics([&statistics_proto]( - const std::string& context, - const std::string& name, + absl::string_view context, + absl::string_view name, int64_t num_calls, int64_t num_rejects, double duration_seconds) { LocalSearchStatistics::LocalSearchFilterStatistics* const diff --git a/ortools/constraint_solver/resource.cc b/ortools/constraint_solver/resource.cc index 33bfbefbfde..f82ed371a34 100644 --- a/ortools/constraint_solver/resource.cc +++ b/ortools/constraint_solver/resource.cc @@ -2595,7 +2595,7 @@ DisjunctiveConstraint::~DisjunctiveConstraint() {} void DisjunctiveConstraint::SetTransitionTime( std::function transition_time) { if (transition_time != nullptr) { - transition_time_ = transition_time; + transition_time_ = std::move(transition_time); } else { transition_time_ = [](int64_t x, int64_t y) { return 0; }; } diff --git a/ortools/constraint_solver/routing.cc b/ortools/constraint_solver/routing.cc index f9cdfeb8943..36daf842ff0 100644 --- a/ortools/constraint_solver/routing.cc +++ b/ortools/constraint_solver/routing.cc @@ -26,7 +26,7 @@ #include #include #include -#include +#include #include #include #include @@ -237,16 +237,20 @@ SweepArranger* RoutingModel::sweep_arranger() const { } void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( - const RoutingModel& routing_model, int num_neighbors, - bool add_vehicle_starts_to_neighbors) { + const RoutingModel& routing_model, const NodeNeighborsParameters& params) { + auto [num_neighbors, add_vehicle_starts_to_neighbors, + only_sort_neighbors_for_partial_neighborhoods] = params; DCHECK_GE(num_neighbors, 0); // TODO(user): consider checking search limits. const int size = routing_model.Size(); const int num_non_start_end_nodes = size - routing_model.vehicles(); const int size_with_vehicle_nodes = size + routing_model.vehicles(); + const int max_num_neighbors = std::max(num_non_start_end_nodes - 1, 0); + num_neighbors = std::min(max_num_neighbors, num_neighbors); node_index_to_neighbors_by_cost_class_.clear(); - if (num_neighbors >= num_non_start_end_nodes) { + if (num_neighbors == max_num_neighbors && + only_sort_neighbors_for_partial_neighborhoods) { all_nodes_.reserve(size); for (int node = 0; node < size; node++) { if (add_vehicle_starts_to_neighbors || !routing_model.IsStart(node)) { @@ -255,78 +259,152 @@ void RoutingModel::NodeNeighborsByCostClass::ComputeNeighbors( } return; } - node_index_to_neighbors_by_cost_class_.resize(size_with_vehicle_nodes); const int num_cost_classes = routing_model.GetCostClassesCount(); - for (int node_index = 0; node_index < size_with_vehicle_nodes; node_index++) { - node_index_to_neighbors_by_cost_class_[node_index].resize(num_cost_classes); + node_index_to_neighbors_by_cost_class_.resize(num_cost_classes); + std::vector>> + node_index_to_neighbor_indicator_by_cost_class(num_cost_classes); + std::vector>> + node_index_to_costs_by_cost_class(num_cost_classes); for (int cc = 0; cc < num_cost_classes; cc++) { - node_index_to_neighbors_by_cost_class_[node_index][cc] = - std::make_unique>(size); + node_index_to_neighbors_by_cost_class_[cc].resize(size_with_vehicle_nodes); + if (!routing_model.HasVehicleWithCostClassIndex( + RoutingCostClassIndex(cc))) { + continue; } + node_index_to_neighbor_indicator_by_cost_class[cc].resize( + size_with_vehicle_nodes); + node_index_to_costs_by_cost_class[cc].resize(size_with_vehicle_nodes); + for (int node = 0; node < size_with_vehicle_nodes; node++) { + node_index_to_neighbors_by_cost_class_[cc][node].reserve( + num_neighbors + routing_model.vehicles()); + node_index_to_neighbor_indicator_by_cost_class[cc][node].resize(size, + false); + node_index_to_costs_by_cost_class[cc][node].resize(size, -1); } - - std::vector> cost_nodes; - cost_nodes.reserve(size); - for (int node_index = 0; node_index < size_with_vehicle_nodes; ++node_index) { - if (routing_model.IsStart(node_index) || routing_model.IsEnd(node_index)) { - // For vehicle starts/ends, we consider all nodes (see below) - continue; } - // TODO(user): Use the model's IndexNeighborFinder when available. + std::vector neighbors; for (int cost_class = 0; cost_class < num_cost_classes; cost_class++) { if (!routing_model.HasVehicleWithCostClassIndex( RoutingCostClassIndex(cost_class))) { // No vehicle with this cost class, avoid unnecessary computations. continue; } - cost_nodes.clear(); + std::vector>& node_index_to_neighbors = + node_index_to_neighbors_by_cost_class_[cost_class]; + std::vector>& node_index_to_neighbor_indicator = + node_index_to_neighbor_indicator_by_cost_class[cost_class]; + std::vector>& node_index_to_costs = + node_index_to_costs_by_cost_class[cost_class]; + for (int node_index = 0; node_index < size; ++node_index) { + if (routing_model.IsStart(node_index)) { + // For vehicle start/ends, we consider all nodes (see below). + continue; + } + + // TODO(user): Use the model's IndexNeighborFinder when available. + neighbors.clear(); + neighbors.reserve(num_non_start_end_nodes); + if (num_neighbors > 0) { + std::vector& costs = node_index_to_costs[node_index]; for (int after_node = 0; after_node < size; ++after_node) { if (after_node != node_index && !routing_model.IsStart(after_node)) { - cost_nodes.push_back( - std::make_pair(routing_model.GetArcCostForClass( - node_index, after_node, cost_class), - after_node)); + costs[after_node] = routing_model.GetArcCostForClass( + node_index, after_node, cost_class); + neighbors.push_back(after_node); } } - DCHECK_GE(cost_nodes.size(), num_neighbors); - // Make sure the order of the n first element is always the same. - absl::c_partial_sort(cost_nodes, cost_nodes.begin() + num_neighbors); - cost_nodes.resize(num_neighbors); - - auto& node_neighbors = - node_index_to_neighbors_by_cost_class_[node_index][cost_class]; - for (const auto& costed_node : cost_nodes) { - const int neighbor = costed_node.second; - node_neighbors->Set(neighbor); + // Get the 'num_neighbors' closest neighbors. + DCHECK_GE(neighbors.size(), num_neighbors); + std::nth_element( + neighbors.begin(), neighbors.begin() + num_neighbors - 1, + neighbors.end(), [&costs](int n1, int n2) { + return std::tie(costs[n1], n1) < std::tie(costs[n2], n2); + }); + neighbors.resize(num_neighbors); + } - // Add reverse neighborhood. + // Add neighborhoods. + for (int neighbor : neighbors) { DCHECK(!routing_model.IsEnd(neighbor) && !routing_model.IsStart(neighbor)); - node_index_to_neighbors_by_cost_class_[neighbor][cost_class]->Set( - node_index); + if (node_index_to_neighbor_indicator[node_index][neighbor]) { + DCHECK(node_index_to_neighbor_indicator[neighbor][node_index]); + continue; } + DCHECK(!node_index_to_neighbor_indicator[node_index][neighbor]); + node_index_to_neighbor_indicator[node_index][neighbor] = true; + node_index_to_neighbors[node_index].push_back(neighbor); + DCHECK(!node_index_to_neighbor_indicator[neighbor][node_index]); + node_index_to_neighbor_indicator[neighbor][node_index] = true; + node_index_to_neighbors[neighbor].push_back(node_index); + } + // Add all vehicle starts as neighbors to this node and vice-versa. // TODO(user): Consider keeping vehicle start/ends out of neighbors, to // prune arcs going from node to start for instance. + // TODO(user): Investigate whether we actually need to keep track of + // neighbors for vehicle ends. + // TODO(user): Investigate if vehicle ends should be considered as + // neighbors for every node too. + // TODO(user): In the current state of the function, vehicle starts + // aren't set as neighbors for vehicle ends. Investigate if that's WAI. for (int vehicle = 0; vehicle < routing_model.vehicles(); vehicle++) { const int vehicle_start = routing_model.Start(vehicle); - if (add_vehicle_starts_to_neighbors) node_neighbors->Set(vehicle_start); - node_index_to_neighbors_by_cost_class_[vehicle_start][cost_class]->Set( - node_index); - node_index_to_neighbors_by_cost_class_[routing_model.End(vehicle)] - [cost_class] - ->Set(node_index); + const int64_t cost_from_start = routing_model.GetArcCostForClass( + vehicle_start, node_index, cost_class); + if (add_vehicle_starts_to_neighbors) { + DCHECK(!node_index_to_neighbor_indicator[node_index][vehicle_start]); + node_index_to_neighbor_indicator[node_index][vehicle_start] = true; + node_index_to_neighbors[node_index].push_back(vehicle_start); + node_index_to_costs[node_index][vehicle_start] = cost_from_start; + } + DCHECK(!node_index_to_neighbor_indicator[vehicle_start][node_index]); + node_index_to_neighbor_indicator[vehicle_start][node_index] = true; + node_index_to_neighbors[vehicle_start].push_back(node_index); + node_index_to_costs[vehicle_start][node_index] = cost_from_start; + + const int vehicle_end = routing_model.End(vehicle); + DCHECK(!node_index_to_neighbor_indicator[vehicle_end][node_index]); + node_index_to_neighbor_indicator[vehicle_end][node_index] = true; + node_index_to_neighbors[vehicle_end].push_back(node_index); + node_index_to_costs[vehicle_end][node_index] = + routing_model.GetArcCostForClass(node_index, vehicle_end, + cost_class); } } } + + // Sort the neighbors into node_index_to_neighbors_by_cost_class_ by cost. + for (int cost_class = 0; cost_class < num_cost_classes; cost_class++) { + if (!routing_model.HasVehicleWithCostClassIndex( + RoutingCostClassIndex(cost_class))) { + // No vehicle with this cost class. + continue; + } + for (int node_index = 0; node_index < size_with_vehicle_nodes; + ++node_index) { + auto& node_neighbors = + node_index_to_neighbors_by_cost_class_[cost_class][node_index]; + const std::vector& costs = + node_index_to_costs_by_cost_class[cost_class][node_index]; + absl::c_sort(node_neighbors, [&costs](int n1, int n2) { + DCHECK_GE(costs[n1], 0); + DCHECK_GE(costs[n2], 0); + return std::tie(costs[n1], n1) < std::tie(costs[n2], n2); + }); + // Check that there are no duplicate elements. + DCHECK(absl::c_adjacent_find(node_neighbors) == node_neighbors.end()); + } + } } const RoutingModel::NodeNeighborsByCostClass* RoutingModel::GetOrCreateNodeNeighborsByCostClass( double neighbors_ratio, int64_t min_neighbors, double& neighbors_ratio_used, - bool add_vehicle_starts_to_neighbors) { + bool add_vehicle_starts_to_neighbors, + bool only_sort_neighbors_for_partial_neighborhoods) { const int64_t num_non_start_end_nodes = Size() - vehicles(); neighbors_ratio_used = neighbors_ratio; int num_neighbors = std::max( @@ -336,27 +414,21 @@ RoutingModel::GetOrCreateNodeNeighborsByCostClass( neighbors_ratio_used = 1; num_neighbors = Size(); } - return GetOrCreateNodeNeighborsByCostClass(num_neighbors, - add_vehicle_starts_to_neighbors); + return GetOrCreateNodeNeighborsByCostClass( + {num_neighbors, add_vehicle_starts_to_neighbors, + only_sort_neighbors_for_partial_neighborhoods}); } const RoutingModel::NodeNeighborsByCostClass* RoutingModel::GetOrCreateNodeNeighborsByCostClass( - int num_neighbors, bool add_vehicle_starts_to_neighbors) { - const NodeNeighborsParameters params = {num_neighbors, - add_vehicle_starts_to_neighbors}; - std::unique_ptr* node_neighbors_by_cost_class_ptr = - gtl::FindOrNull(node_neighbors_by_cost_class_per_size_, params); - if (node_neighbors_by_cost_class_ptr != nullptr) { - return node_neighbors_by_cost_class_ptr->get(); - } + const NodeNeighborsParameters& params) { std::unique_ptr& node_neighbors_by_cost_class = - node_neighbors_by_cost_class_per_size_ - .insert(std::make_pair(params, - std::make_unique())) - .first->second; - node_neighbors_by_cost_class->ComputeNeighbors( - *this, num_neighbors, add_vehicle_starts_to_neighbors); + node_neighbors_by_cost_class_per_size_[params]; + if (node_neighbors_by_cost_class != nullptr) { + return node_neighbors_by_cost_class.get(); + } + node_neighbors_by_cost_class = std::make_unique(); + node_neighbors_by_cost_class->ComputeNeighbors(*this, params); return node_neighbors_by_cost_class.get(); } @@ -589,7 +661,7 @@ int RoutingModel::RegisterStateDependentTransitCallback( StateDependentTransitCallbackCache* const cache = state_dependent_transit_evaluators_cache_.back().get(); state_dependent_transit_evaluators_.push_back( - [cache, callback](int64_t i, int64_t j) { + [cache, callback = std::move(callback)](int64_t i, int64_t j) { StateDependentTransit value; if (gtl::FindCopy(*cache, CacheKey(i, j), &value)) return value; value = callback(i, j); @@ -1340,7 +1412,7 @@ void RoutingModel::ComputeCostClasses( if (cost_class_index == kCostClassIndexOfZeroCost) { has_vehicle_with_zero_cost_class_ = true; } else if (cost_class_index == num_cost_classes) { // New cost class. - cost_classes_.push_back(cost_class); + cost_classes_.push_back(std::move(cost_class)); } cost_class_index_of_vehicle_[vehicle] = cost_class_index; } @@ -1388,7 +1460,8 @@ struct VehicleClass { util_intops::StrongVector dimension_capacities; /// dimension_evaluators[d]->Run(from, to) is the transit value of arc /// from->to for a dimension d. - util_intops::StrongVector dimension_evaluator_classes; + util_intops::StrongVector + dimension_evaluator_classes; /// Hash of the visitability of (non-start/end) nodes. uint64_t visitable_nodes_hash; /// Hash of allowed resources for each resource group, or -1 if a given @@ -1771,15 +1844,11 @@ IntVar* RoutingModel::CreateDisjunction(DisjunctionIndex disjunction) { } } -void RoutingModel::AddSoftSameVehicleConstraint( - const std::vector& indices, int64_t cost) { +void RoutingModel::AddSoftSameVehicleConstraint(std::vector indices, + int64_t cost) { if (!indices.empty()) { - ValuedNodes same_vehicle_cost; - for (const int64_t index : indices) { - same_vehicle_cost.indices.push_back(index); - } - same_vehicle_cost.value = cost; - same_vehicle_costs_.push_back(same_vehicle_cost); + same_vehicle_costs_.push_back( + {.indices = std::move(indices), .value = cost}); } } @@ -1863,6 +1932,39 @@ RoutingModel::GetPickupAndDeliveryPolicyOfVehicle(int vehicle) const { return vehicle_pickup_delivery_policy_[vehicle]; } +std::optional RoutingModel::GetFirstMatchingPickupDeliverySibling( + int64_t node, const std::function& is_match) const { + // NOTE: In most use-cases, where each node is a pickup or delivery in a + // single index pair, this function is in O(k) where k is the number of + // alternative deliveries or pickups for this index pair. + + // A node can't be a pickup and a delivery at the same time. + DCHECK(GetPickupPositions(node).empty() || + GetDeliveryPositions(node).empty()); + + const auto& pickup_and_delivery_pairs = GetPickupAndDeliveryPairs(); + + for (const auto& [pair_index, unused] : GetPickupPositions(node)) { + for (int64_t delivery_sibling : + pickup_and_delivery_pairs[pair_index].delivery_alternatives) { + if (is_match(delivery_sibling)) { + return delivery_sibling; + } + } + } + + for (const auto& [pair_index, unused] : GetDeliveryPositions(node)) { + for (int64_t pickup_sibling : + pickup_and_delivery_pairs[pair_index].pickup_alternatives) { + if (is_match(pickup_sibling)) { + return pickup_sibling; + } + } + } + + return std::nullopt; +} + int RoutingModel::GetNumOfSingletonNodes() const { int count = 0; for (int i = 0; i < Nexts().size(); ++i) { @@ -2018,6 +2120,7 @@ class RoutingModelInspector : public ModelVisitor { public: explicit RoutingModelInspector(RoutingModel* model) : model_(model) { same_vehicle_components_.SetNumberOfNodes(model->Size()); + same_active_var_components_.SetNumberOfNodes(model->Size()); for (const std::string& name : model->GetAllDimensionNames()) { RoutingDimension* const dimension = model->GetMutableDimension(name); const std::vector& cumuls = dimension->cumuls(); @@ -2029,6 +2132,9 @@ class RoutingModelInspector : public ModelVisitor { for (int i = 0; i < vehicle_vars.size(); ++i) { vehicle_var_to_indices_[vehicle_vars[i]] = i; } + for (int i = 0; i < model->Size(); ++i) { + active_var_to_indices_[model->ActiveVar(i)] = i; + } RegisterInspectors(); } ~RoutingModelInspector() override {} @@ -2041,6 +2147,14 @@ class RoutingModelInspector : public ModelVisitor { model_->SetSameVehicleGroup(node, node_to_same_vehicle_component_id[node]); } + const std::vector node_to_same_active_var_component_id = + same_active_var_components_.GetComponentIds(); + model_->InitSameActiveVarGroups( + same_active_var_components_.GetNumberOfComponents()); + for (int node = 0; node < model_->Size(); ++node) { + model_->SetSameActiveVarGroup(node, + node_to_same_active_var_component_id[node]); + } // TODO(user): Perform transitive closure of dimension precedence graphs. // TODO(user): Have a single annotated precedence graph. } @@ -2105,6 +2219,13 @@ class RoutingModelInspector : public ModelVisitor { << right_index << " are equal."; same_vehicle_components_.AddEdge(left_index, right_index); } + if (gtl::FindCopy(active_var_to_indices_, left_, &left_index) && + gtl::FindCopy(active_var_to_indices_, right_, &right_index)) { + VLOG(2) << "Active variables for " << left_index << " and " + << right_index << " are equal."; + same_active_var_components_.AddEdge(left_index, right_index); + } + left_ = nullptr; right_ = nullptr; }; @@ -2129,9 +2250,11 @@ class RoutingModelInspector : public ModelVisitor { RoutingModel* const model_; DenseConnectedComponentsFinder same_vehicle_components_; + DenseConnectedComponentsFinder same_active_var_components_; absl::flat_hash_map> cumul_to_dim_indices_; absl::flat_hash_map vehicle_var_to_indices_; + absl::flat_hash_map active_var_to_indices_; absl::flat_hash_map expr_inspectors_; absl::flat_hash_map array_inspectors_; absl::flat_hash_map constraint_inspectors_; @@ -2566,11 +2689,11 @@ void RoutingModel::CloseModelWithParameters( .path_used_when_empty = vehicle_used_when_empty_, .path_starts = paths_metadata_.Starts(), .path_ends = paths_metadata_.Ends(), - .costs = energy_costs, + .costs = std::move(energy_costs), }; solver_->AddConstraint( - solver_->MakePathEnergyCostConstraint(specification)); + solver_->MakePathEnergyCostConstraint(std::move(specification))); } // cost_ is the sum of cost_elements. cost_ = solver_->MakeSum(cost_elements)->Var(); @@ -4751,6 +4874,18 @@ RoutingModel::CreateLocalSearchFilters( {MakeMaxActiveVehiclesFilter(*this), kAccept, priority}); } + bool has_same_activity_constraints = false; + for (int node = 0; node < Size(); ++node) { + if (GetSameVehicleIndicesOfIndex(node).size() > 1) { + has_same_activity_constraints = true; + break; + } + } + if (has_same_activity_constraints) { + filter_events.push_back( + {MakeActiveNodeGroupFilter(*this), kAccept, priority}); + } + if (!disjunctions_.empty()) { if (options.filter_objective || HasMandatoryDisjunctions() || HasMaxCardinalityConstrainedDisjunctions()) { @@ -5064,6 +5199,9 @@ void RoutingModel::StoreDimensionCumulOptimizers( if (has_span_cost) ++num_linear_constraints; if (has_span_limit) ++num_linear_constraints; if (dimension->HasSoftSpanUpperBounds()) ++num_linear_constraints; + if (dimension->HasQuadraticCostSoftSpanUpperBounds()) { + ++num_linear_constraints; + } if (has_soft_lower_bound) ++num_linear_constraints; if (has_soft_upper_bound) ++num_linear_constraints; if (dimension->HasBreakConstraints()) ++num_linear_constraints; @@ -5251,9 +5389,7 @@ void RoutingModel::CreateFirstSolutionDecisionBuilders( [FirstSolutionStrategy::PATH_CHEAPEST_ARC] = CreateIntVarFilteredDecisionBuilder< EvaluatorCheapestAdditionFilteredHeuristic>( - [this](int64_t i, int64_t j) { - return GetArcCostForFirstSolution(i, j); - }, + eval, GetOrCreateLocalSearchFilterManager( search_parameters, {/*filter_objective=*/false, /*filter_with_cp_solver=*/false})); @@ -5817,7 +5953,7 @@ void RoutingModel::SetupTrace( search_log_ = solver_->MakeSearchLog(search_log_parameters); monitors_.push_back(search_log_); secondary_ls_monitors_.push_back( - solver_->MakeSearchLog(search_log_parameters)); + solver_->MakeSearchLog(std::move(search_log_parameters))); } } @@ -6848,9 +6984,8 @@ void RoutingDimension::SetBreakIntervalsOfVehicle( std::vector node_visit_transits) { if (breaks.empty()) return; const int visit_evaluator = model()->RegisterTransitCallback( - [node_visit_transits](int64_t from, int64_t /*to*/) { - return node_visit_transits[from]; - }, + [node_visit_transits = std::move(node_visit_transits)]( + int64_t from, int64_t /*to*/) { return node_visit_transits[from]; }, RoutingModel::kTransitEvaluatorSignPositiveOrZero); SetBreakIntervalsOfVehicle(std::move(breaks), vehicle, visit_evaluator, -1); } @@ -6861,9 +6996,8 @@ void RoutingDimension::SetBreakIntervalsOfVehicle( std::function delays) { if (breaks.empty()) return; const int visit_evaluator = model()->RegisterTransitCallback( - [node_visit_transits](int64_t from, int64_t /*to*/) { - return node_visit_transits[from]; - }, + [node_visit_transits = std::move(node_visit_transits)]( + int64_t from, int64_t /*to*/) { return node_visit_transits[from]; }, RoutingModel::kTransitEvaluatorSignPositiveOrZero); const int delay_evaluator = model()->RegisterTransitCallback( std::move(delays), RoutingModel::kTransitEvaluatorSignPositiveOrZero); diff --git a/ortools/constraint_solver/routing.h b/ortools/constraint_solver/routing.h index b80cee1ac0d..d6a352a0814 100644 --- a/ortools/constraint_solver/routing.h +++ b/ortools/constraint_solver/routing.h @@ -158,12 +158,12 @@ #include #include -#include #include #include #include #include #include +#include #include #include #include @@ -191,7 +191,6 @@ #include "ortools/routing/enums.pb.h" #include "ortools/routing/parameters.pb.h" #include "ortools/sat/theta_tree.h" -#include "ortools/util/bitset.h" #include "ortools/util/piecewise_linear_function.h" #include "ortools/util/range_query_function.h" #include "ortools/util/saturated_arithmetic.h" @@ -936,8 +935,7 @@ class RoutingModel { /// Adds a soft constraint to force a set of variable indices to be on the /// same vehicle. If all nodes are not on the same vehicle, each extra vehicle /// used adds 'cost' to the cost function. - void AddSoftSameVehicleConstraint(const std::vector& indices, - int64_t cost); + void AddSoftSameVehicleConstraint(std::vector indices, int64_t cost); /// Sets the vehicles which can visit a given node. If the node is in a /// disjunction, this will not prevent it from being unperformed. @@ -1028,6 +1026,12 @@ class RoutingModel { return implicit_pickup_delivery_pairs_without_alternatives_; } #endif // SWIG + + // Returns the first pickup or delivery sibling of the given node matching + // the given predicate. + std::optional GetFirstMatchingPickupDeliverySibling( + int64_t node, const std::function& is_match) const; + /// Set the node visit types and incompatibilities/requirements between the /// types (see below). /// @@ -1549,26 +1553,49 @@ class RoutingModel { /// Returns the sweep arranger to be used by routing heuristics. SweepArranger* sweep_arranger() const; #endif + struct NodeNeighborsParameters { + int num_neighbors; + bool add_vehicle_starts_to_neighbors = true; + // In NodeNeighborsByCostClass, neighbors for each node are sorted by + // increasing "cost" for each node. The following parameter determines if + // neighbors are sorted based on distance only when the neighborhood is + // partial, i.e. when num_neighbors entails that not all nodes are + // neighbors. + bool only_sort_neighbors_for_partial_neighborhoods = true; + + bool operator==(const NodeNeighborsParameters& other) const { + return num_neighbors == other.num_neighbors && + add_vehicle_starts_to_neighbors == + other.add_vehicle_starts_to_neighbors && + only_sort_neighbors_for_partial_neighborhoods == + other.only_sort_neighbors_for_partial_neighborhoods; + } + template + friend H AbslHashValue(H h, const NodeNeighborsParameters& params) { + return H::combine(std::move(h), params.num_neighbors, + params.add_vehicle_starts_to_neighbors, + params.only_sort_neighbors_for_partial_neighborhoods); + } + }; class NodeNeighborsByCostClass { public: NodeNeighborsByCostClass() = default; /// Computes num_neighbors neighbors of all nodes for every cost class in /// routing_model. - void ComputeNeighbors(const RoutingModel& routing_model, int num_neighbors, - bool add_vehicle_starts_to_neighbors); + void ComputeNeighbors(const RoutingModel& routing_model, + const NodeNeighborsParameters& params); /// Returns the neighbors of the given node for the given cost_class. const std::vector& GetNeighborsOfNodeForCostClass( int cost_class, int node_index) const { return node_index_to_neighbors_by_cost_class_.empty() ? all_nodes_ - : node_index_to_neighbors_by_cost_class_ - [node_index][cost_class] - ->PositionsSetAtLeastOnce(); + : node_index_to_neighbors_by_cost_class_[cost_class] + [node_index]; } private: - std::vector>>> + std::vector>> node_index_to_neighbors_by_cost_class_; std::vector all_nodes_; }; @@ -1579,12 +1606,12 @@ class RoutingModel { /// of min-neighbors node considered. const NodeNeighborsByCostClass* GetOrCreateNodeNeighborsByCostClass( double neighbors_ratio, int64_t min_neighbors, - double& neighbors_ratio_used, - bool add_vehicle_starts_to_neighbors = true); + double& neighbors_ratio_used, bool add_vehicle_starts_to_neighbors = true, + bool only_sort_neighbors_for_partial_neighborhoods = true); /// Returns parameters.num_neighbors neighbors of all nodes for every cost /// class. The result is cached and is computed once. const NodeNeighborsByCostClass* GetOrCreateNodeNeighborsByCostClass( - int num_neighbors, bool add_vehicle_starts_to_neighbors = true); + const NodeNeighborsParameters& params); /// Adds a custom local search filter to the list of filters used to speed up /// local search by pruning unfeasible variable assignments. /// Calling this method after the routing model has been closed (CloseModel() @@ -1738,6 +1765,26 @@ class RoutingModel { DCHECK(closed_); return same_vehicle_groups_[same_vehicle_group_[node]]; } + /// Returns variable indices of nodes constrained to have the same activity. + const std::vector& GetSameActivityIndicesOfIndex(int node) const { + DCHECK(closed_); + return same_active_var_groups_[same_active_var_group_[node]]; + } + /// Returns the same activity group of the node. + int GetSameActivityGroupOfIndex(int node) const { + DCHECK(closed_); + return same_active_var_group_[node]; + } + /// Returns the number of same activity groups. + int GetSameActivityGroupsCount() const { + DCHECK(closed_); + return same_active_var_groups_.size(); + } + /// Returns variable indices of nodes in the same activity group. + const std::vector& GetSameActivityIndicesOfGroup(int group) const { + DCHECK(closed_); + return same_active_var_groups_[group]; + } const VehicleTypeContainer& GetVehicleTypeContainer() const { DCHECK(closed_); @@ -2329,6 +2376,15 @@ class RoutingModel { same_vehicle_groups_[group].push_back(index); } + void InitSameActiveVarGroups(int number_of_groups) { + same_active_var_group_.assign(Size(), 0); + same_active_var_groups_.assign(number_of_groups, {}); + } + void SetSameActiveVarGroup(int index, int group) { + same_active_var_group_[index] = group; + same_active_var_groups_[group].push_back(index); + } + /// Returns the internal global/local optimizer index for the given dimension /// if any, and -1 otherwise. int GetGlobalCumulOptimizerIndex(const RoutingDimension& dimension) const; @@ -2451,6 +2507,10 @@ class RoutingModel { std::vector same_vehicle_group_; // Same vehicle node groups. std::vector> same_vehicle_groups_; + // Same active var group to which a node belongs. + std::vector same_active_var_group_; + // Same active var groups. + std::vector> same_active_var_groups_; // Node visit types // Variable index to visit type index. std::vector index_to_visit_type_; @@ -2553,21 +2613,6 @@ class RoutingModel { absl::flat_hash_map local_search_filter_managers_; std::vector extra_filters_; - struct NodeNeighborsParameters { - int num_neighbors; - bool add_vehicle_starts_to_neighbors; - - bool operator==(const NodeNeighborsParameters& other) const { - return num_neighbors == other.num_neighbors && - add_vehicle_starts_to_neighbors == - other.add_vehicle_starts_to_neighbors; - } - template - friend H AbslHashValue(H h, const NodeNeighborsParameters& params) { - return H::combine(std::move(h), params.num_neighbors, - params.add_vehicle_starts_to_neighbors); - } - }; absl::flat_hash_map> node_neighbors_by_cost_class_per_size_; diff --git a/ortools/constraint_solver/routing_breaks.cc b/ortools/constraint_solver/routing_breaks.cc index a530fd846ba..0eb20cd7231 100644 --- a/ortools/constraint_solver/routing_breaks.cc +++ b/ortools/constraint_solver/routing_breaks.cc @@ -946,7 +946,7 @@ void GlobalVehicleBreaksConstraint::PropagateVehicle(int vehicle) { interval->SetEndMax(arc_start_max); if (interval_is_performed) { dimension_->CumulVar(path_[pos]) - ->SetMin(CapSub(interval_end_min, arc_start_offset)); + ->SetMin(CapAdd(interval_end_min, arc_start_offset)); } } continue; diff --git a/ortools/constraint_solver/routing_constraints.cc b/ortools/constraint_solver/routing_constraints.cc index 9ee9c2701af..33f6c8d1aed 100644 --- a/ortools/constraint_solver/routing_constraints.cc +++ b/ortools/constraint_solver/routing_constraints.cc @@ -235,9 +235,9 @@ class ResourceAssignmentConstraint : public Constraint { s, this, &ResourceAssignmentConstraint::ResourceBound, "ResourceBound", v); resource_var->WhenBound(demon); - } } } + } void ResourceBound(int vehicle) { const int64_t resource = vehicle_resource_vars_[vehicle]->Value(); if (resource < 0) return; @@ -254,7 +254,7 @@ class ResourceAssignmentConstraint : public Constraint { dim->CumulVar(model_.End(vehicle)) ->SetRange(attributes.end_domain().Min(), attributes.end_domain().Max()); - } + } } const RoutingModel& model_; @@ -632,8 +632,8 @@ Constraint* MakePathSpansAndTotalSlacks(const RoutingDimension* dimension, RoutingModel* const model = dimension->model(); CHECK_EQ(model->vehicles(), spans.size()); CHECK_EQ(model->vehicles(), total_slacks.size()); - return model->solver()->RevAlloc( - new PathSpansAndTotalSlacks(model, dimension, spans, total_slacks)); + return model->solver()->RevAlloc(new PathSpansAndTotalSlacks( + model, dimension, std::move(spans), std::move(total_slacks))); } namespace { diff --git a/ortools/constraint_solver/routing_decision_builders.cc b/ortools/constraint_solver/routing_decision_builders.cc index b143f08449a..21f317c9d7c 100644 --- a/ortools/constraint_solver/routing_decision_builders.cc +++ b/ortools/constraint_solver/routing_decision_builders.cc @@ -370,8 +370,9 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { } } const bool use_mp_optimizer = - dimension_.HasBreakConstraints() && - !dimension_.GetBreakIntervalsOfVehicle(vehicle).empty(); + dimension_.HasQuadraticCostSoftSpanUpperBounds() || + (dimension_.HasBreakConstraints() && + !dimension_.GetBreakIntervalsOfVehicle(vehicle).empty()); LocalDimensionCumulOptimizer* const optimizer = use_mp_optimizer ? mp_optimizer_ : lp_optimizer_; DCHECK_NE(optimizer, nullptr); @@ -407,7 +408,7 @@ class SetCumulsFromLocalDimensionCosts : public DecisionBuilder { } bool ComputeVehicleResourceClassValuesAndIndices( - const std::vector& vehicles_to_assign, + absl::Span vehicles_to_assign, const util_intops::StrongVector>& used_resources_per_class, const std::function& next_accessor, diff --git a/ortools/constraint_solver/routing_filters.cc b/ortools/constraint_solver/routing_filters.cc index 11255c11aa6..19957c89a00 100644 --- a/ortools/constraint_solver/routing_filters.cc +++ b/ortools/constraint_solver/routing_filters.cc @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -124,6 +125,84 @@ IntVarLocalSearchFilter* MakeMaxActiveVehiclesFilter( namespace { +class ActiveNodeGroupFilter : public IntVarLocalSearchFilter { + public: + explicit ActiveNodeGroupFilter(const RoutingModel& routing_model) + : IntVarLocalSearchFilter(routing_model.Nexts()), + routing_model_(routing_model), + group_is_active_(routing_model.GetSameActivityGroupsCount(), false) {} + bool Accept(const Assignment* delta, const Assignment* /*deltadelta*/, + int64_t /*objective_min*/, int64_t /*objective_max*/) override { + absl::flat_hash_map active_count_per_group; + absl::flat_hash_map lns_count_per_group; + const Assignment::IntContainer& container = delta->IntVarContainer(); + for (const IntVarElement& new_element : container.elements()) { + IntVar* const var = new_element.Var(); + int64_t index = -1; + if (!FindIndex(var, &index)) continue; + if (new_element.Min() != new_element.Max()) { + // LNS detected. + lns_count_per_group[routing_model_.GetSameActivityGroupOfIndex( + index)]++; + continue; + } + int& active_count = gtl::LookupOrInsert( + &active_count_per_group, + routing_model_.GetSameActivityGroupOfIndex(index), 0); + if (new_element.Min() != index) { + if (active_count < 0) return false; + ++active_count; + } else { + if (active_count > 0) return false; + --active_count; + } + } + for (const auto& [group, count] : active_count_per_group) { + const int group_size = + routing_model_.GetSameActivityIndicesOfGroup(group).size(); + if (std::abs(count) + lns_count_per_group[group] == group_size) continue; + if ((count > 0) != group_is_active_[group]) return false; + } + return true; + } + std::string DebugString() const override { return "ActiveNodeGroupFilter"; } + + private: + void OnSynchronize(const Assignment* /*delta*/) override { + for (int group = 0; group < routing_model_.GetSameActivityGroupsCount(); + ++group) { + bool is_group_active = false; + for (int node : routing_model_.GetSameActivityIndicesOfGroup(group)) { + if (IsVarSynced(node)) { + is_group_active = (Value(node) != node); + break; + } + } +#ifndef NDEBUG + for (int node : routing_model_.GetSameActivityIndicesOfGroup(group)) { + if (IsVarSynced(node)) { + DCHECK_EQ((Value(node) != node), is_group_active); + } + } +#endif // NDEBUG + group_is_active_[group] = is_group_active; + } + } + + const RoutingModel& routing_model_; + std::vector group_is_active_; +}; + +} // namespace + +IntVarLocalSearchFilter* MakeActiveNodeGroupFilter( + const RoutingModel& routing_model) { + return routing_model.solver()->RevAlloc( + new ActiveNodeGroupFilter(routing_model)); +} + +namespace { + // Node disjunction filter class. class NodeDisjunctionFilter : public IntVarLocalSearchFilter { public: @@ -933,6 +1012,13 @@ class PathCumulFilter : public BasePathFilter { ? 0 : accepted_objective_value_; } + bool UsesDimensionOptimizers() { + if (!can_use_lp_) return false; + for (int vehicle = 0; vehicle < routing_model_.vehicles(); ++vehicle) { + if (FilterWithDimensionCumulOptimizerForVehicle(vehicle)) return true; + } + return false; + } private: // This structure stores the "best" path cumul value for a solution, the path @@ -1485,7 +1571,9 @@ void PathCumulFilter::OnBeforeSynchronizePaths() { // The only admissible failures here are because of LP timeout. int64_t lp_cumul_cost_value = 0; LocalDimensionCumulOptimizer* const optimizer = - FilterBreakCost(vehicle) ? mp_optimizer_ : optimizer_; + (FilterSoftSpanQuadraticCost(vehicle) || FilterBreakCost(vehicle)) + ? mp_optimizer_ + : optimizer_; DCHECK(optimizer != nullptr); const DimensionSchedulingStatus status = optimizer->ComputeRouteCumulCostWithoutFixedTransits( @@ -1848,7 +1936,7 @@ bool PathCumulFilter::FinalizeAcceptPath(int64_t /*objective_min*/, } DCHECK_NE(mp_optimizer_, nullptr); requires_mp[i] = - FilterBreakCost(vehicle) || + FilterBreakCost(vehicle) || FilterSoftSpanQuadraticCost(vehicle) || (status == DimensionSchedulingStatus::RELAXED_OPTIMAL_ONLY); } @@ -2200,11 +2288,12 @@ void AppendDimensionCumulFilters( const bool use_global_lp = use_global_lp_filter[d]; const bool filter_resource_assignment = use_resource_assignment_filter[d]; if (use_path_cumul_filter[d]) { - filters->push_back( - {MakePathCumulFilter(dimension, /*propagate_own_objective_value*/ - !use_global_lp && !filter_resource_assignment, - filter_objective_cost, has_dimension_optimizers), - kAccept, /*priority*/ 0}); + PathCumulFilter* filter = model.solver()->RevAlloc(new PathCumulFilter( + model, dimension, /*propagate_own_objective_value*/ !use_global_lp && + !filter_resource_assignment, + filter_objective_cost, has_dimension_optimizers)); + const int priority = filter->UsesDimensionOptimizers() ? 1 : 0; + filters->push_back({filter, kAccept, priority}); } else if (use_chain_cumul_filter) { filters->push_back( {model.solver()->RevAlloc(new ChainCumulFilter(model, dimension)), @@ -2215,7 +2304,7 @@ void AppendDimensionCumulFilters( DCHECK(!use_global_lp); DCHECK(!filter_resource_assignment); filters->push_back({MakeCumulBoundsPropagatorFilter(dimension), kAccept, - /*priority*/ 1}); + /*priority*/ 2}); } if (filter_resource_assignment) { @@ -2224,7 +2313,7 @@ void AppendDimensionCumulFilters( model.GetMutableLocalCumulMPOptimizer(dimension), /*propagate_own_objective_value*/ !use_global_lp, filter_objective_cost), - kAccept, /*priority*/ 2}); + kAccept, /*priority*/ 3}); } if (use_global_lp) { @@ -2232,7 +2321,7 @@ void AppendDimensionCumulFilters( model.GetMutableGlobalCumulLPOptimizer(dimension), model.GetMutableGlobalCumulMPOptimizer(dimension), filter_objective_cost), - kAccept, /*priority*/ 3}); + kAccept, /*priority*/ 4}); } } } @@ -3241,6 +3330,829 @@ IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model) { new CPFeasibilityFilter(routing_model)); } +PathState::PathState(int num_nodes, std::vector path_start, + std::vector path_end) + : num_nodes_(num_nodes), + num_paths_(path_start.size()), + num_nodes_threshold_(std::max(16, 4 * num_nodes_)) // Arbitrary value. +{ + DCHECK_EQ(path_start.size(), num_paths_); + DCHECK_EQ(path_end.size(), num_paths_); + for (int p = 0; p < num_paths_; ++p) { + path_start_end_.push_back({path_start[p], path_end[p]}); + } + // 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_nodes_.assign(2 * num_paths_, -1); + chains_.assign(num_paths_ + 1, {-1, -1}); // Reserve 1 more for sentinel. + paths_.assign(num_paths_, {-1, -1}); + for (int path = 0; path < num_paths_; ++path) { + const int index = 2 * path; + const auto& [start, end] = path_start_end_[path]; + committed_index_[start] = index; + committed_index_[end] = index + 1; + + committed_nodes_[index] = start; + committed_nodes_[index + 1] = end; + + committed_paths_[start] = path; + committed_paths_[end] = path; + + chains_[path] = {index, index + 2}; + paths_[path] = {path, path + 1}; + } + chains_[num_paths_] = {0, 0}; // Sentinel. + // Nodes that are not starts or ends are loops. + 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(); + committed_nodes_.push_back(node); + } +} + +PathState::ChainRange PathState::Chains(int path) const { + const PathBounds bounds = paths_[path]; + return PathState::ChainRange(chains_.data() + bounds.begin_index, + chains_.data() + bounds.end_index, + committed_nodes_.data()); +} + +PathState::NodeRange PathState::Nodes(int path) const { + const PathBounds bounds = paths_[path]; + return PathState::NodeRange(chains_.data() + bounds.begin_index, + chains_.data() + bounds.end_index, + committed_nodes_.data()); +} + +void PathState::ChangePath(int path, absl::Span chains) { + changed_paths_.push_back(path); + const int path_begin_index = chains_.size(); + chains_.insert(chains_.end(), chains.begin(), chains.end()); + const int path_end_index = chains_.size(); + paths_[path] = {path_begin_index, path_end_index}; + chains_.emplace_back(0, 0); // Sentinel. +} + +void PathState::ChangeLoops(absl::Span new_loops) { + for (const int loop : new_loops) { + if (Path(loop) == -1) continue; + changed_loops_.push_back(loop); + } +} + +void PathState::Commit() { + DCHECK(!IsInvalid()); + if (committed_nodes_.size() < num_nodes_threshold_) { + IncrementalCommit(); + } else { + FullCommit(); + } +} + +void PathState::Revert() { + is_invalid_ = false; + chains_.resize(num_paths_ + 1); // One per path + sentinel. + for (const int path : changed_paths_) { + paths_[path] = {path, path + 1}; + } + changed_paths_.clear(); + changed_loops_.clear(); +} + +void PathState::CopyNewPathAtEndOfNodes(int path) { + // Copy path's nodes, chain by chain. + const PathBounds path_bounds = paths_[path]; + for (int i = path_bounds.begin_index; i < path_bounds.end_index; ++i) { + const ChainBounds chain_bounds = chains_[i]; + committed_nodes_.insert(committed_nodes_.end(), + committed_nodes_.data() + chain_bounds.begin_index, + committed_nodes_.data() + chain_bounds.end_index); + if (committed_paths_[committed_nodes_.back()] == path) continue; + for (int i = chain_bounds.begin_index; i < chain_bounds.end_index; ++i) { + const int node = committed_nodes_[i]; + committed_paths_[node] = path; + } + } +} + +// TODO(user): Instead of copying paths at the end systematically, +// reuse some of the memory when possible. +void PathState::IncrementalCommit() { + const int new_nodes_begin = committed_nodes_.size(); + for (const int path : ChangedPaths()) { + const int chain_begin = committed_nodes_.size(); + CopyNewPathAtEndOfNodes(path); + const int chain_end = committed_nodes_.size(); + chains_[path] = {chain_begin, chain_end}; + } + // Re-index all copied nodes. + const int new_nodes_end = committed_nodes_.size(); + for (int i = new_nodes_begin; i < new_nodes_end; ++i) { + const int node = committed_nodes_[i]; + committed_index_[node] = i; + } + // New loops stay in place: only change their path to -1, + // committed_index_ does not change. + for (const int loop : ChangedLoops()) { + committed_paths_[loop] = -1; + } + // Committed part of the state is set up, erase incremental changes. + Revert(); +} + +void PathState::FullCommit() { + // Copy all paths at the end of committed_nodes_, + // then remove all old committed_nodes_. + const int old_num_nodes = committed_nodes_.size(); + for (int path = 0; path < num_paths_; ++path) { + const int new_path_begin = committed_nodes_.size() - old_num_nodes; + CopyNewPathAtEndOfNodes(path); + const int new_path_end = committed_nodes_.size() - old_num_nodes; + chains_[path] = {new_path_begin, new_path_end}; + } + committed_nodes_.erase(committed_nodes_.begin(), + committed_nodes_.begin() + old_num_nodes); + + // Reindex path nodes, then loop nodes. + constexpr int kUnindexed = -1; + committed_index_.assign(num_nodes_, kUnindexed); + int index = 0; + for (const int node : committed_nodes_) { + committed_index_[node] = index++; + } + for (int node = 0; node < num_nodes_; ++node) { + if (committed_index_[node] != kUnindexed) continue; + committed_index_[node] = index++; + committed_nodes_.push_back(node); + committed_paths_[node] = -1; + } + // Committed part of the state is set up, erase incremental changes. + Revert(); +} + +namespace { + +class PathStateFilter : public LocalSearchFilter { + public: + std::string DebugString() const override { return "PathStateFilter"; } + PathStateFilter(std::unique_ptr path_state, + const std::vector& nexts); + void Relax(const Assignment* delta, const Assignment* deltadelta) override; + bool Accept(const Assignment*, const Assignment*, int64_t, int64_t) override { + return true; + } + void Synchronize(const Assignment*, const Assignment*) override {}; + void Commit(const Assignment* assignment, const Assignment* delta) override; + void Revert() override; + void Reset() override; + + private: + // Used in arc to chain translation, see below. + struct TailHeadIndices { + int tail_index; + int head_index; + }; + struct IndexArc { + int index; + int arc; + bool operator<(const IndexArc& other) const { return index < other.index; } + }; + + // Translate changed_arcs_ to chains, pass to underlying PathState. + void CutChains(); + // From changed_paths_ and changed_arcs_, fill chains_ and paths_. + // Selection-based algorithm in O(n^2), to use for small change sets. + void MakeChainsFromChangedPathsAndArcsWithSelectionAlgorithm(); + // From changed_paths_ and changed_arcs_, fill chains_ and paths_. + // Generic algorithm in O(sort(n)+n), to use for larger change sets. + void MakeChainsFromChangedPathsAndArcsWithGenericAlgorithm(); + + const std::unique_ptr path_state_; + // 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_; + std::vector path_has_changed_; + std::vector> changed_arcs_; + std::vector changed_loops_; + std::vector tail_head_indices_; + std::vector arcs_by_tail_index_; + std::vector arcs_by_head_index_; + std::vector next_arc_; + std::vector path_chains_; +}; + +PathStateFilter::PathStateFilter(std::unique_ptr path_state, + const std::vector& nexts) + : path_state_(std::move(path_state)) { + { + int min_index = std::numeric_limits::max(); + int max_index = std::numeric_limits::min(); + for (const IntVar* next : nexts) { + const int index = next->index(); + min_index = std::min(min_index, index); + max_index = std::max(max_index, index); + } + variable_index_to_node_.resize(max_index - min_index + 1, -1); + index_offset_ = min_index; + } + + for (int node = 0; node < nexts.size(); ++node) { + const int index = nexts[node]->index() - index_offset_; + variable_index_to_node_[index] = node; + } + path_has_changed_.assign(path_state_->NumPaths(), false); +} + +void PathStateFilter::Relax(const Assignment* delta, const Assignment*) { + path_state_->Revert(); + changed_arcs_.clear(); + for (const IntVarElement& var_value : delta->IntVarContainer().elements()) { + if (var_value.Var() == nullptr) continue; + const int index = var_value.Var()->index() - index_offset_; + if (index < 0 || variable_index_to_node_.size() <= index) continue; + const int node = variable_index_to_node_[index]; + if (node == -1) continue; + if (var_value.Bound()) { + changed_arcs_.emplace_back(node, var_value.Value()); + } else { + path_state_->Revert(); + path_state_->SetInvalid(); + return; + } + } + 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(); +} + +// The solver does not guarantee that a given Commit() corresponds to +// the previous Relax() (or that there has been a call to Relax()), +// so we replay the full change call sequence. +void PathStateFilter::Commit(const Assignment* assignment, + const Assignment* delta) { + path_state_->Revert(); + if (delta == nullptr || delta->Empty()) { + Relax(assignment, nullptr); + } else { + Relax(delta, nullptr); + } + path_state_->Commit(); +} + +void PathStateFilter::Revert() { path_state_->Revert(); } + +void PathStateFilter::CutChains() { + // Filter out unchanged arcs from changed_arcs_, + // translate changed arcs to changed arc indices. + // Fill changed_paths_ while we hold node_path. + for (const int path : changed_paths_) path_has_changed_[path] = false; + changed_paths_.clear(); + tail_head_indices_.clear(); + changed_loops_.clear(); + int num_changed_arcs = 0; + for (const auto [node, next] : changed_arcs_) { + const int node_index = path_state_->CommittedIndex(node); + 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. + 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]) { + path_has_changed_[node_path] = true; + changed_paths_.push_back(node_path); + } + } else if (node == next && node_path != -1) { // New loop. + changed_loops_.push_back(node); + } + } + changed_arcs_.resize(num_changed_arcs); + + path_state_->ChangeLoops(changed_loops_); + if (tail_head_indices_.size() + changed_paths_.size() <= 8) { + MakeChainsFromChangedPathsAndArcsWithSelectionAlgorithm(); + } else { + MakeChainsFromChangedPathsAndArcsWithGenericAlgorithm(); + } +} + +void PathStateFilter:: + MakeChainsFromChangedPathsAndArcsWithSelectionAlgorithm() { + int num_visited_changed_arcs = 0; + const int num_changed_arcs = tail_head_indices_.size(); + // For every path, find all its chains. + for (const int path : changed_paths_) { + path_chains_.clear(); + const auto [start_index, end_index] = path_state_->CommittedPathRange(path); + int current_index = start_index; + while (true) { + // Look for smallest non-visited tail_index that is no smaller than + // current_index. + int selected_arc = -1; + int selected_tail_index = std::numeric_limits::max(); + for (int i = num_visited_changed_arcs; i < num_changed_arcs; ++i) { + const int tail_index = tail_head_indices_[i].tail_index; + if (current_index <= tail_index && tail_index < selected_tail_index) { + selected_arc = i; + selected_tail_index = tail_index; + } + } + // If there is no such tail index, or more generally if the next chain + // would be cut by end of path, + // stack {current_index, end_index + 1} in chains_, and go to next path. + // Otherwise, stack {current_index, tail_index+1} in chains_, + // set current_index = head_index, set pair to visited. + if (start_index <= current_index && current_index < end_index && + end_index <= selected_tail_index) { + path_chains_.emplace_back(current_index, end_index); + break; + } else { + path_chains_.emplace_back(current_index, selected_tail_index + 1); + current_index = tail_head_indices_[selected_arc].head_index; + std::swap(tail_head_indices_[num_visited_changed_arcs], + tail_head_indices_[selected_arc]); + ++num_visited_changed_arcs; + } + } + path_state_->ChangePath(path, path_chains_); + } +} + +void PathStateFilter::MakeChainsFromChangedPathsAndArcsWithGenericAlgorithm() { + // TRICKY: For each changed path, we want to generate a sequence of chains + // that represents the path in the changed state. + // First, notice that if we add a fake end->start arc for each changed path, + // then all chains will be from the head of an arc to the tail of an arc. + // A way to generate the changed chains and paths would be, for each path, + // to start from a fake arc's head (the path start), go down the path until + // the tail of an arc, and go to the next arc until we return on the fake arc, + // enqueuing the [head, tail] chains as we go. + // In turn, to do that, we need to know which arc to go to. + // If we sort all heads and tails by index in two separate arrays, + // the head_index and tail_index at the same rank are such that + // [head_index, tail_index] is a chain. Moreover, the arc that must be visited + // after head_index's arc is tail_index's arc. + + // Add a fake end->start arc for each path. + for (const int path : changed_paths_) { + const auto [start_index, end_index] = path_state_->CommittedPathRange(path); + tail_head_indices_.push_back({end_index - 1, start_index}); + } + + // Generate pairs (tail_index, arc) and (head_index, arc) for all arcs, + // sort those pairs by index. + const int num_arc_indices = tail_head_indices_.size(); + arcs_by_tail_index_.resize(num_arc_indices); + arcs_by_head_index_.resize(num_arc_indices); + for (int i = 0; i < num_arc_indices; ++i) { + arcs_by_tail_index_[i] = {tail_head_indices_[i].tail_index, i}; + arcs_by_head_index_[i] = {tail_head_indices_[i].head_index, i}; + } + std::sort(arcs_by_tail_index_.begin(), arcs_by_tail_index_.end()); + std::sort(arcs_by_head_index_.begin(), arcs_by_head_index_.end()); + // Generate the map from arc to next arc in path. + next_arc_.resize(num_arc_indices); + for (int i = 0; i < num_arc_indices; ++i) { + next_arc_[arcs_by_head_index_[i].arc] = arcs_by_tail_index_[i].arc; + } + + // Generate chains: for every changed path, start from its fake arc, + // jump to next_arc_ until going back to fake arc, + // enqueuing chains as we go. + const int first_fake_arc = num_arc_indices - changed_paths_.size(); + for (int fake_arc = first_fake_arc; fake_arc < num_arc_indices; ++fake_arc) { + path_chains_.clear(); + int32_t arc = fake_arc; + do { + const int chain_begin = tail_head_indices_[arc].head_index; + arc = next_arc_[arc]; + const int chain_end = tail_head_indices_[arc].tail_index + 1; + path_chains_.emplace_back(chain_begin, chain_end); + } while (arc != fake_arc); + const int path = changed_paths_[fake_arc - first_fake_arc]; + path_state_->ChangePath(path, path_chains_); + } +} + +} // namespace + +LocalSearchFilter* MakePathStateFilter(Solver* solver, + std::unique_ptr path_state, + const std::vector& nexts) { + PathStateFilter* filter = new PathStateFilter(std::move(path_state), nexts); + return solver->RevAlloc(filter); +} + +namespace { +using EInterval = DimensionChecker::ExtendedInterval; + +constexpr int64_t kint64min = std::numeric_limits::min(); +constexpr int64_t kint64max = std::numeric_limits::max(); + +EInterval operator&(const EInterval& i1, const EInterval& i2) { + return {std::max(i1.num_negative_infinity == 0 ? i1.min : kint64min, + i2.num_negative_infinity == 0 ? i2.min : kint64min), + std::min(i1.num_positive_infinity == 0 ? i1.max : kint64max, + i2.num_positive_infinity == 0 ? i2.max : kint64max), + std::min(i1.num_negative_infinity, i2.num_negative_infinity), + std::min(i1.num_positive_infinity, i2.num_positive_infinity)}; +} + +EInterval operator&=(EInterval& i1, const EInterval& i2) { + i1 = i1 & i2; + return i1; +} + +bool IsEmpty(const EInterval& interval) { + const int64_t minimum_value = + interval.num_negative_infinity == 0 ? interval.min : kint64min; + const int64_t maximum_value = + interval.num_positive_infinity == 0 ? interval.max : kint64max; + return minimum_value > maximum_value; +} + +EInterval operator+(const EInterval& i1, const EInterval& i2) { + return {CapAdd(i1.min, i2.min), CapAdd(i1.max, i2.max), + i1.num_negative_infinity + i2.num_negative_infinity, + i1.num_positive_infinity + i2.num_positive_infinity}; +} + +EInterval& operator+=(EInterval& i1, const EInterval& i2) { + i1 = i1 + i2; + return i1; +} + +EInterval operator-(const EInterval& i1, const EInterval& i2) { + return {CapSub(i1.min, i2.max), CapSub(i1.max, i2.min), + i1.num_negative_infinity + i2.num_positive_infinity, + i1.num_positive_infinity + i2.num_negative_infinity}; +} + +// Return the interval delta such that from + delta = to. +// Note that the result is not the same as "to + (-from)". +EInterval Delta(const EInterval& from, const EInterval& to) { + return {CapSub(to.min, from.min), CapSub(to.max, from.max), + to.num_negative_infinity - from.num_negative_infinity, + to.num_positive_infinity - from.num_positive_infinity}; +} + +EInterval ToExtendedInterval(DimensionChecker::Interval interval) { + const bool is_neg_infinity = interval.min == kint64min; + const bool is_pos_infinity = interval.max == kint64max; + return {is_neg_infinity ? 0 : interval.min, + is_pos_infinity ? 0 : interval.max, is_neg_infinity ? 1 : 0, + is_pos_infinity ? 1 : 0}; +} + +std::vector ToExtendedIntervals( + absl::Span intervals) { + std::vector extended_intervals; + extended_intervals.reserve(intervals.size()); + for (const auto& interval : intervals) { + extended_intervals.push_back(ToExtendedInterval(interval)); + } + return extended_intervals; +} +} // namespace + +DimensionChecker::DimensionChecker( + const PathState* path_state, std::vector path_capacity, + std::vector path_class, + std::vector> + demand_per_path_class, + std::vector node_capacity, int min_range_size_for_riq) + : path_state_(path_state), + path_capacity_(ToExtendedIntervals(path_capacity)), + path_class_(std::move(path_class)), + demand_per_path_class_(std::move(demand_per_path_class)), + node_capacity_(ToExtendedIntervals(node_capacity)), + index_(path_state_->NumNodes(), 0), + maximum_riq_layer_size_(std::max( + 16, 4 * path_state_->NumNodes())), // 16 and 4 are arbitrary. + min_range_size_for_riq_(min_range_size_for_riq) { + const int num_nodes = path_state_->NumNodes(); + cached_demand_.resize(num_nodes); + const int num_paths = path_state_->NumPaths(); + DCHECK_EQ(num_paths, path_capacity_.size()); + DCHECK_EQ(num_paths, path_class_.size()); + const int maximum_riq_exponent = MostSignificantBitPosition32(num_nodes); + riq_.resize(maximum_riq_exponent + 1); + FullCommit(); +} + +bool DimensionChecker::Check() const { + if (path_state_->IsInvalid()) return true; + for (const int path : path_state_->ChangedPaths()) { + const EInterval path_capacity = path_capacity_[path]; + const int path_class = path_class_[path]; + // Loop invariant: except for the first chain, cumul represents the cumul + // state of the last node of the previous chain, and it is nonempty. + int prev_node = path_state_->Start(path); + EInterval cumul = node_capacity_[prev_node] & path_capacity; + if (IsEmpty(cumul)) return false; + + for (const auto chain : path_state_->Chains(path)) { + const int first_node = chain.First(); + const int last_node = chain.Last(); + + if (prev_node != first_node) { + // Bring cumul state from last node of previous chain to first node of + // current chain. + const EInterval demand = ToExtendedInterval( + demand_per_path_class_[path_class](prev_node, first_node)); + cumul += demand; + cumul &= path_capacity; + cumul &= node_capacity_[first_node]; + if (IsEmpty(cumul)) return false; + prev_node = first_node; + } + + // Bring cumul state from first node to last node of the current chain. + const int first_index = index_[first_node]; + 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]; + // 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. + const bool chain_is_cached = chain_path_class == path_class; + if (last_index - first_index > min_range_size_for_riq_ && + chain_is_cached) { + UpdateCumulUsingChainRIQ(first_index, last_index, path_capacity, cumul); + if (IsEmpty(cumul)) return false; + prev_node = chain.Last(); + } else { + for (const int node : chain.WithoutFirstNode()) { + const EInterval demand = + chain_is_cached + ? cached_demand_[prev_node] + : ToExtendedInterval( + demand_per_path_class_[path_class](prev_node, node)); + cumul += demand; + cumul &= node_capacity_[node]; + cumul &= path_capacity; + if (IsEmpty(cumul)) return false; + prev_node = node; + } + } + } + } + return true; +} + +void DimensionChecker::Commit() { + const int current_layer_size = riq_[0].size(); + int change_size = path_state_->ChangedPaths().size(); + for (const int path : path_state_->ChangedPaths()) { + for (const auto chain : path_state_->Chains(path)) { + change_size += chain.NumNodes(); + } + } + if (current_layer_size + change_size <= maximum_riq_layer_size_) { + IncrementalCommit(); + } else { + FullCommit(); + } +} + +void DimensionChecker::IncrementalCommit() { + for (const int path : path_state_->ChangedPaths()) { + const int begin_index = riq_[0].size(); + AppendPathDemandsToSums(path); + UpdateRIQStructure(begin_index, riq_[0].size()); + } +} + +void DimensionChecker::FullCommit() { + // Clear all structures. + for (auto& layer : riq_) layer.clear(); + // Append all paths. + const int num_paths = path_state_->NumPaths(); + for (int path = 0; path < num_paths; ++path) { + const int begin_index = riq_[0].size(); + AppendPathDemandsToSums(path); + UpdateRIQStructure(begin_index, riq_[0].size()); + } +} + +void DimensionChecker::AppendPathDemandsToSums(int path) { + // Value of forwards_demand_sums_riq_ at node_index must be the sum + // of all demands of nodes from start of path to node. + const int path_class = path_class_[path]; + EInterval demand_sum = {0, 0, 0, 0}; + int prev = path_state_->Start(path); + int index = riq_[0].size(); + for (const int node : path_state_->Nodes(path)) { + // Transition to current node. + const EInterval demand = + prev == node ? EInterval{0, 0, 0, 0} + : ToExtendedInterval( + demand_per_path_class_[path_class](prev, node)); + demand_sum += demand; + cached_demand_[prev] = demand; + prev = node; + // Store all data of current node. + index_[node] = index++; + riq_[0].push_back({.cumuls_to_fst = node_capacity_[node], + .tightest_tsum = demand_sum, + .cumuls_to_lst = node_capacity_[node], + .tsum_at_fst = demand_sum, + .tsum_at_lst = demand_sum}); + } + cached_demand_[path_state_->End(path)] = {0, 0, 0, 0}; +} + +void DimensionChecker::UpdateRIQStructure(int begin_index, int end_index) { + // The max layer is the one used by Range Intersection Query functions on + // (begin_index, end_index - 1). + const int max_layer = + MostSignificantBitPosition32(end_index - begin_index - 1); + for (int layer = 1, half_window = 1; layer <= max_layer; + ++layer, half_window *= 2) { + riq_[layer].resize(end_index); + for (int i = begin_index + 2 * half_window - 1; i < end_index; ++i) { + // The window covered by riq_[layer][i] goes from + // first = i - 2 * half_window + 1 to last = i, inclusive. + // Values are computed from two half-windows of the layer below, + // the F-window = (i - 2 * half_window, i - half_window], and + // the L-window - (i - half_window, i]. + const RIQNode& fw = riq_[layer - 1][i - half_window]; + const RIQNode& lw = riq_[layer - 1][i]; + const EInterval lst_to_lst = Delta(fw.tsum_at_lst, lw.tsum_at_lst); + const EInterval fst_to_fst = Delta(fw.tsum_at_fst, lw.tsum_at_fst); + + riq_[layer][i] = { + .cumuls_to_fst = fw.cumuls_to_fst & lw.cumuls_to_fst - fst_to_fst, + .tightest_tsum = fw.tightest_tsum & lw.tightest_tsum, + .cumuls_to_lst = fw.cumuls_to_lst + lst_to_lst & lw.cumuls_to_lst, + .tsum_at_fst = fw.tsum_at_fst, + .tsum_at_lst = lw.tsum_at_lst}; + } + } +} + +// The RIQ schema decomposes the request into two windows: +// - the F window covers indices [first_index, first_index + window) +// - the L window covers indices (last_index - window, last_index] +// The decomposition uses the first and last nodes of these windows. +void DimensionChecker::UpdateCumulUsingChainRIQ( + int first_index, int last_index, const ExtendedInterval& path_capacity, + ExtendedInterval& cumul) const { + DCHECK_LE(0, first_index); + DCHECK_LT(first_index, last_index); + DCHECK_LT(last_index, riq_[0].size()); + const int layer = MostSignificantBitPosition32(last_index - first_index); + const int window = 1 << layer; + const RIQNode& fw = riq_[layer][first_index + window - 1]; + const RIQNode& lw = riq_[layer][last_index]; + + // Compute the set of cumul values that can reach the last node. + cumul &= fw.cumuls_to_fst; + cumul &= lw.cumuls_to_fst - Delta(fw.tsum_at_fst, lw.tsum_at_fst); + cumul &= path_capacity - + Delta(fw.tsum_at_fst, fw.tightest_tsum & lw.tightest_tsum); + + // We need to check for emptiness before widening the interval with transit. + if (IsEmpty(cumul)) return; + + // Transit to last node. + cumul += Delta(fw.tsum_at_fst, lw.tsum_at_lst); + + // Compute the set of cumul values that are reached from first node. + cumul &= fw.cumuls_to_lst + Delta(fw.tsum_at_lst, lw.tsum_at_lst); + cumul &= lw.cumuls_to_lst; +} + +namespace { + +class DimensionFilter : public LocalSearchFilter { + public: + std::string DebugString() const override { return name_; } + DimensionFilter(std::unique_ptr checker, + absl::string_view dimension_name) + : checker_(std::move(checker)), + name_(absl::StrCat("DimensionFilter(", dimension_name, ")")) {} + + bool Accept(const Assignment*, const Assignment*, int64_t, int64_t) override { + return checker_->Check(); + } + + void Synchronize(const Assignment*, const Assignment*) override { + checker_->Commit(); + } + + private: + std::unique_ptr checker_; + const std::string name_; +}; + +} // namespace + +LocalSearchFilter* MakeDimensionFilter( + Solver* solver, std::unique_ptr checker, + const std::string& dimension_name) { + DimensionFilter* filter = + new DimensionFilter(std::move(checker), dimension_name); + return solver->RevAlloc(filter); +} + +LightVehicleBreaksChecker::LightVehicleBreaksChecker( + PathState* path_state, std::vector path_data) + : path_state_(path_state), path_data_(std::move(path_data)) {} + +void LightVehicleBreaksChecker::Relax() const { + for (const int path : path_state_->ChangedPaths()) { + path_data_[path].start_cumul.Relax(); + path_data_[path].end_cumul.Relax(); + path_data_[path].span.Relax(); + } +} + +bool LightVehicleBreaksChecker::Check() const { + for (const int path : path_state_->ChangedPaths()) { + if (!path_data_[path].span.Exists()) continue; + const int64_t total_transit = path_data_[path].total_transit.Min(); + // Compute lower bound of path span from break and path time windows. + int64_t lb_span_tw = total_transit; + const int64_t start_max = path_data_[path].start_cumul.Max(); + const int64_t end_min = path_data_[path].end_cumul.Min(); + for (const auto& br : path_data_[path].vehicle_breaks) { + if (!br.is_performed_min) continue; + if (br.start_max < end_min && start_max < br.end_min) { + CapAddTo(br.duration_min, &lb_span_tw); + } + } + if (!path_data_[path].span.SetMin(lb_span_tw)) return false; + } + return true; +} + +namespace { + +class LightVehicleBreaksFilter : public LocalSearchFilter { + public: + LightVehicleBreaksFilter(std::unique_ptr checker, + absl::string_view dimension_name) + : checker_(std::move(checker)), + name_(absl::StrCat("LightVehicleBreaksFilter(", dimension_name, ")")) {} + + std::string DebugString() const override { return name_; } + + void Relax(const Assignment*, const Assignment*) override { + checker_->Relax(); + } + + bool Accept(const Assignment*, const Assignment*, int64_t, int64_t) override { + return checker_->Check(); + } + + void Synchronize(const Assignment*, const Assignment*) override { + checker_->Check(); + } + + private: + std::unique_ptr checker_; + const std::string name_; +}; + +} // namespace + +LocalSearchFilter* MakeLightVehicleBreaksFilter( + Solver* solver, std::unique_ptr checker, + const std::string& dimension_name) { + LightVehicleBreaksFilter* filter = + new LightVehicleBreaksFilter(std::move(checker), dimension_name); + return solver->RevAlloc(filter); +} + void WeightedWaveletTree::Clear() { elements_.clear(); tree_location_.clear(); @@ -3396,7 +4308,14 @@ PathEnergyCostChecker::PathEnergyCostChecker( force_per_class_(std::move(force_per_class)), distance_per_class_(std::move(distance_per_class)), path_energy_cost_(std::move(path_energy_cost)), - path_has_cost_when_empty_(std::move(path_has_cost_when_empty)) { + path_has_cost_when_empty_(std::move(path_has_cost_when_empty)), + maximum_range_query_size_(4 * path_state_->NumNodes()), + force_rmq_index_of_node_(path_state_->NumNodes()), + threshold_query_index_of_node_(path_state_->NumNodes()) { + const int num_nodes = path_state_->NumNodes(); + cached_force_.resize(num_nodes); + cached_distance_.resize(num_nodes); + FullCacheAndPrecompute(); committed_total_cost_ = 0; committed_path_cost_.assign(path_state_->NumPaths(), 0); const int num_paths = path_state_->NumPaths(); @@ -3419,36 +4338,118 @@ bool PathEnergyCostChecker::Check() { return true; } +void PathEnergyCostChecker::CacheAndPrecomputeRangeQueriesOfPath(int path) { + // Cache force and distance evaluations, + // precompute force RMQ, energy/distance threshold queries. + const auto& force_evaluator = *force_per_class_[force_class_[path]]; + const auto& distance_evaluator = *distance_per_class_[distance_class_[path]]; + int force_index = force_rmq_.TableSize(); + int threshold_index = energy_query_.TreeSize(); + int64_t total_force = 0; + + const int start_node = path_state_->Start(path); + int prev_node = start_node; + + for (const int node : path_state_->Nodes(path)) { + if (prev_node != node) { + const int64_t distance = distance_evaluator(prev_node, node); + cached_distance_[prev_node] = distance; + energy_query_.PushBack(total_force, total_force * distance); + distance_query_.PushBack(total_force, distance); + prev_node = node; + } + threshold_query_index_of_node_[node] = threshold_index++; + force_rmq_.PushBack(total_force); + force_rmq_index_of_node_[node] = force_index++; + const int64_t force = force_evaluator(node); + cached_force_[node] = force; + total_force += force; + } + force_rmq_.MakeTableFromNewElements(); + energy_query_.MakeTreeFromNewElements(); + distance_query_.MakeTreeFromNewElements(); +} + +void PathEnergyCostChecker::IncrementalCacheAndPrecompute() { + for (const int path : path_state_->ChangedPaths()) { + CacheAndPrecomputeRangeQueriesOfPath(path); + } +} + +void PathEnergyCostChecker::FullCacheAndPrecompute() { + force_rmq_.Clear(); + // Rebuild all paths. + const int num_paths = path_state_->NumPaths(); + for (int path = 0; path < num_paths; ++path) { + CacheAndPrecomputeRangeQueriesOfPath(path); + } +} + void PathEnergyCostChecker::Commit() { + int change_size = path_state_->ChangedPaths().size(); for (const int path : path_state_->ChangedPaths()) { + for (const auto chain : path_state_->Chains(path)) { + change_size += chain.NumNodes(); + } committed_total_cost_ = CapSub(committed_total_cost_, committed_path_cost_[path]); committed_path_cost_[path] = ComputePathCost(path); CapAddTo(committed_path_cost_[path], &committed_total_cost_); } + + const int current_layer_size = force_rmq_.TableSize(); + if (current_layer_size + change_size <= maximum_range_query_size_) { + IncrementalCacheAndPrecompute(); + } else { + FullCacheAndPrecompute(); + } } int64_t PathEnergyCostChecker::ComputePathCost(int64_t path) const { - const int force_class = force_class_[path]; - const auto& force_evaluator = *force_per_class_[force_class]; - const int distance_class = distance_class_[path]; - const auto& distance_evaluator = *distance_per_class_[distance_class]; + const int path_force_class = force_class_[path]; + const auto& force_evaluator = *force_per_class_[path_force_class]; // Find minimal force at which to start. - int64_t force = force_start_min_[path]; - int64_t min_force = force; + int64_t total_force = force_start_min_[path]; + int64_t min_force = total_force; int num_path_nodes = 0; int prev_node = path_state_->Start(path); for (const auto chain : path_state_->Chains(path)) { num_path_nodes += chain.NumNodes(); - // Add force needed to go from prev_node to chain.First() if needed, - // then add force needed to go from chain.First() to chain.Last(). - for (const int node : - (chain.First() == prev_node ? chain.WithoutFirstNode() : chain)) { + // Add force needed to go from prev_node to chain.First() if needed. + if (chain.First() != prev_node) { const int64_t force_to_node = force_evaluator(prev_node); - CapAddTo(force_to_node, &force); - min_force = std::min(min_force, force); + CapAddTo(force_to_node, &total_force); + min_force = std::min(min_force, total_force); + prev_node = chain.First(); + } + + // 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]; + 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()]; + const int last_index = force_rmq_index_of_node_[chain.Last()]; + // Get total force at first, last and lowest point of the chain. + const int64_t first_total_force = force_rmq_.array()[first_index]; + const int64_t last_total_force = force_rmq_.array()[last_index]; + const int64_t min_total_force = + force_rmq_.RangeMinimum(first_index, last_index); + // Compute running minimum total force and total force at chain.Last(). + min_force = std::min(min_force, + total_force - first_total_force + min_total_force); + CapAddTo(last_total_force - first_total_force, &total_force); + prev_node = chain.Last(); + } else { + for (const int node : chain.WithoutFirstNode()) { + const int64_t force = force_is_cached ? cached_force_[prev_node] + : force_evaluator(prev_node); + CapAddTo(force, &total_force); + min_force = std::min(min_force, total_force); prev_node = node; + } } } if (num_path_nodes == 2 && !path_has_cost_when_empty_[path]) return 0; @@ -3457,26 +4458,103 @@ int64_t PathEnergyCostChecker::ComputePathCost(int64_t path) const { // - >= force_end_min_[path] at end // - >= 0 at all intermediate nodes // We set the accumulator to the minimal offset that allows this. - force = std::max( - {0, CapOpp(min_force), CapSub(force_end_min_[path], force)}); - CapAddTo(force_start_min_[path], &force); + total_force = std::max( + {0, CapOpp(min_force), CapSub(force_end_min_[path], total_force)}); + CapAddTo(force_start_min_[path], &total_force); // Compute energy, below and above threshold. + const int path_distance_class = distance_class_[path]; + const auto& distance_evaluator = *distance_per_class_[path_distance_class]; const EnergyCost& cost = path_energy_cost_[path]; int64_t energy_below = 0; int64_t energy_above = 0; prev_node = path_state_->Start(path); for (const auto chain : path_state_->Chains(path)) { - for (const int node : - (chain.First() == prev_node ? chain.WithoutFirstNode() : chain)) { - const int64_t distance = distance_evaluator(prev_node, node); - CapAddTo(force_evaluator(prev_node), &force); - CapAddTo(CapProd(std::min(cost.threshold, force), distance), + // Bring cost computation to first node of the chain. + if (chain.First() != prev_node) { + const int64_t distance = distance_evaluator(prev_node, chain.First()); + CapAddTo(force_evaluator(prev_node), &total_force); + CapAddTo(CapProd(std::min(cost.threshold, total_force), distance), &energy_below); const int64_t force_above = - std::max(0, CapSub(force, cost.threshold)); + std::max(0, CapSub(total_force, cost.threshold)); + CapAddTo(CapProd(force_above, distance), &energy_above); + prev_node = chain.First(); + } + + // Inside chain, try to reuse cached forces and distances instead of more + // 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]; + const int chain_distance_class = + chain_path == -1 ? -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; + + if (force_is_cached && distance_is_cached && chain.NumNodes() >= 2) { + const int first_index = threshold_query_index_of_node_[chain.First()]; + const int last_index = threshold_query_index_of_node_[chain.Last()]; + + const int64_t zero_total_energy = energy_query_.RangeSumWithThreshold( + kint64min, first_index, last_index); + const int64_t total_distance = distance_query_.RangeSumWithThreshold( + kint64min, first_index, last_index); + + // In the following, zero_ values are those computed with the hypothesis + // that the force at the start node is zero. + // The total_force at chain.First() is in general not the same in the + // candidate path and in the zero_ case. We can still query the energy and + // distance totals incurred by transitions above the actual threshold + // during the chain, by offsetting the queries to zero_threshold. + const int64_t zero_total_force_first = + force_rmq_.array()[force_rmq_index_of_node_[chain.First()]]; + const int64_t zero_threshold = + CapSub(cost.threshold, CapSub(total_force, zero_total_force_first)); + // "High" transitions are those that occur with a force at or above the + // threshold. "High" energy is the sum of energy values during high + // transitions, same for "high" distance. + const int64_t zero_high_energy = energy_query_.RangeSumWithThreshold( + zero_threshold, first_index, last_index); + const int64_t zero_high_distance = distance_query_.RangeSumWithThreshold( + zero_threshold, first_index, last_index); + // "Above" energy is the energy caused by total_force above the threshold. + // Since "above" energy is only incurred during "high" transitions, it can + // be computed from "high" energy knowing distance and threshold. + const int64_t zero_energy_above = + CapSub(zero_high_energy, CapProd(zero_high_distance, zero_threshold)); + // To compute the energy values of the candidate, the force dimension + // must be offset back to the candidate's total force. + // Only the "below" energy is changed by the offset, the zero_ energy + // above the zero_ threshold was computed to be the same as the candidate + // energy above the actual threshold. + CapAddTo(zero_energy_above, &energy_above); + CapAddTo(CapAdd(CapSub(zero_total_energy, zero_energy_above), + CapProd(total_distance, + CapSub(cost.threshold, zero_threshold))), + &energy_below); + // We reuse the partial sum of the force query to compute the sum of + // forces incurred by the chain, + const int64_t zero_total_force_last = + force_rmq_.array()[force_rmq_index_of_node_[chain.Last()]]; + CapAddTo(CapSub(zero_total_force_last, zero_total_force_first), + &total_force); + prev_node = chain.Last(); + } else { + for (const int node : chain.WithoutFirstNode()) { + const int64_t force = force_is_cached ? cached_force_[prev_node] + : force_evaluator(prev_node); + const int64_t distance = distance_is_cached + ? cached_distance_[prev_node] + : distance_evaluator(prev_node, node); + CapAddTo(force, &total_force); + CapAddTo(CapProd(std::min(cost.threshold, total_force), distance), + &energy_below); + const int64_t force_above = + std::max(0, CapSub(total_force, cost.threshold)); CapAddTo(CapProd(force_above, distance), &energy_above); prev_node = node; + } } } diff --git a/ortools/constraint_solver/routing_filters.h b/ortools/constraint_solver/routing_filters.h index f52aa5ebd61..314e800e015 100644 --- a/ortools/constraint_solver/routing_filters.h +++ b/ortools/constraint_solver/routing_filters.h @@ -16,12 +16,15 @@ #include #include +#include #include #include #include #include #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" @@ -29,6 +32,7 @@ #include "ortools/constraint_solver/routing_types.h" #include "ortools/routing/parameters.pb.h" #include "ortools/util/bitset.h" +#include "ortools/util/range_minimum_query.h" namespace operations_research { @@ -36,6 +40,11 @@ namespace operations_research { IntVarLocalSearchFilter* MakeMaxActiveVehiclesFilter( const RoutingModel& routing_model); +/// Returns a filter ensuring that all nodes in a same activity group have the +/// same activity. +IntVarLocalSearchFilter* MakeActiveNodeGroupFilter( + const RoutingModel& routing_model); + /// Returns a filter ensuring that node disjunction constraints are enforced. IntVarLocalSearchFilter* MakeNodeDisjunctionFilter( const RoutingModel& routing_model, bool filter_cost); @@ -84,6 +93,500 @@ LocalSearchFilter* MakeResourceAssignmentFilter( /// Returns a filter checking the current solution using CP propagation. IntVarLocalSearchFilter* MakeCPFeasibilityFilter(RoutingModel* routing_model); +#if !defined(SWIG) +// A PathState represents a set of paths and changes made on it. +// +// More accurately, let us define P_{num_nodes, starts, ends}-graphs the set of +// directed graphs with nodes [0, num_nodes) whose connected components are +// paths from starts[i] to ends[i] (for the same i) and loops. +// Let us fix num_nodes, starts and ends, so we call these P-graphs. +// +// A P-graph can be described by the sequence of nodes of each of its paths, +// and its set of loops. To describe a change made on a given P-graph G0 that +// yields another P-graph G1, we choose to describe G1 in terms of G0. When +// the difference between G0 and G1 is small, as is almost always the case in a +// local search setting, the description is compact, allowing for incremental +// filters to be efficient. +// +// In order to describe G1 in terms of G0 succintly, we describe each path of +// G1 as a sequence of chains of G0. A chain of G0 is either a nonempty sequence +// of consecutive nodes of a path of G0, or a node that was a loop in G0. +// For instance, a path that was not modified from G0 to G1 has one chain, +// the sequence of all nodes in the path. Typically, local search operators +// modify one or two paths, and the resulting paths can described as sequences +// of two to four chains of G0. Paths that were modified are listed explicitly, +// allowing to iterate only on changed paths. +// The loops of G1 are described more implicitly: the loops of G1 not in G0 +// are listed explicitly, but those in both G1 and G0 are not listed. +// +// A PathState object can be in two states: committed or changed. +// At construction, the object is committed, G0. +// To enter a changed state G1, one can pass modifications with ChangePath() and +// ChangeLoops(). For reasons of efficiency, a chain is described as a range of +// node indices in the representation of the committed graph G0. To that effect, +// the nodes of a path of G0 are guaranteed to have consecutive indices. +// +// Filters can then browse the change efficiently using ChangedPaths(), +// Chains(), Nodes() and ChangedLoops(). +// +// Then Commit() or Revert() can be called: Commit() sets the changed state G1 +// as the new committed state, Revert() erases all changes. +class PathState { + public: + // A Chain allows to iterate on all nodes of a chain, and access some data: + // first node, last node, number of nodes in the chain. + // Chain is a range, its iterator ChainNodeIterator, its value type int. + // Chains are returned by PathChainIterator's operator*(). + class Chain; + // A ChainRange allows to iterate on all chains of a path. + // ChainRange is a range, its iterator Chain*, its value type Chain. + class ChainRange; + // A NodeRange allows to iterate on all nodes of a path. + // NodeRange is a range, its iterator PathNodeIterator, its value type int. + class NodeRange; + + struct ChainBounds { + ChainBounds() {} + ChainBounds(int begin_index, int end_index) + : begin_index(begin_index), end_index(end_index) {} + int begin_index; + int end_index; + }; + int CommittedIndex(int node) const { return committed_index_[node]; } + ChainBounds CommittedPathRange(int path) const { return chains_[path]; } + + // Path constructor: path_start and path_end must be disjoint, + // their values in [0, num_nodes). + PathState(int num_nodes, std::vector path_start, + std::vector path_end); + + // Instance-constant accessors. + + // Returns the number of nodes in the underlying graph. + int NumNodes() const { return num_nodes_; } + // Returns the number of paths (empty paths included). + int NumPaths() const { return num_paths_; } + // Returns the start of a path. + int Start(int path) const { return path_start_end_[path].start; } + // Returns the end of a path. + int End(int path) const { return path_start_end_[path].end; } + + // State-dependent accessors. + + // Returns the committed path of a given node, -1 if it is a loop. + 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. + const std::vector& ChangedPaths() const { return changed_paths_; } + // Returns the set of loops that were added by the change. + const std::vector& ChangedLoops() const { return changed_loops_; } + // Returns the current range of chains of path. + ChainRange Chains(int path) const; + // Returns the current range of nodes of path. + NodeRange Nodes(int path) const; + + // State modifiers. + + // Changes the path to the given sequence of chains of the committed state. + // Chains are described by semi-open intervals. No optimization is made in + // case two consecutive chains are actually already consecutive in the + // committed state: they are not merged into one chain, and Chains(path) will + // report the two chains. + void ChangePath(int path, absl::Span chains); + // Same as above, but the initializer_list interface avoids the need to pass + // a vector. + void ChangePath(int path, const std::initializer_list& chains) { + changed_paths_.push_back(path); + const int path_begin_index = chains_.size(); + chains_.insert(chains_.end(), chains.begin(), chains.end()); + const int path_end_index = chains_.size(); + paths_[path] = {path_begin_index, path_end_index}; + // Always add sentinel, in case this is the last path change. + chains_.emplace_back(0, 0); + } + + // Describes the nodes that are newly loops in this change. + void ChangeLoops(absl::Span new_loops); + + // Set the current state G1 as committed. See class comment for details. + void Commit(); + // Erase incremental changes. See class comment for details. + void Revert(); + + // LNS Operators may not fix variables, + // in which case we mark the candidate invalid. + void SetInvalid() { is_invalid_ = true; } + bool IsInvalid() const { return is_invalid_; } + + private: + // Most structs below are named pairs of ints, for typing purposes. + + // Start and end are stored together to optimize (likely) simultaneous access. + struct PathStartEnd { + PathStartEnd(int start, int end) : start(start), end(end) {} + int start; + int end; + }; + // Paths are ranges of chains, which are ranges of committed nodes, see below. + struct PathBounds { + int begin_index; + int end_index; + }; + + // Copies nodes in chains of path at the end of nodes, + // and sets those nodes' path member to value path. + void CopyNewPathAtEndOfNodes(int path); + // Commits paths in O(#{changed paths' nodes}) time, + // increasing this object's space usage by O(|changed path nodes|). + void IncrementalCommit(); + // Commits paths in O(num_nodes + num_paths) time, + // reducing this object's space usage to O(num_nodes + num_paths). + void FullCommit(); + + // Instance-constant data. + const int num_nodes_; + const int num_paths_; + std::vector path_start_end_; + + // Representation of the committed and changed paths. + // A path is a range of chains, which is a range of nodes. + // Ranges are represented internally by indices in vectors: + // ChainBounds are indices in committed_nodes_. PathBounds are indices in + // chains_. When committed (after construction, Revert() or Commit()): + // - path ranges are [path, path+1): they have one chain. + // - chain ranges don't overlap, chains_ has an empty sentinel at the end. + // The sentinel allows the Nodes() iterator to maintain its current pointer + // to committed nodes on NodeRange::operator++(). + // - committed_nodes_ contains all nodes, both paths and loops. + // Actually, old duplicates will likely appear, + // the current version of a node is at the index given by + // committed_index_[node]. A Commit() can add nodes at the end of + // committed_nodes_ in a space/time tradeoff, but if committed_nodes_' size + // is above num_nodes_threshold_, Commit() must reclaim useless duplicates' + // space by rewriting the path/chain/nodes structure. + // When changed (after ChangePaths() and ChangeLoops()), + // the structure is updated accordingly: + // - path ranges that were changed have nonoverlapping values [begin, end) + // where begin is >= num_paths_ + 1, i.e. new chains are stored after + // the committed state. + // - additional chain ranges are stored after the committed chains and its + // sentinel to represent the new chains resulting from the changes. + // Those chains do not overlap with one another or with committed chains. + // - committed_nodes_ are not modified, and still represent the committed + // paths. committed_index_ is not modified either. + std::vector committed_nodes_; + // Maps nodes to their path in the latest committed state. + std::vector committed_paths_; + // Maps nodes to their index in the latest committed state. + std::vector committed_index_; + const int num_nodes_threshold_; + std::vector chains_; + std::vector paths_; + + // Incremental information. + std::vector changed_paths_; + std::vector changed_loops_; + + // See IsInvalid() and SetInvalid(). + bool is_invalid_ = false; +}; + +// A Chain is a range of committed nodes. +class PathState::Chain { + public: + class Iterator { + public: + Iterator& operator++() { + ++current_node_; + return *this; + } + int operator*() const { return *current_node_; } + bool operator!=(Iterator other) const { + return current_node_ != other.current_node_; + } + + private: + // Only a Chain can construct its iterator. + friend class PathState::Chain; + explicit Iterator(const int* node) : current_node_(node) {} + const int* current_node_; + }; + + // Chains hold CommittedNode* values, a Chain may be invalidated + // if the underlying vector is modified. + Chain(const int* begin_node, const int* end_node) + : begin_(begin_node), end_(end_node) {} + + int NumNodes() const { return end_ - begin_; } + int First() const { return *begin_; } + int Last() const { return *(end_ - 1); } + Iterator begin() const { return Iterator(begin_); } + Iterator end() const { return Iterator(end_); } + + Chain WithoutFirstNode() const { return Chain(begin_ + 1, end_); } + + private: + const int* const begin_; + const int* const end_; +}; + +// A ChainRange is a range of Chains, committed or not. +class PathState::ChainRange { + public: + class Iterator { + public: + Iterator& operator++() { + ++current_chain_; + return *this; + } + Chain operator*() const { + return {first_node_ + current_chain_->begin_index, + first_node_ + current_chain_->end_index}; + } + bool operator!=(Iterator other) const { + return current_chain_ != other.current_chain_; + } + + private: + // Only a ChainRange can construct its Iterator. + friend class ChainRange; + Iterator(const ChainBounds* chain, const int* const first_node) + : current_chain_(chain), first_node_(first_node) {} + const ChainBounds* current_chain_; + const int* const first_node_; + }; + + // ChainRanges hold ChainBounds* and CommittedNode*, + // a ChainRange may be invalidated if on of the underlying vector is modified. + ChainRange(const ChainBounds* const begin_chain, + const ChainBounds* const end_chain, const int* const first_node) + : begin_(begin_chain), end_(end_chain), first_node_(first_node) {} + + Iterator begin() const { return {begin_, first_node_}; } + Iterator end() const { return {end_, first_node_}; } + + private: + const ChainBounds* const begin_; + const ChainBounds* const end_; + const int* const first_node_; +}; + +// A NodeRange allows to iterate on all nodes of a path, +// by a two-level iteration on ChainBounds* and CommittedNode* of a PathState. +class PathState::NodeRange { + public: + class Iterator { + public: + Iterator& operator++() { + ++current_node_; + if (current_node_ == end_node_) { + ++current_chain_; + // Note: dereferencing bounds is valid because there is a sentinel + // value at the end of PathState::chains_ to that intent. + const ChainBounds bounds = *current_chain_; + current_node_ = first_node_ + bounds.begin_index; + end_node_ = first_node_ + bounds.end_index; + } + return *this; + } + int operator*() const { return *current_node_; } + bool operator!=(Iterator other) const { + return current_chain_ != other.current_chain_; + } + + private: + // Only a NodeRange can construct its Iterator. + friend class NodeRange; + Iterator(const ChainBounds* current_chain, const int* const first_node) + : current_node_(first_node + current_chain->begin_index), + end_node_(first_node + current_chain->end_index), + current_chain_(current_chain), + first_node_(first_node) {} + const int* current_node_; + const int* end_node_; + const ChainBounds* current_chain_; + const int* const first_node_; + }; + + // NodeRanges hold ChainBounds* and int* (first committed node), + // a NodeRange may be invalidated if on of the underlying vector is modified. + NodeRange(const ChainBounds* begin_chain, const ChainBounds* end_chain, + const int* first_node) + : begin_chain_(begin_chain), + end_chain_(end_chain), + first_node_(first_node) {} + Iterator begin() const { return {begin_chain_, first_node_}; } + // Note: there is a sentinel value at the end of PathState::chains_, + // so dereferencing chain_range_.end()->begin_ is always valid. + Iterator end() const { return {end_chain_, first_node_}; } + + private: + const ChainBounds* begin_chain_; + const ChainBounds* end_chain_; + const int* const first_node_; +}; + +// Make a filter that takes ownership of a PathState and synchronizes it with +// solver events. The solver represents a graph with array of variables 'nexts'. +// Solver events are embodied by Assignment* deltas, that are translated to node +// changes during Relax(), committed during Synchronize(), and reverted on +// Revert(). +LocalSearchFilter* MakePathStateFilter(Solver* solver, + std::unique_ptr path_state, + const std::vector& nexts); + +// This checker enforces dimension requirements. +// A dimension requires that there is some valuation of +// cumul and demand such that for all paths: +// - cumul[A] is in interval node_capacity[A] +// - if arc A -> B is on a path of path_class p, +// then cumul[A] + demand[p](A, B) = cumul[B]. +// - if A is on a path of class p, then +// cumul[A] must be inside interval path_capacity[path]. +class DimensionChecker { + public: + struct Interval { + int64_t min; + int64_t max; + }; + + struct ExtendedInterval { + int64_t min; + int64_t max; + int64_t num_negative_infinity; + int64_t num_positive_infinity; + }; + + // TODO(user): the addition of kMinRangeSizeForRIQ slowed down Check(). + // See if using a template parameter makes it faster. + DimensionChecker(const PathState* path_state, + std::vector path_capacity, + std::vector path_class, + std::vector> + demand_per_path_class, + std::vector node_capacity, + int min_range_size_for_riq = kOptimalMinRangeSizeForRIQ); + + // Given the change made in PathState, checks that the dimension + // constraint is still feasible. + bool Check() const; + + // Commits to the changes made in PathState, + // must be called before PathState::Commit(). + void Commit(); + + static constexpr int kOptimalMinRangeSizeForRIQ = 4; + + private: + inline void UpdateCumulUsingChainRIQ(int first_index, int last_index, + const ExtendedInterval& path_capacity, + ExtendedInterval& cumul) const; + + // Commits to the current solution and rebuilds structures from scratch. + void FullCommit(); + // Commits to the current solution and only build structures for paths that + // changed, using additional space to do so in a time-memory tradeoff. + void IncrementalCommit(); + // Adds sums of given path to the bottom layer of the Range Intersection Query + // structure, updates index_ and previous_nontrivial_index_. + void AppendPathDemandsToSums(int path); + // Updates the Range Intersection Query structure from its bottom layer, + // with [begin_index, end_index) the range of the change, + // which must be at the end of the bottom layer. + // Supposes that requests overlapping the range will be inside the range, + // to avoid updating all layers. + void UpdateRIQStructure(int begin_index, int end_index); + + const PathState* const path_state_; + const std::vector path_capacity_; + const std::vector path_class_; + const std::vector> + demand_per_path_class_; + std::vector cached_demand_; + const std::vector node_capacity_; + + // Precomputed data. + // Maps nodes to their pre-computed data, except for isolated nodes, + // which do not have precomputed data. + // Only valid for nodes that are in some path in the committed state. + std::vector index_; + // Range intersection query in , with n = #nodes. + // Let node be in a path, i = index_[node], start the start of node's path. + // Let l such that index_[start] <= i - 2**l. + // - riq_[l][i].tsum_at_lst contains the sum of demands from start to node. + // - riq_[l][i].tsum_at_fst contains the sum of demands from start to the + // node at i - 2**l. + // - riq_[l][i].tightest_tsum contains the intersection of + // riq_[0][j].tsum_at_lst for all j in (i - 2**l, i]. + // - riq_[0][i].cumuls_to_lst and riq_[0][i].cumuls_to_fst contain + // the node's capacity. + // - riq_[l][i].cumuls_to_lst is the intersection, for j in (i - 2**l, i], of + // riq_[0][j].cumuls_to_lst + sum_{k in [j, i)} demand(k, k+1) + // - riq_[l][i].cumuls_to_fst is the intersection, for j in (i - 2**l, i], of + // riq_[0][j].cumuls_to_fst - sum_{k in (i-2**l, j)} demand(k, k+1) + struct RIQNode { + ExtendedInterval cumuls_to_fst; + ExtendedInterval tightest_tsum; + ExtendedInterval cumuls_to_lst; + ExtendedInterval tsum_at_fst; + ExtendedInterval tsum_at_lst; + }; + std::vector> riq_; + // The incremental branch of Commit() may waste space in the layers of the + // RIQ structure. This is the upper limit of a layer's size. + const int maximum_riq_layer_size_; + // Range queries are used on a chain only if the range is larger than this. + const int min_range_size_for_riq_; +}; + +// Make a filter that translates solver events to the input checker's interface. +// Since DimensionChecker has a PathState, the filter returned by this +// must be synchronized to the corresponding PathStateFilter: +// - Relax() must be called after the PathStateFilter's. +// - Accept() must be called after. +// - Synchronize() must be called before. +// - Revert() must be called before. +LocalSearchFilter* MakeDimensionFilter( + Solver* solver, std::unique_ptr checker, + const std::string& dimension_name); +#endif // !defined(SWIG) + +class LightVehicleBreaksChecker { + public: + struct VehicleBreak { + int64_t start_min; + int64_t start_max; + int64_t end_min; + int64_t end_max; + int64_t duration_min; + bool is_performed_min; + bool is_performed_max; + }; + + struct PathData { + std::vector vehicle_breaks; + LocalSearchState::Variable start_cumul; + LocalSearchState::Variable end_cumul; + LocalSearchState::Variable total_transit; + LocalSearchState::Variable span; + }; + + LightVehicleBreaksChecker(PathState* path_state, + std::vector path_data); + + void Relax() const; + + bool Check() const; + + private: + PathState* path_state_; + std::vector path_data_; +}; + +LocalSearchFilter* MakeLightVehicleBreaksFilter( + Solver* solver, std::unique_ptr checker, + const std::string& dimension_name); + // This class allows making fast range queries on sequences of elements. // * Main characteristics. // - queries on sequences of elements {height, weight}, @@ -309,6 +812,10 @@ class WeightedWaveletTree { }; }; +// TODO(user): improve this class by: +// - using WeightedWaveletTree to get the amount of energy above the threshold. +// - detect when costs above and below are the same, to avoid correcting for +// energy above the threshold and get O(1) time per chain. class PathEnergyCostChecker { public: struct EnergyCost { @@ -336,6 +843,10 @@ class PathEnergyCostChecker { private: int64_t ComputePathCost(int64_t path) const; + void CacheAndPrecomputeRangeQueriesOfPath(int path); + void IncrementalCacheAndPrecompute(); + void FullCacheAndPrecompute(); + const PathState* const path_state_; const std::vector force_start_min_; const std::vector force_end_min_; @@ -347,6 +858,24 @@ class PathEnergyCostChecker { const std::vector path_energy_cost_; const std::vector path_has_cost_when_empty_; + // Range queries. + const int maximum_range_query_size_; + // Allows to compute the minimum total_force over any chain, + // supposing the starting force is 0. + RangeMinimumQuery force_rmq_; + std::vector force_rmq_index_of_node_; + // Allows to compute the sum of energies of transitions whose total_force is + // above a threshold over any chain. Supposes total_force at start is 0. + WeightedWaveletTree energy_query_; + // Allows to compute the sum of distances of transitions whose total_force is + // above a threshold over any chain. Supposes total_force at start is 0. + WeightedWaveletTree distance_query_; + // Maps nodes to their common index in both threshold queries. + std::vector threshold_query_index_of_node_; + + std::vector cached_force_; + std::vector cached_distance_; + // Incremental cost computation. int64_t committed_total_cost_; int64_t accepted_total_cost_; diff --git a/ortools/constraint_solver/routing_ils.cc b/ortools/constraint_solver/routing_ils.cc index 07ba462c47e..79c5a08092d 100644 --- a/ortools/constraint_solver/routing_ils.cc +++ b/ortools/constraint_solver/routing_ils.cc @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -38,6 +39,9 @@ namespace operations_research { namespace { +// Returns global cheapest insertion parameters based on the given search +// parameters. +// TODO(user): consider having an ILS specific set of parameters. GlobalCheapestInsertionFilteredHeuristic::GlobalCheapestInsertionParameters MakeGlobalCheapestInsertionParameters( const RoutingSearchParameters& search_parameters, bool is_sequential) { @@ -58,6 +62,8 @@ MakeGlobalCheapestInsertionParameters( return gci_parameters; } +// Returns savings parameters based on the given search parameters. +// TODO(user): consider having an ILS specific set of parameters. SavingsFilteredHeuristic::SavingsParameters MakeSavingsParameters( const RoutingSearchParameters& search_parameters) { SavingsFilteredHeuristic::SavingsParameters savings_parameters; @@ -240,6 +246,7 @@ class LinearCoolingSchedule : public CoolingSchedule { } }; +// Returns a cooling schedule based on the given input parameters. std::unique_ptr MakeCoolingSchedule( const RoutingModel& model, const RoutingSearchParameters& parameters, std::mt19937* rnd) { @@ -300,6 +307,17 @@ class SimulatedAnnealingAcceptanceCriterion std::uniform_real_distribution probability_distribution_; }; +// Returns whether the given assignment has at least one performed node. +bool HasPerformedNodes(const RoutingModel& model, + const Assignment& assignment) { + for (int v = 0; v < model.vehicles(); ++v) { + if (model.Next(assignment, model.Start(v)) != model.End(v)) { + return true; + } + } + return false; +} + } // namespace CloseRoutesRemovalRuinProcedure::CloseRoutesRemovalRuinProcedure( @@ -307,8 +325,9 @@ CloseRoutesRemovalRuinProcedure::CloseRoutesRemovalRuinProcedure( int num_neighbors_for_route_selection) : model_(*model), neighbors_manager_(model->GetOrCreateNodeNeighborsByCostClass( - num_neighbors_for_route_selection, - /*add_vehicle_starts_to_neighbors=*/false)), + {num_neighbors_for_route_selection, + /*add_vehicle_starts_to_neighbors=*/false, + /*only_sort_neighbors_for_partial_neighborhoods=*/false})), num_routes_(num_routes), rnd_(*rnd), customer_dist_(0, model->Size() - 1), @@ -318,7 +337,7 @@ std::function CloseRoutesRemovalRuinProcedure::Ruin( const Assignment* assignment) { removed_routes_.SparseClearAll(); - if (num_routes_ > 0 && HasPerformedNodes(assignment)) { + if (num_routes_ > 0 && HasPerformedNodes(model_, *assignment)) { int64_t seed_node; int seed_route = -1; do { @@ -361,14 +380,238 @@ std::function CloseRoutesRemovalRuinProcedure::Ruin( }; } -bool CloseRoutesRemovalRuinProcedure::HasPerformedNodes( +RandomWalkRemovalRuinProcedure::RandomWalkRemovalRuinProcedure( + RoutingModel* model, std::mt19937* rnd, int walk_length, + int num_neighbors_for_route_selection) + : model_(*model), + routing_solution_(*model), + neighbors_manager_(model->GetOrCreateNodeNeighborsByCostClass( + {num_neighbors_for_route_selection, + /*add_vehicle_starts_to_neighbors=*/false, + /*only_sort_neighbors_for_partial_neighborhoods=*/false})), + rnd_(*rnd), + walk_length_(walk_length), + customer_dist_(0, model->Size() - 1) {} + +std::function RandomWalkRemovalRuinProcedure::Ruin( const Assignment* assignment) { - for (int v = 0; v < model_.vehicles(); ++v) { - if (model_.Next(*assignment, model_.Start(v)) != model_.End(v)) { - return true; + if (!HasPerformedNodes(model_, *assignment)) { + return [&model = model_, assignment](int64_t node) { + return assignment->Value(model.NextVar(node)); + }; + } + + routing_solution_.Reset(assignment); + + int64_t seed_node; + do { + seed_node = customer_dist_(rnd_); + } while (model_.IsStart(seed_node) || + assignment->Value(model_.VehicleVar(seed_node)) == -1); + DCHECK(!model_.IsEnd(seed_node)); + + int64_t curr_node = seed_node; + + // TODO(user): consider whether this should remain fixed or can vary at + // every ruin. + int walk_length = walk_length_; + + while (walk_length-- > 0) { + // Remove the active siblings node of curr before selecting next, so that + // we do not accidentally end up with next being one of these sibling + // nodes. + RemovePickupDeliverySiblings(assignment, curr_node); + + const int64_t next_node = GetNextNodeToRemove(assignment, curr_node); + + routing_solution_.RemoveNode(curr_node); + + if (next_node == -1) { + // We were not able to find a vertex where to move next. + // We thus prematurely abort the ruin. + break; } + + curr_node = next_node; } - return false; + + return + [this](int64_t node) { return routing_solution_.GetNextNodeIndex(node); }; +} + +void RandomWalkRemovalRuinProcedure::RemovePickupDeliverySiblings( + const Assignment* assignment, int node) { + if (const std::optional sibling_node = + model_.GetFirstMatchingPickupDeliverySibling( + node, + [&routing_solution = routing_solution_](int64_t node) { + return routing_solution.CanBeRemoved(node); + }); + sibling_node.has_value()) { + const int sibling_vehicle = + assignment->Value(model_.VehicleVar(sibling_node.value())); + DCHECK_NE(sibling_vehicle, -1); + + routing_solution_.InitializeRouteInfoIfNeeded(sibling_vehicle); + routing_solution_.RemoveNode(sibling_node.value()); + } +} + +int64_t RandomWalkRemovalRuinProcedure::GetNextNodeToRemove( + const Assignment* assignment, int node) { + const int curr_vehicle = assignment->Value(model_.VehicleVar(node)); + routing_solution_.InitializeRouteInfoIfNeeded(curr_vehicle); + + if (!routing_solution_.IsSingleCustomerRoute(curr_vehicle) && + boolean_dist_(rnd_)) { + const bool move_forward = boolean_dist_(rnd_); + int64_t next_node = + move_forward ? routing_solution_.GetNextNodeIndex(node) + : routing_solution_.GetInitializedPrevNodeIndex(node); + if (model_.IsStart(next_node) || model_.IsEnd(next_node)) { + // The Next or Prev of the node is a vehicle start/end, so we go in the + // opposite direction from before (and since the route isn't a + // single-customer route we shouldn't have a start/end again). + next_node = move_forward + ? routing_solution_.GetInitializedPrevNodeIndex(node) + : routing_solution_.GetNextNodeIndex(node); + } + DCHECK(!model_.IsStart(next_node)); + DCHECK(!model_.IsEnd(next_node)); + return next_node; + } + + // Pick the next node by jumping to a neighboring (non empty) route, + // otherwise. + const RoutingCostClassIndex cost_class_index = + model_.GetCostClassIndexOfVehicle(curr_vehicle); + + const std::vector& neighbors = + neighbors_manager_->GetNeighborsOfNodeForCostClass( + cost_class_index.value(), node); + + int64_t same_route_closest_neighbor = -1; + + for (int neighbor : neighbors) { + const int neighbor_vehicle = assignment->Value(model_.VehicleVar(neighbor)); + + if (!routing_solution_.CanBeRemoved(neighbor)) { + continue; + } + + if (neighbor_vehicle == curr_vehicle) { + if (same_route_closest_neighbor == -1) { + same_route_closest_neighbor = neighbor; + } + continue; + } + + return neighbor; + } + + // If we are not able to find a customer in another route, we are ok + // with taking a customer from the current one. + // Note that it can be -1 if no removable neighbor was found for the input + // node. + return same_route_closest_neighbor; +} + +RandomWalkRemovalRuinProcedure::RoutingSolution::RoutingSolution( + const RoutingModel& model) + : model_(model) { + const int all_nodes = model.Size() + model.vehicles(); + + nexts_.resize(all_nodes, -1); + prevs_.resize(all_nodes, -1); +} + +void RandomWalkRemovalRuinProcedure::RoutingSolution::Reset( + const Assignment* assignment) { + assignment_ = assignment; + + // TODO(user): consider resetting only previously set values. + nexts_.assign(nexts_.size(), -1); + prevs_.assign(prevs_.size(), -1); +} + +void RandomWalkRemovalRuinProcedure::RoutingSolution:: + InitializeRouteInfoIfNeeded(int vehicle) { + const int64_t start = model_.Start(vehicle); + + if (BelongsToInitializedRoute(start)) { + return; + } + + const int64_t end = model_.End(vehicle); + + int64_t prev = end; + int64_t curr = start; + + // Setup the start and inner nodes. + while (curr != end) { + const int64_t next = assignment_->Value(model_.NextVar(curr)); + + nexts_[curr] = next; + prevs_[curr] = prev; + + prev = curr; + curr = next; + } + + // Setup the end node. + nexts_[end] = start; + prevs_[end] = prev; +} + +bool RandomWalkRemovalRuinProcedure::RoutingSolution::BelongsToInitializedRoute( + int64_t node_index) const { + DCHECK_EQ(nexts_[node_index] != -1, prevs_[node_index] != -1); + return nexts_[node_index] != -1; +} + +int64_t RandomWalkRemovalRuinProcedure::RoutingSolution::GetNextNodeIndex( + int64_t node_index) const { + return BelongsToInitializedRoute(node_index) + ? nexts_[node_index] + : assignment_->Value(model_.NextVar(node_index)); +} + +int64_t +RandomWalkRemovalRuinProcedure::RoutingSolution::GetInitializedPrevNodeIndex( + int64_t node_index) const { + DCHECK(BelongsToInitializedRoute(node_index)); + return prevs_[node_index]; +} + +bool RandomWalkRemovalRuinProcedure::RoutingSolution::IsSingleCustomerRoute( + int vehicle) const { + const int64_t start = model_.Start(vehicle); + DCHECK(BelongsToInitializedRoute(start)); + + return nexts_[nexts_[start]] == model_.End(vehicle); +} + +bool RandomWalkRemovalRuinProcedure::RoutingSolution::CanBeRemoved( + int64_t node_index) const { + return !model_.IsStart(node_index) && !model_.IsEnd(node_index) && + GetNextNodeIndex(node_index) != node_index; +} + +void RandomWalkRemovalRuinProcedure::RoutingSolution::RemoveNode( + int64_t node_index) { + DCHECK(BelongsToInitializedRoute(node_index)); + + DCHECK_NE(nexts_[node_index], node_index); + DCHECK_NE(prevs_[node_index], node_index); + + const int64_t next = nexts_[node_index]; + const int64_t prev = prevs_[node_index]; + + nexts_[prev] = next; + prevs_[next] = prev; + + nexts_[node_index] = node_index; + prevs_[node_index] = node_index; } class RuinAndRecreateDecisionBuilder : public DecisionBuilder { diff --git a/ortools/constraint_solver/routing_ils.h b/ortools/constraint_solver/routing_ils.h index 0230c4f2611..e3bc2229fc3 100644 --- a/ortools/constraint_solver/routing_ils.h +++ b/ortools/constraint_solver/routing_ils.h @@ -20,6 +20,7 @@ #include #include #include +#include #include "absl/time/time.h" #include "ortools/constraint_solver/constraint_solver.h" @@ -34,11 +35,13 @@ namespace operations_research { class RuinProcedure { public: virtual ~RuinProcedure() = default; + + // Returns next accessors describing the ruined solution. virtual std::function Ruin( const Assignment* assignment) = 0; }; -// Remove a number of routes that are spatially close together. +// Removes a number of routes that are spatially close together. class CloseRoutesRemovalRuinProcedure : public RuinProcedure { public: CloseRoutesRemovalRuinProcedure(RoutingModel* model, std::mt19937* rnd, @@ -51,17 +54,88 @@ class CloseRoutesRemovalRuinProcedure : public RuinProcedure { std::function Ruin(const Assignment* assignment) override; private: - // Returns whether the assignment as at least one performed node. - bool HasPerformedNodes(const Assignment* assignment); - const RoutingModel& model_; const RoutingModel::NodeNeighborsByCostClass* const neighbors_manager_; const size_t num_routes_; - std::mt19937 rnd_; + std::mt19937& rnd_; std::uniform_int_distribution customer_dist_; SparseBitset removed_routes_; }; +// Removes a number of non start/end nodes by performing a random walk on the +// routing solution graph described by the assignment. +// Note that the removal of a pickup and delivery counts as the removal of a +// single entity. +class RandomWalkRemovalRuinProcedure : public RuinProcedure { + public: + RandomWalkRemovalRuinProcedure(RoutingModel* model, std::mt19937* rnd, + int walk_length, + int num_neighbors_for_route_selection); + + std::function Ruin(const Assignment* assignment) override; + + private: + // Wraps a routing assignment providing extra features. + class RoutingSolution { + public: + explicit RoutingSolution(const RoutingModel& model); + + // Initialize the routing solution for the given assignment. + // It must be called at the beginning of every ruin application. + void Reset(const Assignment* assignment); + + // Initializes next and prev pointers for the route served by the given + // vehicle, if not already done. + void InitializeRouteInfoIfNeeded(int vehicle); + + // Returns whether node_index belongs to a route that has been initialized. + bool BelongsToInitializedRoute(int64_t node_index) const; + + // Returns the next node index of the given node_index. + int64_t GetNextNodeIndex(int64_t node_index) const; + + // Returns the previous node index of the given node_index. + // This must be called for node_index belonging to initialized routes. + int64_t GetInitializedPrevNodeIndex(int64_t node_index) const; + + // Returns whether the route served by the given vehicle contains a single + // customer. + // This must be called for vehicle which has been previously initialized. + bool IsSingleCustomerRoute(int vehicle) const; + + // Returns whether node_index can be removed from the solution. + // This must be called for node_index belonging to initialized routes. + bool CanBeRemoved(int64_t node_index) const; + + // Removes the node with the given node_index. + // This must be called for node_index belonging to initialized routes. + void RemoveNode(int64_t node_index); + + private: + const RoutingModel& model_; + std::vector nexts_; + std::vector prevs_; + + // Assignment that the routing solution refers to. It's changed at every + // Reset call. + const Assignment* assignment_ = nullptr; + }; + + // Removes the sibling pickup or delivery of node, if any. + void RemovePickupDeliverySiblings(const Assignment* assignment, int node); + + // Returns the next node towards which the random walk is extended. + int64_t GetNextNodeToRemove(const Assignment* assignment, int node); + + const RoutingModel& model_; + RoutingSolution routing_solution_; + const RoutingModel::NodeNeighborsByCostClass* const neighbors_manager_; + std::mt19937& rnd_; + const int walk_length_; + std::uniform_int_distribution customer_dist_; + std::bernoulli_distribution boolean_dist_; +}; + // Returns a DecisionBuilder implementing a perturbation step of an Iterated // Local Search approach. DecisionBuilder* MakePerturbationDecisionBuilder( diff --git a/ortools/constraint_solver/routing_insertion_lns.cc b/ortools/constraint_solver/routing_insertion_lns.cc index 79f94f73243..e910bece7e2 100644 --- a/ortools/constraint_solver/routing_insertion_lns.cc +++ b/ortools/constraint_solver/routing_insertion_lns.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -389,38 +390,16 @@ void FilteredHeuristicCloseNodesLNSOperator::RemoveNodeAndActiveSibling( if (!IsActive(node)) return; RemoveNode(node); - for (int64_t sibling_node : GetActiveSiblings(node)) { - if (!model_->IsStart(sibling_node) && !model_->IsEnd(sibling_node)) { - RemoveNode(sibling_node); - } - } -} - -std::vector FilteredHeuristicCloseNodesLNSOperator::GetActiveSiblings( - int64_t node) const { - // NOTE: In most use-cases, where each node is a pickup or delivery in a - // single index pair, this function is in O(k) where k is the number of - // alternative deliveries or pickups for this index pair. - std::vector active_siblings; - for (const auto& [pair_index, unused] : model_->GetPickupPositions(node)) { - for (int64_t sibling_delivery : - pickup_delivery_pairs_[pair_index].delivery_alternatives) { - if (IsActive(sibling_delivery)) { - active_siblings.push_back(sibling_delivery); - break; - } - } - } - for (const auto& [pair_index, unused] : model_->GetDeliveryPositions(node)) { - for (int64_t sibling_pickup : - pickup_delivery_pairs_[pair_index].pickup_alternatives) { - if (IsActive(sibling_pickup)) { - active_siblings.push_back(sibling_pickup); - break; - } - } + if (const std::optional sibling_node = + model_->GetFirstMatchingPickupDeliverySibling( + node, + [this](int64_t node) { + return IsActive(node) && !model_->IsStart(node) && + !model_->IsEnd(node); + }); + sibling_node.has_value()) { + RemoveNode(sibling_node.value()); } - return active_siblings; } std::function diff --git a/ortools/constraint_solver/routing_lp_scheduling.cc b/ortools/constraint_solver/routing_lp_scheduling.cc index 64e1499f9fb..bbf5ff21a17 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.cc +++ b/ortools/constraint_solver/routing_lp_scheduling.cc @@ -1059,7 +1059,7 @@ DimensionSchedulingStatus DimensionCumulOptimizerCore::OptimizeAndPack( 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(vehicles, solver, packing_parameters); + status = PackRoutes(std::move(vehicles), solver, packing_parameters); } if (!solver->IsCPSATSolver()) { solver->SetParameters(original_params.SerializeAsString()); @@ -1953,6 +1953,30 @@ bool DimensionCumulOptimizerCore::SetRouteCumulConstraints( solver->SetObjectiveCoefficient(span_violation, bound_cost.cost); } } + if (optimize_costs && solver->IsCPSATSolver() && + dimension_->HasQuadraticCostSoftSpanUpperBounds()) { + // NOTE: the quadratic soft bound might be different from the one above. + const BoundCost bound_cost = + dimension_->GetQuadraticCostSoftSpanUpperBoundForVehicle(vehicle); + 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"); + // end - start <= bound + span_violation + const int violation = solver->CreateNewConstraint( + std::numeric_limits::min(), bound_cost.bound); + solver->SetCoefficient(violation, lp_cumuls.back(), 1.0); + solver->SetCoefficient(violation, lp_cumuls.front(), -1.0); + solver->SetCoefficient(violation, span_violation, -1.0); + // Add variable squared_span_violation, equal to span_violation². + const int squared_span_violation = solver->CreateNewPositiveVariable(); + solver->AddProductConstraint(squared_span_violation, + {span_violation, span_violation}); + // Add squared_span_violation * cost to objective. + solver->SetObjectiveCoefficient(squared_span_violation, bound_cost.cost); + } + } // Add global span constraint. if (optimize_costs && dimension_->global_span_cost_coefficient() > 0) { // min_start_cumul_ <= cumuls[start] @@ -2731,7 +2755,7 @@ void MoveValuesToIndicesFrom(std::vector* out_values, bool ComputeVehicleToResourceClassAssignmentCosts( int v, const RoutingModel::ResourceGroup& resource_group, const util_intops::StrongVector>& + absl::flat_hash_set>& ignored_resources_per_class, const std::function& next_accessor, const std::function& transit_accessor, @@ -2875,11 +2899,12 @@ bool ComputeVehicleToResourceClassAssignmentCosts( } int64_t ComputeBestVehicleToResourceAssignment( - const std::vector& vehicles, + absl::Span vehicles, const util_intops::StrongVector>& resource_indices_per_class, + std::vector>& + resource_indices_per_class, const util_intops::StrongVector>& + absl::flat_hash_set>& ignored_resources_per_class, std::function*(int)> vehicle_to_resource_class_assignment_costs, diff --git a/ortools/constraint_solver/routing_lp_scheduling.h b/ortools/constraint_solver/routing_lp_scheduling.h index 6badb486e37..eb7eeddc100 100644 --- a/ortools/constraint_solver/routing_lp_scheduling.h +++ b/ortools/constraint_solver/routing_lp_scheduling.h @@ -976,11 +976,12 @@ class GlobalDimensionCumulOptimizer { // COMPLEXITY: in practice, should be roughly // O(num_resource_classes * vehicles.size() + resource_indices->size()). int64_t ComputeBestVehicleToResourceAssignment( - const std::vector& vehicles, + absl::Span vehicles, const util_intops::StrongVector>& resource_indices_per_class, + std::vector>& + resource_indices_per_class, const util_intops::StrongVector>& + absl::flat_hash_set>& ignored_resources_per_class, std::function*(int)> vehicle_to_resource_class_assignment_costs, @@ -998,7 +999,7 @@ int64_t ComputeBestVehicleToResourceAssignment( bool ComputeVehicleToResourceClassAssignmentCosts( int v, const RoutingModel::ResourceGroup& resource_group, const util_intops::StrongVector>& + absl::flat_hash_set>& ignored_resources_per_class, const std::function& next_accessor, const std::function& transit_accessor, diff --git a/ortools/constraint_solver/routing_neighborhoods.cc b/ortools/constraint_solver/routing_neighborhoods.cc index 882f218d97c..c9c1cb1e47c 100644 --- a/ortools/constraint_solver/routing_neighborhoods.cc +++ b/ortools/constraint_solver/routing_neighborhoods.cc @@ -460,8 +460,9 @@ LightPairRelocateOperator::LightPairRelocateOperator( std::function start_empty_path_class, const std::vector& pairs, std::function force_lifo) - : LightPairRelocateOperator(vars, secondary_vars, start_empty_path_class, - nullptr, pairs, std::move(force_lifo)) {} + : LightPairRelocateOperator(vars, secondary_vars, + std::move(start_empty_path_class), nullptr, + pairs, std::move(force_lifo)) {} bool LightPairRelocateOperator::MakeNeighbor() { const auto do_move = [this](int64_t node, int64_t destination, diff --git a/ortools/constraint_solver/routing_search.cc b/ortools/constraint_solver/routing_search.cc index fff661054ae..b577ba22a2f 100644 --- a/ortools/constraint_solver/routing_search.cc +++ b/ortools/constraint_solver/routing_search.cc @@ -4209,8 +4209,7 @@ void SweepArranger::ArrangeIndices(std::vector* indices) { double square_distance = x_delta * x_delta + y_delta * y_delta; double angle = square_distance == 0 ? 0 : std::atan2(y_delta, x_delta); angle = angle >= 0 ? angle : 2 * pi_rad + angle; - SweepIndex sweep_index(index, angle, square_distance); - sweep_indices.push_back(sweep_index); + sweep_indices.push_back(SweepIndex(index, angle, square_distance)); } absl::c_sort(sweep_indices, SweepIndexDistanceComparator); @@ -4291,8 +4290,7 @@ class RouteConstructor { // Initial State: Each order is served by its own vehicle. for (int index = 0; index < num_indices_; ++index) { if (!model_->IsStart(index) && !model_->IsEnd(index)) { - std::vector route(1, index); - routes_.push_back(route); + routes_.push_back({index}); in_route_[index] = routes_.size() - 1; } } @@ -4755,8 +4753,8 @@ class SweepBuilder : public DecisionBuilder { if ((model_->IsStart(first) || !model_->IsEnd(first)) && (model_->IsStart(second) || !model_->IsEnd(second))) { if (first != depot && second != depot) { - Link link(std::make_pair(first, second), 0, 0, depot, depot); - links_.push_back(link); + links_.push_back( + Link(std::make_pair(first, second), 0, 0, depot, depot)); } } } @@ -5056,11 +5054,10 @@ DecisionBuilder* RoutingModel::MakeSelfDependentDimensionFinalizer( const RoutingDimension* dimension) { CHECK(dimension != nullptr); CHECK(dimension->base_dimension() == dimension); - std::function slack_guide = [dimension](int64_t index) { - return dimension->ShortestTransitionSlack(index); - }; DecisionBuilder* const guided_finalizer = - MakeGuidedSlackFinalizer(dimension, slack_guide); + MakeGuidedSlackFinalizer(dimension, [dimension](int64_t index) { + return dimension->ShortestTransitionSlack(index); + }); DecisionBuilder* const slacks_finalizer = solver_->MakeSolveOnce(guided_finalizer); std::vector start_cumuls(vehicles_, nullptr); diff --git a/ortools/constraint_solver/routing_search.h b/ortools/constraint_solver/routing_search.h index 0ad3b36e537..3b9cc40ff97 100644 --- a/ortools/constraint_solver/routing_search.h +++ b/ortools/constraint_solver/routing_search.h @@ -14,9 +14,8 @@ #ifndef OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ #define OR_TOOLS_CONSTRAINT_SOLVER_ROUTING_SEARCH_H_ -#include - #include +#include #include #include #include @@ -33,7 +32,6 @@ #include #include -#include "absl/container/flat_hash_map.h" #include "absl/container/flat_hash_set.h" #include "absl/log/check.h" #include "absl/types/span.h" diff --git a/ortools/constraint_solver/routing_types.h b/ortools/constraint_solver/routing_types.h index 257cc256574..6b6a69d1e57 100644 --- a/ortools/constraint_solver/routing_types.h +++ b/ortools/constraint_solver/routing_types.h @@ -16,7 +16,6 @@ #include #include -#include #include #include "ortools/base/int_type.h" diff --git a/ortools/constraint_solver/sched_search.cc b/ortools/constraint_solver/sched_search.cc index 28539370b28..ba6fae68ebf 100644 --- a/ortools/constraint_solver/sched_search.cc +++ b/ortools/constraint_solver/sched_search.cc @@ -16,6 +16,7 @@ #include #include #include +#include #include #include "absl/container/flat_hash_set.h" @@ -813,7 +814,7 @@ class RankFirstIntervalVars : public DecisionBuilder { } const int chosen = s->Rand32(all_candidates.size()); *best_sequence = all_candidates[chosen]; - best_possible_firsts_ = all_possible_firsts[chosen]; + best_possible_firsts_ = std::move(all_possible_firsts[chosen]); return true; } diff --git a/ortools/constraint_solver/search.cc b/ortools/constraint_solver/search.cc index fb40bdcde57..cf21d85a133 100644 --- a/ortools/constraint_solver/search.cc +++ b/ortools/constraint_solver/search.cc @@ -72,7 +72,6 @@ SearchLog::SearchLog(Solver* solver, std::vector vars, display_callback_(std::move(display_callback)), display_on_new_solutions_only_(display_on_new_solutions_only), nsol_(0), - tick_(0), objective_min_(vars_.size(), std::numeric_limits::max()), objective_max_(vars_.size(), std::numeric_limits::min()), min_right_depth_(std::numeric_limits::max()), @@ -92,11 +91,13 @@ void SearchLog::EnterSearch() { min_right_depth_ = std::numeric_limits::max(); neighbors_offset_ = solver()->accepted_neighbors(); nsol_ = 0; + last_objective_value_.clear(); + last_objective_timestamp_ = timer_->GetDuration(); } void SearchLog::ExitSearch() { const int64_t branches = solver()->branches(); - int64_t ms = timer_->GetInMs(); + int64_t ms = absl::ToInt64Milliseconds(timer_->GetDuration()); if (ms == 0) { ms = 1; } @@ -143,7 +144,23 @@ bool SearchLog::AtSolution() { absl::StrAppend(&obj_str, "objective = ", scaled_str(current), ", "); objective_updated = true; } + const absl::Duration now = timer_->GetDuration(); if (objective_updated) { + if (!last_objective_value_.empty()) { + const int64_t elapsed_ms = + absl::ToInt64Milliseconds(now - last_objective_timestamp_); + for (int i = 0; i < current.size(); ++i) { + const double improvement_rate = + 100.0 * 1000.0 * (current[i] - last_objective_value_[i]) / + std::max(1, last_objective_value_[i] * elapsed_ms); + absl::StrAppend(&obj_str, "improvement rate = ", improvement_rate, + "%/s, "); + last_objective_value_[i] = current[i]; + } + } else { + last_objective_value_ = current; + } + last_objective_timestamp_ = now; if (!objective_min_.empty() && std::lexicographical_compare(objective_min_.begin(), objective_min_.end(), current.begin(), @@ -170,7 +187,7 @@ bool SearchLog::AtSolution() { absl::StrAppendFormat(&log, "Solution #%d (%stime = %d ms, branches = %d," " failures = %d, depth = %d", - nsol_++, obj_str, timer_->GetInMs(), + nsol_++, obj_str, absl::ToInt64Milliseconds(now), solver()->branches(), solver()->failures(), depth); if (!solver()->SearchContext().empty()) { absl::StrAppendFormat(&log, ", %s", solver()->SearchContext()); @@ -203,7 +220,8 @@ void SearchLog::NoMoreSolutions() { std::string buffer = absl::StrFormat( "Finished search tree (time = %d ms, branches = %d," " failures = %d", - timer_->GetInMs(), solver()->branches(), solver()->failures()); + absl::ToInt64Milliseconds(timer_->GetDuration()), solver()->branches(), + solver()->failures()); if (solver()->neighbors() != 0) { absl::StrAppendFormat(&buffer, ", neighbors = %d, filtered neighbors = %d," @@ -233,9 +251,9 @@ void SearchLog::RefuteDecision(Decision* const decision) { } void SearchLog::OutputDecision() { - std::string buffer = - absl::StrFormat("%d branches, %d ms, %d failures", solver()->branches(), - timer_->GetInMs(), solver()->failures()); + std::string buffer = absl::StrFormat( + "%d branches, %d ms, %d failures", solver()->branches(), + absl::ToInt64Milliseconds(timer_->GetDuration()), solver()->failures()); if (min_right_depth_ != std::numeric_limits::max() && max_depth_ != 0) { const int depth = solver()->SearchDepth(); @@ -269,10 +287,11 @@ void SearchLog::Maintain() { max_depth_ = std::max(current_depth, max_depth_); } -void SearchLog::BeginInitialPropagation() { tick_ = timer_->GetInMs(); } +void SearchLog::BeginInitialPropagation() { tick_ = timer_->GetDuration(); } void SearchLog::EndInitialPropagation() { - const int64_t delta = std::max(timer_->GetInMs() - tick_, 0); + const int64_t delta = std::max( + absl::ToInt64Milliseconds(timer_->GetDuration() - tick_), 0); const std::string buffer = absl::StrFormat( "Root node processed (time = %d ms, constraints = %d, %s)", delta, solver()->constraints(), MemoryUsage()); @@ -356,10 +375,10 @@ SearchMonitor* Solver::MakeSearchLog(SearchLogParameters parameters) { << "Either variables are empty or objective is nullptr."; std::vector vars = parameters.objective != nullptr ? parameters.objective->objective_vars() - : parameters.variables; - std::vector scaling_factors = parameters.scaling_factors; + : std::move(parameters.variables); + std::vector scaling_factors = std::move(parameters.scaling_factors); scaling_factors.resize(vars.size(), 1.0); - std::vector offsets = parameters.offsets; + std::vector offsets = std::move(parameters.offsets); offsets.resize(vars.size(), 0.0); return RevAlloc(new SearchLog( this, std::move(vars), "", std::move(scaling_factors), std::move(offsets), @@ -1024,7 +1043,7 @@ class HighestRegretSelectorOnMin : public BaseObject { iterators_[i] = vars[i]->MakeDomainIterator(true); } } - ~HighestRegretSelectorOnMin() override{}; + ~HighestRegretSelectorOnMin() override {}; int64_t Choose(Solver* s, const std::vector& vars, int64_t first_unbound, int64_t last_unbound); std::string DebugString() const override { return "MaxRegretSelector"; } @@ -1082,7 +1101,7 @@ class CheapestVarSelector : public BaseObject { public: explicit CheapestVarSelector(std::function var_evaluator) : var_evaluator_(std::move(var_evaluator)) {} - ~CheapestVarSelector() override{}; + ~CheapestVarSelector() override {}; int64_t Choose(Solver* s, const std::vector& vars, int64_t first_unbound, int64_t last_unbound); std::string DebugString() const override { return "CheapestVarSelector"; } @@ -1115,7 +1134,7 @@ int64_t CheapestVarSelector::Choose(Solver* const, class PathSelector : public BaseObject { public: PathSelector() : first_(std::numeric_limits::max()) {} - ~PathSelector() override{}; + ~PathSelector() override {}; int64_t Choose(Solver* s, const std::vector& vars); std::string DebugString() const override { return "ChooseNextOnPath"; } @@ -2117,13 +2136,12 @@ BaseAssignVariables::Mode ChooseMode(Solver::IntValueStrategy val_str) { DecisionBuilder* Solver::MakePhase(const std::vector& vars, Solver::IntVarStrategy var_str, Solver::IntValueStrategy val_str) { - Solver::VariableIndexSelector var_selector = - BaseAssignVariables::MakeVariableSelector(this, vars, var_str); - Solver::VariableValueSelector value_selector = - BaseAssignVariables::MakeValueSelector(this, val_str); - const std::string name = BuildHeuristicsName(var_str, val_str); return BaseAssignVariables::MakePhase( - this, vars, var_selector, value_selector, name, ChooseMode(val_str)); + this, vars, /*var_selector=*/ + BaseAssignVariables::MakeVariableSelector(this, vars, var_str), + /*value_selector=*/BaseAssignVariables::MakeValueSelector(this, val_str), + /*value_selector_name=*/BuildHeuristicsName(var_str, val_str), + ChooseMode(val_str)); } DecisionBuilder* Solver::MakePhase(const std::vector& vars, @@ -2132,49 +2150,50 @@ DecisionBuilder* Solver::MakePhase(const std::vector& vars, CHECK(var_evaluator != nullptr); CheapestVarSelector* const var_selector = RevAlloc(new CheapestVarSelector(std::move(var_evaluator))); - Solver::VariableIndexSelector choose_variable = + return BaseAssignVariables::MakePhase( + this, vars, + /*var_selector=*/ [var_selector](Solver* solver, const std::vector& vars, int first_unbound, int last_unbound) { return var_selector->Choose(solver, vars, first_unbound, last_unbound); - }; - Solver::VariableValueSelector select_value = - BaseAssignVariables::MakeValueSelector(this, val_str); - const std::string name = "ChooseCheapestVariable_" + SelectValueName(val_str); - return BaseAssignVariables::MakePhase( - this, vars, choose_variable, select_value, name, ChooseMode(val_str)); + }, + /*value_selector=*/BaseAssignVariables::MakeValueSelector(this, val_str), + /*value_selector_name=*/"ChooseCheapestVariable_" + + SelectValueName(val_str), + ChooseMode(val_str)); } DecisionBuilder* Solver::MakePhase(const std::vector& vars, Solver::IntVarStrategy var_str, Solver::IndexEvaluator2 value_evaluator) { - Solver::VariableIndexSelector choose_variable = - BaseAssignVariables::MakeVariableSelector(this, vars, var_str); CheapestValueSelector* const value_selector = RevAlloc(new CheapestValueSelector(std::move(value_evaluator), nullptr)); - Solver::VariableValueSelector select_value = + return BaseAssignVariables::MakePhase( + this, vars, + /*var_selector=*/ + BaseAssignVariables::MakeVariableSelector(this, vars, var_str), + /*value_selector=*/ [value_selector](const IntVar* var, int64_t id) { return value_selector->Select(var, id); - }; - const std::string name = ChooseVariableName(var_str) + "_SelectCheapestValue"; - return BaseAssignVariables::MakePhase(this, vars, choose_variable, - select_value, name, - BaseAssignVariables::ASSIGN); + }, + /*value_selector_name=*/ChooseVariableName(var_str) + + "_SelectCheapestValue", + BaseAssignVariables::ASSIGN); } DecisionBuilder* Solver::MakePhase( const std::vector& vars, IntVarStrategy var_str, VariableValueComparator var_val1_val2_comparator) { - Solver::VariableIndexSelector choose_variable = - BaseAssignVariables::MakeVariableSelector(this, vars, var_str); BestValueByComparisonSelector* const value_selector = RevAlloc( new BestValueByComparisonSelector(std::move(var_val1_val2_comparator))); - Solver::VariableValueSelector select_value = + return BaseAssignVariables::MakePhase( + this, vars, /*var_selector=*/ + BaseAssignVariables::MakeVariableSelector(this, vars, var_str), + /*value_selector=*/ [value_selector](const IntVar* var, int64_t id) { return value_selector->Select(var, id); - }; - return BaseAssignVariables::MakePhase(this, vars, choose_variable, - select_value, "CheapestValue", - BaseAssignVariables::ASSIGN); + }, + /*value_selector_name=*/"CheapestValue", BaseAssignVariables::ASSIGN); } DecisionBuilder* Solver::MakePhase(const std::vector& vars, @@ -2182,37 +2201,35 @@ DecisionBuilder* Solver::MakePhase(const std::vector& vars, Solver::IndexEvaluator2 value_evaluator) { CheapestVarSelector* const var_selector = RevAlloc(new CheapestVarSelector(std::move(var_evaluator))); - Solver::VariableIndexSelector choose_variable = + CheapestValueSelector* value_selector = + RevAlloc(new CheapestValueSelector(std::move(value_evaluator), nullptr)); + return BaseAssignVariables::MakePhase( + this, vars, /*var_selector=*/ [var_selector](Solver* solver, const std::vector& vars, int first_unbound, int last_unbound) { return var_selector->Choose(solver, vars, first_unbound, last_unbound); - }; - CheapestValueSelector* value_selector = - RevAlloc(new CheapestValueSelector(std::move(value_evaluator), nullptr)); - Solver::VariableValueSelector select_value = + }, + /*value_selector=*/ [value_selector](const IntVar* var, int64_t id) { return value_selector->Select(var, id); - }; - return BaseAssignVariables::MakePhase(this, vars, choose_variable, - select_value, "CheapestValue", - BaseAssignVariables::ASSIGN); + }, + /*value_selector_name=*/"CheapestValue", BaseAssignVariables::ASSIGN); } DecisionBuilder* Solver::MakePhase(const std::vector& vars, Solver::IntVarStrategy var_str, Solver::IndexEvaluator2 value_evaluator, Solver::IndexEvaluator1 tie_breaker) { - Solver::VariableIndexSelector choose_variable = - BaseAssignVariables::MakeVariableSelector(this, vars, var_str); CheapestValueSelector* value_selector = RevAlloc(new CheapestValueSelector( std::move(value_evaluator), std::move(tie_breaker))); - Solver::VariableValueSelector select_value = + return BaseAssignVariables::MakePhase( + this, vars, /*var_selector=*/ + BaseAssignVariables::MakeVariableSelector(this, vars, var_str), + /*value_selector=*/ [value_selector](const IntVar* var, int64_t id) { return value_selector->Select(var, id); - }; - return BaseAssignVariables::MakePhase(this, vars, choose_variable, - select_value, "CheapestValue", - BaseAssignVariables::ASSIGN); + }, + /*value_selector_name=*/"CheapestValue", BaseAssignVariables::ASSIGN); } DecisionBuilder* Solver::MakePhase(const std::vector& vars, @@ -2221,20 +2238,19 @@ DecisionBuilder* Solver::MakePhase(const std::vector& vars, Solver::IndexEvaluator1 tie_breaker) { CheapestVarSelector* const var_selector = RevAlloc(new CheapestVarSelector(std::move(var_evaluator))); - Solver::VariableIndexSelector choose_variable = + CheapestValueSelector* value_selector = RevAlloc(new CheapestValueSelector( + std::move(value_evaluator), std::move(tie_breaker))); + return BaseAssignVariables::MakePhase( + this, vars, /*var_selector=*/ [var_selector](Solver* solver, const std::vector& vars, int first_unbound, int last_unbound) { return var_selector->Choose(solver, vars, first_unbound, last_unbound); - }; - CheapestValueSelector* value_selector = RevAlloc(new CheapestValueSelector( - std::move(value_evaluator), std::move(tie_breaker))); - Solver::VariableValueSelector select_value = + }, + /*value_selector=*/ [value_selector](const IntVar* var, int64_t id) { return value_selector->Select(var, id); - }; - return BaseAssignVariables::MakePhase(this, vars, choose_variable, - select_value, "CheapestValue", - BaseAssignVariables::ASSIGN); + }, + /*value_selector_name=*/"CheapestValue", BaseAssignVariables::ASSIGN); } DecisionBuilder* Solver::MakePhase(const std::vector& vars, @@ -2251,12 +2267,13 @@ DecisionBuilder* Solver::MakePhase(const std::vector& vars, switch (str) { case Solver::CHOOSE_STATIC_GLOBAL_BEST: { // TODO(user): support tie breaker - selector = RevAlloc(new StaticEvaluatorSelector(this, vars, eval)); + selector = + RevAlloc(new StaticEvaluatorSelector(this, vars, std::move(eval))); break; } case Solver::CHOOSE_DYNAMIC_GLOBAL_BEST: { - selector = RevAlloc(new DynamicEvaluatorSelector(this, vars, eval, - std::move(tie_breaker))); + selector = RevAlloc(new DynamicEvaluatorSelector( + this, vars, std::move(eval), std::move(tie_breaker))); break; } } diff --git a/ortools/constraint_solver/visitor.cc b/ortools/constraint_solver/visitor.cc index 4fa6621314d..ab7a35e0d7e 100644 --- a/ortools/constraint_solver/visitor.cc +++ b/ortools/constraint_solver/visitor.cc @@ -45,9 +45,7 @@ void ArgumentHolder::SetIntegerArrayArgument( void ArgumentHolder::SetIntegerMatrixArgument(const std::string& arg_name, const IntTupleSet& values) { - std::pair to_insert = - std::make_pair(arg_name, values); - matrix_argument_.insert(to_insert); + matrix_argument_.insert(std::make_pair(arg_name, values)); } void ArgumentHolder::SetIntegerExpressionArgument(const std::string& arg_name,